alchemy_cms 8.3.10 → 8.4.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 (370) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +58 -10
  4. data/app/assets/builds/alchemy/admin/page-select.css +1 -1
  5. data/app/assets/builds/alchemy/admin/print.css +1 -1
  6. data/app/assets/builds/alchemy/admin/remote-select.css +1 -0
  7. data/app/assets/builds/alchemy/admin/tags-autocomplete.css +1 -0
  8. data/app/assets/builds/alchemy/admin.css +4 -4
  9. data/app/assets/builds/alchemy/alchemy_admin.min.js +1 -1
  10. data/app/assets/builds/alchemy/alchemy_admin.min.js.map +1 -1
  11. data/app/assets/builds/alchemy/dark-theme.css +1 -1
  12. data/app/assets/builds/alchemy/light-theme.css +1 -1
  13. data/app/assets/builds/alchemy/preview.min.js +1 -1
  14. data/app/assets/builds/alchemy/theme.css +1 -1
  15. data/app/assets/builds/alchemy/welcome.css +1 -1
  16. data/app/assets/builds/tinymce/skins/content/alchemy/content.min.css +1 -1
  17. data/app/assets/builds/tinymce/skins/content/alchemy-dark/content.min.css +1 -1
  18. data/app/assets/builds/tinymce/skins/ui/alchemy/skin.min.css +1 -1
  19. data/app/assets/builds/tinymce/skins/ui/alchemy-dark/skin.min.css +1 -1
  20. data/app/assets/images/alchemy/icons-sprite.svg +1 -1
  21. data/app/components/alchemy/admin/dashboard/widgets/greeting.html.erb +1 -1
  22. data/app/components/alchemy/admin/dashboard/widgets/news_reader.html.erb +38 -0
  23. data/app/components/alchemy/admin/dashboard/widgets/news_reader.rb +54 -0
  24. data/app/components/alchemy/admin/dashboard/widgets/online_users.html.erb +1 -1
  25. data/app/components/alchemy/admin/dashboard/widgets/recent_pages.html.erb +1 -1
  26. data/app/components/alchemy/admin/dashboard/widgets/system_info.html.erb +11 -3
  27. data/app/components/alchemy/admin/dashboard/widgets/system_info.rb +6 -1
  28. data/app/components/alchemy/admin/element_editor.html.erb +1 -1
  29. data/app/components/alchemy/admin/element_select.rb +30 -20
  30. data/app/components/alchemy/admin/element_status_indicators.html.erb +16 -0
  31. data/app/components/alchemy/admin/element_status_indicators.rb +31 -0
  32. data/app/components/alchemy/admin/link_dialog/external_tab.rb +2 -1
  33. data/app/components/alchemy/admin/main_navigation/entry.rb +61 -0
  34. data/app/components/alchemy/admin/main_navigation/menu.rb +21 -0
  35. data/app/components/alchemy/admin/main_navigation/sub_navigation.rb +31 -0
  36. data/app/components/alchemy/admin/page_node.html.erb +1 -1
  37. data/app/components/alchemy/admin/page_permitted_roles_select.html.erb +11 -0
  38. data/app/components/alchemy/admin/page_permitted_roles_select.rb +35 -0
  39. data/app/components/alchemy/admin/page_select.rb +18 -8
  40. data/app/components/alchemy/admin/preview_time_select.rb +1 -1
  41. data/app/components/alchemy/admin/toolbar_button.rb +43 -20
  42. data/app/components/alchemy/admin/uploader_button.rb +66 -0
  43. data/app/components/alchemy/ingredients/base_editor.rb +10 -1
  44. data/app/components/alchemy/ingredients/boolean_editor.rb +1 -0
  45. data/app/components/alchemy/ingredients/color_editor.rb +6 -3
  46. data/app/components/alchemy/ingredients/datetime_view.rb +1 -1
  47. data/app/components/alchemy/ingredients/picture_view.rb +2 -4
  48. data/app/components/alchemy/menubar.html.erb +157 -0
  49. data/app/components/alchemy/menubar.rb +15 -0
  50. data/app/controllers/alchemy/admin/attachments_controller.rb +12 -9
  51. data/app/controllers/alchemy/admin/base_controller.rb +34 -21
  52. data/app/controllers/alchemy/admin/dashboard_controller.rb +4 -0
  53. data/app/controllers/alchemy/admin/elements_controller.rb +4 -23
  54. data/app/controllers/alchemy/admin/languages_controller.rb +1 -1
  55. data/app/controllers/alchemy/admin/nodes_controller.rb +1 -1
  56. data/app/controllers/alchemy/admin/pages_controller.rb +1 -1
  57. data/app/controllers/alchemy/admin/pictures_controller.rb +6 -3
  58. data/app/controllers/alchemy/admin/resources_controller.rb +5 -5
  59. data/app/controllers/alchemy/admin/sites_controller.rb +1 -1
  60. data/app/controllers/alchemy/base_controller.rb +24 -16
  61. data/app/controllers/alchemy/pages_controller.rb +29 -8
  62. data/app/controllers/alchemy/sitemap_controller.rb +58 -0
  63. data/app/controllers/concerns/alchemy/admin/csp_protection.rb +43 -0
  64. data/app/controllers/concerns/alchemy/admin/current_language.rb +1 -1
  65. data/app/controllers/concerns/alchemy/admin/resource_filter.rb +3 -1
  66. data/{lib → app/controllers/concerns}/alchemy/controller_actions.rb +13 -0
  67. data/app/controllers/concerns/alchemy/legacy_page_redirects.rb +4 -4
  68. data/app/helpers/alchemy/admin/base_helper.rb +4 -1
  69. data/app/helpers/alchemy/admin/form_helper.rb +0 -2
  70. data/app/helpers/alchemy/admin/navigation_helper.rb +10 -181
  71. data/app/helpers/alchemy/elements_block_helper.rb +14 -0
  72. data/app/helpers/alchemy/resources_helper.rb +15 -1
  73. data/app/javascript/alchemy_admin/components/attachment_select.js +11 -20
  74. data/app/javascript/alchemy_admin/components/auto_submit.js +2 -4
  75. data/app/javascript/alchemy_admin/components/button.js +0 -6
  76. data/app/javascript/alchemy_admin/components/clear_schedule_button.js +21 -0
  77. data/app/javascript/alchemy_admin/components/color_select.js +50 -45
  78. data/app/javascript/alchemy_admin/components/conditional_field.js +50 -0
  79. data/app/javascript/alchemy_admin/components/dirty_guard.js +77 -0
  80. data/app/javascript/alchemy_admin/components/dom_id_select.js +17 -10
  81. data/app/javascript/alchemy_admin/components/element_editor/delete_element_button.js +7 -2
  82. data/app/javascript/alchemy_admin/components/element_editor.js +29 -56
  83. data/app/javascript/alchemy_admin/components/element_select.js +26 -66
  84. data/app/javascript/alchemy_admin/components/icon.js +9 -4
  85. data/app/javascript/alchemy_admin/{image_cropper.js → components/image_cropper.js} +39 -53
  86. data/app/javascript/alchemy_admin/components/image_overlay_link.js +14 -0
  87. data/app/javascript/alchemy_admin/components/index.js +12 -1
  88. data/app/javascript/alchemy_admin/components/main_navi_entry.js +75 -0
  89. data/app/javascript/alchemy_admin/components/menu_node.js +73 -0
  90. data/app/javascript/alchemy_admin/components/news_reader.js +109 -0
  91. data/app/javascript/alchemy_admin/components/node_folder.js +36 -0
  92. data/app/javascript/alchemy_admin/components/node_form.js +34 -0
  93. data/app/javascript/alchemy_admin/components/node_select.js +25 -30
  94. data/app/javascript/alchemy_admin/components/node_tree.js +56 -0
  95. data/app/javascript/alchemy_admin/components/page_node.js +2 -1
  96. data/app/javascript/alchemy_admin/components/page_select.js +16 -34
  97. data/app/javascript/alchemy_admin/components/picture_archive.js +92 -0
  98. data/app/javascript/alchemy_admin/components/picture_editor.js +2 -1
  99. data/app/javascript/alchemy_admin/components/preview_window.js +11 -3
  100. data/app/javascript/alchemy_admin/components/remote_select.js +461 -81
  101. data/app/javascript/alchemy_admin/components/select.js +26 -88
  102. data/app/javascript/alchemy_admin/components/sitemap.js +2 -1
  103. data/app/javascript/alchemy_admin/components/sortable_elements.js +2 -1
  104. data/app/javascript/alchemy_admin/components/tags_autocomplete.js +63 -48
  105. data/app/javascript/alchemy_admin/components/tinymce.js +2 -1
  106. data/app/javascript/alchemy_admin/components/uploader/file_upload.js +3 -2
  107. data/app/javascript/alchemy_admin/components/uploader.js +19 -3
  108. data/app/javascript/alchemy_admin/confirm_dialog.js +7 -1
  109. data/app/javascript/alchemy_admin/dialog.js +182 -110
  110. data/app/javascript/alchemy_admin/hotkeys.js +0 -5
  111. data/app/javascript/alchemy_admin/i18n.js +3 -9
  112. data/app/javascript/alchemy_admin/image_overlay.js +58 -42
  113. data/app/javascript/alchemy_admin/initializer.js +2 -2
  114. data/app/javascript/alchemy_admin/link_dialog.js +15 -2
  115. data/app/javascript/alchemy_admin/routes.js +18 -0
  116. data/app/javascript/alchemy_admin/turbo_stream_actions.js +88 -0
  117. data/app/javascript/alchemy_admin/utils/ajax.js +14 -3
  118. data/app/javascript/alchemy_admin/utils/events.js +5 -0
  119. data/app/javascript/alchemy_admin/utils/json_script.js +12 -0
  120. data/app/javascript/alchemy_admin/utils/tom_select.js +171 -0
  121. data/app/javascript/alchemy_admin.js +2 -19
  122. data/app/javascript/preview.js +131 -113
  123. data/app/jobs/alchemy/invalidate_elements_cache_job.rb +1 -1
  124. data/app/models/alchemy/admin/content_security_policy.rb +143 -0
  125. data/app/models/alchemy/admin/filters/base.rb +7 -0
  126. data/app/models/alchemy/admin/filters/datepicker.rb +1 -1
  127. data/app/models/alchemy/admin/filters/select.rb +20 -7
  128. data/app/models/alchemy/attachment.rb +14 -7
  129. data/app/models/alchemy/cache_control.rb +104 -0
  130. data/app/models/alchemy/current.rb +1 -1
  131. data/app/models/alchemy/eager_loading.rb +0 -2
  132. data/app/models/alchemy/element.rb +23 -8
  133. data/app/models/alchemy/element_definition.rb +1 -0
  134. data/app/models/alchemy/elements_repository.rb +11 -0
  135. data/app/models/alchemy/folded_page.rb +5 -1
  136. data/app/models/alchemy/ingredient.rb +26 -0
  137. data/app/models/alchemy/ingredients/datetime.rb +1 -1
  138. data/app/models/alchemy/ingredients/link.rb +2 -0
  139. data/app/models/alchemy/ingredients/picture.rb +3 -0
  140. data/app/models/alchemy/ingredients/text.rb +2 -0
  141. data/app/models/alchemy/language.rb +19 -7
  142. data/app/models/alchemy/legacy_page_url.rb +6 -1
  143. data/app/models/alchemy/node.rb +51 -0
  144. data/app/models/alchemy/page/page_natures.rb +41 -8
  145. data/app/models/alchemy/page/page_scopes.rb +23 -0
  146. data/app/models/alchemy/page/sitemap_cache_key.rb +56 -0
  147. data/app/models/alchemy/page.rb +88 -32
  148. data/app/models/alchemy/page_definition.rb +1 -1
  149. data/app/models/alchemy/page_mutex.rb +16 -0
  150. data/app/models/alchemy/page_version.rb +24 -1
  151. data/app/models/alchemy/permissions.rb +6 -5
  152. data/app/models/alchemy/picture.rb +15 -7
  153. data/app/models/alchemy/picture_description.rb +22 -0
  154. data/app/models/alchemy/picture_thumb.rb +18 -0
  155. data/app/models/alchemy/publishable/timestamp_resolver.rb +70 -0
  156. data/app/models/alchemy/resource.rb +1 -1
  157. data/app/models/alchemy/safe_url_validator.rb +34 -0
  158. data/app/models/alchemy/site.rb +8 -3
  159. data/app/models/alchemy/storage_adapter/active_storage/preprocessor.rb +7 -3
  160. data/app/models/alchemy/storage_adapter/active_storage.rb +4 -1
  161. data/app/models/alchemy/tag.rb +10 -4
  162. data/{lib → app/models/concerns}/alchemy/filetypes.rb +5 -0
  163. data/app/models/concerns/alchemy/picture_thumbnails.rb +61 -6
  164. data/app/models/concerns/alchemy/publishable.rb +14 -29
  165. data/app/models/concerns/alchemy/taggable.rb +92 -0
  166. data/app/models/concerns/alchemy/user_methods.rb +95 -0
  167. data/app/services/alchemy/duplicate_element.rb +5 -2
  168. data/app/services/alchemy/element_preloader.rb +1 -2
  169. data/app/stylesheets/alchemy/_custom-properties.scss +1 -0
  170. data/app/stylesheets/alchemy/_extends.scss +0 -8
  171. data/app/stylesheets/alchemy/_themes.scss +10 -9
  172. data/app/stylesheets/alchemy/admin/_empty-state.scss +29 -0
  173. data/app/stylesheets/alchemy/admin/_help.scss +10 -0
  174. data/app/stylesheets/alchemy/admin/_tom-select.scss +104 -15
  175. data/app/stylesheets/alchemy/admin/archive.scss +56 -11
  176. data/app/stylesheets/alchemy/admin/base.scss +4 -5
  177. data/app/stylesheets/alchemy/admin/buttons.scss +12 -1
  178. data/app/stylesheets/alchemy/admin/dashboard.scss +150 -20
  179. data/app/stylesheets/alchemy/admin/dialogs.scss +27 -36
  180. data/app/stylesheets/alchemy/admin/element-select.scss +1 -1
  181. data/app/stylesheets/alchemy/admin/elements.scss +8 -24
  182. data/app/stylesheets/alchemy/admin/flash.scss +2 -1
  183. data/app/stylesheets/alchemy/admin/forms.scss +60 -24
  184. data/app/stylesheets/alchemy/admin/frame.scss +40 -4
  185. data/app/stylesheets/alchemy/admin/image_library.scss +9 -12
  186. data/app/stylesheets/alchemy/admin/navigation.scss +17 -14
  187. data/app/stylesheets/alchemy/admin/node-select.scss +13 -37
  188. data/app/stylesheets/alchemy/admin/nodes.scss +1 -1
  189. data/app/stylesheets/alchemy/admin/page-select.scss +6 -97
  190. data/app/stylesheets/alchemy/admin/print.scss +0 -4
  191. data/app/stylesheets/alchemy/admin/remote-select.scss +170 -0
  192. data/app/stylesheets/alchemy/admin/search.scss +2 -3
  193. data/app/stylesheets/alchemy/admin/selects.scss +1 -294
  194. data/app/stylesheets/alchemy/admin/sitemap.scss +2 -1
  195. data/app/stylesheets/alchemy/admin/tables.scss +7 -1
  196. data/app/stylesheets/alchemy/admin/tags-autocomplete.scss +17 -0
  197. data/app/stylesheets/alchemy/admin/tags.scss +0 -13
  198. data/app/stylesheets/alchemy/admin/toolbar.scss +9 -6
  199. data/app/stylesheets/alchemy/admin/upload.scss +61 -1
  200. data/app/stylesheets/alchemy/admin.scss +4 -1
  201. data/app/stylesheets/tinymce/skins/ui/alchemy/skin.scss +19 -0
  202. data/app/stylesheets/tinymce/skins/ui/alchemy-dark/skin.scss +19 -0
  203. data/app/types/alchemy/cache_control_type.rb +84 -0
  204. data/app/views/alchemy/_edit_mode.html.erb +1 -1
  205. data/app/views/alchemy/_menubar.html.erb +2 -154
  206. data/app/views/alchemy/_preview_mode_code.html.erb +0 -3
  207. data/app/views/alchemy/admin/_left_menu.html.erb +18 -5
  208. data/app/views/alchemy/admin/_top_menu.html.erb +1 -1
  209. data/app/views/alchemy/admin/attachments/_archive_overlay.html.erb +5 -4
  210. data/app/views/alchemy/admin/attachments/_file_to_assign.html.erb +3 -3
  211. data/app/views/alchemy/admin/attachments/_files_list.html.erb +71 -83
  212. data/app/views/alchemy/admin/attachments/assign.turbo_stream.erb +8 -0
  213. data/app/views/alchemy/admin/attachments/index.html.erb +32 -7
  214. data/app/views/alchemy/admin/clipboard/insert.turbo_stream.erb +1 -1
  215. data/app/views/alchemy/admin/crop.html.erb +20 -20
  216. data/app/views/alchemy/admin/dashboard/_footer.html.erb +0 -3
  217. data/app/views/alchemy/admin/dashboard/_top.html.erb +1 -0
  218. data/app/views/alchemy/admin/dashboard/help.html.erb +84 -57
  219. data/app/views/alchemy/admin/dashboard/license.html.erb +6 -0
  220. data/app/views/alchemy/admin/elements/_footer.html.erb +0 -1
  221. data/app/views/alchemy/admin/elements/_form.html.erb +1 -1
  222. data/app/views/alchemy/admin/elements/_header.html.erb +2 -19
  223. data/app/views/alchemy/admin/elements/_preview_text_quote.html.erb +3 -0
  224. data/app/views/alchemy/admin/elements/_schedule.html.erb +7 -7
  225. data/app/views/alchemy/admin/elements/_toolbar.html.erb +1 -1
  226. data/app/views/alchemy/admin/elements/create.turbo_stream.erb +1 -1
  227. data/app/views/alchemy/admin/elements/index.html.erb +1 -1
  228. data/app/views/alchemy/admin/elements/publish.turbo_stream.erb +2 -2
  229. data/app/views/alchemy/admin/elements/update.turbo_stream.erb +31 -0
  230. data/app/views/alchemy/admin/ingredients/update.turbo_stream.erb +6 -3
  231. data/app/views/alchemy/admin/languages/_table.html.erb +0 -2
  232. data/app/views/alchemy/admin/languages/index.html.erb +25 -29
  233. data/app/views/alchemy/admin/layoutpages/_layoutpage.html.erb +1 -3
  234. data/app/views/alchemy/admin/layoutpages/edit.html.erb +1 -1
  235. data/app/views/alchemy/admin/layoutpages/index.html.erb +26 -7
  236. data/app/views/alchemy/admin/nodes/_form.html.erb +3 -21
  237. data/app/views/alchemy/admin/nodes/_node.html.erb +93 -88
  238. data/app/views/alchemy/admin/nodes/index.html.erb +15 -23
  239. data/app/views/alchemy/admin/pages/_create_language_form.html.erb +2 -6
  240. data/app/views/alchemy/admin/pages/_form.html.erb +2 -1
  241. data/app/views/alchemy/admin/pages/_publication_fields.html.erb +1 -1
  242. data/app/views/alchemy/admin/pages/_table.html.erb +2 -4
  243. data/app/views/alchemy/admin/pages/_toolbar.html.erb +2 -1
  244. data/app/views/alchemy/admin/pages/edit.html.erb +10 -21
  245. data/app/views/alchemy/admin/pages/flush.turbo_stream.erb +1 -1
  246. data/app/views/alchemy/admin/pages/index.html.erb +26 -31
  247. data/app/views/alchemy/admin/pages/info.html.erb +2 -2
  248. data/app/views/alchemy/admin/pages/update.turbo_stream.erb +2 -2
  249. data/app/views/alchemy/admin/partials/_language_tree_select.html.erb +1 -1
  250. data/app/views/alchemy/admin/partials/_routes.html.erb +24 -48
  251. data/app/views/alchemy/admin/pictures/_archive.html.erb +3 -17
  252. data/app/views/alchemy/admin/pictures/_archive_overlay.html.erb +1 -1
  253. data/app/views/alchemy/admin/pictures/_filter_and_size_bar.html.erb +5 -3
  254. data/app/views/alchemy/admin/pictures/_form.html.erb +0 -1
  255. data/app/views/alchemy/admin/pictures/_infos.html.erb +5 -1
  256. data/app/views/alchemy/admin/pictures/_picture.html.erb +1 -1
  257. data/app/views/alchemy/admin/pictures/_picture_to_assign.html.erb +3 -4
  258. data/app/views/alchemy/admin/pictures/assign.turbo_stream.erb +7 -0
  259. data/app/views/alchemy/admin/pictures/index.html.erb +38 -30
  260. data/app/views/alchemy/admin/pictures/show.html.erb +41 -41
  261. data/app/views/alchemy/admin/resources/_empty_state.html.erb +27 -0
  262. data/app/views/alchemy/admin/resources/_pagination.html.erb +1 -1
  263. data/app/views/alchemy/admin/resources/_per_page_select.html.erb +1 -1
  264. data/app/views/alchemy/admin/resources/_resource_table.html.erb +0 -2
  265. data/app/views/alchemy/admin/resources/_resource_usage_info.html.erb +3 -2
  266. data/app/views/alchemy/admin/resources/index.html.erb +32 -13
  267. data/app/views/alchemy/admin/sites/index.html.erb +24 -22
  268. data/app/views/alchemy/admin/styleguide/index.html.erb +35 -12
  269. data/app/views/alchemy/admin/tags/index.html.erb +27 -25
  270. data/app/views/alchemy/admin/tinymce/_setup.html.erb +6 -8
  271. data/app/views/alchemy/admin/translations/_en.js +3 -0
  272. data/app/views/alchemy/admin/uploader/_button.html.erb +11 -17
  273. data/app/views/alchemy/admin/uploader/_setup.html.erb +3 -3
  274. data/app/views/alchemy/pages/sitemap.xml.erb +2 -0
  275. data/app/views/kaminari/alchemy/_first_page.html.erb +1 -2
  276. data/app/views/kaminari/alchemy/_gap.html.erb +0 -1
  277. data/app/views/kaminari/alchemy/_last_page.html.erb +1 -2
  278. data/app/views/kaminari/alchemy/_next_page.html.erb +1 -2
  279. data/app/views/kaminari/alchemy/_page.html.erb +1 -2
  280. data/app/views/kaminari/alchemy/_paginator.html.erb +0 -1
  281. data/app/views/kaminari/alchemy/_prev_page.html.erb +1 -2
  282. data/app/views/layouts/alchemy/admin/dialog.html.erb +3 -0
  283. data/app/views/layouts/alchemy/admin.html.erb +5 -6
  284. data/config/brakeman.ignore +23 -1
  285. data/config/importmap.rb +10 -8
  286. data/config/initializers/dragonfly.rb +21 -17
  287. data/config/locales/alchemy.en.yml +53 -12
  288. data/config/routes.rb +3 -3
  289. data/db/migrate/20260727120000_add_permitted_roles_to_alchemy_pages.rb +23 -0
  290. data/db/migrate/20260820120000_add_cached_tag_list_to_taggables.rb +57 -0
  291. data/lib/alchemy/configuration/number_option.rb +13 -0
  292. data/lib/alchemy/configuration/proc_option.rb +13 -0
  293. data/lib/alchemy/configuration.rb +2 -0
  294. data/lib/alchemy/configurations/admin_components.rb +30 -0
  295. data/lib/alchemy/configurations/format_matchers.rb +7 -1
  296. data/lib/alchemy/configurations/main.rb +103 -4
  297. data/lib/alchemy/configurations/menu.rb +91 -0
  298. data/lib/alchemy/configurations/menu_item.rb +123 -0
  299. data/lib/alchemy/configurations/sitemap.rb +1 -0
  300. data/lib/alchemy/configurations/uploader.rb +15 -1
  301. data/lib/alchemy/engine.rb +0 -6
  302. data/lib/alchemy/i18n.rb +26 -0
  303. data/lib/alchemy/modules.rb +52 -50
  304. data/lib/alchemy/tasks/generate_thumbnails.rb +99 -0
  305. data/lib/alchemy/tasks/tidy.rb +1 -1
  306. data/lib/alchemy/test_support/capybara_helpers.rb +48 -37
  307. data/lib/alchemy/test_support/current_language_shared_examples.rb +1 -1
  308. data/lib/alchemy/test_support/factories/dummy_user_factory.rb +4 -1
  309. data/lib/alchemy/test_support/having_picture_thumbnails_examples.rb +132 -0
  310. data/lib/alchemy/test_support/shared_ingredient_examples.rb +52 -0
  311. data/lib/alchemy/test_support/shared_publishable_examples.rb +39 -4
  312. data/lib/alchemy/upgrader/eight_four.rb +237 -0
  313. data/lib/alchemy/upgrader/register_module_rewriter.rb +177 -0
  314. data/lib/alchemy/upgrader.rb +2 -1
  315. data/lib/alchemy/version.rb +1 -1
  316. data/lib/alchemy.rb +0 -1
  317. data/lib/alchemy_cms.rb +0 -13
  318. data/lib/generators/alchemy/elements/templates/view.html.erb +1 -1
  319. data/lib/generators/alchemy/elements/templates/view.html.haml +1 -1
  320. data/lib/generators/alchemy/elements/templates/view.html.slim +1 -1
  321. data/lib/generators/alchemy/install/install_generator.rb +2 -13
  322. data/lib/generators/alchemy/install/templates/alchemy.rb.tt +24 -9
  323. data/lib/generators/alchemy/module/module_generator.rb +1 -0
  324. data/lib/generators/alchemy/module/templates/ability.rb.tt +1 -1
  325. data/lib/generators/alchemy/module/templates/module_config.rb.tt +16 -18
  326. data/lib/generators/alchemy/user_columns_migration/templates/migration.rb.tt +19 -0
  327. data/lib/generators/alchemy/user_columns_migration/user_columns_migration_generator.rb +30 -0
  328. data/lib/tasks/alchemy/thumbnails.rake +16 -29
  329. data/lib/tasks/alchemy/upgrade.rake +45 -3
  330. data/vendor/javascript/cropperjs.min.js +3 -3
  331. data/vendor/javascript/floating-ui.min.js +1 -1
  332. data/vendor/javascript/tinymce.min.js +5 -5
  333. data/vendor/javascript/tom-select.min.js +1 -1
  334. metadata +85 -65
  335. data/app/javascript/alchemy_admin/components/action.js +0 -50
  336. data/app/javascript/alchemy_admin/dirty.js +0 -55
  337. data/app/javascript/alchemy_admin/node_tree.js +0 -82
  338. data/app/javascript/alchemy_admin/picture_selector.js +0 -67
  339. data/app/javascript/alchemy_admin/templates/compiled.js +0 -1
  340. data/app/javascript/alchemy_admin/templates/node_folder.hbs +0 -3
  341. data/app/stylesheets/alchemy/admin/attachment-select.scss +0 -19
  342. data/app/views/alchemy/admin/_main_navi.html.erb +0 -6
  343. data/app/views/alchemy/admin/attachments/assign.js.erb +0 -15
  344. data/app/views/alchemy/admin/pages/_page_layout_filter.html.erb +0 -30
  345. data/app/views/alchemy/admin/partials/_main_navigation_entry.html.erb +0 -20
  346. data/app/views/alchemy/admin/partials/_sub_navigation.html.erb +0 -11
  347. data/app/views/alchemy/admin/pictures/assign.js.erb +0 -13
  348. data/app/views/alchemy/base/error_notice.js.erb +0 -1
  349. data/app/views/alchemy/base/permission_denied.js.erb +0 -3
  350. data/app/views/alchemy/base/redirect.js.erb +0 -11
  351. data/config/alchemy/config.yml +0 -217
  352. data/config/alchemy/modules.yml +0 -82
  353. data/config/initializers/rails_live_reload.rb +0 -13
  354. data/lib/alchemy/dev_support/live_reload_watcher.rb +0 -5
  355. data/lib/alchemy/taggable.rb +0 -49
  356. data/lib/generators/alchemy/install/templates/dragonfly.rb.tt +0 -34
  357. data/vendor/assets/stylesheets/alchemy_admin/select2.css +0 -1
  358. data/vendor/javascript/handlebars.min.js +0 -29
  359. data/vendor/javascript/jquery.min.js +0 -2
  360. data/vendor/javascript/rails-ujs.min.js +0 -1
  361. data/vendor/javascript/select2.min.js +0 -23
  362. /data/{lib → app/controllers/concerns}/alchemy/ability_helper.rb +0 -0
  363. /data/{lib → app/controllers/concerns}/alchemy/admin/locale.rb +0 -0
  364. /data/{lib → app/controllers/concerns}/alchemy/admin/preview_time.rb +0 -0
  365. /data/{lib → app/controllers/concerns}/alchemy/admin/timezone.rb +0 -0
  366. /data/{lib → app/controllers/concerns}/alchemy/configuration_methods.rb +0 -0
  367. /data/{lib → app/models}/alchemy/admin/preview_url.rb +0 -0
  368. /data/{lib → app/models/concerns}/alchemy/hints.rb +0 -0
  369. /data/{lib → app/models/concerns}/alchemy/name_conversions.rb +0 -0
  370. /data/{lib → app/services}/alchemy/elements_finder.rb +0 -0
