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,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ # Renders a search engine compatible XML sitemap.
5
+ #
6
+ # The sitemap contains absolute urls, so it has to follow the site's primary
7
+ # host redirect just like a page request does.
8
+ #
9
+ class SitemapController < Alchemy::BaseController
10
+ include SiteRedirects
11
+
12
+ # Still the pages template, because apps override it at that path.
13
+ #
14
+ # TODO: Move to alchemy/sitemap/show in Alchemy 9.0, together with the
15
+ # deprecated PagesController#sitemap.
16
+ TEMPLATE = "alchemy/pages/sitemap"
17
+
18
+ def show
19
+ if sitemap_caching_enabled?
20
+ expires_in sitemap_max_age, public: true
21
+
22
+ # The etag is checked before the pages are loaded, so a revalidating
23
+ # client only costs us the cache key queries, not a full render.
24
+ return unless stale?(etag: sitemap_cache_key, public: true)
25
+
26
+ @sitemap_cache_key = sitemap_cache_key
27
+ @sitemap_max_age = sitemap_max_age
28
+ end
29
+
30
+ # Left unloaded on purpose. The template wraps the urls in a fragment
31
+ # cache, so a cache hit never runs this query.
32
+ @pages = Page.sitemap
33
+
34
+ render template: TEMPLATE, layout: "alchemy/sitemap"
35
+ end
36
+
37
+ private
38
+
39
+ def sitemap_cache_key
40
+ @_sitemap_cache_key ||= Page::SitemapCacheKey.new(
41
+ site: Current.site,
42
+ base_url: request.base_url,
43
+ max_age: sitemap_max_age
44
+ ).call
45
+ end
46
+
47
+ def sitemap_max_age
48
+ Alchemy.config.sitemap.max_age
49
+ end
50
+
51
+ # The sitemap has its own cache setting and does not follow the page cache.
52
+ # A max_age of zero (or less) disables it, because it would leave the time
53
+ # bucket undefined.
54
+ def sitemap_caching_enabled?
55
+ perform_caching && sitemap_max_age.to_i.positive?
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ module Admin
5
+ # Applies Alchemy's own Content Security Policy to the including controller.
6
+ #
7
+ # Include it into any controller that renders the Alchemy admin but does not
8
+ # inherit from +Alchemy::Admin::BaseController+, so that its responses carry
9
+ # the same policy as the rest of the admin:
10
+ #
11
+ # class Alchemy::Admin::PasswordsController < Devise::PasswordsController
12
+ # include Alchemy::Admin::CspProtection
13
+ # end
14
+ #
15
+ # Including it is opting in, so the policy applies to every action. Override
16
+ # +apply_content_security_policy?+ to narrow that down.
17
+ #
18
+ # The policy itself comes from +Alchemy.config.admin_content_security_policy+
19
+ # and is left alone when the host application configured one of its own.
20
+ module CspProtection
21
+ extend ActiveSupport::Concern
22
+
23
+ included do
24
+ before_action :set_content_security_policy, if: :apply_content_security_policy?
25
+ end
26
+
27
+ private
28
+
29
+ def apply_content_security_policy? = true
30
+
31
+ def set_content_security_policy
32
+ policy_class = Alchemy.config.admin_content_security_policy
33
+ return unless policy_class
34
+ return if request.content_security_policy
35
+
36
+ policy = policy_class.new(request)
37
+ request.content_security_policy_nonce_generator ||= policy.nonce_generator
38
+ request.content_security_policy = policy.call
39
+ request.content_security_policy_report_only = policy.report_only?
40
+ end
41
+ end
42
+ end
43
+ end
@@ -28,7 +28,7 @@ module Alchemy
28
28
  Current.language
29
29
  end
30
30
  if @current_language.nil?
31
- flash[:warning] = Alchemy.t("Please create a language first.")
31
+ flash[:info] = Alchemy.t("Please create a language first.")
32
32
  redirect_to admin_languages_path
33
33
  end
34
34
  end
@@ -21,6 +21,8 @@ module Alchemy
21
21
  # add_alchemy_filter :by_location, type: :select, options: ->(query) { Location.pluck(:name, :id) }
22
22
  # add_alchemy_filter :future, type: :checkbox
23
23
  # add_alchemy_filter :by_timeframe, type: :select, options: ["today", "tomorrow"]
24
+ # add_alchemy_filter :by_tag, type: :select, options: ->(query) { Tag.pluck(:name) }, multiple: true, include_blank: false
25
+ # add_alchemy_filter :by_tag, type: :select, options: ->(query) { Tag.pluck(:name) }, multiple: ->(params) { params[:only].present? }
24
26
  def add_alchemy_filter(name, type:, **args)
