diff --git a/.github/workflows/cypress-tests.yml b/.github/workflows/cypress-tests.yml index 9ec2943e7b..328783f40e 100644 --- a/.github/workflows/cypress-tests.yml +++ b/.github/workflows/cypress-tests.yml @@ -79,7 +79,7 @@ jobs: run: npm run build - name: Set up database - run: npm run cypress:setup -- --wp-version=${{ matrix.core.version }} --wc-version=${{ matrix.core.wcVersion }} + run: npm run cypress:setup -- --wp-version=${{ matrix.core.version }} --wc-version=${{ matrix.core.wcVersion }} --acf-pro-license='${{ secrets.ACF_PRO_LICENSE_KEY }}' - name: Test run: npm run cypress:run -- --env grepTags=${{ matrix.testGroup }} @@ -166,7 +166,7 @@ jobs: run: npm run build - name: Set up database - run: npm run cypress:setup -- --ep-host=${{ secrets.EPIO_HOST }} --es-shield='${{ secrets.EPIO_SHIELD }}' --ep-index-prefix=${{ secrets.EPIO_INDEX_PREFIX }} --wp-version=${{ matrix.core.version }} --wc-version=${{ matrix.core.wcVersion }} + run: npm run cypress:setup -- --ep-host=${{ secrets.EPIO_HOST }} --es-shield='${{ secrets.EPIO_SHIELD }}' --ep-index-prefix=${{ secrets.EPIO_INDEX_PREFIX }} --wp-version=${{ matrix.core.version }} --wc-version=${{ matrix.core.wcVersion }} --acf-pro-license='${{ secrets.ACF_PRO_LICENSE_KEY }}' - name: Test run: npm run cypress:run -- --env grepTags=${{ matrix.testGroup }} diff --git a/.wp-env.json b/.wp-env.json index d56970e7dc..65868db282 100644 --- a/.wp-env.json +++ b/.wp-env.json @@ -18,6 +18,7 @@ ], "mappings": { ".htaccess": "./tests/cypress/wordpress-files/.htaccess", + "wp-content/composer.json": "./tests/cypress/wordpress-files/composer.json", "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", diff --git a/bin/setup-cypress-env.sh b/bin/setup-cypress-env.sh index 8332cb9b72..1fcb2f955f 100755 --- a/bin/setup-cypress-env.sh +++ b/bin/setup-cypress-env.sh @@ -2,22 +2,26 @@ # cat ./bin/2022-02-15-12-49.sql | ./bin/wp-env-cli tests-wordpress "wp --allow-root db import -" +ACF_PRO_LICENSE_KEY="" +DISPLAY_HELP=0 EP_HOST="" ES_SHIELD="" EP_INDEX_PREFIX="" WP_VERSION="" WC_VERSION="" -DISPLAY_HELP=0 for opt in "$@"; do case $opt in - -h=*|--ep-host=*) + --acf-pro-license=*) + ACF_PRO_LICENSE_KEY="${opt#*=}" + ;; + -H=*|--ep-host=*) EP_HOST="${opt#*=}" ;; - -s=*|--es-shield=*) + -S=*|--es-shield=*) ES_SHIELD="${opt#*=}" ;; - -u=*|--ep-index-prefix=*) + -p=*|--ep-index-prefix=*) EP_INDEX_PREFIX="${opt#*=}" ;; -wp=*|--wp-version=*) @@ -39,12 +43,13 @@ if [ $DISPLAY_HELP -eq 1 ]; then echo "Usage: ${0##*/} [OPTIONS...]" echo echo "Optional parameters:" - echo "-h=*, --ep-host=* The remote Elasticsearch Host URL." - echo "-s=*, --es-shield=* The Elasticsearch credentials, used in the ES_SHIELD constant." - echo "-u=*, --ep-index-prefix=* The Elasticsearch credentials, used in the EP_INDEX_PREFIX constant." - echo "-W=*, --wp-version=* WordPress Core version." - echo "-w=*, --wc-version=* WooCommerce version." - echo "-h|--help Display this help screen" + echo "--acf-pro-license=* ACF Pro License Key." + echo "-H=*, --ep-host=* The remote Elasticsearch Host URL." + echo "-S=*, --es-shield=* The Elasticsearch credentials, used in the ES_SHIELD constant." + echo "-p=*, --ep-index-prefix=* The Elasticsearch credentials, used in the EP_INDEX_PREFIX constant." + echo "-W=*, --wp-version=* WordPress Core version." + echo "-w=*, --wc-version=* WooCommerce version." + echo "-h|--help Display this help screen" exit fi @@ -90,6 +95,14 @@ if [ ! -z $EP_INDEX_PREFIX ]; then ./bin/wp-env-cli tests-wordpress "wp --allow-root config set EP_INDEX_PREFIX ${EP_INDEX_PREFIX}" fi +if [ ! -z $ACF_PRO_LICENSE_KEY ]; then + ./bin/wp-env-cli tests-wordpress "composer --working-dir=./wp-content config http-basic.connect.advancedcustomfields.com ${ACF_PRO_LICENSE_KEY} https://elasticpress.test" + ./bin/wp-env-cli tests-wordpress "composer --working-dir=./wp-content install" + ./bin/wp-env-cli tests-wordpress "rm wp-content/auth.json" + ./bin/wp-env-cli tests-wordpress "wp --allow-root plugin activate advanced-custom-fields-pro" + ./bin/wp-env-cli tests-wordpress "wp --allow-root config set ACF_PRO_LICENSE ${ACF_PRO_LICENSE_KEY}" +fi + ./bin/wp-env-cli tests-wordpress "wp --allow-root core multisite-convert" SITES_COUNT=$(./bin/wp-env-cli tests-wordpress "wp --allow-root site list --format=count") diff --git a/elasticpress.php b/elasticpress.php index 3e5227ffde..4b0aa835bd 100644 --- a/elasticpress.php +++ b/elasticpress.php @@ -190,6 +190,10 @@ function register_indexable_posts() { new Feature\Documents\Documents() ); + Features::factory()->register_feature( + new Feature\AcfRepeater\AcfRepeater() + ); + Features::factory()->register_feature( new Feature\Comments\Comments() ); diff --git a/includes/classes/Feature/AcfRepeater/AcfRepeater.php b/includes/classes/Feature/AcfRepeater/AcfRepeater.php new file mode 100644 index 0000000000..e3bf785752 --- /dev/null +++ b/includes/classes/Feature/AcfRepeater/AcfRepeater.php @@ -0,0 +1,210 @@ +slug = 'acf_repeater'; + + parent::__construct(); + } + + /** + * Sets i18n strings. + */ + public function set_i18n_strings(): void { + $this->title = esc_html__( 'ACF Repeater Field Compatibility', 'elasticpress' ); + + $this->short_title = esc_html__( 'ACF Repeater Field', 'elasticpress' ); + + $this->summary = '
' . __( 'Index your ACF Repeater fields as a JSON object and, optionally, make it searchable in the Search Fields & Weighting dashboard.', 'elasticpress' ) . '
'; + + $this->docs_url = __( 'https://www.elasticpress.io/documentation/article/acf-repeater-field-compatibility-feature/', 'elasticpress' ); + } + + /** + * Determine WC feature reqs status + * + * @return FeatureRequirementsStatus + */ + public function requirements_status() { + $status = new FeatureRequirementsStatus( 0 ); + + foreach ( $this->acf_functions as $function ) { + if ( ! function_exists( $function ) ) { + $status->code = 2; + $status->message = esc_html__( 'ACF Pro not installed.', 'elasticpress' ); + break; + } + } + + return $status; + } + + /** + * Setup feature functionality + */ + public function setup() { + add_action( 'acf/render_field_settings', [ $this, 'render_field_settings' ] ); + add_filter( 'ep_prepare_meta_allowed_protected_keys', [ $this, 'allow_meta_keys' ], 10, 2 ); + add_filter( 'ep_prepare_meta_data', [ $this, 'add_meta_keys' ], 10, 2 ); + } + + /** + * Render field in the ACF group admin screen + * + * @param array $field ACF Field array. + * @return void + */ + public function render_field_settings( $field ): void { + // We only want repeaters and fields that are not children of repeaters. + if ( 'repeater' !== $field['type'] || ! empty( $field['parent_repeater'] ) ) { + return; + } + + // Root level fields are children of the post object. + $post_parent = ! empty( $field['parent'] ) ? get_post( $field['parent'] ) : false; + if ( ! $post_parent ) { + return; + } + + /** + * Filter whether EP should or not display the field setting in ACF + * + * @hook ep_acf_repeater_should_display_field_setting + * @since 5.3.0 + * @param {bool} $should_display Whether should or not display the field setting in ACF + * @param {array} $field The ACF Field array + * @return {bool} New value of $should_display + */ + if ( ! apply_filters( 'ep_acf_repeater_should_display_field_setting', true, $field ) ) { + return; + } + + $instructions = wp_kses_post( + sprintf( + /* translators: %s: post type name */ + __( 'Index this field as a JSON object. If you want to make it searchable, do not forget to enable it under the related post types in the Search Fields & Weighting dashboard. To index existent content you can either manually save posts with this field or run a sync.', 'elasticpress' ), + esc_url( admin_url( 'admin.php?page=elasticpress-weighting' ) ), + Utils\get_sync_url() + ) + ); + + \acf_render_field_setting( + $field, + [ + 'label' => esc_html__( 'Index in ElasticPress', 'elasticpress' ), + 'instructions' => $instructions, + 'name' => 'ep_acf_repeater_index_field', + 'type' => 'true_false', + 'ui' => 1, + ] + ); + } + + /** + * Add to the weighting dashboard all the ACF Repeater fields that were checked to be indexed. + * + * @param array $meta List of allowed meta keys + * @param \WP_Post $post The post object. + * @return array + */ + public function allow_meta_keys( $meta, $post ) { + $field_groups = acf_get_field_groups( + array( + 'post_id' => $post->ID, + 'post_type' => $post->post_type, + ) + ); + + if ( empty( $field_groups ) ) { + return $meta; + } + + $ep_fields = []; + + foreach ( $field_groups as $field_group ) { + $fields = acf_get_fields( $field_group ); + foreach ( $fields as $field ) { + if ( empty( $field['ep_acf_repeater_index_field'] ) ) { + continue; + } + + $ep_fields[] = $field['name']; + } + } + + $meta = array_unique( array_merge( $meta, $ep_fields ) ); + + return $meta; + } + + /** + * Add the ACF Repeater fields to the ES document meta data. + * + * @param array $meta All post meta data + * @param \WP_Post $post The post object + * @return array + */ + public function add_meta_keys( $meta, $post ) { + $meta_keys = array_keys( $meta ); + foreach ( $meta_keys as $key ) { + $field = acf_get_field( $key ); + + if ( ! $field || empty( $field['ep_acf_repeater_index_field'] ) || 'repeater' !== $field['type'] ) { + continue; + } + + $value_field = get_field( $key, $post->ID ); + $value_encoded = wp_json_encode( $value_field ); + + /** + * Filter the ACF Repeater field value before it is indexed + * + * @hook ep_acf_repeater_meta_value + * @since 5.3.0 + * @param {string} $value_encoded Repeater field value encoded + * @param {array} $value_field Original field value + * @param {string} $key The meta field key + * @param {WP_Post} $post The Post object + * @return {mixed} New value of $value_encoded + */ + $meta[ $key ] = apply_filters( 'ep_acf_repeater_meta_value', $value_encoded, $value_field, $key, $post ); + } + + return $meta; + } +} diff --git a/tests/cypress/fixtures/acf-repeater-field-test.json b/tests/cypress/fixtures/acf-repeater-field-test.json new file mode 100644 index 0000000000..5455ffada9 --- /dev/null +++ b/tests/cypress/fixtures/acf-repeater-field-test.json @@ -0,0 +1,186 @@ +[ + { + "key": "group_67ab2a1de23c3", + "title": "Repeater Test", + "fields": [ + { + "key": "field_67ab2a1e4d2c2", + "label": "Repeater Test 1", + "name": "repeater_test_1", + "aria-label": "", + "type": "repeater", + "instructions": "", + "required": 0, + "conditional_logic": 0, + "wrapper": { + "width": "", + "class": "", + "id": "" + }, + "layout": "table", + "pagination": 0, + "min": 0, + "max": 0, + "collapsed": "", + "button_label": "Add Row", + "rows_per_page": 20, + "sub_fields": [ + { + "key": "field_67ab2a314d2c3", + "label": "Child 1", + "name": "child_1", + "aria-label": "", + "type": "text", + "instructions": "", + "required": 0, + "conditional_logic": 0, + "wrapper": { + "width": "", + "class": "", + "id": "" + }, + "default_value": "", + "maxlength": "", + "allow_in_bindings": 0, + "placeholder": "", + "prepend": "", + "append": "", + "parent_repeater": "field_67ab2a1e4d2c2" + }, + { + "key": "field_67ab2a374d2c4", + "label": "Child Repeater", + "name": "child_repeater", + "aria-label": "", + "type": "repeater", + "instructions": "", + "required": 0, + "conditional_logic": 0, + "wrapper": { + "width": "", + "class": "", + "id": "" + }, + "layout": "table", + "pagination": 0, + "min": 0, + "max": 0, + "collapsed": "", + "button_label": "Add Row", + "rows_per_page": 20, + "sub_fields": [ + { + "key": "field_67ab2a444d2c5", + "label": "Grandchild 1", + "name": "grandchild_1", + "aria-label": "", + "type": "text", + "instructions": "", + "required": 0, + "conditional_logic": 0, + "wrapper": { + "width": "", + "class": "", + "id": "" + }, + "default_value": "", + "maxlength": "", + "allow_in_bindings": 0, + "placeholder": "", + "prepend": "", + "append": "", + "parent_repeater": "field_67ab2a374d2c4" + } + ], + "parent_repeater": "field_67ab2a1e4d2c2" + } + ] + }, + { + "key": "field_67ab2a514d2c7", + "label": "Root Level Text", + "name": "root_level_text", + "aria-label": "", + "type": "text", + "instructions": "", + "required": 0, + "conditional_logic": 0, + "wrapper": { + "width": "", + "class": "", + "id": "" + }, + "default_value": "", + "maxlength": "", + "allow_in_bindings": 0, + "placeholder": "", + "prepend": "", + "append": "" + }, + { + "key": "field_67ab2a5b4d2c8", + "label": "Repeater Test 2", + "name": "repeater_test_2", + "aria-label": "", + "type": "repeater", + "instructions": "", + "required": 0, + "conditional_logic": 0, + "wrapper": { + "width": "", + "class": "", + "id": "" + }, + "layout": "table", + "pagination": 0, + "min": 0, + "max": 0, + "collapsed": "", + "button_label": "Add Row", + "rows_per_page": 20, + "sub_fields": [ + { + "key": "field_67ab2a6e4d2c9", + "label": "Textarea", + "name": "textarea", + "aria-label": "", + "type": "textarea", + "instructions": "", + "required": 0, + "conditional_logic": 0, + "wrapper": { + "width": "", + "class": "", + "id": "" + }, + "default_value": "", + "maxlength": "", + "allow_in_bindings": 0, + "rows": "", + "placeholder": "", + "new_lines": "", + "parent_repeater": "field_67ab2a5b4d2c8" + } + ] + } + ], + "location": [ + [ + { + "param": "post_type", + "operator": "==", + "value": "post" + } + ] + ], + "menu_order": 0, + "position": "normal", + "style": "default", + "label_placement": "top", + "instruction_placement": "label", + "hide_on_screen": "", + "active": true, + "description": "", + "show_in_rest": 0 + } +] diff --git a/tests/cypress/integration/features/acf-repeater.cy.js b/tests/cypress/integration/features/acf-repeater.cy.js new file mode 100644 index 0000000000..11deaa4001 --- /dev/null +++ b/tests/cypress/integration/features/acf-repeater.cy.js @@ -0,0 +1,86 @@ +describe('ACF Repeater Field Compatibility Feature', () => { + before(() => { + cy.visitAdminPage('edit.php?post_type=acf-field-group&page=acf-tools'); + cy.fixture('acf-repeater-field-test.json', 'utf8').then((fileContent) => { + cy.log(fileContent); + cy.get('#acf_import_file').attachFile({ + fileContent, + fileName: 'acf-repeater-field-test.json', + mimeType: 'application/json', + encoding: 'utf8', + lastModified: new Date().getTime(), + }); + }); + cy.contains('button', 'Import JSON').click(); + }); + + it('Can index an ACF Repeater Field', () => { + // Check ElasticPress controls in the ACF group edit screen + cy.visitAdminPage('edit.php?post_type=acf-field-group'); + cy.get('a[aria-label="Edit “Repeater Test”"]').click({ force: true }); + + cy.get('.edit-field').click({ multiple: true, force: true }); + cy.get('.acf-field-object-repeater').should('have.length', 3); + cy.get('.acf-field-setting-ep_acf_repeater_index_field').should('have.length', 2); // We have 3 repeaters, but nested repeats do not get a toggle + + cy.get('.acf-field-setting-ep_acf_repeater_index_field') + .first() + .find('input[type="checkbox"]') + .then(($el) => { + if (!$el.is(':checked')) { + cy.wrap($el).check({ force: true }); + } + }); + cy.get('button.acf-publish').click(); + + // Save the example post, so the repeater field is indexed + cy.visitAdminPage('edit.php?s=Post+with+ACF+Repeater+Field'); + cy.get('span.edit a').click({ force: true }); + cy.get('body').then(($body) => { + const welcomeGuide = $body.find( + '.edit-post-welcome-guide .components-modal__header button', + ); + if (welcomeGuide.length) { + welcomeGuide.click(); + } + }); + cy.get('.editor-post-publish-button__button').click(); + cy.wait(2000); // eslint-disable-line + + // Make the field searchable + cy.visitAdminPage('admin.php?page=elasticpress-weighting'); + cy.contains('h2', 'Posts').closest('.ep-weighting-post-type').as('postBox'); + cy.get('@postBox') + .contains('button.components-panel__body-toggle', 'Metadata') + .then(($el) => { + if (!$el.prop('aria-expanded')) { + cy.wrap($el).click(); + } + }); + cy.contains('.ep-weighting-field__name', 'repeater_test_1').should('exist'); + cy.contains('.ep-weighting-field__name', 'repeater_test_1') + .closest('fieldset') + .find('input[type="checkbox"]') + .check(); + cy.get('button[type="submit"]').click(); + + // Search using the field + cy.visit('/?s=Grandchild%201.1'); + cy.contains('.site-content article h2', 'Post with ACF Repeater Field').should('exist'); + cy.get('.site-content article a').first().click(); + + cy.get('#wpadminbar li#wp-admin-bar-debug-bar').click(); + cy.get('#debug-menu-link-EP_Debug_Bar_ElasticPress').click(); + cy.contains('a', 'Reload and retrieve raw ES document').click(); + + cy.get('#wpadminbar li#wp-admin-bar-debug-bar').click(); + cy.get('#debug-menu-link-EP_Debug_Bar_ElasticPress').click(); + cy.get('.query-results') + .first() + .should( + 'contain.text', + '[{\\"child_1\\":\\"Repeater Child 1\\",\\"child_repeater\\":[{\\"grandchild_1\\":\\"Grandchild 1\\"},{\\"grandchild_1\\":\\"Grandchild 2\\"}]},{\\"child_1\\":\\"Repeater Child 2\\",\\"child_repeater\\":[{\\"grandchild_1\\":\\"Grandchild 1.1\\"}]}]', + ) + .should('not.contain.text', 'Repeater Test 2 Textarea'); + }); +}); diff --git a/tests/cypress/support/index.js b/tests/cypress/support/index.js index c451af6a51..d5792ef880 100644 --- a/tests/cypress/support/index.js +++ b/tests/cypress/support/index.js @@ -52,6 +52,9 @@ cy.elasticPress = { protected_content: { active: false, }, + acf_repeater: { + active: true, + }, }, }; diff --git a/tests/cypress/wordpress-files/composer.json b/tests/cypress/wordpress-files/composer.json new file mode 100644 index 0000000000..58991b8962 --- /dev/null +++ b/tests/cypress/wordpress-files/composer.json @@ -0,0 +1,33 @@ +{ + "name": "10up/elasticpress-test-env", + "authors": [ + { + "name": "10up" + } + ], + "repositories": [ + { + "type": "composer", + "url": "https://connect.advancedcustomfields.com" + } + ], + "extra": { + "installer-paths": { + "mu-plugins/{$name}": [ + "type:wordpress-muplugin" + ], + "plugins/{$name}": [ + "type:wordpress-plugin" + ] + } + }, + "config": { + "allow-plugins": { + "composer/installers": true, + "dealerdirect/phpcodesniffer-composer-installer": true + } + }, + "require": { + "wpengine/advanced-custom-fields-pro": "*" + } +} diff --git a/tests/cypress/wordpress-files/test-docs/content-example.xml b/tests/cypress/wordpress-files/test-docs/content-example.xml index 5a377bafb4..15bb334b2a 100644 --- a/tests/cypress/wordpress-files/test-docs/content-example.xml +++ b/tests/cypress/wordpress-files/test-docs/content-example.xml @@ -35708,5 +35708,138 @@ Incidunt repellendus voluptatem laudantium et aut delectus rem. Et velit ullam o