Skip to content

Commit

Permalink
Add a way to filter facets in the facets "more" modal (#3367)
Browse files Browse the repository at this point in the history
* Facet typeahead: back end

This creates a new endpoint at /catalog/facet/<facet_name>/<query_fragment>

Co-authored-by: Isha Sinha <[email protected]>

* Facet typeahead: front end

Co-authored-by: Christina Chortaria <[email protected]>

* bundle exec i18n-tasks add-missing

* Incorporate feedback from review

Rather than two very similar controller actions for facets vs. facet_suggest,
this commit combines them into a single controller action.

---------

Co-authored-by: Isha Sinha <[email protected]>
Co-authored-by: Christina Chortaria <[email protected]>
  • Loading branch information
3 people committed Oct 16, 2024
1 parent 6ddb0ec commit 46452d2
Show file tree
Hide file tree
Showing 31 changed files with 277 additions and 5 deletions.
1 change: 1 addition & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ Metrics/BlockLength:

Metrics/ClassLength:
Exclude:
- "app/services/blacklight/search_service.rb"
- "lib/blacklight/configuration.rb"
- "lib/blacklight/search_builder.rb"
- "lib/blacklight/search_state.rb"
Expand Down
9 changes: 9 additions & 0 deletions app/components/blacklight/search/facet_suggest_input.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<label for="facet-suggest-<%= facet.key %>">
<%= I18n.t('blacklight.search.facets.suggest.label', field_label: presenter&.label) %>
</label>
<input class="facet-suggest form-control"
id="facet-suggest-<%= facet.key %>"
data-facet-field="<%= facet.key %>"
name="facet_suggest_<%= facet.key %>"
placeholder="<%= I18n.t('blacklight.search.form.search.placeholder') %>">
</input>
16 changes: 16 additions & 0 deletions app/components/blacklight/search/facet_suggest_input.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# frozen_string_literal: true

module Blacklight
module Search
class FacetSuggestInput < Blacklight::Component
def initialize(facet:, presenter:)
@facet = facet
@presenter = presenter
end

private

attr_accessor :facet, :presenter
end
end
end
9 changes: 8 additions & 1 deletion app/controllers/concerns/blacklight/catalog.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,12 @@ def facet
@facet = blacklight_config.facet_fields[params[:id]]
raise ActionController::RoutingError, 'Not Found' unless @facet

@response = search_service.facet_field_response(@facet.key)
query_fragment = params[:query_fragment] || ''
@response = if query_fragment.present?
search_service.facet_suggest_response(@facet.key, params[:query_fragment])
else
@response = search_service.facet_field_response(@facet.key)
end
@display_facet = @response.aggregations[@facet.field]

@presenter = @facet.presenter.new(@facet, @display_facet, view_context)
Expand All @@ -92,6 +97,8 @@ def facet
format.html do
# Draw the partial for the "more" facet modal window:
return render layout: false if request.xhr?
# Only show the facet names and their values:
return render 'facet_values', layout: false if params[:only_values]
# Otherwise draw the facet selector for users who have javascript disabled.
end
format.json
Expand Down
15 changes: 15 additions & 0 deletions app/javascript/blacklight/debounce.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Usage:
// ```
// const basicFunction = (entry) => console.log(entry)
// const debounced = debounce(basicFunction("I should only be called once"));
//
// debounced // does NOT print to the screen becase it is invoked again less than 200 milliseconds later
// debounced // does print to the screen
// ```
export default function debounce(func, timeout = 200) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => { func.apply(this, args); }, timeout);
};
}
26 changes: 26 additions & 0 deletions app/javascript/blacklight/facet_suggest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import debounce from "blacklight/debounce";

const FacetSuggest = async (e) => {
if (e.target.matches('.facet-suggest')) {
const queryFragment = e.target.value?.trim();
const facetField = e.target.dataset.facetField;
if (!facetField) { return; }

const urlToFetch = `/catalog/facet_suggest/${facetField}/${queryFragment}`
const response = await fetch(urlToFetch);
if (response.ok) {
const blob = await response.blob()
const text = await blob.text()

const facetArea = document.querySelector('.facet-extended-list');

if (text && facetArea) {
facetArea.innerHTML = text
}
}
}
};

document.addEventListener('input', debounce(FacetSuggest));

export default FacetSuggest
2 changes: 2 additions & 0 deletions app/javascript/blacklight/index.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import BookmarkToggle from 'blacklight/bookmark_toggle'
import ButtonFocus from 'blacklight/button_focus'
import FacetSuggest from 'blacklight/facet_suggest'
import Modal from 'blacklight/modal'
import SearchContext from 'blacklight/search_context'
import Core from 'blacklight/core'

