From 307bb4137d6a447bb8ece62002dc86571fe26c63 Mon Sep 17 00:00:00 2001 From: Andrew Brown Date: Thu, 26 Jun 2025 12:21:38 -0700 Subject: [PATCH] galleries cleanup & image reordering --- .../api/v1/gallery_images_controller.rb | 66 ++ app/controllers/content_controller.rb | 20 +- app/models/basil_commission.rb | 6 +- app/models/concerns/has_image_uploads.rb | 15 +- app/models/page_data/image_upload.rb | 7 +- app/models/page_types/content_page.rb | 16 +- .../display/_image_card_header.html.erb | 6 +- .../content/form/gallery/_panel.html.erb | 612 ++++++++++++------ app/views/content/gallery.html.erb | 10 +- app/views/main/dashboard.html.erb | 2 +- config/routes.rb | 8 + ..._add_paperclip_columns_to_image_uploads.rb | 21 + ...626212442_add_position_to_image_uploads.rb | 37 ++ ...12529_add_position_to_basil_commissions.rb | 38 ++ db/schema.rb | 10 +- 15 files changed, 658 insertions(+), 216 deletions(-) create mode 100644 app/controllers/api/v1/gallery_images_controller.rb create mode 100644 db/migrate/20250626193624_add_paperclip_columns_to_image_uploads.rb create mode 100644 db/migrate/20250626212442_add_position_to_image_uploads.rb create mode 100644 db/migrate/20250626212529_add_position_to_basil_commissions.rb diff --git a/app/controllers/api/v1/gallery_images_controller.rb b/app/controllers/api/v1/gallery_images_controller.rb new file mode 100644 index 00000000..b5b1c5d0 --- /dev/null +++ b/app/controllers/api/v1/gallery_images_controller.rb @@ -0,0 +1,66 @@ +class Api::V1::GalleryImagesController < ApplicationController + before_action :authenticate_user! + skip_before_action :verify_authenticity_token, only: [:sort] + + # POST /api/v1/gallery_images/sort + # Handles sorting of gallery images (both ImageUploads and BasilCommissions) + # Expected params: + # - images: Array of hashes with: + # - id: Image ID + # - type: 'image_upload' or 'basil_commission' + # - position: New position + # - content_type: Content type (e.g., 'Character') + # - content_id: Content ID + def sort + # Validate ownership/contribution permissions for the content + content_type = params[:content_type] + content_id = params[:content_id] + content = content_type.constantize.find_by(id: content_id) + + # Check permissions - must own or contribute to this content + unless content && + (content.user_id == current_user.id || + (content.respond_to?(:universe_id) && + content.universe_id.present? && + current_user.contributable_universe_ids.include?(content.universe_id))) + return render json: { error: 'Unauthorized' }, status: :unauthorized + end + + # Process each image in the array + success = true + + # Use a transaction to ensure all positions are updated or none are + ActiveRecord::Base.transaction do + params[:images].each do |image_data| + if image_data[:type] == 'image_upload' + # Update ImageUpload position + image = ImageUpload.find_by(id: image_data[:id]) + if image && image.content_type == content_type && image.content_id.to_s == content_id.to_s + image.insert_at(image_data[:position].to_i) + else + success = false + raise ActiveRecord::Rollback + end + elsif image_data[:type] == 'basil_commission' + # Update BasilCommission position + image = BasilCommission.find_by(id: image_data[:id]) + if image && image.entity_type == content_type && image.entity_id.to_s == content_id.to_s + image.insert_at(image_data[:position].to_i) + else + success = false + raise ActiveRecord::Rollback + end + else + success = false + raise ActiveRecord::Rollback + end + end + end + + if success + render json: { success: true } + else + render json: { error: 'Failed to update image positions' }, status: :unprocessable_entity + end + end +end diff --git a/app/controllers/content_controller.rb b/app/controllers/content_controller.rb index bf326f1e..0c4e42f1 100644 --- a/app/controllers/content_controller.rb +++ b/app/controllers/content_controller.rb @@ -425,16 +425,22 @@ class ContentController < ApplicationController # Serialize content for overview section @serialized_content = ContentSerializer.new(@content) - # Get all images for this content - @images = ImageUpload.where(content_type: @content.class.name, content_id: @content.id) + # Get all images for this content with proper ordering + @images = ImageUpload.where(content_type: @content.class.name, content_id: @content.id).ordered # Get additional context information - @universe = @content.universe_id.present? ? Universe.find_by(id: @content.universe_id) : nil - @other_content = @content.universe_id.present? ? - content_type.where(universe_id: @content.universe_id).where.not(id: @content.id).limit(5) : [] + if @content.is_a?(Universe) + # Universe objects don't have a universe_id field + @universe = nil + @other_content = [] + else + @universe = @content.universe_id.present? ? Universe.find_by(id: @content.universe_id) : nil + @other_content = @content.universe_id.present? ? + content_type.where(universe_id: @content.universe_id).where.not(id: @content.id).limit(5) : [] + end - # Include basil images too - @basil_images = BasilCommission.where(entity: @content).where.not(saved_at: nil) + # Include basil images too with proper ordering + @basil_images = BasilCommission.where(entity: @content).where.not(saved_at: nil).ordered render 'content/gallery' else diff --git a/app/models/basil_commission.rb b/app/models/basil_commission.rb index ea8d679c..47ac3a08 100644 --- a/app/models/basil_commission.rb +++ b/app/models/basil_commission.rb @@ -4,8 +4,9 @@ class BasilCommission < ApplicationRecord belongs_to :user, optional: true belongs_to :entity, polymorphic: true, optional: true - # Add scope for pinned images + # Add scopes for image ordering scope :pinned, -> { where(pinned: true) } + scope :ordered, -> { order(:position) } has_one_attached :image, service: :amazon_basil, @@ -48,6 +49,9 @@ class BasilCommission < ApplicationRecord image.attached? end + # Use acts_as_list for ordering images + acts_as_list scope: [:entity_type, :entity_id] + # Add callback to ensure only one pinned image per entity before_save :ensure_single_pinned_image, if: -> { pinned_changed? && pinned? } diff --git a/app/models/concerns/has_image_uploads.rb b/app/models/concerns/has_image_uploads.rb index a1c34bd2..6b6d8975 100644 --- a/app/models/concerns/has_image_uploads.rb +++ b/app/models/concerns/has_image_uploads.rb @@ -89,11 +89,24 @@ module HasImageUploads nil end + def pinned_or_random_image_including_private(format: :medium) + # First check for pinned images + pinned = pinned_image_upload(format) + return pinned if pinned.present? + + # If no pinned image, fall back to random selection + random_image_including_private(format: format) + end + def header_asset_for(class_name) # Since we use this as a fallback image on SEO content (for example, Twitter cards for shared notebook pages), # we need to include the full protocol + domain + path to ensure they will display the image. A relative path # will not work. - "https://www.notebook.ai" + ActionController::Base.helpers.asset_url("card-headers/#{class_name.downcase.pluralize}.webp") + # + # For direct view rendering, we use the relative asset path which works better with image_tag + Rails.env.production? ? + "https://www.notebook.ai" + ActionController::Base.helpers.asset_url("card-headers/#{class_name.downcase.pluralize}.webp") : + ActionController::Base.helpers.asset_path("card-headers/#{class_name.downcase.pluralize}.webp") end end end diff --git a/app/models/page_data/image_upload.rb b/app/models/page_data/image_upload.rb index 6a75afe8..bf0044cd 100644 --- a/app/models/page_data/image_upload.rb +++ b/app/models/page_data/image_upload.rb @@ -2,8 +2,9 @@ class ImageUpload < ApplicationRecord belongs_to :user, optional: true belongs_to :content, polymorphic: true - # Add scope for pinned images + # Add scopes for image ordering scope :pinned, -> { where(pinned: true) } + scope :ordered, -> { order(:position) } # This is the old way we uploaded files -- now we're transitioning to ActiveStorage's has_one_attached has_attached_file :src, @@ -35,8 +36,8 @@ class ImageUpload < ApplicationRecord alias_attribute 'character_id', :content_id #alias_attribute ... - # Add missing Paperclip attributes for development environment - attr_accessor :src_file_name, :src_content_type, :src_file_size, :src_updated_at + # Use acts_as_list for ordering images + acts_as_list scope: [:content_type, :content_id] # Add callback to ensure only one pinned image per content before_save :ensure_single_pinned_image, if: -> { pinned_changed? && pinned? } diff --git a/app/models/page_types/content_page.rb b/app/models/page_types/content_page.rb index b2ce9ae3..0550baf4 100644 --- a/app/models/page_types/content_page.rb +++ b/app/models/page_types/content_page.rb @@ -10,9 +10,19 @@ class ContentPage < ApplicationRecord self.authorizer_name = 'ContentPageAuthorizer' def random_image_including_private(format: :small) - ImageUpload.where(content_type: self.page_type, content_id: self.id).sample.try(:src, format) \ - || BasilCommission.where(entity_type: self.page_type, entity_id: self.id).where.not(saved_at: nil).includes([:image_attachment]).sample.try(:image) \ - || ActionController::Base.helpers.asset_path("card-headers/#{self.page_type.downcase.pluralize}.webp") + pinned_image = ImageUpload.where(content_type: self.page_type, content_id: self.id, pinned: true).first + return pinned_image.src(format) if pinned_image + + pinned_commission = BasilCommission.where(entity_type: self.page_type, entity_id: self.id, pinned: true).where.not(saved_at: nil).includes([:image_attachment]).first + return pinned_commission.image if pinned_commission + + random_image = ImageUpload.where(content_type: self.page_type, content_id: self.id).sample + return random_image.src(format) if random_image + + random_commission = BasilCommission.where(entity_type: self.page_type, entity_id: self.id).where.not(saved_at: nil).includes([:image_attachment]).sample + return random_commission.image if random_commission + + ActionController::Base.helpers.asset_path("card-headers/#{self.page_type.downcase.pluralize}.webp") end def icon diff --git a/app/views/content/display/_image_card_header.html.erb b/app/views/content/display/_image_card_header.html.erb index ef3d97d9..2b3d3171 100644 --- a/app/views/content/display/_image_card_header.html.erb +++ b/app/views/content/display/_image_card_header.html.erb @@ -63,7 +63,11 @@ %> <% @content.public_image_uploads.each do |image_source| %>
  • - <%= image_tag image_source.is_a?(String) ? image_source : image_source.src(:large) %> + <% if image_source.is_a?(String) %> + + <% else %> + <%= image_tag image_source.src(:large) %> + <% end %>

    <% if @content.persisted? %> diff --git a/app/views/content/form/gallery/_panel.html.erb b/app/views/content/form/gallery/_panel.html.erb index a43000de..57b9f882 100644 --- a/app/views/content/form/gallery/_panel.html.erb +++ b/app/views/content/form/gallery/_panel.html.erb @@ -1,9 +1,9 @@ <% raw_model = content.is_a?(ContentSerializer) ? content.raw_model : content - # Get both image types - regular_images = raw_model.image_uploads.to_a - basil_images = (@basil_images || []) + # Get both image types with ordering + regular_images = raw_model.image_uploads.ordered.to_a + basil_images = (@basil_images || []).sort_by(&:position) # Keep track of regular and AI images separately for display images = regular_images @@ -13,6 +13,32 @@ # Find the pinned image (if any) - there should only be one pinned image pinned_image = regular_images.find(&:pinned?) || basil_images.find(&:pinned?) + + # Create a combined list of all images for sortable functionality + # We'll maintain type information for each + combined_images = [] + regular_images.each do |image| + combined_images << { + id: image.id, + type: 'image_upload', + data: image, + pinned: image.pinned? + } + end + + basil_images.each do |commission| + combined_images << { + id: commission.id, + type: 'basil_commission', + data: commission, + pinned: commission.pinned? + } + end + + # If there's a pinned image, move it to the front + if pinned_image + combined_images.sort_by! { |img| img[:pinned] ? 0 : 1 } + end %>
    @@ -23,204 +49,118 @@ -