alchemy_cms 6.0.0.pre.rc5 → 6.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +16 -8
- data/.github/workflows/stale.yml +21 -7
- data/.gitignore +0 -1
- data/.rspec +1 -0
- data/CHANGELOG.md +89 -0
- data/Gemfile +10 -7
- data/Rakefile +5 -1
- data/alchemy_cms.gemspec +3 -3
- data/app/assets/javascripts/alchemy/admin.js +0 -2
- data/app/assets/javascripts/alchemy/alchemy.dialog.js.coffee +6 -1
- data/app/assets/javascripts/alchemy/alchemy.link_dialog.js.coffee +2 -0
- data/app/assets/javascripts/alchemy/page_select.js +13 -8
- data/app/assets/javascripts/alchemy/templates/index.js +1 -0
- data/app/assets/javascripts/alchemy/templates/page.hbs +17 -7
- data/app/assets/javascripts/alchemy/templates/page_folder.hbs +3 -0
- data/app/assets/stylesheets/alchemy/archive.scss +9 -0
- data/app/assets/stylesheets/alchemy/elements.scss +4 -0
- data/app/assets/stylesheets/alchemy/page-select.scss +29 -4
- data/app/assets/stylesheets/alchemy/sitemap.scss +9 -7
- data/app/controllers/alchemy/admin/elements_controller.rb +2 -6
- data/app/controllers/alchemy/admin/pages_controller.rb +16 -18
- data/app/controllers/alchemy/api/contents_controller.rb +1 -5
- data/app/controllers/alchemy/api/elements_controller.rb +2 -6
- data/app/controllers/alchemy/api/pages_controller.rb +16 -10
- data/app/helpers/alchemy/elements_helper.rb +2 -2
- data/app/models/alchemy/ingredient.rb +6 -1
- data/app/models/alchemy/page.rb +3 -2
- data/app/models/alchemy/picture.rb +1 -1
- data/app/serializers/alchemy/page_serializer.rb +7 -1
- data/app/serializers/alchemy/page_tree_serializer.rb +3 -3
- data/app/views/alchemy/admin/pages/_form.html.erb +19 -0
- data/app/views/alchemy/admin/pages/_new_page_form.html.erb +16 -5
- data/app/views/alchemy/admin/pages/_page.html.erb +111 -133
- data/app/views/alchemy/admin/pages/_sitemap.html.erb +10 -16
- data/app/views/alchemy/admin/pages/_toolbar.html.erb +0 -12
- data/app/views/alchemy/admin/pages/edit.html.erb +1 -1
- data/app/views/alchemy/admin/pages/index.html.erb +1 -1
- data/app/views/alchemy/admin/pages/update.js.erb +7 -5
- data/app/views/alchemy/admin/partials/_routes.html.erb +12 -1
- data/app/views/alchemy/admin/resources/_form.html.erb +5 -0
- data/app/views/alchemy/essences/_essence_page_editor.html.erb +1 -1
- data/config/alchemy/config.yml +1 -0
- data/config/locales/alchemy.en.yml +0 -4
- data/config/routes.rb +4 -2
- data/lib/alchemy/engine.rb +12 -1
- data/lib/alchemy/essence.rb +0 -26
- data/lib/alchemy/permissions.rb +0 -1
- data/lib/alchemy/resource.rb +16 -1
- data/lib/alchemy/test_support/essence_shared_examples.rb +0 -12
- data/lib/alchemy/test_support/shared_ingredient_examples.rb +4 -2
- data/lib/alchemy/upgrader/tasks/ingredients_migrator.rb +1 -1
- data/lib/alchemy/version.rb +1 -1
- data/lib/generators/alchemy/install/install_generator.rb +6 -1
- data/package/admin.js +5 -1
- data/package/src/image_loader.js +4 -2
- data/package/src/node_tree.js +13 -6
- data/package/src/page_publication_fields.js +28 -0
- data/package/src/page_sorter.js +62 -0
- data/package/src/picture_editors.js +8 -8
- data/package/src/sitemap.js +148 -0
- data/package/src/utils/__tests__/ajax.spec.js +52 -16
- data/package/src/utils/ajax.js +12 -0
- data/package.json +1 -1
- metadata +43 -43
- data/app/assets/javascripts/alchemy/alchemy.page_sorter.js +0 -24
- data/app/assets/javascripts/alchemy/alchemy.sitemap.js.coffee +0 -119
- data/app/views/alchemy/admin/pages/fold.js.erb +0 -2
- data/app/views/alchemy/admin/pages/sort.html.erb +0 -19
- data/vendor/assets/javascripts/jquery_plugins/jquery.ui.nestedSortable.js +0 -434
@@ -5,6 +5,10 @@
|
|
5
5
|
return '<%= alchemy.admin_picture_path(id: 1) %>'.replace(/1/, id);
|
6
6
|
},
|
7
7
|
|
8
|
+
url_admin_picture_path: function(id) {
|
9
|
+
return '<%= alchemy.url_admin_picture_path(id: 1) %>'.replace(/1/, id);
|
10
|
+
},
|
11
|
+
|
8
12
|
fold_admin_element_path: function(id) {
|
9
13
|
return '<%= alchemy.fold_admin_element_path(id: 1) %>'.replace(/1/, id);
|
10
14
|
},
|
@@ -19,8 +23,15 @@
|
|
19
23
|
},
|
20
24
|
},
|
21
25
|
|
26
|
+
move_admin_page_path: function(id) {
|
27
|
+
return '<%= alchemy.move_api_page_path(id: 1) %>'.replace(/1/, id);
|
28
|
+
},
|
29
|
+
|
30
|
+
fold_admin_page_path: function(id) {
|
31
|
+
return '<%= alchemy.fold_admin_page_path(id: 1) %>'.replace(/1/, id);
|
32
|
+
},
|
33
|
+
|
22
34
|
order_admin_elements_path: '<%= alchemy.order_admin_elements_path %>',
|
23
|
-
order_admin_pages_path: '<%= alchemy.order_admin_pages_path %>',
|
24
35
|
link_admin_pages_path: '<%= alchemy.link_admin_pages_path %>',
|
25
36
|
api_pages_path: '<%= alchemy.api_pages_path %>',
|
26
37
|
api_elements_path: '<%= alchemy.api_elements_path %>'
|
@@ -8,6 +8,11 @@
|
|
8
8
|
input_html: {class: 'alchemy_selectbox'} %>
|
9
9
|
<% elsif attribute[:type].in? %i[date time datetime] %>
|
10
10
|
<%= f.datepicker attribute[:name], resource_attribute_field_options(attribute) %>
|
11
|
+
<% elsif attribute[:enum].present? %>
|
12
|
+
<%= f.input attribute[:name],
|
13
|
+
collection: attribute[:enum],
|
14
|
+
include_blank: Alchemy.t(:blank, scope: 'resources.relation_select'),
|
15
|
+
input_html: {class: 'alchemy_selectbox'} %>
|
11
16
|
<% else %>
|
12
17
|
<%= f.input attribute[:name], resource_attribute_field_options(attribute) %>
|
13
18
|
<% end %>
|
@@ -14,7 +14,7 @@
|
|
14
14
|
<script>
|
15
15
|
$('#<%= essence_page_editor.form_field_id %>').alchemyPageSelect({
|
16
16
|
placeholder: "<%= Alchemy.t(:search_page) %>",
|
17
|
-
url: "<%= alchemy.api_pages_path %>",
|
17
|
+
url: "<%= alchemy.api_pages_path language_id: essence_page_editor.page&.language_id %>",
|
18
18
|
query_params: <%== essence_page_editor.settings[:query_params].to_json %>,
|
19
19
|
<% if essence_page_editor.essence.page %>
|
20
20
|
initialSelection: {
|
data/config/alchemy/config.yml
CHANGED
@@ -334,7 +334,6 @@ en:
|
|
334
334
|
"Site successfully removed": "Website successfully removed."
|
335
335
|
"Site successfully updated": "Website successfully updated."
|
336
336
|
"Size": "Size"
|
337
|
-
"Sort pages": "Reorder pages"
|
338
337
|
"Successfully added content": "Successfully added %{content}"
|
339
338
|
"Successfully deleted content": "Successfully deleted %{content}"
|
340
339
|
"Successfully deleted element": "Successfully deleted %{element}"
|
@@ -437,7 +436,6 @@ en:
|
|
437
436
|
enter_external_link: "Please enter the URL you want to link with"
|
438
437
|
explain_cropping: "<p>Move the frame and change its size with the mouse or arrow keys to adjust the image mask. Click on \"apply\" when you are satisfied with your selection.</p><p>If you want to return to the original centered image mask like it was defined in the layout, click \"reset\" and \"apply\" afterwards.</p>"
|
439
438
|
explain_publishing: "Publish current page content"
|
440
|
-
explain_sitemap_dragndrop_sorting: "Tip: Drag the pages at the icon in order to sort them."
|
441
439
|
explain_unlocking: "Leave page and unlock it for other users."
|
442
440
|
external_link_notice_1: "Please enter the complete url with http:// or a similar protocol."
|
443
441
|
external_link_notice_2: "To refer a path from your website url, start with a /."
|
@@ -534,7 +532,6 @@ en:
|
|
534
532
|
or_replace_it_with_an_existing_tag: 'Or replace it with an existing tag'
|
535
533
|
"Page created": "Page: '%{name}' created."
|
536
534
|
page_infos: 'Page info'
|
537
|
-
page_layout_changed_notice: "Page type was changed. Elements not usable anymore have been hided."
|
538
535
|
page_properties: "Page properties"
|
539
536
|
page_public: "published"
|
540
537
|
page_published: "Published page"
|
@@ -593,7 +590,6 @@ en:
|
|
593
590
|
robot_follow: "robot may follow links."
|
594
591
|
robot_index: "allow robot to index."
|
595
592
|
save: "Save"
|
596
|
-
"save order": "Save order"
|
597
593
|
saved_link: "Link saved."
|
598
594
|
search: "search"
|
599
595
|
search_engines: "Search engines"
|
data/config/routes.rb
CHANGED
@@ -28,13 +28,12 @@ Alchemy::Engine.routes.draw do
|
|
28
28
|
post :copy_language_tree
|
29
29
|
get :create_language
|
30
30
|
get :link
|
31
|
-
get :sort
|
32
31
|
get :tree
|
33
32
|
end
|
34
33
|
member do
|
35
34
|
post :unlock
|
36
35
|
post :publish
|
37
|
-
|
36
|
+
patch :fold
|
38
37
|
get :configure
|
39
38
|
get :preview
|
40
39
|
get :info
|
@@ -139,6 +138,9 @@ Alchemy::Engine.routes.draw do
|
|
139
138
|
collection do
|
140
139
|
get :nested
|
141
140
|
end
|
141
|
+
member do
|
142
|
+
patch :move
|
143
|
+
end
|
142
144
|
end
|
143
145
|
|
144
146
|
get "/pages/*urlname(.:format)" => "pages#show", as: "page"
|
data/lib/alchemy/engine.rb
CHANGED
@@ -37,7 +37,7 @@ module Alchemy
|
|
37
37
|
end
|
38
38
|
end
|
39
39
|
|
40
|
-
|
40
|
+
config.after_initialize do
|
41
41
|
if Alchemy.user_class
|
42
42
|
ActiveSupport.on_load(:active_record) do
|
43
43
|
Alchemy.user_class.model_stamper
|
@@ -45,5 +45,16 @@ module Alchemy
|
|
45
45
|
end
|
46
46
|
end
|
47
47
|
end
|
48
|
+
|
49
|
+
initializer "alchemy.webp-mime_type" do
|
50
|
+
# Rails does not know anything about webp even in 2022
|
51
|
+
unless Mime::Type.lookup_by_extension(:webp)
|
52
|
+
Mime::Type.register("image/webp", :webp)
|
53
|
+
end
|
54
|
+
# Dragonfly uses Rack to read the mime type and guess what
|
55
|
+
unless Rack::Mime::MIME_TYPES[".webp"]
|
56
|
+
Rack::Mime::MIME_TYPES[".webp"] = "image/webp"
|
57
|
+
end
|
58
|
+
end
|
48
59
|
end
|
49
60
|
end
|
data/lib/alchemy/essence.rb
CHANGED
@@ -3,18 +3,6 @@
|
|
3
3
|
require "active_record"
|
4
4
|
|
5
5
|
module Alchemy #:nodoc:
|
6
|
-
# A bogus association that skips eager loading for essences not having an ingredient association
|
7
|
-
class IngredientAssociation < ActiveRecord::Associations::BelongsToAssociation
|
8
|
-
# Skip eager loading if called by Rails' preloader
|
9
|
-
def klass
|
10
|
-
if caller.any? { |line| line =~ /preloader\.rb/ }
|
11
|
-
nil
|
12
|
-
else
|
13
|
-
super
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
6
|
module Essence #:nodoc:
|
19
7
|
def self.included(base)
|
20
8
|
base.extend(ClassMethods)
|
@@ -43,8 +31,6 @@ module Alchemy #:nodoc:
|
|
43
31
|
ingredient_column: "body",
|
44
32
|
}.update(options)
|
45
33
|
|
46
|
-
@_classes_with_ingredient_association ||= []
|
47
|
-
|
48
34
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
49
35
|
attr_writer :validation_errors
|
50
36
|
include Alchemy::Essence::InstanceMethods
|
@@ -87,18 +73,6 @@ module Alchemy #:nodoc:
|
|
87
73
|
alias_method :#{configuration[:ingredient_column]}, :ingredient_association
|
88
74
|
alias_method :#{configuration[:ingredient_column]}=, :ingredient_association=
|
89
75
|
RUBY
|
90
|
-
|
91
|
-
@_classes_with_ingredient_association << self
|
92
|
-
end
|
93
|
-
end
|
94
|
-
|
95
|
-
# Overwrite ActiveRecords method to return a bogus association class that skips eager loading
|
96
|
-
# for essence classes that do not have an ingredient association
|
97
|
-
def _reflect_on_association(name)
|
98
|
-
if name == :ingredient_association && !in?(@_classes_with_ingredient_association)
|
99
|
-
OpenStruct.new(association_class: Alchemy::IngredientAssociation)
|
100
|
-
else
|
101
|
-
super
|
102
76
|
end
|
103
77
|
end
|
104
78
|
|
data/lib/alchemy/permissions.rb
CHANGED
data/lib/alchemy/resource.rb
CHANGED
@@ -169,10 +169,25 @@ module Alchemy
|
|
169
169
|
name: col.name,
|
170
170
|
type: resource_column_type(col),
|
171
171
|
relation: resource_relation(col.name),
|
172
|
-
|
172
|
+
enum: enum_values_collection_for_select(col.name),
|
173
|
+
}.delete_if { |_k, v| v.blank? }
|
173
174
|
end.compact
|
174
175
|
end
|
175
176
|
|
177
|
+
def enum_values_collection_for_select(column_name)
|
178
|
+
enum = model.defined_enums[column_name]
|
179
|
+
return if enum.blank?
|
180
|
+
|
181
|
+
enum.keys.map do |key|
|
182
|
+
[
|
183
|
+
::I18n.t(key, scope: [
|
184
|
+
:activerecord, :attributes, model.model_name.i18n_key, "#{column_name}_values"
|
185
|
+
], default: key.humanize),
|
186
|
+
key,
|
187
|
+
]
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
176
191
|
def sorted_attributes
|
177
192
|
@_sorted_attributes ||= attributes.
|
178
193
|
sort_by { |attr| attr[:name] == "name" ? 0 : 1 }.
|
@@ -7,18 +7,6 @@ RSpec.shared_examples_for "an essence" do
|
|
7
7
|
let(:content) { Alchemy::Content.new(name: "foo") }
|
8
8
|
let(:content_definition) { { "name" => "foo" } }
|
9
9
|
|
10
|
-
describe "eager loading" do
|
11
|
-
before do
|
12
|
-
2.times { described_class.create! }
|
13
|
-
end
|
14
|
-
|
15
|
-
it "does not throw error if eager loaded" do
|
16
|
-
expect {
|
17
|
-
described_class.all.includes(:ingredient_association).to_a
|
18
|
-
}.to_not raise_error
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
10
|
it "touches the element after save" do
|
23
11
|
element = FactoryBot.create(:alchemy_element)
|
24
12
|
content = FactoryBot.create(:alchemy_content, element: element, essence: essence, essence_type: essence.class.name)
|
@@ -28,7 +28,7 @@ RSpec.shared_examples_for "an alchemy ingredient" do
|
|
28
28
|
|
29
29
|
context "with element" do
|
30
30
|
before do
|
31
|
-
expect(element).to receive(:ingredient_definition_for) do
|
31
|
+
expect(element).to receive(:ingredient_definition_for).at_least(:once) do
|
32
32
|
{
|
33
33
|
settings: {
|
34
34
|
linkable: true,
|
@@ -63,7 +63,9 @@ RSpec.shared_examples_for "an alchemy ingredient" do
|
|
63
63
|
end
|
64
64
|
|
65
65
|
before do
|
66
|
-
expect(element).to receive(:ingredient_definition_for)
|
66
|
+
expect(element).to receive(:ingredient_definition_for).at_least(:once) do
|
67
|
+
definition
|
68
|
+
end
|
67
69
|
end
|
68
70
|
|
69
71
|
it "returns ingredient definition" do
|
@@ -16,7 +16,7 @@ module Alchemy::Upgrader::Tasks
|
|
16
16
|
# eager load all elements that have ingredients defined but no ingredient records yet.
|
17
17
|
all_elements = Alchemy::Element
|
18
18
|
.named(elements_with_ingredients.map { |d| d[:name] })
|
19
|
-
.includes(contents:
|
19
|
+
.includes(contents: :essence)
|
20
20
|
.left_outer_joins(:ingredients).where(alchemy_ingredients: { id: nil })
|
21
21
|
.to_a
|
22
22
|
elements_with_ingredients.map do |element_definition|
|
data/lib/alchemy/version.rb
CHANGED
@@ -23,6 +23,11 @@ module Alchemy
|
|
23
23
|
default: false,
|
24
24
|
desc: "Skip running the webpacker installer."
|
25
25
|
|
26
|
+
class_option :skip_db_create,
|
27
|
+
type: :boolean,
|
28
|
+
default: false,
|
29
|
+
desc: "Skip creting the database during install."
|
30
|
+
|
26
31
|
source_root File.expand_path("files", __dir__)
|
27
32
|
|
28
33
|
def setup
|
@@ -104,7 +109,7 @@ module Alchemy
|
|
104
109
|
end
|
105
110
|
|
106
111
|
def setup_database
|
107
|
-
rake("db:create", abort_on_failure: true)
|
112
|
+
rake("db:create", abort_on_failure: true) unless options[:skip_db_create]
|
108
113
|
# We can't invoke this rake task, because Rails will use wrong engine names otherwise
|
109
114
|
rake("railties:install:migrations", abort_on_failure: true)
|
110
115
|
rake("db:migrate", abort_on_failure: true)
|
data/package/admin.js
CHANGED
@@ -6,6 +6,8 @@ import pictureEditors from "./src/picture_editors"
|
|
6
6
|
import ImageLoader from "./src/image_loader"
|
7
7
|
import ImageCropper from "./src/image_cropper"
|
8
8
|
import Datepicker from "./src/datepicker"
|
9
|
+
import Sitemap from "./src/sitemap"
|
10
|
+
import PagePublicationFields from "./src/page_publication_fields.js"
|
9
11
|
|
10
12
|
// Global Alchemy object
|
11
13
|
if (typeof window.Alchemy === "undefined") {
|
@@ -22,5 +24,7 @@ Object.assign(Alchemy, {
|
|
22
24
|
pictureEditors,
|
23
25
|
ImageLoader: ImageLoader.init,
|
24
26
|
ImageCropper,
|
25
|
-
Datepicker
|
27
|
+
Datepicker,
|
28
|
+
Sitemap,
|
29
|
+
PagePublicationFields
|
26
30
|
})
|
data/package/src/image_loader.js
CHANGED
@@ -39,9 +39,11 @@ export default class ImageLoader {
|
|
39
39
|
this.unbind()
|
40
40
|
}
|
41
41
|
|
42
|
-
onError() {
|
42
|
+
onError(evt) {
|
43
|
+
const message = `Could not load "${this.image.src}"`
|
43
44
|
this.removeSpinner()
|
44
|
-
this.parent.
|
45
|
+
this.parent.innerHTML = `<span class="icon error fas fa-exclamation-triangle" title="${message}" />`
|
46
|
+
console.error(message, evt)
|
45
47
|
this.unbind()
|
46
48
|
}
|
47
49
|
|
data/package/src/node_tree.js
CHANGED
@@ -1,12 +1,16 @@
|
|
1
1
|
import Sortable from "sortablejs"
|
2
|
-
import
|
2
|
+
import { patch } from "./utils/ajax"
|
3
3
|
import { on } from "./utils/events"
|
4
4
|
|
5
5
|
function displayNodeFolders() {
|
6
6
|
document.querySelectorAll("li.menu-item").forEach((el) => {
|
7
7
|
const leftIconArea = el.querySelector(".nodes_tree-left_images")
|
8
8
|
const list = el.querySelector(".children")
|
9
|
-
const node = {
|
9
|
+
const node = {
|
10
|
+
folded: el.dataset.folded === "true",
|
11
|
+
id: el.dataset.id,
|
12
|
+
type: el.dataset.type
|
13
|
+
}
|
10
14
|
|
11
15
|
if (list.children.length > 0 || node.folded) {
|
12
16
|
leftIconArea.innerHTML = HandlebarsTemplates.node_folder({ node: node })
|
@@ -17,13 +21,15 @@ function displayNodeFolders() {
|
|
17
21
|
}
|
18
22
|
|
19
23
|
function onFinishDragging(evt) {
|
20
|
-
const url = Alchemy.routes[evt.item.dataset.type].move_api_path(
|
24
|
+
const url = Alchemy.routes[evt.item.dataset.type].move_api_path(
|
25
|
+
evt.item.dataset.id
|
26
|
+
)
|
21
27
|
const data = {
|
22
28
|
target_parent_id: evt.to.dataset.recordId,
|
23
29
|
new_position: evt.newIndex
|
24
30
|
}
|
25
31
|
|
26
|
-
|
32
|
+
patch(url, data)
|
27
33
|
.then(() => {
|
28
34
|
const message = Alchemy.t("Successfully moved menu item")
|
29
35
|
Alchemy.growl(message)
|
@@ -38,10 +44,11 @@ function handleNodeFolders() {
|
|
38
44
|
on("click", ".nodes_tree", ".node_folder", function () {
|
39
45
|
const nodeId = this.dataset.recordId
|
40
46
|
const menu_item = this.closest("li.menu-item")
|
41
|
-
const url =
|
47
|
+
const url =
|
48
|
+
Alchemy.routes[this.dataset.recordType].toggle_folded_api_path(nodeId)
|
42
49
|
const list = menu_item.querySelector(".children")
|
43
50
|
|
44
|
-
|
51
|
+
patch(url)
|
45
52
|
.then(() => {
|
46
53
|
list.classList.toggle("folded")
|
47
54
|
menu_item.dataset.folded =
|
@@ -0,0 +1,28 @@
|
|
1
|
+
// Handles the page publication date fields
|
2
|
+
export default function () {
|
3
|
+
document.addEventListener("DialogReady.Alchemy", function (evt) {
|
4
|
+
const dialog = evt.detail.body
|
5
|
+
const public_on_field = dialog.querySelector("#page_public_on")
|
6
|
+
const public_until_field = dialog.querySelector("#page_public_until")
|
7
|
+
const publication_date_fields = dialog.querySelector(
|
8
|
+
".page-publication-date-fields"
|
9
|
+
)
|
10
|
+
const public_field = dialog.querySelector("#page_public")
|
11
|
+
|
12
|
+
if(!public_field) return
|
13
|
+
|
14
|
+
public_field.addEventListener("click", function (evt) {
|
15
|
+
const checkbox = evt.target
|
16
|
+
const now = new Date()
|
17
|
+
|
18
|
+
if (checkbox.checked) {
|
19
|
+
publication_date_fields.classList.remove("hidden")
|
20
|
+
public_on_field._flatpickr.setDate(now)
|
21
|
+
} else {
|
22
|
+
publication_date_fields.classList.add("hidden")
|
23
|
+
public_on_field.value = ""
|
24
|
+
}
|
25
|
+
public_until_field.value = ""
|
26
|
+
})
|
27
|
+
})
|
28
|
+
}
|
@@ -0,0 +1,62 @@
|
|
1
|
+
import Sortable from "sortablejs"
|
2
|
+
import { patch } from "./utils/ajax"
|
3
|
+
|
4
|
+
function onFinishDragging(evt) {
|
5
|
+
const pageId = evt.item.dataset.pageId
|
6
|
+
const url = Alchemy.routes.move_admin_page_path(pageId)
|
7
|
+
const data = {
|
8
|
+
target_parent_id: evt.to.dataset.parentId,
|
9
|
+
new_position: evt.newIndex
|
10
|
+
}
|
11
|
+
|
12
|
+
patch(url, data)
|
13
|
+
.then(async (response) => {
|
14
|
+
const pageData = await response.data
|
15
|
+
const pageEl = document.getElementById(`page_${pageId}`)
|
16
|
+
const urlPathEl = pageEl.querySelector(".sitemap_url")
|
17
|
+
|
18
|
+
Alchemy.growl(Alchemy.t("Successfully moved page"))
|
19
|
+
urlPathEl.textContent = pageData.url_path
|
20
|
+
displayPageFolders()
|
21
|
+
})
|
22
|
+
.catch((error) => {
|
23
|
+
Alchemy.growl(error.message || error, "error")
|
24
|
+
})
|
25
|
+
}
|
26
|
+
|
27
|
+
export function displayPageFolders() {
|
28
|
+
document.querySelectorAll("li.sitemap-item").forEach((el) => {
|
29
|
+
const pageFolderEl = el.querySelector(".page_folder")
|
30
|
+
const list = el.querySelector(".children")
|
31
|
+
const page = {
|
32
|
+
folded: el.dataset.folded === "true",
|
33
|
+
id: el.dataset.pageId,
|
34
|
+
type: el.dataset.type
|
35
|
+
}
|
36
|
+
|
37
|
+
if (list.children.length > 0 || page.folded) {
|
38
|
+
pageFolderEl.outerHTML = HandlebarsTemplates.page_folder({ page })
|
39
|
+
} else {
|
40
|
+
pageFolderEl.innerHTML = ""
|
41
|
+
}
|
42
|
+
})
|
43
|
+
}
|
44
|
+
|
45
|
+
export function createSortables(sortables) {
|
46
|
+
sortables.forEach((el) => {
|
47
|
+
new Sortable(el, {
|
48
|
+
group: "pages",
|
49
|
+
animation: 150,
|
50
|
+
fallbackOnBody: true,
|
51
|
+
swapThreshold: 0.65,
|
52
|
+
handle: ".handle",
|
53
|
+
onEnd: onFinishDragging
|
54
|
+
})
|
55
|
+
})
|
56
|
+
}
|
57
|
+
|
58
|
+
export default function () {
|
59
|
+
const sortables = document.querySelectorAll("ul.children")
|
60
|
+
displayPageFolders()
|
61
|
+
createSortables(sortables)
|
62
|
+
}
|
@@ -1,11 +1,10 @@
|
|
1
|
-
import debounce from "lodash/debounce"
|
2
|
-
import max from "lodash/max"
|
3
|
-
import
|
1
|
+
import debounce from "lodash-es/debounce"
|
2
|
+
import max from "lodash-es/max"
|
3
|
+
import { get } from "./utils/ajax"
|
4
4
|
import ImageLoader from "./image_loader"
|
5
5
|
|
6
6
|
const UPDATE_DELAY = 125
|
7
7
|
const IMAGE_PLACEHOLDER = '<i class="icon far fa-image fa-fw"></i>'
|
8
|
-
const EMPTY_IMAGE = '<img src="" class="img_paddingtop" />'
|
9
8
|
const THUMBNAIL_SIZE = "160x120"
|
10
9
|
|
11
10
|
class PictureEditor {
|
@@ -62,7 +61,7 @@ class PictureEditor {
|
|
62
61
|
this.image.removeAttribute("alt")
|
63
62
|
this.image.removeAttribute("src")
|
64
63
|
this.imageLoader.load(true)
|
65
|
-
|
64
|
+
get(Alchemy.routes.url_admin_picture_path(this.pictureId), {
|
66
65
|
crop: this.imageCropperEnabled,
|
67
66
|
crop_from: this.cropFrom,
|
68
67
|
crop_size: this.cropSize,
|
@@ -83,9 +82,10 @@ class PictureEditor {
|
|
83
82
|
ensureImage() {
|
84
83
|
if (this.image) return
|
85
84
|
|
86
|
-
|
87
|
-
this.
|
88
|
-
this.
|
85
|
+
const img = new Image()
|
86
|
+
this.thumbnailBackground.replaceChildren(img)
|
87
|
+
this.image = img
|
88
|
+
this.imageLoader = new ImageLoader(img)
|
89
89
|
}
|
90
90
|
|
91
91
|
removeImage() {
|
@@ -0,0 +1,148 @@
|
|
1
|
+
// The admin sitemap Alchemy class
|
2
|
+
import PageSorter from "./page_sorter"
|
3
|
+
import { on } from "./utils/events"
|
4
|
+
import { get, patch } from "./utils/ajax"
|
5
|
+
import { createSortables, displayPageFolders } from "./page_sorter"
|
6
|
+
|
7
|
+
export default class Sitemap {
|
8
|
+
// Storing some objects.
|
9
|
+
constructor(options) {
|
10
|
+
const list_template_html = document
|
11
|
+
.getElementById("sitemap-list")
|
12
|
+
.innerHTML.replace(/__ID__/g, "{{id}}")
|
13
|
+
this.search_field = document.querySelector(".search_input_field")
|
14
|
+
this.filter_field_clear = document.querySelector(".search_field_clear")
|
15
|
+
this.filter_field_clear.removeAttribute("href")
|
16
|
+
this.display = document.getElementById("page_filter_result")
|
17
|
+
this.sitemap_wrapper = document.getElementById("sitemap-wrapper")
|
18
|
+
this.template = Handlebars.compile(
|
19
|
+
document.getElementById("sitemap-template").innerHTML
|
20
|
+
)
|
21
|
+
this.list_template = Handlebars.compile(list_template_html)
|
22
|
+
this.items = null
|
23
|
+
this.options = options
|
24
|
+
Handlebars.registerPartial("list", list_template_html)
|
25
|
+
this.load(options.page_root_id)
|
26
|
+
}
|
27
|
+
|
28
|
+
// Loads the sitemap
|
29
|
+
load(pageId) {
|
30
|
+
const spinner = new Alchemy.Spinner("medium")
|
31
|
+
const spinTarget = this.sitemap_wrapper
|
32
|
+
spinTarget.innerHTML = ""
|
33
|
+
spinner.spin(spinTarget)
|
34
|
+
get(this.options.url, { id: pageId })
|
35
|
+
.then(async (response) => {
|
36
|
+
this.render(await response.data)
|
37
|
+
this.handlePageFolders()
|
38
|
+
spinner.stop()
|
39
|
+
})
|
40
|
+
.catch(this.errorHandler)
|
41
|
+
}
|
42
|
+
|
43
|
+
// Watch page folder clicks and re-render the page branch
|
44
|
+
handlePageFolders() {
|
45
|
+
on(
|
46
|
+
"click",
|
47
|
+
"#sitemap",
|
48
|
+
".page_folder",
|
49
|
+
function (evt) {
|
50
|
+
const spinner = new Alchemy.Spinner("small")
|
51
|
+
const pageFolder = evt.target.closest(".page_folder")
|
52
|
+
const pageId = pageFolder.dataset.pageId
|
53
|
+
pageFolder.innerHTML = ""
|
54
|
+
spinner.spin(pageFolder)
|
55
|
+
|
56
|
+
patch(Alchemy.routes.fold_admin_page_path(pageId))
|
57
|
+
.then(async (response) => {
|
58
|
+
this.reRender(pageId, await response.data)
|
59
|
+
spinner.stop()
|
60
|
+
})
|
61
|
+
.catch(this.errorHandler)
|
62
|
+
}.bind(this)
|
63
|
+
)
|
64
|
+
}
|
65
|
+
|
66
|
+
// Renders the sitemap
|
67
|
+
render(data) {
|
68
|
+
const renderTarget = this.sitemap_wrapper
|
69
|
+
const renderTemplate = this.template
|
70
|
+
|
71
|
+
renderTarget.innerHTML = renderTemplate({ children: data.pages })
|
72
|
+
this.items = document
|
73
|
+
.getElementById("sitemap")
|
74
|
+
.querySelectorAll(".sitemap_page")
|
75
|
+
this.sitemap_wrapper = document.getElementById("sitemap-wrapper")
|
76
|
+
this._observe()
|
77
|
+
PageSorter()
|
78
|
+
}
|
79
|
+
|
80
|
+
reRender(pageId, data) {
|
81
|
+
let pageEl = document.getElementById(`page_${pageId}`)
|
82
|
+
pageEl.outerHTML = this.list_template({ children: data.pages })
|
83
|
+
pageEl = document.getElementById(`page_${pageId}`)
|
84
|
+
const sortables = pageEl.querySelectorAll("ul.children")
|
85
|
+
createSortables(sortables)
|
86
|
+
displayPageFolders()
|
87
|
+
}
|
88
|
+
|
89
|
+
// Filters the sitemap
|
90
|
+
filter(term) {
|
91
|
+
const results = []
|
92
|
+
|
93
|
+
this.items.forEach(function (item) {
|
94
|
+
if (
|
95
|
+
term !== "" &&
|
96
|
+
item.getAttribute("name").toLowerCase().indexOf(term) !== -1
|
97
|
+
) {
|
98
|
+
item.classList.add("highlight")
|
99
|
+
item.classList.remove("no-match")
|
100
|
+
results.push(item)
|
101
|
+
} else {
|
102
|
+
item.classList.add("no-match")
|
103
|
+
item.classList.remove("highlight")
|
104
|
+
}
|
105
|
+
})
|
106
|
+
this.filter_field_clear.style.display = "inline-block"
|
107
|
+
const { length } = results
|
108
|
+
|
109
|
+
if (length === 1) {
|
110
|
+
this.display.style.display = "block"
|
111
|
+
this.display.innerText = `1 ${Alchemy.t("page_found")}`
|
112
|
+
results[0].scrollIntoView({ behavior: "smooth", block: "center" })
|
113
|
+
} else if (length > 1) {
|
114
|
+
this.display.style.display = "block"
|
115
|
+
this.display.innerText = `${length} ${Alchemy.t("pages_found")}`
|
116
|
+
} else {
|
117
|
+
this.items.forEach((item) =>
|
118
|
+
item.classList.remove("no-match", "highlight")
|
119
|
+
)
|
120
|
+
this.display.style.display = "none"
|
121
|
+
window.scrollTo({
|
122
|
+
top: 0,
|
123
|
+
left: 0,
|
124
|
+
behavior: "smooth"
|
125
|
+
})
|
126
|
+
this.filter_field_clear.style.display = "none"
|
127
|
+
}
|
128
|
+
}
|
129
|
+
|
130
|
+
// Adds onkey up observer to search field
|
131
|
+
_observe() {
|
132
|
+
this.search_field.addEventListener("keyup", (evt) => {
|
133
|
+
const term = evt.target.value
|
134
|
+
this.filter(term.toLowerCase())
|
135
|
+
})
|
136
|
+
this.search_field.addEventListener("focus", () => key.setScope("search"))
|
137
|
+
this.filter_field_clear.addEventListener("click", () => {
|
138
|
+
this.search_field.value = ""
|
139
|
+
this.filter("")
|
140
|
+
return false
|
141
|
+
})
|
142
|
+
}
|
143
|
+
|
144
|
+
errorHandler(error) {
|
145
|
+
Alchemy.growl(error.message || error, "error")
|
146
|
+
console.error(error)
|
147
|
+
}
|
148
|
+
}
|