lato_cms 3.0.9 → 3.0.11

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a2f231b3624de0d429fc3f73d0e659d0556543c9a657d54b939308b581ad53b4
4
- data.tar.gz: be81b76ce28bc8b5e72345ac640eb4279285a9e70b9bf345f80399f7e4773cfb
3
+ metadata.gz: 18323c23906137ac104d760d833ca76cd64041f254372a8a7e037bee23a5d5c5
4
+ data.tar.gz: 5324f43d1305e9aea41bcf19b2b41c96e555edd70ace601b788e649f5cb80b7d
5
5
  SHA512:
6
- metadata.gz: 28b6cc9fe5db40ff256bfed0055470a4c1d0ce1d22e9a601373832b6daac8b8215126555ab9e37d67ac58918e3b1d9c188cb0669716b7fb8370d8262b32c5276
7
- data.tar.gz: 7ec553cebf51a13d07823c77fbab3c06043365f30b14753ec86ff9d6847c386955d6536833b199754007e1f91073b0e353ef635e0eee7f87f19843e111ef410e
6
+ metadata.gz: d1c86cbe7a2a9dc66ffad24bb681f5a9022243c86f39ea4f2e8273c5500efec5f5ea20f610979a2f1bc49a4e85e96f1c5471faf00a5d42011b5df02da18cff5a
7
+ data.tar.gz: eebc417e788f0275c9b6daf1e50777487008358f8b080b75b48cab6f192f1cea94d70f177da537f881ee723b90cf58d365bdcf51f197dc2f9dc0c88c072c9919
@@ -1,9 +1,17 @@
1
1
  import { Controller } from '@hotwired/stimulus'
2
2
 
3
3
  export default class extends Controller {
4
- static targets = ['items', 'template', 'item', 'position', 'orderInput']
4
+ static targets = ['items', 'template', 'item', 'position', 'orderInput', 'addButton']
5
+ static values = { min: { type: Number, default: 0 }, max: { type: Number, default: 0 } }
6
+
7
+ connect () {
8
+ this.refresh()
9
+ }
5
10
 
6
11
  add () {
12
+ // Guard against exceeding max even if the button state is stale.
13
+ if (this.maxValue > 0 && this.itemTargets.length >= this.maxValue) return
14
+
7
15
  const itemId = window.crypto.randomUUID()
8
16
  const html = this.templateTarget.innerHTML
9
17
  .replaceAll('NEW_RECORD', itemId)
@@ -14,6 +22,9 @@ export default class extends Controller {
14
22
  }
15
23
 
16
24
  remove (event) {
25
+ // Block removal below the required minimum.
26
+ if (this.minValue > 0 && this.itemTargets.length <= this.minValue) return
27
+
17
28
  const item = event.currentTarget.closest('[data-lato-cms-repeater-target="item"]')
18
29
  if (!item) return
19
30
 
@@ -29,5 +40,22 @@ export default class extends Controller {
29
40
  const orderInput = item.querySelector('[data-lato-cms-repeater-target="orderInput"]')
30
41
  if (orderInput) orderInput.value = item.dataset.repeaterItemId
31
42
  })
43
+
44
+ this.updateControls()
45
+ }
46
+
47
+ // Toggle add/remove availability against the min/max constraints.
48
+ updateControls () {
49
+ const count = this.itemTargets.length
50
+
51
+ if (this.hasAddButtonTarget) {
52
+ this.addButtonTarget.disabled = this.maxValue > 0 && count >= this.maxValue
53
+ }
54
+
55
+ const disableRemove = this.minValue > 0 && count <= this.minValue
56
+ this.itemTargets.forEach((item) => {
57
+ const removeButton = item.querySelector('[data-action~="lato-cms-repeater#remove"]')
58
+ if (removeButton) removeButton.disabled = disableRemove
59
+ })
32
60
  }
33
61
  }
