alchemy_cms 7.1.13 → 7.2.0.b

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 (316) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +132 -39
  3. data/Gemfile +2 -11
  4. data/LICENSE +1 -1
  5. data/README.md +5 -6
  6. data/SECURITY.md +1 -1
  7. data/alchemy_cms.gemspec +3 -4
  8. data/app/assets/javascripts/alchemy/admin.js +0 -9
  9. data/app/assets/javascripts/alchemy/alchemy.dialog.js.coffee +5 -15
  10. data/app/assets/javascripts/alchemy/alchemy.image_overlay.coffee +5 -4
  11. data/app/assets/javascripts/alchemy/templates/index.js +0 -1
  12. data/app/assets/javascripts/alchemy/templates/node_folder.hbs +1 -1
  13. data/app/assets/javascripts/alchemy/templates/page_folder.hbs +1 -1
  14. data/app/assets/javascripts/tinymce/plugins/alchemy_link/plugin.min.js +20 -7
  15. data/app/assets/stylesheets/alchemy/_custom-properties.scss +12 -0
  16. data/app/assets/stylesheets/alchemy/_mixins.scss +10 -6
  17. data/app/assets/stylesheets/alchemy/_variables.scss +3 -0
  18. data/app/assets/stylesheets/alchemy/admin.scss +2 -2
  19. data/app/assets/stylesheets/alchemy/archive.scss +4 -3
  20. data/app/assets/stylesheets/alchemy/attachment-select.scss +19 -0
  21. data/app/assets/stylesheets/alchemy/base.scss +31 -18
  22. data/app/assets/stylesheets/alchemy/buttons.scss +3 -4
  23. data/app/assets/stylesheets/alchemy/dashboard.scss +1 -1
  24. data/app/assets/stylesheets/alchemy/dialogs.scss +2 -5
  25. data/app/assets/stylesheets/alchemy/elements.scss +74 -42
  26. data/app/assets/stylesheets/alchemy/flash.scss +20 -70
  27. data/app/assets/stylesheets/alchemy/forms.scss +41 -36
  28. data/app/assets/stylesheets/alchemy/frame.scss +12 -3
  29. data/app/assets/stylesheets/alchemy/icons.scss +34 -2
  30. data/app/assets/stylesheets/alchemy/image_library.scss +18 -9
  31. data/app/assets/stylesheets/alchemy/{filter_field.scss → list_filter.scss} +8 -7
  32. data/app/assets/stylesheets/alchemy/lists.scss +1 -1
  33. data/app/assets/stylesheets/alchemy/navigation.scss +9 -12
  34. data/app/assets/stylesheets/alchemy/node-select.scss +1 -1
  35. data/app/assets/stylesheets/alchemy/nodes.scss +15 -13
  36. data/app/assets/stylesheets/alchemy/notices.scss +56 -39
  37. data/app/assets/stylesheets/alchemy/page-select.scss +1 -4
  38. data/app/assets/stylesheets/alchemy/pagination.scss +11 -1
  39. data/app/assets/stylesheets/alchemy/preview_window.scss +3 -3
  40. data/app/assets/stylesheets/alchemy/search.scss +4 -4
  41. data/app/assets/stylesheets/alchemy/selects.scss +13 -7
  42. data/app/assets/stylesheets/alchemy/shoelace.scss +33 -2
  43. data/app/assets/stylesheets/alchemy/sitemap.scss +155 -159
  44. data/app/assets/stylesheets/alchemy/tables.scss +49 -12
  45. data/app/assets/stylesheets/alchemy/tags.scss +17 -11
  46. data/app/assets/stylesheets/alchemy/toolbar.scss +2 -2
  47. data/app/assets/stylesheets/alchemy/typography.scss +41 -22
  48. data/app/assets/stylesheets/alchemy/upload.scss +5 -4
  49. data/app/components/alchemy/admin/attachment_select.rb +39 -0
  50. data/app/components/alchemy/admin/icon.rb +72 -0
  51. data/app/components/alchemy/admin/link_dialog/anchor_tab.rb +41 -0
  52. data/app/components/alchemy/admin/link_dialog/base_tab.rb +75 -0
  53. data/app/components/alchemy/admin/link_dialog/external_tab.rb +42 -0
  54. data/app/components/alchemy/admin/link_dialog/file_tab.rb +45 -0
  55. data/app/components/alchemy/admin/link_dialog/internal_tab.rb +66 -0
  56. data/app/components/alchemy/admin/link_dialog/tabs.rb +33 -0
  57. data/app/components/alchemy/admin/list_filter.rb +42 -0
  58. data/app/components/alchemy/admin/message.rb +19 -0
  59. data/app/components/alchemy/admin/tags_autocomplete.rb +25 -0
  60. data/app/components/alchemy/admin/toolbar_button.rb +111 -0
  61. data/app/components/alchemy/ingredients/datetime_view.rb +2 -3
  62. data/app/components/alchemy/ingredients/link_view.rb +1 -7
  63. data/app/components/alchemy/ingredients/picture_view.rb +2 -2
  64. data/app/components/alchemy/ingredients/text_view.rb +1 -2
  65. data/app/controllers/alchemy/admin/base_controller.rb +3 -27
  66. data/app/controllers/alchemy/admin/elements_controller.rb +4 -2
  67. data/app/controllers/alchemy/admin/ingredients_controller.rb +2 -0
  68. data/app/controllers/alchemy/admin/languages_controller.rb +2 -2
  69. data/app/controllers/alchemy/admin/layoutpages_controller.rb +0 -19
  70. data/app/controllers/alchemy/admin/legacy_page_urls_controller.rb +12 -4
  71. data/app/controllers/alchemy/admin/nodes_controller.rb +26 -0
  72. data/app/controllers/alchemy/admin/pages_controller.rb +16 -79
  73. data/app/controllers/alchemy/admin/picture_descriptions_controller.rb +15 -0
  74. data/app/controllers/alchemy/admin/pictures_controller.rb +18 -1
  75. data/app/controllers/alchemy/admin/resources_controller.rb +16 -11
  76. data/app/controllers/alchemy/api/attachments_controller.rb +44 -0
  77. data/app/controllers/alchemy/api/pages_controller.rb +10 -6
  78. data/app/controllers/alchemy/base_controller.rb +2 -2
  79. data/app/controllers/alchemy/messages_controller.rb +3 -3
  80. data/app/controllers/alchemy/pages_controller.rb +8 -6
  81. data/app/controllers/concerns/alchemy/admin/current_language.rb +1 -11
  82. data/app/controllers/concerns/alchemy/legacy_page_redirects.rb +1 -1
  83. data/app/decorators/alchemy/element_editor.rb +2 -2
  84. data/app/helpers/alchemy/admin/base_helper.rb +8 -60
  85. data/app/helpers/alchemy/admin/elements_helper.rb +1 -1
  86. data/app/helpers/alchemy/admin/ingredients_helper.rb +1 -1
  87. data/app/helpers/alchemy/base_helper.rb +9 -91
  88. data/app/helpers/alchemy/elements_helper.rb +3 -3
  89. data/app/helpers/alchemy/pages_helper.rb +16 -9
  90. data/app/javascript/alchemy_admin/components/attachment_select.js +24 -0
  91. data/app/javascript/alchemy_admin/components/button.js +3 -0
  92. data/app/javascript/alchemy_admin/components/clipboard_button.js +3 -2
  93. data/app/javascript/alchemy_admin/components/dialog_link.js +10 -7
  94. data/app/javascript/alchemy_admin/components/dom_id_select.js +69 -0
  95. data/app/javascript/alchemy_admin/components/element_editor/delete_element_button.js +42 -0
  96. data/app/javascript/alchemy_admin/components/element_editor/publish_element_button.js +4 -2
  97. data/app/javascript/alchemy_admin/components/element_editor.js +21 -13
  98. data/app/javascript/alchemy_admin/components/elements_window.js +87 -0
  99. data/app/javascript/alchemy_admin/components/growl.js +13 -0
  100. data/app/javascript/alchemy_admin/components/icon.js +51 -0
  101. data/app/javascript/alchemy_admin/components/index.js +24 -0
  102. data/app/javascript/alchemy_admin/components/ingredient_group.js +6 -0
  103. data/app/javascript/alchemy_admin/components/link_buttons/link_button.js +21 -11
  104. data/app/javascript/alchemy_admin/components/link_buttons/unlink_button.js +2 -1
  105. data/app/javascript/alchemy_admin/components/link_buttons.js +1 -0
  106. data/app/javascript/alchemy_admin/components/list_filter.js +68 -0
  107. data/app/javascript/alchemy_admin/components/message.js +69 -0
  108. data/app/javascript/alchemy_admin/components/node_select.js +1 -1
  109. data/app/javascript/alchemy_admin/components/overlay.js +6 -6
  110. data/app/javascript/alchemy_admin/components/page_select.js +3 -7
  111. data/app/javascript/alchemy_admin/components/preview_window.js +121 -0
  112. data/app/javascript/alchemy_admin/components/remote_select.js +4 -1
  113. data/app/javascript/alchemy_admin/components/select.js +37 -1
  114. data/app/javascript/alchemy_admin/components/tags_autocomplete.js +57 -0
  115. data/app/javascript/alchemy_admin/components/uploader/file_upload.js +4 -3
  116. data/app/javascript/alchemy_admin/components/uploader/progress.js +1 -1
  117. data/app/javascript/alchemy_admin/confirm_dialog.js +133 -0
  118. data/app/javascript/alchemy_admin/dirty.js +19 -14
  119. data/app/javascript/alchemy_admin/fixed_elements.js +24 -0
  120. data/app/javascript/alchemy_admin/growler.js +15 -0
  121. data/app/javascript/alchemy_admin/gui.js +2 -4
  122. data/app/javascript/alchemy_admin/hotkeys.js +60 -0
  123. data/app/javascript/alchemy_admin/image_loader.js +2 -2
  124. data/app/javascript/alchemy_admin/ingredient_anchor_link.js +2 -3
  125. data/app/javascript/alchemy_admin/initializer.js +1 -8
  126. data/app/javascript/alchemy_admin/link_dialog.js +131 -0
  127. data/app/javascript/alchemy_admin/locales/en.js +3 -0
  128. data/app/javascript/alchemy_admin/node_tree.js +4 -3
  129. data/app/javascript/alchemy_admin/page_sorter.js +23 -14
  130. data/app/javascript/alchemy_admin/picture_editors.js +6 -5
  131. data/app/javascript/alchemy_admin/shoelace_theme.js +60 -0
  132. data/app/javascript/alchemy_admin/sitemap.js +9 -3
  133. data/app/javascript/alchemy_admin/sortable_elements.js +4 -6
  134. data/app/javascript/alchemy_admin.js +18 -42
  135. data/app/models/alchemy/current.rb +26 -0
  136. data/app/models/alchemy/element.rb +1 -1
  137. data/app/models/alchemy/ingredients/audio.rb +0 -11
  138. data/app/models/alchemy/ingredients/datetime.rb +1 -1
  139. data/app/models/alchemy/ingredients/headline.rb +8 -1
  140. data/app/models/alchemy/ingredients/picture.rb +6 -0
  141. data/app/models/alchemy/ingredients/video.rb +0 -12
  142. data/app/models/alchemy/language.rb +8 -6
  143. data/app/models/alchemy/node.rb +2 -2
  144. data/app/models/alchemy/page/page_elements.rb +8 -8
  145. data/app/models/alchemy/page/page_layouts.rb +3 -3
  146. data/app/models/alchemy/page/page_natures.rb +13 -9
  147. data/app/models/alchemy/page/page_scopes.rb +2 -2
  148. data/app/models/alchemy/page/publisher.rb +1 -0
  149. data/app/models/alchemy/page.rb +16 -31
  150. data/app/models/alchemy/picture.rb +8 -0
  151. data/app/models/alchemy/picture_description.rb +8 -0
  152. data/app/models/alchemy/picture_variant.rb +1 -1
  153. data/app/models/alchemy/site.rb +10 -7
  154. data/app/models/concerns/alchemy/picture_thumbnails.rb +5 -4
  155. data/app/serializers/alchemy/attachment_serializer.rb +8 -0
  156. data/app/serializers/alchemy/page_node_serializer.rb +9 -0
  157. data/app/views/alchemy/_menubar.html.erb +1 -1
  158. data/app/views/alchemy/_preview_mode_code.html.erb +1 -1
  159. data/app/views/alchemy/admin/attachments/_tag_list.html.erb +2 -2
  160. data/app/views/alchemy/admin/attachments/archive_overlay.js.erb +0 -1
  161. data/app/views/alchemy/admin/attachments/edit.html.erb +3 -4
  162. data/app/views/alchemy/admin/clipboard/clear.js.erb +1 -1
  163. data/app/views/alchemy/admin/clipboard/index.html.erb +1 -1
  164. data/app/views/alchemy/admin/clipboard/insert.js.erb +1 -1
  165. data/app/views/alchemy/admin/clipboard/remove.js.erb +1 -1
  166. data/app/views/alchemy/admin/dashboard/_locked_pages.html.erb +1 -1
  167. data/app/views/alchemy/admin/dashboard/_sites.html.erb +1 -1
  168. data/app/views/alchemy/admin/dashboard/help.html.erb +48 -12
  169. data/app/views/alchemy/admin/dashboard/index.html.erb +1 -1
  170. data/app/views/alchemy/admin/dashboard/info.html.erb +5 -8
  171. data/app/views/alchemy/admin/elements/_add_nested_element_form.html.erb +1 -1
  172. data/app/views/alchemy/admin/elements/_element.html.erb +5 -5
  173. data/app/views/alchemy/admin/elements/_footer.html.erb +1 -1
  174. data/app/views/alchemy/admin/elements/_header.html.erb +6 -2
  175. data/app/views/alchemy/admin/elements/_toolbar.html.erb +8 -6
  176. data/app/views/alchemy/admin/elements/create.js.erb +0 -5
  177. data/app/views/alchemy/admin/elements/index.html.erb +70 -34
  178. data/app/views/alchemy/admin/ingredients/_file_fields.html.erb +1 -2
  179. data/app/views/alchemy/admin/ingredients/_picture_fields.html.erb +3 -5
  180. data/app/views/alchemy/admin/languages/_language.html.erb +1 -1
  181. data/app/views/alchemy/admin/languages/index.html.erb +2 -2
  182. data/app/views/alchemy/admin/layoutpages/_layoutpage.html.erb +18 -18
  183. data/app/views/alchemy/admin/layoutpages/edit.html.erb +4 -5
  184. data/app/views/alchemy/admin/layoutpages/index.html.erb +2 -2
  185. data/app/views/alchemy/admin/legacy_page_urls/_legacy_page_url.html.erb +10 -11
  186. data/app/views/alchemy/admin/legacy_page_urls/_new.html.erb +15 -17
  187. data/app/views/alchemy/admin/legacy_page_urls/_table.html.erb +16 -0
  188. data/app/views/alchemy/admin/legacy_page_urls/_update.turbo_stream.erb +12 -0
  189. data/app/views/alchemy/admin/legacy_page_urls/create.turbo_stream.erb +8 -0
  190. data/app/views/alchemy/admin/legacy_page_urls/destroy.turbo_stream.erb +1 -0
  191. data/app/views/alchemy/admin/legacy_page_urls/edit.html.erb +27 -0
  192. data/app/views/alchemy/admin/legacy_page_urls/show.html.erb +1 -0
  193. data/app/views/alchemy/admin/legacy_page_urls/update.turbo_stream.erb +1 -0
  194. data/app/views/alchemy/admin/nodes/_form.html.erb +12 -11
  195. data/app/views/alchemy/admin/nodes/_label.html.erb +1 -0
  196. data/app/views/alchemy/admin/nodes/_node.html.erb +19 -19
  197. data/app/views/alchemy/admin/nodes/_page_nodes.html.erb +48 -0
  198. data/app/views/alchemy/admin/nodes/_update.turbo_stream.erb +9 -0
  199. data/app/views/alchemy/admin/nodes/create.turbo_stream.erb +1 -0
  200. data/app/views/alchemy/admin/nodes/destroy.turbo_stream.erb +1 -0
  201. data/app/views/alchemy/admin/nodes/index.html.erb +3 -3
  202. data/app/views/alchemy/admin/pages/_form.html.erb +3 -4
  203. data/app/views/alchemy/admin/pages/_legacy_urls.html.erb +4 -15
  204. data/app/views/alchemy/admin/pages/_page.html.erb +39 -39
  205. data/app/views/alchemy/admin/pages/_table_row.html.erb +3 -3
  206. data/app/views/alchemy/admin/pages/_toolbar.html.erb +2 -2
  207. data/app/views/alchemy/admin/pages/configure.html.erb +6 -0
  208. data/app/views/alchemy/admin/pages/edit.html.erb +15 -62
  209. data/app/views/alchemy/admin/pages/unlock.js.erb +3 -3
  210. data/app/views/alchemy/admin/partials/_autocomplete_tag_list.html.erb +3 -1
  211. data/app/views/alchemy/admin/partials/_flash_notices.html.erb +4 -2
  212. data/app/views/alchemy/admin/partials/_language_tree_select.html.erb +1 -1
  213. data/app/views/alchemy/admin/partials/_main_navigation_entry.html.erb +5 -2
  214. data/app/views/alchemy/admin/partials/_remote_search_form.html.erb +2 -2
  215. data/app/views/alchemy/admin/partials/_search_form.html.erb +2 -2
  216. data/app/views/alchemy/admin/partials/_site_select.html.erb +1 -1
  217. data/app/views/alchemy/admin/picture_descriptions/_form.html.erb +11 -0
  218. data/app/views/alchemy/admin/picture_descriptions/edit.html.erb +6 -0
  219. data/app/views/alchemy/admin/pictures/_form.html.erb +4 -3
  220. data/app/views/alchemy/admin/pictures/_infos.html.erb +1 -1
  221. data/app/views/alchemy/admin/pictures/_picture_description_field.html.erb +29 -0
  222. data/app/views/alchemy/admin/pictures/_tag_list.html.erb +2 -2
  223. data/app/views/alchemy/admin/pictures/archive_overlay.js.erb +0 -2
  224. data/app/views/alchemy/admin/pictures/edit_multiple.html.erb +3 -3
  225. data/app/views/alchemy/admin/pictures/show.html.erb +3 -3
  226. data/app/views/alchemy/admin/resources/_form.html.erb +3 -4
  227. data/app/views/alchemy/admin/resources/_per_page_select.html.erb +1 -1
  228. data/app/views/alchemy/admin/resources/_tag_list.html.erb +2 -2
  229. data/app/views/alchemy/admin/resources/index.html.erb +2 -2
  230. data/app/views/alchemy/admin/sites/index.html.erb +1 -1
  231. data/app/views/alchemy/admin/styleguide/index.html.erb +29 -24
  232. data/app/views/alchemy/admin/tags/_tag.html.erb +1 -1
  233. data/app/views/alchemy/admin/tags/edit.html.erb +1 -1
  234. data/app/views/alchemy/admin/tags/index.html.erb +1 -1
  235. data/app/views/alchemy/base/500.html.erb +7 -18
  236. data/app/views/alchemy/base/error_notice.html.erb +3 -1
  237. data/app/views/alchemy/ingredients/_boolean_editor.html.erb +1 -1
  238. data/app/views/alchemy/ingredients/_datetime_editor.html.erb +2 -3
  239. data/app/views/alchemy/ingredients/_headline_editor.html.erb +13 -8
  240. data/app/views/alchemy/ingredients/_picture_editor.html.erb +1 -1
  241. data/app/views/alchemy/ingredients/shared/_picture_tools.html.erb +1 -1
  242. data/app/views/alchemy/language_links/_language.html.erb +1 -1
  243. data/app/views/kaminari/alchemy/_first_page.html.erb +2 -2
  244. data/app/views/kaminari/alchemy/_gap.html.erb +1 -1
  245. data/app/views/kaminari/alchemy/_last_page.html.erb +2 -2
  246. data/app/views/kaminari/alchemy/_next_page.html.erb +2 -2
  247. data/app/views/kaminari/alchemy/_prev_page.html.erb +2 -2
  248. data/app/views/layouts/alchemy/admin.html.erb +2 -1
  249. data/bundles/shoelace.js +3 -1
  250. data/config/locales/alchemy.en.yml +16 -3
  251. data/config/routes.rb +4 -2
  252. data/db/migrate/20240314105244_create_alchemy_picture_descriptions.rb +11 -0
  253. data/lib/alchemy/configuration_methods.rb +1 -1
  254. data/lib/alchemy/controller_actions.rb +3 -3
  255. data/lib/alchemy/element_definition.rb +10 -6
  256. data/lib/alchemy/engine.rb +19 -2
  257. data/lib/alchemy/page_layout.rb +10 -6
  258. data/lib/alchemy/permissions.rb +4 -3
  259. data/lib/alchemy/resource.rb +4 -14
  260. data/lib/alchemy/routing_constraints.rb +1 -1
  261. data/lib/alchemy/seeder.rb +2 -2
  262. data/lib/alchemy/test_support/capybara_helpers.rb +4 -0
  263. data/lib/alchemy/test_support/factories/language_factory.rb +1 -1
  264. data/lib/alchemy/test_support/shared_contexts.rb +8 -0
  265. data/lib/alchemy/tinymce.rb +2 -1
  266. data/lib/alchemy/version.rb +1 -1
  267. data/lib/alchemy.rb +36 -0
  268. data/lib/alchemy_cms.rb +0 -1
  269. data/lib/generators/alchemy/menus/templates/node.html.erb +2 -2
  270. data/lib/generators/alchemy/menus/templates/node.html.haml +2 -2
  271. data/lib/generators/alchemy/menus/templates/node.html.slim +2 -2
  272. data/lib/generators/alchemy/menus/templates/wrapper.html.erb +1 -1
  273. data/lib/generators/alchemy/menus/templates/wrapper.html.haml +1 -1
  274. data/lib/generators/alchemy/menus/templates/wrapper.html.slim +1 -1
  275. data/lib/tasks/alchemy/sitemap.rake +97 -0
  276. data/lib/tasks/alchemy/tidy.rake +1 -0
  277. data/package.json +8 -8
  278. data/vendor/assets/fonts/remixicon.symbol.svg +11 -0
  279. data/vendor/javascript/shoelace.min.js +333 -118
  280. data/vendor/javascript/sortable.min.js +1 -1
  281. data/vendor/javascript/tinymce.min.js +1 -1
  282. data/vendor/javascript/ungap-custom-elements.min.js +1 -1
  283. metadata +63 -55
  284. data/app/assets/javascripts/alchemy/alchemy.confirm_dialog.js.coffee +0 -85
  285. data/app/assets/javascripts/alchemy/alchemy.elements_window.js.coffee +0 -107
  286. data/app/assets/javascripts/alchemy/alchemy.file_progress.js.coffee +0 -66
  287. data/app/assets/javascripts/alchemy/alchemy.fixed_elements.js +0 -45
  288. data/app/assets/javascripts/alchemy/alchemy.growler.js.coffee +0 -24
  289. data/app/assets/javascripts/alchemy/alchemy.hotkeys.js.coffee +0 -49
  290. data/app/assets/javascripts/alchemy/alchemy.initializer.js.coffee +0 -0
  291. data/app/assets/javascripts/alchemy/alchemy.link_dialog.js.coffee +0 -230
  292. data/app/assets/javascripts/alchemy/alchemy.list_filter.js.coffee +0 -49
  293. data/app/assets/javascripts/alchemy/alchemy.preview_window.js.coffee +0 -82
  294. data/app/assets/javascripts/alchemy/alchemy.string_extension.js.coffee +0 -11
  295. data/app/assets/javascripts/alchemy/templates/page.hbs +0 -19
  296. data/app/javascript/alchemy_admin/tags_autocomplete.js +0 -46
  297. data/app/models/alchemy/tree_node.rb +0 -7
  298. data/app/views/alchemy/admin/elements/destroy.js.erb +0 -8
  299. data/app/views/alchemy/admin/legacy_page_urls/_form.html.erb +0 -5
  300. data/app/views/alchemy/admin/legacy_page_urls/create.js.erb +0 -9
  301. data/app/views/alchemy/admin/legacy_page_urls/destroy.js.erb +0 -6
  302. data/app/views/alchemy/admin/legacy_page_urls/update.js.erb +0 -2
  303. data/app/views/alchemy/admin/pages/_anchor_link.html.erb +0 -22
  304. data/app/views/alchemy/admin/pages/_external_link.html.erb +0 -31
  305. data/app/views/alchemy/admin/pages/_file_link.html.erb +0 -31
  306. data/app/views/alchemy/admin/pages/_internal_link.html.erb +0 -35
  307. data/app/views/alchemy/admin/pages/link.html.erb +0 -26
  308. data/app/views/alchemy/admin/partials/_flash.html.erb +0 -4
  309. data/app/views/alchemy/admin/partials/_toolbar_button.html.erb +0 -29
  310. data/lib/alchemy/test_support/current_language_shared_examples.rb +0 -33
  311. data/vendor/assets/fonts/remixicon.eot +0 -0
  312. data/vendor/assets/fonts/remixicon.svg +0 -7816
  313. data/vendor/assets/fonts/remixicon.ttf +0 -0
  314. data/vendor/assets/fonts/remixicon.woff +0 -0
  315. data/vendor/assets/fonts/remixicon.woff2 +0 -0
  316. data/vendor/assets/stylesheets/remixicon.scss +0 -10480
