Skip to content

Commit 689c813

Browse files
committed
Implement unified 'all' search tab
Why these changes are being introduced: A key requirement for USE UI is a tab that combines results from multiple sources. The current interface requires users to search each source separately, creating friction in the discovery process. Relevant ticket(s): - [USE-176](https://mitlibraries.atlassian.net/browse/USE-176) - [USE-177](https://mitlibraries.atlassian.net/browse/USE-177) How this addresses that need: - Add new 'all' tab as the default search mode - Implement parallel API fetching using Ruby threads to query both Primo and TIMDEX simultaneously - Use zipper merge pattern to interleave results from both sources - Update view templates to render source-specific results for 'all' tab - Add tab parameter validation to prevent invalid selections - Change default tab from 'primo' to 'all' in ApplicationController Side effects of this change: - Default search behavior changes from Primo-only to combined results - A `valid_tab?` method now confirms that the tab param is one of 'primo,' 'timdex', or 'all' - Possible increased server load due to parallel API calls (I haven't confirmed this, but it seems likely) - Some peripherally related tests have been updated to account for new behavior - Pagination is not yet implemented (this will be done in USE-178 and USE-179)
1 parent 3f1acf8 commit 689c813

File tree

6 files changed

+232
-44
lines changed

6 files changed

+232
-44
lines changed

app/controllers/application_controller.rb

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,21 @@ def set_active_tab
99
@active_tab = if Feature.enabled?(:geodata)
1010
# Determine which tab to load - default to primo unless gdt is enabled
1111
'geodata'
12-
elsif params[:tab].present?
13-
# If params[:tab] is set, use it and set session
12+
elsif params[:tab].present? && valid_tab?(params[:tab])
13+
# If params[:tab] is set and valid, use it and set session
1414
cookies[:last_tab] = params[:tab]
15-
elsif cookies[:last_tab].present?
16-
# Otherwise, check for last used tab in session
15+
elsif cookies[:last_tab].present? && valid_tab?(cookies[:last_tab])
16+
# Otherwise, check for last used tab in session if valid
1717
cookies[:last_tab]
1818
else
1919
# Default behavior when no tab is specified in params or session
20-
cookies[:last_tab] = 'primo'
20+
cookies[:last_tab] = 'all'
2121
end
2222
end
23+
24+
private
25+
26+
def valid_tab?(tab)
27+
%w[primo timdex all].include?(tab)
28+
end
2329
end

app/controllers/search_controller.rb

Lines changed: 67 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ def results
2323
load_primo_results
2424
when 'timdex'
2525
load_timdex_results
26+
when 'all'
27+
load_all_results
2628
when 'geodata'
2729
load_gdt_results
2830
render 'results_geo'
@@ -66,49 +68,96 @@ def load_gdt_results
6668
end
6769

6870
def load_primo_results
71+
data = fetch_primo_data
72+
@results = data[:results]
73+
@pagination = data[:pagination]
74+
@errors = data[:errors]
75+
@show_primo_continuation = data[:show_continuation]
76+
end
77+
78+
def load_timdex_results
79+
data = fetch_timdex_data
80+
@results = data[:results]
81+
@pagination = data[:pagination]
82+
@errors = data[:errors]
83+
end
84+
85+
def load_all_results
86+
# Parallel fetching from both APIs
87+
primo_thread = Thread.new { fetch_primo_data }
88+
timdex_thread = Thread.new { fetch_timdex_data }
89+
90+
# Wait for both threads to complete
91+
primo_data = primo_thread.value
92+
timdex_data = timdex_thread.value
93+
94+
# Collect any errors from either API
95+
all_errors = []
96+
all_errors.concat(primo_data[:errors]) if primo_data[:errors]
97+
all_errors.concat(timdex_data[:errors]) if timdex_data[:errors]
98+
@errors = all_errors.any? ? all_errors : nil
99+
100+
# Zipper merge results from both APIs
101+
primo_results = primo_data[:results] || []
102+
timdex_results = timdex_data[:results] || []
103+
@results = primo_results.zip(timdex_results).flatten.compact
104+
105+
# For now, just use primo pagination as a placeholder
106+
@pagination = primo_data[:pagination] || {}
107+
108+
# Handle primo continuation for high page numbers
109+
@show_primo_continuation = primo_data[:show_continuation] || false
110+
end
111+
112+
def fetch_primo_data
69113
current_page = @enhanced_query[:page] || 1
70114
per_page = @enhanced_query[:per_page] || 20
71115
offset = (current_page - 1) * per_page
72116

73117
# Check if we're beyond Primo API limits before making the request.
74118
if offset >= Analyzer::PRIMO_MAX_OFFSET
75-
@show_primo_continuation = true
76-
return
119+
return { results: [], pagination: {}, errors: nil, show_continuation: true }
77120
end
78121

79122
primo_response = query_primo
80-
@results = NormalizePrimoResults.new(primo_response, @enhanced_query[:q]).normalize
123+
results = NormalizePrimoResults.new(primo_response, @enhanced_query[:q]).normalize
124+
pagination = Analyzer.new(@enhanced_query, primo_response, :primo).pagination
81125

82126
# Handle empty results from Primo API. Sometimes Primo will return no results at a given offset,
83127
# despite claiming in the initial query that more are available. This happens randomly and
84128
# seemingly for no reason (well below the recommended offset of 2,000). While the bug also
85129
# exists in Primo UI, sending users there seems like the best we can do.
86-
if @results.empty?
130+
show_continuation = false
131+
errors = nil
132+
133+
if results.empty?
87134
docs = primo_response['docs'] if primo_response.is_a?(Hash)
88135
if docs.nil? || docs.empty?
89136
# Only show continuation for pagination scenarios (page > 1), not for searches with no results
90-
@show_primo_continuation = true if current_page > 1
137+
show_continuation = true if current_page > 1
91138
else
92-
@errors = [{ 'message' => 'No more results available at this page number.' }]
139+
errors = [{ 'message' => 'No more results available at this page number.' }]
93140
end
94141
end
95142

96-
# Use Analyzer for consistent pagination across all search types
97-
@pagination = Analyzer.new(@enhanced_query, primo_response, :primo).pagination
143+
{ results: results, pagination: pagination, errors: errors, show_continuation: show_continuation }
98144
rescue StandardError => e
99-
@errors = handle_primo_errors(e)
145+
{ results: [], pagination: {}, errors: handle_primo_errors(e), show_continuation: false }
100146
end
101147

102-
def load_timdex_results
148+
def fetch_timdex_data
103149
query = QueryBuilder.new(@enhanced_query).query
104150
response = query_timdex(query)
105-
106-
@errors = extract_errors(response)
107-
return unless @errors.nil?
108-
109-
@pagination = Analyzer.new(@enhanced_query, response, :timdex).pagination
110-
raw_results = extract_results(response)
111-
@results = NormalizeTimdexResults.new(raw_results, @enhanced_query[:q]).normalize
151+
errors = extract_errors(response)
152+
153+
if errors.nil?
154+
pagination = Analyzer.new(@enhanced_query, response, :timdex).pagination
155+
raw_results = extract_results(response)
156+
results = NormalizeTimdexResults.new(raw_results, @enhanced_query[:q]).normalize
157+
{ results: results, pagination: pagination, errors: nil }
158+
else
159+
{ results: [], pagination: {}, errors: errors }
160+
end
112161
end
113162

114163
def active_filters
@@ -123,7 +172,7 @@ def query_timdex(query)
123172
Rails.cache.fetch("#{cache_key}/#{@active_tab}", expires_in: 12.hours) do
124173
raw = if @active_tab == 'geodata'
125174
execute_geospatial_query(query)
126-
elsif @active_tab == 'timdex'
175+
elsif @active_tab == 'timdex' || @active_tab == 'all'
127176
TimdexBase::Client.query(TimdexSearch::BaseQuery, variables: query)
128177
end
129178

app/views/search/_source_tabs.html.erb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
<!-- Tab Navigation -->
22
<nav id="tabs" class="tab-navigation" aria-label="Result type navigation">
33
<ul>
4+
<li><%= link_to "All", results_path(params.permit(:q, :per_page, :booleanType, :tab).merge(tab: 'all')),
5+
class: "tab-link #{'active' if @active_tab == 'all'}",
6+
data: { turbo_frame: "search-results", turbo_action: "advance" } %></li>
47
<li>
58
<%= link_to "Primo", results_path(params.permit(:q, :per_page, :booleanType, :tab).merge(tab: 'primo')),
69
class: "tab-link #{'active' if @active_tab == 'primo'}",

app/views/search/results.html.erb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@
2323
<%= render(partial: 'search/result_primo', collection: @results, as: :result) %>
2424
<% when 'timdex' %>
2525
<%= render(partial: 'search/result', collection: @results, as: :result) %>
26+
<% when 'all' %>
27+
<% @results.each do |result| %>
28+
<% if result[:source] == 'Primo' %>
29+
<%= render(partial: 'search/result_primo', locals: { result: result }) %>
30+
<% else %>
31+
<%= render(partial: 'search/result', locals: { result: result }) %>
32+
<% end %>
33+
<% end %>
2634
<% end %>
2735
</ol>
2836
<% elsif @errors.blank? %>

test/controllers/application_controller_test.rb

Lines changed: 75 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
require 'test_helper'
22

33
class ApplicationControllerTest < ActionDispatch::IntegrationTest
4-
test 'set_active_tab sets default to primo when no feature flag or params' do
4+
test 'set_active_tab sets default to all when no feature flag or params' do
55
assert_nil cookies[:last_tab]
66

77
get root_path
88
assert_select '#tab-to-target' do
9-
assert_select '[value=?]', 'primo'
9+
assert_select '[value=?]', 'all'
1010
refute_select '[value=?]', 'geodata'
11+
refute_select '[value=?]', 'primo'
1112
refute_select '[value=?]', 'timdex'
1213
end
1314

14-
assert_equal cookies[:last_tab], 'primo'
15+
assert_equal cookies[:last_tab], 'all'
1516
end
1617

1718
test 'set_active_tab sets to geodata when feature flag enabled' do
@@ -32,7 +33,7 @@ class ApplicationControllerTest < ActionDispatch::IntegrationTest
3233
get root_path, params: { tab: 'primo' }
3334
assert_select '#tab-to-target' do
3435
refute_select '[value=?]', 'primo'
35-
assert_select '[value=?]', 'geodata'
36+
assert_select '[value=?]', 'all'
3637
refute_select '[value=?]', 'timdex'
3738
end
3839
end
@@ -43,31 +44,95 @@ class ApplicationControllerTest < ActionDispatch::IntegrationTest
4344

4445
assert_select '#tab-to-target' do
4546
refute_select '[value=?]', 'primo'
46-
refute_select '[value=?]', 'geodata'
47+
refute_select '[value=?]', 'all'
4748
assert_select '[value=?]', 'timdex'
4849
end
4950
end
5051

5152
test 'set_active_tab sets to param tab when provided even if cookie is set and updates cookie' do
5253
cookies[:last_tab] = 'timdex'
53-
get root_path, params: { tab: 'geodata' }
54+
get root_path, params: { tab: 'primo' }
5455

5556
assert_select '#tab-to-target' do
56-
refute_select '[value=?]', 'primo'
57-
assert_select '[value=?]', 'geodata'
57+
assert_select '[value=?]', 'primo'
58+
refute_select '[value=?]', 'geodata'
5859
refute_select '[value=?]', 'timdex'
60+
refute_select '[value=?]', 'all'
5961
end
6062

61-
assert_equal cookies[:last_tab], 'geodata'
63+
assert_equal cookies[:last_tab], 'primo'
6264
end
6365

6466
test 'set_active_tab uses cookie last_tab when no param provided' do
6567
cookies[:last_tab] = 'timdex'
6668
get root_path
6769
assert_select '#tab-to-target' do
70+
refute_select '[value=?]', 'all'
6871
refute_select '[value=?]', 'primo'
69-
refute_select '[value=?]', 'geodata'
7072
assert_select '[value=?]', 'timdex'
7173
end
7274
end
75+
76+
test 'valid_tab returns true for valid tabs' do
77+
app_controller = ApplicationController.new
78+
79+
assert app_controller.send(:valid_tab?, 'primo')
80+
assert app_controller.send(:valid_tab?, 'timdex')
81+
assert app_controller.send(:valid_tab?, 'all')
82+
end
83+
84+
test 'valid_tab returns false for invalid tabs' do
85+
app_controller = ApplicationController.new
86+
87+
refute app_controller.send(:valid_tab?, 'foo')
88+
refute app_controller.send(:valid_tab?, '')
89+
refute app_controller.send(:valid_tab?, nil)
90+
end
91+
92+
test 'set_active_tab ignores invalid tab parameter and uses default' do
93+
get root_path, params: { tab: 'invalid_tab' }
94+
95+
assert_select '#tab-to-target' do
96+
assert_select '[value=?]', 'all'
97+
refute_select '[value=?]', 'invalid_tab'
98+
end
99+
100+
assert_equal cookies[:last_tab], 'all'
101+
end
102+
103+
test 'set_active_tab ignores invalid cookie value and uses default' do
104+
cookies[:last_tab] = 'invalid_cookie_value'
105+
get root_path
106+
107+
assert_select '#tab-to-target' do
108+
assert_select '[value=?]', 'all'
109+
refute_select '[value=?]', 'invalid_cookie_value'
110+
end
111+
112+
assert_equal cookies[:last_tab], 'all'
113+
end
114+
115+
test 'set_active_tab prioritizes valid param over invalid cookie' do
116+
cookies[:last_tab] = 'invalid_cookie'
117+
get root_path, params: { tab: 'timdex' }
118+
119+
assert_select '#tab-to-target' do
120+
refute_select '[value=?]', 'invalid_cookie'
121+
assert_select '[value=?]', 'timdex'
122+
end
123+
124+
assert_equal cookies[:last_tab], 'timdex'
125+
end
126+
127+
test 'set_active_tab falls back to valid cookie when param is invalid' do
128+
cookies[:last_tab] = 'primo'
129+
get root_path, params: { tab: 'foo' }
130+
131+
assert_select '#tab-to-target' do
132+
refute_select '[value=?]', 'foo'
133+
assert_select '[value=?]', 'primo'
134+
end
135+
136+
assert_equal cookies[:last_tab], 'primo'
137+
end
73138
end

0 commit comments

Comments
 (0)