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

Backport #3367 to 8.x (Add a way to filter facets in the facets "more" modal) #3369

Merged
merged 6 commits into from
Dec 17, 2024
Merged
Changes from 1 commit
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
Next Next commit
Backport #3367: Add a way to filter facets in the facets "more" modal
* 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]>
3 people committed Oct 22, 2024
commit e9e75d2dc0380ec6b7d6ddba8fc4e1757a4741f7
1 change: 1 addition & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -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"
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"
cbeer marked this conversation as resolved.
Show resolved Hide resolved
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
@@ -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?
sandbergja marked this conversation as resolved.
Show resolved Hide resolved
search_service.facet_suggest_response(@facet.key, params[:query_fragment])
else
@response = search_service.facet_field_response(@facet.key)
sandbergja marked this conversation as resolved.
Show resolved Hide resolved
end
@display_facet = @response.aggregations[@facet.field]

@presenter = @facet.presenter.new(@facet, @display_facet, view_context)
@@ -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
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
sandbergja marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉

// 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,
3 changes: 2 additions & 1 deletion app/presenters/blacklight/facet_field_presenter.rb
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions app/services/blacklight/search_service.rb
Original file line number Diff line number Diff line change
@@ -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 = {})
1 change: 1 addition & 0 deletions app/views/catalog/facet.html.erb
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@
<% end %>

<% component.with_title { facet_field_label(@facet.key) } %>
<%= render Blacklight::Search::FacetSuggestInput.new(facet: @facet, presenter: @presenter) %>
sandbergja marked this conversation as resolved.
Show resolved Hide resolved

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

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
@@ -111,6 +111,9 @@ ar:
sort:
count: ترتيب رقمي
index: ترتيب أبجدي
suggest:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it expected that these be in the target language? Or is it ok to start with English and then have a future edit with the real translation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing this out. It looks like I used the add_missing task from i18n-tasks, when I should have used translate_missing, which grabs a preliminary translation from Google Translate.

label: Filter %{field_label}
placeholder: Filter...
title: تحديد نطاق البحث
filters:
label: "%{label}:"
3 changes: 3 additions & 0 deletions config/locales/blacklight.de.yml
Original file line number Diff line number Diff line change
@@ -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}:"
3 changes: 3 additions & 0 deletions config/locales/blacklight.en.yml
Original file line number Diff line number Diff line change
@@ -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}:"
3 changes: 3 additions & 0 deletions config/locales/blacklight.es.yml
Original file line number Diff line number Diff line change
@@ -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}:"
3 changes: 3 additions & 0 deletions config/locales/blacklight.fr.yml
Original file line number Diff line number Diff line change
@@ -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}:"
3 changes: 3 additions & 0 deletions config/locales/blacklight.hu.yml
Original file line number Diff line number Diff line change
@@ -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}:"
3 changes: 3 additions & 0 deletions config/locales/blacklight.it.yml
Original file line number Diff line number Diff line change
@@ -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}:"
3 changes: 3 additions & 0 deletions config/locales/blacklight.nl.yml
Original file line number Diff line number Diff line change
@@ -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}:"
3 changes: 3 additions & 0 deletions config/locales/blacklight.pt-BR.yml
Original file line number Diff line number Diff line change
@@ -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}:"
3 changes: 3 additions & 0 deletions config/locales/blacklight.sq.yml
Original file line number Diff line number Diff line change
@@ -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}:"
3 changes: 3 additions & 0 deletions config/locales/blacklight.zh.yml
Original file line number Diff line number Diff line change
@@ -99,6 +99,9 @@ zh:
sort:
count: 按数量排序
index: 按字母排序
suggest:
label: Filter %{field_label}
placeholder: Filter...
title: 限定搜索
filters:
label: "%{label}:"
3 changes: 2 additions & 1 deletion lib/blacklight/configuration.rb
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions lib/blacklight/routes/searchable.rb
Original file line number Diff line number Diff line change
@@ -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
13 changes: 13 additions & 0 deletions lib/blacklight/search_builder.rb
Original file line number Diff line number Diff line change
@@ -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.
10 changes: 9 additions & 1 deletion lib/blacklight/solr/search_builder_behavior.rb
Original file line number Diff line number Diff line change
@@ -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
@@ -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}"
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
@@ -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
@@ -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'
64 changes: 64 additions & 0 deletions spec/models/blacklight/solr/search_builder_behavior_spec.rb
Original file line number Diff line number Diff line change
@@ -824,6 +824,70 @@
end
end

describe "#add_facet_suggestion_parameters" do
it "does not add anything when the builder has no facet_suggestion_query and no facet" do
expect(subject.facet).to be_nil
expect(subject.facet_suggestion_query).to be_nil
solr_params = Blacklight::Solr::Request.new

expect do
subject.add_facet_suggestion_parameters(solr_params)
end.not_to(change { solr_params })
end

it "does not add anything when the builder has a facet_suggestion_query but no facet" do
subject.facet_suggestion_query = 'artic'
expect(subject.facet_suggestion_query).to eq 'artic'
expect(subject.facet).to be_nil
solr_params = Blacklight::Solr::Request.new

expect do
subject.add_facet_suggestion_parameters(solr_params)
end.not_to(change { solr_params })
end

it "does not add anything when the builder has a facet but no facet_suggestion_query" do
subject.facet = 'subject_facet'
expect(subject.facet_suggestion_query).to be_nil
expect(subject.facet).to eq 'subject_facet'
solr_params = Blacklight::Solr::Request.new

expect do
subject.add_facet_suggestion_parameters(solr_params)
end.not_to(change { solr_params })
end

it "adds the facet_suggestion_query to facet.contains" do
subject.facet = 'subject_facet'
subject.facet_suggestion_query = 'artic'
solr_params = Blacklight::Solr::Request.new

subject.add_facet_suggestion_parameters(solr_params)

expect(solr_params[:'facet.contains']).to eq 'artic'
end

it "adds the first part of facet_suggestion_query to facet.contains if it is extremely long" do
subject.facet = 'subject_facet'
subject.facet_suggestion_query = 'Call me Ishmael. Some years ago—never mind how long precisely'
solr_params = Blacklight::Solr::Request.new

subject.add_facet_suggestion_parameters(solr_params)

expect(solr_params[:'facet.contains']).to eq 'Call me Ishmael. Some years ago—never mind how long'
end

it "adds facet.contains.ignoreCase" do
subject.facet = 'subject_facet'
subject.facet_suggestion_query = 'artic'
solr_params = Blacklight::Solr::Request.new

subject.add_facet_suggestion_parameters(solr_params)

expect(solr_params[:'facet.contains.ignoreCase']).to be true
end
end

describe "#with_tag_ex" do
it "adds an !ex local parameter if the facet configuration requests it" do
expect(subject.with_ex_local_param("xyz", "some-value")).to eq "{!ex=xyz}some-value"
12 changes: 11 additions & 1 deletion spec/presenters/blacklight/facet_field_presenter_spec.rb
Original file line number Diff line number Diff line change
@@ -68,7 +68,7 @@
end

describe '#in_modal?' do
context 'for a modal-like action' do
context 'for action #facet, which is a modal-like action' do
before do
controller.params[:action] = 'facet'
end
@@ -78,6 +78,16 @@
end
end

context 'for action #facet_suggest, which is a modal-like action' do
before do
controller.params[:action] = 'facet_suggest'
end

it 'is true' do
expect(presenter.in_modal?).to be true
end
end

it 'is false' do
expect(presenter.in_modal?).to be false
end