Skip to content

Commit 2ef8e28

Browse files
committed
Persist filters, per user, per view, per browser session
User feedback suggested that filters maintain state because they often filter patients, move away from the page, and then come back having to apply the same filters again. This update will ensure each page with filters maintains its own unique state. This state can only be cleared with the "Clear filters" button on the form.
1 parent d6ee775 commit 2ef8e28

File tree

5 files changed

+297
-19
lines changed

5 files changed

+297
-19
lines changed

app/components/app_search_component.rb

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,15 +98,15 @@ class AppSearchComponent < ViewComponent::Base
9898
<% if show_buttons_in_details? %>
9999
<div class="app-button-group">
100100
<%= f.govuk_submit "Update results", secondary: true, class: "app-button--small" %>
101-
<%= govuk_button_link_to "Clear filters", @url, class: "app-button--small app-button--secondary" %>
101+
<%= govuk_button_link_to "Clear filters", clear_filters_path, class: "app-button--small app-button--secondary" %>
102102
</div>
103103
<% end %>
104104
<% end %>
105105
106106
<% unless show_buttons_in_details? %>
107107
<div class="app-button-group">
108108
<%= f.govuk_submit "Update results", secondary: true, class: "app-button--small" %>
109-
<%= govuk_button_link_to "Clear filters", @url, class: "app-button--small app-button--secondary" %>
109+
<%= govuk_button_link_to "Clear filters", clear_filters_path, class: "app-button--small app-button--secondary" %>
110110
</div>
111111
<% end %>
112112
<% end %>
@@ -159,4 +159,8 @@ def show_buttons_in_details?
159159
triage_statuses.any? || year_groups.any?
160160
)
161161
end
162+
163+
def clear_filters_path
164+
"#{@url}?search_form[clear_filters]=true"
165+
end
162166
end

app/controllers/concerns/search_form_concern.rb

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ module SearchFormConcern
66
def set_search_form
77
@form =
88
SearchForm.new(
9-
params.fetch(:search_form, {}).permit(
9+
**params.fetch(:search_form, {}).permit(
10+
:clear_filters,
1011
:consent_status,
1112
:date_of_birth_day,
1213
:date_of_birth_month,
@@ -18,7 +19,9 @@ def set_search_form
1819
:session_status,
1920
:triage_status,
2021
year_groups: []
21-
)
22+
),
23+
session: session,
24+
request_path: request.path
2225
)
2326
end
2427
end

app/forms/search_form.rb

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ class SearchForm
55
include ActiveModel::Attributes
66
include ActiveRecord::AttributeAssignment
77

8+
SESSION_KEY = "search_filters"
9+
10+
attribute :clear_filters, :boolean
811
attribute :consent_status, :string
912
attribute :date_of_birth_day, :integer
1013
attribute :date_of_birth_month, :integer
@@ -17,6 +20,13 @@ class SearchForm
1720
attribute :triage_status, :string
1821
attribute :year_groups, array: true
1922

23+
attr_accessor :session, :request_path
24+
25+
def initialize(params = {})
26+
super(params)
27+
handle_session_filters
28+
end
29+
2030
def year_groups=(values)
2131
super(values&.compact_blank&.map(&:to_i)&.compact || [])
2232
end
@@ -62,4 +72,54 @@ def apply(scope, programme: nil)
6272

