alchemy_cms 8.0.11 → 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 (284) 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/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 +2 -5
  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 -4
  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/serializers/alchemy/page_tree_serializer.rb +11 -31
  124. data/app/services/alchemy/copy_page.rb +17 -0
  125. data/app/services/alchemy/duplicate_element.rb +1 -1
  126. data/app/services/alchemy/page_tree_preloader.rb +105 -0
  127. data/app/stylesheets/alchemy/_extends.scss +3 -9
  128. data/app/stylesheets/alchemy/_mixins.scss +3 -1
  129. data/app/stylesheets/alchemy/_themes.scss +19 -10
  130. data/app/stylesheets/alchemy/admin/archive.scss +1 -0
  131. data/app/stylesheets/alchemy/admin/base.scss +5 -2
  132. data/app/stylesheets/alchemy/admin/buttons.scss +3 -3
  133. data/app/stylesheets/alchemy/admin/element-select.scss +18 -0
  134. data/app/stylesheets/alchemy/admin/elements.scss +123 -23
  135. data/app/stylesheets/alchemy/admin/errors.scss +1 -1
  136. data/app/stylesheets/alchemy/admin/flash.scss +6 -4
  137. data/app/stylesheets/alchemy/admin/images.scss +9 -5
  138. data/app/stylesheets/alchemy/admin/list_filter.scss +4 -4
  139. data/app/stylesheets/alchemy/admin/navigation.scss +1 -1
  140. data/app/stylesheets/alchemy/admin/notices.scss +1 -2
  141. data/app/stylesheets/alchemy/admin/selects.scss +36 -21
  142. data/app/stylesheets/alchemy/admin/shoelace.scss +14 -1
  143. data/app/stylesheets/alchemy/admin/sitemap.scss +11 -3
  144. data/app/stylesheets/alchemy/admin/tags.scss +3 -1
  145. data/app/stylesheets/alchemy/admin/toolbar.scss +1 -1
  146. data/app/views/alchemy/_edit_mode.html.erb +1 -1
  147. data/app/views/alchemy/_menubar.html.erb +1 -1
  148. data/app/views/alchemy/admin/attachments/_archive_overlay.html.erb +35 -31
  149. data/app/views/alchemy/admin/attachments/_library_sidebar.html.erb +6 -0
  150. data/app/views/alchemy/admin/attachments/_overlay_file_list.html.erb +1 -1
  151. data/app/views/alchemy/admin/attachments/_replace_button.html.erb +1 -8
  152. data/app/views/alchemy/admin/attachments/_sorting_select.html.erb +13 -0
  153. data/app/views/alchemy/admin/attachments/_tag_list.html.erb +2 -3
  154. data/app/views/alchemy/admin/attachments/index.html.erb +5 -11
  155. data/app/views/alchemy/admin/attachments/show.html.erb +1 -1
  156. data/app/views/alchemy/admin/clipboard/_button.html.erb +1 -0
  157. data/app/views/alchemy/admin/clipboard/index.html.erb +4 -5
  158. data/app/views/alchemy/admin/clipboard/insert.turbo_stream.erb +1 -1
  159. data/app/views/alchemy/admin/crop.html.erb +5 -7
  160. data/app/views/alchemy/admin/dashboard/widgets/_locked_pages.html.erb +1 -1
  161. data/app/views/alchemy/admin/elements/_add_nested_element_form.html.erb +6 -6
  162. data/app/views/alchemy/admin/elements/_fixed_element.html.erb +1 -1
  163. data/app/views/alchemy/admin/elements/_footer.html.erb +7 -1
  164. data/app/views/alchemy/admin/elements/_header.html.erb +5 -5
  165. data/app/views/alchemy/admin/elements/_toolbar.html.erb +33 -8
  166. data/app/views/alchemy/admin/elements/create.turbo_stream.erb +10 -10
  167. data/app/views/alchemy/admin/elements/index.html.erb +29 -16
  168. data/app/views/alchemy/admin/elements/new.html.erb +2 -2
  169. data/app/views/alchemy/admin/ingredients/update.turbo_stream.erb +3 -5
  170. data/app/views/alchemy/admin/leave.html.erb +1 -1
  171. data/app/views/alchemy/admin/nodes/_node.html.erb +19 -0
  172. data/app/views/alchemy/admin/nodes/edit.html.erb +1 -1
  173. data/app/views/alchemy/admin/nodes/index.html.erb +3 -1
  174. data/app/views/alchemy/admin/nodes/new.html.erb +14 -1
  175. data/app/views/alchemy/admin/pages/_current_page.html.erb +3 -1
  176. data/app/views/alchemy/admin/pages/_form.html.erb +21 -9
  177. data/app/views/alchemy/admin/pages/_page_status.html.erb +1 -1
  178. data/app/views/alchemy/admin/pages/_publication_fields.html.erb +28 -26
  179. data/app/views/alchemy/admin/pages/_table.html.erb +0 -7
  180. data/app/views/alchemy/admin/pages/_toolbar.html.erb +3 -6
  181. data/app/views/alchemy/admin/pages/edit.html.erb +5 -11
  182. data/app/views/alchemy/admin/pages/flush.turbo_stream.erb +2 -0
  183. data/app/views/alchemy/admin/pages/fold.turbo_stream.erb +5 -0
  184. data/app/views/alchemy/admin/pages/index.html.erb +5 -3
  185. data/app/views/alchemy/admin/pages/new.html.erb +2 -12
  186. data/app/views/alchemy/admin/pages/publish.turbo_stream.erb +12 -0
  187. data/app/views/alchemy/admin/pages/tree.html.erb +13 -0
  188. data/app/views/alchemy/admin/pages/update.turbo_stream.erb +5 -16
  189. data/app/views/alchemy/admin/partials/_flash_notices.html.erb +1 -1
  190. data/app/views/alchemy/admin/partials/{_remote_search_form.html.erb → _overlay_search_form.html.erb} +1 -2
  191. data/app/views/alchemy/admin/partials/_paste_from_clipboard_form.html.erb +12 -0
  192. data/app/views/alchemy/admin/pictures/_archive_overlay.html.erb +24 -21
  193. data/app/views/alchemy/admin/pictures/_filter_and_size_bar.html.erb +18 -26
  194. data/app/views/alchemy/admin/pictures/_picture.html.erb +11 -15
  195. data/app/views/alchemy/admin/pictures/_picture_to_assign.html.erb +3 -6
  196. data/app/views/alchemy/admin/pictures/_tag_list.html.erb +2 -3
  197. data/app/views/alchemy/admin/pictures/index.html.erb +0 -1
  198. data/app/views/alchemy/admin/pictures/update.turbo_stream.erb +1 -1
  199. data/app/views/alchemy/admin/resources/_resource_usage_info.html.erb +1 -1
  200. data/app/views/alchemy/admin/resources/_tag_list.html.erb +2 -3
  201. data/app/views/alchemy/admin/styleguide/index.html.erb +25 -20
  202. data/app/views/alchemy/admin/tags/edit.html.erb +1 -1
  203. data/app/views/alchemy/admin/tinymce/_setup.html.erb +2 -2
  204. data/app/views/alchemy/admin/uploader/_button.html.erb +1 -15
  205. data/app/views/alchemy/attachments/show.html.erb +1 -1
  206. data/app/views/alchemy/base/permission_denied.js.erb +1 -1
  207. data/app/views/alchemy/ingredients/shared/_anchor.html.erb +9 -7
  208. data/app/views/alchemy/ingredients/shared/_link_tools.html.erb +12 -5
  209. data/app/views/alchemy/ingredients/shared/_picture_tools.html.erb +10 -11
  210. data/app/views/alchemy/language_links/_spacer.html.erb +1 -1
  211. data/app/views/alchemy/messages_mailer/new.html.erb +1 -1
  212. data/app/views/alchemy/welcome.html.erb +1 -1
  213. data/config/locales/alchemy.en.yml +12 -3
  214. data/config/routes.rb +2 -2
  215. data/db/migrate/20230123112425_add_searchable_to_alchemy_pages.rb +1 -1
  216. data/db/migrate/20230505132743_add_indexes_to_alchemy_pictures.rb +1 -1
  217. data/db/migrate/20231113104432_create_page_mutexes.rb +1 -1
  218. data/db/migrate/20240314105244_create_alchemy_picture_descriptions.rb +1 -1
  219. data/db/migrate/20250626160259_add_unique_index_to_picture_descriptions.rb +1 -1
  220. data/db/migrate/20250905140323_add_created_at_index_to_pictures_and_attachments.rb +1 -1
  221. data/db/migrate/20251106150010_convert_select_value_for_multiple.rb +11 -0
  222. data/db/migrate/20260102121232_add_metadata_to_page_versions.rb +9 -0
  223. data/db/migrate/20260115164704_add_publication_timestamps_to_alchemy_elements.rb +30 -0
  224. data/db/migrate/20260115164705_add_index_to_element_publication_timestamps.rb +13 -0
  225. data/lib/alchemy/ability_helper.rb +1 -3
  226. data/lib/alchemy/auth_accessors.rb +51 -117
  227. data/lib/alchemy/configuration.rb +1 -0
  228. data/lib/alchemy/configurations/main.rb +63 -0
  229. data/lib/alchemy/controller_actions.rb +2 -3
  230. data/lib/alchemy/engine.rb +9 -12
  231. data/lib/alchemy/error_tracking/error_logger.rb +1 -1
  232. data/lib/alchemy/errors.rb +1 -1
  233. data/lib/alchemy/logger.rb +34 -4
  234. data/lib/alchemy/name_conversions.rb +0 -6
  235. data/lib/alchemy/seeder.rb +2 -2
  236. data/lib/alchemy/tasks/usage.rb +4 -4
  237. data/lib/alchemy/test_support/factories/page_version_factory.rb +3 -0
  238. data/lib/alchemy/test_support/having_picture_thumbnails_examples.rb +30 -0
  239. data/lib/alchemy/test_support/shared_ingredient_editor_examples.rb +26 -6
  240. data/lib/alchemy/test_support/shared_publishable_examples.rb +114 -0
  241. data/lib/alchemy/upgrader/eight_one.rb +56 -0
  242. data/lib/alchemy/upgrader.rb +9 -1
  243. data/lib/alchemy/version.rb +1 -1
  244. data/lib/alchemy.rb +1 -4
  245. data/lib/alchemy_cms.rb +0 -1
  246. data/lib/generators/alchemy/elements/templates/view.html.erb +3 -3
  247. data/lib/generators/alchemy/ingredient/ingredient_generator.rb +6 -8
  248. data/lib/generators/alchemy/ingredient/templates/editor_component.rb.tt +22 -0
  249. data/lib/generators/alchemy/page_layouts/templates/layout.html.erb +1 -1
  250. data/lib/generators/alchemy/site_layouts/templates/layout.html.erb +1 -1
  251. data/lib/tasks/alchemy/upgrade.rake +21 -7
  252. data/vendor/javascript/shoelace.min.js +713 -31
  253. data/vendor/javascript/tinymce.min.js +1 -1
  254. metadata +104 -84
  255. data/app/decorators/alchemy/element_editor.rb +0 -90
  256. data/app/helpers/alchemy/admin/pictures_helper.rb +0 -14
  257. data/app/javascript/alchemy_admin/file_editors.js +0 -28
  258. data/app/javascript/alchemy_admin/image_loader.js +0 -54
  259. data/app/javascript/alchemy_admin/page_sorter.js +0 -71
  260. data/app/javascript/alchemy_admin/sitemap.js +0 -154
  261. data/app/javascript/alchemy_admin/templates/page_folder.hbs +0 -3
  262. data/app/views/alchemy/admin/attachments/archive_overlay.js.erb +0 -4
  263. data/app/views/alchemy/admin/pages/_page.html.erb +0 -163
  264. data/app/views/alchemy/admin/pages/_sitemap.html.erb +0 -30
  265. data/app/views/alchemy/admin/pages/flush.js.erb +0 -2
  266. data/app/views/alchemy/admin/pictures/archive_overlay.js.erb +0 -5
  267. data/app/views/alchemy/admin/pictures/index.js.erb +0 -2
  268. data/app/views/alchemy/ingredients/_audio_editor.html.erb +0 -5
  269. data/app/views/alchemy/ingredients/_boolean_editor.html.erb +0 -11
  270. data/app/views/alchemy/ingredients/_datetime_editor.html.erb +0 -20
  271. data/app/views/alchemy/ingredients/_file_editor.html.erb +0 -52
  272. data/app/views/alchemy/ingredients/_headline_editor.html.erb +0 -44
  273. data/app/views/alchemy/ingredients/_html_editor.html.erb +0 -8
  274. data/app/views/alchemy/ingredients/_link_editor.html.erb +0 -30
  275. data/app/views/alchemy/ingredients/_node_editor.html.erb +0 -13
  276. data/app/views/alchemy/ingredients/_number_editor.html.erb +0 -24
  277. data/app/views/alchemy/ingredients/_page_editor.html.erb +0 -13
  278. data/app/views/alchemy/ingredients/_picture_editor.html.erb +0 -59
  279. data/app/views/alchemy/ingredients/_richtext_editor.html.erb +0 -15
  280. data/app/views/alchemy/ingredients/_select_editor.html.erb +0 -31
  281. data/app/views/alchemy/ingredients/_text_editor.html.erb +0 -29
  282. data/app/views/alchemy/ingredients/_video_editor.html.erb +0 -5
  283. data/lib/generators/alchemy/ingredient/templates/editor.html.erb +0 -14
  284. /data/{lib → app/models}/alchemy/permissions.rb +0 -0
