alchemy_cms 8.3.11 → 8.4.1

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 (368) 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 -27
  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/types/alchemy/cache_control_type.rb +84 -0
  202. data/app/views/alchemy/_edit_mode.html.erb +1 -1
  203. data/app/views/alchemy/_menubar.html.erb +2 -154
  204. data/app/views/alchemy/_preview_mode_code.html.erb +0 -3
  205. data/app/views/alchemy/admin/_left_menu.html.erb +18 -5
  206. data/app/views/alchemy/admin/_top_menu.html.erb +1 -1
  207. data/app/views/alchemy/admin/attachments/_archive_overlay.html.erb +5 -4
  208. data/app/views/alchemy/admin/attachments/_file_to_assign.html.erb +3 -3
  209. data/app/views/alchemy/admin/attachments/_files_list.html.erb +71 -83
  210. data/app/views/alchemy/admin/attachments/assign.turbo_stream.erb +8 -0
  211. data/app/views/alchemy/admin/attachments/index.html.erb +32 -7
  212. data/app/views/alchemy/admin/clipboard/insert.turbo_stream.erb +1 -1
  213. data/app/views/alchemy/admin/crop.html.erb +20 -20
  214. data/app/views/alchemy/admin/dashboard/_footer.html.erb +0 -3
  215. data/app/views/alchemy/admin/dashboard/_top.html.erb +1 -0
  216. data/app/views/alchemy/admin/dashboard/help.html.erb +84 -57
  217. data/app/views/alchemy/admin/dashboard/license.html.erb +6 -0
  218. data/app/views/alchemy/admin/elements/_footer.html.erb +0 -1
  219. data/app/views/alchemy/admin/elements/_form.html.erb +1 -1
  220. data/app/views/alchemy/admin/elements/_header.html.erb +2 -19
  221. data/app/views/alchemy/admin/elements/_preview_text_quote.html.erb +3 -0
  222. data/app/views/alchemy/admin/elements/_schedule.html.erb +7 -7
  223. data/app/views/alchemy/admin/elements/_toolbar.html.erb +1 -1
  224. data/app/views/alchemy/admin/elements/create.turbo_stream.erb +1 -1
  225. data/app/views/alchemy/admin/elements/index.html.erb +1 -1
  226. data/app/views/alchemy/admin/elements/publish.turbo_stream.erb +2 -2
  227. data/app/views/alchemy/admin/elements/update.turbo_stream.erb +31 -0
  228. data/app/views/alchemy/admin/ingredients/update.turbo_stream.erb +6 -3
  229. data/app/views/alchemy/admin/languages/_table.html.erb +0 -2
  230. data/app/views/alchemy/admin/languages/index.html.erb +25 -29
  231. data/app/views/alchemy/admin/layoutpages/_layoutpage.html.erb +1 -3
  232. data/app/views/alchemy/admin/layoutpages/edit.html.erb +1 -1
  233. data/app/views/alchemy/admin/layoutpages/index.html.erb +26 -7
  234. data/app/views/alchemy/admin/nodes/_form.html.erb +3 -21
  235. data/app/views/alchemy/admin/nodes/_node.html.erb +93 -88
  236. data/app/views/alchemy/admin/nodes/index.html.erb +15 -23
  237. data/app/views/alchemy/admin/pages/_create_language_form.html.erb +2 -6
  238. data/app/views/alchemy/admin/pages/_form.html.erb +2 -1
  239. data/app/views/alchemy/admin/pages/_publication_fields.html.erb +1 -1
  240. data/app/views/alchemy/admin/pages/_table.html.erb +2 -4
  241. data/app/views/alchemy/admin/pages/_toolbar.html.erb +2 -1
  242. data/app/views/alchemy/admin/pages/edit.html.erb +10 -21
  243. data/app/views/alchemy/admin/pages/flush.turbo_stream.erb +1 -1
  244. data/app/views/alchemy/admin/pages/index.html.erb +26 -31
  245. data/app/views/alchemy/admin/pages/info.html.erb +2 -2
  246. data/app/views/alchemy/admin/pages/update.turbo_stream.erb +2 -2
  247. data/app/views/alchemy/admin/partials/_language_tree_select.html.erb +1 -1
  248. data/app/views/alchemy/admin/partials/_routes.html.erb +24 -48
  249. data/app/views/alchemy/admin/pictures/_archive.html.erb +3 -17
  250. data/app/views/alchemy/admin/pictures/_archive_overlay.html.erb +1 -1
  251. data/app/views/alchemy/admin/pictures/_filter_and_size_bar.html.erb +5 -3
  252. data/app/views/alchemy/admin/pictures/_form.html.erb +0 -1
  253. data/app/views/alchemy/admin/pictures/_infos.html.erb +5 -1
  254. data/app/views/alchemy/admin/pictures/_picture.html.erb +1 -1
  255. data/app/views/alchemy/admin/pictures/_picture_to_assign.html.erb +3 -4
  256. data/app/views/alchemy/admin/pictures/assign.turbo_stream.erb +7 -0
  257. data/app/views/alchemy/admin/pictures/index.html.erb +38 -30
  258. data/app/views/alchemy/admin/pictures/show.html.erb +41 -41
  259. data/app/views/alchemy/admin/resources/_empty_state.html.erb +27 -0
  260. data/app/views/alchemy/admin/resources/_pagination.html.erb +1 -1
  261. data/app/views/alchemy/admin/resources/_per_page_select.html.erb +1 -1
  262. data/app/views/alchemy/admin/resources/_resource_table.html.erb +0 -2
  263. data/app/views/alchemy/admin/resources/_resource_usage_info.html.erb +3 -2
  264. data/app/views/alchemy/admin/resources/index.html.erb +32 -13
  265. data/app/views/alchemy/admin/sites/index.html.erb +24 -22
  266. data/app/views/alchemy/admin/styleguide/index.html.erb +35 -12
  267. data/app/views/alchemy/admin/tags/index.html.erb +27 -25
  268. data/app/views/alchemy/admin/tinymce/_setup.html.erb +6 -8
  269. data/app/views/alchemy/admin/translations/_en.js +3 -0
  270. data/app/views/alchemy/admin/uploader/_button.html.erb +11 -17
  271. data/app/views/alchemy/admin/uploader/_setup.html.erb +3 -3
  272. data/app/views/alchemy/pages/sitemap.xml.erb +2 -0
  273. data/app/views/kaminari/alchemy/_first_page.html.erb +1 -2
  274. data/app/views/kaminari/alchemy/_gap.html.erb +0 -1
  275. data/app/views/kaminari/alchemy/_last_page.html.erb +1 -2
  276. data/app/views/kaminari/alchemy/_next_page.html.erb +1 -2
  277. data/app/views/kaminari/alchemy/_page.html.erb +1 -2
  278. data/app/views/kaminari/alchemy/_paginator.html.erb +0 -1
  279. data/app/views/kaminari/alchemy/_prev_page.html.erb +1 -2
  280. data/app/views/layouts/alchemy/admin/dialog.html.erb +3 -0
  281. data/app/views/layouts/alchemy/admin.html.erb +5 -6
  282. data/config/brakeman.ignore +23 -1
  283. data/config/importmap.rb +10 -8
  284. data/config/initializers/dragonfly.rb +21 -17
  285. data/config/locales/alchemy.en.yml +52 -11
  286. data/config/routes.rb +3 -3
  287. data/db/migrate/20260727120000_add_permitted_roles_to_alchemy_pages.rb +23 -0
  288. data/db/migrate/20260820120000_add_cached_tag_list_to_taggables.rb +57 -0
  289. data/lib/alchemy/configuration/number_option.rb +13 -0
  290. data/lib/alchemy/configuration/proc_option.rb +13 -0
  291. data/lib/alchemy/configuration.rb +2 -0
  292. data/lib/alchemy/configurations/admin_components.rb +30 -0
  293. data/lib/alchemy/configurations/format_matchers.rb +7 -1
  294. data/lib/alchemy/configurations/main.rb +103 -4
  295. data/lib/alchemy/configurations/menu.rb +91 -0
  296. data/lib/alchemy/configurations/menu_item.rb +123 -0
  297. data/lib/alchemy/configurations/sitemap.rb +1 -0
  298. data/lib/alchemy/configurations/uploader.rb +15 -1
  299. data/lib/alchemy/engine.rb +0 -6
  300. data/lib/alchemy/i18n.rb +26 -0
  301. data/lib/alchemy/modules.rb +52 -50
  302. data/lib/alchemy/tasks/generate_thumbnails.rb +99 -0
  303. data/lib/alchemy/tasks/tidy.rb +1 -1
  304. data/lib/alchemy/test_support/capybara_helpers.rb +48 -37
  305. data/lib/alchemy/test_support/current_language_shared_examples.rb +1 -1
  306. data/lib/alchemy/test_support/factories/dummy_user_factory.rb +4 -1
  307. data/lib/alchemy/test_support/having_picture_thumbnails_examples.rb +132 -0
  308. data/lib/alchemy/test_support/shared_ingredient_examples.rb +52 -0
  309. data/lib/alchemy/test_support/shared_publishable_examples.rb +29 -4
  310. data/lib/alchemy/upgrader/eight_four.rb +237 -0
  311. data/lib/alchemy/upgrader/register_module_rewriter.rb +177 -0
  312. data/lib/alchemy/upgrader.rb +2 -1
  313. data/lib/alchemy/version.rb +1 -1
  314. data/lib/alchemy.rb +0 -1
  315. data/lib/alchemy_cms.rb +0 -13
  316. data/lib/generators/alchemy/elements/templates/view.html.erb +1 -1
  317. data/lib/generators/alchemy/elements/templates/view.html.haml +1 -1
  318. data/lib/generators/alchemy/elements/templates/view.html.slim +1 -1
  319. data/lib/generators/alchemy/install/install_generator.rb +2 -13
  320. data/lib/generators/alchemy/install/templates/alchemy.rb.tt +24 -9
  321. data/lib/generators/alchemy/module/module_generator.rb +1 -0
  322. data/lib/generators/alchemy/module/templates/ability.rb.tt +1 -1
  323. data/lib/generators/alchemy/module/templates/module_config.rb.tt +16 -18
  324. data/lib/generators/alchemy/user_columns_migration/templates/migration.rb.tt +19 -0
  325. data/lib/generators/alchemy/user_columns_migration/user_columns_migration_generator.rb +30 -0
  326. data/lib/tasks/alchemy/thumbnails.rake +16 -29
  327. data/lib/tasks/alchemy/upgrade.rake +45 -3
  328. data/vendor/javascript/cropperjs.min.js +3 -3
  329. data/vendor/javascript/floating-ui.min.js +1 -1
  330. data/vendor/javascript/tinymce.min.js +5 -5
  331. data/vendor/javascript/tom-select.min.js +1 -1
  332. metadata +85 -65
  333. data/app/javascript/alchemy_admin/components/action.js +0 -50
  334. data/app/javascript/alchemy_admin/dirty.js +0 -55
  335. data/app/javascript/alchemy_admin/node_tree.js +0 -82
  336. data/app/javascript/alchemy_admin/picture_selector.js +0 -67
  337. data/app/javascript/alchemy_admin/templates/compiled.js +0 -1
  338. data/app/javascript/alchemy_admin/templates/node_folder.hbs +0 -3
  339. data/app/stylesheets/alchemy/admin/attachment-select.scss +0 -19
  340. data/app/views/alchemy/admin/_main_navi.html.erb +0 -6
  341. data/app/views/alchemy/admin/attachments/assign.js.erb +0 -15
  342. data/app/views/alchemy/admin/pages/_page_layout_filter.html.erb +0 -30
  343. data/app/views/alchemy/admin/partials/_main_navigation_entry.html.erb +0 -20
  344. data/app/views/alchemy/admin/partials/_sub_navigation.html.erb +0 -11
  345. data/app/views/alchemy/admin/pictures/assign.js.erb +0 -13
  346. data/app/views/alchemy/base/error_notice.js.erb +0 -1
  347. data/app/views/alchemy/base/permission_denied.js.erb +0 -3
  348. data/app/views/alchemy/base/redirect.js.erb +0 -11
  349. data/config/alchemy/config.yml +0 -217
  350. data/config/alchemy/modules.yml +0 -82
  351. data/config/initializers/rails_live_reload.rb +0 -13
  352. data/lib/alchemy/dev_support/live_reload_watcher.rb +0 -5
  353. data/lib/alchemy/taggable.rb +0 -49
  354. data/lib/generators/alchemy/install/templates/dragonfly.rb.tt +0 -34
  355. data/vendor/assets/stylesheets/alchemy_admin/select2.css +0 -1
  356. data/vendor/javascript/handlebars.min.js +0 -29
  357. data/vendor/javascript/jquery.min.js +0 -2
  358. data/vendor/javascript/rails-ujs.min.js +0 -1
  359. data/vendor/javascript/select2.min.js +0 -23
  360. /data/{lib → app/controllers/concerns}/alchemy/ability_helper.rb +0 -0
  361. /data/{lib → app/controllers/concerns}/alchemy/admin/locale.rb +0 -0
  362. /data/{lib → app/controllers/concerns}/alchemy/admin/preview_time.rb +0 -0
  363. /data/{lib → app/controllers/concerns}/alchemy/admin/timezone.rb +0 -0
  364. /data/{lib → app/controllers/concerns}/alchemy/configuration_methods.rb +0 -0
  365. /data/{lib → app/models}/alchemy/admin/preview_url.rb +0 -0
  366. /data/{lib → app/models/concerns}/alchemy/hints.rb +0 -0
  367. /data/{lib → app/models/concerns}/alchemy/name_conversions.rb +0 -0
  368. /data/{lib → app/services}/alchemy/elements_finder.rb +0 -0
