Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Porting essential blacklight range limit functionality #3098

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<%= render(@layout.new(facet_field: @facet_field)) do |component| %>
<% component.with_label do %>
<%= @facet_field.label %>
<% end %>

<% component.with_body do %>
<%# Don't display form if the missing facet is selected. Otherwise provide
the form as an easy way for users to updated the range. %>
<% unless @facet_field.missing_selected? %>
<%= render Blacklight::FacetFieldListRangeFormComponent.new(facet_field: @facet_field) %>
<% end %>

<ul class="facet-values list-unstyled">
<%= render facet_items %>
</ul>
<% end %>
<% end %>
33 changes: 33 additions & 0 deletions app/components/blacklight/facet_field_list_range_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# frozen_string_literal: true

module Blacklight
class FacetFieldListRangeComponent < Blacklight::Component
# @param [Blacklight::FacetFieldRangePresenter] facet_field
def initialize(facet_field:, layout: nil)
@facet_field = facet_field
@layout = layout == false ? FacetFieldNoLayoutComponent : Blacklight::FacetFieldComponent
end

def facet_items(wrapping_element: :li, **item_args)
facet_item_component_class.with_collection(facet_item_presenters, wrapping_element: wrapping_element, **item_args)
end

def facet_item_presenters
@facet_field.paginator.items.map do |item|
facet_item_presenter(item)
end
end

def facet_item_presenter(facet_item, deprecated_facet_config = nil, facet_field = nil)
(deprecated_facet_config || facet_config).item_presenter.new(facet_item, deprecated_facet_config || facet_config, helpers, facet_field || @facet_field.key)
end

def facet_item_component_class(deprecated_facet_config = nil)
(deprecated_facet_config || facet_config).item_component
end

def facet_config
@facet_field.facet_field
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<%= form_tag search_action_path, method: :get, class: ['range_limit subsection form-inline', "range_#{@facet_field.key} d-flex justify-content-center"].join(' ') do %>
<%= render hidden_search_state %>

<div class="input-group input-group-sm mb-3 flex-nowrap range-limit-input-group">
<%= render_range_input(:start, start_label) %>
<%= render_range_input(:end, end_label) %>
<div class="input-group-append visually-hidden">
<%= submit_tag t('blacklight.search.facets.range.form.submit'), class: 'submit btn btn-secondary', name: nil %>
</div>
<%= submit_tag t('blacklight.search.facets.range.form.submit'), class: "submit btn btn-secondary sr-only", "aria-hidden": "true", name: nil %>
</div>
<% end %>
59 changes: 59 additions & 0 deletions app/components/blacklight/facet_field_list_range_form_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# frozen_string_literal: true

module Blacklight
class FacetFieldListRangeFormComponent < Blacklight::Component
delegate :search_action_path, to: :helpers

def initialize(facet_field:)
@facet_field = facet_field
end

def start_label
t('blacklight.search.facets.range.form.start_label', field_label: @facet_field.label)
end

def end_label
t('blacklight.search.facets.range.form.end_label', field_label: @facet_field.label)
end

def input_options
return {} unless range_config

range_config.fetch(:input, {})
.slice(:min, :max, :placeholder, :step)
end

# type is 'start' or 'end'
def render_range_input(type, input_label = nil)
type = type.to_s

default = if @facet_field.selected_range.is_a?(Range)
case type
when 'start' then @facet_field.selected_range.first
when 'end' then @facet_field.selected_range.last
end
end
html = number_field_tag("range[#{@facet_field.key}][#{type}]", default, class: "form-control text-center range_#{type}", **input_options)
html += label_tag("range[#{@facet_field.key}][#{type}]", input_label, class: 'sr-only visually-hidden') if input_label.present?
html
end

private

