alchemy_cms 6.0.0.pre.rc6 → 6.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +23 -8
  3. data/.github/workflows/stale.yml +21 -7
  4. data/.gitignore +0 -1
  5. data/.rspec +1 -0
  6. data/CHANGELOG.md +102 -0
  7. data/Gemfile +20 -5
  8. data/README.md +4 -3
  9. data/Rakefile +5 -1
  10. data/alchemy_cms.gemspec +3 -3
  11. data/app/assets/javascripts/alchemy/admin.js +0 -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 +2 -6
  21. data/app/controllers/alchemy/admin/elements_controller.rb +20 -17
  22. data/app/controllers/alchemy/admin/pages_controller.rb +24 -19
  23. data/app/controllers/alchemy/api/base_controller.rb +4 -3
  24. data/app/controllers/alchemy/api/contents_controller.rb +1 -5
  25. data/app/controllers/alchemy/api/elements_controller.rb +2 -6
  26. data/app/controllers/alchemy/api/nodes_controller.rb +1 -0
  27. data/app/controllers/alchemy/api/pages_controller.rb +16 -10
  28. data/app/controllers/alchemy/base_controller.rb +7 -0
  29. data/app/controllers/alchemy/messages_controller.rb +0 -3
  30. data/app/controllers/alchemy/pages_controller.rb +0 -7
  31. data/app/helpers/alchemy/elements_helper.rb +17 -12
  32. data/app/models/alchemy/element.rb +13 -6
  33. data/app/models/alchemy/ingredient.rb +10 -1
  34. data/app/models/alchemy/ingredient_validator.rb +1 -1
  35. data/app/models/alchemy/language.rb +1 -1
  36. data/app/models/alchemy/page/page_elements.rb +2 -2
  37. data/app/models/alchemy/page/page_naming.rb +1 -1
  38. data/app/models/alchemy/page.rb +1 -1
  39. data/app/models/alchemy/picture/transformations.rb +2 -2
  40. data/app/models/alchemy/picture.rb +1 -1
  41. data/app/models/alchemy/picture_variant.rb +3 -1
  42. data/app/models/alchemy/site.rb +1 -1
  43. data/app/serializers/alchemy/page_serializer.rb +7 -1
  44. data/app/serializers/alchemy/page_tree_serializer.rb +3 -3
  45. data/app/views/alchemy/admin/clipboard/insert.js.erb +13 -0
  46. data/app/views/alchemy/admin/elements/_add_nested_element_form.html.erb +27 -0
  47. data/app/views/alchemy/admin/elements/_element.html.erb +1 -23
  48. data/app/views/alchemy/admin/elements/_form.html.erb +5 -1
  49. data/app/views/alchemy/admin/pages/_form.html.erb +19 -0
  50. data/app/views/alchemy/admin/pages/_new_page_form.html.erb +16 -5
  51. data/app/views/alchemy/admin/pages/_page.html.erb +111 -133
  52. data/app/views/alchemy/admin/pages/_sitemap.html.erb +2 -8
  53. data/app/views/alchemy/admin/pages/_toolbar.html.erb +0 -12
  54. data/app/views/alchemy/admin/pages/index.html.erb +1 -1
  55. data/app/views/alchemy/admin/pages/update.js.erb +7 -0
  56. data/app/views/alchemy/admin/partials/_routes.html.erb +12 -1
  57. data/app/views/alchemy/admin/resources/_form.html.erb +5 -0
  58. data/app/views/alchemy/essences/_essence_node_editor.html.erb +1 -1
  59. data/app/views/alchemy/essences/_essence_page_editor.html.erb +1 -1
  60. data/config/alchemy/config.yml +1 -0
  61. data/config/initializers/dragonfly.rb +2 -0
  62. data/config/locales/alchemy.en.yml +0 -3
  63. data/config/routes.rb +4 -2
  64. data/lib/alchemy/config.rb +5 -1
  65. data/lib/alchemy/controller_actions.rb +2 -1
  66. data/lib/alchemy/dragonfly/processors/thumbnail.rb +27 -0
  67. data/lib/alchemy/element_definition.rb +2 -3
  68. data/lib/alchemy/elements_finder.rb +1 -2
  69. data/lib/alchemy/engine.rb +12 -1
  70. data/lib/alchemy/essence.rb +1 -27
  71. data/lib/alchemy/page_layout.rb +5 -1
  72. data/lib/alchemy/permissions.rb +2 -3
  73. data/lib/alchemy/resource.rb +16 -1
  74. data/lib/alchemy/test_support/essence_shared_examples.rb +0 -12
  75. data/lib/alchemy/test_support/shared_ingredient_examples.rb +4 -2
  76. data/lib/alchemy/upgrader/tasks/ingredients_migrator.rb +1 -1
  77. data/lib/alchemy/version.rb +1 -1
  78. data/lib/alchemy.rb +2 -4
  79. data/lib/generators/alchemy/base.rb +7 -3
  80. data/lib/generators/alchemy/install/install_generator.rb +10 -2
  81. data/package/src/image_loader.js +4 -2
  82. data/package/src/node_tree.js +13 -6
  83. data/package/src/page_publication_fields.js +15 -14
  84. data/package/src/page_sorter.js +62 -0
  85. data/package/src/picture_editors.js +8 -8
  86. data/package/src/sitemap.js +51 -36
  87. data/package/src/utils/__tests__/ajax.spec.js +52 -16
  88. data/package/src/utils/ajax.js +12 -0
  89. data/package.json +1 -1
  90. metadata +43 -42
  91. data/app/assets/javascripts/alchemy/alchemy.page_sorter.js +0 -24
  92. data/app/views/alchemy/admin/pages/fold.js.erb +0 -2
  93. data/app/views/alchemy/admin/pages/sort.html.erb +0 -19
  94. data/vendor/assets/javascripts/jquery_plugins/jquery.ui.nestedSortable.js +0 -434
