alchemy_cms 8.3.11 → 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 (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
@@ -14,6 +14,7 @@ module Alchemy
14
14
  render_hint_for(ingredient, size: "1x", fixed_width: false)
15
15
  ])
16
16
  }
17
+ concat validation_error
17
18
  end
18
19
  end
19
20
  end
@@ -4,18 +4,21 @@ module Alchemy
4
4
  module Ingredients
5
5
  class ColorEditor < BaseEditor
6
6
  def input_field
7
- content_tag("alchemy-color-select") do
8
- if colors.none?
7
+ if colors.none?
8
+ content_tag("alchemy-color-input", class: "color-select") do
9
9
  concat text_field_tag(
10
10
  form_field_name,
11
11
  value,
12
12
  disabled: !editable?
13
13
  )
14
14
  concat color_field_tag(nil, value, disabled: disabled?)
15
- else
15
+ end
16
+ else
17
+ content_tag("div", class: "color-select") do
16
18
  concat select_tag(
17
19
  form_field_name,
18
20
  color_options.join.html_safe,
21
+ is: "alchemy-color-select",
19
22
  disabled: !editable?
20
23
  )
21
24
  concat color_field_tag(form_field_name, value, disabled: disabled?)
@@ -17,7 +17,7 @@ module Alchemy
17
17
  if date_format == "rfc822"
18
18
  datetime.to_fs(:rfc822)
19
19
  else
20
- ::I18n.l(datetime, format: date_format)
20
+ Alchemy.l(datetime, format: date_format)
21
21
  end.html_safe
22
22
  end
23
23
  end
@@ -96,10 +96,8 @@ module Alchemy
96
96
  end
97
97
 
98
98
  def srcset_options
99
- srcset.map do |size|
100
- url = ingredient.picture_url(size: size)
101
- width, height = size.split("x")
102
- width.present? ? "#{url} #{width}w" : "#{url} #{height}h"
99
+ ingredient.picture_srcset_options(srcset).map do |source|
100
+ "#{source[:url]} #{source[:desc]}"
103
101
  end
104
102
  end
105
103
 