6373
scope.order_by_name
6474
end
75+
76+
private
77+
78+
def handle_session_filters
79+
if clear_filters
80+
clear_from_session
81+
elsif has_filters?
82+
store_in_session
83+
else
84+
load_from_session
85+
end
86+
end
87+
88+
def store_in_session
89+
return if session.nil?
90+
91+
if has_filters?
92+
session[SESSION_KEY] ||= {}
93+
session[SESSION_KEY][path_key] = attributes
94+
end
95+
end
96+
97+
def load_from_session
98+
return if session.nil? || session[SESSION_KEY].blank?
99+
100+
stored_filters = session[SESSION_KEY][path_key]
101+
return if stored_filters.blank?
102+
103+
assign_attributes(stored_filters)
104+
end
105+
106+
def clear_from_session
107+
return if session.nil? || session[SESSION_KEY].blank?
108+
109+
session[SESSION_KEY].delete(path_key)
110+
end
111+
112+
def has_filters?
113+
# An empty string represents the "Any" option
114+
attributes
115+
.except(:clear_filters)
116+
.values
117+
.any? { |value| value.present? || value == "" }
118+
end
119+
120+
def path_key
121+
# 8 should be more than enough to avoid collisions
122+
# for the number of distinct paths.
123+
Digest::MD5.hexdigest(request_path).first(8)
124+
end
65125
end
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# frozen_string_literal: true
2+
3+
describe "Filter state persistence" do
4+
scenario "filters maintain state across navigation" do
5+
given_i_am_signed_in
6+
7+
when_i_visit_the_consent_page
8+
and_i_apply_consent_filters
9+
then_the_consent_filters_are_applied
10+
11+
when_i_navigate_to_another_page
12+
and_i_return_to_the_consent_page
13+
then_the_consent_filters_are_still_applied
14+
15+
when_i_clear_the_consent_filters
16+
then_i_should_see_no_applied_filters
17+
18+
when_i_visit_the_triage_page
19+
and_i_apply_triage_filters
20+
then_the_triage_filters_are_applied
21+
22+
when_i_navigate_to_another_page
23+
and_i_return_to_the_triage_page
24+
then_the_triage_filters_are_still_applied
25+
26+
when_i_visit_the_consent_page
27+
then_i_should_see_no_applied_filters
28+
end
29+
30+
def given_i_am_signed_in
31+
@programme = create(:programme, :hpv)
32+
@organisation =
33+
create(:organisation, :with_one_nurse, programmes: [@programme])
34+
@session =
35+
create(:session, organisation: @organisation, programmes: [@programme])
36+
37+
sign_in @organisation.users.first
38+
end
39+
40+
def when_i_visit_the_consent_page
41+
visit session_consent_path(@session)
42+
end
43+
44+
def and_i_apply_consent_filters
45+
choose "Consent given"
46+
click_on "Update results"
47+
end
48+
49+
def then_the_consent_filters_are_applied
50+
expect(page).to have_checked_field("Consent given")
51+
end
52+
53+
def then_the_consent_filters_are_still_applied
54+
expect(page).to have_checked_field("Consent given")
55+
end
56+
57+
def when_i_clear_the_consent_filters
58+
click_on "Clear filters"
59+
end
60+
61+
def when_i_navigate_to_another_page
62+
visit root_path
63+
end
64+
65+
def and_i_return_to_the_consent_page
66+
visit session_consent_path(@session)
67+
end
68+
69+
def when_i_visit_the_triage_page
70+
visit session_triage_path(@session)
71+
end
72+
73+
def and_i_apply_triage_filters
74+
choose "Safe to vaccinate"
75+
click_on "Update results"
76+
end
77+
78+
def then_the_triage_filters_are_applied
79+
expect(page).to have_checked_field("Safe to vaccinate")
80+
end
81+
82+
def and_i_return_to_the_triage_page
83+
visit session_triage_path(@session)
84+
end
85+
86+
def then_the_triage_filters_are_still_applied
87+
expect(page).to have_checked_field("Safe to vaccinate")
88+
end
89+
90+
def when_i_clear_the_triage_filters
91+
click_on "Clear filters"
92+
end
93+
94+
def then_i_should_see_no_applied_filters
95+
expect(page).to have_checked_field("Any")
96+
end
97+
end

spec/forms/search_form_spec.rb

Lines changed: 129 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,10 @@
11
# frozen_string_literal: true
22

33
describe SearchForm do
4-
subject(:form) do
5-
described_class.new(
6-
consent_status:,
7-
date_of_birth_day:,
8-
date_of_birth_month:,
9-
date_of_birth_year:,
10-
missing_nhs_number:,
11-
programme_status:,
12-
q:,
13-
register_status:,
14-
session_status:,
15-
triage_status:,
16-
year_groups:
17-
)
18-
end
4+
subject(:form) { described_class.new(**params, session:, request_path:) }
195

