alchemy_cms 8.4.1 → 8.4.3
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.
- checksums.yaml +4 -4
- data/app/assets/builds/alchemy/admin.css +1 -1
- data/app/assets/builds/alchemy/alchemy_admin.min.js +1 -1
- data/app/assets/builds/alchemy/alchemy_admin.min.js.map +1 -1
- data/app/components/alchemy/admin/main_navigation/menu.rb +19 -2
- data/app/controllers/alchemy/admin/dashboard/widgets_controller.rb +6 -3
- data/app/controllers/alchemy/base_controller.rb +1 -1
- data/app/helpers/alchemy/admin/base_helper.rb +6 -0
- data/app/javascript/alchemy_admin/components/elements_window.js +2 -0
- data/app/javascript/alchemy_admin/components/preview_window.js +8 -1
- data/app/models/alchemy/element/element_ingredients.rb +15 -1
- data/app/models/alchemy/ingredient.rb +8 -0
- data/app/stylesheets/alchemy/admin/archive.scss +2 -1
- data/app/stylesheets/alchemy/admin/frame.scss +2 -0
- data/app/stylesheets/alchemy/admin/sitemap.scss +2 -2
- data/app/views/layouts/alchemy/admin.html.erb +1 -1
- data/lib/alchemy/configurations/menu_item.rb +29 -5
- data/lib/alchemy/upgrader/eight_four.rb +6 -3
- data/lib/alchemy/upgrader/register_module_rewriter.rb +24 -7
- data/lib/alchemy/version.rb +1 -1
- metadata +1 -1
|
@@ -11,9 +11,26 @@ module Alchemy
|
|
|
11
11
|
</div>
|
|
12
12
|
ERB
|
|
13
13
|
|
|
14
|
+
# Entries without a position that were inserted between Alchemy's own
|
|
15
|
+
# entries are listed after the positioned entry preceding them, so
|
|
16
|
+
# +Alchemy.config.menu.insert+ can replace the deprecated position.
|
|
17
|
+
# Appended entries without a position are listed last.
|
|
14
18
|
def items
|
|
15
|
-
|
|
16
|
-
|
|
19
|
+
menu = Alchemy.config.menu.to_a
|
|
20
|
+
core_names = Alchemy::Configurations::Menu::DEFAULT.map { |entry| entry[:name] }
|
|
21
|
+
last_core_index = menu.rindex { |item| core_names.include?(item.name) } || -1
|
|
22
|
+
preceding_position = 0
|
|
23
|
+
menu.each_with_index.map do |item, index|
|
|
24
|
+
preceding_position = item.position if item.position
|
|
25
|
+
key = if item.position
|
|
26
|
+
[item.position, 0, index]
|
|
27
|
+
elsif index < last_core_index
|
|
28
|
+
[preceding_position, 1, index]
|
|
29
|
+
else
|
|
30
|
+
[Float::INFINITY, 0, index]
|
|
31
|
+
end
|
|
32
|
+
[item, key]
|
|
33
|
+
end.sort_by(&:last).map(&:first)
|
|
17
34
|
end
|
|
18
35
|
end
|
|
19
36
|
end
|
|
@@ -11,10 +11,13 @@ module Alchemy
|
|
|
11
11
|
private
|
|
12
12
|
|
|
13
13
|
def get_widget(id)
|
|
14
|
-
"#{MODULE_NAMESPACE}::#{id.camelcase}".
|
|
15
|
-
|
|
14
|
+
widget = "#{MODULE_NAMESPACE}::#{id.camelcase}".safe_constantize
|
|
15
|
+
# Constant lookup also finds classes in the ancestors of a widget,
|
|
16
|
+
# e.g. "system_info/template" resolves to a ViewComponent internal.
|
|
17
|
+
return widget if widget.is_a?(Class) && widget.module_parent_name == MODULE_NAMESPACE
|
|
18
|
+
|
|
16
19
|
Alchemy::Logger.error "No dashboard widget found for id: #{id}"
|
|
17
|
-
raise ActionController::RoutingError,
|
|
20
|
+
raise ActionController::RoutingError, "No dashboard widget found for id: #{id}"
|
|
18
21
|
end
|
|
19
22
|
end
|
|
20
23
|
end
|
|
@@ -258,6 +258,12 @@ module Alchemy
|
|
|
258
258
|
].compact
|
|
259
259
|
end
|
|
260
260
|
|
|
261
|
+
# Restores the elements window width the user dragged it to.
|
|
262
|
+
def alchemy_body_style
|
|
263
|
+
width = cookies["alchemy-elements-window-width"]
|
|
264
|
+
"--elements-window-width: #{width}px" if width&.match?(/\A\d+(\.\d+)?\z/)
|
|
265
|
+
end
|
|
266
|
+
|
|
261
267
|
# (internal) Returns options for the clipboard select tag
|
|
262
268
|
def clipboard_select_tag_options(items)
|
|
263
269
|
options = items.map do |item|
|
|
@@ -32,6 +32,8 @@ class ElementsWindow extends HTMLElement {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
#onWindowMessage = (event) => {
|
|
35
|
+
if (event.source !== this.previewWindow?.contentWindow) return
|
|
36
|
+
|
|
35
37
|
const data = event.data
|
|
36
38
|
if (data?.message == "Alchemy.focusElementEditor") {
|
|
37
39
|
const element = document.getElementById(`element_${data.element_id}`)
|
|
@@ -5,6 +5,7 @@ class PreviewWindow extends HTMLIFrameElement {
|
|
|
5
5
|
#afterLoad
|
|
6
6
|
#reloadIcon
|
|
7
7
|
#loadTimeout
|
|
8
|
+
#previewOrigin = null
|
|
8
9
|
|
|
9
10
|
handleEvent(evt) {
|
|
10
11
|
if (evt.type === "load") {
|
|
@@ -15,7 +16,10 @@ class PreviewWindow extends HTMLIFrameElement {
|
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
#onPreviewReady = (event) => {
|
|
19
|
+
if (event.source !== this.contentWindow) return
|
|
20
|
+
|
|
18
21
|
if (event.data.message === "Alchemy.previewReady") {
|
|
22
|
+
this.#previewOrigin = event.origin
|
|
19
23
|
this.#clearLoadTimeout()
|
|
20
24
|
this.#stopSpinner()
|
|
21
25
|
// Deliberately does not resolve #afterLoad: previewReady fires before the
|
|
@@ -47,7 +51,9 @@ class PreviewWindow extends HTMLIFrameElement {
|
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
postMessage(data) {
|
|
50
|
-
this
|
|
54
|
+
if (this.#previewOrigin) {
|
|
55
|
+
this.contentWindow.postMessage(data, this.#previewOrigin)
|
|
56
|
+
}
|
|
51
57
|
}
|
|
52
58
|
|
|
53
59
|
resize(width) {
|
|
@@ -55,6 +61,7 @@ class PreviewWindow extends HTMLIFrameElement {
|
|
|
55
61
|
}
|
|
56
62
|
|
|
57
63
|
refresh(url) {
|
|
64
|
+
this.#previewOrigin = null
|
|
58
65
|
this.#startSpinner()
|
|
59
66
|
|
|
60
67
|
if (url) {
|
|
@@ -18,10 +18,24 @@ module Alchemy
|
|
|
18
18
|
before_create :build_ingredients,
|
|
19
19
|
unless: -> { autogenerate_ingredients == false }
|
|
20
20
|
|
|
21
|
-
accepts_nested_attributes_for :ingredients
|
|
21
|
+
accepts_nested_attributes_for :ingredients, reject_if: ->(attributes) { attributes[:id].blank? }
|
|
22
22
|
validates_associated :ingredients, on: :update
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
# Only the editable attributes of each ingredient's own class are
|
|
26
|
+
# assigned, so +type+, +role+ or +related_object_type+ can never be.
|
|
27
|
+
def ingredients_attributes=(attributes)
|
|
28
|
+
attributes = attributes.to_h if attributes.respond_to?(:permitted?)
|
|
29
|
+
if attributes.is_a?(Hash) && !attributes.key?("id") && !attributes.key?(:id)
|
|
30
|
+
attributes = attributes.values
|
|
31
|
+
end
|
|
32
|
+
super(Array.wrap(attributes).map { |ingredient_attributes|
|
|
33
|
+
ingredient_attributes = ingredient_attributes.to_h.with_indifferent_access
|
|
34
|
+
ingredient = ingredients.detect { _1.id.to_s == ingredient_attributes[:id].to_s }
|
|
35
|
+
ingredient_attributes.slice(:id, *ingredient&.class&.editable_attributes)
|
|
36
|
+
})
|
|
37
|
+
end
|
|
38
|
+
|
|
25
39
|
# The value of an ingredient of the element by role
|
|
26
40
|
def value_for(role)
|
|
27
41
|
ingredient_by_role(role)&.value
|
|
@@ -34,6 +34,8 @@ module Alchemy
|
|
|
34
34
|
|
|
35
35
|
attribute :data, :json
|
|
36
36
|
|
|
37
|
+
class_attribute :related_object_id_alias, instance_accessor: false
|
|
38
|
+
|
|
37
39
|
belongs_to :element, touch: true, class_name: "Alchemy::Element", inverse_of: :ingredients
|
|
38
40
|
belongs_to :related_object, polymorphic: true, optional: true
|
|
39
41
|
|
|
@@ -72,6 +74,7 @@ module Alchemy
|
|
|
72
74
|
# @param [String|Symbol] The name of the alias
|
|
73
75
|
# @param [String] The class name of the related object
|
|
74
76
|
def related_object_alias(name, class_name:)
|
|
77
|
+
self.related_object_id_alias = :"#{name}_id"
|
|
75
78
|
alias_method name, :related_object
|
|
76
79
|
alias_method :"#{name}=", :related_object=
|
|
77
80
|
|
|
@@ -116,6 +119,11 @@ module Alchemy
|
|
|
116
119
|
def allowed_settings
|
|
117
120
|
@allowed_settings ||= []
|
|
118
121
|
end
|
|
122
|
+
|
|
123
|
+
# Attributes editors may assign through the element form
|
|
124
|
+
def editable_attributes
|
|
125
|
+
[:value, *stored_attributes[:data], related_object_id_alias].compact
|
|
126
|
+
end
|
|
119
127
|
end
|
|
120
128
|
|
|
121
129
|
# The value or the related object if present
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
</head>
|
|
38
38
|
<%= content_tag :body, id: 'alchemy',
|
|
39
39
|
class: alchemy_body_class,
|
|
40
|
-
style:
|
|
40
|
+
style: alchemy_body_style do %>
|
|
41
41
|
<noscript>
|
|
42
42
|
<h1><%= Alchemy.t(:javascript_disabled_headline) %></h1>
|
|
43
43
|
<p><%= Alchemy.t(:javascript_disabled_text) %></p>
|
|
@@ -24,14 +24,38 @@ module Alchemy
|
|
|
24
24
|
option :nested_actions, :collection, item_type: :string
|
|
25
25
|
option :nested, :collection, item_type: :configuration, config_class: MenuItem
|
|
26
26
|
|
|
27
|
+
alias_method :set_position, :position=
|
|
28
|
+
private :set_position
|
|
29
|
+
|
|
30
|
+
# Alchemy's own entries still need their positions for sorting,
|
|
31
|
+
# so only positions of other entries are deprecated.
|
|
32
|
+
def position=(value)
|
|
33
|
+
if value && Menu::DEFAULT.none? { |entry| entry[:name] == name }
|
|
34
|
+
Alchemy::Deprecation.warn(
|
|
35
|
+
"The `position` of menu entries is deprecated and will be removed in Alchemy 9.0, " \
|
|
36
|
+
"which lists the entries in the order of `Alchemy.config.menu`. " \
|
|
37
|
+
"Use `Alchemy.config.menu.insert` to place the #{name.inspect} entry instead."
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
set_position(value)
|
|
41
|
+
end
|
|
42
|
+
|
|
27
43
|
def self.from_legacy_hash(hash)
|
|
28
44
|
hash = hash.to_h.deep_stringify_keys
|
|
29
|
-
new(
|
|
45
|
+
item = new(
|
|
30
46
|
from_legacy_navigation(hash["navigation"] || {}, engine_name: hash["engine_name"]).merge(
|
|
31
|
-
"name" => hash["name"]
|
|
32
|
-
"position" => hash["position"]
|
|
47
|
+
"name" => hash["name"]
|
|
33
48
|
).compact
|
|
34
49
|
)
|
|
50
|
+
# +register_module+ is deprecated already
|
|
51
|
+
item.send(:set_position, hash["position"]) if hash["position"]
|
|
52
|
+
item
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Positions sort entries around the default menu's entries, which use
|
|
56
|
+
# the positions 1 to 7, so +4.1+ maps to the index before Sites.
|
|
57
|
+
def self.index_for_legacy_position(position)
|
|
58
|
+
[position.ceil - 1, 0].max
|
|
35
59
|
end
|
|
36
60
|
|
|
37
61
|
def self.from_legacy_navigation(navigation, engine_name:)
|
|
@@ -82,8 +106,8 @@ module Alchemy
|
|
|
82
106
|
current_path = view_context.request.path
|
|
83
107
|
path_matches = if match_path
|
|
84
108
|
match_path.match?(current_path)
|
|
85
|
-
elsif (own_path = local_path(view_context))
|
|
86
|
-
current_path == own_path || current_path.start_with?("#{own_path
|
|
109
|
+
elsif (own_path = local_path(view_context)&.chomp("/"))
|
|
110
|
+
current_path.chomp("/") == own_path || current_path.start_with?("#{own_path}/")
|
|
87
111
|
end
|
|
88
112
|
|
|
89
113
|
path_matches || [self, *nested].any? do |entry|
|
|
@@ -169,10 +169,11 @@ module Alchemy
|
|
|
169
169
|
list = ->(files) { files.map { |file| " - #{Pathname.new(file).relative_path_from(Rails.root)}" }.join("\n") }
|
|
170
170
|
|
|
171
171
|
if rewritten.any?
|
|
172
|
-
todo(<<~TODO.strip, "`register_module` calls were rewritten to `Alchemy.config.menu
|
|
172
|
+
todo(<<~TODO.strip, "`register_module` calls were rewritten to `Alchemy.config.menu`")
|
|
173
173
|
`Alchemy::Modules.register_module` is deprecated and will be removed in
|
|
174
174
|
Alchemy 9.0. We rewrote its calls in the following initializers to
|
|
175
|
-
`Alchemy.config.menu.add
|
|
175
|
+
`Alchemy.config.menu.add`, or to `Alchemy.config.menu.insert` if they
|
|
176
|
+
had a position between Alchemy's own entries:
|
|
176
177
|
|
|
177
178
|
#{list.call(rewritten)}
|
|
178
179
|
|
|
@@ -212,7 +213,9 @@ module Alchemy
|
|
|
212
213
|
the former `navigation` hash now live on the entry itself, its `name`
|
|
213
214
|
is now `title`, `sub_navigation` is now `children`, `icon-style` is now
|
|
214
215
|
`icon_style`, `data: {turbo: false}` is now `turbo: false`, and `params`
|
|
215
|
-
is no longer supported.
|
|
216
|
+
is no longer supported. Instead of a `position`, use
|
|
217
|
+
`config.menu.insert(index, ...)` to place an entry between Alchemy's
|
|
218
|
+
own entries, e.g. `4` to place it between Languages and Sites.
|
|
216
219
|
|
|
217
220
|
Call `config.menu.add` once at boot and not inside `to_prepare`, as it
|
|
218
221
|
does not skip entries that are already present like `register_module`
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
module Alchemy
|
|
4
4
|
class Upgrader
|
|
5
5
|
# Rewrites +Alchemy::Modules.register_module+ calls with a static literal
|
|
6
|
-
# argument into +Alchemy.config.menu.add+ calls.
|
|
6
|
+
# argument into +Alchemy.config.menu.add+ calls, or +Alchemy.config.menu.insert+
|
|
7
|
+
# calls if they have a position within the default menu.
|
|
7
8
|
#
|
|
8
9
|
# Calls inside a +to_prepare+ block are guarded by the entry's name, because
|
|
9
10
|
# +Alchemy.config.menu.add+ does not skip entries that are already present.
|
|
@@ -15,7 +16,7 @@ module Alchemy
|
|
|
15
16
|
RECEIVERS = ["Alchemy::Modules", "::Alchemy::Modules"].freeze
|
|
16
17
|
|
|
17
18
|
KEYS = %w[
|
|
18
|
-
name title engine_name
|
|
19
|
+
name title engine_name icon icon_style image inline_image turbo
|
|
19
20
|
controller action nested_controllers nested_actions children nested
|
|
20
21
|
].freeze
|
|
21
22
|
|
|
@@ -70,24 +71,36 @@ module Alchemy
|
|
|
70
71
|
return unless arguments.first.is_a?(Prism::HashNode) || arguments.first.is_a?(Prism::KeywordHashNode)
|
|
71
72
|
return unless heredocs_inside?(arguments.first, call, source)
|
|
72
73
|
|
|
73
|
-
|
|
74
|
+
definition = literal(arguments.first)
|
|
75
|
+
item = Alchemy::Configurations::MenuItem.from_legacy_hash(definition)
|
|
74
76
|
return if in_to_prepare && item.name.blank?
|
|
75
77
|
|
|
78
|
+
index = menu_index(definition["position"])
|
|
79
|
+
|
|
76
80
|
indent = " " * call.location.start_column
|
|
77
81
|
if in_to_prepare
|
|
78
82
|
<<~RUBY.chomp
|
|
79
83
|
unless Alchemy.config.menu.any? { |item| item.name == #{item.name.inspect} }
|
|
80
|
-
#{indent} #{menu_add(item, "#{indent} ")}
|
|
84
|
+
#{indent} #{menu_add(item, "#{indent} ", index)}
|
|
81
85
|
#{indent}end
|
|
82
86
|
RUBY
|
|
83
87
|
else
|
|
84
|
-
menu_add(item, indent)
|
|
88
|
+
menu_add(item, indent, index)
|
|
85
89
|
end
|
|
86
90
|
rescue NonLiteralError, Alchemy::Configuration::ConfigurationError
|
|
87
91
|
nil
|
|
88
92
|
end
|
|
89
93
|
|
|
90
|
-
|
|
94
|
+
# @return [Integer, nil] The index to insert the entry at or +nil+ to append it
|
|
95
|
+
def menu_index(position)
|
|
96
|
+
return if position.nil?
|
|
97
|
+
raise NonLiteralError unless position.is_a?(Numeric)
|
|
98
|
+
|
|
99
|
+
index = Alchemy::Configurations::MenuItem.index_for_legacy_position(position)
|
|
100
|
+
index if index < Alchemy::Configurations::Menu::DEFAULT.size
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def menu_add(item, indent, index)
|
|
91
104
|
pairs = attributes(item).map do |key, value|
|
|
92
105
|
if key == "children" || key == "nested"
|
|
93
106
|
entries = value.map { |entry| "#{indent} #{inline_hash(entry)}" }
|
|
@@ -96,7 +109,11 @@ module Alchemy
|
|
|
96
109
|
"#{indent} #{key}: #{value.inspect}"
|
|
97
110
|
end
|
|
98
111
|
end
|
|
99
|
-
|
|
112
|
+
if index
|
|
113
|
+
"Alchemy.config.menu.insert(\n#{indent} #{index},\n#{pairs.join(",\n")}\n#{indent})"
|
|
114
|
+
else
|
|
115
|
+
"Alchemy.config.menu.add(\n#{pairs.join(",\n")}\n#{indent})"
|
|
116
|
+
end
|
|
100
117
|
end
|
|
101
118
|
|
|
102
119
|
def inline_hash(item)
|
data/lib/alchemy/version.rb
CHANGED