@@ -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
@@ -55,7 +41,7 @@ module Alchemy #:nodoc:
55
41
  has_one :element, through: :content, class_name: "Alchemy::Element"
56
42
  has_one :page, through: :element, class_name: "Alchemy::Page"
57
43
 
58
- scope :available, -> { joins(:element).merge(Alchemy::Element.available) }
44
+ scope :available, -> { joins(:element).merge(Alchemy::Element.published) }
59
45
  scope :from_element, ->(name) { joins(:element).where(Element.table_name => { name: name }) }
60
46
 
61
47
  delegate :restricted?, to: :page, allow_nil: true
@@ -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
 
@@ -151,7 +151,11 @@ module Alchemy
151
151
  #
152
152
  def read_definitions_file
153
153
  if File.exist?(layouts_file_path)
154
- YAML.safe_load(ERB.new(File.read(layouts_file_path)).result, YAML_WHITELIST_CLASSES, [], true) || []
154
+ YAML.safe_load(
155
+ ERB.new(File.read(layouts_file_path)).result,
156
+ permitted_classes: YAML_PERMITTED_CLASSES,
157
+ aliases: true,
158
+ ) || []
155
159
  else
156
160
  raise LoadError, "Could not find page_layouts.yml file! Please run `rails generate alchemy:install`"
157
161
  end
@@ -41,7 +41,7 @@ module Alchemy
41
41
  c.public? && !c.restricted?
42
42
  end
43
43
 
44
- can :read, Alchemy::Element, Alchemy::Element.available.not_restricted do |e|
44
+ can :read, Alchemy::Element, Alchemy::Element.published.not_restricted do |e|
45
45
  e.public? && !e.restricted?
46
46
  end
47
47
 
@@ -68,7 +68,7 @@ module Alchemy
68
68
  c.public?
69
69
  end
70
70
 
71
- can :read, Alchemy::Element, Alchemy::Element.available do |e|
71
+ can :read, Alchemy::Element, Alchemy::Element.published do |e|
72
72
  e.public?