@@ -1,12 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "alchemy/configuration"
4
+ require "alchemy/configurations/admin_components"
4
5
  require "alchemy/configurations/dashboard"
5
6
  require "alchemy/configurations/default_language"
6
7
  require "alchemy/configurations/default_site"
7
8
  require "alchemy/configurations/importmap"
8
9
  require "alchemy/configurations/format_matchers"
9
10
  require "alchemy/configurations/mailer"
11
+ require "alchemy/configurations/menu"
12
+ require "alchemy/configurations/menu_item"
10
13
  require "alchemy/configurations/page_cache"
11
14
  require "alchemy/configurations/preview"
12
15
  require "alchemy/configurations/sitemap"
@@ -50,6 +53,7 @@ module Alchemy
50
53
  #
51
54
  # show_root [Boolean] # Show language root page in sitemap?
52
55
  # show_flag [Boolean] # Enables the Checkbox in Page#update overlay. So your customer can set the visibility of pages in the sitemap.
56
+ # max_age [Integer] # How long the sitemap is cached, in seconds. Set to 0 to disable sitemap caching.
53
57
  configuration :sitemap, Sitemap
54
58
 
55
59
  # === Default items per page in admin views
@@ -81,6 +85,14 @@ module Alchemy
81
85
  #
82
86
  configuration :preview, Preview
