blacklight 7.12.1 → 7.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/VERSION +1 -1
  4. data/app/components/blacklight/constraints_component.rb +7 -5
  5. data/app/components/blacklight/document_component.html.erb +1 -0
  6. data/app/components/blacklight/document_component.rb +14 -1
  7. data/app/components/blacklight/facet_field_component.html.erb +1 -0
  8. data/app/controllers/concerns/blacklight/search_context.rb +1 -1
  9. data/app/controllers/concerns/blacklight/searchable.rb +1 -1
  10. data/app/helpers/blacklight/configuration_helper_behavior.rb +3 -9
  11. data/app/helpers/blacklight/facets_helper_behavior.rb +8 -2
  12. data/app/helpers/blacklight/render_constraints_helper_behavior.rb +7 -5
  13. data/app/presenters/blacklight/document_presenter.rb +4 -0
  14. data/app/presenters/blacklight/facet_item_presenter.rb +6 -2
  15. data/app/presenters/blacklight/index_presenter.rb +2 -2
  16. data/app/presenters/blacklight/rendering/link_to_facet.rb +3 -1
  17. data/app/presenters/blacklight/show_presenter.rb +0 -4
  18. data/app/services/blacklight/search_service.rb +13 -11
  19. data/app/views/catalog/_search_form.html.erb +1 -1
  20. data/app/views/catalog/index.json.jbuilder +3 -1
  21. data/lib/blacklight/configuration/facet_field.rb +7 -0
  22. data/lib/blacklight/configuration/search_field.rb +5 -0
  23. data/lib/blacklight/configuration/tool_config.rb +4 -0
  24. data/lib/blacklight/configuration/view_config.rb +12 -0
  25. data/lib/blacklight/nested_open_struct_with_hash_access.rb +1 -1
  26. data/lib/blacklight/search_builder.rb +13 -23
  27. data/lib/blacklight/search_state.rb +82 -70
  28. data/lib/blacklight/search_state/filter_field.rb +122 -0
  29. data/lib/blacklight/solr/search_builder_behavior.rb +71 -51
  30. data/package.json +4 -0
  31. data/spec/components/blacklight/document_component_spec.rb +15 -0
  32. data/spec/features/search_spec.rb +0 -5
  33. data/spec/helpers/blacklight/configuration_helper_behavior_spec.rb +1 -2
  34. data/spec/lib/blacklight/configuration/view_config_spec.rb +15 -0
  35. data/spec/lib/blacklight/nested_open_struct_with_hash_access_spec.rb +9 -0
  36. data/spec/lib/blacklight/search_state/filter_field_spec.rb +125 -0
  37. data/spec/lib/blacklight/search_state_spec.rb +132 -3
  38. data/spec/models/blacklight/configuration_spec.rb +8 -0
  39. data/spec/models/blacklight/solr/search_builder_spec.rb +32 -2
  40. metadata +7 -3
  41. data/.npmignore +0 -23
@@ -10,6 +10,10 @@
10
10
  "type": "git",
11
11
  "url": "git+https://github.com/projectblacklight/blacklight.git"
12
12
  },
13
+ "files": [
14
+ "app/assets",
15
+ "app/javascript"
16
+ ],
13
17
  "author": "",
14
18
  "license": "Apache-2.0",