export default {
BookmarkToggle,
ButtonFocus,
FacetSuggest,
Modal,
SearchContext,
Core,
Expand Down
3 changes: 2 additions & 1 deletion app/presenters/blacklight/facet_field_presenter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ def in_advanced_search?
end

def in_modal?
search_state.params[:action] == "facet"
modal_like_actions = %w[facet facet_suggest]
modal_like_actions.include? search_state.params[:action]
end

def modal_path
Expand Down
5 changes: 5 additions & 0 deletions app/services/blacklight/search_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ def facet_field_response(facet_field, extra_controller_params = {})
repository.search(query.merge(extra_controller_params))
end

def facet_suggest_response(facet_field, facet_suggestion_query, extra_controller_params = {})
query = search_builder.with(search_state).facet(facet_field).facet_suggestion_query(facet_suggestion_query)
repository.search(query.merge(extra_controller_params))
end

# Get the previous and next document from a search result
# @return [Blacklight::Solr::Response, Array<Blacklight::SolrDocument>] the solr response and a list of the first and last document
def previous_and_next_documents_for_search(index, request_params, extra_controller_params = {})
Expand Down
1 change: 1 addition & 0 deletions app/views/catalog/facet.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<% end %>

<% component.with_title { facet_field_label(@facet.key) } %>
<%= render Blacklight::Search::FacetSuggestInput.new(facet: @facet, presenter: @presenter) %>

<%= render partial: 'facet_index_navigation' if @facet.index_range && @display_facet.index? %>

Expand Down
3 changes: 3 additions & 0 deletions app/views/catalog/facet_values.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<%= render Blacklight::FacetComponent.new(display_facet: @display_facet,
blacklight_config: blacklight_config,
layout: false) %>
3 changes: 3 additions & 0 deletions config/locales/blacklight.ar.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ ar:
sort:
count: ترتيب رقمي
index: ترتيب أبجدي
suggest:
label: Filter %{field_label}
placeholder: Filter...
title: تحديد نطاق البحث
filters:
label: "%{label}:"
Expand Down
3 changes: 3 additions & 0 deletions config/locales/blacklight.de.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ de:
sort:
count: Numerisch ordnen
index: A-Z Ordnen
suggest:
label: Filter %{field_label}
placeholder: Filter...
title: Suche beschränken
filters:
label: "%{label}:"
Expand Down
3 changes: 3 additions & 0 deletions config/locales/blacklight.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@ en:
sort:
count: Numerical Sort
index: A-Z Sort
suggest:
label: "Filter %{field_label}"
placeholder: Filter...
title: Limit your search
filters:
label: "%{label}:"
Expand Down
3 changes: 3 additions & 0 deletions config/locales/blacklight.es.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ es:
sort:
count: Ordenación numérica
index: Ordenación A-Z
suggest:
label: Filter %{field_label}
placeholder: Filter...
title: Limite su búsqueda
filters:
label: "%{label}:"
Expand Down
3 changes: 3 additions & 0 deletions config/locales/blacklight.fr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ fr:
sort:
count: Du + au - fréquent
index: Tri de A à Z
suggest:
label: Filter %{field_label}
placeholder: Filter...
title: Limiter votre recherche
filters:
label: "%{label}:"
Expand Down
3 changes: 3 additions & 0 deletions config/locales/blacklight.hu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ hu:
sort:
count: Numerikus rendezés
index: A-Z rendezés
suggest:
label: Filter %{field_label}
placeholder: Filter...
title: Szűrje tovább a keresését
filters:
label: "%{label}:"
Expand Down
3 changes: 3 additions & 0 deletions config/locales/blacklight.it.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ it:
sort:
count: Ordina per numero
index: Ordina A-Z
suggest:
label: Filter %{field_label}
placeholder: Filter...
title: Affina la ricerca
filters:
label: "%{label}:"
Expand Down
3 changes: 3 additions & 0 deletions config/locales/blacklight.nl.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ nl:
sort:
count: Numeriek sorteren
index: A-Z Sorteren
suggest:
label: Filter %{field_label}
placeholder: Filter...
title: Verfijn uw zoekopdracht
filters:
label: "%{label}:"
Expand Down
3 changes: 3 additions & 0 deletions config/locales/blacklight.pt-BR.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ pt-BR:
sort:
count: Ordenar por Número
index: Ordem Alfabética A-Z
suggest:
label: Filter %{field_label}
placeholder: Filter...
title: Filtre sua busca
filters:
label: "%{label}:"
Expand Down
3 changes: 3 additions & 0 deletions config/locales/blacklight.sq.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ sq:
sort:
count: Renditja numerike
index: A-Z Renditja
suggest:
label: Filter %{field_label}
placeholder: Filter...
title: Kufizo këkimin
filters:
label: "%{label}:"
Expand Down
3 changes: 3 additions & 0 deletions config/locales/blacklight.zh.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ zh:
sort:
count: 按数量排序
index: 按字母排序
suggest:
label: Filter %{field_label}
placeholder: Filter...
title: 限定搜索
filters:
label: "%{label}:"
Expand Down
3 changes: 2 additions & 1 deletion lib/blacklight/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ def initialized_default_configuration?
end
end

BASIC_SEARCH_PARAMETERS = [:q, :qt, :page, :per_page, :search_field, :sort, :controller, :action, :'facet.page', :'facet.prefix', :'facet.sort', :rows, :format, :view].freeze
BASIC_SEARCH_PARAMETERS = [:q, :qt, :page, :per_page, :search_field, :sort, :controller, :action, :'facet.page', :'facet.prefix', :'facet.sort', :rows, :format, :view, :id, :facet_id,
:query_fragment, :only_values].freeze
ADVANCED_SEARCH_PARAMETERS = [{ clause: {} }, :op].freeze

# rubocop:disable Metrics/BlockLength
Expand Down
1 change: 1 addition & 0 deletions lib/blacklight/routes/searchable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def call(mapper, _options = {})
mapper.get "opensearch"
mapper.get 'suggest', as: 'suggest_index'
mapper.get "facet/:id", action: 'facet', as: 'facet'
mapper.get "facet_suggest/:id/(:query_fragment)", action: 'facet', as: 'facet_suggest', defaults: { only_values: true }
end
end
end
Expand Down
13 changes: 13 additions & 0 deletions lib/blacklight/search_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,19 @@ def facet(value = nil)
@facet
end

def facet_suggestion_query=(value)
params_will_change!
@facet_suggestion_query = value
end

def facet_suggestion_query(value = nil)
if value
self.facet_suggestion_query = value
return self
end
@facet_suggestion_query
end

# Decode the user provided 'sort' parameter into a sort string that can be
# passed to the search. This sanitizes the input by ensuring only
# configured search values are passed through to the search.
Expand Down
10 changes: 9 additions & 1 deletion lib/blacklight/solr/search_builder_behavior.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ module SearchBuilderBehavior
:add_query_to_solr, :add_facet_fq_to_solr,
:add_facetting_to_solr, :add_solr_fields_to_query, :add_paging_to_solr,
:add_sorting_to_solr, :add_group_config_to_solr,
:add_facet_paging_to_solr, :add_adv_search_clauses,
:add_facet_paging_to_solr, :add_facet_suggestion_parameters,
:add_adv_search_clauses,
:add_additional_filters
]
end
Expand Down Expand Up @@ -276,6 +277,13 @@ def add_facet_paging_to_solr(solr_params)
solr_params[:"f.#{facet_config.field}.facet.prefix"] = prefix if prefix
end

