Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion app/controllers/story_ideas_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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,
Expand All @@ -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
47 changes: 47 additions & 0 deletions app/frontend/javascript/controllers/workshop_toggle_controller.js
Original file line number Diff line number Diff line change
@@ -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 = "";
}
}
}
12 changes: 12 additions & 0 deletions app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -577,4 +577,16 @@ 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
9 changes: 9 additions & 0 deletions app/models/story_idea.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
66 changes: 43 additions & 23 deletions app/views/story_ideas/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -56,29 +56,49 @@
onchange: select_caret_onchange
} %>
</div>
<div class="flex-1 mb-4 md:mb-0">
<%= 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)
} %>
<div class="flex-1 mb-4 md:mb-0" data-controller="workshop-toggle">
<div data-workshop-toggle-target="dropdown">
<%= 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" } %>
</div>
<div data-workshop-toggle-target="externalField" class="<%= 'hidden' unless show_external_workshop_field?(f.object) %>">
<div class="flex items-start gap-2">
<div class="flex-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" } %>
</div>
<% unless promoted_to_story %>
<button type="button"
class="mt-7 p-2 text-gray-400 hover:text-gray-600"
data-action="click->workshop-toggle#showDropdown"
title="Back to dropdown">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<% end %>
</div>
</div>
</div>
<div class="flex-1 mb-4 md:mb-0">
<% default_org = default_organization_for_form(f.object) %>
Expand Down
58 changes: 47 additions & 11 deletions spec/models/story_idea_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -22,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
Expand All @@ -40,29 +74,31 @@
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
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

Expand Down
13 changes: 13 additions & 0 deletions spec/requests/story_ideas_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Loading