alchemy_cms 8.0.12 → 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 (286) 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 -15
  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/languages_controller.rb +0 -3
  46. data/app/controllers/alchemy/admin/layoutpages_controller.rb +2 -1
  47. data/app/controllers/alchemy/admin/legacy_page_urls_controller.rb +1 -1
  48. data/app/controllers/alchemy/admin/nodes_controller.rb +24 -1
  49. data/app/controllers/alchemy/admin/pages_controller.rb +36 -42
  50. data/app/controllers/alchemy/admin/pictures_controller.rb +4 -28
  51. data/app/controllers/alchemy/admin/resources_controller.rb +1 -1
  52. data/app/controllers/alchemy/api/ingredients_controller.rb +1 -1
  53. data/app/controllers/alchemy/api/pages_controller.rb +5 -3
  54. data/app/controllers/alchemy/base_controller.rb +6 -6
  55. data/app/controllers/alchemy/pages_controller.rb +12 -6
  56. data/app/controllers/concerns/alchemy/admin/archive_overlay.rb +0 -1
  57. data/app/controllers/concerns/alchemy/admin/clipboard.rb +57 -0
  58. data/app/controllers/concerns/alchemy/admin/uploader_responses.rb +2 -2
  59. data/app/controllers/concerns/alchemy/site_redirects.rb +1 -1
  60. data/app/decorators/alchemy/ingredient_editor.rb +37 -4
  61. data/app/helpers/alchemy/admin/base_helper.rb +10 -6
  62. data/app/helpers/alchemy/admin/ingredients_helper.rb +6 -3
  63. data/app/helpers/alchemy/base_helper.rb +1 -1
  64. data/app/helpers/alchemy/pages_helper.rb +1 -1
  65. data/app/javascript/alchemy_admin/components/action.js +5 -1
  66. data/app/javascript/alchemy_admin/components/color_select.js +73 -0
  67. data/app/javascript/alchemy_admin/components/element_editor/delete_element_button.js +11 -3
  68. data/app/javascript/alchemy_admin/components/element_editor/publish_element_button.js +7 -2
  69. data/app/javascript/alchemy_admin/components/element_editor.js +11 -12
  70. data/app/javascript/alchemy_admin/components/element_select.js +39 -17
  71. data/app/javascript/alchemy_admin/components/elements_window.js +0 -2
  72. data/app/javascript/alchemy_admin/components/file_editor.js +26 -0
  73. data/app/javascript/alchemy_admin/components/index.js +9 -0
  74. data/app/javascript/alchemy_admin/components/list_filter.js +57 -8
  75. data/app/javascript/alchemy_admin/components/message.js +9 -3
  76. data/app/javascript/alchemy_admin/components/page_node.js +119 -0
  77. data/app/javascript/alchemy_admin/{page_publication_fields.js → components/page_publication_fields.js} +9 -8
  78. data/app/javascript/alchemy_admin/{picture_editors.js → components/picture_editor.js} +30 -45
  79. data/app/javascript/alchemy_admin/components/picture_thumbnail.js +107 -0
  80. data/app/javascript/alchemy_admin/components/publish_page_button.js +41 -0
  81. data/app/javascript/alchemy_admin/components/select.js +3 -1
  82. data/app/javascript/alchemy_admin/components/sitemap.js +210 -0
  83. data/app/javascript/alchemy_admin/{sortable_elements.js → components/sortable_elements.js} +22 -25
  84. data/app/javascript/alchemy_admin/components/tinymce.js +10 -5
  85. data/app/javascript/alchemy_admin/components/uploader.js +30 -0
  86. data/app/javascript/alchemy_admin/image_overlay.js +0 -2
  87. data/app/javascript/alchemy_admin/initializer.js +0 -3
  88. data/app/javascript/alchemy_admin/link_dialog.js +1 -6
  89. data/app/javascript/alchemy_admin/templates/compiled.js +1 -1
  90. data/app/javascript/alchemy_admin/utils/ajax.js +15 -3
  91. data/app/javascript/alchemy_admin.js +0 -6
  92. data/app/models/alchemy/attachment.rb +4 -44
  93. data/app/models/alchemy/element/definitions.rb +1 -2
  94. data/app/models/alchemy/element/element_ingredients.rb +6 -2
  95. data/app/models/alchemy/element.rb +54 -13
  96. data/app/models/alchemy/element_definition.rb +4 -1
  97. data/app/models/alchemy/elements_repository.rb +6 -0
  98. data/app/models/alchemy/folded_page.rb +2 -2
  99. data/app/models/alchemy/ingredient.rb +38 -1
  100. data/app/models/alchemy/ingredient_definition.rb +4 -1
  101. data/app/models/alchemy/ingredient_validator.rb +6 -2
  102. data/app/models/alchemy/ingredients/color.rb +10 -0
  103. data/app/models/alchemy/ingredients/headline.rb +2 -17
  104. data/app/models/alchemy/ingredients/picture.rb +4 -4
  105. data/app/models/alchemy/ingredients/select.rb +19 -0
  106. data/app/models/alchemy/language/code.rb +0 -1
  107. data/app/models/alchemy/node.rb +28 -1
  108. data/app/models/alchemy/page/page_naming.rb +0 -7
  109. data/app/models/alchemy/page/page_natures.rb +7 -3
  110. data/app/models/alchemy/page/page_scopes.rb +13 -1
  111. data/app/models/alchemy/page/publisher.rb +14 -2
  112. data/app/models/alchemy/page.rb +102 -23
  113. data/app/models/alchemy/page_definition.rb +4 -1
  114. data/app/models/alchemy/page_version.rb +22 -6
  115. data/app/models/alchemy/picture.rb +10 -11
  116. data/app/models/alchemy/picture_variant.rb +1 -3
  117. data/app/models/alchemy/resource.rb +1 -1
  118. data/app/models/alchemy/storage_adapter/active_storage.rb +14 -2
  119. data/app/models/alchemy/storage_adapter/dragonfly.rb +12 -0
  120. data/app/models/alchemy/storage_adapter.rb +2 -0
  121. data/app/models/concerns/alchemy/picture_thumbnails.rb +4 -4
  122. data/app/models/concerns/alchemy/publishable.rb +54 -0
  123. data/app/models/concerns/alchemy/relatable_resource.rb +4 -14
  124. data/app/serializers/alchemy/page_tree_serializer.rb +11 -31
  125. data/app/services/alchemy/copy_page.rb +17 -0
  126. data/app/services/alchemy/duplicate_element.rb +1 -1
  127. data/app/services/alchemy/page_tree_preloader.rb +105 -0
  128. data/app/stylesheets/alchemy/_extends.scss +3 -9
  129. data/app/stylesheets/alchemy/_mixins.scss +3 -1
  130. data/app/stylesheets/alchemy/_themes.scss +19 -10
  131. data/app/stylesheets/alchemy/admin/archive.scss +1 -0
  132. data/app/stylesheets/alchemy/admin/base.scss +5 -2
  133. data/app/stylesheets/alchemy/admin/buttons.scss +3 -3
  134. data/app/stylesheets/alchemy/admin/element-select.scss +18 -0
  135. data/app/stylesheets/alchemy/admin/elements.scss +123 -23
  136. data/app/stylesheets/alchemy/admin/errors.scss +1 -1
  137. data/app/stylesheets/alchemy/admin/flash.scss +6 -4
  138. data/app/stylesheets/alchemy/admin/images.scss +9 -5
  139. data/app/stylesheets/alchemy/admin/list_filter.scss +4 -4
  140. data/app/stylesheets/alchemy/admin/navigation.scss +1 -1
  141. data/app/stylesheets/alchemy/admin/notices.scss +1 -2
  142. data/app/stylesheets/alchemy/admin/selects.scss +36 -21
  143. data/app/stylesheets/alchemy/admin/shoelace.scss +14 -1
  144. data/app/stylesheets/alchemy/admin/sitemap.scss +11 -3
  145. data/app/stylesheets/alchemy/admin/tags.scss +3 -1
  146. data/app/stylesheets/alchemy/admin/toolbar.scss +1 -1
  147. data/app/views/alchemy/_edit_mode.html.erb +1 -1
  148. data/app/views/alchemy/_menubar.html.erb +1 -1
  149. data/app/views/alchemy/admin/attachments/_archive_overlay.html.erb +35 -31
  150. data/app/views/alchemy/admin/attachments/_files_list.html.erb +1 -1
  151. data/app/views/alchemy/admin/attachments/_library_sidebar.html.erb +6 -0
  152. data/app/views/alchemy/admin/attachments/_overlay_file_list.html.erb +1 -1
  153. data/app/views/alchemy/admin/attachments/_replace_button.html.erb +1 -8
  154. data/app/views/alchemy/admin/attachments/_sorting_select.html.erb +13 -0
  155. data/app/views/alchemy/admin/attachments/_tag_list.html.erb +2 -3
  156. data/app/views/alchemy/admin/attachments/index.html.erb +5 -11
  157. data/app/views/alchemy/admin/attachments/show.html.erb +1 -1
  158. data/app/views/alchemy/admin/clipboard/_button.html.erb +1 -0
  159. data/app/views/alchemy/admin/clipboard/index.html.erb +4 -5
  160. data/app/views/alchemy/admin/clipboard/insert.turbo_stream.erb +1 -1
  161. data/app/views/alchemy/admin/crop.html.erb +5 -7
  162. data/app/views/alchemy/admin/dashboard/widgets/_locked_pages.html.erb +1 -1
  163. data/app/views/alchemy/admin/elements/_add_nested_element_form.html.erb +6 -6
  164. data/app/views/alchemy/admin/elements/_fixed_element.html.erb +1 -1
  165. data/app/views/alchemy/admin/elements/_footer.html.erb +7 -1
  166. data/app/views/alchemy/admin/elements/_header.html.erb +5 -5
  167. data/app/views/alchemy/admin/elements/_toolbar.html.erb +33 -8
  168. data/app/views/alchemy/admin/elements/create.turbo_stream.erb +10 -10
  169. data/app/views/alchemy/admin/elements/index.html.erb +29 -16
  170. data/app/views/alchemy/admin/elements/new.html.erb +2 -2
  171. data/app/views/alchemy/admin/ingredients/update.turbo_stream.erb +3 -5
  172. data/app/views/alchemy/admin/leave.html.erb +1 -1
  173. data/app/views/alchemy/admin/nodes/_node.html.erb +19 -0
  174. data/app/views/alchemy/admin/nodes/edit.html.erb +1 -1
  175. data/app/views/alchemy/admin/nodes/index.html.erb +3 -1
  176. data/app/views/alchemy/admin/nodes/new.html.erb +14 -1
  177. data/app/views/alchemy/admin/pages/_current_page.html.erb +3 -1
  178. data/app/views/alchemy/admin/pages/_form.html.erb +21 -9
  179. data/app/views/alchemy/admin/pages/_page_status.html.erb +1 -1
  180. data/app/views/alchemy/admin/pages/_publication_fields.html.erb +28 -26
  181. data/app/views/alchemy/admin/pages/_table.html.erb +0 -7
  182. data/app/views/alchemy/admin/pages/_toolbar.html.erb +3 -6
  183. data/app/views/alchemy/admin/pages/edit.html.erb +5 -11
  184. data/app/views/alchemy/admin/pages/flush.turbo_stream.erb +2 -0
  185. data/app/views/alchemy/admin/pages/fold.turbo_stream.erb +5 -0
  186. data/app/views/alchemy/admin/pages/index.html.erb +5 -3
  187. data/app/views/alchemy/admin/pages/new.html.erb +2 -12
  188. data/app/views/alchemy/admin/pages/publish.turbo_stream.erb +12 -0
  189. data/app/views/alchemy/admin/pages/tree.html.erb +13 -0
  190. data/app/views/alchemy/admin/pages/update.turbo_stream.erb +5 -16
  191. data/app/views/alchemy/admin/partials/_flash_notices.html.erb +1 -1
  192. data/app/views/alchemy/admin/partials/{_remote_search_form.html.erb → _overlay_search_form.html.erb} +1 -2
  193. data/app/views/alchemy/admin/partials/_paste_from_clipboard_form.html.erb +12 -0
  194. data/app/views/alchemy/admin/pictures/_archive_overlay.html.erb +24 -21
  195. data/app/views/alchemy/admin/pictures/_filter_and_size_bar.html.erb +18 -26
  196. data/app/views/alchemy/admin/pictures/_picture.html.erb +12 -16
  197. data/app/views/alchemy/admin/pictures/_picture_to_assign.html.erb +3 -6
  198. data/app/views/alchemy/admin/pictures/_tag_list.html.erb +2 -3
  199. data/app/views/alchemy/admin/pictures/index.html.erb +0 -1
  200. data/app/views/alchemy/admin/pictures/update.turbo_stream.erb +1 -1
  201. data/app/views/alchemy/admin/resources/_resource_usage_info.html.erb +1 -1
  202. data/app/views/alchemy/admin/resources/_tag_list.html.erb +2 -3
  203. data/app/views/alchemy/admin/styleguide/index.html.erb +25 -20
  204. data/app/views/alchemy/admin/tags/edit.html.erb +1 -1
  205. data/app/views/alchemy/admin/tinymce/_setup.html.erb +2 -2
  206. data/app/views/alchemy/admin/uploader/_button.html.erb +1 -15
  207. data/app/views/alchemy/attachments/show.html.erb +1 -1
  208. data/app/views/alchemy/base/permission_denied.js.erb +1 -1
  209. data/app/views/alchemy/ingredients/shared/_anchor.html.erb +9 -7
  210. data/app/views/alchemy/ingredients/shared/_link_tools.html.erb +12 -5
  211. data/app/views/alchemy/ingredients/shared/_picture_tools.html.erb +10 -11
  212. data/app/views/alchemy/language_links/_spacer.html.erb +1 -1
  213. data/app/views/alchemy/messages_mailer/new.html.erb +1 -1
  214. data/app/views/alchemy/welcome.html.erb +1 -1
  215. data/config/locales/alchemy.en.yml +12 -3
  216. data/config/routes.rb +2 -2
  217. data/db/migrate/20230123112425_add_searchable_to_alchemy_pages.rb +1 -1
  218. data/db/migrate/20230505132743_add_indexes_to_alchemy_pictures.rb +1 -1
  219. data/db/migrate/20231113104432_create_page_mutexes.rb +1 -1
  220. data/db/migrate/20240314105244_create_alchemy_picture_descriptions.rb +1 -1
  221. data/db/migrate/20250626160259_add_unique_index_to_picture_descriptions.rb +1 -1
  222. data/db/migrate/20250905140323_add_created_at_index_to_pictures_and_attachments.rb +1 -1
  223. data/db/migrate/20251106150010_convert_select_value_for_multiple.rb +11 -0
  224. data/db/migrate/20260102121232_add_metadata_to_page_versions.rb +9 -0
  225. data/db/migrate/20260115164704_add_publication_timestamps_to_alchemy_elements.rb +30 -0
  226. data/db/migrate/20260115164705_add_index_to_element_publication_timestamps.rb +13 -0
  227. data/lib/alchemy/ability_helper.rb +1 -3
  228. data/lib/alchemy/auth_accessors.rb +51 -117
  229. data/lib/alchemy/configuration.rb +1 -2
  230. data/lib/alchemy/configurations/main.rb +63 -0
  231. data/lib/alchemy/controller_actions.rb +2 -3
  232. data/lib/alchemy/engine.rb +9 -12
  233. data/lib/alchemy/error_tracking/error_logger.rb +1 -1
  234. data/lib/alchemy/errors.rb +1 -1
  235. data/lib/alchemy/logger.rb +34 -4
  236. data/lib/alchemy/name_conversions.rb +0 -6
  237. data/lib/alchemy/seeder.rb +2 -2
  238. data/lib/alchemy/tasks/usage.rb +4 -4
  239. data/lib/alchemy/test_support/factories/page_version_factory.rb +3 -0
  240. data/lib/alchemy/test_support/having_picture_thumbnails_examples.rb +30 -0
  241. data/lib/alchemy/test_support/shared_ingredient_editor_examples.rb +26 -6
  242. data/lib/alchemy/test_support/shared_publishable_examples.rb +114 -0
  243. data/lib/alchemy/upgrader/eight_one.rb +56 -0
  244. data/lib/alchemy/upgrader.rb +9 -1
  245. data/lib/alchemy/version.rb +1 -1
  246. data/lib/alchemy.rb +1 -4
  247. data/lib/alchemy_cms.rb +0 -1
  248. data/lib/generators/alchemy/elements/templates/view.html.erb +3 -3
  249. data/lib/generators/alchemy/ingredient/ingredient_generator.rb +6 -8
  250. data/lib/generators/alchemy/ingredient/templates/editor_component.rb.tt +22 -0
  251. data/lib/generators/alchemy/page_layouts/templates/layout.html.erb +1 -1
  252. data/lib/generators/alchemy/site_layouts/templates/layout.html.erb +1 -1
  253. data/lib/tasks/alchemy/upgrade.rake +21 -7
  254. data/vendor/javascript/shoelace.min.js +713 -31
  255. data/vendor/javascript/tinymce.min.js +1 -1
  256. metadata +104 -84
  257. data/app/decorators/alchemy/element_editor.rb +0 -90
  258. data/app/helpers/alchemy/admin/pictures_helper.rb +0 -14
  259. data/app/javascript/alchemy_admin/file_editors.js +0 -28
  260. data/app/javascript/alchemy_admin/image_loader.js +0 -54
  261. data/app/javascript/alchemy_admin/page_sorter.js +0 -71
  262. data/app/javascript/alchemy_admin/sitemap.js +0 -154
  263. data/app/javascript/alchemy_admin/templates/page_folder.hbs +0 -3
  264. data/app/views/alchemy/admin/attachments/archive_overlay.js.erb +0 -4
  265. data/app/views/alchemy/admin/pages/_page.html.erb +0 -163
  266. data/app/views/alchemy/admin/pages/_sitemap.html.erb +0 -30
  267. data/app/views/alchemy/admin/pages/flush.js.erb +0 -2
  268. data/app/views/alchemy/admin/pictures/archive_overlay.js.erb +0 -5
  269. data/app/views/alchemy/admin/pictures/index.js.erb +0 -2
  270. data/app/views/alchemy/ingredients/_audio_editor.html.erb +0 -5
  271. data/app/views/alchemy/ingredients/_boolean_editor.html.erb +0 -11
  272. data/app/views/alchemy/ingredients/_datetime_editor.html.erb +0 -20
  273. data/app/views/alchemy/ingredients/_file_editor.html.erb +0 -52
  274. data/app/views/alchemy/ingredients/_headline_editor.html.erb +0 -44
  275. data/app/views/alchemy/ingredients/_html_editor.html.erb +0 -8
  276. data/app/views/alchemy/ingredients/_link_editor.html.erb +0 -30
  277. data/app/views/alchemy/ingredients/_node_editor.html.erb +0 -13
  278. data/app/views/alchemy/ingredients/_number_editor.html.erb +0 -24
  279. data/app/views/alchemy/ingredients/_page_editor.html.erb +0 -13
  280. data/app/views/alchemy/ingredients/_picture_editor.html.erb +0 -59
  281. data/app/views/alchemy/ingredients/_richtext_editor.html.erb +0 -15
  282. data/app/views/alchemy/ingredients/_select_editor.html.erb +0 -31
  283. data/app/views/alchemy/ingredients/_text_editor.html.erb +0 -29
  284. data/app/views/alchemy/ingredients/_video_editor.html.erb +0 -5
  285. data/lib/generators/alchemy/ingredient/templates/editor.html.erb +0 -14
  286. /data/{lib → app/models}/alchemy/permissions.rb +0 -0
