alchemy_cms 8.0.9 → 8.1.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.
Files changed (280) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +14 -10
  3. data/app/assets/builds/alchemy/admin.css +1 -1
  4. data/app/assets/builds/alchemy/dark-theme.css +1 -1
  5. data/app/assets/builds/alchemy/light-theme.css +1 -1
  6. data/app/assets/builds/alchemy/preview.min.js +1 -1
  7. data/app/assets/builds/alchemy/theme.css +1 -1
  8. data/app/{views/alchemy/admin/elements/_element.html.erb → components/alchemy/admin/element_editor.html.erb} +34 -29
  9. data/app/components/alchemy/admin/element_editor.rb +115 -0
  10. data/app/components/alchemy/admin/element_select.rb +12 -9
  11. data/app/components/alchemy/admin/ingredient_editor.rb +54 -0
  12. data/app/components/alchemy/admin/list_filter.rb +16 -5
  13. data/app/components/alchemy/admin/page_node.html.erb +214 -0
  14. data/app/components/alchemy/admin/page_node.rb +70 -0
  15. data/app/components/alchemy/admin/picture_thumbnail.rb +36 -0
  16. data/app/components/alchemy/admin/publish_page_button.html.erb +15 -0
  17. data/app/components/alchemy/admin/publish_page_button.rb +54 -0
  18. data/app/{helpers/alchemy/admin/tags_helper.rb → components/alchemy/admin/tags_list.rb} +19 -11
  19. data/app/components/alchemy/admin/toolbar_button.rb +17 -13
  20. data/app/components/alchemy/ingredients/audio_editor.rb +8 -0
  21. data/app/components/alchemy/ingredients/base_editor.rb +222 -0
  22. data/app/components/alchemy/ingredients/boolean_editor.rb +21 -0
  23. data/app/components/alchemy/ingredients/color_editor.rb +80 -0
  24. data/app/components/alchemy/ingredients/color_view.rb +13 -0
  25. data/app/components/alchemy/ingredients/datetime_editor.rb +28 -0
  26. data/app/components/alchemy/ingredients/file_editor.rb +69 -0
  27. data/app/components/alchemy/ingredients/headline_editor.rb +88 -0
  28. data/app/components/alchemy/ingredients/html_editor.rb +11 -0
  29. data/app/components/alchemy/ingredients/link_editor.rb +29 -0
  30. data/app/components/alchemy/ingredients/node_editor.rb +23 -0
  31. data/app/components/alchemy/ingredients/number_editor.rb +28 -0
  32. data/app/components/alchemy/ingredients/page_editor.rb +19 -0
  33. data/app/components/alchemy/ingredients/picture_editor.rb +81 -0
  34. data/app/components/alchemy/ingredients/richtext_editor.rb +31 -0
  35. data/app/components/alchemy/ingredients/select_editor.rb +37 -0
  36. data/app/components/alchemy/ingredients/select_view.rb +7 -0
  37. data/app/components/alchemy/ingredients/text_editor.rb +41 -0
  38. data/app/components/alchemy/ingredients/video_editor.rb +8 -0
  39. data/app/controllers/alchemy/admin/attachments_controller.rb +8 -6
  40. data/app/controllers/alchemy/admin/base_controller.rb +7 -18
  41. data/app/controllers/alchemy/admin/clipboard_controller.rb +15 -11
  42. data/app/controllers/alchemy/admin/dashboard_controller.rb +2 -2
  43. data/app/controllers/alchemy/admin/elements_controller.rb +34 -32
  44. data/app/controllers/alchemy/admin/ingredients_controller.rb +1 -0
  45. data/app/controllers/alchemy/admin/layoutpages_controller.rb +2 -1
  46. data/app/controllers/alchemy/admin/legacy_page_urls_controller.rb +1 -1
  47. data/app/controllers/alchemy/admin/nodes_controller.rb +24 -1
  48. data/app/controllers/alchemy/admin/pages_controller.rb +36 -42
  49. data/app/controllers/alchemy/admin/pictures_controller.rb +2 -5
  50. data/app/controllers/alchemy/admin/resources_controller.rb +1 -1
  51. data/app/controllers/alchemy/api/ingredients_controller.rb +1 -1
  52. data/app/controllers/alchemy/api/pages_controller.rb +5 -3
  53. data/app/controllers/alchemy/base_controller.rb +6 -6
  54. data/app/controllers/alchemy/pages_controller.rb +12 -6
  55. data/app/controllers/concerns/alchemy/admin/archive_overlay.rb +0 -1
  56. data/app/controllers/concerns/alchemy/admin/clipboard.rb +57 -0
  57. data/app/controllers/concerns/alchemy/admin/uploader_responses.rb +2 -2
  58. data/app/controllers/concerns/alchemy/site_redirects.rb +1 -1
  59. data/app/decorators/alchemy/ingredient_editor.rb +37 -4
  60. data/app/helpers/alchemy/admin/base_helper.rb +10 -6
  61. data/app/helpers/alchemy/admin/ingredients_helper.rb +6 -3
  62. data/app/helpers/alchemy/base_helper.rb +1 -1
  63. data/app/helpers/alchemy/pages_helper.rb +1 -1
  64. data/app/javascript/alchemy_admin/components/action.js +5 -1
  65. data/app/javascript/alchemy_admin/components/color_select.js +73 -0
  66. data/app/javascript/alchemy_admin/components/element_editor/delete_element_button.js +11 -3
  67. data/app/javascript/alchemy_admin/components/element_editor/publish_element_button.js +7 -2
  68. data/app/javascript/alchemy_admin/components/element_editor.js +11 -12
  69. data/app/javascript/alchemy_admin/components/element_select.js +39 -17
  70. data/app/javascript/alchemy_admin/components/elements_window.js +0 -2
  71. data/app/javascript/alchemy_admin/components/file_editor.js +26 -0
  72. data/app/javascript/alchemy_admin/components/index.js +9 -0
  73. data/app/javascript/alchemy_admin/components/list_filter.js +57 -8
  74. data/app/javascript/alchemy_admin/components/message.js +9 -3
  75. data/app/javascript/alchemy_admin/components/page_node.js +119 -0
  76. data/app/javascript/alchemy_admin/{page_publication_fields.js → components/page_publication_fields.js} +9 -8
  77. data/app/javascript/alchemy_admin/{picture_editors.js → components/picture_editor.js} +30 -45
  78. data/app/javascript/alchemy_admin/components/picture_thumbnail.js +107 -0
  79. data/app/javascript/alchemy_admin/components/publish_page_button.js +41 -0
  80. data/app/javascript/alchemy_admin/components/select.js +3 -1
  81. data/app/javascript/alchemy_admin/components/sitemap.js +210 -0
  82. data/app/javascript/alchemy_admin/{sortable_elements.js → components/sortable_elements.js} +22 -25
  83. data/app/javascript/alchemy_admin/components/tinymce.js +10 -5
  84. data/app/javascript/alchemy_admin/components/uploader.js +30 -0
  85. data/app/javascript/alchemy_admin/image_overlay.js +0 -2
  86. data/app/javascript/alchemy_admin/initializer.js +0 -3
  87. data/app/javascript/alchemy_admin/link_dialog.js +1 -6
  88. data/app/javascript/alchemy_admin/templates/compiled.js +1 -1
  89. data/app/javascript/alchemy_admin/utils/ajax.js +15 -3
  90. data/app/javascript/alchemy_admin.js +0 -6
  91. data/app/models/alchemy/attachment.rb +4 -4
  92. data/app/models/alchemy/element/definitions.rb +1 -2
  93. data/app/models/alchemy/element/element_ingredients.rb +6 -2
  94. data/app/models/alchemy/element.rb +54 -13
  95. data/app/models/alchemy/element_definition.rb +4 -1
  96. data/app/models/alchemy/elements_repository.rb +6 -0
  97. data/app/models/alchemy/folded_page.rb +2 -2
  98. data/app/models/alchemy/ingredient.rb +38 -1
  99. data/app/models/alchemy/ingredient_definition.rb +4 -1
  100. data/app/models/alchemy/ingredient_validator.rb +6 -2
  101. data/app/models/alchemy/ingredients/color.rb +10 -0
  102. data/app/models/alchemy/ingredients/headline.rb +2 -17
  103. data/app/models/alchemy/ingredients/picture.rb +4 -4
  104. data/app/models/alchemy/ingredients/select.rb +19 -0
  105. data/app/models/alchemy/node.rb +28 -1
  106. data/app/models/alchemy/page/page_naming.rb +0 -7
  107. data/app/models/alchemy/page/page_natures.rb +7 -3
  108. data/app/models/alchemy/page/page_scopes.rb +13 -1
  109. data/app/models/alchemy/page/publisher.rb +14 -2
  110. data/app/models/alchemy/page.rb +102 -23
  111. data/app/models/alchemy/page_definition.rb +4 -1
  112. data/app/models/alchemy/page_version.rb +22 -6
  113. data/app/models/alchemy/picture.rb +10 -11
  114. data/app/models/alchemy/picture_variant.rb +1 -3
  115. data/app/models/alchemy/resource.rb +1 -1
  116. data/app/models/alchemy/storage_adapter/active_storage.rb +14 -2
  117. data/app/models/alchemy/storage_adapter/dragonfly.rb +12 -0
  118. data/app/models/alchemy/storage_adapter.rb +2 -0
  119. data/app/models/concerns/alchemy/picture_thumbnails.rb +4 -4
  120. data/app/models/concerns/alchemy/publishable.rb +54 -0
  121. data/app/serializers/alchemy/page_tree_serializer.rb +11 -31
  122. data/app/services/alchemy/copy_page.rb +17 -0
  123. data/app/services/alchemy/duplicate_element.rb +1 -1
  124. data/app/services/alchemy/page_tree_preloader.rb +105 -0
  125. data/app/stylesheets/alchemy/_extends.scss +3 -9
  126. data/app/stylesheets/alchemy/_mixins.scss +3 -1
  127. data/app/stylesheets/alchemy/_themes.scss +19 -10
  128. data/app/stylesheets/alchemy/admin/archive.scss +1 -0
  129. data/app/stylesheets/alchemy/admin/base.scss +5 -2
  130. data/app/stylesheets/alchemy/admin/buttons.scss +3 -3
  131. data/app/stylesheets/alchemy/admin/element-select.scss +18 -0
  132. data/app/stylesheets/alchemy/admin/elements.scss +123 -23
  133. data/app/stylesheets/alchemy/admin/errors.scss +1 -1
  134. data/app/stylesheets/alchemy/admin/flash.scss +6 -4
  135. data/app/stylesheets/alchemy/admin/images.scss +9 -5
  136. data/app/stylesheets/alchemy/admin/list_filter.scss +4 -4
  137. data/app/stylesheets/alchemy/admin/notices.scss +1 -2
  138. data/app/stylesheets/alchemy/admin/selects.scss +36 -21
  139. data/app/stylesheets/alchemy/admin/shoelace.scss +14 -1
  140. data/app/stylesheets/alchemy/admin/sitemap.scss +11 -3
  141. data/app/stylesheets/alchemy/admin/tags.scss +3 -1
  142. data/app/stylesheets/alchemy/admin/toolbar.scss +1 -1
  143. data/app/views/alchemy/_edit_mode.html.erb +1 -1
  144. data/app/views/alchemy/_menubar.html.erb +1 -1
  145. data/app/views/alchemy/admin/attachments/_archive_overlay.html.erb +35 -31
  146. data/app/views/alchemy/admin/attachments/_library_sidebar.html.erb +6 -0
  147. data/app/views/alchemy/admin/attachments/_overlay_file_list.html.erb +1 -1
  148. data/app/views/alchemy/admin/attachments/_replace_button.html.erb +1 -8
  149. data/app/views/alchemy/admin/attachments/_sorting_select.html.erb +13 -0
  150. data/app/views/alchemy/admin/attachments/_tag_list.html.erb +2 -3
  151. data/app/views/alchemy/admin/attachments/index.html.erb +5 -11
  152. data/app/views/alchemy/admin/attachments/show.html.erb +1 -1
  153. data/app/views/alchemy/admin/clipboard/_button.html.erb +1 -0
  154. data/app/views/alchemy/admin/clipboard/index.html.erb +4 -5
  155. data/app/views/alchemy/admin/clipboard/insert.turbo_stream.erb +1 -1
  156. data/app/views/alchemy/admin/crop.html.erb +5 -7
  157. data/app/views/alchemy/admin/elements/_add_nested_element_form.html.erb +6 -6
  158. data/app/views/alchemy/admin/elements/_fixed_element.html.erb +1 -1
  159. data/app/views/alchemy/admin/elements/_footer.html.erb +7 -1
  160. data/app/views/alchemy/admin/elements/_header.html.erb +5 -5
  161. data/app/views/alchemy/admin/elements/_toolbar.html.erb +33 -8
  162. data/app/views/alchemy/admin/elements/create.turbo_stream.erb +10 -10
  163. data/app/views/alchemy/admin/elements/index.html.erb +29 -16
  164. data/app/views/alchemy/admin/elements/new.html.erb +2 -2
  165. data/app/views/alchemy/admin/ingredients/update.turbo_stream.erb +3 -5
  166. data/app/views/alchemy/admin/leave.html.erb +1 -1
  167. data/app/views/alchemy/admin/nodes/_node.html.erb +19 -0
  168. data/app/views/alchemy/admin/nodes/edit.html.erb +1 -1
  169. data/app/views/alchemy/admin/nodes/index.html.erb +3 -1
  170. data/app/views/alchemy/admin/nodes/new.html.erb +14 -1
  171. data/app/views/alchemy/admin/pages/_current_page.html.erb +3 -1
  172. data/app/views/alchemy/admin/pages/_form.html.erb +21 -9
  173. data/app/views/alchemy/admin/pages/_page_status.html.erb +1 -1
  174. data/app/views/alchemy/admin/pages/_publication_fields.html.erb +28 -26
  175. data/app/views/alchemy/admin/pages/_table.html.erb +0 -7
  176. data/app/views/alchemy/admin/pages/_toolbar.html.erb +3 -6
  177. data/app/views/alchemy/admin/pages/edit.html.erb +5 -11
  178. data/app/views/alchemy/admin/pages/flush.turbo_stream.erb +2 -0
  179. data/app/views/alchemy/admin/pages/fold.turbo_stream.erb +5 -0
  180. data/app/views/alchemy/admin/pages/index.html.erb +5 -3
  181. data/app/views/alchemy/admin/pages/new.html.erb +2 -12
  182. data/app/views/alchemy/admin/pages/publish.turbo_stream.erb +12 -0
  183. data/app/views/alchemy/admin/pages/tree.html.erb +13 -0
  184. data/app/views/alchemy/admin/pages/update.turbo_stream.erb +5 -16
  185. data/app/views/alchemy/admin/partials/_flash_notices.html.erb +1 -1
  186. data/app/views/alchemy/admin/partials/{_remote_search_form.html.erb → _overlay_search_form.html.erb} +1 -2
  187. data/app/views/alchemy/admin/partials/_paste_from_clipboard_form.html.erb +12 -0
  188. data/app/views/alchemy/admin/pictures/_archive_overlay.html.erb +24 -21
  189. data/app/views/alchemy/admin/pictures/_filter_and_size_bar.html.erb +18 -26
  190. data/app/views/alchemy/admin/pictures/_picture.html.erb +11 -15
  191. data/app/views/alchemy/admin/pictures/_picture_to_assign.html.erb +3 -6
  192. data/app/views/alchemy/admin/pictures/_tag_list.html.erb +2 -3
  193. data/app/views/alchemy/admin/pictures/index.html.erb +0 -1
  194. data/app/views/alchemy/admin/pictures/update.turbo_stream.erb +1 -1
  195. data/app/views/alchemy/admin/resources/_resource_usage_info.html.erb +1 -1
  196. data/app/views/alchemy/admin/resources/_tag_list.html.erb +2 -3
  197. data/app/views/alchemy/admin/styleguide/index.html.erb +25 -20
  198. data/app/views/alchemy/admin/tags/edit.html.erb +1 -1
  199. data/app/views/alchemy/admin/tinymce/_setup.html.erb +2 -2
  200. data/app/views/alchemy/admin/uploader/_button.html.erb +1 -15
  201. data/app/views/alchemy/attachments/show.html.erb +1 -1
  202. data/app/views/alchemy/base/permission_denied.js.erb +1 -1
  203. data/app/views/alchemy/ingredients/shared/_anchor.html.erb +9 -7
  204. data/app/views/alchemy/ingredients/shared/_link_tools.html.erb +12 -5
  205. data/app/views/alchemy/ingredients/shared/_picture_tools.html.erb +10 -11
  206. data/app/views/alchemy/language_links/_spacer.html.erb +1 -1
  207. data/app/views/alchemy/messages_mailer/new.html.erb +1 -1
  208. data/app/views/alchemy/welcome.html.erb +1 -1
  209. data/config/locales/alchemy.en.yml +12 -3
  210. data/config/routes.rb +2 -2
  211. data/db/migrate/20230123112425_add_searchable_to_alchemy_pages.rb +1 -1
  212. data/db/migrate/20230505132743_add_indexes_to_alchemy_pictures.rb +1 -1
  213. data/db/migrate/20231113104432_create_page_mutexes.rb +1 -1
  214. data/db/migrate/20240314105244_create_alchemy_picture_descriptions.rb +1 -1
  215. data/db/migrate/20250626160259_add_unique_index_to_picture_descriptions.rb +1 -1
  216. data/db/migrate/20250905140323_add_created_at_index_to_pictures_and_attachments.rb +1 -1
  217. data/db/migrate/20251106150010_convert_select_value_for_multiple.rb +11 -0
  218. data/db/migrate/20260102121232_add_metadata_to_page_versions.rb +9 -0
  219. data/db/migrate/20260115164704_add_publication_timestamps_to_alchemy_elements.rb +30 -0
  220. data/db/migrate/20260115164705_add_index_to_element_publication_timestamps.rb +13 -0
  221. data/lib/alchemy/ability_helper.rb +1 -3
  222. data/lib/alchemy/auth_accessors.rb +51 -117
  223. data/lib/alchemy/configuration.rb +1 -0
  224. data/lib/alchemy/configurations/main.rb +63 -0
  225. data/lib/alchemy/controller_actions.rb +1 -1
  226. data/lib/alchemy/engine.rb +9 -12
  227. data/lib/alchemy/error_tracking/error_logger.rb +1 -1
  228. data/lib/alchemy/errors.rb +1 -1
  229. data/lib/alchemy/logger.rb +34 -4
  230. data/lib/alchemy/name_conversions.rb +0 -6
  231. data/lib/alchemy/seeder.rb +2 -2
  232. data/lib/alchemy/tasks/usage.rb +4 -4
  233. data/lib/alchemy/test_support/factories/page_version_factory.rb +3 -0
  234. data/lib/alchemy/test_support/having_picture_thumbnails_examples.rb +30 -0
  235. data/lib/alchemy/test_support/shared_ingredient_editor_examples.rb +26 -6
  236. data/lib/alchemy/test_support/shared_publishable_examples.rb +114 -0
  237. data/lib/alchemy/upgrader/eight_one.rb +56 -0
  238. data/lib/alchemy/upgrader.rb +9 -1
  239. data/lib/alchemy/version.rb +1 -1
  240. data/lib/alchemy.rb +1 -4
  241. data/lib/alchemy_cms.rb +0 -1
  242. data/lib/generators/alchemy/elements/templates/view.html.erb +3 -3
  243. data/lib/generators/alchemy/ingredient/ingredient_generator.rb +6 -8
  244. data/lib/generators/alchemy/ingredient/templates/editor_component.rb.tt +22 -0
  245. data/lib/generators/alchemy/page_layouts/templates/layout.html.erb +1 -1
  246. data/lib/generators/alchemy/site_layouts/templates/layout.html.erb +1 -1
  247. data/lib/tasks/alchemy/upgrade.rake +21 -7
  248. data/vendor/javascript/shoelace.min.js +713 -31
  249. data/vendor/javascript/tinymce.min.js +1 -1
  250. metadata +103 -83
  251. data/app/decorators/alchemy/element_editor.rb +0 -90
  252. data/app/helpers/alchemy/admin/pictures_helper.rb +0 -14
  253. data/app/javascript/alchemy_admin/file_editors.js +0 -28
  254. data/app/javascript/alchemy_admin/image_loader.js +0 -54
  255. data/app/javascript/alchemy_admin/page_sorter.js +0 -71
  256. data/app/javascript/alchemy_admin/sitemap.js +0 -154
  257. data/app/javascript/alchemy_admin/templates/page_folder.hbs +0 -3
  258. data/app/views/alchemy/admin/attachments/archive_overlay.js.erb +0 -4
  259. data/app/views/alchemy/admin/pages/_page.html.erb +0 -163
  260. data/app/views/alchemy/admin/pages/_sitemap.html.erb +0 -30
  261. data/app/views/alchemy/admin/pages/flush.js.erb +0 -2
  262. data/app/views/alchemy/admin/pictures/archive_overlay.js.erb +0 -5
  263. data/app/views/alchemy/admin/pictures/index.js.erb +0 -2
  264. data/app/views/alchemy/ingredients/_audio_editor.html.erb +0 -5
  265. data/app/views/alchemy/ingredients/_boolean_editor.html.erb +0 -11
  266. data/app/views/alchemy/ingredients/_datetime_editor.html.erb +0 -20
  267. data/app/views/alchemy/ingredients/_file_editor.html.erb +0 -52
  268. data/app/views/alchemy/ingredients/_headline_editor.html.erb +0 -44
  269. data/app/views/alchemy/ingredients/_html_editor.html.erb +0 -8
  270. data/app/views/alchemy/ingredients/_link_editor.html.erb +0 -30
  271. data/app/views/alchemy/ingredients/_node_editor.html.erb +0 -13
  272. data/app/views/alchemy/ingredients/_number_editor.html.erb +0 -24
  273. data/app/views/alchemy/ingredients/_page_editor.html.erb +0 -13
  274. data/app/views/alchemy/ingredients/_picture_editor.html.erb +0 -59
  275. data/app/views/alchemy/ingredients/_richtext_editor.html.erb +0 -15
  276. data/app/views/alchemy/ingredients/_select_editor.html.erb +0 -31
  277. data/app/views/alchemy/ingredients/_text_editor.html.erb +0 -29
  278. data/app/views/alchemy/ingredients/_video_editor.html.erb +0 -5
  279. data/lib/generators/alchemy/ingredient/templates/editor.html.erb +0 -14
  280. /data/{lib → app/models}/alchemy/permissions.rb +0 -0
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ # Preloads page trees with all associations and children
5
+ #
6
+ # This service efficiently loads page trees to avoid N+1 queries.
7
+ # It handles folded pages and preloads all necessary associations.
8
+ #
9
+ # @example Preload subtree from a specific page
10
+ # preloader = Alchemy::PageTreePreloader.new(page: page, user: current_user)
11
+ # page_with_descendants = preloader.call
12
+ #
13
+ class PageTreePreloader
14
+ # @param page [Page] Starting page for loading descendants
15
+ # @param user [User, nil] User for folding support
16
+ # @param admin_includes [Boolean] Whether to include admin-only associations like :locker
17
+ def initialize(page:, user: nil, admin_includes: false)
18
+ @page = page
19
+ @user = user
20
+ @admin_includes = admin_includes
21
+ end
22
+
23
+ # Preloads and returns the page tree
24
+ #
25
+ # @return [Array<Page>] Pages with preloaded children, or array with single page when using from:
26
+ def call
27
+ pages = page.self_and_descendants
28
+ folded_page_ids = load_folded_page_ids
29
+ if folded_page_ids.any?
30
+ pages = pages.where(
31
+ "parent_id IS NULL OR parent_id NOT IN (?)",
32
+ folded_page_ids
33
+ )
34
+ end
35
+ pages = pages.preload(*preload_associations)
36
+ pages = pages.map { PageTreePage.new(_1) }
37
+
38
+ preload_children_associations(pages, folded_page_ids:)
39
+
40
+ # Return the starting page, which now has preloaded descendants
41
+ # We need to return the actual instance from the pages array, not the @page instance
42
+ # because the children associations were set on the pages array instances
43
+ pages.find { |p| p.id == page.id }
44
+ end
45
+
46
+ private
47
+
48
+ attr_reader :page, :user, :admin_includes
49
+
50
+ # Load folded page IDs for the user
51
+ def load_folded_page_ids
52
+ if user && Alchemy.config.user_class < ActiveRecord::Base
53
+ FoldedPage.folded_for_user(user).pluck(:page_id).to_set
54
+ else
55
+ Set.new
56
+ end
57
+ end
58
+
59
+ # Preload children associations for a collection of pages
60
+ # This manually populates the children association to prevent N+1 queries
61
+ #
62
+ # @param pages [Array<Page>] The pages to preload children for
63
+ # @param folded_page_ids [Set] Optional set of page IDs that should have empty children
64
+ # @return [void]
65
+ def preload_children_associations(pages, folded_page_ids: Set.new)
66
+ # Group pages by parent_id for efficient lookup
67
+ pages_by_parent = pages.group_by(&:parent_id)
68
+
69
+ # Manually populate the children association for each page
70
+ pages.each do |page|
71
+ children_records = pages_by_parent[page.id] || []
72
+
73
+ # If page is folded, set children to empty array
74
+ collection = if folded_page_ids.include?(page.id)
75
+ page.folded = true
76
+ []
77
+ else
78
+ page.folded = false
79
+ children_records.sort_by(&:lft)
80
+ end
81
+ page.association(:children).target = collection
82
+ page.association(:children).loaded!
83
+ end
84
+ end
85
+
86
+ # Associations to preload for sitemap rendering
87
+ def preload_associations
88
+ associations = [
89
+ {
90
+ language: {
91
+ site: :languages
92
+ }
93
+ },
94
+ :public_version
95
+ ]
96
+ associations.push(:locker) if admin_includes
97
+ associations
98
+ end
99
+ end
100
+
101
+ class PageTreePage < SimpleDelegator
102
+ attr_accessor :folded
103
+ alias_method :folded?, :folded
104
+ end
105
+ end
@@ -34,16 +34,15 @@
34
34
  line-height: var(--form-field-line-height);
