diff --git a/app/frontend/modules/search.js b/app/frontend/modules/search.js index d9d233ab1..5e22c0cf5 100644 --- a/app/frontend/modules/search.js +++ b/app/frontend/modules/search.js @@ -12,9 +12,13 @@ const clean = (text) => .toLowerCase() const cleanseOption = (option) => { + const synonyms = (option.synonyms || []).map(clean) + option.clean = { name: clean(option.name), nameWithoutStopWords: removeStopWords(option.name), + synonyms, + synonymsWithoutStopWords: synonyms.map(removeStopWords), boost: option.boost || 1 } @@ -41,19 +45,34 @@ const startsWith = (word, query) => word.search(startsWithRegExp(query)) === 0 const wordsStartsWithQuery = (word, regExps) => regExps.every((regExp) => word.search(regExp) >= 0) -const calculateWeight = ({ name, nameWithoutStopWords }, query) => { +const anyMatch = (words, query, evaluatorFunc) => words.some((word) => evaluatorFunc(word, query)) +const synonymsExactMatch = (synonyms, query) => anyMatch(synonyms, query, exactMatch) +const synonymsStartsWith = (synonyms, query) => anyMatch(synonyms, query, startsWith) + +const wordInSynonymStartsWithQuery = (synonyms, startsWithQueryWordsRegexes) => + anyMatch(synonyms, startsWithQueryWordsRegexes, wordsStartsWithQuery) + +const calculateWeight = ({ name, synonyms, nameWithoutStopWords, synonymsWithoutStopWords }, query) => { const queryWithoutStopWords = removeStopWords(query) if (exactMatch(name, query)) return 100 if (exactMatch(nameWithoutStopWords, queryWithoutStopWords)) return 95 + if (synonymsExactMatch(synonyms, query)) return 75 + if (synonymsExactMatch(synonymsWithoutStopWords, queryWithoutStopWords)) return 70 + if (startsWith(name, query)) return 60 if (startsWith(nameWithoutStopWords, queryWithoutStopWords)) return 55 + + if (synonymsStartsWith(synonyms, query)) return 50 + if (synonymsStartsWith(synonyms, queryWithoutStopWords)) return 40 + const startsWithRegExps = queryWithoutStopWords .split(/\s+/) .map(startsWithRegExp) if (wordsStartsWithQuery(nameWithoutStopWords, startsWithRegExps)) return 25 + if (wordInSynonymStartsWithQuery(synonymsWithoutStopWords, startsWithRegExps)) return 10 return 0 } @@ -91,8 +110,8 @@ export const sort = (query, options) => { export const suggestion = (value, options) => { const option = options.find((o) => o.name === value) if (option) { - const html = `${value}` - return html + const html = option.append ? `${value} ${option.append}` : `${value}` + return option.hint ? `${html}
${option.hint}
` : html } else { return 'No results found' } @@ -101,6 +120,9 @@ export const suggestion = (value, options) => { export const enhanceOption = (option) => { return { name: option.label, + synonyms: (option.getAttribute('data-synonyms') ? option.getAttribute('data-synonyms').split('|') : []), + append: option.getAttribute('data-append'), + hint: option.getAttribute('data-hint'), boost: parseFloat(option.getAttribute('data-boost')) || 1 } } diff --git a/app/models/form/question.rb b/app/models/form/question.rb index 9dd7488cb..d98fe5388 100644 --- a/app/models/form/question.rb +++ b/app/models/form/question.rb @@ -191,6 +191,25 @@ class Form::Question label end + def answer_option_synonyms(answer_id) + if id == "scheme_id" + Scheme.find(answer_id).locations.map(&:postcode).join(",") + end + end + + def answer_option_append(answer_id) + if id == "scheme_id" + "(" + Scheme.find(answer_id).locations.count.to_s + " locations)" + end + end + + def answer_option_hint(answer_id) + if id == "scheme_id" + scheme = Scheme.find(answer_id) + [scheme.primary_client_group, scheme.secondary_client_group].filter { |x| x.present? }.join(", ") + end + end + private def selected_answer_option_is_derived?(case_log) diff --git a/app/views/form/_select_question.html.erb b/app/views/form/_select_question.html.erb index 52c47d66d..bd25041ab 100644 --- a/app/views/form/_select_question.html.erb +++ b/app/views/form/_select_question.html.erb @@ -2,12 +2,17 @@ <% selected = @case_log.public_send(question.id) || "" %> <% answers = question.displayed_answer_options(@case_log).map { |key, value| OpenStruct.new(id: key, name: value) } %> - <%= f.govuk_collection_select question.id.to_sym, - answers, - :id, - :name, - caption: caption(caption_text, page_header, conditional), - label: legend(question, page_header, conditional), - hint: { text: question.hint_text&.html_safe }, - options: { disabled: [""], selected: }, - "data-controller": "accessible-autocomplete" %> + <%= f.govuk_select(question.id.to_sym, + label: legend(question, page_header, conditional), + "data-controller": "accessible-autocomplete", + caption: caption(caption_text, page_header, conditional), + hint: { text: question.hint_text&.html_safe }) do %> + <% answers.each do |answer| %> + + <% end %> + <% end %> diff --git a/spec/features/form/accessible_autocomplete_spec.rb b/spec/features/form/accessible_autocomplete_spec.rb index a8537199b..55bbba6bb 100644 --- a/spec/features/form/accessible_autocomplete_spec.rb +++ b/spec/features/form/accessible_autocomplete_spec.rb @@ -23,6 +23,12 @@ RSpec.describe "Accessible Automcomplete" do context "when using accessible autocomplete" do before do + allow_any_instance_of(Form::Question).to receive(:answer_option_synonyms).and_return(nil) + allow_any_instance_of(Form::Question).to receive(:answer_option_synonyms).with("E08000003").and_return("synonym") + allow_any_instance_of(Form::Question).to receive(:answer_option_append).and_return(nil) + allow_any_instance_of(Form::Question).to receive(:answer_option_append).with("E08000003").and_return(" (append)") + allow_any_instance_of(Form::Question).to receive(:answer_option_hint).and_return(nil) + allow_any_instance_of(Form::Question).to receive(:answer_option_hint).with("E08000003").and_return("hint") visit("/logs/#{case_log.id}/accessible-select") end @@ -46,6 +52,21 @@ RSpec.describe "Accessible Automcomplete" do expect(find("#case-log-prevloc-field").value).to eq("The one and only york town") end + it "can match on synonyms", js: true do + find("#case-log-prevloc-field").click.native.send_keys("s", "y", "n", "o", "n", :down, :enter) + expect(find("#case-log-prevloc-field").value).to eq("Manchester") + end + + it "displays appended text next to the options", js: true do + find("#case-log-prevloc-field").click.native.send_keys("m", "a", "n", :down, :enter) + expect(find(".autocomplete__option__append", visible: :hidden, text: /(append)/)).to be_present + end + + it "displays hint text under the options", js: true do + find("#case-log-prevloc-field").click.native.send_keys("m", "a", "n", :down, :enter) + expect(find(".autocomplete__option__hint", visible: :hidden, text: /hint/)).to be_present + end + it "maintains enhancement state across back navigation", js: true do find("#case-log-prevloc-field").click.native.send_keys("T", "h", "a", "n", :down, :enter) click_button("Save and continue") @@ -59,7 +80,7 @@ RSpec.describe "Accessible Automcomplete" do end it "has the correct option selected if one has been saved" do - case_log.update!(postcode_known: 0, previous_la_known: 1, prevloc: "E07000178") + case_log.update!(postcode_known: 0, previous_la_known: 1, prevloc: "Oxford") visit("/logs/#{case_log.id}/accessible-select") expect(page).to have_select("case-log-prevloc-field", selected: %w[Oxford]) end