spina 2.13.1 → 2.14.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of spina might be problematic. Click here for more details.

Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/spina/tailwind.css +554 -589
  3. data/app/assets/javascripts/spina/controllers/form_controller.js +14 -1
  4. data/app/assets/javascripts/spina/controllers/page_collapse_controller.js +17 -0
  5. data/app/assets/javascripts/spina/controllers/page_select_controller.js +38 -0
  6. data/app/assets/javascripts/spina/libraries/debounce.js +65 -0
  7. data/app/components/spina/forms/search_component.html.erb +12 -0
  8. data/app/components/spina/forms/search_component.rb +12 -0
  9. data/app/components/spina/forms/trix_toolbar_component.html.erb +5 -6
  10. data/app/components/spina/media_picker/modal_component.html.erb +24 -16
  11. data/app/components/spina/pages/page_component.html.erb +23 -10
  12. data/app/components/spina/pages/page_component.rb +15 -3
  13. data/app/controllers/spina/admin/attachments_controller.rb +1 -1
  14. data/app/controllers/spina/admin/images_controller.rb +1 -1
  15. data/app/controllers/spina/admin/media_picker_controller.rb +1 -1
  16. data/app/controllers/spina/admin/page_select_options_controller.rb +24 -0
  17. data/app/controllers/spina/admin/pages_controller.rb +1 -1
  18. data/app/controllers/spina/admin/password_resets_controller.rb +1 -1
  19. data/app/models/concerns/spina/attachable.rb +21 -0
  20. data/app/models/spina/attachment.rb +2 -6
  21. data/app/models/spina/image.rb +2 -6
  22. data/app/models/spina/navigation_item.rb +1 -1
  23. data/app/models/spina/page.rb +3 -1
  24. data/app/models/spina/parts/page_link.rb +13 -0
  25. data/app/models/spina/user.rb +1 -1
  26. data/app/views/spina/admin/attachments/index.html.erb +7 -1
  27. data/app/views/spina/admin/images/index.html.erb +8 -1
  28. data/app/views/spina/admin/page_select_options/index.html.erb +21 -0
  29. data/app/views/spina/admin/page_select_options/show.html.erb +3 -0
  30. data/app/views/spina/admin/pages/children.html.erb +3 -0
  31. data/app/views/spina/admin/parts/multi_lines/_form.html.erb +4 -1
  32. data/app/views/spina/admin/parts/page_links/_form.html.erb +36 -0
  33. data/config/initializers/importmap.rb +6 -0
  34. data/config/locales/en.yml +1 -0
  35. data/config/locales/nl.yml +43 -0
  36. data/config/routes.rb +8 -3
  37. data/db/migrate/16_add_ancestry_cache_columns_to_spina_pages.rb +6 -0
  38. data/lib/spina/attr_json_spina_parts_model.rb +1 -1
  39. data/lib/spina/engine.rb +2 -1
  40. data/lib/spina/version.rb +1 -1
  41. metadata +18 -17
@@ -1,10 +1,23 @@
1
1
  import { Controller } from "@hotwired/stimulus"
2
+ import debounce from "libraries/debounce"
2
3
  import formRequestSubmitPolyfill from "libraries/form-request-submit-polyfill"
3
4
 
