Skip to content

Commit

Permalink
feature: media library/gallery (#3561)
Browse files Browse the repository at this point in the history
* feature: media library

* renamve components

* make it work for trix and rhino too

* add request.js

* wip

* wip

* wip

* wip

* wip

* wip

* can upload

* add configuration

* add helpers

* add authorization

* fix update and destroy

* wip

* wip

* wip

* wip

* Potential fix for code scanning alert no. 43: DOM text reinterpreted as HTML

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* properly escape the filename

* lint

* 18n-tasks translate-missing

* revert unique_id for trix input

* npx update-browserslist-db@latest

* fix unique_selector

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: Paul Bob <[email protected]>
Co-authored-by: Paul Bob <[email protected]>
  • Loading branch information
4 people authored Jan 30, 2025
1 parent ba6578f commit 8ed64be
Show file tree
Hide file tree
Showing 60 changed files with 915 additions and 107 deletions.
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
7 changes: 7 additions & 0 deletions app/assets/stylesheets/css/fields/trix.css
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,10 @@ trix-toolbar {
@apply ml-0;
}
}

/* Hack to remove border from trix when rendered from ActionText */
.trix-content {
& .trix-content {
border: none;
}
}
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
10 changes: 3 additions & 7 deletions app/components/avo/fields/tiptap_field/show_component.html.erb
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
<%= field_wrapper(**field_wrapper_args) do %>
<%
content_classes = 'tiptap__content py-2 max-w-4xl'
content_classes << ' hidden' unless @field.always_show
%>
<div data-controller="hidden-input">
<%= field_wrapper(**field_wrapper_args, data: { controller: 'hidden-input' }) do %>
<div data-controller="hidden-input" class="flex w-full">
<% unless @field.always_show %>
<%= link_to t('avo.show_content'), 'javascript:void(0);', class: 'font-bold inline-block', data: { action: 'click->hidden-input#showContent' } %>
<% end %>
<div class="<%= content_classes %> " data-hidden-input-target="content">
<div class="<%= class_names("tiptap__content py-2 max-w-4xl", "hidden": !@field.always_show) %> " data-hidden-input-target="content">
<%= sanitize @field.value.to_s %>
</div>
</div>
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", @input_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: @input_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: @input_id,
placeholder: @field.placeholder,
style: @field.get_html(:style, view: view, element: :input)
%>
Expand Down
22 changes: 15 additions & 7 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,21 @@ def initialize(**args)
@resource_name = args[:resource_name] || @resource&.singular_route_key

super(**args)
end

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

def data_values
{
# 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: ".#{@input_id}", # mandatory
attachments_disabled: @field.attachments_disabled,
attachment_key: @field.attachment_key,
hide_attachment_filename: @field.hide_attachment_filename,
Expand All @@ -33,5 +34,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
10 changes: 3 additions & 7 deletions app/components/avo/fields/trix_field/show_component.html.erb
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
<%= field_wrapper(**field_wrapper_args) do %>
<%
content_classes = 'trix-content py-2 max-w-4xl'
content_classes << ' hidden' unless @field.always_show
button_classes = 'font-bold inline-block pt-3'
%>
<%= field_wrapper(**field_wrapper_args, full_width: true) do %>
<% button_classes = 'font-semibold inline-block pt-3 text-sm' %>
<%= content_tag :div,
data: {
controller: "trix-body",
trix_body_always_show_value: @field.always_show
} do %>
<div class="<%= content_classes %>" data-trix-body-target="content">
<div class="<%= class_names("trix-content border-none px-0 py-2 max-w-4xl", "hidden": !@field.always_show) %>" data-trix-body-target="content">
<%= sanitize @field.value.to_s %>
</div>
<% unless @field.always_show %>
Expand Down
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
59 changes: 59 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,59 @@
<%= 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 pb-0",
data: {
controller: 'media-library-attach',
media_library_attach_direct_uploads_url_value: helpers.main_app.rails_direct_uploads_url,
} 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: {
media_library_attach_target: 'dropzone',
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 %>

<%= content_tag :div, "", class: "m-2 flex flex-wrap gap-2 empty:m-0", data: {
media_library_attach_target: 'uploadingContainer',
turbo_permanent: true,
} %>
<% 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
Loading

0 comments on commit 8ed64be

Please sign in to comment.