diff --git a/app/controllers/story_ideas_controller.rb b/app/controllers/story_ideas_controller.rb index a99572a7c..d5be5f52a 100644 --- a/app/controllers/story_ideas_controller.rb +++ b/app/controllers/story_ideas_controller.rb @@ -110,7 +110,9 @@ def set_form_variables @organizations = (@user || current_user)&.organizations&.order(:name) || Organization.none @windows_types = WindowsType.all - @workshops = authorized_scope(Workshop.all).includes(:windows_type).order(:title) + # 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 @@ -156,7 +158,7 @@ def set_story_idea end def story_idea_params - params.require(:story_idea).permit( + permitted_params = params.require(:story_idea).permit( :title, :body, :youtube_url, :permission_given, :publish_preferences, :promoted_to_story, :windows_type_id, :organization_id, :workshop_id, :external_workshop_title, @@ -166,5 +168,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 000000000..a20f27473 --- /dev/null +++ b/app/frontend/javascript/controllers/workshop_toggle_controller.js @@ -0,0 +1,47 @@ +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"]; + + 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 = ""; + } + + // 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 4e9f8c3f5..8bb16f585 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -169,4 +169,17 @@ 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 + + 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 c0124a2be..11c8768e1 100644 --- a/app/models/story_idea.rb +++ b/app/models/story_idea.rb @@ -44,6 +44,7 @@ def self.search_by_params(params) validates :body, presence: true validates :permission_given, presence: true validates :publish_preferences, presence: true + validate :workshop_or_external_title_present # Nested attributes accepts_nested_attributes_for :primary_asset, allow_destroy: true, reject_if: :all_blank @@ -83,4 +84,13 @@ 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 7ad4e4a4e..91e6be863 100644 --- a/app/views/story_ideas/_form.html.erb +++ b/app/views/story_ideas/_form.html.erb @@ -51,28 +51,53 @@ 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: true, - disabled: promoted_to_story, - selected: 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 }" }, - label_html: { class: "block text-sm font-medium text-gray-700 mb-1" } %> - <%= f.input :external_workshop_title, - as: :text, - label: "External title (if no workshop above)", - 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: 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 }", + 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 %> +
+
+
+ <% end %> +
+
<% if current_user.person&.affiliations&.count != 1 %> diff --git a/spec/models/story_idea_spec.rb b/spec/models/story_idea_spec.rb new file mode 100644 index 000000000..e51d8e835 --- /dev/null +++ b/spec/models/story_idea_spec.rb @@ -0,0 +1,52 @@ +require "rails_helper" + +RSpec.describe StoryIdea, type: :model do + describe "validations" do + context "workshop selection" do + let(:valid_story_idea) { create(:story_idea) } + + 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 workshop is set" do + workshop = create(:workshop, title: "Art Therapy 101") + story_idea = create(:story_idea, workshop: workshop, external_workshop_title: nil) + expect(story_idea.workshop_title).to eq("Art Therapy 101") + end + + it "returns external title in brackets when no workshop" do + story_idea = create(:story_idea, workshop: nil, external_workshop_title: "Community Workshop") + expect(story_idea.workshop_title).to eq("[Community Workshop]") + end + end +end diff --git a/spec/requests/story_ideas_spec.rb b/spec/requests/story_ideas_spec.rb index d763388b0..93f878c5f 100644 --- a/spec/requests/story_ideas_spec.rb +++ b/spec/requests/story_ideas_spec.rb @@ -115,6 +115,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 root after create" do post story_ideas_url, params: { story_idea: valid_attributes } expect(response).to redirect_to(root_path)