4
5
  export default class extends Controller {
5
6
 
6
- requestSubmit() {
7
+ submitForm = debounce(function() {
7
8
  this.element.requestSubmit()
9
+ }.bind(this), this.debounceTime)
10
+
11
+ requestSubmit() {
12
+ this.submitForm()
13
+ }
14
+
15
+ submit() {
16
+ this.submitForm()
17
+ }
18
+
19
+ get debounceTime() {
20
+ return this.element.dataset.debounceTime || 0
8
21
  }
9
22
 
10
23
  }
@@ -0,0 +1,17 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = [ "children", "indicator" ]
5
+
6
+ toggle(event) {
7
+ this.childrenTarget.toggleAttribute("hidden")
8
+ this.indicatorTarget.classList.toggle("rotate-90")
9
+
10
+ if (this.collapsed) event.preventDefault()
11
+ }
12
+
13
+ get collapsed() {
14
+ return this.childrenTarget.hidden
15
+ }
16
+
17
+ }
@@ -0,0 +1,38 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+
5
+ static get targets() {
6
+ return ['input', 'label', 'search']
7
+ }
8
+
9
+ connect() {
10
+ // Show placeholder if there is no page selected yet
11
+ if (this.labelTarget.querySelector("turbo-frame") == undefined) {
12
+ this.clear()
13
+ }
14
+ }
15
+
16
+ select(event) {
17
+ let button = event.currentTarget
18
+
19
+ this.inputTarget.value = button.dataset.id
20
+ this.labelTarget.innerText = button.dataset.title
21
+ }
22
+
23
+ clear() {
24
+ this.inputTarget.value = ""
25
+ this.labelTarget.innerHTML = `
26
+ <span class="text-gray-400">
27
+ ${this.element.dataset.placeholder}
28
+ </span>
29
+ `
30
+ }
31
+
32
+ autofocus() {
33
+ setTimeout(function() {
34
+ this.searchTarget.focus()
35
+ }.bind(this), 100)
36
+ }
37
+
38
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Returns a function, that, as long as it continues to be invoked, will not
3
+ * be triggered. The function will be called after it stops being called for
4
+ * N milliseconds. If `immediate` is passed, trigger the function on the
5
+ * leading edge, instead of the trailing. The function also has a property 'clear'
6
+ * that is a function which will clear the timer to prevent previously scheduled executions.
7
+ *
8
+ * @source underscore.js
9
+ * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/
10
+ * @param {Function} function to wrap
11
+ * @param {Number} timeout in ms (`100`)
12
+ * @param {Boolean} whether to execute at the beginning (`false`)
13
+ * @api public
14
+ */
15
+ export default function debounce(func, wait, immediate){
16
+ var timeout, args, context, timestamp, result;
17
+ if (null == wait) wait = 100;
18
+
19
+ function later() {
20
+ var last = Date.now() - timestamp;
21
+
22
+ if (last < wait && last >= 0) {
23
+ timeout = setTimeout(later, wait - last);
24
+ } else {
25
+ timeout = null;
26
+ if (!immediate) {
27
+ result = func.apply(context, args);
28
+ context = args = null;
29
+ }
30
+ }
31
+ };
32
+
33
+ var debounced = function(){
34
+ context = this;
35
+ args = arguments;
36
+ timestamp = Date.now();
37
+ var callNow = immediate && !timeout;
38
+ if (!timeout) timeout = setTimeout(later, wait);
39
+ if (callNow) {
40
+ result = func.apply(context, args);
41
+ context = args = null;
42
+ }
43
+
44
+ return result;
45
+ };
46
+
47
+ debounced.clear = function() {
48
+ if (timeout) {
49
+ clearTimeout(timeout);
50
+ timeout = null;
51
+ }
52
+ };
53
+
54
+ debounced.flush = function() {
55
+ if (timeout) {
56
+ result = func.apply(context, args);
57
+ context = args = null;
58
+
59
+ clearTimeout(timeout);
60
+ timeout = null;
61
+ }
62
+ };
63
+
64
+ return debounced;
65
+ };
@@ -0,0 +1,12 @@
1
+ <div class="flex items-center relative">
2
+ <div class="absolute left-2">
3
+ <%= helpers.heroicon('search', style: :solid, class: 'text-gray-300 w-5 h-5') %>
4
+ </div>
5
+
6
+ <%= f.search_field @method,
7
+ value: params[@method.to_sym],
8
+ placeholder: t("spina.search"),
9
+ class: "form-input rounded-full pl-8 text-sm h-9 w-full",
10
+ data: {action: "input->form#requestSubmit"}
11
+ %>
12
+ </div>
@@ -0,0 +1,12 @@
1
+ module Spina
2
+ module Forms
3
+ class SearchComponent < ApplicationComponent
4
+ attr_accessor :f, :method
5
+
6
+ def initialize(f, method)
7
+ @f = f
8
+ @method = method
9
+ end
10
+ end
11
+ end
12
+ end
@@ -66,11 +66,7 @@
66
66
  <svg class="w-4 h-4" fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M432 424H16a16 16 0 0 0-16 16v16a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16v-16a16 16 0 0 0-16-16zM27.31 363.3l96-96a16 16 0 0 0 0-22.62l-96-96C17.27 138.66 0 145.78 0 160v192c0 14.31 17.33 21.3 27.31 11.3zM435.17 168H204.83A12.82 12.82 0 0 0 192 180.83v22.34A12.82 12.82 0 0 0 204.83 216h230.34A12.82 12.82 0 0 0 448 203.17v-22.34A12.82 12.82 0 0 0 435.17 168zM432 48H16A16 16 0 0 0 0 64v16a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16V64a16 16 0 0 0-16-16zm3.17 248H204.83A12.82 12.82 0 0 0 192 308.83v22.34A12.82 12.82 0 0 0 204.83 344h230.34A12.82 12.82 0 0 0 448 331.17v-22.34A12.82 12.82 0 0 0 435.17 296z"/></svg>
