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.

Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +16 -8
  3. data/.github/workflows/stale.yml +21 -7
  4. data/.gitignore +0 -1
  5. data/.rspec +1 -0
  6. data/CHANGELOG.md +89 -0
  7. data/Gemfile +10 -7
  8. data/Rakefile +5 -1
  9. data/alchemy_cms.gemspec +3 -3
  10. data/app/assets/javascripts/alchemy/admin.js +0 -2
  11. data/app/assets/javascripts/alchemy/alchemy.dialog.js.coffee +6 -1
  12. data/app/assets/javascripts/alchemy/alchemy.link_dialog.js.coffee +2 -0
  13. data/app/assets/javascripts/alchemy/page_select.js +13 -8
  14. data/app/assets/javascripts/alchemy/templates/index.js +1 -0
  15. data/app/assets/javascripts/alchemy/templates/page.hbs +17 -7
  16. data/app/assets/javascripts/alchemy/templates/page_folder.hbs +3 -0
  17. data/app/assets/stylesheets/alchemy/archive.scss +9 -0
  18. data/app/assets/stylesheets/alchemy/elements.scss +4 -0
  19. data/app/assets/stylesheets/alchemy/page-select.scss +29 -4
  20. data/app/assets/stylesheets/alchemy/sitemap.scss +9 -7
  21. data/app/controllers/alchemy/admin/elements_controller.rb +2 -6
  22. data/app/controllers/alchemy/admin/pages_controller.rb +16 -18
  23. data/app/controllers/alchemy/api/contents_controller.rb +1 -5
  24. data/app/controllers/alchemy/api/elements_controller.rb +2 -6
  25. data/app/controllers/alchemy/api/pages_controller.rb +16 -10
  26. data/app/helpers/alchemy/elements_helper.rb +2 -2
  27. data/app/models/alchemy/ingredient.rb +6 -1
  28. data/app/models/alchemy/page.rb +3 -2
  29. data/app/models/alchemy/picture.rb +1 -1
  30. data/app/serializers/alchemy/page_serializer.rb +7 -1
  31. data/app/serializers/alchemy/page_tree_serializer.rb +3 -3
  32. data/app/views/alchemy/admin/pages/_form.html.erb +19 -0
  33. data/app/views/alchemy/admin/pages/_new_page_form.html.erb +16 -5
  34. data/app/views/alchemy/admin/pages/_page.html.erb +111 -133
  35. data/app/views/alchemy/admin/pages/_sitemap.html.erb +10 -16
  36. data/app/views/alchemy/admin/pages/_toolbar.html.erb +0 -12
  37. data/app/views/alchemy/admin/pages/edit.html.erb +1 -1
  38. data/app/views/alchemy/admin/pages/index.html.erb +1 -1
  39. data/app/views/alchemy/admin/pages/update.js.erb +7 -5
  40. data/app/views/alchemy/admin/partials/_routes.html.erb +12 -1
  41. data/app/views/alchemy/admin/resources/_form.html.erb +5 -0
  42. data/app/views/alchemy/essences/_essence_page_editor.html.erb +1 -1
  43. data/config/alchemy/config.yml +1 -0
  44. data/config/locales/alchemy.en.yml +0 -4
  45. data/config/routes.rb +4 -2
  46. data/lib/alchemy/engine.rb +12 -1
  47. data/lib/alchemy/essence.rb +0 -26
  48. data/lib/alchemy/permissions.rb +0 -1
  49. data/lib/alchemy/resource.rb +16 -1
  50. data/lib/alchemy/test_support/essence_shared_examples.rb +0 -12
  51. data/lib/alchemy/test_support/shared_ingredient_examples.rb +4 -2
  52. data/lib/alchemy/upgrader/tasks/ingredients_migrator.rb +1 -1
  53. data/lib/alchemy/version.rb +1 -1
  54. data/lib/generators/alchemy/install/install_generator.rb +6 -1
  55. data/package/admin.js +5 -1
  56. data/package/src/image_loader.js +4 -2
  57. data/package/src/node_tree.js +13 -6
  58. data/package/src/page_publication_fields.js +28 -0
  59. data/package/src/page_sorter.js +62 -0
  60. data/package/src/picture_editors.js +8 -8
  61. data/package/src/sitemap.js +148 -0
  62. data/package/src/utils/__tests__/ajax.spec.js +52 -16
  63. data/package/src/utils/ajax.js +12 -0
  64. data/package.json +1 -1
  65. metadata +43 -43
  66. data/app/assets/javascripts/alchemy/alchemy.page_sorter.js +0 -24
  67. data/app/assets/javascripts/alchemy/alchemy.sitemap.js.coffee +0 -119
  68. data/app/views/alchemy/admin/pages/fold.js.erb +0 -2
  69. data/app/views/alchemy/admin/pages/sort.html.erb +0 -19
  70. 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: {
@@ -173,6 +173,7 @@ uploader:
173
173
  - gif
174
174
  - png
175
175
  - svg
176
+ - webp
176
177
 
177
178
  # === Link Target Options
178
179
  #
@@ -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
- post :fold
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"
@@ -37,7 +37,7 @@ module Alchemy
37
37
  end
38
38
  end
39
39
 
40
- initializer "alchemy.userstamp" do
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
@@ -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
 
@@ -147,7 +147,6 @@ module Alchemy
147
147
  :copy_language_tree,
148
148
  :flush,
149
149
  :order,
150
- :sort,
151
150
  :switch_language,
152
151
  ], Alchemy::Page
153
152
 
@@ -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
- }.delete_if { |_k, v| v.nil? }
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) { definition }
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: { essence: :ingredient_association })
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|
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Alchemy
4
- VERSION = "6.0.0-rc5"
4
+ VERSION = "6.0.0"
5
5
 
6
6
  def self.version
7
7
  VERSION
@@ -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
  })
@@ -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.innerHtml = '<span class="icon warn"></span>'
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
 
@@ -1,12 +1,16 @@
1
1
  import Sortable from "sortablejs"
2
- import ajax from "./utils/ajax"
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 = { folded: el.dataset.folded === "true", id: el.dataset.id, type: el.dataset.type }
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(evt.item.dataset.id)
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
- ajax("PATCH", url, data)
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 = Alchemy.routes[this.dataset.recordType].toggle_folded_api_path(nodeId)
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
- ajax("PATCH", url)
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 ajax from "./utils/ajax"
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
- ajax("GET", `/admin/pictures/${this.pictureId}/url`, {
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
- this.thumbnailBackground.innerHTML = EMPTY_IMAGE
87
- this.image = this.container.querySelector("img")
88
- this.imageLoader = new ImageLoader(this.image)
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
+ }