83
87
 
88
+ # === Preview load timeout
89
+ #
90
+ # The number of seconds the admin preview window waits for the preview
91
+ # frame to load before it stops the loading spinner and warns the user
92
+ # that the preview failed to load.
93
+ #
94
+ option :preview_load_timeout, :integer, default: 5
95
+
84
96
  # === Picture rendering settings
85
97
  #
86
98
  # Alchemy uses Dragonfly to render images. Settings for image rendering are specific to elements and are defined in elements.yml
@@ -177,17 +189,42 @@ module Alchemy
177
189
 
178
190
  # === Link Target Options
179
191
  #
180
- # Values for the link target selectbox inside the page link overlay.
181
- # The value gets attached as a data-link-target attribute to the link.
192
+ # Values for the link target selectbox inside the link dialog.
193
+ #
194
+ # Each value is stored with a leading underscore, so that it can be used
195
+ # as the link's `target` attribute right away: `blank` is stored as
196
+ # `_blank` and rendered as `target="_blank"`. Views rendering a link
197
+ # themselves should pass the stored value through
198
+ # `Alchemy::Ingredients::LinkTarget#link_target_value`, which also maps
199
+ # the legacy `blank` (stored before the underscore was added) to
200
+ # `_blank`.
182
201
  #
183
202
  # == Example:
184
203
  #
185
- # Open all links set to overlay inside an jQuery UI Dialog Window.
204
+ # Open all links set to overlay in a dialog window in your frontend code,
205
+ # based on the `target="_overlay"` attribute. Such a target has no meaning
206
+ # to the browser, so your code needs to handle the click itself.
186
207
  #
187
- # jQuery(a[data-link-target="overlay"]).dialog();
208
+ # config.link_target_options = %w[blank overlay]
188
209
  #
189
210
  option :link_target_options, :collection, item_type: :string, default: %w[blank]
190
211
 
212
+ # === Allowed url schemes
213
+ #
214
+ # Url schemes editors are allowed to store in menu nodes and linkable
215
+ # ingredients. Any other scheme is rejected, because a scheme like
216
+ # `javascript:` turns an editable url into script execution in your
217
+ # site's own origin.
218
+ #
219
+ # Urls without a scheme (absolute and relative paths, anchors, queries
220
+ # and protocol relative urls) are always allowed.
221
+ #
222
+ # == Example:
223
+ #
224
+ # config.allowed_url_schemes = %w[http https mailto]
225
+ #
226
+ option :allowed_url_schemes, :collection, item_type: :string, default: %w[http https mailto tel ftp]
227
+
191
228
  # === Format matchers
192
229
  #