@@ -1,7 +1,9 @@
1
1
  import Sortable from "sortablejs"
2
+ import { growl } from "alchemy_admin/growler"
2
3
  import { patch } from "alchemy_admin/utils/ajax"
4
+ import pleaseWaitOverlay from "alchemy_admin/please_wait_overlay"
3
5
 
4
- function onFinishDragging(evt) {
6
+ function onSort(evt) {
5
7
  const pageId = evt.item.dataset.pageId
6
8
  const url = Alchemy.routes.move_admin_page_path(pageId)
7
9
  const data = {
@@ -9,19 +11,26 @@ function onFinishDragging(evt) {
9
11
  new_position: evt.newIndex
10
12
  }
11
13
 
12
- patch(url, data)
13
- .then(async (response) => {
14
- const pageData = await response.data
15
- const pageEl = document.getElementById(`page_${pageId}`)
16
- const urlPathEl = pageEl.querySelector(".sitemap_url")
14
+ if (evt.target === evt.to) {
15
+ pleaseWaitOverlay(true)
16
+ patch(url, data)
17
+ .then(async (response) => {
18
+ const pageData = await response.data
19
+ const pageEl = document.getElementById(`page_${pageId}`)
20
+ const urlPathEl = pageEl.querySelector(".sitemap_url")
17
21
 
18
- Alchemy.growl(Alchemy.t("Successfully moved page"))
19
- urlPathEl.textContent = pageData.url_path
20
- displayPageFolders()
21
- })
22
- .catch((error) => {
23
- Alchemy.growl(error.message || error, "error")
24
- })
22
+ growl(Alchemy.t("Successfully moved page"))
23
+ urlPathEl.textContent = pageData.url_path
24
+ displayPageFolders()
25
+ })
26
+ .catch((error) => {
27
+ growl(error.message || error, "error")
28
+ Alchemy.currentSitemap.reload()
29
+ })
30
+ .finally(() => {
31
+ pleaseWaitOverlay(false)
32
+ })
33
+ }
25
34
  }
26
35
 
27
36
  export function displayPageFolders() {
@@ -50,7 +59,7 @@ export function createSortables(sortables) {
50
59
  fallbackOnBody: true,
51
60
  swapThreshold: 0.65,
52
61
  handle: ".handle",
53
- onEnd: onFinishDragging
62
+ onSort
54
63
  })
55
64
  })