@@ -4,10 +4,11 @@ module Alchemy
4
4
  module Admin
5
5
  class PagesController < ResourcesController
6
6
  include OnPageLayout::CallbacksRunner
7
+ include Alchemy::Admin::Clipboard
7
8
 
8
9
  helper "alchemy/pages"
9
10
 
10
- before_action :load_resource, except: [:index, :flush, :new, :create, :copy_language_tree, :link]
11
+ before_action :load_resource, except: [:index, :flush, :new, :create, :copy_language_tree, :link, :tree]
11
12
 
12
13
  authorize_resource class: Alchemy::Page, except: [:index]
13
14
 
@@ -20,13 +21,9 @@ module Alchemy
20
21
  before_action :set_translation,
21
22
  except: [:show]
22
23
 
23
- before_action :set_root_page,
24
- only: [:index, :show]
25
-
26
24
  before_action :set_preview_mode, only: [:show]
27
25
 
28
26
  before_action :load_languages_and_layouts,
29
- unless: -> { @page_root },
30
27
  only: [:index]
31
28
 
32
29
  before_action :set_view, only: [:index, :update]
@@ -62,13 +59,17 @@ module Alchemy
62
59
 
63
60
  items = items.page(params[:page] || 1).per(items_per_page)
64
61
  @pages = items