25
27
  alchemy_filters << "Alchemy::Admin::Filters::#{type.to_s.camelize}".constantize.new(
26
28
  name:,
@@ -74,7 +76,7 @@ module Alchemy
74
76
  def permitted_ransack_search_fields
75
77
  [
76
78
  resource_handler.search_field_name
77
- ] + alchemy_filters.map(&:name)
79
+ ] + alchemy_filters.flat_map(&:permitted_search_params)
78
80
  end
79
81
 
80
82
  def resource_has_filters
@@ -7,6 +7,7 @@ module Alchemy
7
7
  included do
8
8
  before_action :set_current_alchemy_site
9
9
  before_action :set_alchemy_language
10
+ before_action :set_current_alchemy_user
10
11
 
11
12
  helper "alchemy/pages"
12
13
 
@@ -59,6 +60,18 @@ module Alchemy
59
60
  Current.site = current_alchemy_site
60
61
  end
61
62
 
63
+ # Stores the current user in +Current.user+ so models and services can
64
+ # access the acting user without threading it through every call.
65
+ #
66
+ # Apps without a configured +current_user_method+ leave it +nil+ instead
67
+ # of raising, so the public frontend keeps working without authentication.
68
+ #
69
+ def set_current_alchemy_user
70
+ Current.user = current_alchemy_user
71
+ rescue NoCurrentUserFoundError
72
+ Current.user = nil
73
+ end
74
+
62
75
  # Sets the current language for Alchemy.
63
76
  #
64
77
  def set_alchemy_language(lang = nil)
@@ -3,8 +3,8 @@
3
3
  module Alchemy
4
4
  # Handles Legacy page redirects
5
5
  #
6
- # If the page could not be found via its urlname we try to find
7
- # a legacy page url for requested url to redirect to.
6
+ # If the page could not be found via its urlname or is not public we try to find
7
+ # a legacy page url of a published page for requested url to redirect to.
8
8
  #
9
9
  module LegacyPageRedirects
10
10
  extend ActiveSupport::Concern
@@ -22,7 +22,7 @@ module Alchemy
22
22
  end
23
23
 
24
24
  def redirect_to_legacy_url?
25
- (@page.nil? || request.format.nil?) && last_legacy_url
25
+ (@page.nil? || !@page.public? || request.format.nil?) && last_legacy_url
26
26
  end
27
27
 
28
28
  # Use the bare minimum to redirect to legacy page
@@ -48,7 +48,7 @@ module Alchemy
48
48
  Page.table_name => {
49
49
  language_id: Current.language.id
50
50
  }
51
- )
51
+ ).where(page_id: Page.published.select(:id))
52
52
  end
53
53
 
54
54
  def last_legacy_url
@@ -272,8 +272,11 @@ module Alchemy
272
272
  end
273
273
 
274
274
  # Returns the regular expression used for external url validation in link dialog.
275
+ #
276
+ # Anchored with ^ instead of \A because the link dialog receives this as a
277
+ # JavaScript literal, where Ruby's anchors are a syntax error.
275
278
  def link_url_regexp
276
- Alchemy.config.format_matchers.link_url || /^(mailto:|\/|[a-z]+:\/\/)/
279
+ Alchemy.config.format_matchers.link_url || Alchemy::Configurations::FormatMatchers::LINK_URL
277
280
  end
278
281
 
279
282
  # Renders a hint with tooltip
@@ -10,13 +10,11 @@ module Alchemy
10
10
  # == Defaults
11
11
  #
12
12
  # * It uses Alchemy::Forms::Builder as builder
13
- # * It makes a remote request, if the request was XHR request.
14
13
  # * It adds the alchemy class to form
15
14
  #
16
15
  def alchemy_form_for(object, *args, &block)
17
16
  options = args.extract_options!
18
17
  options[:builder] = Alchemy::Forms::Builder