@@ -0,0 +1,164 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["input", "newPreview", "newPreviewContainer", "newPreviewName", "newPreviewSize", "currentContainer", "currentVideo", "removedNotice"]
5
+
6
+ connect() {
7
+ this.afterSave = this.afterSave.bind(this)
8
+ this.element.closest('form')?.addEventListener('lato-cms:fields-save-success', this.afterSave)
9
+ }
10
+
11
+ disconnect() {
12
+ this.element.closest('form')?.removeEventListener('lato-cms:fields-save-success', this.afterSave)
13
+ this.revokeObjectUrl()
14
+ }
15
+
16
+ preview() {
17
+ const file = this.inputTarget.files[0]
18
+ if (!file || !file.type.startsWith('video/')) return
19
+
20
+ // Object URL instead of FileReader: videos can be large and the browser
21
+ // streams from the original file without loading it in memory.
22
+ this.revokeObjectUrl()
23
+ this.objectUrl = URL.createObjectURL(file)
24
+ this.newPreviewTarget.src = this.objectUrl
25
+ this.newPreviewNameTarget.textContent = file.name
26
+ this.newPreviewSizeTarget.textContent = `(${this.humanSize(file.size)})`
27
+ this.newPreviewContainerTarget.classList.remove('d-none')
28
+ this.undo()
29
+ if (this.hasCurrentContainerTarget) this.currentContainerTarget.classList.add('d-none')
30
+ }
31
+
32
+ removeNew() {
33
+ this.inputTarget.value = ''
34
+ this.revokeObjectUrl()
35
+ this.newPreviewTarget.removeAttribute('src')
36
+ this.newPreviewNameTarget.textContent = ''
37
+ this.newPreviewSizeTarget.textContent = ''
38
+ this.newPreviewContainerTarget.classList.add('d-none')
39
+ if (this.hasCurrentContainerTarget) this.currentContainerTarget.classList.remove('d-none')
40
+ }
41
+
42
+ remove() {
43
+ if (!this.hasCurrentContainerTarget) return
44
+
45
+ this.currentContainerTarget.classList.add('lato-cms-file-field__item--removing')
46
+ this.removedNoticeTarget.classList.remove('d-none')
47
+ this.ensureRemoveInput()
48
+ }
49
+
50
+ undo() {
51
+ if (!this.hasCurrentContainerTarget) return
52
+
53
+ this.currentContainerTarget.classList.remove('lato-cms-file-field__item--removing')
54
+ this.removedNoticeTarget.classList.add('d-none')
55
+ this.removeInput?.remove()
56
+ this.removeInput = null
57
+ }
58
+
59
+ ensureRemoveInput() {
60
+ if (this.removeInput) return
61
+
62
+ const input = document.createElement('input')
63
+ input.type = 'hidden'
64
+ input.name = this.inputTarget.name.replace(/\[files\]\[\]$/, '[remove_file_ids][]')
65
+ input.value = this.currentContainerTarget.dataset.attachmentId
66
+ this.element.appendChild(input)
67
+ this.removeInput = input
68
+ }
69
+
70
+ afterSave(event) {
71
+ if (!event.detail.success) return
72
+
73
+ this.inputTarget.value = ''
74
+ this.removeInput = null
75
+ this.revokeObjectUrl()
76
+ const field = this.findSavedField(event.detail.data?.fields || [])
77
+ if (!field) return
78
+
79
+ const attachment = field.attachments[0]
80
+ this.element.querySelector('[data-lato-cms-video-field-target="currentContainer"]')?.remove()
81
+ this.newPreviewContainerTarget.classList.add('d-none')
82
+
83
+ if (attachment) {
84
+ this.element.insertBefore(this.buildCurrentContainer(attachment), this.newPreviewContainerTarget)
85
+ }
86
+ }
87
+
88
+ findSavedField(fields) {
89
+ return fields.find(field => {
90
+ return field.persisted_field_id === this.fieldId || field.field_id === this.fieldId
91
+ })
92
+ }
93
+
94
+ buildCurrentContainer(attachment) {
95
+ const container = document.createElement('div')
96
+ container.className = 'lato-cms-file-field__item lato-cms-video-field__item'
97
+ container.dataset.latoCmsVideoFieldTarget = 'currentContainer'
98
+ container.dataset.attachmentId = attachment.id
99
+ // poster_url is usually still null right after save: the poster is
100
+ // generated by a background job and shows up on the next page load.
101
+ const poster = attachment.poster_url ? ` poster="${attachment.poster_url}"` : ''
102
+ container.innerHTML = `
103
+ <div class="lato-cms-video-field__preview">
104
+ <video controls preload="metadata" class="lato-cms-video-field__player" src="${attachment.url}"${poster} data-lato-cms-video-field-target="currentVideo"></video>
105
+ </div>
106
+ <div class="lato-cms-file-field__info">
107
+ <i class="bi bi-film"></i>
108
+ <span>${this.escapeHtml(attachment.filename)}</span>
109
+ <span class="text-muted small">(${this.humanSize(attachment.byte_size || 0)})</span>
110
+ </div>
111
+ <button type="button" class="lato-cms-attachment-field__remove" title="${this.removeLabel}" aria-label="${this.removeLabel}" data-action="lato-cms-video-field#remove">
112
+ <i class="bi bi-trash"></i>
113
+ </button>
114
+ <div class="lato-cms-file-field__removed d-none" data-lato-cms-video-field-target="removedNotice">
115
+ <span class="text-danger small">
116
+ <i class="bi bi-trash me-1"></i>${this.removePendingLabel}
117
+ </span>
118
+ <button type="button" class="btn btn-link btn-sm p-0" data-action="lato-cms-video-field#undo">
119
+ ${this.undoLabel}
120
+ </button>
121
+ </div>
122
+ `
123
+ return container
124
+ }
125
+
126
+ revokeObjectUrl() {
127
+ if (!this.objectUrl) return
128
+
129
+ URL.revokeObjectURL(this.objectUrl)
130
+ this.objectUrl = null
131
+ }
132
+
133
+ humanSize(size) {
134
+ if (size < 1024) return `${size} B`
135
+ if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`
136
+ return `${(size / 1024 / 1024).toFixed(1)} MB`
137
+ }
138
+
139
+ escapeHtml(value) {
140
+ return value.replace(/[&<>"']/g, char => ({
141
+ '&': '&amp;',
142
+ '<': '&lt;',
143
+ '>': '&gt;',
144
+ '"': '&quot;',
145
+ "'": '&#39;'
146
+ }[char]))
147
+ }
148
+
149
+ get fieldId() {
150
+ return this.element.dataset.fieldId
151
+ }
152
+
153
+ get removeLabel() {
154
+ return this.element.dataset.removeLabel || 'Remove'
155
+ }
156
+
157
+ get removePendingLabel() {
158
+ return this.element.dataset.removePendingLabel || 'Video will be removed on save'
159
+ }
160
+
161
+ get undoLabel() {
162
+ return this.element.dataset.undoLabel || 'Undo'
163
+ }
164
+ }
@@ -268,6 +268,7 @@ body.controller-pages.action-show {
268
268
 
269
269
  .lato-cms-file-field__info,
270
270
  .lato-cms-image-field__preview,
271
+ .lato-cms-video-field__preview,
271
272
  .lato-cms-attachment-field__remove,
272
273
  > .btn {
273
274
  display: none;
@@ -339,6 +340,27 @@ body.controller-pages.action-show {
339
340
  font-size: 10px;
340
341
  }
341
342
 
343
+ // ── Video field ────────────────────────────────────────────────────────────────
344
+ .lato-cms-video-field__item {
345
+ align-items: stretch;
346
+ }
347
+
348
+ .lato-cms-video-field__preview {
349
+ align-self: center;
350
+ background: #000;
351
+ border: 1px solid rgba(33, 37, 41, 0.08);
352
+ border-radius: 6px;
353
+ flex: 0 0 auto;
354
+ overflow: hidden;
355
+ width: 180px;
356
+ }
357
+
358
+ .lato-cms-video-field__player {
359
+ display: block;
360
+ max-height: 110px;
361
+ width: 100%;
362
+ }
363
+
342
364
  // ── Gallery field ──────────────────────────────────────────────────────────────
343
365
  .lato-cms-gallery-field__grid {
344
366
  display: flex;
@@ -233,6 +233,19 @@ module LatoCms
233
233
  component_id = template_component[:component_id]
234
234
  item_ids = Array(repeater_order).reject(&:blank?).map(&:to_s)
235
235
 
236
+ # Enforce min/max item count (authoritative check). Missing settings = no constraint.
237
+ settings = template_component[:settings] || {}
238
+ min = settings['min'].presence && settings['min'].to_i
239
+ max = settings['max'].presence && settings['max'].to_i
240
+ if min && item_ids.size < min
241
+ errors << { field_id: template_component_id, errors: [t('lato_cms.repeater_min_error', count: min)] }
242
+ return
243
+ end
244
+ if max && item_ids.size > max
245
+ errors << { field_id: template_component_id, errors: [t('lato_cms.repeater_max_error', count: max)] }
246
+ return
247
+ end
248
+
236
249
  order_field = @page.fields.find_or_initialize_by(
237
250
  template_id: @page.template_id,
238
251
  template_component_id: template_component_id,
@@ -286,6 +299,9 @@ module LatoCms
286
299
  when 'image'
287
300
  field.save if field.new_record?
288
301
  replace_field_image(field, field_data)
302
+ when 'video'
303
+ field.save if field.new_record?
304
+ replace_field_video(field, field_data)
289
305
  when 'gallery'
290
306
  field.save if field.new_record?
291
307
  attach_field_files(field, field_data)
@@ -341,6 +357,22 @@ module LatoCms
341
357
  end
342
358
  end
343
359
 
360
+ # Single-video semantics: uploading a new video replaces both the previous
361
+ # video and its auto-generated poster; removing the video drops the poster
362
+ # too. Poster generation runs in a background job to keep the save fast.
363
+ def replace_field_video(field, field_data)
364
+ if field_data[:files].present?
365
+ new_file = Array(field_data[:files]).compact.first
366
+ return unless new_file
367
+
368
+ field.files.each(&:purge)
369
+ field.files.attach(new_file)
370
+ LatoCms::GenerateVideoPosterJob.perform_later(field.id)
371
+ elsif field_data[:remove_file_ids].present?
372
+ field.files.each(&:purge)
373
+ end
374
+ end
375
+
344
376
  def create_params
345
377
  params.require(:page).permit(:title, :locale)
346
378
  end
@@ -1,6 +1,6 @@
1
1
  module LatoCms
2
2
  module PagesHelper
3
- BUILTIN_FIELD_TYPES = %w[string textarea text number date datetime boolean select multiselect color json file image gallery].freeze
3
+ BUILTIN_FIELD_TYPES = %w[string textarea text number date datetime boolean select multiselect color json file image video gallery].freeze
4
4
 
5
5
  # Returns the path for an Active Storage attachment.
6
6
  # Using main_app avoids the missing `attachment_path` error inside the engine.
@@ -0,0 +1,12 @@
1
+ module LatoCms
2
+ # Generates the poster image for a video field in the background so the save
3
+ # request is not blocked by ffmpeg processing. Failures are swallowed by the
4
+ # model (logged as warnings): a missing poster must never break the field.
5
+ class GenerateVideoPosterJob < ApplicationJob
6
+ queue_as :default
7
+
8
+ def perform(page_field_id)
9
+ LatoCms::PageField.find_by(id: page_field_id)&.generate_video_poster!
10
+ end
11
+ end
12
+ end
@@ -75,6 +75,38 @@ module LatoCms
75
75
  field_config&.dig('settings') || {}
76
76
  end
77
77
 
78
+ # A video field stores two attachments in `files`: the video itself and an
79
+ # optional auto-generated poster image. Content type is the discriminator,
80
+ # so no extra column or naming convention is needed.
81
+ def video_attachment
82
+ files.find { |file| file.content_type.to_s.start_with?("video/") }
83
+ end
84
+
85
+ def poster_attachment
86
+ files.find { |file| file.content_type.to_s.start_with?("image/") }
87
+ end
88
+
89
+ # Generates a poster image from the video via Active Storage previews
90
+ # (ffmpeg) and attaches it alongside the video. Best effort: any failure is
91
+ # logged and the video keeps working without a poster.
92
+ def generate_video_poster!
93
+ video = video_attachment
94
+ return unless video
95
+ return if poster_attachment
96
+
97
+ unless video.previewable?
98
+ Rails.logger.warn("LatoCms: video preview unavailable (ffmpeg missing?) for attachment #{video.id}, skipping poster generation")
99
+ return
100
+ end
101
+
102
+ preview = video.preview(resize_to_limit: [1280, 720]).processed
103
+ preview.image.blob.open do |file|
104
+ files.attach(io: file, filename: "#{video.filename.base}_poster.jpg", content_type: preview.image.blob.content_type)
105
+ end
106
+ rescue StandardError => e
107
+ Rails.logger.warn("LatoCms: failed to generate video poster for field #{id}: #{e.message}")
108
+ end
109
+
78
110
  def as_json(_options = {})
79
111
  result = {
80
112
  id: id,
@@ -93,6 +125,9 @@ module LatoCms
93
125
  when 'image'
94
126
  attached = files.first
95
127
  result[:attachments] = attached ? [attachment_as_json(attached, with_variants: true)] : []
128
+ when 'video'
129
+ attached = video_attachment
130
+ result[:attachments] = attached ? [video_attachment_as_json(attached)] : []
96
131
  when 'gallery'
97
132
  order = value ? (JSON.parse(value) rescue []) : []
98
133
  all_files = files.to_a
@@ -119,6 +154,15 @@ module LatoCms
119
154
  json
120
155
  end
121
156
 
157
+ # Video attachment json gains a `poster_url` key (nil until the poster job
158
+ # runs) so API consumers get video and poster URLs in a single object.
159
+ def video_attachment_as_json(attachment)
160
+ poster = poster_attachment
161
+ attachment_as_json(attachment).merge(
162
+ poster_url: poster && Rails.application.routes.url_helpers.rails_blob_path(poster, only_path: true)
163
+ )
164
+ end
165
+
122
166
  # Builds a map of { size_name => variant_url } from the field's `settings.sizes`.
123
167
  # Variants are processed lazily by Active Storage on first request to their URL.
124
168
  def attachment_variant_urls(attachment)
@@ -2,9 +2,14 @@
2
2
  <% saved_order = begin; order_field&.value.present? ? JSON.parse(order_field.value) : []; rescue JSON::ParserError; []; end %>
3
3
  <% fields_by_item = component_fields.reject(&:repeater_order?).group_by(&:repeater_item_id) %>
4
4
  <% item_ids = (saved_order.presence || fields_by_item.keys.compact) %>
5
- <% item_ids = [SecureRandom.uuid] if item_ids.blank? && tc[:settings]['min'].to_i.positive? %>
5
+ <% min = tc[:settings]['min'].to_i %>
6
+ <% max = tc[:settings]['max'].to_i %>
7
+ <%# Seed `min` empty items when the list is empty, so the minimum is met up front. %>
8
+ <% item_ids = Array.new(min) { SecureRandom.uuid } if item_ids.blank? && min.positive? %>
6
9
 
7
- <div data-controller="lato-cms-repeater">
10
+ <div data-controller="lato-cms-repeater"
11
+ data-lato-cms-repeater-min-value="<%= min %>"
12
+ data-lato-cms-repeater-max-value="<%= max %>">
8
13
  <div data-lato-cms-repeater-target="items">
9
14
  <% item_ids.each_with_index do |item_id, item_index| %>
10
15
  <% item_fields = (fields_by_item[item_id] || []).index_by(&:base_field_id) %>
@@ -16,7 +21,7 @@
16
21
  <%= render 'lato_cms/pages/repeater_item', tc: tc, item_id: 'NEW_RECORD', item_index: 'NEW_INDEX', item_fields: {} %>
17
22
  </template>
18
23
 
19
- <button type="button" class="btn btn-sm btn-outline-primary" data-action="lato-cms-repeater#add">
24
+ <button type="button" class="btn btn-sm btn-outline-primary" data-action="lato-cms-repeater#add" data-lato-cms-repeater-target="addButton">
20
25
  <i class="bi bi-plus-lg me-1"></i><%= t('lato_cms.repeater_add_item') %>
21
26
  </button>
22
27
  </div>
@@ -0,0 +1,68 @@
1
+ <% label = field_config['name'] || field_id.humanize %>
2
+ <% required = field_config['required'] == true %>
3
+ <% settings = field_config['settings'] || {} %>
4
+ <% accept = settings['accept'] || 'video/*' %>
5
+ <% current_file = page_field&.video_attachment %>
6
+ <% poster_file = page_field&.poster_attachment %>
7
+ <% files_input_name = lato_cms_field_input_name(field_id, 'files', input_name_prefix: local_assigns[:input_name_prefix], multiple: true) %>
8
+ <% remove_input_name = lato_cms_field_input_name(field_id, 'remove_file_ids', input_name_prefix: local_assigns[:input_name_prefix], multiple: true) %>
9
+ <% input_id = lato_cms_field_dom_id(field_id, 'files', dom_id_prefix: local_assigns[:dom_id_prefix]) %>
10
+
11
+ <label class="form-label">
12
+ <%= label %><%= ' *' if required %>
13
+ </label>
14
+
15
+ <div data-controller="lato-cms-video-field"
16
+ data-field-id="<%= field_id %>"
17
+ data-new-badge-label="<%= t('lato_cms.field_video_new_badge') %>"
18
+ data-remove-label="<%= t('lato_cms.field_video_remove') %>"
19
+ data-remove-pending-label="<%= t('lato_cms.field_video_remove_pending') %>"
20
+ data-undo-label="<%= t('lato_cms.field_file_remove_undo') %>">
21
+ <% if current_file %>
22
+ <div class="lato-cms-file-field__item lato-cms-video-field__item" data-lato-cms-video-field-target="currentContainer" data-attachment-id="<%= current_file.id %>">
23
+ <div class="lato-cms-video-field__preview">
24
+ <video controls preload="metadata" class="lato-cms-video-field__player" src="<%= lato_cms_attachment_path(current_file) %>" <% if poster_file %>poster="<%= lato_cms_attachment_path(poster_file) %>"<% end %> data-lato-cms-video-field-target="currentVideo"></video>
25
+ </div>
26
+ <div class="lato-cms-file-field__info">
27
+ <i class="bi bi-film"></i>
28
+ <span><%= current_file.blob.filename %></span>
29
+ <span class="text-muted small">(<%= number_to_human_size(current_file.blob.byte_size) %>)</span>
30
+ </div>
31
+ <button type="button" class="lato-cms-attachment-field__remove" title="<%= t('lato_cms.field_video_remove') %>" aria-label="<%= t('lato_cms.field_video_remove') %>" data-action="lato-cms-video-field#remove">
32
+ <i class="bi bi-trash"></i>
33
+ </button>
34
+ <div class="lato-cms-file-field__removed d-none" data-lato-cms-video-field-target="removedNotice">
35
+ <span class="text-danger small">
36
+ <i class="bi bi-trash me-1"></i><%= t('lato_cms.field_video_remove_pending') %>
37
+ </span>
38
+ <button type="button" class="btn btn-link btn-sm p-0" data-action="lato-cms-video-field#undo">
39
+ <%= t('lato_cms.field_file_remove_undo') %>
40
+ </button>
41
+ </div>
42
+ </div>
43
+ <% end %>
44
+
45
+ <div class="lato-cms-file-field__item lato-cms-file-field__item--new lato-cms-video-field__item d-none" data-lato-cms-video-field-target="newPreviewContainer">
46
+ <div class="lato-cms-video-field__preview">
47
+ <video controls preload="metadata" class="lato-cms-video-field__player" data-lato-cms-video-field-target="newPreview"></video>
48
+ </div>
49
+ <div class="lato-cms-file-field__info">
50
+ <i class="bi bi-film"></i>
51
+ <span data-lato-cms-video-field-target="newPreviewName"></span>
52
+ <span class="text-muted small" data-lato-cms-video-field-target="newPreviewSize"></span>
53
+ <span class="badge bg-success"><%= t('lato_cms.field_video_new_badge') %></span>
54
+ </div>
55
+ <button type="button" class="lato-cms-attachment-field__remove" title="<%= t('lato_cms.field_video_remove') %>" aria-label="<%= t('lato_cms.field_video_remove') %>" data-action="lato-cms-video-field#removeNew">
56
+ <i class="bi bi-trash"></i>
57
+ </button>
58
+ </div>
59
+
60
+ <input type="file"
61
+ class="form-control"
62
+ name="<%= files_input_name %>"
63
+ id="<%= input_id %>"
64
+ accept="<%= accept %>"
65
+ data-lato-cms-video-field-target="input"
66
+ data-action="change->lato-cms-video-field#preview"
67
+ <%= 'required' if required && !current_file %>>
68
+ </div>
@@ -80,6 +80,8 @@ en:
80
80
  fields_save_component: Save
81
81
  repeater_add_item: Add item
82
82
  repeater_remove_item: Remove
83
+ repeater_min_error: "At least %{count} items are required"
84
+ repeater_max_error: "At most %{count} items are allowed"
83
85
  fields_component_missing_alert_html: "Component <strong>%{component_id}</strong> not found. The component configuration may have been removed."
84
86
  fields_previously_saved_data: "Previously saved data:"
85
87
  field_file_remove: Remove
@@ -92,6 +94,9 @@ en:
92
94
  field_image_remove: Remove
93
95
  field_image_remove_pending: Image will be removed on save
94
96
  field_image_new_badge: New
97
+ field_video_remove: Remove
98
+ field_video_remove_pending: Video will be removed on save
99
+ field_video_new_badge: New
95
100
  field_multiselect_hint: Hold Ctrl/Cmd to select multiple
96
101
  field_select_placeholder: "-- Select --"
97
102
  field_text_toolbar_bold: Bold
@@ -80,6 +80,8 @@ it:
80
80
  fields_save_component: Salva
81
81
  repeater_add_item: Aggiungi elemento
82
82
  repeater_remove_item: Rimuovi
83
+ repeater_min_error: "Sono richiesti almeno %{count} elementi"
84
+ repeater_max_error: "Sono ammessi al massimo %{count} elementi"
83
85
  fields_component_missing_alert_html: "Componente <strong>%{component_id}</strong> non trovato. La configurazione del componente potrebbe essere stata rimossa."
84
86
  fields_previously_saved_data: "Dati salvati in precedenza:"
85
87
  field_file_remove: Rimuovi
@@ -92,6 +94,9 @@ it:
92
94
  field_image_remove: Rimuovi
93
95
  field_image_remove_pending: L'immagine verra rimossa al salvataggio
94
96
  field_image_new_badge: Nuovo
97
+ field_video_remove: Rimuovi
98
+ field_video_remove_pending: Il video verra rimosso al salvataggio
99
+ field_video_new_badge: Nuovo
95
100
  field_multiselect_hint: Tieni premuto Ctrl/Cmd per selezionare piu opzioni
96
101
  field_select_placeholder: "-- Seleziona --"
97
102
  field_text_toolbar_bold: Grassetto
@@ -1,3 +1,3 @@
1
1
  module LatoCms
2
- VERSION = "3.0.9"
2
+ VERSION = "3.0.11"
3
3
  end
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.0.9
4
+ version: 3.0.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gregorio Galante
@@ -103,6 +103,7 @@ files:
103
103
  - app/assets/javascripts/lato_cms/controllers/lato_cms_page_preview_controller.js
104
104
  - app/assets/javascripts/lato_cms/controllers/lato_cms_repeater_controller.js
105
105
  - app/assets/javascripts/lato_cms/controllers/lato_cms_text_field_controller.js
106
+ - app/assets/javascripts/lato_cms/controllers/lato_cms_video_field_controller.js
106
107
  - app/assets/stylesheets/lato_cms/application.scss
107
108
  - app/controllers/lato_cms/api/pages_controller.rb
108
109
  - app/controllers/lato_cms/application_controller.rb
@@ -110,6 +111,7 @@ files:
110
111
  - app/helpers/lato_cms/application_helper.rb
111
112
  - app/helpers/lato_cms/pages_helper.rb
112
113
  - app/jobs/lato_cms/application_job.rb
114
+ - app/jobs/lato_cms/generate_video_poster_job.rb
113
115
  - app/mailers/lato_cms/application_mailer.rb
114
116
  - app/models/lato_cms/page.rb
115
117
  - app/models/lato_cms/page_field.rb
@@ -136,6 +138,7 @@ files:
136
138
  - app/views/lato_cms/pages/fields/_string.html.erb
137
139
  - app/views/lato_cms/pages/fields/_text.html.erb
138
140
  - app/views/lato_cms/pages/fields/_textarea.html.erb
141
+ - app/views/lato_cms/pages/fields/_video.html.erb
139
142
  - app/views/lato_cms/pages/index.html.erb
140
143
  - app/views/lato_cms/pages/show.html.erb
141
144
  - app/views/lato_cms/pages/translations.html.erb