56
65
  }
@@ -1,13 +1,14 @@
1
1
  import debounce from "alchemy_admin/utils/debounce"
2
2
  import max from "alchemy_admin/utils/max"
3
3
  import { get } from "alchemy_admin/utils/ajax"
4
+ import { growl } from "alchemy_admin/growler"
4
5
  import ImageLoader from "alchemy_admin/image_loader"
5
6
 
6
7
  const UPDATE_DELAY = 125
7
- const IMAGE_PLACEHOLDER = '<i class="icon ri-image-line ri-fw"></i>'
8
+ const IMAGE_PLACEHOLDER = '<alchemy-icon name="image"></alchemy-icon>'
8
9
  const THUMBNAIL_SIZE = "160x120"
9
10
 
10
- export class PictureEditor {
11
+ class PictureEditor {
11
12
  constructor(container) {
12
13
  this.container = container
13
14
  this.cropFromField = container.querySelector("[data-crop-from]")
@@ -77,7 +78,7 @@ export class PictureEditor {
77
78
  })
78
79
  .catch((error) => {
79
80
  console.error(error.message || error)
80
- Alchemy.growl(error.message || error, "error")
81
+ growl(error.message || error, "error")
81
82
  })
82
83
  }
83
84
 
@@ -131,10 +132,10 @@ export class PictureEditor {
131
132
  if (!this.imageCropperEnabled) return []
132
133
 
133
134
  const mask = this.targetSize.split("x").map((n) => parseInt(n))
134
- const zoom = max(
135
+ const zoom = max([
135
136
  mask[0] / this.imageFileWidth,
136
137
  mask[1] / this.imageFileHeight
137
- )
138
+ ])
138
139
 
139
140
  return [Math.round(mask[0] / zoom), Math.round(mask[1] / zoom)]
140
141
  }
@@ -0,0 +1,60 @@
1
+ import { registerIconLibrary, setDefaultAnimation } from "shoelace"
2
+
3
+ // Change the default animation for all tooltips
4
+ setDefaultAnimation("tooltip.show", {
5
+ keyframes: [
6
+ { transform: "translateY(10px)", opacity: "0" },
7
+ { transform: "translateY(0)", opacity: "1" }
8
+ ],
9
+ options: {
10
+ duration: 100
11
+ }
12
+ })
13
+
14
+ setDefaultAnimation("tooltip.hide", {
15
+ keyframes: [
16
+ { transform: "translateY(0)", opacity: "1" },
17
+ { transform: "translateY(10px)", opacity: "0" }
18
+ ],
19
+ options: {
20
+ duration: 100
21
+ }
22
+ })
23
+
24
+ // Change the default animation for all dialogs
25
+ setDefaultAnimation("dialog.show", {
26
+ keyframes: [
27
+ { transform: "scale(0.98)", opacity: "0" },
28
+ { transform: "scale(1)", opacity: "1" }
29
+ ],
30
+ options: {
31
+ duration: 150
32
+ }
33
+ })
34
+
35
+ setDefaultAnimation("dialog.hide", {
36
+ keyframes: [
37
+ { transform: "scale(1)", opacity: "1" },
38
+ { transform: "scale(0.98)", opacity: "0" }
39
+ ],
40
+ options: {
41
+ duration: 150
42
+ }
43
+ })
44
+
45
+ const spriteUrl = document
46
+ .querySelector('meta[name="alchemy-icon-sprite"]')
47
+ .getAttribute("content")
48
+
49
+ const iconMap = {
50
+ "x-lg": "close"
51
+ }
52
+
53
+ const options = {
54
+ resolver: (name) => `${spriteUrl}#ri-${iconMap[name] || name}-line`,
55
+ mutator: (svg) => svg.setAttribute("fill", "currentColor"),
56
+ spriteSheet: true
57
+ }
58
+
59
+ registerIconLibrary("default", options)
60
+ registerIconLibrary("system", options)
@@ -2,6 +2,7 @@
2
2
  import PageSorter from "alchemy_admin/page_sorter"
3
3
  import { on } from "alchemy_admin/utils/events"
4
4
  import { get, patch } from "alchemy_admin/utils/ajax"
5
+ import { growl } from "alchemy_admin/growler"
5
6
  import { createSortables, displayPageFolders } from "alchemy_admin/page_sorter"
6
7
 
7
8
  export default class Sitemap {
@@ -40,6 +41,11 @@ export default class Sitemap {
40
41
  .catch(this.errorHandler)
41
42
  }
42
43
 
44
+ // Reloads the sitemap
45
+ reload() {
46
+ this.load(this.options.page_root_id)
47
+ }
48
+
43
49
  // Watch page folder clicks and re-render the page branch
44
50
  handlePageFolders() {
45
51
  on(
@@ -103,7 +109,6 @@ export default class Sitemap {
103
109
  item.classList.remove("highlight")
104
110
  }
105
111
  })
106
- this.filter_field_clear.style.display = "inline-block"
107
112
  const { length } = results
108
113
 
109
114
  if (length === 1) {
@@ -123,7 +128,6 @@ export default class Sitemap {
123
128
  left: 0,
124
129
  behavior: "smooth"
125
130
  })
126
- this.filter_field_clear.style.display = "none"
127
131
  }
128
132
  }