62
+ elsif @current_language.root_page
63
+ @root_page = @current_language.root_page
65
64
  end
66
65
  end
67
66
 
68
- # Returns all pages as a tree from the root given by the id parameter
69
- #
70
67
  def tree
71
- render json: serialized_page_tree
68
+ @root_page = Alchemy::PageTreePreloader.new(
69
+ page: @current_language.root_page,
70
+ user: current_alchemy_user,
71
+ admin_includes: true
72
+ ).call
72
73
  end
73
74
 
74
75
  # Used by page preview iframe in Page#edit view.
@@ -89,8 +90,6 @@ module Alchemy
89
90
  def new
90
91
  @page ||= Page.new(layoutpage: params[:layoutpage] == "true", parent_id: params[:parent_id])
91
92
  @page_layouts = Page.layouts_for_select(@current_language.id, layoutpages: @page.layoutpage?)
92
- @clipboard = get_clipboard("pages")
93
- @clipboard_items = Page.all_from_clipboard_for_select(@clipboard, @current_language.id, layoutpages: @page.layoutpage?)
94
93
  end
95
94
 
96
95
  def create
@@ -144,12 +143,8 @@ module Alchemy
144
143
  if @view == "list"
145
144
  flash[:notice] = @notice
146
145
  end
147
-
148
- unless @while_page_edit
149
- @tree = serialized_page_tree
150
- end
151
146
  else