19
- options.key?(:remote) || options[:remote] = request.xhr?
20
18
  options[:html] = {
21
19
  id: options.delete(:id),
22
20
  class: ["alchemy", options.delete(:class)].compact.join(" "),
@@ -11,212 +11,41 @@ module Alchemy
11
11
  # The Hash representing a Alchemy module
12
12
  #
13
13
  def alchemy_main_navigation_entry(alchemy_module)
14
- validate_controller_existence!(alchemy_module)
15
- render(
16
- "alchemy/admin/partials/main_navigation_entry",
17
- alchemy_module: alchemy_module,
18
- navigation: alchemy_module["navigation"]
14
+ render Alchemy::Admin::MainNavigation::Entry.new(
15
+ item: Alchemy::Configurations::MenuItem.from_legacy_hash(alchemy_module)
19
16
  )
20
17
  end
21
-
22
- # Used for checking the main navi permissions
23
- #
24
- # To let your module be navigatable by the user you have to provide an Ability for it.
25
- #
26
- # === Example:
27
- #
28
- # # module.yml
29
- # name: 'my_module'
30
- # navigation: {
31
- # controller: 'my/admin/posts'
32
- # action: 'index'
33
- # }
34
- #
35
- # # ability.rb
36
- # can :index, :my_admin_posts
37
- #
38
- def navigate_module(navigation)
39
- [
40
- navigation["action"].to_sym,
41
- navigation["controller"].to_s.gsub(/\A\//, "").tr("/", "_").to_sym
42
- ]
43
- end
44
-
45
- # CSS classes for main navigation entry.
46
- #
47
- def main_navigation_css_classes(navigation)
48
- [
49
- "main_navi_entry",
50
- admin_mainnavi_active?(navigation) ? "active" : nil,
51
- navigation.key?("sub_navigation") ? "has_sub_navigation" : nil
52
- ].compact
53
- end
54
-
55
- # Returns true if given navi entry is in params controller and action
56
- #
57
- # == Example
58
- # <%= entry_active?({controller: 'admin/users', action: 'index'}) %>
59
- #
60
- # @param [Hash]
61
- # A Alchemy module definition navigation entry
62
- #
63
- def entry_active?(entry)
64
- is_entry_controller_active?(entry) && is_entry_action_active?(entry)
65
- end
18
+ deprecate alchemy_main_navigation_entry: "Render `Alchemy::Admin::MainNavigation::Entry` instead",
19
+ deprecator: Alchemy::Deprecation
66
20
 
67
21
  # Returns url for given Alchemy module.
68
22
  #
69
- # If the module is inside an engine it calls the +url_for+ helper on the engines routing proxy.
70
- #
71
- # If the module is inside the host rails app it calls the +url_for+ helper on the main_app routing proxy.
72
- #
73
23
  # @param [Hash]
74
24
  # A Alchemy module definition
75
25
  #
76
26
  def url_for_module(alchemy_module)
77
- route_from_engine_or_main_app(
78
- alchemy_module["engine_name"],
79
- url_options_for_module(alchemy_module)
80
- )
81
- end
82
-
83
- # Returns url for given Alchemy module sub navigation entry.
84
- #
85
- # If the module is inside an engine it calls the +url_for+ helper on the engines routing proxy.
86
- #
87
- # If the module is inside the host rails app it calls the +url_for+ helper on the main_app routing proxy.
88
- #
89
- # @param [Hash]
90
- # A Alchemy module sub navigation definition
91
- #
92
- def url_for_module_sub_navigation(navigation)
93
- alchemy_module = module_definition_for(navigation)
94
- return if alchemy_module.nil?
95
-
96
- route_from_engine_or_main_app(
97
- alchemy_module["engine_name"],
98
- url_options_for_navigation_entry(navigation)
99
- )
27
+ Alchemy::Configurations::MenuItem.from_legacy_hash(alchemy_module).path(self)
100
28
  end
29
+ deprecate url_for_module: "Use `Alchemy::Configurations::MenuItem#path` instead",
30
+ deprecator: Alchemy::Deprecation
101
31
 
102
32
  # Alchemy modules for main navigation.
103
33
  #
104
34
  # Sorted by position attribute, if given.
105
35
  #
106
36
  def sorted_alchemy_modules
107
- sorted = []
108
- not_sorted = []
109
- alchemy_modules.map do |m|
110
- if m["position"].blank?
111
- not_sorted << m
112
- else
113
- sorted << m
114
- end
115
- end
116
- sorted.sort_by { |m| m["position"] } + not_sorted
37
+ Alchemy::Admin::MainNavigation::Menu.new.items.map { |menu_item| Alchemy::Modules.to_legacy_hash(menu_item) }
117
38
  end
39
+ deprecate sorted_alchemy_modules: "Use `Alchemy.config.menu` instead",
40
+ deprecator: Alchemy::Deprecation
118
41
 
119
42
  private
120
43
 
121
- # Calls +url_for+ helper on engine if present or on host app.
122
- #
123
- # @param [String]
124
- # A name of an engine
125
- # @param [Hash]
126
- # url options hash passed to +url_for+ helper
127
- #
128
- def route_from_engine_or_main_app(engine_name, url_options)
129
- if engine_name.present?
130
- send(engine_name).url_for(url_options)
131
- else
132
- main_app.url_for(url_options)
133
- end
134
- end
135
-
136
- # Returns a url options hash for given Alchemy module.
137
- #
138
- # @param [Hash]
139
- # A Alchemy module definition
140
- #
141
- def url_options_for_module(alchemy_module)
142
- url_options_for_navigation_entry(alchemy_module["navigation"] || {})
143
- end
144
-
145
- # Validates the existence of a given controller configuration.
146
- #
147
- # @param String
148
- # The controller name
149
- def validate_controller_existence!(definition_hash)
150
- controllers = [definition_hash["navigation"]["controller"]]
151
-
152
- if definition_hash["navigation"]["sub_navigation"].is_a?(Array)
153
- controllers.concat(definition_hash["navigation"]["sub_navigation"].map { |x| x["controller"] })
154
- end
155
-
156
- controllers.each do |controller|
157
- controller_const_name = "#{controller.camelize}Controller"
158
- controller_const_name.constantize
159
- rescue NameError
160
- raise "Error in AlchemyCMS module definition: '#{definition_hash["name"]}'. Could not find the " \
161
- "matching controller class #{controller_const_name} for the specified controller: '#{controller}'"
162
- end
163
- end
164
-
165
- # Returns a url options hash for given navigation entry.
166
- #
167
- # @param [Hash]
168
- # A Alchemy module navigation entry
169
- #
170
- def url_options_for_navigation_entry(entry)
171
- {
172
- controller: entry["controller"],
173
- action: entry["action"],
174
- only_path: true,
175
- params: entry["params"]
176
- }.delete_if { |_k, v| v.nil? }
177
- end
178
-
179
44
  # Retrieves the current Alchemy module from controller and index action.
180
45
  #
181
46
  def current_alchemy_module
182
47
  module_definition_for(controller: params[:controller], action: "index")
183
48
  end
184
-
185
- # Returns true if the current controller and action is in a modules navigation definition.
186
- #
187
- def admin_mainnavi_active?(navigation)
188
- # Has the given navigation entry a active sub navigation?
189
- has_active_entry?(navigation["sub_navigation"] || []) ||
190
- # Has the given navigation entry a active nested navigation?
191
- has_active_entry?(navigation["nested"] || []) ||
192
- # Is the navigation entry active?
193
- entry_active?(navigation || {})
194
- end
195
-
196
- # Returns true if the given entry's controller is current controller
197
- #
198
- def is_entry_controller_active?(entry)
199
- entry["controller"].gsub(/\A\//, "") == params[:controller] ||
200
- entry.fetch("nested_controllers", []).include?(params[:controller])
201
- end
202
-
203
- # Returns true if the given entry's action is current controllers action
204
- #
205
- # Also checks if given entry has a +nested_actions+ key, if so it checks if one of them is current controller's action
206
- #
207
- def is_entry_action_active?(entry)
208
- entry["action"] == params[:action] ||
209
- entry.fetch("nested_actions", []).include?(params[:action])
210
- end
211
-
212
- # Returns true if an entry of given entries is active.
213
- #
214
- # @param [Array]
215
- # Alchemy module navigation entries.
216
- #
217
- def has_active_entry?(entries)
218
- entries.any? { |entry| entry_active?(entry) }
219
- end
220
49
  end
221
50
  end
222
51
  end
@@ -58,6 +58,20 @@ module Alchemy
58
58
  element.ingredient_by_role(role)
59
59
  end
60
60
  alias_method :ingredient, :ingredient_by_role
61
+
62
+ # The element's visible nested children.
63
+ #
64
+ # Reads them from the page version's shared in-memory element repository
65
+ # instead of the +nested_elements+ association, so rendering a deeply
66
+ # nested element tree does not issue one query per parent.
67
+ #
68
+ # Returns an +Alchemy::ElementsRepository+ so callers can keep chaining
69
+ # scopes (e.g. +nested_elements.named(:slide)+) while still rendering it
70
+ # directly as a collection (+render el.nested_elements+).
71
+ #
72
+ def nested_elements
73
+ element.page_version.element_repository.children_of(element).visible
74
+ end
61
75
  end
62
76
 
63
77
  # Block-level helper for element views. Constructs a DOM element wrapping
@@ -15,6 +15,20 @@ module Alchemy
15
15
  @resource_window_size ||= "480x#{100 + resource_handler.attributes.length * 40}"
16
16
  end
17
17
 
18
+ # Returns the translated label for the resource dialog title and button text
19
+ # Add a scoped label by adding a "Create <resource-name>" translation key
20
+ # or rely on the generic "Create" translation key with interpolated `%{resource_name}`
21
+ # generated from `resouce_model.model_name.human`.
22
+ def create_resource_label
23
+ Alchemy.t(
24
+ "Create #{resource_name}",
25
+ default: Alchemy.t(
26
+ "Create",
27
+ resource_name: resource_model.model_name.human
28
+ )
29
+ )
30
+ end
31
+
18
32
  def resource_instance_variable
19
33
  instance_variable_get(:"@#{resource_name}")
20
34
  end
@@ -84,7 +98,7 @@ module Alchemy
84
98
  else
85
99
  options[:time_format] || :"alchemy.time"
86
100
  end
87
- value = l(attribute_value, format: localization_format)
101
+ value = Alchemy.l(attribute_value, format: localization_format)
88
102
  elsif attribute[:type] == :boolean
89
103
  value = attribute_value ? '<alchemy-icon name="check"></alchemy-icon>'.html_safe : nil
90
104
  else
@@ -1,27 +1,18 @@
1
- import {
2
- RemoteSelect,
3
- escapeHtml
4
- } from "alchemy_admin/components/remote_select"
1
+ import { RemoteSelect } from "alchemy_admin/components/remote_select"
5
2
 
6
3
  export class AttachmentSelect extends RemoteSelect {
7
- _renderResult(item) {
8
- return this._renderListEntry(item)
4
+ _entry(attachment, term) {
5
+ return {
6
+ icon: attachment.icon_css_class,
7
+ primary: this._hightlightTerm(attachment.name, term)
8
+ }
9
9
  }
10
10
 
11
- /**
12
- * html template for each list entry
13
- * @param {object} page
14
- * @param {string} term
15
- * @returns {string}
16
- * @private
17
- */
18
- _renderListEntry(attachment, term) {
19
- return `
20
- <div class="attachment-select--attachment">
21
- <alchemy-icon name="${escapeHtml(attachment.icon_css_class)}"></alchemy-icon>
22
- <span class="attachment-select--attachment-name">${this._hightlightTerm(attachment.name, term)}</span>
23
- </div>
24
- `
11
+ _selectedEntry(attachment) {
12
+ return {
13
+ icon: attachment.icon_css_class,
14
+ primary: attachment.name
15
+ }
25
16
  }
26
17
  }
27
18
 
@@ -2,13 +2,11 @@
2
2
  // contained in a form, so that Turbo can submit the form.
3
3
  class AutoSubmit extends HTMLElement {
4
4
  connectedCallback() {
5
- // Still using jQuery here, because select2 does not emit
6
- // the event from the original select element.
7
- $(this).on("change", this.#onChange)
5
+ this.addEventListener("change", this.#onChange)
8
6
  }
9
7
 
10
8
  disconnectedCallback() {
11
- $(this).off("change", this.#onChange)
9
+ this.removeEventListener("change", this.#onChange)
12
10
  }
13
11
 
14
12
  #onChange = (event) => {
@@ -4,11 +4,6 @@ class Button extends HTMLButtonElement {
4
4
  connectedCallback() {
5
5
  if (this.form) {
6
6
  this.form.addEventListener("submit", this)
7
-
8
- if (this.form.dataset.remote == "true") {
9
- this.form.addEventListener("ajax:complete", this)
10
- }
11
-
12
7
  this.form.addEventListener("turbo:submit-end", this)
13
8
  } else {
14
9
  console.warn("No form for button found!", this)
@@ -27,7 +22,6 @@ class Button extends HTMLButtonElement {
27
22
  this.disable()
28
23
  }
29
24
  break
30
- case "ajax:complete":
31
25
  case "turbo:submit-end":
32
26
  this.enable()
33
27
  break
@@ -0,0 +1,21 @@
1
+ class ClearScheduleButton extends HTMLButtonElement {
2
+ connectedCallback() {
3
+ this.addEventListener("click", this)
4
+ }
5
+
6
+ disconnectedCallback() {
7
+ this.removeEventListener("click", this)
8
+ }
9
+
10
+ handleEvent() {
11
+ this.form
12
+ ?.querySelectorAll('input[type="datetime-local"]')
13
+ .forEach((input) => {
14
+ input.value = ""
15
+ })
16
+ }
17
+ }
18
+
19
+ customElements.define("alchemy-clear-schedule-button", ClearScheduleButton, {
20
+ extends: "button"
21
+ })