Skip to content

Commit

Permalink
Merge pull request #3484 from alphagov/filter-pres
Browse files Browse the repository at this point in the history
Implement filter summary
  • Loading branch information
csutter authored Oct 4, 2024
2 parents 8b09a4c + d68d142 commit 0077783
Show file tree
Hide file tree
Showing 22 changed files with 663 additions and 41 deletions.
8 changes: 6 additions & 2 deletions app/controllers/finders_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ class UnsupportedContentItem < StandardError; end

attr_reader :search_query

helper_method :facet_tags, :i_am_a_topic_page_finder, :result_set_presenter, :content_item, :signup_links, :filter_params, :facets
helper_method :facet_tags, :i_am_a_topic_page_finder, :result_set_presenter, :content_item, :signup_links, :filter_params, :facets, :filters_presenter

def redirect_to_destination
@redirect = content_item.redirect
Expand Down Expand Up @@ -130,7 +130,7 @@ def all_facets
value_hash: filter_params,
).facets.tap do |built_facets|
if content_item.all_content_finder? && enable_new_all_content_finder_ui?
built_facets.prepend(SortFacet.new)
built_facets.prepend(SortFacet.new(content_item, filter_params))
end
end
end
Expand Down Expand Up @@ -166,6 +166,10 @@ def sort_presenter
@sort_presenter ||= content_item.sorter_class.new(content_item, filter_params)
end

def filters_presenter
@filters_presenter ||= FiltersPresenter.new(facets, finder_url_builder)
end

def pagination_presenter
PaginationPresenter.new(
per_page: content_item.default_documents_per_page,
Expand Down
3 changes: 2 additions & 1 deletion app/lib/facets_iterator.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
class FacetsIterator
delegate :select, :any?, to: :@facets
include Enumerable
delegate :each, to: :@facets

def initialize(facets)
@facets = facets
Expand Down
38 changes: 38 additions & 0 deletions app/lib/hash_with_deep_except.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
module HashWithDeepExcept
refine Hash do
# Allows removing arbitarily nested params from a hash, used for clearing specific filters from
# a user's search.
#
# For example, given:
# { a: 1, b: { c: 2, d: 3 } }
# Removing
# { b: { c: 2 } }
# would result in
# { a: 1, b: { d: 3 } }
def deep_except(other)
each_with_object({}) do |(key, value), result|
if other.key?(key)
child = other[key]

if value.is_a?(Hash) && child.is_a?(Hash)
# Recursively remove nested values
nested_result = value.deep_except(child)
result[key] = nested_result unless nested_result.empty?
elsif value.is_a?(Array) && child.is_a?(Array)
result[key] = value - child
elsif child.is_a?(Array) && child == [value]
# Some parameters can be given both as an array and a single value (e.g. `organisation`),
# and should be removed if a single value is present that's equal to the only array
# element
#
# Skip this key-value pair, effectively removing it
elsif value != child
result[key] = value
end
else
result[key] = value
end
end
end
end
end
18 changes: 14 additions & 4 deletions app/lib/url_builder.rb
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
# The UrlBuilder class is responsible for creating URLs to be rendered as
# HTML links.
class UrlBuilder
using HashWithDeepExcept

def initialize(path, query_params = {})
@path = path
@query_params = query_params
end

def url(additional_params = {})
[
path,
query_params.merge(additional_params).to_query,
].reject(&:blank?).join("?")
build_url(query_params.merge(additional_params))
end

def url_except(excepted_param)
build_url(query_params.deep_except(excepted_param))
end

private

attr_reader :path, :query_params

def build_url(params)
[
path,
params.to_query,
].reject(&:blank?).join("?")
end
end
11 changes: 11 additions & 0 deletions app/models/date_facet.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,17 @@ def sentence_fragment
}
end

def applied_filters
present_values.map do |type, date|
preposition = preposition_mappings[type]
{
name: "#{name} #{preposition}",
label: date.date.strftime("%e %B %Y"),
query_params: { key => { type => date.original_input } },
}
end
end

def has_filters?
present_values.any?
end
Expand Down
4 changes: 4 additions & 0 deletions app/models/facet.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ def allowed_values
facet["allowed_values"] || []
end

def applied_filters
[]
end

def query_params
{}
end
Expand Down
10 changes: 10 additions & 0 deletions app/models/hidden_clearable_facet.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ def has_filters?
selected_values.any?
end

def applied_filters
selected_values.map do |value|
{
name:,
label: value["label"],
query_params: { key => [value["value"]] },
}
end
end

def query_params
{ key => selected_values.map { |value| value["value"] } }
end
Expand Down
10 changes: 10 additions & 0 deletions app/models/option_select_facet.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ def has_filters?
selected_values.any?
end

def applied_filters
selected_values.map do |value|
{
name:,
label: value["label"],
query_params: { key => [value["value"]] },
}
end
end

def unselected?
selected_values.empty?
end
Expand Down
38 changes: 33 additions & 5 deletions app/models/sort_facet.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
# sorting forms part of the overall filtering UI instead of being separate
class SortFacet
KEY = "order".freeze
DEFAULT_SORT_OPTIONS = %w[relevance most-viewed].freeze

def initialize(content_item, filter_params)
@content_item = content_item
@filter_params = filter_params
end

