spina 2.13.1 → 2.14.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.
Potentially problematic release.
This version of spina might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/app/assets/builds/spina/tailwind.css +554 -589
- data/app/assets/javascripts/spina/controllers/form_controller.js +14 -1
- data/app/assets/javascripts/spina/controllers/page_collapse_controller.js +17 -0
- data/app/assets/javascripts/spina/controllers/page_select_controller.js +38 -0
- data/app/assets/javascripts/spina/libraries/debounce.js +65 -0
- data/app/components/spina/forms/search_component.html.erb +12 -0
- data/app/components/spina/forms/search_component.rb +12 -0
- data/app/components/spina/forms/trix_toolbar_component.html.erb +5 -6
- data/app/components/spina/media_picker/modal_component.html.erb +24 -16
- data/app/components/spina/pages/page_component.html.erb +23 -10
- data/app/components/spina/pages/page_component.rb +15 -3
- data/app/controllers/spina/admin/attachments_controller.rb +1 -1
- data/app/controllers/spina/admin/images_controller.rb +1 -1
- data/app/controllers/spina/admin/media_picker_controller.rb +1 -1
- data/app/controllers/spina/admin/page_select_options_controller.rb +24 -0
- data/app/controllers/spina/admin/pages_controller.rb +1 -1
- data/app/controllers/spina/admin/password_resets_controller.rb +1 -1
- data/app/models/concerns/spina/attachable.rb +21 -0
- data/app/models/spina/attachment.rb +2 -6
- data/app/models/spina/image.rb +2 -6
- data/app/models/spina/navigation_item.rb +1 -1
- data/app/models/spina/page.rb +3 -1
- data/app/models/spina/parts/page_link.rb +13 -0
- data/app/models/spina/user.rb +1 -1
- data/app/views/spina/admin/attachments/index.html.erb +7 -1
- data/app/views/spina/admin/images/index.html.erb +8 -1
- data/app/views/spina/admin/page_select_options/index.html.erb +21 -0
- data/app/views/spina/admin/page_select_options/show.html.erb +3 -0
- data/app/views/spina/admin/pages/children.html.erb +3 -0
- data/app/views/spina/admin/parts/multi_lines/_form.html.erb +4 -1
- data/app/views/spina/admin/parts/page_links/_form.html.erb +36 -0
- data/config/initializers/importmap.rb +6 -0
- data/config/locales/en.yml +1 -0
- data/config/locales/nl.yml +43 -0
- data/config/routes.rb +8 -3
- data/db/migrate/16_add_ancestry_cache_columns_to_spina_pages.rb +6 -0
- data/lib/spina/attr_json_spina_parts_model.rb +1 -1
- data/lib/spina/engine.rb +2 -1
- data/lib/spina/version.rb +1 -1
- 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
|
-
|
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>
|
@@ -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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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="
|
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_<%=
|
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(
|
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(
|
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
|
-
|
22
|
-
<%=
|
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(
|
37
|
+
<%= render Spina::Pages::TranslationsComponent.new(page) %>
|
30
38
|
</div>
|
31
39
|
|
32
40
|
</div>
|
33
41
|
|
34
|
-
<% if
|
35
|
-
<%=
|
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
|
-
|
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 ||=
|
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
|
@@ -11,7 +11,7 @@ module Spina
|
|
11
11
|
def create
|
12
12
|
user = User.find_by(email: params[:email])
|
13
13
|
|
14
|
-
if user&.
|
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
|
-
|
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
|
data/app/models/spina/image.rb
CHANGED
@@ -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")
|
data/app/models/spina/page.rb
CHANGED
@@ -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
|
data/app/models/spina/user.rb
CHANGED
@@ -27,7 +27,13 @@
|
|
27
27
|
<% end %>
|
28
28
|
<% end %>
|
29
29
|
|
30
|
-
<div class="
|
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="
|
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 %>
|
@@ -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
|
-
|
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>
|