@@ -7,43 +7,23 @@ module Alchemy
7
7
  end
8
8
 
9
9
  def pages
10
- tree = []
11
- path = [{id: object.parent_id, children: tree}]
12
- page_list = object.self_and_descendants.includes(:public_version, {language: :site})
13
10
  base_level = object.level - 1
14
- # Load folded pages in advance
15
- folded_user_pages = FoldedPage.folded_for_user(opts[:user]).pluck(:page_id)
16
- folded_depth = Float::INFINITY
17
-
18
- page_list.each_with_index do |page, i|
19
- has_children = page_list[i + 1] && page_list[i + 1].parent_id == page.id
20
- folded = has_children && folded_user_pages.include?(page.id)
11
+ build_pages_tree([object], base_level)
12
+ end
21
13
 
22
- if page.depth > folded_depth
23
- next
24
- else
25
- folded_depth = Float::INFINITY
26
- end
14
+ private
27
15
 
28
- # If this page is folded, skip all pages that are on a higher level (further down the tree).
29
- if folded && !opts[:full]
30
- folded_depth = page.depth
31
- end
16
+ def build_pages_tree(pages, level)
17
+ pages.map do |page|
18
+ # Use association target directly to avoid triggering queries
19
+ children = page.association(:children).loaded? ? page.association(:children).target : []
20
+ has_children = children.any?
21
+ folded = !has_children && !page.leaf?
32
22
 
