Skip to content

Commit

Permalink
Add number range search options (#2020)
Browse files Browse the repository at this point in the history
  • Loading branch information
doekenorg authored Apr 11, 2024
2 parents c98f42b + c7bfc7b commit a9dc6c4
Show file tree
Hide file tree
Showing 10 changed files with 310 additions and 64 deletions.
34 changes: 34 additions & 0 deletions assets/js/fe-views.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ jQuery( function ( $ ) {
$( '.gv-search-clear' ).on( 'click', this.clear_search );

$( 'a.gv-sort' ).on( 'click', this.multiclick_sort );

this.number_range();
},

/**
Expand Down Expand Up @@ -143,6 +145,38 @@ jQuery( function ( $ ) {
e.preventDefault();
location.href = $( this ).data( 'multisort-href' );
}
},

/**
* Client side logic to prevent invalid search values.
* @since $ver$
*/
number_range() {
$( '.gv-search-number-range' )
.on( 'change', 'input', function () {
const $name = $( this ).attr( 'name' );
const current_type = $name.includes( 'max' ) ? 'max' : 'min';
const other_type = 'max' === current_type ? 'min' : 'max';
const $other = $( this )
.closest( '.gv-search-number-range' )
.find( 'input[name="' + $name.replace( /(min|max)/, other_type ) + '"]' );

// Push to end of the stack to avoid timing issues.
setTimeout( function () {
if ( $( this ).attr( other_type ) && '' !== $( this ).val() ) {
const value = parseFloat( $( this ).val() );

if ( 'max' === current_type && value < parseFloat( $( this ).attr( 'min' ) ) ) {
$( this ).val( $( this ).attr( 'min' ) );
} else if ( 'min' === current_type && value > parseFloat( $( this ).attr( 'max' ) ) ) {
$( this ).val( $( this ).attr( 'max' ) );
}
}

$other.attr( current_type, $( this ).val() );
}.bind( this ), 2 );
} )
.find( 'input' ).trigger( 'change' ); // Initial trigger.
}
};

Expand Down
2 changes: 1 addition & 1 deletion assets/js/fe-views.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

142 changes: 121 additions & 21 deletions includes/widgets/search-widget/class-search-widget.php
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ public static function get_input_types_by_field_type() {
$input_types = array(
'text' => array( 'input_text' ),
'address' => array( 'input_text' ),
'number' => array( 'input_text' ),
'number' => array( 'input_text', 'number_range' ),
'date' => array( 'date', 'date_range' ),
'boolean' => array( 'single_checkbox' ),
'select' => array( 'select', 'radio', 'link' ),
Expand All @@ -226,7 +226,7 @@ public static function get_input_types_by_field_type() {
// hybrids
'created_by' => array( 'select', 'radio', 'checkbox', 'multiselect', 'link', 'input_text' ),
'multi_text' => array( 'select', 'radio', 'checkbox', 'multiselect', 'link', 'input_text' ),
'product' => array( 'select', 'radio', 'link', 'input_text' ),
'product' => array( 'select', 'radio', 'link', 'input_text', 'number_range' ),
);

/**
Expand Down Expand Up @@ -263,6 +263,7 @@ public static function get_search_input_labels() {
'single_checkbox' => esc_html__( 'Checkbox', 'gk-gravityview' ),
'link' => esc_html__( 'Links', 'gk-gravityview' ),
'date_range' => esc_html__( 'Date range', 'gk-gravityview' ),
'number_range' => esc_html__( 'Number range', 'gk-gravityview' ),
);

/**
Expand Down Expand Up @@ -471,7 +472,7 @@ public static function get_search_input_types( $field_id = '', $field_type = nul
$input_type = 'select';
} elseif ( in_array( $field_type, array( 'date' ) ) || in_array( $field_id, array( 'payment_date' ) ) ) {
$input_type = 'date';
} elseif ( in_array( $field_type, array( 'number' ) ) || in_array( $field_id, array( 'payment_amount' ) ) ) {
} elseif ( in_array( $field_type, array( 'number', 'quantity', 'total' ) ) || in_array( $field_id, array( 'payment_amount' ) ) ) {
$input_type = 'number';
} elseif ( in_array( $field_type, array( 'product' ) ) ) {
$input_type = 'product';
Expand Down Expand Up @@ -838,9 +839,10 @@ public function filter_entries( $search_criteria, $form_id = null, $args = array
if ( isset( $filter[0]['value'] ) ) {
$filter[0]['value'] = $trim_search_value ? trim( $filter[0]['value'] ) : $filter[0]['value'];

unset($filter['operator']);
$search_criteria['field_filters'] = array_merge( $search_criteria['field_filters'], $filter );

// if date range type, set search mode to ALL
// if range type, set search mode to ALL
if ( ! empty( $filter[0]['operator'] ) && in_array( $filter[0]['operator'], array( '>=', '<=', '>', '<' ) ) ) {
$mode = 'all';
}
Expand Down Expand Up @@ -1022,6 +1024,7 @@ public function gf_query_filter( &$query, $view, $request ) {
unset( $search_criteria['field_filters'][ $key ] );
}
}
unset( $filter );

if ( ! empty( $search_criteria['start_date'] ) || ! empty( $search_criteria['end_date'] ) ) {
$date_criteria = array();
Expand All @@ -1042,7 +1045,7 @@ public function gf_query_filter( &$query, $view, $request ) {
$search_conditions = array();

if ( $filters = array_filter( $search_criteria['field_filters'] ) ) {
foreach ( $filters as &$filter ) {
foreach ( $filters as $filter ) {
if ( ! is_array( $filter ) ) {
continue;
}
Expand All @@ -1067,27 +1070,78 @@ public function gf_query_filter( &$query, $view, $request ) {
$search_conditions[] = $search_condition;
} else {
$left = $search_condition->left;

// When casting a column value to a certain type (e.g., happens with the Number field), GF_Query_Column is wrapped in a GF_Query_Call class.
if ( $left instanceof GF_Query_Call ) {
try {
$reflectionProperty = new \ReflectionProperty( $left, '_parameters' );
$reflectionProperty->setAccessible( true );
if ( $left instanceof GF_Query_Call && $left->parameters ) {
// Update columns to include the correct alias.
$parameters = array_map( static function ( $parameter ) use ( $query ) {
return $parameter instanceof GF_Query_Column
? new GF_Query_Column(
$parameter->field_id,
$parameter->source,
$query->_alias( $parameter->field_id, $parameter->source, $parameter->is_entry_column() ? 't' : 'm' )
)
: $parameter;
}, $left->parameters );

$left = new GF_Query_Call( $left->function_name, $parameters );
} else {
$alias = $query->_alias( $left->field_id, $left->source, $left->is_entry_column() ? 't' : 'm' );
$left = new GF_Query_Column( $left->field_id, $left->source, $alias );
}

if ( $this->is_product_field( $filter ) ) {
$original_left = clone $left;
$column = $left instanceof GF_Query_Call ? $left->columns[0] ?? null : $left;
$column_name = sprintf( '`%s`.`%s`', $column->alias, $column->is_entry_column() ? $column->field_id : 'meta_value' );

// Add the original join back.
$search_conditions[] = new GF_Query_Condition( $column, null, $column );

// Split product name for.
$position = new GF_Query_Call( 'POSITION', [ sprintf( '"|" IN %s', $column_name ) ] );
$left = new GF_Query_Call( 'SUBSTR', [
$column_name,
sprintf( "%s + 1", $position->sql( $query ) ),
] );

// Remove currency symbol and format properly.
$currency = RGCurrency::get_currency( GFCommon::get_currency() );
$symbol = html_entity_decode( rgar( $currency, 'symbol_left' ) );
$thousand_separator = rgar( $currency, 'thousand_separator' );
$decimal_separator = rgar( $currency, 'decimal_separator' );

$replacements = [ $symbol => '', $thousand_separator => '' ];
if ( ',' === $decimal_separator ) {
$replacements[','] = '.';
}

$value = $reflectionProperty->getValue( $left );
foreach ( $replacements as $key => $value ) {
$left = new GF_Query_Call( 'REPLACE', [
$left->sql( $query ),
'"' . $key . '"',
'"' . $value . '"',
] );
}

if ( ! empty( $value[0] ) && $value[0] instanceof GF_Query_Column ) {
$left = $value[0];
} else {
continue;
// Return original function call.
if ( $original_left instanceof GF_Query_Call ) {
$parameters = $original_left->parameters;
$function_name = $original_left->function_name;

$parameters[0] = $left->sql( $query );
if ( $function_name === 'CAST' ) {
$function_name = ' ' . $function_name; // prevent regular `CAST` sql.
if ( GF_Query::TYPE_DECIMAL === ( $parameters[1] ?? '' ) ) {
$parameters[1] = 'DECIMAL(65,6)';
}
// CAST needs 'AND' as a separator.
$parameters = [ implode( ' AS ', $parameters ) ];
}
} catch ( ReflectionException $e ) {
continue;

$left = new GF_Query_Call( $function_name, $parameters );
}
}

$alias = $query->_alias( $left->field_id, $left->source, $left->is_entry_column() ? 't' : 'm' );

if ( $view->joins && GF_Query_Column::META == $left->field_id ) {
foreach ( $view->joins as $_join ) {
$on = $_join->join_on;
Expand All @@ -1110,7 +1164,7 @@ public function gf_query_filter( &$query, $view, $request ) {
}
} else {
$search_conditions[] = new GF_Query_Condition(
new GF_Query_Column( $left->field_id, $left->source, $alias ),
$left,
$search_condition->operator,
$search_condition->right
);
Expand All @@ -1119,7 +1173,9 @@ public function gf_query_filter( &$query, $view, $request ) {
}

if ( $search_conditions ) {
$search_conditions = array( call_user_func_array( '\GF_Query_Condition::' . ( 'all' == $mode ? '_and' : '_or' ), $search_conditions ) );
$search_conditions = 'all' === $mode
? [ GF_Query_Condition::_and( ...$search_conditions ) ]
: [ GF_Query_Condition::_or( ...$search_conditions ) ];
}
}

Expand Down Expand Up @@ -1178,6 +1234,20 @@ public function gf_query_filter( &$query, $view, $request ) {
$query->where( $where );
}

/**
* Whether the field in the filter is a product field.
* @since $ver$
*
* @param array $filter The filter object.
*
* @return bool
*/
private function is_product_field( array $filter ): bool {
$field = GFAPI::get_field( $filter['form_id'] ?? 0, $filter['key'] ?? 0 );

return $field && \GFCommon::is_product_field( $field->type );
}

/**
* Convert $_GET/$_POST key to the field/meta ID
*
Expand Down Expand Up @@ -1447,6 +1517,29 @@ public function prepare_field_filter( $filter_key, $value, $view, $searchable_fi
$filter['operator'] = 'contains';
}

break;
case 'number':
case 'quantity':
case 'product':
case 'total':
if ( is_array( $value ) ) {
$filter = []; // Reset the filter.

$min = $value['min'] ?? null; // Can't trust `rgar` here.
$max = $value['max'] ?? null;

if ( is_numeric( $min ) && is_numeric( $max ) && $min > $max) {
// Reverse the polarity!
[$min, $max] = [$max, $min];
}

if ( is_numeric( $min ) ) {
$filter[] = [ 'key' => $field_id, 'operator' => '>=', 'value' => $min, 'is_numeric' => true ];
}
if ( is_numeric( $max ) ) {
$filter[] = [ 'key' => $field_id, 'operator' => '<=', 'value' => $max, 'is_numeric' => true ];
}
}
break;
} // switch field type

Expand Down Expand Up @@ -1812,6 +1905,13 @@ private function get_search_filter_details( $field, $context, $widget_args ) {
);
}

if ( 'number_range' === $field['input'] && empty( $value ) ) {
$filter['value'] = array(
'min' => '',
'max' => '',
);
}

if ( 'created_by' === $field['field'] ) {
$filter['choices'] = self::get_created_by_choices( ( isset( $context->view ) ? $context->view : null ) );
$filter['type'] = 'created_by';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php
/**
* Display the search by numeric range.
*
* @file class-search-widget.php See for usage
*/

$gravityview_view = GravityView_View::getInstance();
$view_id = $gravityview_view->getViewId();
$value = $gravityview_view->search_field['value'];
$label = $gravityview_view->search_field['label'];
$name = $gravityview_view->search_field['name'];

$min = $value['min'] ?? null; // Can't trust `rgar` here.
$max = $value['max'] ?? null;

$error = null;
if ( is_numeric( $min ) && is_numeric( $max ) && $min > $max ) {
$error = esc_html__( 'The "from" value is lower than the "to" value.', 'gk-gravityview' );
}

$is_currency = 'total' === $gravityview_view->search_field['type'];
if ( ! $is_currency ) {
// could still be currency from the field.
$field = GVCommon::get_field( $gravityview_view->getForm() ?? [], $gravityview_view->search_field['key'] );
$is_currency = 'currency' === $field->numberFormat;
}

/**
* Modify the step value for the input fields.
*
* @filter gk/gravityview/search/number-range/step
*
* @since $ver$
*
* @param string $value The step size.
* @param GravityView_View $gravityview_view The view object.
*/
$step = apply_filters(
'gk/gravityview/search/number-range/step',
'quantity' === $gravityview_view->search_field['type'] ? '1' : 'any',
$gravityview_view
);
?>

<div class="gv-search-box gv-search-number gv-search-number-range">
<?php if ( ! gv_empty( $label, false, false ) ) { ?>
<label for="search-box-<?php echo esc_attr( $name ) . '-start'; ?>">
<?php echo esc_html( $label ) . ( $is_currency ? ' (' . GFCommon::get_currency() . ')' : '' ); ?>
</label>
<?php } ?>
<p>
<input name="<?php echo esc_attr( $name ) . '[min]'; ?>"
id="search-box-<?php echo esc_attr( $name ) . '-min'; ?>"
type="number"
placeholder="<?php esc_attr_e( 'From', 'gk-gravityview' ); ?>"
step="<?php echo esc_attr( $step ); ?>"
value="<?php echo esc_attr( $min ); ?>">

<input name="<?php echo esc_attr( $name ) . '[max]'; ?>"
id="search-box-<?php echo esc_attr( $name ) . '-max'; ?>"
type="number"
placeholder="<?php esc_attr_e( 'To', 'gk-gravityview' ); ?>"
step="<?php echo esc_attr( $step ); ?>"
value="<?php echo esc_attr( $max ); ?>">
</p>
<?php if ( $error ) {
printf( '<p class="error">%s</p>', $error );
} ?>
</div>
Loading

0 comments on commit a9dc6c4

Please sign in to comment.