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