35
35
  transition: var(--transition-duration);
36
36
 
37
- &:focus:not(.readonly) {
37
+ &:focus:not(.readonly),
38
+ &:focus:not([readonly]) {
38
39
  @include mixins.default-focus-style(
39
40
  $box-shadow: 0 0 0 1px var(--focus-color)
40
41
  );
41
42
  }
42
43
 
43
44
  &[disabled],
44
- &.disabled,
45
- &:not(.flatpickr-input)[readonly],
46
- &:not(.flatpickr-input).readonly {
45
+ &.disabled {
47
46
  color: var(--form-field-disabled-text-color);
48
47
  background-color: var(--form-field-disabled-bg-color);
49
48
  cursor: default;
@@ -53,11 +52,6 @@
53
52
  &.disabled {
54
53
  cursor: not-allowed;
55
54
  }
56
-
57
- &:not(.flatpickr-input)[readonly],
58
- &:not(.flatpickr-input).readonly {
59
- pointer-events: none;
60
- }
61
55
  }
62
56
 
63
57
  %gradiated-toolbar {
@@ -16,6 +16,7 @@
16
16
 
17
17
  @mixin button-defaults(
18
18
  $background-color: var(--button-bg-color),
19
+ $disabled-background-color: var(--button-disabled-bg-color),
19
20
  $hover-color: var(--button-hover-bg-color),
20
21
  $hover-border-color: var(--button-hover-border-color),
21
22
  $border-radius: var(--button-border-radius),
@@ -68,8 +69,9 @@
68
69
 
69
70
  &.disabled,
70
71
  &[disabled] {
71
- background-color: var(--button-disabled-bg-color);
72
+ background-color: $disabled-background-color;
72
73
  border-color: var(--button-disabled-border-color);
74
+ color: var(--text-color-muted);
73
75
  cursor: not-allowed;
74
76
  box-shadow: none;
75
77
  outline: none;
@@ -83,8 +83,8 @@
83
83
  --form-field-box-shadow: inset 0 0 1px var(--a-dark-grey);
84
84
  --form-field-label-color: var(--text-color-muted);
85
85
  --form-field-text-color: var(--text-color);
86
- --form-field-disabled-text-color: hsl(0deg, 0%, 53%);
87
- --form-field-disabled-bg-color: hsla(0deg, 0%, 20%, 0.5);
86
+ --form-field-disabled-text-color: var(--form-field-text-color);
87
+ --form-field-disabled-bg-color: transparent;
88
88
  --form-field-error-bg-color: hsl(0deg, 47%, 10%);
89
89
  --form-field-error-box-shadow: inset 0 0 1px hsla(0deg, 25%, 30%, 0.5);
90
90
 
@@ -153,6 +153,9 @@
153
153
  --picture-tool-background-color: var(--a-darker-grey);
154
154
  --picture-tool-hover-background-color: var(--a-darkest-grey);
155
155
 
156
+ --scrollbar-thumb-color: var(--a-dark-grey);
157
+ --scrollbar-track-color: var(--a-darkest-grey);
158
+
156
159
  --search-field-background-color: var(--a-dark-grey);
157
160
 
158
161
  /* Selects */
@@ -161,12 +164,12 @@
161
164
  --select-text-color: var(--text-color);
162
165
  --select-hover-bg-color: var(--color-blue_dark);
163
166
  --select-hover-border-color: var(--form-field-border-color);
164
- --select-hover-text-color: var(--color-white);
167
+ --select-hover-text-color: var(--text-color);
165
168
  --select-background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" fill="hsl(200deg, 5%, 70%)" viewBox="0 0 24 24"><path d="M11.9997 13.1714L16.9495 8.22168L18.3637 9.63589L11.9997 15.9999L5.63574 9.63589L7.04996 8.22168L11.9997 13.1714Z"></path></svg>');
166
169
  --select-close-icon: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="hsl(200deg, 5%, 70%)"><path d="M11.9997 10.5865L16.9495 5.63672L18.3637 7.05093L13.4139 12.0007L18.3637 16.9504L16.9495 18.3646L11.9997 13.4149L7.04996 18.3646L5.63574 16.9504L10.5855 12.0007L5.63574 7.05093L7.04996 5.63672L11.9997 10.5865Z"></path></svg>');
167
- --select-disabled-background-color: var(--a-darker-grey);
170
+ --select-disabled-background-color: transparent;
168
171
  --select-disabled-border-color: var(--a-grey);
169
- --select-disabled-text-color: var(--a-lighter-grey);
172
+ --select-disabled-text-color: var(--select-text-color);
170
173
  --select-dropdown-background-color: var(--a-darker-grey);
171
174
  --select-dropdown-box-shadow: 0 var(--spacing-2) var(--spacing-2)
172
175
  hsla(0deg, 0%, 0%, 0.75);
@@ -213,6 +216,7 @@
213
216
  /* Toolbar */
214
217
  --toolbar-bg-color: var(--a-darker-grey);
215
218
  --toolbar-border-bottom: 1px solid var(--a-darkest-grey);
219
+ --toolbar-spacer-color: var(--a-grey);
216
220
 
217
221
  /* Tooltips */
218
222
  --tooltip-background-color: var(--color-blue_dark);
@@ -327,7 +331,7 @@
327
331
  --dialog-header-color: var(--color-blue_dark);
328
332
  --dialog-header-text-color: var(--color-white);
329
333
  --dialog-box-shadow: 0 var(--spacing-2) var(--spacing-4)
330
- hsla(0deg, 0%, 14%, 0.5);
334
+ hsla(0deg, 0%, 14%, 0.15);
331
335
  --dialog-overlay-color: hsla(0deg, 0%, 39%, 0.4);
332
336
  --dialog-background-color: var(--color-grey_very_light);
333
337
 
@@ -354,8 +358,8 @@
354
358
  --form-field-box-shadow: inset 0 0 1px var(--color-grey_light);
355
359
  --form-field-label-color: var(--text-color);
356
360
  --form-field-text-color: var(--text-color);
357
- --form-field-disabled-text-color: hsl(0deg, 0%, 53%);
358
- --form-field-disabled-bg-color: hsla(0deg, 0%, 100%, 0.5);
361
+ --form-field-disabled-text-color: var(--form-field-text-color);
362
+ --form-field-disabled-bg-color: transparent;
359
363
  --form-field-error-bg-color: hsl(0deg, 47%, 96%);
360
364
  --form-field-error-box-shadow: inset 0 0 1px hsla(0deg, 25%, 69%, 0.5);
361
365
 
@@ -422,6 +426,9 @@
422
426
  --picture-tool-background-color: var(--color-white);
423
427
  --picture-tool-hover-background-color: var(--color-grey_very_light);
424
428
 
429
+ --scrollbar-thumb-color: var(--color-white);
430
+ --scrollbar-track-color: var(--color-grey_light);
431
+
425
432
  --search-field-background-color: var(--color-grey_very_light);
426
433
 
427
434
  /* Selects */
@@ -433,8 +440,9 @@
433
440
  --select-hover-text-color: var(--color-white);
434
441
  --select-background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="hsla(224deg, 23%, 26%, 0.75)"><path d="M11.9999 13.1714L16.9497 8.22168L18.3639 9.63589L11.9999 15.9999L5.63599 9.63589L7.0502 8.22168L11.9999 13.1714Z"></path></svg>');
435
442
  --select-close-icon: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="hsla(224deg, 23%, 26%, 0.75)"><path d="M11.9997 10.5865L16.9495 5.63672L18.3637 7.05093L13.4139 12.0007L18.3637 16.9504L16.9495 18.3646L11.9997 13.4149L7.04996 18.3646L5.63574 16.9504L10.5855 12.0007L5.63574 7.05093L7.04996 5.63672L11.9997 10.5865Z"></path></svg>');
436
- --select-disabled-background-color: var(--color-grey_very_light);
443
+ --select-disabled-background-color: transparent;
437
444
  --select-disabled-border-color: var(--border-inset-color);
445
+ --select-disabled-text-color: var(--select-text-color);
438
446
  --select-dropdown-background-color: var(--color-white);
439
447
  --select-dropdown-box-shadow: 0 var(--spacing-2) var(--spacing-2)
440
448
  hsla(0deg, 0%, 0%, 0.15);
@@ -465,7 +473,7 @@
465
473
 
466
474
  /* Text */
467
475
  --text-color: hsla(224deg, 23%, 26%, 0.8);
468
- --text-color-muted: hsla(224deg, 23%, 26%, 0.5);
476
+ --text-color-muted: hsla(224deg, 23%, 26%, 0.6);
469
477
  --text-link-color: var(--color-orange_very_dark);
470
478
  --text-shadow-light: 0 0 var(--spacing-1) hsla(0deg, 0%, 100%, 0.5);
471
479
 
@@ -483,6 +491,7 @@
483
491
  /* Toolbar */
484
492
  --toolbar-bg-color: var(--color-grey_light);
485
493
  --toolbar-border-bottom: var(--border-default);
494
+ --toolbar-spacer-color: var(--border-color);
486
495
 
487
496
  /* Tooltips */
488
497
  --tooltip-background-color: var(--color-blue_dark);
@@ -184,6 +184,7 @@ div.assign_image_list_image {
184
184
  z-index: 10;
185
185
  border-radius: var(--border-radius_medium);
186
186
  box-shadow: 0 0 1px var(--color-grey_dark);
187
+ border: 0;
187
188
 
188
189
  &:hover {
189
190
  text-decoration: none;
@@ -10,6 +10,8 @@ html {
10
10
  box-sizing: border-box;
11
11
  height: 100%;
12
12
  font-size: var(--font-size_medium);
13
+ scrollbar-width: thin;
14
+ scrollbar-color: var(--scrollbar-thumb-color) var(--scrollbar-track-color);
13
15
 
14
16
  &.turbo-progress-bar::before,
15
17
  .turbo-progress-bar {
@@ -21,6 +23,7 @@ html {
21
23
  *,
22
24
  *::before,
23
25
  *::after {
26
+ scrollbar-width: inherit;
24
27
  box-sizing: inherit;
25
28
  }
26
29
 
@@ -91,8 +94,8 @@ a {
91
94
  }
92
95
  }
93
96
 
94
- a:focus,
95
- [tabindex]:focus {
97
+ a:focus:not(.disabled),
98
+ [tabindex]:focus:not(.disabled) {
96
99
  @include mixins.default-focus-style;
97
100
  }
98
101
 
@@ -84,21 +84,21 @@ input.button {
84
84
 
85
85
  &.disabled,
86
86
  &[disabled] {
87
- pointer-events: none;
88
-
89
87
  svg {
90
88
  opacity: 0.3;
91
89
  }
92
90
 
93
91
  &:hover {
94
92
  text-decoration: none;
95
- cursor: default;
93
+ cursor: not-allowed;
94
+ border-color: initial;
96
95
  }
97
96
  }
98
97
 
99
98
  &.linked {
100
99
  @include mixins.button-defaults(
101
100
  $background-color: var(--icon-button-linked-color),
101
+ $disabled-background-color: var(--icon-button-linked-color),
102
102
  $hover-color: var(--icon-button-linked-hover-color),
103
103
  $border: 1px solid var(--icon-button-linked-border-color),
104
104
  $hover-border-color: var(--icon-button-linked-hover-border-color),
@@ -1,11 +1,29 @@
1
1
  .element-select-item {
2
+ display: flex;
3
+ flex-direction: column;
4
+ }
5
+
6
+ .element-select-name {
2
7
  display: flex;
3
8
  align-items: center;
4
9
 
5
10
  > svg {
11
+ flex-shrink: 0;
6
12
  width: auto;
7
13
  height: var(--spacing-4);
8
14
  fill: currentColor;
9
15
  padding-right: var(--spacing-2);
10
16
  }
11
17
  }
18
+
19
+ .element-select-description {
20
+ margin-top: var(--spacing-0);
21
+ padding-left: var(--spacing-6);
22
+ font-size: var(--font-size_small);
23
+ color: var(--text-color-muted);
24
+ max-width: 270px;
25
+
26
+ .select2-highlighted & {
27
+ color: var(--select-hover-text-color);
28
+ }
29
+ }
@@ -8,7 +8,6 @@
8
8
  position: fixed;
9
9
  right: 0;
10
10
  top: var(--top-menu-height);
11
- z-index: 20;
12
11
  display: block;
13
12
  width: var(--width);
14
13
  min-width: var(--elements-window-min-width);
@@ -74,16 +73,37 @@ alchemy-elements-window-handle {
74
73
  padding: var(--spacing-2);
75
74
 
76
75
  .right {
77
- display: inline-block;
76
+ display: inline-flex;
77
+ gap: var(--spacing-2);
78
78
  margin-left: auto;
79
79
  }
80
- }
81
80
 
82
- #element_area {
83
- .sortable-elements {
84
- min-height: 100%;
81
+ alchemy-list-filter {
82
+ width: 58px;
83
+ transition: var(--transition-duration);
84
+
85
+ &:focus-within,
86
+ &:has(input:not(:placeholder-shown)) {
87
+ width: 180px;
88
+
89
+ input {
90
+ background-color: var(--form-field-background-color);
91
+ }
92
+ }
93
+
94
+ input {
95
+ height: 100%;
96
+ background-color: var(--toolbar-bg-color);
97
+ }
85
98
  }
99
+ }
100
+
101
+ alchemy-sortable-elements {
102
+ display: block;
103
+ min-height: 100%;
104
+ }
86
105
 
106
+ #element_area {
87
107
  textarea {
88
108
  width: 100%;
89
109
  }
@@ -149,7 +169,10 @@ alchemy-elements-window {
149
169
  flex-shrink: 0;
150
170
  width: var(--spacing-4);
151
171
  height: var(--spacing-4);
152
- cursor: move;
172
+
173
+ &.draggable {
174
+ cursor: move;
175
+ }
153
176
  }
154
177
 
155
178
  button.element-toggle {
@@ -188,14 +211,10 @@ button.element-toggle {
188
211
  --element-editor-background-color,
189
212
  --dialog-background-color
190
213
  );
191
- margin: var(--spacing-2) 0;
214
+ margin-bottom: var(--spacing-2);
192
215
  transition: box-shadow var(--transition-duration);
193
216
  scroll-margin: var(--spacing-2);
194
217
 
195
- &:first-child {
196
- margin-top: 0;
197
- }
198
-
199
218
  &.element-hidden {
200
219
  border-style: dashed;
201
220
 
@@ -485,35 +504,35 @@ button.element-toggle {
485
504
  border-bottom-right-radius: 0;
486
505
  }
487
506
 
488
- .icon {
507
+ .element-icon {
489
508
  fill: currentColor;
490
509
  transition: fill var(--transition-duration);
491
510
  }
492
511
 
493
512
  > .element-handle {
494
- alchemy-icon,
495
- .icon {
513
+ .handle-icon,
514
+ .element-icon {
496
515
  position: absolute;
497
516
  width: var(--spacing-4);
498
517
  height: var(--spacing-4);
499
518
  transition: opacity var(--transition-duration);
500
519
  }
501
520
 
502
- alchemy-icon {
521
+ .handle-icon {
503
522
  opacity: 0;
504
523
  }
505
524
 
506
- .icon {
525
+ .element-icon {
507
526
  opacity: 1;
508
527
  }
509
528
  }
510
529
 
511
530
  &:hover {
512
- > .element-handle alchemy-icon {
531
+ > .element-handle .handle-icon {
513
532
  opacity: 1;
514
533
  }
515
534
 
516
- > .element-handle .icon.element {
535
+ > .element-handle .element-icon {
517
536
  opacity: 0;
518
537
  }
519
538
  }
@@ -604,6 +623,68 @@ alchemy-publish-element-button {
604
623
  min-height: 100px;
605
624
  }
606
625
 
626
+ alchemy-color-select {
627
+ display: flex;
628
+ justify-content: space-between;
629
+ align-items: center;
630
+
631
+ > input[type="text"],
632
+ > .select2-container {
633
+ width: 100%;
634
+ }
635
+
636
+ > input[type="color"] {
637
+ border-width: var(--form-field-border-width);
638
+ border-style: var(--form-field-border-style);
639
+ border-color: var(--form-field-border-color);
640
+ border-radius: var(--border-radius_medium);
641
+ height: var(--form-field-height);
642
+ background: var(--form-field-background-color);
643
+ padding: var(--spacing-1);
644
+ width: 100%;
645
+
646
+ &[disabled] {
647
+ display: none;
648
+ }
649
+ }
650
+
651
+ &:has(input[type="color"]:not([disabled])) {
652
+ input[type="text"],
653
+ .select2-container,
654
+ .select2-choice {
655
+ border-top-right-radius: 0;
656
+ border-bottom-right-radius: 0;
657
+ }
658
+ }
659
+
660
+ input[type="text"] + input[type="color"],
661
+ select + input[type="color"] {
662
+ width: calc(2 * var(--form-field-addon-width));
663
+ flex-shrink: 0;
664
+ border-top-left-radius: 0;
665
+ border-bottom-left-radius: 0;
666
+ border-left-width: 0;
667
+ }
668
+ }
669
+
670
+ // place the select color options outside, because Select2
671
+ // is opening the selection container outside the `alchemy-color-select`
672
+ // component
673
+ .select-color-option {
674
+ .color-indicator {
675
+ background: var(--color) 100% 100% no-repeat;
676
+ border: var(--border-default);
677
+ border-radius: var(--border-radius_medium);
678
+ display: inline-block;
679
+ height: var(--icon-size-md);
680
+ width: var(--icon-size-md);
681
+ }
682
+
683
+ display: flex;
684
+ align-items: center;
685
+ gap: var(--spacing-2);
686
+ }
687
+
607
688
  .ingredient_link_buttons {
608
689
  display: flex;
609
690
  position: absolute;
@@ -623,8 +704,8 @@ alchemy-publish-element-button {
623
704
  width: var(--form-field-addon-width);
624
705
  height: var(--form-field-height);
625
706
 
626
- &:hover {
627
- border-color: #c0c0c0;
707
+ &:hover:not([disabled]):not(.disabled) {
708
+ border-color: var(--button-hover-border-color);
628
709
  }
629
710
 
630
711
  &:focus {
@@ -688,6 +769,14 @@ alchemy-publish-element-button {
688
769
  }
689
770
  }
690
771
 
772
+ .ingredient-editor.boolean {
773
+ label {
774
+ display: inline-flex;
775
+ align-items: center;
776
+ gap: var(--spacing-0);
777
+ }
778
+ }
779
+
691
780
  .ingredient-editor.picture {
692
781
  position: relative;
693
782
  width: 50%;
@@ -765,6 +854,12 @@ alchemy-publish-element-button {
765
854
  }
766
855
  }
767
856
 
857
+ .ingredient-editor.richtext {
858
+ alchemy-tinymce {
859
+ margin: var(--spacing-2) 0;
860
+ }
861
+ }
862
+
768
863
  .ingredient-editor.headline {
769
864
  &.with-level-select {
770
865
  input[type="text"] {
@@ -1036,14 +1131,19 @@ select.long {
1036
1131
  text-indent: 1px;
1037
1132
  color: var(--form-field-label-color);
1038
1133
 
1134
+ > sl-tooltip {
1135
+ display: inline-flex;
1136
+ margin-left: var(--spacing-1);
1137
+ }
1138
+
1039
1139
  span.warning.icon {
1040
1140
  position: relative;
1041
1141
  top: 2px;
1042
1142
  }
1043
1143
 
1044
1144
  &.inline {
1045
- display: inline-block;
1046
- vertical-align: middle;
1145
+ display: inline-flex;
1146
+ align-items: center;
1047
1147
  min-width: 90px;
1048
1148
  margin-right: var(--spacing-1);
1049
1149
  }
@@ -8,7 +8,7 @@
8
8
  border-radius: var(--border-radius_medium);
9
9
 
10
10
  h2 {
11
- font-size: 1.2em;
11
+ font-size: var(--font-size_medium);
12
12
  }
13
13
 
14
14
  ul {
@@ -1,16 +1,18 @@
1
1
  div#flash_notices {
2
2
  position: fixed;
3
- right: 0;
3
+ right: 50%;
4
+ transform: translateX(50%);
4
5
  z-index: 400000;
5
6
  width: 400px;
6
- top: 0;
7
+ top: var(--spacing-1);
7
8
 
8
9
  alchemy-message {
9
10
  font-weight: bold;
10
- margin: var(--spacing-1) var(--spacing-1) var(--spacing-2);
11
+ margin: var(--spacing-4) 0;
11
12
  opacity: 0.95;
12
13
  transition-property: opacity, transform;
13
- transition-duration: 0.4s;
14
+ transition-duration: var(--transition-duration);
15
+ box-shadow: var(--dialog-box-shadow);
14
16
 
15
17
  &.dismissed {
16
18
  display: block;
@@ -1,8 +1,12 @@
1
- img {
2
- opacity: 1;
3
- transition: opacity var(--transition-duration);
1
+ alchemy-picture-thumbnail {
2
+ &[loading] {
3
+ img {
4
+ opacity: 0;
5
+ }
6
+ }
4
7
 
5
- &.loading {
6
- opacity: 0;
8
+ img {
9
+ opacity: 1;
10
+ transition: opacity var(--transition-duration);
7
11
  }
8
12
  }
@@ -1,8 +1,8 @@
1
1
  alchemy-list-filter {
2
- display: flex;
2
+ display: block;
3
3
  position: relative;
4
4
 
5
- > alchemy-icon {
5
+ > label alchemy-icon {
6
6
  position: absolute;
7
7
  left: var(--spacing-2);
8
8
  top: 50%;
@@ -12,7 +12,7 @@ alchemy-list-filter {
12
12
  .js_filter_field {
13
13
  width: 100%;
14
14
  padding-left: var(--spacing-7);
15
- padding-right: var(--spacing-6);
15
+ padding-right: var(--spacing-7);
16
16
  margin: 0;
17
17
 
18
18
  form .input & {
@@ -25,7 +25,7 @@ alchemy-list-filter {
25
25
  display: flex;
26
26
  visibility: hidden;
27
27
  position: absolute;
28
- right: var(--spacing-1);
28
+ right: var(--spacing-2);
29
29
  top: 50%;
30
30
  transform: translateY(-50%);
31
31
  width: 16px;
@@ -64,8 +64,7 @@ alchemy-message {
64
64
  }
65
65
 
66
66
  h1 {
67
- font-size: 1.3rem;
68
- line-height: 1.1;
67
+ font-size: var(--font-size_medium);
69
68
  }
70
69
 
71
70
  h1,