def name
"Sort by"
Expand All @@ -20,13 +26,25 @@ def user_visible?
true
end

# The methods below are the minimum required for this virtual facet to take the place of a real
# `Facet`

def has_filters?
false
sort_options.keys.include?(selected_sort_option) &&
!DEFAULT_SORT_OPTIONS.include?(selected_sort_option)
end

def applied_filters
return [] unless has_filters?

[{
name:,
label: sort_options[selected_sort_option],
query_params: { KEY => selected_sort_option },
visually_hidden_prefix: "Remove",
}]
end

# The methods below are the minimum required for this virtual facet to take the place of a real
# `Facet`

def filterable?
true
end
Expand All @@ -41,5 +59,15 @@ def metadata?

private

attr_reader :sort_presenter
attr_reader :content_item, :filter_params

def sort_options
# Finder Frontend's sort handling is somewhat bizarre - it doesn't use the sort option keys from
# the content item, but rather the sort options' names parameterized.
content_item.sort_options.to_h { [_1["name"].parameterize, _1["name"]] }
end

def selected_sort_option
filter_params[KEY]
end
end
24 changes: 24 additions & 0 deletions app/models/taxon_facet.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,30 @@ def has_filters?
selected_level_one_value.present?
end

def applied_filters
return [] unless has_filters?

level_one_filter = {
name: "Topic",
label: selected_level_one_value[:text],
query_params: {
# Note that removing a topic should always remove the sub-topic too
LEVEL_ONE_TAXON_KEY => selected_level_one_value[:value],
LEVEL_TWO_TAXON_KEY => selected_level_two_value&.fetch(:value),
}.compact,
}

if selected_level_two_value
level_two_filter = {
name: "Sub-topic",
label: selected_level_two_value[:text],
query_params: { LEVEL_TWO_TAXON_KEY => selected_level_two_value[:value] },
}
end

[level_one_filter, level_two_filter].compact
end

def query_params
{
LEVEL_ONE_TAXON_KEY => (selected_level_one_value || {})[:value],
Expand Down
29 changes: 29 additions & 0 deletions app/presenters/filters_presenter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
class FiltersPresenter
def initialize(facets, finder_url_builder)
@facets = facets
@finder_url_builder = finder_url_builder
end

def any_filters?
facets.any?(&:has_filters?)
end

def summary_items
facets.flat_map(&:applied_filters).map do |filter|
{
label: filter[:name],
value: filter[:label],
remove_href: finder_url_builder.url_except(filter[:query_params]),
visually_hidden_prefix: "Remove filter",
}
end
end

def reset_url
"#"
end

private

attr_reader :facets, :finder_url_builder
end
37 changes: 11 additions & 26 deletions app/views/finders/show_all_content_finder.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
button_text: "Filter and sort",
result_text: result_set_presenter.displayed_total,
open: @search_query.invalid?,
show_reset_link: filters_presenter.any_filters?,
reset_link_href: filters_presenter.reset_url,
} do %>
<% facets.each_with_visible_index_and_count do |facet, index, count| %>
<%=
Expand All @@ -59,32 +61,15 @@
<% end %>
<% end %>
<%= render "components/filter_summary", {
clear_all_href: "#",
clear_all_text: "Clear all filters",
heading_level: 3,
heading_text: "Selected filters",
filters: [
{
label: "Filter 1",
value: "Value 1",
remove_href: "#",
visually_hidden_prefix: "Remove filter"
},
{
label: "Filter 2",
value: "Value 2",
remove_href: "#",
visually_hidden_prefix: "Remove filter"
},
{
label: "Filter 3",
value: "Value 3",
remove_href: "#",
visually_hidden_prefix: "Remove filter"
}
]
} %>
<% if filters_presenter.any_filters? %>
<%= render "components/filter_summary", {
clear_all_href: filters_presenter.reset_url,
clear_all_text: "Clear all filters",
heading_level: 3,
heading_text: "Selected filters",
filters: filters_presenter.summary_items,
} %>
<% end %>
<% if result_set_presenter.total_count.positive? %>
<%= render "govuk_publishing_components/components/document_list", {
Expand Down
11 changes: 11 additions & 0 deletions features/all_content_finder.feature
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,17 @@ Feature: All content finder ("site search")
And I apply the filters
Then I can see sorted results
Scenario: Removing a filter
When I search all content for "chandeliers flickering"
And I open the filter panel
And I open the "Topic" filter section
And I select "Music" as the Topic
And I select "Best songs" as the Sub-topic
And I apply the filters
And I click on the "Sub-topic: Best songs" filter tag
Then the "Sub-topic: Best songs" filter has been removed
@javascript
Scenario: Entering an incorrect date
When I search all content for "chandeliers flickering"
Expand Down
8 changes: 8 additions & 0 deletions features/step_definitions/site_search_steps.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@
fill_in field, with: text
end

When(/I click on the "([^"]*)" filter tag/) do |filter|
click_on filter
end

Then(/the "([^"]*)" filter has been removed/) do |filter|
expect(page).not_to have_link(filter)
end

Then("I can see filtered results") do
expect(page).to have_link("Death by a thousand cuts")
end
Expand Down
Loading

0 comments on commit 0077783

Please sign in to comment.