##
# the form needs to serialize any search parameters, including other potential range filters,
# as hidden fields. The parameters for this component's range filter are serialized as number
# inputs, and should not be in the hidden params.
# @return [Blacklight::HiddenSearchStateComponent]
def hidden_search_state
hidden_search_params = @facet_field.search_state.params_for_search.except(:utf8, :page)
hidden_search_params[:range]&.except!(@facet_field.key)
Blacklight::HiddenSearchStateComponent.new(params: hidden_search_params)
end

def range_config
config = @facet_field.facet_field.range
config == true ? {} : config
end
end
end
40 changes: 40 additions & 0 deletions app/presenters/blacklight/facet_field_range_presenter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# frozen_string_literal: true

module Blacklight
class FacetFieldRangePresenter < Blacklight::FacetFieldPresenter
delegate :response, to: :display_facet
delegate :blacklight_config, to: :search_state

# Paginator will return the selected item or if no facet is selected, the [Missing] facet.
def paginator
return unless display_facet

@paginator ||= blacklight_config.facet_paginator_class.new(
Array.wrap(selected_item || display_facet.items.select(&:missing)),
sort: display_facet.sort,
offset: display_facet.offset,
prefix: display_facet.prefix,
limit: facet_limit
)
end

def selected_range
values&.first
end

# Wraps selected range in Blacklight::Solr::Response::Facets::FacetItem object.
#
# @return [Blacklight::Solr::Response::Facets::FacetItem] if range is selected
# @return [NilClass] if no range is selected
def selected_item
return unless selected_range

Blacklight::Solr::Response::Facets::FacetItem.new(value: selected_range, hits: response.total)
end

# Returns true if [Missing] facet is selected.
def missing_selected?
selected_range == Blacklight::SearchState::FilterField::MISSING
end
end
end
28 changes: 28 additions & 0 deletions app/presenters/blacklight/facet_item_range_presenter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# frozen_string_literal: true

module Blacklight
# Override the default item presenter to provide custom labels for
# range data.
class FacetItemRangePresenter < Blacklight::FacetItemPresenter
# Overriding method to generate a more descriptive label
def label
label_for_range || super
end

private

def label_for_range
return unless value.is_a? Range

view_context.t(range_limit_label_key, start: value.first, end: value.last)
end

def range_limit_label_key
if value.first == value.last
'blacklight.search.facets.range.single_value'
else
'blacklight.search.facets.range.range_value'
end
end
end
end
7 changes: 7 additions & 0 deletions config/locales/blacklight.ar.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,13 @@ ar:
count: ترتيب رقمي
index: ترتيب أبجدي
title: تحديد نطاق البحث
range:
form:
start_label: "%{field_label} بداية المدة"
end_label: "%{field_label} نهاية المدة"
submit: 'تطبيق'
single_value: '%{begin}'
range_value: '%{begin} الى %{end}'
filters:
label: "%{label}:"
remove:
Expand Down
7 changes: 7 additions & 0 deletions config/locales/blacklight.de.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,13 @@ de:
count: Numerisch ordnen
index: A-Z Ordnen
title: Suche beschränken
range:
form:
start_label: "%{field_label} Bereichsanfang"
end_label: "%{field_label} Bereichsende"
submit: 'Anwenden'
single_value: '%{begin}'
range_value: '%{begin} bis %{end}'
filters:
label: "%{label}:"
remove:
Expand Down
7 changes: 7 additions & 0 deletions config/locales/blacklight.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,13 @@ en:
toggle: Toggle facets
open: Show facets
close: Hide facets
range:
form:
start_label: "%{field_label} range start"
end_label: "%{field_label} range end"
submit: 'Apply'
single_value: "%{start}"
range_value: "%{start} to %{end}"
group:
more: 'more »'
filters:
Expand Down
7 changes: 7 additions & 0 deletions config/locales/blacklight.it.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,13 @@ it:
count: Ordina per numero
index: Ordina A-Z
title: Affina la ricerca
range:
form:
start_label: "%{field_label} da"
end_label: "%{field_label} a"
submit: 'Invia'
single_value: '%{begin}'
range_value: '%{begin} a %{end}'
filters:
label: "%{label}:"
remove:
Expand Down
9 changes: 9 additions & 0 deletions lib/blacklight/configuration/facet_field.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ def normalize! blacklight_config = nil
query.stringify_keys! if query