6+
let(:session) { {} }
7+
let(:empty_params) { {} }
208
let(:consent_status) { nil }
219
let(:date_of_birth_day) { Date.current.day }
2210
let(:date_of_birth_month) { Date.current.month }
@@ -28,6 +16,23 @@
2816
let(:session_status) { nil }
2917
let(:triage_status) { nil }
3018
let(:year_groups) { %w[8 9 10 11] }
19+
let(:request_path) { "/a-path" }
20+
21+
let(:params) do
22+
{
23+
consent_status:,
24+
date_of_birth_day:,
25+
date_of_birth_month:,
26+
date_of_birth_year:,
27+
missing_nhs_number:,
28+
programme_status:,
29+
q:,
30+
register_status:,
31+
session_status:,
32+
triage_status:,
33+
year_groups:
34+
}
35+
end
3136

3237
context "for patients" do
3338
let(:scope) { Patient.all }
@@ -207,4 +212,113 @@
207212
end
208213
end
209214
end
215+
216+
describe "session filter persistence" do
217+
let(:another_path) { "/another-path" }
218+
219+
context "when clear_filters param is present" do
220+
it "only clears filters for the current path" do
221+
described_class.new(**{ q: "John" }, session:, request_path:)
222+
described_class.new(
223+
**{ q: "Jane" },
224+
session:,
225+
request_path: another_path
226+
)
227+
228+
described_class.new(
229+
**{ clear_filters: "true" },
230+
session:,
231+
request_path:
232+
)
233+
234+
form1 = described_class.new(**empty_params, session:, request_path:)
235+
expect(form1.q).to be_nil
236+
237+
form2 =
238+
described_class.new(
239+
**empty_params,
240+
session:,
241+
request_path: another_path
242+
)
243+
expect(form2.q).to eq("Jane")
244+
end
245+
end
246+
247+
context "when filters are present in params" do
248+
it "persists filters to be loaded in subsequent requests" do
249+
described_class.new(**{ q: "John" }, session:, request_path:)
250+
251+
form = described_class.new(**empty_params, session:, request_path:)
252+
expect(form.q).to eq("John")
253+
end
254+
255+
it "overwrites previously stored filters" do
256+
described_class.new(**{ q: "John" }, session:, request_path:)
257+
258+
form1 = described_class.new(**{ q: "Jane" }, session:, request_path:)
259+
expect(form1.q).to eq("Jane")
260+
261+
form2 = described_class.new(**empty_params, session:, request_path:)
262+
expect(form2.q).to eq("Jane")
263+
end
264+
265+
it "overrides session filters when 'Any' option is selected (empty string)" do
266+
described_class.new(
267+
**{ consent_status: "given" },
268+
session:,
269+
request_path:
270+
)
271+
272+
form1 = described_class.new(**empty_params, session:, request_path:)
273+
expect(form1.consent_status).to eq("given")
274+
275+
form2 =
276+
described_class.new(**{ consent_status: "" }, session:, request_path:)
277+
expect(form2.consent_status).to eq("")
278+
279+
form3 = described_class.new(**empty_params, session:, request_path:)
280+
expect(form3.consent_status).to eq("")
281+
end
282+
end
283+
284+
context "when no filters are present in params but exist in session" do
285+
before do
286+
described_class.new(
287+
**{ q: "John", year_groups: %w[8 11], consent_status: "given" },
288+
session:,
289+
request_path:
290+
)
291+
end
292+
293+
it "loads filters from the session" do
294+
form = described_class.new(**empty_params, session:, request_path:)
295+
296+
expect(form.q).to eq("John")
297+
expect(form.year_groups).to eq([8, 11])
298+
expect(form.consent_status).to eq("given")
299+
end
300+
end
301+
302+
context "with path-specific filters" do
303+
it "maintains separate filters for different paths" do
304+
described_class.new(**{ q: "John" }, session:, request_path:)
305+
described_class.new(
306+
**{ q: "Jane" },
307+
session:,
308+
request_path: another_path
309+
)
310+
311+
form1 = described_class.new(**empty_params, session:, request_path:)
312+
expect(form1.q).to eq("John")
313+
314+
form2 =
315+
described_class.new(
316+
**empty_params,
317+
session:,
318+
request_path: another_path
319+
)
320+
expect(form2.q).to eq("Jane")
321+
end
322+
end
323+
end
210324
end

0 commit comments

Comments
 (0)