193
230
  # Named aliases for regular expressions that can be used in various places.
@@ -291,6 +328,34 @@ module Alchemy
291
328
  #
292
329
  option :admin_importmaps, :collection, collection_class: Set, item_type: :configuration, config_class: Alchemy::Configurations::Importmap, default: []
293
330
 
331
+ # The entries of the admin main navigation
332
+ #
333
+ # +url+ and +condition+ are evaluated in the view context.
334
+ #
335
+ # Entries without a +condition+ (and without a legacy +controller+ and
336
+ # +action+) are shown to every admin user.
337
+ #
338
+ # == Example
339
+ #
340
+ # Alchemy.configure do |config|
341
+ # config.menu.add(
342
+ # name: "events",
343
+ # title: "Events",
344
+ # icon: "calendar",
345
+ # url: -> { main_app.admin_events_path },
346
+ # condition: -> { can?(:index, :admin_events) },
347
+ # children: [
348
+ # {
349
+ # title: "Locations",
350
+ # url: -> { main_app.admin_locations_path },
351
+ # condition: -> { can?(:index, :admin_locations) }
352
+ # }
353
+ # ]
354
+ # )
355
+ # end
356
+ #
357
+ option :menu, :collection, item_type: :configuration, config_class: Alchemy::Configurations::MenuItem, default: Alchemy::Configurations::Menu::DEFAULT
358
+
294
359
  # Additional stylesheets to be included in the Alchemy admin UI
