spina 2.15.1 → 2.17.0

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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/spina/tailwind.css +39 -19
  3. data/app/assets/javascripts/spina/controllers/auto_file_upload_controller.js +49 -0
  4. data/app/assets/javascripts/spina/controllers/editor_insert_images_controller.js +38 -0
  5. data/app/assets/javascripts/spina/controllers/repeater_controller.js +4 -2
  6. data/app/assets/javascripts/spina/controllers/trix_controller.js +14 -2
  7. data/app/components/spina/forms/auto_file_upload_component.html.erb +6 -0
  8. data/app/components/spina/forms/auto_file_upload_component.rb +15 -0
  9. data/app/components/spina/forms/editor_insert_images_meta_component.html.erb +12 -0
  10. data/app/components/spina/forms/editor_insert_images_meta_component.rb +12 -0
  11. data/app/components/spina/forms/file_upload_component.html.erb +14 -0
  12. data/app/components/spina/forms/file_upload_component.rb +19 -0
  13. data/app/components/spina/forms/text_field_component.rb +3 -2
  14. data/app/components/spina/forms/trix_toolbar_component.html.erb +2 -2
  15. data/app/components/spina/media_picker/modal_component.html.erb +1 -11
  16. data/app/components/spina/pages/location_component.html.erb +6 -5
  17. data/app/components/spina/pages/page_select_component.rb +1 -1
  18. data/app/controllers/spina/admin/images_controller.rb +9 -2
  19. data/app/controllers/spina/admin/layout_controller.rb +6 -0
  20. data/app/controllers/spina/admin/navigation_items_controller.rb +29 -9
  21. data/app/models/spina/account.rb +9 -2
  22. data/app/models/spina/navigation_item.rb +24 -7
  23. data/app/models/spina/page.rb +1 -1
  24. data/app/models/spina/parts/image.rb +4 -1
  25. data/app/presenters/spina/menu_presenter.rb +1 -1
  26. data/app/views/layouts/spina/admin/admin.html.erb +2 -0
  27. data/app/views/spina/admin/layout/edit.html.erb +38 -29
  28. data/app/views/spina/admin/navigation_items/_form.html.erb +24 -13
  29. data/app/views/spina/admin/navigation_items/_navigation_item.html.erb +15 -2
  30. data/app/views/spina/admin/navigation_items/_page_form.html.erb +5 -0
  31. data/app/views/spina/admin/navigation_items/_url_form.html.erb +13 -0
  32. data/app/views/spina/admin/navigation_items/edit.html.erb +3 -0
  33. data/app/views/spina/admin/parts/texts/_form.html.erb +2 -2
  34. data/app/views/spina/admin/shared/_navigation.html.erb +4 -2
  35. data/config/locales/cs.yml +415 -0
  36. data/config/locales/en.yml +6 -0
  37. data/config/locales/fr.yml +2 -2
  38. data/config/locales/ru.yml +76 -16
  39. data/db/migrate/17_add_custom_urls_to_spina_navigation_items.rb +18 -0
  40. data/lib/generators/spina/install_generator.rb +3 -2
  41. data/lib/generators/spina/tailwind_config_generator.rb +7 -1
  42. data/lib/spina/version.rb +1 -1
  43. metadata +21 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8a4c285b310a44ed59a8188549402477161efe9e559dbbb6e5e52bac3a38ffc3
4
- data.tar.gz: b7055f33ccf17811f980f0e79843245a3ce7d9a5b11910b6f5ef080ed25adbe1
3
+ metadata.gz: a0077bbb44829d71ab719fc2b7e51d625eff7a904c6d96d73fa4fb5d1b53c124
4
+ data.tar.gz: 685b9ed0d11538e6118b13cf72627fe70d8a4edf76d5ff2bbe00e72386d9e55e
5
5
  SHA512:
