alchemy_cms 8.1.9 → 8.2.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.
- checksums.yaml +4 -4
- data/README.md +12 -1
- data/app/assets/builds/alchemy/admin.css +1 -1
- data/app/assets/builds/alchemy/alchemy_admin.min.js +2 -0
- data/app/assets/builds/alchemy/alchemy_admin.min.js.map +1 -0
- data/app/assets/builds/alchemy/dark-theme.css +1 -1
- data/app/assets/builds/alchemy/light-theme.css +1 -1
- data/app/assets/builds/alchemy/theme.css +1 -1
- data/app/assets/builds/alchemy/welcome.css +1 -1
- data/app/assets/builds/tinymce/skins/content/alchemy/content.min.css +1 -1
- data/app/assets/builds/tinymce/skins/content/alchemy-dark/content.min.css +1 -1
- data/app/assets/images/alchemy/icons-sprite.svg +1 -1
- data/app/components/alchemy/admin/current_user_name.rb +34 -0
- data/app/components/alchemy/admin/locale_select.rb +12 -8
- data/app/components/alchemy/admin/page_node.html.erb +3 -2
- data/app/components/alchemy/admin/picture_thumbnail.rb +1 -1
- data/app/components/alchemy/admin/preview_time_select.rb +55 -0
- data/app/components/alchemy/admin/publish_element_button.html.erb +41 -0
- data/app/components/alchemy/admin/publish_element_button.rb +13 -0
- data/app/components/alchemy/admin/timezone_select.rb +47 -0
- data/app/components/alchemy/ingredients/select_editor.rb +6 -1
- data/app/controllers/alchemy/admin/base_controller.rb +1 -0
- data/app/controllers/alchemy/admin/elements_controller.rb +54 -34
- data/app/controllers/alchemy/admin/pages_controller.rb +1 -0
- data/app/controllers/alchemy/admin/resources_controller.rb +11 -6
- data/app/controllers/alchemy/pages_controller.rb +1 -2
- data/app/helpers/alchemy/admin/base_helper.rb +4 -7
- data/app/helpers/alchemy/url_helper.rb +2 -10
- data/app/javascript/alchemy_admin/components/element_editor/publish_element_button.js +28 -27
- data/app/javascript/alchemy_admin/components/element_editor.js +11 -2
- data/app/javascript/alchemy_admin/components/message.js +5 -1
- data/app/javascript/alchemy_admin/components/uploader/file_upload.js +5 -5
- data/app/javascript/alchemy_admin/image_cropper.js +10 -6
- data/app/javascript/alchemy_admin/initializer.js +6 -33
- data/app/javascript/alchemy_admin/shoelace_theme.js +6 -2
- data/app/javascript/alchemy_admin/templates/compiled.js +1 -1
- data/app/javascript/alchemy_admin.js +12 -2
- data/app/models/alchemy/attachment.rb +1 -1
- data/app/models/alchemy/current.rb +5 -1
- data/app/models/alchemy/element/element_ingredients.rb +11 -3
- data/app/models/alchemy/element.rb +10 -0
- data/app/models/alchemy/ingredient.rb +2 -0
- data/app/models/alchemy/ingredients/select.rb +1 -2
- data/app/models/alchemy/page/etag_generator.rb +21 -0
- data/app/models/alchemy/page/url_path.rb +11 -2
- data/app/models/alchemy/page.rb +12 -2
- data/app/models/alchemy/page_version.rb +5 -5
- data/app/models/alchemy/picture.rb +19 -2
- data/app/models/alchemy/storage_adapter/active_storage.rb +9 -0
- data/app/models/alchemy/storage_adapter/dragonfly.rb +9 -0
- data/app/models/alchemy/storage_adapter.rb +1 -0
- data/app/models/concerns/alchemy/publishable.rb +20 -12
- data/app/models/concerns/alchemy/relatable_resource.rb +16 -2
- data/app/models/concerns/alchemy/touch_elements.rb +3 -3
- data/app/services/alchemy/element_preloader.rb +107 -0
- data/app/stylesheets/alchemy/_custom-properties.scss +1 -0
- data/app/stylesheets/alchemy/_mixins.scss +1 -1
- data/app/stylesheets/alchemy/_themes.scss +2 -0
- data/app/stylesheets/alchemy/admin/base.scss +2 -1
- data/app/stylesheets/alchemy/admin/elements.scss +22 -19
- data/app/stylesheets/alchemy/admin/form_fields.scss +3 -0
- data/app/stylesheets/alchemy/admin/forms.scss +14 -1
- data/app/stylesheets/alchemy/admin/frame.scss +9 -8
- data/app/stylesheets/alchemy/admin/notices.scss +1 -1
- data/app/stylesheets/alchemy/admin/popover.scss +37 -0
- data/app/stylesheets/alchemy/admin/selects.scss +4 -0
- data/app/stylesheets/alchemy/admin/shoelace.scss +16 -4
- data/app/stylesheets/alchemy/admin/toolbar.scss +8 -0
- data/app/stylesheets/alchemy/admin.scss +1 -0
- data/app/views/alchemy/admin/_header.html.erb +4 -0
- data/app/views/alchemy/admin/_left_menu.html.erb +24 -0
- data/app/views/alchemy/admin/_main_navi.html.erb +6 -0
- data/app/views/alchemy/admin/_top_menu.html.erb +6 -0
- data/app/views/alchemy/admin/_user_info.html.erb +5 -0
- data/app/views/alchemy/admin/crop.html.erb +6 -11
- data/app/views/alchemy/admin/elements/_header.html.erb +16 -6
- data/app/views/alchemy/admin/elements/_schedule.html.erb +62 -0
- data/app/views/alchemy/admin/elements/_toolbar.html.erb +1 -15
- data/app/views/alchemy/admin/elements/publish.turbo_stream.erb +28 -0
- data/app/views/alchemy/admin/nodes/index.html.erb +1 -1
- data/app/views/alchemy/admin/pages/_locked_pages.html.erb +5 -0
- data/app/views/alchemy/admin/pages/_publication_fields.html.erb +4 -4
- data/app/views/alchemy/admin/pages/_table.html.erb +1 -1
- data/app/views/alchemy/admin/pages/edit.html.erb +6 -2
- data/app/views/alchemy/admin/partials/_language_tree_select.html.erb +10 -10
- data/app/views/alchemy/admin/partials/_site_select.html.erb +6 -3
- data/app/views/alchemy/admin/pictures/index.html.erb +2 -2
- data/app/views/alchemy/admin/tinymce/_setup.html.erb +9 -16
- data/app/views/alchemy/admin/uploader/_setup.html.erb +1 -6
- data/app/views/alchemy/language_links/_language.html.erb +1 -2
- data/app/views/layouts/alchemy/admin.html.erb +2 -45
- data/config/importmap.rb +7 -2
- data/config/locales/alchemy.en.yml +35 -5
- data/lib/alchemy/admin/preview_time.rb +23 -0
- data/lib/alchemy/admin/preview_url.rb +13 -2
- data/lib/alchemy/admin/timezone.rb +56 -0
- data/lib/alchemy/configuration.rb +2 -0
- data/lib/alchemy/configurations/main.rb +13 -1
- data/lib/alchemy/tasks/tidy.rb +6 -7
- data/lib/alchemy/test_support/factories/element_factory.rb +2 -2
- data/lib/alchemy/test_support/relatable_resource_examples.rb +2 -2
- data/lib/alchemy/test_support/shared_publishable_examples.rb +44 -2
- data/lib/alchemy/upgrader.rb +3 -1
- data/lib/alchemy/version.rb +1 -1
- data/lib/alchemy_cms.rb +2 -0
- data/lib/generators/alchemy/install/install_generator.rb +2 -1
- data/vendor/javascript/handlebars.min.js +4 -4
- data/vendor/javascript/shoelace.min.js +1419 -1323
- data/vendor/javascript/sortable.min.js +2 -2
- data/vendor/javascript/tinymce.min.js +1 -1
- metadata +35 -1
|
@@ -20,20 +20,29 @@ module Alchemy
|
|
|
20
20
|
# link_to page.url
|
|
21
21
|
#
|
|
22
22
|
class UrlPath
|
|
23
|
-
def initialize(page)
|
|
23
|
+
def initialize(page, optional_params = {})
|
|
24
24
|
@page = page
|
|
25
25
|
@language = @page.language
|
|
26
26
|
@site = @language.site
|
|
27
|
+
@optional_params = optional_params
|
|
27
28
|
end
|
|
28
29
|
|
|
29
30
|
def call
|
|
30
|
-
if @page.language_root?
|
|
31
|
+
path = if @page.language_root?
|
|
31
32
|
language_root_path
|
|
32
33
|
elsif @site.languages.count(&:public?) > 1
|
|
33
34
|
page_path_with_language_prefix
|
|
34
35
|
else
|
|
35
36
|
page_path_with_leading_slash
|
|
36
37
|
end
|
|
38
|
+
|
|
39
|
+
if @optional_params.present?
|
|
40
|
+
uri = URI(path)
|
|
41
|
+
uri.query = @optional_params.to_query
|
|
42
|
+
uri.to_s
|
|
43
|
+
else
|
|
44
|
+
path
|
|
45
|
+
end
|
|
37
46
|
end
|
|
38
47
|
|
|
39
48
|
private
|
data/app/models/alchemy/page.rb
CHANGED
|
@@ -135,6 +135,8 @@ module Alchemy
|
|
|
135
135
|
|
|
136
136
|
has_many :page_ingredients, class_name: "Alchemy::Ingredients::Page", foreign_key: :related_object_id, dependent: :nullify
|
|
137
137
|
|
|
138
|
+
before_destroy :check_descendants_for_menu_nodes
|
|
139
|
+
|
|
138
140
|
before_validation :set_language,
|
|
139
141
|
if: -> { language.nil? }
|
|
140
142
|
|
|
@@ -303,8 +305,8 @@ module Alchemy
|
|
|
303
305
|
# = The url_path for this page
|
|
304
306
|
#
|
|
305
307
|
# @see Alchemy::Page::UrlPath#call
|
|
306
|
-
def url_path
|
|
307
|
-
self.class.url_path_class.new(self).call
|
|
308
|
+
def url_path(optional_params = {})
|
|
309
|
+
self.class.url_path_class.new(self, optional_params).call
|
|
308
310
|
end
|
|
309
311
|
|
|
310
312
|
# The page's view partial is dependent from its page layout
|
|
@@ -611,5 +613,13 @@ module Alchemy
|
|
|
611
613
|
ids = node_ids + nodes.flat_map { |n| n.ancestors.pluck(:id) }
|
|
612
614
|
Node.where(id: ids).touch_all
|
|
613
615
|
end
|
|
616
|
+
|
|
617
|
+
def check_descendants_for_menu_nodes
|
|
618
|
+
pages_with_nodes = descendants.joins(:nodes).reorder("alchemy_pages.lft").distinct
|
|
619
|
+
if pages_with_nodes.exists?
|
|
620
|
+
errors.add(:descendants, :still_attached_to_nodes, page_names: pages_with_nodes.map(&:name).to_sentence)
|
|
621
|
+
throw :abort
|
|
622
|
+
end
|
|
623
|
+
end
|
|
614
624
|
end
|
|
615
625
|
end
|
|
@@ -37,21 +37,21 @@ module Alchemy
|
|
|
37
37
|
#
|
|
38
38
|
# @param time [DateTime] (Time.current)
|
|
39
39
|
# @returns Boolean
|
|
40
|
-
def public?(time =
|
|
40
|
+
def public?(time = Current.preview_time)
|
|
41
41
|
already_public_for?(time) && still_public_for?(time)
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
# Determines if this version is already public for given time
|
|
45
|
-
# @param time [DateTime] (
|
|
45
|
+
# @param time [DateTime] (Current.preview_time)
|
|
46
46
|
# @returns Boolean
|
|
47
|
-
def already_public_for?(time =
|
|
47
|
+
def already_public_for?(time = Current.preview_time)
|
|
48
48
|
!public_on.nil? && public_on <= time
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
# Determines if this version is still public for given time
|
|
52
|
-
# @param time [DateTime] (
|
|
52
|
+
# @param time [DateTime] (Current.preview_time)
|
|
53
53
|
# @returns Boolean
|
|
54
|
-
def still_public_for?(time =
|
|
54
|
+
def still_public_for?(time = Current.preview_time)
|
|
55
55
|
public_until.nil? || public_until >= time
|
|
56
56
|
end
|
|
57
57
|
|
|
@@ -133,6 +133,16 @@ module Alchemy
|
|
|
133
133
|
def file_formats(scope = all)
|
|
134
134
|
Alchemy.storage_adapter.file_formats(name, scope:)
|
|
135
135
|
end
|
|
136
|
+
|
|
137
|
+
# Preload associations for element editor display
|
|
138
|
+
#
|
|
139
|
+
# @param pictures [Array<Picture>] Collection of pictures to preload for
|
|
140
|
+
def alchemy_element_preloads(pictures)
|
|
141
|
+
return if pictures.blank?
|
|
142
|
+
|
|
143
|
+
# Preload storage-specific associations to avoid N+1 when rendering thumbnails
|
|
144
|
+
Alchemy.storage_adapter.preload_picture_associations(pictures)
|
|
145
|
+
end
|
|
136
146
|
end
|
|
137
147
|
|
|
138
148
|
# Instance methods
|
|
@@ -183,8 +193,15 @@ module Alchemy
|
|
|
183
193
|
end
|
|
184
194
|
|
|
185
195
|
# Returns the picture description for a given language.
|
|
196
|
+
#
|
|
197
|
+
# @param language [Language] The language to get description for
|
|
198
|
+
# @return [String, nil] The description text or nil
|
|
186
199
|
def description_for(language)
|
|
187
|
-
descriptions.
|
|
200
|
+
if descriptions.loaded?
|
|
201
|
+
descriptions.detect { _1.language == language }&.text
|
|
202
|
+
else
|
|
203
|
+
descriptions.find_by(language: language)&.text
|
|
204
|
+
end
|
|
188
205
|
end
|
|
189
206
|
|
|
190
207
|
# Returns an uri escaped name.
|
|
@@ -236,7 +253,7 @@ module Alchemy
|
|
|
236
253
|
# even if it is also assigned on a restricted page.
|
|
237
254
|
#
|
|
238
255
|
def restricted?
|
|
239
|
-
|
|
256
|
+
related_pages.any? && related_pages.not_restricted.blank?
|
|
240
257
|
end
|
|
241
258
|
|
|
242
259
|
def image_file_name
|
|
@@ -188,6 +188,15 @@ module Alchemy
|
|
|
188
188
|
pictures.with_attached_image_file
|
|
189
189
|
end
|
|
190
190
|
|
|
191
|
+
# Preload picture associations on already-loaded records
|
|
192
|
+
# @param [Array<Alchemy::Picture>] pictures
|
|
193
|
+
def preload_picture_associations(pictures)
|
|
194
|
+
ActiveRecord::Associations::Preloader.new(
|
|
195
|
+
records: pictures,
|
|
196
|
+
associations: {image_file_attachment: :blob}
|
|
197
|
+
).call
|
|
198
|
+
end
|
|
199
|
+
|
|
191
200
|
# @param [Alchemy::Attachment]
|
|
192
201
|
# @return [TrueClass, FalseClass]
|
|
193
202
|
def set_attachment_name?(attachment)
|
|
@@ -213,6 +213,15 @@ module Alchemy
|
|
|
213
213
|
pictures.includes(:thumbs)
|
|
214
214
|
end
|
|
215
215
|
|
|
216
|
+
# Preload picture associations on already-loaded records
|
|
217
|
+
# @param [Array<Alchemy::Picture>] pictures
|
|
218
|
+
def preload_picture_associations(pictures)
|
|
219
|
+
ActiveRecord::Associations::Preloader.new(
|
|
220
|
+
records: pictures,
|
|
221
|
+
associations: :thumbs
|
|
222
|
+
).call
|
|
223
|
+
end
|
|
224
|
+
|
|
216
225
|
# @param [Alchemy::Attachment]
|
|
217
226
|
# @return [TrueClass, FalseClass]
|
|
218
227
|
def set_attachment_name?(attachment)
|
|
@@ -6,13 +6,21 @@ module Alchemy
|
|
|
6
6
|
scope :draft, -> { where(public_on: nil) }
|
|
7
7
|
scope :scheduled, -> { where.not(public_on: nil) }
|
|
8
8
|
|
|
9
|
-
scope :published, ->(at:
|
|
9
|
+
scope :published, ->(at: Current.preview_time) {
|
|
10
10
|
scheduled
|
|
11
11
|
.where("#{table_name}.public_on <= :at", at:)
|
|
12
12
|
.where(public_until: nil).or(
|
|
13
13
|
where("#{table_name}.public_until > :at", at:)
|
|
14
14
|
)
|
|
15
15
|
}
|
|
16
|
+
|
|
17
|
+
validate do
|
|
18
|
+
if public_on.present? && public_until.present?
|
|
19
|
+
if public_until <= public_on
|
|
20
|
+
errors.add(:public_until, :must_be_after_public_on)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
16
24
|
end
|
|
17
25
|
|
|
18
26
|
# Determines if this record is public
|
|
@@ -21,15 +29,15 @@ module Alchemy
|
|
|
21
29
|
# and returns true if the time given (+Time.current+ per default)
|
|
22
30
|
# is in this timespan.
|
|
23
31
|
#
|
|
24
|
-
# @param
|
|
32
|
+
# @param at [DateTime] (Time.current)
|
|
25
33
|
# @returns Boolean
|
|
26
|
-
def public?(
|
|
27
|
-
already_public_for?(
|
|
34
|
+
def public?(at: Current.preview_time)
|
|
35
|
+
already_public_for?(at:) && still_public_for?(at:)
|
|
28
36
|
end
|
|
29
37
|
alias_method :public, :public?
|
|
30
38
|
|
|
31
|
-
def scheduled?
|
|
32
|
-
public_on
|
|
39
|
+
def scheduled?(at: Current.preview_time)
|
|
40
|
+
(public_on.present? && public_on > at) || (public_until.present? && public_until > at)
|
|
33
41
|
end
|
|
34
42
|
|
|
35
43
|
# Determines if this record is publishable
|
|
@@ -42,17 +50,17 @@ module Alchemy
|
|
|
42
50
|
end
|
|
43
51
|
|
|
44
52
|
# Determines if this record is already public for given time
|
|
45
|
-
# @param
|
|
53
|
+
# @param at [DateTime] (Time.current)
|
|
46
54
|
# @returns Boolean
|
|
47
|
-
def already_public_for?(
|
|
48
|
-
!public_on.nil? && public_on <=
|
|
55
|
+
def already_public_for?(at: Current.preview_time)
|
|
56
|
+
!public_on.nil? && public_on <= at
|
|
49
57
|
end
|
|
50
58
|
|
|
51
59
|
# Determines if this record is still public for given time
|
|
52
|
-
# @param
|
|
60
|
+
# @param at [DateTime] (Time.current)
|
|
53
61
|
# @returns Boolean
|
|
54
|
-
def still_public_for?(
|
|
55
|
-
public_until.nil? || public_until
|
|
62
|
+
def still_public_for?(at: Current.preview_time)
|
|
63
|
+
public_until.nil? || public_until > at
|
|
56
64
|
end
|
|
57
65
|
end
|
|
58
66
|
end
|
|
@@ -2,6 +2,20 @@ module Alchemy
|
|
|
2
2
|
module RelatableResource
|
|
3
3
|
extend ActiveSupport::Concern
|
|
4
4
|
|
|
5
|
+
class_methods do
|
|
6
|
+
# Preload associations for element editor display
|
|
7
|
+
#
|
|
8
|
+
# Override this method in models that need custom preloading
|
|
9
|
+
# when displayed in the element editor (e.g., preloading
|
|
10
|
+
# picture thumbnails or products).
|
|
11
|
+
#
|
|
12
|
+
# @param records [Array] Collection of records to preload for
|
|
13
|
+
def alchemy_element_preloads(records)
|
|
14
|
+
# Default implementation does nothing
|
|
15
|
+
# Override in subclasses that need preloading
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
5
19
|
included do
|
|
6
20
|
scope :deletable, -> do
|
|
7
21
|
where(
|
|
@@ -15,8 +29,8 @@ module Alchemy
|
|
|
15
29
|
foreign_key: "related_object_id",
|
|
16
30
|
as: :related_object
|
|
17
31
|
|
|
18
|
-
has_many :
|
|
19
|
-
has_many :
|
|
32
|
+
has_many :related_elements, through: :related_ingredients, source: :element
|
|
33
|
+
has_many :related_pages, through: :related_elements, source: :page
|
|
20
34
|
end
|
|
21
35
|
|
|
22
36
|
# Returns true if object is not assigned to any ingredient.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Alchemy
|
|
4
|
-
# If the model has a +
|
|
4
|
+
# If the model has a +related_elements+ association,
|
|
5
5
|
# it updates all their timestamps after save.
|
|
6
6
|
#
|
|
7
7
|
# Should only be used on bottom to top relations,
|
|
@@ -16,9 +16,9 @@ module Alchemy
|
|
|
16
16
|
private
|
|
17
17
|
|
|
18
18
|
def touch_elements
|
|
19
|
-
return unless respond_to?(:
|
|
19
|
+
return unless respond_to?(:related_elements)
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
related_elements.touch_all
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
24
|
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemy
|
|
4
|
+
# Preloads element trees with all associations and nested elements
|
|
5
|
+
#
|
|
6
|
+
# This service efficiently loads element trees to avoid N+1 queries.
|
|
7
|
+
# It recursively preloads all nested elements to unlimited depth.
|
|
8
|
+
#
|
|
9
|
+
# @example Preload elements for a page version
|
|
10
|
+
# preloader = Alchemy::ElementPreloader.new(page_version: page_version)
|
|
11
|
+
# preloaded_elements = preloader.call
|
|
12
|
+
#
|
|
13
|
+
class ElementPreloader
|
|
14
|
+
# @param page_version [PageVersion] The page version to preload elements for
|
|
15
|
+
def initialize(page_version:)
|
|
16
|
+
@page_version = page_version
|
|
17
|
+
ActiveRecord::Associations::Preloader.new(
|
|
18
|
+
records: [page_version],
|
|
19
|
+
associations: {page: :language}
|
|
20
|
+
).call
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Preloads and returns the element tree with all associations loaded
|
|
24
|
+
#
|
|
25
|
+
# @return [Array<Element>] Elements with preloaded nested elements
|
|
26
|
+
def call
|
|
27
|
+
# Load all elements for the page version with associations
|
|
28
|
+
all_elements = load_all_elements
|
|
29
|
+
return [] if all_elements.empty?
|
|
30
|
+
|
|
31
|
+
# Build parent -> children lookup and populate associations
|
|
32
|
+
populate_nested_associations(all_elements)
|
|
33
|
+
|
|
34
|
+
# Root elements are those without a parent
|
|
35
|
+
root_elements = all_elements.values
|
|
36
|
+
.select { |e| e.parent_element_id.nil? }
|
|
37
|
+
.sort_by(&:position)
|
|
38
|
+
return [] if root_elements.empty?
|
|
39
|
+
|
|
40
|
+
preload_related_objects(root_elements)
|
|
41
|
+
|
|
42
|
+
root_elements
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
attr_reader :page_version
|
|
48
|
+
|
|
49
|
+
# Load all elements for the page version and preload their associations
|
|
50
|
+
def load_all_elements
|
|
51
|
+
Element
|
|
52
|
+
.where(page_version_id: page_version.id)
|
|
53
|
+
.includes(*element_includes)
|
|
54
|
+
.index_by(&:id)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Populate the all_nested_elements association for each element
|
|
58
|
+
def populate_nested_associations(elements_by_id)
|
|
59
|
+
# Group elements by parent_id
|
|
60
|
+
elements_by_parent = elements_by_id.values.group_by(&:parent_element_id)
|
|
61
|
+
|
|
62
|
+
elements_by_id.each_value do |element|
|
|
63
|
+
children = elements_by_parent[element.id] || []
|
|
64
|
+
children = children.sort_by(&:position)
|
|
65
|
+
|
|
66
|
+
# Manually set the association target
|
|
67
|
+
element.association(:all_nested_elements).target = children
|
|
68
|
+
element.association(:all_nested_elements).loaded!
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Associations to preload for element rendering
|
|
73
|
+
def element_includes
|
|
74
|
+
[
|
|
75
|
+
{ingredients: :related_object},
|
|
76
|
+
:tags
|
|
77
|
+
]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Preload related objects for all ingredients in elements
|
|
81
|
+
# Allows related objects to preload their associations (e.g., picture thumbnails)
|
|
82
|
+
def preload_related_objects(root_elements)
|
|
83
|
+
related_objects_by_class = collect_related_objects(root_elements)
|
|
84
|
+
return if related_objects_by_class.empty?
|
|
85
|
+
|
|
86
|
+
related_objects_by_class.each do |klass, objects|
|
|
87
|
+
if klass.respond_to?(:alchemy_element_preloads)
|
|
88
|
+
klass.alchemy_element_preloads(objects)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Collect unique related objects from element tree, grouped by class
|
|
94
|
+
def collect_related_objects(elements, collected = Hash.new { |h, k| h[k] = {} })
|
|
95
|
+
elements.each do |element|
|
|
96
|
+
element.ingredients.each do |ingredient|
|
|
97
|
+
obj = ingredient.related_object
|
|
98
|
+
collected[obj.class][obj.id] = obj if obj
|
|
99
|
+
end
|
|
100
|
+
if element.association(:all_nested_elements).loaded?
|
|
101
|
+
collect_related_objects(element.all_nested_elements, collected)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
collected.transform_values(&:values)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
--button-secondary-hover-bg-color: var(--a-dark-grey);
|
|
25
25
|
--button-secondary-hover-border-color: var(--a-grey);
|
|
26
26
|
--button-secondary-text-color: var(--color-white);
|
|
27
|
+
--button-primary-text-color: var(--a-darker-grey);
|
|
27
28
|
|
|
28
29
|
--code-background-color: var(--a-darkest-grey);
|
|
29
30
|
--code-border-color: var(--a-grey);
|
|
@@ -309,6 +310,7 @@
|
|
|
309
310
|
--button-secondary-hover-bg-color: var(--color-white);
|
|
310
311
|
--button-secondary-hover-border-color: var(--color-grey_medium);
|
|
311
312
|
--button-secondary-text-color: var(--text-color);
|
|
313
|
+
--button-primary-text-color: var(--color-white);
|
|
312
314
|
|
|
313
315
|
--code-background-color: var(--color-grey_light);
|
|
314
316
|
--code-border-color: var(--border-color);
|
|
@@ -237,21 +237,17 @@ button.element-toggle {
|
|
|
237
237
|
.element-toggle {
|
|
238
238
|
margin-left: 0;
|
|
239
239
|
}
|
|
240
|
-
|
|
241
|
-
.element-hidden-icon {
|
|
242
|
-
display: inline-flex;
|
|
243
|
-
align-items: center;
|
|
244
|
-
gap: var(--spacing-1);
|
|
245
|
-
margin-left: auto;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
.element-hidden-label {
|
|
249
|
-
line-height: 1;
|
|
250
|
-
font-size: var(--font-size_small);
|
|
251
|
-
}
|
|
252
240
|
}
|
|
253
241
|
}
|
|
254
242
|
|
|
243
|
+
.element-status-icons {
|
|
244
|
+
display: inline-flex;
|
|
245
|
+
align-items: center;
|
|
246
|
+
gap: var(--spacing-1);
|
|
247
|
+
margin-left: auto;
|
|
248
|
+
font-size: var(--font-size_small);
|
|
249
|
+
}
|
|
250
|
+
|
|
255
251
|
&.is-fixed {
|
|
256
252
|
border-width: 0;
|
|
257
253
|
border-radius: 0;
|
|
@@ -355,10 +351,6 @@ button.element-toggle {
|
|
|
355
351
|
}
|
|
356
352
|
|
|
357
353
|
&.compact {
|
|
358
|
-
.element-hidden-label {
|
|
359
|
-
display: none;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
354
|
.element-toolbar {
|
|
363
355
|
visibility: hidden;
|
|
364
356
|
position: absolute;
|
|
@@ -437,6 +429,16 @@ button.element-toggle {
|
|
|
437
429
|
transform 1s ease-in;
|
|
438
430
|
}
|
|
439
431
|
|
|
432
|
+
.alchemy-popover {
|
|
433
|
+
width: 360px;
|
|
434
|
+
padding-bottom: var(--spacing-0);
|
|
435
|
+
|
|
436
|
+
label {
|
|
437
|
+
margin-bottom: var(--spacing-1);
|
|
438
|
+
font-size: var(--font-size_small);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
440
442
|
.element-body {
|
|
441
443
|
margin: var(--spacing-2);
|
|
442
444
|
}
|
|
@@ -449,7 +451,7 @@ button.element-toggle {
|
|
|
449
451
|
text-align: left;
|
|
450
452
|
}
|
|
451
453
|
|
|
452
|
-
alchemy-message {
|
|
454
|
+
> alchemy-message {
|
|
453
455
|
margin: var(--spacing-2);
|
|
454
456
|
}
|
|
455
457
|
|
|
@@ -538,7 +540,7 @@ button.element-toggle {
|
|
|
538
540
|
}
|
|
539
541
|
}
|
|
540
542
|
|
|
541
|
-
.element-
|
|
543
|
+
.element-status-icons {
|
|
542
544
|
display: none;
|
|
543
545
|
white-space: nowrap;
|
|
544
546
|
flex-shrink: 0;
|
|
@@ -1075,7 +1077,8 @@ select.long {
|
|
|
1075
1077
|
.picture_thumbnail,
|
|
1076
1078
|
.select2-container,
|
|
1077
1079
|
.tinymce_container {
|
|
1078
|
-
|
|
1080
|
+
box-shadow: 0 0 0 1px var(--element-dirty-border-color);
|
|
1081
|
+
border-color: var(--element-dirty-border-color);
|
|
1079
1082
|
}
|
|
1080
1083
|
}
|
|
1081
1084
|
|
|
@@ -260,11 +260,24 @@ form {
|
|
|
260
260
|
}
|
|
261
261
|
|
|
262
262
|
.submit {
|
|
263
|
+
display: flex;
|
|
263
264
|
padding: var(--spacing-1) 0;
|
|
264
|
-
|
|
265
|
+
justify-content: space-between;
|
|
266
|
+
|
|
267
|
+
> button:last-of-type {
|
|
268
|
+
margin-left: auto;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
&.simple_form {
|
|
273
|
+
.buttons {
|
|
274
|
+
margin-top: var(--spacing-1);
|
|
275
|
+
margin-left: var(--form-left-column-width);
|
|
276
|
+
}
|
|
265
277
|
}
|
|
266
278
|
|
|
267
279
|
.input-column {
|
|
280
|
+
width: 50%;
|
|
268
281
|
padding: 0 var(--spacing-1);
|
|
269
282
|
|
|
270
283
|
input[type] {
|
|
@@ -112,22 +112,23 @@ div#overlay_text_box {
|
|
|
112
112
|
border-left: var(--border-default);
|
|
113
113
|
white-space: nowrap;
|
|
114
114
|
background-color: var(--toolbar-bg-color);
|
|
115
|
-
|
|
116
|
-
select {
|
|
117
|
-
background-color: transparent;
|
|
118
|
-
border: none;
|
|
119
|
-
border-radius: 0;
|
|
120
|
-
border-left: var(--border-default);
|
|
121
|
-
}
|
|
115
|
+
gap: var(--spacing-1);
|
|
122
116
|
|
|
123
117
|
.current-user-name {
|
|
124
118
|
display: flex;
|
|
125
119
|
align-items: center;
|
|
126
120
|
gap: var(--spacing-1);
|
|
127
|
-
padding:
|
|
121
|
+
padding-left: var(--spacing-3);
|
|
128
122
|
|
|
129
123
|
@media screen and (max-width: vars.$large-screen-break-point) {
|
|
130
124
|
display: none;
|
|
131
125
|
}
|
|
132
126
|
}
|
|
133
127
|
}
|
|
128
|
+
|
|
129
|
+
#admin_locale {
|
|
130
|
+
background-color: transparent;
|
|
131
|
+
border: none;
|
|
132
|
+
border-radius: 0;
|
|
133
|
+
border-left: var(--border-default);
|
|
134
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
.alchemy-popover {
|
|
2
|
+
position: relative;
|
|
3
|
+
padding: var(--spacing-2);
|
|
4
|
+
|
|
5
|
+
alchemy-message {
|
|
6
|
+
margin-left: 0;
|
|
7
|
+
margin-right: 0;
|
|
8
|
+
margin-bottom: var(--spacing-3);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.timezone-select {
|
|
12
|
+
display: flex;
|
|
13
|
+
gap: var(--spacing-2);
|
|
14
|
+
align-items: center;
|
|
15
|
+
|
|
16
|
+
.alchemy_selectbox {
|
|
17
|
+
min-width: var(--select-x-large-width);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
span.error {
|
|
22
|
+
display: block;
|
|
23
|
+
background-color: var(--notice-error-background-color);
|
|
24
|
+
color: var(--notice-error-text-color);
|
|
25
|
+
padding: var(--spacing-2);
|
|
26
|
+
border-radius: var(--border-radius_medium);
|
|
27
|
+
font-size: var(--font-size_small);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.submit {
|
|
31
|
+
gap: var(--spacing-2);
|
|
32
|
+
|
|
33
|
+
button {
|
|
34
|
+
width: 50%;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|