@@ -0,0 +1,157 @@
1
+ <alchemy-menubar>
2
+ <template>
3
+ <style>
4
+ .menubar {
5
+ --icon-size: 24px;
6
+ --panel-width: 525px;
7
+ --border-radius: 3px;
8
+ --left-offset: 0px;
9
+ display: flex;
10
+ position: fixed;
11
+ top: 0;
12
+ left: calc(-1 * (var(--panel-width) - var(--left-offset)));
13
+ z-index: 10000;
14
+ background-color: #214166cc;
15
+ backdrop-filter: blur(3px);
16
+ transition: left 0.25s cubic-bezier(0.23, 1, 0.32, 1), background-color .2s linear , backdrop-filter .2s linear;
17
+ box-shadow: 0 0 0 1px #ffffff80;
18
+ box-sizing: border-box;
19
+ border-bottom-right-radius: var(--border-radius);
20
+ padding: 12px 16px 12px 12px;
21
+ gap: 12px;
22
+ justify-content: space-between;
23
+ align-items: center;
24
+ flex-wrap: nowrap;
25
+ overflow: hidden;
26
+ font-family: "Open Sans", "Lucida Grande", "Lucida Sans Unicode",
27
+ "Lucida Sans", Verdana, Tahoma, sans-serif;
28
+ font-size: 13px;
29
+ line-height: normal;
30
+
31
+ &:hover {
32
+ background-color: #214166;
33
+ }
34
+ }
35
+
36
+ .menubar * {
37
+ box-sizing: border-box;
38
+ margin: 0;
39
+ padding: 0;
40
+ }
41
+
42
+ .menubar:hover,
43
+ .menubar:focus-within,
44
+ .menubar.is-open {
45
+ left: 0;
46
+ }
47
+
48
+ .menubar > svg {
49
+ width: var(--icon-size);
50
+ height: var(--icon-size);
51
+ flex-shrink: 0;
52
+ margin-left: 4px;
53
+ }
54
+
55
+ .menubar .button {
56
+ display: inline-flex;
57
+ align-items: center;
58
+ justify-content: center;
59
+ gap: 8px;
60
+ font-size: inherit;
61
+ font-weight: 700;
62
+ cursor: pointer;
63
+ border-radius: var(--border-radius);
64
+ background-color: transparent;
65
+ border: 1px solid rgba(255, 255, 255, 0.5);
66
+ color: #fff;
67
+ margin: 0;
68
+ padding: 0.75em 1.5em;
69
+ appearance: none;
70
+ transition: all 250ms;
71
+ -webkit-font-smoothing: antialiased;
72
+ -moz-osx-font-smoothing: grayscale;
73
+ text-decoration: none;
74
+ white-space: nowrap;
75
+ }
76
+
77
+ .menubar .button:hover {
78
+ text-decoration: none;
79
+ background-color: rgba(0, 0, 0, 0.25);
80
+ border-color: rgba(255, 255, 255, 0.75)
81
+ }
82
+
83
+ .menubar .button:active {
84
+ box-shadow: inset 0px 1px 1px -1px #333;
85
+ }
86
+
87
+ .menubar .button:focus {
88
+ outline: solid 2px #eca96e;
89
+ outline-offset: 2px;
90
+ }
91
+ </style>
92
+
93
+ <div class="menubar" data-turbo="false">
94
+ <%= link_to alchemy.admin_dashboard_url, class: "button" do %>
95
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
96
+ <path d="M7.82843 10.9999H20V12.9999H7.82843L13.1924 18.3638L11.7782 19.778L4 11.9999L11.7782 4.22168L13.1924 5.63589L7.82843 10.9999Z"></path>
97
+ </svg>
98
+ <%= Alchemy.t(:to_alchemy) %>
99
+ <% end %>
100
+ <%= link_to alchemy.edit_admin_page_url(page), class: "button" do %>
101
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
102
+ <path d="M6.41421 15.89L16.5563 5.74785L15.1421 4.33363L5 14.4758V15.89H6.41421ZM7.24264 17.89H3V13.6473L14.435 2.21231C14.8256 1.82179 15.4587 1.82179 15.8492 2.21231L18.6777 5.04074C19.0682 5.43126 19.0682 6.06443 18.6777 6.45495L7.24264 17.89ZM3 19.89H21V21.89H3V19.89Z"></path>
103
+ </svg>
104
+ <%= Alchemy.t(:edit_page) %>
105
+ <% end %>
106
+ <%= form_tag Alchemy.config.logout_path, method: Alchemy.config.logout_method do %>
107
+ <%= button_tag class: "button" do %>
108
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
109
+ <path d="M5 22C4.44772 22 4 21.5523 4 21V3C4 2.44772 4.44772 2 5 2H19C19.5523 2 20 2.44772 20 3V6H18V4H6V20H18V18H20V21C20 21.5523 19.5523 22 19 22H5ZM18 16V13H11V11H18V8L23 12L18 16Z"></path>
110
+ </svg>
111
+ <%= Alchemy.t(:logout) %>
112
+ <% end %>
113
+ <% end %>
114
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30">
115
+ <path fill="#fff" d="M15.7 7.9L9.9 2.2 2.1 4.3 0 12.1l5.7 5.8 7.8-2.1 2.2-7.9zm-5.3 10.3l-1.2 4.4 3.2 3.2 4.4-1.2 1.2-4.4-3.2-3.2-4.4 1.2zM23.5 7.3L17.2 9l-1.7 6.2 4.5 4.6 6.2-1.7 1.7-6.2-4.4-4.6z" />
116
+ </svg>
117
+ </div>
118
+ </template>
119
+
120
+ <%= tag.script(type: "module", data: {turbo_eval: false}, nonce: helpers.content_security_policy_nonce) do %>
121
+ class Menubar extends HTMLElement {
122
+ connectedCallback() {
123
+ const template = this.querySelector("template")
124
+ const attachedShadowRoot = this.attachShadow({ mode: "open" })
125
+ attachedShadowRoot.appendChild(template.content.cloneNode(true))
126
+ const width = this.bar.offsetWidth
127
+ this.bar.style = `--panel-width: ${width}px; --left-offset: calc(var(--icon-size) + 32px);`
128
+ this.bar.addEventListener("touchstart", this, { passive: false })
129
+ document.addEventListener("touchstart", this, { passive: true })
130
+ }
131
+
132
+ handleEvent(event) {
133
+ // click on menubar
134
+ if (event.currentTarget === this.bar) {
135
+ if (!this.bar.classList.contains("is-open")) {
136
+ event.preventDefault()
137
+ this.bar.classList.add("is-open")
138
+ }
139
+ // click outside of menubar
140
+ } else if (!event.composedPath().includes(this.bar)) {
141
+ this.bar.classList.remove("is-open")
142
+ }
143
+ }
144
+
145
+ disconnectedCallback() {
146
+ this.bar.removeEventListener("touchstart", this)
147
+ document.removeEventListener("touchstart", this)
148
+ }
149
+
150
+ get bar() {
151
+ return this.shadowRoot.querySelector(".menubar")
152
+ }
153
+ }
154
+
155
+ customElements.define("alchemy-menubar", Menubar)
156
+ <% end %>
157
+ </alchemy-menubar>
@@ -0,0 +1,15 @@
1
+ module Alchemy
2
+ class Menubar < ViewComponent::Base
3
+ attr_reader :page
4
+
5
+ delegate :alchemy, :can?, to: :helpers
6
+
7
+ def initialize(page: Alchemy::Current.page)
8
+ @page = page
9
+ end
10
+
11
+ def render?
12
+ page && !Alchemy::Current.preview_page? && can?(:edit_content, page)
13
+ end
14
+ end
15
+ end
@@ -6,16 +6,19 @@ module Alchemy
6
6
  include UploaderResponses
