From 2041a78db4cfa8a71d55abf25aaf3fc55f79326f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 15:25:51 +0000 Subject: [PATCH 1/7] Initial plan From e47299cf54452adc03b87a079a5335713e3bcbde Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 15:29:45 +0000 Subject: [PATCH 2/7] Add Workshop toggle UX with New Workshop option Co-authored-by: maebeale <7607813+maebeale@users.noreply.github.com> --- app/controllers/story_ideas_controller.rb | 13 +++- .../controllers/workshop_toggle_controller.js | 41 ++++++++++++ app/views/story_ideas/_form.html.erb | 66 ++++++++++++------- 3 files changed, 96 insertions(+), 24 deletions(-) create mode 100644 app/frontend/javascript/controllers/workshop_toggle_controller.js diff --git a/app/controllers/story_ideas_controller.rb b/app/controllers/story_ideas_controller.rb index fae4b9508c..e885455cd1 100644 --- a/app/controllers/story_ideas_controller.rb +++ b/app/controllers/story_ideas_controller.rb @@ -121,6 +121,10 @@ def set_form_variables @organizations = (@user || current_user)&.organizations&.order(:name) || Organization.none @windows_types = WindowsType.all + # Create a special "New Workshop" option + new_workshop_option = OpenStruct.new(id: "new", type_name: "New Workshop") + @workshops = [ new_workshop_option ] + authorized_scope(Workshop.all).includes(:windows_type).order(:title).to_a + users = authorized_scope(User.has_access.includes(:person)) users = users.or(User.where(id: @story_idea.created_by_id)) if @story_idea&.created_by_id @users = users.distinct.order("people.first_name, people.last_name") @@ -156,7 +160,7 @@ def set_story_idea end def story_idea_params - params.require(:story_idea).permit( + permitted_params = params.require(:story_idea).permit( :title, :rhino_body, :youtube_url, :permission_given, :author_credit_preference, :promoted_to_story, :windows_type_id, :organization_id, :workshop_id, :external_workshop_title, @@ -166,5 +170,12 @@ def story_idea_params primary_asset_attributes: [ :id, :file, :_destroy ], gallery_assets_attributes: [ :id, :file, :_destroy ] ) + + # Clear workshop_id if "new" was selected (triggers external_workshop_title) + if permitted_params[:workshop_id] == "new" + permitted_params[:workshop_id] = nil + end + + permitted_params end end diff --git a/app/frontend/javascript/controllers/workshop_toggle_controller.js b/app/frontend/javascript/controllers/workshop_toggle_controller.js new file mode 100644 index 0000000000..7e1d48428d --- /dev/null +++ b/app/frontend/javascript/controllers/workshop_toggle_controller.js @@ -0,0 +1,41 @@ +import { Controller } from "@hotwired/stimulus"; + +// Connects to data-controller="workshop-toggle" +// Handles toggling between workshop dropdown and external title field +export default class extends Controller { + static targets = ["dropdown", "externalField", "closeButton"]; + + connect() { + this.checkInitialState(); + } + + checkInitialState() { + const select = this.dropdownTarget.querySelector("select"); + if (select && select.value === "new") { + this.showExternalField(); + } + } + + handleChange(event) { + const value = event.target.value; + if (value === "new") { + this.showExternalField(); + } + } + + showExternalField() { + this.dropdownTarget.classList.add("hidden"); + this.externalFieldTarget.classList.remove("hidden"); + } + + showDropdown() { + this.externalFieldTarget.classList.add("hidden"); + this.dropdownTarget.classList.remove("hidden"); + + // Clear the selection back to prompt + const select = this.dropdownTarget.querySelector("select"); + if (select) { + select.value = ""; + } + } +} diff --git a/app/views/story_ideas/_form.html.erb b/app/views/story_ideas/_form.html.erb index f96d2d17c7..71bd2a5ab3 100644 --- a/app/views/story_ideas/_form.html.erb +++ b/app/views/story_ideas/_form.html.erb @@ -56,29 +56,49 @@ onchange: select_caret_onchange } %> -
- <%= f.input :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: { - class: "w-full px-3 py-2 border border-gray-300 rounded-lg", - data: { - controller: "remote-select", - remote_select_model_value: "workshop" - } - }, - prompt: "Type to search workshops…", - label: "Workshop", - label_html: { class: "block font-medium mb-1 text-gray-700 #{ 'readonly' if promoted_to_story }" } %> - - <%= f.input :external_workshop_title, - as: :text, - label: "Workshop title (if different or unlisted)", - input_html: { - rows: 1, - class: ("readonly" if promoted_to_story) - } %> +
+
+ <%= f.input :workshop_id, + as: :select, + collection: @workshops, + label_method: :type_name, + value_method: :id, + label: "Workshop", + prompt: "Select a Workshop", + required: false, + disabled: promoted_to_story, + selected: (params[:workshop_id].presence || f.object.workshop_id) unless f.object.workshop_id.nil? && f.object.external_workshop_title.present?, + input_html: { value: params[:workshop_id].presence || f.object.workshop_id, + class: "block w-full rounded-md border-gray-300 shadow-sm focus:ring-blue-500 + focus:border-blue-500 #{ 'readonly' if promoted_to_story }", + data: { action: "change->workshop-toggle#handleChange" } }, + label_html: { class: "block text-sm font-medium text-gray-700 mb-1" } %> +
+
+
+
+ <%= f.input :external_workshop_title, + as: :text, + label: "Workshop Title", + wrapper: false, + input_html: { + rows: 1, + class: ("readonly" if promoted_to_story) + }, + label_html: { class: "block text-sm font-medium text-gray-700 mb-1" } %> +
+ <% unless promoted_to_story %> + + <% end %> +
+
<% default_org = default_organization_for_form(f.object) %> From a003ba8467da1e11a06772e7a747c627dff4715d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 15:33:17 +0000 Subject: [PATCH 3/7] Fix syntax errors and rubocop offenses Co-authored-by: maebeale <7607813+maebeale@users.noreply.github.com> --- app/views/story_ideas/_form.html.erb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/views/story_ideas/_form.html.erb b/app/views/story_ideas/_form.html.erb index 71bd2a5ab3..a63d438c14 100644 --- a/app/views/story_ideas/_form.html.erb +++ b/app/views/story_ideas/_form.html.erb @@ -67,7 +67,7 @@ prompt: "Select a Workshop", required: false, disabled: promoted_to_story, - selected: (params[:workshop_id].presence || f.object.workshop_id) unless f.object.workshop_id.nil? && f.object.external_workshop_title.present?, + selected: (f.object.workshop_id.nil? && f.object.external_workshop_title.present? ? nil : (params[:workshop_id].presence || f.object.workshop_id)), input_html: { value: params[:workshop_id].presence || f.object.workshop_id, class: "block w-full rounded-md border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500 #{ 'readonly' if promoted_to_story }", @@ -99,6 +99,10 @@ <% end %>
+ + <% end %> + +
<% default_org = default_organization_for_form(f.object) %> From 337978eb3e260500c84c0a74b1d3527f4d49c1aa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 15:38:02 +0000 Subject: [PATCH 4/7] Address code review feedback - improve validation and readability Co-authored-by: maebeale <7607813+maebeale@users.noreply.github.com> --- .../javascript/controllers/workshop_toggle_controller.js | 4 ++-- app/helpers/application_helper.rb | 9 +++++++++ app/models/story_idea.rb | 9 +++++++++ app/views/story_ideas/_form.html.erb | 2 +- 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/app/frontend/javascript/controllers/workshop_toggle_controller.js b/app/frontend/javascript/controllers/workshop_toggle_controller.js index 7e1d48428d..6a53755b2e 100644 --- a/app/frontend/javascript/controllers/workshop_toggle_controller.js +++ b/app/frontend/javascript/controllers/workshop_toggle_controller.js @@ -3,7 +3,7 @@ import { Controller } from "@hotwired/stimulus"; // Connects to data-controller="workshop-toggle" // Handles toggling between workshop dropdown and external title field export default class extends Controller { - static targets = ["dropdown", "externalField", "closeButton"]; + static targets = ["dropdown", "externalField"]; connect() { this.checkInitialState(); @@ -31,7 +31,7 @@ export default class extends Controller { showDropdown() { this.externalFieldTarget.classList.add("hidden"); this.dropdownTarget.classList.remove("hidden"); - + // Clear the selection back to prompt const select = this.dropdownTarget.querySelector("select"); if (select) { diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 5d84242875..87ade914ea 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -577,4 +577,13 @@ def us_time_zone_fundamentals ] ActiveSupport::TimeZone.us_zones.select { |z| zone_names.include?(z.name) }.sort_by { |z| zone_names.index(z.name) }.map { |z| [ z.to_s, z.name ] } end + + def workshop_selected_value(story_idea, params) + # If editing and has external title but no workshop, don't select anything + return nil if story_idea.workshop_id.nil? && story_idea.external_workshop_title.present? + + # Otherwise use param or object value + params[:workshop_id].presence || story_idea.workshop_id + end end + diff --git a/app/models/story_idea.rb b/app/models/story_idea.rb index 65155be0bb..d245f5bb3e 100644 --- a/app/models/story_idea.rb +++ b/app/models/story_idea.rb @@ -43,6 +43,7 @@ def self.search_by_params(params) validates :permission_given, presence: true validates :author_credit_preference, presence: true validates :rhino_body, presence: true + validate :workshop_or_external_title_present # Nested attributes accepts_nested_attributes_for :primary_asset, allow_destroy: true, reject_if: :all_blank @@ -73,4 +74,12 @@ def organization_locality def organization_description organization&.organization_description end + + private + + def workshop_or_external_title_present + if workshop_id.blank? && external_workshop_title.blank? + errors.add(:base, "Please select a workshop or enter an external workshop title") + end + end end diff --git a/app/views/story_ideas/_form.html.erb b/app/views/story_ideas/_form.html.erb index a63d438c14..71535d8895 100644 --- a/app/views/story_ideas/_form.html.erb +++ b/app/views/story_ideas/_form.html.erb @@ -67,7 +67,7 @@ prompt: "Select a Workshop", required: false, disabled: promoted_to_story, - selected: (f.object.workshop_id.nil? && f.object.external_workshop_title.present? ? nil : (params[:workshop_id].presence || f.object.workshop_id)), + selected: workshop_selected_value(f.object, params), input_html: { value: params[:workshop_id].presence || f.object.workshop_id, class: "block w-full rounded-md border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500 #{ 'readonly' if promoted_to_story }", From 4b22be48c84ae3ad52f3531b4737d1c29dfd8e97 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 15:40:54 +0000 Subject: [PATCH 5/7] Add tests for workshop toggle functionality Co-authored-by: maebeale <7607813+maebeale@users.noreply.github.com> --- app/models/story_idea.rb | 1 + spec/models/story_idea_spec.rb | 50 ++++++++++++++++++++++++++----- spec/requests/story_ideas_spec.rb | 13 ++++++++ 3 files changed, 56 insertions(+), 8 deletions(-) diff --git a/app/models/story_idea.rb b/app/models/story_idea.rb index d245f5bb3e..6befe6ac29 100644 --- a/app/models/story_idea.rb +++ b/app/models/story_idea.rb @@ -83,3 +83,4 @@ def workshop_or_external_title_present end end end + diff --git a/spec/models/story_idea_spec.rb b/spec/models/story_idea_spec.rb index 663e12a8a5..fb6d7982a3 100644 --- a/spec/models/story_idea_spec.rb +++ b/spec/models/story_idea_spec.rb @@ -3,6 +3,40 @@ RSpec.describe StoryIdea, type: :model do it_behaves_like "author_creditable", factory: :story_idea + describe "validations" do + context "workshop selection" do + it "is valid with a workshop_id" do + story_idea = create(:story_idea, external_workshop_title: nil) + expect(story_idea).to be_persisted + expect(story_idea.workshop).to be_present + end + + it "is valid with external_workshop_title and no workshop" do + story_idea = create(:story_idea, + workshop: nil, + external_workshop_title: "My External Workshop") + expect(story_idea).to be_persisted + expect(story_idea.external_workshop_title).to eq("My External Workshop") + end + + it "is invalid without workshop_id or external_workshop_title" do + story_idea = build(:story_idea, + workshop: nil, + external_workshop_title: nil) + expect(story_idea).not_to be_valid + expect(story_idea.errors[:base]).to include("Please select a workshop or enter an external workshop title") + end + + it "is valid with both workshop_id and external_workshop_title" do + story_idea = create(:story_idea, + external_workshop_title: "My External Workshop") + expect(story_idea).to be_persisted + expect(story_idea.workshop).to be_present + expect(story_idea.external_workshop_title).to eq("My External Workshop") + end + end + end + describe "#workshop_title" do it "returns workshop title when only workshop is present" do workshop = create(:workshop, title: "Healing Art") @@ -46,23 +80,23 @@ end end - describe '.search_by_params' do - let!(:idea_alpha) { create(:story_idea, title: 'Art Healing Journey') } - let!(:idea_beta) { create(:story_idea, title: 'Community Impact Report') } + describe ".search_by_params" do + let!(:idea_alpha) { create(:story_idea, title: "Art Healing Journey") } + let!(:idea_beta) { create(:story_idea, title: "Community Impact Report") } - it 'returns all when no params' do + it "returns all when no params" do results = StoryIdea.search_by_params({}) expect(results).to include(idea_alpha, idea_beta) end - it 'filters by query matching title' do - results = StoryIdea.search_by_params(query: 'Art Healing') + it "filters by query matching title" do + results = StoryIdea.search_by_params(query: "Art Healing") expect(results).to include(idea_alpha) expect(results).not_to include(idea_beta) end - it 'returns empty for non-matching query' do - results = StoryIdea.search_by_params(query: 'nonexistent') + it "returns empty for non-matching query" do + results = StoryIdea.search_by_params(query: "nonexistent") expect(results).not_to include(idea_alpha, idea_beta) end diff --git a/spec/requests/story_ideas_spec.rb b/spec/requests/story_ideas_spec.rb index 9106dfaa7c..66eb9a09a7 100644 --- a/spec/requests/story_ideas_spec.rb +++ b/spec/requests/story_ideas_spec.rb @@ -139,6 +139,19 @@ }.to change(StoryIdea, :count).by(1) end + it "creates a StoryIdea with external_workshop_title when workshop_id is 'new'" do + attrs = valid_attributes.merge( + workshop_id: "new", + external_workshop_title: "My Custom Workshop" + ) + + post story_ideas_url, params: { story_idea: attrs } + + story_idea = StoryIdea.last + expect(story_idea.workshop_id).to be_nil + expect(story_idea.external_workshop_title).to eq("My Custom Workshop") + end + it "redirects to show after create" do post story_ideas_url, params: { story_idea: valid_attributes } expect(response).to redirect_to(story_idea_url(StoryIdea.last)) From 28892d00a8695ddbcbaa61c50d0f3f98de7008cf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 15:43:30 +0000 Subject: [PATCH 6/7] Address final code review feedback Co-authored-by: maebeale <7607813+maebeale@users.noreply.github.com> --- .../javascript/controllers/workshop_toggle_controller.js | 6 ++++++ app/helpers/application_helper.rb | 4 ++++ app/views/story_ideas/_form.html.erb | 2 +- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/frontend/javascript/controllers/workshop_toggle_controller.js b/app/frontend/javascript/controllers/workshop_toggle_controller.js index 6a53755b2e..a20f274731 100644 --- a/app/frontend/javascript/controllers/workshop_toggle_controller.js +++ b/app/frontend/javascript/controllers/workshop_toggle_controller.js @@ -37,5 +37,11 @@ export default class extends Controller { if (select) { select.value = ""; } + + // Clear the external workshop title field + const externalField = this.externalFieldTarget.querySelector("input, textarea"); + if (externalField) { + externalField.value = ""; + } } } diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 87ade914ea..1fc826e0f1 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -585,5 +585,9 @@ def workshop_selected_value(story_idea, params) # Otherwise use param or object value params[:workshop_id].presence || story_idea.workshop_id end + + def show_external_workshop_field?(story_idea) + story_idea.workshop_id.nil? && story_idea.external_workshop_title.present? + end end diff --git a/app/views/story_ideas/_form.html.erb b/app/views/story_ideas/_form.html.erb index 71535d8895..0e0ff4b513 100644 --- a/app/views/story_ideas/_form.html.erb +++ b/app/views/story_ideas/_form.html.erb @@ -74,7 +74,7 @@ data: { action: "change->workshop-toggle#handleChange" } }, label_html: { class: "block text-sm font-medium text-gray-700 mb-1" } %>
-
+
<%= f.input :external_workshop_title, From e5f1c77d135f5741cfff2f703387305d6d482529 Mon Sep 17 00:00:00 2001 From: maebeale Date: Sun, 8 Mar 2026 06:40:59 -0400 Subject: [PATCH 7/7] Fix rebase conflicts and test failures for workshop toggle UX Resolve trailing whitespace from merge, remove duplicate closing tags in form view, and update tests to use build instead of create where workshop validation now prevents nil workshop + nil external title. Co-Authored-By: Claude Opus 4.6 --- app/helpers/application_helper.rb | 1 - app/models/story_idea.rb | 1 - app/views/story_ideas/_form.html.erb | 4 ---- spec/models/story_idea_spec.rb | 8 +++++--- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 1fc826e0f1..b49a8f8932 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -590,4 +590,3 @@ def show_external_workshop_field?(story_idea) story_idea.workshop_id.nil? && story_idea.external_workshop_title.present? end end - diff --git a/app/models/story_idea.rb b/app/models/story_idea.rb index 6befe6ac29..d245f5bb3e 100644 --- a/app/models/story_idea.rb +++ b/app/models/story_idea.rb @@ -83,4 +83,3 @@ def workshop_or_external_title_present end end end - diff --git a/app/views/story_ideas/_form.html.erb b/app/views/story_ideas/_form.html.erb index 0e0ff4b513..98122e9671 100644 --- a/app/views/story_ideas/_form.html.erb +++ b/app/views/story_ideas/_form.html.erb @@ -99,10 +99,6 @@ <% end %>
-
- <% end %> -
-
<% default_org = default_organization_for_form(f.object) %> diff --git a/spec/models/story_idea_spec.rb b/spec/models/story_idea_spec.rb index fb6d7982a3..f0b08737ac 100644 --- a/spec/models/story_idea_spec.rb +++ b/spec/models/story_idea_spec.rb @@ -56,12 +56,12 @@ end it "returns nil when both workshop and external_workshop_title are absent" do - idea = create(:story_idea, workshop: nil, external_workshop_title: nil) + idea = build(:story_idea, workshop: nil, external_workshop_title: nil) expect(idea.workshop_title).to be_nil end it "returns nil when external_workshop_title is blank" do - idea = create(:story_idea, workshop: nil, external_workshop_title: "") + idea = build(:story_idea, workshop: nil, external_workshop_title: "") expect(idea.workshop_title).to be_nil end end @@ -74,7 +74,9 @@ end it "omits workshop section when no workshop or external title" do - idea = create(:story_idea, workshop: nil, external_workshop_title: nil) + idea = create(:story_idea, workshop: nil, external_workshop_title: "Temp") + idea.update_column(:external_workshop_title, nil) + idea.reload expect(idea.full_name).not_to include(":") expect(idea.full_name).to include(idea.author_credit) end