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}
+
+
+
+
+
+ {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 ? (
+
+
+
+ {__('Count', 'elasticpress')}
+ {__('Error type', 'elasticpress')}
+
+
+ {errorCounts.map((e) => (
+
+ {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 %s .',
'elasticpress',
);
+ case 'synonyms-error':
+ /* translators: %1$s Sync start date and time. */
+ return __(
+ 'Started manually from an error on the Synonyms Settings page at %s .',
+ '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 (
+ <>
+
+
+ {isSolr
+ ? __('Switch to visual editor', 'elasticpress')
+ : __('Switch to advanced text editor', 'elasticpress')}
+
+
+
+ {__(
+ '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',
+ )}
+
+
+
+
+ )}
+
+ {__('Save changes', '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}{' '}
-
- {isSolrEditable ? pageToggleSimpleText : pageToggleAdvanceText}
-
-
- {pageDescription}
-
- {!isSolrEditable && (
- <>
-
-
{`${setsTitle} (${sets.length})`}
-
{setsDescription}
-
-
-
-
{`${alternativesTitle} (${alternatives.length})`}
-
{alternativesDescription}
-
-
- >
- )}
-
-
- {isSolrVisible &&
{solrTitle} }
- {isSolrVisible &&
{solrDescription}
}
-
-
-
-
-
-
-
- {submitText}
-
-
- >
- );
-};
-
-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}
+
+
+
+
+
+ );
+};
+
+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(
+ () => (
+
+ ),
+ [checked, onDeleteChecked],
+ );
+
+ return (
+
+
+
+ );
+};
diff --git a/assets/js/synonyms/components/common/row-actions.js b/assets/js/synonyms/components/common/row-actions.js
new file mode 100644
index 0000000000..17bdd9e1d4
--- /dev/null
+++ b/assets/js/synonyms/components/common/row-actions.js
@@ -0,0 +1,56 @@
+/**
+ * WordPress dependencies.
+ */
+import { Button, Flex, FlexItem, Icon, Tooltip } from '@wordpress/components';
+import { WPElement } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+import { edit, trash } from '@wordpress/icons';
+
+/**
+ * Internal dependencies.
+ */
+import error from '../icons/error';
+
+/**
+ * List table row actions component.
+ *
+ * @param {object} props Component props.
+ * @param {boolean} props.disabled Are actions disabled?
+ * @param {string} props.errorMessage Error message.
+ * @param {boolean} props.isSelected Is the row selected?
+ * @param {Function} props.onSelect Select handler.
+ * @param {Function} props.onDelete Delete handler.
+ * @returns {WPElement}
+ */
+export default ({ disabled, errorMessage, isSelected, onSelect, onDelete }) => {
+ return (
+
+
+ {errorMessage ? (
+
+
+
+
+
+ ) : null}
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/assets/js/synonyms/components/editors/AlternativeEditor.js b/assets/js/synonyms/components/editors/AlternativeEditor.js
deleted file mode 100644
index af2b6dd547..0000000000
--- a/assets/js/synonyms/components/editors/AlternativeEditor.js
+++ /dev/null
@@ -1,88 +0,0 @@
-/**
- * WordPress dependencies.
- */
-import { useContext, useEffect, useMemo, useRef, useState, WPElement } from '@wordpress/element';
-
-/**
- * Internal dependencies.
- */
-import { Dispatch } from '../../context';
-import LinkedMultiInput from '../shared/LinkedMultiInput';
-
-/**
- * Alternative Editor
- *
- * @param {object} props Props.
- * @returns {WPElement} AlternativeEditor component
- */
-const AlternativeEditor = (props) => {
- const { id, synonyms, removeAction, updateAction } = props;
- const primary = synonyms.find((item) => item.primary);
- const [primaryTerm, setPrimaryTerm] = useState(primary ? primary.value : '');
- const dispatch = useContext(Dispatch);
- const primaryRef = useRef(null);
-
- /**
- * Create primary token
- *
- * @param {string} label Label.
- * @returns {object} Primary token
- */
- const createPrimaryToken = (label) => {
- return {
- label,
- value: label,
- primary: true,
- };
- };
-
- /**
- * Handle key down.
- *
- * @param {Event} event Keydown event.
- */
- const handleKeyDown = (event) => {
- switch (event.key) {
- case 'Enter':
- event.preventDefault();
- break;
- default:
- }
- };
-
- useEffect(() => {
- dispatch({
- type: 'UPDATE_ALTERNATIVE_PRIMARY',
- data: { id, token: createPrimaryToken(primaryTerm) },
- });
- }, [primaryTerm, id, dispatch]);
-
- useEffect(() => {
- primaryRef.current.focus();
- }, [primaryRef]);
-
- const memoizedSynonyms = useMemo(() => {
- return synonyms.filter((item) => !item.primary);
- }, [synonyms]);
-
- return (
- <>
- setPrimaryTerm(e.target.value)}
- value={primaryTerm}
- onKeyDown={handleKeyDown}
- ref={primaryRef}
- />
-
- >
- );
-};
-
-export default AlternativeEditor;
diff --git a/assets/js/synonyms/components/editors/AlternativesEditor.js b/assets/js/synonyms/components/editors/AlternativesEditor.js
deleted file mode 100644
index e48d2f4be5..0000000000
--- a/assets/js/synonyms/components/editors/AlternativesEditor.js
+++ /dev/null
@@ -1,77 +0,0 @@
-/**
- * WordPress dependencies.
- */
-import { Fragment, useContext, WPElement } from '@wordpress/element';
-
-/**
- * Internal dependencies.
- */
-import { Dispatch, State } from '../../context';
-import AlternativeEditor from './AlternativeEditor';
-
-/**
- * Synonyms editor component.
- *
- * @param {object} props Props.
- * @param {object[]} props.alternatives Defined alternatives (explicit mappings).
- * @returns {WPElement} AlternativesEditor component
- */
-const AlternativesEditor = ({ alternatives }) => {
- const dispatch = useContext(Dispatch);
- const state = useContext(State);
- const {
- alternativesInputHeading,
- alternativesPrimaryHeading,
- alternativesAddButtonText,
- alternativesErrorMessage,
- } = window.epSynonyms.i18n;
-
- /**
- * Handle click.
- *
- * @param {Event} e Event.
- */
- const handleClick = (e) => {
- const [lastItem] = state.alternatives.slice(-1);
- if (!alternatives.length || lastItem.synonyms.filter(({ value }) => value.length).length) {
- dispatch({ type: 'ADD_ALTERNATIVE' });
- }
- e.preventDefault();
- };
-
- return (
-
-
-
-
- {alternativesPrimaryHeading}
-
-
- {alternativesInputHeading}
-
-
-
- {alternatives.map((props) => (
-
-
- {!props.valid && (
- {alternativesErrorMessage}
- )}
-
- ))}
-
- {alternativesAddButtonText}
-
-
-
-
- );
-};
-
-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}
- )}
-
- ))}
-
- {setsAddButtonText}
-
-
-
-
- );
-};
-
-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}
-
-
-
-
- );
-};
-
-export default SolrEditor;
diff --git a/assets/js/synonyms/components/editors/solr-editor.js b/assets/js/synonyms/components/editors/solr-editor.js
new file mode 100644
index 0000000000..41496565c9
--- /dev/null
+++ b/assets/js/synonyms/components/editors/solr-editor.js
@@ -0,0 +1,42 @@
+/**
+ * WordPress dependencies.
+ */
+import { TextareaControl } from '@wordpress/components';
+import { WPElement } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies.
+ */
+import { useSynonymsSettings } from '../../provider';
+
+/**
+ * Solr editor component.
+ *
+ * @returns {WPElement}
+ */
+const SolrEditor = () => {
+ const { isBusy, solr, updateSolr } = useSynonymsSettings();
+
+ /**
+ * Handle changes to the Solr synonyms value.
+ *
+ * @param {Event} value Textarea control value.
+ */
+ const onChange = (value) => {
+ updateSolr(value);
+ };
+
+ return (
+
+ );
+};
+
+export default SolrEditor;
diff --git a/assets/js/synonyms/components/editors/visual-editor.js b/assets/js/synonyms/components/editors/visual-editor.js
new file mode 100644
index 0000000000..b767a0b1e5
--- /dev/null
+++ b/assets/js/synonyms/components/editors/visual-editor.js
@@ -0,0 +1,334 @@
+/**
+ * WordPress dependencies.
+ */
+import { CheckboxControl, Flex, FlexItem } from '@wordpress/components';
+import { useCallback, useEffect, useMemo, useRef, useState, WPElement } from '@wordpress/element';
+
+/**
+ * Internal dependencies.
+ */
+import { useSettingsScreen } from '../../../settings-screen';
+import { useSynonymsSettings } from '../../provider';
+import EditPanel from '../common/edit-panel';
+import ListTable from '../common/list-table';
+import RowActions from '../common/row-actions';
+
+/**
+ * Visual editor component.
+ *
+ * @typedef Synonym
+ * @property {string} value The synonym value.
+ * @property {boolean} primary Whether the synonym is a primary term.
+ *
+ * @typedef Rule
+ * @property {string} id Rule ID.
+ * @property {Synonym[]} synonyms Rule synonyms.
+ * @property {boolean} valid Whether the rule is valid.
+ *
+ * @param {object} props Component props.
+ * @param {object} props.labels Labels.
+ * @param {object} props.messages Messages.
+ * @param {'hyponyms'|'synonyms'|'replacements'} props.mode Editor mode.
+ * @param {Rule[]} props.rules Synonym rules.
+ * @returns {WPElement}
+ */
+export default ({ labels, messages, mode, rules }) => {
+ const { createNotice } = useSettingsScreen();
+ const {
+ addRule,
+ deleteRules,
+ isBusy,
+ isHyponymsValid,
+ isReplacementsValid,
+ isSynonymsValid,
+ select,
+ selected,
+ updateRule,
+ } = useSynonymsSettings();
+
+ /**
+ * Edit panel reference.
+ */
+ const editPanelRef = useRef();
+
+ /**
+ * Values for the primary terms and synonyms in the editor.
+ */
+ const [primary, setPrimary] = useState([]);
+ const [synonyms, setSynonyms] = useState([]);
+
+ /**
+ * The set currently being edited. This is the currently selected set if it
+ * is a set of synonyms.
+ */
+ const edited = useMemo(() => rules.find((s) => s.id === selected), [rules, selected]);
+
+ /**
+ * Whether the form is valid.
+ *
+ * @type {boolean}
+ */
+ const isValid = useMemo(() => {
+ const rule = [...primary, ...synonyms];
+
+ switch (mode) {
+ case 'hyponyms':
+ return isHyponymsValid(rule);
+ case 'replacements':
+ return isReplacementsValid(rule);
+ case 'synonyms':
+ default:
+ return isSynonymsValid(rule);
+ }
+ }, [isHyponymsValid, isReplacementsValid, isSynonymsValid, mode, primary, synonyms]);
+
+ /**
+ * Filter a list of synonyms to include only primary terms.
+ *
+ * @param {Synonym} synonym Synonym to filter.
+ */
+ const isPrimary = useCallback((synonym) => {
+ return synonym.primary;
+ }, []);
+
+ /**
+ * Filter a list of synonyms to exclude primary terms.
+ *
+ * When in hyponyms mode, also exclude the hypernym from the list of
+ * synonyms.
+ *
+ * @param {object} synonym Synonym to filter.
+ * @param {number} index Index of the synonym being filtered.
+ * @param {Array} synonyms Synonyms being filtered.
+ * @returns {boolean} True to keep the synonym, or false to filter it.
+ */
+ const isNotPrimary = useCallback(
+ (synonym, index, synonyms) => {
+ if (
+ mode === 'hyponyms' &&
+ synonyms.some((s) => s.primary && s.value === synonym.value)
+ ) {
+ return false;
+ }
+
+ return !synonym.primary;
+ },
+ [mode],
+ );
+
+ /**
+ * Handle changes to the primary terms.
+ *
+ * @param {Array|string} value Updated value.
+ * @returns {void}
+ */
+ const onChangePrimary = (value) => {
+ const values = typeof value === 'string' ? [value] : value;
+ const updated = values.map((value) => ({ label: value, primary: true, value }));
+
+ setPrimary(updated);
+ };
+
+ /**
+ * Handle changes to the hyponyms.
+ *
+ * @param {Array} values Updated values.
+ * @returns {void}
+ */
+ const onChangeSynonyms = (values) => {
+ const updated = values.map((value) => ({ label: value, primary: false, value }));
+
+ setSynonyms(updated);
+ };
+
+ /**
+ * Handle deleting rules.
+ *
+ * @param {string} indices Indices of rules to delete.
+ */
+ const onDelete = (indices) => {
+ const ids = indices.filter((index) => rules[index]).map((index) => rules[index].id);
+
+ deleteRules(ids);
+ createNotice('success', messages.deleted);
+ };
+
+ /**
+ * Handle click event for the Cancel button.
+ *
+ * @returns {void}
+ */
+ const onReset = () => {
+ select(null);
+ };
+
+ /**
+ * Handle form submission.
+ *
+ * @param {Event} event Submit event.
+ */
+ const onSubmit = (event) => {
+ event.preventDefault();
+
+ let updatedSynonyms = synonyms;
+
+ if (mode === 'hyponyms') {
+ const hypernym = primary.find((p) => p.value);
+
+ updatedSynonyms = [
+ { ...hypernym, primary: false },
+ ...synonyms.filter((s) => s.value !== hypernym.value),
+ ];
+ }
+
+ const updated = [...primary, ...updatedSynonyms];
+
+ if (edited) {
+ updateRule(edited.id, updated);
+ createNotice('success', messages.updated);
+ select(null);
+ } else {
+ addRule(updated);
+ createNotice('success', messages.added);
+ setPrimary([]);
+ setSynonyms([]);
+ }
+ };
+
+ /**
+ * Handle changes to the edited set.
+ *
+ * @returns {void}
+ */
+ const handleEdited = () => {
+ if (edited) {
+ const primary = edited.synonyms.filter(isPrimary);
+ const synonyms = edited.synonyms.filter(isNotPrimary);
+
+ setPrimary(primary);
+ setSynonyms(synonyms);
+
+ editPanelRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ } else {
+ setPrimary([]);
+ setSynonyms([]);
+ }
+ };
+
+ /**
+ * List table colgroup.
+ *
+ * Outputs an appropriate contents for the list table colgroup.
+ *
+ * @type {WPElement}
+ */
+ const Colgroup = useCallback(
+ () => (
+ <>
+
+ {labels.primary ? : null}
+
+
+ >
+ ),
+ [labels],
+ );
+
+ /**
+ * List table head.
+ *
+ * Outputs appropriate contents for the list table head. Accepts
+ * components for interacting with the table as props.
+ *
+ * @param {object} props Component props.
+ * @param {WPElement} props.CheckAll Check all component.
+ * @param {WPElement} props.DeleteChecked Delete checked component.
+ * @type {WPElement}
+ */
+ const Head = useCallback(
+ ({ CheckAllControl, DeleteCheckedButton }) => (
+
+
+
+
+ {labels.primary ? {labels.primary} : null}
+ {labels.synonyms}
+
+
+
+
+
+
+
+
+ ),
+ [labels],
+ );
+
+ /**
+ * Effects.
+ */
+ useEffect(handleEdited, [edited, isPrimary, isNotPrimary, mode]);
+
+ return (
+ <>
+
+
+ {({ check, checked, remove }) =>
+ rules.map((s, i) => (
+
+
+ check(i, isChecked)}
+ />
+
+ {labels.primary ? (
+
+ {s.synonyms
+ .filter(isPrimary)
+ .map((s) => s.value)
+ .join(', ')}
+
+ ) : null}
+
+ {s.synonyms
+ .filter(isNotPrimary)
+ .map((s) => s.value)
+ .join(', ')}
+
+
+ remove(i)}
+ onSelect={() => select(s.id)}
+ />
+
+
+ ))
+ }
+
+ >
+ );
+};
diff --git a/assets/js/synonyms/components/groups/hyponyms.js b/assets/js/synonyms/components/groups/hyponyms.js
new file mode 100644
index 0000000000..bf9c0fc9bc
--- /dev/null
+++ b/assets/js/synonyms/components/groups/hyponyms.js
@@ -0,0 +1,60 @@
+/**
+ * WordPress dependencies.
+ */
+import { safeHTML } from '@wordpress/dom';
+import { RawHTML, WPElement } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies.
+ */
+import { useSynonymsSettings } from '../../provider';
+import VisualEditor from '../editors/visual-editor';
+
+/**
+ * Hyponyms group component.
+ *
+ * @returns {WPElement}
+ */
+export default () => {
+ const { hyponyms } = useSynonymsSettings();
+
+ return (
+ <>
+
+ {safeHTML(
+ __(
+ 'Hyponyms are terms with a more specific meaning than another more generic terms, called a hypernym . For example, aqua , azure , and cerulean are all hyponyms of blue , their hypernym.
',
+ 'elasticpress',
+ ),
+ )}
+ {safeHTML(
+ __(
+ 'Use hyponyms when you want search queries for a parent term to return results relevant to itself or any of its child terms, but search queries for a child term to only return results that are relevant to that term. For example, when a search for "blue" should return anything blue, whether it be aqua, azure, or cerulean, but a search for "cerulean" should return only items that are specifically cerulean blue.
',
+ 'elasticpress',
+ ),
+ )}
+
+
+ >
+ );
+};
diff --git a/assets/js/synonyms/components/groups/replacements.js b/assets/js/synonyms/components/groups/replacements.js
new file mode 100644
index 0000000000..b3e8369b3c
--- /dev/null
+++ b/assets/js/synonyms/components/groups/replacements.js
@@ -0,0 +1,78 @@
+/**
+ * WordPress dependencies.
+ */
+import { safeHTML } from '@wordpress/dom';
+import { createInterpolateElement, RawHTML, WPElement } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies.
+ */
+import { useSynonymsSettings } from '../../provider';
+import VisualEditor from '../editors/visual-editor';
+
+/**
+ * Replacements group component.
+ *
+ * @returns {WPElement}
+ */
+export default () => {
+ const { replacements } = useSynonymsSettings();
+
+ return (
+ <>
+
+ {safeHTML(
+ __(
+ 'Replacements are terms that replace other incorrect or obsolete terms.
',
+ 'elasticpress',
+ ),
+ )}
+ {safeHTML(
+ __(
+ 'Use replacements when you want search queries for certain terms to return results that are only relevant to another term, or set of terms. This can be useful for supporting specific typos or incorrect phrasing. For example, when a search for the phrase "intensive purposes" should only return results including the phrase "intents and purposes".
',
+ 'elasticpress',
+ ),
+ )}
+
+
+ {createInterpolateElement(
+ __(
+ 'You may need to disable fuzziness to have it working properly.',
+ 'elasticpress',
+ ),
+ {
+ a: (
+ // eslint-disable-next-line jsx-a11y/anchor-has-content, jsx-a11y/control-has-associated-label
+
+ ),
+ },
+ )}
+
+
+ >
+ );
+};
diff --git a/assets/js/synonyms/components/groups/synonyms.js b/assets/js/synonyms/components/groups/synonyms.js
new file mode 100644
index 0000000000..7826463ea0
--- /dev/null
+++ b/assets/js/synonyms/components/groups/synonyms.js
@@ -0,0 +1,56 @@
+/**
+ * WordPress dependencies.
+ */
+import { safeHTML } from '@wordpress/dom';
+import { RawHTML, WPElement } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies.
+ */
+import { useSynonymsSettings } from '../../provider';
+import VisualEditor from '../editors/visual-editor';
+
+/**
+ * Synonyms group component.
+ *
+ * @returns {WPElement}
+ */
+export default () => {
+ const { synonyms } = useSynonymsSettings();
+
+ return (
+ <>
+
+ {safeHTML(
+ __(
+ 'Synonyms are terms with similar meanings. For example, sneaker , tennis shoe , trainer , and running shoe could all refer to a particular type of shoe.
',
+ 'elasticpress',
+ ),
+ )}
+ {safeHTML(
+ __(
+ 'Use synonyms when you want queries for a specific term to also return results relevant to any of its synonyms. This can be useful for supporting products and services whose names have changed over time or regional variations in terminology. For example, when a search for "sneaker" should return sneakers, tennis shoes, trainers and running shoes.
',
+ 'elasticpress',
+ ),
+ )}
+
+
+ >
+ );
+};
diff --git a/assets/js/synonyms/components/icons/error.js b/assets/js/synonyms/components/icons/error.js
new file mode 100644
index 0000000000..2d16608d88
--- /dev/null
+++ b/assets/js/synonyms/components/icons/error.js
@@ -0,0 +1,15 @@
+import { SVG, Path } from '@wordpress/primitives';
+
+export default () => {
+ return (
+
+
+
+ );
+};
diff --git a/assets/js/synonyms/components/shared/LinkedMultiInput.js b/assets/js/synonyms/components/shared/LinkedMultiInput.js
deleted file mode 100644
index c728d210c3..0000000000
--- a/assets/js/synonyms/components/shared/LinkedMultiInput.js
+++ /dev/null
@@ -1,68 +0,0 @@
-/**
- * WordPress dependencies.
- */
-import { FormTokenField } from '@wordpress/components';
-import { useContext, WPElement } from '@wordpress/element';
-
-/**
- * Internal dependencies.
- */
-import { Dispatch } from '../../context';
-
-/**
- * Linked MultiInput
- *
- * @param {object} props Props.
- * @param {string} props.id Set/Alternative id.
- * @param {object[]} props.synonyms Array of synonyms.
- * @param {string} props.removeAction Name of action to dispatch on remove.
- * @param {string} props.updateAction Name of action to dispatch on update.
- * @returns {WPElement} LinkedMultiInput component
- */
-const LinkedMultiInput = ({ id, synonyms, removeAction, updateAction }) => {
- const dispatch = useContext(Dispatch);
- const { removeItemText } = window.epSynonyms.i18n;
-
- /**
- * Handle change to tokens.
- *
- * @param {string[]} value Array of tokens.
- */
- const handleChange = (value) => {
- const tokens = value.map((v) => {
- const token = {
- label: v,
- value: v,
- primary: false,
- };
-
- return token;
- });
-
- dispatch({ type: updateAction, data: { id, tokens } });
- };
-
- /**
- * Handle clearing the synonym.
- */
- const handleClear = () => {
- dispatch({ type: removeAction, data: id });
- };
-
- return (
- <>
- s.value)}
- />
-
-
- {removeItemText}
-
- >
- );
-};
-
-export default LinkedMultiInput;
diff --git a/assets/js/synonyms/config.js b/assets/js/synonyms/config.js
new file mode 100644
index 0000000000..55605d676c
--- /dev/null
+++ b/assets/js/synonyms/config.js
@@ -0,0 +1,6 @@
+/**
+ * Window dependencies.
+ */
+const { apiUrl, defaultIsSolr, defaultSolr, syncUrl } = window.epSynonyms;
+
+export { apiUrl, defaultIsSolr, defaultSolr, syncUrl };
diff --git a/assets/js/synonyms/context.js b/assets/js/synonyms/context.js
deleted file mode 100644
index 7e5c458560..0000000000
--- a/assets/js/synonyms/context.js
+++ /dev/null
@@ -1,31 +0,0 @@
-/**
- * WordPress dependencies.
- */
-import { createContext, useReducer, WPElement } from '@wordpress/element';
-
-/**
- * Internal dependencies.
- */
-import { editorReducer, initialState } from './reducers/editorReducer';
-
-const State = createContext();
-const Dispatch = createContext();
-
-/**
- * App Context.
- *
- * @param {object} props Props.
- * @returns {WPElement} AppContext component
- */
-const AppContext = (props) => {
- const { children } = props;
- const [state, dispatch] = useReducer(editorReducer, initialState);
-
- return (
-
- {children}
-
- );
-};
-
-export { AppContext, State, Dispatch };
diff --git a/assets/js/synonyms/css/edit-panel.css b/assets/js/synonyms/css/edit-panel.css
new file mode 100644
index 0000000000..4973fc7909
--- /dev/null
+++ b/assets/js/synonyms/css/edit-panel.css
@@ -0,0 +1,15 @@
+.ep-synonyms-edit-panel {
+ margin: 16px 0;
+
+ & .components-notice {
+ margin: 0 0 16px 0;
+ }
+
+ & .components-form-token-field {
+
+ & .components-flex {
+ padding-bottom: 4px;
+ padding-top: 4px;
+ }
+ }
+}
diff --git a/assets/js/synonyms/css/error-badge.css b/assets/js/synonyms/css/error-badge.css
new file mode 100644
index 0000000000..3e211e33af
--- /dev/null
+++ b/assets/js/synonyms/css/error-badge.css
@@ -0,0 +1,12 @@
+.ep-synonyms-error-badge {
+ align-items: center;
+ display: flex;
+ height: 36px;
+ justify-content: center;
+ width: 36px;
+
+ & svg {
+ display: block;
+ fill: #cc1818;
+ }
+}
diff --git a/assets/js/synonyms/css/list-table.css b/assets/js/synonyms/css/list-table.css
new file mode 100644
index 0000000000..e9af067b42
--- /dev/null
+++ b/assets/js/synonyms/css/list-table.css
@@ -0,0 +1,57 @@
+.ep-synonyms-list-table {
+ border-collapse: collapse;
+ border-spacing: 16px;
+ table-layout: fixed;
+ width: 100%;
+
+ & td,
+ & th {
+ padding: 8px 8px 8px 16px;
+ }
+
+ & th {
+ font-weight: inherit;
+ text-align: inherit;
+ }
+
+ & thead {
+
+
+ & th {
+ font-weight: 500;
+ }
+ }
+
+ & tbody {
+
+ & tr {
+ border-top: 1px solid #e0e0e0;
+ }
+ }
+
+ & svg {
+ display: block;
+ }
+
+ & .components-base-control__field,
+ & .components-checkbox-control__input-container,
+ & .components-checkbox-control__input {
+ margin: 0;
+ }
+}
+
+.ep-synonyms-list-table__checkbox-column {
+ width: 36px;
+}
+
+.ep-synonyms-list-table__primary-column {
+ width: 25%;
+
+ @nest .ep-synonyms-list-table--replacements & {
+ width: auto;
+ }
+}
+
+.ep-synonyms-list-table__actions-column {
+ width: 140px;
+}
diff --git a/assets/js/synonyms/css/panel.css b/assets/js/synonyms/css/panel.css
new file mode 100644
index 0000000000..53f8abe276
--- /dev/null
+++ b/assets/js/synonyms/css/panel.css
@@ -0,0 +1,16 @@
+.ep-synonyms-panel {
+ margin-bottom: 16px;
+
+ & .components-tab-panel__tabs-item {
+ margin: -1px 0;
+
+ & svg {
+ display: block;
+ fill: #cc1818;
+ }
+ }
+
+ & p {
+ margin-top: 0;
+ }
+}
diff --git a/assets/js/synonyms/css/solr-editor.css b/assets/js/synonyms/css/solr-editor.css
new file mode 100644
index 0000000000..29b10a3982
--- /dev/null
+++ b/assets/js/synonyms/css/solr-editor.css
@@ -0,0 +1,11 @@
+.ep-synonyms-solr-editor {
+ margin-top: 16px;
+
+ & textarea {
+ font-family: Menlo, Consolas, monaco, monospace !important;
+ font-size: 13px !important;
+ line-height: 1.5;
+ overflow-wrap: break-word;
+ overflow-x: hidden;
+ }
+}
diff --git a/assets/js/synonyms/index.js b/assets/js/synonyms/index.js
index b30282d41d..5a6461a075 100644
--- a/assets/js/synonyms/index.js
+++ b/assets/js/synonyms/index.js
@@ -1,35 +1,52 @@
/**
* WordPress dependencies.
*/
-import { createRoot, render } from '@wordpress/element';
+import { createRoot, render, WPElement } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
/**
* Internal dependencies.
*/
-import { AppContext } from './context';
-import SynonymsEditor from './components/SynonymsEditor';
+import { SettingsScreenProvider } from '../settings-screen';
+import { apiUrl, defaultIsSolr, defaultSolr, syncUrl } from './config';
+import { SynonymsSettingsProvider } from './provider';
+import SynonymsSettings from './apps/synonyms-settings';
-const SELECTOR = '#synonym-root';
+/**
+ * Styles.
+ */
+import './style.css';
/**
- * Get Root.
+ * App component.
*
- * @returns {Element|false} Root element
+ * @returns {WPElement}
+ */
+const App = () => (
+
+
+
+
+
+);
+
+/**
+ * Root element.
*/
-const getRoot = () => document.querySelector(SELECTOR) || false;
+const el = document.getElementById('ep-synonyms');
+/**
+ * Render.
+ */
if (typeof createRoot === 'function') {
- const root = createRoot(getRoot());
- root.render(
-
-
- ,
- );
+ const root = createRoot(el);
+
+ root.render( );
} else {
- render(
-
-
- ,
- getRoot(),
- );
+ render( , el);
}
diff --git a/assets/js/synonyms/provider.js b/assets/js/synonyms/provider.js
new file mode 100644
index 0000000000..152d434b5c
--- /dev/null
+++ b/assets/js/synonyms/provider.js
@@ -0,0 +1,291 @@
+/**
+ * WordPress dependencies.
+ */
+import apiFetch from '@wordpress/api-fetch';
+import {
+ createContext,
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useState,
+ WPElement,
+} from '@wordpress/element';
+
+/**
+ * Internal dependencies.
+ */
+import {
+ isHyponyms,
+ isHyponymsValid,
+ isReplacements,
+ isReplacementsValid,
+ isSynonyms,
+ isSynonymsValid,
+ getRule,
+ getRulesFromSolr,
+ getSolrFromRules,
+} from './utils';
+
+/**
+ * Sync context.
+ */
+const Context = createContext();
+
+/**
+ * Synonyms settings context.
+ *
+ * @typedef Synonym
+ * @property {string} value The synonym value.
+ * @property {boolean} primary Whether the synonym is a primary term.
+ *
+ * @typedef Rule
+ * @property {string} id Rule ID.
+ * @property {Synonym[]} synonyms Rule synonyms.
+ * @property {boolean} valid Whether the rule is valid.
+ *
+ * @param {object} props Props.
+ * @param {string} props.apiUrl API Url.
+ * @param {WPElement} props.children Component children.
+ * @param {boolean} props.defaultIsSolr Whether the Solr editor is being used.
+ * @param {Array} props.defaultSolr Default Solr.
+ * @returns {WPElement} AppContext component
+ */
+export const SynonymsSettingsProvider = ({ apiUrl, children, defaultIsSolr, defaultSolr }) => {
+ const defaultRules = useMemo(() => getRulesFromSolr(defaultSolr), [defaultSolr]);
+
+ /**
+ * State.
+ */
+ const [selected, setSelected] = useState(null);
+ const [isBusy, setIsBusy] = useState(false);
+ const [isDirty, setIsDirty] = useState(false);
+ const [isSolr, setIsSolr] = useState(defaultIsSolr);
+ const [rules, setRules] = useState(defaultRules);
+ const [solr, setSolr] = useState(defaultSolr);
+
+ /**
+ * Hyponym rules.
+ *
+ * @type {Rule[]}
+ */
+ const hyponyms = useMemo(() => rules.filter(isHyponyms), [rules]);
+
+ /**
+ * Replacement rules.
+ *
+ * @type {Rule[]}
+ */
+ const replacements = useMemo(() => rules.filter(isReplacements), [rules]);
+
+ /**
+ * Synonym rules.
+ *
+ * @type {Rule[]}
+ */
+ const synonyms = useMemo(() => rules.filter(isSynonyms), [rules]);
+
+ /**
+ * Add a rule.
+ *
+ * @param {Synonym[]} synonyms New synonyms.
+ * @returns {void}
+ */
+ const addRule = (synonyms) => {
+ const updatedRules = [...rules, getRule(synonyms)];
+
+ setRules(updatedRules);
+ setIsDirty(true);
+ };
+
+ /**
+ * Delete rules.
+ *
+ * @param {string[]} ids IDs of rules to remove.
+ * @returns {void}
+ */
+ const deleteRules = (ids) => {
+ const updatedRules = rules.filter((s) => !ids.includes(s.id));
+
+ setRules(updatedRules);
+ setIsDirty(true);
+ };
+
+ /**
+ * Update a rule.
+ *
+ * @param {string} id ID of rule to update.
+ * @param {Synonym[]} synonyms New synonyms.
+ * @returns {void}
+ */
+ const updateRule = (id, synonyms) => {
+ const updatedRules = rules.map((s) => (s.id === id ? getRule(synonyms, id) : s));
+
+ setRules(updatedRules);
+ setIsDirty(true);
+ };
+
+ /**
+ * Update rules.
+ *
+ * @param {Rule[]} rules New rules.
+ * @returns {void}
+ */
+ const updateRules = (rules) => {
+ setRules(rules);
+ setIsDirty(true);
+ };
+
+ /**
+ * Update Solr data.
+ *
+ * @param {string} solr Solr data.
+ */
+ const updateSolr = (solr) => {
+ setSolr(solr);
+ setIsDirty(false);
+ };
+
+ /**
+ * Update Solr data from rules.
+ *
+ * @param {Rule[]} rules Rules.
+ * @returns {void}
+ */
+ const updateSolrFromRules = useCallback((rules) => {
+ const updatedSolr = getSolrFromRules(rules);
+
+ updateSolr(updatedSolr);
+ }, []);
+
+ /**
+ * Update synonym groups from Solr data.
+ *
+ * @param {string} solr Solr data.
+ * @returns {void}
+ */
+ const updateRulesFromSolr = useCallback((solr) => {
+ const updatedRules = getRulesFromSolr(solr);
+
+ updateRules(updatedRules);
+ }, []);
+
+ /**
+ * Validate synonyms.
+ *
+ * @returns {void}
+ */
+ const validate = () => {
+ setRules((rules) =>
+ rules.map((r) => {
+ const rule = { ...r };
+
+ if (isHyponyms(rule)) {
+ rule.valid = isHyponymsValid(rule.synonyms);
+ } else if (isReplacements(rule)) {
+ rule.valid = isReplacementsValid(rule.synonyms);
+ } else {
+ rule.valid = isSynonymsValid(rule.synonyms);
+ }
+
+ return rule;
+ }),
+ );
+ };
+
+ /**
+ * Select a rule for editing.
+ *
+ * @param {string|null} id ID of the rule to select.
+ * @returns {void}
+ */
+ const select = (id) => {
+ setSelected(id);
+ };
+
+ /**
+ * Switch between Solr and visual editing.
+ *
+ * @returns {void}
+ */
+ const switchEditor = useCallback(() => {
+ if (isSolr) {
+ updateRulesFromSolr(solr);
+ } else {
+ updateSolrFromRules(rules);
+ }
+
+ setIsSolr(!isSolr);
+ validate();
+ }, [isSolr, rules, solr, updateRulesFromSolr, updateSolrFromRules]);
+
+ /**
+ * Save settings.
+ *
+ * @returns {void}
+ */
+ const save = async () => {
+ setIsBusy(true);
+
+ const updated = isDirty ? getSolrFromRules(rules) : solr;
+
+ try {
+ const response = await apiFetch({
+ body: JSON.stringify({
+ mode: isSolr ? 'advanced' : 'simple',
+ solr: updated,
+ }),
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ method: 'PUT',
+ url: apiUrl,
+ });
+
+ updateSolr(response.data);
+ updateRulesFromSolr(response.data);
+ } catch (e) {
+ console.error(e); // eslint-disable-line no-console
+ throw e;
+ } finally {
+ setIsBusy(false);
+ }
+ };
+
+ /**
+ * Effects.
+ */
+ useEffect(validate, []);
+
+ // eslint-disable-next-line react/jsx-no-constructed-context-values
+ const contextValue = {
+ addRule,
+ deleteRules,
+ hyponyms,
+ isBusy,
+ isHyponymsValid,
+ isReplacementsValid,
+ isSolr,
+ isSynonymsValid,
+ select,
+ selected,
+ solr,
+ replacements,
+ save,
+ switchEditor,
+ synonyms,
+ updateRule,
+ updateSolr,
+ };
+
+ return {children} ;
+};
+
+/**
+ * Use the Synonyms context.
+ *
+ * @returns {object} API Search Context.
+ */
+export const useSynonymsSettings = () => {
+ return useContext(Context);
+};
diff --git a/assets/js/synonyms/reducers/editorReducer.js b/assets/js/synonyms/reducers/editorReducer.js
deleted file mode 100644
index 89feaafc5e..0000000000
--- a/assets/js/synonyms/reducers/editorReducer.js
+++ /dev/null
@@ -1,145 +0,0 @@
-import { reduceSolrToState, reduceStateToSolr, mapEntry } from '../utils';
-
-/**
- * The synonym editor reducer.
- */
-
-const { alternatives, sets, initialMode } = window.epSynonyms.data;
-const mappedSets = sets ? sets.map(mapEntry) : [mapEntry()];
-const mappedAlternatives = alternatives ? alternatives.map(mapEntry) : [mapEntry()];
-const initialState = {
- isSolrEditable: initialMode === 'advanced',
- isSolrVisible: initialMode === 'advanced',
- alternatives: mappedAlternatives,
- sets: mappedSets,
- solr: reduceStateToSolr({ sets: mappedSets, alternatives: mappedAlternatives }),
- dirty: false,
- submit: false,
-};
-
-/**
- * editorReducer
- *
- * @param {object} state Current state.
- * @param {object} action The action.
- * @returns {object} New state.
- */
-const editorReducer = (state, action) => {
- switch (action.type) {
- case 'ADD_SET':
- return {
- ...state,
- sets: [...state.sets, mapEntry()],
- dirty: true,
- };
- case 'UPDATE_SET':
- return {
- ...state,
- sets: state.sets.map((entry) => {
- if (entry.id !== action.data.id) {
- return entry;
- }
- return mapEntry(action.data.tokens, action.data.id);
- }),
- dirty: true,
- };
- case 'REMOVE_SET':
- return {
- ...state,
- sets: state.sets.filter(({ id }) => id !== action.data),
- dirty: true,
- };
- case 'ADD_ALTERNATIVE':
- return {
- ...state,
- alternatives: [...state.alternatives, mapEntry()],
- dirty: true,
- };
- case 'UPDATE_ALTERNATIVE':
- return {
- ...state,
- alternatives: [
- ...state.alternatives.map((entry) => {
- if (entry.id !== action.data.id) {
- return entry;
- }
- return mapEntry(
- [...action.data.tokens, ...entry.synonyms.filter((t) => t.primary)],
- action.data.id,
- );
- }),
- ],
- dirty: true,
- };
- case 'UPDATE_ALTERNATIVE_PRIMARY':
- return {
- ...state,
- alternatives: [
- ...state.alternatives.map((entry) => {
- if (entry.id !== action.data.id) {
- return entry;
- }
- return mapEntry(
- [action.data.token, ...entry.synonyms.filter((t) => !t.primary)],
- action.data.id,
- );
- }),
- ],
- dirty: true,
- };
- case 'REMOVE_ALTERNATIVE':
- return {
- ...state,
- alternatives: state.alternatives.filter(({ id }) => id !== action.data),
- dirty: true,
- };
- case 'SET_SOLR_EDITABLE':
- return {
- ...state,
- isSolrEditable: !!action.data,
- isSolrVisible: !!action.data,
- };
- case 'UPDATE_SOLR':
- return {
- ...state,
- solr: action.data,
- dirty: true,
- };
- case 'REDUCE_SOLR_TO_STATE':
- return {
- ...reduceSolrToState(state.solr, state),
- dirty: true,
- };
- case 'REDUCE_STATE_TO_SOLR':
- return {
- ...state,
- solr: reduceStateToSolr(state),
- };
- case 'VALIDATE_ALL':
- return {
- ...state,
- sets: state.sets.map((set) => ({
- ...set,
- valid: set.synonyms.length > 1,
- })),
- alternatives: state.alternatives.map((alternative) => ({
- ...alternative,
- valid:
- alternative.synonyms.length > 1 &&
- !!alternative.synonyms.filter(
- ({ primary, value }) => primary && value.length,
- ).length,
- })),
- dirty: false,
- };
- case 'SUBMIT':
- return {
- ...state,
- submit: true,
- };
- default:
- return state;
- }
-};
-
-export { editorReducer, initialState };
diff --git a/assets/js/synonyms/style.css b/assets/js/synonyms/style.css
new file mode 100644
index 0000000000..fa4590c95f
--- /dev/null
+++ b/assets/js/synonyms/style.css
@@ -0,0 +1,5 @@
+@import "./css/edit-panel.css";
+@import "./css/error-badge.css";
+@import "./css/list-table.css";
+@import "./css/panel.css";
+@import "./css/solr-editor.css";
diff --git a/assets/js/synonyms/utils.js b/assets/js/synonyms/utils.js
index a75b0b1c41..57cf0afddc 100644
--- a/assets/js/synonyms/utils.js
+++ b/assets/js/synonyms/utils.js
@@ -1,22 +1,52 @@
+/**
+ * External dependencies.
+ */
import { v4 as uuidv4 } from 'uuid';
/**
- * Generate universally unique identifier.
+ * WordPress dependencies.
+ */
+import { __ } from '@wordpress/i18n';
+
+/**
+ * @typedef Synonym
+ * @property {string} value The synonym value.
+ * @property {boolean} primary Whether the synonym is a primary term.
*
- * @returns {string} A universally unique identifier
+ * @typedef Rule
+ * @property {string} id Rule ID.
+ * @property {Synonym[]} synonyms Rule synonyms.
+ * @property {boolean} valid Whether the rule is valid.
*/
-const uuid = () => {
- return uuidv4();
+
+/**
+ * Determine whether a synonym is a primary term.
+ *
+ * @param {Synonym} synonym Synonym.
+ * @returns {boolean}
+ */
+const isPrimary = (synonym) => {
+ return synonym.primary;
+};
+
+/**
+ * Determine whether a synonym is not a primary term.
+ *
+ * @param {Synonym} synonym Synonym.
+ * @returns {boolean}
+ */
+const isNotPrimary = (synonym) => {
+ return !synonym.primary;
};
/**
- * Map entry
+ * Get a rule object for a list of synonyms,
*
- * @param {Array} synonyms Array of synonyms.
- * @param {string} id The id, default generated by the application.
- * @returns {object} Map entry
+ * @param {Synonym[]} synonyms Array of synonyms.
+ * @param {string} id Rule ID.
+ * @returns {Rule} Map entry
*/
-const mapEntry = (synonyms = [], id = '') => {
+const getRule = (synonyms = [], id = '') => {
return {
id: id.length ? id : uuidv4(),
synonyms,
@@ -24,107 +54,218 @@ const mapEntry = (synonyms = [], id = '') => {
};
};
+/**
+ * Get a rule in Solr format.
+ *
+ * @param {Rule} rule Rule set.
+ * @param {Synonym[]} rule.synonyms Rule synonyms.
+ * @returns {string}
+ */
+const getSolr = ({ synonyms }) => {
+ const terms = synonyms.filter(isPrimary);
+ const replacements = synonyms.filter(isNotPrimary);
+
+ const sides = [terms, replacements]
+ .map((side) =>
+ side
+ .map((synonym) => synonym.value.trim())
+ .filter((synonym) => !!synonym)
+ .join(', '),
+ )
+ .filter((side) => side);
+
+ return sides.join(' => ');
+};
+
+/**
+ * Get synonyms from a Solr line.
+ *
+ * @param {string} line Solr line.
+ * @returns {Synonym[]}
+ */
+const getSynonyms = (line) => {
+ const parts = line.split('=>').map((p, i, a) => {
+ const part = p
+ .split(',')
+ .map((v) => v.trim())
+ .filter((v) => v);
+
+ return part
+ .filter((v, i) => part.indexOf(v) === i)
+ .filter((v) => v)
+ .map((v) => ({
+ label: v,
+ value: v,
+ primary: a.length === 2 && i === 0,
+ }));
+ });
+
+ return parts.flat();
+};
+
+/**
+ * Determine whether a rule describes hyponyms.
+ *
+ * Hyponyms are rules where there is a single primary term and where the
+ * primary term is also included as a replacement.
+ *
+ * @param {Rule} rule Rule set.
+ * @param {Synonym[]} rule.synonyms Rule synonyms.
+ * @returns {boolean}
+ */
+const isHyponyms = (rule) => {
+ const hypernyms = rule.synonyms.filter(isPrimary);
+
+ return (
+ hypernyms.length === 1 &&
+ rule.synonyms.filter(isNotPrimary).some((s) => hypernyms.some((h) => h.value === s.value))
+ );
+};
+
+/**
+ * Validate a new set of hyponyms.
+ *
+ * Hyponyms are valid if there is only one primary term and at least one
+ * replacement that is not also the primary term.
+ *
+ * This function is used before the hypernym is automatically injected as a
+ * hypernym, so make sure to use `isHyponyms` first to verify that the hypernym
+ * is included as a hyponym.
+ *
+ * @param {Array} synonyms Synonyms.
+ * @returns {boolean}
+ */
+const isHyponymsValid = (synonyms) => {
+ const hypernyms = synonyms.filter(isPrimary);
+ const hyponyms = synonyms
+ .filter(isNotPrimary)
+ .filter((s) => !hypernyms.some((h) => h.value === s.value));
+
+ return hypernyms.length === 1 && hyponyms.length > 0;
+};
+
+/**
+ * Determine whether a rule describes replacements.
+ *
+ * Replacements are rules where there are terms and replacements that do not
+ * otherwise describe hyponyms.
+ *
+ * @param {Rule} rule Rule set.
+ * @param {Synonym[]} rule.synonyms Rule synonyms.
+ * @returns {boolean}
+ */
+const isReplacements = ({ synonyms }) => {
+ return !isHyponyms({ synonyms }) && synonyms.some(isPrimary);
+};
+
+/**
+ * Validate a new set of replacements.
+ *
+ * Replacements are valid if there is at least one term and one replacement.
+ *
+ * @param {Array} synonyms Synonyms.
+ * @returns {boolean}
+ */
+const isReplacementsValid = (synonyms) => {
+ return !isHyponyms({ synonyms }) && synonyms.some(isPrimary) && synonyms.some(isNotPrimary);
+};
+
+/**
+ * Is a list of synonyms a synonyms rule set.
+ *
+ *
+ * @param {Rule} rule Rule set.
+ * @param {Synonym[]} rule.synonyms Rule synonyms.
+ * @returns {boolean}
+ */
+const isSynonyms = ({ synonyms }) => {
+ return synonyms.every(isNotPrimary);
+};
+
+/**
+ * Is a synonyms rule set valid.
+ *
+ * @param {Array} synonyms Rule synonyms.
+ * @returns {boolean}
+ */
+const isSynonymsValid = (synonyms) => {
+ return synonyms.length > 1 && synonyms.every(isNotPrimary);
+};
+
/**
* Reduce state to Solr spec.
*
- * @param {object} state Current state.
- * @param {object[]} state.sets Array of synonym sets.
- * @param {object[]} state.alternatives Array of alternative sets.
+ * @param {Rule[]} rules Array of rule sets.
* @returns {string} new state
*/
-const reduceStateToSolr = ({ sets, alternatives }) => {
- const synonymsList = [];
-
- // Handle sets.
- synonymsList.push('# Defined sets ( equivalent synonyms).');
- synonymsList.push(...sets.map(({ synonyms }) => synonyms.map(({ value }) => value).join(', ')));
-
- // Handle alternatives.
- synonymsList.push('\r');
- synonymsList.push('# Defined alternatives (explicit mappings).');
- synonymsList.push(
- ...alternatives.map((alternative) =>
- alternative.synonyms.find((item) => item.primary && item.value.length)
- ? alternative.synonyms
- .find((item) => item.primary)
- .value.concat(' => ')
- .concat(
- alternative.synonyms
- .filter((i) => !i.primary)
- .map(({ value }) => value)
- .join(', '),
- )
- : false,
- ),
- );
+const getSolrFromRules = (rules) => {
+ const synonyms = rules.filter(isSynonyms).map(getSolr);
+ const hyponyms = rules.filter(isHyponyms).map(getSolr);
+ const replacements = rules.filter(isReplacements).map(getSolr);
- return synonymsList.filter(Boolean).join('\n');
+ const lines = [
+ __('# Defined synonyms.', 'elasticpress'),
+ '',
+ ...synonyms,
+ '',
+ __('# Defined hyponyms.', 'elasticpress'),
+ '',
+ ...hyponyms,
+ '',
+ __('# Defined replacements.', 'elasticpress'),
+ '',
+ ...replacements,
+ '',
+ ];
+
+ return lines.join('\n');
};
/**
* Reduce Solr text file to State.
*
- * @param {string} solr A string in the Solr parseable synonym format.
- * @param {object} currentState The current sate.
- * @returns {object} State
- */
-const reduceSolrToState = (solr, currentState) => {
- /**
- * Format token.
- *
- * @param {string} value The value.
- * @param {boolean} primary Whether it's a primary.
- * @returns {object} Formatted token
- */
- const formatToken = (value, primary = false) => {
- return {
- label: value,
- value,
- primary,
- };
- };
+ * @param {string} solr A string in the Solr parseable synonym format.
+ * @returns {Rule[]} State
+ */
+const getRulesFromSolr = (solr) => {
+ const rules = solr.split(/\r?\n/).reduce((rules, line) => {
+ if (line.indexOf('#') === 0) {
+ return rules;
+ }
- return {
- ...currentState,
- ...solr.split(/\r?\n/).reduce(
- (newState, line) => {
- if (line.indexOf('#') === 0 || !line.trim().length) {
- return newState;
- }
-
- if (line.indexOf('=>') !== -1) {
- const parts = line.split('=>');
- return {
- ...newState,
- alternatives: [
- ...newState.alternatives,
- mapEntry([
- formatToken(parts[0].trim(), true),
- ...parts[1]
- .split(',')
- .filter((v) => v.trim())
- .map((token) => formatToken(token.trim())),
- ]),
- ],
- };
- }
-
- return {
- ...newState,
- sets: [
- ...newState.sets,
- mapEntry([
- ...line
- .split(',')
- .filter((v) => v.trim())
- .map((token) => formatToken(token.trim())),
- ]),
- ],
- };
- },
- { alternatives: [], sets: [] },
- ),
- };
+ if (line.trim().length === 0) {
+ return rules;
+ }
+
+ const synonyms = getSynonyms(line);
+ const rule = getRule(synonyms);
+
+ rules.push(rule);
+
+ return rules;
+ }, []);
+
+ return rules;
};
-export { reduceStateToSolr, reduceSolrToState, uuid, mapEntry };
+/**
+ * Generate universally unique identifier.
+ *
+ * @returns {string} A universally unique identifier
+ */
+const uuid = () => {
+ return uuidv4();
+};
+
+export {
+ isHyponyms,
+ isHyponymsValid,
+ isReplacements,
+ isReplacementsValid,
+ isSynonyms,
+ isSynonymsValid,
+ getRule,
+ getRulesFromSolr,
+ getSolrFromRules,
+ uuid,
+};
diff --git a/assets/js/weighting/components/field.js b/assets/js/weighting/components/field.js
index 8477456aa9..814542eb53 100644
--- a/assets/js/weighting/components/field.js
+++ b/assets/js/weighting/components/field.js
@@ -1,7 +1,7 @@
/**
* WordPress Dependencies.
*/
-import { Button, CheckboxControl, RangeControl } from '@wordpress/components';
+import { Button, CheckboxControl, RangeControl, Tooltip } from '@wordpress/components';
import { WPElement } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { trash } from '@wordpress/icons';
@@ -14,9 +14,10 @@ import { trash } from '@wordpress/icons';
* @param {Function} props.onChange Change handler.
* @param {Function} props.onDelete Delete handler.
* @param {object} props.value Values.
+ * @param {boolean} props.showTooltip Whether to show field name tooltip.
* @returns {WPElement} Component element.
*/
-export default ({ label, onChange, onDelete, value }) => {
+export default ({ label, onChange, onDelete, value, showTooltip }) => {
const { enabled = false, weight = 0 } = value;
/**
@@ -45,7 +46,15 @@ export default ({ label, onChange, onDelete, value }) => {
return (
- {label}
+
+ {showTooltip ? (
+
+ {label}
+
+ ) : (
+ label
+ )}
+
{
{
onChange(value, key);
}}
@@ -161,13 +161,14 @@ export default ({ group, postType }) => {
{
onChange(value, key);
}}
onDelete={() => {
onDelete(key);
}}
+ showTooltip
/>
))}
diff --git a/bin/es-docker/elasticsearch.yml b/bin/es-docker/elasticsearch.yml
index 614266091e..9f0857c285 100755
--- a/bin/es-docker/elasticsearch.yml
+++ b/bin/es-docker/elasticsearch.yml
@@ -5,3 +5,5 @@ http.cors.enabled : true
http.cors.allow-origin : "*"
http.cors.allow-methods : OPTIONS, HEAD, GET, POST, PUT, DELETE
http.cors.allow-headers : X-Requested-With,X-Auth-Token,x-elasticpress-request-id,Content-Type,Content-Length
+
+action.destructive_requires_name : false
diff --git a/composer.lock b/composer.lock
index 851ae63483..1afa65d713 100644
--- a/composer.lock
+++ b/composer.lock
@@ -364,16 +364,16 @@
},
{
"name": "composer/ca-bundle",
- "version": "1.3.7",
+ "version": "1.4.0",
"source": {
"type": "git",
"url": "https://github.com/composer/ca-bundle.git",
- "reference": "76e46335014860eec1aa5a724799a00a2e47cc85"
+ "reference": "b66d11b7479109ab547f9405b97205640b17d385"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/composer/ca-bundle/zipball/76e46335014860eec1aa5a724799a00a2e47cc85",
- "reference": "76e46335014860eec1aa5a724799a00a2e47cc85",
+ "url": "https://api.github.com/repos/composer/ca-bundle/zipball/b66d11b7479109ab547f9405b97205640b17d385",
+ "reference": "b66d11b7479109ab547f9405b97205640b17d385",
"shasum": ""
},
"require": {
@@ -385,7 +385,7 @@
"phpstan/phpstan": "^0.12.55",
"psr/log": "^1.0",
"symfony/phpunit-bridge": "^4.2 || ^5",
- "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0 || ^6.0"
+ "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0"
},
"type": "library",
"extra": {
@@ -420,7 +420,7 @@
"support": {
"irc": "irc://irc.freenode.org/composer",
"issues": "https://github.com/composer/ca-bundle/issues",
- "source": "https://github.com/composer/ca-bundle/tree/1.3.7"
+ "source": "https://github.com/composer/ca-bundle/tree/1.4.0"
},
"funding": [
{
@@ -436,7 +436,7 @@
"type": "tidelift"
}
],
- "time": "2023-08-30T09:31:38+00:00"
+ "time": "2023-12-18T12:05:55+00:00"
},
{
"name": "composer/class-map-generator",
@@ -513,16 +513,16 @@
},
{
"name": "composer/composer",
- "version": "2.6.5",
+ "version": "2.7.0",
"source": {
"type": "git",
"url": "https://github.com/composer/composer.git",
- "reference": "4b0fe89db9e65b1e64df633a992e70a7a215ab33"
+ "reference": "96d107e2bfe61bb9eafe55a9d45bd7faed1dd461"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/composer/composer/zipball/4b0fe89db9e65b1e64df633a992e70a7a215ab33",
- "reference": "4b0fe89db9e65b1e64df633a992e70a7a215ab33",
+ "url": "https://api.github.com/repos/composer/composer/zipball/96d107e2bfe61bb9eafe55a9d45bd7faed1dd461",
+ "reference": "96d107e2bfe61bb9eafe55a9d45bd7faed1dd461",
"shasum": ""
},
"require": {
@@ -554,7 +554,7 @@
"phpstan/phpstan-phpunit": "^1.0",
"phpstan/phpstan-strict-rules": "^1",
"phpstan/phpstan-symfony": "^1.2.10",
- "symfony/phpunit-bridge": "^6.0 || ^7"
+ "symfony/phpunit-bridge": "^6.4.1 || ^7.0.1"
},
"suggest": {
"ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages",
@@ -567,7 +567,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "2.6-dev"
+ "dev-main": "2.7-dev"
},
"phpstan": {
"includes": [
@@ -607,7 +607,7 @@
"irc": "ircs://irc.libera.chat:6697/composer",
"issues": "https://github.com/composer/composer/issues",
"security": "https://github.com/composer/composer/security/policy",
- "source": "https://github.com/composer/composer/tree/2.6.5"
+ "source": "https://github.com/composer/composer/tree/2.7.0"
},
"funding": [
{
@@ -623,7 +623,7 @@
"type": "tidelift"
}
],
- "time": "2023-10-06T08:11:52+00:00"
+ "time": "2024-02-08T14:09:19+00:00"
},
{
"name": "composer/metadata-minifier",
@@ -848,16 +848,16 @@
},
{
"name": "composer/spdx-licenses",
- "version": "1.5.7",
+ "version": "1.5.8",
"source": {
"type": "git",
"url": "https://github.com/composer/spdx-licenses.git",
- "reference": "c848241796da2abf65837d51dce1fae55a960149"
+ "reference": "560bdcf8deb88ae5d611c80a2de8ea9d0358cc0a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/c848241796da2abf65837d51dce1fae55a960149",
- "reference": "c848241796da2abf65837d51dce1fae55a960149",
+ "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/560bdcf8deb88ae5d611c80a2de8ea9d0358cc0a",
+ "reference": "560bdcf8deb88ae5d611c80a2de8ea9d0358cc0a",
"shasum": ""
},
"require": {
@@ -906,9 +906,9 @@
"validator"
],
"support": {
- "irc": "irc://irc.freenode.org/composer",
+ "irc": "ircs://irc.libera.chat:6697/composer",
"issues": "https://github.com/composer/spdx-licenses/issues",
- "source": "https://github.com/composer/spdx-licenses/tree/1.5.7"
+ "source": "https://github.com/composer/spdx-licenses/tree/1.5.8"
},
"funding": [
{
@@ -924,7 +924,7 @@
"type": "tidelift"
}
],
- "time": "2022-05-23T07:37:50+00:00"
+ "time": "2023-11-20T07:44:33+00:00"
},
{
"name": "composer/xdebug-handler",
@@ -2466,24 +2466,24 @@
},
{
"name": "react/promise",
- "version": "v3.0.0",
+ "version": "v3.1.0",
"source": {
"type": "git",
"url": "https://github.com/reactphp/promise.git",
- "reference": "c86753c76fd3be465d93b308f18d189f01a22be4"
+ "reference": "e563d55d1641de1dea9f5e84f3cccc66d2bfe02c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/reactphp/promise/zipball/c86753c76fd3be465d93b308f18d189f01a22be4",
- "reference": "c86753c76fd3be465d93b308f18d189f01a22be4",
+ "url": "https://api.github.com/repos/reactphp/promise/zipball/e563d55d1641de1dea9f5e84f3cccc66d2bfe02c",
+ "reference": "e563d55d1641de1dea9f5e84f3cccc66d2bfe02c",
"shasum": ""
},
"require": {
"php": ">=7.1.0"
},
"require-dev": {
- "phpstan/phpstan": "1.10.20 || 1.4.10",
- "phpunit/phpunit": "^9.5 || ^7.5"
+ "phpstan/phpstan": "1.10.39 || 1.4.10",
+ "phpunit/phpunit": "^9.6 || ^7.5"
},
"type": "library",
"autoload": {
@@ -2527,7 +2527,7 @@
],
"support": {
"issues": "https://github.com/reactphp/promise/issues",
- "source": "https://github.com/reactphp/promise/tree/v3.0.0"
+ "source": "https://github.com/reactphp/promise/tree/v3.1.0"
},
"funding": [
{
@@ -2535,7 +2535,7 @@
"type": "open_collective"
}
],
- "time": "2023-07-11T16:12:49+00:00"
+ "time": "2023-11-16T16:21:57+00:00"
},
{
"name": "sebastian/cli-parser",
@@ -3503,16 +3503,16 @@
},
{
"name": "seld/jsonlint",
- "version": "1.10.0",
+ "version": "1.10.2",
"source": {
"type": "git",
"url": "https://github.com/Seldaek/jsonlint.git",
- "reference": "594fd6462aad8ecee0b45ca5045acea4776667f1"
+ "reference": "9bb7db07b5d66d90f6ebf542f09fc67d800e5259"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/594fd6462aad8ecee0b45ca5045acea4776667f1",
- "reference": "594fd6462aad8ecee0b45ca5045acea4776667f1",
+ "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/9bb7db07b5d66d90f6ebf542f09fc67d800e5259",
+ "reference": "9bb7db07b5d66d90f6ebf542f09fc67d800e5259",
"shasum": ""
},
"require": {
@@ -3539,7 +3539,7 @@
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
- "homepage": "http://seld.be"
+ "homepage": "https://seld.be"
}
],
"description": "JSON Linter",
@@ -3551,7 +3551,7 @@
],
"support": {
"issues": "https://github.com/Seldaek/jsonlint/issues",
- "source": "https://github.com/Seldaek/jsonlint/tree/1.10.0"
+ "source": "https://github.com/Seldaek/jsonlint/tree/1.10.2"
},
"funding": [
{
@@ -3563,7 +3563,7 @@
"type": "tidelift"
}
],
- "time": "2023-05-11T13:16:46+00:00"
+ "time": "2024-02-07T12:57:50+00:00"
},
{
"name": "seld/phar-utils",
@@ -3886,16 +3886,16 @@
},
{
"name": "symfony/cache-contracts",
- "version": "v3.3.0",
+ "version": "v3.4.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/cache-contracts.git",
- "reference": "ad945640ccc0ae6e208bcea7d7de4b39b569896b"
+ "reference": "1d74b127da04ffa87aa940abe15446fa89653778"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/ad945640ccc0ae6e208bcea7d7de4b39b569896b",
- "reference": "ad945640ccc0ae6e208bcea7d7de4b39b569896b",
+ "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/1d74b127da04ffa87aa940abe15446fa89653778",
+ "reference": "1d74b127da04ffa87aa940abe15446fa89653778",
"shasum": ""
},
"require": {
@@ -3942,7 +3942,7 @@
"standards"
],
"support": {
- "source": "https://github.com/symfony/cache-contracts/tree/v3.3.0"
+ "source": "https://github.com/symfony/cache-contracts/tree/v3.4.0"
},
"funding": [
{
@@ -3958,20 +3958,20 @@
"type": "tidelift"
}
],
- "time": "2023-05-23T14:45:45+00:00"
+ "time": "2023-09-25T12:52:38+00:00"
},
{
"name": "symfony/console",
- "version": "v5.4.28",
+ "version": "v5.4.35",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
- "reference": "f4f71842f24c2023b91237c72a365306f3c58827"
+ "reference": "dbdf6adcb88d5f83790e1efb57ef4074309d3931"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/console/zipball/f4f71842f24c2023b91237c72a365306f3c58827",
- "reference": "f4f71842f24c2023b91237c72a365306f3c58827",
+ "url": "https://api.github.com/repos/symfony/console/zipball/dbdf6adcb88d5f83790e1efb57ef4074309d3931",
+ "reference": "dbdf6adcb88d5f83790e1efb57ef4074309d3931",
"shasum": ""
},
"require": {
@@ -4041,7 +4041,7 @@
"terminal"
],
"support": {
- "source": "https://github.com/symfony/console/tree/v5.4.28"
+ "source": "https://github.com/symfony/console/tree/v5.4.35"
},
"funding": [
{
@@ -4057,11 +4057,11 @@
"type": "tidelift"
}
],
- "time": "2023-08-07T06:12:30+00:00"
+ "time": "2024-01-23T14:28:09+00:00"
},
{
"name": "symfony/deprecation-contracts",
- "version": "v3.3.0",
+ "version": "v3.4.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
@@ -4108,7 +4108,7 @@
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/deprecation-contracts/tree/v3.3.0"
+ "source": "https://github.com/symfony/deprecation-contracts/tree/v3.4.0"
},
"funding": [
{
@@ -4128,16 +4128,16 @@
},
{
"name": "symfony/filesystem",
- "version": "v6.3.1",
+ "version": "v6.4.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
- "reference": "edd36776956f2a6fcf577edb5b05eb0e3bdc52ae"
+ "reference": "7f3b1755eb49297a0827a7575d5d2b2fd11cc9fb"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/filesystem/zipball/edd36776956f2a6fcf577edb5b05eb0e3bdc52ae",
- "reference": "edd36776956f2a6fcf577edb5b05eb0e3bdc52ae",
+ "url": "https://api.github.com/repos/symfony/filesystem/zipball/7f3b1755eb49297a0827a7575d5d2b2fd11cc9fb",
+ "reference": "7f3b1755eb49297a0827a7575d5d2b2fd11cc9fb",
"shasum": ""
},
"require": {
@@ -4171,7 +4171,7 @@
"description": "Provides basic utilities for the filesystem",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/filesystem/tree/v6.3.1"
+ "source": "https://github.com/symfony/filesystem/tree/v6.4.3"
},
"funding": [
{
@@ -4187,20 +4187,20 @@
"type": "tidelift"
}
],
- "time": "2023-06-01T08:30:39+00:00"
+ "time": "2024-01-23T14:51:35+00:00"
},
{
"name": "symfony/finder",
- "version": "v5.4.27",
+ "version": "v5.4.35",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
- "reference": "ff4bce3c33451e7ec778070e45bd23f74214cd5d"
+ "reference": "abe6d6f77d9465fed3cd2d029b29d03b56b56435"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/finder/zipball/ff4bce3c33451e7ec778070e45bd23f74214cd5d",
- "reference": "ff4bce3c33451e7ec778070e45bd23f74214cd5d",
+ "url": "https://api.github.com/repos/symfony/finder/zipball/abe6d6f77d9465fed3cd2d029b29d03b56b56435",
+ "reference": "abe6d6f77d9465fed3cd2d029b29d03b56b56435",
"shasum": ""
},
"require": {
@@ -4234,7 +4234,7 @@
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/finder/tree/v5.4.27"
+ "source": "https://github.com/symfony/finder/tree/v5.4.35"
},
"funding": [
{
@@ -4250,20 +4250,20 @@
"type": "tidelift"
}
],
- "time": "2023-07-31T08:02:31+00:00"
+ "time": "2024-01-23T13:51:25+00:00"
},
{
"name": "symfony/polyfill-ctype",
- "version": "v1.28.0",
+ "version": "v1.29.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
- "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb"
+ "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
- "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
+ "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4",
+ "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4",
"shasum": ""
},
"require": {
@@ -4277,9 +4277,6 @@
},
"type": "library",
"extra": {
- "branch-alias": {
- "dev-main": "1.28-dev"
- },
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
@@ -4316,7 +4313,7 @@
"portable"
],
"support": {
- "source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0"
+ "source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0"
},
"funding": [
{
@@ -4332,20 +4329,20 @@
"type": "tidelift"
}
],
- "time": "2023-01-26T09:26:14+00:00"
+ "time": "2024-01-29T20:11:03+00:00"
},
{
"name": "symfony/polyfill-intl-grapheme",
- "version": "v1.28.0",
+ "version": "v1.29.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
- "reference": "875e90aeea2777b6f135677f618529449334a612"
+ "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/875e90aeea2777b6f135677f618529449334a612",
- "reference": "875e90aeea2777b6f135677f618529449334a612",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/32a9da87d7b3245e09ac426c83d334ae9f06f80f",
+ "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f",
"shasum": ""
},
"require": {
@@ -4356,9 +4353,6 @@
},
"type": "library",
"extra": {
- "branch-alias": {
- "dev-main": "1.28-dev"
- },
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
@@ -4397,7 +4391,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.28.0"
+ "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.29.0"
},
"funding": [
{
@@ -4413,20 +4407,20 @@
"type": "tidelift"
}
],
- "time": "2023-01-26T09:26:14+00:00"
+ "time": "2024-01-29T20:11:03+00:00"
},
{
"name": "symfony/polyfill-intl-normalizer",
- "version": "v1.28.0",
+ "version": "v1.29.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
- "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92"
+ "reference": "bc45c394692b948b4d383a08d7753968bed9a83d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92",
- "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/bc45c394692b948b4d383a08d7753968bed9a83d",
+ "reference": "bc45c394692b948b4d383a08d7753968bed9a83d",
"shasum": ""
},
"require": {
@@ -4437,9 +4431,6 @@
},
"type": "library",
"extra": {
- "branch-alias": {
- "dev-main": "1.28-dev"
- },
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
@@ -4481,7 +4472,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.28.0"
+ "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.29.0"
},
"funding": [
{
@@ -4497,20 +4488,20 @@
"type": "tidelift"
}
],
- "time": "2023-01-26T09:26:14+00:00"
+ "time": "2024-01-29T20:11:03+00:00"
},
{
"name": "symfony/polyfill-mbstring",
- "version": "v1.28.0",
+ "version": "v1.29.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
- "reference": "42292d99c55abe617799667f454222c54c60e229"
+ "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229",
- "reference": "42292d99c55abe617799667f454222c54c60e229",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec",
+ "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec",
"shasum": ""
},
"require": {
@@ -4524,9 +4515,6 @@
},
"type": "library",
"extra": {
- "branch-alias": {
- "dev-main": "1.28-dev"
- },
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
@@ -4564,7 +4552,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0"
+ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0"
},
"funding": [
{
@@ -4580,20 +4568,20 @@
"type": "tidelift"
}
],
- "time": "2023-07-28T09:04:16+00:00"
+ "time": "2024-01-29T20:11:03+00:00"
},
{
"name": "symfony/polyfill-php73",
- "version": "v1.28.0",
+ "version": "v1.29.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php73.git",
- "reference": "fe2f306d1d9d346a7fee353d0d5012e401e984b5"
+ "reference": "21bd091060673a1177ae842c0ef8fe30893114d2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/fe2f306d1d9d346a7fee353d0d5012e401e984b5",
- "reference": "fe2f306d1d9d346a7fee353d0d5012e401e984b5",
+ "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/21bd091060673a1177ae842c0ef8fe30893114d2",
+ "reference": "21bd091060673a1177ae842c0ef8fe30893114d2",
"shasum": ""
},
"require": {
@@ -4601,9 +4589,6 @@
},
"type": "library",
"extra": {
- "branch-alias": {
- "dev-main": "1.28-dev"
- },
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
@@ -4643,7 +4628,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-php73/tree/v1.28.0"
+ "source": "https://github.com/symfony/polyfill-php73/tree/v1.29.0"
},
"funding": [
{
@@ -4659,20 +4644,20 @@
"type": "tidelift"
}
],
- "time": "2023-01-26T09:26:14+00:00"
+ "time": "2024-01-29T20:11:03+00:00"
},
{
"name": "symfony/polyfill-php80",
- "version": "v1.28.0",
+ "version": "v1.29.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
- "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5"
+ "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5",
- "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5",
+ "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/87b68208d5c1188808dd7839ee1e6c8ec3b02f1b",
+ "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b",
"shasum": ""
},
"require": {
@@ -4680,9 +4665,6 @@
},
"type": "library",
"extra": {
- "branch-alias": {
- "dev-main": "1.28-dev"
- },
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
@@ -4726,7 +4708,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0"
+ "source": "https://github.com/symfony/polyfill-php80/tree/v1.29.0"
},
"funding": [
{
@@ -4742,20 +4724,20 @@
"type": "tidelift"
}
],
- "time": "2023-01-26T09:26:14+00:00"
+ "time": "2024-01-29T20:11:03+00:00"
},
{
"name": "symfony/polyfill-php81",
- "version": "v1.28.0",
+ "version": "v1.29.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php81.git",
- "reference": "7581cd600fa9fd681b797d00b02f068e2f13263b"
+ "reference": "c565ad1e63f30e7477fc40738343c62b40bc672d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/7581cd600fa9fd681b797d00b02f068e2f13263b",
- "reference": "7581cd600fa9fd681b797d00b02f068e2f13263b",
+ "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/c565ad1e63f30e7477fc40738343c62b40bc672d",
+ "reference": "c565ad1e63f30e7477fc40738343c62b40bc672d",
"shasum": ""
},
"require": {
@@ -4763,9 +4745,6 @@
},
"type": "library",
"extra": {
- "branch-alias": {
- "dev-main": "1.28-dev"
- },
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
@@ -4805,7 +4784,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-php81/tree/v1.28.0"
+ "source": "https://github.com/symfony/polyfill-php81/tree/v1.29.0"
},
"funding": [
{
@@ -4821,20 +4800,20 @@
"type": "tidelift"
}
],
- "time": "2023-01-26T09:26:14+00:00"
+ "time": "2024-01-29T20:11:03+00:00"
},
{
"name": "symfony/process",
- "version": "v6.3.4",
+ "version": "v6.4.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
- "reference": "0b5c29118f2e980d455d2e34a5659f4579847c54"
+ "reference": "31642b0818bfcff85930344ef93193f8c607e0a3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/process/zipball/0b5c29118f2e980d455d2e34a5659f4579847c54",
- "reference": "0b5c29118f2e980d455d2e34a5659f4579847c54",
+ "url": "https://api.github.com/repos/symfony/process/zipball/31642b0818bfcff85930344ef93193f8c607e0a3",
+ "reference": "31642b0818bfcff85930344ef93193f8c607e0a3",
"shasum": ""
},
"require": {
@@ -4866,7 +4845,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/process/tree/v6.3.4"
+ "source": "https://github.com/symfony/process/tree/v6.4.3"
},
"funding": [
{
@@ -4882,7 +4861,7 @@
"type": "tidelift"
}
],
- "time": "2023-08-07T10:39:22+00:00"
+ "time": "2024-01-23T14:51:35+00:00"
},
{
"name": "symfony/service-contracts",
@@ -4965,16 +4944,16 @@
},
{
"name": "symfony/string",
- "version": "v6.3.5",
+ "version": "v6.4.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
- "reference": "13d76d0fb049051ed12a04bef4f9de8715bea339"
+ "reference": "7a14736fb179876575464e4658fce0c304e8c15b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/string/zipball/13d76d0fb049051ed12a04bef4f9de8715bea339",
- "reference": "13d76d0fb049051ed12a04bef4f9de8715bea339",
+ "url": "https://api.github.com/repos/symfony/string/zipball/7a14736fb179876575464e4658fce0c304e8c15b",
+ "reference": "7a14736fb179876575464e4658fce0c304e8c15b",
"shasum": ""
},
"require": {
@@ -4988,11 +4967,11 @@
"symfony/translation-contracts": "<2.5"
},
"require-dev": {
- "symfony/error-handler": "^5.4|^6.0",
- "symfony/http-client": "^5.4|^6.0",
- "symfony/intl": "^6.2",
+ "symfony/error-handler": "^5.4|^6.0|^7.0",
+ "symfony/http-client": "^5.4|^6.0|^7.0",
+ "symfony/intl": "^6.2|^7.0",
"symfony/translation-contracts": "^2.5|^3.0",
- "symfony/var-exporter": "^5.4|^6.0"
+ "symfony/var-exporter": "^5.4|^6.0|^7.0"
},
"type": "library",
"autoload": {
@@ -5031,7 +5010,7 @@
"utf8"
],
"support": {
- "source": "https://github.com/symfony/string/tree/v6.3.5"
+ "source": "https://github.com/symfony/string/tree/v6.4.3"
},
"funding": [
{
@@ -5047,7 +5026,7 @@
"type": "tidelift"
}
],
- "time": "2023-09-18T10:38:32+00:00"
+ "time": "2024-01-25T09:26:29+00:00"
},
{
"name": "symfony/var-exporter",
@@ -5314,5 +5293,5 @@
"php": ">=7.4"
},
"platform-dev": [],
- "plugin-api-version": "2.3.0"
+ "plugin-api-version": "2.6.0"
}
diff --git a/elasticpress.php b/elasticpress.php
index 4265135e26..0564b589e1 100644
--- a/elasticpress.php
+++ b/elasticpress.php
@@ -3,7 +3,7 @@
* Plugin Name: ElasticPress
* Plugin URI: https://github.com/10up/ElasticPress
* Description: A fast and flexible search and query engine for WordPress.
- * Version: 5.0.2
+ * Version: 5.1.0
* Requires at least: 6.0
* Requires PHP: 7.4
* Author: 10up
@@ -32,7 +32,7 @@
define( 'EP_URL', plugin_dir_url( __FILE__ ) );
define( 'EP_PATH', plugin_dir_path( __FILE__ ) );
define( 'EP_FILE', plugin_basename( __FILE__ ) );
-define( 'EP_VERSION', '5.0.2' );
+define( 'EP_VERSION', '5.1.0' );
define( 'EP_PHP_VERSION_MIN', '7.4' );
@@ -105,7 +105,7 @@ function( $class ) {
*
* @since 2.2
*/
-define( 'EP_ES_VERSION_MAX', '7.10' );
+define( 'EP_ES_VERSION_MAX', '8.99' );
define( 'EP_ES_VERSION_MIN', '5.2' );
require_once __DIR__ . '/includes/compat.php';
diff --git a/includes/classes/Elasticsearch.php b/includes/classes/Elasticsearch.php
index 69f8990173..e5d139ac18 100644
--- a/includes/classes/Elasticsearch.php
+++ b/includes/classes/Elasticsearch.php
@@ -78,7 +78,7 @@ public static function factory() {
* @param array $document Formatted Elasticsearch document.
* @param boolean $blocking Blocking HTTP request or not.
* @since 3.0
- * @return boolean|array
+ * @return boolean|object
*/
public function index_document( $index, $type, $document, $blocking = true ) {
/**
@@ -404,6 +404,14 @@ public function query( $index, $type, $query, $query_args, $query_object = null
* @param {array} $query_args Current WP Query arguments
*/
do_action( 'ep_retrieve_aggregations', $response['aggregations'], $query, '', $query_args );
+
+ if ( is_object( $query_object ) ) {
+ if ( method_exists( $query_object, 'set' ) ) {
+ $query_object->set( 'ep_aggregations', $response['aggregations'] );
+ } else {
+ $query_object->query_vars['ep_aggregations'] = $response['aggregations'];
+ }
+ }
}
/**
@@ -456,11 +464,15 @@ public function query( $index, $type, $query, $query_args, $query_object = null
*
* @hook ep_es_query_results
* @param {array} $results Results from Elasticsearch
- * @param {response} $response Raw response from Elasticsearch
- * @param {array} $query Raw Elasticsearch query
- * @param {array} $query_args Query arguments
- * @param {mixed} $query_object Could be WP_Query, WP_User_Query, etc.
- * @return {array} New results
+ * @param {int} $results.found_documents Total number of documents.
+ * @param {array} $results.documents Array of documents.
+ * @param {array} $results.aggregations Array of aggregations.
+ * @param {array} $results.suggest Array of suggestions.
+ * @param {response} $response Raw response from Elasticsearch
+ * @param {array} $query Raw Elasticsearch query
+ * @param {array} $query_args Query arguments
+ * @param {mixed} $query_object Could be WP_Query, WP_User_Query, etc.
+ * @return {array} New results
*/
return apply_filters(
'ep_es_query_results',
@@ -1483,18 +1495,22 @@ public function set_elasticsearch_info( $force = false ) {
// Save version of last node. We assume all nodes are same version.
$this->elasticsearch_version = $node['version'];
+ // Elasticsearch calls "modules" all default plugins that can't be uninstalled
+ if ( isset( $node['modules'] ) && is_array( $node['modules'] ) ) {
+ foreach ( $node['modules'] as $plugin ) {
+ $this->elasticsearch_plugins[ $plugin['name'] ] = $plugin['version'];
+ }
+
+ if ( ! empty( $node['modules'] ) && ! empty( $node['modules'][0]['opensearch_version'] ) ) {
+ $this->server_type = 'opensearch';
+ }
+ }
+
if ( isset( $node['plugins'] ) && is_array( $node['plugins'] ) ) {
foreach ( $node['plugins'] as $plugin ) {
$this->elasticsearch_plugins[ $plugin['name'] ] = $plugin['version'];
}
}
- if ( isset( $node['modules'] )
- && is_array( $node['modules'] )
- && ! empty( $node['modules'] )
- && ! empty( $node['modules'][0]['opensearch_version'] )
- ) {
- $this->server_type = 'opensearch';
- }
}
/**
diff --git a/includes/classes/ElasticsearchErrorInterpreter.php b/includes/classes/ElasticsearchErrorInterpreter.php
index 37692062f9..cf39e2060d 100644
--- a/includes/classes/ElasticsearchErrorInterpreter.php
+++ b/includes/classes/ElasticsearchErrorInterpreter.php
@@ -97,7 +97,24 @@ public function maybe_suggest_solution_for_es( $error ) {
];
}
+ if ( preg_match( '/Number of (.*) index errors/', $error, $matches ) ) {
+ return [
+ 'error' => $error,
+ 'solution' => '',
+ ];
+ }
+
if ( Utils\is_epio() ) {
+ if ( preg_match( '/you have reached the limit of indices your plan supports/', $error, $matches ) ) {
+ return [
+ 'error' => $error,
+ 'solution' => sprintf(
+ /* translators: ElasticPress.io Article URL */
+ __( 'Please refer to this article outlining how to address this issue.', 'elasticpress' ),
+ 'https://elasticpress.zendesk.com/hc/en-us/articles/26165267320461'
+ ),
+ ];
+ }
return [
'error' => $error,
'solution' => sprintf(
diff --git a/includes/classes/Feature/Autosuggest/Autosuggest.php b/includes/classes/Feature/Autosuggest/Autosuggest.php
index c121ae105a..d77f4cdbc1 100644
--- a/includes/classes/Feature/Autosuggest/Autosuggest.php
+++ b/includes/classes/Feature/Autosuggest/Autosuggest.php
@@ -180,7 +180,7 @@ public function mapping( $mapping ) {
* @return array
*/
public function set_fuzziness( $fuzziness, $search_fields, $args ) {
- if ( Utils\is_integrated_request( $this->slug, [ 'public' ] ) && ! empty( $args['s'] ) ) {
+ if ( Utils\is_integrated_request( $this->slug, $this->get_contexts() ) && ! empty( $args['s'] ) ) {
return 'auto';
}
return $fuzziness;
@@ -195,7 +195,7 @@ public function set_fuzziness( $fuzziness, $search_fields, $args ) {
* @return array $query adjusted ES Query arguments
*/
public function adjust_fuzzy_fields( $query, $post_type, $args ) {
- if ( ! Utils\is_integrated_request( $this->slug, [ 'public' ] ) || empty( $args['s'] ) ) {
+ if ( ! Utils\is_integrated_request( $this->slug, $this->get_contexts() ) || empty( $args['s'] ) ) {
return $query;
}
@@ -901,15 +901,17 @@ protected function set_settings_schema() {
$this->maybe_add_epio_settings_schema();
- $set_in_wp_config = defined( 'EP_AUTOSUGGEST_ENDPOINT' ) && EP_AUTOSUGGEST_ENDPOINT;
-
- $this->settings_schema[] = [
- 'disabled' => $set_in_wp_config,
- 'help' => $set_in_wp_config ? __( 'This address will be exposed to the public.', 'elasticpress' ) : '',
- 'key' => 'endpoint_url',
- 'label' => __( 'Endpoint URL', 'elasticpress' ),
- 'type' => 'url',
- ];
+ if ( ! Utils\is_epio() ) {
+ $set_in_wp_config = defined( 'EP_AUTOSUGGEST_ENDPOINT' ) && EP_AUTOSUGGEST_ENDPOINT;
+
+ $this->settings_schema[] = [
+ 'disabled' => $set_in_wp_config,
+ 'help' => $set_in_wp_config ? __( 'This address will be exposed to the public.', 'elasticpress' ) : '',
+ 'key' => 'endpoint_url',
+ 'label' => __( 'Endpoint URL', 'elasticpress' ),
+ 'type' => 'url',
+ ];
+ }
}
/**
@@ -924,4 +926,22 @@ public function delete_cached_query() {
'ElasticPress 4.7.0'
);
}
+
+ /**
+ * Get the contexts for autosuggest.
+ *
+ * @since 5.1.0
+ * @return array
+ */
+ protected function get_contexts() : array {
+ /**
+ * Filter contexts for autosuggest.
+ *
+ * @hook ep_autosuggest_contexts
+ * @since 5.1.0
+ * @param {array} $contexts Contexts for autosuggest
+ * @return {array} New contexts
+ */
+ return apply_filters( 'ep_autosuggest_contexts', [ 'public', 'ajax' ] );
+ }
}
diff --git a/includes/classes/Feature/Documents/Documents.php b/includes/classes/Feature/Documents/Documents.php
index f5046b011d..f7e0d552b3 100644
--- a/includes/classes/Feature/Documents/Documents.php
+++ b/includes/classes/Feature/Documents/Documents.php
@@ -60,6 +60,8 @@ public function setup() {
add_filter( 'ep_weighting_fields_for_post_type', [ $this, 'filter_weightable_fields_for_post_type' ], 10, 2 );
add_filter( 'ep_weighting_default_post_type_weights', [ $this, 'filter_attachment_post_type_weights' ], 10, 2 );
+
+ add_filter( 'ep_ajax_wp_query_integration', [ $this, 'maybe_enable_ajax_wp_query_integration' ] );
}
/**
@@ -110,10 +112,7 @@ public function attachments_mapping( $mapping ) {
}
/**
- * This is some complex logic to handle the front end search query. If we have a search query,
- * add the attachment post type to post_type and inherit to post_status. If post_status is not set,
- * we assume publish/inherit is wanted. post_type should always be set. We also add allowed mime types.
- * If mime types are already set, append.
+ * Handle the search query
*
* @param WP_Query $query WP_Query to modify to search.
* @since 2.3
@@ -123,57 +122,21 @@ public function setup_document_search( $query ) {
return;
}
+ // If not a search, return.
$s = $query->get( 's', false );
-
if ( empty( $s ) ) {
return;
}
- $post_status = $query->get( 'post_status', [] );
- $post_type = $query->get( 'post_type', [] );
- $mime_types = $query->get( 'post_mime_type', [] );
-
- if ( ! empty( $post_type ) ) {
- if ( 'any' !== $post_type ) {
- if ( is_string( $post_type ) ) {
- $post_type = explode( ' ', $post_type );
- $post_type[] = 'attachment';
-
- $query->set( 'post_type', array_unique( $post_type ) );
- }
- }
- }
-
- if ( empty( $post_status ) ) {
- $post_status = array_values(
- get_post_stati(
- [
- 'public' => true,
- 'exclude_from_search' => false,
- ]
- )
- );
-
- // Add inherit for documents
- $post_status[] = 'inherit';
- } else {
- if ( is_string( $post_status ) ) {
- $post_status = explode( ' ', $post_status );
- }
-
- $post_status[] = 'inherit';
- }
-
- $query->set( 'post_status', array_unique( $post_status ) );
-
- if ( ! empty( $mime_types ) && is_string( $mime_types ) ) {
- $mime_types = explode( ' ', $mime_types );
+ // Return if attachments are not involved in the query.
+ // If post_type is empty, attachments will be included automatically.
+ $post_type = (array) $query->get( 'post_type', [] );
+ if ( ! empty( $post_type ) && ! in_array( 'attachment', $post_type, true ) ) {
+ return;
}
- $mime_types = array_merge( $mime_types, $this->get_allowed_ingest_mime_types() );
- $mime_types[] = ''; // This let's us query non-attachments as well as attachments.
-
- $query->set( 'post_mime_type', array_unique( array_values( $mime_types ) ) );
+ $this->maybe_set_post_status( $query );
+ $this->maybe_set_mime_type( $query );
}
/**
@@ -190,25 +153,29 @@ public function index_request_path( $path, $post, $type ) {
return $path;
}
- if ( 'attachment' === $post['post_type'] ) {
- if ( ! empty( $post['attachments'][0]['data'] ) && isset( $post['post_mime_type'] ) && in_array( $post['post_mime_type'], $this->get_allowed_ingest_mime_types(), true ) ) {
- $index = Indexables::factory()->get( 'post' )->get_index_name();
+ if ( 'attachment' !== $post['post_type'] ) {
+ return $path;
+ }
- /**
- * Filter documents pipeline ID
- *
- * @hook ep_documents_pipeline_id
- * @param {string} $id Pipeline ID
- * @return {string} new ID
- */
- $pipeline_id = apply_filters( 'ep_documents_pipeline_id', Indexables::factory()->get( 'post' )->get_index_name() . '-attachment' );
+ if ( empty( $post['attachments'][0]['data'] ) || ! isset( $post['post_mime_type'] ) || ! in_array( $post['post_mime_type'], $this->get_allowed_ingest_mime_types(), true ) ) {
+ return $path;
+ }
- if ( version_compare( (string) Elasticsearch::factory()->get_elasticsearch_version(), '7.0', '<' ) ) {
- $path = trailingslashit( $index ) . 'post/' . $post['ID'] . '?pipeline=' . $pipeline_id;
- } else {
- $path = trailingslashit( $index ) . '_doc/' . $post['ID'] . '?pipeline=' . $pipeline_id;
- }
- }
+ $index = Indexables::factory()->get( 'post' )->get_index_name();
+
+ /**
+ * Filter documents pipeline ID
+ *
+ * @hook ep_documents_pipeline_id
+ * @param {string} $id Pipeline ID
+ * @return {string} new ID
+ */
+ $pipeline_id = apply_filters( 'ep_documents_pipeline_id', Indexables::factory()->get( 'post' )->get_index_name() . '-attachment' );
+
+ if ( version_compare( (string) Elasticsearch::factory()->get_elasticsearch_version(), '7.0', '<' ) ) {
+ $path = trailingslashit( $index ) . 'post/' . $post['ID'] . '?pipeline=' . $pipeline_id;
+ } else {
+ $path = trailingslashit( $index ) . '_doc/' . $post['ID'] . '?pipeline=' . $pipeline_id;
}
return $path;
@@ -438,6 +405,8 @@ public function get_allowed_ingest_mime_types() {
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'doc' => 'application/msword',
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ 'csv' => 'text/csv',
+ 'txt' => 'text/plain',
)
);
}
@@ -517,4 +486,117 @@ public function filter_attachment_post_type_weights( $weights, $post_type ) {
return $weights;
}
+
+ /**
+ * Enable integration if we are in the media library admin ajax search
+ *
+ * @param bool $integrate Whether it should be integrated or not
+ * @return bool
+ */
+ public function maybe_enable_ajax_wp_query_integration( $integrate ) {
+ return ( $this->is_admin_ajax_search() && $this->is_media_library_ajax_enabled() ) ? true : $integrate;
+ }
+
+ /**
+ * If post_status is not set, we assume publish/inherit is wanted.
+ *
+ * @param WP_Query $query WP_Query to modify to search.
+ * @return void
+ */
+ protected function maybe_set_post_status( $query ) {
+ $post_status = $query->get( 'post_status', [] );
+
+ if ( empty( $post_status ) ) {
+ $post_status = array_values(
+ get_post_stati(
+ [
+ 'public' => true,
+ 'exclude_from_search' => false,
+ ]
+ )
+ );
+
+ // Add inherit for documents
+ $post_status[] = 'inherit';
+ } else {
+ if ( is_string( $post_status ) ) {
+ $post_status = explode( ' ', $post_status );
+ }
+
+ $post_status[] = 'inherit';
+ }
+
+ $query->set( 'post_status', array_unique( $post_status ) );
+ }
+
+ /**
+ * Add allowed mime types. If mime types are already set, append.
+ *
+ * @param WP_Query $query WP_Query to modify to search.
+ * @return void
+ */
+ protected function maybe_set_mime_type( $query ) {
+ /**
+ * Mime types
+ *
+ * By default, we do not restrict results by mime types in the Media Library AJAX search,
+ * otherwise images, and SVGs, for example, will not be returned.
+ */
+ $should_set_mime_types = ! $this->is_admin_ajax_search() || ! $this->is_media_library_ajax_enabled();
+
+ /**
+ * Filter whether mime type restriction should be applied to the current WP Query
+ *
+ * @since 5.1.0
+ * @hook ep_documents_wp_query_set_mime_types
+ * @param {bool} $should_set Whether to restrict this query with mime types or not
+ * @param {WP_Query} $query WP Query object
+ * @return {bool} New value
+ */
+ $should_set_mime_types = apply_filters( 'ep_documents_wp_query_set_mime_types', $should_set_mime_types, $query );
+
+ if ( ! $should_set_mime_types ) {
+ return;
+ }
+
+ // Set mime types
+ $mime_types = $query->get( 'post_mime_type', [] );
+
+ if ( ! empty( $mime_types ) && is_string( $mime_types ) ) {
+ $mime_types = explode( ' ', $mime_types );
+ }
+
+ $mime_types = array_merge( $mime_types, $this->get_allowed_ingest_mime_types() );
+ $mime_types[] = ''; // This let's us query non-attachments as well as attachments.
+
+ $query->set( 'post_mime_type', array_unique( array_values( $mime_types ) ) );
+ }
+
+ /**
+ * Whether the feature should work on the Media Library admin ajax request
+ *
+ * @return boolean
+ */
+ protected function is_media_library_ajax_enabled() {
+ $protected_content = \ElasticPress\Features::factory()->get_registered_feature( 'protected_content' );
+
+ /**
+ * Filter whether the feature should work on the Media Library admin ajax request
+ *
+ * @since 5.1.0
+ * @hook ep_documents_media_library_ajax_enabled
+ * @param {bool} $enabled Whether to integrate or not
+ * @return {bool} New value
+ */
+ return apply_filters( 'ep_documents_media_library_ajax_enabled', $protected_content->is_active() );
+ }
+
+ /**
+ * Whether we are in the admin ajax search request for the media library
+ *
+ * @return boolean
+ */
+ protected function is_admin_ajax_search() {
+ return wp_doing_ajax() && isset( $_REQUEST['action'] ) && 'query-attachments' === $_REQUEST['action']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ }
}
diff --git a/includes/classes/Feature/Facets/Facets.php b/includes/classes/Feature/Facets/Facets.php
index 7e7ee46e77..5860cd4743 100644
--- a/includes/classes/Feature/Facets/Facets.php
+++ b/includes/classes/Feature/Facets/Facets.php
@@ -111,8 +111,17 @@ public function __construct() {
public function setup() {
global $pagenow;
- // This feature should not run while in the editor.
- if ( in_array( $pagenow, [ 'post-new.php', 'post.php' ], true ) ) {
+ $in_editor = in_array( $pagenow, [ 'post-new.php', 'post.php' ], true );
+
+ /**
+ * Filter if facet should be enabled in the editor. Default: false
+ *
+ * @hook ep_facet_enabled_in_editor
+ * @since 5.1.0
+ * @param {bool} $enabled
+ * @return {bool} If enabled or not
+ */
+ if ( $in_editor && ! apply_filters( 'ep_facet_enabled_in_editor', false ) ) {
return;
}
@@ -131,6 +140,23 @@ public function setup() {
add_action( 'rest_api_init', [ $this, 'setup_endpoints' ] );
}
+ /**
+ * Unsetup Facets related hooks
+ *
+ * @since 5.1.0
+ */
+ public function tear_down() {
+ remove_filter( 'widget_types_to_hide_from_legacy_widget_block', [ $this, 'hide_legacy_widget' ] );
+ remove_action( 'ep_valid_response', [ $this, 'get_aggs' ] );
+ remove_action( 'wp_enqueue_scripts', [ $this, 'front_scripts' ] );
+ remove_action( 'enqueue_block_editor_assets', [ $this, 'front_scripts' ] );
+ remove_action( 'ep_feature_box_settings_facets', [ $this, 'settings' ] );
+ remove_filter( 'ep_post_formatted_args', [ $this, 'set_agg_filters' ] );
+ remove_action( 'pre_get_posts', [ $this, 'facet_query' ] );
+ remove_filter( 'ep_post_filters', [ $this, 'apply_facets_filters' ] );
+ remove_action( 'rest_api_init', [ $this, 'setup_endpoints' ] );
+ }
+
/**
* Dashboard facet settings
*
diff --git a/includes/classes/Feature/Search/Search.php b/includes/classes/Feature/Search/Search.php
index 0e7ea785a1..1d9f7c7868 100644
--- a/includes/classes/Feature/Search/Search.php
+++ b/includes/classes/Feature/Search/Search.php
@@ -285,14 +285,18 @@ public function allow_excerpt_html() {
* @return string $text the new excerpt
*/
public function ep_highlight_excerpt( $text, $post ) {
-
$settings = $this->get_settings();
// reproduces wp_trim_excerpt filter, preserving the excerpt_more and excerpt_length filters
if ( '' === $text ) {
$text = get_the_content( '', false, $post );
+
+ $text = strip_shortcodes( $text );
+ $text = excerpt_remove_blocks( $text );
+
$text = apply_filters( 'the_content', $text );
$text = str_replace( '\]\]\>', ']]>', $text );
+
$text = strip_tags( $text, '<' . esc_html( $settings['highlight_tag'] ) . '>' );
// use the defined length, if already applied...
@@ -300,9 +304,12 @@ public function ep_highlight_excerpt( $text, $post ) {
// use defined excerpt_more filter if it is used
$excerpt_more = apply_filters( 'excerpt_more', $text );
-
$excerpt_more = $excerpt_more !== $text ? $excerpt_more : '[…]';
+ /**
+ * WordPress would handle this using `wp_trim_words` but that removes all tags,
+ * including the one used to highlight the search term.
+ */
$words = explode( ' ', $text, $excerpt_length + 1 );
if ( count( $words ) > $excerpt_length ) {
array_pop( $words );
diff --git a/includes/classes/Feature/Search/Synonyms.php b/includes/classes/Feature/Search/Synonyms.php
index a45955ea2b..0f089704e0 100644
--- a/includes/classes/Feature/Search/Synonyms.php
+++ b/includes/classes/Feature/Search/Synonyms.php
@@ -11,6 +11,7 @@
use ElasticPress\FeatureRequirementsStatus;
use ElasticPress\Features;
use ElasticPress\Indexables;
+use ElasticPress\REST;
use ElasticPress\Utils;
if ( ! defined( 'ABSPATH' ) ) {
@@ -108,16 +109,12 @@ public function setup() {
add_action( 'admin_menu', [ $this, 'admin_menu' ], 50 );
add_action( 'admin_enqueue_scripts', [ $this, 'scripts' ] );
- // Handle the update synonyms action.
- $action = $this->get_action();
- add_action( "admin_post_$action", [ $this, 'handle_update_synonyms' ] );
-
- // Handle the admin notices.
- add_action( 'admin_notices', [ $this, 'admin_notices' ] );
-
// Add the synonyms to the elasticsearch query.
add_filter( 'ep_config_mapping', [ $this, 'add_search_synonyms' ], 20, 2 );
+ // Register REST routes.
+ add_action( 'rest_api_init', [ $this, 'setup_endpoint' ] );
+
return true;
}
@@ -143,6 +140,14 @@ public function scripts() {
wp_enqueue_style( 'wp-edit-post' );
+ wp_enqueue_style(
+ 'ep_synonyms_scripts',
+ EP_URL . 'dist/css/synonyms-script.css',
+ [ 'wp-components', 'wp-edit-post' ],
+ Utils\get_asset_info( 'synonyms-styles', 'version' ),
+ 'all'
+ );
+
wp_enqueue_style(
'ep_synonyms_styles',
EP_URL . 'dist/css/synonyms-styles.css',
@@ -151,13 +156,18 @@ public function scripts() {
'all'
);
+ $api_url = rest_url( 'elasticpress/v1/synonyms' );
+ $sync_url = Utils\get_sync_url();
+
wp_localize_script(
'ep_synonyms_scripts',
'epSynonyms',
- array(
- 'i18n' => $this->get_localized_strings(),
- 'data' => $this->get_localized_data(),
- )
+ [
+ 'apiUrl' => esc_url_raw( $api_url ),
+ 'defaultIsSolr' => $this->synonyms_editor_mode() === 'advanced',
+ 'defaultSolr' => $this->get_synonyms_raw(),
+ 'syncUrl' => esc_url_raw( $sync_url ),
+ ]
);
}
@@ -171,7 +181,7 @@ public function admin_menu() {
'elasticpress',
esc_html__( 'ElasticPress Synonyms', 'elasticpress' ),
esc_html__( 'Synonyms', 'elasticpress' ),
- Utils\get_capability(),
+ Utils\get_capability( 'synonyms' ),
'elasticpress-synonyms',
[ $this, 'admin_page' ]
);
@@ -187,10 +197,7 @@ public function admin_page() {
?>
is_synonym_page() ) {
return;
}
@@ -248,7 +258,7 @@ public function register_post_type() {
'show_ui' => false,
'show_in_menu' => false,
'query_var' => true,
- 'capabilities' => Utils\get_post_map_capabilities(),
+ 'capabilities' => Utils\get_post_map_capabilities( 'synonyms' ),
'has_archive' => false,
'hierarchical' => false,
'menu_position' => 100,
@@ -388,10 +398,12 @@ public function add_search_synonyms( $mapping, $index ) {
$mapping['settings']['analysis']['filter'][ $filter_name ] = $this->get_synonym_filter();
// Tell the analyzer to use our newly created filter.
- $mapping['settings']['analysis']['analyzer']['default_search']['filter'] = array_values(
- array_merge(
- [ $filter_name ],
- $mapping['settings']['analysis']['analyzer']['default_search']['filter']
+ $mapping['settings']['analysis']['analyzer']['default_search']['filter'] = $this->maybe_change_filter_position(
+ array_values(
+ array_merge(
+ [ $filter_name ],
+ $mapping['settings']['analysis']['analyzer']['default_search']['filter'],
+ )
)
);
@@ -402,8 +414,11 @@ public function add_search_synonyms( $mapping, $index ) {
* Handles updating the synonym list.
*
* @return void
+ * @deprecated 5.1.0
*/
public function handle_update_synonyms() {
+ _deprecated_function( 'ElasticPress\Feature\Search\Synonyms::handle_update_synonyms', '5.1.0' );
+
$nonce = filter_input( INPUT_POST, $this->get_nonce_field(), FILTER_SANITIZE_SPECIAL_CHARS );
$referer = filter_input( INPUT_POST, '_wp_http_referer', FILTER_SANITIZE_URL );
$post_id = false;
@@ -463,7 +478,12 @@ public function update_synonyms() {
function( $success, $index ) {
$filter = $this->get_synonym_filter();
$mapping = Elasticsearch::factory()->get_mapping( $index );
- $filters = $mapping[ $index ]['settings']['index']['analysis']['analyzer']['default_search']['filter'];
+
+ if ( empty( $mapping ) || empty( $mapping[ $index ] ) ) {
+ return false;
+ }
+
+ $filters = (array) $mapping[ $index ]['settings']['index']['analysis']['analyzer']['default_search']['filter'];
/*
* Due to limitations in Elasticsearch, we can't remove the filter and analyzer
@@ -478,11 +498,13 @@ function( $success, $index ) {
$setting['index']['analysis']['filter']['ep_synonyms_filter'] = $filter;
// Add the analyzer.
- $setting['index']['analysis']['analyzer']['default_search']['filter'] = array_values(
- array_unique(
- array_merge(
- [ $this->get_synonym_filter_name() ],
- $filters
+ $setting['index']['analysis']['analyzer']['default_search']['filter'] = $this->maybe_change_filter_position(
+ array_values(
+ array_unique(
+ array_merge(
+ [ $this->get_synonym_filter_name() ],
+ $filters
+ )
)
)
);
@@ -561,8 +583,11 @@ public function get_synonym_filter() {
*
* @access protected
* @return string The admin post form action url.
+ * @deprecated 5.1.0
*/
public function get_form_action() {
+ _deprecated_function( 'ElasticPress\Feature\Search\Synonyms::get_form_action', '5.1.0' );
+
return esc_url_raw( admin_url( 'admin-post.php' ) );
}
@@ -570,8 +595,11 @@ public function get_form_action() {
* Render admin page form hidden fields.
*
* @return void
+ * @deprecated 5.1.0
*/
public function form_hidden_fields() {
+ _deprecated_function( 'ElasticPress\Feature\Search\Synonyms::get_form_action', '5.1.0', );
+
wp_nonce_field( $this->get_nonce_action(), $this->get_nonce_field() );
?>
@@ -582,8 +610,11 @@ public function form_hidden_fields() {
* Get nonce action for admin page form.
*
* @return string
+ * @deprecated 5.1.0
*/
public function get_nonce_action() {
+ _deprecated_function( 'ElasticPress\Feature\Search\Synonyms::get_form_action', '5.1.0', );
+
return $this->get_action();
}
@@ -591,8 +622,11 @@ public function get_nonce_action() {
* Get nonce field for admin page form.
*
* @return string
+ * @deprecated 5.1.0
*/
public function get_nonce_field() {
+ _deprecated_function( 'ElasticPress\Feature\Search\Synonyms::get_nonce_field', '5.1.0', );
+
return 'ep_synonyms_nonce';
}
@@ -600,8 +634,11 @@ public function get_nonce_field() {
* Get synonym field name for admin page form.
*
* @return string
+ * @deprecated 5.1.0
*/
public function get_synonym_field() {
+ _deprecated_function( 'ElasticPress\Feature\Search\Synonyms::get_synonym_field', '5.1.0', );
+
return 'ep_synonyms';
}
@@ -609,8 +646,11 @@ public function get_synonym_field() {
* Get the action slug for admin page form.
*
* @return string
+ * @deprecated 5.1.0
*/
public function get_action() {
+ _deprecated_function( 'ElasticPress\Feature\Search\Synonyms::get_action', '5.1.0', );
+
return 'ep_synonyms_update';
}
@@ -636,11 +676,16 @@ public function is_synonym_page() {
*/
public function example_synonym_list( $as_array = false ) {
$lines = [
- __( '# Defined sets (equivalent synonyms).', 'elasticpress' ),
- 'sneakers, tennis shoes, trainers, runners',
+ __( '# Defined synonyms.', 'elasticpress' ),
+ 'runner, running shoe, sneaker, tennis shoe, trainer',
'',
- __( '# Defined alternatives (explicit mappings).', 'elasticpress' ),
- 'shoes => sneaker, sandal, boots, high heels',
+ __( '# Defined hyponyms.', 'elasticpress' ),
+ 'blue => blue, aqua, azure, cerulean, cyan, ultramarine',
+ '',
+ __( '# Defined replacements.', 'elasticpress' ),
+ 'supposably => supposedly',
+ 'flustrated => flustered, frustrated',
+ 'intensive purposes => intents and purposes',
];
return $as_array ? $lines : implode( PHP_EOL, $lines );
@@ -650,8 +695,11 @@ public function example_synonym_list( $as_array = false ) {
* Gets localized strings for use on the front end.
*
* @return array
+ * @deprecated 5.1.0
*/
public function get_localized_strings() {
+ _deprecated_function( 'ElasticPress\Feature\Search\Synonyms::get_localized_strings', '5.1.0' );
+
return array(
'pageHeading' => __( 'Manage Synonyms', 'elasticpress' ),
'pageDescription' => __( 'Synonyms enable more flexible search results that show relevant results even without an exact match. Synonyms can be defined as a sets where all words are synonyms for each other, or as alternatives where searches for the primary word will also match the rest, but no vice versa.', 'elasticpress' ),
@@ -688,8 +736,11 @@ public function get_localized_strings() {
* Get data to export to the frontend with localization strings.
*
* @return array
+ * @deprecated 5.1.0
*/
public function get_localized_data() {
+ _deprecated_function( 'ElasticPress\Feature\Search\Synonyms::get_localized_strings', '5.1.0' );
+
$data = array(
'sets' => array(),
'alternatives' => array(),
@@ -772,8 +823,11 @@ public function synonyms_editor_mode() {
* @param string $token The synonym token to prepare.
* @param boolean $primary Whether this string is the primary term of an alternative.
* @return array
+ * @deprecated 5.1.0
*/
public static function prepare_localized_token( $token, $primary = false ) {
+ _deprecated_function( 'ElasticPress\Feature\Search\Synonyms::prepare_localized_token', '5.1.0' );
+
return array(
'label' => trim( sanitize_text_field( $token ) ),
'value' => trim( sanitize_text_field( $token ) ),
@@ -802,7 +856,7 @@ private function insert_default_synonym_post() {
* @param string $content The content post.
* @return int|WP_Error
*/
- private function update_synonym_post( $content ) {
+ public function update_synonym_post( $content ) {
$synonym_post_id = $this->get_synonym_post_id();
if ( ! $synonym_post_id ) {
@@ -818,4 +872,32 @@ private function update_synonym_post( $content ) {
true
);
}
+
+ /**
+ * Setup REST endpoints
+ *
+ * @since 5.1.0
+ */
+ public function setup_endpoint() {
+ $controller = new REST\Synonyms();
+ $controller->register_routes();
+ }
+
+ /**
+ * Change the position of the lowercase filter to the beginning of the array.
+ *
+ * @since 5.1.0
+ * @param array $filters Array of filters.
+ * @return array
+ */
+ protected function maybe_change_filter_position( array $filters ) : array {
+ $lowercase_filter = array_search( 'lowercase', $filters, true );
+
+ if ( false !== $lowercase_filter ) {
+ unset( $filters[ $lowercase_filter ] );
+ array_unshift( $filters, 'lowercase' );
+ }
+
+ return $filters;
+ }
}
diff --git a/includes/classes/Feature/SearchOrdering/SearchOrdering.php b/includes/classes/Feature/SearchOrdering/SearchOrdering.php
index 833991dc1a..be4931d9e6 100644
--- a/includes/classes/Feature/SearchOrdering/SearchOrdering.php
+++ b/includes/classes/Feature/SearchOrdering/SearchOrdering.php
@@ -215,7 +215,7 @@ public function admin_menu() {
'elasticpress',
esc_html__( 'Custom Results', 'elasticpress' ),
esc_html__( 'Custom Results', 'elasticpress' ),
- Utils\get_capability(),
+ Utils\get_capability( 'search-ordering' ),
'edit.php?post_type=' . self::POST_TYPE_NAME
);
}
@@ -294,7 +294,7 @@ public function register_post_type() {
'show_in_menu' => false,
'query_var' => true,
'rewrite' => array( 'slug' => 'ep-pointer' ),
- 'capabilities' => Utils\get_post_map_capabilities(),
+ 'capabilities' => Utils\get_post_map_capabilities( 'search-ordering' ),
'has_archive' => false,
'hierarchical' => false,
'menu_position' => 100,
diff --git a/includes/classes/Feature/WooCommerce/Orders.php b/includes/classes/Feature/WooCommerce/Orders.php
index dd3a7d3c93..849820c4bd 100644
--- a/includes/classes/Feature/WooCommerce/Orders.php
+++ b/includes/classes/Feature/WooCommerce/Orders.php
@@ -45,6 +45,7 @@ public function setup() {
add_action( 'parse_query', [ $this, 'maybe_hook_woocommerce_search_fields' ], 1 );
add_action( 'parse_query', [ $this, 'search_order' ], 11 );
add_action( 'pre_get_posts', [ $this, 'translate_args' ], 11, 1 );
+ add_filter( 'ep_admin_notices', [ $this, 'hpos_compatibility_notice' ] );
}
/**
@@ -335,6 +336,48 @@ public function get_supported_post_types() : array {
return $supported_post_types;
}
+ /**
+ * Display a notice if WooCommerce Orders are not compatible with ElasticPress
+ *
+ * If the user has WooCommerce, Protected Content, and HPOS enabled, orders will not go through ElasticPress.
+ *
+ * @param array $notices Current EP notices
+ * @return array
+ */
+ public function hpos_compatibility_notice( array $notices ) : array {
+ $current_screen = \get_current_screen();
+ if ( empty( $current_screen->id ) || 'woocommerce_page_wc-orders' !== $current_screen->id ) {
+ return $notices;
+ }
+
+ if ( \ElasticPress\Utils\get_option( 'ep_hide_wc_orders_incompatible_notice' ) ) {
+ return $notices;
+ }
+
+ $protected_content = \ElasticPress\Features::factory()->get_registered_feature( 'protected_content' );
+ if ( ! $protected_content->is_active() ) {
+ return $notices;
+ }
+
+ if (
+ ! class_exists( '\Automattic\WooCommerce\Utilities\OrderUtil' )
+ || ! method_exists( '\Automattic\WooCommerce\Utilities\OrderUtil', 'custom_orders_table_usage_is_enabled' ) ) {
+ return $notices;
+ }
+
+ if ( ! \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled() ) {
+ return $notices;
+ }
+
+ $notices['wc_orders_incompatible'] = [
+ 'html' => esc_html__( "Although the WooCommerce and Protected Content features are enabled, ElasticPress will not integrate with the WooCommerce Orders list if WooCommerce's High-performance order storage is enabled.", 'elasticpress' ),
+ 'type' => 'warning',
+ 'dismiss' => true,
+ ];
+
+ return $notices;
+ }
+
/**
* If the query has a search term, add the order fields that need to be searched.
*
@@ -477,7 +520,7 @@ public function __call( $method_name, $arguments ) {
"\ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' )->orders_autosuggest->{$method_name}()" // phpcs:ignore
);
- if ( $this->woocommerce->is_orders_autosuggest_enabled() && method_exists( $this->woocommerce->orders_autosuggest, $method_name ) ) {
+ if ( $this->woocommerce->orders_autosuggest->is_enabled() && method_exists( $this->woocommerce->orders_autosuggest, $method_name ) ) {
call_user_func_array( [ $this->woocommerce->orders_autosuggest, $method_name ], $arguments );
}
}
diff --git a/includes/classes/Feature/WooCommerce/OrdersAutosuggest.php b/includes/classes/Feature/WooCommerce/OrdersAutosuggest.php
index 25ae2fa50a..ad4a76cca8 100644
--- a/includes/classes/Feature/WooCommerce/OrdersAutosuggest.php
+++ b/includes/classes/Feature/WooCommerce/OrdersAutosuggest.php
@@ -9,6 +9,7 @@
namespace ElasticPress\Feature\WooCommerce;
use ElasticPress\Elasticsearch;
+use ElasticPress\Features;
use ElasticPress\Indexables;
use ElasticPress\REST;
use ElasticPress\Utils;
@@ -36,12 +37,23 @@ class OrdersAutosuggest {
protected $search_template;
/**
- * Initialize feature.
+ * WooCommerce feature object instance
*
- * @return void
+ * @since 5.1.0
+ * @var WooCommerce
*/
- public function __construct() {
- $this->index = Indexables::factory()->get( 'post' )->get_index_name();
+ protected $woocommerce;
+
+ /**
+ * Class constructor
+ *
+ * @param WooCommerce|null $woocommerce WooCommerce feature object instance
+ */
+ public function __construct( WooCommerce $woocommerce = null ) {
+ $this->index = Indexables::factory()->get( 'post' )->get_index_name();
+ $this->woocommerce = $woocommerce ?
+ $woocommerce :
+ Features::factory()->get_registered_feature( 'woocommerce' );
}
/**
@@ -50,6 +62,13 @@ public function __construct() {
* @return void
*/
public function setup() {
+ add_filter( 'ep_woocommerce_settings_schema', [ $this, 'add_settings_schema' ] );
+
+ // Orders Autosuggest feature.
+ if ( ! $this->is_enabled() ) {
+ return;
+ }
+
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] );
add_filter( 'ep_after_update_feature', [ $this, 'after_update_feature' ], 10, 3 );
add_filter( 'ep_after_sync_index', [ $this, 'epio_save_search_template' ] );
@@ -548,4 +567,121 @@ public function maybe_set_posts_where( $where, $query ) {
return $where;
}
+
+ /**
+ * Whether orders autosuggest is available or not
+ *
+ * @since 5.1.0
+ * @return boolean
+ */
+ public function is_available() : bool {
+ /**
+ * Whether the autosuggest feature is available for non
+ * ElasticPress.io customers.
+ *
+ * @since 4.5.0
+ * @hook ep_woocommerce_orders_autosuggest_available
+ * @param {boolean} $available Whether the feature is available.
+ */
+ return apply_filters( 'ep_woocommerce_orders_autosuggest_available', Utils\is_epio() && $this->is_hpos_compatible() );
+ }
+
+ /**
+ * Whether orders autosuggest is enabled or not
+ *
+ * @since 5.1.0
+ * @return boolean
+ */
+ public function is_enabled() : bool {
+ return $this->is_available() && '1' === $this->woocommerce->get_setting( 'orders' );
+ }
+
+ /**
+ * Whether the current setup is compatible with WooCommerce's HPOS or not
+ *
+ * @since 5.1.0
+ * @return boolean
+ */
+ public function is_hpos_compatible() {
+ if (
+ ! class_exists( '\Automattic\WooCommerce\Utilities\OrderUtil' )
+ || ! method_exists( '\Automattic\WooCommerce\Utilities\OrderUtil', 'custom_orders_table_usage_is_enabled' ) ) {
+ return true;
+ }
+
+ if ( ! \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled() ) {
+ return true;
+ }
+
+ if ( wc_get_container()->get( \Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer::class )->data_sync_is_enabled() ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Add the orders autosuggest field to the WooCommerce feature schema
+ *
+ * @since 5.1.0
+ * @param array $settings_schema Current settings schema
+ * @return array
+ */
+ public function add_settings_schema( array $settings_schema ) : array {
+ $available = $this->is_available();
+
+ $settings_schema[] = [
+ 'default' => '0',
+ 'disabled' => ! $available,
+ 'help' => $this->get_setting_help_message(),
+ 'key' => 'orders',
+ 'label' => __( 'Show suggestions when searching for Orders', 'elasticpress' ),
+ 'requires_sync' => true,
+ 'type' => 'checkbox',
+ ];
+
+ return $settings_schema;
+ }
+
+ /**
+ * Return the help message for the setting schema field
+ *
+ * @since 5.1.0
+ * @return string
+ */
+ protected function get_setting_help_message() : string {
+ $available = $this->is_available();
+
+ $epio_autosuggest_kb_link = 'https://elasticpress.zendesk.com/hc/en-us/articles/13374461690381-Configuring-ElasticPress-io-Order-Autosuggest';
+
+ if ( $available ) {
+ /* translators: 1: tag (ElasticPress.io); 2. ; 3: tag (KB article); 4. ; */
+ $message = __( 'You are directly connected to %1$sElasticPress.io%2$s! Enable autosuggest for Orders to enhance Dashboard results and quickly find WooCommerce Orders. %3$sLearn More%4$s.', 'elasticpress' );
+
+ return sprintf(
+ wp_kses( $message, 'ep-html' ),
+ '',
+ ' ',
+ '',
+ ' '
+ );
+ }
+
+ if ( ! $this->is_hpos_compatible() ) {
+ return esc_html__( 'Currently, autosuggest for orders is only available if WooCommerce order data storage is set in legacy or compatibility mode.', 'elasticpress' );
+ }
+
+ /* translators: 1: tag (ElasticPress.io); 2. ; 3: tag (KB article); 4. ; */
+ $message = __( 'Due to the sensitive nature of orders, this autosuggest feature is available only to %1$sElasticPress.io%2$s customers. %3$sLearn More%4$s.', 'elasticpress' );
+
+ $message = sprintf(
+ wp_kses( $message, 'ep-html' ),
+ '',
+ ' ',
+ '',
+ ' '
+ );
+
+ return $message;
+ }
}
diff --git a/includes/classes/Feature/WooCommerce/WooCommerce.php b/includes/classes/Feature/WooCommerce/WooCommerce.php
index 839a46dcb4..85f8a2a7b3 100644
--- a/includes/classes/Feature/WooCommerce/WooCommerce.php
+++ b/includes/classes/Feature/WooCommerce/WooCommerce.php
@@ -72,7 +72,7 @@ public function __construct() {
$this->orders = new Orders( $this );
$this->products = new Products( $this );
- $this->orders_autosuggest = new OrdersAutosuggest();
+ $this->orders_autosuggest = new OrdersAutosuggest( $this );
parent::__construct();
}
@@ -93,11 +93,7 @@ public function setup() {
$this->products->setup();
$this->orders->setup();
-
- // Orders Autosuggest feature.
- if ( $this->is_orders_autosuggest_enabled() ) {
- $this->orders_autosuggest->setup();
- }
+ $this->orders_autosuggest->setup();
}
/**
@@ -128,11 +124,7 @@ public function tear_down() {
$this->products->tear_down();
$this->orders->tear_down();
-
- // Orders Autosuggest feature.
- if ( $this->is_orders_autosuggest_enabled() ) {
- $this->orders_autosuggest->tear_down();
- }
+ $this->orders_autosuggest->tear_down();
}
/**
@@ -180,44 +172,6 @@ public function output_feature_box_long() {
is_orders_autosuggest_available();
- $enabled = $this->is_orders_autosuggest_enabled();
- ?>
-
- settings_schema = apply_filters( 'ep_woocommerce_settings_schema', $this->settings_schema );
}
/**
- * Whether orders autosuggest is enabled or not
+ * DEPRECATED. Dashboard WooCommerce settings
*
* @since 4.5.0
- * @return boolean
+ * @deprecated 5.1.0
*/
- public function is_orders_autosuggest_enabled() : bool {
- return $this->is_orders_autosuggest_available() && '1' === $this->get_setting( 'orders' );
+ public function output_feature_box_settings() {
+ _doing_it_wrong(
+ __METHOD__,
+ esc_html__( 'Settings are now generated via the set_settings_schema() method.', 'elasticpress' ),
+ '5.0.0'
+ );
}
/**
- * Set the `settings_schema` attribute
+ * DEPRECATED. Whether orders autosuggest is available or not
*
- * @since 5.0.0
+ * @since 4.5.0
+ * @deprecated 5.1.0
+ * @return boolean
*/
- protected function set_settings_schema() {
- $available = $this->is_orders_autosuggest_available();
-
- $epio_autosuggest_kb_link = 'https://elasticpress.zendesk.com/hc/en-us/articles/13374461690381-Configuring-ElasticPress-io-Order-Autosuggest';
-
- $message = ( $available ) ?
- /* translators: 1: tag (ElasticPress.io); 2. ; 3: tag (KB article); 4. ; */
- __( 'You are directly connected to %1$sElasticPress.io%2$s! Enable autosuggest for Orders to enhance Dashboard results and quickly find WooCommerce Orders. %3$sLearn More%4$s.', 'elasticpress' ) :
- /* translators: 1: tag (ElasticPress.io); 2. ; 3: tag (KB article); 4. ; */
- __( 'Due to the sensitive nature of orders, this autosuggest feature is available only to %1$sElasticPress.io%2$s customers. %3$sLearn More%4$s.', 'elasticpress' );
-
- $message = sprintf(
- wp_kses( $message, 'ep-html' ),
- '',
- ' ',
- '',
- ' '
- );
+ public function is_orders_autosuggest_available() : bool {
+ _deprecated_function( __METHOD__, '5.1.0', "\ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' )->orders_autosuggest->is_available()" );
+ return $this->orders_autosuggest->is_available();
+ }
- $this->settings_schema = [
- [
- 'default' => '0',
- 'disabled' => ! $available,
- 'help' => $message,
- 'key' => 'orders',
- 'label' => __( 'Show suggestions when searching for Orders', 'elasticpress' ),
- 'requires_sync' => true,
- 'type' => 'checkbox',
- ],
- ];
+ /**
+ * DEPRECATED. Whether orders autosuggest is enabled or not
+ *
+ * @since 4.5.0
+ * @deprecated 5.1.0
+ * @return boolean
+ */
+ public function is_orders_autosuggest_enabled() : bool {
+ _deprecated_function( __METHOD__, '5.1.0', "\ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' )->orders_autosuggest->is_enabled()" );
+ return $this->orders_autosuggest->is_enabled();
}
/**
@@ -368,6 +312,7 @@ protected function set_settings_schema() {
*
* @param \WP_Query $query WP Query
* @since 2.1
+ * @deprecated 4.7.0
*/
public function translate_args( $query ) {
_deprecated_function( __METHOD__, '4.7.0', "\ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' )->products->translate_args() OR \ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' )->orders->translate_args()" );
@@ -380,6 +325,7 @@ public function translate_args( $query ) {
*
* @param array $meta_key The meta key to get the mapping for.
* @since 2.1
+ * @deprecated 4.7.0
* @return string The mapped meta key.
*/
public function get_orderby_meta_mapping( $meta_key ) {
@@ -392,6 +338,7 @@ public function get_orderby_meta_mapping( $meta_key ) {
*
* @param array $search_fields Array of search fields.
* @since 3.0
+ * @deprecated 4.7.0
* @return array
*/
public function remove_author( $search_fields ) {
@@ -404,6 +351,7 @@ public function remove_author( $search_fields ) {
*
* @param array $meta Existing post meta.
* @param array $post Post arguments array.
+ * @deprecated 4.7.0
* @since 2.1
* @return array
*/
@@ -422,6 +370,7 @@ public function whitelist_meta_keys( $meta, $post ) {
*
* @param array $taxonomies Index taxonomies array.
* @param array $post Post properties array.
+ * @deprecated 4.7.0
* @since 2.1
* @return array
*/
@@ -434,6 +383,7 @@ public function whitelist_taxonomies( $taxonomies, $post ) {
* DEPRECATED. Returns the WooCommerce-oriented post types in admin that EP will search
*
* @since 4.4.0
+ * @deprecated 4.7.0
* @return mixed|void
*/
public function get_admin_searchable_post_types() {
@@ -447,6 +397,7 @@ public function get_admin_searchable_post_types() {
* @param bool $enabled Coupons enabled or not
* @param WP_Query $query WP Query
* @since 2.1
+ * @deprecated 4.7.0
* @return bool
*/
public function blacklist_coupons( $enabled, $query ) {
@@ -459,6 +410,7 @@ public function blacklist_coupons( $enabled, $query ) {
*
* @since 2.1
* @param bool $override Original order perms check value
+ * @deprecated 4.7.0
* @param int $post_id Post ID
* @return bool
*/
@@ -475,6 +427,7 @@ public function bypass_order_permissions_check( $override, $post_id ) {
* If we were to always return array() on this filter, we'd break admin searches when WooCommerce module is activated
* without the Protected Content Module
*
+ * @deprecated 4.7.0
* @param \WP_Query $query Current query
*/
public function maybe_hook_woocommerce_search_fields( $query ) {
@@ -490,6 +443,7 @@ public function maybe_hook_woocommerce_search_fields( $query ) {
* 3. If the search key is integer but not an order id ( might be phone number ), use ES to find it
*
* @param WP_Query $wp WP Query
+ * @deprecated 4.7.0
* @since 2.3
*/
public function search_order( $wp ) {
@@ -508,7 +462,7 @@ public function search_order( $wp ) {
*
* @param array $post_args Post arguments
* @param string|int $post_id Post id
- *
+ * @deprecated 4.7.0
* @return array
*/
public function add_order_items_search( $post_args, $post_id ) {
@@ -520,6 +474,7 @@ public function add_order_items_search( $post_args, $post_id ) {
* DEPRECATED. Add WooCommerce Product Attributes to EP Facets.
*
* @param array $taxonomies Taxonomies array
+ * @deprecated 4.7.0
* @return array
*/
public function add_product_attributes( $taxonomies = [] ) {
@@ -531,7 +486,7 @@ public function add_product_attributes( $taxonomies = [] ) {
* DEPRECATED. Add WooCommerce Fields to the Weighting Dashboard.
*
* @since 3.x
- *
+ * @deprecated 4.7.0
* @param array $fields Current weighting fields.
* @param string $post_type Current post type.
* @return array New fields.
@@ -545,7 +500,7 @@ public function add_product_attributes_to_weighting( $fields, $post_type ) {
* DEPRECATED. Add WooCommerce Fields to the default values of the Weighting Dashboard.
*
* @since 3.x
- *
+ * @deprecated 4.7.0
* @param array $defaults Default values for the post type.
* @param string $post_type Current post type.
* @return array
@@ -560,6 +515,7 @@ public function add_product_default_post_type_weights( $defaults, $post_type ) {
*
* @param array $post_types Array of post types (e.g. post, page).
* @since 2.6
+ * @deprecated 4.7.0
* @return array
*/
public function suggest_wc_add_post_type( $post_types ) {
@@ -574,6 +530,7 @@ public function suggest_wc_add_post_type( $post_types ) {
* @param array $query_args WP_Query args
* @param WP_Query $query WP_Query object
* @since 3.2
+ * @deprecated 4.7.0
* @return array
*/
public function price_filter( $args, $query_args, $query ) {
@@ -590,6 +547,7 @@ public function price_filter( $args, $query_args, $query ) {
* @see https://github.com/10up/ElasticPress/issues/2726
*
* @since 4.2.0
+ * @deprecated 4.7.0
* @param bool $skip Whether the password protected content should have their content, and meta removed
* @param array $post_args Post arguments
* @return bool
@@ -603,6 +561,7 @@ public function keep_order_fields( $skip, $post_args ) {
* DEPRECATED. Add a new `_variations_skus` meta field to the product to be indexed in Elasticsearch.
*
* @since 4.2.0
+ * @deprecated 4.7.0
* @param array $post_meta Post meta
* @param WP_Post $post Post object
* @return array
@@ -622,6 +581,7 @@ public function add_variations_skus_meta( $post_meta, $post ) {
* by ElasticPress.
*
* @since 4.2.0
+ * @deprecated 4.7.0
* @param array $query_vars Query vars.
* @return array
*/
@@ -634,6 +594,7 @@ public function admin_product_list_request_query( $query_vars ) {
* DEPRECATED. Apply the necessary changes to WP_Query in WooCommerce Admin Product List.
*
* @param WP_Query $query The WP Query being executed.
+ * @deprecated 4.7.0
*/
public function translate_args_admin_products_list( $query ) {
_deprecated_function( __METHOD__, '4.7.0', "\ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' )->products->price_filter()" );
@@ -644,6 +605,7 @@ public function translate_args_admin_products_list( $query ) {
* DEPRECATED. Depending on the number of products display an admin notice in the custom sort screen for WooCommerce Products
*
* @since 4.4.0
+ * @deprecated 4.7.0
* @param array $notices Current ElasticPress admin notices
* @return array
*/
@@ -656,6 +618,7 @@ public function maybe_display_notice_about_product_ordering( $notices ) {
* DEPRECATED. Conditionally resync products after applying a custom order.
*
* @since 4.4.0
+ * @deprecated 4.7.0
* @param int $sorting_id ID of post dragged and dropped
* @param array $menu_orders Post IDs and their new menu_order value
*/
@@ -668,6 +631,7 @@ public function action_sync_on_woocommerce_sort_single( $sorting_id, $menu_order
* DEPRECATED. Add weight by date settings related to WooCommerce
*
* @since 4.6.0
+ * @deprecated 4.7.0
* @param array $settings Current settings.
*/
public function add_weight_settings_search( $settings ) {
@@ -679,6 +643,7 @@ public function add_weight_settings_search( $settings ) {
* DEPRECATED. Conditionally disable decaying by date based on WooCommerce Decay settings.
*
* @since 4.6.0
+ * @deprecated 4.7.0
* @param bool $is_decaying_enabled Whether decay by date is enabled or not
* @param array $settings Settings
* @param array $args WP_Query args
diff --git a/includes/classes/IndexHelper.php b/includes/classes/IndexHelper.php
index 21a90f8188..a5d6516d20 100644
--- a/includes/classes/IndexHelper.php
+++ b/includes/classes/IndexHelper.php
@@ -1412,8 +1412,10 @@ protected function on_error_update_and_clean( $error, $context = 'sync' ) {
esc_html__( 'Mapping failed: %s', 'elasticpress' ),
Utils\get_elasticsearch_error_reason( $error['message'] )
);
- $message .= "\n";
- $message .= esc_html__( 'Mapping has failed, which will cause ElasticPress search results to be incorrect. Please click `Delete all Data and Start a Fresh Sync` to retry mapping.', 'elasticpress' );
+ if ( $this->should_suggest_retry( $message ) ) {
+ $message .= "\n";
+ $message .= esc_html__( 'Mapping has failed, which will cause ElasticPress search results to be incorrect. Please click `Delete all Data and Start a Fresh Sync` to retry mapping.', 'elasticpress' );
+ }
break;
default:
/* translators: Error message */
@@ -1523,6 +1525,16 @@ protected function maybe_apply_feature_settings() {
Features::factory()->apply_draft_feature_settings();
}
+ /**
+ * Whether to suggest retrying the sync or not.
+ *
+ * @param string $message The message returned by the hosting server
+ * @return boolean
+ */
+ protected function should_suggest_retry( $message ) {
+ return ! preg_match( '/you have reached the limit of indices your plan supports/', $message );
+ }
+
/**
* Return singleton instance of class.
*
diff --git a/includes/classes/Indexable.php b/includes/classes/Indexable.php
index 10692475b5..6e974b96ea 100644
--- a/includes/classes/Indexable.php
+++ b/includes/classes/Indexable.php
@@ -260,7 +260,7 @@ public function delete_index( $blog_id = null ) {
* @param int $object_id Object to index.
* @param boolean $blocking Blocking HTTP request or not.
* @since 3.0
- * @return boolean
+ * @return object|boolean
*/
public function index( $object_id, $blocking = false ) {
$document = $this->prepare_document( $object_id );
@@ -299,7 +299,7 @@ public function index( $object_id, $blocking = false ) {
*
* @hook ep_after_index_{indexable_slug}
* @param {array} $document Document to index
- * @param {array|boolean} $return ES response on success, false on failure
+ * @param {object|boolean} $return ES response on success, false on failure
* @since 3.0
*/
do_action( 'ep_after_index_' . $this->slug, $document, $return );
diff --git a/includes/classes/Indexable/Post/Post.php b/includes/classes/Indexable/Post/Post.php
index 91306985f7..a381cef789 100644
--- a/includes/classes/Indexable/Post/Post.php
+++ b/includes/classes/Indexable/Post/Post.php
@@ -2825,7 +2825,10 @@ public function get_predicted_indexable_meta_keys( bool $force_refresh = false )
$empty_post = new \WP_Post( (object) [] );
$meta_keys = $this->get_distinct_meta_field_keys_db( $force_refresh );
- $fake_meta_values = array_combine( $meta_keys, array_fill( 0, count( $meta_keys ), 'test-value' ) );
+ $fake_meta_values = array_combine(
+ $meta_keys,
+ array_fill( 0, count( $meta_keys ), $this->get_test_meta_value() )
+ );
$filtered_meta = apply_filters( 'ep_prepare_meta_data', $fake_meta_values, $empty_post );
$all_keys = array_filter(
@@ -2840,6 +2843,24 @@ function( $meta_key ) use ( $empty_post ) {
return $all_keys;
}
+ /**
+ * Return the value used to fill meta fields while predicting indexable content.
+ *
+ * @since 5.1.0
+ * @return string
+ */
+ public function get_test_meta_value() : string {
+ /**
+ * Filter the value used to fill meta fields while predicting indexable content.
+ *
+ * @hook ep_post_test_meta_value
+ * @since 5.1.0
+ * @param {string} $test_meta_value The test meta value. Default: test-value
+ * @return {string} New test meta value
+ */
+ return (string) apply_filters( 'ep_post_test_meta_value', 'test-value' );
+ }
+
/**
* Given a post type, *yields* their Post IDs.
*
diff --git a/includes/classes/Indexable/Term/QueryIntegration.php b/includes/classes/Indexable/Term/QueryIntegration.php
index 9977c1bb22..1788bbfdf6 100644
--- a/includes/classes/Indexable/Term/QueryIntegration.php
+++ b/includes/classes/Indexable/Term/QueryIntegration.php
@@ -53,6 +53,8 @@ public function __construct( $indexable_slug = 'term' ) {
// Filter term query
add_filter( 'terms_pre_query', [ $this, 'maybe_filter_query' ], 10, 2 );
+
+ add_filter( 'rest_post_tag_query', [ $this, 'maybe_set_search_fields' ], 10, 2 );
}
/**
@@ -241,6 +243,27 @@ public function maybe_filter_query( $results, WP_Term_Query $query ) {
return $new_terms;
}
+ /**
+ * Conditionally set search fields for term queries in REST API requests.
+ *
+ * If in an REST API request that came from WordPress edit screen, do not search for term description.
+ *
+ * @param array $prepared_args Array of arguments for get_terms().
+ * @param WP_REST_Request $request The REST API request.
+ * @return array
+ */
+ public function maybe_set_search_fields( $prepared_args, $request ) {
+ $referer = $request->get_header( 'referer' );
+
+ if ( empty( $referer ) || ! preg_match( '/post\.php\?post=([0-9]*)&action=edit/', $referer ) ) {
+ return $prepared_args;
+ }
+
+ $prepared_args['search_fields'] = [ 'name', 'slug' ];
+
+ return $prepared_args;
+ }
+
/**
* Format the ES hits/results as term objects.
*
diff --git a/includes/classes/Indexable/Term/Term.php b/includes/classes/Indexable/Term/Term.php
index df11f6b619..63eda6059e 100644
--- a/includes/classes/Indexable/Term/Term.php
+++ b/includes/classes/Indexable/Term/Term.php
@@ -62,456 +62,22 @@ public function setup() {
* @return array
*/
public function format_args( $query_vars ) {
- /**
- * Support `number` query var
- */
- if ( ! empty( $query_vars['number'] ) ) {
- $number = (int) $query_vars['number'];
- } else {
- /**
- * Set the maximum results window size.
- *
- * The request will return a HTTP 500 Internal Error if the size of the
- * request is larger than the [index.max_result_window] parameter in ES.
- * See the scroll api for a more efficient way to request large data sets.
- *
- * @return int The max results window size.
- *
- * @since 2.3.0
- */
- $number = apply_filters( 'ep_max_results_window', 10000 );
- }
+ $query_vars = $this->sanitize_query_vars( $query_vars );
$formatted_args = [
- 'from' => 0,
- 'size' => $number,
- ];
-
- /**
- * Support `offset` query var
- */
- if ( isset( $query_vars['offset'] ) ) {
- $formatted_args['from'] = (int) $query_vars['offset'];
- }
-
- /**
- * Support `order` and `orderby` query vars
- */
-
- // Set sort order, default is 'ASC'.
- if ( ! empty( $query_vars['order'] ) ) {
- $order = $this->parse_order( $query_vars['order'] );
- } else {
- $order = 'desc';
- }
-
- // Set orderby, default is 'name'.
- if ( empty( $query_vars['orderby'] ) ) {
- $query_vars['orderby'] = 'name';
- }
-
- // Set sort type.
- $formatted_args['sort'] = $this->parse_orderby( $query_vars['orderby'], $order, $query_vars );
-
- $filter = [
- 'bool' => [
- 'must' => [],
- ],
+ 'from' => $this->parse_from( $query_vars ),
+ 'size' => $this->parse_size( $query_vars ),
];
- $use_filters = false;
-
- /**
- * Support `taxonomy` query var
- */
- $taxonomy = [];
- if ( ! empty( $query_vars['taxonomy'] ) ) {
- $taxonomy = (array) $query_vars['taxonomy'];
- $terms_map_name = 'terms';
-
- if ( count( $taxonomy ) < 2 ) {
- $terms_map_name = 'term';
- $taxonomy = $taxonomy[0];
- }
-
- $filter['bool']['must'][] = [
- $terms_map_name => [
- 'taxonomy.raw' => $taxonomy,
- ],
- ];
-
- $use_filters = true;
- }
-
- /**
- * Support `object_ids` query var
- */
- if ( ! empty( $query_vars['object_ids'] ) ) {
- $filter['bool']['must'][]['bool']['must'][] = [
- 'terms' => [
- 'object_ids.value' => (array) $query_vars['object_ids'],
- ],
- ];
-
- $use_filters = true;
- }
-
- /**
- * Support `get` query var
- */
- if ( ! empty( $query_vars['get'] ) && 'all' === $query_vars['get'] ) {
- $query_vars['childless'] = false;
- $query_vars['child_of'] = 0;
- $query_vars['hide_empty'] = false;
- $query_vars['hierarchical'] = false;
- $query_vars['pad_counts'] = false;
- }
-
- /**
- * Support `include` query var
- */
- if ( ! empty( $query_vars['include'] ) ) {
- $filter['bool']['must'][]['bool']['must'] = [
- 'terms' => [
- 'term_id' => array_values( (array) $query_vars['include'] ),
- ],
- ];
-
- $use_filters = true;
- }
-
- /**
- * Support `exclude` query var
- */
- if ( empty( $query_vars['include'] ) && ! empty( $query_vars['exclude'] ) ) {
- $filter['bool']['must'][]['bool']['must_not'] = [
- 'terms' => [
- 'term_id' => array_values( (array) $query_vars['exclude'] ),
- ],
- ];
-
- $use_filters = true;
- }
-
- /**
- * Support `exclude_tree` query var
- */
- if ( empty( $query_vars['include'] ) && ! empty( $query_vars['exclude_tree'] ) ) {
- $filter['bool']['must'][]['bool']['must_not'] = [
- 'terms' => [
- 'term_id' => array_values( (array) $query_vars['exclude_tree'] ),
- ],
- ];
-
- $filter['bool']['must'][]['bool']['must_not'] = [
- 'terms' => [
- 'parent' => array_values( (array) $query_vars['exclude_tree'] ),
- ],
- ];
-
- $use_filters = true;
- }
-
- /**
- * Support `name` query var
- */
- if ( ! empty( $query_vars['name'] ) ) {
- $filter['bool']['must'][] = [
- 'terms' => [
- 'name.raw' => (array) $query_vars['name'],
- ],
- ];
-
- $use_filters = true;
- }
-
- /**
- * Support `slug` query var
- */
- if ( ! empty( $query_vars['slug'] ) ) {
- if ( ! is_array( $query_vars['slug'] ) ) {
- $query_vars['slug'] = array( $query_vars['slug'] );
- }
-
- $query_vars['slug'] = array_map( 'sanitize_title', $query_vars['slug'] );
-
- $filter['bool']['must'][] = [
- 'terms' => [
- 'slug.raw' => (array) $query_vars['slug'],
- ],
- ];
-
- $use_filters = true;
- }
-
- /**
- * Support `term_taxonomy_id` query var
- */
- if ( ! empty( $query_vars['term_taxonomy_id'] ) ) {
- $filter['bool']['must'][]['bool']['must'] = [
- 'terms' => [
- 'term_taxonomy_id' => array_values( (array) $query_vars['term_taxonomy_id'] ),
- ],
- ];
-
- $use_filters = true;
- }
-
- /**
- * Support `hierarchical` and `hide_empty` query var
- *
- * `hierarchical` needs to work in conjunction with `hide_empty`, as per WP docs:
- * > `hierarchical`: Whether to include terms that have non-empty descendants (even if $hide_empty is set to true).
- *
- * In summary:
- * - hide_empty AND hierarchical: count > 1 OR hierarchy.children > 1
- * - hide_empty AND NOT hierarchical: count > 1 (ignore hierarchy.children)
- * - NOT hide_empty (AND hierarchical): there is no need to limit the query
- *
- * @see https://developer.wordpress.org/reference/classes/WP_Term_Query/__construct/
- */
- $hide_empty = isset( $query_vars['hide_empty'] ) ? $query_vars['hide_empty'] : '';
- if ( $hide_empty ) {
- $hierarchical = isset( $query_vars['hierarchical'] ) ? $query_vars['hierarchical'] : '';
-
- if ( $hierarchical ) {
- $filter_clause = [ 'bool' => [ 'should' => [] ] ];
-
- $filter_clause['bool']['should'][] = [
- 'range' => [
- 'count' => [
- 'gte' => 1,
- ],
- ],
- ];
-
- $filter_clause['bool']['should'][] = [
- 'range' => [
- 'hierarchy.children.count' => [
- 'gte' => 1,
- ],
- ],
- ];
-
- $filter['bool']['must'][] = $filter_clause;
-
- $use_filters = true;
- } else {
- $filter['bool']['must'][] = [
- 'range' => [
- 'count' => [
- 'gte' => 1,
- ],
- ],
- ];
- }
-
- $use_filters = true;
- }
-
- /**
- * Support `search`, `name__like` and `description__like` query_vars
- */
- if ( ! empty( $query_vars['search'] ) || ! empty( $query_vars['name__like'] ) || ! empty( $query_vars['description__like'] ) ) {
-
- $search = ! empty( $query_vars['search'] ) ? $query_vars['search'] : '';
- $search_fields = [];
-
- if ( ! empty( $query_vars['name__like'] ) ) {
- $search = $query_vars['name__like'];
- $search_fields[] = 'name';
- }
-
- if ( ! empty( $query_vars['description__like'] ) ) {
- $search = $query_vars['description__like'];
- $search_fields[] = 'description';
- }
-
- /**
- * Allow for search field specification
- */
- if ( ! empty( $query_vars['search_fields'] ) ) {
- $search_fields = $query_vars['search_fields'];
- }
-
- if ( ! empty( $search_fields ) ) {
- $prepared_search_fields = [];
-
- if ( ! empty( $search_fields['meta'] ) ) {
- $metas = (array) $search_fields['meta'];
-
- foreach ( $metas as $meta ) {
- $prepared_search_fields[] = 'meta.' . $meta . '.value';
- }
-
- unset( $search_fields['meta'] );
- }
-
- $prepared_search_fields = array_merge( $search_fields, $prepared_search_fields );
- } else {
- $prepared_search_fields = [
- 'name',
- 'slug',
- 'taxonomy',
- 'description',
- ];
- }
-
- /**
- * Filter fields to search on Term query
- *
- * @hook ep_term_search_fields
- * @param {array} $search_fields Search fields
- * @param {array} $query_vars Query variables
- * @since 3.4
- * @return {array} New search fields
- */
- $prepared_search_fields = apply_filters( 'ep_term_search_fields', $prepared_search_fields, $query_vars );
-
- $search_algorithm = $this->get_search_algorithm( $search, $prepared_search_fields, $query_vars );
- $formatted_args['query'] = $search_algorithm->get_query( 'term', $search, $prepared_search_fields, $query_vars );
- } else {
- $formatted_args['query']['match_all'] = [
- 'boost' => 1,
- ];
- }
-
- /**
- * Support `child_of` query var.
- */
- if ( ! empty( $query_vars['child_of'] ) && ( is_string( $taxonomy ) || count( $taxonomy ) < 2 ) ) {
- $filter['bool']['must'][]['bool']['must'][] = [
- 'match_phrase' => [
- 'hierarchy.ancestors.terms' => (int) $query_vars['child_of'],
- ],
- ];
-
- $use_filters = true;
- }
-
- /**
- * Support `parent` query var.
- */
- if ( isset( $query_vars['parent'] ) && '' !== $query_vars['parent'] ) {
- $filter['bool']['must'][]['bool']['must'] = [
- 'term' => [
- 'parent' => (int) $query_vars['parent'],
- ],
- ];
-
- $use_filters = true;
- }
-
- /**
- * Support `childless` query var.
- */
- if ( ! empty( $query_vars['childless'] ) ) {
- $filter['bool']['must'][]['bool']['must'] = [
- 'term' => [
- 'hierarchy.children.terms' => 0,
- ],
- ];
-
- $use_filters = true;
- }
-
- $meta_queries = [];
-
- /**
- * Support `meta_key`, `meta_value`, and `meta_compare` query args
- */
- if ( ! empty( $query_vars['meta_key'] ) ) {
- $meta_query_array = [
- 'key' => $query_vars['meta_key'],
- ];
-
- if ( isset( $query_vars['meta_value'] ) && '' !== $query_vars['meta_value'] ) {
- $meta_query_array['value'] = $query_vars['meta_value'];
- }
-
- if ( isset( $query_vars['meta_compare'] ) ) {
- $meta_query_array['compare'] = $query_vars['meta_compare'];
- }
-
- $meta_queries[] = $meta_query_array;
- }
-
- /**
- * Support 'meta_query' query var.
- */
- if ( ! empty( $query_vars['meta_query'] ) ) {
- $meta_queries = array_merge( $meta_queries, $query_vars['meta_query'] );
- }
-
- if ( ! empty( $meta_queries ) ) {
- $built_meta_queries = $this->build_meta_query( $meta_queries );
-
- if ( $built_meta_queries ) {
- $filter['bool']['must'][] = $built_meta_queries;
- $use_filters = true;
- }
- }
-
- /**
- * Support `fields` query var.
- */
- if ( isset( $query_vars['fields'] ) ) {
- switch ( $query_vars['fields'] ) {
- case 'ids':
- $formatted_args['_source'] = [
- 'includes' => [
- 'term_id',
- ],
- ];
- break;
-
- case 'id=>name':
- $formatted_args['_source'] = [
- 'includes' => [
- 'term_id',
- 'name',
- ],
- ];
- break;
-
- case 'id=>parent':
- $formatted_args['_source'] = [
- 'includes' => [
- 'term_id',
- 'parent',
- ],
- ];
- break;
-
- case 'id=>slug':
- $formatted_args['_source'] = [
- 'includes' => [
- 'term_id',
- 'slug',
- ],
- ];
- break;
+ $formatted_args = $this->maybe_orderby( $formatted_args, $query_vars );
- case 'names':
- $formatted_args['_source'] = [
- 'includes' => [
- 'name',
- ],
- ];
- break;
- case 'tt_ids':
- $formatted_args['_source'] = [
- 'includes' => [
- 'term_taxonomy_id',
- ],
- ];
- break;
- }
+ $filters = $this->parse_filters( $query_vars );
+ if ( ! empty( $filters ) ) {
+ $formatted_args['post_filter'] = $filters;
}
- if ( $use_filters ) {
- $formatted_args['post_filter'] = $filter;
- }
+ $formatted_args = $this->maybe_set_search_fields( $formatted_args, $query_vars );
+ $formatted_args = $this->maybe_set_fields( $formatted_args, $query_vars );
/**
* Filter full Elasticsearch query for Terms indexable
@@ -944,4 +510,634 @@ protected function parse_orderby( $orderby, $order, $args ) {
return $sort;
}
+ /**
+ * Sanitize WP_Term_Query arguments to be used to create the ES query.
+ *
+ * @since 5.1.0
+ * @param array $query_vars WP_Term_Query arguments
+ * @return array
+ */
+ protected function sanitize_query_vars( $query_vars ) {
+ if ( ! empty( $query_vars['get'] ) && 'all' === $query_vars['get'] ) {
+ $query_vars['childless'] = false;
+ $query_vars['child_of'] = 0;
+ $query_vars['hide_empty'] = false;
+ $query_vars['hierarchical'] = false;
+ $query_vars['pad_counts'] = false;
+ }
+
+ $query_vars['taxonomy'] = ( ! empty( $query_vars['taxonomy'] ) ) ?
+ (array) $query_vars['taxonomy'] :
+ [];
+
+ return $query_vars;
+ }
+
+ /**
+ * Parse the `from` clause of the ES Query.
+ *
+ * @since 5.1.0
+ * @param array $query_vars WP_Term_Query arguments
+ * @return int
+ */
+ protected function parse_from( $query_vars ) {
+ return ( isset( $query_vars['offset'] ) ) ? (int) $query_vars['offset'] : 0;
+ }
+
+ /**
+ * Parse the `size` clause of the ES Query.
+ *
+ * @since 5.1.0
+ * @param array $query_vars WP_Term_Query arguments
+ * @return int
+ */
+ protected function parse_size( $query_vars ) {
+ if ( ! empty( $query_vars['number'] ) ) {
+ $number = (int) $query_vars['number'];
+ } else {
+ /**
+ * Set the maximum results window size.
+ *
+ * The request will return a HTTP 500 Internal Error if the size of the
+ * request is larger than the [index.max_result_window] parameter in ES.
+ * See the scroll api for a more efficient way to request large data sets.
+ *
+ * @return int The max results window size.
+ *
+ * @since 2.3.0
+ */
+ $number = apply_filters( 'ep_max_results_window', 10000 );
+ }
+
+ return $number;
+ }
+
+ /**
+ * Parse the order of results in the ES query.
+ *
+ * @since 5.1.0
+ * @param array $formatted_args Formatted Elasticsearch query
+ * @param array $query_vars WP_Term_Query arguments
+ * @return array
+ */
+ protected function maybe_orderby( $formatted_args, $query_vars ) {
+ // Set sort order, default is 'ASC'.
+ if ( ! empty( $query_vars['order'] ) ) {
+ $order = $this->parse_order( $query_vars['order'] );
+ } else {
+ $order = 'desc';
+ }
+
+ // Set orderby, default is 'name'.
+ if ( empty( $query_vars['orderby'] ) ) {
+ $query_vars['orderby'] = 'name';
+ }
+
+ // Set sort type.
+ $formatted_args['sort'] = $this->parse_orderby( $query_vars['orderby'], $order, $query_vars );
+
+ return $formatted_args;
+ }
+
+ /**
+ * Based on WP_Term_Query arguments, parses the various filters that could be applied into the ES query.
+ *
+ * @since 5.1.0
+ * @param array $query_vars WP_Term_Query arguments
+ * @return array
+ */
+ protected function parse_filters( $query_vars ) {
+ $filters = [
+ 'taxonomy' => $this->parse_taxonomy( $query_vars ),
+ 'object_ids' => $this->parse_object_ids( $query_vars ),
+ 'include' => $this->parse_include( $query_vars ),
+ 'exclude' => $this->parse_exclude( $query_vars ),
+ 'exclude_tree' => $this->parse_exclude_tree( $query_vars ),
+ 'name' => $this->parse_name( $query_vars ),
+ 'slug' => $this->parse_slug( $query_vars ),
+ 'term_taxonomy_id' => $this->parse_term_taxonomy_id( $query_vars ),
+ 'hierarchical_hide_empty' => $this->parse_hierarchical_hide_empty( $query_vars ),
+ 'child_of' => $this->parse_child_of( $query_vars ),
+ 'parent' => $this->parse_parent( $query_vars ),
+ 'childless' => $this->parse_childless( $query_vars ),
+ 'meta_query' => $this->parse_meta_queries( $query_vars ),
+ ];
+
+ $filters = array_values( array_filter( $filters ) );
+
+ if ( ! empty( $filters ) ) {
+ $filters = [
+ 'bool' => [
+ 'must' => $filters,
+ ],
+ ];
+ }
+
+ return $filters;
+ }
+
+ /**
+ * Parse the `taxonomy` WP Term Query arg and transform it into an ES query clause.
+ *
+ * @since 5.1.0
+ * @param array $query_vars WP_Term_Query arguments
+ * @return array
+ */
+ protected function parse_taxonomy( $query_vars ) {
+ if ( empty( $query_vars['taxonomy'] ) ) {
+ return [];
+ }
+
+ if ( count( $query_vars['taxonomy'] ) < 2 ) {
+ return [
+ 'term' => [
+ 'taxonomy.raw' => $query_vars['taxonomy'][0],
+ ],
+ ];
+ }
+
+ return [
+ 'terms' => [
+ 'taxonomy.raw' => $query_vars['taxonomy'],
+ ],
+ ];
+ }
+
+ /**
+ * Parse the `object_ids` WP Term Query arg and transform it into an ES query clause.
+ *
+ * @since 5.1.0
+ * @param array $query_vars WP_Term_Query arguments
+ * @return array
+ */
+ protected function parse_object_ids( $query_vars ) {
+ if ( empty( $query_vars['object_ids'] ) ) {
+ return [];
+ }
+
+ return [
+ 'bool' => [
+ 'must' => [
+ 'terms' => [
+ 'object_ids.value' => (array) $query_vars['object_ids'],
+ ],
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * Parse the `include` WP Term Query arg and transform it into an ES query clause.
+ *
+ * @since 5.1.0
+ * @param array $query_vars WP_Term_Query arguments
+ * @return array
+ */
+ protected function parse_include( $query_vars ) {
+ if ( empty( $query_vars['include'] ) ) {
+ return [];
+ }
+
+ return [
+ 'bool' => [
+ 'must' => [
+ 'terms' => [
+ 'term_id' => array_values( (array) $query_vars['include'] ),
+ ],
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * Parse the `exclude` WP Term Query arg and transform it into an ES query clause.
+ *
+ * @since 5.1.0
+ * @param array $query_vars WP_Term_Query arguments
+ * @return array
+ */
+ protected function parse_exclude( $query_vars ) {
+ if ( ! empty( $query_vars['include'] ) || empty( $query_vars['exclude'] ) ) {
+ return [];
+ }
+
+ return [
+ 'bool' => [
+ 'must_not' => [
+ 'terms' => [
+ 'term_id' => array_values( (array) $query_vars['exclude'] ),
+ ],
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * Parse the `exclude_tree` WP Term Query arg and transform it into an ES query clause.
+ *
+ * @since 5.1.0
+ * @param array $query_vars WP_Term_Query arguments
+ * @return array
+ */
+ protected function parse_exclude_tree( $query_vars ) {
+ if ( ! empty( $query_vars['include'] ) || empty( $query_vars['exclude_tree'] ) ) {
+ return [];
+ }
+
+ return [
+ 'bool' => [
+ 'must_not' => [
+ [
+ 'terms' => [
+ 'term_id' => array_values( (array) $query_vars['exclude_tree'] ),
+ ],
+ ],
+ [
+ 'terms' => [
+ 'parent' => array_values( (array) $query_vars['exclude_tree'] ),
+ ],
+ ],
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * Parse the `name` WP Term Query arg and transform it into an ES query clause.
+ *
+ * @since 5.1.0
+ * @param array $query_vars WP_Term_Query arguments
+ * @return array
+ */
+ protected function parse_name( $query_vars ) {
+ if ( empty( $query_vars['name'] ) ) {
+ return [];
+ }
+
+ return [
+ 'terms' => [
+ 'name.raw' => (array) $query_vars['name'],
+ ],
+ ];
+ }
+
+ /**
+ * Parse the `slug` WP Term Query arg and transform it into an ES query clause.
+ *
+ * @since 5.1.0
+ * @param array $query_vars WP_Term_Query arguments
+ * @return array
+ */
+ protected function parse_slug( $query_vars ) {
+ if ( empty( $query_vars['slug'] ) ) {
+ return [];
+ }
+
+ $query_vars['slug'] = (array) $query_vars['slug'];
+ $query_vars['slug'] = array_map( 'sanitize_title', $query_vars['slug'] );
+
+ return [
+ 'terms' => [
+ 'slug.raw' => (array) $query_vars['slug'],
+ ],
+ ];
+ }
+
+ /**
+ * Parse the `term_taxonomy_id` WP Term Query arg and transform it into an ES query clause.
+ *
+ * @since 5.1.0
+ * @param array $query_vars WP_Term_Query arguments
+ * @return array
+ */
+ protected function parse_term_taxonomy_id( $query_vars ) {
+ if ( empty( $query_vars['term_taxonomy_id'] ) ) {
+ return [];
+ }
+
+ return [
+ 'bool' => [
+ 'must' => [
+ 'terms' => [
+ 'term_taxonomy_id' => array_values( (array) $query_vars['term_taxonomy_id'] ),
+ ],
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * Parse the `hide_empty` and `hierarchical` WP Term Query args and transform them into ES query clauses.
+ *
+ * `hierarchical` needs to work in conjunction with `hide_empty`, as per WP docs:
+ * > `hierarchical`: Whether to include terms that have non-empty descendants (even if $hide_empty is set to true).
+ *
+ * In summary:
+ * - hide_empty AND hierarchical: count > 1 OR hierarchy.children > 1
+ * - hide_empty AND NOT hierarchical: count > 1 (ignore hierarchy.children)
+ * - NOT hide_empty (AND hierarchical): there is no need to limit the query
+ *
+ * @see https://developer.wordpress.org/reference/classes/WP_Term_Query/__construct/
+ * @since 5.1.0
+ * @param array $query_vars WP_Term_Query arguments
+ * @return array
+ */
+ protected function parse_hierarchical_hide_empty( $query_vars ) {
+ $hide_empty = isset( $query_vars['hide_empty'] ) ? $query_vars['hide_empty'] : '';
+ if ( ! $hide_empty ) {
+ return [];
+ }
+
+ $hierarchical = isset( $query_vars['hierarchical'] ) ? $query_vars['hierarchical'] : '';
+ if ( ! $hierarchical ) {
+ return [
+ 'range' => [
+ 'count' => [
+ 'gte' => 1,
+ ],
+ ],
+ ];
+ }
+
+ return [
+ 'bool' => [
+ 'should' => [
+ [
+ 'range' => [
+ 'count' => [
+ 'gte' => 1,
+ ],
+ ],
+ ],
+ [
+ 'range' => [
+ 'hierarchy.children.count' => [
+ 'gte' => 1,
+ ],
+ ],
+ ],
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * Parse the `child_of` WP Term Query arg and transform it into an ES query clause.
+ *
+ * @since 5.1.0
+ * @param array $query_vars WP_Term_Query arguments
+ * @return array
+ */
+ protected function parse_child_of( $query_vars ) {
+ if ( empty( $query_vars['child_of'] ) || count( $query_vars['taxonomy'] ) > 1 ) {
+ return [];
+ }
+
+ return [
+ 'bool' => [
+ 'must' => [
+ 'match_phrase' => [
+ 'hierarchy.ancestors.terms' => (int) $query_vars['child_of'],
+ ],
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * Parse the `parent` WP Term Query arg and transform it into an ES query clause.
+ *
+ * @since 5.1.0
+ * @param array $query_vars WP_Term_Query arguments
+ * @return array
+ */
+ protected function parse_parent( $query_vars ) {
+ if ( ! isset( $query_vars['parent'] ) || '' === $query_vars['parent'] ) {
+ return [];
+ }
+
+ return [
+ 'bool' => [
+ 'must' => [
+ 'term' => [
+ 'parent' => (int) $query_vars['parent'],
+ ],
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * Parse the `childless` WP Term Query arg and transform it into an ES query clause.
+ *
+ * @since 5.1.0
+ * @param array $query_vars WP_Term_Query arguments
+ * @return array
+ */
+ protected function parse_childless( $query_vars ) {
+ if ( empty( $query_vars['childless'] ) ) {
+ return [];
+ }
+
+ return [
+ 'bool' => [
+ 'must' => [
+ 'term' => [
+ 'hierarchy.children.terms' => 0,
+ ],
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * Parse WP Term Query meta queries and transform them into ES query clauses.
+ *
+ * @since 5.1.0
+ * @param array $query_vars WP_Term_Query arguments
+ * @return array
+ */
+ protected function parse_meta_queries( $query_vars ) {
+ $meta_queries = [];
+ /**
+ * Support `meta_key`, `meta_value`, and `meta_compare` query args
+ */
+ if ( ! empty( $query_vars['meta_key'] ) ) {
+ $meta_query_array = [
+ 'key' => $query_vars['meta_key'],
+ ];
+
+ if ( isset( $query_vars['meta_value'] ) && '' !== $query_vars['meta_value'] ) {
+ $meta_query_array['value'] = $query_vars['meta_value'];
+ }
+
+ if ( isset( $query_vars['meta_compare'] ) ) {
+ $meta_query_array['compare'] = $query_vars['meta_compare'];
+ }
+
+ $meta_queries[] = $meta_query_array;
+ }
+
+ /**
+ * Support 'meta_query' query var.
+ */
+ if ( ! empty( $query_vars['meta_query'] ) ) {
+ $meta_queries = array_merge( $meta_queries, $query_vars['meta_query'] );
+ }
+
+ if ( ! empty( $meta_queries ) ) {
+ $built_meta_queries = $this->build_meta_query( $meta_queries );
+
+ if ( $built_meta_queries ) {
+ return $built_meta_queries;
+ }
+ }
+
+ return [];
+ }
+
+ /**
+ * If in a search context, using `name__like`, or `description__like` set search fields, otherwise query everything.
+ *
+ * @since 5.1.0
+ * @param array $formatted_args Formatted Elasticsearch query
+ * @param array $query_vars WP_Term_Query arguments
+ * @return array
+ */
+ protected function maybe_set_search_fields( $formatted_args, $query_vars ) {
+ if ( empty( $query_vars['search'] ) && empty( $query_vars['name__like'] ) && empty( $query_vars['description__like'] ) ) {
+ $formatted_args['query']['match_all'] = [
+ 'boost' => 1,
+ ];
+
+ return $formatted_args;
+ }
+
+ $search = ! empty( $query_vars['search'] ) ? $query_vars['search'] : '';
+ $search_fields = [];
+
+ if ( ! empty( $query_vars['name__like'] ) ) {
+ $search = $query_vars['name__like'];
+ $search_fields[] = 'name';
+ }
+
+ if ( ! empty( $query_vars['description__like'] ) ) {
+ $search = $query_vars['description__like'];
+ $search_fields[] = 'description';
+ }
+
+ /**
+ * Allow for search field specification
+ */
+ if ( ! empty( $query_vars['search_fields'] ) ) {
+ $search_fields = $query_vars['search_fields'];
+ }
+
+ if ( ! empty( $search_fields ) ) {
+ $prepared_search_fields = [];
+
+ if ( ! empty( $search_fields['meta'] ) ) {
+ $metas = (array) $search_fields['meta'];
+
+ foreach ( $metas as $meta ) {
+ $prepared_search_fields[] = 'meta.' . $meta . '.value';
+ }
+
+ unset( $search_fields['meta'] );
+ }
+
+ $prepared_search_fields = array_merge( $search_fields, $prepared_search_fields );
+ } else {
+ $prepared_search_fields = [
+ 'name',
+ 'slug',
+ 'taxonomy',
+ 'description',
+ ];
+ }
+
+ /**
+ * Filter fields to search on Term query
+ *
+ * @hook ep_term_search_fields
+ * @param {array} $search_fields Search fields
+ * @param {array} $query_vars Query variables
+ * @since 3.4
+ * @return {array} New search fields
+ */
+ $prepared_search_fields = apply_filters( 'ep_term_search_fields', $prepared_search_fields, $query_vars );
+
+ $search_algorithm = $this->get_search_algorithm( $search, $prepared_search_fields, $query_vars );
+ $formatted_args['query'] = $search_algorithm->get_query( 'term', $search, $prepared_search_fields, $query_vars );
+
+ return $formatted_args;
+ }
+
+ /**
+ * If needed set the `fields` ES query clause.
+ *
+ * @since 5.1.0
+ * @param array $formatted_args Formatted Elasticsearch query
+ * @param array $query_vars WP_Term_Query arguments
+ * @return array
+ */
+ protected function maybe_set_fields( $formatted_args, $query_vars ) {
+ if ( ! isset( $query_vars['fields'] ) ) {
+ return $formatted_args;
+ }
+
+ switch ( $query_vars['fields'] ) {
+ case 'ids':
+ $formatted_args['_source'] = [
+ 'includes' => [
+ 'term_id',
+ ],
+ ];
+ break;
+
+ case 'id=>name':
+ $formatted_args['_source'] = [
+ 'includes' => [
+ 'term_id',
+ 'name',
+ ],
+ ];
+ break;
+
+ case 'id=>parent':
+ $formatted_args['_source'] = [
+ 'includes' => [
+ 'term_id',
+ 'parent',
+ ],
+ ];
+ break;
+
+ case 'id=>slug':
+ $formatted_args['_source'] = [
+ 'includes' => [
+ 'term_id',
+ 'slug',
+ ],
+ ];
+ break;
+
+ case 'names':
+ $formatted_args['_source'] = [
+ 'includes' => [
+ 'name',
+ ],
+ ];
+ break;
+ case 'tt_ids':
+ $formatted_args['_source'] = [
+ 'includes' => [
+ 'term_taxonomy_id',
+ ],
+ ];
+ break;
+ }
+
+ return $formatted_args;
+ }
}
diff --git a/includes/classes/REST/SearchOrdering.php b/includes/classes/REST/SearchOrdering.php
index 3420137474..48d02c61ed 100644
--- a/includes/classes/REST/SearchOrdering.php
+++ b/includes/classes/REST/SearchOrdering.php
@@ -71,7 +71,7 @@ public function get_args() {
* @return boolean
*/
public function check_permission() {
- $capability = Utils\get_capability();
+ $capability = Utils\get_capability( 'search-ordering' );
return current_user_can( $capability );
}
diff --git a/includes/classes/REST/Synonyms.php b/includes/classes/REST/Synonyms.php
new file mode 100644
index 0000000000..7d5870b935
--- /dev/null
+++ b/includes/classes/REST/Synonyms.php
@@ -0,0 +1,119 @@
+ $this->get_args(),
+ 'callback' => [ $this, 'update_synonyms' ],
+ 'methods' => 'PUT',
+ 'permission_callback' => [ $this, 'check_permission' ],
+ ]
+ );
+ }
+
+ /**
+ * Get args schema.
+ *
+ * @return array
+ */
+ public function get_args() {
+ $feature = Features::factory()->get_registered_feature( 'search' )->synonyms;
+
+ $args = [
+ 'mode' => [
+ 'default' => 'simple',
+ 'description' => __( 'Synonyms editor mode.', 'elasticpress' ),
+ 'enum' => [ 'advanced', 'simple' ],
+ ],
+ 'solr' => [
+ 'description' => __( 'Synonyms in Solr format.', 'elasticpress' ),
+ 'type' => 'string',
+ 'sanitize_callback' => [ $this, 'sanitize_solr' ],
+ ],
+ ];
+
+ return $args;
+ }
+
+ /**
+ * Check that the request has permission to save synonyms.
+ *
+ * @return boolean
+ */
+ public function check_permission() {
+ $capability = Utils\get_capability();
+
+ return current_user_can( $capability );
+ }
+
+ /**
+ * Sanitize Solr synonyms.
+ *
+ * @param string $value Solr synonyms,
+ * @return string
+ */
+ public function sanitize_solr( $value ) {
+ $solr = trim( $value );
+ $solr = sanitize_textarea_field( $value );
+
+ return $solr;
+ }
+
+ /**
+ * Update synonyms settings.
+ *
+ * @param \WP_REST_Request $request Full details about the request.
+ * @return array
+ */
+ public function update_synonyms( \WP_REST_Request $request ) {
+ $feature = Features::factory()->get_registered_feature( 'search' )->synonyms;
+
+ $mode = $request->get_param( 'mode' );
+ $solr = $request->get_param( 'solr' );
+
+ $post_id = $feature->update_synonym_post( $solr );
+
+ if ( ! $post_id || is_wp_error( $post_id ) ) {
+ return new \WP_Error( 'error-update-post' );
+ }
+
+ $updated = $feature->update_synonyms();
+
+ if ( ! $updated ) {
+ return new \WP_Error( 'error-update-index' );
+ }
+
+ $feature->save_editor_mode( $mode );
+
+ return [
+ 'data' => $solr,
+ 'success' => true,
+ ];
+ }
+}
diff --git a/includes/compat.php b/includes/compat.php
index d521fb9e69..bd746ab507 100644
--- a/includes/compat.php
+++ b/includes/compat.php
@@ -65,7 +65,7 @@ function ep_find_related( $post_id, $return = 5 ) {
* Index a post given an ID
*
* @param int $post_id Post ID
- * @return boolean|array
+ * @return boolean|object
*/
function ep_index_post( $post_id ) {
_deprecated_function( __FUNCTION__, '3.0', "ElasticPress\Indexables::factory()->get( 'post' )->index" );
diff --git a/includes/dashboard.php b/includes/dashboard.php
index 951c0b54c3..8c83a6937c 100644
--- a/includes/dashboard.php
+++ b/includes/dashboard.php
@@ -493,15 +493,28 @@ function action_admin_enqueue_dashboard_scripts() {
$weightable_fields = $weighting->get_weightable_fields();
$weighting_configuration = $weighting->get_weighting_configuration_with_defaults();
- wp_localize_script(
- 'ep_weighting_script',
- 'epWeighting',
- array(
+ /**
+ * Filter weighting dashboard options.
+ *
+ * @hook ep_weighting_options
+ * @param {array} $data Weighting dashboard options
+ * @return {array} New options array
+ * @since 5.1.0
+ */
+ $data = apply_filters(
+ 'ep_weighting_options',
+ [
'apiUrl' => $api_url,
'metaMode' => $meta_mode,
'weightableFields' => $weightable_fields,
'weightingConfiguration' => $weighting_configuration,
- )
+ ]
+ );
+
+ wp_localize_script(
+ 'ep_weighting_script',
+ 'epWeighting',
+ $data
);
wp_set_script_translations( 'ep_weighting_script', 'elasticpress' );
diff --git a/includes/utils.php b/includes/utils.php
index 59d338d49c..be0f5bc7d7 100644
--- a/includes/utils.php
+++ b/includes/utils.php
@@ -54,47 +54,66 @@ function get_epio_credentials() {
/**
* Get WP capability needed for a user to interact with ElasticPress in the admin
*
- * @since 4.5.0
+ * @since 4.5.0, 5.1.0 added $context
+ * @param string $context Context for the capability. Defaults to empty string.
* @return string
*/
-function get_capability() : string {
+function get_capability( string $context = '' ) : string {
/**
* Filter the WP capability needed to interact with ElasticPress in the admin
*
- * @since 4.5.0
+ * Example:
+ * ```
+ * add_filter(
+ * 'ep_capability',
+ * function ( $cacapability, $context ) {
+ * return ( 'synonyms' === $context ) ?
+ * 'manage_elasticpress_synonyms' :
+ * $cacapability;
+ * },
+ * 10,
+ * 2
+ * );
+ * ```
+ *
+ * @since 4.5.0, 5.1.0 added $context
* @hook ep_capability
* @param {string} $capability Capability name. Defaults to `'manage_elasticpress'`
+ * @param {string} $context Additional context
* @return {string} New capability value
*/
- return apply_filters( 'ep_capability', 'manage_elasticpress' );
+ return apply_filters( 'ep_capability', 'manage_elasticpress', $context );
}
/**
* Get WP capability needed for a user to interact with ElasticPress in the network admin
*
- * @since 4.5.0
+ * @since 4.5.0, 5.1.0 added $context
+ * @param string $context Context for the capability. Defaults to empty string.
* @return string
*/
-function get_network_capability() : string {
+function get_network_capability( string $context = '' ) : string {
/**
* Filter the WP capability needed to interact with ElasticPress in the network admin
*
- * @since 4.5.0
+ * @since 4.5.0, 5.1.0 added $context
* @hook ep_network_capability
* @param {string} $capability Capability name. Defaults to `'manage_network_elasticpress'`
+ * @param {string} $context Additional context
* @return {string} New capability value
*/
- return apply_filters( 'ep_network_capability', 'manage_network_elasticpress' );
+ return apply_filters( 'ep_network_capability', 'manage_network_elasticpress', $context );
}
/**
* Get mapped capabilities for post types
*
- * @since 4.5.0
+ * @since 4.5.0, 5.1.0 added $context
+ * @param string $context Context for the capability. Defaults to empty string.
* @return array
*/
-function get_post_map_capabilities() : array {
- $capability = get_capability();
+function get_post_map_capabilities( string $context = '' ) : array {
+ $capability = get_capability( $context );
return [
'edit_post' => $capability,
@@ -656,7 +675,7 @@ function is_integrated_request( $context, $types = [] ) {
}
$is_admin_request = is_admin();
- $is_ajax_request = defined( 'DOING_AJAX' ) && DOING_AJAX;
+ $is_ajax_request = wp_doing_ajax();
$is_rest_request = defined( 'REST_REQUEST' ) && REST_REQUEST;
$is_integrated_admin_request = false;
$is_integrated_ajax_request = false;
diff --git a/lang/elasticpress.pot b/lang/elasticpress.pot
index 109d4e4933..df9843acd4 100644
--- a/lang/elasticpress.pot
+++ b/lang/elasticpress.pot
@@ -2,14 +2,14 @@
# This file is distributed under the GPL v2 or later.
msgid ""
msgstr ""
-"Project-Id-Version: ElasticPress 5.0.2\n"
+"Project-Id-Version: ElasticPress 5.1.0\n"
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/elasticpress\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: 2024-01-16T13:04:02+00:00\n"
+"POT-Creation-Date: 2024-04-29T12:35:08+00:00\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"X-Generator: WP-CLI 2.8.1\n"
"X-Domain: elasticpress\n"
@@ -136,7 +136,7 @@ msgstr ""
#: includes/classes/AdminNotices.php:813
#: includes/classes/ElasticsearchErrorInterpreter.php:93
#: includes/classes/StatusReport/ElasticPressIo.php:30
-#: assets/js/sync/index.js:230
+#: assets/js/sync/index.js:269
#: dist/js/sync-script.js:1
msgid "ElasticPress.io"
msgstr ""
@@ -144,7 +144,7 @@ msgstr ""
#: includes/classes/AdminNotices.php:797
#: includes/classes/AdminNotices.php:813
#: includes/classes/ElasticsearchErrorInterpreter.php:93
-#: assets/js/sync/index.js:231
+#: assets/js/sync/index.js:270
#: dist/js/sync-script.js:1
msgid "Elasticsearch"
msgstr ""
@@ -411,19 +411,19 @@ msgstr ""
msgid "This command is deprecated. Please use %s instead."
msgstr ""
-#: includes/classes/Elasticsearch.php:987
+#: includes/classes/Elasticsearch.php:999
msgid "Error while getting the index settings."
msgstr ""
-#: includes/classes/Elasticsearch.php:1391
+#: includes/classes/Elasticsearch.php:1403
msgid "Site not indexed. Please run: wp elasticpress index --setup --network-wide
using WP-CLI. Or use the index button on the left of this screen.
"
msgstr ""
-#: includes/classes/Elasticsearch.php:1395
+#: includes/classes/Elasticsearch.php:1407
msgid "Site not indexed. Please run: wp elasticpress index --setup
using WP-CLI. Or use the index button on the left of this screen.
"
msgstr ""
-#: includes/classes/Elasticsearch.php:1577
+#: includes/classes/Elasticsearch.php:1593
msgid "Elasticsearch Host is not available."
msgstr ""
@@ -452,13 +452,18 @@ msgstr ""
msgid "Your website content has more public custom fields than %1$s is able to store. Check our articles about Elasticsearch field limitations and how to index just the custom fields you need and sync again."
msgstr ""
+#. translators: ElasticPress.io Article URL
+#: includes/classes/ElasticsearchErrorInterpreter.php:113
+msgid "Please refer to this article outlining how to address this issue."
+msgstr ""
+
#. translators: ElasticPress.io My Account URL
-#: includes/classes/ElasticsearchErrorInterpreter.php:105
+#: includes/classes/ElasticsearchErrorInterpreter.php:122
msgid "We did not recognize this error. Please open an ElasticPress.io support ticket so we can troubleshoot further."
msgstr ""
#. translators: New GitHub issue URL
-#: includes/classes/ElasticsearchErrorInterpreter.php:115
+#: includes/classes/ElasticsearchErrorInterpreter.php:132
msgid "We did not recognize this error. Please consider opening a GitHub Issue so we can add it to our list of supported errors."
msgstr ""
@@ -484,7 +489,7 @@ msgstr ""
#: includes/classes/Feature.php:392
#: includes/classes/StatusReport/ElasticPress.php:79
-#: includes/dashboard.php:635
+#: includes/dashboard.php:648
#: includes/partials/settings-page.php:34
#: assets/js/blocks/facets/common/edit.js:87
#: assets/js/blocks/facets/date/edit.js:23
@@ -507,20 +512,18 @@ msgstr ""
#: includes/classes/Feature.php:398
#: includes/classes/Feature/Autosuggest/Autosuggest.php:112
#: includes/classes/Feature/InstantResults/InstantResults.php:184
-#: includes/classes/Feature/Search/Search.php:625
-#: includes/classes/Feature/Search/Search.php:642
-#: includes/classes/Feature/Search/Search.php:663
-#: includes/classes/Feature/WooCommerce/WooCommerce.php:195
+#: includes/classes/Feature/Search/Search.php:632
+#: includes/classes/Feature/Search/Search.php:649
+#: includes/classes/Feature/Search/Search.php:670
msgid "Enabled"
msgstr ""
#: includes/classes/Feature.php:399
#: includes/classes/Feature/Autosuggest/Autosuggest.php:113
#: includes/classes/Feature/InstantResults/InstantResults.php:187
-#: includes/classes/Feature/Search/Search.php:626
-#: includes/classes/Feature/Search/Search.php:643
-#: includes/classes/Feature/Search/Search.php:664
-#: includes/classes/Feature/WooCommerce/WooCommerce.php:196
+#: includes/classes/Feature/Search/Search.php:633
+#: includes/classes/Feature/Search/Search.php:650
+#: includes/classes/Feature/Search/Search.php:671
msgid "Disabled"
msgstr ""
@@ -567,7 +570,7 @@ msgid "When enabled, a gtag tracking event is fired when an autosuggest result i
msgstr ""
#: includes/classes/Feature/Autosuggest/Autosuggest.php:128
-#: includes/classes/Feature/Autosuggest/Autosuggest.php:910
+#: includes/classes/Feature/Autosuggest/Autosuggest.php:911
msgid "Endpoint URL"
msgstr ""
@@ -576,7 +579,7 @@ msgid "Your autosuggest endpoint is set in wp-config.php"
msgstr ""
#: includes/classes/Feature/Autosuggest/Autosuggest.php:136
-#: includes/classes/Feature/Autosuggest/Autosuggest.php:908
+#: includes/classes/Feature/Autosuggest/Autosuggest.php:909
msgid "This address will be exposed to the public."
msgstr ""
@@ -722,7 +725,7 @@ msgid "Documents"
msgstr ""
#: includes/classes/Feature/Documents/Documents.php:30
-#: includes/classes/Feature/Documents/Documents.php:359
+#: includes/classes/Feature/Documents/Documents.php:326
msgid "Website search results will include popular document file types, using file names as well as their content. Supported file types include: ppt, pptx, doc, docx, xls, xlsx, pdf."
msgstr ""
@@ -730,23 +733,23 @@ msgstr ""
msgid "https://elasticpress.zendesk.com/hc/en-us/articles/360050447492-Configuring-ElasticPress-via-the-Plugin-Dashboard#documents"
msgstr ""
-#: includes/classes/Feature/Documents/Documents.php:343
+#: includes/classes/Feature/Documents/Documents.php:310
msgid "The Ingest Attachment plugin for Elasticsearch is not installed. To get the most out of ElasticPress, without the hassle of Elasticsearch management, check out ElasticPress.io hosting."
msgstr ""
-#: includes/classes/Feature/Documents/Documents.php:346
+#: includes/classes/Feature/Documents/Documents.php:313
msgid "This feature modifies the default user experience for your visitors by adding popular document file types to search results. All supported documents (PDFs and Microsoft Office) uploaded to your media library will appear in search results."
msgstr ""
-#: includes/classes/Feature/Documents/Documents.php:489
+#: includes/classes/Feature/Documents/Documents.php:458
msgid "Description"
msgstr ""
-#: includes/classes/Feature/Documents/Documents.php:490
+#: includes/classes/Feature/Documents/Documents.php:459
msgid "Caption"
msgstr ""
-#: includes/classes/Feature/Documents/Documents.php:495
+#: includes/classes/Feature/Documents/Documents.php:464
msgid "Document Content"
msgstr ""
@@ -770,51 +773,51 @@ msgstr ""
msgid "https://elasticpress.zendesk.com/hc/en-us/articles/360050447492-Configuring-ElasticPress-via-the-Plugin-Dashboard#facets"
msgstr ""
-#: includes/classes/Feature/Facets/Facets.php:143
+#: includes/classes/Feature/Facets/Facets.php:169
#: includes/classes/Feature/InstantResults/InstantResults.php:167
msgid "Match Type"
msgstr ""
-#: includes/classes/Feature/Facets/Facets.php:145
+#: includes/classes/Feature/Facets/Facets.php:171
#: includes/classes/Feature/InstantResults/InstantResults.php:171
msgid "Show any content tagged to all selected terms"
msgstr ""
-#: includes/classes/Feature/Facets/Facets.php:146
+#: includes/classes/Feature/Facets/Facets.php:172
#: includes/classes/Feature/InstantResults/InstantResults.php:175
msgid "Show all content tagged to any selected term"
msgstr ""
-#: includes/classes/Feature/Facets/Facets.php:147
+#: includes/classes/Feature/Facets/Facets.php:173
#: includes/classes/Feature/InstantResults/InstantResults.php:177
msgid "\"All\" will only show content that matches all filters. \"Any\" will show content that matches any filter."
msgstr ""
-#: includes/classes/Feature/Facets/Facets.php:215
+#: includes/classes/Feature/Facets/Facets.php:241
msgid "Facets no longer require admin styles."
msgstr ""
#. translators: Widgets Edit Screen URL
-#: includes/classes/Feature/Facets/Facets.php:508
+#: includes/classes/Feature/Facets/Facets.php:534
msgid "Adds filter widgets that administrators can add to the website's sidebars (widgetized areas), so that visitors can filter applicable content and search results by one or more taxonomy terms."
msgstr ""
#. translators: Site Editor URL
-#: includes/classes/Feature/Facets/Facets.php:516
+#: includes/classes/Feature/Facets/Facets.php:542
msgid "Adds filter blocks that administrators can add to the website's templates and template parts, so that visitors can filter applicable content and search results by one or more taxonomy terms."
msgstr ""
-#: includes/classes/Feature/Facets/Facets.php:671
+#: includes/classes/Feature/Facets/Facets.php:697
#: includes/classes/Feature/InstantResults/InstantResults.php:1095
msgid "Filter matching"
msgstr ""
-#: includes/classes/Feature/Facets/Facets.php:674
+#: includes/classes/Feature/Facets/Facets.php:700
#: includes/classes/Feature/InstantResults/InstantResults.php:1098
msgid "Show results that match all selected filters"
msgstr ""
-#: includes/classes/Feature/Facets/Facets.php:678
+#: includes/classes/Feature/Facets/Facets.php:704
#: includes/classes/Feature/InstantResults/InstantResults.php:1102
msgid "Show results that match any selected filter"
msgstr ""
@@ -905,6 +908,8 @@ msgid "any"
msgstr ""
#: includes/classes/Feature/Facets/Types/Taxonomy/Widget.php:116
+#: assets/js/sync-ui/components/errors.js:27
+#: dist/js/sync-script.js:1
msgid "Count"
msgstr ""
@@ -964,13 +969,13 @@ msgid "ElasticPress.io plan"
msgstr ""
#: includes/classes/Feature/InstantResults/InstantResults.php:142
-#: includes/classes/Feature/Search/Search.php:648
+#: includes/classes/Feature/Search/Search.php:655
msgid "Highlight tag "
msgstr ""
#: includes/classes/Feature/InstantResults/InstantResults.php:145
#: includes/classes/Feature/InstantResults/InstantResults.php:1059
-#: includes/classes/Feature/Search/Search.php:894
+#: includes/classes/Feature/Search/Search.php:901
msgid "None"
msgstr ""
@@ -1016,12 +1021,12 @@ msgid "Price"
msgstr ""
#: includes/classes/Feature/InstantResults/InstantResults.php:1054
-#: includes/classes/Feature/Search/Search.php:889
+#: includes/classes/Feature/Search/Search.php:896
msgid "Select the HTML tag used to highlight search terms."
msgstr ""
#: includes/classes/Feature/InstantResults/InstantResults.php:1056
-#: includes/classes/Feature/Search/Search.php:891
+#: includes/classes/Feature/Search/Search.php:898
msgid "Highlight tag"
msgstr ""
@@ -1096,7 +1101,7 @@ msgid "Instantly find the content you’re looking for. The first time."
msgstr ""
#: includes/classes/Feature/Search/Search.php:58
-#: includes/classes/Feature/Search/Search.php:572
+#: includes/classes/Feature/Search/Search.php:579
msgid "Overcome higher-end performance and functional limits posed by the traditional WordPress structured (SQL) database to deliver superior keyword search, instantly. ElasticPress indexes custom fields, tags, and other metadata to improve search results. Fuzzy matching accounts for misspellings and verb tenses."
msgstr ""
@@ -1104,197 +1109,211 @@ msgstr ""
msgid "https://elasticpress.zendesk.com/hc/en-us/articles/360050447492-Configuring-ElasticPress-via-the-Plugin-Dashboard#post-search"
msgstr ""
-#: includes/classes/Feature/Search/Search.php:623
-#: includes/classes/Feature/Search/Search.php:867
+#: includes/classes/Feature/Search/Search.php:630
+#: includes/classes/Feature/Search/Search.php:874
msgid "Weight results by date"
msgstr ""
-#: includes/classes/Feature/Search/Search.php:640
+#: includes/classes/Feature/Search/Search.php:647
msgid "Highlighting status"
msgstr ""
-#: includes/classes/Feature/Search/Search.php:661
+#: includes/classes/Feature/Search/Search.php:668
msgid "Excerpt highlighting"
msgstr ""
-#: includes/classes/Feature/Search/Search.php:665
+#: includes/classes/Feature/Search/Search.php:672
msgid "By default, WordPress strips HTML from content excerpts. Enable when using the_excerpt() to display search results. "
msgstr ""
-#: includes/classes/Feature/Search/Search.php:671
-#: includes/classes/Feature/Search/Search.php:934
+#: includes/classes/Feature/Search/Search.php:678
+#: includes/classes/Feature/Search/Search.php:941
msgid "Advanced fields and weighting settings"
msgstr ""
-#: includes/classes/Feature/Search/Search.php:672
-#: includes/classes/Feature/Search/Search.php:936
+#: includes/classes/Feature/Search/Search.php:679
+#: includes/classes/Feature/Search/Search.php:943
msgid "Add synonyms to your post searches"
msgstr ""
-#: includes/classes/Feature/Search/Search.php:776
+#: includes/classes/Feature/Search/Search.php:783
#: assets/js/search/editor/plugins/exclude-from-search.js:23
#: dist/js/search-editor-script.js:1
msgid "Exclude from search results"
msgstr ""
-#: includes/classes/Feature/Search/Search.php:779
+#: includes/classes/Feature/Search/Search.php:786
msgid "Excludes this media from the results of your site's search form while ElasticPress is active."
msgstr ""
-#: includes/classes/Feature/Search/Search.php:781
+#: includes/classes/Feature/Search/Search.php:788
#: assets/js/search/editor/plugins/exclude-from-search.js:24
#: dist/js/search-editor-script.js:1
msgid "Excludes this post from the results of your site's search form while ElasticPress is active."
msgstr ""
-#: includes/classes/Feature/Search/Search.php:860
+#: includes/classes/Feature/Search/Search.php:867
msgid "Weighting by date"
msgstr ""
-#: includes/classes/Feature/Search/Search.php:863
+#: includes/classes/Feature/Search/Search.php:870
msgid "Don't weight results by date"
msgstr ""
-#: includes/classes/Feature/Search/Search.php:877
+#: includes/classes/Feature/Search/Search.php:884
msgid "Highlight search terms"
msgstr ""
-#: includes/classes/Feature/Search/Search.php:882
+#: includes/classes/Feature/Search/Search.php:889
msgid "By default, WordPress strips HTML from content excerpts. Enable when using the_excerpt()
to display search results."
msgstr ""
-#: includes/classes/Feature/Search/Search.php:884
+#: includes/classes/Feature/Search/Search.php:891
msgid "Highlight search terms in excerpts"
msgstr ""
-#: includes/classes/Feature/Search/Synonyms.php:88
+#: includes/classes/Feature/Search/Synonyms.php:89
msgid "This feature requires the \"Post Search\" feature to be enabled"
msgstr ""
-#: includes/classes/Feature/Search/Synonyms.php:172
+#: includes/classes/Feature/Search/Synonyms.php:182
msgid "ElasticPress Synonyms"
msgstr ""
-#: includes/classes/Feature/Search/Synonyms.php:173
+#: includes/classes/Feature/Search/Synonyms.php:183
#: includes/classes/StatusReport/Features.php:91
+#: assets/js/synonyms/components/groups/synonyms.js:43
+#: dist/js/synonyms-script.js:1
msgid "Synonyms"
msgstr ""
-#: includes/classes/Feature/Search/Synonyms.php:219
+#: includes/classes/Feature/Search/Synonyms.php:229
msgid "Successfully updated synonym filter."
msgstr ""
-#: includes/classes/Feature/Search/Synonyms.php:222
+#: includes/classes/Feature/Search/Synonyms.php:232
msgid "There was an error storing your synonyms."
msgstr ""
-#: includes/classes/Feature/Search/Synonyms.php:225
+#: includes/classes/Feature/Search/Synonyms.php:235
msgid "There was a problem updating the index with your synonyms. If you have not indexed your data, please run an index."
msgstr ""
-#: includes/classes/Feature/Search/Synonyms.php:228
+#: includes/classes/Feature/Search/Synonyms.php:238
msgid "There was an error updating the synonym list."
msgstr ""
-#: includes/classes/Feature/Search/Synonyms.php:245
+#: includes/classes/Feature/Search/Synonyms.php:255
msgid "Elasticsearch Synonyms"
msgstr ""
-#: includes/classes/Feature/Search/Synonyms.php:639
-msgid "# Defined sets (equivalent synonyms)."
+#: includes/classes/Feature/Search/Synonyms.php:679
+#: assets/js/synonyms/utils.js:207
+#: dist/js/synonyms-script.js:1
+msgid "# Defined synonyms."
+msgstr ""
+
+#: includes/classes/Feature/Search/Synonyms.php:682
+#: assets/js/synonyms/utils.js:211
+#: dist/js/synonyms-script.js:1
+msgid "# Defined hyponyms."
msgstr ""
-#: includes/classes/Feature/Search/Synonyms.php:642
-msgid "# Defined alternatives (explicit mappings)."
+#: includes/classes/Feature/Search/Synonyms.php:685
+#: assets/js/synonyms/utils.js:215
+#: dist/js/synonyms-script.js:1
+msgid "# Defined replacements."
msgstr ""
-#: includes/classes/Feature/Search/Synonyms.php:656
+#: includes/classes/Feature/Search/Synonyms.php:704
+#: assets/js/synonyms/index.js:26
+#: dist/js/synonyms-script.js:1
msgid "Manage Synonyms"
msgstr ""
-#: includes/classes/Feature/Search/Synonyms.php:657
+#: includes/classes/Feature/Search/Synonyms.php:705
msgid "Synonyms enable more flexible search results that show relevant results even without an exact match. Synonyms can be defined as a sets where all words are synonyms for each other, or as alternatives where searches for the primary word will also match the rest, but no vice versa."
msgstr ""
-#: includes/classes/Feature/Search/Synonyms.php:658
+#: includes/classes/Feature/Search/Synonyms.php:706
msgid "Switch to Advanced Text Editor"
msgstr ""
-#: includes/classes/Feature/Search/Synonyms.php:659
+#: includes/classes/Feature/Search/Synonyms.php:707
msgid "Switch to Visual Editor"
msgstr ""
-#: includes/classes/Feature/Search/Synonyms.php:661
+#: includes/classes/Feature/Search/Synonyms.php:709
msgid "Sets"
msgstr ""
-#: includes/classes/Feature/Search/Synonyms.php:662
+#: includes/classes/Feature/Search/Synonyms.php:710
msgid "Sets are terms that will all match each other for search results. This is useful where all words are considered equivalent, such as product renaming or regional variations like sneakers, tennis shoes, trainers, and runners."
msgstr ""
-#: includes/classes/Feature/Search/Synonyms.php:663
+#: includes/classes/Feature/Search/Synonyms.php:711
msgid "Comma separated list of terms"
msgstr ""
-#: includes/classes/Feature/Search/Synonyms.php:664
+#: includes/classes/Feature/Search/Synonyms.php:712
msgid "Add Set"
msgstr ""
-#: includes/classes/Feature/Search/Synonyms.php:665
+#: includes/classes/Feature/Search/Synonyms.php:713
msgid "This set must contain at least 2 terms."
msgstr ""
-#: includes/classes/Feature/Search/Synonyms.php:667
+#: includes/classes/Feature/Search/Synonyms.php:715
msgid "Alternatives"
msgstr ""
-#: includes/classes/Feature/Search/Synonyms.php:668
+#: includes/classes/Feature/Search/Synonyms.php:716
msgid "Alternatives are terms that will also be matched when you search for the primary term. For instance, a search for shoes can also include results for sneaker, sandals, boots, and high heels."
msgstr ""
-#: includes/classes/Feature/Search/Synonyms.php:669
+#: includes/classes/Feature/Search/Synonyms.php:717
msgid "Primary term"
msgstr ""
-#: includes/classes/Feature/Search/Synonyms.php:670
+#: includes/classes/Feature/Search/Synonyms.php:718
msgid "Comma separated list of alternatives"
msgstr ""
-#: includes/classes/Feature/Search/Synonyms.php:671
+#: includes/classes/Feature/Search/Synonyms.php:719
msgid "Add Alternative"
msgstr ""
-#: includes/classes/Feature/Search/Synonyms.php:672
+#: includes/classes/Feature/Search/Synonyms.php:720
msgid "You must enter both a primary term and at least one alternative term."
msgstr ""
-#: includes/classes/Feature/Search/Synonyms.php:674
+#: includes/classes/Feature/Search/Synonyms.php:722
msgid "Advanced Synonym Editor"
msgstr ""
-#: includes/classes/Feature/Search/Synonyms.php:675
+#: includes/classes/Feature/Search/Synonyms.php:723
msgid "When you add Sets and Alternatives above, we reduce them to SolrSynonyms which Elasticsearch can understand. If you are an advanced user, you can edit synonyms directly using Solr synonym formatting. This is beneficial if you want to import a large dictionary of synonyms, or want to export this site's synonyms for use on another site."
msgstr ""
-#: includes/classes/Feature/Search/Synonyms.php:676
+#: includes/classes/Feature/Search/Synonyms.php:724
msgid "SolrSynonym Text"
msgstr ""
-#: includes/classes/Feature/Search/Synonyms.php:677
+#: includes/classes/Feature/Search/Synonyms.php:725
msgid "Alternatives must have both a primary term and at least one alternative term."
msgstr ""
-#: includes/classes/Feature/Search/Synonyms.php:678
+#: includes/classes/Feature/Search/Synonyms.php:726
msgid "Sets must contain at least 2 terms."
msgstr ""
-#: includes/classes/Feature/Search/Synonyms.php:680
-#: assets/js/weighting/components/field.js:71
+#: includes/classes/Feature/Search/Synonyms.php:728
+#: assets/js/weighting/components/field.js:80
#: dist/js/weighting-script.js:1
msgid "Remove"
msgstr ""
-#: includes/classes/Feature/Search/Synonyms.php:681
+#: includes/classes/Feature/Search/Synonyms.php:729
msgid "Update Synonyms"
msgstr ""
@@ -1520,6 +1539,28 @@ msgstr ""
msgid "This feature will empower your website to overcome traditional WordPress term search and query limitations that can present themselves at scale."
msgstr ""
+#: includes/classes/Feature/WooCommerce/Orders.php:373
+msgid "Although the WooCommerce and Protected Content features are enabled, ElasticPress will not integrate with the WooCommerce Orders list if WooCommerce's High-performance order storage is enabled."
+msgstr ""
+
+#: includes/classes/Feature/WooCommerce/OrdersAutosuggest.php:638
+msgid "Show suggestions when searching for Orders"
+msgstr ""
+
+#. translators: 1: tag (ElasticPress.io); 2. ; 3: tag (KB article); 4. ;
+#: includes/classes/Feature/WooCommerce/OrdersAutosuggest.php:659
+msgid "You are directly connected to %1$sElasticPress.io%2$s! Enable autosuggest for Orders to enhance Dashboard results and quickly find WooCommerce Orders. %3$sLearn More%4$s."
+msgstr ""
+
+#: includes/classes/Feature/WooCommerce/OrdersAutosuggest.php:671
+msgid "Currently, autosuggest for orders is only available if WooCommerce order data storage is set in legacy or compatibility mode."
+msgstr ""
+
+#. translators: 1: tag (ElasticPress.io); 2. ; 3: tag (KB article); 4. ;
+#: includes/classes/Feature/WooCommerce/OrdersAutosuggest.php:675
+msgid "Due to the sensitive nature of orders, this autosuggest feature is available only to %1$sElasticPress.io%2$s customers. %3$sLearn More%4$s."
+msgstr ""
+
#: includes/classes/Feature/WooCommerce/Products.php:326
msgid "SKU"
msgstr ""
@@ -1554,7 +1595,7 @@ msgid "WooCommerce"
msgstr ""
#: includes/classes/Feature/WooCommerce/WooCommerce.php:59
-#: includes/classes/Feature/WooCommerce/WooCommerce.php:179
+#: includes/classes/Feature/WooCommerce/WooCommerce.php:171
msgid "Most caching and performance tools can’t keep up with the nearly infinite ways your visitors might filter or navigate your products. No matter how many products, filters, or customers you have, ElasticPress will keep your online store performing quickly. If used in combination with the Protected Content feature, ElasticPress will also accelerate order searches and back end product management."
msgstr ""
@@ -1562,32 +1603,12 @@ msgstr ""
msgid "https://elasticpress.zendesk.com/hc/en-us/articles/360050447492-Configuring-ElasticPress-via-the-Plugin-Dashboard#woocommerce"
msgstr ""
-#: includes/classes/Feature/WooCommerce/WooCommerce.php:193
-msgid "Orders Autosuggest"
-msgstr ""
-
-#. translators: 1: tag (ElasticPress.io); 2. ; 3: tag (KB article); 4. ;
-#: includes/classes/Feature/WooCommerce/WooCommerce.php:203
-msgid "You are directly connected to %1$sElasticPress.io%2$s! Enable Orders Autosuggest to enhance Dashboard results and quickly find WooCommerce Orders. %3$sLearn More%4$s."
-msgstr ""
-
-#. translators: 1: tag (ElasticPress.io); 2. ; 3: tag (KB article); 4. ;
-#: includes/classes/Feature/WooCommerce/WooCommerce.php:205
-#: includes/classes/Feature/WooCommerce/WooCommerce.php:343
-msgid "Due to the sensitive nature of orders, this autosuggest feature is available only to %1$sElasticPress.io%2$s customers. %3$sLearn More%4$s."
-msgstr ""
-
-#: includes/classes/Feature/WooCommerce/WooCommerce.php:232
+#: includes/classes/Feature/WooCommerce/WooCommerce.php:186
msgid "WooCommerce not installed."
msgstr ""
-#. translators: 1: tag (ElasticPress.io); 2. ; 3: tag (KB article); 4. ;
-#: includes/classes/Feature/WooCommerce/WooCommerce.php:341
-msgid "You are directly connected to %1$sElasticPress.io%2$s! Enable autosuggest for Orders to enhance Dashboard results and quickly find WooCommerce Orders. %3$sLearn More%4$s."
-msgstr ""
-
-#: includes/classes/Feature/WooCommerce/WooCommerce.php:359
-msgid "Show suggestions when searching for Orders"
+#: includes/classes/Feature/WooCommerce/WooCommerce.php:281
+msgid "Settings are now generated via the set_settings_schema() method."
msgstr ""
#: includes/classes/HealthCheck/HealthCheckElasticsearch.php:41
@@ -1633,7 +1654,7 @@ msgstr ""
#: includes/classes/Indexable/Comment/QueryIntegration.php:140
#: includes/classes/Indexable/Post/QueryIntegration.php:295
-#: includes/classes/Indexable/Term/QueryIntegration.php:109
+#: includes/classes/Indexable/Term/QueryIntegration.php:111
msgid "sites is deprecated. Use site__in instead."
msgstr ""
@@ -1742,12 +1763,12 @@ msgstr ""
msgid "Index %s deleted"
msgstr ""
-#: includes/classes/IndexHelper.php:1416
+#: includes/classes/IndexHelper.php:1417
msgid "Mapping has failed, which will cause ElasticPress search results to be incorrect. Please click `Delete all Data and Start a Fresh Sync` to retry mapping."
msgstr ""
#. translators: Error message
-#: includes/classes/IndexHelper.php:1420
+#: includes/classes/IndexHelper.php:1422
msgid "Index failed: %s"
msgstr ""
@@ -1833,6 +1854,14 @@ msgstr ""
msgid "Processed %1$d/%2$d. Last Object ID: %3$d"
msgstr ""
+#: includes/classes/REST/Synonyms.php:51
+msgid "Synonyms editor mode."
+msgstr ""
+
+#: includes/classes/REST/Synonyms.php:55
+msgid "Synonyms in Solr format."
+msgstr ""
+
#: includes/classes/Screen/HealthInfo.php:39
msgid "ElasticPress - Last Sync"
msgstr ""
@@ -2188,47 +2217,49 @@ msgstr ""
msgid "Dashboard"
msgstr ""
-#: includes/dashboard.php:625
+#: includes/dashboard.php:638
msgid "ElasticPress Features"
msgstr ""
-#: includes/dashboard.php:626
+#: includes/dashboard.php:639
#: assets/js/features/index.js:34
#: dist/js/features-script.js:1
msgid "Features"
msgstr ""
-#: includes/dashboard.php:634
+#: includes/dashboard.php:647
msgid "ElasticPress Settings"
msgstr ""
-#: includes/dashboard.php:643
-#: includes/dashboard.php:644
+#: includes/dashboard.php:656
+#: includes/dashboard.php:657
#: assets/js/features/apps/features.js:82
+#: assets/js/synonyms/apps/synonyms-settings.js:61
#: dist/js/features-script.js:1
+#: dist/js/synonyms-script.js:1
msgid "Sync"
msgstr ""
-#: includes/dashboard.php:652
+#: includes/dashboard.php:665
msgid "ElasticPress Index Health"
msgstr ""
-#: includes/dashboard.php:653
+#: includes/dashboard.php:666
#: includes/partials/stats-page.php:35
msgid "Index Health"
msgstr ""
-#: includes/dashboard.php:661
+#: includes/dashboard.php:674
msgid "ElasticPress Status Report"
msgstr ""
-#: includes/dashboard.php:662
+#: includes/dashboard.php:675
#: assets/js/status-report/index.js:21
#: dist/js/status-report-script.js:1
msgid "Status Report"
msgstr ""
-#: includes/dashboard.php:860
+#: includes/dashboard.php:873
msgid "ElasticPress Indexing"
msgstr ""
@@ -2623,8 +2654,11 @@ msgid "Save and sync now"
msgstr ""
#: assets/js/features/apps/features.js:245
+#: assets/js/synonyms/apps/synonyms-settings.js:174
+#: assets/js/synonyms/components/common/edit-panel.js:95
#: assets/js/weighting/apps/weighting.js:59
#: dist/js/features-script.js:1
+#: dist/js/synonyms-script.js:1
#: dist/js/weighting-script.js:1
msgid "Save changes"
msgstr ""
@@ -2681,7 +2715,7 @@ msgstr ""
msgid "Select filters"
msgstr ""
-#: assets/js/instant-results/apps/modal.js:89
+#: assets/js/instant-results/apps/modal.js:100
#: dist/js/instant-results-script.js:1
msgid "Search results"
msgstr ""
@@ -2933,48 +2967,48 @@ msgstr ""
msgid "Copy status report to clipboard"
msgstr ""
-#: assets/js/sync-ui/apps/sync.js:39
+#: assets/js/sync-ui/apps/sync.js:64
#: dist/js/sync-script.js:1
msgid "Sync completed."
msgstr ""
-#: assets/js/sync-ui/apps/sync.js:51
+#: assets/js/sync-ui/apps/sync.js:82
#: dist/js/sync-script.js:1
msgid "Starting delete and sync…"
msgstr ""
-#: assets/js/sync-ui/apps/sync.js:70
+#: assets/js/sync-ui/apps/sync.js:101
#: dist/js/sync-script.js:1
msgid "Starting sync…"
msgstr ""
-#: assets/js/sync-ui/apps/sync.js:80
+#: assets/js/sync-ui/apps/sync.js:111
#: dist/js/sync-script.js:1
msgid "If you are missing data in your search results or have recently added custom content types to your site, you should run a sync to reflect these changes."
msgstr ""
#. translators: %s: Index type. ElasticPress.io or Elasticsearch.
-#: assets/js/sync-ui/apps/sync.js:86
+#: assets/js/sync-ui/apps/sync.js:117
#: dist/js/sync-script.js:1
msgid "Run a sync to index your existing content %s. Once syncing finishes, your site is officially supercharged."
msgstr ""
-#: assets/js/sync-ui/apps/sync.js:91
+#: assets/js/sync-ui/apps/sync.js:122
#: dist/js/sync-script.js:1
msgid "on ElasticPress.io"
msgstr ""
-#: assets/js/sync-ui/apps/sync.js:92
+#: assets/js/sync-ui/apps/sync.js:123
#: dist/js/sync-script.js:1
msgid "in Elasticsearch"
msgstr ""
-#: assets/js/sync-ui/apps/sync.js:109
+#: assets/js/sync-ui/apps/sync.js:140
#: dist/js/sync-script.js:1
msgid "Advanced options"
msgstr ""
-#: assets/js/sync-ui/apps/sync.js:114
+#: assets/js/sync-ui/apps/sync.js:145
#: dist/js/sync-script.js:1
msgid "Sync history"
msgstr ""
@@ -3019,6 +3053,16 @@ msgstr ""
msgid "Learn more about Sync"
msgstr ""
+#: assets/js/sync-ui/components/errors.js:28
+#: dist/js/sync-script.js:1
+msgid "Error type"
+msgstr ""
+
+#: assets/js/sync-ui/components/errors.js:44
+#: dist/js/sync-script.js:1
+msgid "No errors found in the log."
+msgstr ""
+
#: assets/js/sync-ui/components/include.js:52
#: dist/js/sync-script.js:1
msgid "Object IDs"
@@ -3049,28 +3093,28 @@ msgstr ""
msgid "Higher object ID"
msgstr ""
-#: assets/js/sync-ui/components/log.js:59
+#: assets/js/sync-ui/components/log.js:60
#: dist/js/sync-script.js:1
msgid "Copied log to clipboard."
msgstr ""
#: assets/js/sync-ui/components/log.js:71
#: dist/js/sync-script.js:1
-msgid "Full Log"
+msgid "Log"
msgstr ""
#. translators: %d: Error message count.
-#: assets/js/sync-ui/components/log.js:78
+#: assets/js/sync-ui/components/log.js:77
#: dist/js/sync-script.js:1
msgid "Errors (%d)"
msgstr ""
-#: assets/js/sync-ui/components/log.js:107
+#: assets/js/sync-ui/components/log.js:104
#: dist/js/sync-script.js:1
msgid "Clear log"
msgstr ""
-#: assets/js/sync-ui/components/log.js:112
+#: assets/js/sync-ui/components/log.js:109
#: dist/js/sync-script.js:1
msgid "Copy log to clipboard"
msgstr ""
@@ -3155,36 +3199,41 @@ msgstr ""
#: assets/js/sync-ui/components/previous-sync.js:80
#: dist/js/sync-script.js:1
-msgid "Manual sync from Sync Settings."
+msgid "Manual sync following an error in synonyms settings."
msgstr ""
#: assets/js/sync-ui/components/previous-sync.js:82
#: dist/js/sync-script.js:1
+msgid "Manual sync from Sync Settings."
+msgstr ""
+
+#: assets/js/sync-ui/components/previous-sync.js:84
+#: dist/js/sync-script.js:1
msgid "Automatic sync after plugin update."
msgstr ""
#. translators: %1$s Sync date and time. %2%s sync trigger.
-#: assets/js/sync-ui/components/previous-sync.js:113
+#: assets/js/sync-ui/components/previous-sync.js:115
#: dist/js/sync-script.js:1
msgctxt "Sync info"
msgid "%1$s — %2$s"
msgstr ""
#: assets/js/sync-ui/components/progress.js:48
-#: assets/js/sync/index.js:199
+#: assets/js/sync/index.js:238
#: dist/js/sync-script.js:1
msgid "Sync failed"
msgstr ""
#: assets/js/sync-ui/components/progress.js:56
-#: assets/js/sync/index.js:341
-#: assets/js/sync/index.js:517
+#: assets/js/sync/index.js:384
+#: assets/js/sync/index.js:560
#: dist/js/sync-script.js:1
msgid "Sync paused"
msgstr ""
#: assets/js/sync-ui/components/progress.js:60
-#: assets/js/sync/index.js:514
+#: assets/js/sync/index.js:557
#: dist/js/sync-script.js:1
msgid "WP CLI sync in progress"
msgstr ""
@@ -3221,12 +3270,18 @@ msgstr ""
#. translators: %1$s Sync start date and time.
#: assets/js/sync-ui/components/progress.js:97
#: dist/js/sync-script.js:1
-msgid "Started automatically after updating the ElasticPress plugin at %s ."
+msgid "Started manually from an error on the Synonyms Settings page at %s ."
msgstr ""
#. translators: %1$s Sync start date and time.
#: assets/js/sync-ui/components/progress.js:103
#: dist/js/sync-script.js:1
+msgid "Started automatically after updating the ElasticPress plugin at %s ."
+msgstr ""
+
+#. translators: %1$s Sync start date and time.
+#: assets/js/sync-ui/components/progress.js:109
+#: dist/js/sync-script.js:1
msgid "Started on %s ."
msgstr ""
@@ -3251,12 +3306,12 @@ msgid "Sync Settings"
msgstr ""
#. translators: %s: Index type. ElasticPress.io or Elasticsearch.
-#: assets/js/sync/index.js:225
+#: assets/js/sync/index.js:264
#: dist/js/sync-script.js:1
msgid "Your indexing process has been stopped by WP-CLI and your %s index could be missing content. To restart indexing, please click the Start button or use WP-CLI commands to perform the reindex. Please note that search results could be incorrect or incomplete until the reindex finishes."
msgstr ""
-#: assets/js/sync/index.js:233
+#: assets/js/sync/index.js:272
#: dist/js/sync-script.js:1
msgid "Sync interrupted by WP-CLI command."
msgstr ""
@@ -3281,16 +3336,251 @@ msgstr ""
msgid "Unable to parse response. Find troubleshooting steps at https://elasticpress.zendesk.com/hc/en-us/articles/20857557098125/."
msgstr ""
-#: assets/js/weighting/apps/weighting.js:34
-#: dist/js/weighting-script.js:1
-msgid "Settings saved."
+#: assets/js/synonyms/apps/synonyms-settings.js:48
+#: dist/js/synonyms-script.js:1
+msgid "Synonym settings saved."
msgstr ""
+#: assets/js/synonyms/apps/synonyms-settings.js:53
+#: dist/js/synonyms-script.js:1
+msgid "Could not update index with synonyms. Make sure your data is synced."
+msgstr ""
+
+#: assets/js/synonyms/apps/synonyms-settings.js:69
#: assets/js/weighting/apps/weighting.js:36
+#: dist/js/synonyms-script.js:1
#: dist/js/weighting-script.js:1
msgid "Something went wrong. Please try again."
msgstr ""
+#. translators: Synonyms count
+#: assets/js/synonyms/apps/synonyms-settings.js:87
+#: dist/js/synonyms-script.js:1
+msgid "Synonyms (%d)"
+msgstr ""
+
+#. translators: Hyponyms count
+#: assets/js/synonyms/apps/synonyms-settings.js:98
+#: dist/js/synonyms-script.js:1
+msgid "Hyponyms (%d)"
+msgstr ""
+
+#. translators: Replacements count
+#: assets/js/synonyms/apps/synonyms-settings.js:109
+#: dist/js/synonyms-script.js:1
+msgid "Replacements (%d)"
+msgstr ""
+
+#: assets/js/synonyms/apps/synonyms-settings.js:121
+#: dist/js/synonyms-script.js:1
+msgid "Switch to visual editor"
+msgstr ""
+
+#: assets/js/synonyms/apps/synonyms-settings.js:122
+#: dist/js/synonyms-script.js:1
+msgid "Switch to advanced text editor"
+msgstr ""
+
+#: assets/js/synonyms/apps/synonyms-settings.js:126
+#: dist/js/synonyms-script.js:1
+msgid "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."
+msgstr ""
+
+#: assets/js/synonyms/apps/synonyms-settings.js:154
+#: dist/js/synonyms-script.js:1
+msgid "Advanced Synonyms Editor"
+msgstr ""
+
+#: assets/js/synonyms/apps/synonyms-settings.js:158
+#: dist/js/synonyms-script.js:1
+msgid "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."
+msgstr ""
+
+#: assets/js/synonyms/components/common/edit-panel.js:106
+#: dist/js/synonyms-script.js:1
+msgid "Cancel"
+msgstr ""
+
+#: assets/js/synonyms/components/common/list-table.js:148
+#: dist/js/synonyms-script.js:1
+msgid "Delete selected"
+msgstr ""
+
+#: assets/js/synonyms/components/common/row-actions.js:42
+#: dist/js/synonyms-script.js:1
+msgid "Edit"
+msgstr ""
+
+#: assets/js/synonyms/components/common/row-actions.js:50
+#: dist/js/synonyms-script.js:1
+msgid "Delete"
+msgstr ""
+
+#: assets/js/synonyms/components/editors/solr-editor.js:34
+#: dist/js/synonyms-script.js:1
+msgid "Solr synonyms"
+msgstr ""
+
+#: assets/js/synonyms/components/groups/hyponyms.js:26
+#: dist/js/synonyms-script.js:1
+msgid "Hyponyms are terms with a more specific meaning than another more generic terms, called a hypernym . For example, aqua , azure , and cerulean are all hyponyms of blue , their hypernym.
"
+msgstr ""
+
+#: assets/js/synonyms/components/groups/hyponyms.js:32
+#: dist/js/synonyms-script.js:1
+msgid "Use hyponyms when you want search queries for a parent term to return results relevant to itself or any of its child terms, but search queries for a child term to only return results that are relevant to that term. For example, when a search for \"blue\" should return anything blue, whether it be aqua, azure, or cerulean, but a search for \"cerulean\" should return only items that are specifically cerulean blue.
"
+msgstr ""
+
+#: assets/js/synonyms/components/groups/hyponyms.js:40
+#: dist/js/synonyms-script.js:1
+msgid "Add hyponyms"
+msgstr ""
+
+#: assets/js/synonyms/components/groups/hyponyms.js:41
+#: dist/js/synonyms-script.js:1
+msgid "Edit Hyponyms"
+msgstr ""
+
+#: assets/js/synonyms/components/groups/hyponyms.js:42
+#: dist/js/synonyms-script.js:1
+msgid "Add Hyponyms"
+msgstr ""
+
+#: assets/js/synonyms/components/groups/hyponyms.js:43
+#: dist/js/synonyms-script.js:1
+msgid "Hypernym"
+msgstr ""
+
+#: assets/js/synonyms/components/groups/hyponyms.js:44
+#: dist/js/synonyms-script.js:1
+msgid "Hyponyms"
+msgstr ""
+
+#: assets/js/synonyms/components/groups/hyponyms.js:47
+#: dist/js/synonyms-script.js:1
+msgid "Added hyponyms."
+msgstr ""
+
+#: assets/js/synonyms/components/groups/hyponyms.js:48
+#: dist/js/synonyms-script.js:1
+msgid "Deleted hyponyms."
+msgstr ""
+
+#: assets/js/synonyms/components/groups/hyponyms.js:49
+#: dist/js/synonyms-script.js:1
+msgid "Hyponym sets require a hypernym and at least one hyponym that is not the hypernym."
+msgstr ""
+
+#: assets/js/synonyms/components/groups/hyponyms.js:53
+#: dist/js/synonyms-script.js:1
+msgid "Updated hyponyms."
+msgstr ""
+
+#: assets/js/synonyms/components/groups/replacements.js:26
+#: dist/js/synonyms-script.js:1
+msgid "Replacements are terms that replace other incorrect or obsolete terms.
"
+msgstr ""
+
+#: assets/js/synonyms/components/groups/replacements.js:32
+#: dist/js/synonyms-script.js:1
+msgid "Use replacements when you want search queries for certain terms to return results that are only relevant to another term, or set of terms. This can be useful for supporting specific typos or incorrect phrasing. For example, when a search for the phrase \"intensive purposes\" should only return results including the phrase \"intents and purposes\".
"
+msgstr ""
+
+#: assets/js/synonyms/components/groups/replacements.js:40
+#: dist/js/synonyms-script.js:1
+msgid "You may need to disable fuzziness to have it working properly."
+msgstr ""
+
+#: assets/js/synonyms/components/groups/replacements.js:58
+#: dist/js/synonyms-script.js:1
+msgid "Add replacements"
+msgstr ""
+
+#: assets/js/synonyms/components/groups/replacements.js:59
+#: dist/js/synonyms-script.js:1
+msgid "Edit Replacements"
+msgstr ""
+
+#: assets/js/synonyms/components/groups/replacements.js:60
+#: dist/js/synonyms-script.js:1
+msgid "Add Replacements"
+msgstr ""
+
+#: assets/js/synonyms/components/groups/replacements.js:62
+#: dist/js/synonyms-script.js:1
+msgid "Replacements"
+msgstr ""
+
+#: assets/js/synonyms/components/groups/replacements.js:65
+#: dist/js/synonyms-script.js:1
+msgid "Added replacements."
+msgstr ""
+
+#: assets/js/synonyms/components/groups/replacements.js:66
+#: dist/js/synonyms-script.js:1
+msgid "Deleted replacements."
+msgstr ""
+
+#: assets/js/synonyms/components/groups/replacements.js:67
+#: dist/js/synonyms-script.js:1
+msgid "Replacement sets require at least one term and one replacement."
+msgstr ""
+
+#: assets/js/synonyms/components/groups/replacements.js:71
+#: dist/js/synonyms-script.js:1
+msgid "Updated replacements."
+msgstr ""
+
+#: assets/js/synonyms/components/groups/synonyms.js:26
+#: dist/js/synonyms-script.js:1
+msgid "Synonyms are terms with similar meanings. For example, sneaker , tennis shoe , trainer , and running shoe could all refer to a particular type of shoe.
"
+msgstr ""
+
+#: assets/js/synonyms/components/groups/synonyms.js:32
+#: dist/js/synonyms-script.js:1
+msgid "Use synonyms when you want queries for a specific term to also return results relevant to any of its synonyms. This can be useful for supporting products and services whose names have changed over time or regional variations in terminology. For example, when a search for \"sneaker\" should return sneakers, tennis shoes, trainers and running shoes.
"
+msgstr ""
+
+#: assets/js/synonyms/components/groups/synonyms.js:40
+#: dist/js/synonyms-script.js:1
+msgid "Add synonyms"
+msgstr ""
+
+#: assets/js/synonyms/components/groups/synonyms.js:41
+#: dist/js/synonyms-script.js:1
+msgid "Edit Synonyms"
+msgstr ""
+
+#: assets/js/synonyms/components/groups/synonyms.js:42
+#: dist/js/synonyms-script.js:1
+msgid "Add Synonyms"
+msgstr ""
+
+#: assets/js/synonyms/components/groups/synonyms.js:46
+#: dist/js/synonyms-script.js:1
+msgid "Added synonyms."
+msgstr ""
+
+#: assets/js/synonyms/components/groups/synonyms.js:47
+#: dist/js/synonyms-script.js:1
+msgid "Deleted synonyms."
+msgstr ""
+
+#: assets/js/synonyms/components/groups/synonyms.js:48
+#: dist/js/synonyms-script.js:1
+msgid "Synonym sets require at least two synonyms."
+msgstr ""
+
+#: assets/js/synonyms/components/groups/synonyms.js:49
+#: dist/js/synonyms-script.js:1
+msgid "Updated synonyms."
+msgstr ""
+
+#: assets/js/weighting/apps/weighting.js:34
+#: dist/js/weighting-script.js:1
+msgid "Settings saved."
+msgstr ""
+
#: assets/js/weighting/apps/weighting.js:43
#: dist/js/weighting-script.js:1
msgid "This dashboard enables you to select which fields ElasticPress should sync, whether to use those fields in searches, and how heavily to weight fields in the search algorithm. In general, increasing the Weight of a field will increase the relevancy score of a post that has matching text in that field."
@@ -3301,12 +3591,12 @@ msgstr ""
msgid "For example, adding more weight to the title attribute will cause search matches on the post title to appear more prominently."
msgstr ""
-#: assets/js/weighting/components/field.js:52
+#: assets/js/weighting/components/field.js:61
#: dist/js/weighting-script.js:1
msgid "Searchable"
msgstr ""
-#: assets/js/weighting/components/field.js:59
+#: assets/js/weighting/components/field.js:68
#: dist/js/weighting-script.js:1
msgid "Weight"
msgstr ""
@@ -3317,22 +3607,22 @@ msgstr ""
msgid "%s is already being synced."
msgstr ""
-#: assets/js/weighting/components/group.js:177
+#: assets/js/weighting/components/group.js:178
#: dist/js/weighting-script.js:1
msgid "Make sure to Sync after adding new fields to ensure that the fields are synced for any existing content that uses them."
msgstr ""
-#: assets/js/weighting/components/group.js:181
+#: assets/js/weighting/components/group.js:182
#: dist/js/weighting-script.js:1
msgid "Add field"
msgstr ""
-#: assets/js/weighting/components/group.js:184
+#: assets/js/weighting/components/group.js:185
#: dist/js/weighting-script.js:1
msgid "Metadata key"
msgstr ""
-#: assets/js/weighting/components/group.js:188
+#: assets/js/weighting/components/group.js:189
#: dist/js/weighting-script.js:1
msgid "Add"
msgstr ""
diff --git a/package-lock.json b/package-lock.json
index c381fcc2b3..1b6031db9e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "elasticpress",
- "version": "5.0.2",
+ "version": "5.1.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "elasticpress",
- "version": "5.0.2",
+ "version": "5.1.0",
"license": "GPL-2.0-or-later",
"dependencies": {
"@10up/component-tooltip": "^2.0.0",
@@ -35,7 +35,7 @@
"wp-hookdoc": "^0.2.0"
},
"engines": {
- "node": ">=14",
+ "node": ">=18",
"npm": ">=8"
}
},
diff --git a/package.json b/package.json
index fa045ca098..d8bbd3ef6f 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "elasticpress",
- "version": "5.0.2",
+ "version": "5.1.0",
"license": "GPL-2.0-or-later",
"description": "A fast and flexible search and query engine for WordPress.",
"devDependencies": {
@@ -35,7 +35,7 @@
"lint-style": "10up-toolkit lint-style",
"env": "wp-env",
"env:install-tests-cli": "./bin/install-wp-cli.sh tests-wordpress",
- "env:start": "wp-env start && npm run env:install-tests-cli && cd bin/es-docker/ && docker-compose up -d",
+ "env:start": "wp-env start && npm run env:install-tests-cli && cd bin/es-docker/ && docker-compose build --build-arg ES_VERSION=${ES_VERSION-7.10.2} && docker-compose up -d",
"env:stop": "wp-env stop && cd bin/es-docker/ && docker-compose down",
"env:reset": "wp-env clean all && npm run env:start && npm run cypress:setup",
"cypress:setup": "./bin/setup-cypress-env.sh",
@@ -49,7 +49,7 @@
},
"engineStrict": true,
"engines": {
- "node": ">=14",
+ "node": ">=18",
"npm": ">=8"
},
"10up-toolkit": {
diff --git a/readme.txt b/readme.txt
index 8a60c1d764..4a0fc34c5d 100644
--- a/readme.txt
+++ b/readme.txt
@@ -2,7 +2,7 @@
Contributors: 10up, tlovett1, vhauri, tott, felipeelia, oscarssanchez, cmmarslender
Tags: performance, slow, search, elasticsearch, fuzzy, facet, aggregation, searching, autosuggest, suggest, elastic, advanced search, woocommerce, related posts, woocommerce
Tested up to: 6.4
-Stable tag: 5.0.2
+Stable tag: 5.1.0
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
@@ -79,6 +79,54 @@ For sure! Feel free to submit ideas or feedback in general to our [GitHub repo](
== Changelog ==
+= 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).
+* Official support to Elasticsearch 8.x. Props [@felipeelia](https://github.com/felipeelia).
+* 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).
+* [WooCommerce] HPOS compatibility notice for WooCommerce Orders. Props [@felipeelia](https://github.com/felipeelia).
+* [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).
+* 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).
+* Context parameter to the `get_capability()` function. Props [@felipeelia](https://github.com/felipeelia) and [@selim13](https://github.com/selim13).
+* 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).
+* New `ep_weighting_options` filter to modify the weighting dashboard options. Props [@burhandodhy](https://github.com/burhandodhy).
+* New `ep_post_test_meta_value` filter. Props [@felipeelia](https://github.com/felipeelia).
+* New message related to indices limits on ElasticPress.io. Props [@felipeelia](https://github.com/felipeelia).
+
+__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).
+* [Documents] Index CSV and TXT file contents. Props [@felipeelia](https://github.com/felipeelia).
+* [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).
+* Automatically open the error log when a sync completes with errors. Props [@JakePT](https://github.com/JakePT) and [@felipeelia](https://github.com/felipeelia).
+* Aggregations created with the 'aggs' WP_Query parameter, are now retrievable using `$query->query_vars['ep_aggregations']`. Props [@felipeelia](https://github.com/felipeelia).
+* 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).
+* Replaced `lee-dohm/no-response` with `actions/stale` to help with closing no-response/stale issues. Props [@jeffpaul](https://github.com/jeffpaul).
+* Bumped actions/upload-artifact from v3 to v4. Props [@iamdharmesh](https://github.com/iamdharmesh).
+* Required node version. Props [@oscarssanchez](https://github.com/oscarssanchez).
+
+__Fixed:__
+
+* [Autosuggest] Hide the Autosuggest Endpoint URL field for EP.io users. Props [@felipeelia](https://github.com/felipeelia) and [@JakePT](https://github.com/JakePT).
+* [Autosuggest] Google Analytics integration gtag call. Props [@felipeelia](https://github.com/felipeelia) and [@JakePT](https://github.com/JakePT).
+* [Autosuggest] Link click when using a touchpad. Props [@romanberdnikov](https://github.com/romanberdnikov).
+* [Autosuggest] Pressing Enter to select an Autosuggest suggestion would instead open Instant Results. Props [@JakePT](https://github.com/JakePT).
+* [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).
+* [Synonyms] Fix Synonyms case sensitive issue. Props [@burhandodhy](https://github.com/burhandodhy).
+* [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).
+* [WooCommerce] E2e tests. Props [@felipeelia](https://github.com/felipeelia).
+* [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).
+* Inconsistent search results when calling the same function via PHP and Ajax. Props [@burhandodhy](https://github.com/burhandodhy).
+* Unit test related to blog creation. Props [@felipeelia](https://github.com/felipeelia) and [@burhandodhy](https://github.com/burhandodhy).
+* Correct PHPdoc return type for `Elasticsearch::index_document` and related methods. Props [@ictbeheer](https://github.com/ictbeheer).
+* 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).
+
= 5.0.2 - 2024-01-16 =
__Changed:__
diff --git a/tests/cypress/fixtures/csv-file.csv b/tests/cypress/fixtures/csv-file.csv
new file mode 100644
index 0000000000..ee6b058b6d
--- /dev/null
+++ b/tests/cypress/fixtures/csv-file.csv
@@ -0,0 +1,130 @@
+"LatD", "LatM", "LatS", "NS", "LonD", "LonM", "LonS", "EW", "City", "State"
+ 41, 5, 59, "N", 80, 39, 0, "W", "Youngstown", OH
+ 42, 52, 48, "N", 97, 23, 23, "W", "Yankton", SD
+ 46, 35, 59, "N", 120, 30, 36, "W", "Yakima", WA
+ 42, 16, 12, "N", 71, 48, 0, "W", "Worcester", MA
+ 43, 37, 48, "N", 89, 46, 11, "W", "Wisconsin Dells", WI
+ 36, 5, 59, "N", 80, 15, 0, "W", "Winston-Salem", NC
+ 49, 52, 48, "N", 97, 9, 0, "W", "Winnipeg", MB
+ 39, 11, 23, "N", 78, 9, 36, "W", "Winchester", VA
+ 34, 14, 24, "N", 77, 55, 11, "W", "Wilmington", NC
+ 39, 45, 0, "N", 75, 33, 0, "W", "Wilmington", DE
+ 48, 9, 0, "N", 103, 37, 12, "W", "Williston", ND
+ 41, 15, 0, "N", 77, 0, 0, "W", "Williamsport", PA
+ 37, 40, 48, "N", 82, 16, 47, "W", "Williamson", WV
+ 33, 54, 0, "N", 98, 29, 23, "W", "Wichita Falls", TX
+ 37, 41, 23, "N", 97, 20, 23, "W", "Wichita", KS
+ 40, 4, 11, "N", 80, 43, 12, "W", "Wheeling", WV
+ 26, 43, 11, "N", 80, 3, 0, "W", "West Palm Beach", FL
+ 47, 25, 11, "N", 120, 19, 11, "W", "Wenatchee", WA
+ 41, 25, 11, "N", 122, 23, 23, "W", "Weed", CA
+ 31, 13, 11, "N", 82, 20, 59, "W", "Waycross", GA
+ 44, 57, 35, "N", 89, 38, 23, "W", "Wausau", WI
+ 42, 21, 36, "N", 87, 49, 48, "W", "Waukegan", IL
+ 44, 54, 0, "N", 97, 6, 36, "W", "Watertown", SD
+ 43, 58, 47, "N", 75, 55, 11, "W", "Watertown", NY
+ 42, 30, 0, "N", 92, 20, 23, "W", "Waterloo", IA
+ 41, 32, 59, "N", 73, 3, 0, "W", "Waterbury", CT
+ 38, 53, 23, "N", 77, 1, 47, "W", "Washington", DC
+ 41, 50, 59, "N", 79, 8, 23, "W", "Warren", PA
+ 46, 4, 11, "N", 118, 19, 48, "W", "Walla Walla", WA
+ 31, 32, 59, "N", 97, 8, 23, "W", "Waco", TX
+ 38, 40, 48, "N", 87, 31, 47, "W", "Vincennes", IN
+ 28, 48, 35, "N", 97, 0, 36, "W", "Victoria", TX
+ 32, 20, 59, "N", 90, 52, 47, "W", "Vicksburg", MS
+ 49, 16, 12, "N", 123, 7, 12, "W", "Vancouver", BC
+ 46, 55, 11, "N", 98, 0, 36, "W", "Valley City", ND
+ 30, 49, 47, "N", 83, 16, 47, "W", "Valdosta", GA
+ 43, 6, 36, "N", 75, 13, 48, "W", "Utica", NY
+ 39, 54, 0, "N", 79, 43, 48, "W", "Uniontown", PA
+ 32, 20, 59, "N", 95, 18, 0, "W", "Tyler", TX
+ 42, 33, 36, "N", 114, 28, 12, "W", "Twin Falls", ID
+ 33, 12, 35, "N", 87, 34, 11, "W", "Tuscaloosa", AL
+ 34, 15, 35, "N", 88, 42, 35, "W", "Tupelo", MS
+ 36, 9, 35, "N", 95, 54, 36, "W", "Tulsa", OK
+ 32, 13, 12, "N", 110, 58, 12, "W", "Tucson", AZ
+ 37, 10, 11, "N", 104, 30, 36, "W", "Trinidad", CO
+ 40, 13, 47, "N", 74, 46, 11, "W", "Trenton", NJ
+ 44, 45, 35, "N", 85, 37, 47, "W", "Traverse City", MI
+ 43, 39, 0, "N", 79, 22, 47, "W", "Toronto", ON
+ 39, 2, 59, "N", 95, 40, 11, "W", "Topeka", KS
+ 41, 39, 0, "N", 83, 32, 24, "W", "Toledo", OH
+ 33, 25, 48, "N", 94, 3, 0, "W", "Texarkana", TX
+ 39, 28, 12, "N", 87, 24, 36, "W", "Terre Haute", IN
+ 27, 57, 0, "N", 82, 26, 59, "W", "Tampa", FL
+ 30, 27, 0, "N", 84, 16, 47, "W", "Tallahassee", FL
+ 47, 14, 24, "N", 122, 25, 48, "W", "Tacoma", WA
+ 43, 2, 59, "N", 76, 9, 0, "W", "Syracuse", NY
+ 32, 35, 59, "N", 82, 20, 23, "W", "Swainsboro", GA
+ 33, 55, 11, "N", 80, 20, 59, "W", "Sumter", SC
+ 40, 59, 24, "N", 75, 11, 24, "W", "Stroudsburg", PA
+ 37, 57, 35, "N", 121, 17, 24, "W", "Stockton", CA
+ 44, 31, 12, "N", 89, 34, 11, "W", "Stevens Point", WI
+ 40, 21, 36, "N", 80, 37, 12, "W", "Steubenville", OH
+ 40, 37, 11, "N", 103, 13, 12, "W", "Sterling", CO
+ 38, 9, 0, "N", 79, 4, 11, "W", "Staunton", VA
+ 39, 55, 11, "N", 83, 48, 35, "W", "Springfield", OH
+ 37, 13, 12, "N", 93, 17, 24, "W", "Springfield", MO
+ 42, 5, 59, "N", 72, 35, 23, "W", "Springfield", MA
+ 39, 47, 59, "N", 89, 39, 0, "W", "Springfield", IL
+ 47, 40, 11, "N", 117, 24, 36, "W", "Spokane", WA
+ 41, 40, 48, "N", 86, 15, 0, "W", "South Bend", IN
+ 43, 32, 24, "N", 96, 43, 48, "W", "Sioux Falls", SD
+ 42, 29, 24, "N", 96, 23, 23, "W", "Sioux City", IA
+ 32, 30, 35, "N", 93, 45, 0, "W", "Shreveport", LA
+ 33, 38, 23, "N", 96, 36, 36, "W", "Sherman", TX
+ 44, 47, 59, "N", 106, 57, 35, "W", "Sheridan", WY
+ 35, 13, 47, "N", 96, 40, 48, "W", "Seminole", OK
+ 32, 25, 11, "N", 87, 1, 11, "W", "Selma", AL
+ 38, 42, 35, "N", 93, 13, 48, "W", "Sedalia", MO
+ 47, 35, 59, "N", 122, 19, 48, "W", "Seattle", WA
+ 41, 24, 35, "N", 75, 40, 11, "W", "Scranton", PA
+ 41, 52, 11, "N", 103, 39, 36, "W", "Scottsbluff", NB
+ 42, 49, 11, "N", 73, 56, 59, "W", "Schenectady", NY
+ 32, 4, 48, "N", 81, 5, 23, "W", "Savannah", GA
+ 46, 29, 24, "N", 84, 20, 59, "W", "Sault Sainte Marie", MI
+ 27, 20, 24, "N", 82, 31, 47, "W", "Sarasota", FL
+ 38, 26, 23, "N", 122, 43, 12, "W", "Santa Rosa", CA
+ 35, 40, 48, "N", 105, 56, 59, "W", "Santa Fe", NM
+ 34, 25, 11, "N", 119, 41, 59, "W", "Santa Barbara", CA
+ 33, 45, 35, "N", 117, 52, 12, "W", "Santa Ana", CA
+ 37, 20, 24, "N", 121, 52, 47, "W", "San Jose", CA
+ 37, 46, 47, "N", 122, 25, 11, "W", "San Francisco", CA
+ 41, 27, 0, "N", 82, 42, 35, "W", "Sandusky", OH
+ 32, 42, 35, "N", 117, 9, 0, "W", "San Diego", CA
+ 34, 6, 36, "N", 117, 18, 35, "W", "San Bernardino", CA
+ 29, 25, 12, "N", 98, 30, 0, "W", "San Antonio", TX
+ 31, 27, 35, "N", 100, 26, 24, "W", "San Angelo", TX
+ 40, 45, 35, "N", 111, 52, 47, "W", "Salt Lake City", UT
+ 38, 22, 11, "N", 75, 35, 59, "W", "Salisbury", MD
+ 36, 40, 11, "N", 121, 39, 0, "W", "Salinas", CA
+ 38, 50, 24, "N", 97, 36, 36, "W", "Salina", KS
+ 38, 31, 47, "N", 106, 0, 0, "W", "Salida", CO
+ 44, 56, 23, "N", 123, 1, 47, "W", "Salem", OR
+ 44, 57, 0, "N", 93, 5, 59, "W", "Saint Paul", MN
+ 38, 37, 11, "N", 90, 11, 24, "W", "Saint Louis", MO
+ 39, 46, 12, "N", 94, 50, 23, "W", "Saint Joseph", MO
+ 42, 5, 59, "N", 86, 28, 48, "W", "Saint Joseph", MI
+ 44, 25, 11, "N", 72, 1, 11, "W", "Saint Johnsbury", VT
+ 45, 34, 11, "N", 94, 10, 11, "W", "Saint Cloud", MN
+ 29, 53, 23, "N", 81, 19, 11, "W", "Saint Augustine", FL
+ 43, 25, 48, "N", 83, 56, 24, "W", "Saginaw", MI
+ 38, 35, 24, "N", 121, 29, 23, "W", "Sacramento", CA
+ 43, 36, 36, "N", 72, 58, 12, "W", "Rutland", VT
+ 33, 24, 0, "N", 104, 31, 47, "W", "Roswell", NM
+ 35, 56, 23, "N", 77, 48, 0, "W", "Rocky Mount", NC
+ 41, 35, 24, "N", 109, 13, 48, "W", "Rock Springs", WY
+ 42, 16, 12, "N", 89, 5, 59, "W", "Rockford", IL
+ 43, 9, 35, "N", 77, 36, 36, "W", "Rochester", NY
+ 44, 1, 12, "N", 92, 27, 35, "W", "Rochester", MN
+ 37, 16, 12, "N", 79, 56, 24, "W", "Roanoke", VA
+ 37, 32, 24, "N", 77, 26, 59, "W", "Richmond", VA
+ 39, 49, 48, "N", 84, 53, 23, "W", "Richmond", IN
+ 38, 46, 12, "N", 112, 5, 23, "W", "Richfield", UT
+ 45, 38, 23, "N", 89, 25, 11, "W", "Rhinelander", WI
+ 39, 31, 12, "N", 119, 48, 35, "W", "Reno", NV
+ 50, 25, 11, "N", 104, 39, 0, "W", "Regina", SA
+ 40, 10, 48, "N", 122, 14, 23, "W", "Red Bluff", CA
+ 40, 19, 48, "N", 75, 55, 48, "W", "Reading", PA
+ 41, 9, 35, "N", 81, 14, 23, "W", "Ravenna", OH
+
diff --git a/tests/cypress/fixtures/txt-file.txt b/tests/cypress/fixtures/txt-file.txt
new file mode 100644
index 0000000000..9cd0d6d3e7
--- /dev/null
+++ b/tests/cypress/fixtures/txt-file.txt
@@ -0,0 +1,9 @@
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque quis mi at nisl facilisis ultrices. Vivamus efficitur a dolor sed varius. Aenean turpis ipsum, rhoncus sed mi in, tincidunt pharetra ante. Mauris egestas tortor vel metus elementum mollis. In nec pretium velit. Aliquam nibh nisi, fermentum a aliquet nec, ullamcorper ac nisi. Sed et faucibus eros. Fusce est lectus, volutpat in massa eu, venenatis tristique mauris. Cras bibendum, nibh at iaculis vestibulum, lorem ipsum porta justo, eu commodo neque orci a turpis. Sed et erat urna. Fusce euismod lacus lorem, quis suscipit sem tempus et. Curabitur interdum id turpis ac viverra.
+
+Praesent nec mollis dui. Pellentesque sit amet vehicula orci. Proin eros neque, commodo scelerisque nisi vitae, rhoncus pretium enim. Integer convallis tortor sem, nec maximus urna tempus id. Curabitur et neque scelerisque, faucibus elit mattis, aliquam tellus. Quisque sit amet hendrerit lacus, nec semper mi. Fusce pretium nisi at orci vestibulum, non interdum magna malesuada. Cras vestibulum vitae tellus in faucibus. Donec tempor hendrerit dignissim.
+
+Nam nec viverra justo. Quisque convallis dictum dolor, vel pellentesque lectus pharetra a. Nunc purus massa, ultrices sit amet nulla vel, pharetra lacinia tortor. Etiam ac risus non magna fringilla feugiat faucibus sit amet quam. Aliquam erat volutpat. Donec ultricies orci tortor, ac aliquam orci tempor eget. Donec risus dui, suscipit nec ante ac, auctor varius lectus. Ut id lorem ut leo tincidunt dictum. Praesent sapien justo, luctus non dolor vel, sollicitudin imperdiet elit. Etiam eros metus, ornare nec finibus non, consequat id nisl. Morbi molestie in turpis at suscipit. Nam risus felis, fringilla vitae purus nec, ornare fringilla metus.
+
+Sed euismod nisl purus, vel consectetur purus consectetur sed. Suspendisse eget lacus fermentum, convallis lectus eget, laoreet mauris. Morbi id varius neque. In hac habitasse platea dictumst. Duis ut eros aliquet, aliquam enim non, euismod sem. Duis suscipit odio dolor, sit amet viverra orci cursus feugiat. Phasellus scelerisque, justo at auctor eleifend, diam purus fringilla dolor, at malesuada augue risus tristique tortor. Vivamus ac commodo augue. Aliquam sapien mi, interdum ut ipsum nec, sagittis semper lacus. Maecenas magna odio, fringilla tempus purus quis, auctor rutrum diam. Proin blandit velit quis diam convallis facilisis.
+
+Aenean lobortis elit ac posuere imperdiet. Maecenas interdum neque sapien, sollicitudin interdum eros elementum volutpat. Nunc at lorem neque. Pellentesque lacinia hendrerit sapien non commodo. Phasellus sollicitudin diam ac sem ultrices vestibulum. Cras sollicitudin dui id lectus pellentesque lacinia. In eu odio sem. Donec ac lobortis eros, vehicula rutrum orci. Donec tristique massa a iaculis dapibus.
diff --git a/tests/cypress/integration/dashboard-sync.cy.js b/tests/cypress/integration/dashboard-sync.cy.js
index 73c9a43162..e1b14cc765 100644
--- a/tests/cypress/integration/dashboard-sync.cy.js
+++ b/tests/cypress/integration/dashboard-sync.cy.js
@@ -20,6 +20,7 @@ describe('Dashboard Sync', () => {
}
before(() => {
+ cy.deactivatePlugin('sync-error', 'wpCli');
cy.login();
});
@@ -187,4 +188,45 @@ describe('Dashboard Sync', () => {
cy.reload();
cy.contains('.components-checkbox-control', 'Delete all data').should('exist');
});
+
+ it('Should display a list of error types when errors occur during sync', () => {
+ /**
+ * With the error plugin active, an error should appear in the errors tab.
+ */
+ cy.activatePlugin('sync-error', 'wpCli');
+ cy.visitAdminPage('admin.php?page=elasticpress-sync');
+
+ cy.contains('button', 'Log').click();
+ cy.contains('button', 'Errors').click();
+ cy.contains('.ep-sync-errors', 'No errors found in the log.').should('exist');
+
+ /**
+ * Reload the page, so we can check if the Error Log tab is opened by default when an error occurs.
+ */
+ cy.visitAdminPage('admin.php?page=elasticpress-sync');
+ cy.contains('button', 'Start sync').click();
+ cy.get('.ep-sync-errors__table', {
+ timeout: Cypress.config('elasticPressIndexTimeout'),
+ }).should('be.visible');
+ cy.get('.ep-sync-errors tr', { timeout: Cypress.config('elasticPressIndexTimeout') })
+ .contains('Limit of total fields [???] in index [???] has been exceeded')
+ .should('exist');
+ cy.get('.ep-sync-errors tr', { timeout: Cypress.config('elasticPressIndexTimeout') })
+ .contains('Number of posts index errors')
+ .should('not.exist');
+
+ /**
+ * With the error plugin inactive, no errors should appear in the errors tab.
+ */
+ cy.deactivatePlugin('sync-error', 'wpCli');
+ cy.visitAdminPage('admin.php?page=elasticpress-sync');
+
+ cy.contains('button', 'Start sync').click();
+ cy.get('.ep-sync-progress strong', {
+ timeout: Cypress.config('elasticPressIndexTimeout'),
+ }).should('contain.text', 'Sync complete');
+ cy.contains('button', 'Log').click();
+ cy.contains('button', 'Errors').click();
+ cy.contains('.ep-sync-errors', 'No errors found in the log.').should('exist');
+ });
});
diff --git a/tests/cypress/integration/features/autosuggest.cy.js b/tests/cypress/integration/features/autosuggest.cy.js
index f766dd08ea..3e4e248b08 100644
--- a/tests/cypress/integration/features/autosuggest.cy.js
+++ b/tests/cypress/integration/features/autosuggest.cy.js
@@ -4,6 +4,7 @@ describe('Autosuggest Feature', () => {
});
beforeEach(() => {
+ cy.maybeDisableFeature('instant-results');
cy.deactivatePlugin('custom-headers-for-autosuggest', 'wpCli');
});
@@ -102,4 +103,11 @@ describe('Autosuggest Feature', () => {
cy.get('.ep-autosuggest li a').first().click();
cy.url().should('include', 'cypress=foobar');
});
+
+ it('Can select an Autosuggest suggestion even if Instant Results is active', () => {
+ cy.maybeEnableFeature('instant-results');
+ cy.visit('/');
+ cy.get('.wp-block-search__input').type('blog{downArrow}{enter}');
+ cy.url().should('include', 'blog');
+ });
});
diff --git a/tests/cypress/integration/features/custom-results.cy.js b/tests/cypress/integration/features/custom-results.cy.js
index 3ad4ca057c..5fc01e4e29 100644
--- a/tests/cypress/integration/features/custom-results.cy.js
+++ b/tests/cypress/integration/features/custom-results.cy.js
@@ -6,8 +6,8 @@ describe('Custom Results', () => {
`
$ep_pointers = get_posts(
[
- 'post_type' => 'ep-pointer',
- 'per_page' => 999,
+ 'post_type' => 'ep-pointer',
+ 'numberposts' => 999,
]
);
foreach( $ep_pointers as $pointer ) {
@@ -19,7 +19,7 @@ describe('Custom Results', () => {
's' => '${testPost}',
'ep_integrate' => false,
'fields' => 'ids',
- 'per_page' => 999,
+ 'numberposts' => 999,
]
);
foreach( $posts->posts as $post ) {
diff --git a/tests/cypress/integration/features/documents.cy.js b/tests/cypress/integration/features/documents.cy.js
index 26f7627b7e..b8f978f000 100644
--- a/tests/cypress/integration/features/documents.cy.js
+++ b/tests/cypress/integration/features/documents.cy.js
@@ -69,16 +69,29 @@ describe('Documents Feature', () => {
});
});
- it('Can search .pptx', () => {
+ it('Can search .pptx, .txt, and .csv files', () => {
cy.login();
enableDocumentsFeature();
+
uploadFile(
'pptx-file.pptx',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
);
cy.visit('/?s=dummy+slide');
-
cy.get('.hentry').should('contain.text', 'pptx-file');
+
+ cy.wpCli('config set ALLOW_UNFILTERED_UPLOADS true --raw').then(() => {
+ uploadFile('txt-file.txt', 'text/plain');
+ uploadFile('csv-file.csv', 'text/csv');
+
+ cy.visit('/?s=Curabitur+interdum+id+turpis+ac+viverra');
+ cy.get('.hentry').should('contain.text', 'txt-file');
+
+ cy.visit('/?s=Winchester');
+ cy.get('.hentry').should('contain.text', 'csv-file');
+
+ cy.wpCli('config set ALLOW_UNFILTERED_UPLOADS false --raw');
+ });
});
});
diff --git a/tests/cypress/integration/features/instant-results.cy.js b/tests/cypress/integration/features/instant-results.cy.js
index 999c808eb9..d863d62da7 100644
--- a/tests/cypress/integration/features/instant-results.cy.js
+++ b/tests/cypress/integration/features/instant-results.cy.js
@@ -17,6 +17,19 @@ describe('Instant Results Feature', { tags: '@slow' }, () => {
cy.wait('@sidebarsRest');
}
+ /**
+ * Create a Product Search widget.
+ */
+ function createProductSearchWidget() {
+ cy.openWidgetsPage();
+ cy.openBlockInserter();
+ cy.getBlocksList().should('contain.text', 'Product Search'); // Checking if it exists give JS time to process the full list.
+ cy.insertBlock('Product Search');
+ cy.intercept('/wp-json/wp/v2/sidebars/*').as('sidebarsRest');
+ cy.get('.edit-widgets-header__actions button').contains('Update').click();
+ cy.wait('@sidebarsRest');
+ }
+
before(() => {
cy.deactivatePlugin('classic-widgets woocommerce', 'wpCli');
createSearchWidget();
@@ -366,6 +379,62 @@ describe('Instant Results Feature', { tags: '@slow' }, () => {
cy.wait('@apiRequest');
cy.get('.ep-search-suggestion a').should('have.text', 'wordpress');
});
+
+ it('Is possible to set the default post type from a search form', () => {
+ cy.maybeEnableFeature('instant-results');
+
+ createProductSearchWidget();
+
+ /**
+ * If the Post Type filter is in use, entering a new search
+ * term should reset post type the filter.
+ */
+ cy.visitAdminPage('admin.php?page=elasticpress');
+ cy.intercept('/wp-json/elasticpress/v1/features*').as('apiRequest');
+ cy.contains('button', 'Instant Results').click();
+ cy.get('.components-form-token-field__input').type(
+ '{backspace}{backspace}{backspace}post type{downArrow}{enter}{esc}',
+ );
+ cy.contains('button', 'Save changes').click();
+ cy.wait('@apiRequest');
+
+ cy.visit('/');
+ cy.intercept('*search=heavy*').as('apiRequest');
+ cy.get('.wc-block-product-search,.wp-block-search').last().as('productSearchBlock');
+ cy.get('@productSearchBlock').find('input[type="search"]').type('heavy{enter}');
+ cy.get('.ep-search-modal').should('be.visible');
+ cy.wait('@apiRequest');
+ cy.url().should('include', 'post_type=product');
+ cy.get('.ep-search-input').type(' duty');
+ cy.wait(300); // eslint-disable-line
+ cy.wait('@apiRequest');
+ cy.url().should('not.include', 'post_type=product');
+
+ /**
+ * If the Post Type filter is not in use, entering a new search
+ * term should not reset the post type filter.
+ */
+ cy.visitAdminPage('admin.php?page=elasticpress');
+ cy.intercept('/wp-json/elasticpress/v1/features*').as('apiRequest');
+ cy.contains('button', 'Instant Results').click();
+ cy.contains('.components-form-token-field__token', 'Post type')
+ .find('button')
+ .click();
+ cy.contains('button', 'Save changes').click();
+ cy.wait('@apiRequest');
+
+ cy.visit('/');
+ cy.intercept('*search=ergo*').as('apiRequest');
+ cy.get('.wc-block-product-search,.wp-block-search').last().as('productSearchBlock');
+ cy.get('@productSearchBlock').find('input[type="search"]').type('heavy{enter}');
+ cy.get('.ep-search-modal').should('be.visible');
+ cy.wait('@apiRequest');
+ cy.url().should('include', 'post_type=product');
+ cy.get('.ep-search-input').type(' duty');
+ cy.wait(300); // eslint-disable-line
+ cy.wait('@apiRequest');
+ cy.url().should('include', 'post_type=product');
+ });
});
it('Is possible to filter the arguments schema', () => {
diff --git a/tests/cypress/integration/features/search/synonyms.cy.js b/tests/cypress/integration/features/search/synonyms.cy.js
index c107237755..c52f88e809 100644
--- a/tests/cypress/integration/features/search/synonyms.cy.js
+++ b/tests/cypress/integration/features/search/synonyms.cy.js
@@ -1,148 +1,466 @@
describe('Post Search Feature - Synonyms Functionality', () => {
- const word1 = 'authenticity';
- const word2 = 'credibility';
+ /**
+ * Save synonyms settings.
+ */
+ function saveSynonyms() {
+ cy.intercept('/wp-json/elasticpress/v1/synonyms*').as('apiRequest');
+ cy.contains('button', 'Save changes').click();
+ cy.wait('@apiRequest');
+ cy.contains('Synonym settings saved.').should('exist');
+ }
+ /**
+ * Delete synonyms recreate test posts before running tests.
+ */
before(() => {
- cy.wpCli("wp post list --post_type='ep-synonym' --format=ids", true).then(
- (wpCliResponse) => {
- if (wpCliResponse.code === 0) {
- cy.wpCli(`wp post delete ${wpCliResponse.stdout} --force`, true);
- }
- },
- );
- cy.wpCli(
- "wp post list --s='Testing Synonyms' --ep_integrate='false' --format=ids",
- true,
- ).then((wpCliResponse) => {
- if (wpCliResponse.code === 0) {
- cy.wpCli(`wp post delete ${wpCliResponse.stdout} --force`, true);
+ cy.wpCliEval(
+ `
+ $ep_synonyms_tests = get_posts(
+ [
+ 'post_type' => 'any',
+ 'meta_key' => '_synonyms_tests',
+ 'meta_value' => 1,
+ 'numberposts' => 999,
+ ]
+ );
+ foreach( $ep_synonyms_tests as $test ) {
+ wp_delete_post( $test->ID, true );
}
- });
-
- cy.login();
- const postsData = [
- {
- title: `Testing Synonyms - ${word1}`,
- },
- {
- title: `Testing Synonyms - ${word2}`,
- },
- ];
- postsData.forEach((postData) => {
- cy.publishPost(postData);
- });
+ $posts = [
+ 'Plugin',
+ 'Extension',
+ 'Module',
+ 'ElasticPress',
+ 'Safe Redirect Manager',
+ 'Bandeirole',
+ 'Flag',
+ 'Banner',
+ 'Red',
+ 'Carmine',
+ 'Cordovan',
+ 'Crimson',
+ ];
+ foreach ( $posts as $post ) {
+ wp_insert_post(
+ [
+ 'post_title' => $post,
+ 'post_content' => '',
+ 'post_status' => 'publish',
+ 'meta_input' => [
+ '_synonyms_tests' => 1,
+ ],
+ ]
+ );
+ }`,
+ );
});
+
+ /**
+ * Log in before each test.
+ */
beforeEach(() => {
cy.login();
- });
- it('Can create, search, and delete synonyms sets', () => {
- // Add the set
- cy.visitAdminPage('admin.php?page=elasticpress-synonyms');
- cy.get('.synonym-sets-editor').within(() => {
- cy.get('.synonym__remove').click();
- cy.contains('.button', 'Add Set').click();
- cy.get('.components-form-token-field__input').type(`${word1}{enter}${word2}{enter}`);
- });
- cy.get('#synonym-root .button-primary').click();
-
- // Check if it works
- cy.visit(`/?s=${word2}`);
- cy.contains('.site-content article h2', word1).should('exist');
-
- // Remove the set
- cy.visitAdminPage('admin.php?page=elasticpress-synonyms');
- cy.get('.synonym-sets-editor .synonym__remove').click();
- cy.get('#synonym-root .button-primary').click();
+ cy.wpCliEval(
+ `
+ $ep_synonyms = get_posts(
+ [
+ 'post_type' => 'ep-synonym',
+ 'post_status' => 'any',
+ 'numberposts' => 999,
+ ]
+ );
+ foreach( $ep_synonyms as $synonym ) {
+ wp_delete_post( $synonym->ID, true );
+ }`,
+ );
- // Check if it works
- cy.visit(`/?s=${word2}`);
- cy.contains('.site-content article h2', word1).should('not.exist');
- });
- it('Can create, search, and delete synonyms alternatives', () => {
- // Add the set
- cy.visitAdminPage('admin.php?page=elasticpress-synonyms');
- cy.get('.synonym-alternatives-editor').within(() => {
- cy.get('.synonym__remove').click();
- cy.contains('.button', 'Add Alternative').click();
- cy.get('.ep-synonyms__input').type(word1);
- cy.get('.components-form-token-field__input').type(`${word2}{enter}`);
- });
- cy.get('#synonym-root .button-primary').click();
-
- // Check if it works
- cy.visit(`/?s=${word1}`);
- cy.contains('.site-content article h2', word2).should('exist');
- cy.visit(`/?s=${word2}`);
- cy.contains('.site-content article h2', word1).should('not.exist');
-
- // Remove the set
+ /**
+ * Save synonyms settings.
+ */
cy.visitAdminPage('admin.php?page=elasticpress-synonyms');
- cy.get('.synonym-alternatives-editor .synonym__remove').click();
- cy.get('#synonym-root .button-primary').click();
+ saveSynonyms();
+ });
+
+ /**
+ * Test that synonyms work as expected.
+ */
+ it('Is possible to create, edit, and delete synonym rules', () => {
+ /**
+ * Confirm that only results with our search term are returned.
+ */
+ cy.visit('/?s=plugin');
+ cy.contains('article h2', 'Plugin').should('exist');
+ cy.contains('article h2', 'Extension').should('not.exist');
+ cy.contains('article h2', 'Module').should('not.exist');
+
+ /**
+ * Enter a synonym.
+ */
+ cy.visit('/wp-admin/admin.php?page=elasticpress-synonyms');
+ cy.get('.ep-synonyms-edit-panel').as('panel');
+ cy.get('@panel').contains('Add Synonyms').should('exist');
+ cy.get('@panel').find('input[type="text"]').as('input').type('plugin,');
+
+ /**
+ * Add button should be disabled when there's only one synonym.
+ */
+ cy.get('@panel').contains('button', 'Add synonyms').as('add').should('be.disabled');
+
+ /**
+ * Enter another synonym and submit.
+ */
+ cy.get('@input').type('extension,');
+ cy.get('@add').click();
+
+ /**
+ * The synonyms should appear in the list.
+ */
+ cy.contains('.ep-synonyms-list-table tr', 'plugin, extension').should('exist');
+
+ saveSynonyms();
- // Check if it works
- cy.visit(`/?s=${word1}`);
- cy.contains('.site-content article h2', word2).should('not.exist');
+ /**
+ * Results should reflect the synonym rules.
+ */
+ cy.visit('/?s=plugin');
+ cy.contains('article h2', 'Plugin').should('exist');
+ cy.contains('article h2', 'Extension').should('exist');
+ cy.contains('article h2', 'Module').should('not.exist');
+
+ /**
+ * It should be possible to edit synonym rules.
+ */
+ cy.visit('/wp-admin/admin.php?page=elasticpress-synonyms');
+ cy.contains('.ep-synonyms-list-table tr', 'plugin, extension').as('row');
+ cy.get('@row').find('button[aria-label="Edit"]').click();
+ cy.get('.ep-synonyms-edit-panel').as('panel');
+ cy.get('@panel').contains('Edit Synonyms').should('exist');
+ cy.get('@panel').find('input').type('{backspace}module,');
+ cy.get('@panel').contains('button', 'Save changes').click();
+ cy.contains('.ep-synonyms-list-table tr', 'plugin, module').should('exist');
+
+ saveSynonyms();
+
+ /**
+ * Results should reflect the new synonyms.
+ */
+ cy.visit('/?s=plugin');
+ cy.contains('article h2', 'Plugin').should('exist');
+ cy.contains('article h2', 'Extension').should('not.exist');
+ cy.contains('article h2', 'Module').should('exist');
+
+ /**
+ * In the advanced editor, synonyms should be represented as expected.
+ */
+ cy.visit('/wp-admin/admin.php?page=elasticpress-synonyms');
+ cy.contains('button', 'Switch to advanced text editor').click();
+ cy.get('textarea').should('contain', 'plugin, module');
+
+ /**
+ * It should be possible to delete synonym rules.
+ */
+ cy.contains('button', 'Switch to visual editor').click();
+ cy.contains('.ep-synonyms-list-table tr', 'plugin, module').as('row');
+ cy.get('@row').find('button[aria-label="Delete"]').click();
+ cy.contains('.ep-synonyms-list-table tr', 'plugin').should('not.exist');
+
+ saveSynonyms();
+
+ /**
+ * Results should reflect the deleted synonyms.
+ */
+ cy.visit('/?s=plugin');
+ cy.contains('article h2', 'Plugin').should('exist');
+ cy.contains('article h2', 'Extension').should('not.exist');
+ cy.contains('article h2', 'Module').should('not.exist');
});
- it('Can use the Advanced Text Editor', () => {
- cy.visitAdminPage('admin.php?page=elasticpress-synonyms');
- cy.contains('.page-title-action', 'Switch to Advanced Text Editor').click();
- cy.get('#ep-synonym-input').clearThenType(`{enter}
- {enter}
- {enter}
- {enter}
- {enter}
- {enter}
- {enter}
- {enter}
- {enter}
- {enter}
- {enter}
- foo => bar
- test =>
- list,of,words
- `);
- cy.get('#synonym-root .button-primary').click();
-
- cy.contains(
- '.synonym-solr-editor__validation',
- 'Alternatives must have both a primary term and at least one alternative term.',
- ).should('exist');
-
- cy.get('#ep-synonym-input').clearThenType('foo => bar{enter}list,of,words');
- cy.get('#synonym-root .button-primary').click();
- cy.contains('.notice-success', 'Successfully updated synonym filter.').should('exist');
-
- cy.contains('.page-title-action', 'Switch to Visual Editor').click();
- cy.contains('.synonym-set-editor .components-form-token-field span', 'list').should(
- 'exist',
- );
- cy.contains('.synonym-set-editor .components-form-token-field span', 'of').should('exist');
- cy.contains('.synonym-set-editor .components-form-token-field span', 'words').should(
- 'exist',
- );
- cy.get('.synonym-alternative-editor input[value="foo"]').should('exist');
- cy.contains('.synonym-alternative-editor .components-form-token-field span', 'bar').should(
- 'exist',
+
+ /**
+ * Test that hyponyms work as expected.
+ */
+ it('Is possible to create, edit, and delete hyponym rules', () => {
+ /**
+ * Confirm that only results with our search term are returned.
+ */
+ cy.visit('/?s=plugin');
+ cy.contains('article h2', 'Plugin').should('exist');
+ cy.contains('article h2', 'ElasticPress').should('not.exist');
+ cy.contains('article h2', 'Safe Redirect Manager').should('not.exist');
+
+ /**
+ * Enter a hypernym.
+ */
+ cy.visit('/wp-admin/admin.php?page=elasticpress-synonyms');
+ cy.contains('button', 'Hyponyms').click();
+ cy.get('.ep-synonyms-edit-panel').as('panel');
+ cy.get('@panel').contains('Add Hyponyms').should('exist');
+ cy.get('@panel').find('input[type="text"]').eq(0).type('plugin');
+
+ /**
+ * Add button should be disabled when there's no hyponyms.
+ */
+ cy.get('@panel').contains('button', 'Add hyponyms').as('add').should('be.disabled');
+
+ /**
+ * Enter a hyponym and submit.
+ */
+ cy.get('@panel').find('input[type="text"]').eq(1).type('ElasticPress,');
+ cy.get('@add').click();
+
+ /**
+ * The rule should appear in the list,
+ */
+ cy.contains('.ep-synonyms-list-table tr', 'plugin').should('exist');
+
+ saveSynonyms();
+
+ /**
+ * Results should reflect the hyponym rules.
+ */
+ cy.visit('/?s=plugin');
+ cy.contains('article h2', 'Plugin').should('exist');
+ cy.contains('article h2', 'ElasticPress').should('exist');
+ cy.contains('article h2', 'Safe Redirect Manager').should('not.exist');
+
+ cy.visit('/?s=elasticpress');
+ cy.contains('article h2', 'Plugin').should('not.exist');
+ cy.contains('article h2', 'ElasticPress').should('exist');
+ cy.contains('article h2', 'Safe Redirect Manager').should('not.exist');
+
+ cy.visit('/?s=redirect');
+ cy.contains('article h2', 'Plugin').should('not.exist');
+ cy.contains('article h2', 'ElasticPress').should('not.exist');
+ cy.contains('article h2', 'Safe Redirect Manager').should('exist');
+
+ /**
+ * It should be possible to edit hyponym rules.
+ */
+ cy.visit('/wp-admin/admin.php?page=elasticpress-synonyms');
+ cy.contains('button', 'Hyponyms').click();
+ cy.contains('.ep-synonyms-list-table tr', 'plugin').as('row');
+ cy.get('@row').find('button[aria-label="Edit"]').click();
+ cy.get('.ep-synonyms-edit-panel').as('panel');
+ cy.get('@panel').contains('Edit Hyponyms').should('exist');
+ cy.get('@panel').find('input').eq(1).type('Safe Redirect Manager,');
+ cy.get('@panel').contains('button', 'Save changes').click();
+ cy.get('@row')
+ .contains('td', 'ElasticPress, Safe Redirect Manager')
+ .should('exist')
+ .should('not.contain', 'plugin');
+
+ saveSynonyms();
+
+ /**
+ * Results should reflect the new hyponyms.
+ */
+ cy.visit('/?s=plugin');
+ cy.contains('article h2', 'Plugin').should('exist');
+ cy.contains('article h2', 'ElasticPress').should('exist');
+ cy.contains('article h2', 'Safe Redirect Manager').should('exist');
+
+ cy.visit('/?s=elasticpress');
+ cy.contains('article h2', 'Plugin').should('not.exist');
+ cy.contains('article h2', 'ElasticPress').should('exist');
+ cy.contains('article h2', 'Safe Redirect Manager').should('not.exist');
+
+ cy.visit('/?s=redirect');
+ cy.contains('article h2', 'Plugin').should('not.exist');
+ cy.contains('article h2', 'ElasticPress').should('not.exist');
+ cy.contains('article h2', 'Safe Redirect Manager').should('exist');
+
+ /**
+ * In the advanced editor, hyponyms should be represented as
+ * replacements where the hypernym is also included as a replacement.
+ */
+ cy.visit('/wp-admin/admin.php?page=elasticpress-synonyms');
+ cy.contains('button', 'Switch to advanced text editor').click();
+ cy.get('textarea').should(
+ 'contain',
+ 'plugin => plugin, ElasticPress, Safe Redirect Manager',
);
+
+ /**
+ * It should be possible to delete hyponym rules.
+ */
+ cy.contains('button', 'Switch to visual editor').click();
+ cy.contains('button', 'Hyponyms').click();
+ cy.contains('.ep-synonyms-list-table tr', 'plugin').as('row');
+ cy.get('@row').find('button[aria-label="Delete"]').click();
+ cy.contains('.ep-synonyms-list-table tr', 'plugin').should('not.exist');
+
+ saveSynonyms();
+
+ /**
+ * Results should not longer reflect the deleted rule.
+ */
+ cy.visit('/?s=plugin');
+ cy.contains('article h2', 'Plugin').should('exist');
+ cy.contains('article h2', 'ElasticPress').should('not.exist');
+ cy.contains('article h2', 'Safe Redirect Manager').should('not.exist');
+
+ cy.visit('/?s=elasticpress');
+ cy.contains('article h2', 'Plugin').should('not.exist');
+ cy.contains('article h2', 'ElasticPress').should('exist');
+ cy.contains('article h2', 'Safe Redirect Manager').should('not.exist');
+
+ cy.visit('/?s=redirect');
+ cy.contains('article h2', 'Plugin').should('not.exist');
+ cy.contains('article h2', 'ElasticPress').should('not.exist');
+ cy.contains('article h2', 'Safe Redirect Manager').should('exist');
});
- it('Can preserve synonyms if a sync is performed', () => {
- cy.visitAdminPage('admin.php?page=elasticpress-synonyms');
- cy.get('.page-title-action').then(($button) => {
- if ($button.text() === 'Switch to Advanced Text Editor') {
- $button.click();
- }
- });
- cy.get('#ep-synonym-input').clearThenType('foo => bar{enter}list,of,words');
- cy.get('#synonym-root .button-primary').click();
+ /**
+ * Test that replacements work as expected.
+ */
+ it('Is possible to create, edit, and delete replacement rules', () => {
+ cy.activatePlugin('disable-fuzziness', 'wpCli');
- cy.wpCli('elasticpress sync --setup --yes');
+ /**
+ * Confirm that our replacements are not returned yet.
+ */
+ cy.visit('/?s=bandeirole');
+ cy.contains('article h2', 'Bandeirole').should('exist');
+ cy.contains('article h2', 'Flag').should('not.exist');
+ cy.contains('article h2', 'Banner').should('not.exist');
- cy.visitAdminPage('admin.php?page=elasticpress-synonyms');
- cy.get('#ep-synonym-input')
- .should('contain', 'foo => bar')
- .should('contain', 'list, of, words');
+ /**
+ * Enter a term.
+ */
+ cy.visit('/wp-admin/admin.php?page=elasticpress-synonyms');
+ cy.contains('button', 'Replacements').click();
+ cy.get('.ep-synonyms-edit-panel').as('panel');
+ cy.get('@panel').contains('Add Replacements').should('exist');
+ cy.get('@panel').find('input[type="text"]').eq(0).type('bandeirole,');
+
+ /**
+ * Add button should be disabled when there's no replacements.
+ */
+ cy.get('@panel').contains('button', 'Add replacements').as('add').should('be.disabled');
+
+ /**
+ * Enter a replacement and submit.
+ */
+ cy.get('@panel').find('input[type="text"]').eq(1).type('flag,');
+ cy.get('@add').click();
+
+ /**
+ * The replacements should appear in the list.
+ */
+ cy.contains('.ep-synonyms-list-table tr', 'bandeirole').should('exist');
+
+ saveSynonyms();
+
+ /**
+ * Results should reflect the replacement rules.
+ */
+ cy.visit('/?s=bandeirole');
+ cy.contains('article h2', 'Bandeirole').should('not.exist');
+ cy.contains('article h2', 'Flag').should('exist');
+ cy.contains('article h2', 'Banner').should('not.exist');
+
+ /**
+ * It should be possible to edit replacement rules.
+ */
+ cy.visit('/wp-admin/admin.php?page=elasticpress-synonyms');
+ cy.contains('button', 'Replacements').click();
+ cy.contains('.ep-synonyms-list-table tr', 'bandeirole').as('row');
+ cy.get('@row').find('button[aria-label="Edit"]').click();
+ cy.get('.ep-synonyms-edit-panel').as('panel');
+ cy.get('@panel').contains('Edit Replacements').should('exist');
+ cy.get('@panel').find('input').eq(1).type('banner,');
+ cy.get('@panel').contains('button', 'Save changes').click();
+ cy.contains('.ep-synonyms-list-table tr', 'flag, banner').should('exist');
+
+ saveSynonyms();
+
+ /**
+ * Results should reflect the new replacements.
+ */
+ cy.visit('/?s=bandeirole');
+ cy.contains('article h2', 'Bandeirole').should('not.exist');
+ cy.contains('article h2', 'Flag').should('exist');
+ cy.contains('article h2', 'Banner').should('exist');
+
+ /**
+ * In the advanced editor, replacements hould be represented as
+ * expected.
+ */
+ cy.visit('/wp-admin/admin.php?page=elasticpress-synonyms');
+ cy.contains('button', 'Switch to advanced text editor').click();
+ cy.get('textarea').should('contain', 'bandeirole => flag, banner');
+
+ /**
+ * It should be possible to delete replacement rules.
+ */
+ cy.contains('button', 'Switch to visual editor').click();
+ cy.contains('button', 'Replacements').click();
+ cy.contains('.ep-synonyms-list-table tr', 'bandeirole').as('row');
+ cy.get('@row').find('button[aria-label="Delete"]').click();
+ cy.contains('.ep-synonyms-list-table tr', 'bandeirole').should('not.exist');
+
+ saveSynonyms();
+
+ /**
+ * Results should not longer reflect the deleted rule.
+ */
+ cy.visit('/?s=bandeirole');
+ cy.contains('article h2', 'Bandeirole').should('exist');
+ cy.contains('article h2', 'Flag').should('not.exist');
+ cy.contains('article h2', 'Banner').should('not.exist');
+
+ cy.deactivatePlugin('disable-fuzziness', 'wpCli');
+ });
+
+ /**
+ * Test the advanced text editor.
+ */
+ it('Is possible to edit rules using the text editor', () => {
+ /**
+ * Our rule should not be reflected in results yet.
+ */
+ cy.visit('/?s=red');
+ cy.contains('article h2', 'Red').should('exist');
+ cy.contains('article h2', 'Carmine').should('not.exist');
+ cy.contains('article h2', 'Cordovan').should('not.exist');
+ cy.contains('article h2', 'Crimson').should('not.exist');
+
+ /**
+ * Add a hyponym rule to the text editor.
+ */
+ cy.visit('/wp-admin/admin.php?page=elasticpress-synonyms');
+ cy.contains('button', 'Switch to advanced text editor').click();
+ cy.get('textarea').type('red => red, carmine, cordovan, crimson');
+
+ saveSynonyms();
+
+ /**
+ * Our rule should be reflected in results.
+ */
+ cy.visit('/?s=red');
+ cy.contains('article h2', 'Red').should('exist');
+ cy.contains('article h2', 'Carmine').should('exist');
+ cy.contains('article h2', 'Cordovan').should('exist');
+ cy.contains('article h2', 'Crimson').should('exist');
+
+ cy.visit('/?s=carmine');
+ cy.contains('article h2', 'Red').should('not.exist');
+ cy.contains('article h2', 'Carmine').should('exist');
+ cy.contains('article h2', 'Cordovan').should('not.exist');
+ cy.contains('article h2', 'Crimson').should('not.exist');
+
+ /**
+ * The settings page should remember that we used the text editor.
+ */
+ cy.visit('/wp-admin/admin.php?page=elasticpress-synonyms');
+ cy.get('textarea').should('exist');
+
+ /**
+ * Our rule should be visible under Hyponyms when we switch to the
+ * visual editor.
+ */
+ cy.contains('button', 'Switch to visual editor').click();
+ cy.contains('button', 'Hyponyms').click();
+ cy.contains('.ep-synonyms-list-table tr', 'carmine, cordovan, crimson').should('exist');
});
});
diff --git a/tests/cypress/integration/features/woocommerce.cy.js b/tests/cypress/integration/features/woocommerce.cy.js
index d1292bef68..4e02d71bf7 100644
--- a/tests/cypress/integration/features/woocommerce.cy.js
+++ b/tests/cypress/integration/features/woocommerce.cy.js
@@ -169,7 +169,7 @@ describe('WooCommerce Feature', { tags: '@slow' }, () => {
cy.get('#billing-address_1, #billing_address_1').type(userData.address);
cy.get('#billing-city, #billing_city').type(userData.city);
cy.get('#billing-postcode, #billing_postcode').type(userData.postCode);
- cy.get('#billing-phone, #billing_phone').type(userData.phoneNumber);
+ cy.get('#billing-phone, #billing_phone').type(userData.phoneNumber, { force: true }); // Label covers it
cy.get('#email, #billing_email').clearThenType(userData.email);
/**
diff --git a/tests/cypress/support/commands/block-editor.js b/tests/cypress/support/commands/block-editor.js
index cc0fa84629..0c6754f3ad 100644
--- a/tests/cypress/support/commands/block-editor.js
+++ b/tests/cypress/support/commands/block-editor.js
@@ -4,10 +4,14 @@ Cypress.Commands.add('openBlockSettingsSidebar', () => {
cy.get('body').then(($el) => {
if ($el.hasClass('widgets-php')) {
cy.get('.edit-widgets-header__actions button[aria-label="Settings"]').click();
- cy.get('.edit-widgets-sidebar__panel-tab').contains('Block').click();
+ cy.get('.edit-widgets-sidebar__panel-tab,.edit-widgets-sidebar__panel-tabs button')
+ .contains('Block')
+ .click();
} else {
cy.get('.edit-post-header__settings button[aria-label="Settings"]').click();
- cy.get('.edit-post-sidebar__panel-tab').contains('Block').click();
+ cy.get('.edit-post-sidebar__panel-tab,.edit-post-sidebar__panel-tabs button')
+ .contains('Block')
+ .click();
}
});
});
@@ -21,7 +25,9 @@ Cypress.Commands.add('openBlockInserter', () => {
if ($body.hasClass('widgets-php')) {
cy.get('.edit-widgets-header-toolbar__inserter-toggle').click();
} else {
- cy.get('.edit-post-header-toolbar__inserter-toggle').click();
+ cy.get(
+ '.edit-post-header-toolbar__inserter-toggle,.editor-document-tools__inserter-toggle',
+ ).click();
}
});
});
diff --git a/tests/cypress/wordpress-files/test-plugins/disable-fuzziness.php b/tests/cypress/wordpress-files/test-plugins/disable-fuzziness.php
new file mode 100644
index 0000000000..39851ebc66
--- /dev/null
+++ b/tests/cypress/wordpress-files/test-plugins/disable-fuzziness.php
@@ -0,0 +1,11 @@
+ META_COUNT );
+
+add_filter(
+ 'ep_prepare_meta_data',
+ function ( $post_meta, $post ) {
+ if ( 0 === $post->ID % 2 ) {
+ for ( $i = 0; $i < META_COUNT; $i++ ) {
+ $post_meta[ "test_meta_{$i}_title_{$post->ID}" ] = 'Lorem';
+ $post_meta[ "test_meta_{$i}_body_{$post->ID}" ] = 'Ipsum';
+ }
+ }
+ return $post_meta;
+ },
+ 10,
+ 2
+);
+
+add_filter(
+ 'ep_prepare_meta_allowed_protected_keys',
+ function ( $allowed_meta, $post ) {
+ for ( $i = 0; $i < META_COUNT; $i++ ) {
+ $allowed_meta[] = "test_meta_{$i}_title_{$post->ID}";
+ $allowed_meta[] = "test_meta_{$i}_body_{$post->ID}";
+ }
+
+ return $allowed_meta;
+ },
+ 10,
+ 2
+);
diff --git a/tests/cypress/wordpress-files/test-plugins/unsupported-elasticsearch-version.php b/tests/cypress/wordpress-files/test-plugins/unsupported-elasticsearch-version.php
index 9bc43a6dfd..182f24c664 100644
--- a/tests/cypress/wordpress-files/test-plugins/unsupported-elasticsearch-version.php
+++ b/tests/cypress/wordpress-files/test-plugins/unsupported-elasticsearch-version.php
@@ -18,6 +18,6 @@ function() {
add_filter(
'ep_elasticsearch_version',
function() {
- return '8.0';
+ return '9.0';
}
);
diff --git a/tests/php/TestDashboard.php b/tests/php/TestDashboard.php
index 641f4f17b1..5886cba8f7 100644
--- a/tests/php/TestDashboard.php
+++ b/tests/php/TestDashboard.php
@@ -111,7 +111,14 @@ public function test_use_language_in_setting_for_stop() {
* @group dashboard
*/
public function test_use_language_in_setting_for_multisite() {
- $site_pt_br = wp_insert_site(
+ $enable_pt_br = function ( $languages ) {
+ $languages[] = 'pt_BR';
+ return $languages;
+ };
+ add_filter( 'get_available_languages', $enable_pt_br );
+
+ $site_factory = new \WP_UnitTest_Factory_For_Blog();
+ $site_pt_br = $site_factory->create(
[
'domain' => 'example.org',
'path' => '/pt_BR',
@@ -120,7 +127,7 @@ public function test_use_language_in_setting_for_multisite() {
],
]
);
- $site_he_il = wp_insert_site(
+ $site_he_il = $site_factory->create(
[
'domain' => 'example.org',
'path' => '/he_IL',
diff --git a/tests/php/TestElasticsearchErrorInterpreter.php b/tests/php/TestElasticsearchErrorInterpreter.php
index 12a6271027..1072460feb 100644
--- a/tests/php/TestElasticsearchErrorInterpreter.php
+++ b/tests/php/TestElasticsearchErrorInterpreter.php
@@ -132,4 +132,23 @@ public function test_maybe_suggest_solution_for_es_limit_fields() {
$this->assertSame( 'Limit of total fields [???] in index [???] has been exceeded', $suggested['error'] );
$this->assertSame( $solution, $suggested['solution'] );
}
+
+ /**
+ * Test the `maybe_suggest_solution_for_es` method when the indices limit was reached on EP.io
+ *
+ * @since 5.1.0
+ * @group elasticsearch-error-interpreter
+ */
+ public function test_maybe_suggest_solution_for_es_limit_of_indices() {
+ $this->force_epio();
+
+ $error_interpreter = new ElasticsearchErrorInterpreter();
+
+ $error = 'It seems you have reached the limit of indices your plan supports and we were not able to create a new index. Currently, you can have up to 3 indices.';
+ $solution = 'Please refer to this article outlining how to address this issue.';
+ $suggested = $error_interpreter->maybe_suggest_solution_for_es( $error );
+
+ $this->assertSame( $error, $suggested['error'] );
+ $this->assertSame( $solution, $suggested['solution'] );
+ }
}
diff --git a/tests/php/TestStats.php b/tests/php/TestStats.php
index 553f084ddc..aefd47c363 100644
--- a/tests/php/TestStats.php
+++ b/tests/php/TestStats.php
@@ -78,9 +78,11 @@ public function testTotals() {
$this->assertEquals( 1, $totals['docs'] );
$this->assertTrue( ! empty( $totals['size'] ) );
- $this->assertTrue( ! empty( $totals['memory'] ) );
+ $this->assertTrue( isset( $totals['memory'] ) );
$this->assertEmpty( Stats::factory()->get_failed_queries() );
+
+ $this->markTestIncomplete( 'Memory numbers are always 0 with Elasticsearch 8.x' );
}
/**
diff --git a/tests/php/TestUtils.php b/tests/php/TestUtils.php
index 6328ba51ba..67ab8d6ca3 100644
--- a/tests/php/TestUtils.php
+++ b/tests/php/TestUtils.php
@@ -276,13 +276,14 @@ public function testGetCapability() {
/**
* Test the `ep_capability` filter.
*/
- $change_cap_name = function( $cap ) {
+ $change_cap_name = function( $cap, $context ) {
$this->assertSame( 'manage_elasticpress', $cap );
+ $this->assertSame( 'context', $context );
return 'custom_manage_ep';
};
- add_filter( 'ep_capability', $change_cap_name );
+ add_filter( 'ep_capability', $change_cap_name, 10, 2 );
- $this->assertSame( 'custom_manage_ep', Utils\get_capability() );
+ $this->assertSame( 'custom_manage_ep', Utils\get_capability( 'context' ) );
}
/**
@@ -296,13 +297,14 @@ public function testGetNetworkCapability() {
/**
* Test the `ep_network_capability` filter.
*/
- $change_cap_name = function( $cap ) {
+ $change_cap_name = function( $cap, $context ) {
$this->assertSame( 'manage_network_elasticpress', $cap );
+ $this->assertSame( 'context', $context );
return 'custom_manage_network_ep';
};
- add_filter( 'ep_network_capability', $change_cap_name );
+ add_filter( 'ep_network_capability', $change_cap_name, 10, 2 );
- $this->assertSame( 'custom_manage_network_ep', Utils\get_network_capability() );
+ $this->assertSame( 'custom_manage_network_ep', Utils\get_network_capability( 'context' ) );
}
/**
@@ -324,6 +326,32 @@ public function testGetPostMapCapabilities() {
$this->assertSame( $expected, Utils\get_post_map_capabilities() );
}
+ /**
+ * Test the `get_post_map_capabilities` function passing context
+ *
+ * @since 5.1.0
+ */
+ public function test_get_post_map_capabilities_with_context() {
+ $change_cap_name = function( $cap, $context ) {
+ $this->assertSame( 'manage_elasticpress', $cap );
+ $this->assertSame( 'context', $context );
+ return 'custom_manage_ep';
+ };
+ add_filter( 'ep_capability', $change_cap_name, 10, 2 );
+
+ $expected = [
+ 'edit_post' => 'custom_manage_ep',
+ 'edit_posts' => 'custom_manage_ep',
+ 'edit_others_posts' => 'custom_manage_ep',
+ 'publish_posts' => 'custom_manage_ep',
+ 'read_post' => 'custom_manage_ep',
+ 'read_private_posts' => 'custom_manage_ep',
+ 'delete_post' => 'custom_manage_ep',
+ ];
+
+ $this->assertSame( $expected, Utils\get_post_map_capabilities( 'context' ) );
+ }
+
/**
* Test the `get_elasticsearch_error_reason` function
*
diff --git a/tests/php/features/TestAutosuggest.php b/tests/php/features/TestAutosuggest.php
index 796c821a22..d4bfa8b563 100644
--- a/tests/php/features/TestAutosuggest.php
+++ b/tests/php/features/TestAutosuggest.php
@@ -321,4 +321,200 @@ public function test_get_settings_schema() {
$settings_keys
);
}
+
+ /**
+ * Test whether autosuggest ngram fields apply to the search query when AJAX integration and weighting is enabled.
+ *
+ * @since 5.1.0
+ * @group autosuggest
+ */
+ public function test_autosuggest_ngram_fields_for_ajax_request() {
+ add_filter( 'wp_doing_ajax', '__return_true' );
+ add_filter( 'ep_ajax_wp_query_integration', '__return_true' );
+ add_filter( 'ep_enable_do_weighting', '__return_true' );
+
+ $this->get_feature()->setup();
+
+ $this->ep_factory->post->create(
+ array(
+ 'post_title' => 'search me',
+ 'post_type' => 'post',
+ )
+ );
+
+ ElasticPress\Elasticsearch::factory()->refresh_indices();
+
+ add_filter(
+ 'ep_query_request_path',
+ function( $path, $index, $type, $query, $query_args, $query_object ) {
+ $fields = $query['query']['function_score']['query']['bool']['should'][0]['bool']['must'][0]['bool']['should'][1]['multi_match']['fields'];
+
+ $this->assertContains( 'term_suggest^1', $fields );
+ $this->assertContains( 'post_title.suggest^1', $fields );
+ return $path;
+ },
+ 10,
+ 6
+ );
+
+ $query = new \WP_Query(
+ array(
+ 's' => 'search me',
+ 'ep_integrate' => true,
+ )
+ );
+
+ $this->assertTrue( $query->elasticsearch_success );
+ $this->assertEquals( 1, $query->found_posts );
+ }
+
+ /**
+ * Test whether autosuggest ngram fields do not apply to the search query when `ep_autosuggest_contexts` is only set to public.
+ *
+ * @since 5.1.0
+ * @group autosuggest
+ */
+ public function test_autosuggest_ngram_fields_for_ajax_request_negative() {
+ add_filter( 'wp_doing_ajax', '__return_true' );
+ add_filter( 'ep_ajax_wp_query_integration', '__return_true' );
+ add_filter( 'ep_enable_do_weighting', '__return_true' );
+
+ $autosuggest_context = function() {
+ return [ 'public' ];
+ };
+
+ add_filter( 'ep_autosuggest_contexts', $autosuggest_context );
+
+ $this->get_feature()->setup();
+
+ $this->ep_factory->post->create(
+ array(
+ 'post_title' => 'search me',
+ 'post_type' => 'post',
+ )
+ );
+
+ ElasticPress\Elasticsearch::factory()->refresh_indices();
+
+ add_filter(
+ 'ep_query_request_path',
+ function( $path, $index, $type, $query, $query_args, $query_object ) {
+ $fields = $query['query']['function_score']['query']['bool']['should'][0]['bool']['must'][0]['bool']['should'][1]['multi_match']['fields'];
+
+ $this->assertNotContains( 'term_suggest^1', $fields );
+ $this->assertNotContains( 'post_title.suggest^1', $fields );
+ return $path;
+ },
+ 10,
+ 6
+ );
+
+ $query = new \WP_Query(
+ array(
+ 's' => 'search me',
+ 'ep_integrate' => true,
+ )
+ );
+
+ $this->assertTrue( $query->elasticsearch_success );
+ $this->assertEquals( 1, $query->found_posts );
+ }
+
+ /**
+ * Test whether fuzziness is set to `auto` for AJAX calls.
+ *
+ * @since 5.1.0
+ * @group autosuggest
+ */
+ public function test_fuziness_with_type_auto_set_for_ajax_call() {
+ add_filter( 'wp_doing_ajax', '__return_true' );
+ add_filter( 'ep_ajax_wp_query_integration', '__return_true' );
+
+ $algorithm = function() {
+ return 'default';
+ };
+ add_filter( 'ep_search_algorithm_version', $algorithm );
+
+ $this->get_feature()->setup();
+
+ $this->ep_factory->post->create(
+ array(
+ 'post_title' => 'search me',
+ 'post_type' => 'post',
+ )
+ );
+
+ ElasticPress\Elasticsearch::factory()->refresh_indices();
+
+ add_filter(
+ 'ep_query_request_path',
+ function( $path, $index, $type, $query, $query_args, $query_object ) {
+ $this->assertEquals( 'auto', $query['query']['function_score']['query']['bool']['should'][2]['multi_match']['fuzziness'] );
+ return $path;
+ },
+ 10,
+ 6
+ );
+
+ $query = new \WP_Query(
+ array(
+ 's' => 'search me',
+ 'ep_integrate' => true,
+ )
+ );
+
+ $this->assertTrue( $query->elasticsearch_success );
+ }
+
+ /**
+ * Test whether fuzziness is not set to `auto` for AJAX calls when `ep_autosuggest_contexts` is only set to public.
+ *
+ * @since 5.1.0
+ * @group autosuggest
+ */
+ public function test_fuziness_with_type_auto_set_for_ajax_call_negative() {
+ add_filter( 'wp_doing_ajax', '__return_true' );
+ add_filter( 'ep_ajax_wp_query_integration', '__return_true' );
+
+ $algorithm = function() {
+ return 'default';
+ };
+ add_filter( 'ep_search_algorithm_version', $algorithm );
+
+ $autosuggest_context = function() {
+ return [ 'public' ];
+ };
+ add_filter( 'ep_autosuggest_contexts', $autosuggest_context );
+
+ $this->get_feature()->setup();
+
+ $this->ep_factory->post->create(
+ array(
+ 'post_title' => 'search me',
+ 'post_type' => 'post',
+ )
+ );
+
+ ElasticPress\Elasticsearch::factory()->refresh_indices();
+
+ add_filter(
+ 'ep_query_request_path',
+ function( $path, $index, $type, $query, $query_args, $query_object ) {
+ $this->assertNotEquals( 'auto', $query['query']['function_score']['query']['bool']['should'][2]['multi_match']['fuzziness'] );
+ return $path;
+ },
+ 10,
+ 6
+ );
+
+ $query = new \WP_Query(
+ array(
+ 's' => 'search me',
+ 'ep_integrate' => true,
+ )
+ );
+
+ $this->assertTrue( $query->elasticsearch_success );
+
+ }
}
diff --git a/tests/php/features/TestDocuments.php b/tests/php/features/TestDocuments.php
index 59c15d772e..eb3525a5d4 100644
--- a/tests/php/features/TestDocuments.php
+++ b/tests/php/features/TestDocuments.php
@@ -107,8 +107,6 @@ public function testSearchDisallowedMimeType() {
// Need to call this since it's hooked to init
ElasticPress\Features::factory()->get_registered_feature( 'search' )->search_setup();
- $post_ids = array();
-
$this->ep_factory->post->create();
$this->ep_factory->post->create(
array(
@@ -152,8 +150,6 @@ public function testSearchNormalPost() {
// Need to call this since it's hooked to init
ElasticPress\Features::factory()->get_registered_feature( 'search' )->search_setup();
- $post_ids = array();
-
$this->ep_factory->post->create(
array(
'post_content' => 'findme',
@@ -226,4 +222,135 @@ public function testExcludeFromSearchQuery() {
$this->assertTrue( $query->elasticsearch_success );
$this->assertEquals( 2, $query->post_count );
}
+
+ /**
+ * Test that search in media library is working correctly.
+ *
+ * @since 5.1.0
+ * @group documents
+ */
+ public function testQueryForAttachments() {
+ add_filter( 'wp_doing_ajax', '__return_true' );
+ ElasticPress\Features::factory()->activate_feature( 'search' );
+ ElasticPress\Features::factory()->activate_feature( 'documents' );
+ ElasticPress\Features::factory()->activate_feature( 'protected_content' );
+ ElasticPress\Features::factory()->setup_features();
+
+ $this->ep_factory->post->create(
+ array(
+ 'post_content' => 'search me',
+ 'post_type' => 'attachment',
+ 'post_mime_type' => 'image',
+ )
+ );
+
+ ElasticPress\Elasticsearch::factory()->refresh_indices();
+
+ $_REQUEST['action'] = 'query-attachments';
+ $args = array(
+ 's' => 'search me',
+ 'post_type' => 'attachment',
+ 'post_status' => 'inherit',
+ );
+
+ $query = new \WP_Query( $args );
+
+ $this->assertTrue( $query->elasticsearch_success );
+ $this->assertEquals( 1, $query->post_count );
+ }
+
+ /**
+ * Test the `get_allowed_ingest_mime_types` method.
+ *
+ * @since 5.1.0
+ * @group documents
+ */
+ public function test_get_allowed_ingest_mime_types() {
+ $feature = ElasticPress\Features::factory()->get_registered_feature( 'documents' );
+
+ $expected = [
+ 'pdf' => 'application/pdf',
+ 'ppt' => 'application/vnd.ms-powerpoint',
+ 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ 'xls' => 'application/vnd.ms-excel',
+ 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+ 'doc' => 'application/msword',
+ 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ 'csv' => 'text/csv',
+ 'txt' => 'text/plain',
+ ];
+ $this->assertSame( $expected, $feature->get_allowed_ingest_mime_types() );
+ }
+
+
+ /**
+ * Test the `ep_allowed_documents_ingest_mime_types` filter.
+ *
+ * @since 5.1.0
+ * @group documents
+ */
+ public function test_ep_allowed_documents_ingest_mime_types_filter() {
+ $feature = ElasticPress\Features::factory()->get_registered_feature( 'documents' );
+
+ $change_filter = function ( $allowed_mime_types ) {
+ $allowed_mime_types['test'] = 'text/test';
+ return $allowed_mime_types;
+ };
+ add_filter( 'ep_allowed_documents_ingest_mime_types', $change_filter );
+
+ $this->assertSame( 'text/test', $feature->get_allowed_ingest_mime_types()['test'] );
+ }
+
+ /**
+ * Depending on the WP_Query post_type parameter, attachments should be added by default.
+ *
+ * @since 5.1.0
+ * @group documents
+ */
+ public function test_empty_post_type() {
+ ElasticPress\Features::factory()->activate_feature( 'search' );
+ ElasticPress\Features::factory()->activate_feature( 'documents' );
+ ElasticPress\Features::factory()->setup_features();
+
+ $this->ep_factory->post->create(
+ array(
+ 'post_title' => 'findme',
+ 'post_type' => 'post',
+ )
+ );
+ $this->ep_factory->post->create(
+ array(
+ 'post_title' => 'findme',
+ 'post_type' => 'attachment',
+ 'post_mime_type' => 'application/msword',
+ )
+ );
+
+ ElasticPress\Elasticsearch::factory()->refresh_indices();
+
+ // No post type, attachment added by default
+ $query = new \WP_Query( [ 's' => 'findme' ] );
+ $this->assertTrue( $query->elasticsearch_success );
+ $this->assertEquals( 2, $query->post_count );
+
+ // Post type as string, attachment not added by default
+ $query = new \WP_Query(
+ [
+ 'post_type' => 'post',
+ 's' => 'findme',
+ ]
+ );
+ $this->assertTrue( $query->elasticsearch_success );
+ $this->assertEquals( 1, $query->post_count );
+
+ // Post type as array, attachment not added by default
+ $query = new \WP_Query(
+ [
+ 'post_type' => [ 'post' ],
+ 's' => 'findme',
+ ]
+ );
+ $this->assertTrue( $query->elasticsearch_success );
+ $this->assertEquals( 1, $query->post_count );
+ }
}
diff --git a/tests/php/features/TestFacet.php b/tests/php/features/TestFacet.php
index 67083ee9fe..c084575539 100644
--- a/tests/php/features/TestFacet.php
+++ b/tests/php/features/TestFacet.php
@@ -13,6 +13,68 @@
* Facet test class
*/
class TestFacet extends BaseTestCase {
+ /**
+ * Clean up after each test.
+ *
+ * @since 5.1.0
+ */
+ public function tear_down() {
+ parent::tear_down();
+
+ $GLOBALS['pagenow'] = '';
+ }
+
+ /**
+ * Test the setup method
+ *
+ * @since 5.1.0
+ * @group facets
+ */
+ public function test_setup() {
+ $facet_feature = Features::factory()->get_registered_feature( 'facets' );
+ $facet_feature->setup();
+
+ $this->assertSame( 10, has_action( 'rest_api_init', [ $facet_feature, 'setup_endpoints' ] ) );
+ }
+
+ /**
+ * Test the feature is not loaded in the editor screen
+ *
+ * @since 5.1.0
+ * @group facets
+ */
+ public function test_setup_editor_screen() {
+ $GLOBALS['pagenow'] = 'post-new.php';
+ set_current_screen( 'post-new.php' );
+
+ $facet_feature = Features::factory()->get_registered_feature( 'facets' );
+ $facet_feature->tear_down();
+ $facet_feature->setup();
+
+ $this->assertFalse( has_action( 'rest_api_init', [ $facet_feature, 'setup_endpoints' ] ) );
+
+ set_current_screen( 'front' );
+ }
+
+ /**
+ * Test the ep_facet_enabled_in_editor filter
+ *
+ * @since 5.1.0
+ * @group facets
+ */
+ public function test_setup_ep_facet_enabled_in_editor() {
+ add_filter( 'ep_facet_enabled_in_editor', '__return_true' );
+
+ $GLOBALS['pagenow'] = 'post-new.php';
+ set_current_screen( 'post-new.php' );
+
+ $facet_feature = Features::factory()->get_registered_feature( 'facets' );
+ $facet_feature->tear_down();
+ $facet_feature->setup();
+
+ $this->assertSame( 10, has_action( 'rest_api_init', [ $facet_feature, 'setup_endpoints' ] ) );
+ }
+
/**
* Test facet type registration
*
diff --git a/tests/php/features/TestFacetTypeDate.php b/tests/php/features/TestFacetTypeDate.php
index f3a3df3f87..65d5379aae 100644
--- a/tests/php/features/TestFacetTypeDate.php
+++ b/tests/php/features/TestFacetTypeDate.php
@@ -26,6 +26,10 @@ class TestFacetTypeDate extends BaseTestCase {
* Setup each test.
*/
public function set_up() {
+ ElasticPress\Elasticsearch::factory()->delete_all_indices();
+ ElasticPress\Indexables::factory()->get( 'post' )->put_mapping();
+ ElasticPress\Indexables::factory()->get( 'post' )->sync_manager->reset_sync_queue();
+
$facet_feature = Features::factory()->get_registered_feature( 'facets' );
$this->facet_type = $facet_feature->types['date'];
@@ -332,6 +336,7 @@ public function testQueryPost() {
'ep_integrate' => true,
]
);
+ $this->assertTrue( $query->elasticsearch_success );
$this->assertEquals( 2, $query->found_posts );
// get all posts published on or after 2022-01-01.
@@ -341,6 +346,7 @@ public function testQueryPost() {
'ep_integrate' => true,
]
);
+ $this->assertTrue( $query->elasticsearch_success );
$this->assertEquals( 4, $query->found_posts );
// get all posts published on or before 2022-01-01.
@@ -350,6 +356,7 @@ public function testQueryPost() {
'ep_integrate' => true,
]
);
+ $this->assertTrue( $query->elasticsearch_success );
$this->assertEquals( 2, $query->found_posts );
// passing invalid date shouldn't apply any filter.
@@ -359,6 +366,7 @@ public function testQueryPost() {
'ep_integrate' => true,
]
);
+ $this->assertTrue( $query->elasticsearch_success );
$this->assertEquals( 5, $query->found_posts );
}
}
diff --git a/tests/php/features/TestSynonyms.php b/tests/php/features/TestSynonyms.php
index b8a705065c..27e1ea4b24 100644
--- a/tests/php/features/TestSynonyms.php
+++ b/tests/php/features/TestSynonyms.php
@@ -109,8 +109,11 @@ function ( $synonym ) {
);
$this->assertNotEmpty( $synonyms );
- $this->assertContains( 'sneakers, tennis shoes, trainers, runners', $synonyms );
- $this->assertContains( 'shoes => sneaker, sandal, boots, high heels', $synonyms );
+ $this->assertContains( 'runner, running shoe, sneaker, tennis shoe, trainer', $synonyms );
+ $this->assertContains( 'blue => blue, aqua, azure, cerulean, cyan, ultramarine', $synonyms );
+ $this->assertContains( 'supposably => supposedly', $synonyms );
+ $this->assertContains( 'flustrated => flustered, frustrated', $synonyms );
+ $this->assertContains( 'intensive purposes => intents and purposes', $synonyms );
}
/**
@@ -160,4 +163,46 @@ public function test_synonyms_with_spaces() {
$this->assertTrue( $query->elasticsearch_success );
$this->assertSame( $post_id, $query->posts['0'] );
}
+
+ /**
+ * Tests synonyms are case insensitive
+ *
+ * @since 5.1.0
+ * @group synonyms
+ */
+ public function test_synonyms_case_insensitive() {
+ $instance = $this->getFeature();
+
+ $this->ep_factory->post->create(
+ [
+ 'ID' => $instance->get_synonym_post_id(),
+ 'post_content' => 'hoodie, sweatshirt',
+ 'post_type' => $instance::POST_TYPE_NAME,
+ ]
+ );
+
+ $instance->update_synonyms();
+
+ $post_id = $this->ep_factory->post->create( [ 'post_content' => 'sweatshirt' ] );
+
+ ElasticPress\Elasticsearch::factory()->refresh_indices();
+
+ $query = new \WP_Query(
+ [
+ 's' => 'HoOdiE',
+ 'fields' => 'ids',
+ ]
+ );
+ $this->assertTrue( $query->elasticsearch_success );
+ $this->assertSame( $post_id, $query->posts['0'] );
+
+ $query = new \WP_Query(
+ [
+ 's' => 'HOODIE',
+ 'fields' => 'ids',
+ ]
+ );
+ $this->assertTrue( $query->elasticsearch_success );
+ $this->assertSame( $post_id, $query->posts['0'] );
+ }
}
diff --git a/tests/php/features/WooCommerce/TestWooCommerce.php b/tests/php/features/WooCommerce/TestWooCommerce.php
index 93ea0fe491..05e8d7aa60 100644
--- a/tests/php/features/WooCommerce/TestWooCommerce.php
+++ b/tests/php/features/WooCommerce/TestWooCommerce.php
@@ -153,6 +153,7 @@ public function testSearchQueryForCouponWhenProtectedContentIsNotEnable() {
*
* @since 4.5.0
* @group woocommerce
+ * @expectedDeprecated ElasticPress\Feature\WooCommerce\WooCommerce::is_orders_autosuggest_available
*/
public function testIsOrdersAutosuggestAvailable() {
$woocommerce_feature = ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' );
@@ -170,10 +171,11 @@ public function testIsOrdersAutosuggestAvailable() {
}
/**
- * Test the `is_orders_autosuggest_available` method
+ * Test the `is_orders_autosuggest_enabled` method
*
* @since 4.5.0
* @group woocommerce
+ * @expectedDeprecated ElasticPress\Feature\WooCommerce\WooCommerce::is_orders_autosuggest_enabled
*/
public function testIsOrdersAutosuggestEnabled() {
$woocommerce_feature = ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' );
diff --git a/tests/php/features/WooCommerce/TestWooCommerceOrders.php b/tests/php/features/WooCommerce/TestWooCommerceOrders.php
index 8be8e86c31..2f5a3257ce 100644
--- a/tests/php/features/WooCommerce/TestWooCommerceOrders.php
+++ b/tests/php/features/WooCommerce/TestWooCommerceOrders.php
@@ -329,4 +329,45 @@ public function ordersAutosuggestMethodsDataProvider() : array {
[ 'set_search_fields', [] ],
];
}
+
+ /**
+ * Test the `hpos_compatibility_notice` method
+ *
+ * @group woocommerce
+ * @group woocommerce-orders
+ */
+ public function test_hpos_compatibility_notice() {
+ $notices = [
+ 'test' => [],
+ ];
+ $this->assertCount( 1, $this->orders->hpos_compatibility_notice( $notices ) );
+
+ \set_current_screen( 'woocommerce_page_wc-orders' );
+ $this->assertCount( 1, $this->orders->hpos_compatibility_notice( $notices ) );
+
+ ElasticPress\Features::factory()->activate_feature( 'protected_content' );
+ $this->assertCount( 1, $this->orders->hpos_compatibility_notice( $notices ) );
+
+ $option_name = \Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION;
+ $change_value = function() {
+ return 'yes';
+ };
+ add_filter( 'pre_option_' . $option_name, $change_value );
+
+ $new_notices = $this->orders->hpos_compatibility_notice( $notices );
+ $this->assertCount( 2, $new_notices );
+ $this->assertArrayHasKey( 'wc_orders_incompatible', $new_notices );
+
+ /**
+ * Test if the notice is hidden when the user already dismissed it
+ */
+ $change_hide_option = function() {
+ return 1;
+ };
+ add_filter( 'pre_option_ep_hide_wc_orders_incompatible_notice', $change_hide_option );
+ add_filter( 'pre_site_option_ep_hide_wc_orders_incompatible_notice', $change_hide_option );
+ $new_notices = $this->orders->hpos_compatibility_notice( $notices );
+ $this->assertCount( 1, $new_notices );
+ $this->assertArrayNotHasKey( 'wc_orders_incompatible', $new_notices );
+ }
}
diff --git a/tests/php/features/WooCommerce/TestWooCommerceOrdersAutosuggest.php b/tests/php/features/WooCommerce/TestWooCommerceOrdersAutosuggest.php
index de3682e8d6..b183c90a08 100644
--- a/tests/php/features/WooCommerce/TestWooCommerceOrdersAutosuggest.php
+++ b/tests/php/features/WooCommerce/TestWooCommerceOrdersAutosuggest.php
@@ -294,4 +294,148 @@ public function testSetSearchFields() {
$this->assertSame( $expected_fields, $changed_search_fields );
}
+
+ /**
+ * Test the `is_available` method
+ *
+ * @since 5.1.0
+ * @group woocommerce
+ * @group woocommerce-orders-autosuggest
+ */
+ public function test_is_available() {
+ $this->assertSame( $this->orders_autosuggest->is_available(), \ElasticPress\Utils\is_epio() );
+
+ /**
+ * Test the `ep_woocommerce_orders_autosuggest_available` filter
+ */
+ add_filter( 'ep_woocommerce_orders_autosuggest_available', '__return_true' );
+ $this->assertTrue( $this->orders_autosuggest->is_available() );
+
+ add_filter( 'ep_woocommerce_orders_autosuggest_available', '__return_false' );
+ $this->assertFalse( $this->orders_autosuggest->is_available() );
+ }
+
+ /**
+ * Test the `is_enabled` method
+ *
+ * @since 5.1.0
+ * @group woocommerce
+ * @group woocommerce-orders-autosuggest
+ */
+ public function test_is_enabled() {
+ $this->assertFalse( $this->orders_autosuggest->is_enabled() );
+
+ /**
+ * Make it available but it won't be enabled
+ */
+ add_filter( 'ep_woocommerce_orders_autosuggest_available', '__return_true' );
+ $this->assertFalse( $this->orders_autosuggest->is_enabled() );
+
+ /**
+ * Enable it
+ */
+ $filter = function() {
+ return [
+ 'woocommerce' => [
+ 'orders' => '1',
+ ],
+ ];
+ };
+ add_filter( 'pre_site_option_ep_feature_settings', $filter );
+ add_filter( 'pre_option_ep_feature_settings', $filter );
+ $this->assertTrue( $this->orders_autosuggest->is_enabled() );
+
+ /**
+ * Make it unavailable. Even activated, it should not be considered enabled if not available anymore.
+ */
+ remove_filter( 'ep_woocommerce_orders_autosuggest_available', '__return_true' );
+ $this->assertFalse( $this->orders_autosuggest->is_enabled() );
+ }
+
+ /**
+ * Test the `is_hpos_compatible` method
+ *
+ * @since 5.1.0
+ * @group woocommerce
+ * @group woocommerce-orders-autosuggest
+ */
+ public function test_is_hpos_compatible() {
+ $this->assertTrue( $this->orders_autosuggest->is_hpos_compatible() );
+
+ // Turn HPOS on
+ $custom_orders_table = \Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION;
+ $change_custom_orders_table = function() {
+ return 'yes';
+ };
+ add_filter( 'pre_option_' . $custom_orders_table, $change_custom_orders_table );
+
+ // Disable legacy mode
+ $legacy_mode = \Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION;
+ $change_legacy_mode = function() {
+ return 'no';
+ };
+ add_filter( 'pre_option_' . $legacy_mode, $change_legacy_mode );
+
+ $this->assertFalse( $this->orders_autosuggest->is_hpos_compatible() );
+ }
+
+ /**
+ * Test the `add_settings_schema` method
+ *
+ * @since 5.1.0
+ * @group woocommerce
+ * @group woocommerce-orders-autosuggest
+ */
+ public function test_add_settings_schema() {
+ $new_settings_schema = $this->orders_autosuggest->add_settings_schema( [] );
+ $this->assertSame( 'orders', $new_settings_schema[0]['key'] );
+ }
+
+ /**
+ * Test the `get_setting_help_message` method when the feature is available
+ *
+ * @since 5.1.0
+ * @group woocommerce
+ * @group woocommerce-orders-autosuggest
+ */
+ public function test_get_setting_help_message_feature_available() {
+ /**
+ * Make it available but it won't be enabled
+ */
+ add_filter( 'ep_woocommerce_orders_autosuggest_available', '__return_true' );
+
+ $new_settings_schema = $this->orders_autosuggest->add_settings_schema( [] );
+ $this->assertStringContainsString( 'You are directly connected to', $new_settings_schema[0]['help'] );
+ }
+
+ /**
+ * Test the `get_setting_help_message` method when incompatible with HPOS
+ *
+ * @since 5.1.0
+ * @group woocommerce
+ * @group woocommerce-orders-autosuggest
+ */
+ public function test_get_setting_help_message_feature_hpos_incompatible() {
+ // Turn HPOS on
+ $custom_orders_table = \Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION;
+ $change_custom_orders_table = function() {
+ return 'yes';
+ };
+ add_filter( 'pre_option_' . $custom_orders_table, $change_custom_orders_table );
+
+ $new_settings_schema = $this->orders_autosuggest->add_settings_schema( [] );
+ $this->assertStringContainsString( 'Currently, autosuggest for orders is only available', $new_settings_schema[0]['help'] );
+ }
+
+ /**
+ * Test the `get_setting_help_message` method when the feature is not available
+ *
+ * @since 5.1.0
+ * @group woocommerce
+ * @group woocommerce-orders-autosuggest
+ */
+ public function test_get_setting_help_message_feature_not_available() {
+ $new_settings_schema = $this->orders_autosuggest->add_settings_schema( [] );
+ $this->assertStringContainsString( 'Due to the sensitive nature of orders', $new_settings_schema[0]['help'] );
+ }
}
diff --git a/tests/php/includes/classes/BaseTestCase.php b/tests/php/includes/classes/BaseTestCase.php
index da87c4ed66..44f5c5a468 100644
--- a/tests/php/includes/classes/BaseTestCase.php
+++ b/tests/php/includes/classes/BaseTestCase.php
@@ -218,4 +218,13 @@ public function assertDecayDisabled( $query ) {
)
);
}
+
+ /**
+ * Forces tests to use EP.io
+ *
+ * @since 5.1.0
+ */
+ protected function force_epio() {
+ update_site_option( 'ep_host', 'https://prefix.elasticpress.io/' );
+ }
}
diff --git a/tests/php/indexables/TestPost.php b/tests/php/indexables/TestPost.php
index 0a5bf7f02f..ea3252754a 100644
--- a/tests/php/indexables/TestPost.php
+++ b/tests/php/indexables/TestPost.php
@@ -9146,4 +9146,33 @@ public function test_mapping_ep_stop_filter() {
$index_settings = $settings[ $index_name ]['settings'];
$this->assertSame( '_arabic_', $index_settings['index.analysis.filter.ep_stop.stopwords'] );
}
+
+ /**
+ * Test if aggregations are set
+ *
+ * @since 5.1.0
+ * @group post
+ */
+ public function test_aggregations_return() {
+ $query = new \WP_Query(
+ [
+ 'ep_integrate' => true,
+ 'fields' => 'ids',
+ 'aggs' => [
+ 'name' => 'my_aggs',
+ 'aggs' => [
+ 'terms' => [
+ 'size' => 10000,
+ 'field' => 'terms.category.slug',
+ ],
+ ],
+ ],
+ 'ep_custom_id' => 'my_query',
+ ]
+ );
+
+ $this->assertTrue( $query->elasticsearch_success );
+ $this->assertArrayHasKey( 'ep_aggregations', $query->query_vars );
+ $this->assertArrayHasKey( 'my_aggs', $query->query_vars['ep_aggregations'] );
+ }
}
diff --git a/tests/php/indexables/TestTerm.php b/tests/php/indexables/TestTerm.php
index 725196123f..33f99dc0cb 100644
--- a/tests/php/indexables/TestTerm.php
+++ b/tests/php/indexables/TestTerm.php
@@ -844,8 +844,8 @@ public function testFormatArgsIncludeExclude() {
]
);
- $this->assertSame( 123, $args['post_filter']['bool']['must'][0]['bool']['must_not']['terms']['term_id'][0] );
- $this->assertSame( 123, $args['post_filter']['bool']['must'][1]['bool']['must_not']['terms']['parent'][0] );
+ $this->assertSame( 123, $args['post_filter']['bool']['must'][0]['bool']['must_not'][0]['terms']['term_id'][0] );
+ $this->assertSame( 123, $args['post_filter']['bool']['must'][0]['bool']['must_not'][1]['terms']['parent'][0] );
}
/**
@@ -1173,7 +1173,7 @@ public function testFormatArgsChildParent() {
);
$this->assertSame( 'category', $args['post_filter']['bool']['must'][0]['term']['taxonomy.raw'] );
- $this->assertSame( 123, $args['post_filter']['bool']['must'][1]['bool']['must'][0]['match_phrase']['hierarchy.ancestors.terms'] );
+ $this->assertSame( 123, $args['post_filter']['bool']['must'][1]['bool']['must']['match_phrase']['hierarchy.ancestors.terms'] );
$args = $term->format_args(
[