67
67
  </button>
68
68
  </div>
69
-
70
- <div class="absolute hidden w-full" data-trix-target="imageFields">
71
- <input type="text" class="h-8 px-2 mt-1 border-0 ring-0 focus:ring-0 w-full text-sm italic" placeholder="Alt text" data-trix-target="altField" data-action="keyup->trix#setAltText keydown->trix#preventSubmission" />
72
- </div>
73
-
69
+
74
70
  <div hidden data-reveal data-trix-dialogs>
75
71
  <div class="trix-dialog" data-trix-dialog="href" data-trix-dialog-attribute="href">
76
72
  <div class="fixed inset-0 flex justify-center items-center bg-gray-700 bg-opacity-25 z-50">
@@ -98,4 +94,7 @@
98
94
  </div>
99
95
 
100
96
  </div>
101
- </div>
97
+ </div>
98
+ <div class="absolute hidden w-full" data-trix-target="imageFields">
99
+ <input type="text" class="h-8 px-2 mt-1 border-0 ring-0 focus:ring-0 w-full text-sm italic" placeholder="Alt text" data-trix-target="altField" data-action="keyup->trix#setAltText keydown->trix#preventSubmission" />
100
+ </div>
@@ -4,28 +4,36 @@
4
4
  <div class="flex flex-col md:flex-row h-full w-full">
5
5
  <div class="flex-1 bg-white flex flex-col max-h-full relative overflow-hidden">
6
6
  <div data-infinite-scroll-target="container" data-controller="selectable" class="p-6 h-full w-full overflow-scroll" data-action="scroll->infinite-scroll#load">
7
-
8
- <!-- Images are loaded using nested turbo-frame-tags -->
9
- <!-- Only load images in multiples of 4 so that this grid -->
10
- <!-- will not show any gaps. -->
11
- <turbo-frame id="images-<%= @images.current_page %>">
12
- <div class="grid grid-cols-2 md:grid-cols-4 gap-6 auto-rows-min">
13
- <%= render Spina::MediaPicker::ImageComponent.with_collection(@images) %>
14
- </div>
15
-
16
- <% if @images.next_page %>
17
- <turbo-frame id="images-<%= @images.next_page %>" data-action= "turbo:frame-load->infinite-scroll#load">
18
- <%= link_to "Load more images", helpers.path_to_next_page(@images), class: "btn btn-default mt-6", data: {infinite_scroll_target: "button"} %>
19
- </turbo-frame>
20
- <% end %>
21
-
7
+ <turbo-frame id="images">
8
+ <!-- Images are loaded using nested turbo-frame-tags -->
9
+ <!-- Only load images in multiples of 4 so that this grid -->
10
+ <!-- will not show any gaps. -->
11
+ <turbo-frame id="images-<%= @images.current_page %>">
12
+ <div class="grid grid-cols-2 md:grid-cols-4 gap-6 auto-rows-min">
13
+ <%= render Spina::MediaPicker::ImageComponent.with_collection(@images) %>
14
+ </div>
15
+
16
+ <% if @images.next_page %>
17
+ <turbo-frame id="images-<%= @images.next_page %>" data-action= "turbo:frame-load->infinite-scroll#load">
18
+ <%= link_to "Load more images", helpers.path_to_next_page(@images), class: "btn btn-default mt-6", data: {infinite_scroll_target: "button"} %>
19
+ </turbo-frame>
20
+ <% end %>
21
+
22
+ </turbo-frame>
22
23
  </turbo-frame>
