Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: media library/gallery #3561

Merged
merged 31 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f0ea2d7
feature: media library
adrianthedev Dec 19, 2024
3917495
renamve components
adrianthedev Dec 19, 2024
41833cb
make it work for trix and rhino too
adrianthedev Dec 27, 2024
b9f0caa
add request.js
adrianthedev Dec 27, 2024
9276375
wip
adrianthedev Dec 29, 2024
013feea
wip
adrianthedev Dec 29, 2024
7a994cb
Merge branch 'main' into feature/media-library
adrianthedev Dec 29, 2024
2bd7d85
wip
adrianthedev Dec 29, 2024
ec848cc
wip
adrianthedev Jan 7, 2025
827cfee
Merge branch 'main' into feature/media-library
adrianthedev Jan 27, 2025
06f180e
wip
adrianthedev Jan 27, 2025
b1381f1
wip
adrianthedev Jan 29, 2025
11cb630
can upload
adrianthedev Jan 29, 2025
6011c20
Merge branch 'main' into feature/media-library
adrianthedev Jan 29, 2025
657ce41
add configuration
adrianthedev Jan 29, 2025
c7f5035
add helpers
adrianthedev Jan 29, 2025
1310d0b
add authorization
adrianthedev Jan 29, 2025
6db9746
fix update and destroy
adrianthedev Jan 29, 2025
73ddd4a
wip
adrianthedev Jan 29, 2025
05455f0
wip
adrianthedev Jan 29, 2025
85fa308
wip
adrianthedev Jan 30, 2025
22be558
wip
adrianthedev Jan 30, 2025
91dcbfb
Potential fix for code scanning alert no. 43: DOM text reinterpreted …
adrianthedev Jan 30, 2025
f564527
properly escape the filename
adrianthedev Jan 30, 2025
116a761
Merge branch 'main' into feature/media-library
Paul-Bob Jan 30, 2025
628afee
lint
Paul-Bob Jan 30, 2025
9363706
18n-tasks translate-missing
Paul-Bob Jan 30, 2025
753cce7
revert unique_id for trix input
Paul-Bob Jan 30, 2025
f1071ff
npx update-browserslist-db@latest
Paul-Bob Jan 30, 2025
13f9686
fix unique_selector
Paul-Bob Jan 30, 2025
263cd12
Merge branch 'main' into feature/media-library
Paul-Bob Jan 30, 2025
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
12 changes: 12 additions & 0 deletions app/assets/stylesheets/avo.base.css
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,18 @@ dialog#turbo-confirm {
@apply bg-transparent;
}

dl {
@apply text-sm;

dt {
@apply font-bold inline-block mt-1;
}

dd {
@apply inline-block ml-0;
}
}