7
7
  include ArchiveOverlay
8
8
 
9
- add_alchemy_filter :by_file_type, type: :select, options: ->(_query, params) do
10
- case params&.to_h
11
- in {except:}
12
- Attachment.file_types - Attachment.file_types(from_extensions: except)
13
- in {only:}
14
- Attachment.file_types(from_extensions: only)
15
- else
16
- Attachment.file_types
9
+ add_alchemy_filter :by_file_type, type: :select,
10
+ multiple: ->(params) { params[:only].presence&.many? || params[:except].present? },
11
+ include_blank: ->(params) { params[:only].blank? && params[:except].blank? },
12
+ options: ->(_query, params) do
13
+ case params&.to_h
14
+ in {except:}
15
+ Attachment.file_types - Attachment.file_types(from_extensions: except)
16
+ in {only:}
17
+ Attachment.file_types(from_extensions: only)
18
+ else
19
+ Attachment.file_types
20
+ end
17
21
  end
18
- end
19
22
 
20
23
  add_alchemy_filter :recent, type: :checkbox
21
24
  add_alchemy_filter :last_upload, type: :checkbox
@@ -2,15 +2,19 @@
2
2
 
3
3
  module Alchemy
4
4
  module Admin
5
+ # Turbo Frame id wrapping admin dialog content. A form inside submits into
6
+ # this frame, so its request carries the id and the server can tell a dialog
7
+ # submission apart from the same form rendered on its own page.
8
+ DIALOG_FRAME_ID = "alchemy_dialog_frame"
9
+
5
10
  class BaseController < Alchemy::BaseController
6
11
  include Userstamp
7
12
  include Locale
8
13
  include Timezone
14
+ include CspProtection
9
15
 
10
16
  before_action :load_locked_pages
11
17
 
12
- helper_method :is_admin?
13
-
14
18
  check_authorization
15
19
 
16
20
  rescue_from Exception do |exception|
@@ -27,11 +31,15 @@ module Alchemy
27
31
 
28
32
  def leave
29
33
  authorize! :leave, :alchemy_admin
30
- render template: "/alchemy/admin/leave", layout: !request.xhr?
34
+ render template: "/alchemy/admin/leave", layout: !turbo_frame_request?
31
35
  end
32
36
 
33
37
  private
34
38
 
39
+ # Controllers of the host application can inherit from this class, and
40
+ # their views would not be ours to make assumptions about.
41
+ def apply_content_security_policy? = controller_path.start_with?("alchemy/")
42
+
35
43
  def safe_redirect_path(path = params[:redirect_to], fallback: admin_path)
36
44
  if is_safe_redirect_path?(path)
37
45
  path
@@ -53,9 +61,19 @@ module Alchemy
53
61
  URI(referer).path
54
62
  end
55
63
 
56
- # Disable layout rendering for xhr requests.
64
+ # Disable layout rendering for Turbo Frame requests.
65
+ #
66
+ # Content requested by the dialog's Turbo Frame is wrapped in that frame,
67
+ # so Turbo can render it. Only the dialog's own request carries this
68
+ # frame id, so the same action rendered as a full page stays unwrapped.
57
69
  def set_layout
58
- (request.xhr? || turbo_frame_request?) ? false : "alchemy/admin"
70
+ if turbo_frame_request_id == DIALOG_FRAME_ID
71
+ "alchemy/admin/dialog"
72
+ elsif turbo_frame_request?
73
+ false
74
+ else
75
+ "alchemy/admin"
76
+ end
59
77
  end
