From bc5052150c1fb0421e621f03c79907076cfb957d Mon Sep 17 00:00:00 2001 From: Burhan Nasir Date: Wed, 11 Oct 2023 11:27:43 +0500 Subject: [PATCH 1/5] Add Date block --- assets/css/facets.css | 87 +++++ assets/js/blocks/facets/date/block.json | 38 ++ assets/js/blocks/facets/date/edit.js | 50 +++ assets/js/blocks/facets/date/index.js | 20 + assets/js/blocks/facets/date/view.js | 56 +++ includes/classes/Feature/Facets/Facets.php | 2 + .../Feature/Facets/Types/Date/Block.php | 115 ++++++ .../Feature/Facets/Types/Date/FacetType.php | 276 +++++++++++++ .../Feature/Facets/Types/Date/Renderer.php | 271 +++++++++++++ package.json | 2 + .../cypress/integration/features/facets.cy.js | 106 +++++ tests/php/features/TestFacetTypeDate.php | 364 ++++++++++++++++++ 12 files changed, 1387 insertions(+) create mode 100644 assets/js/blocks/facets/date/block.json create mode 100644 assets/js/blocks/facets/date/edit.js create mode 100644 assets/js/blocks/facets/date/index.js create mode 100644 assets/js/blocks/facets/date/view.js create mode 100644 includes/classes/Feature/Facets/Types/Date/Block.php create mode 100644 includes/classes/Feature/Facets/Types/Date/FacetType.php create mode 100644 includes/classes/Feature/Facets/Types/Date/Renderer.php create mode 100644 tests/php/features/TestFacetTypeDate.php diff --git a/assets/css/facets.css b/assets/css/facets.css index 49478a5dad..3614d97e1c 100644 --- a/assets/css/facets.css +++ b/assets/css/facets.css @@ -146,3 +146,90 @@ height: var(--ep-range-slider-thumb-size); width: var(--ep-range-slider-thumb-size); } + +.ep-facet-date-option { + align-items: center; + display: flex; +} + +.ep-radio { + appearance: none; + outline: transparent; + position: relative; + width: 1rem; + + &::before { + background: transparent; + border: 1px solid #eee; + border-radius: 50%; + content: ""; + display: inline-block; + height: 0.8rem; + left: 50%; + position: absolute; + top: 50%; + transform: translate(-50%, -50%); + vertical-align: middle; + width: 0.8rem; + } + + &::after { + border-radius: 50%; + content: ""; + display: block; + height: 0.3rem; + left: 50%; + position: absolute; + top: 50%; + transform: translate(-50%, -50%); + width: 0.3rem; + } + + &:checked { + background: transparent; + outline-color: transparent; + + &::before { + background: #5e5e5e; + border-color: inherit; + } + + &::after { + background: #eee; + } + } +} + +.ep-date-range-picker { + column-gap: 1rem; + display: grid; + grid-template-columns: 1fr 1fr; + margin-top: 1rem; + + &.is-hidden { + display: none; + } + + & label { + margin-right: 0.5rem; + } +} + +.ep-date-range-picker__from, +.ep-date-range-picker__to { + align-items: baseline; + display: grid; + grid-template-columns: max-content 1fr; +} + +.ep-date-range-picker__action { + grid-column: 1 / -1; + text-align: right; +} + +.ep-facet-date-form__action { + align-items: center; + display: flex; + gap: 1.5rem; + margin-top: 0.75rem; +} diff --git a/assets/js/blocks/facets/date/block.json b/assets/js/blocks/facets/date/block.json new file mode 100644 index 0000000000..3a00f831c8 --- /dev/null +++ b/assets/js/blocks/facets/date/block.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 2, + "name": "elasticpress/facet-date", + "title": "Filter by Post Date", + "category": "elasticpress", + "description": "Let visitors filter your content by post date.", + "keywords": ["custom date", "facets"], + "textdomain": "elasticpress", + "attributes": { + "displayCustomDate": { + "type": "boolean", + "default": true + } + }, + "supports": { + "color": { + "background": true, + "link": true, + "text": false + }, + "html": false, + "position": { + "sticky": true + }, + "spacing": { + "margin": true, + "padding": true + }, + "typography": { + "fontSize": true, + "lineHeight": true + } + }, + "editorScript": "ep-facets-date-block-script", + "viewScript": "ep-facets-date-block-view-script", + "style": "elasticpress-facets" +} diff --git a/assets/js/blocks/facets/date/edit.js b/assets/js/blocks/facets/date/edit.js new file mode 100644 index 0000000000..e71da66bd1 --- /dev/null +++ b/assets/js/blocks/facets/date/edit.js @@ -0,0 +1,50 @@ +/** + * WordPress dependencies. + */ +import { InspectorControls, useBlockProps } from '@wordpress/block-editor'; +import { Disabled, PanelBody, ToggleControl } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import ServerSideRender from '@wordpress/server-side-render'; + +/** + * Internal dependencies. + */ +import EmptyResponsePlaceholder from '../common/components/empty-response-placeholder'; +import LoadingResponsePlaceholder from '../common/components/loading-response-placeholder'; + +const FacetDate = (props) => { + const blockProps = useBlockProps(); + const { attributes, name, setAttributes } = props; + const { displayCustomDate } = attributes; + + return ( + <> + + + setAttributes({ displayCustomDate })} + /> + + + +
+ + + +
+ + ); +}; + +export default FacetDate; diff --git a/assets/js/blocks/facets/date/index.js b/assets/js/blocks/facets/date/index.js new file mode 100644 index 0000000000..499afc1e23 --- /dev/null +++ b/assets/js/blocks/facets/date/index.js @@ -0,0 +1,20 @@ +/** + * WordPress dependencies. + */ +import { registerBlockType } from '@wordpress/blocks'; + +/** + * Internal dependencies. + */ +import icon from '../common/icon'; +import edit from './edit'; +import { name } from './block.json'; + +/** + * Register block. + */ +registerBlockType(name, { + icon, + edit, + save: () => {}, +}); diff --git a/assets/js/blocks/facets/date/view.js b/assets/js/blocks/facets/date/view.js new file mode 100644 index 0000000000..0922f55c5a --- /dev/null +++ b/assets/js/blocks/facets/date/view.js @@ -0,0 +1,56 @@ +/** + * WordPress dependencies. + */ +import domReady from '@wordpress/dom-ready'; + +/** + * Initializes the date facet functionality. + * + */ +const initFacet = () => { + const forms = document.querySelectorAll('.ep-facet-date-form'); + // eslint-disable-next-line no-undef + const filterName = epFacetDate.dateFilterName; + + forms.forEach(function (form) { + form.addEventListener('submit', function (event) { + event.preventDefault(); + + const { value } = this.querySelector(`[name="${filterName}"]:checked`); + const { value: startDateValue } = this.querySelector( + '.ep-date-range-picker', + ).querySelector(`[name="${filterName}_from"]`); + + const { value: endDateValue } = this.querySelector( + '.ep-date-range-picker', + ).querySelector(`[name="${filterName}_to"]`); + + const currentURL = window.location.href; + const newUrl = new URL(currentURL); + + if (value !== 'custom') { + newUrl.searchParams.set(filterName, value); + } else { + newUrl.searchParams.set(filterName, `${startDateValue},${endDateValue}`); + } + + window.location.href = decodeURIComponent(newUrl); + }); + + const radioButtons = form.querySelectorAll('.ep-radio'); + radioButtons.forEach(function (element) { + element.addEventListener('change', function () { + const dateRangePicker = element + .closest('.ep-facet-date-form') + .querySelector('.ep-date-range-picker'); + if (element.value === 'custom') { + dateRangePicker.classList.remove('is-hidden'); + } else { + dateRangePicker.classList.add('is-hidden'); + } + }); + }); + }); +}; + +domReady(initFacet); diff --git a/includes/classes/Feature/Facets/Facets.php b/includes/classes/Feature/Facets/Facets.php index b89c4b511f..a9a4ec3a69 100644 --- a/includes/classes/Feature/Facets/Facets.php +++ b/includes/classes/Feature/Facets/Facets.php @@ -58,6 +58,8 @@ public function __construct() { $types['meta'] = __NAMESPACE__ . '\Types\Meta\FacetType'; $types['meta-range'] = __NAMESPACE__ . '\Types\MetaRange\FacetType'; $types['post-type'] = __NAMESPACE__ . '\Types\PostType\FacetType'; + $types['date'] = __NAMESPACE__ . '\Types\Date\FacetType'; + } /** diff --git a/includes/classes/Feature/Facets/Types/Date/Block.php b/includes/classes/Feature/Facets/Types/Date/Block.php new file mode 100644 index 0000000000..07792396d0 --- /dev/null +++ b/includes/classes/Feature/Facets/Types/Date/Block.php @@ -0,0 +1,115 @@ + [ $this, 'render_block' ], + ] + ); + } + + /** + * Enqueue block assets. + * + * @return void + */ + public function enqueue_assets() { + wp_register_script( + 'ep-facets-date-block-view-script', + EP_URL . 'dist/js/facets-date-block-view-script.js', + Utils\get_asset_info( 'facets-date-block-view-script', 'dependencies' ), + Utils\get_asset_info( 'facets-date-block-view-script', 'version' ), + true + ); + + /** + * Filter the data passed to the date facet script. + * + * @hook ep_facets_date_script_data + * @since 5.0.0 + * @param {array} $data Data passed to the script. + * $return {array} New data passed to the script. + */ + $data = apply_filters( 'ep_facets_date_script_data', [] ); + + wp_localize_script( 'ep-facets-date-block-view-script', 'epFacetDate', $data ); + } + + /** + * Render the block. + * + * @param array $attributes Block attributes. + * @return string + */ + public function render_block( $attributes ) { + global $wp_query; + + /** This filter is documented in includes/classes/Feature/Facets/Types/Taxonomy/Block.php */ + $renderer_class = apply_filters( 'ep_facet_renderer_class', __NAMESPACE__ . '\Renderer', 'post-type', 'block', $attributes ); + + $renderer = new $renderer_class(); + + ob_start(); + + $renderer->render( [], $attributes ); + + $block_content = ob_get_clean(); + + if ( empty( $block_content ) ) { + return; + } + + $wrapper_attributes = get_block_wrapper_attributes( [ 'class' => 'wp-block-elasticpress-facet' ] ); + + return sprintf( + '
%2$s
', + wp_kses_data( $wrapper_attributes ), + $block_content + ); + } +} diff --git a/includes/classes/Feature/Facets/Types/Date/FacetType.php b/includes/classes/Feature/Facets/Types/Date/FacetType.php new file mode 100644 index 0000000000..83bae35cd1 --- /dev/null +++ b/includes/classes/Feature/Facets/Types/Date/FacetType.php @@ -0,0 +1,276 @@ +block = new Block(); + $this->block->setup(); + } + + /** + * Get the facet filter name. + * + * @return string The filter name. + */ + public function get_filter_name() : string { + /** + * Filter the facet filter name that's added to the URL + * + * @hook ep_facet_date_filter_name + * @since 5.0.0 + * @param {string} Facet filter name + * @return {string} New facet filter name + */ + return apply_filters( 'ep_facet_date_filter_name', 'ep_date_filter' ); + } + + /** + * Get the facet filter type. + * + * @return string The filter name. + */ + public function get_filter_type() : string { + /** + * Filter the facet filter type. Used by the Facet feature to organize filters. + * + * @hook ep_facet_date_filter_type + * @since 5.0.0 + * @param {string} Facet filter type + * @return {string} New facet filter type + */ + return apply_filters( 'ep_facet_date_filter_type', 'ep_date' ); + } + + /** + * Add selected filters to the Facet filter in the ES query + * + * @param array $filters Current Facet filters + * @return array + */ + public function add_query_filters( $filters ) { + $feature = Features::factory()->get_registered_feature( 'facets' ); + + $selected_filters = $feature->get_selected(); + + if ( empty( $selected_filters ) || empty( $selected_filters[ $this->get_filter_type() ] ) ) { + return $filters; + } + + $dates = $this->parse_dates( array_keys( $selected_filters[ $this->get_filter_type() ]['terms'] ) ); + + $start_date = $dates[0] ?? null; + $end_date = $dates[1] ?? null; + + if ( ! empty( $start_date ) ) { + $filters[] = [ + 'range' => [ + 'post_date' => [ + 'gte' => $start_date, + ], + ], + ]; + } + + if ( ! empty( $end_date ) ) { + $filters[] = [ + 'range' => [ + 'post_date' => [ + 'lte' => $end_date, + ], + ], + ]; + } + + return $filters; + } + + /** + * Format selected values. + * + * @param string $facet Facet name + * @param mixed $value Facet value + * @param array $filters Selected filters + * @return array + */ + public function format_selected( string $facet, $value, array $filters ) { + $terms = explode( ',', rtrim( $value, ',' ) ); + + $filters[ $this->get_filter_type() ] = [ + 'terms' => array_fill_keys( array_map( $this->get_sanitize_callback(), $terms ), true ), + ]; + + return $filters; + } + + /** + * Add selected filters to the query string. + * + * @param array $query_params Existent query parameters + * @param array $filters Selected filters + * @return array + */ + public function add_query_params( array $query_params, array $filters ) : array { + $selected = $filters[ $this->get_filter_type() ] ?? []; + + if ( ! empty( $selected['terms'] ) ) { + $query_params[ $this->get_filter_name() ] = implode( ',', array_keys( $selected['terms'] ) ); + } + + return $query_params; + } + + /** + * Get the options for the date facet. + * + * @return array The options for the date facet. + */ + public function get_facet_options() { + /** + * The options array for the date facet. + * + * Each option is an associative array with the following keys: + * - 'label': The display name of the option. + * - 'value': The relative date string for the option. This string is used to modify a DateTime object. + * It should be in a format recognized by the PHP strtotime function, e.g., '-3 months'. + * - 'urlSlug': The URL parameter for the option. + */ + $options = [ + [ + 'label' => __( 'Last 3 months', 'elasticpress' ), + 'value' => '-3 months', + 'url-param' => 'last-3-months', + ], + [ + 'label' => __( 'Last 6 months', 'elasticpress' ), + 'value' => '-6 months', + 'url-param' => 'last-6-months', + ], + [ + 'label' => __( 'Last 12 months', 'elasticpress' ), + 'value' => '-12 months', + 'url-param' => 'last-12-months', + ], + ]; + + /** + * Filter the options for the date facet. + * + * Example: + * ``` + * add_filter( + * 'ep_facet_date_options', + * function( $options ) { + * $options = [ + * [ + * 'label' => esc_html__( 'Last 7 days', 'elasticpress' ), + * 'value' => '-7 days', + * 'url-param' => 'last-7-days', + * ], + * [ + * 'label' => esc_html__( 'Last 1 month', 'elasticpress' ), + * 'value' => '-1 month', + * 'url-param' => 'last-1-month', + * ], + * [ + * 'label' => esc_html__( 'Last 6 months', 'elasticpress' ), + * 'value' => '-6 months', + * 'url-param' => 'last-6-months', + * ], + * [ + * 'label' => esc_html__( 'Last 1 year', 'elasticpress' ), + * 'value' => '-1 year', + * 'url-param' => 'last-1-year', + * ], + * [ + * 'label' => esc_html__( 'Last 5 years', 'elasticpress' ), + * 'value' => '-5 years', + * 'url-param' => 'last-5-years', + * ], + * ]; + * + * return $options; + * } + * ); + * ``` + * + * @since 5.0.0 + * + * @param {array} $options The options for the date facet. + * @return {array} The options for the date facet. + */ + return apply_filters( 'ep_facet_date_options', $options ); + } + + /** + * Parses an array of dates and returns an array of formatted dates. + * + * @param array $dates An array of dates to parse. + * + * @return array An array of formatted dates. + */ + public function parse_dates( $dates ) : array { + $options = array_column( $this->get_facet_options(), 'value', 'url-param' ); + + // Only use the first two dates. + $dates = array_slice( $dates, 0, 2 ); + + foreach ( $dates as $index => $date ) { + $date_string = isset( $options[ $date ] ) ? $options[ $date ] : $date; + + if ( empty( $date_string ) || ! strtotime( $date_string ) ) { + $formatted_dates[] = ''; + continue; + } + + $date = new \DateTime(); + $date->modify( $date_string ); + + $date_format = 1 === $index ? 'Y-m-d 23:59:59' : 'Y-m-d 00:00:00'; + $formatted_dates[] = $date->format( $date_format ); + } + + return $formatted_dates; + } + + /** + * Adds the filter name. + * + * @param array $data The data array passed to localize script. + * @return array The updated data array with the filter name added. + */ + public function add_filter_name( $data ) { + $data['dateFilterName'] = $this->get_filter_name(); + return $data; + } + +} diff --git a/includes/classes/Feature/Facets/Types/Date/Renderer.php b/includes/classes/Feature/Facets/Types/Date/Renderer.php new file mode 100644 index 0000000000..f358c6bfd7 --- /dev/null +++ b/includes/classes/Feature/Facets/Types/Date/Renderer.php @@ -0,0 +1,271 @@ +display_custom_date = $instance['displayCustomDate']; + $feature = Features::factory()->get_registered_feature( 'facets' ); + + $facet_type = $feature->types['date']; + $selected_filters = $feature->get_selected(); + $is_custom_date = $this->is_custom_date(); + $applied_dates = isset( $selected_filters[ $facet_type->get_filter_type() ]['terms'] ) ? array_keys( $selected_filters[ $facet_type->get_filter_type() ]['terms'] ) : []; + ?> + +
+ get_facet_options() as $date ) : + $is_selected = false; + $field_filters = $selected_filters; + + if ( isset( $field_filters[ $facet_type->get_filter_type() ]['terms'][ $date['url-param'] ] ) ) { + unset( $field_filters[ $facet_type->get_filter_type() ]['terms'][ $date['url-param'] ] ); + $is_selected = true; + } else { + $field_filters[ $facet_type->get_filter_type() ]['terms'] = []; + $field_filters[ $facet_type->get_filter_type() ]['terms'][ $date['url-param'] ] = $date['url-param']; + } + + $item = [ + 'label' => $date['label'], + 'is_selected' => $is_selected, + 'value' => $date['url-param'], + ]; + ?> + + get_facet_item_value_html( $item, $feature->build_query_url( $field_filters ) ); + // phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped + ?> + + + display_custom_date ) : ?> + get_facet_custom_date_item( $is_custom_date, $applied_dates ); + // phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped + ?> + + + get_facet_action_item( $applied_dates ); + // phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped + ?> +
+ ', + esc_attr( $item['value'] ), + esc_attr( $this->get_filter_name() ), + esc_attr( $checked ), + $id, + wp_kses_post( $item['label'] ), + ); + + /** + * Filter the HTML for an individual facet value. + * + * @since 5.0.0 + * @hook ep_facet_date_value_html + * @param {string} $html Facet value HTML. + * @param {array} $item Value array. It contains `value`, `label` and `is_selected`. + * @return {string} Individual facet value HTML. + */ + return apply_filters( 'ep_facet_date_value_html', $html, $item ); + } + + /** + * Returns the HTML for the facet action item. + * + * @param array $applied_dates Applied dates. + * + * @return string The HTML for the facet action item. + */ + public function get_facet_action_item( $applied_dates ) { + $filter_button = sprintf( + '', + esc_html__( 'Filter', 'elasticpress' ), + ); + + $clear_filter_link = sprintf( + '%s', + esc_url( $this->get_clear_filter_url() ), + esc_html__( 'Clear', 'elasticpress' ), + ); + + $html = sprintf( + '
%s%s
', + $filter_button, + $applied_dates ? $clear_filter_link : '', + ); + + /** + * Filter the HTML for the facet action. + * + * @since 5.0.0 + * @hook ep_facet_date_action_html + * @param {string} $html Facet action item HTML. + * @param {array} $selected_terms Selected terms. + * @return {string} Individual facet action item HTML. + */ + return apply_filters( 'ep_facet_date_action_html', $html, $applied_dates ); + } + + /** + * Returns the HTML for the facet custom date item. + * + * This method generates the HTML for the custom date range option in the date facet. + * It includes a radio button to select the custom date range option and a date picker to select the date range. + * + * @since 5.0.0 + * + * @param bool $is_custom_date Whether the selected date filter is custom. + * @param array $applied_dates Applied dates. + * @return string The HTML for the facet custom date item. + */ + public function get_facet_custom_date_item( $is_custom_date, $applied_dates ) { + $radio_button = sprintf( + '
', + esc_attr( $this->get_filter_name() ), + sprintf( 'custom-%s-%s', esc_attr( $this->get_filter_name() ), esc_attr( self::$instance_count ) ), + $is_custom_date ? 'checked' : '', + esc_html__( 'Custom', 'elasticpress' ) + ); + + $date_picker = sprintf( + '
', + ! $is_custom_date ? 'is-hidden' : '', + esc_html__( 'From:', 'elasticpress' ), + esc_attr( $this->get_filter_name() ), + esc_attr( $applied_dates[0] ?? '' ), + esc_html__( 'To:', 'elasticpress' ), + esc_attr( $applied_dates[1] ?? '' ) + ); + + $html = sprintf( + '%s%s', + $radio_button, + $date_picker, + ); + + /** + * Filter the HTML for the facet custom date. + * + * @since 5.0.0 + * @hook ep_facet_date_custom_date_html + * @param {string} $html Facet custom date item HTML. + * @param {bool} $is_custom_date Whether the selected date filter is custom. + * @param {array} $applied_dates Applied dates. + * @return {string} Individual facet custom date item HTML. + */ + return apply_filters( 'ep_facet_date_custom_date_html', $html, $is_custom_date, $applied_dates ); + } + + /** + * Returns the URL to clear the selected date filter. + * + * @return string The URL to clear the selected date filter. + */ + public function get_clear_filter_url(): string { + $feature = Features::factory()->get_registered_feature( 'facets' ); + $facet_type = $feature->types['date']; + + $selected_filters = $feature->get_selected(); + unset( $selected_filters[ $facet_type->get_filter_type() ] ); + + return $feature->build_query_url( $selected_filters ); + } + + /** + * Checks if the selected date filter is custom. If the selected date filter has more than one term, it is considered as custom + * + * @return bool True if the selected date filter is custom, false otherwise. + */ + protected function is_custom_date(): bool { + $feature = Features::factory()->get_registered_feature( 'facets' ); + + $facet_type = $feature->types['date']; + $selected_filters = $feature->get_selected(); + + if ( empty( $selected_filters[ $facet_type->get_filter_type() ] ) ) { + return false; + } + + $selected_dates = array_keys( $selected_filters[ $facet_type->get_filter_type() ]['terms'] ); + $default_options = array_column( $facet_type->get_facet_options(), 'url-param' ); + + $selected_dates = array_diff( $selected_dates, $default_options ); + return count( $selected_dates ) > 0; + } + + /** + * Get the filter name for the date facet type. + * + * @return string The filter name. + */ + protected function get_filter_name() : string { + $feature = Features::factory()->get_registered_feature( 'facets' ); + $facet_type = $feature->types['date']; + return $facet_type->get_filter_name(); + } +} diff --git a/package.json b/package.json index c7813a3d05..1785bf73b1 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,8 @@ "facets-meta-range-block-script": "./assets/js/blocks/facets/meta-range/index.js", "facets-post-type-block-script": "./assets/js/blocks/facets/post-type/index.js", "facets-meta-range-block-view-script": "./assets/js/blocks/facets/meta-range/view.js", + "facets-date-block-script": "./assets/js/blocks/facets/date/index.js", + "facets-date-block-view-script": "./assets/js/blocks/facets/date/view.js", "related-posts-block-script": "./assets/js/blocks/related-posts/index.js", "settings-script": "./assets/js/settings.js", "sync-script": "./assets/js/sync-ui/index.js", diff --git a/tests/cypress/integration/features/facets.cy.js b/tests/cypress/integration/features/facets.cy.js index ee0ee69101..03a9b017b9 100644 --- a/tests/cypress/integration/features/facets.cy.js +++ b/tests/cypress/integration/features/facets.cy.js @@ -849,4 +849,110 @@ describe('Facets Feature', { tags: '@slow' }, () => { cy.url().should('not.include', 'ep_post_type_filter=post'); }); }); + + describe('Facet by Date', () => { + it('Can insert, configure, and use the Facet by Date block', () => { + /** + * Insert a Facet block. + */ + cy.openWidgetsPage(); + cy.openBlockInserter(); + cy.getBlocksList().should('contain.text', 'Filter by Post Date'); + cy.insertBlock('Filter by Post Date'); + cy.get('.wp-block.wp-block-elasticpress-facet-date').last().as('block'); + + /** + * Verify that there are 4 options + */ + cy.get('.ep-facet-date-form .ep-facet-date-option').should('have.length', 4); + + /** + * Unselect the Custom Date option + */ + cy.get('@block').click(); + cy.openBlockSettingsSidebar(); + cy.get('.block-editor-block-inspector input[type="checkbox"]').uncheck(); + cy.intercept('/wp-json/wp/v2/block-renderer/elasticpress/facet-date*').as( + 'blockPreview', + ); + cy.wait('@blockPreview'); + + cy.get('.ep-facet-date-form .ep-facet-date-option').should('have.length', 3); + + /** + * Revert back the settings + */ + cy.get('@block').click(); + cy.get('.block-editor-block-inspector input[type="checkbox"]').check(); + + cy.wait('@blockPreview'); + + cy.get('.ep-facet-date-form .ep-facet-date-option').should('have.length', 4); + + /** + * Test that the block supports changing styles. + */ + cy.get('@block').supportsBlockColors(true); + cy.get('@block').supportsBlockTypography(true); + cy.get('@block').supportsBlockDimensions(true); + + /** + * Save widgets and visit the front page. + */ + cy.intercept('/wp-json/wp/v2/sidebars/*').as('sidebarsRest'); + cy.get('.edit-widgets-header__actions button').contains('Update').click(); + cy.wait('@sidebarsRest'); + cy.visit('/'); + + /** + * Verify the blocks have the expected output on the front-end. + */ + cy.get('.wp-block-elasticpress-facet-date').first().as('block'); + + cy.get('@block') + .find('.ep-facet-date-form .ep-facet-date-option') + .should('have.length', 4); + + cy.get('@block') + .find('.ep-facet-date-form__action-submit') + .should('exist') + .contains('Filter'); + + /** + * Verify that the block supports changing styles. + */ + cy.get('@block').supportsBlockColors(); + cy.get('@block').supportsBlockTypography(); + cy.get('@block').supportsBlockDimensions(); + + /** + * Selecting the last 3 months option should lead to the correct URL, mark the correct + */ + cy.get('@block').find('.ep-facet-date-option label').first().click(); + cy.get('@block').find('.wp-element-button').click(); + + cy.url().should('include', 'ep_date_filter=last-3-months'); + cy.get('@block') + .find('.ep-facet-date-option') + .first() + .find('input') + .should('be.checked'); + + /** + * Verify the custom date range + */ + cy.get('@block').find('.ep-facet-date-option').last().find('label').click(); + + cy.get('@block').find("[name='ep_date_filter_from']").type('2023-01-01'); + cy.get('@block').find("[name='ep_date_filter_to']").type('2023-12-31'); + cy.get('@block').find('.wp-element-button').click(); + cy.url().should('include', 'ep_date_filter=2023-01-01,2023-12-31'); + + /** + * Clear filter + */ + cy.get('@block').find('.ep-facet-date-form__action-clear').click(); + cy.url().should('not.include', 'ep_date_filter'); + }); + }); }); diff --git a/tests/php/features/TestFacetTypeDate.php b/tests/php/features/TestFacetTypeDate.php new file mode 100644 index 0000000000..f3a3df3f87 --- /dev/null +++ b/tests/php/features/TestFacetTypeDate.php @@ -0,0 +1,364 @@ +get_registered_feature( 'facets' ); + $this->facet_type = $facet_feature->types['date']; + + parent::set_up(); + } + + /** + * Test get_filter_name method. + * + * @group facets + */ + public function testGetFilterName() { + /** + * Test default behavior + */ + $this->assertEquals( 'ep_date_filter', $this->facet_type->get_filter_name() ); + + /** + * Test the `ep_facet_date_filter_name` filter + */ + $change_filter_name = function( $filter_name ) { + return $filter_name . '_'; + }; + add_filter( 'ep_facet_date_filter_name', $change_filter_name ); + $this->assertEquals( 'ep_date_filter_', $this->facet_type->get_filter_name() ); + } + + /** + * Test get_filter_type method. + * + * @group facets + */ + public function testGetFilterType() { + /** + * Test default behavior + */ + $this->assertEquals( 'ep_date', $this->facet_type->get_filter_type() ); + + /** + * Test the `ep_facet_date_filter_type` filter + */ + $change_filter_type = function( $filter_type ) { + return $filter_type . '_'; + }; + add_filter( 'ep_facet_date_filter_type', $change_filter_type ); + $this->assertEquals( 'ep_date_', $this->facet_type->get_filter_type() ); + } + + /** + * Test add_query_filters method. + * + * @group facets + */ + public function testAddQueryFilters() { + parse_str( 'ep_date_filter=2023-01-01,2023-10-01', $_GET ); + + $expected = [ + [ + 'range' => [ + 'post_date' => [ + 'gte' => '2023-01-01 00:00:00', + ], + ], + ], + [ + 'range' => [ + 'post_date' => [ + 'lte' => '2023-10-01 23:59:59', + ], + ], + ], + ]; + $this->assertSame( $expected, $this->facet_type->add_query_filters( [] ) ); + } + + /** + * Test the add_query_filters method when there is only a start date. + * + * @group facets + */ + public function testAddQueryFiltersWithOnlyStartDate() { + parse_str( 'ep_date_filter=2023-01-01,', $_GET ); + + $expected = [ + [ + 'range' => [ + 'post_date' => [ + 'gte' => '2023-01-01 00:00:00', + ], + ], + ], + ]; + $this->assertSame( $expected, $this->facet_type->add_query_filters( [] ) ); + } + + /** + * Test add_query_filters method when there is only an end date. + * + * @group facets + */ + public function testAddQueryFiltersWithOnlyEndDate() { + parse_str( 'ep_date_filter=,2023-10-01', $_GET ); + + $expected = [ + [ + 'range' => [ + 'post_date' => [ + 'lte' => '2023-10-01 23:59:59', + ], + ], + ], + ]; + $this->assertSame( $expected, $this->facet_type->add_query_filters( [] ) ); + } + + /** + * Test the format_selected method. + * + * @group facets + */ + public function testFormatSelected() { + $facet = 'test_facet'; + + $filters = []; + + // Test with start and end date. + $value = '2023-01-01,2023-12-12'; + $result = $this->facet_type->format_selected( $facet, $value, $filters ); + $this->assertArrayHasKey( 'terms', $result[ $this->facet_type->get_filter_type() ] ); + $this->assertEquals( + [ + '2023-01-01' => true, + '2023-12-12' => true, + ], + $result[ $this->facet_type->get_filter_type() ]['terms'] + ); + + // Test with only start date. + $value = '2023-12-12,'; + $result = $this->facet_type->format_selected( $facet, $value, $filters ); + $this->assertEquals( + [ + '2023-12-12' => true, + ], + $result[ $this->facet_type->get_filter_type() ]['terms'] + ); + + // Test with only end date. + $value = ',2023-12-12'; + $result = $this->facet_type->format_selected( $facet, $value, $filters ); + $this->assertEquals( + [ + '' => true, + '2023-12-12' => true, + ], + $result[ $this->facet_type->get_filter_type() ]['terms'] + ); + } + + /** + * Test the add_query_params method. + * + * @group facets + */ + public function testAddQueryParams() { + $new_filters = [ + 'ep_date' => [ + 'terms' => [ + '2023-01-01' => true, + '2023-12-12' => true, + ], + ], + ]; + $filters = $this->facet_type->add_query_params( [ 's' => 'test' ], $new_filters ); + $expected = [ + 's' => 'test', + 'ep_date_filter' => '2023-01-01,2023-12-12', + ]; + + $this->assertSame( $expected, $filters ); + } + + /** + * Test the get_facet_options method. + * + * @group facets + */ + public function testGetFacetOptions() { + $expected_result = [ + [ + 'label' => 'Last 3 months', + 'value' => '-3 months', + 'url-param' => 'last-3-months', + ], + [ + 'label' => 'Last 6 months', + 'value' => '-6 months', + 'url-param' => 'last-6-months', + ], + [ + 'label' => 'Last 12 months', + 'value' => '-12 months', + 'url-param' => 'last-12-months', + ], + ]; + + $this->assertSame( $expected_result, $this->facet_type->get_facet_options() ); + + /** + * Test the `ep_facet_date_options` filter + */ + $modified_options = [ + [ + 'label' => 'Last 1 week', + 'value' => '-1 weeks', + 'url-param' => 'last-1-week', + ], + [ + 'label' => 'Last 2 weeks', + 'value' => '-1 weeks', + 'url-param' => 'last-1-weeks', + ], + ]; + + $change_filter_type = function( $options ) use ( $modified_options ) { + return $modified_options; + }; + + add_filter( 'ep_facet_date_options', $change_filter_type ); + $this->assertSame( $modified_options, $this->facet_type->get_facet_options() ); + } + + /** + * Data provider for the parseDateDataProvider method + * + * @return array + */ + public function parseDateDataProvider() : array { + return [ + [ + [ '-1 week', '-1 month', '-1 year' ], + [ + gmdate( 'Y-m-d 00:00:00', strtotime( '-1 week' ) ), + gmdate( 'Y-m-d 23:59:59', strtotime( '-1 month' ) ), + ], + ], + [ + [ 'invalid date', '-1 month' ], + [ + '', + gmdate( 'Y-m-d 23:59:59', strtotime( '-1 month' ) ), + ], + ], + [ + [ '-1 week', '-1 month' ], + [ + gmdate( 'Y-m-d 00:00:00', strtotime( '-1 week' ) ), + gmdate( 'Y-m-d 23:59:59', strtotime( '-1 month' ) ), + ], + ], + [ + [ '2023-01-01', '2023-12-31' ], + [ + '2023-01-01 00:00:00', + '2023-12-31 23:59:59', + ], + ], + ]; + } + + /** + * Test parse_dates method. + * + * @param array $dates Array of dates to parse. + * @param array $expected Expected result. + * + * @dataProvider parseDateDataProvider + * @group facets + */ + public function testParseDates( $dates, $expected ) { + $this->assertSame( $expected, $this->facet_type->parse_dates( $dates ) ); + } + + /** + * Test WP Query integration. + * + * @group facets + */ + public function testQueryPost() { + $this->ep_factory->post->create( [ 'post_date' => '2021-12-31 23:59:59' ] ); + $this->ep_factory->post->create( [ 'post_date' => '2022-01-01 00:00:00' ] ); + $this->ep_factory->post->create( [ 'post_date' => '2022-12-31 23:59:59' ] ); + $this->ep_factory->post->create( [ 'post_date' => '2023-01-01 00:00:00' ] ); + $this->ep_factory->post->create( [ 'post_date' => '2023-06-01 23:59:59' ] ); + + ElasticPress\Elasticsearch::factory()->refresh_indices(); + + add_filter( 'ep_is_facetable', '__return_true' ); + + // get all the post between 2022-01-01 and 2022-12-31 + parse_str( 'ep_date_filter=2022-01-01,2022-12-31', $_GET ); + $query = new \WP_Query( + [ + 'ep_integrate' => true, + ] + ); + $this->assertEquals( 2, $query->found_posts ); + + // get all posts published on or after 2022-01-01. + parse_str( 'ep_date_filter=2022-01-01', $_GET ); + $query = new \WP_Query( + [ + 'ep_integrate' => true, + ] + ); + $this->assertEquals( 4, $query->found_posts ); + + // get all posts published on or before 2022-01-01. + parse_str( 'ep_date_filter=,2022-01-01', $_GET ); + $query = new \WP_Query( + [ + 'ep_integrate' => true, + ] + ); + $this->assertEquals( 2, $query->found_posts ); + + // passing invalid date shouldn't apply any filter. + parse_str( 'ep_date_filter=invalid date', $_GET ); + $query = new \WP_Query( + [ + 'ep_integrate' => true, + ] + ); + $this->assertEquals( 5, $query->found_posts ); + } +} From 7da08771ed38dc51d562722d1803ef9e1c9e9684 Mon Sep 17 00:00:00 2001 From: Burhan Nasir Date: Fri, 20 Oct 2023 19:40:11 +0500 Subject: [PATCH 2/5] Fix: Tests for WordPress 6.0 --- assets/css/facets.css | 9 ++++----- .../classes/Feature/Facets/Types/Date/Block.php | 15 ++++++++++----- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/assets/css/facets.css b/assets/css/facets.css index 3614d97e1c..437e792aa1 100644 --- a/assets/css/facets.css +++ b/assets/css/facets.css @@ -154,6 +154,7 @@ .ep-radio { appearance: none; + height: 1rem; outline: transparent; position: relative; width: 1rem; @@ -163,13 +164,11 @@ border: 1px solid #eee; border-radius: 50%; content: ""; - display: inline-block; height: 0.8rem; left: 50%; position: absolute; top: 50%; transform: translate(-50%, -50%); - vertical-align: middle; width: 0.8rem; } @@ -178,10 +177,10 @@ content: ""; display: block; height: 0.3rem; - left: 50%; + left: 50% !important; position: absolute; - top: 50%; - transform: translate(-50%, -50%); + top: 50% !important; + transform: translate(-50%, -50%) !important; width: 0.3rem; } diff --git a/includes/classes/Feature/Facets/Types/Date/Block.php b/includes/classes/Feature/Facets/Types/Date/Block.php index 07792396d0..ecf97cf17f 100644 --- a/includes/classes/Feature/Facets/Types/Date/Block.php +++ b/includes/classes/Feature/Facets/Types/Date/Block.php @@ -87,17 +87,22 @@ public function enqueue_assets() { * @return string */ public function render_block( $attributes ) { - global $wp_query; - /** This filter is documented in includes/classes/Feature/Facets/Types/Taxonomy/Block.php */ $renderer_class = apply_filters( 'ep_facet_renderer_class', __NAMESPACE__ . '\Renderer', 'post-type', 'block', $attributes ); + $renderer = new $renderer_class(); - $renderer = new $renderer_class(); + /** + * Prior to WP 6.1, if you set `viewScript` while using a `render_callback` function, + * the script was not enqueued. + * + * @see https://core.trac.wordpress.org/changeset/54367 + */ + if ( version_compare( get_bloginfo( 'version' ), '6.1', '<' ) ) { + wp_enqueue_script( 'ep-facets-date-block-view-script' ); + } ob_start(); - $renderer->render( [], $attributes ); - $block_content = ob_get_clean(); if ( empty( $block_content ) ) { From 95fcd1ea4042ced4a19520b2c7f91a7d44f8dc77 Mon Sep 17 00:00:00 2001 From: Burhan Nasir Date: Sun, 22 Oct 2023 12:37:26 +0500 Subject: [PATCH 3/5] Change html structure --- assets/css/facets.css | 66 ++++++------------- .../Feature/Facets/Types/Date/Renderer.php | 17 +---- 2 files changed, 23 insertions(+), 60 deletions(-) diff --git a/assets/css/facets.css b/assets/css/facets.css index 437e792aa1..22a9f6e8ca 100644 --- a/assets/css/facets.css +++ b/assets/css/facets.css @@ -148,53 +148,29 @@ } .ep-facet-date-option { - align-items: center; - display: flex; -} - -.ep-radio { - appearance: none; - height: 1rem; - outline: transparent; - position: relative; - width: 1rem; - - &::before { - background: transparent; - border: 1px solid #eee; - border-radius: 50%; - content: ""; - height: 0.8rem; - left: 50%; - position: absolute; - top: 50%; - transform: translate(-50%, -50%); - width: 0.8rem; - } - &::after { - border-radius: 50%; - content: ""; - display: block; - height: 0.3rem; - left: 50% !important; - position: absolute; - top: 50% !important; - transform: translate(-50%, -50%) !important; - width: 0.3rem; - } - - &:checked { - background: transparent; - outline-color: transparent; - - &::before { - background: #5e5e5e; - border-color: inherit; - } + & label { + align-items: center; + display: flex; - &::after { - background: #eee; + & .ep-radio { + appearance: none; + border: 1px solid #eee; + border-radius: 50%; + height: 1em; + margin-right: 0.25em; + outline: transparent; + position: relative; + width: 1em; + + &:checked { + background-color: transparent; + border: 5px solid #5e5e5e; + + &::after { + opacity: 0; + } + } } } } diff --git a/includes/classes/Feature/Facets/Types/Date/Renderer.php b/includes/classes/Feature/Facets/Types/Date/Renderer.php index f358c6bfd7..094ef33a9e 100644 --- a/includes/classes/Feature/Facets/Types/Date/Renderer.php +++ b/includes/classes/Feature/Facets/Types/Date/Renderer.php @@ -26,15 +26,6 @@ class Renderer extends \ElasticPress\Feature\Facets\Renderer { */ protected $display_custom_date; - /** - * The number of instances of the class. - * - * This is used to generate unique IDs for the HTML elements. - * - * @var int - */ - public static $instance_count = 1; - /** * Output the widget or block HTML. * @@ -98,7 +89,6 @@ public function render( $args, $instance ) { // Enqueue Script & Styles wp_enqueue_script( 'elasticpress-facets' ); wp_enqueue_style( 'elasticpress-facets' ); - self::$instance_count++; } /** @@ -111,14 +101,12 @@ public function render( $args, $instance ) { */ public function get_facet_item_value_html( $item, string $url ): string { $checked = $item['is_selected'] ? 'checked' : ''; - $id = sprintf( 'option-%s-%s', esc_attr( $item['value'] ), self::$instance_count ); $html = sprintf( - '
', + '
', esc_attr( $item['value'] ), esc_attr( $this->get_filter_name() ), esc_attr( $checked ), - $id, wp_kses_post( $item['label'] ), ); @@ -185,9 +173,8 @@ public function get_facet_action_item( $applied_dates ) { */ public function get_facet_custom_date_item( $is_custom_date, $applied_dates ) { $radio_button = sprintf( - '
', + '
', esc_attr( $this->get_filter_name() ), - sprintf( 'custom-%s-%s', esc_attr( $this->get_filter_name() ), esc_attr( self::$instance_count ) ), $is_custom_date ? 'checked' : '', esc_html__( 'Custom', 'elasticpress' ) ); From 7a2d34d56426de8e3879d669f1544d4baf5af79a Mon Sep 17 00:00:00 2001 From: Burhan Nasir Date: Mon, 23 Oct 2023 18:21:25 +0500 Subject: [PATCH 4/5] Js error --- assets/js/blocks/facets/date/view.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/assets/js/blocks/facets/date/view.js b/assets/js/blocks/facets/date/view.js index 0922f55c5a..c646626e42 100644 --- a/assets/js/blocks/facets/date/view.js +++ b/assets/js/blocks/facets/date/view.js @@ -17,13 +17,15 @@ const initFacet = () => { event.preventDefault(); const { value } = this.querySelector(`[name="${filterName}"]:checked`); - const { value: startDateValue } = this.querySelector( - '.ep-date-range-picker', - ).querySelector(`[name="${filterName}_from"]`); + const { value: startDateValue } = + this.querySelector('.ep-date-range-picker')?.querySelector( + `[name="${filterName}_from"]`, + ) || ''; - const { value: endDateValue } = this.querySelector( - '.ep-date-range-picker', - ).querySelector(`[name="${filterName}_to"]`); + const { value: endDateValue } = + this.querySelector('.ep-date-range-picker')?.querySelector( + `[name="${filterName}_to"]`, + ) || ''; const currentURL = window.location.href; const newUrl = new URL(currentURL); @@ -44,9 +46,9 @@ const initFacet = () => { .closest('.ep-facet-date-form') .querySelector('.ep-date-range-picker'); if (element.value === 'custom') { - dateRangePicker.classList.remove('is-hidden'); + dateRangePicker?.classList.remove('is-hidden'); } else { - dateRangePicker.classList.add('is-hidden'); + dateRangePicker?.classList.add('is-hidden'); } }); }); From c7e05233f2ec13bd7113a1a442135fcd4b290125 Mon Sep 17 00:00:00 2001 From: Burhan Nasir Date: Mon, 23 Oct 2023 18:57:57 +0500 Subject: [PATCH 5/5] Add additonal tests --- .../cypress/integration/features/facets.cy.js | 57 ++++++++++++------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/tests/cypress/integration/features/facets.cy.js b/tests/cypress/integration/features/facets.cy.js index d62b8d7fdd..8fe38db611 100644 --- a/tests/cypress/integration/features/facets.cy.js +++ b/tests/cypress/integration/features/facets.cy.js @@ -876,28 +876,8 @@ describe('Facets Feature', { tags: '@slow' }, () => { */ cy.get('.ep-facet-date-form .ep-facet-date-option').should('have.length', 4); - /** - * Unselect the Custom Date option - */ cy.get('@block').click(); cy.openBlockSettingsSidebar(); - cy.get('.block-editor-block-inspector input[type="checkbox"]').uncheck(); - cy.intercept('/wp-json/wp/v2/block-renderer/elasticpress/facet-date*').as( - 'blockPreview', - ); - cy.wait('@blockPreview'); - - cy.get('.ep-facet-date-form .ep-facet-date-option').should('have.length', 3); - - /** - * Revert back the settings - */ - cy.get('@block').click(); - cy.get('.block-editor-block-inspector input[type="checkbox"]').check(); - - cy.wait('@blockPreview'); - - cy.get('.ep-facet-date-form .ep-facet-date-option').should('have.length', 4); /** * Test that the block supports changing styles. @@ -963,6 +943,43 @@ describe('Facets Feature', { tags: '@slow' }, () => { */ cy.get('@block').find('.ep-facet-date-form__action-clear').click(); cy.url().should('not.include', 'ep_date_filter'); + + cy.openWidgetsPage(); + cy.openBlockInserter(); + + /** + * Unselect the Custom Date option + */ + cy.get('@block').click(); + cy.openBlockSettingsSidebar(); + cy.get('.block-editor-block-inspector input[type="checkbox"]').uncheck(); + cy.intercept('/wp-json/wp/v2/block-renderer/elasticpress/facet-date*').as( + 'blockPreview', + ); + cy.wait('@blockPreview'); + + cy.get('.ep-facet-date-form .ep-facet-date-option').should('have.length', 3); + + /** + * Save widgets and visit the front page. + */ + cy.intercept('/wp-json/wp/v2/widgets*').as('widgetsRest'); + cy.get('.edit-widgets-header__actions button').contains('Update').click(); + cy.wait('@widgetsRest'); + cy.visit('/'); + + /** + * Click on the last option and check its last-12-months. + */ + cy.get('@block').find('.ep-facet-date-option label').last().click(); + cy.get('@block').find('.wp-element-button').click(); + + cy.url().should('include', 'ep_date_filter=last-12-months'); + cy.get('@block') + .find('.ep-facet-date-option') + .last() + .find('input') + .should('be.checked'); }); }); });