/* TODO: make content like tailwindcss */
.floating-row-controls {
&:before {
Expand Down
2 changes: 2 additions & 0 deletions app/components/avo/base_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ def has_with_trial(ability)
Avo.license.has_with_trial(ability)
end

def component_name = self.class.name.to_s.underscore

private

# Use the @parent_resource to fetch the field using the @reflection name.
Expand Down
12 changes: 4 additions & 8 deletions app/components/avo/fields/trix_field/edit_component.html.erb
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
<%= field_wrapper(**field_wrapper_args) do %>
<%= content_tag :div,
class: "relative block overflow-x-auto max-w-4xl",
data: {
controller: "trix-field",
trix_field_target: "controller",
**data_values,
} do %>
class: class_names("relative block overflow-x-auto max-w-4xl", unique_id),
data: do %>
<%= content_tag 'trix-editor',
class: 'trix-content',
data: {
"trix-field-target": "editor",
**@field.get_html(:data, view: view, element: :input)
},
input: trix_id,
input: unique_id,
placeholder: @field.placeholder do %>
<%= sanitize @field.value.to_s %>
<% end %>
Expand All @@ -21,7 +17,7 @@
class: classes("w-full hidden"),
data: @field.get_html(:data, view: view, element: :input),
disabled: disabled?,
id: trix_id,
id: unique_id,
placeholder: @field.placeholder,
style: @field.get_html(:style, view: view, element: :input)
%>
Expand Down
24 changes: 19 additions & 5 deletions app/components/avo/fields/trix_field/edit_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,27 @@ def initialize(**args)
@resource_name = args[:resource_name] || @resource&.singular_route_key

super(**args)

@unique_random_id = SecureRandom.hex(4)
end

def trix_id
def unique_id
if @resource_name.present?
"trix_#{@resource_name}_#{@field.id}"
"trix_#{@resource_name}_#{@field.id}_#{@unique_random_id}"
elsif form.present?
"trix_#{form.index}_#{@field.id}"
"trix_#{form.index}_#{@field.id}_#{@unique_random_id}"
end
end

def data_values
{
def unique_selector = ".#{unique_id}"

# The controller element should have a unique_selector attribute.
# It's used to identify the specific editor for the media library to delegate the attach event to.
def data
values = {
resource_name: @resource_name,
resource_id: @resource_id,
unique_selector:, # mandatory
attachments_disabled: @field.attachments_disabled,
attachment_key: @field.attachment_key,
hide_attachment_filename: @field.hide_attachment_filename,
Expand All @@ -33,5 +40,12 @@ def data_values
attachment_disable_warning: t("avo.this_field_has_attachments_disabled"),
attachment_key_warning: t("avo.you_havent_set_attachment_key")
}.transform_keys { |key| "trix_field_#{key}_value" }

{
controller: "trix-field",
trix_field_target: "controller",
action: "insert-attachment->trix-field#insertAttachment",
**values,
}
end
end
32 changes: 32 additions & 0 deletions app/components/avo/media_library/item_details_component.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<div class="relative flex flex-col w-full max @container/details">
<%= link_to helpers.svg('heroicons/outline/x-mark', class: "size-6"), helpers.avo.media_library_index_path,
class: "absolute z-10 inset-auto right-0 top-0 mt-2 mr-2 block bg-white p-1 rounded-lg text-slate-600 hover:text-slate-900",
title: t('avo.close'),
data: {
tippy: :tooltip,
} %>
<div class="flex flex-1 flex-row w-full">
<div class="flex flex-col justify-center w-1/2 @3xl/details:w-2/3 p-4 gap-2">
<% if @blob.image? %>
<%= image_tag helpers.main_app.url_for(@blob), class: "max-w-full rounded-lg max-h-xl", loading: :lazy %>
<% elsif @blob.audio? %>
<%= audio_tag(helpers.main_app.url_for(@blob), controls: true, preload: false, class: 'w-full') %>
<% elsif @blob.video? %>
<%= video_tag(helpers.main_app.url_for(@blob), controls: true, preload: false, class: 'w-full') %>
<% else %>
<div class="relative h-full flex flex-col justify-center items-center w-full bg-slate-100">
<%= helpers.svg "heroicons/outline/document-text", class: 'h-10 text-gray-600 mb-2' %>
</div>
<% end %>
<div class="flex justify-center w-full text-sm gap-4">
<%= link_to "Download", helpers.main_app.url_for(@blob), download: true %>
<%= link_to "Copy URL to clipboard", helpers.main_app.url_for(@blob), data: {controller: "copy-to-clipboard", text: helpers.main_app.url_for(@blob), action: "click->copy-to-clipboard#copy"} %>
<%= link_to "Delete", helpers.avo.media_library_path(@blob), class: "text-red-500", data: {turbo_confirm: "Are you sure you want to destroy this attachment?", turbo_method: :delete} %>
</div>
</div>
<div class="flex flex-col w-1/2 @3xl/details:w-1/3 border-l">
<%= render partial: "avo/media_library/information", locals: {blob: @blob} %>
<%= render partial: "avo/media_library/form", locals: {blob: @blob} %>
</div>
</div>
</div>
12 changes: 12 additions & 0 deletions app/components/avo/media_library/item_details_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true

module Avo
module MediaLibrary
class ItemDetailsComponent < Avo::BaseComponent
include Turbo::FramesHelper
include Avo::ApplicationHelper

prop :blob
end
end
end
52 changes: 52 additions & 0 deletions app/components/avo/media_library/list_component.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<%= render Avo::PanelComponent.new title: t("avo.media_library.title"),
data: {
controller: 'media-library',
media_library_controller_selector_value: params[:controller_selector],
media_library_controller_name_value: params[:controller_name],
media_library_item_details_frame_id_value: ::Avo::MEDIA_LIBRARY_ITEM_DETAILS_FRAME_ID,
} do |c| %>
<%= c.with_tools do %>
<% if false && @attaching %>
<%= a_button data: {
action: 'click->media-library#selectItems',
} do %>
Attach
<% end %>
<% end %>
<% end %>
<% c.with_body do %>
<% if !@attaching %>
<%= content_tag :div, class: "p-4" do %>
<%= content_tag :div,
class: 'dropzone relative py-6 text-center border-dashed border-2 border-gray-300 rounded-lg justify-center items-center flex flex-col text-gray-400 hover:border-primary-500 cursor-pointer',
data: {
controller: 'media-library-attach',
media_library_attach_target: 'dropzone',
media_library_attach_direct_uploads_url_value: helpers.main_app.rails_direct_uploads_url,
action: 'click->media-library-attach#triggerFileBrowser',
} do %>
<%= helpers.svg 'heroicons/outline/cloud-arrow-up', class: 'size-6 text-gray-400' %>
Upload a file
<small>Click to browse or drag and drop</small>
<% end %>
<% end %>
<% end %>
<div class="grid grow-0 min-h-24 gap-x-4 @container/index" style="grid-template-areas: 'stack';">
<div class="grid grid-cols-1 @sm/index:grid-cols-2 @lg/index:grid-cols-3 @3xl/index:grid-cols-4 @5xl/index:grid-cols-6 gap-4 min-h-0 min-w-0 auto-rows-max p-4" style="grid-area: stack;">
<%= render Avo::MediaLibrary::ListItemComponent.with_collection(@blobs, attaching: @attaching, multiple: @attaching) %>
</div>
<%# TODO: fix the extra margin %>
<%= helpers.turbo_frame_tag ::Avo::MEDIA_LIBRARY_ITEM_DETAILS_FRAME_ID, class: 'relative empty:hidden bg-white inset-0 w-full h-full block empty:-ml-4 max-h-full', style: 'grid-area: stack;' %>
</div>
<% end %>
<% c.with_bare_content do %>
<div class="flex-1 flex w-full mt-4">
<div class="flex-2 w-full sm:flex sm:items-center sm:justify-between space-y-2 sm:space-y-0 text-center sm:text-left pagy-gem-version-<%= helpers.pagy_major_version %> ">
<div class="text-sm text-slate-600 mr-4"><%== helpers.pagy_info @pagy %></div>
<% if @pagy.pages > 1 %>
<%== helpers.pagy_nav(@pagy, xanchor_string: "data-turbo-frame=\"#{@turbo_frame}\"") %>
<% end %>
</div>
</div>
<% end %>
<% end %>
28 changes: 28 additions & 0 deletions app/components/avo/media_library/list_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# frozen_string_literal: true

module Avo
module MediaLibrary
class ListComponent < Avo::BaseComponent
include Avo::ApplicationHelper
include Pagy::Backend

def initialize(attaching: false, turbo_frame: nil)
@attaching = attaching
@pagy, @blobs = pagy(query, limit:)
turbo_frame ||= params[:turbo_frame]
@turbo_frame = turbo_frame.present? ? CGI.escapeHTML(turbo_frame.to_s) : :_top
end

def controller = Avo::Current.view_context.controller

def query
ActiveStorage::Blob.includes(:attachments)
# ignore blobs who are just a variant to avoid "n+1" blob creation
.where.not(id: ActiveStorage::Attachment.where(record_type: "ActiveStorage::VariantRecord").pluck(:blob_id))
.order(created_at: :desc)
end

def limit = @attaching ? 12 : 24
end
end
end
26 changes: 26 additions & 0 deletions app/components/avo/media_library/list_item_component.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<%= link_to helpers.avo.media_library_path(blob),
id: dom_id(blob),
class: "relative group min-h-full max-w-full flex-1 flex flex-col justify-between gap-2 border border-slate-200 p-1.5 rounded-xl hover:border-blue-500 hover:outline data-[selected=true]:border-blue-500 data-[selected=true]:outline outline-2 outline-blue-500",
data: do %>
<% if false && @attaching %>
<div class="absolute bg-blue-500 group-hover:opacity-100 group-data-[selected=true]:opacity-100 opacity-0 inset-auto left-0 top-0 text-white rounded-tl-xl rounded-br-xl -ml-px -mt-px p-2"><div class="border border-white"><%= helpers.svg "heroicons/outline/check", class: 'group-data-[selected=true]:opacity-100 opacity-0 size-4' %></div></div>
<% end %>
<div class="flex flex-col h-full aspect-video overflow-hidden rounded-lg justify-center items-center">
<% if blob.image? %>
<%= image_tag helpers.main_app.url_for(blob.variant(resize_to_limit: [600, 600])), class: "max-w-full self-start #{@extra_classes}", loading: :lazy, width: blob.metadata["width"], height: blob.metadata["height"] %>
<% elsif blob.audio? %>
<%= audio_tag(helpers.main_app.url_for(blob), controls: true, preload: false, class: 'w-full') %>
<% elsif blob.video? %>
<%= video_tag(helpers.main_app.url_for(blob), controls: true, preload: false, class: 'w-full') %>
<% else %>
<div class="relative h-full flex flex-col justify-center items-center w-full bg-slate-100">
<%= helpers.svg "heroicons/outline/document-text", class: 'h-10 text-gray-600 mb-2' %>
</div>
<% end %>
</div>
<div class="flex space-x-2 mb-1">
<% if @display_filename %>
<span class="text-gray-500 group-hover:text-blue-700 mt-1 text-sm truncate" title="<%= blob.filename %>"><%= blob.filename %></span>
<% end %>
</div>
<% end %>
34 changes: 34 additions & 0 deletions app/components/avo/media_library/list_item_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# frozen_string_literal: true

module Avo
module MediaLibrary
class ListItemComponent < Avo::BaseComponent
with_collection_parameter :blob

prop :blob, reader: :public
prop :display_filename, default: true
prop :attaching, default: false
prop :multiple, default: false

def data
{
component: component_name,
blob_id: blob.id,
media_library_blob_param: blob.as_json,
media_library_path_param: helpers.main_app.url_for(blob),
media_library_attaching_param: @attaching,
media_library_multiple_param: @multiple,
media_library_selected_item: params[:controller_selector],
action: 'click->media-library#selectItem'
}.tap do |result|
if @attaching
result[:turbo_frame] = Avo::MEDIA_LIBRARY_ITEM_DETAILS_FRAME_ID
result[:turbo_prefetch] = false
else
result[:turbo_prefetch] = true
end
end
end
end
end
end
2 changes: 1 addition & 1 deletion app/components/avo/paginator_component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
</div>
</div>
<div class="flex">
<div class="flex-2 sm:flex sm:items-center sm:justify-between space-y-2 sm:space-y-0 text-center sm:text-left pagy-gem-version-<%= pagy_major_version %>">
<div class="flex-2 sm:flex sm:items-center sm:justify-between space-y-2 sm:space-y-0 text-center sm:text-left pagy-gem-version-<%= helpers.pagy_major_version %>">
<% if @resource.pagination_type.default? %>
<div class="text-sm text-slate-600 mr-4"><%== helpers.pagy_info @pagy %></div>
<% end %>
Expand Down
9 changes: 0 additions & 9 deletions app/components/avo/paginator_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,4 @@ def per_page_options
options.sort.uniq
end
end

def pagy_major_version
return nil unless defined?(Pagy::VERSION)
version = Pagy::VERSION&.split(".")&.first&.to_i

return "8-or-more" if version >= 8

version
end
end
2 changes: 2 additions & 0 deletions app/components/avo/sidebar_component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
<div class="space-y-6 mb-4">
<%= render Avo::Sidebar::LinkComponent.new label: 'Get started', path: helpers.avo.root_path, active: :exclusive if Rails.env.development? && Avo.configuration.home_path.nil? %>

<%= render Avo::Sidebar::LinkComponent.new label: 'Media Library', path: helpers.avo.media_library_index_path, active: :exclusive if Avo::MediaLibrary.configuration.visible? %>

<% if Avo.plugin_manager.installed?(:avo_menu) && Avo.has_main_menu? %>
<% Avo.main_menu.items.each do |item| %>
<%= render Avo::Sidebar::ItemSwitcherComponent.new item: item %>
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/avo/actions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def build_background_url
params = URI.decode_www_form(uri.query || "").to_h

params.delete("action_id")
params[:turbo_frame] = ACTIONS_BACKGROUND_FRAME
params[:turbo_frame] = ACTIONS_BACKGROUND_FRAME_ID

# Reconstruct the query string
new_query_string = URI.encode_www_form(params)
Expand Down
42 changes: 42 additions & 0 deletions app/controllers/avo/media_library_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
module Avo
class MediaLibraryController < ApplicationController
include Pagy::Backend
before_action :authorize_access!

def index
@attaching = false
end

def show
@blob = ActiveStorage::Blob.find(params[:id])
end

def destroy
@blob = ActiveStorage::Blob.find(params[:id])
@blob.destroy!

redirect_to avo.media_library_index_path
end

def update
@blob = ActiveStorage::Blob.find(params[:id])
@blob.update!(blob_params)
end

def attach
@attaching = true

render :index
end

private

def blob_params
params.require(:blob).permit(:filename, metadata: [:title, :alt, :description])
end

def authorize_access!
raise_404 unless Avo::MediaLibrary.configuration.visible?
end
end
end
9 changes: 9 additions & 0 deletions app/helpers/avo/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,15 @@ def possibly_rails_authentication?
defined?(Authentication) && Authentication.private_instance_methods.include?(:require_authentication) && Authentication.private_instance_methods.include?(:authenticated?)
end

def pagy_major_version
return nil unless defined?(Pagy::VERSION)
version = Pagy::VERSION&.split(".")&.first&.to_i

return "8-or-more" if version >= 8

version
end

def container_is_full_width?
if @container_full_width.present?
@container_full_width
Expand Down
Loading
Loading