23
24
 
24
25
  </div>
25
26
  </div>
26
27
 
27
28
  <div class="md:w-72 p-4 border-t rounded-b-lg md:rounded-none md:border-t-0 border-gray-200 flex flex-col justify-between bg-gray-100 md:bg-opacity-50 md:rounded-l-lg">
28
- <div class="hidden md:block md:mb-6 overflow-scroll">
29
+ <div class="mb-4">
30
+ <%= form_with method: :get, url: helpers.spina.admin_media_picker_path, data: {controller: "form", turbo_frame: "images"} do |f| %>
31
+ <%= f.hidden_field :media_folder_id, value: @media_folder&.id %>
32
+ <%= render Spina::Forms::SearchComponent.new(f, :query) %>
33
+ <% end %>
34
+ </div>
35
+
36
+ <div class="hidden md:block md:mb-6 overflow-scroll flex-1">
29
37
  <%= link_to helpers.spina.admin_media_picker_path, class: "font-medium w-full text-sm px-3 py-2 rounded-lg flex items-center justify-between #{media_folder_classes(nil)}", data: {turbo_frame: "media_picker"} do %>
30
38
  <div class="flex items-center">
31
39
  <%= helpers.heroicon("collection", style: :solid, class: "w-5 h-5 mr-2 text-spina-light") %>
@@ -1,4 +1,4 @@
1
- <turbo-frame id="page_<%= @page.id %>" data-id="<%= @page.id %>">
1
+ <turbo-frame id="page_<%= page.id %>" data-id="<%= page.id %>" data-controller="<%= 'page-collapse' if has_children? %>">
2
2
  <div class="flex items-center border-gray-200 border-b <%= css_class %> bg-opacity-50">
3
3
  <% if sortable? %>
4
4
  <% if draggable? %>
@@ -7,31 +7,44 @@
7
7
  </div>
8
8
  <% else %>
9
9
  <div class="pl-2">
10
- <%= button_to helpers.spina.sort_one_admin_page_path(@page, direction: "up"), class: "btn btn-default shadow-none border-gray-200 border-b-0 rounded-b-none px-1 h-5 sort-up" do %>
10
+ <%= button_to helpers.spina.sort_one_admin_page_path(page, direction: "up"), class: "btn btn-default shadow-none border-gray-200 border-b-0 rounded-b-none px-1 h-5 sort-up" do %>
11
11
  <%= helpers.heroicon("chevron-up", style: :solid, class: 'w-4 h-4 text-gray-600') %>
12
12
  <% end %>
13
13
 
14
- <%= button_to helpers.spina.sort_one_admin_page_path(@page), class: "btn btn-default shadow-none border-gray-200 rounded-t-none px-1 h-5 sort-down" do %>
14
+ <%= button_to helpers.spina.sort_one_admin_page_path(page), class: "btn btn-default shadow-none border-gray-200 rounded-t-none px-1 h-5 sort-down" do %>
15
15
  <%= helpers.heroicon("chevron-down", style: :solid, class: 'w-4 h-4 text-gray-600') %>
16
16
  <% end %>
17
17
  </div>
18
18
  <% end %>
19
19
  <% end %>