normalize_pivot_config! if pivot
normalize_range_config! if range
self.collapse = true if collapse.nil?
self.show = true if show.nil?
self.if = show if self.if.nil?
Expand Down Expand Up @@ -98,5 +99,13 @@ def normalize_pivot_config!
self.filter_class ||= Blacklight::SearchState::PivotFilterField
self.filter_query_builder ||= Blacklight::SearchState::PivotFilterField::QueryBuilder
end

def normalize_range_config!
self.presenter ||= Blacklight::FacetFieldRangePresenter
self.item_presenter ||= Blacklight::FacetItemRangePresenter
self.component ||= Blacklight::FacetFieldListRangeComponent
self.filter_class ||= Blacklight::SearchState::RangeFilterField
self.solr_params = (solr_params || {}).merge({ 'facet.missing' => true })
end
end
end
1 change: 1 addition & 0 deletions lib/blacklight/search_state.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require 'blacklight/search_state/filter_field'
require 'blacklight/search_state/pivot_filter_field'
require 'blacklight/search_state/range_filter_field'

module Blacklight
# This class encapsulates the search state as represented by the query
Expand Down
84 changes: 84 additions & 0 deletions lib/blacklight/search_state/range_filter_field.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# frozen_string_literal: true

module Blacklight
class SearchState
# Modeling access to filter query parameters
class RangeFilterField < FilterField
# this accessor is unnecessary after Blacklight 7.25.0
attr_accessor :filters_key

def initialize(config, search_state)
super
@filters_key = :range
end

# @param [String,#value] a filter item to add to the url
# @return [Blacklight::SearchState] new state
def add(item)
new_state = search_state.reset_search
params = new_state.params
value = as_url_parameter(item)

if value.is_a? Range
param_key = filters_key
params[param_key] = (params[param_key] || {}).dup
params[param_key][config.key] = { start: value.first, end: value.last }
new_state.reset(params)
else
super
end
end

# @param [String,#value] a filter to remove from the url
# @return [Blacklight::SearchState] new state
def remove(item)
new_state = search_state.reset_search
params = new_state.params
value = as_url_parameter(item)

if value.is_a? Range
param_key = filters_key
params[param_key] = (params[param_key] || {}).dup
params[param_key]&.delete(config.key)
new_state.reset(params)
else
super
end
end

# @return [Array] an array of applied filters
def values(except: [])
params = search_state.params
param_key = filters_key

range = if params.dig(param_key, config.key).is_a? Range
params.dig(param_key, config.key)
elsif params.dig(param_key, config.key).is_a? Hash
b_bound = params.dig(param_key, config.key, :start).presence
e_bound = params.dig(param_key, config.key, :end).presence
Range.new(b_bound&.to_i, e_bound&.to_i) if b_bound && e_bound
end

f = except.include?(:filters) ? [] : [range].compact
f_missing = [Blacklight::SearchState::FilterField::MISSING] if params.dig(filters_key, "-#{key}")&.any? { |v| v == Blacklight::Engine.config.blacklight.facet_missing_param }
f_missing = [] if except.include?(:missing)

f + (f_missing || [])
end

# @param [String,#value] a filter to remove from the url
# @return [Boolean] whether the provided filter is currently applied/selected
delegate :include?, to: :values

# @since Blacklight v7.25.2
# normal filter fields demangle when they encounter a hash, which they assume to be a number-indexed map
# this filter should allow (expect) hashes if the keys include 'start' or 'end'
def permitted_params
{
filters_key => { config.key => [:start, :end], "-#{config.key}" => [] },
inclusive_filters_key => { config.key => [:start, :end] }
}
end
end
end
end
Loading
Loading