Browse Source

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
pull/701/head
kosiakkatrina 2 years ago committed by GitHub
parent
commit
8a6148f9d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      app/frontend/controllers/accessible_autocomplete_controller.js
  2. 28
      app/frontend/modules/search.js
  3. 22
      app/models/form/question.rb
  4. 4
      app/models/form/setup/questions/scheme_id.rb
  5. 12
      app/models/scheme.rb
  6. 23
      app/views/form/_select_question.html.erb
  7. 27
      spec/features/form/accessible_autocomplete_spec.rb
  8. 2
      spec/models/form/setup/questions/scheme_id_spec.rb

2
app/frontend/controllers/accessible_autocomplete_controller.js

@ -15,7 +15,7 @@ export default class extends Controller {
accessibleAutocomplete.enhanceSelectElement({ accessibleAutocomplete.enhanceSelectElement({
defaultValue: '', defaultValue: '',
selectElement: selectEl, selectElement: selectEl,
minLength: 2, minLength: 1,
source: (query, populateResults) => { source: (query, populateResults) => {
if (/\S/.test(query)) { if (/\S/.test(query)) {
populateResults(sort(query, options)) populateResults(sort(query, options))

28
app/frontend/modules/search.js

@ -12,9 +12,13 @@ const clean = (text) =>
.toLowerCase() .toLowerCase()
const cleanseOption = (option) => { const cleanseOption = (option) => {
const synonyms = (option.synonyms || []).map(clean)
option.clean = { option.clean = {
name: clean(option.name), name: clean(option.name),
nameWithoutStopWords: removeStopWords(option.name), nameWithoutStopWords: removeStopWords(option.name),
synonyms,
synonymsWithoutStopWords: synonyms.map(removeStopWords),
boost: option.boost || 1 boost: option.boost || 1
} }
@ -41,19 +45,34 @@ const startsWith = (word, query) => word.search(startsWithRegExp(query)) === 0
const wordsStartsWithQuery = (word, regExps) => const wordsStartsWithQuery = (word, regExps) =>
regExps.every((regExp) => word.search(regExp) >= 0) 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) const queryWithoutStopWords = removeStopWords(query)
if (exactMatch(name, query)) return 100 if (exactMatch(name, query)) return 100
if (exactMatch(nameWithoutStopWords, queryWithoutStopWords)) return 95 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(name, query)) return 60
if (startsWith(nameWithoutStopWords, queryWithoutStopWords)) return 55 if (startsWith(nameWithoutStopWords, queryWithoutStopWords)) return 55
if (synonymsStartsWith(synonyms, query)) return 50
if (synonymsStartsWith(synonyms, queryWithoutStopWords)) return 40
const startsWithRegExps = queryWithoutStopWords const startsWithRegExps = queryWithoutStopWords
.split(/\s+/) .split(/\s+/)
.map(startsWithRegExp) .map(startsWithRegExp)
if (wordsStartsWithQuery(nameWithoutStopWords, startsWithRegExps)) return 25 if (wordsStartsWithQuery(nameWithoutStopWords, startsWithRegExps)) return 25
if (wordInSynonymStartsWithQuery(synonymsWithoutStopWords, startsWithRegExps)) return 10
return 0 return 0
} }
@ -91,8 +110,8 @@ export const sort = (query, options) => {
export const suggestion = (value, options) => { export const suggestion = (value, options) => {
const option = options.find((o) => o.name === value) const option = options.find((o) => o.name === value)
if (option) { if (option) {
const html = `<span>${value}</span>` const html = option.append ? `<span>${value}</span> <span class="autocomplete__option__append">${option.append}</span>` : `<span>${value}</span>`
return html return option.hint ? `${html}<div class="autocomplete__option__hint">${option.hint}</div>` : html
} else { } else {
return '<span>No results found</span>' return '<span>No results found</span>'
} }
@ -101,6 +120,9 @@ export const suggestion = (value, options) => {
export const enhanceOption = (option) => { export const enhanceOption = (option) => {
return { return {
name: option.label, 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 boost: parseFloat(option.getAttribute('data-boost')) || 1
} }
} }

22
app/models/form/question.rb

@ -136,7 +136,11 @@ class Form::Question
labels = answer_options[value.to_s] labels = answer_options[value.to_s]
labels["value"] if labels labels["value"] if labels
when "select" when "select"
if answer_options[value.to_s].respond_to?(:service_name)
answer_options[value.to_s].service_name
else
answer_options[value.to_s] answer_options[value.to_s]
end
else else
value.to_s value.to_s
end end
@ -191,6 +195,24 @@ class Form::Question
label label
end 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 private
def selected_answer_option_is_derived?(case_log) def selected_answer_option_is_derived?(case_log)

4
app/models/form/setup/questions/scheme_id.rb

@ -13,8 +13,8 @@ class Form::Setup::Questions::SchemeId < ::Form::Question
answer_opts = {} answer_opts = {}
return answer_opts unless ActiveRecord::Base.connected? return answer_opts unless ActiveRecord::Base.connected?
Scheme.select(:id, :service_name).each_with_object(answer_opts) do |scheme, hsh| 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.service_name hsh[scheme.id.to_s] = scheme
hsh hsh
end end
end end

12
app/models/scheme.rb

@ -142,4 +142,16 @@ class Scheme < ApplicationRecord
{ name: "Intended length of stay", value: intended_stay }, { name: "Intended length of stay", value: intended_stay },
] ]
end 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 end

23
app/views/form/_select_question.html.erb

@ -1,13 +1,18 @@
<%= render partial: "form/guidance/#{question.guidance_partial}" if question.guidance_partial %> <%= render partial: "form/guidance/#{question.guidance_partial}" if question.guidance_partial %>
<% selected = @case_log.public_send(question.id) || "" %> <% selected = @case_log.public_send(question.id) || "" %>
<% answers = question.displayed_answer_options(@case_log).map { |key, value| OpenStruct.new(id: key, name: value) } %> <% 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_collection_select question.id.to_sym, <%= f.govuk_select(question.id.to_sym,
answers,
:id,
:name,
caption: caption(caption_text, page_header, conditional),
label: legend(question, page_header, conditional), label: legend(question, page_header, conditional),
hint: { text: question.hint_text&.html_safe }, "data-controller": "accessible-autocomplete",
options: { disabled: [""], selected: }, caption: caption(caption_text, page_header, conditional),
"data-controller": "accessible-autocomplete" %> hint: { text: question.hint_text&.html_safe }) do %>
<% answers.each do |answer| %>
<option value="<%= answer.id %>"
data-synonyms="<%= question.answer_option_synonyms(answer.resource) %>"
data-append="<%= question.answer_option_append(answer.resource) %>"
data-hint="<%= question.answer_option_hint(answer.resource) %>"
<%= @case_log[question.id] == answer.name || @case_log[question.id] == answer.resource || @case_log[question.id].to_s == answer.id ? "selected" : "" %>
<%= answer.id == "" ? "disabled" : "" %>><%= answer.name || answer.resource %></option>
<% end %>
<% end %>

27
spec/features/form/accessible_autocomplete_spec.rb

@ -14,6 +14,7 @@ RSpec.describe "Accessible Automcomplete" do
is_la_inferred: false, is_la_inferred: false,
owning_organisation: user.organisation, owning_organisation: user.organisation,
managing_organisation: user.organisation, managing_organisation: user.organisation,
created_by: user,
) )
end end
@ -58,6 +59,32 @@ RSpec.describe "Accessible Automcomplete" do
end end
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 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: "E07000178")
visit("/logs/#{case_log.id}/accessible-select") visit("/logs/#{case_log.id}/accessible-select")

2
spec/models/form/setup/questions/scheme_id_spec.rb

@ -51,7 +51,7 @@ RSpec.describe Form::Setup::Questions::SchemeId, type: :model do
end end
it "has the correct answer_options based on the schemes the user's organisation owns or manages" do 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) expect(question.displayed_answer_options(case_log)).to eq(expected_answer)
end end
end end

Loading…
Cancel
Save