alchemy_cms 8.2.3 → 8.2.5

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.
@@ -10,12 +10,12 @@ module Alchemy
10
10
  end
11
11
 
12
12
  def call
13
- form_tag(helpers.url_for, method: :get) do
14
- if auto_submit
13
+ if auto_submit
14
+ form_tag(helpers.url_for, method: :get) do
15
15
  content_tag("alchemy-auto-submit", locale_select)
16
- else
17
- locale_select
18
16
  end
17
+ else
18
+ locale_select
19
19
  end
20
20
  end
21
21
 
@@ -14,6 +14,12 @@ export class RemoteSelect extends AlchemyHTMLElement {
14
14
  url: { default: "" }
15
15
  }
16
16
 
17
+ // Select2 manages its own DOM after initialization, so attribute changes
18
+ // must not trigger the default re-render which would destroy the widget.
19
+ static get observedAttributes() {
20
+ return []
21
+ }
22
+
17
23
  async connected() {
18
24
  await setupSelectLocale()
19
25
 
@@ -34,6 +40,11 @@ export class RemoteSelect extends AlchemyHTMLElement {
34
40
  * @param {Event} event
35
41
  */
36
42
  onChange(event) {
43
+ // Update selection attribute so re-attaching the select2 component to
44
+ // the same input (e.g. after dragndrop) does not reset the selection.
45
+ if (event.added) {
46
+ this.setAttribute("selection", JSON.stringify(event.added))
47
+ }
37
48
  this.dispatchCustomEvent("RemoteSelect.Change", {
38
49
  removed: event.removed,
39
50
  added: event.added
@@ -89,7 +89,20 @@ export default class ImageCropper {
89
89
  }
90
90
 
91
91
  reset() {
92
- this.#cropper.setData(this.defaultBoxSize)
92
+ const cropper = this.#cropper
93
+ // Apply the default size. cropperjs clamps the crop box to its maximum size.
94
+ cropper.setData(this.defaultBoxSize)
95
+ // When the default box sits at the maximum crop box size, sub-pixel rounding
96
+ // makes cropperjs treat setData's box as oversized and revert its position
97
+ // (renderCropBox resets top/left to their old values). Re-apply the position
98
+ // in canvas coordinates afterwards – that does not touch the size, so it is
99
+ // not reverted and the mask actually moves to the default box.
100
+ const canvas = cropper.getCanvasData()
101
+ const scale = canvas.width / canvas.naturalWidth
102
+ cropper.setCropBoxData({
103
+ left: canvas.left + this.defaultBoxSize.x * scale,
104
+ top: canvas.top + this.defaultBoxSize.y * scale
105
+ })
93
106
  this.update(this.defaultBoxSize)
94
107
  }
95
108
 
@@ -49,24 +49,16 @@ module Alchemy
49
49
  # not tracked via the polymorphic +related_object+ association, so the
50
50
  # base scope cannot see them.
51
51
  #
52
- # Uses a correlated +NOT EXISTS+ subquery that builds the per-row LIKE
53
- # pattern with +Arel::Nodes::Concat+, which compiles to +||+ on
54
- # SQLite/PostgreSQL and +CONCAT()+ on MySQL.
52
+ # Extracts referenced attachment IDs from ingredient values via Ruby
53
+ # regex to stay database-agnostic.
55
54
  scope :deletable, -> do
56
- ingredients = Alchemy::Ingredient.arel_table
57
- pattern = Arel::Nodes::Concat.new(
58
- Arel::Nodes::Concat.new(
59
- Arel::Nodes.build_quoted("%/attachment/"),
60
- arel_table[:id]
61
- ),
62
- Arel::Nodes.build_quoted("/download%")
63
- )
64
- referenced = ingredients
65
- .project(1)
66
- .where(ingredients[:value].matches(pattern))
67
-
68
- where("#{table_name}.id NOT IN (#{RelatableResource::RELATED_INGREDIENTS_SUBQUERY})", type: name)
69
- .where.not(referenced.exists)
55
+ referenced_ids = Alchemy::Ingredient
56
+ .where("value LIKE '%/attachment/%/download%'")
57
+ .pluck(:value)
58
+ .flat_map { |v| v.scan(%r{/attachment/(\d+)/download}).flatten.map(&:to_i) }
59
+
60
+ scope = where("#{table_name}.id NOT IN (#{RelatableResource::RELATED_INGREDIENTS_SUBQUERY})", type: name)
61
+ referenced_ids.any? ? scope.where.not(id: referenced_ids) : scope
70
62
  end
71
63
 
72
64
  # We need to define this method here to have it available in the validations below.
@@ -615,7 +615,17 @@ module Alchemy
615
615
  end
616
616
 
617
617
  def check_descendants_for_menu_nodes
618
- pages_with_nodes = descendants.joins(:nodes).reorder("alchemy_pages.lft").distinct
618
+ # awesome_nested_set's before_destroy runs first and closes the gap left
619
+ # by this page, shifting the following siblings' lft/rgt leftward. For a
620
+ # leaf the next sibling then lands exactly on this page's lft, so the
621
+ # inclusive comparison the `descendants` helper uses (lft >= self.lft)
622
+ # would match it. Restrict to true descendants with strict bounds. Build
623
+ # on nested_set_scope so the acts_as_nested_set scope stays in sync.
624
+ pages_with_nodes = nested_set_scope
625
+ .where("alchemy_pages.lft > ? AND alchemy_pages.rgt < ?", lft, rgt)
626
+ .joins(:nodes)
627
+ .reorder("alchemy_pages.lft")
628
+ .distinct
619
629
  if pages_with_nodes.exists?
620
630
  errors.add(:descendants, :still_attached_to_nodes, page_names: pages_with_nodes.map(&:name).to_sentence)
621
631
  throw :abort
@@ -29,32 +29,6 @@ module Alchemy
29
29
 
30
30
  before_destroy :delete_elements
31
31
 
32
- # Determines if this version is public
33
- #
34
- # Takes the two timestamps +public_on+ and +public_until+
35
- # and returns true if the time given (+Time.current+ per default)
36
- # is in this timespan.
37
- #
38
- # @param time [DateTime] (Time.current)
39
- # @returns Boolean
40
- def public?(time = Current.preview_time)
41
- already_public_for?(time) && still_public_for?(time)
42
- end
43
-
44
- # Determines if this version is already public for given time
45
- # @param time [DateTime] (Current.preview_time)
46
- # @returns Boolean
47
- def already_public_for?(time = Current.preview_time)
48
- !public_on.nil? && public_on <= time
49
- end
50
-
51
- # Determines if this version is still public for given time
52
- # @param time [DateTime] (Current.preview_time)
53
- # @returns Boolean
54
- def still_public_for?(time = Current.preview_time)
55
- public_until.nil? || public_until >= time
56
- end
57
-
58
32
  def element_repository
59
33
  ElementsRepository.new(elements)
60
34
  end
@@ -46,7 +46,7 @@ module Alchemy
46
46
  #
47
47
  # @returns Boolean
48
48
  def publishable?
49
- !public_on.nil? && still_public_for?
49
+ !public_on.nil? && still_public_for?(at: Time.current)
50
50
  end
51
51
 
52
52
  # Determines if this record is already public for given time
@@ -61,7 +61,7 @@ module Alchemy
61
61
 
62
62
  elements_by_id.each_value do |element|
63
63
  children = elements_by_parent[element.id] || []
64
- children = children.sort_by(&:position)
64
+ children = children.sort_by { |c| c.position.to_i }
65
65
 
66
66
  # Manually set the association target
67
67
  element.association(:all_nested_elements).target = children
@@ -70,6 +70,15 @@ alchemy-message {
70
70
  font-size: var(--font-size_medium);
71
71
  }
72
72
 
73
+ h1,
74
+ h2,
75
+ h3,
76
+ p {
77
+ &:last-child {
78
+ margin-bottom: 0;
79
+ }
80
+ }
81
+
73
82
  a[href] {
74
83
  text-decoration-color: inherit;
75
84
  text-decoration-thickness: 1px;
@@ -6,7 +6,7 @@
6
6
  <%= render_icon "file-edit", size: "xl" %>
7
7
  </sl-tooltip>
8
8
  <% else %>
9
- <%= render_icon "file-edit", size: "xl" %>
9
+ <%= render_icon "file", size: "xl" %>
10
10
  <% end %>
11
11
  <% else %>
12
12
  <sl-tooltip class="like-hint-tooltip" content="<%= Alchemy.t("Your user role does not allow you to edit this page") %>" placement="bottom-start">
@@ -1,11 +1,18 @@
1
1
  class ConvertSelectValueForMultiple < ActiveRecord::Migration[7.1]
2
2
  def up
3
3
  say_with_time "Converting Alchemy::Ingredients::Select values to multiple" do
4
- update <<-SQL.squish
5
- UPDATE alchemy_ingredients
6
- SET value = '["' || value || '"]'
7
- WHERE type = 'Alchemy::Ingredients::Select' AND value NOT LIKE '["%"]';
8
- SQL
4
+ Alchemy::Ingredients::Select
5
+ .where.not("value LIKE ?", '["%')
6
+ .update_all(
7
+ Arel.sql(
8
+ case ActiveRecord::Base.connection.adapter_name
9
+ when /mysql|mariadb/i
10
+ "value = CONCAT('[\"', value, '\"]')"
11
+ else
12
+ "value = '[\"' || value || '\"]'"
13
+ end
14
+ )
15
+ )
9
16
  end
10
17
  end
11
18
  end
@@ -66,7 +66,7 @@ module Alchemy
66
66
  end
67
67
 
68
68
  def get_item_class(item_type)
69
- "Alchemy::Configuration::#{item_type.to_s.classify}Option".constantize
69
+ "Alchemy::Configuration::#{item_type.to_s.camelcase}Option".constantize
70
70
  end
71
71
  end
72
72
  end
@@ -16,11 +16,21 @@ module Alchemy
16
16
  @request = request
17
17
  @params = @request.params
18
18
 
19
- handable_format? && no_rails_route?
19
+ handable_format? && no_dotfile_route? && no_rails_route?
20
20
  end
21
21
 
22
22
  private
23
23
 
24
+ # We don't want to handle requests to dotfile URLs.
25
+ #
26
+ # A page urlname never starts with a dot, so any such request
27
+ # (/.well-known/ ACME challenges, /.env or /.git probes, etc.)
28
+ # should never be served by a page.
29
+ #
30
+ def no_dotfile_route?
31
+ !@params["urlname"].start_with?(".")
32
+ end
33
+
24
34
  # We only want html requests to be handled by us.
25
35
  #
26
36
  # If an unknown format is requested we want to handle this,
@@ -212,4 +212,41 @@ RSpec.shared_examples_for "being publishable" do |factory_name|
212
212
  end
213
213
  end
214
214
  end
215
+
216
+ describe "#publishable?" do
217
+ context "when public_on is nil" do
218
+ let(:page_version) { build(factory_name, public_on: nil) }
219
+
220
+ it { expect(page_version.publishable?).to be(false) }
221
+ end
222
+
223
+ context "when public_on is set and public_until is nil" do
224
+ let(:page_version) { build(factory_name, public_on: Time.current) }
225
+
226
+ it { expect(page_version.publishable?).to be(true) }
227
+ end
228
+
229
+ context "when public_on is set and public_until is in the past" do
230
+ let(:page_version) do
231
+ build(factory_name,
232
+ public_on: Time.current - 2.days,
233
+ public_until: Time.current - 1.day)
234
+ end
235
+
236
+ it { expect(page_version.publishable?).to be(false) }
237
+ end
238
+
239
+ context "when Current.preview_time is set to a future time" do
240
+ let(:page_version) do
241
+ build(factory_name,
242
+ public_on: Time.current - 1.day,
243
+ public_until: Time.current + 1.day)
244
+ end
245
+
246
+ it "uses Time.current instead of the preview_time" do
247
+ Alchemy::Current.preview_time = Time.current + 1.week
248
+ expect(page_version.publishable?).to be(true)
249
+ end
250
+ end
251
+ end
215
252
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Alchemy
4
- VERSION = "8.2.3"
4
+ VERSION = "8.2.5"
5
5
 
6
6
  def self.version
7
7
  VERSION
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: alchemy_cms
3
3
  version: !ruby/object:Gem::Version
4
- version: 8.2.3
4
+ version: 8.2.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thomas von Deyen
@@ -1490,7 +1490,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
1490
1490
  version: '0'
1491
1491
  requirements:
1492
1492
  - ImageMagick (libmagick), v6.6 or greater.
1493
- rubygems_version: 4.0.6
1493
+ rubygems_version: 4.0.10
1494
1494
  specification_version: 4
1495
1495
  summary: A powerful, userfriendly and flexible CMS for Rails
1496
1496
  test_files: []