60
78
 
61
79
  # Handles exceptions
@@ -72,8 +90,6 @@ module Alchemy
72
90
  @notice = error.message[0..255]
73
91
  if request.format.json?
74
92
  render json: {message: @notice}, status: 500
75
- elsif request.xhr?
76
- render action: "error_notice"
77
93
  else
78
94
  @trace = error.backtrace[0..50]
79
95
  render "500", status: 500
@@ -92,13 +108,6 @@ module Alchemy
92
108
  end
93
109
  end
94
110
 
95
- # Returns true if the current_alchemy_user (The logged-in Alchemy User) has the admin role.
96
- def is_admin?
97
- return false if !current_alchemy_user
98
-
99
- current_alchemy_user.admin?
100
- end
101
-
102
111
  # Displays errors in a #errors div if any errors are present on the object.
103
112
  # Or redirects to the given redirect url.
104
113
  #
@@ -122,16 +131,20 @@ module Alchemy
122
131
  #
123
132
  def do_redirect_to(url_or_path)
124
133
  redirect_path = safe_redirect_path(url_or_path)
134
+ # A dialog form submits into the dialog frame, so a redirect would be
135
+ # followed inside it and render the target page within the dialog. Let
136
+ # the dialog close itself and visit the destination instead.
137
+ if turbo_frame_request_id == DIALOG_FRAME_ID
138
+ return render(turbo_stream: helpers.turbo_stream_action_tag(
139
+ :dialog_visit, url: redirect_path
140
+ ))
141
+ end
125
142
  respond_to do |format|
126
- format.js do
127
- @redirect_url = redirect_path
128
- render :redirect
129
- end
130
143
  format.turbo_stream do
131
- redirect_to(redirect_path, allow_other_host: false)
144
+ redirect_to(redirect_path, allow_other_host: false, status: :see_other)
132
145
  end
133
146
  format.html do
134
- redirect_to(redirect_path, allow_other_host: false)
147
+ redirect_to(redirect_path, allow_other_host: false, status: :see_other)
135
148
  end
136
149
  end
137
150
  end
@@ -150,7 +163,7 @@ module Alchemy
150
163
  end
151
164
 
152
165
  def load_locked_pages
153
- @locked_pages = Page.locked_by(current_alchemy_user).order(:locked_at)
166
+ @locked_pages ||= current_alchemy_user&.locked_pages
154
167
  end
155
168
 
156
169
  # Returns the current site for admin controllers.
@@ -15,6 +15,10 @@ module Alchemy
15
15
  Requesting `admin/dashboard/info` is deprecated. Please render Alchemy::Admin::Dashboard::Widgets::SystemInfo instead.
16
16
  WARN
17
17
  end
18
+
19
+ def license
20
+ @license = Alchemy::Engine.root.join("LICENSE").read
21
+ end
18
22
  end
19
23
  end
20
24
  end
@@ -59,31 +59,12 @@ module Alchemy
59
59
  # Updates the element and all ingredients in the element.
60
60
  #
61
61
  def update
62
- if @element.update(element_params)
63
- render json: {
64
- notice: Alchemy.t(:element_saved),
65
- previewText: Rails::Html::SafeListSanitizer.new.sanitize(@element.preview_text),
66
- ingredientAnchors: @element.ingredients.filter_map do |ingredient|
67
- if ingredient.settings[:anchor]
68
- {
69
- ingredientId: ingredient.id,
70
- active: ingredient.dom_id.present?
71
- }
72
- end
73
- end
74
- }.merge(pagePublicationData(@element.page))
62
+ @element_validated = @element.update(element_params)
63
+ if @element_validated
64
+ render :update
75
65
  else
76
66
  @warning = Alchemy.t("Validation failed")
77
- render json: {
78
- warning: @warning,
79
- errorMessage: Alchemy.t(:ingredient_validations_headline),
80
- ingredientsWithErrors: @element.ingredients_with_errors.map do |ingredient|
81
- {
82
- id: ingredient.id,
83
- errorMessage: ingredient.errors.messages[:value].to_sentence
84
- }
85
- end
86
- }, status: 422
67
+ render :update, status: 422
87
68
  end
88
69
  end
89
70
 
@@ -51,7 +51,7 @@ module Alchemy
51
51
  def load_current_site
52
52
  @current_site = Alchemy::Current.site
53
53
  if @current_site.nil?
