From 8a6148f9d134fc71e499cfcb9c0039ba09a24b73 Mon Sep 17 00:00:00 2001 From: kosiakkatrina <54268893+kosiakkatrina@users.noreply.github.com> Date: Thu, 14 Jul 2022 08:56:58 +0100 Subject: [PATCH] add synonyms, appended text and hint text to the autocomplete (#739) * add synonyms, appended text and hint text to the autocomplete * update test part 1 * refactor synonym, append and hint methods to take in resource * Fix tests and lint * Change min autocomplete character length to 1 * change test --- .../accessible_autocomplete_controller.js | 2 +- app/frontend/modules/search.js | 28 +++++++++++++++++-- app/models/form/question.rb | 24 +++++++++++++++- app/models/form/setup/questions/scheme_id.rb | 4 +-- app/models/scheme.rb | 12 ++++++++ app/views/form/_select_question.html.erb | 25 ++++++++++------- .../form/accessible_autocomplete_spec.rb | 27 ++++++++++++++++++ .../form/setup/questions/scheme_id_spec.rb | 2 +- 8 files changed, 106 insertions(+), 18 deletions(-) diff --git a/app/frontend/controllers/accessible_autocomplete_controller.js b/app/frontend/controllers/accessible_autocomplete_controller.js index 0b17e8aa8..812cd37d3 100644 --- a/app/frontend/controllers/accessible_autocomplete_controller.js +++ b/app/frontend/controllers/accessible_autocomplete_controller.js @@ -15,7 +15,7 @@ export default class extends Controller { accessibleAutocomplete.enhanceSelectElement({ defaultValue: '', selectElement: selectEl, - minLength: 2, + minLength: 1, source: (query, populateResults) => { if (/\S/.test(query)) { populateResults(sort(query, options)) 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..0a2bc9998 100644 --- a/app/models/form/question.rb +++ b/app/models/form/question.rb @@ -136,7 +136,11 @@ class Form::Question labels = answer_options[value.to_s] labels["value"] if labels when "select" - answer_options[value.to_s] + if answer_options[value.to_s].respond_to?(:service_name) + answer_options[value.to_s].service_name + else + answer_options[value.to_s] + end else value.to_s end @@ -191,6 +195,24 @@ class Form::Question label end + def answer_option_synonyms(resource) + return unless resource.respond_to?(:synonyms) + + resource.synonyms + end + + def answer_option_append(resource) + return unless resource.respond_to?(:appended_text) + + resource.appended_text + end + + def answer_option_hint(resource) + return unless resource.respond_to?(:hint) + + resource.hint + end + private def selected_answer_option_is_derived?(case_log) diff --git a/app/models/form/setup/questions/scheme_id.rb b/app/models/form/setup/questions/scheme_id.rb index d17cbc806..e69cd05a3 100644 --- a/app/models/form/setup/questions/scheme_id.rb +++ b/app/models/form/setup/questions/scheme_id.rb @@ -13,8 +13,8 @@ class Form::Setup::Questions::SchemeId < ::Form::Question answer_opts = {} return answer_opts unless ActiveRecord::Base.connected? - Scheme.select(:id, :service_name).each_with_object(answer_opts) do |scheme, hsh| - hsh[scheme.id.to_s] = scheme.service_name + Scheme.select(:id, :service_name, :primary_client_group, :secondary_client_group).each_with_object(answer_opts) do |scheme, hsh| + hsh[scheme.id.to_s] = scheme hsh end end diff --git a/app/models/scheme.rb b/app/models/scheme.rb index bafa7d395..daf7c2564 100644 --- a/app/models/scheme.rb +++ b/app/models/scheme.rb @@ -142,4 +142,16 @@ class Scheme < ApplicationRecord { name: "Intended length of stay", value: intended_stay }, ] end + + def synonyms + locations.map(&:postcode).join(",") + end + + def appended_text + "(#{locations.count} locations)" + end + + def hint + [primary_client_group, secondary_client_group].filter(&:present?).join(", ") + end end diff --git a/app/views/form/_select_question.html.erb b/app/views/form/_select_question.html.erb index 52c47d66d..adbddfcd1 100644 --- a/app/views/form/_select_question.html.erb +++ b/app/views/form/_select_question.html.erb @@ -1,13 +1,18 @@ <%= render partial: "form/guidance/#{question.guidance_partial}" if question.guidance_partial %> <% 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" %> +<% answers = question.displayed_answer_options(@case_log).map { |key, value| OpenStruct.new(id: key, name: value.respond_to?(:service_name) ? value.service_name : nil, resource: value) } %> +<%= 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..e948056b4 100644 --- a/spec/features/form/accessible_autocomplete_spec.rb +++ b/spec/features/form/accessible_autocomplete_spec.rb @@ -14,6 +14,7 @@ RSpec.describe "Accessible Automcomplete" do is_la_inferred: false, owning_organisation: user.organisation, managing_organisation: user.organisation, + created_by: user, ) end @@ -58,6 +59,32 @@ RSpec.describe "Accessible Automcomplete" do end end + context "when searching schemes" do + let(:scheme) { FactoryBot.create(:scheme, owning_organisation_id: case_log.created_by.organisation_id, primary_client_group: "Q", secondary_client_group: "P") } + + before do + FactoryBot.create(:location, scheme:, postcode: "W6 0ST") + FactoryBot.create(:location, scheme:, postcode: "SE6 1LB") + case_log.update!(needstype: 2) + visit("/logs/#{case_log.id}/scheme") + end + + it "can match on synonyms", js: true do + find("#case-log-scheme-id-field").click.native.send_keys("w", "6", :down, :enter) + expect(find("#case-log-scheme-id-field").value).to eq(scheme.service_name) + end + + it "displays appended text next to the options", js: true do + find("#case-log-scheme-id-field").click.native.send_keys("w", "6", :down, :enter) + expect(find(".autocomplete__option__append", visible: :hidden, text: /(2 locations)/)).to be_present + end + + it "displays hint text under the options", js: true do + find("#case-log-scheme-id-field").click.native.send_keys("w", "6", :down, :enter) + expect(find(".autocomplete__option__hint", visible: :hidden, text: /Young people at risk, Young people leaving care/)).to be_present + end + 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") visit("/logs/#{case_log.id}/accessible-select") diff --git a/spec/models/form/setup/questions/scheme_id_spec.rb b/spec/models/form/setup/questions/scheme_id_spec.rb index c40d5302c..9dde00b84 100644 --- a/spec/models/form/setup/questions/scheme_id_spec.rb +++ b/spec/models/form/setup/questions/scheme_id_spec.rb @@ -51,7 +51,7 @@ RSpec.describe Form::Setup::Questions::SchemeId, type: :model do end it "has the correct answer_options based on the schemes the user's organisation owns or manages" do - expected_answer = { scheme.id.to_s => scheme.service_name } + expected_answer = { scheme.id.to_s => scheme } expect(question.displayed_answer_options(case_log)).to eq(expected_answer) end end