152
- render :configure, status: :unprocessable_entity
147
+ render :configure, status: 422
153
148
  end
154
149
  end
155
150
 
@@ -159,8 +154,7 @@ module Alchemy
159
154
  flash[:notice] = Alchemy.t("Page deleted", name: @page.name)
160
155
 
161
156
  # Remove page from clipboard
162
- clipboard = get_clipboard("pages")
163
- clipboard.delete_if { |item| item["id"] == @page.id.to_s }
157
+ remove_resource_from_clipboard(@page)
164
158
  else
165
159
  flash[:warning] = @page.errors.full_messages.to_sentence
166
160
  end
@@ -178,8 +172,22 @@ module Alchemy
178
172
 
179
173
  def fold
180
174
  # @page is fetched via before filter
181
- @page.fold!(current_alchemy_user.id, !@page.folded?(current_alchemy_user.id))
182
- render json: serialized_page_tree
175
+ was_folded = @page.folded?(current_alchemy_user.id)
176
+ @page.fold!(current_alchemy_user.id, !was_folded)
177
+
178
+ respond_to do |format|
179
+ format.turbo_stream do
180
+ if was_folded
181
+ @page = PageTreePreloader.new(
182
+ page: @page,
183
+ user: current_alchemy_user,
184
+ admin_includes: true
185
+ ).call
186
+ else
187
+ head 200
188
+ end
189
+ end
190
+ end
183
191
  end
184
192
 
185
193
  # Leaves the page editing mode and unlocks the page for other users
@@ -205,9 +213,6 @@ module Alchemy
205
213
  def publish
206
214
  # fetching page via before filter
207
215
  @page.publish!
208
-
209
- flash[:notice] = Alchemy.t(:page_published, name: @page.name)
210
- redirect_back(fallback_location: admin_pages_path)
211
216
  end
212
217
 
213
218
  def copy_language_tree
@@ -217,8 +222,12 @@ module Alchemy
217
222
  end
218
223
 
219
224
  def flush
220
- PageVersion.where(page_id: @current_language.pages.select(:id)).published.touch_all
221
- respond_to { |format| format.js }
225
+ @current_language.pages.flushables.update_all(published_at: Time.current)
226
+ # We need to ensure, that also all layoutpages get the +published_at+ timestamp set,
227
+ # but not set to public true, because the cache_key for an element is +published_at+
228
+ # and we don't want the layout pages to be present in +Page.published+ scope.
229
+ @current_language.pages.flushable_layoutpages.update_all(published_at: Time.current)
230
+ respond_to { |format| format.turbo_stream }
222
231
  end
223
232
 
224
233
  private
@@ -309,30 +318,15 @@ module Alchemy
309
318
  @page.locker.try!(:id) != current_alchemy_user.try!(:id)
310
319
  end
311
320
 
312
- def paste_from_clipboard
313
- if params[:paste_from_clipboard]
314
- source = Page.find(params[:paste_from_clipboard])
315
- parent = Page.find_by(id: params[:page][:parent_id])
316
- Page.copy_and_paste(source, parent, params[:page][:name])
317
- end
318
- end
319
-
320
- def set_root_page
321
- @page_root = @current_language.root_page
321
+ # Overridden from Alchemy::Admin::Clipboard concern
322
+ def clipboard_items
323
+ @clipboard_items ||= Page.all_from_clipboard_for_select(clipboard, @current_language.id, layoutpages: @page.layoutpage?)
322
324
  end
323
325
 
324
326
  def set_page_version
325
327
  @page_version = @page.draft_version
326
328
  end
327
329
 
328
- def serialized_page_tree
329
- PageTreeSerializer.new(
330
- @page,
331
- ability: current_ability,
332
- user: current_alchemy_user
333
- )
334
- end
335
-
336
330
  def load_languages_and_layouts
337
331
  @language = @current_language
338
332
  @languages_with_page_tree = Language.on_current_site.with_root_page