129
133
 
@@ -131,6 +135,8 @@ export default class Sitemap {
131
135
  _observe() {
132
136
  this.search_field.addEventListener("keyup", (evt) => {
133
137
  const term = evt.target.value
138
+ this.filter_field_clear.style.visibility =
139
+ term == "" ? "hidden" : "visible"
134
140
  this.filter(term.toLowerCase())
135
141
  })
136
142
  this.search_field.addEventListener("focus", () => key.setScope("search"))
@@ -142,7 +148,7 @@ export default class Sitemap {
142
148
  }
143
149
 
144
150
  errorHandler(error) {
145
- Alchemy.growl(error.message || error, "error")
151
+ growl(error.message || error, "error")
146
152
  console.error(error)
147
153
  }
148
154
  }
@@ -1,5 +1,7 @@
1
1
  import Sortable from "sortablejs"
2
+ import { growl } from "alchemy_admin/growler"
2
3
  import { post } from "alchemy_admin/utils/ajax"
4
+ import { reloadPreview } from "alchemy_admin/components/preview_window"
3
5
 
4
6
  const SORTABLE_OPTIONS = {
5
7
  draggable: ".element-editor",
@@ -35,8 +37,8 @@ function onSort(event) {
35
37
  if (event.target === event.to) {
36
38
  post(Alchemy.routes.order_admin_elements_path, params).then((response) => {
37
39
  const data = response.data
38
- Alchemy.growl(data.message)
39
- Alchemy.PreviewWindow.refresh()
40
+ growl(data.message)
41
+ reloadPreview()
40
42
  item.updateTitle(data.preview_text)
41
43
  })
42
44
  }
@@ -76,8 +78,4 @@ export default function SortableElements(selector) {
76
78
  })
77
79
 
78
80
  sortable_areas.forEach((element) => createSortable(element))
79
-
80
- document.querySelectorAll(".nested-elements").forEach((nestedElement) => {
81
- createSortable(nestedElement)
82
- })
83
81
  }
@@ -1,62 +1,34 @@
1
1
  import "@ungap/custom-elements"
2
2
  import "@hotwired/turbo-rails"
3
- import "keymaster"
4
3
 
5
4
  import Rails from "@rails/ujs"
6
5
 
7
6
  import GUI from "alchemy_admin/gui"
8
7
  import { translate } from "alchemy_admin/i18n"
9
8
  import Dirty from "alchemy_admin/dirty"
9
+ import * as FixedElements from "alchemy_admin/fixed_elements"
10
+ import { growl } from "alchemy_admin/growler"
10
11
  import IngredientAnchorLink from "alchemy_admin/ingredient_anchor_link"
11
12
  import ImageLoader from "alchemy_admin/image_loader"
12
13
  import ImageCropper from "alchemy_admin/image_cropper"
13
14
  import Initializer from "alchemy_admin/initializer"
15
+ import { LinkDialog } from "alchemy_admin/link_dialog"
14
16
  import pictureSelector from "alchemy_admin/picture_selector"
15
17
  import pleaseWaitOverlay from "alchemy_admin/please_wait_overlay"
16
18
  import Sitemap from "alchemy_admin/sitemap"
17
- import SortableElements from "alchemy_admin/sortable_elements"
18
19
  import Spinner from "alchemy_admin/spinner"
19
20
  import PagePublicationFields from "alchemy_admin/page_publication_fields"
21
+ import { reloadPreview } from "alchemy_admin/components/preview_window"
22
+ import {
23
+ openConfirmDialog,
24
+ confirmToDeleteDialog
25
+ } from "alchemy_admin/confirm_dialog"
20
26
 
21
27
  // Web Components
22
- import "alchemy_admin/components/button"
23
- import "alchemy_admin/components/char_counter"
24
- import "alchemy_admin/components/clipboard_button"
25
- import "alchemy_admin/components/datepicker"
26
- import "alchemy_admin/components/dialog_link"
27
- import "alchemy_admin/components/element_editor"
28
- import "alchemy_admin/components/ingredient_group"
29
- import "alchemy_admin/components/link_buttons"
30
- import "alchemy_admin/components/node_select"
31
- import "alchemy_admin/components/uploader"
32
- import "alchemy_admin/components/overlay"
33
- import "alchemy_admin/components/page_select"
34
- import "alchemy_admin/components/select"
35
- import "alchemy_admin/components/spinner"
36
- import "alchemy_admin/components/tinymce"
28
+ import "alchemy_admin/components"
37
29
 
38
- import { setDefaultAnimation } from "shoelace"
39
-
40
- // Change the default animation for all dialogs
41
- setDefaultAnimation("tooltip.show", {
42
- keyframes: [
43
- { transform: "translateY(10px)", opacity: "0" },
44
- { transform: "translateY(0)", opacity: "1" }
45
- ],
46
- options: {
47
- duration: 100
48
- }
49
- })
50
-
51
- setDefaultAnimation("tooltip.hide", {
52
- keyframes: [
53
- { transform: "translateY(0)", opacity: "1" },
54
- { transform: "translateY(10px)", opacity: "0" }
55
- ],
56
- options: {
57
- duration: 100
58
- }
59
- })
30
+ // Shoelace Setup
31
+ import "alchemy_admin/shoelace_theme"
60
32
 
61
33
  // Global Alchemy object
62
34
  if (typeof window.Alchemy === "undefined") {
@@ -68,16 +40,20 @@ Object.assign(Alchemy, {
68
40
  ...Dirty,
69
41
  GUI,
70
42
  t: translate, // Global utility method for translating a given string
43
+ FixedElements,
44
+ growl,
71
45
  ImageLoader: ImageLoader.init,
72
46
  ImageCropper,
73
- Initializer,
74
47
  IngredientAnchorLink,
48
+ LinkDialog,
75
49
  pictureSelector,
76
50
  pleaseWaitOverlay,
77
51
  Sitemap,
78
- SortableElements,
79
52
  Spinner,
80
- PagePublicationFields
53
+ PagePublicationFields,
54
+ reloadPreview,
55
+ openConfirmDialog,
56
+ confirmToDeleteDialog
81
57
  })
82
58
 
83
59
  Rails.start()
@@ -0,0 +1,26 @@
1
+ module Alchemy
2
+ class Current < ActiveSupport::CurrentAttributes
3
+ attribute :preview_page, :page, :language, :site
4
+
5
+ def language
6
+ super || Language.default
7
+ end
8
+
9
+ def site
10
+ super || Site.first
11
+ end
12
+
13
+ def preview_page=(page)
14
+ super
15
+
16
+ self.page = page
17
+ self.language = page&.language
18
+ self.site = page&.site
19
+ end
20
+
21
+ def preview_page?(page = Current.page)
22
+ return false if preview_page.nil?
23
+ preview_page == page
24
+ end
25
+ end
26
+ end
@@ -101,7 +101,7 @@ module Alchemy
101
101
  scope :excluded, ->(names) { where.not(name: names) }
102
102
  scope :fixed, -> { where(fixed: true) }
103
103
  scope :unfixed, -> { where(fixed: false) }
104
- scope :from_current_site, -> { where(Language.table_name => {site_id: Site.current || Site.default}).joins(page: "language") }
104
+ scope :from_current_site, -> { where(Language.table_name => {site_id: Current.site}).joins(page: "language") }
105
105
  scope :folded, -> { where(folded: true) }
106
106
  scope :expanded, -> { where(folded: false) }
107
107
  scope :not_nested, -> { where(parent_element_id: nil) }
@@ -26,17 +26,6 @@ module Alchemy
26
26
  def preview_text(max_length = 30)
27
27
  name.to_s[0..max_length - 1]
28
28
  end
29
-
30
- %i[
31
- autoplay
32
- controls
33
- loop
34
- muted
35
- ].each do |method|
36
- define_method(:"#{method}=") do |value|
37
- super(ActiveModel::Type::Boolean.new.cast(value))
38
- end
39
- end
40
29
  end
41
30
  end
42
31
  end
@@ -5,7 +5,7 @@ module Alchemy
5
5
  # A datetime value
6
6
  #
7
7
  class Datetime < Alchemy::Ingredient
8
- allow_settings %i[date_format input_type]
8
+ allow_settings %i[date_format]
9
9
 
10
10
  def value
11
11
  ActiveRecord::Type::DateTime.new.cast(self[:value])
@@ -29,7 +29,14 @@ module Alchemy
29
29
  end
30
30
 
31
31
  def size_options
32
- sizes.map { |size| [".h#{size}", size] }
32
+ sizes.map do |size|
33
+ case size
34
+ when Array
35
+ size
36
+ else
37
+ [".h#{size}", size]
38
+ end
39
+ end
33
40
  end
34
41
 
35
42
  private
@@ -38,6 +38,12 @@ module Alchemy
38
38
  upsample
39
39
  ]
40
40
 
41
+ def alt_text(language: Alchemy::Current.language)
42
+ alt_tag.presence ||
43
+ picture&.description_for(language) ||
44
+ picture&.name&.humanize
45
+ end
46
+
41
47
  # The first 30 characters of the pictures name
42
48
  #
43
49
  # Used by the Element#preview_text method.
@@ -31,18 +31,6 @@ module Alchemy
31
31
  def preview_text(max_length = 30)
32
32
  name.to_s[0..max_length - 1]
33
33
  end
34
-
35
- %i[
36
- autoplay
37
- controls
38
- loop
39
- muted
40
- playsinline
41
- ].each do |method|
42
- define_method(:"#{method}=") do |value|
43
- super(ActiveModel::Type::Boolean.new.cast(value))
44
- end
45
- end
46
34
  end
47
35
  end
48
36
  end
@@ -68,24 +68,26 @@ module Alchemy
68
68
  end
69
69
 
70
70
  def on_current_site
71
- on_site(Site.current)
71
+ on_site(Current.site)
72
72
  end
73
73
 
74
74
  # Store the current language in the current thread.
75
+ # @deprecated Use {Alchemy::Current#language=} instead.
75
76
  def current=(language)
76
- RequestStore.store[:alchemy_current_language] = language
77
+ Current.language = language
77
78
  end
79
+ deprecate "current=": :"Alchemy::Current.language=", deprecator: Alchemy::Deprecation
78
80
 
79
81
  # Current language from current thread or default.
82
+ # @deprecated Use {Alchemy::Current#language} instead.
80
83
  def current
81
- RequestStore.store[:alchemy_current_language] || default
84
+ Current.language
82
85
  end
86
+ deprecate current: :"Alchemy::Current.language", deprecator: Alchemy::Deprecation
83
87
 
84
88
  # The root page of the current language.
85
89
  def current_root_page
86
- return unless current
87
-
88
- current.pages.language_roots.first
90
+ Current.language&.pages&.language_roots&.first
89
91
  end
90
92
 
91
93
  # Default language for current site
@@ -37,9 +37,9 @@ module Alchemy
37
37
  class << self
38
38
  # Returns all root nodes for current language
39
39
  def language_root_nodes
40
- raise "No language found" if Language.current.nil?
40
+ raise "No language found" if Current.language.nil?
41
41
 
42
- roots.where(language_id: Language.current.id)
42
+ roots.where(language_id: Current.language.id)
43
43
  end
44
44
 
45
45
  def available_menu_names
@@ -80,21 +80,21 @@ module Alchemy
80
80
  # type: Richtext
81
81
  #
82
82
  def available_element_definitions(only_element_named = nil)
83
- @_element_definitions ||= if only_element_named
83
+ @_available_element_definitions ||= if only_element_named
84
84
  definition = Element.definition_by_name(only_element_named)
85
85
  element_definitions_by_name(definition["nestable_elements"])
86
86
  else
87
- element_definitions
87
+ element_definitions.dup
88
88
  end
89
89
 
90
- return [] if @_element_definitions.blank?
90
+ return [] if @_available_element_definitions.blank?
91
91
 
92
92
  existing_elements = draft_version.elements.not_nested
93
93
  @_existing_element_names = existing_elements.pluck(:name)
94
94
  delete_unique_element_definitions!
95
95
  delete_outnumbered_element_definitions!
96
96
 
97
- @_element_definitions
97
+ @_available_element_definitions
98
98
  end
99
99
 
100
100
  # All names of elements that can actually be placed on current page.
@@ -186,18 +186,18 @@ module Alchemy
186
186
  end
187
187
  end
188
188
 
189
- # Deletes unique and already present definitions from @_element_definitions.
189
+ # Deletes unique and already present definitions from @_available_element_definitions.
190
190
  #
191
191
  def delete_unique_element_definitions!
192
- @_element_definitions.delete_if do |element|
192
+ @_available_element_definitions.delete_if do |element|
193
193
  element["unique"] && @_existing_element_names.include?(element["name"])
194
194
  end
195
195
  end
196
196
 
197
- # Deletes limited and outnumbered definitions from @_element_definitions.
197
+ # Deletes limited and outnumbered definitions from @_available_element_definitions.
198
198
  #
199
199
  def delete_outnumbered_element_definitions!
200
- @_element_definitions.delete_if do |element|
200
+ @_available_element_definitions.delete_if do |element|
201
201
  outnumbered = @_existing_element_names.select { |name| name == element["name"] }
202
202
  element["amount"] && outnumbered.count >= element["amount"].to_i
203
203
  end
@@ -103,10 +103,10 @@ module Alchemy
103
103
  # page_layouts: [default_intro]
104
104
  #
105
105
  def available_on_site?(layout)
106
- return false unless Alchemy::Site.current
106
+ return false unless Alchemy::Current.site
107
107
 
108
- Alchemy::Site.current.definition.blank? ||
109
- Alchemy::Site.current.definition.fetch("page_layouts", []).include?(layout["name"])
108
+ Alchemy::Current.site.definition.blank? ||
109
+ Alchemy::Current.site.definition.fetch("page_layouts", []).include?(layout["name"])
110
110
  end
111
111
  end
112
112
  end
@@ -101,18 +101,22 @@ module Alchemy
101
101
  page_layout.parameterize.underscore
102
102
  end
103
103
 
104
- # Returns the version that's taken for Rails' recycable cache key.
104
+ # Returns the version string that's taken for Rails' recycable cache key.
105
105
  #
106
- # Uses the +published_at+ value that's updated when the user publishes the page.
106
+ def cache_version
107
+ last_modified_at&.to_s
108
+ end
109
+
110
+ # Returns the timestamp that the page was last modified at, regardless of through
111
+ # publishing or editing page, or through a change of related objects through ingredients.
112
+ # Respects the public version not changing if editing a preview.
107
113
  #
108
- # If the page is the current preview it uses the +updated_at+ value as cache key.
114
+ # In preview mode, it will take the draft version's updated_at timestamp.
115
+ # In public mode, it will take the public version's updated_at timestamp.
109
116
  #
110
- def cache_version
111
- if Page.current_preview == id
112
- updated_at.to_s
113
- else
114
- published_at.to_s
115
- end
117
+ def last_modified_at
118
+ relevant_page_version = (Current.preview_page == self) ? draft_version : public_version
119
+ relevant_page_version&.updated_at
116
120
  end
117
121
 
118
122
  # Returns true if the page cache control headers should be set.
@@ -94,11 +94,11 @@ module Alchemy
94
94
  #
95
95
  scope :searchables, -> { not_restricted.published.contentpages }
96
96
 
97
- # All pages from +Alchemy::Site.current+
97
+ # All pages from +Alchemy::Current.site+
98
98
  #
99
99
  scope :from_current_site,
100
100
  -> {
101
- where(Language.table_name => {site_id: Site.current || Site.default}).joins(:language)
101
+ where(Language.table_name => {site_id: Current.site}).joins(:language)
102
102
  }
103
103
 
104
104
  # All pages for xml sitemap
@@ -35,6 +35,7 @@ module Alchemy
35
35
  end
36
36
  end
37
37
  end
38
+ version.update(updated_at: public_on)
38
39
  page.update(published_at: public_on)
39
40
  end
40
41
  end