33
- if page.parent_id != path.last[:id]
34
- if path.map { |o| o[:id] }.include?(page.parent_id) # Lower level
35
- path.pop while path.last[:id] != page.parent_id
36
- else # One level up
37
- path << path.last[:children].last
38
- end
23
+ page_hash(page, level, folded).tap do |hash|
24
+ hash[:children] = build_pages_tree(children, level + 1)
39
25
  end
40
-
41
- level = path.count + base_level
42
-
43
- path.last[:children] << page_hash(page, level, folded)
44
26
  end
45
-
46
- tree
47
27
  end
48
28
 
49
29
  protected
@@ -28,8 +28,14 @@ module Alchemy
28
28
  depth
29
29
  urlname
30
30
  cached_tag_list
31
+ title
32
+ meta_description
33
+ meta_keywords
31
34
  ]
32
35
 
36
+ # Metadata to copy via nested attributes (title is derived from page.name)
37
+ METADATA_ATTRIBUTES_TO_COPY = (Alchemy::PageVersion::METADATA_ATTRIBUTES - %w[title]).freeze
38
+
33
39
  attr_reader :page
34
40
 
35
41
  # @param page [Alchemy::Page]
@@ -70,6 +76,7 @@ module Alchemy
70
76
  .merge(DEFAULT_ATTRIBUTES_FOR_COPY)
