lato_cms 3.2.1 → 3.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/assets/javascripts/lato_cms/controllers/lato_cms_media_picker_controller.js +10 -18
- data/app/assets/javascripts/lato_cms/controllers/lato_cms_upload_controller.js +91 -0
- data/app/controllers/lato_cms/media_controller.rb +53 -7
- data/app/helpers/lato_cms/media_helper.rb +27 -6
- data/app/helpers/lato_cms/pages_helper.rb +10 -1
- data/app/models/lato_cms/media.rb +24 -0
- data/app/views/lato_cms/media/_form_create.html.erb +16 -2
- data/app/views/lato_cms/media/_form_replace_file.html.erb +25 -0
- data/app/views/lato_cms/media/_form_update.html.erb +43 -31
- data/app/views/lato_cms/media/_preview.html.erb +19 -0
- data/app/views/lato_cms/media/_upload_progress.html.erb +8 -0
- data/app/views/lato_cms/media/_usages.html.erb +20 -0
- data/app/views/lato_cms/media/index.html.erb +8 -0
- data/app/views/lato_cms/media/picker_action.html.erb +7 -2
- data/app/views/lato_cms/media/show.html.erb +47 -0
- data/config/locales/en.yml +24 -0
- data/config/locales/it.yml +24 -0
- data/config/routes.rb +2 -0
- data/lib/lato_cms/version.rb +1 -1
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 25204dad4edacdca80789144382c513eb02da298f4bbad559552d3d4ad3616c1
|
|
4
|
+
data.tar.gz: 48de425da30efd01481cee27f556da6429049862a987d63087d645b156f85d78
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8db389eef91bc7ee627a5f91f57ae02872bcb7317706d6cdc89b9ff8040efbf1a6ab6a467681bb330e0cae415796885646afdeabbe87e9023ece8bc9beab91bc
|
|
7
|
+
data.tar.gz: 544be385fbb01838f1df124fb3582dc553f5bddbf2b18dbe1a7ff9ba4fb7d8a694bc8fb341473246d7327094140d191e002cf32d93f9e3d5dab0b545a2a49ace
|
|
@@ -6,7 +6,7 @@ import { Controller } from '@hotwired/stimulus'
|
|
|
6
6
|
// `lato-cms:media-selected` event on `document`, correlated by the picker's
|
|
7
7
|
// own turbo-frame id (unique per field instance, even inside a repeater).
|
|
8
8
|
export default class extends Controller {
|
|
9
|
-
static targets = ['grid', 'item', 'selectedCount', 'confirmButton', '
|
|
9
|
+
static targets = ['grid', 'item', 'selectedCount', 'confirmButton', 'uploadNotice']
|
|
10
10
|
static values = { multiple: Boolean, selectedLabel: String }
|
|
11
11
|
|
|
12
12
|
connect () {
|
|
@@ -41,22 +41,11 @@ export default class extends Controller {
|
|
|
41
41
|
this.closeModal()
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
method: 'POST',
|
|
50
|
-
headers: { Accept: 'application/json' },
|
|
51
|
-
body: new FormData(form)
|
|
52
|
-
})
|
|
53
|
-
const data = await response.json()
|
|
54
|
-
|
|
55
|
-
if (!response.ok) {
|
|
56
|
-
this.uploadNoticeTarget.innerHTML = `<div class="alert alert-danger">${Object.values(data).flat().join(', ')}</div>`
|
|
57
|
-
return
|
|
58
|
-
}
|
|
59
|
-
|
|
44
|
+
// Result of the XHR upload driven by `lato-cms-upload` (which owns the
|
|
45
|
+
// request and its progress bar); this only turns the created media into a
|
|
46
|
+
// selection.
|
|
47
|
+
uploadCompleted (event) {
|
|
48
|
+
const data = event.detail
|
|
60
49
|
const item = {
|
|
61
50
|
id: data.id,
|
|
62
51
|
name: data.name,
|
|
@@ -74,10 +63,13 @@ export default class extends Controller {
|
|
|
74
63
|
|
|
75
64
|
this.pending.set(item.id, item)
|
|
76
65
|
this.updateSelectionUi()
|
|
77
|
-
form.reset()
|
|
78
66
|
this.uploadNoticeTarget.innerHTML = `<div class="alert alert-success">${item.name}</div>`
|
|
79
67
|
}
|
|
80
68
|
|
|
69
|
+
uploadFailed (event) {
|
|
70
|
+
this.uploadNoticeTarget.innerHTML = `<div class="alert alert-danger">${event.detail.message}</div>`
|
|
71
|
+
}
|
|
72
|
+
|
|
81
73
|
buildItemFromElement (element) {
|
|
82
74
|
return {
|
|
83
75
|
id: element.dataset.mediaId,
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus'
|
|
2
|
+
|
|
3
|
+
// Submits a file form over XHR so the real upload progress can be shown.
|
|
4
|
+
// Neither of the two alternatives can: a plain form submit just freezes the
|
|
5
|
+
// page until the server answers, and `fetch` exposes no upload progress
|
|
6
|
+
// events at all (only download ones). Large videos made that gap obvious.
|
|
7
|
+
//
|
|
8
|
+
// On success it dispatches `lato-cms:upload-complete` on the form with the
|
|
9
|
+
// parsed JSON body (the media picker uses it to select the fresh media), or
|
|
10
|
+
// navigates to `redirectUrlValue` when the form is a standalone page.
|
|
11
|
+
export default class extends Controller {
|
|
12
|
+
static targets = ['progress', 'bar', 'label', 'submit', 'notice']
|
|
13
|
+
static values = { redirectUrl: String, processingLabel: String, confirm: String }
|
|
14
|
+
|
|
15
|
+
submit (event) {
|
|
16
|
+
// This handler owns the submission, so Turbo never sees it and
|
|
17
|
+
// `data-turbo-confirm` would never fire: the confirmation is asked here.
|
|
18
|
+
event.preventDefault()
|
|
19
|
+
if (this.confirmValue && !window.confirm(this.confirmValue)) return
|
|
20
|
+
|
|
21
|
+
const xhr = new window.XMLHttpRequest()
|
|
22
|
+
xhr.open('POST', this.element.action, true)
|
|
23
|
+
xhr.setRequestHeader('Accept', 'application/json')
|
|
24
|
+
xhr.upload.addEventListener('progress', (e) => {
|
|
25
|
+
if (e.lengthComputable) this.setProgress((e.loaded / e.total) * 100)
|
|
26
|
+
})
|
|
27
|
+
xhr.addEventListener('load', () => this.finish(xhr))
|
|
28
|
+
xhr.addEventListener('error', () => this.fail())
|
|
29
|
+
|
|
30
|
+
this.start()
|
|
31
|
+
xhr.send(new window.FormData(this.element))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
start () {
|
|
35
|
+
this.clearNotice()
|
|
36
|
+
if (this.hasSubmitTarget) this.submitTarget.disabled = true
|
|
37
|
+
if (this.hasProgressTarget) this.progressTarget.classList.remove('d-none')
|
|
38
|
+
this.setProgress(0)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Past 100% the bytes are in, but the server is still attaching and
|
|
42
|
+
// processing them: the bar stays full and animated, the label says so.
|
|
43
|
+
setProgress (percent) {
|
|
44
|
+
const value = Math.round(percent)
|
|
45
|
+
if (this.hasBarTarget) this.barTarget.style.width = `${value}%`
|
|
46
|
+
if (this.hasLabelTarget) this.labelTarget.textContent = value >= 100 ? this.processingLabelValue : `${value}%`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
finish (xhr) {
|
|
50
|
+
const data = this.parse(xhr.responseText)
|
|
51
|
+
|
|
52
|
+
if (xhr.status < 200 || xhr.status >= 300) {
|
|
53
|
+
this.fail(data)
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (this.redirectUrlValue) {
|
|
58
|
+
window.Turbo.visit(this.redirectUrlValue)
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
this.reset()
|
|
63
|
+
this.element.reset()
|
|
64
|
+
this.element.dispatchEvent(new CustomEvent('lato-cms:upload-complete', { bubbles: true, detail: data }))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
fail (data) {
|
|
68
|
+
this.reset()
|
|
69
|
+
const message = data ? Object.values(data).flat().join(', ') : ''
|
|
70
|
+
this.element.dispatchEvent(new CustomEvent('lato-cms:upload-error', { bubbles: true, detail: { message } }))
|
|
71
|
+
if (this.hasNoticeTarget) this.noticeTarget.innerHTML = `<div class="alert alert-danger">${message}</div>`
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
reset () {
|
|
75
|
+
if (this.hasSubmitTarget) this.submitTarget.disabled = false
|
|
76
|
+
if (this.hasProgressTarget) this.progressTarget.classList.add('d-none')
|
|
77
|
+
this.setProgress(0)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
clearNotice () {
|
|
81
|
+
if (this.hasNoticeTarget) this.noticeTarget.innerHTML = ''
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
parse (body) {
|
|
85
|
+
try {
|
|
86
|
+
return JSON.parse(body)
|
|
87
|
+
} catch (err) {
|
|
88
|
+
return null
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
module LatoCms
|
|
2
2
|
class MediaController < ApplicationController
|
|
3
|
-
ADMIN_ONLY_ACTIONS = %i[destroy_action].freeze
|
|
3
|
+
ADMIN_ONLY_ACTIONS = %i[replace_file_action destroy_action].freeze
|
|
4
4
|
|
|
5
5
|
before_action { active_sidebar(:lato_cms_media) }
|
|
6
6
|
before_action :authenticate_lato_cms_admin, only: ADMIN_ONLY_ACTIONS
|
|
7
7
|
|
|
8
8
|
def index
|
|
9
|
+
media = query_media
|
|
10
|
+
# "Unused" = no page field references it at all: the only media that can
|
|
11
|
+
# be deleted, so it's worth being able to list just those.
|
|
12
|
+
media = media.where.missing(:page_field_media) if params[:usage] == 'unused'
|
|
13
|
+
|
|
9
14
|
@media = lato_index_collection(
|
|
10
|
-
|
|
11
|
-
columns: %i[name media_type actions],
|
|
15
|
+
media.order(created_at: :desc),
|
|
16
|
+
columns: %i[name media_type usages actions],
|
|
12
17
|
sortable_columns: %i[name media_type created_at],
|
|
13
18
|
searchable_columns: %i[name alt_text title],
|
|
14
19
|
default_sort_by: 'created_at|DESC',
|
|
@@ -26,6 +31,13 @@ module LatoCms
|
|
|
26
31
|
@media = media.order(created_at: :desc).page(params[:page]).per(24)
|
|
27
32
|
end
|
|
28
33
|
|
|
34
|
+
# Read-only detail page: the media's own data plus where it's used. Kept
|
|
35
|
+
# apart from the edit form, which is a modal from the index and has no room
|
|
36
|
+
# for a usage list.
|
|
37
|
+
def show
|
|
38
|
+
@media = query_media.find(params[:id])
|
|
39
|
+
end
|
|
40
|
+
|
|
29
41
|
def create
|
|
30
42
|
@media = LatoCms::Media.new
|
|
31
43
|
end
|
|
@@ -35,6 +47,12 @@ module LatoCms
|
|
|
35
47
|
|
|
36
48
|
respond_to do |format|
|
|
37
49
|
if @media.save
|
|
50
|
+
# The JSON branch serves XHR uploads (progress bar, see
|
|
51
|
+
# lato_cms_upload_controller.js). `notify` is sent only by forms that
|
|
52
|
+
# navigate somewhere afterwards, so the flash lands on that page; the
|
|
53
|
+
# media picker uploads in place and asks for no flash.
|
|
54
|
+
flash[:notice] = t('lato_cms.media_created') if params[:notify].present?
|
|
55
|
+
|
|
38
56
|
format.html { redirect_to lato_cms.media_path, notice: t('lato_cms.media_created') }
|
|
39
57
|
format.json { render json: @media }
|
|
40
58
|
else
|
|
@@ -89,6 +107,34 @@ module LatoCms
|
|
|
89
107
|
end
|
|
90
108
|
end
|
|
91
109
|
|
|
110
|
+
# Replaces the file of an existing media in place, so every page already
|
|
111
|
+
# using it picks up the new file. Kept out of `update_action` (and off
|
|
112
|
+
# `update_params`) on purpose: this is a destructive, admin-only edit to
|
|
113
|
+
# every usage at once, not a metadata change.
|
|
114
|
+
def replace_file_action
|
|
115
|
+
@media = query_media.find(params[:id])
|
|
116
|
+
file = params.dig(:media, :file)
|
|
117
|
+
|
|
118
|
+
respond_to do |format|
|
|
119
|
+
if file.present? && @media.replace_file!(file)
|
|
120
|
+
format.html { redirect_to lato_cms.media_show_path(@media), notice: t('lato_cms.media_file_replaced') }
|
|
121
|
+
format.json { render json: @media }
|
|
122
|
+
else
|
|
123
|
+
message = t('lato_cms.media_file_replace_failed')
|
|
124
|
+
format.html { redirect_to lato_cms.media_update_path(@media), alert: message }
|
|
125
|
+
format.json { render json: { error: message }, status: :unprocessable_entity }
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
rescue StandardError => e
|
|
129
|
+
Rails.logger.error("LatoCms: failed to replace file for media #{params[:id]}: #{e.message}")
|
|
130
|
+
message = t('lato_cms.media_file_replace_failed')
|
|
131
|
+
|
|
132
|
+
respond_to do |format|
|
|
133
|
+
format.html { redirect_to lato_cms.media_update_path(@media), alert: message }
|
|
134
|
+
format.json { render json: { error: message }, status: :unprocessable_entity }
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
92
138
|
def destroy_action
|
|
93
139
|
@media = query_media.find(params[:id])
|
|
94
140
|
in_use = @media.usage_count.positive?
|
|
@@ -112,10 +158,10 @@ module LatoCms
|
|
|
112
158
|
params.require(:media).permit(:file, :name, :alt_text, :title)
|
|
113
159
|
end
|
|
114
160
|
|
|
115
|
-
# :file is intentionally not permitted here:
|
|
116
|
-
#
|
|
117
|
-
#
|
|
118
|
-
#
|
|
161
|
+
# :file is intentionally not permitted here: a media can be reused by many
|
|
162
|
+
# fields across many pages, so swapping its file silently changes what
|
|
163
|
+
# renders everywhere it's referenced. That swap is possible, but only
|
|
164
|
+
# through the explicit, admin-only `replace_file_action`.
|
|
119
165
|
def update_params
|
|
120
166
|
translation_keys = LatoCms::Media::TRANSLATABLE_ATTRIBUTES.product(LatoCms.config.locales).map { |attribute, locale| :"#{attribute}_#{locale}" }
|
|
121
167
|
params.require(:media).permit(:name, *translation_keys)
|
|
@@ -30,21 +30,42 @@ module LatoCms
|
|
|
30
30
|
end
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
# Index usages cell: how many page fields reference this media, so the
|
|
34
|
+
# admin sees at a glance which files are actually in use (the detail is in
|
|
35
|
+
# the edit form, which lists the pages).
|
|
36
|
+
def lato_cms_media_usages(media)
|
|
37
|
+
count = media.usage_count
|
|
38
|
+
return content_tag(:span, t('lato_cms.media_usages_none'), class: 'text-muted small') if count.zero?
|
|
39
|
+
|
|
40
|
+
content_tag(:span, t('lato_cms.media_usages_count', count: count), class: 'badge bg-info text-dark')
|
|
41
|
+
end
|
|
42
|
+
|
|
33
43
|
def lato_cms_media_media_type(media)
|
|
34
44
|
content_tag(:span, media.media_type, class: 'badge bg-secondary')
|
|
35
45
|
end
|
|
36
46
|
|
|
37
|
-
# Index actions cell: edit metadata + delete (delete gated to
|
|
38
|
-
# convention as pages).
|
|
47
|
+
# Index actions cell: detail page + edit metadata + delete (delete gated to
|
|
48
|
+
# admins, same convention as pages). Delete is rendered inert while the
|
|
49
|
+
# media is still referenced: the action would be refused server-side
|
|
50
|
+
# anyway, so the button says why instead of failing after the fact.
|
|
39
51
|
def lato_cms_media_actions(media)
|
|
40
52
|
content_tag(:div, class: 'btn-group btn-group-sm') do
|
|
53
|
+
concat link_to(t('lato_cms.cta_show'), lato_cms.media_show_path(media), class: 'btn btn-primary')
|
|
41
54
|
concat link_to(t('lato_cms.cta_edit'), lato_cms.media_update_path(media), class: 'btn btn-secondary',
|
|
42
55
|
data: { lato_action_target: 'trigger', turbo_frame: dom_id(media, 'form'), action_title: t('lato_cms.media_update_title') })
|
|
43
|
-
if lato_cms_admin?
|
|
44
|
-
concat link_to(t('lato_cms.cta_delete'), lato_cms.media_destroy_action_path(media), class: 'btn btn-danger',
|
|
45
|
-
data: { turbo_method: 'DELETE', turbo_confirm: t('lato_cms.cta_delete_confirm') })
|
|
46
|
-
end
|
|
56
|
+
concat lato_cms_media_delete_action(media) if lato_cms_admin?
|
|
47
57
|
end
|
|
48
58
|
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def lato_cms_media_delete_action(media)
|
|
63
|
+
usage_count = media.usage_count
|
|
64
|
+
return link_to(t('lato_cms.cta_delete'), lato_cms.media_destroy_action_path(media), class: 'btn btn-danger',
|
|
65
|
+
data: { turbo_method: 'DELETE', turbo_confirm: t('lato_cms.cta_delete_confirm') }) if usage_count.zero?
|
|
66
|
+
|
|
67
|
+
content_tag(:span, t('lato_cms.cta_delete'), class: 'btn btn-danger disabled',
|
|
68
|
+
title: t('lato_cms.media_delete_in_use', count: usage_count), data: { controller: 'lato-tooltip' })
|
|
69
|
+
end
|
|
49
70
|
end
|
|
50
71
|
end
|
|
@@ -126,9 +126,18 @@ module LatoCms
|
|
|
126
126
|
multiple ? "#{name}[]" : name
|
|
127
127
|
end
|
|
128
128
|
|
|
129
|
+
# Sanitizes only: `parameterize` also downcased, which silently broke the
|
|
130
|
+
# repeater's "NEW_RECORD" placeholder. The repeater JS swaps that literal
|
|
131
|
+
# for a fresh uuid when an item is added (see
|
|
132
|
+
# lato_cms_repeater_controller#add), but it never reached the JS as
|
|
133
|
+
# "NEW_RECORD" — it arrived lowercased inside every dom id and, worse,
|
|
134
|
+
# inside each media field's picker frame id. Every item added in one
|
|
135
|
+
# session therefore shared those ids, and since the picker correlates its
|
|
136
|
+
# selection event by frame id, choosing a media for one item dropped it
|
|
137
|
+
# into all of them at once.
|
|
129
138
|
def lato_cms_field_dom_id(field_id, suffix = 'value', dom_id_prefix: nil)
|
|
130
139
|
base = dom_id_prefix.presence || "fields_#{field_id}"
|
|
131
|
-
"#{base}_#{suffix}".
|
|
140
|
+
"#{base}_#{suffix}".gsub(/[^a-zA-Z0-9_]/, '_')
|
|
132
141
|
end
|
|
133
142
|
|
|
134
143
|
private
|
|
@@ -78,6 +78,14 @@ module LatoCms
|
|
|
78
78
|
page_field_media.count
|
|
79
79
|
end
|
|
80
80
|
|
|
81
|
+
# Every page field referencing this media, grouped by page (sorted by page
|
|
82
|
+
# title). Drives the "used in" list in the admin: a media is shared, so
|
|
83
|
+
# both deleting it and replacing its file are edits to every page listed
|
|
84
|
+
# here, and the admin has to see that before doing either.
|
|
85
|
+
def usages
|
|
86
|
+
page_fields.includes(:page).group_by(&:page).sort_by { |page, _fields| page.title.to_s.downcase }
|
|
87
|
+
end
|
|
88
|
+
|
|
81
89
|
def filename
|
|
82
90
|
file.filename.to_s if file.attached?
|
|
83
91
|
end
|
|
@@ -119,6 +127,22 @@ module LatoCms
|
|
|
119
127
|
Rails.application.routes.url_helpers.rails_blob_path(poster_file, only_path: true) if poster_file.attached?
|
|
120
128
|
end
|
|
121
129
|
|
|
130
|
+
# Swaps the underlying file while keeping the same record, so every field
|
|
131
|
+
# already referencing this media renders the new file: the point of the
|
|
132
|
+
# action (replace a logo everywhere at once) and its danger at the same
|
|
133
|
+
# time. Active Storage purges the previous blob on attach, the stale video
|
|
134
|
+
# poster is dropped explicitly, and media_type is re-inferred since the
|
|
135
|
+
# new file can be of a different kind. Variants need no cleanup: they are
|
|
136
|
+
# derived from the blob, so the old ones die with it.
|
|
137
|
+
def replace_file!(new_file)
|
|
138
|
+
file.attach(new_file)
|
|
139
|
+
poster_file.purge if poster_file.attached?
|
|
140
|
+
update!(media_type: self.class.infer_media_type(file.content_type))
|
|
141
|
+
enqueue_poster_generation if video?
|
|
142
|
+
|
|
143
|
+
true
|
|
144
|
+
end
|
|
145
|
+
|
|
122
146
|
# Best effort: generates a poster from the video via Active Storage previews
|
|
123
147
|
# (ffmpeg). Any failure is logged, the video keeps working without a poster.
|
|
124
148
|
def generate_video_poster!
|
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
<% media ||= LatoCms::Media.new %>
|
|
2
2
|
|
|
3
3
|
<%= turbo_frame_tag dom_id(media, 'form') do %>
|
|
4
|
-
|
|
4
|
+
<%# Submitted over XHR (see lato-cms-upload) to show upload progress; the
|
|
5
|
+
`notify` flag asks the JSON branch to leave a flash for the page we
|
|
6
|
+
navigate to once the upload is done. %>
|
|
7
|
+
<%= form_with model: media, url: lato_cms.media_create_action_path, data: {
|
|
8
|
+
turbo_frame: '_self',
|
|
9
|
+
controller: 'lato-form lato-cms-upload',
|
|
10
|
+
action: 'submit->lato-cms-upload#submit',
|
|
11
|
+
lato_cms_upload_redirect_url_value: lato_cms.media_path,
|
|
12
|
+
lato_cms_upload_processing_label_value: t('lato_cms.media_upload_processing')
|
|
13
|
+
} do |form| %>
|
|
5
14
|
<%= lato_form_notices class: %w[mb-3] %>
|
|
6
15
|
<%= lato_form_errors media, class: %w[mb-3] %>
|
|
16
|
+
<%= hidden_field_tag :notify, true %>
|
|
17
|
+
|
|
18
|
+
<div data-lato-cms-upload-target="notice"></div>
|
|
7
19
|
|
|
8
20
|
<%# Name/alt text are edited afterwards from this same list's "edit"
|
|
9
21
|
action — they default to the filename on upload, so there's nothing
|
|
@@ -13,8 +25,10 @@
|
|
|
13
25
|
<%= lato_form_item_input_file_dropzone form, :file %>
|
|
14
26
|
</div>
|
|
15
27
|
|
|
28
|
+
<%= render 'lato_cms/media/upload_progress' %>
|
|
29
|
+
|
|
16
30
|
<div class="d-flex justify-content-end">
|
|
17
|
-
<%= lato_form_submit form, t('lato_cms.media_create_cta'), class: %w[btn-success] %>
|
|
31
|
+
<%= lato_form_submit form, t('lato_cms.media_create_cta'), class: %w[btn-success], data: { lato_cms_upload_target: 'submit' } %>
|
|
18
32
|
</div>
|
|
19
33
|
<% end %>
|
|
20
34
|
<% end %>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<%# Swaps the file behind an existing media: every page using it renders the
|
|
2
|
+
new file. Deliberately not part of the metadata form (different action,
|
|
3
|
+
admin only, destructive across every usage). %>
|
|
4
|
+
<p class="text-muted small">
|
|
5
|
+
<%= t('lato_cms.media_replace_file_description', count: media.usage_count) %>
|
|
6
|
+
</p>
|
|
7
|
+
|
|
8
|
+
<%= form_with model: media, url: lato_cms.media_replace_file_action_path(media), method: :patch, data: {
|
|
9
|
+
controller: 'lato-cms-upload',
|
|
10
|
+
action: 'submit->lato-cms-upload#submit',
|
|
11
|
+
lato_cms_upload_redirect_url_value: lato_cms.media_show_path(media),
|
|
12
|
+
lato_cms_upload_processing_label_value: t('lato_cms.media_upload_processing'),
|
|
13
|
+
lato_cms_upload_confirm_value: t('lato_cms.media_replace_file_confirm')
|
|
14
|
+
} do |form| %>
|
|
15
|
+
<div data-lato-cms-upload-target="notice"></div>
|
|
16
|
+
|
|
17
|
+
<%= lato_form_item_input_file_dropzone form, :file %>
|
|
18
|
+
<%= render 'lato_cms/media/upload_progress' %>
|
|
19
|
+
|
|
20
|
+
<div class="d-flex justify-content-end">
|
|
21
|
+
<button type="submit" class="btn btn-warning" data-lato-cms-upload-target="submit">
|
|
22
|
+
<%= t('lato_cms.media_replace_file_cta') %>
|
|
23
|
+
</button>
|
|
24
|
+
</div>
|
|
25
|
+
<% end %>
|
|
@@ -1,42 +1,54 @@
|
|
|
1
1
|
<% media ||= LatoCms::Media.new %>
|
|
2
|
+
<% tab_id = dom_id(media, 'tab') %>
|
|
2
3
|
|
|
3
4
|
<%= turbo_frame_tag dom_id(media, 'form') do %>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
<%# Two tabs: the metadata form, and the file swap — same media, but editing
|
|
6
|
+
its name is a local change while replacing its file edits every page
|
|
7
|
+
using it, so they don't share a submit button. %>
|
|
8
|
+
<ul class="nav nav-tabs mb-3">
|
|
9
|
+
<li class="nav-item">
|
|
10
|
+
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#<%= tab_id %>_details" type="button">
|
|
11
|
+
<%= t('lato_cms.media_update_tab_details') %>
|
|
12
|
+
</button>
|
|
13
|
+
</li>
|
|
14
|
+
<% if lato_cms_admin? %>
|
|
15
|
+
<li class="nav-item">
|
|
16
|
+
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#<%= tab_id %>_replace" type="button">
|
|
17
|
+
<%= t('lato_cms.media_replace_file_title') %>
|
|
18
|
+
</button>
|
|
19
|
+
</li>
|
|
20
|
+
<% end %>
|
|
21
|
+
</ul>
|
|
7
22
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
<%=
|
|
13
|
-
class: 'd-block bg-light rounded overflow-hidden' do %>
|
|
14
|
-
<%= image_tag media.preview_url, class: 'd-block mx-auto', style: 'max-width: 100%; max-height: 420px;', alt: media.alt_text %>
|
|
15
|
-
<% end %>
|
|
16
|
-
<% elsif media.video? %>
|
|
17
|
-
<video controls preload="metadata" class="d-block w-100 rounded bg-black" style="max-height: 420px;" src="<%= media.url %>"></video>
|
|
18
|
-
<% end %>
|
|
23
|
+
<div class="tab-content">
|
|
24
|
+
<div class="tab-pane fade show active" id="<%= tab_id %>_details">
|
|
25
|
+
<%= form_with model: media, url: lato_cms.media_update_action_path(media), method: :patch, data: { turbo_frame: '_self', controller: 'lato-form lato-cms-reload-on-modal-close' } do |form| %>
|
|
26
|
+
<%= lato_form_notices class: %w[mb-3] %>
|
|
27
|
+
<%= lato_form_errors media, class: %w[mb-3] %>
|
|
19
28
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
29
|
+
<%= render 'lato_cms/media/preview', media: media %>
|
|
30
|
+
|
|
31
|
+
<div class="mb-3">
|
|
32
|
+
<%= lato_form_item_label form, :name %>
|
|
33
|
+
<%= lato_form_item_input_text form, :name, required: true %>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<% if media.image? %>
|
|
37
|
+
<%= render "lato_cms/media/translations_field", form: form, media: media, attribute: "alt_text" %>
|
|
23
38
|
<% end %>
|
|
24
|
-
|
|
25
|
-
</div>
|
|
26
|
-
</div>
|
|
39
|
+
<%= render "lato_cms/media/translations_field", form: form, media: media, attribute: "title" %>
|
|
27
40
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
41
|
+
<div class="d-flex justify-content-between align-items-center">
|
|
42
|
+
<%= link_to t('lato_cms.media_usages_show_cta'), lato_cms.media_show_path(media), class: 'btn btn-link px-0' %>
|
|
43
|
+
<%= lato_form_submit form, t('lato_cms.cta_update'), class: %w[btn-success] %>
|
|
44
|
+
</div>
|
|
45
|
+
<% end %>
|
|
31
46
|
</div>
|
|
32
47
|
|
|
33
|
-
<% if
|
|
34
|
-
|
|
48
|
+
<% if lato_cms_admin? %>
|
|
49
|
+
<div class="tab-pane fade" id="<%= tab_id %>_replace">
|
|
50
|
+
<%= render 'lato_cms/media/form_replace_file', media: media %>
|
|
51
|
+
</div>
|
|
35
52
|
<% end %>
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
<div class="d-flex justify-content-end">
|
|
39
|
-
<%= lato_form_submit form, t('lato_cms.cta_update'), class: %w[btn-success] %>
|
|
40
|
-
</div>
|
|
41
|
-
<% end %>
|
|
53
|
+
</div>
|
|
42
54
|
<% end %>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<%# Images and videos get a large view (the index thumbnail is too small to
|
|
2
|
+
judge a file by), everything else keeps the small type icon. %>
|
|
3
|
+
<div class="mb-3">
|
|
4
|
+
<% if media.image? %>
|
|
5
|
+
<%= link_to media.url, target: '_blank', rel: 'noopener', title: t('lato_cms.media_open_original'),
|
|
6
|
+
class: 'd-block bg-light rounded overflow-hidden' do %>
|
|
7
|
+
<%= image_tag media.preview_url, class: 'd-block mx-auto', style: 'max-width: 100%; max-height: 420px;', alt: media.alt_text %>
|
|
8
|
+
<% end %>
|
|
9
|
+
<% elsif media.video? %>
|
|
10
|
+
<video controls preload="metadata" class="d-block w-100 rounded bg-black" style="max-height: 420px;" src="<%= media.url %>"></video>
|
|
11
|
+
<% end %>
|
|
12
|
+
|
|
13
|
+
<div class="d-flex align-items-center gap-2 mt-2">
|
|
14
|
+
<% unless media.image? || media.video? %>
|
|
15
|
+
<%= lato_cms_media_thumb(media, size: 64) %>
|
|
16
|
+
<% end %>
|
|
17
|
+
<%= link_to media.filename, media.url, target: '_blank', rel: 'noopener', class: 'text-muted small' %>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<%# Progress UI driven by lato_cms_upload_controller.js: hidden until a
|
|
2
|
+
submit starts, then fed the real bytes-sent percentage. %>
|
|
3
|
+
<div class="d-none my-3" data-lato-cms-upload-target="progress">
|
|
4
|
+
<div class="progress" style="height: 6px;">
|
|
5
|
+
<div class="progress-bar progress-bar-striped progress-bar-animated" style="width: 0%;" role="progressbar" data-lato-cms-upload-target="bar"></div>
|
|
6
|
+
</div>
|
|
7
|
+
<div class="small text-muted mt-1" data-lato-cms-upload-target="label">0%</div>
|
|
8
|
+
</div>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<%# Where this media is actually rendered: a media is shared, so this is the
|
|
2
|
+
blast radius of replacing its file, and the reason deleting it can be
|
|
3
|
+
refused. %>
|
|
4
|
+
<% usages = media.usages %>
|
|
5
|
+
|
|
6
|
+
<% if usages.empty? %>
|
|
7
|
+
<p class="text-muted mb-0"><%= t('lato_cms.media_usages_empty') %></p>
|
|
8
|
+
<% else %>
|
|
9
|
+
<ul class="list-group list-group-flush">
|
|
10
|
+
<% usages.each do |page, fields| %>
|
|
11
|
+
<li class="list-group-item d-flex justify-content-between align-items-center gap-2 px-0">
|
|
12
|
+
<div>
|
|
13
|
+
<div><%= page.title %> <span class="badge bg-secondary ms-1"><%= page.locale.upcase %></span></div>
|
|
14
|
+
<div class="text-muted small"><%= fields.map(&:field_name).uniq.join(', ') %></div>
|
|
15
|
+
</div>
|
|
16
|
+
<%= link_to t('lato_cms.cta_show'), lato_cms.pages_show_path(page), class: 'btn btn-sm btn-outline-secondary' %>
|
|
17
|
+
</li>
|
|
18
|
+
<% end %>
|
|
19
|
+
</ul>
|
|
20
|
+
<% end %>
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
<%= lato_page_head t('lato_cms.media_index_title'), [{ label: t('lato_cms.media_index_title') }] %>
|
|
2
2
|
|
|
3
3
|
<div class="card">
|
|
4
|
+
<div class="card-header d-flex gap-2">
|
|
5
|
+
<%= link_to lato_cms.media_path, class: "btn btn-sm #{params[:usage].blank? ? 'btn-primary' : 'btn-outline-secondary'}" do %>
|
|
6
|
+
<i class="bi bi-images me-1"></i><%= t('lato_cms.media_filter_all') %>
|
|
7
|
+
<% end %>
|
|
8
|
+
<%= link_to lato_cms.media_path(usage: 'unused'), class: "btn btn-sm #{params[:usage] == 'unused' ? 'btn-primary' : 'btn-outline-secondary'}" do %>
|
|
9
|
+
<i class="bi bi-unlink me-1"></i><%= t('lato_cms.media_filter_unused') %>
|
|
10
|
+
<% end %>
|
|
11
|
+
</div>
|
|
4
12
|
<div class="card-body">
|
|
5
13
|
<%= lato_index @media,
|
|
6
14
|
custom_actions: {
|
|
@@ -84,12 +84,17 @@
|
|
|
84
84
|
ActiveRecord::Relation delegates an unrecognized `#name` call to
|
|
85
85
|
its model class, silently pre-filling a `:name` field with the
|
|
86
86
|
literal string "LatoCms::Media" (bit us once already here). %>
|
|
87
|
+
<%# Uploaded over XHR by `lato-cms-upload` (progress bar) instead of a
|
|
88
|
+
plain submit; the picker only reacts to the result events. %>
|
|
87
89
|
<%= form_with model: LatoCms::Media.new, url: lato_cms.media_create_action_path, data: {
|
|
88
|
-
|
|
90
|
+
controller: 'lato-cms-upload',
|
|
91
|
+
action: 'submit->lato-cms-upload#submit lato-cms:upload-complete->lato-cms-media-picker#uploadCompleted lato-cms:upload-error->lato-cms-media-picker#uploadFailed',
|
|
92
|
+
lato_cms_upload_processing_label_value: t('lato_cms.media_upload_processing')
|
|
89
93
|
} do |form| %>
|
|
90
94
|
<%= lato_form_item_input_file_dropzone form, :file, accept: { 'image' => 'image/*', 'video' => 'video/*' }[params[:type]] %>
|
|
95
|
+
<%= render 'lato_cms/media/upload_progress' %>
|
|
91
96
|
<div class="d-flex justify-content-end">
|
|
92
|
-
<button type="submit" class="btn btn-success"><%= t('lato_cms.media_upload_cta') %></button>
|
|
97
|
+
<button type="submit" class="btn btn-success" data-lato-cms-upload-target="submit"><%= t('lato_cms.media_upload_cta') %></button>
|
|
93
98
|
</div>
|
|
94
99
|
<% end %>
|
|
95
100
|
</div>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<%= lato_page_head @media.name, [
|
|
2
|
+
{ label: t('lato_cms.media_index_title'), path: lato_cms.media_path },
|
|
3
|
+
{ label: @media.name }
|
|
4
|
+
] %>
|
|
5
|
+
|
|
6
|
+
<div class="card mb-4">
|
|
7
|
+
<div class="card-header d-flex justify-content-between align-items-center gap-2">
|
|
8
|
+
<h2 class="fs-4 mb-0"><%= t('lato_cms.media_summary_title') %></h2>
|
|
9
|
+
<%= link_to t('lato_cms.cta_edit'), lato_cms.media_update_path(@media), class: 'btn btn-sm btn-secondary' %>
|
|
10
|
+
</div>
|
|
11
|
+
<div class="card-body">
|
|
12
|
+
<%= render 'lato_cms/media/preview', media: @media %>
|
|
13
|
+
|
|
14
|
+
<dl class="row mb-0">
|
|
15
|
+
<dt class="col-sm-3"><%= LatoCms::Media.human_attribute_name(:name) %></dt>
|
|
16
|
+
<dd class="col-sm-9"><%= @media.name %></dd>
|
|
17
|
+
|
|
18
|
+
<dt class="col-sm-3"><%= t('lato_cms.media_type_label') %></dt>
|
|
19
|
+
<dd class="col-sm-9"><%= lato_cms_media_media_type(@media) %></dd>
|
|
20
|
+
|
|
21
|
+
<%# Alt text is image-only, mirroring the edit form. Both are translatable:
|
|
22
|
+
one row per configured locale, blanks included, so a missing
|
|
23
|
+
translation is visible instead of just absent. %>
|
|
24
|
+
<% attributes = @media.image? ? LatoCms::Media::TRANSLATABLE_ATTRIBUTES : %w[title] %>
|
|
25
|
+
<% attributes.each do |attribute| %>
|
|
26
|
+
<dt class="col-sm-3"><%= t("lato_cms.media_#{attribute}") %></dt>
|
|
27
|
+
<dd class="col-sm-9">
|
|
28
|
+
<% LatoCms.config.locales.each do |locale| %>
|
|
29
|
+
<div class="d-flex gap-2">
|
|
30
|
+
<span class="badge bg-secondary align-self-start"><%= locale.to_s.upcase %></span>
|
|
31
|
+
<span><%= @media.public_send(attribute, locale).presence || content_tag(:span, '—', class: 'text-muted') %></span>
|
|
32
|
+
</div>
|
|
33
|
+
<% end %>
|
|
34
|
+
</dd>
|
|
35
|
+
<% end %>
|
|
36
|
+
</dl>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div class="card">
|
|
41
|
+
<div class="card-header">
|
|
42
|
+
<h2 class="fs-4 mb-0"><%= t('lato_cms.media_usages_title') %></h2>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="card-body">
|
|
45
|
+
<%= render 'lato_cms/media/usages', media: @media %>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
data/config/locales/en.yml
CHANGED
|
@@ -3,6 +3,8 @@ en:
|
|
|
3
3
|
attributes:
|
|
4
4
|
lato/user:
|
|
5
5
|
lato_cms_admin_role: Lato CMS role
|
|
6
|
+
lato_cms/media:
|
|
7
|
+
usages: Usages
|
|
6
8
|
lato_cms:
|
|
7
9
|
admin_roles:
|
|
8
10
|
none: No access
|
|
@@ -121,6 +123,28 @@ en:
|
|
|
121
123
|
media_picker_selected_label: selected
|
|
122
124
|
media_search_placeholder: Search media...
|
|
123
125
|
media_upload_cta: Upload
|
|
126
|
+
media_upload_processing: Processing...
|
|
127
|
+
media_usages_title: Used in
|
|
128
|
+
media_filter_all: All
|
|
129
|
+
media_filter_unused: Unused
|
|
130
|
+
media_summary_title: Media details
|
|
131
|
+
media_type_label: Type
|
|
132
|
+
media_update_tab_details: Details
|
|
133
|
+
media_usages_show_cta: See where it is used
|
|
134
|
+
media_usages_empty: This media is not used in any page yet.
|
|
135
|
+
media_usages_none: Unused
|
|
136
|
+
media_usages_count:
|
|
137
|
+
one: "%{count} usage"
|
|
138
|
+
other: "%{count} usages"
|
|
139
|
+
media_replace_file_title: Replace file
|
|
140
|
+
media_replace_file_description:
|
|
141
|
+
zero: "Uploads a new file in place of the current one, keeping the same media record. This media is not used yet."
|
|
142
|
+
one: "Uploads a new file in place of the current one, keeping the same media record: the %{count} page field using it will show the new file."
|
|
143
|
+
other: "Uploads a new file in place of the current one, keeping the same media record: the %{count} page fields using it will show the new file."
|
|
144
|
+
media_replace_file_cta: Replace file
|
|
145
|
+
media_replace_file_confirm: "The current file will be replaced everywhere this media is used. Continue?"
|
|
146
|
+
media_file_replaced: File replaced successfully
|
|
147
|
+
media_file_replace_failed: Failed to replace the file
|
|
124
148
|
media_type_all: All
|
|
125
149
|
media_type_image: Images
|
|
126
150
|
media_type_video: Videos
|
data/config/locales/it.yml
CHANGED
|
@@ -3,6 +3,8 @@ it:
|
|
|
3
3
|
attributes:
|
|
4
4
|
lato/user:
|
|
5
5
|
lato_cms_admin_role: Ruolo Lato CMS
|
|
6
|
+
lato_cms/media:
|
|
7
|
+
usages: Utilizzi
|
|
6
8
|
lato_cms:
|
|
7
9
|
admin_roles:
|
|
8
10
|
none: Nessun accesso
|
|
@@ -121,6 +123,28 @@ it:
|
|
|
121
123
|
media_picker_selected_label: selezionati
|
|
122
124
|
media_search_placeholder: Cerca media...
|
|
123
125
|
media_upload_cta: Carica
|
|
126
|
+
media_upload_processing: Elaborazione...
|
|
127
|
+
media_usages_title: Utilizzato in
|
|
128
|
+
media_filter_all: Tutti
|
|
129
|
+
media_filter_unused: Senza utilizzi
|
|
130
|
+
media_summary_title: Dettagli media
|
|
131
|
+
media_type_label: Tipo
|
|
132
|
+
media_update_tab_details: Dettagli
|
|
133
|
+
media_usages_show_cta: Vedi dove è utilizzato
|
|
134
|
+
media_usages_empty: Questo media non è ancora utilizzato in nessuna pagina.
|
|
135
|
+
media_usages_none: Non utilizzato
|
|
136
|
+
media_usages_count:
|
|
137
|
+
one: "%{count} utilizzo"
|
|
138
|
+
other: "%{count} utilizzi"
|
|
139
|
+
media_replace_file_title: Sostituisci file
|
|
140
|
+
media_replace_file_description:
|
|
141
|
+
zero: "Carica un nuovo file al posto di quello attuale, mantenendo lo stesso media. Questo media non è ancora utilizzato."
|
|
142
|
+
one: "Carica un nuovo file al posto di quello attuale, mantenendo lo stesso media: il %{count} campo che lo utilizza mostrerà il nuovo file."
|
|
143
|
+
other: "Carica un nuovo file al posto di quello attuale, mantenendo lo stesso media: i %{count} campi che lo utilizzano mostreranno il nuovo file."
|
|
144
|
+
media_replace_file_cta: Sostituisci file
|
|
145
|
+
media_replace_file_confirm: "Il file attuale verrà sostituito ovunque questo media sia utilizzato. Continuare?"
|
|
146
|
+
media_file_replaced: File sostituito con successo
|
|
147
|
+
media_file_replace_failed: Impossibile sostituire il file
|
|
124
148
|
media_type_all: Tutti
|
|
125
149
|
media_type_image: Immagini
|
|
126
150
|
media_type_video: Video
|
data/config/routes.rb
CHANGED
|
@@ -30,8 +30,10 @@ LatoCms::Engine.routes.draw do
|
|
|
30
30
|
get 'picker', to: 'media#picker_action', as: :media_picker_action
|
|
31
31
|
get 'create', to: 'media#create', as: :media_create
|
|
32
32
|
post 'create', to: 'media#create_action', as: :media_create_action
|
|
33
|
+
get ':id', to: 'media#show', as: :media_show
|
|
33
34
|
get ':id/update', to: 'media#update', as: :media_update
|
|
34
35
|
patch ':id/update', to: 'media#update_action', as: :media_update_action
|
|
36
|
+
patch ':id/replace-file', to: 'media#replace_file_action', as: :media_replace_file_action
|
|
35
37
|
post ':id/regenerate/:attribute', to: 'media#regenerate_text_action', as: :media_regenerate_text_action, constraints: { attribute: /alt_text|title/ }
|
|
36
38
|
delete ':id', to: 'media#destroy_action', as: :media_destroy_action
|
|
37
39
|
end
|
data/lib/lato_cms/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lato_cms
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.2.
|
|
4
|
+
version: 3.2.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gregorio Galante
|
|
@@ -118,6 +118,7 @@ files:
|
|
|
118
118
|
- app/assets/javascripts/lato_cms/controllers/lato_cms_reload_on_modal_close_controller.js
|
|
119
119
|
- app/assets/javascripts/lato_cms/controllers/lato_cms_repeater_controller.js
|
|
120
120
|
- app/assets/javascripts/lato_cms/controllers/lato_cms_text_field_controller.js
|
|
121
|
+
- app/assets/javascripts/lato_cms/controllers/lato_cms_upload_controller.js
|
|
121
122
|
- app/assets/stylesheets/lato_cms/application.scss
|
|
122
123
|
- app/controllers/lato_cms/api/pages_controller.rb
|
|
123
124
|
- app/controllers/lato_cms/application_controller.rb
|
|
@@ -139,11 +140,16 @@ files:
|
|
|
139
140
|
- app/models/lato_cms/page_field_media.rb
|
|
140
141
|
- app/models/lato_cms/template_manager.rb
|
|
141
142
|
- app/views/lato_cms/media/_form_create.html.erb
|
|
143
|
+
- app/views/lato_cms/media/_form_replace_file.html.erb
|
|
142
144
|
- app/views/lato_cms/media/_form_update.html.erb
|
|
145
|
+
- app/views/lato_cms/media/_preview.html.erb
|
|
143
146
|
- app/views/lato_cms/media/_translations_field.html.erb
|
|
147
|
+
- app/views/lato_cms/media/_upload_progress.html.erb
|
|
148
|
+
- app/views/lato_cms/media/_usages.html.erb
|
|
144
149
|
- app/views/lato_cms/media/create.html.erb
|
|
145
150
|
- app/views/lato_cms/media/index.html.erb
|
|
146
151
|
- app/views/lato_cms/media/picker_action.html.erb
|
|
152
|
+
- app/views/lato_cms/media/show.html.erb
|
|
147
153
|
- app/views/lato_cms/media/update.html.erb
|
|
148
154
|
- app/views/lato_cms/pages/_component_accordion.html.erb
|
|
149
155
|
- app/views/lato_cms/pages/_fields_editor.html.erb
|