54
- flash[:warning] = Alchemy.t("Please create a site first.")
54
+ flash[:info] = Alchemy.t("Please create a site first.")
55
55
  do_redirect_to admin_sites_path
56
56
  end
57
57
  end
@@ -18,7 +18,7 @@ module Alchemy
18
18
  end
19
19
 
20
20
  def create
21
- if turbo_frame_request?
21
+ if turbo_frame_request_id == "page_nodes"
22
22
  @page = Alchemy::Page.find(resource_params[:page_id])
23
23
  @node = @page.nodes.build(resource_params)
24
24
  if @node.valid?
@@ -85,7 +85,7 @@ module Alchemy
85
85
  end
86
86
 
87
87
  def info
88
- render layout: !request.xhr?
88
+ render layout: !turbo_frame_request?
89
89
  end
90
90
 
91
91
  def new
@@ -27,9 +27,12 @@ module Alchemy
27
27
  # that would otherwise fire for every picture the view renders.
28
28
  before_action :load_deletable_picture_ids, only: [:index, :update]
29
29
 
30
- add_alchemy_filter :by_file_format, type: :select, options: ->(query) do
31
- Alchemy::Picture.file_formats(query.result)
32
- end
30
+ add_alchemy_filter :by_file_format, type: :select,
31
+ multiple: ->(params) { params[:only].presence&.many? || params[:except].present? },
32
+ include_blank: ->(params) { params[:only].blank? && params[:except].blank? },
33
+ options: ->(query) do
34
+ Alchemy::Picture.file_formats(query.result)
35
+ end
33
36
  add_alchemy_filter :recent, type: :checkbox
34
37
  add_alchemy_filter :last_upload, type: :checkbox
35
38
  add_alchemy_filter :without_tag, type: :checkbox
@@ -100,12 +100,12 @@ module Alchemy
100
100
  default: Alchemy.t("Successfully #{verb}"))
101
101
  end
102
102
 
103
- def is_alchemy_module?
104
- !alchemy_module.nil? && !alchemy_module["engine_name"].nil?
105
- end
106
-
107
103
  def alchemy_module
108
- @alchemy_module ||= module_definition_for(controller: controller_path, action: "index")
104
+ @alchemy_module ||= {
105
+ "engine_name" => Rails::Engine.subclasses.detect { |engine|
106
+ engine.routes.equal?(self.class._routes)
107
+ }&.engine_name
108
+ }
109
109
  end
110
110
 
111
111
  def load_resource
@@ -6,7 +6,7 @@ module Alchemy
6
6
  def create
7
7
  @site = Alchemy::Site.new(resource_params)
8
8
  if @site.save
9
- flash[:notice] = Alchemy.t("Please create a default language for this site.")
9
+ flash[:info] = Alchemy.t("Please create a default language for this site.")
10
10
  do_redirect_to alchemy.admin_languages_path(site_id: @site)
11
11
  else
12
12
  render :new
@@ -53,9 +53,7 @@ module Alchemy
53
53
  #{current_alchemy_user.inspect}
54
54
  WARN
55
55
  end
56
- if request.format.json?
57
- render json: {message: Alchemy.t("You are not authorized")}, status: 401
58
- elsif current_alchemy_user
56
+ if current_alchemy_user
59
57
  handle_redirect_for_user
60
58
  else
61
59
  handle_redirect_for_guest
@@ -63,6 +61,10 @@ module Alchemy
63
61
  end
64
62
 
65
63
  def handle_redirect_for_user
64
+ if api_request?
65
+ return render json: {message: Alchemy.t("You are not authorized")}, status: :forbidden
66
+ end
67
+
66
68
  flash[:warning] = Alchemy.t("You are not authorized")
67
69
  if can?(:index, :alchemy_admin_dashboard)
68
70
  redirect_or_render_notice
@@ -72,17 +74,7 @@ module Alchemy
72
74
  end
73
75
 
74
76
  def redirect_or_render_notice
75
- if request.xhr?
76
- respond_to do |format|
77
- format.js do
78
- render plain: flash.discard(:warning), status: 403
79
- end
80
- format.html do
81
- render partial: "alchemy/admin/partials/flash",
82
- locals: {message: flash[:warning], flash_type: "warning"}
83
- end
84
- end
85
- elsif turbo_frame_request?
77
+ if turbo_frame_request?
86
78
  render "403", status: 403
87
79
  else
88
80
  redirect_to(alchemy.admin_dashboard_path)
@@ -91,14 +83,30 @@ module Alchemy
91
83
 