71
77
  .merge(differences)
72
78
  desired_attributes["name"] = best_name_for_copy(source_attributes, desired_attributes)
79
+ desired_attributes["draft_version_attributes"] = draft_version_attributes_for_copy
73
80
  desired_attributes.except(*SKIPPED_ATTRIBUTES_ON_COPY)
74
81
  end
75
82
 
@@ -94,5 +101,15 @@ module Alchemy
94
101
  desired_name
95
102
  end
96
103
  end
104
+
105
+ # Builds nested attributes for draft_version metadata (except title).
106
+ # Title is handled by PageVersion#set_title_from_page callback based on page.name.
107
+ def draft_version_attributes_for_copy
108
+ return {} unless page.draft_version
109
+
110
+ METADATA_ATTRIBUTES_TO_COPY.each_with_object({}) do |attr, hash|
111
+ hash[attr] = page.draft_version.send(attr)
112
+ end
113
+ end
97
114
  end
98
115
  end
@@ -36,7 +36,7 @@ module Alchemy
36
36
  new_element.save!
37
37
 
38
38
  nested_elements = repository.children_of(source_element)
39
- nested_elements = nested_elements.visible if publishable_only
39
+ nested_elements = nested_elements.publishable if publishable_only
40
40
  Element.acts_as_list_no_update do
41
41
  nested_elements.each.with_index(1) do |nested_element, position|
42
42
  self.class.new(nested_element, repository: repository, publishable_only: publishable_only).call(
@@ -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
+ }