15
19
  "bugs": {
@@ -42,11 +42,13 @@ RSpec.describe Blacklight::DocumentComponent, type: :component do
42
42
 
43
43
  it 'has some defined content areas' do
44
44
  component.with(:title, 'Title')
45
+ component.with(:embed, 'Embed')
45
46
  component.with(:metadata, 'Metadata')
46
47
  component.with(:thumbnail, 'Thumbnail')
47
48
  component.with(:actions, 'Actions')
48
49
 
49
50
  expect(rendered).to have_content 'Title'
51
+ expect(rendered).to have_content 'Embed'
50
52
  expect(rendered).to have_content 'Metadata'
51
53
  expect(rendered).to have_content 'Thumbnail'
52
54
  expect(rendered).to have_content 'Actions'
@@ -126,6 +128,19 @@ RSpec.describe Blacklight::DocumentComponent, type: :component do
126
128
  expect(rendered).to have_selector 'dt', text: 'ISBN:'
127
129
  expect(rendered).to have_selector 'dd', text: 'Value'
128
130
  end
131
+
132
+ it 'renders an embed' do
133
+ stub_const('StubComponent', Class.new(ViewComponent::Base) do
134
+ def initialize(**); end
135
+
136
+ def call
137
+ 'embed'
138
+ end
139
+ end)
140
+
141
+ blacklight_config.show.embed_component = StubComponent
142
+ expect(rendered).to have_content 'embed'
143
+ end
129
144
  end
130
145
 
131
146
  it 'renders metadata' do
@@ -115,9 +115,4 @@ RSpec.describe "Search Page" do
115
115
  expect(page).to have_content "Welcome!"
116
116
  expect(page).not_to have_selector "#q[value='history']"
117
117
  end
118
-
119
- it "handles searches with invalid facet parameters" do
120
- visit root_path f: { missing_s: [1] }
121
- expect(page).to have_content "No results found for your search"
122
- end
123
118
  end
@@ -122,8 +122,7 @@ RSpec.describe Blacklight::ConfigurationHelperBehavior do
122
122
 
123
123
  describe "#view_label" do
124
124
  it "looks up the label to display for the view" do
125
- allow(blacklight_config).to receive(:view).and_return("my_view" => double(label: "some label", title: nil))
126
- allow(helper).to receive(:field_label).with(:"blacklight.search.view_title.my_view", :"blacklight.search.view.my_view", "some label", nil, "My view")
125
+ allow(blacklight_config).to receive(:view).and_return("my_view" => double(display_label: "some label"))
127
126
 
128
127
  helper.view_label "my_view"
129
128
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Blacklight::Configuration::ViewConfig do
4
+ subject { described_class.new(key: key, label: label) }
5
+
6
+ let(:key) { 'my_view' }
7
+ let(:label) { 'some label' }
8
+
9
+ describe '#display_label' do
10
+ it "looks up the label to display for the given document and field" do
11
+ allow(I18n).to receive(:t).with(:"blacklight.search.view_title.my_view", default: [:"blacklight.search.view.my_view", label, nil, "My view"]).and_return('x')
12
+ expect(subject.display_label(key)).to eq 'x'
13
+ end
14
+ end
15
+ end
@@ -14,4 +14,13 @@ RSpec.describe Blacklight::NestedOpenStructWithHashAccess do
14
14
  expect(copy.a[:b]).to eq 1
15
15
  end
16
16
  end
17
+
18
+ describe '#<<' do
19
+ subject { described_class.new(Blacklight::Configuration::Field) }
20
+
21
+ it 'includes the key in the hash' do
22
+ subject << :blah
23
+ expect(subject.blah).to have_attributes(key: :blah)
24
+ end
25
+ end
17
26
  end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Blacklight::SearchState::FilterField do
4
+ let(:search_state) { Blacklight::SearchState.new(params.with_indifferent_access, blacklight_config, controller) }
5
+
6
+ let(:params) { { f: { some_field: %w[1 2], another_field: ['3'] } } }
7
+ let(:blacklight_config) do
8
+ Blacklight::Configuration.new.configure do |config|
9
+ config.add_facet_field 'some_field'
10
+ config.add_facet_field 'another_field', single: true
11
+ end
12
+ end
13
+ let(:controller) { double }
14
+
15
+ describe '#add' do
16
+ it 'adds the parameter to the filter list' do
17
+ filter = search_state.filter('some_field')
18
+ new_state = filter.add('4')
19
+
20
+ expect(new_state.filter('some_field').values).to eq %w[1 2 4]
21
+ end
22
+
23
+ it 'creates new parameter as needed' do
24
+ filter = search_state.filter('unknown_field')
25
+ new_state = filter.add('4')
26
+
27
+ expect(new_state.filter('unknown_field').values).to eq %w[4]
28
+ expect(new_state.params[:f]).to include(:unknown_field)
29
+ end
30
+
31
+ context 'without any parameters in the url' do
32
+ let(:params) { {} }
33
+
34
+ it 'adds the necessary structure' do
35
+ filter = search_state.filter('some_field')
36
+ new_state = filter.add('1')
37
+
38
+ expect(new_state.filter('some_field').values).to eq %w[1]
39
+ expect(new_state.params).to include(:f)
40
+ end
41
+ end
42
+
43
+ context 'with a single-valued field' do
44
+ it 'replaces any existing parameter from the filter list' do
45
+ filter = search_state.filter('another_field')
46
+ new_state = filter.add('5')
47
+
48
+ expect(new_state.filter('another_field').values).to eq %w[5]
49
+ end
50
+ end
51
+
52
+ context 'with a pivot facet-type item' do
53
+ it 'includes the pivot facet fqs' do
54
+ filter = search_state.filter('some_field')
55
+ new_state = filter.add(OpenStruct.new(fq: { some_other_field: '5' }, value: '4'))
56
+
57
+ expect(new_state.filter('some_field').values).to eq %w[1 2 4]
58
+ expect(new_state.filter('some_other_field').values).to eq %w[5]
59
+ end
60
+
61
+ it 'handles field indirection' do
62
+ filter = search_state.filter('some_field')
63
+ new_state = filter.add(OpenStruct.new(field: 'some_other_field', value: '4'))
64
+
65
+ expect(new_state.filter('some_other_field').values).to eq %w[4]
66
+ end
67
+
68
+ it 'handles value indirection' do
69
+ filter = search_state.filter('some_field')
70
+ new_state = filter.add(OpenStruct.new(value: '4'))
71
+
72
+ expect(new_state.filter('some_field').values).to eq %w[1 2 4]
73
+ end
74
+ end
75
+ end
76
+
77
+ describe '#remove' do
78
+ it 'returns a search state without the given filter applied' do
79
+ filter = search_state.filter('some_field')
80
+ new_state = filter.remove('1')
81
+
82
+ expect(new_state.filter('some_field').values).to eq ['2']
83
+ end
84
+
85
+ it 'removes the whole field if there are no filter left for the field' do
86
+ filter = search_state.filter('another_field')
87
+ new_state = filter.remove('3')
88
+
89
+ expect(new_state.filter('another_field').values).to eq []
90
+ expect(new_state.params[:f]).not_to include :another_field
91
+ end
92
+
93
+ it 'removes the filter parameter entirely if there are no filters left' do
94
+ new_state = search_state.filter('some_field').remove('1')
95
+ new_state = new_state.filter('some_field').remove('2')
96
+ new_state = new_state.filter('another_field').remove('3')
97
+
98
+ expect(new_state.params).not_to include :f
99
+ end
100
+
101
+ it 'handles value indirection' do
102
+ filter = search_state.filter('some_field')
103
+ new_state = filter.remove(OpenStruct.new(value: '1'))
104
+
105
+ expect(new_state.filter('some_field').values).to eq ['2']
106
+ end
107
+ end
108
+
109
+ describe '#values' do
110
+ it 'returns the currently selected values of the filter' do
111
+ expect(search_state.filter('some_field').values).to eq %w[1 2]
112
+ end
113
+ end
114
+
115
+ describe '#include?' do
116
+ it 'checks whether the value is currently selected' do
117
+ expect(search_state.filter('some_field').include?('1')).to eq true
118
+ expect(search_state.filter('some_field').include?('3')).to eq false
119
+ end
120
+
121
+ it 'handles value indirection' do
122
+ expect(search_state.filter('some_field').include?(OpenStruct.new(value: '1'))).to eq true
123
+ end
124
+ end
125
+ end
@@ -3,6 +3,8 @@
3
3
  RSpec.describe Blacklight::SearchState do
4
4
  subject(:search_state) { described_class.new(params, blacklight_config, controller) }
5
5
 
6
+ around { |test| Deprecation.silence(described_class) { test.call } }
7
+
6
8
  let(:blacklight_config) do
7
9
  Blacklight::Configuration.new.configure do |config|
8
10
  config.index.title_field = 'title_tsim'
@@ -242,9 +244,9 @@ RSpec.describe Blacklight::SearchState do
242
244
  end
243
245
 
244
246
  it "uses the facet's key in the url" do
245
- allow(search_state).to receive(:facet_configuration_for_field).with('some_field').and_return(double(single: true, field: "a_solr_field", key: "some_key"))
247
+ blacklight_config.add_facet_field 'some_key', single: true, field: "a_solr_field"
246
248
 
247
- result_params = search_state.add_facet_params('some_field', 'my_value')
249
+ result_params = search_state.add_facet_params('some_key', 'my_value')
248
250
 
249
251
  expect(result_params[:f]['some_key']).to have(1).item
250
252
  expect(result_params[:f]['some_key'].first).to eq 'my_value'
@@ -284,7 +286,7 @@ RSpec.describe Blacklight::SearchState do
284
286
  let(:params) { parameter_class.new f: { 'single_value_facet_field' => 'other_value' } }
285
287
 
286
288
  it "replaces facets configured as single" do
287
- allow(search_state).to receive(:facet_configuration_for_field).with('single_value_facet_field').and_return(double(single: true, key: "single_value_facet_field"))
289
+ blacklight_config.add_facet_field 'single_value_facet_field', single: true
288
290
  result_params = search_state.add_facet_params('single_value_facet_field', 'my_value')
289
291
 
290
292
  expect(result_params[:f]['single_value_facet_field']).to have(1).item
@@ -415,4 +417,131 @@ RSpec.describe Blacklight::SearchState do
415
417
  expect(new_state.to_hash).to eq('a' => 1)
416
418
  end
417
419
  end
420
+
421
+ describe '#page' do
422
+ context 'with a page' do
423
+ let(:params) { { 'page' => '3' } }
424
+
425
+ it 'is mapped from page' do
426
+ expect(search_state.page).to eq 3
427
+ end
428
+ end
429
+
430
+ context 'without a page' do
431
+ let(:params) { {} }
432
+
433
+ it 'is defaults to page 1' do
434
+ expect(search_state.page).to eq 1
435
+ end
436
+
437
+ context 'with negative numbers or other bad data' do
438
+ let(:params) { { 'page' => '-3' } }
439
+
440
+ it 'is defaults to page 1' do
441
+ expect(search_state.page).to eq 1
442
+ end
443
+ end
444
+ end
445
+ end
446
+
447
+ describe '#per_page' do
448
+ context 'with rows' do
449
+ let(:params) { { rows: '30' } }
450
+
451
+ it 'maps from rows' do
452
+ expect(search_state.per_page).to eq 30
453
+ end
454
+ end
455
+
456
+ context 'with per_page' do
457
+ let(:params) { { per_page: '14' } }
458
+
459
+ it 'maps from rows' do
460
+ expect(search_state.per_page).to eq 14
461
+ end
462
+ end
463
+
464
+ context 'it defaults to the configured value' do
465
+ let(:params) { {} }
466
+
467
+ it 'maps from rows' do
468
+ expect(search_state.per_page).to eq 10
469
+ end
470
+ end
471
+ end
472
+
473
+ describe '#sort_field' do
474
+ let(:params) { { 'sort' => 'author' } }
475
+
476
+ before do
477
+ blacklight_config.add_sort_field 'relevancy', label: 'relevance'
478
+ blacklight_config.add_sort_field 'author', label: 'asd'
479
+ end
480
+
481
+ it 'returns the current search field' do
482
+ expect(search_state.sort_field).to have_attributes(key: 'author')
483
+ end
484
+
485
+ context 'without a search field' do
486
+ let(:params) { {} }
487
+
488
+ it 'returns the current search field' do
489
+ expect(search_state.sort_field).to have_attributes(key: 'relevancy')
490
+ end
491
+ end
492
+ end
493
+
494
+ describe '#search_field' do
495
+ let(:params) { { 'search_field' => 'author' } }
496
+
497
+ before do
498
+ blacklight_config.add_search_field 'author', label: 'asd'
499
+ end
500
+
501
+ it 'returns the current search field' do
502
+ expect(search_state.search_field).to have_attributes(key: 'author')
503
+ end
504
+ end
505
+
506
+ describe '#facet_page' do
507
+ context 'with a page' do
508
+ let(:params) { { 'facet.page' => '3' } }
509
+
510
+ it 'is mapped from facet.page' do
511
+ expect(search_state.facet_page).to eq 3
512
+ end
513
+ end
514
+
515
+ context 'without a page' do
516
+ let(:params) { {} }
517
+
518
+ it 'is defaults to page 1' do
519
+ expect(search_state.facet_page).to eq 1
520
+ end
521
+ end
522
+
523
+ context 'with negative numbers or other bad data' do
524
+ let(:params) { { 'facet.page' => '-3' } }
525
+
526
+ it 'is defaults to page 1' do
527
+ expect(search_state.facet_page).to eq 1
528
+ end
529
+ end
530
+ end
531
+
532
+ describe '#facet_sort' do
533
+ let(:params) { { 'facet.sort' => 'index' } }
534
+
535
+ it 'is mapped from facet.sort' do
536
+ expect(search_state.facet_sort).to eq 'index'
537
+ end
538
+ end
539
+
540
+ describe '#facet_prefix' do
541
+ let(:params) { { 'facet.prefix' => 'A' } }
542
+
543
+ it 'is mapped from facet.prefix' do
544
+ expect(search_state.facet_prefix).to eq 'A'
545
+ end
546
+ end
418
547
  end
@@ -62,6 +62,14 @@ RSpec.describe "Blacklight::Configuration", api: true do
62
62
  end
63
63
  end
64
64
 
65
+ describe 'config.index.document_actions' do
66
+ it 'allows you to use the << operator' do
67
+ config.index.document_actions << :blah
68
+ expect(config.index.document_actions.blah).to have_attributes key: :blah
69
+ expect(config.index.document_actions.blah.name).to eq :blah
70
+ end
71
+ end
72
+
65
73
  describe "config.index.respond_to" do
66
74
  it "has a list of additional formats for index requests to respond to" do
67
75
  config.index.respond_to.xml = true
@@ -186,9 +186,9 @@ RSpec.describe Blacklight::Solr::SearchBuilderBehavior, api: true do
186
186
  end
187
187
 
188
188
  describe "for request params also passed in as argument" do
189
- let(:user_params) { { q: "some query", 'q' => 'another value' } }
189
+ let(:user_params) { { 'q' => 'another value', q: "some query" } }
190
190
 
191
- it "onlies have one value for the key 'q' regardless if a symbol or string" do
191
+ it "only has one value for the key 'q' regardless if a symbol or string" do
192
192
  expect(subject[:q]).to eq 'some query'
193
193
  expect(subject['q']).to eq 'some query'
194
194
  end
@@ -244,6 +244,21 @@ RSpec.describe Blacklight::Solr::SearchBuilderBehavior, api: true do
244
244
  end
245
245
  end
246
246
 
247
+ describe 'with a facet with a custom filter query builder' do
248
+ let(:user_params) { { f: { some: ['value'] } }.with_indifferent_access }
249
+
250
+ before do
251
+ blacklight_config.add_facet_field 'some', filter_query_builder: (lambda do |*_args|
252
+ ['some:filter', { qq1: 'abc' }]
253
+ end)
254
+ end
255
+
256
+ it "has proper solr parameters" do
257
+ expect(subject[:fq]).to include('some:filter')
258
+ expect(subject[:qq1]).to include('abc')
259
+ end
260
+ end
261
+
247
262
  describe "solr parameters for a field search from config (subject)" do
248
263
  let(:user_params) { subject_search_params }
249
264
 
@@ -384,6 +399,21 @@ RSpec.describe Blacklight::Solr::SearchBuilderBehavior, api: true do
384
399
  end
385
400
  end
386
401
 
402
+ describe 'the search field query_builder config' do
403
+ let(:blacklight_config) do
404
+ Blacklight::Configuration.new do |config|
405
+ config.add_search_field('built_query', query_builder: ->(builder, *_args) { [builder.blacklight_params[:q].reverse, qq1: 'xyz'] })
406
+ end
407
+ end
408
+
409
+ let(:user_params) { { search_field: 'built_query', q: 'value' } }
410
+
411
+ it 'uses the provided query builder' do
412
+ expect(subject[:q]).to eq 'eulav'
413
+ expect(subject[:qq1]).to eq 'xyz'
414
+ end
415
+ end
416
+
387
417
  describe "mapping facet.field" do
388
418
  let(:blacklight_config) do
389
419
  Blacklight::Configuration.new do |config|