20
20
 
21
- <%= link_to helpers.spina.edit_admin_page_path(@page), class: "block text-spina font-medium text-sm p-4 hover:text-spina-dark", data: {turbo_frame: "_top"} do %>
22
- <%= @page.menu_title %>
21
+ <% if has_children? %>
22
+ <%= link_to helpers.spina.children_admin_page_path(page, sortable: sortable?, draggable: draggable?), class: "ml-2 -mr-3 px-1 h-7 font-medium rounded-md flex items-center text-spina hover:bg-spina/10 transition transition-bg transform", data: {turbo_frame: "page_#{page.id}_children", page_collapse_target: "button", action: "page-collapse#toggle"} do %>
23
+ <div data-page-collapse-target="indicator" class="transform transition <%= 'rotate-90' unless collapsed? %>">
24
+ <%= helpers.heroicon('chevron-right', style: :mini, class: 'w-5 h-5') %>
25
+ </div>
26
+ <% end %>
27
+ <% end %>
28
+
29
+ <%= link_to helpers.spina.edit_admin_page_path(page), class: "text-spina font-medium text-sm p-4 hover:text-spina-dark flex items-center", data: {turbo_frame: "_top"} do %>
30
+ <%= page.menu_title %>
23
31
 
24
- <small class="font-normal"><%= label %></small>
32
+ <small class="font-normal ml-2"><%= label %></small>
25
33
  <% end %>
26
34
 
27
35
  <div class='flex-1'></div>
28
36
  <div class="mr-3 flex space-x-2">
29
- <%= render Spina::Pages::TranslationsComponent.new(@page) %>
37
+ <%= render Spina::Pages::TranslationsComponent.new(page) %>
30
38
  </div>
31
39
 
32
40
  </div>
33
41
 
34
- <% if children.any? %>
35
- <%= render Spina::Pages::ListComponent.new(pages: children, sortable: sortable, draggable: draggable) %>
42
+ <% if has_children? %>
43
+ <turbo-frame id="page_<%= page.id %>_children" data-page-collapse-target="children" <%= 'hidden' if collapsed? %>>
44
+ <% unless collapsed? %>
45
+ <%= render Spina::Pages::ListComponent.new(pages: children, sortable: sortable?, draggable: draggable?) %>
46
+ <% end %>
47
+ </turbo-frame>
36
48
  <% end %>
37
- </turbo-frame>
49
+ </turbo-frame>
50
+
@@ -1,7 +1,7 @@
1
1
  module Spina
2
2
  module Pages
3
3
  class PageComponent < ApplicationComponent
4
- attr_reader :sortable, :draggable
4
+ attr_reader :page, :sortable, :draggable
5
5
 
6
6
  def initialize(page:, sortable: true, draggable: true)
7
7
  @page = page
@@ -25,9 +25,14 @@ module Spina
25
25
  def draggable?
26
26
  draggable
27
27
  end
28
+
29
+ # Pages are collapsed by default if they're inside a resource
30
+ def collapsed?
31
+ page.resource_id.present?
32
+ end
28
33
 
29
34
  def depth
30
- @page.depth
35
+ page.depth
31
36
  end
32
37
 
33
38
  def css_class
@@ -38,9 +43,16 @@ module Spina
38
43
  "pl-10 bg-gray-200"
39
44
  end
40
45
  end
46
+
47
+ # Explicitly check for "== 0" to account for older
48
+ # Spina setups where ancestry_children_count is still NULL
49
+ def has_children?
50
+ return false if page.ancestry_children_count == 0
51
+ page.has_children?
52
+ end
41
53
 
42
54
  def children
43
- @children ||= @page.children.active.sorted
55
+ @children ||= page.children.active.sorted
44
56
  end
45
57
  end
46
58
  end
@@ -4,7 +4,7 @@ module Spina
4
4
  before_action :set_breadcrumbs
5
5
 
6
6
  def index