295
360
  #
296
361
  # == Example
@@ -468,6 +533,40 @@ module Alchemy
468
533
  # === Dashboard configuration
469
534
  #
470
535
  configuration :dashboard, Dashboard
536
+
537
+ # === Admin component registry
538
+ #
539
+ # Overridable admin ViewComponents keyed by role, so extensions can swap
540
+ # a component for their own.
541
+ #
542
+ configuration :admin_components, AdminComponents
543
+
544
+ # === Admin Content Security Policy
545
+ #
546
+ # The class building the Content Security Policy Alchemy sends with its
547
+ # own admin responses. Defaults to +nil+, which sends no policy at all.
548
+ # It will be enabled by default in Alchemy 9.0.
549
+ #
550
+ # It never replaces a policy the host application has configured, and it
551
+ # only applies to Alchemy's own controllers.
552
+ #
553
+ # Alchemy.config.admin_content_security_policy = "Alchemy::Admin::ContentSecurityPolicy"
554
+ #
555
+ option :admin_content_security_policy, :class, default: nil
556
+
557
+ # === Publishable resolver
558
+ #
559
+ # The class used to resolve publication state for Publishable records
560
+ # (currently +Alchemy::Element+ and +Alchemy::PageVersion+).
561
+ #
562
+ # The default resolver uses the +public_on+ / +public_until+ timestamp
563
+ # columns. Apps or gems can provide a custom resolver that implements
564
+ # the same interface — see +Alchemy::Publishable::TimestampResolver+
565
+ # for the interface.
566
+ #
567
+ # == Example
568
+ # Alchemy.config.publishable_resolver = "MyApp::CustomResolver"
569
+ option :publishable_resolver, :class, default: "Alchemy::Publishable::TimestampResolver"
471
570
  end
472
571
  end
473
572
  end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ module Configurations
