alchemy_cms 6.0.0.pre.rc5 → 6.0.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 alchemy_cms might be problematic. Click here for more details.
- 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
|
+
}
|