def add_facet_suggestion_parameters(solr_params)
return if facet.blank? || facet_suggestion_query.blank?

solr_params[:'facet.contains'] = facet_suggestion_query[0..50]
solr_params[:'facet.contains.ignoreCase'] = true
end

def with_ex_local_param(ex, value)
if ex
"{!ex=#{ex}}#{value}"
Expand Down
33 changes: 33 additions & 0 deletions spec/components/blacklight/search/facet_suggest_input_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Blacklight::Search::FacetSuggestInput, type: :component do
let(:facet) { Blacklight::Configuration::FacetField.new key: 'language_facet' }
let(:presenter) { instance_double(Blacklight::FacetFieldPresenter) }

before do
allow(presenter).to receive(:label).and_return 'Language'
end

it 'has an input with the facet-suggest class, which the javascript needs to find it' do
rendered = render_inline(described_class.new(facet: facet, presenter: presenter))
expect(rendered.css("input.facet-suggest").count).to eq 1
end

it 'has an input with the data-facet-field attribute, which the javascript needs to determine the correct query' do
rendered = render_inline(described_class.new(facet: facet, presenter: presenter))
expect(rendered.css('input[data-facet-field="language_facet"]').count).to eq 1
end

it 'has a visible label that is associated with the input' do
rendered = render_inline(described_class.new(facet: facet, presenter: presenter))
label = rendered.css('label').first
expect(label.text.strip).to eq 'Filter Language'

id_in_label_for = label.attribute('for').text
expect(id_in_label_for).to eq('facet-suggest-language_facet')

expect(rendered.css('input').first.attribute('id').text).to eq id_in_label_for
end
end
13 changes: 13 additions & 0 deletions spec/features/facets_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,17 @@
end
end
end

describe 'Facet modal' do
it 'allows the user to filter a long list of facet values', :js do
visit '/catalog/facet/subject_ssim'
expect(page).to have_no_link 'Old age' # This is on the second page of facet values
expect(page).to have_css 'a.facet-select', count: 20

fill_in 'facet_suggest_subject_ssim', with: "ag"

expect(page).to have_link 'Old age'
expect(page).to have_css 'a.facet-select', count: 2
end
end
end
10 changes: 10 additions & 0 deletions spec/models/blacklight/search_builder_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,16 @@
end
end

describe "#facet_suggestion_query" do
it "is nil if no value is set" do
expect(subject.facet_suggestion_query).to be_nil
end

it "sets facet_suggestion_query value" do
expect(subject.facet_suggestion_query('antel').facet_suggestion_query).to eq 'antel'
end
end

describe "#search_field" do
it "uses the requested search field" do
blacklight_config.add_search_field 'x'
Expand Down
Loading

0 comments on commit 46452d2

Please sign in to comment.