spina 2.15.1 → 2.17.0

Sign up to get free protection for your applications and to get access to all the features.
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