spina 2.13.1 → 2.15.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.
- 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/new_page_button_component.html.erb +2 -2
- 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/accounts/edit.html.erb +1 -1
- data/app/views/spina/admin/attachments/index.html.erb +9 -3
- data/app/views/spina/admin/images/_image.html.erb +2 -2
- data/app/views/spina/admin/images/index.html.erb +10 -3
- data/app/views/spina/admin/layout/edit.html.erb +1 -1
- data/app/views/spina/admin/navigations/edit.html.erb +1 -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/_form.html.erb +3 -3
- data/app/views/spina/admin/pages/children.html.erb +3 -0
- data/app/views/spina/admin/pages/index.html.erb +2 -2
- 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/app/views/spina/admin/resources/edit.html.erb +1 -1
- data/app/views/spina/admin/settings/edit.html.erb +1 -1
- data/app/views/spina/admin/shared/_navigation.html.erb +4 -4
- data/app/views/spina/admin/theme/edit.html.erb +1 -1
- data/app/views/spina/admin/users/_form.html.erb +1 -1
- data/app/views/spina/admin/users/index.html.erb +1 -1
- data/config/initializers/importmap.rb +6 -0
- data/config/locales/de.yml +1 -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 +26 -19
@@ -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,12 +1,12 @@
|
|
1
1
|
<% if view_templates.many? %>
|
2
2
|
|
3
3
|
<%= render Spina::UserInterface::DropdownComponent.new do |dropdown| %>
|
4
|
-
<% dropdown.
|
4
|
+
<% dropdown.with_button(classes: "btn btn-primary w-full") do %>
|
5
5
|
<%= helpers.heroicon("document-plus", style: :solid, class: "w-5 h-5 -ml-1 mr-1") %>
|
6
6
|
<%=t 'spina.pages.new' %>
|
7
7
|
<% end %>
|
8
8
|
|
9
|
-
<% dropdown.
|
9
|
+
<% dropdown.with_menu do %>
|
10
10
|
<% view_templates.each do |template| %>
|
11
11
|
<%= link_to helpers.spina.new_admin_page_path(view_template: template.name, resource_id: @resource&.id), class: "block px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900", data: {turbo_frame: "modal", action: "reveal#hide"} do %>
|
12
12
|
<div class="font-medium text-gray-700">
|
@@ -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
@@ -1,7 +1,7 @@
|
|
1
1
|
<%= form_with model: current_spina_account, url: spina.admin_account_path do |f| %>
|
2
2
|
|
3
3
|
<%= render Spina::UserInterface::HeaderComponent.new do |header| %>
|
4
|
-
<% header.
|
4
|
+
<% header.with_actions do %>
|
5
5
|
|
6
6
|
<%= button_tag type: :submit, class: 'btn btn-primary', data: {controller: "button hotkeys", hotkeys: "command+s, ctrl+s", hotkeys_target: "button", action: "button#loading", loading_message: t("spina.ui.saving")} do %>
|
7
7
|
<%= heroicon('check', style: :mini, class: 'w-5 h-5 -ml-1 mr-1') %>
|
@@ -1,5 +1,5 @@
|
|
1
1
|
<%= render Spina::UserInterface::HeaderComponent.new do |header| %>
|
2
|
-
<% header.
|
2
|
+
<% header.with_actions do %>
|
3
3
|
<%= form_with model: [:admin, Spina::Attachment.new], url: spina.admin_attachments_path, data: {controller: "form loading-button", loading_message: t('spina.media_library.uploading'), action: "turbo:submit-end->loading-button#doneLoading"} do |f| %>
|
4
4
|
|
5
5
|
<%= f.file_field :files, multiple: true, id: "new_attachment_file_field", class: 'hidden', data: {action: "loading-button#loading form#requestSubmit"} %>
|
@@ -10,7 +10,7 @@
|
|
10
10
|
</button>
|
11
11
|
<% end %>
|
12
12
|
<% end %>
|
13
|
-
<% header.
|
13
|
+
<% header.with_navigation do %>
|
14
14
|
<nav class="-mb-3 mt-4">
|
15
15
|
<ul class="inline-flex w-auto rounded-md bg-white">
|
16
16
|
<%= render Spina::UserInterface::TabLinkComponent.new(spina.admin_images_path) do %>
|
@@ -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 %>
|
@@ -27,13 +27,13 @@
|
|
27
27
|
|
28
28
|
<% if @media_folders.present? %>
|
29
29
|
<%= render Spina::UserInterface::DropdownComponent.new do |dropdown| %>
|
30
|
-
<% dropdown.
|
30
|
+
<% dropdown.with_button(classes: 'btn btn-default h-7 px-2 text-xs') do %>
|
31
31
|
<%= heroicon('folder', style: :mini, class: 'w-4 h-4 mr-1 text-gray-600') %>
|
32
32
|
<%=t 'spina.ui.move_to' %>
|
33
33
|
<%= heroicon('chevron-down', style: :solid, class: 'w-4 h-4') %>
|
34
34
|
<% end %>
|
35
35
|
|
36
|
-
<% dropdown.
|
36
|
+
<% dropdown.with_menu do %>
|
37
37
|
<% if image.media_folder_id.present? %>
|
38
38
|
<%= form_with model: image, url: spina.admin_image_path(image) do |f| %>
|
39
39
|
<%= f.hidden_field :media_folder_id, value: nil %>
|
@@ -1,5 +1,5 @@
|
|
1
1
|
<%= render Spina::UserInterface::HeaderComponent.new do |header| %>
|
2
|
-
<% header.
|
2
|
+
<% header.with_actions do %>
|
3
3
|
<% if @media_folder.blank? %>
|
4
4
|
<%= link_to spina.new_admin_media_folder_path, class: "btn btn-default", data: {turbo_frame: "modal"} do %>
|
5
5
|
<%= heroicon("folder", style: :solid, class: 'w-4 h-4 mr-1 -ml-1') %>
|
@@ -24,7 +24,7 @@
|
|
24
24
|
<% end %>
|
25
25
|
<% end %>
|
26
26
|
|
27
|
-
<% header.
|
27
|
+
<% header.with_navigation do %>
|
28
28
|
<nav class="-mb-3 mt-4">
|
29
29
|
<ul class="inline-flex w-auto rounded-md bg-white">
|
30
30
|
<%= render Spina::UserInterface::TabLinkComponent.new(spina.admin_images_path, active: true) 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 %>
|
@@ -1,5 +1,5 @@
|
|
1
1
|
<%= render Spina::UserInterface::HeaderComponent.new do |header| %>
|
2
|
-
<% header.
|
2
|
+
<% header.with_actions do %>
|
3
3
|
<%= render Spina::UserInterface::TranslationsComponent.new(@account, label: @locale.upcase) %>
|
4
4
|
|
5
5
|
<% if Spina::Current.theme.layout_parts.any? %>
|