<% end %>
diff --git a/app/components/avo/paginator_component.rb b/app/components/avo/paginator_component.rb
index 318197b7fa..cfac81f33d 100644
--- a/app/components/avo/paginator_component.rb
+++ b/app/components/avo/paginator_component.rb
@@ -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
diff --git a/app/components/avo/sidebar_component.html.erb b/app/components/avo/sidebar_component.html.erb
index f43d39f811..8284ebc6f7 100644
--- a/app/components/avo/sidebar_component.html.erb
+++ b/app/components/avo/sidebar_component.html.erb
@@ -12,6 +12,8 @@
<%= 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 %>
diff --git a/app/controllers/avo/actions_controller.rb b/app/controllers/avo/actions_controller.rb
index 256449902f..eb0cd67e44 100644
--- a/app/controllers/avo/actions_controller.rb
+++ b/app/controllers/avo/actions_controller.rb
@@ -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)
diff --git a/app/controllers/avo/media_library_controller.rb b/app/controllers/avo/media_library_controller.rb
new file mode 100644
index 0000000000..6e9c4280fe
--- /dev/null
+++ b/app/controllers/avo/media_library_controller.rb
@@ -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
diff --git a/app/helpers/avo/application_helper.rb b/app/helpers/avo/application_helper.rb
index 0daa474c82..7b1ba7c001 100644
--- a/app/helpers/avo/application_helper.rb
+++ b/app/helpers/avo/application_helper.rb
@@ -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
diff --git a/app/helpers/avo/resources_helper.rb b/app/helpers/avo/resources_helper.rb
index de16039368..54055f9abd 100644
--- a/app/helpers/avo/resources_helper.rb
+++ b/app/helpers/avo/resources_helper.rb
@@ -57,5 +57,18 @@ def resource_show_path(resource:, parent_resource: nil, parent_record: nil, pare
resource_path(record: resource.record, resource: parent_or_child_resource, **args)
end
+
+ def resource_for_record(record)
+ klass = Avo.resource_manager.get_resource_by_model_class(record.class)
+ klass.new(record: record)
+ end
+
+ def record_title(record)
+ resource_for_record(record).record_title
+ end
+
+ def record_path(record)
+ resource_for_record(record).record_path
+ end
end
end
diff --git a/app/javascript/js/controllers.js b/app/javascript/js/controllers.js
index 48ad154aee..3876d7104e 100644
--- a/app/javascript/js/controllers.js
+++ b/app/javascript/js/controllers.js
@@ -22,6 +22,8 @@ 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 MediaLibraryAttachController from './controllers/media_library_attach_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'
@@ -68,6 +70,8 @@ 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('media-library-attach', MediaLibraryAttachController)
application.register('menu', MenuController)
application.register('modal', ModalController)
application.register('multiple-select-filter', MultipleSelectFilterController)
diff --git a/app/javascript/js/controllers/copy_to_clipboard_controller.js b/app/javascript/js/controllers/copy_to_clipboard_controller.js
index 4ff80bf602..47d4e2551b 100644
--- a/app/javascript/js/controllers/copy_to_clipboard_controller.js
+++ b/app/javascript/js/controllers/copy_to_clipboard_controller.js
@@ -1,7 +1,10 @@
import { Controller } from '@hotwired/stimulus'
+// Connects to data-controller="copy-to-clipboard"
+//
export default class extends Controller {
- copy() {
+ copy(event) {
+ event.preventDefault()
const str = this.context.element.dataset.text
/* ——— Derived from: https://hackernoon.com/copying-text-to-clipboard-with-javascript-df4d4988697f
improved to add iOS device compatibility——— */
@@ -33,5 +36,15 @@ export default class extends Controller {
el.contentEditable = storeContentEditable
el.readOnly = storeReadOnly
+
+ const target = this.element
+ const originalHTML = target.innerHTML
+ target.innerHTML = 'Copied 👌'
+ target.classList.add('transition', 'opacity-80', 'bg-green-100')
+
+ setTimeout(() => {
+ target.innerHTML = originalHTML
+ target.classList.remove('opacity-80', 'bg-green-100')
+ }, 1500)
}
}
diff --git a/app/javascript/js/controllers/fields/trix_field_controller.js b/app/javascript/js/controllers/fields/trix_field_controller.js
index 9b9796c352..a4131468d5 100644
--- a/app/javascript/js/controllers/fields/trix_field_controller.js
+++ b/app/javascript/js/controllers/fields/trix_field_controller.js
@@ -1,9 +1,14 @@
+/* eslint-disable no-console */
+/* eslint-disable camelcase */
/* eslint-disable no-alert */
import 'trix'
import URI from 'urijs'
import { Controller } from '@hotwired/stimulus'
+// eslint-disable-next-line max-len
+const galleryButtonSVG = '
'
+
export default class extends Controller {
static targets = ['editor', 'controller']
@@ -21,63 +26,132 @@ export default class extends Controller {
attachmentKeyWarning: String,
}
+ get rootPath() {
+ return new URI(window.Avo.configuration.root_path)
+ }
+
get uploadUrl() {
// Parse the current URL
const url = new URI(window.location.origin)
- // Parse the root path
- const rootPath = new URI(window.Avo.configuration.root_path)
// Build the trix field path
- url.path(`${rootPath.path()}/avo_api/resources/${this.resourceNameValue}/${this.resourceIdValue}/attachments`)
+ url.path(`${this.rootPath.path()}/avo_api/resources/${this.resourceNameValue}/${this.resourceIdValue}/attachments`)
// Add the params back
- url.query(rootPath.query())
+ url.query(this.rootPath.query())
return url.toString()
}
connect() {
- if (this.attachmentsDisabledValue) {
- // Remove the attachments button
- window.addEventListener('trix-initialize', (event) => {
- if (event.target === this.editorTarget) {
- this.controllerTarget.querySelector('.trix-button-group--file-tools').remove()
- }
- })
- }
+ this.#attachTrixListeners()
+ }
- window.addEventListener('trix-file-accept', (event) => {
- if (event.target === this.editorTarget) {
- // Prevent file uploads for fields that have attachments disabled.
- if (this.attachmentsDisabledValue) {
- event.preventDefault()
- alert(this.attachmentDisableWarningValue)
+ disconnect() {
+ this.#removeTrixListeners()
+ }
- return
- }
+ // Invoked by the other controllers (media-library)
+ insertAttachments(attachments, event) {
+ if (!attachments) {
+ console.warning('[Avo->] No attachments present.')
- // Prevent file uploads for resources that haven't been saved yet.
- if (!this.resourceIdValue) {
- event.preventDefault()
- alert(this.uploadWarningValue)
+ return
+ }
- return
- }
+ attachments.forEach((attachment) => {
+ const { path, blob } = attachment
- // Prevent file uploads for fields without an attachment key.
- // When is rich text, attachment key is not needed.
- if (!this.isActionTextValue && !this.attachmentKeyValue) {
- event.preventDefault()
- alert(this.attachmentKeyWarningValue)
- }
+ const payload = {
+ url: path,
+ filename: blob.filename,
+ contentType: blob.content_type,
+ previewable: true,
}
+
+ this.#injectAttachment(payload, event)
})
+ }
+
+ // eslint-disable-next-line no-unused-vars
+ #injectAttachment(attachment, event) {
+ const model = new window.Trix.models.Attachment(attachment)
+ this.editorTarget.editorController.editor.insertAttachment(model)
+ }
- window.addEventListener('trix-attachment-add', (event) => {
+ #removeTrixListeners() {
+ this.element.removeEventListener('trix-file-accept', this.#trixFileAccept.bind(this))
+ this.element.removeEventListener('trix-attachment-add', this.#trixAttachmentAdd.bind(this))
+ this.element.removeEventListener('trix-initialize', this.#trixInitialize.bind(this))
+ }
+
+ #attachTrixListeners() {
+ this.element.addEventListener('trix-file-accept', this.#trixFileAccept.bind(this))
+ this.element.addEventListener('trix-attachment-add', this.#trixAttachmentAdd.bind(this))
+ this.element.addEventListener('trix-initialize', this.#trixInitialize.bind(this))
+ }
+
+ #trixInitialize(event) {
+ // Remove the attachments button from the toolbar if the field has attachments disabled
+ if (this.attachmentsDisabledValue) {
if (event.target === this.editorTarget) {
- if (event.attachment.file) {
- this.uploadFileAttachment(event.attachment)
- }
+ this.controllerTarget.querySelector('.trix-button-group--file-tools').remove()
}
- })
+ }
+
+ const controllerElement = this.element.closest('[data-trix-field-target="controller"]')
+ const params = {
+ resource_name: this.resourceNameValue,
+ record_id: this.resourceIdValue,
+ controller_selector: controllerElement.dataset.trixFieldUniqueSelectorValue,
+ controller_name: this.identifier,
+ }
+
+ const mediaLibraryPath = new URI(`${this.rootPath.path()}/attach-media`)
+ mediaLibraryPath.addSearch(params)
+ const mediaLibraryVisible = window.Avo.configuration.media_library.visible && window.Avo.configuration.media_library.enabled
+
+ // Add the gallery button to the toolbar
+ // const buttonHTML = `
`
+ const buttonHTML = `
${galleryButtonSVG}`
+ if (mediaLibraryVisible && event.target.toolbarElement && event.target.toolbarElement.querySelector('.trix-button-group--file-tools')) {
+ event.target.toolbarElement
+ .querySelector('.trix-button-group--file-tools')
+ .insertAdjacentHTML('beforeend', buttonHTML)
+ }
+ }
+
+ #trixAttachmentAdd(event) {
+ if (event.target === this.editorTarget) {
+ if (event.attachment.file) {
+ this.uploadFileAttachment(event.attachment)
+ }
+ }
+ }
+
+ #trixFileAccept(event) {
+ if (event.target === this.editorTarget) {
+ // Prevent file uploads for fields that have attachments disabled.
+ if (this.attachmentsDisabledValue) {
+ event.preventDefault()
+ alert(this.attachmentDisableWarningValue)
+
+ return
+ }
+
+ // Prevent file uploads for resources that haven't been saved yet.
+ if (!this.resourceIdValue) {
+ event.preventDefault()
+ alert(this.uploadWarningValue)
+
+ return
+ }
+
+ // Prevent file uploads for fields without an attachment key.
+ // When is rich text, attachment key is not needed.
+ if (!this.isActionTextValue && !this.attachmentKeyValue) {
+ event.preventDefault()
+ alert(this.attachmentKeyWarningValue)
+ }
+ }
}
uploadFileAttachment(attachment) {
diff --git a/app/javascript/js/controllers/media_library_attach_controller.js b/app/javascript/js/controllers/media_library_attach_controller.js
new file mode 100644
index 0000000000..e740c47bdd
--- /dev/null
+++ b/app/javascript/js/controllers/media_library_attach_controller.js
@@ -0,0 +1,160 @@
+/* eslint-disable no-console */
+// eslint-disable-next-line max-classes-per-file
+import { Controller } from '@hotwired/stimulus'
+import { DirectUpload } from '@rails/activestorage'
+import { escape } from 'lodash'
+
+class UploadObject extends EventTarget {
+ constructor(file, controller) {
+ super()
+
+ this.file = file
+ this.controller = controller
+ this.finished = false
+ }
+
+ hasFinished() {
+ return this.finished
+ }
+
+ handle() {
+ const upload = new DirectUpload(this.file, this.controller.directUploadsUrlValue, this)
+
+ this.#addUploadingItem(this.file)
+
+ upload.create((error, blob) => {
+ if (error) {
+ console.log('Error', error)
+ }
+ this.listItem.classList.add('text-gray-400')
+
+ this.finished = true
+ this.dispatchEvent(new Event('done'))
+ })
+ }
+
+ #addUploadingItem(file) {
+ const div = document.createElement('div')
+ div.classList.add('flex', 'justify-between', 'gap-2', 'text-sm')
+ div.innerHTML = `
${escape(file.name)}
0%
`
+ this.listItem = this.controller.uploadingContainerTarget.appendChild(div)
+ }
+
+ directUploadWillStoreFileWithXHR(request) {
+ request.upload.addEventListener(
+ 'progress',
+ (event) => this.#directUploadDidProgress(event),
+ )
+ }
+
+ #directUploadDidProgress(event) {
+ const progress = (event.loaded / event.total) * 100
+ const element = this.listItem.querySelector('.progress')
+ element.textContent = `${progress.toFixed(2)}%`
+ }
+}
+
+// Connects to data-controller="media-library-new"
+export default class extends Controller {
+ static targets = ['dropzone', 'uploadingContainer']
+
+ static values = {
+ directUploadsUrl: String,
+ }
+
+ dragCopy = 'drag file or click to upload'
+
+ dropCopy = "drop it like it's hot"
+
+ droppedCopy = 'uploading file. hang tight'
+
+ unsupportedCopy = 'wrong file type. try again'
+
+ draggingClasses = ['dropzone-dragging', '!border-gray-700', '!text-gray-700']
+
+ connect() {
+ this.attachHandlers()
+ this.setupFileInput()
+ }
+
+ setupFileInput() {
+ // Create a hidden file input element
+ this.fileInput = document.createElement('input')
+ this.fileInput.type = 'file'
+ this.fileInput.multiple = true
+ this.fileInput.style.display = 'none'
+ this.element.appendChild(this.fileInput)
+
+ // Handle file selection
+ this.fileInput.addEventListener('change', (e) => {
+ const files = Array.from(e.target.files)
+ this.#submitFiles(files)
+ this.fileInput.value = '' // Reset the input for future selections
+ })
+ }
+
+ triggerFileBrowser() {
+ this.fileInput.click()
+ }
+
+ attachHandlers() {
+ const vm = this
+
+ Array.from(['drag', 'dragstart', 'dragend', 'dragover', 'dragenter', 'dragleave', 'drop']).forEach((event) => {
+ vm.dropzoneTarget.addEventListener(event, (e) => {
+ e.preventDefault()
+ e.stopPropagation()
+ })
+ })
+
+ this.dropzoneTarget.addEventListener('dragover', (e) => {
+ vm.dragAction()
+ }, false)
+
+ this.dropzoneTarget.addEventListener('dragleave', (e) => {
+ vm.dropAction()
+ }, false)
+
+ this.dropzoneTarget.addEventListener('drop', (e) => {
+ vm.dropAction()
+ const { files } = e.dataTransfer
+ this.#submitFiles(files)
+ }, false)
+ }
+
+ #submitFiles(files) {
+ this.uploadObjects = []
+ Array.from(files).forEach((file) => {
+ const uploadObject = new UploadObject(file, this)
+ this.uploadObjects.push(uploadObject)
+ uploadObject.addEventListener('done', () => {
+ this.#checkIfAllFinished()
+ })
+ uploadObject.handle()
+ })
+ }
+
+ #checkIfAllFinished() {
+ if (this.uploadObjects.every((uploadObject) => uploadObject.hasFinished())) {
+ this.#refreshPage()
+ }
+ }
+
+ #refreshPage() {
+ const streamElement = document.createElement('turbo-stream')
+ streamElement.setAttribute('action', 'refresh')
+ streamElement.setAttribute('method', 'morph')
+ streamElement.setAttribute('render', 'morph')
+ this.element.appendChild(streamElement)
+ }
+
+ dragAction() {
+ this.dropzoneTarget.classList.add(...this.draggingClasses)
+ this.dropzoneTarget.innerHTML = this.dropCopy
+ }
+
+ dropAction() {
+ this.dropzoneTarget.classList.remove(...this.draggingClasses)
+ this.dropzoneTarget.innerHTML = this.droppedCopy
+ }
+}
diff --git a/app/javascript/js/controllers/media_library_controller.js b/app/javascript/js/controllers/media_library_controller.js
new file mode 100644
index 0000000000..b6746c4d02
--- /dev/null
+++ b/app/javascript/js/controllers/media_library_controller.js
@@ -0,0 +1,88 @@
+/* eslint-disable no-console */
+import { Controller } from '@hotwired/stimulus'
+import { closeModal } from '../helpers'
+
+// Connects to data-controller="media-library"
+export default class extends Controller {
+ static outlets = ['field']
+
+ static values = {
+ itemDetailsFrameId: String,
+ controllerSelector: String,
+ controllerName: String,
+ selectedItems: Array,
+ }
+
+ // get the controller for the selector and name
+ get otherController() {
+ return this.application.getControllerForElementAndIdentifier(document.querySelector(this.controllerSelectorValue), this.controllerNameValue)
+ }
+
+ // get the controller for the selector and name
+ get modalController() {
+ return this.application.getControllerForElementAndIdentifier(document.querySelector('[data-controller="modal"]'), 'modal')
+ }
+
+ // selectItems(event) {
+ // // Search the DOM for the media library list component
+ // const mediaLibraryList = document.querySelector('[data-controller="media-library"]')
+
+ // // Get the elements that have the data-selected attribute set to true
+ // const selectedItems = mediaLibraryList.querySelectorAll('[data-selected="true"]')
+
+ // // get the attachment and blob information from the selected items
+ // const attachments = Array.from(selectedItems).map((item) => {
+ // const attachment = JSON.parse(item.dataset.mediaLibraryAttachmentParam)
+ // const blob = JSON.parse(item.dataset.mediaLibraryBlobParam)
+ // const path = item.dataset.mediaLibraryPathParam
+
+ // return { attachment, blob, path }
+ // })
+
+ // this.insertAttachments(attachments, event)
+ // this.modalController.closeModal()
+ // }
+
+ selectItem(event) {
+ const { params } = event
+ const { attaching, multiple } = params
+ const item = event.target.closest('[data-component="avo/media_library/list_item_component"]')
+ const attachments = [this.#extractMetadataFromItem(item)]
+ // When attaching, we want to prevent showing the details screen and instead just check the attachment
+ if (attaching) {
+ event.preventDefault()
+ this.insertAttachments(attachments, event)
+ closeModal()
+
+ // TODO: allow multiple attachments
+ // if (multiple) {
+ // const element = document.querySelector(`[data-attachment-id="${params.attachment.id}"]`)
+ // element.dataset.selected = !(element.dataset.selected === 'true')
+ // } else {
+ // this.insertAttachments(attachments, event)
+ // }
+ }
+ }
+
+ insertAttachments(attachments, event) {
+ // show an error if the controller is not found
+ if (!this.otherController) {
+ console.error('[Avo->] The Media Library failed to find any field outlets to inject the asset.')
+
+ return
+ }
+ // Trigger the insertAttachment action on the other controller
+ this.otherController.insertAttachments(attachments, event)
+ }
+
+ closeItemDetails() {
+ document.querySelector(`turbo-frame#${this.itemDetailsFrameIdValue}`).innerHTML = ''
+ }
+
+ #extractMetadataFromItem(item) {
+ const blob = JSON.parse(item.dataset.mediaLibraryBlobParam)
+ const path = item.dataset.mediaLibraryPathParam
+
+ return { blob, path }
+ }
+}
diff --git a/app/javascript/js/controllers/modal_controller.js b/app/javascript/js/controllers/modal_controller.js
index f071983aa4..8d84d6f736 100644
--- a/app/javascript/js/controllers/modal_controller.js
+++ b/app/javascript/js/controllers/modal_controller.js
@@ -1,5 +1,6 @@
import { Controller } from '@hotwired/stimulus'
+// Connects to data-controller="modal"
export default class extends Controller {
static targets = ['modal', 'backdrop']
@@ -7,11 +8,16 @@ export default class extends Controller {
closeModalOnBackdropClick: true,
}
- close() {
+ close(event) {
if (event.target === this.backdropTarget && !this.closeModalOnBackdropClickValue) return
+ this.closeModal()
+ }
+
+ // May be invoked by the other controllers
+ closeModal() {
this.modalTarget.remove()
- document.dispatchEvent(new Event('actions-modal:close'))
+ document.dispatchEvent(new Event('modal-controller:close'))
}
}
diff --git a/app/javascript/js/helpers/index.js b/app/javascript/js/helpers/index.js
new file mode 100644
index 0000000000..19c94e1b9f
--- /dev/null
+++ b/app/javascript/js/helpers/index.js
@@ -0,0 +1,5 @@
+const closeModal = () => {
+ document.querySelector(`turbo-frame#${window.Avo.configuration.modal_frame_id}`).innerHTML = ''
+}
+
+export { closeModal }
diff --git a/app/views/avo/actions/show.html.erb b/app/views/avo/actions/show.html.erb
index c56ce68fa3..68cdaf5f75 100644
--- a/app/views/avo/actions/show.html.erb
+++ b/app/views/avo/actions/show.html.erb
@@ -1,4 +1,4 @@
-<%= turbo_frame_tag Avo::ACTIONS_BACKGROUND_FRAME, src: @background_url, loading: :lazy, target: :_top, class: "block" do %>
+<%= turbo_frame_tag Avo::ACTIONS_BACKGROUND_FRAME_ID, src: @background_url, loading: :lazy, target: :_top, class: "block" do %>
<%= render Avo::LoadingComponent.new(title: "...") %>
<% end %>
diff --git a/app/views/avo/media_library/_form.html.erb b/app/views/avo/media_library/_form.html.erb
new file mode 100644
index 0000000000..ce3064d534
--- /dev/null
+++ b/app/views/avo/media_library/_form.html.erb
@@ -0,0 +1,40 @@
+<%= form_with model: @blob, url: avo.media_library_path(@blob), method: :patch, class: "pb-6" do |f| %>
+ <%= field_container do %>
+ <%= avo_edit_field :filename, as: :text, short: true, stacked: true, form: f, required: true %>
+ <%= f.fields_for :metadata do |meta_form| %>
+ <%= avo_edit_field :title,
+ as: :text,
+ value: @blob.metadata[:title],
+ short: true,
+ stacked: true,
+ form: meta_form,
+ required: true
+ %>
+ <%= avo_edit_field :alt,
+ as: :text,
+ value: @blob.metadata[:alt],
+ short: true,
+ stacked: true,
+ form: meta_form
+ %>
+ <%= avo_edit_field :description,
+ as: :textarea,
+ value: @blob.metadata[:description],
+ short: true,
+ stacked: true,
+ form: meta_form
+ %>
+ <% end %>
+ <% end %>
+
+
+ <%= a_button type: :submit,
+ icon: "avo/save",
+ size: :sm,
+ style: :outline,
+ data: {
+ } do %>
+ Update
+ <% end %>
+
+<% end %>
diff --git a/app/views/avo/media_library/_information.html.erb b/app/views/avo/media_library/_information.html.erb
new file mode 100644
index 0000000000..21f2d075d1
--- /dev/null
+++ b/app/views/avo/media_library/_information.html.erb
@@ -0,0 +1,50 @@
+
+
+ - Filename:
+ - <%= @blob.filename %>
+
+
+ - Key:
+ - <%= @blob.key %>
+
+
+ - Content type:
+ - <%= @blob.content_type %>
+
+
+ - Size:
+ - <%= number_to_human_size(@blob.byte_size) %>
+
+
+ - Service name:
+ - <%= @blob.service_name %>
+
+
+ - Created at:
+ - <%= @blob.created_at %>
+
+
+ <% if @blob.attachments.present? %>
+ - Attached to:
+ -
+ <% @blob.attachments.each do |attachment| %>
+ <%= link_to record_title(attachment.record), record_path(attachment.record) %>
+ <% end %>
+
+ <% end %>
+
+
+ - Type:
+ - <%= @blob.filename %>
+
+
+ - Metadata:
+ -
+ <% @blob.metadata.each do |key, value| %>
+
+ <%= key %>: <%= value %>
+
+ <% end %>
+
+
+
diff --git a/app/views/avo/media_library/index.html.erb b/app/views/avo/media_library/index.html.erb
new file mode 100644
index 0000000000..aef9076e22
--- /dev/null
+++ b/app/views/avo/media_library/index.html.erb
@@ -0,0 +1,9 @@
+<%= render Avo::TurboFrameWrapperComponent.new(turbo_frame_request_id) do %>
+ <% if turbo_frame_request_id %>
+ <%= render Avo::ModalComponent.new do |c| %>
+ <%= render Avo::MediaLibrary::ListComponent.new attaching: @attaching %>
+ <% end %>
+ <% else %>
+ <%= render Avo::MediaLibrary::ListComponent.new attaching: @attaching %>
+ <% end %>
+<% end %>
diff --git a/app/views/avo/media_library/show.html.erb b/app/views/avo/media_library/show.html.erb
new file mode 100644
index 0000000000..dae6705382
--- /dev/null
+++ b/app/views/avo/media_library/show.html.erb
@@ -0,0 +1,5 @@
+<%= render Avo::PanelComponent.new title: t("avo.media_library.title") do |c| %>
+ <%= c.with_body do %>
+ <%= render Avo::MediaLibrary::ItemDetailsComponent.new(blob: @blob) %>
+ <% end %>
+<% end %>
diff --git a/app/views/avo/partials/_javascript.html.erb b/app/views/avo/partials/_javascript.html.erb
index 7e8fc0fbfb..b88831b87e 100644
--- a/app/views/avo/partials/_javascript.html.erb
+++ b/app/views/avo/partials/_javascript.html.erb
@@ -13,4 +13,8 @@
<% end %>
Avo.configuration.modal_frame_id = '<%= ::Avo::MODAL_FRAME_ID %>'
Avo.configuration.stimulus_controllers = []
+ Avo.configuration.media_library = {
+ enabled: <%= Avo::MediaLibrary.configuration.enabled %>,
+ visible: <%= Avo::MediaLibrary.configuration.visible? %>
+ }
<% end %>
diff --git a/config/routes.rb b/config/routes.rb
index e5480b2117..41afeb9569 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -9,6 +9,9 @@
instance_exec(&Avo.mount_engines)
end
+ resources :media_library, only: [:index, :show, :update, :destroy], path: "media-library"
+ get "attach-media", to: "media_library#attach"
+
post "/rails/active_storage/direct_uploads", to: "/active_storage/direct_uploads#create"
scope "avo_api", as: "avo_api" do
diff --git a/lib/avo.rb b/lib/avo.rb
index 90e03ceb9d..2518696edb 100644
--- a/lib/avo.rb
+++ b/lib/avo.rb
@@ -17,10 +17,13 @@ module Avo
IN_DEVELOPMENT = ENV["AVO_IN_DEVELOPMENT"] == "1"
PACKED = !IN_DEVELOPMENT
COOKIES_KEY = "avo"
- MODAL_FRAME_ID = :modal_frame
- ACTIONS_BACKGROUND_FRAME = :actions_background
CACHED_SVGS = {}
+ # Frame IDs
+ MODAL_FRAME_ID = :modal_frame
+ MEDIA_LIBRARY_ITEM_DETAILS_FRAME_ID = :media_library_item_details
+ ACTIONS_BACKGROUND_FRAME_ID = :actions_background
+
class LicenseVerificationTemperedError < StandardError; end
class LicenseInvalidError < StandardError; end
diff --git a/lib/avo/media_library/configuration.rb b/lib/avo/media_library/configuration.rb
new file mode 100644
index 0000000000..5d796caca5
--- /dev/null
+++ b/lib/avo/media_library/configuration.rb
@@ -0,0 +1,22 @@
+module Avo
+ module MediaLibrary
+ class Configuration
+ include ActiveSupport::Configurable
+
+ config_accessor(:visible) { true }
+ config_accessor(:enabled) { false }
+
+ def visible?
+ Avo::ExecutionContext.new(target: config[:visible]).handle
+ end
+ end
+
+ def self.configuration
+ @configuration ||= Configuration.new
+ end
+
+ def self.configure
+ yield configuration
+ end
+ end
+end
diff --git a/lib/generators/avo/templates/locales/avo.ar.yml b/lib/generators/avo/templates/locales/avo.ar.yml
index 9c132a9fc1..00c5b43005 100644
--- a/lib/generators/avo/templates/locales/avo.ar.yml
+++ b/lib/generators/avo/templates/locales/avo.ar.yml
@@ -28,6 +28,7 @@ ar:
choose_item: اختر %{item}
clear_value: مسح القيمة
click_to_reveal_filters: انقر لإظهار المرشحات
+ close: إغلاق
close_modal: أغلق النافذة
confirm: تأكيد
copy: نسخ
@@ -71,6 +72,8 @@ ar:
less_content: محتوى أقل
list_is_empty: القائمة فارغة
loading: جاري التحميل
+ media_library:
+ title: مكتبة الوسائط
more: المزيد
more_content: المزيد من المحتوى
more_records_available: هناك المزيد من السجلات المتاحة.
diff --git a/lib/generators/avo/templates/locales/avo.de.yml b/lib/generators/avo/templates/locales/avo.de.yml
index 7b470689e7..35ea2ff2c3 100644
--- a/lib/generators/avo/templates/locales/avo.de.yml
+++ b/lib/generators/avo/templates/locales/avo.de.yml
@@ -22,6 +22,7 @@ de:
choose_item: "%{item} auswählen"
clear_value: Wert löschen
click_to_reveal_filters: Klicken, um Filter anzuzeigen
+ close: Schließen
close_modal: Modal schließen
confirm: Bestätigen
copy: Kopieren
@@ -61,6 +62,8 @@ de:
less_content: Weniger Inhalt
list_is_empty: Liste ist leer
loading: Lade...
+ media_library:
+ title: Medienbibliothek
more: Mehr
more_content: Mehr Inhalt
more_records_available: Es sind weitere Datensätze verfügbar.
diff --git a/lib/generators/avo/templates/locales/avo.en.yml b/lib/generators/avo/templates/locales/avo.en.yml
index ca4b5e87e2..b6fa650c94 100644
--- a/lib/generators/avo/templates/locales/avo.en.yml
+++ b/lib/generators/avo/templates/locales/avo.en.yml
@@ -22,6 +22,7 @@ en:
choose_item: Choose %{item}
clear_value: Clear value
click_to_reveal_filters: Click to reveal filters
+ close: Close
close_modal: Close modal
confirm: Confirm
copy: Copy
@@ -61,6 +62,8 @@ en:
less_content: Less content
list_is_empty: List is empty
loading: Loading
+ media_library:
+ title: Media Library
more: More
more_content: More content
more_records_available: There are more records available.
diff --git a/lib/generators/avo/templates/locales/avo.es.yml b/lib/generators/avo/templates/locales/avo.es.yml
index 5b8f4f968e..3b26bfa1ef 100644
--- a/lib/generators/avo/templates/locales/avo.es.yml
+++ b/lib/generators/avo/templates/locales/avo.es.yml
@@ -24,6 +24,7 @@ es:
choose_item: Elige %{item}
clear_value: Borrar el valor
click_to_reveal_filters: Pulsa para mostrar los filtros
+ close: Cerrar
close_modal: Cerrar modal
confirm: Confirmar
copy: Copiar
@@ -63,6 +64,8 @@ es:
less_content: Menos contenido
list_is_empty: La lista está vacía
loading: Cargando
+ media_library:
+ title: Biblioteca de medios
more: Más
more_content: Más contenido
more_records_available: Hay más registros disponibles.
diff --git a/lib/generators/avo/templates/locales/avo.fr.yml b/lib/generators/avo/templates/locales/avo.fr.yml
index 469734a976..64cf8360c8 100644
--- a/lib/generators/avo/templates/locales/avo.fr.yml
+++ b/lib/generators/avo/templates/locales/avo.fr.yml
@@ -24,6 +24,7 @@ fr:
choose_item: Choisir %{item}
clear_value: Effacer la valeur
click_to_reveal_filters: Cliquez pour révéler les filtres
+ close: Fermer
close_modal: Fermer la fenêtre modale
confirm: Confirmer
copy: Copier
@@ -63,6 +64,8 @@ fr:
less_content: Moins de contenu
list_is_empty: La liste est vide
loading: Chargement
+ media_library:
+ title: Bibliothèque multimédia
more: Plus
more_content: Plus de contenu
more_records_available: Il y a plus d'enregistrements disponibles.
diff --git a/lib/generators/avo/templates/locales/avo.it.yml b/lib/generators/avo/templates/locales/avo.it.yml
index 4948c695b3..088ab48029 100644
--- a/lib/generators/avo/templates/locales/avo.it.yml
+++ b/lib/generators/avo/templates/locales/avo.it.yml
@@ -22,6 +22,7 @@ it:
choose_item: Scegli %{item}
clear_value: Cancella valore
click_to_reveal_filters: Clicca per mostrare i filtri
+ close: Chiudi
close_modal: Chiudi modale
confirm: Conferma
copy: Copia
@@ -61,6 +62,8 @@ it:
less_content: Meno contenuti
list_is_empty: La lista è vuota
loading: Caricamento in corso
+ media_library:
+ title: Libreria multimediale
more: Altro
more_content: Più contenuti
more_records_available: Sono disponibili più record.
diff --git a/lib/generators/avo/templates/locales/avo.ja.yml b/lib/generators/avo/templates/locales/avo.ja.yml
index 29503a65c4..8b6516a595 100644
--- a/lib/generators/avo/templates/locales/avo.ja.yml
+++ b/lib/generators/avo/templates/locales/avo.ja.yml
@@ -24,6 +24,7 @@ ja:
choose_item: "%{item}を選択"
clear_value: 値をクリア
click_to_reveal_filters: フィルターを表示するにはクリック
+ close: 閉じる
close_modal: モーダルを閉じる
confirm: 確認
copy: コピー
@@ -63,6 +64,8 @@ ja:
less_content: コンテンツが少ない
list_is_empty: リストは空です
loading: 読み込み中
+ media_library:
+ title: メディアライブラリ
more: もっと
more_content: さらなるコンテンツ
more_records_available: さらに多くのレコードが利用可能です。
diff --git a/lib/generators/avo/templates/locales/avo.nb.yml b/lib/generators/avo/templates/locales/avo.nb.yml
index 804688b0ef..3b5f59bb02 100644
--- a/lib/generators/avo/templates/locales/avo.nb.yml
+++ b/lib/generators/avo/templates/locales/avo.nb.yml
@@ -24,6 +24,7 @@ nb:
choose_item: Velge %{item}
clear_value: Nullstill verdi
click_to_reveal_filters: Vis filter
+ close: Lukk
close_modal: Lukk modal
confirm: Bekreft
copy: Kopier
@@ -63,6 +64,8 @@ nb:
less_content: Mindre innhold
list_is_empty: Listen er tom
loading: Laster
+ media_library:
+ title: Mediebibliotek
more: Mer
more_content: Mer innhold
more_records_available: Det er flere poster tilgjengelig.
diff --git a/lib/generators/avo/templates/locales/avo.nl.yml b/lib/generators/avo/templates/locales/avo.nl.yml
index 3cf83b2f0a..dcaf35263c 100644
--- a/lib/generators/avo/templates/locales/avo.nl.yml
+++ b/lib/generators/avo/templates/locales/avo.nl.yml
@@ -22,6 +22,7 @@ nl:
choose_item: Kies %{item}
clear_value: Waarde wissen
click_to_reveal_filters: Klik om filters te tonen
+ close: Sluiten
close_modal: Modaal sluiten
confirm: Bevestigen
copy: Kopie
@@ -61,6 +62,8 @@ nl:
less_content: Minder inhoud
list_is_empty: Lijst is leeg
loading: Laden...
+ media_library:
+ title: Mediabibliotheek
more: Meer
more_content: Meer inhoud
more_records_available: Er zijn meer records beschikbaar.
diff --git a/lib/generators/avo/templates/locales/avo.nn.yml b/lib/generators/avo/templates/locales/avo.nn.yml
index 91006ffc43..e5b9098427 100644
--- a/lib/generators/avo/templates/locales/avo.nn.yml
+++ b/lib/generators/avo/templates/locales/avo.nn.yml
@@ -24,6 +24,7 @@ nn:
choose_item: Vel %{item}
clear_value: Nullstill verdi
click_to_reveal_filters: Vis filter
+ close: Lukk
close_modal: Lukk modal
confirm: Stadfest
copy: Kopier
@@ -63,6 +64,8 @@ nn:
less_content: Mindre innhold
list_is_empty: Lista er tom
loading: Lastar
+ media_library:
+ title: Media bibliotek
more: Meir
more_content: Mer innhold
more_records_available: Det finst fleire opptak tilgjengelege.
diff --git a/lib/generators/avo/templates/locales/avo.pl.yml b/lib/generators/avo/templates/locales/avo.pl.yml
index 225c5e1d8a..4f3a0189da 100644
--- a/lib/generators/avo/templates/locales/avo.pl.yml
+++ b/lib/generators/avo/templates/locales/avo.pl.yml
@@ -22,6 +22,7 @@ pl:
choose_item: Wybierz %{item}
clear_value: Wyczyść wartość
click_to_reveal_filters: Kliknij, aby pokazać filtry
+ close: Zamknij
close_modal: Zamknij okno modalne
confirm: Potwierdź
copy: Kopiuj
@@ -63,6 +64,8 @@ pl:
less_content: Pokaż mniej
list_is_empty: Lista jest pusta
loading: Ładowanie
+ media_library:
+ title: Biblioteka mediów
more: Więcej
more_content: Pokaż wiecej
more_records_available: Dostępnych jest więcej rekordów.
diff --git a/lib/generators/avo/templates/locales/avo.pt-BR.yml b/lib/generators/avo/templates/locales/avo.pt-BR.yml
index 6fc43325d4..82d220bb10 100644
--- a/lib/generators/avo/templates/locales/avo.pt-BR.yml
+++ b/lib/generators/avo/templates/locales/avo.pt-BR.yml
@@ -24,6 +24,7 @@ pt-BR:
choose_item: Escolher %{item}
clear_value: Limpar valor
click_to_reveal_filters: Clique para revelar os filtros
+ close: Fechar
close_modal: Fechar modal
confirm: Confirmar
copy: Copiar
@@ -63,6 +64,8 @@ pt-BR:
less_content: Menos conteúdo
list_is_empty: Lista vazia
loading: Carregando
+ media_library:
+ title: Biblioteca de Mídia
more: Mais
more_content: Mais conteúdo
more_records_available: Existem mais registros disponíveis.
diff --git a/lib/generators/avo/templates/locales/avo.pt.yml b/lib/generators/avo/templates/locales/avo.pt.yml
index 1af99f480c..bb163a138d 100644
--- a/lib/generators/avo/templates/locales/avo.pt.yml
+++ b/lib/generators/avo/templates/locales/avo.pt.yml
@@ -24,6 +24,7 @@ pt:
choose_item: Escolher %{item}
clear_value: Apagar valor
click_to_reveal_filters: Clique para mostrar os filtros
+ close: Fechar
close_modal: Fechar modal
confirm: Confirmar
copy: Copiar
@@ -63,6 +64,8 @@ pt:
less_content: Menos conteúdo
list_is_empty: Lista vazia
loading: A carregar
+ media_library:
+ title: Biblioteca de Mídia
more: Mais
more_content: Mais conteúdo
more_records_available: Existem mais registos disponíveis.
diff --git a/lib/generators/avo/templates/locales/avo.ro.yml b/lib/generators/avo/templates/locales/avo.ro.yml
index 0e55df44f2..027d0fff1d 100644
--- a/lib/generators/avo/templates/locales/avo.ro.yml
+++ b/lib/generators/avo/templates/locales/avo.ro.yml
@@ -25,6 +25,7 @@ ro:
choose_item: Alege %{item}
clear_value: Șterge valoarea
click_to_reveal_filters: Faceți clic pentru a afișa filtrele
+ close: Închide
close_modal: Închide modalul
confirm: Confirm
copy: Copiază
@@ -65,6 +66,8 @@ ro:
less_content: Mai puțin conținut
list_is_empty: Lista este goală
loading: Se incarcă
+ media_library:
+ title: Bibliotecă media
more: Mai multe
more_content: Mai mult conținut
more_records_available: Sunt mai multe înregistrări disponibile.
diff --git a/lib/generators/avo/templates/locales/avo.ru.yml b/lib/generators/avo/templates/locales/avo.ru.yml
index 4176255b50..f939543240 100644
--- a/lib/generators/avo/templates/locales/avo.ru.yml
+++ b/lib/generators/avo/templates/locales/avo.ru.yml
@@ -22,6 +22,7 @@ ru:
choose_item: Выбрать %{item}
clear_value: Очистить значение
click_to_reveal_filters: Нажмите, чтобы открыть фильтры
+ close: Закрыть
close_modal: Закрыть модальное окно
confirm: Подтвердить
copy: Копировать
@@ -63,6 +64,8 @@ ru:
less_content: Меньше контента
list_is_empty: Список пуст
loading: Загрузка...
+ media_library:
+ title: Медиатека
more: Ещё
more_content: Больше контента
more_records_available: Доступно больше записей.
diff --git a/lib/generators/avo/templates/locales/avo.tr.yml b/lib/generators/avo/templates/locales/avo.tr.yml
index 85736fe6cc..edfcc498cb 100644
--- a/lib/generators/avo/templates/locales/avo.tr.yml
+++ b/lib/generators/avo/templates/locales/avo.tr.yml
@@ -24,6 +24,7 @@ tr:
choose_item: "%{item} seçin"
clear_value: Değeri temizle
click_to_reveal_filters: Filtreleri ortaya çıkarmak için tıklayın
+ close: Kapat
close_modal: Modali kapat
confirm: Onayla
copy: Kopya
@@ -63,6 +64,8 @@ tr:
less_content: Daha az içerik
list_is_empty: Boş liste
loading: Yükleniyor
+ media_library:
+ title: Ortam Kütüphanesi
more: Daha fazla
more_content: Daha fazla içerik
more_records_available: Daha fazla kayıt mevcut.
diff --git a/lib/generators/avo/templates/locales/avo.uk.yml b/lib/generators/avo/templates/locales/avo.uk.yml
index f3bbb8f73f..4fdc4a0724 100644
--- a/lib/generators/avo/templates/locales/avo.uk.yml
+++ b/lib/generators/avo/templates/locales/avo.uk.yml
@@ -22,6 +22,7 @@ uk:
choose_item: Виберіть %{item}
clear_value: Очистити значення
click_to_reveal_filters: Натисніть, щоб показати фільтри
+ close: Закрити
close_modal: Закрити вікно
confirm: Підтвердити
copy: Копіювати
@@ -63,6 +64,8 @@ uk:
less_content: Менше вмісту
list_is_empty: Список порожній
loading: Завантаження
+ media_library:
+ title: Медіатека
more: Ще
more_content: Більше вмісту
more_records_available: Є більше доступних записів.
diff --git a/lib/generators/avo/templates/locales/avo.zh.yml b/lib/generators/avo/templates/locales/avo.zh.yml
index 9e6b8b4070..5910022bc0 100644
--- a/lib/generators/avo/templates/locales/avo.zh.yml
+++ b/lib/generators/avo/templates/locales/avo.zh.yml
@@ -22,6 +22,7 @@ zh:
choose_item: 选择 %{item}
clear_value: 清除值
click_to_reveal_filters: 点击以显示筛选器
+ close: 关闭
close_modal: 关闭模态框
confirm: 确认
copy: 复制
@@ -61,6 +62,8 @@ zh:
less_content: 内容较少
list_is_empty: 列表为空
loading: 加载中...
+ media_library:
+ title: 媒体库
more: 更多
more_content: 更多内容
more_records_available: 还有更多记录可供参考。
diff --git a/package.json b/package.json
index 889ba15e2e..31a99407fc 100644
--- a/package.json
+++ b/package.json
@@ -27,6 +27,7 @@
"@rails/activestorage": "^6.1.710",
"@stimulus-components/clipboard": "^5.0.0",
"@stimulus-components/password-visibility": "^3.0.0",
+ "@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/typography": "^0.5.16",
"@tiptap/core": "^2.11.3",
diff --git a/spec/dummy/app/views/avo/resource_tools/_fish_information.html.erb b/spec/dummy/app/views/avo/resource_tools/_fish_information.html.erb
index 591cbc49c7..f75219d79b 100644
--- a/spec/dummy/app/views/avo/resource_tools/_fish_information.html.erb
+++ b/spec/dummy/app/views/avo/resource_tools/_fish_information.html.erb
@@ -8,10 +8,10 @@
<% if form.present? %>
<% c.with_body do %>
-
- <%= avo_edit_field :fish_type, as: :text, form: form, help: "Set the fish type", required: true, component_options: {compact: false, stacked: false} %>
- <%= avo_edit_field :properties, as: :text, form: form, name: "Property 1", help: "Prop 1", required: true, component_options: {multiple: true} %>
-
+
+ <%= avo_edit_field :fish_type, as: :text, form: form, help: "Set the fish type", required: true, component_options: {compact: false, stacked: false} %>
+ <%= avo_edit_field :properties, as: :text, form: form, name: "Property 1", help: "Prop 1", required: true, component_options: {multiple: true} %>
+
<%= avo_edit_field :properties, as: :text, form: form, name: "Property 2", help: "Prop 2", required: true, component_options: {multiple: true} %>
<%= form.fields_for :information do |information_form| %>
diff --git a/spec/dummy/config/initializers/avo.rb b/spec/dummy/config/initializers/avo.rb
index fb438bf5c7..715a034407 100644
--- a/spec/dummy/config/initializers/avo.rb
+++ b/spec/dummy/config/initializers/avo.rb
@@ -109,6 +109,13 @@
end
end
+if defined?(Avo::MediaLibrary)
+ Avo::MediaLibrary.configure do |config|
+ config.visible = -> { Avo::Current.user.is_developer? }
+ config.enabled = true
+ end
+end
+
Rails.configuration.to_prepare do
Avo::Fields::BaseField.include ActionView::Helpers::UrlHelper
Avo::Fields::BaseField.include ActionView::Context
diff --git a/spec/dummy/config/locales/avo.en.yml b/spec/dummy/config/locales/avo.en.yml
index 94c1036381..5f22ca6f1f 100644
--- a/spec/dummy/config/locales/avo.en.yml
+++ b/spec/dummy/config/locales/avo.en.yml
@@ -1,6 +1,8 @@
---
en:
avo:
+ media_library:
+ title: Media Library
resource_translations:
product:
save: "Save the product!"
@@ -25,6 +27,7 @@ en:
clear_value: Clear value
click_to_reveal_filters: Click to reveal filters
close_modal: Close modal
+ close: Close
confirm: Confirm
create_new_item: Create new %{item}
dashboard: Dashboard
diff --git a/tailwind.preset.js b/tailwind.preset.js
index 4b1aeac104..419c35a2f2 100644
--- a/tailwind.preset.js
+++ b/tailwind.preset.js
@@ -32,6 +32,7 @@ module.exports = {
'cover-sm': '9/2',
'cover-md': '9/3',
'cover-lg': '9/4',
+ 'media-library-item': '4/3',
},
colors: {
blue,
@@ -132,7 +133,7 @@ module.exports = {
},
},
variants: {
- margin: ['responsive', 'hover', 'focus', 'group-hover', 'checked', 'kanban-dragging'],
+ margin: ['responsive', 'hover', 'focus', 'group-hover', 'checked', 'empty', 'kanban-dragging'],
display: ['responsive', 'hover', 'focus', 'group-hover', 'checked', 'kanban-dragging'],
padding: ['responsive', 'group-hover', 'kanban-dragging'],
borderColor: ['responsive', 'hover', 'focus', 'disabled'],
@@ -145,6 +146,7 @@ module.exports = {
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/typography'),
+ require('@tailwindcss/container-queries'),
plugin(({ addUtilities, addVariant }) => {
const newUtilities = {
'.backface-hidden': {
diff --git a/yarn.lock b/yarn.lock
index 1cadf718f9..3901b58d88 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1590,6 +1590,11 @@
resolved "https://registry.yarnpkg.com/@stimulus-components/password-visibility/-/password-visibility-3.0.0.tgz#314c1bae571ba13b9a4da6014f3e5c5b776fe4cc"
integrity sha512-ikyuZ/4VX4YrCckVHDc4H6UkTr5UjtkxSH+UdlEIgP9v93W1Bbvnoao0hgQODm8cvNWigmpLpKoGUzgyjBlrdw==
+"@tailwindcss/container-queries@^0.1.1":
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/@tailwindcss/container-queries/-/container-queries-0.1.1.tgz#9a759ce2cb8736a4c6a0cb93aeb740573a731974"
+ integrity sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA==
+
"@tailwindcss/forms@^0.5.10":
version "0.5.10"
resolved "https://registry.yarnpkg.com/@tailwindcss/forms/-/forms-0.5.10.tgz#0a1cd67b6933402f1985a04595bd24f9785aa302"
@@ -2099,25 +2104,10 @@ camelcase-css@^2.0.1:
resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5"
integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
-caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001464:
- version "1.0.30001486"
- resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001486.tgz#56a08885228edf62cbe1ac8980f2b5dae159997e"
- integrity sha512-uv7/gXuHi10Whlj0pp5q/tsK/32J2QSqVRKQhs2j8VsDCjgyruAh/eEXHF822VqO9yT6iZKw3nRwZRSPBE9OQg==
-
-caniuse-lite@^1.0.30001565:
- version "1.0.30001566"
- resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001566.tgz#61a8e17caf3752e3e426d4239c549ebbb37fef0d"
- integrity sha512-ggIhCsTxmITBAMmK8yZjEhCO5/47jKXPu6Dha/wuCS4JePVL+3uiDEBuhu2aIoT+bqTOR8L76Ip1ARL9xYsEJA==
-
-caniuse-lite@^1.0.30001646:
- version "1.0.30001650"
- resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001650.tgz#dd1eba0938e39536d184c3c99b2569a13788bc16"
- integrity sha512-fgEc7hP/LB7iicdXHUI9VsBsMZmUmlVJeQP2qqQW+3lkqVhbmjEU8zp+h5stWeilX+G7uXuIUIIlWlDw9jdt8g==
-
-caniuse-lite@^1.0.30001669:
- version "1.0.30001676"
- resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001676.tgz#fe133d41fe74af8f7cc93b8a714c3e86a86e6f04"
- integrity sha512-Qz6zwGCiPghQXGJvgQAem79esjitvJ+CxSbSQkW9H/UX5hg8XM88d4lp2W+MEQ81j+Hip58Il+jGVdazk1z9cw==
+caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001464, caniuse-lite@^1.0.30001565, caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001669:
+ version "1.0.30001696"
+ resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001696.tgz"
+ integrity sha512-pDCPkvzfa39ehJtJ+OwGT/2yvT2SbjfHhiIW2LWOAcMQ7BzwxT/XuyUp4OTOd0XFWA6BKw0JalnBHgSi5DGJBQ==
chalk@^2.4.2:
version "2.4.2"