@@ -8,8 +8,6 @@ module Alchemy
8
8
  include CurrentLanguage
9
9
  include PictureDescriptionsFormHelper
10
10
 
11
- helper "alchemy/admin/tags"
12
-
13
11
  before_action :load_resource,
14
12
  only: [:edit, :update, :url, :destroy]
15
13
 
@@ -45,7 +43,7 @@ module Alchemy
45
43
  @previous = @pictures.prev_page
46
44
  @next = @pictures.next_page
47
45
 
48
- @assignments = @picture.related_ingredients.joins(element: :page).merge(PageVersion.drafts)
46
+ @assignments = @picture.related_ingredients.joins(element: :page).merge(PageVersion.draft)
49
47
  @picture_description = @picture.descriptions.find_or_initialize_by(
50
48
  language_id: Alchemy::Current.language.id
51
49
  )
@@ -66,7 +64,6 @@ module Alchemy
66
64
 
67
65
  def create
68
66
  @picture = Picture.new(picture_params)
69
- @picture.name = @picture.humanized_name
70
67
  if @picture.save
71
68
  render successful_uploader_response(file: @picture)
72
69
  else
@@ -91,7 +88,7 @@ module Alchemy
91
88
  type: "error"
92
89
  }
93
90
  end
94
- render :update, status: (@message[:type] == "notice") ? :ok : :unprocessable_entity
91
+ render :update, status: (@message[:type] == "notice") ? 200 : 422
95
92
  end
96
93
 
97
94
  def update_multiple
@@ -9,7 +9,7 @@ module Alchemy
9
9
  extend Alchemy::Admin::ResourceName
10
10
  include Alchemy::Admin::ResourceFilter
11
11
 
12
- helper Alchemy::ResourcesHelper, TagsHelper
12
+ helper Alchemy::ResourcesHelper
13
13
  helper_method :resource_handler, :items_per_page, :items_per_page_options
14
14
 
15
15
  before_action :load_resource,
@@ -12,7 +12,7 @@ module Alchemy
12
12
  if params[:page_id].present?
13
13
  @ingredients = @ingredients
14
14
  .where(alchemy_page_versions: {page_id: params[:page_id]})
15
- .merge(Alchemy::PageVersion.drafts)
15
+ .merge(Alchemy::PageVersion.draft)
16
16
  .joins(element: :page_version)
17
17
  end
18
18
 
@@ -28,12 +28,14 @@ module Alchemy
28
28
  def nested
29
29
  @page = Page.find_by(id: params[:page_id]) || Language.current_root_page
30
30
 
31
+ # Preload the full tree from this page
32
+ preloaded_page = PageTreePreloader.new(page: @page, user: current_alchemy_user).call
33
+
31
34
  render json: PageTreeSerializer.new(
32
- @page,
35
+ preloaded_page,
33
36
  ability: current_ability,
34
37
  user: current_alchemy_user,
35
- elements: params[:elements],
36
- full: true
38
+ elements: params[:elements]
37
39
  )
38
40
  end
39
41
 
@@ -48,13 +48,13 @@ module Alchemy
48
48
 
49
49
  def permission_denied(exception = nil)
50
50
  if exception
51
- Rails.logger.debug <<-WARN.strip_heredoc
51
+ Logger.debug <<-WARN.strip_heredoc
52
52
  /!\\ Failed to permit #{exception.action} on #{exception.subject.inspect} for:
53
53
  #{current_alchemy_user.inspect}
54
54
  WARN
55
55
  end
56
56
  if request.format.json?
57
- render json: {message: Alchemy.t("You are not authorized")}, status: :unauthorized
57
+ render json: {message: Alchemy.t("You are not authorized")}, status: 401
58
58
  elsif current_alchemy_user
59
59
  handle_redirect_for_user
60
60
  else
@@ -67,7 +67,7 @@ module Alchemy
67
67
  if can?(:index, :alchemy_admin_dashboard)
68
68
  redirect_or_render_notice
69
69
  else
70
- redirect_to Alchemy.unauthorized_path
70
+ redirect_to Alchemy.config.unauthorized_path
71
71
  end
72
72
  end
73
73
 
@@ -95,14 +95,14 @@ module Alchemy
95
95
  render :permission_denied
96
96
  else
97
97
  store_location
98
- redirect_to Alchemy.login_path
98
+ redirect_to Alchemy.config.login_path
99
99
  end
100
100
  end
101
101
 
102
102
  # Logs the current exception to the error log.
103
103
  def exception_logger(error)
