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

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 5 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
12 changes: 12 additions & 0 deletions app/assets/stylesheets/avo.base.css
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,15 @@ trix-editor {
dialog#turbo-confirm {
@apply bg-transparent;
}

dl {
@apply text-sm;

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

dd {
@apply inline-block ml-0;
}
}
3 changes: 3 additions & 0 deletions app/components/avo/button_component.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# frozen_string_literal: true

# A button/link can have the following settings:
# style: primary/outline/text
# size: :xs :sm, :md, :lg
class Avo::ButtonComponent < Avo::BaseComponent
prop :path, kind: :positional
prop :size, default: :md
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
38 changes: 38 additions & 0 deletions app/components/avo/media_library/item_component.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<%= link_to helpers.avo.medium_path(attachment),
id: dom_id(attachment),
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: {
turbo_frame: false ? Avo::MEDIA_LIBRARY_ITEM_DETAILS_FRAME_ID : nil,
turbo_prefetch: !@attaching, # don't try to prefetch the item details frame if we are not visiting that page.
attachment_id: attachment.id,
media_library_attachment_param: attachment.as_json,
media_library_blob_param: attachment.blob.as_json,
media_library_path_param: helpers.main_app.url_for(attachment),
media_library_attaching_param: @attaching,
media_library_multiple_param: @multiple,
media_library_selected_item: params[:controller_selector],
action: 'click->media-library#selectItem',
} do %>
<% if @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 attachment.representable? %>
<%= image_tag helpers.main_app.url_for(attachment.blob), class: "max-w-full self-start #{@extra_classes}", loading: :lazy, width: attachment.blob.metadata["width"], height: attachment.blob.metadata["height"] %>
<% elsif attachment.blob.audio? %>
<%= audio_tag(helpers.main_app.url_for(attachment), controls: true, preload: false, class: 'w-full') %>
<% elsif attachment.blob.video? %>
<%= video_tag(helpers.main_app.url_for(attachment), 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="<%= attachment.filename %>"><%= attachment.filename %></span>
<% end %>
<%#= render Avo::Fields::Common::Files::ControlsComponent.new(field: @field, file: attachment, resource: @resource) %>
</div>
<% end %>
14 changes: 14 additions & 0 deletions app/components/avo/media_library/item_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# frozen_string_literal: true

module Avo
module MediaLibrary
class ItemComponent < Avo::BaseComponent
with_collection_parameter :attachment

prop :attachment, reader: :public
prop :display_filename, default: true
prop :attaching, default: false
prop :multiple, default: false
end
end
end
26 changes: 26 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,26 @@
<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_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: {
action: "click->avo-media-library-item-details#close",
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 @attachment.representable? %>
<%= image_tag helpers.main_app.url_for(@attachment.blob), class: "max-w-full rounded-lg max-h-xl", loading: :lazy %>
<% end %>
<div class="flex justify-center w-full text-sm gap-4">
<%= link_to "Download", helpers.main_app.url_for(@attachment.blob), download: true %>
<%= link_to "Copy URL to clipboard", helpers.main_app.url_for(@attachment.blob), data: {controller: "copy-to-clipboard", text: helpers.main_app.url_for(@attachment.blob), action: "click->copy-to-clipboard#copy"} %>
<%= link_to "Delete", helpers.avo.media_library_item_path(@attachment), 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/items/information", locals: {attachment: @attachment} %>
<%= render partial: "avo/media_library/items/form", locals: {attachment: @attachment} %>
</div>
</div>
</div>
22 changes: 22 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,22 @@
# frozen_string_literal: true

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

prop :attachment

def parent_title(parent)
# TODO: find the resource and get the title attribute from there
parent.to_param
end

def parent_path(parent)
# TODO: find the resource and get the path from there
parent.to_param
end
end
end
end
34 changes: 34 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,34 @@
<%= 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 %>
<%= a_button data: {
action: 'click->media-library#selectItems',
} do %>
Attach
<% end %>
<% end %>
<%= c.with_body do %>
<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::ItemComponent.with_collection(@attachments, 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;', xsrc: helpers.avo.media_library_item_path(@attachments.first) if @attachments.first %>
</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) %>
<% end %>
</div>
</div>
<% end %>
<% end %>
22 changes: 22 additions & 0 deletions app/components/avo/media_library/list_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

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

def initialize(parent:, attaching: false)
@parent = parent
@attaching = attaching
@pagy, @attachments = pagy(query, limit: 12)
end

def controller = Avo::Current.view_context.controller

def query
ActiveStorage::Attachment.includes(:blob)
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_path, active: :exclusive %>

<% 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 @@ -34,7 +34,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
10 changes: 10 additions & 0 deletions app/controllers/avo/media_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module Avo
class MediaController < ApplicationController
def index
end

def show
@attachment = ActiveStorage::Attachment.find(params[:id])
end
end
end
38 changes: 38 additions & 0 deletions app/controllers/avo/media_library/items_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
module Avo
module MediaLibrary
class ItemsController < Avo::ApplicationController
include Pagy::Backend

def index
end

def show
@attachment = ActiveStorage::Attachment.find(params[:id])
end

def create
resource = Avo.resource_manager.get_resource_by_name(params[:resource_name])
@parent = resource.find_record(params[:record_id])
end

def destroy
@attachment = ActiveStorage::Attachment.find(params[:id])
@attachment.destroy!
redirect_to avo.media_library_path
end

def update
@attachment = ActiveStorage::Attachment.find(params[:id])
@attachment.update!(attachment_params)
redirect_to avo.media_library_path
end

private

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

adrianthedev marked this conversation as resolved.
Show resolved Hide resolved
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

private

# Taken from the original library
Expand Down
2 changes: 2 additions & 0 deletions app/javascript/js/controllers.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import ItemSelectAllController from './controllers/item_select_all_controller'
import ItemSelectorController from './controllers/item_selector_controller'
import KeyValueController from './controllers/fields/key_value_controller'
import LoadingButtonController from './controllers/loading_button_controller'
import MediaLibraryController from './controllers/media_library_controller'
import MenuController from './controllers/menu_controller'
import ModalController from './controllers/modal_controller'
import MultipleSelectFilterController from './controllers/multiple_select_filter_controller'
Expand Down Expand Up @@ -67,6 +68,7 @@ application.register('input-autofocus', InputAutofocusController)
application.register('item-select-all', ItemSelectAllController)
application.register('item-selector', ItemSelectorController)
application.register('loading-button', LoadingButtonController)
application.register('media-library', MediaLibraryController)
application.register('menu', MenuController)
application.register('modal', ModalController)
application.register('multiple-select-filter', MultipleSelectFilterController)
Expand Down
Loading
Loading