7
- @attachments = Attachment.sorted.with_attached_file.page(params[:page]).per(25)
7
+ @attachments = Attachment.sorted.with_attached_file.with_filename(params[:query].to_s).page(params[:page]).per(25)
8
8
  end
9
9
 
10
10
  def show
@@ -6,7 +6,7 @@ module Spina
6
6
 
7
7
  def index
8
8
  @media_folders = MediaFolder.order(:name).includes(:images)
9
- @images = Image.sorted.where(media_folder: @media_folder).with_attached_file.page(params[:page]).per(25)
9
+ @images = Image.sorted.where(media_folder: @media_folder).with_attached_file.with_filename(params[:query].to_s).page(params[:page]).per(25)
10
10
  end
11
11
 
12
12
  def show
@@ -2,7 +2,7 @@ module Spina
2
2
  module Admin
3
3
  class MediaPickerController < AdminController
4
4
  def show
5
- @images = Spina::Image.sorted.with_attached_file.page(params[:page]).per(16)
5
+ @images = Spina::Image.sorted.with_attached_file.with_filename(params[:query].to_s).page(params[:page]).per(16)
6
6
 
7
7
  if (@media_folder = Spina::MediaFolder.find_by(id: params[:media_folder_id]))
8
8
  @images = @images.where(media_folder: @media_folder)
@@ -0,0 +1,24 @@
1
+ module Spina
2
+ module Admin
3
+ class PageSelectOptionsController < AdminController
4
+
5
+ def show
6
+ @page = Page.find(params[:id])
7
+ end
8
+
9
+ def index
10
+ end
11
+
12
+ def search
13
+ if params[:resource].present?
14
+ @pages = Resource.find_by(name: params[:resource])&.pages
15
+ end
16
+
17
+ @pages ||= Page.all
18
+ @pages = @pages.joins(:translations).where("spina_page_translations.title ILIKE :query OR materialized_path ILIKE :query", query: "%#{params[:search]}%").order(created_at: :desc).distinct.page(params[:page]).per(20)
19
+ render :index
20
+ end
21
+
22
+ end
23
+ end
24
+ end
@@ -108,7 +108,7 @@ module Spina
108
108
 
109
109
  redirect_to spina.admin_pages_url(resource_id: @page.resource_id)
110
110
  end
111
-
111
+
112
112
  private
113
113
 
114
114
  def set_locale
@@ -11,7 +11,7 @@ module Spina
11
11
  def create
12
12
  user = User.find_by(email: params[:email])
13
13
 
14
- if user&.reset_passord!
14
+ if user&.reset_password!
15
15
  UserMailer.forgot_password(user, request.user_agent).deliver_later
16
16
  redirect_to admin_login_path, flash: {success: t("spina.forgot_password.instructions_sent")}
17
17
  else
@@ -0,0 +1,21 @@
1
+ module Spina
2
+ module Attachable
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ has_one_attached :file
7
+
8
+ scope :with_filename, ->(query) do
9
+ joins(:file_blob).where(
10
+ "active_storage_blobs.filename ILIKE ?",
11
+ "%" + Image.sanitize_sql_like(query) + "%"
12
+ )
13
+ end
14
+ end
15
+
16
+ def name
17
+ file&.filename.to_s
18
+ end
19
+
20
+ end
21
+ end
@@ -1,15 +1,11 @@
1
1
  module Spina
2
2
  class Attachment < ApplicationRecord
3
- has_one_attached :file
4
-
3
+ include Attachable
4
+
5
5
  attr_accessor :_destroy
6
6
 
7
7
  scope :sorted, -> { order("created_at DESC") }
8
8
 
9
- def name
10
- file.filename.to_s
11
- end
12
-
13
9
  def content
14
10
  file if file.attached?
15
11
  end
@@ -1,15 +1,11 @@
1
1
  module Spina
2
2
  class Image < ApplicationRecord
3
+ include Attachable
4
+
3
5
  belongs_to :media_folder, optional: true
4
6
 
5
- has_one_attached :file
6
-
7
7
  scope :sorted, -> { order("created_at DESC") }
