Add searchable image gallery with editable titles (#1512)#1543
Conversation
The team wants to use gallery images as an image database, but images were impossible to find: there was no gallery page, no search, and titles could not be edited to tag images with their organization, workshop, etc. This adds an admin-only Image gallery that lists every attached gallery image, lets you search by title/owner type/filename, and edit each image's title inline so it can be tagged and found again. Closes #1512 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
||
| # Free-text search across the editable title, the owning record type, and the | ||
| # original filename — the metadata the team tags images with (org, workshop, etc.). | ||
| scope :search_metadata, ->(query) { |
There was a problem hiding this comment.
search_metadata references active_storage_blobs, so it must be chained after .images (which joins the blob). The controller always does GalleryAsset.images.search_metadata(...). Matching on owner_type lets a search for "workshop" surface every workshop image even before it's been given a title.
|
|
||
| def update | ||
| authorize! @gallery_asset | ||
| @gallery_asset.update(gallery_asset_params) |
There was a problem hiding this comment.
The form lives inside a per-card <turbo-frame>, so rendering just the _card partial here swaps that one card in place — editing a title never reloads the grid or loses the user's scroll/search position.
| def index? = admin? | ||
| def update? = admin? | ||
|
|
||
| relation_scope do |relation| |
There was a problem hiding this comment.
Admin-only: this is an internal image database. relation_scope returns none for non-admins so authorized_scope is safe even if the policy is later relaxed to expose index?.
There was a problem hiding this comment.
Pull request overview
This PR adds an admin-only Image gallery feature for browsing, searching, and retitling existing GalleryAsset images so the portal’s gallery can be used as a searchable image database (matching on title, owner type, and original filename).
Changes:
- Added
GalleryAssetsController(index,update) plus routes and an admin-only navbar entry under Curriculum. - Implemented
GalleryAsset.imagesandGalleryAsset.search_metadatascopes to filter to images and support free-text search over key metadata. - Added gallery UI (Turbo-frame-loaded results, searchable form, inline title editing) and accompanying policy + request/model/routing specs.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| spec/routing/gallery_assets_routing_spec.rb | Adds routing coverage for the new gallery assets endpoints. |
| spec/requests/gallery_assets_spec.rb | Adds request coverage for authorization, search, and inline title updates. |
| spec/policies/gallery_asset_policy_spec.rb | Verifies admin-only access for index/update. |
| spec/models/gallery_asset_spec.rb | Covers the new .images and .search_metadata scopes. |
| config/routes.rb | Exposes gallery_assets#index and gallery_assets#update. |
| app/views/shared/_navbar_menu.html.erb | Adds the admin-only “Image gallery” link in the Curriculum dropdown. |
| app/views/gallery_assets/results.html.erb | Turbo-frame results view: count, grid, pagination, empty state. |
| app/views/gallery_assets/index.html.erb | Gallery page shell + Turbo-frame skeleton loader. |
| app/views/gallery_assets/_search.html.erb | Search form wired to Turbo frame via the existing collection controller. |
| app/views/gallery_assets/_card.html.erb | Image card UI with inline title edit form and owner type display. |
| app/policies/gallery_asset_policy.rb | Introduces admin-only authorization and relation scoping for gallery assets. |
| app/models/gallery_asset.rb | Adds scopes for image filtering and metadata search. |
| app/controllers/gallery_assets_controller.rb | Implements listing/searching/pagination and inline title update rendering. |
| AGENTS.md | Updates directory file counts after adding controller/policy. |
| def index | ||
| authorize! GalleryAsset | ||
| @query = params[:query].to_s.strip | ||
|
|
||
| scope = authorized_scope(GalleryAsset.images.with_attached_file.includes(:owner)) | ||
| scope = scope.search_metadata(@query) if @query.present? | ||
| scope = scope.order(created_at: :desc) | ||
|
|
||
| @count = scope.count | ||
| @gallery_assets = scope.paginate(page: params[:page], per_page: 24) | ||
|
|
||
| render :results if turbo_frame_request? |
| scope :search_metadata, ->(query) { | ||
| next all if query.blank? | ||
|
|
||
| term = "%#{sanitize_sql_like(query)}%" | ||
| where( | ||
| "assets.title LIKE :term OR assets.owner_type LIKE :term OR active_storage_blobs.filename LIKE :term", | ||
| term: term | ||
| ) |
| <button type="submit" class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700"> | ||
| <i class="fa fa-search"></i> | ||
| </button> |
| <% if allowed_to?(:index?, GalleryAsset) %> | ||
| <%= link_to gallery_assets_path, | ||
| class: "admin-only bg-blue-100 flex items-center gap-2 px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" do %> | ||
| <i class="fas fa-images"></i> | ||
| <span>Image gallery</span> | ||
| <% end %> |
Closes #1512
What is the goal of this PR and why is this important?
How did you approach the change?
GalleryAssetsController(index,update), reachable from the Curriculum → Image gallery nav item (admin-only).indexlists every gallery asset whose attached file is an image, newest first, paginated (24/page).collectionStimulus controller) and matches on the editable title, the owner type (Workshop, Story, …), and the original filename, via newGalleryAsset.images/GalleryAsset.search_metadatascopes.update; the response re-renders just that card's Turbo frame, so tagging an image never reloads the page.GalleryAssetPolicyrestricts both actions to admins (super users), consistent with the default policy; non-admins are redirected like elsewhere in the app.Anything else to add?
18 examples, 0 failures).🤖 Generated with Claude Code