6
- metadata.gz: 715e97080cea0a49d9329eb0c52960e21438f9401548f079d04afb9d1c0d1e601671de1153c3c4d6ae8ab305c5ee726d2881a08cf85fea77fc6b415b36dcdca7
7
- data.tar.gz: 2623fea9312d74ea949df1588ff4b67bbd9c929af292c7097a1ec0cf09e738b75befb4e633092ca6fd19e248d7fd816e8f903f244fb75b0ea25a501955957f56
6
+ metadata.gz: 208e7e279bd356cdb47d9a12cafc7545caa73a824f25328cd119abb1d34c11c6a667f3feb49ec85390c8e836a024ce9bcde3c6cf764f8e8112a2123bc53be34f
7
+ data.tar.gz: b1eb0d8626ffc0873833ee45eec600ab7434392691c44877eac6353a3b9ebad0daf7641e0cbf4f5f84dcb66c5320c4f12cf91a74318211d4d0dc273741f87020
@@ -1,5 +1,5 @@
1
1
  /*
2
- ! tailwindcss v3.2.7 | MIT License | https://tailwindcss.com
2
+ ! tailwindcss v3.3.2 | MIT License | https://tailwindcss.com
3
3
  */
4
4
 
5
5
  /*
@@ -31,6 +31,7 @@
31
31
  3. Use a more readable tab size.
32
32
  4. Use the user's configured `sans` font-family by default.
33
33
  5. Use the user's configured `sans` font-feature-settings by default.
34
+ 6. Use the user's configured `sans` font-variation-settings by default.
34
35
  */
35
36
 