92
84
  def handle_redirect_for_guest
93
85
  flash[:info] = Alchemy.t("Please log in")
94
- if request.xhr?
95
- render :permission_denied
86
+ if api_request?
87
+ render json: {
88
+ message: Alchemy.t("Please log in"),
89
+ redirect_url: Alchemy.config.login_path
90
+ }, status: :unauthorized
91
+ elsif turbo_frame_request?
92
+ # Redirecting would be followed inside the frame and render the login
93
+ # page in the dialog. Turbo runs a stream action from any response,
94
+ # whatever status or Accept header, so it can break out of the frame.
95
+ render turbo_stream: helpers.turbo_stream_action_tag(
96
+ :dialog_visit, url: Alchemy.config.login_path
97
+ ), status: :unauthorized
96
98
  else
97
99
  store_location
98
100
  redirect_to Alchemy.config.login_path
99
101
  end
100
102
  end
101
103
 
104
+ # Requests from our own admin JavaScript, that expect a machine readable
105
+ # response instead of a page to render.
106
+ def api_request?
107
+ request.format.json? || request.xhr?
108
+ end
109
+
102
110
  # Logs the current exception to the error log.
103
111
  def exception_logger(error)
104
112
  Logger.error("\n#{error.class} #{error.message} in #{error.backtrace.first}")
@@ -73,11 +73,16 @@ module Alchemy
73
73
  end
74
74
 
75
75
  # Renders a search engine compatible xml sitemap.
76
+ #
77
+ # @deprecated Point the +/sitemap.xml+ route at {Alchemy::SitemapController} instead.
78
+ #
76
79
  def sitemap
80
+ Alchemy::Deprecation.warn(
81
+ "Alchemy::PagesController#sitemap is deprecated and will be removed in Alchemy 9.0. " \
82
+ "Route /sitemap.xml to Alchemy::SitemapController#show instead."
83
+ )
77
84
  @pages = Page.sitemap
78
- respond_to do |format|
79
- format.xml { render layout: "alchemy/sitemap" }
80
- end
85
+ render layout: "alchemy/sitemap"
81
86
  end
82
87
 
83
88
  private
@@ -190,9 +195,14 @@ module Alchemy
190
195
 
191
196
  def set_expiration_headers
192
197
  if must_not_cache?
198
+ # no-store, not no-cache: no-cache still lets caches store the response and
199
+ # reuse it after a 304, which serves stale content (e.g. flash messages) and
200
+ # skips re-rendering elements that opted out of page caching.
201
+ no_store
202
+ elsif page_cache_control.no_cache?
193
203
  expires_now
194
204
  else
195
- expires_in @page.expiration_time, {public: !@page.restricted}.merge(caching_options)
205
+ expires_in @page.expiration_time, {public: page_cache_control.public?}.merge(caching_options)
196
206
  end
197
207
  end
198
208
 
@@ -209,7 +219,13 @@ module Alchemy
209
219
  end
210
220
 
211
221
  def signup_required?
212
- if Alchemy.config.user_class.respond_to?(:admins)
222
+ if Alchemy.config.user_class.respond_to?(:alchemy_admins)
223
+ Alchemy.config.user_class.alchemy_admins.empty?
224
+ elsif Alchemy.config.user_class.respond_to?(:admins)
225
+ Alchemy::Deprecation.warn(
226
+ "Using `#{Alchemy.config.user_class}.admins` is deprecated. " \
227
+ "Include `Alchemy::UserMethods` into your user class to provide `alchemy_admins` instead."
228
+ )
213
229
  Alchemy.config.user_class.admins.empty?
214
230
  end
215
231
  end
@@ -240,14 +256,19 @@ module Alchemy
240
256
  def render_fresh_page?
241
257
  must_not_cache? || stale?(
242
258
  etag: page_etag,
243
- public: !@page.restricted,
259
+ public: page_cache_control.public?,
244
260
  template: "pages/show"
245
261
  )
246
262
  end
247
263
 
248
- # don't cache pages if we have flash message to display or the page has caching disabled
264
+ # don't cache pages if app caching is off, a flash message is present, or the
265
+ # page's effective Cache-Control forbids storing the response.
249
266
  def must_not_cache?
250
- !caching_enabled? || !@page.cache_page? || flash.present?
267
+ !caching_enabled? || flash.present? || page_cache_control.no_store?
268
+ end
269
+
270
+ def page_cache_control
271
+ @page.cache_control
251
272
  end
252
273
 
253
274
  def caching_enabled?