Skip to content
Open
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
31 changes: 31 additions & 0 deletions app/controllers/gallery_assets_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
class GalleryAssetsController < ApplicationController
before_action :set_asset, only: [ :destroy, :make_primary ]

# Remove an image from the gallery
def destroy
authorize! @asset.owner, to: :manage? # check the policy of the record that owns the asset
@asset.destroy!

redirect_back_or_to polymorphic_path(@asset.owner), notice: "Image removed."
end

# Promote an existing gallery image to be the featured (primary) image
def make_primary
authorize! @asset.owner, to: :manage?
ActiveRecord::Base.transaction do
if existing_primary = @asset.owner.assets.find_by(type: "PrimaryAsset")
existing_primary.update!(type: "GalleryAsset")

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Promotion is a type swap, not a new upload: the current PrimaryAsset is demoted back to GalleryAsset and the chosen gallery image becomes the PrimaryAsset. This mirrors the existing PrimaryAssetsController#create flow used by the "Change photo" picker, so there's a single definition of "featured".

end

@asset.update!(type: "PrimaryAsset")
end

redirect_back_or_to polymorphic_path(@asset.owner), notice: "Featured image updated."
end
Comment on lines +13 to +24

private

def set_asset
@asset = GalleryAsset.find(params[:id])
end
end
12 changes: 10 additions & 2 deletions app/views/assets/_display_gallery_media.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,18 @@
<% gallery_assets = resource.gallery_assets.select { |img| img.file.attached? } %>
<% align = (defined?(align) && align.present?) ? align.to_s : "center" %>
<% if gallery_assets.any? %>
<% can_manage = allowed_to?(:manage?, resource) %>

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Controls are gated by allowed_to?(:manage?, resource) so they only render for users who can manage the owning record. The controller re-checks the same policy server-side, so this is purely a UI gate. Because it lives in the shared gallery partial, the feature works for every asset owner (stories, story ideas, workshops, events, …), not just stories.

<div class="<%= resource.class.table_name %>-gallery text-<%= align %> mb-4">
<div class="flex flex-wrap justify-<%= align %> gap-4 <%= 'mx-auto' if align == 'center' %> max-w-4xl">
<% gallery_assets.each_with_index do |gallery_assets, idx| %>
<%= render "assets/display_image", item: gallery_assets, idx: idx, variant: :gallery, link: (defined?(link) ? link : nil) %>
<% gallery_assets.each_with_index do |gallery_asset, idx| %>
<% if can_manage %>
<div class="relative group">
<%= render "assets/display_image", item: gallery_asset, idx: idx, variant: :gallery, link: (defined?(link) ? link : nil) %>
<%= render "assets/gallery_item_controls", asset: gallery_asset %>
</div>
<% else %>
<%= render "assets/display_image", item: gallery_asset, idx: idx, variant: :gallery, link: (defined?(link) ? link : nil) %>
<% end %>
<% end %>
</div>
</div>
Expand Down
16 changes: 16 additions & 0 deletions app/views/assets/_gallery_item_controls.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<div class="absolute top-2 right-2 flex gap-1 opacity-0 group-hover:opacity-100 focus-within:opacity-100 transition">
<%= button_to make_primary_gallery_asset_path(asset),
method: :post,
class: "bg-white text-gray-700 rounded-full w-8 h-8 flex items-center justify-center shadow hover:bg-gray-100 transition",
title: "Set as featured image",
data: { turbo_confirm: "Make this the featured image?" } do %>
<i class="fa-solid fa-star"></i>
<% end %>
<%= button_to gallery_asset_path(asset),
method: :delete,
class: "bg-white text-red-600 rounded-full w-8 h-8 flex items-center justify-center shadow hover:bg-gray-100 transition",
title: "Remove image",
data: { turbo_confirm: "Remove this image from the gallery?" } do %>
Comment on lines +2 to +13
<i class="fa-solid fa-trash"></i>
<% end %>
</div>
5 changes: 5 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
# end
resources :primary_assets
resources :rich_text_assets
resources :gallery_assets, only: [ :destroy ] do

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Routed under /gallery_assets rather than resources :assets on purpose: the Rails asset pipeline owns the /assets/* URL prefix and returns 405 for non-GET requests, which would shadow these DELETE/POST routes.

member do
post :make_primary
end
end

namespace :images do
resources :primary_images, only: [ :show ]
Expand Down
77 changes: 77 additions & 0 deletions spec/requests/gallery_assets_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
require "rails_helper"

RSpec.describe "/gallery_assets", type: :request do
let(:admin) { create(:user, :admin) }
let(:regular_user) { create(:user) }
let(:story) { create(:story) }

describe "DELETE /gallery_assets/:id" do
let!(:gallery_asset) { create(:gallery_asset, :with_file, owner: story) }

context "as admin" do
before { sign_in admin }

it "removes the gallery image" do
expect {
delete gallery_asset_url(gallery_asset)
}.to change(Asset, :count).by(-1)

expect(response).to redirect_to(story_url(story))
expect(Asset.exists?(gallery_asset.id)).to be(false)
end
end

context "as a non-admin user" do
before { sign_in regular_user }

it "does not remove the image" do
expect {
delete gallery_asset_url(gallery_asset)
}.not_to change(Asset, :count)
end
end

context "when signed out" do
it "does not remove the image" do
expect {
delete gallery_asset_url(gallery_asset)
}.not_to change(Asset, :count)
end
end
end

describe "POST /gallery_assets/:id/make_primary" do
let!(:gallery_asset) { create(:gallery_asset, :with_file, owner: story) }

context "as admin" do
before { sign_in admin }

it "promotes the gallery image to the featured image" do
post make_primary_gallery_asset_url(gallery_asset)

expect(response).to redirect_to(story_url(story))
expect(Asset.find(gallery_asset.id).type).to eq("PrimaryAsset")
end

it "demotes the existing featured image to the gallery" do
existing_primary = create(:primary_asset, :with_file, owner: story)

post make_primary_gallery_asset_url(gallery_asset)

expect(Asset.find(existing_primary.id).type).to eq("GalleryAsset")
expect(Asset.find(gallery_asset.id).type).to eq("PrimaryAsset")
expect(story.assets.where(type: "PrimaryAsset").count).to eq(1)
end
end

context "as a non-admin user" do
before { sign_in regular_user }

it "does not change the asset type" do
post make_primary_gallery_asset_url(gallery_asset)

expect(Asset.find(gallery_asset.id).type).to eq("GalleryAsset")
end
end
end
Comment on lines +67 to +76
end
Loading