@@ -0,0 +1,237 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ class Upgrader
5
+ module EightFour
6
+ # Alchemy does not depend on the dragonfly gem anymore.
7
+ # Apps still using the dragonfly storage adapter need to
8
+ # declare the gem in their own Gemfile.
9
+ def add_dragonfly_gem
10
+ return unless Alchemy.storage_adapter.dragonfly?
11
+
12
+ run %(bundle add dragonfly --version "~> 1.4")
13
+ end
14
+
15
+ # The attachment uploader used to accept every file type.
16
+ def notify_attachment_filetypes_default
17
+ default = Alchemy::Configurations::Uploader::AllowedFileTypes::DEFAULT_ATTACHMENT_FILE_TYPES
18
+ current = Alchemy.config.uploader.allowed_filetypes.alchemy_attachments.to_a
19
+ return unless current == default || current.include?("*")
20
+
21
+ closing = if current.include?("*")
22
+ <<~TEXT.strip
23
+ Your app configures `["*"]`, so it still accepts every file type.
24
+ Please consider adopting the new default.
25
+ TEXT
26
+ else
27
+ <<~TEXT.strip
28
+ Setting it back to `["*"]` restores the old behaviour and disables
29
+ file type validation entirely.
30
+ TEXT
31
+ end
32
+
33
+ todo(<<~TODO.strip, "Attachment uploads are now restricted to an allowlist")
34
+ The `uploader.allowed_filetypes.alchemy_attachments` setting used to
35
+ default to `["*"]`, which accepts every file type, including
36
+ executables. It now defaults to a list of document, media and archive
37
+ formats:
38
+
39
+ #{default.each_slice(8).map { |slice| " #{slice.join(" ")}" }.join("\n")}
40
+
41
+ Please check whether your editors upload file types that are not on
42
+ this list. If they do, add them in `config/initializers/alchemy.rb`:
43
+
44
+ config.uploader.allowed_filetypes.alchemy_attachments = %w[#{default.first(3).join(" ")} ...]
45
+
46
+ #{closing}
47
+ TODO
48
+ end
49
+
50
+ def notify_admin_content_security_policy
51
+ todo(<<~TODO.strip, "The admin can send a Content Security Policy")
52
+ Alchemy 9.0 will send a Content Security Policy with its admin
53
+ responses by default. Opt in now to find out what it would block:
54
+
55
+ config.admin_content_security_policy = "Alchemy::Admin::ContentSecurityPolicy"
56
+
57
+ If you have inline scripts or anything else in your admin that a CSP
58
+ would block, subclass `Alchemy::Admin::ContentSecurityPolicy`.
59
+ TODO
60
+ end
61
+
62
+ def notify_picture_format_setting
63
+ todo(<<~TODO.strip, "Picture ingredients now respects the `format` setting")
64
+ New formats create new image variants. To generate them upfront instead
65
+ of on the first request, run the thumbnail task:
66
+
67
+ bin/rake alchemy:generate:ingredient_picture_thumbnails
68
+
69
+ Set `ELEMENTS=element1,element2` to only generate thumbnails for some
70
+ elements, and `ASYNC=true` to enqueue them as background jobs
71
+ (active_storage only). `bin/rake alchemy:generate:thumbnails`
72
+ generates the thumbnails of the picture library as well.
73
+ TODO
74
+ end
75
+
76
+ # Element partials that render nested elements through the
77
+ # +nested_elements+ association issue one database query per parent
78
+ # element. Rendering through the block helper's +nested_elements+
79
+ # instead reuses the page version's preloaded element set.
80
+ def upgrade_nested_elements_rendering
81
+ rewritten = []
82
+ manual = []
83
+ Dir.glob(Rails.root.join("app/views/alchemy/elements/*")).each do |file|
84
+ next unless File.file?(file)
85
+
86
+ content = File.read(file)
87
+ block_variable = content[/element_view_for\b.*?do\s*\|\s*(\w+)\s*\|/m, 1]
88
+
89
+ if block_variable
90
+ pattern = /render\s+(?!#{block_variable}\b)\w+\.nested_elements(?:\.published)?/
91
+ if content.match?(pattern)
92
+ gsub_file(file, pattern, "render #{block_variable}.nested_elements")
93
+ rewritten << file
94
+ end
95
+ elsif content.match?(/render\s+\w+\.nested_elements/)
96
+ manual << file
97
+ end
98
+ end
99
+
100
+ list = ->(files) { files.map { |file| " - #{Pathname.new(file).relative_path_from(Rails.root)}" }.join("\n") }
101
+
102
+ if rewritten.any?
103
+ todo(<<~TODO.strip, "Nested elements are now rendered through the block helper")
104
+ We rewrote the following element partials to render nested elements
105
+ through the `element_view_for` block helper instead of the
106
+ `nested_elements` association, which issues one query per element:
107
+
108
+ #{list.call(rewritten)}
109
+
110
+ Please review these changes and commit them. Also check your views
111
+ for other uses of `element.nested_elements` that were not rewritten
112
+ automatically (for example assigned to a variable first).
113
+ TODO
114
+ end
115
+
116
+ if manual.any?
117
+ todo(<<~TODO.strip, "Nested elements rendered outside a block helper")
118
+ The following element partials render nested elements through the
119
+ `nested_elements` association but not inside an `element_view_for`
120
+ block, so we could not rewrite them automatically:
121
+
122
+ #{list.call(manual)}
123
+
124
+ Please render them through the block helper (`render el.nested_elements`)
125
+ so nested rendering reuses the preloaded element set instead of
126
+ querying per parent.
127
+ TODO
128
+ end
129
+ end
130
+
131
+ NAVIGATION_PARTIALS = %w[
132
+ app/views/alchemy/admin/_main_navi.html.erb
133
+ app/views/alchemy/admin/partials/_main_navigation_entry.html.erb
134
+ app/views/alchemy/admin/partials/_sub_navigation.html.erb
135
+ ].freeze
136
+
137
+ # +Alchemy::Modules.register_module+ is deprecated in favor of
138
+ # +Alchemy.config.menu+. Calls with a static literal argument are
139
+ # rewritten, everything else has to be migrated by hand.
140
+ def upgrade_menu_config
141
+ rewritten = []
142
+ manual = []
143
+ rewriter = begin
144
+ require "prism"
145
+ RegisterModuleRewriter.new
146
+ rescue LoadError
147
+ nil
148
+ end
149
+
150
+ Dir.glob(Rails.root.join("config/initializers/**/*.rb")).sort.each do |file|
151
+ content = File.read(file)
152
+ next unless content.include?("register_module")
153
+
154
+ unless rewriter
155
+ manual << file
156
+ next
157
+ end
158
+
159
+ new_content, rewritten_count, manual_count = rewriter.rewrite(content)
160
+ if rewritten_count > 0
161
+ File.write(file, new_content)
162
+ rewritten << file
163
+ end
164
+ manual << file if manual_count > 0
165
+ end
166
+
167
+ overridden_partials = NAVIGATION_PARTIALS.map { |partial| Rails.root.join(partial).to_s }.select { |file| File.exist?(file) }
168
+
169
+ list = ->(files) { files.map { |file| " - #{Pathname.new(file).relative_path_from(Rails.root)}" }.join("\n") }
170
+
171
+ if rewritten.any?
172
+ todo(<<~TODO.strip, "`register_module` calls were rewritten to `Alchemy.config.menu.add`")
173
+ `Alchemy::Modules.register_module` is deprecated and will be removed in
174
+ Alchemy 9.0. We rewrote its calls in the following initializers to
175
+ `Alchemy.config.menu.add`:
176
+
177
+ #{list.call(rewritten)}
178
+
179
+ Please review these changes and commit them.
180
+ TODO
181
+ end
182
+
183
+ if manual.any?
184
+ todo(<<~TODO.strip, "The admin main navigation is now configured with `Alchemy.config.menu`")
185
+ `Alchemy::Modules.register_module` is deprecated and will be removed in
186
+ Alchemy 9.0. The following initializers call it in a way we could not
187
+ rewrite automatically:
188
+
189
+ #{list.call(manual)}
190
+
191
+ Please replace these `register_module` calls with
192
+ `Alchemy.config.menu.add` in `config/initializers/alchemy.rb`:
193
+
194
+ Alchemy.configure do |config|
195
+ config.menu.add(
196
+ name: "events",
197
+ title: "Events",
198
+ icon: "calendar",
199
+ url: -> { main_app.admin_events_path },
200
+ condition: -> { can?(:index, :admin_events) },
201
+ children: [
202
+ {
203
+ title: "Locations",
204
+ url: -> { main_app.admin_locations_path },
205
+ condition: -> { can?(:index, :admin_locations) }
206
+ }
207
+ ]
208
+ )
209
+ end
210
+
211
+ `url` and `condition` are evaluated in the view context. The keys of
212
+ the former `navigation` hash now live on the entry itself, its `name`
213
+ is now `title`, `sub_navigation` is now `children`, `icon-style` is now
214
+ `icon_style`, `data: {turbo: false}` is now `turbo: false`, and `params`
215
+ is no longer supported.
216
+
217
+ Call `config.menu.add` once at boot and not inside `to_prepare`, as it
218
+ does not skip entries that are already present like `register_module`
219
+ did.
220
+ TODO
221
+ end
222
+
223
+ if overridden_partials.any?
224
+ todo(<<~TODO.strip, "The admin main navigation partials have been replaced")
225
+ Your app overrides the following partials, which have been replaced by
226
+ the `Alchemy::Admin::MainNavigation` components and are no longer
227
+ rendered:
228
+
229
+ #{list.call(overridden_partials)}
230
+
231
+ Please adapt your changes.
232
+ TODO
233
+ end
234
+ end
235
+ end
236
+ end
237
+ end
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ class Upgrader
5
+ # Rewrites +Alchemy::Modules.register_module+ calls with a static literal
6
+ # argument into +Alchemy.config.menu.add+ calls.
7
+ #
8
+ # Calls inside a +to_prepare+ block are guarded by the entry's name, because
9
+ # +Alchemy.config.menu.add+ does not skip entries that are already present.
10
+ #
11
+ # Requires Prism, which only ships with Ruby >= 3.3.
12
+ class RegisterModuleRewriter
13
+ class NonLiteralError < StandardError; end
14
+
15
+ RECEIVERS = ["Alchemy::Modules", "::Alchemy::Modules"].freeze
16
+
17
+ KEYS = %w[
18
+ name title engine_name position icon icon_style image inline_image turbo
19
+ controller action nested_controllers nested_actions children nested
20
+ ].freeze
21
+
22
+ # @param [String] source
23
+ # @return [Array(String, Integer, Integer)]
24
+ # The new source, the number of rewritten and of untouched calls
25
+ def rewrite(source)
26
+ require "prism"
27
+
28
+ result = Prism.parse(source)
29
+ return [source, 0, 1] if result.errors.any?
30
+
31
+ replacements = []
32
+ manual = 0
33
+ each_register_module_call(result.value) do |call, in_to_prepare|
34
+ replacement = replacement_for(call, source, in_to_prepare:)
35
+ if replacement
36
+ replacements << [call.location.start_offset, call.location.end_offset, replacement]
37
+ else
38
+ manual += 1
39
+ end
40
+ end
41
+
42
+ new_source = source.b
43
+ replacements.sort_by(&:first).reverse_each do |start_offset, end_offset, replacement|
44
+ new_source[start_offset...end_offset] = replacement.b
45
+ end
46
+
47
+ [new_source.force_encoding(source.encoding), replacements.size, manual]
48
+ end
49
+
50
+ private
51
+
52
+ def each_register_module_call(node, in_to_prepare = false, &block)
53
+ if node.is_a?(Prism::CallNode) && node.name.to_s == "register_module"
54
+ yield node, in_to_prepare
55
+ end
56
+
57
+ node.compact_child_nodes.each do |child|
58
+ child_in_to_prepare = in_to_prepare ||
59
+ (node.is_a?(Prism::CallNode) && node.name.to_s == "to_prepare" && child.equal?(node.block))
60
+ each_register_module_call(child, child_in_to_prepare, &block)
61
+ end
62
+ end
63
+
64
+ def replacement_for(call, source, in_to_prepare:)
65
+ return unless RECEIVERS.include?(call.receiver&.slice)
66
+ return if call.block
67
+
68
+ arguments = call.arguments&.arguments || []
69
+ return unless arguments.size == 1
70
+ return unless arguments.first.is_a?(Prism::HashNode) || arguments.first.is_a?(Prism::KeywordHashNode)
71
+ return unless heredocs_inside?(arguments.first, call, source)
72
+
73
+ item = Alchemy::Configurations::MenuItem.from_legacy_hash(literal(arguments.first))
74
+ return if in_to_prepare && item.name.blank?
75
+
76
+ indent = " " * call.location.start_column
77
+ if in_to_prepare
78
+ <<~RUBY.chomp
79
+ unless Alchemy.config.menu.any? { |item| item.name == #{item.name.inspect} }
80
+ #{indent} #{menu_add(item, "#{indent} ")}
81
+ #{indent}end
82
+ RUBY
83
+ else
84
+ menu_add(item, indent)
85
+ end
86
+ rescue NonLiteralError, Alchemy::Configuration::ConfigurationError
87
+ nil
88
+ end
89
+
90
+ def menu_add(item, indent)
91
+ pairs = attributes(item).map do |key, value|
92
+ if key == "children" || key == "nested"
93
+ entries = value.map { |entry| "#{indent} #{inline_hash(entry)}" }
94
+ "#{indent} #{key}: [\n#{entries.join(",\n")}\n#{indent} ]"
95
+ else
96
+ "#{indent} #{key}: #{value.inspect}"
97
+ end
98
+ end
99
+ "Alchemy.config.menu.add(\n#{pairs.join(",\n")}\n#{indent})"
100
+ end
101
+
102
+ def inline_hash(item)
103
+ pairs = attributes(item).map do |key, value|
104
+ if key == "children" || key == "nested"
105
+ "#{key}: [#{value.map { |entry| inline_hash(entry) }.join(", ")}]"
106
+ else
107
+ "#{key}: #{value.inspect}"
108
+ end
109
+ end
110
+ "{#{pairs.join(", ")}}"
111
+ end
112
+
113
+ def attributes(item)
114
+ KEYS.filter_map do |key|
115
+ value = item.public_send(key)
116
+ value = value.to_a if value.respond_to?(:to_a) && !value.is_a?(String)
117
+ next if key == "turbo" && value != false
118
+ next if value.nil? || value == []
119
+
120
+ [key, value]
121
+ end
122
+ end
123
+
124
+ def literal(node)
125
+ case node
126
+ when Prism::StringNode, Prism::SymbolNode
127
+ node.unescaped
128
+ # Squiggly heredocs and adjacent string literals are made of static parts
129
+ when Prism::InterpolatedStringNode
130
+ node.compact_child_nodes.map do |part|
131
+ raise NonLiteralError unless part.is_a?(Prism::StringNode)
132
+
133
+ part.unescaped
134
+ end.join
135
+ when Prism::IntegerNode
136
+ Integer(node.slice)
137
+ when Prism::FloatNode
138
+ Float(node.slice)
139
+ when Prism::TrueNode
140
+ true
141
+ when Prism::FalseNode
142
+ false
143
+ when Prism::NilNode
144
+ nil
145
+ when Prism::ArrayNode
146
+ node.elements.map { |element| literal(element) }
147
+ when Prism::HashNode, Prism::KeywordHashNode
148
+ node.elements.to_h do |element|
149
+ raise NonLiteralError unless element.is_a?(Prism::AssocNode)
150
+ raise NonLiteralError unless element.key.is_a?(Prism::SymbolNode) || element.key.is_a?(Prism::StringNode)
151
+
152
+ [element.key.unescaped, literal(element.value)]
153
+ end
154
+ else
155
+ raise NonLiteralError
156
+ end
157
+ end
158
+
159
+ # A heredoc body starts on the line after its opening, so it is only part
160
+ # of the call if the call ends on a later line.
161
+ def heredocs_inside?(node, call, source)
162
+ heredocs = []
163
+ collect_heredocs(node, heredocs)
164
+ heredocs.all? do |heredoc|
165
+ source.byteslice(heredoc.location.end_offset...call.location.end_offset).include?("\n")
166
+ end
167
+ end
168
+
169
+ def collect_heredocs(node, heredocs)
170
+ if (node.is_a?(Prism::StringNode) || node.is_a?(Prism::InterpolatedStringNode)) && node.slice.start_with?("<<")
171
+ heredocs << node
172
+ end
173
+ node.compact_child_nodes.each { |child| collect_heredocs(child, heredocs) }
174
+ end
175
+ end
176
+ end
177
+ end
@@ -13,7 +13,8 @@ module Alchemy
13
13
 
14
14
  VERSION_MODULE_MAP = {
15
15
  "8.0" => "Alchemy::Upgrader::EightZero",
16
- "8.1" => "Alchemy::Upgrader::EightOne"
16
+ "8.1" => "Alchemy::Upgrader::EightOne",
17
+ "8.4" => "Alchemy::Upgrader::EightFour"
17
18
  }
18
19
 
19
20
  source_root Alchemy::Engine.root.join("lib/generators/alchemy/install")
@@ -3,7 +3,7 @@
3
3
  module Alchemy
4
4
  extend self
5
5
 
6
- VERSION = "8.3.11"
6
+ VERSION = "8.4.1"
7
7
 
8
8
  def version
9
9
  VERSION
data/lib/alchemy.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "alchemy/admin/preview_url"
4
3
  require "importmap-rails"
5
4
  require "alchemy/configurations/main"
6
5
  require "alchemy/config_missing"
data/lib/alchemy_cms.rb CHANGED
@@ -10,7 +10,6 @@ require "active_model_serializers"
10
10
  require "active_storage/engine"
11
11
  require "awesome_nested_set"
12
12
  require "cancan"
13
- require "dragonfly"
14
13
  require "gutentag"
15
14
  require "importmap-rails"
16
15
  require "kaminari"
@@ -21,31 +20,19 @@ require "userstamp"
21
20
  require "view_component"
22
21
 
23
22
  # Require globally used Alchemy mixins
24
- require_relative "alchemy/ability_helper"
25
- require_relative "alchemy/admin/locale"
26
- require_relative "alchemy/admin/timezone"
27
- require_relative "alchemy/admin/preview_time"
28
- require_relative "alchemy/admin/preview_url"
29
23
  require_relative "alchemy/auth_accessors"
30
24
  require_relative "alchemy/cache_digests/template_tracker"
31
- require_relative "alchemy/configuration_methods"
32
- require_relative "alchemy/controller_actions"
33
25
  require_relative "alchemy/deprecation"
34
- require_relative "alchemy/elements_finder"
35
26
  require_relative "alchemy/error_tracking"
36
27
  require_relative "alchemy/errors"
37
- require_relative "alchemy/filetypes"
38
28
  require_relative "alchemy/forms/builder"
39
- require_relative "alchemy/hints"
40
29
  require_relative "alchemy/i18n"
41
30
  require_relative "alchemy/logger"
42
31
  require_relative "alchemy/modules"
43
- require_relative "alchemy/name_conversions"
44
32
  require_relative "alchemy/on_page_layout"
45
33
  require_relative "alchemy/on_page_layout/callbacks_runner"
46
34
  require_relative "alchemy/paths"
47
35
  require_relative "alchemy/tinymce"
48
- require_relative "alchemy/taggable"
49
36
  require_relative "alchemy/version"
50
37
 
51
38
  # Finally require Alchemy itself
@@ -10,7 +10,7 @@
10
10
  <%- end -%>
11
11
  <%- end -%>
12
12
  <%- if @element['nestable_elements'].present? -%>
13
- <%%= render <%= @element_name %>.nested_elements.published %>
13
+ <%%= render el.nested_elements %>
14
14
  <%- end -%>
15
15
  <%% end -%>
16
16
  <%% end -%>
@@ -10,5 +10,5 @@
10
10
  <%- end -%>
11
11
  <%- end -%>
12
12
  <%- if @element['nestable_elements'].present? -%>
13
- = render <%= @element_name -%>.nested_elements.published
13
+ = render el.nested_elements
14
14
  <%- end -%>
@@ -10,5 +10,5 @@
10
10
  <%- end -%>
11
11
  <%- end -%>
12
12
  <%- if @element['nestable_elements'].present? -%>
13
- = render <%= @element_name -%>.nested_elements.published
13
+ = render el.nested_elements
14
14
  <%- end -%>
@@ -88,16 +88,9 @@ module Alchemy
88
88
  copy_file "alchemy.en.yml", app_config_path.join("locales", "alchemy.en.yml")
89
89
  end
90
90
 
91
- def copy_dragonfly_config
92
- template(
93
- "#{__dir__}/templates/dragonfly.rb.tt",
94
- app_config_path.join("initializers", "dragonfly.rb"),
95
- skip: options[:auto_accept]
96
- )
97
- end
98
-
99
91
  def install_gutentag_migrations
100
- rake "gutentag:install:migrations"
92
+ rake "gutentag:install:migrations", abort_on_failure: true
93
+ generate "gutentag:migration_versions", abort_on_failure: true
101
94
  end
102
95
 
103
96
  def setup_database
@@ -144,10 +137,6 @@ module Alchemy
144
137
  puts " #{something}"
145
138
  end
146
139
 
147
- def gem_config_path
148
- @_config_path ||= File.expand_path("../../../../config/alchemy", __dir__)
149
- end
150
-
151
140
  def app_config_path
152
141
  @_app_config_path ||= app_root.join("config")
153
142
  end
@@ -28,10 +28,12 @@ Alchemy.configure do |config|
28
28
  #
29
29
  # show_root [Boolean] # Show language root page in sitemap?
30
30
  # show_flag [Boolean] # Enables the Checkbox in Page#update overlay. So your customer can set the visibility of pages in the sitemap.
31
+ # max_age [Integer] # How long the sitemap is cached, in seconds. Set to 0 to disable sitemap caching.
31
32
  #
32
33
  # config.sitemap.tap do |sitemap|
33
34
  # sitemap.show_root = <%= @default_config.sitemap.show_root.inspect %>
34
35
  # sitemap.show_flag = <%= @default_config.sitemap.show_flag.inspect %>
36
+ # sitemap.max_age = <%= @default_config.sitemap.max_age.inspect %>
35
37
  # end
36
38
 
37
39
  # === Default items per page in admin views
@@ -164,27 +166,29 @@ Alchemy.configure do |config|
164
166
  # upload_limit [Integer] # Set an amount of files upload limit of files which can be uploaded at once. Set 0 for unlimited.
165
167
  # file_size_limit* [Integer] # Set a file size limit in mega bytes for a per file limit.
166
168
  #
167
- # *) Allow filetypes to upload. Pass * to allow all kind of files.
169
+ # *) Allow filetypes to upload. Passing * disables file type validation and
170
+ # allows every kind of file, including executables.
168
171
  #
169
172
  # config.uploader.tap do |uploader|
170
173
  # uploader.upload_limit = <%= @default_config.uploader.upload_limit.inspect %>
171
174
  # uploader.file_size_limit = <%= @default_config.uploader.file_size_limit.inspect %>
172
175
  # uploader.allowed_filetypes.tap do |file_types|
173
- # file_types.alchemy_attachments = <%= @default_config.uploader.allowed_filetypes.alchemy_attachments.map(&:to_s) %>
176
+ # file_types.alchemy_attachments = %w[
177
+ <% @default_config.uploader.allowed_filetypes.alchemy_attachments.each_slice(8) do |extensions| -%>
178
+ # <%= extensions.join(" ") %>
179
+ <% end -%>
180
+ # ]
174
181
  # file_types.alchemy_pictures = <%= @default_config.uploader.allowed_filetypes.alchemy_pictures.map(&:to_s) %>
175
182
  # end
176
183
  # end
177
184
 
178
185
  # === Link Target Options
179
186
  #
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.
187
+ # Values for the link target selectbox inside the link dialog.
182
188
  #
183
- # == Example:
184
- #
185
- # Open all links set to overlay inside an jQuery UI Dialog Window.
186
- #
187
- # jQuery(a[data-link-target="overlay"]).dialog();
189
+ # Each value is stored with a leading underscore, so that it can be used as
190
+ # the link's `target` attribute right away: `blank` is stored as `_blank` and
191
+ # rendered as `target="_blank"`.
188
192
  #
189
193
  # config.link_target_options = <%= @default_config.link_target_options.map(&:to_s) %>
190
194
 
@@ -252,6 +256,17 @@ Alchemy.configure do |config|
252
256
  # Additional stylesheets to be included in the Alchemy admin UI
253
257
  # config.admin_stylesheets.add("my_app/admin_extension")
254
258
 
259
+ # === Admin Content Security Policy
260
+ #
261
+ # The class building the Content Security Policy Alchemy sends with its own
262
+ # admin responses. No policy is sent by default, until Alchemy 9.0.
263
+ #
264
+ # Alchemy never replaces a policy your application has configured itself.
265
+ # Your asset host, your admin stylesheets and modules pinned to a CDN are
266
+ # allowed automatically. Subclass it to allow anything else.
267
+ #
268
+ # config.admin_content_security_policy = "Alchemy::Admin::ContentSecurityPolicy"
269
+
255
270
  # Define page publish targets
256
271
  #
257
272
  # A publish target is a ActiveJob that gets performed
@@ -14,6 +14,7 @@ module Alchemy
14
14
  @class_name = @module_name.singularize.classify
15
15
  @controller_name = @module_name.tableize
16
16
  @controller_class = @controller_name.classify.pluralize
17
+ @index_route = (@controller_name.singularize == @controller_name) ? "#{@controller_name}_index" : @controller_name
17
18
  end
18
19
 
19
20
  def make_route
@@ -2,7 +2,7 @@ class <%= @controller_class %>Ability
2
2
  include CanCan::Ability
3
3
 
4
4
  def initialize(user)
5
- if user.present? && user.is_admin?
5
+ if user&.is_alchemy_admin?
6
6
  can :manage, <%= @class_name %>
7
7
  can :manage, :admin_<%= @controller_name %>
8
8
  end
@@ -1,19 +1,17 @@
1
- Rails.application.config.to_prepare do
2
- Alchemy::Modules.register_module({
3
- name: '<%= @module_name %>',
4
- order: 1,
5
- navigation: {
6
- name: 'modules.<%= @module_name %>',
7
- controller: '/admin/<%= @module_name %>',
8
- action: 'index',
9
- image: 'alchemy/<%= @module_name %>_module.png',
10
- sub_navigation: [{
11
- name: 'modules.<%= @module_name %>',
12
- controller: '/admin/<%= @module_name %>',
13
- action: 'index'
14
- }]
15
- }
16
- })
17
-
18
- Alchemy.register_ability(<%= @controller_class %>Ability)
1
+ Alchemy.configure do |config|
2
+ config.menu.add(
3
+ name: "<%= @module_name %>",
4
+ title: "modules.<%= @module_name %>",
5
+ image: "alchemy/<%= @module_name %>_module.png",
6
+ url: -> { main_app.admin_<%= @index_route %>_path },
7
+ condition: -> { can?(:index, :admin_<%= @controller_name %>) },
8
+ children: [
9
+ {
10
+ title: "modules.<%= @module_name %>",
11
+ url: -> { main_app.admin_<%= @index_route %>_path },
12
+ condition: -> { can?(:index, :admin_<%= @controller_name %>) }
13
+ }
14
+ ]
15
+ )
16
+ config.abilities.add("<%= @controller_class %>Ability")
19
17
  end