36
37
  html {
@@ -47,6 +48,8 @@ html {
47
48
  /* 4 */
48
49
  font-feature-settings: normal;
49
50
  /* 5 */
51
+ font-variation-settings: normal;
52
+ /* 6 */
50
53
  }
51
54
 
52
55
  /*
@@ -620,6 +623,9 @@ a, button {
620
623
  --tw-pan-y: ;
621
624
  --tw-pinch-zoom: ;
622
625
  --tw-scroll-snap-strictness: proximity;
626
+ --tw-gradient-from-position: ;
627
+ --tw-gradient-via-position: ;
628
+ --tw-gradient-to-position: ;
623
629
  --tw-ordinal: ;
624
630
  --tw-slashed-zero: ;
625
631
  --tw-numeric-figure: ;
@@ -667,6 +673,9 @@ a, button {
667
673
  --tw-pan-y: ;
668
674
  --tw-pinch-zoom: ;
669
675
  --tw-scroll-snap-strictness: proximity;
676
+ --tw-gradient-from-position: ;
677
+ --tw-gradient-via-position: ;
678
+ --tw-gradient-to-position: ;
670
679
  --tw-ordinal: ;
671
680
  --tw-slashed-zero: ;
672
681
  --tw-numeric-figure: ;
@@ -1779,10 +1788,7 @@ a, button {
1779
1788
  align-items: center;
1780
1789
  justify-content: center;
1781
1790
  position: fixed;
1782
- top: 0px;
1783
- right: 0px;
1784
- bottom: 0px;
1785
- left: 0px;
1791
+ inset: 0px;
1786
1792
  z-index: 40;
1787
1793
  height: 100%;
1788
1794
  padding: 1.5rem;
@@ -1943,10 +1949,7 @@ trix-editor [data-trix-mutable]::selection,
1943
1949
  }
1944
1950
 
1945
1951
  .inset-0 {
1946
- top: 0px;
1947
- right: 0px;
1948
- bottom: 0px;
1949
- left: 0px;
1952
+ inset: 0px;
1950
1953
  }
1951
1954
 
1952
1955
  .bottom-0 {
@@ -2066,6 +2069,14 @@ trix-editor [data-trix-mutable]::selection,
2066
2069
  margin-right: -1rem;
2067
2070
  }
2068
2071
 
2072
+ .-mt-0 {
2073
+ margin-top: -0px;
2074
+ }
2075
+
2076
+ .-mt-0\.5 {
2077
+ margin-top: -0.125rem;
2078
+ }
2079
+
2069
2080
  .-mt-4 {
2070
2081
  margin-top: -1rem;
2071
2082
  }
@@ -2110,6 +2121,10 @@ trix-editor [data-trix-mutable]::selection,
2110
2121
  margin-left: 2rem;
2111
2122
  }
2112
2123
 
2124
+ .ml-auto {
2125
+ margin-left: auto;
2126
+ }
2127
+
2113
2128
  .mr-1 {
2114
2129
  margin-right: 0.25rem;
2115
2130
  }
@@ -2266,6 +2281,10 @@ trix-editor [data-trix-mutable]::selection,
2266
2281
  width: 3.5rem;
2267
2282
  }
2268
2283
 
2284
+ .w-2\/3 {
2285
+ width: 66.666667%;
2286
+ }
2287
+
2269
2288
  .w-28 {
2270
2289
  width: 7rem;
2271
2290
  }
@@ -2842,13 +2861,13 @@ trix-editor [data-trix-mutable]::selection,
2842
2861
  }
2843
2862
 
2844
2863
  .from-spina-dark {
2845
- --tw-gradient-from: #3a3a70;
2846
- --tw-gradient-to: rgb(58 58 112 / 0);
2864
+ --tw-gradient-from: #3a3a70 var(--tw-gradient-from-position);
2865
+ --tw-gradient-to: rgb(58 58 112 / 0) var(--tw-gradient-to-position);
2847
2866
  --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
2848
2867
  }
2849
2868
 
2850
2869
  .to-spina-light {
2851
- --tw-gradient-to: #797ab8;
2870
+ --tw-gradient-to: #797ab8 var(--tw-gradient-to-position);
2852
2871
  }
2853
2872
 
2854
2873
  .object-contain {
@@ -2971,6 +2990,10 @@ trix-editor [data-trix-mutable]::selection,
2971
2990
  padding-bottom: 1.25rem;
2972
2991
  }
2973
2992
 
2993
+ .pl-1 {
2994
+ padding-left: 0.25rem;
2995
+ }
2996
+
2974
2997
  .pl-10 {
2975
2998
  padding-left: 2.5rem;
2976
2999
  }
@@ -3600,10 +3623,7 @@ trix-editor [data-trix-mutable]::selection,
3600
3623
  }
3601
3624
 
3602
3625
  .md\:inset-0 {
3603
- top: 0px;
3604
- right: 0px;
3605
- bottom: 0px;
3606
- left: 0px;
3626
+ inset: 0px;
3607
3627
  }
3608
3628
 
3609
3629
  .md\:-top-0 {
@@ -3769,13 +3789,13 @@ trix-editor [data-trix-mutable]::selection,
3769
3789
  }
3770
3790
 
3771
3791
  .md\:from-spina-dark {
3772
- --tw-gradient-from: #3a3a70;
3773
- --tw-gradient-to: rgb(58 58 112 / 0);
3792
+ --tw-gradient-from: #3a3a70 var(--tw-gradient-from-position);
3793
+ --tw-gradient-to: rgb(58 58 112 / 0) var(--tw-gradient-to-position);
3774
3794
  --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
3775
3795
  }
3776
3796
 
3777
3797
  .md\:to-spina-light {
3778
- --tw-gradient-to: #797ab8;
3798
+ --tw-gradient-to: #797ab8 var(--tw-gradient-to-position);
3779
3799
  }
3780
3800
 
3781
3801
  .md\:p-3 {
@@ -0,0 +1,49 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+
5
+ start(event) {
6
+ const file = event.detail.file
7
+ if (!file) return
8
+
9
+ const trixId = event.detail.trixId
10
+ this.trixTargetId = trixId
11
+
12
+ // DataTransfer() avoids the "TypeError: Failed to set the 'files' property on 'HTMLInputElement': Failed to convert
13
+ // value to 'FileList'." error which occurs if you try to directly assign: `this.fileField.files = [file]`
14
+ // https://stackoverflow.com/questions/52078853/is-it-possible-to-update-filelist
15
+ const fileList = new DataTransfer()
16
+ fileList.items.add(file)
17
+ this.fileField.files = fileList.files
18
+
19
+ const changeEvent = new Event("input"); // This triggers the upload
20
+ this.fileField.dispatchEvent(changeEvent);
21
+ }
22
+
23
+ get fileField() {
24
+ return this.element.querySelector('form input.image-upload-file-field')
25
+ }
26
+
27
+ get form() {
28
+ return this.element.querySelector('form')
29
+ }
30
+
31
+ get trixTargetId() {
32
+ return this.element.querySelector('form > #trix_target_id').value
33
+ }
34
+
35
+ set trixTargetId(value) {
36
+ this.element.querySelector('form > #trix_target_id').value = value
37
+ }
38
+
39
+ #imageData(imageMeta) {
40
+ return {
41
+ filename: imageMeta.dataset.filename,
42
+ signedBlobId: imageMeta.dataset.signedBlobId,
43
+ imageId: imageMeta.dataset.imageId,
44
+ embeddedUrl: imageMeta.dataset.embeddedUrl,
45
+ thumbnail: imageMeta.dataset.thumbnail
46
+ }
47
+ }
48
+
49
+ }
@@ -0,0 +1,38 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+
5
+ connect() {
6
+ // Gets triggered when refreshed by the ImagesController, and will process any images here
7
+ this.insertImages()
8
+ }
9
+
10
+ insertImages() {
11
+ const imagesMetadata = this.element.querySelectorAll('div.trix-insert-image')
12
+ imagesMetadata.forEach(tag => {
13
+ const imageData = this.#imageData(tag)
14
+ let insertImageEvent = new CustomEvent("media-picker:done", {detail: imageData})
15
+ const targetEditor = document.getElementById(this.trixTargetId)
16
+ this.trixTarget.dispatchEvent(insertImageEvent)
17
+ })
18
+ }
19
+
20
+ get trixTarget() {
21
+ return document.getElementById(this.trixTargetId)
22
+ }
23
+
24
+ get trixTargetId() {
25
+ return this.element.dataset.trixTargetId
26
+ }
27
+
28
+ #imageData(imageMeta) {
29
+ return {
30
+ filename: imageMeta.dataset.filename,
31
+ signedBlobId: imageMeta.dataset.signedBlobId,
32
+ imageId: imageMeta.dataset.imageId,
33
+ embeddedUrl: imageMeta.dataset.embeddedUrl,
34
+ thumbnail: imageMeta.dataset.thumbnail
35
+ }
36
+ }
37
+
38
+ }
@@ -40,9 +40,11 @@ export default class extends Controller {
40
40
 
41
41
  // Insert button
42
42
  this.listTarget.insertAdjacentHTML('beforeend', this.buttonHTML(time))
43
-
44
43
  // Insert fields
45
- this.contentTarget.insertAdjacentHTML('beforeend', html)
44
+ const parser = new DOMParser();
45
+ const docFields = parser.parseFromString(html, 'text/html');
46
+ this.contentTarget.appendChild(docFields.body.firstChild);
47
+
46
48
  }
47
49
 
48
50
  removeFields(event) {
@@ -19,7 +19,7 @@ export default class extends Controller {
19
19
  }
20
20
  }.bind(this))
21
21
  }
22
-
22
+
23
23
  insertEmbeddable(html) {
24
24
  let embeddable = new Trix.Attachment({
25
25
  content: html,
@@ -38,7 +38,15 @@ export default class extends Controller {
38
38
  </span>`, contentType: "Spina::Image"})
39
39
  this.editor.insertAttachment(attachment)
40
40
  }
41
-
41
+
42
+ fileAccept(event) {
43
+ const file = event.file
44
+ if(file) {
45
+ const startUploadEvent = new CustomEvent("auto-file-upload:start", { detail: { file, trixId: this.trixId }})
46
+ window.dispatchEvent(startUploadEvent)
47
+ }
48
+ }
49
+
42
50
  setAltText(event) {
43
51
  let alt = event.currentTarget.value
44
52
  let altLabel = alt
@@ -81,6 +89,10 @@ export default class extends Controller {
81
89
  get trixAttachment() {
82
90
  return this.getTrixAttachment(this.mutableImageAttachment.dataset.trixId)
83
91
  }
92
+
93
+ get trixId() {
94
+ return this.element.id
95
+ }
84
96
 
85
97
  get mutableImageAttachment() {
86
98
  return this.element.querySelector(`figure[data-trix-mutable][data-trix-content-type="Spina::Image"]`)
@@ -0,0 +1,6 @@
1
+ <div data-controller="auto-file-upload" data-action="auto-file-upload:start@window->auto-file-upload#start">
2
+ <%= render Spina::Forms::FileUploadComponent.new(origin: 'auto-file-upload', css_classes: 'hidden', turbo_frame: turbo_frame_id) %>
3
+ <turbo-frame id="auto-file-upload-images" data-trix-insert-target="">
4
+ <%= render Spina::Forms::EditorInsertImagesMetaComponent.new %>
5
+ </turbo-frame>
6
+ </div>
@@ -0,0 +1,15 @@
1
+ module Spina
2
+ module Forms
3
+
4
+ # Meant to be a hidden component for facilitating quick file uploads from drag and drop or paste action within Trix
5
+ class AutoFileUploadComponent < ApplicationComponent
6
+ attr_reader :turbo_frame_id
7
+
8
+ def initialize
9
+ @turbo_frame_id = "auto-file-upload-images"
10
+ end
11
+
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,12 @@
1
+ <div id="auto-uploaded-images" data-controller="editor-insert-images" data-trix-target-id="<%= trix_target_id %>">
2
+ <% @images.each do |image| %>
3
+ <div
4
+ class="hidden trix-insert-image"
5
+ data-image-id="<%= image.id %>"
6
+ data-signed-blob-id="<%= image.file.blob&.signed_id %>"
7
+ data-filename="<%= image.file.filename %>"
8
+ data-thumbnail="<%= helpers.thumbnail_url(image) %>"
9
+ data-embedded-url="<%= helpers.embedded_image_url(image) %>"
10
+ ></div>
11
+ <% end %>
12
+ </div>
@@ -0,0 +1,12 @@
1
+ module Spina
2
+ module Forms
3
+ class EditorInsertImagesMetaComponent < ApplicationComponent
4
+ attr_reader :images, :trix_target_id
5
+
6
+ def initialize(trix_target_id: nil, images:[])
7
+ @trix_target_id = trix_target_id
8
+ @images = images
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,14 @@
1
+ <div data-controller="file-upload-component" class=<%= @css_classes %>>
2
+ <%= form_with model: [:admin, Spina::Image.new], url: helpers.spina.admin_images_path, data: {controller: "form loading-button", loading_message: t('spina.media_library.uploading'), action: "turbo:submit-end->loading-button#doneLoading", origin: origin, turbo_frame: turbo_frame} do |f| %>
3
+ <%= hidden_field_tag :origin, origin %>
4
+ <%= hidden_field_tag :trix_target_id, value: @trix_target_id %>
5
+ <%= f.hidden_field :media_folder_id, value: media_folder&.id %>
6
+
7
+ <%= f.file_field :files, multiple: true, accept: "image/*", id: file_field_id, class: 'image-upload-file-field hidden', data: {action: "loading-button#loading form#requestSubmit"} %>
8
+
9
+ <%= button_tag(type: "button", class: "font-medium w-full text-gray-600 text-sm hover:bg-gray-200 px-3 py-2 cursor-pointer rounded-lg flex items-center w-full", data: { controller:"delegate-click", action: "delegate-click#click", 'loading-button-target': "button", 'delegate-click-target': "##{file_field_id}" }) do %>
10
+ <%= helpers.heroicon("upload", style: :solid, class: "w-5 h-5 text-spina mr-2") %>
11
+ <%=t 'spina.media_library.upload_from_device' %>
12
+ <% end %>
13
+ <% end %>
14
+ </div>
@@ -0,0 +1,19 @@
1
+ module Spina
2
+ module Forms
3
+ class FileUploadComponent < ApplicationComponent
4
+ attr_reader :css_classes, :id, :origin, :file_field_id, :media_folder, :trix_id, :turbo_frame
5
+
6
+ def initialize(origin:, css_classes: '', id: nil, trix_target_id: nil, media_folder: nil, turbo_frame: nil)
7
+ @origin = origin
8
+ @css_classes = css_classes
9
+ @id = id || SecureRandom.uuid
10
+ @media_folder = media_folder
11
+ @trix_target_id = trix_target_id # If inserting an image into Trix, specifies which one receives it
12
+ @turbo_frame = turbo_frame
13
+
14
+ @file_field_id = "image_upload_file_field_#{@id}"
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -3,11 +3,12 @@ module Spina
3
3
  class TextFieldComponent < ApplicationComponent
4
4
  attr_accessor :f, :method, :size, :autofocus
5
5
 
6
- def initialize(f, method, size: "md", autofocus: false)
6
+ def initialize(f, method, size: "md", autofocus: false, placeholder: nil)
7
7
  @f = f
8
8
  @method = method
9
9
  @size = size
10
10
  @autofocus = autofocus
11
+ @placeholder = placeholder
11
12
  end
12
13
 
13
14
  def controllers
@@ -42,7 +43,7 @@ module Spina
42
43
  end
43
44
 
44
45
  def placeholder
45
- f.object.class.human_attribute_name(method)
46
+ @placeholder || f.object.class.human_attribute_name(method)
46
47
  end
47
48
  end
48
49
  end
@@ -1,6 +1,6 @@
1
1
  <div class="relative sticky z-10 top-0 pt-4 bg-white trix-toolbar" id="<%= @trix_id %>">
2
2
  <div class="flex items-center flex-wrap" data-controller="reveal">
3
- <div class="flex items-center bg-gray-200 rounded overflow-hidden mb-3 mr-3">
3
+ <div class="flex items-center bg-gray-200 rounded overflow-hidden mb-3 mr-3" data-trix-button-group="text-tools">
4
4
  <button type="button" class="hover:bg-gray-300 text-gray-700 w-9 h-9 flex items-center justify-center" data-trix-attribute="bold" data-trix-key="b" title="${Trix.config.lang.bold}" tabindex="-1">
5
5
  <svg class="w-4 h-4" fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path d="M333.49 238a122 122 0 0 0 27-65.21C367.87 96.49 308 32 233.42 32H34a16 16 0 0 0-16 16v48a16 16 0 0 0 16 16h31.87v288H34a16 16 0 0 0-16 16v48a16 16 0 0 0 16 16h209.32c70.8 0 134.14-51.75 141-122.4 4.74-48.45-16.39-92.06-50.83-119.6zM145.66 112h87.76a48 48 0 0 1 0 96h-87.76zm87.76 288h-87.76V288h87.76a56 56 0 0 1 0 112z"/></svg>
6
6
  </button>
@@ -27,7 +27,7 @@
27
27
  </button>
28
28
  </div>
29
29
 
30
- <div class="flex items-center bg-gray-200 rounded overflow-hidden mr-3 mb-3">
30
+ <div class="flex items-center bg-gray-200 rounded overflow-hidden mr-3 mb-3" data-trix-button-group="file-tools">
31
31
 
32
32
  <%= link_to helpers.spina.admin_media_picker_path(target: "insert_#{@trix_id}"), class: "hover:bg-gray-300 text-gray-700 w-9 h-9 flex items-center justify-center", data: {turbo_frame: "modal"} do %>
33
33
  <svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
@@ -58,17 +58,7 @@
58
58
  <% end %>
59
59
  <% end %>
60
60
 
61
- <%= form_with model: [:admin, Spina::Image.new], url: helpers.spina.admin_images_path, data: {controller: "form loading-button", loading_message: t('spina.media_library.uploading'), action: "turbo:submit-end->loading-button#doneLoading", turbo_frame: "media_picker"} do |f| %>
62
- <%= hidden_field_tag :modal, true %>
63
- <%= f.hidden_field :media_folder_id, value: @media_folder&.id %>
64
-
65
- <%= f.file_field :files, multiple: true, accept: "image/*", id: "new_image_file_field", class: 'hidden', data: {action: "loading-button#loading form#requestSubmit"} %>
66
-
67
- <button type="button" class="font-medium w-full text-gray-600 text-sm hover:bg-gray-200 px-3 py-2 cursor-pointer rounded-lg flex items-center w-full" data-controller="delegate-click" data-action="delegate-click#click" data-loading-button-target="button" data-delegate-click-target="#new_image_file_field">
68
- <%= helpers.heroicon("upload", style: :solid, class: "w-5 h-5 text-spina mr-2") %>
69
- <%=t 'spina.media_library.upload_from_device' %>
70
- </button>
71
- <% end %>
61
+ <%= render Spina::Forms::FileUploadComponent.new(origin: 'media-picker', media_folder: @media_folder, turbo_frame: 'media_picker') %>
72
62
  </div>
73
63
 
74
64
  <button type="button" class="btn btn-primary w-full mt-3" data-action="media-picker-modal#confirm modal#close">
@@ -1,10 +1,11 @@
1
1
  <div class="flex items-center border border-gray-300 rounded-md bg-white p-1 relative shadow-sm" data-controller="parent-pages">
2
+ <% if resources.many? %>
3
+ <%= helpers.heroicon("collection", style: :solid, class: 'w-5 h-5 ml-2 text-gray-400 absolute') %>
4
+
5
+ <%= f.select :resource_id, resources, {}, class: "form-select hover:bg-gray-100 bg-none shadow-none w-auto bg-transparent border-none focus:ring-0 cursor-pointer pr-2 pl-8 flex-none truncate", data: {controller: "select-placeholder", action: "select-placeholder#update parent-pages#update"} %>
2
6
 
3
- <%= helpers.heroicon("collection", style: :solid, class: 'w-5 h-5 ml-2 text-gray-400 absolute') %>
4
-
5
- <%= f.select :resource_id, resources, {}, class: "form-select hover:bg-gray-100 bg-none shadow-none w-auto bg-transparent border-none focus:ring-0 cursor-pointer pr-2 pl-8 flex-none truncate", data: {controller: "select-placeholder", action: "select-placeholder#update parent-pages#update"} %>
6
-
7
- <%= helpers.heroicon('chevron-right', style: :solid, class: 'w-5 h-5 text-gray-600 flex-none') %>
7
+ <%= helpers.heroicon('chevron-right', style: :solid, class: 'w-5 h-5 text-gray-600 flex-none') %>
8
+ <% end %>
8
9
 
9
10
  <%= helpers.turbo_frame_tag "parent_pages", src: default_parent_pages_path, class: 'flex-auto', data: {parent_pages_target: "frame"} %>
10
11
  </div>
@@ -12,7 +12,7 @@ module Spina::Pages
12
12
 
13
13
  def options
14
14
  Spina::Page.sort_by_ancestry(pages.arrange(order: :position)).map do |page|
15
- page_menu_title = page.menu_title.indent(page.depth, "–")
15
+ page_menu_title = page.menu_title&.indent(page.depth, "–")
16
16
  [page_menu_title, page.id]
17
17
  end
18
18
  end
@@ -35,8 +35,15 @@ module Spina
35
35
  image
36
36
  end.compact
37
37
 
38
- if params[:modal]
38
+ if params[:origin] == 'media-picker'
39
39
  redirect_to spina.admin_media_picker_path(media_folder_id: image_params[:media_folder_id])
40
+ elsif params[:origin] == 'auto-file-upload'
41
+ render turbo_stream: turbo_stream.update(
42
+ 'auto-file-upload-images',
43
+ Spina::Forms::EditorInsertImagesMetaComponent
44
+ .new(images: @images, trix_target_id: params[:trix_target_id])
45
+ .render_in(view_context)
46
+ )
40
47
  else
41
48
  render turbo_stream: turbo_stream.prepend("images", partial: "image", collection: @images)
42
49
  end
@@ -90,7 +97,7 @@ module Spina
90
97
  end
91
98
 
92
99
  def image_params
93
- params.require(:image).permit(:media_folder_id, :file)
100
+ params.require(:image).permit(:media_folder_id, :file, files: [])
94
101
  end
95
102
  end
96
103
  end
@@ -3,6 +3,7 @@ module Spina::Admin
3
3
  before_action :set_account
4
4
  before_action :set_locale
5
5
  before_action :set_breadcrumb
6
+ before_action :get_layout_parts
6
7
 
7
8
  admin_section :content
8
9
 
@@ -24,6 +25,11 @@ module Spina::Admin
24
25
  def layout_params
25
26
  params.require(:account).permit!
26
27
  end
28
+
29
+ def get_layout_parts
30
+ @layout_parts = Spina::Current.theme.layout_parts
31
+ @layout_parts = {parts: @layout_parts} if @layout_parts.is_a?(Array)
32
+ end
27
33
 
28
34
  def set_breadcrumb
29
35
  add_breadcrumb t("spina.layout.layout")
@@ -4,14 +4,33 @@ module Spina
4
4
  before_action :set_navigation
5
5
 
6
6
  def new
7
- @navigation_item = @navigation.navigation_items.new(parent_id: params[:parent_id])
7
+ @navigation_item = @navigation.navigation_items.new(parent_id: params[:parent_id], kind: params[:kind].presence || "page")
8
8
  @pages = Page.sorted.main.includes(:translations)
9
9
  end
10
10
 
11
11
  def create
12
- @navigation_item = NavigationItem.new(navigation_item_params)
12
+ @navigation_item = @navigation.navigation_items.new(navigation_item_params)
13
13
  if @navigation_item.save
14
- render turbo_stream: turbo_stream.append(@navigation_item.parent || @navigation, @navigation_item)
14
+ redirect_to spina.edit_admin_navigation_path(@navigation)
15
+ else
16
+ @pages = Page.sorted.main.includes(:translations)
17
+ render turbo_stream: turbo_stream.update(:navigation_item_form, partial: "form")
18
+ end
19
+ end
20
+
21
+ def edit
22
+ @navigation_item = NavigationItem.find(params[:id])
23
+ @pages = Page.sorted.main.includes(:translations)
24
+ end
25
+
26
+ def update
27
+ @navigation_item = NavigationItem.find(params[:id])
28
+
29
+ if @navigation_item.update(navigation_item_params)
30
+ redirect_to spina.edit_admin_navigation_path(@navigation)
31
+ else
32
+ @pages = Page.sorted.main.includes(:translations)
33
+ render turbo_stream: turbo_stream.update(:navigation_item_form, partial: "form")
15
34
  end
16
35
  end
17
36
 
@@ -23,13 +42,14 @@ module Spina
23
42
 
24
43
  private
25
44
 
26
- def navigation_item_params
27
- params.require(:navigation_item).permit(:page_id, :parent_id).merge(navigation_id: @navigation.id)
28
- end
45
+ def navigation_item_params
46
+ params.require(:navigation_item).permit(:kind, :page_id, :parent_id, :url_title, :url).merge(navigation_id: @navigation.id)
47
+ end
48
+
49
+ def set_navigation
50
+ @navigation = Navigation.find(params[:navigation_id])
51
+ end
29
52
 
30
- def set_navigation
31
- @navigation = Navigation.find(params[:navigation_id])
32
- end
33
53
  end
34
54
  end
35
55
  end
@@ -5,7 +5,7 @@ module Spina
5
5
  include Partable
6
6
  include TranslatedContent
7
7
 
8
- serialize :preferences
8
+ serialize :preferences, coder: Psych
9
9
 
10
10
  after_save :bootstrap_website
11
11
 
@@ -72,9 +72,16 @@ module Spina
72
72
 
73
73
  def find_or_create_custom_pages(theme)
74
74
  theme.custom_pages.each do |page|
75
+ ancestry = nil
76
+
77
+ if page[:parent].present?
78
+ parent_page = Page.find_by(name: page[:parent])
79
+ ancestry = [parent_page&.ancestry, parent_page&.id].compact.join("/")
80
+ end
81
+
75
82
  Page.where(name: page[:name])
76
83
  .first_or_create(title: page[:title])
77
- .update(view_template: page[:view_template], deletable: page[:deletable])
84
+ .update(view_template: page[:view_template], deletable: page[:deletable], ancestry: ancestry)
78
85
  end
79
86
  end
80
87
 
@@ -1,18 +1,35 @@
1
1
  module Spina
2
2
  class NavigationItem < ApplicationRecord
3
- belongs_to :navigation, touch: true
4
- belongs_to :page
3
+ belongs_to :navigation, touch: true, class_name: "Spina::Navigation"
4
+ belongs_to :page, optional: true, class_name: "Spina::Page"
5
+
6
+ # NavigationItems can be of two different kinds:
7
+ # - A link to a page
8
+ # - A link to a URL
9
+ enum(:kind, {page: "page", url: "url"}, default: :page, suffix: true)
5
10
 
6
11
  has_ancestry
7
12
 
8
13
  scope :regular_pages, -> { joins(:page).where(spina_pages: {resource_id: nil}) }
9
14
  scope :sorted, -> { order("spina_navigation_items.position") }
10
- scope :live, -> { joins(:page).where(spina_pages: {draft: false, active: true}) }
11
- scope :in_menu, -> { joins(:page).where(spina_pages: {show_in_menu: true}) }
12
- scope :active, -> { joins(:page).where(spina_pages: {active: true}) }
15
+ scope :live, -> { left_outer_joins(:page).where("spina_pages.draft = ? AND spina_pages.active = ? OR spina_pages.id IS NULL", false, true) }
16
+ scope :in_menu, -> { left_outer_joins(:page).where("spina_pages.show_in_menu = ? OR spina_pages.id IS NULL", true) }
17
+ scope :active, -> { left_outer_joins(:page).where("spina_pages.active = ? OR spina_pages.id IS NULL", true) }
13
18
 
14
- validates :page, uniqueness: {scope: :navigation}
19
+ validates :page, uniqueness: {scope: :navigation}, presence: true, if: :page_kind?
20
+ validates :url, presence: true, if: :url_kind?
21
+ validates :url_title, presence: true, if: :url_kind?
15
22
 
16
- delegate :menu_title, :materialized_path, :draft?, :homepage?, to: :page
23
+ delegate :draft?, :homepage?, to: :page, allow_nil: true
24
+
25
+ def menu_title
26
+ return url_title if url_kind?
27
+ page&.menu_title
28
+ end
29
+
30
+ def materialized_path
31
+ return url if url_kind?
32
+ page&.materialized_path
33
+ end
17
34
  end
18
35
  end