From afec872230696a2be68e57c679af6e278b04f94a Mon Sep 17 00:00:00 2001 From: maebeale Date: Tue, 3 Mar 2026 08:20:45 -0500 Subject: [PATCH 1/7] Show windows type in workshop select dropdowns Workshop remote-select and searchable-select fields now display type_name (title + windows type short_name + ID) instead of just the title, making it easier to distinguish workshops of different types. Co-Authored-By: Claude Opus 4.6 --- app/models/workshop.rb | 8 ++++++++ app/views/stories/_form.html.erb | 2 +- app/views/story_ideas/_form.html.erb | 2 +- app/views/workshop_logs/_form.html.erb | 2 +- app/views/workshop_variation_ideas/_form.html.erb | 2 +- app/views/workshop_variations/_form.html.erb | 2 +- 6 files changed, 13 insertions(+), 5 deletions(-) diff --git a/app/models/workshop.rb b/app/models/workshop.rb index 86a2f645d..14e3dc4df 100644 --- a/app/models/workshop.rb +++ b/app/models/workshop.rb @@ -326,6 +326,14 @@ def attach_assets_from_idea! remote_searchable_by :title + def self.remote_search(query) + super.includes(:windows_type) + end + + def remote_search_label + { id: id, label: type_name } + end + private def assign_pending_associations diff --git a/app/views/stories/_form.html.erb b/app/views/stories/_form.html.erb index 22de452bd..161d8ef11 100644 --- a/app/views/stories/_form.html.erb +++ b/app/views/stories/_form.html.erb @@ -50,7 +50,7 @@
<%= f.input :workshop_id, - collection: f.object.workshop.present? ? [[ f.object.workshop.title, f.object.workshop.id ]] : [], + collection: f.object.workshop.present? ? [[ f.object.workshop.type_name, f.object.workshop.id ]] : [], include_blank: true, input_html: { class: "w-full px-3 py-2 border border-gray-300 rounded-lg", diff --git a/app/views/story_ideas/_form.html.erb b/app/views/story_ideas/_form.html.erb index 5647878b1..523136ded 100644 --- a/app/views/story_ideas/_form.html.erb +++ b/app/views/story_ideas/_form.html.erb @@ -58,7 +58,7 @@
<%= f.input :workshop_id, - collection: f.object.workshop.present? ? [[ f.object.workshop.title, f.object.workshop.id]] : [], + collection: f.object.workshop.present? ? [[ f.object.workshop.type_name, f.object.workshop.id]] : [], include_blank: true, required: true, input_html: { diff --git a/app/views/workshop_logs/_form.html.erb b/app/views/workshop_logs/_form.html.erb index ebdca4f07..ace36e634 100644 --- a/app/views/workshop_logs/_form.html.erb +++ b/app/views/workshop_logs/_form.html.erb @@ -12,7 +12,7 @@
<%= f.input :workshop_id, - collection: f.object.workshop.present? ? [[ f.object.workshop.title, f.object.workshop.id]] : [], + collection: f.object.workshop.present? ? [[ f.object.workshop.type_name, f.object.workshop.id]] : [], include_blank: true, required: true, input_html: { diff --git a/app/views/workshop_variation_ideas/_form.html.erb b/app/views/workshop_variation_ideas/_form.html.erb index 5379ad976..81fd31e42 100644 --- a/app/views/workshop_variation_ideas/_form.html.erb +++ b/app/views/workshop_variation_ideas/_form.html.erb @@ -35,7 +35,7 @@
<%= f.input :workshop_id, - collection: f.object.workshop.present? ? [[ f.object.workshop.title, f.object.workshop.id]] : [], + collection: f.object.workshop.present? ? [[ f.object.workshop.type_name, f.object.workshop.id]] : [], include_blank: true, required: true, input_html: { diff --git a/app/views/workshop_variations/_form.html.erb b/app/views/workshop_variations/_form.html.erb index 9709f0a5e..5f967c787 100644 --- a/app/views/workshop_variations/_form.html.erb +++ b/app/views/workshop_variations/_form.html.erb @@ -14,7 +14,7 @@
<% else %> <%= f.input :workshop_id, - collection: f.object.workshop.present? ? [[ f.object.workshop.title, f.object.workshop.id]] : [], + collection: f.object.workshop.present? ? [[ f.object.workshop.type_name, f.object.workshop.id]] : [], include_blank: true, required: true, input_html: { From 5e5cdb9f4b1e59ff80d45e117f4a463f21ad3d31 Mon Sep 17 00:00:00 2001 From: maebeale Date: Tue, 3 Mar 2026 09:32:36 -0500 Subject: [PATCH 2/7] Add tests for workshop remote_search_label and remote_search Co-Authored-By: Claude Opus 4.6 --- spec/models/workshop_spec.rb | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/spec/models/workshop_spec.rb b/spec/models/workshop_spec.rb index 6634cbb60..d59fd0a13 100644 --- a/spec/models/workshop_spec.rb +++ b/spec/models/workshop_spec.rb @@ -119,5 +119,38 @@ end end + describe "#remote_search_label" do + it "returns id and type_name as label" do + record = create(:workshop, title: "Art Therapy", windows_type: create(:windows_type, :children)) + + expect(record.remote_search_label).to eq({ id: record.id, label: "Art Therapy (CHILDREN) ##{record.id}" }) + end + + it "returns label without windows type when none is set" do + record = create(:workshop, title: "Art Therapy", windows_type: nil) + + expect(record.remote_search_label).to eq({ id: record.id, label: "Art Therapy ##{record.id}" }) + end + end + + describe ".remote_search" do + it "finds workshops matching the query" do + matching = create(:workshop, title: "Healing Through Art") + create(:workshop, title: "Unrelated Workshop") + + results = Workshop.remote_search("Healing") + + expect(results).to contain_exactly(matching) + end + + it "eager loads windows_type to avoid N+1" do + create(:workshop, title: "Healing Through Art") + + results = Workshop.remote_search("Healing") + + expect(results.eager_loading?).to be true + end + end + # Add tests for scopes, methods like #rating, #log_count, SearchCop etc. end From 821334e2ec63a6476857b48bacada97ea4fb07b4 Mon Sep 17 00:00:00 2001 From: maebeale Date: Tue, 3 Mar 2026 09:46:55 -0500 Subject: [PATCH 3/7] Only show workshop ID in search results when titles collide Workshop search labels now show "Title (Type)" by default and only append "#ID" when multiple results share the same title and type. Adds remote_search_labels class method to RemoteSearchable for collection-level label disambiguation. Co-Authored-By: Claude Opus 4.6 --- app/controllers/search_controller.rb | 2 +- app/models/concerns/remote_searchable.rb | 4 ++ app/models/workshop.rb | 10 ++++- app/views/stories/_form.html.erb | 2 +- app/views/story_ideas/_form.html.erb | 2 +- app/views/workshop_logs/_form.html.erb | 2 +- .../workshop_variation_ideas/_form.html.erb | 2 +- app/views/workshop_variations/_form.html.erb | 2 +- spec/models/workshop_spec.rb | 38 +++++++++++++++++-- 9 files changed, 53 insertions(+), 11 deletions(-) diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index a5c669b8b..013402001 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -25,7 +25,7 @@ def index records = records.limit(25) - render json: records.map(&:remote_search_label) + render json: model_class.remote_search_labels(records) end private diff --git a/app/models/concerns/remote_searchable.rb b/app/models/concerns/remote_searchable.rb index c7554e7de..b37c6c67e 100644 --- a/app/models/concerns/remote_searchable.rb +++ b/app/models/concerns/remote_searchable.rb @@ -10,6 +10,10 @@ def remote_search_columns @remote_search_columns || [] end + def remote_search_labels(records) + records.map(&:remote_search_label) + end + def remote_search(query) return none if query.blank? raise "remote_searchable_by not defined for #{name}" if remote_search_columns.empty? diff --git a/app/models/workshop.rb b/app/models/workshop.rb index 14e3dc4df..c2e7ee0e7 100644 --- a/app/models/workshop.rb +++ b/app/models/workshop.rb @@ -331,7 +331,15 @@ def self.remote_search(query) end def remote_search_label - { id: id, label: type_name } + label = windows_type ? "#{title} (#{windows_type.short_name})" : title + { id: id, label: label } + end + + def self.remote_search_labels(records) + labels = records.map(&:remote_search_label) + dupes = labels.group_by { |l| l[:label] }.select { |_, v| v.size > 1 }.keys.to_set + labels.each { |l| l[:label] = "#{l[:label]} ##{l[:id]}" if dupes.include?(l[:label]) } + labels end private diff --git a/app/views/stories/_form.html.erb b/app/views/stories/_form.html.erb index 161d8ef11..e061662fc 100644 --- a/app/views/stories/_form.html.erb +++ b/app/views/stories/_form.html.erb @@ -50,7 +50,7 @@
<%= f.input :workshop_id, - collection: f.object.workshop.present? ? [[ f.object.workshop.type_name, f.object.workshop.id ]] : [], + collection: f.object.workshop.present? ? [[ f.object.workshop.remote_search_label[:label], f.object.workshop.id ]] : [], include_blank: true, input_html: { class: "w-full px-3 py-2 border border-gray-300 rounded-lg", diff --git a/app/views/story_ideas/_form.html.erb b/app/views/story_ideas/_form.html.erb index 523136ded..fb4e8ee00 100644 --- a/app/views/story_ideas/_form.html.erb +++ b/app/views/story_ideas/_form.html.erb @@ -58,7 +58,7 @@
<%= f.input :workshop_id, - collection: f.object.workshop.present? ? [[ f.object.workshop.type_name, f.object.workshop.id]] : [], + collection: f.object.workshop.present? ? [[ f.object.workshop.remote_search_label[:label], f.object.workshop.id]] : [], include_blank: true, required: true, input_html: { diff --git a/app/views/workshop_logs/_form.html.erb b/app/views/workshop_logs/_form.html.erb index ace36e634..320a3465f 100644 --- a/app/views/workshop_logs/_form.html.erb +++ b/app/views/workshop_logs/_form.html.erb @@ -12,7 +12,7 @@
<%= f.input :workshop_id, - collection: f.object.workshop.present? ? [[ f.object.workshop.type_name, f.object.workshop.id]] : [], + collection: f.object.workshop.present? ? [[ f.object.workshop.remote_search_label[:label], f.object.workshop.id]] : [], include_blank: true, required: true, input_html: { diff --git a/app/views/workshop_variation_ideas/_form.html.erb b/app/views/workshop_variation_ideas/_form.html.erb index 81fd31e42..c9ca35437 100644 --- a/app/views/workshop_variation_ideas/_form.html.erb +++ b/app/views/workshop_variation_ideas/_form.html.erb @@ -35,7 +35,7 @@
<%= f.input :workshop_id, - collection: f.object.workshop.present? ? [[ f.object.workshop.type_name, f.object.workshop.id]] : [], + collection: f.object.workshop.present? ? [[ f.object.workshop.remote_search_label[:label], f.object.workshop.id]] : [], include_blank: true, required: true, input_html: { diff --git a/app/views/workshop_variations/_form.html.erb b/app/views/workshop_variations/_form.html.erb index 5f967c787..601ab16bc 100644 --- a/app/views/workshop_variations/_form.html.erb +++ b/app/views/workshop_variations/_form.html.erb @@ -14,7 +14,7 @@
<% else %> <%= f.input :workshop_id, - collection: f.object.workshop.present? ? [[ f.object.workshop.type_name, f.object.workshop.id]] : [], + collection: f.object.workshop.present? ? [[ f.object.workshop.remote_search_label[:label], f.object.workshop.id]] : [], include_blank: true, required: true, input_html: { diff --git a/spec/models/workshop_spec.rb b/spec/models/workshop_spec.rb index d59fd0a13..bb1432563 100644 --- a/spec/models/workshop_spec.rb +++ b/spec/models/workshop_spec.rb @@ -120,16 +120,46 @@ end describe "#remote_search_label" do - it "returns id and type_name as label" do + it "returns title with windows type short_name" do record = create(:workshop, title: "Art Therapy", windows_type: create(:windows_type, :children)) - expect(record.remote_search_label).to eq({ id: record.id, label: "Art Therapy (CHILDREN) ##{record.id}" }) + expect(record.remote_search_label).to eq({ id: record.id, label: "Art Therapy (CHILDREN)" }) end - it "returns label without windows type when none is set" do + it "returns just the title when no windows type" do record = create(:workshop, title: "Art Therapy", windows_type: nil) - expect(record.remote_search_label).to eq({ id: record.id, label: "Art Therapy ##{record.id}" }) + expect(record.remote_search_label).to eq({ id: record.id, label: "Art Therapy" }) + end + end + + describe ".remote_search_labels" do + it "omits id when labels are unique" do + wt = create(:windows_type, :adult) + w1 = create(:workshop, title: "Art Therapy", windows_type: wt) + w2 = create(:workshop, title: "Music Therapy", windows_type: wt) + + labels = Workshop.remote_search_labels([ w1, w2 ]) + + expect(labels).to contain_exactly( + { id: w1.id, label: "Art Therapy (ADULT)" }, + { id: w2.id, label: "Music Therapy (ADULT)" } + ) + end + + it "appends id only to duplicate labels" do + wt = create(:windows_type, :adult) + w1 = create(:workshop, title: "Art Therapy", windows_type: wt) + w2 = create(:workshop, title: "Art Therapy", windows_type: wt) + w3 = create(:workshop, title: "Music Therapy", windows_type: wt) + + labels = Workshop.remote_search_labels([ w1, w2, w3 ]) + + expect(labels).to contain_exactly( + { id: w1.id, label: "Art Therapy (ADULT) ##{w1.id}" }, + { id: w2.id, label: "Art Therapy (ADULT) ##{w2.id}" }, + { id: w3.id, label: "Music Therapy (ADULT)" } + ) end end From 12f88e83b8cdf554c41ebbf09a3a86b8ec4013f7 Mon Sep 17 00:00:00 2001 From: maebeale Date: Tue, 3 Mar 2026 09:50:06 -0500 Subject: [PATCH 4/7] Add duplicate-title seed workshops for search disambiguation Two workshops named "Healing Through Color" with the same windows type so the ID suffix disambiguation logic can be tested in dev. Co-Authored-By: Claude Opus 4.6 --- db/seeds/dummy_dev_seeds.rb | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/db/seeds/dummy_dev_seeds.rb b/db/seeds/dummy_dev_seeds.rb index 9cf0f20d0..7a0e4388b 100644 --- a/db/seeds/dummy_dev_seeds.rb +++ b/db/seeds/dummy_dev_seeds.rb @@ -322,6 +322,34 @@ Workshop.where(title: workshop_data[:title]).first_or_create!(workshop_data) end +# Duplicate-title workshops to exercise ID disambiguation in search +[ + { + title: "Healing Through Color", + windows_type: adult_wt, + full_name: "Maria Torres", + month: 3, + year: 2020, + description: "Uses color mixing and painting to help participants explore emotions and find calm. Participants create a personal color wheel that maps feelings to colors.", + published: true, + searchable: true, + created_by: admin_user + }, + { + title: "Healing Through Color", + windows_type: adult_wt, + full_name: "James Whitfield", + month: 9, + year: 2022, + description: "A revised version exploring color as a pathway to emotional awareness. Participants blend watercolors while discussing how color connects to memory and healing.", + published: true, + searchable: true, + created_by: admin_user + } +].each do |workshop_data| + Workshop.create!(workshop_data) +end + puts "Assigning workshop categories and sectors…" workshops = Workshop.all categories = Category.all.to_a From 7cf0317da169774375be4e20794ff0464e428be5 Mon Sep 17 00:00:00 2001 From: maebeale Date: Tue, 3 Mar 2026 10:36:05 -0500 Subject: [PATCH 5/7] Show workshop title with windows type on edit page heading Co-Authored-By: Claude Opus 4.6 --- app/views/workshops/edit.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/workshops/edit.html.erb b/app/views/workshops/edit.html.erb index 1794ed784..de32c8f51 100644 --- a/app/views/workshops/edit.html.erb +++ b/app/views/workshops/edit.html.erb @@ -5,7 +5,7 @@ <%= link_to "Workshops", workshops_path, class: "text-sm text-gray-500 hover:text-gray-700 px-2 py-1" %> <%= link_to "View", workshop_path(@workshop), class: "text-sm text-gray-500 hover:text-gray-700 px-2 py-1" %>
-

Edit workshop

+

Edit workshop: <%= @workshop.remote_search_label[:label] %>

From 4aa8720a18fc3bd916d56bec6f9f5bfc6e367e5a Mon Sep 17 00:00:00 2001 From: maebeale Date: Tue, 3 Mar 2026 10:51:04 -0500 Subject: [PATCH 6/7] Fix eager loading test to check includes_values instead of eager_loading? eager_loading? is an internal Rails method that may not return true in all cases. includes_values reliably checks that the association is queued for eager loading. Co-Authored-By: Claude Opus 4.6 --- spec/models/workshop_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/models/workshop_spec.rb b/spec/models/workshop_spec.rb index bb1432563..2c6ee42b5 100644 --- a/spec/models/workshop_spec.rb +++ b/spec/models/workshop_spec.rb @@ -178,7 +178,7 @@ results = Workshop.remote_search("Healing") - expect(results.eager_loading?).to be true + expect(results.includes_values).to include(:windows_type) end end From 316ede1a5b8664502802ad3ddb53c31e7f053672 Mon Sep 17 00:00:00 2001 From: maebeale Date: Wed, 4 Mar 2026 19:59:35 -0500 Subject: [PATCH 7/7] Make duplicate label resolution opt-in via respond_to? check Move resolve_duplicate_labels to Workshop only and call it conditionally in SearchController, keeping RemoteSearchable concern unchanged. Per PR #1318 review feedback. Co-Authored-By: Claude Opus 4.6 --- app/controllers/search_controller.rb | 5 ++++- app/models/concerns/remote_searchable.rb | 4 ---- app/models/workshop.rb | 3 +-- spec/models/workshop_spec.rb | 6 +++--- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 013402001..f181c56f6 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -25,7 +25,10 @@ def index records = records.limit(25) - render json: model_class.remote_search_labels(records) + labels = records.map(&:remote_search_label) + labels = model_class.resolve_duplicate_labels(labels) if model_class.respond_to?(:resolve_duplicate_labels) + + render json: labels end private diff --git a/app/models/concerns/remote_searchable.rb b/app/models/concerns/remote_searchable.rb index b37c6c67e..c7554e7de 100644 --- a/app/models/concerns/remote_searchable.rb +++ b/app/models/concerns/remote_searchable.rb @@ -10,10 +10,6 @@ def remote_search_columns @remote_search_columns || [] end - def remote_search_labels(records) - records.map(&:remote_search_label) - end - def remote_search(query) return none if query.blank? raise "remote_searchable_by not defined for #{name}" if remote_search_columns.empty? diff --git a/app/models/workshop.rb b/app/models/workshop.rb index c2e7ee0e7..c683b1ece 100644 --- a/app/models/workshop.rb +++ b/app/models/workshop.rb @@ -335,8 +335,7 @@ def remote_search_label { id: id, label: label } end - def self.remote_search_labels(records) - labels = records.map(&:remote_search_label) + def self.resolve_duplicate_labels(labels) dupes = labels.group_by { |l| l[:label] }.select { |_, v| v.size > 1 }.keys.to_set labels.each { |l| l[:label] = "#{l[:label]} ##{l[:id]}" if dupes.include?(l[:label]) } labels diff --git a/spec/models/workshop_spec.rb b/spec/models/workshop_spec.rb index 2c6ee42b5..f5f326352 100644 --- a/spec/models/workshop_spec.rb +++ b/spec/models/workshop_spec.rb @@ -133,13 +133,13 @@ end end - describe ".remote_search_labels" do + describe ".resolve_duplicate_labels" do it "omits id when labels are unique" do wt = create(:windows_type, :adult) w1 = create(:workshop, title: "Art Therapy", windows_type: wt) w2 = create(:workshop, title: "Music Therapy", windows_type: wt) - labels = Workshop.remote_search_labels([ w1, w2 ]) + labels = Workshop.resolve_duplicate_labels([ w1, w2 ].map(&:remote_search_label)) expect(labels).to contain_exactly( { id: w1.id, label: "Art Therapy (ADULT)" }, @@ -153,7 +153,7 @@ w2 = create(:workshop, title: "Art Therapy", windows_type: wt) w3 = create(:workshop, title: "Music Therapy", windows_type: wt) - labels = Workshop.remote_search_labels([ w1, w2, w3 ]) + labels = Workshop.resolve_duplicate_labels([ w1, w2, w3 ].map(&:remote_search_label)) expect(labels).to contain_exactly( { id: w1.id, label: "Art Therapy (ADULT) ##{w1.id}" },