diff --git a/app/controllers/stories_controller.rb b/app/controllers/stories_controller.rb index 0a0b9e722..6e2e8ca12 100644 --- a/app/controllers/stories_controller.rb +++ b/app/controllers/stories_controller.rb @@ -12,7 +12,10 @@ def index :created_by, :bookmarks, :primary_asset, :story_idea)) filtered = base_scope.search_by_params(params) - .order(created_at: :desc) + sortable = %w[title updated_at created_at windows_type workshop author organization] + @sort = sortable.include?(params[:sort]) ? params[:sort] : "created_at" + @sort_direction = params[:direction] == "asc" ? "asc" : "desc" + filtered = apply_sort(filtered, @sort, @sort_direction) @stories = filtered.paginate(page: params[:page], per_page: per_page).decorate @count_display = filtered.count == base_scope.count ? base_scope.count : "#{filtered.count}/#{base_scope.count}" @@ -170,6 +173,28 @@ def set_story @story = Story.find(params[:id]) end + def apply_sort(scope, column, direction) + dir = direction.to_sym + case column + when "title", "updated_at", "created_at" + scope.reorder(column => dir) + when "windows_type" + scope.left_joins(:windows_type) + .reorder(WindowsType.arel_table[:short_name].public_send(dir)) + when "workshop" + scope.left_joins(:workshop) + .reorder(Workshop.arel_table[:title].public_send(dir)) + when "author" + scope.left_joins(created_by: :person) + .reorder(Person.arel_table[:first_name].public_send(dir)) + when "organization" + scope.left_joins(:organization) + .reorder(Organization.arel_table[:name].public_send(dir)) + else + scope.reorder(created_at: :desc) + end + end + # Strong parameters def story_params params.require(:story).permit( diff --git a/app/views/stories/_story_results.html.erb b/app/views/stories/_story_results.html.erb index 3b6bcefe3..96c9fad09 100644 --- a/app/views/stories/_story_results.html.erb +++ b/app/views/stories/_story_results.html.erb @@ -1,15 +1,33 @@ <%= turbo_stream.replace("story_count", partial: "story_count") %> <% if @stories.any? %> + <% + sort_base = params.permit(:title, :query, :published, :number_of_items_per_page).to_h.symbolize_keys + sort_icon = ->(column) { + if @sort == column + @sort_direction == "asc" ? "fa-arrow-up" : "fa-arrow-down" + else + "fa-sort" + end + } + sort_link = ->(column, label) { + link_to stories_path(sort_base.merge(sort: column, direction: (@sort == column && @sort_direction == "desc") ? "asc" : "desc", page: nil)), + data: { turbo_frame: "story_results" }, + class: "inline-flex items-center gap-1 text-gray-700 hover:text-gray-900" do + concat label + concat content_tag(:i, "", class: "fa-solid #{sort_icon.call(column)} text-xs opacity-70") + end + } + %>
- - - - - - + + + + + + diff --git a/spec/requests/stories_spec.rb b/spec/requests/stories_spec.rb index fea2ba38c..a825f06c1 100644 --- a/spec/requests/stories_spec.rb +++ b/spec/requests/stories_spec.rb @@ -52,6 +52,102 @@ expect(response.body).to include(public_story.title) expect(response.body).to include(private_story.title) end + + describe "sorting" do + let(:turbo_headers) { { "Turbo-Frame" => "story_results" } } + + let(:org_alpha) { create(:organization, name: "Alpha Org") } + let(:org_zulu) { create(:organization, name: "Zulu Org") } + let(:wt_adult) { create(:windows_type, short_name: "ADULT") } + let(:wt_children) { create(:windows_type, short_name: "CHILDREN") } + let(:ws_art) { create(:workshop, title: "Art Workshop") } + let(:ws_music) { create(:workshop, title: "Music Workshop") } + + let(:author_alice) do + person = create(:person, first_name: "Alice", last_name: "Smith") + person.user + end + let(:author_zara) do + person = create(:person, first_name: "Zara", last_name: "Jones") + person.user + end + + let!(:story_a) do + create(:story, :published, + title: "Alpha Story", + windows_type: wt_adult, + workshop: ws_art, + organization: org_alpha, + created_by: author_alice).tap do |s| + s.update_columns(created_at: 3.days.ago, updated_at: 1.day.ago) + end + end + + let!(:story_z) do + create(:story, :published, + title: "Zulu Story", + windows_type: wt_children, + workshop: ws_music, + organization: org_zulu, + created_by: author_zara).tap do |s| + s.update_columns(created_at: 1.day.ago, updated_at: 3.days.ago) + end + end + + def titles_in_response + response.body.scan(/(?:Alpha|Zulu) Story/) + end + + it "defaults to created_at desc when no sort param" do + get stories_url, params: {}, headers: turbo_headers + expect(titles_in_response).to eq([ "Zulu Story", "Alpha Story" ]) + end + + it "sorts by title asc" do + get stories_url, params: { sort: "title", direction: "asc" }, headers: turbo_headers + expect(titles_in_response).to eq([ "Alpha Story", "Zulu Story" ]) + end + + it "sorts by title desc" do + get stories_url, params: { sort: "title", direction: "desc" }, headers: turbo_headers + expect(titles_in_response).to eq([ "Zulu Story", "Alpha Story" ]) + end + + it "sorts by updated_at asc" do + get stories_url, params: { sort: "updated_at", direction: "asc" }, headers: turbo_headers + expect(titles_in_response).to eq([ "Zulu Story", "Alpha Story" ]) + end + + it "sorts by windows_type asc" do + get stories_url, params: { sort: "windows_type", direction: "asc" }, headers: turbo_headers + expect(titles_in_response).to eq([ "Alpha Story", "Zulu Story" ]) + end + + it "sorts by workshop asc" do + get stories_url, params: { sort: "workshop", direction: "asc" }, headers: turbo_headers + expect(titles_in_response).to eq([ "Alpha Story", "Zulu Story" ]) + end + + it "sorts by author asc" do + get stories_url, params: { sort: "author", direction: "asc" }, headers: turbo_headers + expect(titles_in_response).to eq([ "Alpha Story", "Zulu Story" ]) + end + + it "sorts by organization asc" do + get stories_url, params: { sort: "organization", direction: "asc" }, headers: turbo_headers + expect(titles_in_response).to eq([ "Alpha Story", "Zulu Story" ]) + end + + it "falls back to created_at desc for invalid sort column" do + get stories_url, params: { sort: "bogus" }, headers: turbo_headers + expect(titles_in_response).to eq([ "Zulu Story", "Alpha Story" ]) + end + + it "combines sort with title filter" do + get stories_url, params: { sort: "title", direction: "desc", title: "Story" }, headers: turbo_headers + expect(titles_in_response).to eq([ "Zulu Story", "Alpha Story" ]) + end + end end describe "GET /show" do
TitleWindows TypeWorkshopAuthorOrganizationUpdated At<%= sort_link.call("title", "Title") %><%= sort_link.call("windows_type", "Windows Type") %><%= sort_link.call("workshop", "Workshop") %><%= sort_link.call("author", "Author") %><%= sort_link.call("organization", "Organization") %><%= sort_link.call("updated_at", "Updated At") %> Actions