73
73
  end
74
74
 
@@ -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-rc6"
4
+ VERSION = "6.0.1"
5
5
 
6
6
  def self.version
7
7
  VERSION
data/lib/alchemy.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  require "alchemy/admin/preview_url"
4
4
 
5
5
  module Alchemy
6
- YAML_WHITELIST_CLASSES = %w(Symbol Date Regexp)
6
+ YAML_PERMITTED_CLASSES = %w[Symbol Date Regexp]
7
7
 
8
8
  # Define page preview sources
9
9
  #
@@ -35,9 +35,7 @@ module Alchemy
35
35
  # acme/preview_source: Acme Vorschau
36
36
  #
37
37
  def self.preview_sources
38
- @_preview_sources ||= begin
39
- Set.new << Alchemy::Admin::PreviewUrl
40
- end
38
+ @_preview_sources ||= Set.new << Alchemy::Admin::PreviewUrl
41
39
  end
42
40
 
43
41
  # Define page publish targets
@@ -17,8 +17,8 @@ module Alchemy
17
17
  # source and destination file names to use that engine.
18
18
  if ext != template_engine.to_s
19
19
  say_status :warning, "View uses unexpected template engine '#{ext}'.", :cyan
20
- destination.gsub!(/#{template_engine}$/, ext)
21
- source.gsub!(/#{template_engine}$/, ext)
20
+ destination = destination.gsub(/#{template_engine}$/, ext)
21
+ source = source.gsub(/#{template_engine}$/, ext)
22
22
  end
23
23
  end
24
24
 
@@ -33,7 +33,11 @@ module Alchemy
33
33
  end
34
34
 
35
35
  def load_alchemy_yaml(name)
36
- YAML.safe_load(ERB.new(File.read("#{Rails.root}/config/alchemy/#{name}")).result, YAML_WHITELIST_CLASSES, [], true)
36
+ YAML.safe_load(
37
+ ERB.new(File.read(Rails.root.join("config", "alchemy", name))).result,
38
+ permitted_classes: YAML_PERMITTED_CLASSES,
39
+ aliases: true,
40
+ )
37
41
  rescue Errno::ENOENT
38
42
  puts "\nERROR: Could not read config/alchemy/#{name} file. Please run: `rails generate alchemy:install`"
39
43
  end
@@ -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
@@ -93,7 +98,10 @@ module Alchemy
93
98
  end
94
99
 
95
100
  def copy_alchemy_entry_point
96
- webpack_config = YAML.load_file(app_root.join("config", "webpacker.yml"))[Rails.env]
101
+ webpack_config = YAML.safe_load(
102
+ File.read(app_root.join("config", "webpacker.yml")),
103
+ aliases: true
104
+ )[Rails.env]
97
105
  copy_file "alchemy_admin.js",
98
106
  app_root.join(webpack_config["source_path"], webpack_config["source_entry_path"], "alchemy/admin.js")
99
107
  end
@@ -104,7 +112,7 @@ module Alchemy
104
112
  end
105
113
 
106
114
  def setup_database
107
- rake("db:create", abort_on_failure: true)
115
+ rake("db:create", abort_on_failure: true) unless options[:skip_db_create]
108
116
  # We can't invoke this rake task, because Rails will use wrong engine names otherwise
109
117
  rake("railties:install:migrations", abort_on_failure: true)
110
118
  rake("db:migrate", abort_on_failure: true)
@@ -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 =
@@ -7,21 +7,22 @@ export default function () {
7
7
  const publication_date_fields = dialog.querySelector(
8
8
  ".page-publication-date-fields"
9
9
  )
10
+ const public_field = dialog.querySelector("#page_public")
10
11
 
11
- dialog
12
- .querySelector("#page_public")
13
- .addEventListener("click", function (evt) {
14
- const checkbox = evt.target
15
- const now = new Date()
12
+ if(!public_field) return
16
13
 
17
- if (checkbox.checked) {
18
- publication_date_fields.classList.remove("hidden")
19
- public_on_field._flatpickr.setDate(now)
20
- } else {
21
- publication_date_fields.classList.add("hidden")
22
- public_on_field.value = ""
23
- }
24
- public_until_field.value = ""
25
- })
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
+ })
26
27
  })
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() {
@@ -1,12 +1,15 @@
1
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"
2
6
 
3
7
  export default class Sitemap {
4
8
  // Storing some objects.
5
9
  constructor(options) {
6
- const list_template_regexp = new RegExp("/" + options.page_root_id, "g")
7
10
  const list_template_html = document
8
11
  .getElementById("sitemap-list")
9
- .innerHTML.replace(list_template_regexp, "/{{id}}")
12
+ .innerHTML.replace(/__ID__/g, "{{id}}")
10
13
  this.search_field = document.querySelector(".search_input_field")
11
14
  this.filter_field_clear = document.querySelector(".search_field_clear")
12
15
  this.filter_field_clear.removeAttribute("href")
@@ -24,56 +27,63 @@ export default class Sitemap {
24
27
 
25
28
  // Loads the sitemap
26
29
  load(pageId) {
27
- const spinner = this.options.spinner || new Alchemy.Spinner("medium")
30
+ const spinner = new Alchemy.Spinner("medium")
28
31
  const spinTarget = this.sitemap_wrapper
29
32
  spinTarget.innerHTML = ""
30
33
  spinner.spin(spinTarget)
31
- this.fetch(
32
- `${this.options.url}?id=${pageId}&full=${this.options.full}`
33
- ).then(async (response) => {
34
- this.render(await response.json())
35
- spinner.stop()
36
- })
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)
37
41
  }
38
42
 
39
- // Reload the sitemap for a specific branch
40
- reload(pageId) {
41
- const spinner = new Alchemy.Spinner("small")
42
- const spinTarget = document.getElementById(`fold_button_${pageId}`)
43
- spinTarget.querySelector(".far").remove()
44
- spinner.spin(spinTarget)
45
- this.fetch(`${this.options.url}?id=${pageId}`).then(async (response) => {
46
- this.render(await response.json(), pageId)
47
- spinner.stop()
48
- })
49
- }
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)
50
55
 
51
- fetch(url) {
52
- return fetch(url).catch((error) => console.warn(`Request failed: ${error}`))
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
+ )
53
64
  }
54
65
 
55
66
  // Renders the sitemap
56
- render(data, foldingId) {
57
- let renderTarget, renderTemplate
67
+ render(data) {
68
+ const renderTarget = this.sitemap_wrapper
69
+ const renderTemplate = this.template
58
70
 
59
- if (foldingId) {
60
- renderTarget = document.getElementById(`page_${foldingId}`)
61
- renderTemplate = this.list_template
62
- renderTarget.outerHTML = renderTemplate({ children: data.pages })
63
- } else {
64
- renderTarget = this.sitemap_wrapper
65
- renderTemplate = this.template
66
- renderTarget.innerHTML = renderTemplate({ children: data.pages })
67
- }
71
+ renderTarget.innerHTML = renderTemplate({ children: data.pages })
68
72
  this.items = document
69
73
  .getElementById("sitemap")
70
74
  .querySelectorAll(".sitemap_page")
71
75
  this.sitemap_wrapper = document.getElementById("sitemap-wrapper")
72
76
  this._observe()
77
+ PageSorter()
78
+ }
73
79
 
74
- if (this.options.ready) {
75
- this.options.ready()
76
- }
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()
77
87
  }
78
88
 
79
89
  // Filters the sitemap
@@ -130,4 +140,9 @@ export default class Sitemap {
130
140
  return false
131
141
  })
132
142
  }
143
+
144
+ errorHandler(error) {
145
+ Alchemy.growl(error.message || error, "error")
146
+ console.error(error)
147
+ }
133
148
  }