5
+ module Menu
6
+ DEFAULT = [
7
+ {
8
+ name: "dashboard",
9
+ title: "Home",
10
+ position: 1,
11
+ icon: "home-2",
12
+ url: -> { alchemy.admin_dashboard_path },
13
+ condition: -> { can?(:index, :alchemy_admin_dashboard) }
14
+ },
15
+ {
16
+ name: "pages",
17
+ title: "modules.pages",
18
+ position: 2,
19
+ icon: "pages",
20
+ url: -> { alchemy.admin_pages_path },
21
+ condition: -> { can?(:index, :alchemy_admin_pages) },
22
+ children: [
23
+ {
24
+ title: "modules.pages",
25
+ url: -> { alchemy.admin_pages_path },
26
+ condition: -> { can?(:index, :alchemy_admin_pages) }
27
+ },
28
+ {
29
+ title: "modules.layoutpages",
30
+ url: -> { alchemy.admin_layoutpages_path },
31
+ condition: -> { can?(:index, :alchemy_admin_layoutpages) }
32
+ }
33
+ ]
34
+ },
35
+ {
36
+ name: "menus",
37
+ title: "modules.menus",
38
+ position: 3,
39
+ icon: "menu-2",
40
+ url: -> { alchemy.admin_nodes_path },
41
+ condition: -> { can?(:index, :alchemy_admin_nodes) }
42
+ },
43
+ {
44
+ name: "languages",
45
+ title: "modules.languages",
46
+ position: 4,
47
+ icon: "translate-2",
48
+ icon_style: "none",
49
+ url: -> { alchemy.admin_languages_path },
50
+ condition: -> { can?(:index, :alchemy_admin_languages) }
51
+ },
52
+ {
53
+ name: "sites",
54
+ title: "modules.sites",
55
+ position: 5,
56
+ icon: "global",
57
+ url: -> { alchemy.admin_sites_path },
58
+ condition: -> { can?(:index, :alchemy_admin_sites) }
59
+ },
60
+ {
61
+ name: "tags",
62
+ title: "modules.tags",
63
+ position: 6,
64
+ icon: "price-tag-3",
65
+ url: -> { alchemy.admin_tags_path },
66
+ condition: -> { can?(:index, :alchemy_admin_tags) }
67
+ },
68
+ {
69
+ name: "archive",
70
+ title: "modules.library",
71
+ position: 7,
72
+ icon: "archive-drawer",
73
+ url: -> { alchemy.admin_pictures_path },
74
+ condition: -> { can?(:index, :alchemy_admin_pictures) },
75
+ children: [
76
+ {
77
+ title: "modules.pictures",
78
+ url: -> { alchemy.admin_pictures_path },
79
+ condition: -> { can?(:index, :alchemy_admin_pictures) }
80
+ },
81
+ {
82
+ title: "modules.attachments",
83
+ url: -> { alchemy.admin_attachments_path },
84
+ condition: -> { can?(:index, :alchemy_admin_attachments) }
85
+ }
86
+ ]
87
+ }
88
+ ].freeze
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ module Configurations
5
+ class MenuItem < Alchemy::Configuration
6
+ option :name, :string
7
+ option :title, :string
8
+ option :engine_name, :string
9
+ option :position, :number
10
+ option :icon, :string
11
+ option :icon_style, :string
12
+ option :image, :string
13
+ option :inline_image, :string
14
+ option :turbo, :boolean, default: true
15
+ option :url, :proc
16
+ option :condition, :proc
17
+ option :match_path, :regexp
18
+ option :children, :collection, item_type: :configuration, config_class: MenuItem
19
+
20
+ # Legacy options of entries registered with +Alchemy::Modules.register_module+
21
+ option :controller, :string
22
+ option :action, :string
23
+ option :nested_controllers, :collection, item_type: :string
24
+ option :nested_actions, :collection, item_type: :string
25
+ option :nested, :collection, item_type: :configuration, config_class: MenuItem
26
+
27
+ def self.from_legacy_hash(hash)
28
+ hash = hash.to_h.deep_stringify_keys
29
+ new(
30
+ from_legacy_navigation(hash["navigation"] || {}, engine_name: hash["engine_name"]).merge(
31
+ "name" => hash["name"],
32
+ "position" => hash["position"]
33
+ ).compact
34
+ )
35
+ end
36
+
37
+ def self.from_legacy_navigation(navigation, engine_name:)
38
+ {
39
+ "title" => navigation["name"],
40
+ "engine_name" => engine_name,
41
+ "controller" => navigation["controller"],
42
+ "action" => navigation["action"],
43
+ "url" => navigation["url"],
44
+ "condition" => navigation["condition"],
45
+ "icon" => navigation["icon"],
46
+ "icon_style" => navigation["icon-style"],
47
+ "image" => navigation["image"],
48
+ "inline_image" => navigation["inline_image"],
49
+ "turbo" => navigation.dig("data", "turbo").to_s != "false",
50
+ "nested_controllers" => navigation["nested_controllers"],
51
+ "nested_actions" => navigation["nested_actions"],
52
+ "children" => navigation["sub_navigation"]&.map { |entry| new(from_legacy_navigation(entry, engine_name:)) },
53
+ "nested" => navigation["nested"]&.map { |entry| new(from_legacy_navigation(entry, engine_name:)) }
54
+ }.compact
55
+ end
56
+ private_class_method :from_legacy_navigation
57
+
58
+ def path(view_context)
59
+ if url
60
+ view_context.instance_exec(&url)
61
+ elsif controller
62
+ routes = engine_name.present? ? view_context.public_send(engine_name) : view_context.main_app
63
+ routes.url_for(controller:, action:, only_path: true)
64
+ end
65
+ end
66
+
67
+ def visible?(view_context)
68
+ if condition
69
+ view_context.instance_exec(&condition)
70
+ elsif controller
71
+ view_context.can?(action.to_sym, controller.delete_prefix("/").tr("/", "_").to_sym)
72
+ else
73
+ true
74
+ end
75
+ end
76
+
77
+ def active?(view_context)
78
+ current?(view_context) || children.any? { |child| child.active?(view_context) }
79
+ end
80
+
81
+ def current?(view_context)
82
+ current_path = view_context.request.path
83
+ path_matches = if match_path
84
+ match_path.match?(current_path)
85
+ elsif (own_path = local_path(view_context))
86
+ current_path == own_path || current_path.start_with?("#{own_path.chomp("/")}/")
87
+ end
88
+
89
+ path_matches || [self, *nested].any? do |entry|
90
+ entry.matches_controller?(controller: view_context.params[:controller], action: view_context.params[:action])
91
+ end
92
+ end
93
+
94
+ def matches_controller?(controller:, action:)
95
+ return false unless self.controller
96
+
97
+ controller = controller.to_s.delete_prefix("/")
98
+ action = action.to_s
99
+ (self.controller.delete_prefix("/") == controller || nested_controllers.include?(controller)) &&
100
+ (self.action == action || nested_actions.include?(action))
101
+ end
102
+
103
+ def validate_controller_existence!
104
+ [controller, *children.map(&:controller)].compact.each do |controller|
105
+ controller_const_name = "#{controller.camelize}Controller"
106
+ controller_const_name.constantize
107
+ rescue NameError
108
+ raise "Error in AlchemyCMS module definition: '#{name}'. Could not find the " \
109
+ "matching controller class #{controller_const_name} for the specified controller: '#{controller}'"
110
+ end
111
+ end
112
+
113
+ private
114
+
115
+ def local_path(view_context)
116
+ uri = URI.parse(path(view_context).to_s)
117
+ uri.path.presence if uri.host.nil? || uri.host == view_context.request.host
118
+ rescue URI::InvalidURIError
119
+ nil
120
+ end
121
+ end
122
+ end
123
+ end
@@ -5,6 +5,7 @@ module Alchemy
5
5
  class Sitemap < Alchemy::Configuration
6
6
  option :show_root, :boolean, default: true
7
7
  option :show_flag, :boolean, default: false
8
+ option :max_age, :integer, default: 3600
8
9
  end
9
10
  end
10
11
  end
@@ -4,7 +4,21 @@ module Alchemy
4
4
  module Configurations
5
5
  class Uploader < Alchemy::Configuration
6
6
  class AllowedFileTypes < Alchemy::Configuration
7
- option :alchemy_attachments, :collection, item_type: :string, default: ["*"]
7
+ # Document, media and archive formats Alchemy knows how to display.
8
+ # Executables and other active content are deliberately absent.
9
+ DEFAULT_ATTACHMENT_FILE_TYPES = %w[
10
+ pdf
11
+ doc docx odt rtf txt
12
+ xls xlsx ods csv
13
+ ppt pptx odp
14
+ vcf
15
+ zip
16
+ avif gif jpeg jpg png psd svg tif tiff webp
17
+ aac flac m4a mp3 oga ogg wav weba
18
+ avi flv m4v mov mp4 mpeg mpg ogv webm wmv
19
+ ].freeze
20
+
21
+ option :alchemy_attachments, :collection, item_type: :string, default: DEFAULT_ATTACHMENT_FILE_TYPES
8
22
  option :alchemy_pictures, :collection, item_type: :string, default: %w[jpg jpeg gif png svg webp]
9
23
 
10
24
  def set(configuration_hash)
@@ -159,12 +159,6 @@ module Alchemy
159
159
  Alchemy.config.user_class.stampable(stamper_class_name: Alchemy.config.user_class_name)
160
160
  end
161
161
  end
162
-
163
- if defined?(RailsLiveReload) && Rails.env.development?
164
- require "alchemy/dev_support/live_reload_watcher"
165
-
166
- Alchemy::LiveReloadWatcher.init
167
- end
168
162
  end
169
163
 
170
164
  initializer "alchemy.webp-mime_type" do |app|
data/lib/alchemy/i18n.rb CHANGED
@@ -15,6 +15,20 @@ module Alchemy
15
15
  def t(msg, **kwargs)