8
8
 
9
- def name
10
- file.try(:filename).to_s
11
- end
12
-
13
9
  def variant(options)
14
10
  return "" unless file.attached?
15
11
  return file if file.content_type.include?("svg")
@@ -13,6 +13,6 @@ module Spina
13
13
 
14
14
  validates :page, uniqueness: {scope: :navigation}
15
15
 
16
- delegate :menu_title, :materialized_path, :draft?, to: :page
16
+ delegate :menu_title, :materialized_path, :draft?, :homepage?, to: :page
17
17
  end
18
18
  end
@@ -10,7 +10,9 @@ module Spina
10
10
  attr_accessor :old_path
11
11
 
12
12
  # Orphaned pages are adopted by parent pages if available, otherwise become root
13
- has_ancestry orphan_strategy: :adopt
13
+ has_ancestry orphan_strategy: :adopt,
14
+ counter_cache: :ancestry_children_count,
15
+ cache_depth: true
14
16
 
15
17
  # Pages can belong to navigations (optional)
16
18
  has_many :navigation_items, dependent: :destroy
@@ -0,0 +1,13 @@
1
+ module Spina
2
+ module Parts
3
+ class PageLink < Base
4
+ attr_json :page_id, :integer, default: nil
5
+
6
+ attr_accessor :options
7
+
8
+ def content
9
+ Page.live.find_by(id: page_id)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -16,7 +16,7 @@ module Spina
16
16
  name
17
17
  end
18
18
 
19
- def reset_passord!
19
+ def reset_password!
20
20
  regenerate_password_reset_token
21
21
  self.password_reset_sent_at = Time.current
22
22
  save!
@@ -27,7 +27,13 @@
27
27
  <% end %>
28
28
  <% end %>
29
29
 
30
- <div class="my-8 border-t border-gray-200">
30
+ <div class="px-8 mt-4 flex items-center justify-end">
31
+ <%= form_with method: :get, url: spina.admin_attachments_path, data: {controller: "form", turbo_frame: "attachments"} do |f| %>
32
+ <%= render Spina::Forms::SearchComponent.new(f, :query) %>
33
+ <% end %>
34
+ </div>
35
+
36
+ <div class="my-4 border-t border-gray-200">
31
37
 
32
38
  <div data-controller="infinite-scroll">
33
39
  <%= turbo_frame_tag "attachments" do %>
@@ -41,7 +41,14 @@
41
41
  <% end %>
42
42
  <% end %>
43
43
 
44
- <div class="my-8 border-t border-gray-200">
44
+ <div class="px-8 mt-4 flex items-center justify-end">
45
+ <%= form_with method: :get, url: spina.admin_images_path, data: {controller: "form", turbo_frame: "images"} do |f| %>
46
+ <%= f.hidden_field :media_folder_id, value: @media_folder&.id %>
47
+ <%= render Spina::Forms::SearchComponent.new(f, :query) %>
48
+ <% end %>
49
+ </div>
50
+
51
+ <div class="my-4 border-t border-gray-200">
45
52
  <% if @media_folder.present? %>
46
53
  <div class="flex items-center h-12 hover:bg-white border-b border-gray-200 px-8">
47
54
  <%= link_to spina.admin_images_path, class: 'flex h-full items-center flex-1' do %>