104
- Rails.logger.error("\n#{error.class} #{error.message} in #{error.backtrace.first}")
105
- Rails.logger.error(error.backtrace[1..50].each { |line|
104
+ Logger.error("\n#{error.class} #{error.message} in #{error.backtrace.first}")
105
+ Logger.error(error.backtrace[1..50].each { |line|
106
106
  line.gsub(/#{Rails.root}/, "")
107
107
  }.join("\n"))
108
108
  end
@@ -157,7 +157,7 @@ module Alchemy
157
157
 
158
158
  # Redirects to given url with 301 status
159
159
  def redirect_permanently_to(url)
160
- redirect_to url, status: :moved_permanently
160
+ redirect_to url, status: 301
161
161
  end
162
162
 
163
163
  # Returns url parameters that are not internal show page params.
@@ -208,14 +208,20 @@ module Alchemy
208
208
  end
209
209
 
210
210
  def signup_required?
211
- if Alchemy.user_class.respond_to?(:admins)
212
- Alchemy.user_class.admins.empty?
211
+ if Alchemy.config.user_class.respond_to?(:admins)
212
+ Alchemy.config.user_class.admins.empty?
213
213
  end
214
214
  end
215
215
 
216
216
  # Returns the etag used for response headers.
217
217
  #
218
- # If a user is logged in, we append theirs etag to prevent caching of user related content.
218
+ # The etag is composed of:
219
+ # - The page's cache key (includes updated_at timestamp)
220
+ # - Published element IDs (changes when elements enter/leave the published scope)
221
+ # - The current user's cache key (for user-specific content)
222
+ #
223
+ # This ensures HTTP caches invalidate when scheduled elements become visible
224
+ # or hidden, even though the page's updated_at hasn't changed.
219
225
  #
220
226
  # IMPORTANT:
221
227
  #
@@ -224,7 +230,8 @@ module Alchemy
224
230
  # Otherwise all users will see the same cached page, regardless of user's state.
225
231
  #
226
232
  def page_etag
227
- [@page, current_alchemy_user]
233
+ elements_cache_key = @page.public_version&.elements&.published&.order(:id)&.pluck(:id)
234
+ [@page, elements_cache_key, current_alchemy_user]
228
235
  end
229
236
 
230
237
  # We only render the page if either the cache is disabled for this page
@@ -233,7 +240,6 @@ module Alchemy
233
240
  def render_fresh_page?
234
241
  must_not_cache? || stale?(
235
242
  etag: page_etag,
236
- last_modified: @page.last_modified_at,
237
243
  public: !@page.restricted,
238
244
  template: "pages/show"
239
245
  )
@@ -24,7 +24,6 @@ module Alchemy
24
24
 
25
25
  respond_to do |format|
26
26
  format.html { render partial: "archive_overlay" }
27
- format.js { render action: "archive_overlay" }
28
27
  end
29
28
  end
30
29
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Clipboard functionality for Alchemy admin controllers.
4
+ module Alchemy::Admin::Clipboard
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ helper_method :clipboard, :clipboard_empty?, :clipboard_items
9
+ end
10
+
11
+ # Checks if clipboard for given category is blank
12
+ def clipboard_empty?(category)
13
+ clipboard.blank?
14
+ end
15
+
16
+ private
17
+
18
+ # Returns clipboard items for given category
19
+ def get_clipboard(category)
20
+ session[:alchemy_clipboard] ||= {}
21
+ session[:alchemy_clipboard][category.to_s] ||= []
22
+ end
23
+
24
+ def clipboard_type
25
+ controller_name
26
+ end
27
+
28
+ def clipboard
29
+ @clipboard ||= get_clipboard(clipboard_type)
30
+ end
31
+
32
+ # Overridden in some controllers which use a different scope/parameters
33
+ def clipboard_items
34
+ @clipboard_items ||= model_class.all_from_clipboard(clipboard)
35
+ end
36
+
37
+ def item_from_clipboard
38
+ @item_from_clipboard ||= clipboard.detect { |item| item["id"].to_i == params[:paste_from_clipboard].to_i }
39
+ end
40
+
41
+ def paste_from_clipboard
42
+ if params[:paste_from_clipboard]
43
+ source = model_class.find(params[:paste_from_clipboard])
44
+ parent = model_class.find_by(id: resource_params[:parent_id]) if resource_params[:parent_id]
45
+ model_class.copy_and_paste(source, parent, params.dig(clipboard_type.singularize.to_sym, :name))
46
+ end
47
+ end
48
+
49
+ def model_class
50
+ "alchemy/#{clipboard_type.singularize}".classify.constantize
51
+ end
52
+
53
+ def remove_resource_from_clipboard(resource)
54
+ clipboard = get_clipboard(clipboard_type)
55
+ clipboard.delete_if { |item| item["id"] == resource.id.to_s }
56
+ end
57
+ end
@@ -5,7 +5,7 @@ module Alchemy
5
5
  module UploaderResponses
6
6
  extend ActiveSupport::Concern
7
7
 
8
- def successful_uploader_response(file:, status: :created)
8
+ def successful_uploader_response(file:, status: 201)
9
9
  message = Alchemy.t(:upload_success,
10
10
  scope: [:uploader, file.class.model_name.i18n_key],
11
11
  name: file.name)
@@ -24,7 +24,7 @@ module Alchemy
24
24
 
25
25
  {
26
26
  json: {message: message},
27
- status: :unprocessable_entity
27
+ status: 422
28
28
  }
29
29
  end
30
30
  end
@@ -12,7 +12,7 @@ module Alchemy
12
12
  private
13
13
 
14
14
  def enforce_primary_host_for_site
15
- redirect_to url_for(host: current_alchemy_site.host), status: :moved_permanently, allow_other_host: true
15
+ redirect_to url_for(host: current_alchemy_site.host), status: 301, allow_other_host: true
16
16
  end
17
17
 
18
18
  def needs_redirect_to_primary_host?
@@ -4,9 +4,11 @@ module Alchemy
4
4
  class IngredientEditor < SimpleDelegator
5
5
  alias_method :ingredient, :__getobj__
6
6
 
7
+ # @deprecated
7
8
  def to_partial_path
8
9
  "alchemy/ingredients/#{partial_name}_editor"
9
10
  end
11
+ deprecate :to_partial_path, deprecator: Alchemy::Deprecation
10
12
 
11
13
  # Returns the translated role for displaying in labels
12
14
  #
@@ -23,6 +25,7 @@ module Alchemy
23
25
  # article:
24
26
  # foo: Baz
25
27
  #
28
+ # @deprecated
26
29
  def translated_role
27
30
  Alchemy.t(
28
31
  role,
@@ -30,7 +33,9 @@ module Alchemy
30
33
  default: Alchemy.t("ingredient_roles.#{role}", default: role.humanize)
31
34
  )
32
35
  end
36
+ deprecate translated_role: "Use Ingredient#translated_role instead", deprecator: Alchemy::Deprecation
33
37
 
38
+ # @deprecated
34
39
  def css_classes
35
40
  [
36
41
  "ingredient-editor",
@@ -42,13 +47,16 @@ module Alchemy
42
47
  settings[:anchor] ? "with-anchor" : nil
43
48
  ].compact
44
49
  end
50
+ deprecate :css_classes, deprecator: Alchemy::Deprecation
45
51
 
52
+ # @deprecated
46
53
  def data_attributes
47
54
  {
48
55
  ingredient_id: id,
49
56
  ingredient_role: role
50
57
  }
51
58
  end
59
+ deprecate :data_attributes, deprecator: Alchemy::Deprecation
52
60
 
53
61
  # Returns a string to be passed to Rails form field tags to ensure it can be used with Rails' nested attributes.
54
62
  #
@@ -64,17 +72,20 @@ module Alchemy
64
72
  #
65
73
  # <%= text_field_tag text_editor.form_field_name(:link), text_editor.value %>
66
74
  #
75
+ # @deprecated
67
76
  def form_field_name(column = "value")
68
77
  "element[ingredients_attributes][#{form_field_counter}][#{column}]"
69
78
  end
79
+ deprecate :form_field_name, deprecator: Alchemy::Deprecation
70
80
 
71
81
  # Returns a unique string to be passed to a form field id.
72
82
  #
73
83
  # @param column [String] A Ingredient column_name. Default is 'value'
74
- #
84
+ # @deprecated
75
85
  def form_field_id(column = "value")
76
86
  "element_#{element.id}_ingredient_#{id}_#{column}"
77
87
  end
88
+ deprecate :form_field_id, deprecator: Alchemy::Deprecation
78
89
 
79
90
  # Fixes Rails partial renderer calling to_model on the object
80
91
  # which reveals the delegated ingredient instead of this decorator.
@@ -84,29 +95,38 @@ module Alchemy
84
95
  super
85
96
  end
86
97
 
98
+ # @deprecated
87
99
  def has_warnings?
88
100
  definition.blank? || deprecated?
89
101
  end
102
+ deprecate :has_warnings?, deprecator: Alchemy::Deprecation
90
103
 
104
+ # @deprecated
91
105
  def linked?
92
106
  link.try(:present?)
93
107
  end
108
+ deprecate :linked?, deprecator: Alchemy::Deprecation
94
109
 
110
+ # @deprecated
95
111
  def warnings
96
112
  return unless has_warnings?
97
113
 
98
114
  if definition.blank?
99
- Logger.warn("ingredient #{role} is missing its definition", caller(1..1))
115
+ Logger.warn("ingredient '#{role}' is missing its definition! Please check your element definitions.")
100
116
  Alchemy.t(:ingredient_definition_missing)
101
117
  else
102
118
  definition.deprecation_notice(element_name: element&.name)
103
119
  end
104
120
  end
121
+ deprecate :warnings, deprecator: Alchemy::Deprecation
105
122
 
123
+ # @deprecated
106
124
  def validations
107
125
  definition.validate
108
126
  end
127
+ deprecate :validations, deprecator: Alchemy::Deprecation
109
128
 
129
+ # @deprecated
110
130
  def format_validation
111
131
  format = validations.select { _1.is_a?(Hash) }.find { _1[:format] }&.fetch(:format)
112
132
  return nil unless format
@@ -118,15 +138,28 @@ module Alchemy
118
138
  format
119
139
  end
120
140
  end
141
+ deprecate :format_validation, deprecator: Alchemy::Deprecation
121
142
 
143
+ # @deprecated
122
144
  def length_validation
123
145
  validations.select { _1.is_a?(Hash) }.find { _1[:length] }&.fetch(:length)
124
146
  end
147
+ deprecate :length_validation, deprecator: Alchemy::Deprecation
125
148
 
149
+ # @deprecated
126
150
  def presence_validation?
127
- validations.include?("presence") ||
128
- validations.any? { _1.is_a?(Hash) && _1[:presence] == true }
151
+ validations.any? do |validation|
152
+ case validation
153
+ when :presence, "presence"
154
+ true
155
+ when Hash
156
+ validation[:presence] == true || validation["presence"] == true
157
+ else
158
+ false
159
+ end
160
+ end
129
161
  end
162
+ deprecate :presence_validation?, deprecator: Alchemy::Deprecation
130
163
 
131
164
  private
132
165
 
@@ -57,10 +57,14 @@ module Alchemy
57
57
  if html_options[:title]
58
58
  tooltip = html_options.delete(:title)
59
59
  end
60
- anchor = link_to(content, url, html_options.merge(
61
- "data-dialog-options" => options.to_json,
62
- :is => "alchemy-dialog-link"
63
- ))
60
+ anchor = if url.nil?
61
+ tag.a(content, class: "disabled #{html_options[:class]}".strip, tabindex: "-1")
62
+ else
63
+ link_to(content, url, html_options.merge(
64
+ "data-dialog-options" => options.to_json,
65
+ :is => "alchemy-dialog-link"
66
+ ))
67
+ end
64
68
  if tooltip
65
69
  content_tag("sl-tooltip", anchor, content: tooltip)
66
70
  else
@@ -285,9 +289,9 @@ module Alchemy
285
289
  # @param icon: 'alert' [String] - Icon name
286
290
  #
287
291
  # @return [String]
288
- def hint_with_tooltip(text, icon: "alert")
292
+ def hint_with_tooltip(text, icon: "alert", icon_class: nil)
289
293
  content_tag :"sl-tooltip", class: "like-hint-tooltip", content: text, placement: "bottom" do
290
- render_icon(icon)
294
+ render_icon(icon, class: icon_class)
291
295
  end
292
296
  end
293
297
 
@@ -9,8 +9,8 @@ module Alchemy
9
9
  #
10
10
  # Displays a warning icon if ingredient is missing its definition.
11
11
  #
12
- # Displays a mandatory field indicator, if the ingredient has validations.
13
- #
12
+ # Displays a mandatory field indicator, if the ingredient has a presence validation.
13
+ # @deprecated
14
14
  def render_ingredient_role(ingredient)
15
15
  if ingredient.blank?
16
16
  warning("Ingredient is nil")
@@ -24,14 +24,16 @@ module Alchemy
24
24
  content = "#{icon} #{content}".html_safe
25
25
  end
26
26
 
27
- if ingredient.has_validations?
27
+ if ingredient.presence_validation?
28
28
  "#{content}<span class='validation_indicator'>*</span>".html_safe
29
29
  else
30
30
  content
31
31
  end
32
32
  end
33
+ deprecate :render_ingredient_role, deprecator: Alchemy::Deprecation
33
34
 
34
35
  # Renders the label and hint for a ingredient.
36
+ # @deprecated
35
37
  def ingredient_label(ingredient, column = :value, html_options = {})
36
38
  label_tag ingredient.form_field_id(column), html_options do
37
39
  [
@@ -40,6 +42,7 @@ module Alchemy
40
42
  ].compact.join("&nbsp;").html_safe
41
43
  end
42
44
  end
45
+ deprecate :ingredient_label, deprecator: Alchemy::Deprecation
43
46
  end
44
47
  end
45
48
  end
@@ -5,7 +5,7 @@ module Alchemy
5
5
  # Logs a message in the Rails logger (warn level)
6
6
  # and optionally displays an error message to the user.
7
7
  def warning(message, text = nil)
8
- Logger.warn(message, caller(1..1))
8
+ Logger.warn(message)
9
9
  unless text.nil?
10
10
  render_message(:warning) do
11
11
  text.html_safe
@@ -172,7 +172,7 @@ module Alchemy
172
172
  if Rails.application.config.consider_all_requests_local?
173
173
  raise error, message
174
174
  else
175
- Rails.logger.error message
175
+ Logger.error message
176
176
  ""
177
177
  end
178
178
  end
@@ -2,6 +2,7 @@ import { reloadPreview } from "alchemy_admin/components/preview_window"
2
2
  import { removeTab } from "alchemy_admin/fixed_elements"
3
3
  import { closeCurrentDialog } from "alchemy_admin/dialog"
4
4
  import IngredientAnchorLink from "alchemy_admin/ingredient_anchor_link"
5
+ import pleaseWaitOverlay from "alchemy_admin/please_wait_overlay"
5
6
 
6
7
  class Action extends HTMLElement {
7
8
  constructor() {
@@ -15,7 +16,10 @@ class Action extends HTMLElement {
15
16
  closeCurrentDialog,
16
17
  reloadPreview,
17
18
  removeFixedElement: removeTab,
18
- updateAnchorIcon: IngredientAnchorLink.updateIcon
19
+ updateAnchorIcon: IngredientAnchorLink.updateIcon,
20
+ hidePleaseWaitOverlay() {
21
+ pleaseWaitOverlay(false)
22
+ }
19
23
  }
20
24
  }
21
25