16
16
  Alchemy::I18n.translate(msg, **kwargs)
17
17
  end
18
+
19
+ # Alchemy shortcut localization method
20
+ #
21
+ # Instead of having to call:
22
+ #
23
+ # Alchemy::I18n.localize(datetime)
24
+ #
25
+ # You can use this shortcut method:
26
+ #
27
+ # Alchemy.l(datetime)
28
+ #
29
+ def l(datetime, **kwargs)
30
+ Alchemy::I18n.localize(datetime, **kwargs)
31
+ end
18
32
  end
19
33
 
20
34
  module I18n
@@ -53,6 +67,18 @@ module Alchemy
53
67
  ::I18n.t(msg, **options.merge(scope: scope))
54
68
  end
55
69
 
70
+ # Returns a localized time string for the given time and format.
71
+ #
72
+ # @param datetime [Time|Date|nil]
73
+ # @param format [Symbol] (:"alchemy.default")
74
+ #
75
+ # @return [String|nil]
76
+ def localize(datetime, format: :"alchemy.default")
77
+ return nil if datetime.nil?
78
+
79
+ ::I18n.l(datetime, format:)
80
+ end
81
+
56
82
  def available_locales
57
83
  Rails.application.config.i18n.available_locales || begin
58
84
  @@available_locales ||= nil
@@ -2,10 +2,6 @@
2
2
 
3
3
  module Alchemy
4
4
  module Modules
5
- mattr_accessor :alchemy_modules
6
-
7
- @@alchemy_modules = YAML.load_file(File.expand_path("../../config/alchemy/modules.yml", __dir__))
8
-
9
5
  class << self
10
6
  def included(base)
11
7
  base.send :helper_method, :alchemy_modules, :module_definition_for
@@ -25,68 +21,74 @@ module Alchemy
25
21
  # }
26
22
  #
27
23
  def register_module(module_definition)
28
- definition_hash = module_definition.deep_stringify_keys
24
+ menu_item = Alchemy::Configurations::MenuItem.from_legacy_hash(module_definition)
25
+ return if menu_item.name.present? && Alchemy.config.menu.any? { |item| item.name == menu_item.name }
26
+
27
+ Alchemy.config.menu.add(menu_item)
28
+ end
29
+ deprecate register_module: "Use `Alchemy.config.menu.add` instead", deprecator: Alchemy::Deprecation
30
+
31
+ def alchemy_modules
32
+ Alchemy.config.menu.map { |menu_item| to_legacy_hash(menu_item) }
33
+ end
34
+ deprecate alchemy_modules: "Use `Alchemy.config.menu` instead", deprecator: Alchemy::Deprecation
35
+
36
+ def to_legacy_hash(menu_item)
37
+ {
38
+ "name" => menu_item.name,
39
+ "engine_name" => menu_item.engine_name,
40
+ "position" => menu_item.position,
41
+ "navigation" => to_legacy_navigation(menu_item)
42
+ }.compact
43
+ end
44
+
45
+ private
29
46
 
30
- @@alchemy_modules |= [definition_hash]
47
+ def to_legacy_navigation(menu_item)
48
+ {
49
+ "name" => menu_item.title,
50
+ "controller" => menu_item.controller,
51
+ "action" => menu_item.action,
52
+ "url" => menu_item.url,
53
+ "condition" => menu_item.condition,
54
+ "icon" => menu_item.icon,
55
+ "icon-style" => menu_item.icon_style,
56
+ "image" => menu_item.image,
57
+ "inline_image" => menu_item.inline_image,
58
+ "data" => menu_item.turbo ? nil : {turbo: false},
59
+ "nested_controllers" => menu_item.nested_controllers.to_a.presence,
60
+ "nested_actions" => menu_item.nested_actions.to_a.presence,
61
+ "sub_navigation" => menu_item.children.map { |child| to_legacy_navigation(child) }.presence,
62
+ "nested" => menu_item.nested.map { |entry| to_legacy_navigation(entry) }.presence
63
+ }.compact
31
64
  end
32
65
  end
33
66
 
67
+ def alchemy_modules
68
+ Alchemy::Modules.alchemy_modules
69
+ end
70
+
34
71
  # Get the module definition for given module name
35
72
  #
36
73
  # You can also pass a hash of an module definition.
37
74
  # It then tries to find the module defintion from controller name and action name
38
75
  #
39
76
  def module_definition_for(name_or_params)
40
- case name_or_params
77
+ menu_item = case name_or_params
41
78
  when String
42
- alchemy_modules.detect { |m| m["name"] == name_or_params }
79
+ Alchemy.config.menu.detect { |item| item.name == name_or_params }
43
80
  when Hash
44
- name_or_params.stringify_keys!
45
- alchemy_modules.detect do |alchemy_module|
46
- module_navi = alchemy_module.fetch("navigation", {})
47
- definition_from_mainnavi(module_navi, name_or_params) ||
48
- definition_from_subnavi(module_navi, name_or_params) ||
49
- definition_from_nested(module_navi, name_or_params)
81
+ params = name_or_params.stringify_keys
82
+ Alchemy.config.menu.detect do |item|
83
+ [item, *item.children, *item.nested].any? do |entry|
84
+ entry.matches_controller?(controller: params["controller"], action: params["action"])
85
+ end
50
86
  end
51
87
  else
52
88
  raise ArgumentError, "Could not find module definition for #{name_or_params}"
53
89
  end
90
+ Alchemy::Modules.to_legacy_hash(menu_item) if menu_item
54
91
  end
55
-
56
- private
57
-
58
- def definition_from_mainnavi(module_navi, params)
59
- controller_matches?(module_navi, params) && action_matches?(module_navi, params)
60
- end
61
-
62
- def definition_from_subnavi(module_navi, params)
63
- subnavi = module_navi["sub_navigation"]
64
- return if subnavi.nil?
65
-
66
- subnavi.any? do |navi|
67
- controller_matches?(navi, params) && action_matches?(navi, params)
68
- end
69
- end
70
-
71
- def definition_from_nested(module_navi, params)
72
- nested = module_navi["nested"]
73
- return if nested.nil?
74
-
75
- nested.any? do |navi|
76
- controller_matches?(navi, params) && action_matches?(navi, params)
77
- end
78
- end
79
-
80
- def controller_matches?(navi, params)
81
- remove_slash(navi["controller"]) == remove_slash(params["controller"])
82
- end
83
-
84
- def action_matches?(navi, params)
85
- navi["action"] == params["action"]
86
- end
87
-
88
- def remove_slash(str)
89
- str.gsub(/^\//, "")
90
- end
92
+ deprecate module_definition_for: "Use `Alchemy.config.menu` instead", deprecator: Alchemy::Deprecation
91
93
  end
92
94
  end