@@ -0,0 +1,21 @@
1
+ <%= turbo_frame_tag "page_select_options_#{params[:object_id]}" do %>
2
+ <% if params[:search].blank? %>
3
+ <button type="button" data-action="page-select#clear reveal#hide" class="block hover:bg-gray-100 w-full text-left px-5 py-2 font-medium leading-5 text-gray-700 hover:text-gray-900 text-sm border-t border-gray-150">
4
+
5
+ </button>
6
+ <% end %>
7
+
8
+ <% @pages.each do |page| %>
9
+ <button type="button" data-action="page-select#select reveal#hide" data-title="<%= page.title %>" data-id="<%= page.id %>" class="block hover:bg-gray-100 w-full text-left px-5 py-2 font-medium leading-5 text-gray-700 hover:text-gray-900 text-sm border-t border-gray-150">
10
+ <%= page.title %>
11
+ <% if page.draft? %>
12
+ <span class="font-normal text-gray-400">
13
+ (<%=t "spina.pages.concept" %>)
14
+ </span>
15
+ <% end %>
16
+ <div class="text-xs font-normal text-gray-400">
17
+ <%= page.materialized_path %>
18
+ </div>
19
+ </button>
20
+ <% end %>
21
+ <% end %>
@@ -0,0 +1,3 @@
1
+ <%= turbo_frame_tag :page_title do %>
2
+ <%= @page.title %>
3
+ <% end %>
@@ -0,0 +1,3 @@
1
+ <%= turbo_frame_tag "page_#{@page.id}_children" do %>
2
+ <%= render Spina::Pages::ListComponent.new(pages: @children, sortable: params[:sortable] == "true", draggable: params[:draggable] == "true") %>
3
+ <% end %>
@@ -1,4 +1,7 @@
1
1
  <div class="mt-6">
2
2
  <label for="content" class="block text-sm leading-5 font-medium text-gray-700"><%= f.object.title %></label>
3
- <%= f.text_area :content, placeholder: f.object.title, class: "form-input mt-1 w-full" %>
3
+ <div class="text-gray-400 text-sm"><%= f.object.hint %></div>
4
+ <div class="mt-1">
5
+ <%= f.text_area :content, placeholder: f.object.title, class: "form-input mt-1 w-full" %>
6
+ </div>
4
7
  </div>
@@ -0,0 +1,36 @@
1
+ <div class="mt-6">
2
+ <label class="block text-sm leading-5 font-medium text-gray-700"><%= f.object.title %></label>
3
+ <div class="text-gray-400 text-sm"><%= f.object.hint %></div>
4
+
5
+ <div data-controller="page-select reveal" data-placeholder="<%=t "spina.pages.select_page" %>" class="relative mt-1" data-reveal-away-value>
6
+ <%= f.hidden_field :page_id, data: {page_select_target: "input"} %>
7
+
8
+ <button type="button" class="btn btn-default px-3 inline-flex items-center text-sm font-medium" data-action="reveal#toggle page-select#autofocus">
9
+ <%= heroicon("link", style: :mini, class: "w-4 h-4 mr-1 text-gray-600") %>
10
+ <div data-page-select-target="label">
11
+ <% if f.object.page_id.present? %>
12
+ <%= turbo_frame_tag :page_title, src: spina.admin_page_select_option_path(f.object.page_id) %>
13
+ <% end %>
14
+ </div>
15
+ </button>
16
+
17
+ <div class="relative mt-1">
18
+ <div data-reveal data-transition hidden class="absolute shadow-lg border border-gray-200 origin-top-right rounded-md z-10 top-0">
19
+ <div class="rounded-md bg-white shadow-xs">
20
+ <%= form_with url: spina.search_admin_page_select_options_path, data: {turbo_frame: "page_select_options_#{f.object.object_id}", controller: "form", debounce_time: 100} do |ff| %>
21
+ <%= ff.hidden_field :object_id, value: f.object.object_id %>
22
+ <%= ff.hidden_field :resource, value: f.object.options&.dig(:resource) %>
23
+ <div class="p-2">
24
+ <%= ff.search_field :search, placeholder: t("spina.search"), class: "form-input sticky top-0 text-sm w-80", data: {action: "input->form#submit focus->form#submit", page_select_target: "search"} %>
25
+ </div>
26
+ <% end %>
27
+
28
+ <div class="overflow-scroll max-h-80">
29
+ <%= turbo_frame_tag "page_select_options_#{f.object.object_id}" do %>
30
+ <% end %>
31
+ </div>
32
+ </div>
33
+ </div>
34
+ </div>
35
+ </div>
36
+ </div>