diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index f29c386ae..0b35f0bda 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -3,12 +3,14 @@ class SearchController < ApplicationController before_action :format_integer_params before_action :sanitize_params, only: %i[classified_listings] - CLASSIFIED_LISTINGS_PARAMS = %i[ - category - classified_listing_search - page - per_page - tags + CLASSIFIED_LISTINGS_PARAMS = [ + :category, + :classified_listing_search, + :page, + :per_page, + { + tags: [] + }, ].freeze USER_PARAMS = %i[ @@ -17,15 +19,17 @@ class SearchController < ApplicationController per_page ].freeze - FEED_PARAMS = %i[ - page - per_page - published_at - search_fields - sort_by - tag_names - user_id - class_name + FEED_PARAMS = [ + :class_name, + :page, + :per_page, + :search_fields, + :sort_by, + :user_id, + { + tag_names: [], + published_at: [:gte] + }, ].freeze def tags diff --git a/app/javascript/src/utils/__tests__/search.test.js b/app/javascript/src/utils/__tests__/search.test.js index ef589e678..8a3a1c7ea 100644 --- a/app/javascript/src/utils/__tests__/search.test.js +++ b/app/javascript/src/utils/__tests__/search.test.js @@ -5,6 +5,7 @@ import { hasInstantClick, displaySearchResults, fetchSearch, + createSearchUrl, } from '../search'; import '../../../../assets/javascripts/lib/xss'; @@ -61,7 +62,7 @@ describe('Search utilities', () => { describe('preloadSearchResults', () => { beforeEach(() => { global.InstantClick = { - preload: url => url, + preload: (url) => url, }; jest.spyOn(InstantClick, 'preload'); }); @@ -145,7 +146,7 @@ describe('Search utilities', () => { describe('displaySearchResults', () => { beforeEach(() => { global.InstantClick = { - display: url => url, + display: (url) => url, }; jest.spyOn(InstantClick, 'display'); }); @@ -242,10 +243,18 @@ describe('Search utilities', () => { }); test('should return response formatted as JSON', () => { - responsePromise.then(response => { + responsePromise.then((response) => { expect(response).toBeInstanceOf(Object); expect(response).toMatchObject({ results: expect.any(Array) }); }); }); }); + + describe('createSearchUrl', () => { + test('should return a url string', () => { + const dataHash = { name: 'jav', tags: ['one', 'two'] }; + const responseString = createSearchUrl(dataHash); + expect(responseString).toEqual('name=jav&tags%5B%5D=one&tags%5B%5D=two'); + }); + }); }); diff --git a/app/javascript/src/utils/search.js b/app/javascript/src/utils/search.js index 5d5388c0a..a78f21d93 100644 --- a/app/javascript/src/utils/search.js +++ b/app/javascript/src/utils/search.js @@ -33,7 +33,7 @@ function fixedEncodeURIComponent(str) { // from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent return encodeURIComponent(str).replace( /[!'()*]/g, - c => `%${c.charCodeAt(0).toString(16)}`, + (c) => `%${c.charCodeAt(0).toString(16)}`, ); } @@ -79,6 +79,22 @@ export function preloadSearchResults({ ); } +export function createSearchUrl(dataHash) { + const searchParams = new URLSearchParams(); + Object.keys(dataHash).forEach((key) => { + const value = dataHash[key]; + if (Array.isArray(value)) { + value.forEach((arrayValue) => { + searchParams.append(`${key}[]`, arrayValue); + }); + } else { + searchParams.append(key, value); + } + }); + + return searchParams.toString(); +} + /** * A helper method to call /search endpoints. * @@ -88,9 +104,9 @@ export function preloadSearchResults({ * @returns {Promise} A promise object with response formatted as JSON. */ export function fetchSearch(endpoint, dataHash) { - const searchParams = new URLSearchParams(dataHash).toString(); + const searchUrl = createSearchUrl(dataHash); - return fetch(`/search/${endpoint}?${searchParams}`, { + return fetch(`/search/${endpoint}?${searchUrl}`, { method: 'GET', headers: { Accept: 'application/json', @@ -98,5 +114,5 @@ export function fetchSearch(endpoint, dataHash) { 'Content-Type': 'application/json', }, credentials: 'same-origin', - }).then(response => response.json()); + }).then((response) => response.json()); } diff --git a/app/services/search/query_builders/classified_listing.rb b/app/services/search/query_builders/classified_listing.rb index db5090e9a..09359e3c3 100644 --- a/app/services/search/query_builders/classified_listing.rb +++ b/app/services/search/query_builders/classified_listing.rb @@ -63,7 +63,7 @@ module Search TERM_KEYS.map do |term_key| next unless @params.key? term_key - { term: { term_key => @params[term_key] } } + { terms: { term_key => Array.wrap(@params[term_key]) } } end.compact end diff --git a/spec/services/search/query_builders/classified_listing_spec.rb b/spec/services/search/query_builders/classified_listing_spec.rb index 3d66e3b26..ea3147d50 100644 --- a/spec/services/search/query_builders/classified_listing_spec.rb +++ b/spec/services/search/query_builders/classified_listing_spec.rb @@ -19,10 +19,10 @@ RSpec.describe Search::QueryBuilders::ClassifiedListing, type: :service do params = { category: "cfp", tags: ["beginner"], contact_via_connect: false } filter = described_class.new(params: params) exepcted_filters = [ - { "term" => { "category" => "cfp" } }, - { "term" => { "tags" => ["beginner"] } }, - { "term" => { "contact_via_connect" => false } }, - { "term" => { "published" => true } }, + { "terms" => { "category" => ["cfp"] } }, + { "terms" => { "tags" => ["beginner"] } }, + { "terms" => { "contact_via_connect" => [false] } }, + { "terms" => { "published" => [true] } }, ] expect(filter.as_hash.dig("query", "bool", "filter")).to match_array(exepcted_filters) end @@ -34,7 +34,7 @@ RSpec.describe Search::QueryBuilders::ClassifiedListing, type: :service do exepcted_filters = [ { "range" => { "bumped_at" => Time.current } }, { "range" => { "expires_at" => 1.day.from_now } }, - { "term" => { "published" => true } }, + { "terms" => { "published" => [true] } }, ] expect(filter.as_hash.dig("query", "bool", "filter")).to match_array(exepcted_filters) end @@ -60,8 +60,8 @@ RSpec.describe Search::QueryBuilders::ClassifiedListing, type: :service do }] exepcted_filters = [ { "range" => { "bumped_at" => Time.current } }, - { "term" => { "category" => "cfp" } }, - { "term" => { "published" => true } }, + { "terms" => { "category" => ["cfp"] } }, + { "terms" => { "published" => [true] } }, ] expect(filter.as_hash.dig("query", "bool", "must")).to match_array(exepcted_query) expect(filter.as_hash.dig("query", "bool", "filter")).to match_array(exepcted_filters) @@ -72,8 +72,8 @@ RSpec.describe Search::QueryBuilders::ClassifiedListing, type: :service do params = { not_supported: "trash", category: "cfp" } filter = described_class.new(params: params) exepcted_filters = [ - { "term" => { "category" => "cfp" } }, - { "term" => { "published" => true } }, + { "terms" => { "category" => ["cfp"] } }, + { "terms" => { "published" => [true] } }, ] expect(filter.as_hash.dig("query", "bool", "filter")).to match_array(exepcted_filters) end @@ -82,7 +82,7 @@ RSpec.describe Search::QueryBuilders::ClassifiedListing, type: :service do filter = described_class.new(params: {}).as_hash expect(filter.dig("sort")).to eq("bumped_at" => "desc") expect(filter.dig("size")).to eq(0) - expect(filter.dig("query", "bool", "filter")).to match_array([{ "term" => { "published" => true } }]) + expect(filter.dig("query", "bool", "filter")).to match_array([{ "terms" => { "published" => [true] } }]) end it "allows default params to be overriden" do