alchemy_cms 8.0.6 → 8.1.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 (292) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +14 -10
  3. data/app/assets/builds/alchemy/admin.css +1 -1
  4. data/app/assets/builds/alchemy/dark-theme.css +1 -1
  5. data/app/assets/builds/alchemy/light-theme.css +1 -1
  6. data/app/assets/builds/alchemy/preview.min.js +1 -1
  7. data/app/assets/builds/alchemy/theme.css +1 -1
  8. data/app/assets/builds/alchemy/welcome.css +1 -1
  9. data/app/assets/builds/tinymce/skins/content/alchemy/content.min.css +1 -1
  10. data/app/assets/builds/tinymce/skins/content/alchemy-dark/content.min.css +1 -1
  11. data/app/assets/builds/tinymce/skins/ui/alchemy/skin.min.css +1 -1
  12. data/app/assets/builds/tinymce/skins/ui/alchemy-dark/skin.min.css +1 -1
  13. data/app/{views/alchemy/admin/elements/_element.html.erb → components/alchemy/admin/element_editor.html.erb} +34 -29
  14. data/app/components/alchemy/admin/element_editor.rb +115 -0
  15. data/app/components/alchemy/admin/element_select.rb +12 -9
  16. data/app/components/alchemy/admin/ingredient_editor.rb +54 -0
  17. data/app/components/alchemy/admin/list_filter.rb +16 -5
  18. data/app/components/alchemy/admin/page_node.html.erb +214 -0
  19. data/app/components/alchemy/admin/page_node.rb +70 -0
  20. data/app/components/alchemy/admin/picture_thumbnail.rb +36 -0
  21. data/app/components/alchemy/admin/publish_page_button.html.erb +15 -0
  22. data/app/components/alchemy/admin/publish_page_button.rb +54 -0
  23. data/app/{helpers/alchemy/admin/tags_helper.rb → components/alchemy/admin/tags_list.rb} +19 -11
  24. data/app/components/alchemy/admin/toolbar_button.rb +16 -12
  25. data/app/components/alchemy/ingredients/audio_editor.rb +8 -0
  26. data/app/components/alchemy/ingredients/base_editor.rb +222 -0
  27. data/app/components/alchemy/ingredients/boolean_editor.rb +21 -0
  28. data/app/components/alchemy/ingredients/color_editor.rb +80 -0
  29. data/app/components/alchemy/ingredients/color_view.rb +13 -0
  30. data/app/components/alchemy/ingredients/datetime_editor.rb +28 -0
  31. data/app/components/alchemy/ingredients/file_editor.rb +69 -0
  32. data/app/components/alchemy/ingredients/headline_editor.rb +88 -0
  33. data/app/components/alchemy/ingredients/html_editor.rb +11 -0
  34. data/app/components/alchemy/ingredients/link_editor.rb +29 -0
  35. data/app/components/alchemy/ingredients/node_editor.rb +23 -0
  36. data/app/components/alchemy/ingredients/number_editor.rb +28 -0
  37. data/app/components/alchemy/ingredients/page_editor.rb +19 -0
  38. data/app/components/alchemy/ingredients/picture_editor.rb +81 -0
  39. data/app/components/alchemy/ingredients/richtext_editor.rb +31 -0
  40. data/app/components/alchemy/ingredients/select_editor.rb +37 -0
  41. data/app/components/alchemy/ingredients/select_view.rb +7 -0
  42. data/app/components/alchemy/ingredients/text_editor.rb +41 -0
  43. data/app/components/alchemy/ingredients/video_editor.rb +8 -0
  44. data/app/controllers/alchemy/admin/attachments_controller.rb +8 -6
  45. data/app/controllers/alchemy/admin/base_controller.rb +7 -18
  46. data/app/controllers/alchemy/admin/clipboard_controller.rb +15 -11
  47. data/app/controllers/alchemy/admin/dashboard_controller.rb +2 -2
  48. data/app/controllers/alchemy/admin/elements_controller.rb +34 -32
  49. data/app/controllers/alchemy/admin/ingredients_controller.rb +1 -0
  50. data/app/controllers/alchemy/admin/layoutpages_controller.rb +2 -1
  51. data/app/controllers/alchemy/admin/legacy_page_urls_controller.rb +1 -1
  52. data/app/controllers/alchemy/admin/nodes_controller.rb +24 -1
  53. data/app/controllers/alchemy/admin/pages_controller.rb +31 -41
  54. data/app/controllers/alchemy/admin/pictures_controller.rb +2 -5
  55. data/app/controllers/alchemy/admin/resources_controller.rb +1 -1
  56. data/app/controllers/alchemy/api/ingredients_controller.rb +1 -1
  57. data/app/controllers/alchemy/api/pages_controller.rb +5 -3
  58. data/app/controllers/alchemy/base_controller.rb +6 -6
  59. data/app/controllers/alchemy/pages_controller.rb +12 -6
  60. data/app/controllers/concerns/alchemy/admin/archive_overlay.rb +0 -1
  61. data/app/controllers/concerns/alchemy/admin/clipboard.rb +57 -0
  62. data/app/controllers/concerns/alchemy/admin/uploader_responses.rb +2 -2
  63. data/app/controllers/concerns/alchemy/site_redirects.rb +1 -1
  64. data/app/decorators/alchemy/ingredient_editor.rb +37 -4
  65. data/app/helpers/alchemy/admin/base_helper.rb +12 -7
  66. data/app/helpers/alchemy/admin/ingredients_helper.rb +6 -3
  67. data/app/helpers/alchemy/base_helper.rb +1 -1
  68. data/app/helpers/alchemy/pages_helper.rb +1 -1
  69. data/app/javascript/alchemy_admin/components/action.js +5 -1
  70. data/app/javascript/alchemy_admin/components/color_select.js +73 -0
  71. data/app/javascript/alchemy_admin/components/element_editor/delete_element_button.js +11 -3
  72. data/app/javascript/alchemy_admin/components/element_editor/publish_element_button.js +7 -2
  73. data/app/javascript/alchemy_admin/components/element_editor.js +11 -12
  74. data/app/javascript/alchemy_admin/components/element_select.js +39 -17
  75. data/app/javascript/alchemy_admin/components/elements_window.js +0 -2
  76. data/app/javascript/alchemy_admin/components/file_editor.js +26 -0
  77. data/app/javascript/alchemy_admin/components/index.js +9 -0
  78. data/app/javascript/alchemy_admin/components/list_filter.js +57 -8
  79. data/app/javascript/alchemy_admin/components/message.js +9 -3
  80. data/app/javascript/alchemy_admin/components/page_node.js +119 -0
  81. data/app/javascript/alchemy_admin/{page_publication_fields.js → components/page_publication_fields.js} +9 -8
  82. data/app/javascript/alchemy_admin/{picture_editors.js → components/picture_editor.js} +30 -45
  83. data/app/javascript/alchemy_admin/components/picture_thumbnail.js +107 -0
  84. data/app/javascript/alchemy_admin/components/publish_page_button.js +41 -0
  85. data/app/javascript/alchemy_admin/components/select.js +3 -1
  86. data/app/javascript/alchemy_admin/components/sitemap.js +210 -0
  87. data/app/javascript/alchemy_admin/{sortable_elements.js → components/sortable_elements.js} +22 -25
  88. data/app/javascript/alchemy_admin/components/tinymce.js +10 -5
  89. data/app/javascript/alchemy_admin/components/update_check.js +1 -1
  90. data/app/javascript/alchemy_admin/components/uploader.js +30 -0
  91. data/app/javascript/alchemy_admin/image_overlay.js +0 -2
  92. data/app/javascript/alchemy_admin/initializer.js +0 -3
  93. data/app/javascript/alchemy_admin/templates/compiled.js +1 -1
  94. data/app/javascript/alchemy_admin/utils/ajax.js +15 -3
  95. data/app/javascript/alchemy_admin.js +0 -6
  96. data/app/models/alchemy/attachment.rb +4 -4
  97. data/app/models/alchemy/element/definitions.rb +1 -2
  98. data/app/models/alchemy/element/element_ingredients.rb +6 -2
  99. data/app/models/alchemy/element.rb +55 -13
  100. data/app/models/alchemy/element_definition.rb +5 -1
  101. data/app/models/alchemy/elements_repository.rb +6 -0
  102. data/app/models/alchemy/folded_page.rb +2 -2
  103. data/app/models/alchemy/ingredient.rb +38 -1
  104. data/app/models/alchemy/ingredient_definition.rb +5 -1
  105. data/app/models/alchemy/ingredient_validator.rb +6 -2
  106. data/app/models/alchemy/ingredients/color.rb +10 -0
  107. data/app/models/alchemy/ingredients/headline.rb +2 -17
  108. data/app/models/alchemy/ingredients/picture.rb +4 -4
  109. data/app/models/alchemy/ingredients/select.rb +19 -0
  110. data/app/models/alchemy/node.rb +28 -1
  111. data/app/models/alchemy/page/page_naming.rb +0 -7
  112. data/app/models/alchemy/page/page_natures.rb +7 -3
  113. data/app/models/alchemy/page/page_scopes.rb +1 -1
  114. data/app/models/alchemy/page/publisher.rb +14 -2
  115. data/app/models/alchemy/page.rb +102 -23
  116. data/app/models/alchemy/page_definition.rb +4 -1
  117. data/app/models/alchemy/page_version.rb +22 -6
  118. data/app/models/alchemy/picture.rb +10 -11
  119. data/app/models/alchemy/picture_variant.rb +1 -3
  120. data/app/models/alchemy/resource.rb +1 -1
  121. data/app/models/alchemy/storage_adapter/active_storage.rb +14 -2
  122. data/app/models/alchemy/storage_adapter/dragonfly.rb +12 -0
  123. data/app/models/alchemy/storage_adapter.rb +2 -0
  124. data/app/models/concerns/alchemy/picture_thumbnails.rb +4 -4
  125. data/app/models/concerns/alchemy/publishable.rb +54 -0
  126. data/app/serializers/alchemy/page_tree_serializer.rb +11 -31
  127. data/app/services/alchemy/copy_page.rb +17 -0
  128. data/app/services/alchemy/duplicate_element.rb +1 -1
  129. data/app/services/alchemy/page_tree_preloader.rb +105 -0
  130. data/app/stylesheets/alchemy/_custom-properties.scss +1 -0
  131. data/app/stylesheets/alchemy/_extends.scss +3 -9
  132. data/app/stylesheets/alchemy/_mixins.scss +3 -1
  133. data/app/stylesheets/alchemy/_themes.scss +19 -10
  134. data/app/stylesheets/alchemy/admin/archive.scss +1 -0
  135. data/app/stylesheets/alchemy/admin/base.scss +13 -4
  136. data/app/stylesheets/alchemy/admin/buttons.scss +3 -3
  137. data/app/stylesheets/alchemy/admin/dialogs.scss +11 -5
  138. data/app/stylesheets/alchemy/admin/element-select.scss +18 -0
  139. data/app/stylesheets/alchemy/admin/elements.scss +127 -25
  140. data/app/stylesheets/alchemy/admin/errors.scss +1 -1
  141. data/app/stylesheets/alchemy/admin/flash.scss +6 -4
  142. data/app/stylesheets/alchemy/admin/images.scss +9 -5
  143. data/app/stylesheets/alchemy/admin/list_filter.scss +4 -4
  144. data/app/stylesheets/alchemy/admin/notices.scss +1 -2
  145. data/app/stylesheets/alchemy/admin/preview_window.scss +1 -1
  146. data/app/stylesheets/alchemy/admin/resource_info.scss +7 -0
  147. data/app/stylesheets/alchemy/admin/selects.scss +36 -21
  148. data/app/stylesheets/alchemy/admin/shoelace.scss +14 -1
  149. data/app/stylesheets/alchemy/admin/sitemap.scss +11 -3
  150. data/app/stylesheets/alchemy/admin/tags.scss +3 -1
  151. data/app/stylesheets/alchemy/admin/toolbar.scss +1 -1
  152. data/app/stylesheets/tinymce/skins/ui/alchemy/skin.scss +2 -2
  153. data/app/stylesheets/tinymce/skins/ui/alchemy-dark/skin.scss +2 -2
  154. data/app/views/alchemy/_edit_mode.html.erb +1 -1
  155. data/app/views/alchemy/_menubar.html.erb +1 -1
  156. data/app/views/alchemy/admin/attachments/_archive_overlay.html.erb +35 -31
  157. data/app/views/alchemy/admin/attachments/_library_sidebar.html.erb +6 -0
  158. data/app/views/alchemy/admin/attachments/_overlay_file_list.html.erb +1 -1
  159. data/app/views/alchemy/admin/attachments/_replace_button.html.erb +1 -8
  160. data/app/views/alchemy/admin/attachments/_sorting_select.html.erb +13 -0
  161. data/app/views/alchemy/admin/attachments/_tag_list.html.erb +2 -3
  162. data/app/views/alchemy/admin/attachments/index.html.erb +5 -11
  163. data/app/views/alchemy/admin/attachments/show.html.erb +1 -1
  164. data/app/views/alchemy/admin/clipboard/_button.html.erb +1 -0
  165. data/app/views/alchemy/admin/clipboard/index.html.erb +4 -5
  166. data/app/views/alchemy/admin/clipboard/insert.turbo_stream.erb +1 -1
  167. data/app/views/alchemy/admin/crop.html.erb +5 -7
  168. data/app/views/alchemy/admin/elements/_add_nested_element_form.html.erb +6 -6
  169. data/app/views/alchemy/admin/elements/_fixed_element.html.erb +1 -1
  170. data/app/views/alchemy/admin/elements/_footer.html.erb +7 -1
  171. data/app/views/alchemy/admin/elements/_header.html.erb +5 -5
  172. data/app/views/alchemy/admin/elements/_toolbar.html.erb +33 -8
  173. data/app/views/alchemy/admin/elements/create.turbo_stream.erb +10 -10
  174. data/app/views/alchemy/admin/elements/index.html.erb +29 -16
  175. data/app/views/alchemy/admin/elements/new.html.erb +2 -2
  176. data/app/views/alchemy/admin/ingredients/update.turbo_stream.erb +3 -5
  177. data/app/views/alchemy/admin/leave.html.erb +9 -9
  178. data/app/views/alchemy/admin/nodes/_node.html.erb +19 -0
  179. data/app/views/alchemy/admin/nodes/edit.html.erb +1 -1
  180. data/app/views/alchemy/admin/nodes/index.html.erb +3 -1
  181. data/app/views/alchemy/admin/nodes/new.html.erb +14 -1
  182. data/app/views/alchemy/admin/pages/_current_page.html.erb +3 -1
  183. data/app/views/alchemy/admin/pages/_form.html.erb +21 -9
  184. data/app/views/alchemy/admin/pages/_page_status.html.erb +1 -1
  185. data/app/views/alchemy/admin/pages/_publication_fields.html.erb +28 -26
  186. data/app/views/alchemy/admin/pages/_table.html.erb +0 -7
  187. data/app/views/alchemy/admin/pages/_toolbar.html.erb +3 -5
  188. data/app/views/alchemy/admin/pages/edit.html.erb +5 -13
  189. data/app/views/alchemy/admin/pages/flush.turbo_stream.erb +2 -0
  190. data/app/views/alchemy/admin/pages/fold.turbo_stream.erb +5 -0
  191. data/app/views/alchemy/admin/pages/index.html.erb +5 -3
  192. data/app/views/alchemy/admin/pages/new.html.erb +2 -12
  193. data/app/views/alchemy/admin/pages/publish.turbo_stream.erb +12 -0
  194. data/app/views/alchemy/admin/pages/tree.html.erb +13 -0
  195. data/app/views/alchemy/admin/pages/update.turbo_stream.erb +5 -16
  196. data/app/views/alchemy/admin/partials/_flash_notices.html.erb +1 -1
  197. data/app/views/alchemy/admin/partials/{_remote_search_form.html.erb → _overlay_search_form.html.erb} +1 -2
  198. data/app/views/alchemy/admin/partials/_paste_from_clipboard_form.html.erb +12 -0
  199. data/app/views/alchemy/admin/pictures/_archive_overlay.html.erb +24 -21
  200. data/app/views/alchemy/admin/pictures/_filter_and_size_bar.html.erb +18 -26
  201. data/app/views/alchemy/admin/pictures/_picture.html.erb +11 -15
  202. data/app/views/alchemy/admin/pictures/_picture_to_assign.html.erb +3 -6
  203. data/app/views/alchemy/admin/pictures/_tag_list.html.erb +2 -3
  204. data/app/views/alchemy/admin/pictures/index.html.erb +0 -1
  205. data/app/views/alchemy/admin/pictures/update.turbo_stream.erb +1 -1
  206. data/app/views/alchemy/admin/resources/_resource_usage_info.html.erb +1 -1
  207. data/app/views/alchemy/admin/resources/_tag_list.html.erb +2 -3
  208. data/app/views/alchemy/admin/styleguide/index.html.erb +25 -20
  209. data/app/views/alchemy/admin/tags/edit.html.erb +1 -1
  210. data/app/views/alchemy/admin/tinymce/_setup.html.erb +2 -2
  211. data/app/views/alchemy/admin/uploader/_button.html.erb +1 -15
  212. data/app/views/alchemy/attachments/show.html.erb +1 -1
  213. data/app/views/alchemy/base/permission_denied.js.erb +1 -1
  214. data/app/views/alchemy/ingredients/shared/_anchor.html.erb +9 -7
  215. data/app/views/alchemy/ingredients/shared/_link_tools.html.erb +12 -5
  216. data/app/views/alchemy/ingredients/shared/_picture_tools.html.erb +10 -11
  217. data/app/views/alchemy/language_links/_spacer.html.erb +1 -1
  218. data/app/views/alchemy/messages_mailer/new.html.erb +1 -1
  219. data/app/views/alchemy/welcome.html.erb +1 -1
  220. data/app/views/layouts/alchemy/admin.html.erb +1 -1
  221. data/config/locales/alchemy.en.yml +12 -2
  222. data/config/routes.rb +2 -2
  223. data/db/migrate/20230123112425_add_searchable_to_alchemy_pages.rb +1 -1
  224. data/db/migrate/20230505132743_add_indexes_to_alchemy_pictures.rb +1 -1
  225. data/db/migrate/20231113104432_create_page_mutexes.rb +1 -1
  226. data/db/migrate/20240314105244_create_alchemy_picture_descriptions.rb +1 -1
  227. data/db/migrate/20250626160259_add_unique_index_to_picture_descriptions.rb +1 -1
  228. data/db/migrate/20250905140323_add_created_at_index_to_pictures_and_attachments.rb +1 -1
  229. data/db/migrate/20251106150010_convert_select_value_for_multiple.rb +11 -0
  230. data/db/migrate/20260102121232_add_metadata_to_page_versions.rb +9 -0
  231. data/db/migrate/20260115164704_add_publication_timestamps_to_alchemy_elements.rb +30 -0
  232. data/db/migrate/20260115164705_add_index_to_element_publication_timestamps.rb +13 -0
  233. data/lib/alchemy/ability_helper.rb +1 -3
  234. data/lib/alchemy/auth_accessors.rb +51 -117
  235. data/lib/alchemy/configuration.rb +1 -0
  236. data/lib/alchemy/configurations/main.rb +63 -0
  237. data/lib/alchemy/controller_actions.rb +1 -1
  238. data/lib/alchemy/engine.rb +9 -12
  239. data/lib/alchemy/error_tracking/error_logger.rb +1 -1
  240. data/lib/alchemy/errors.rb +1 -1
  241. data/lib/alchemy/logger.rb +34 -4
  242. data/lib/alchemy/name_conversions.rb +0 -6
  243. data/lib/alchemy/seeder.rb +2 -2
  244. data/lib/alchemy/tasks/usage.rb +4 -4
  245. data/lib/alchemy/test_support/factories/page_version_factory.rb +3 -0
  246. data/lib/alchemy/test_support/having_picture_thumbnails_examples.rb +30 -0
  247. data/lib/alchemy/test_support/shared_ingredient_editor_examples.rb +26 -6
  248. data/lib/alchemy/test_support/shared_publishable_examples.rb +114 -0
  249. data/lib/alchemy/upgrader/eight_one.rb +56 -0
  250. data/lib/alchemy/upgrader.rb +9 -1
  251. data/lib/alchemy/version.rb +1 -1
  252. data/lib/alchemy.rb +1 -4
  253. data/lib/alchemy_cms.rb +0 -1
  254. data/lib/generators/alchemy/elements/templates/view.html.erb +3 -3
  255. data/lib/generators/alchemy/ingredient/ingredient_generator.rb +6 -8
  256. data/lib/generators/alchemy/ingredient/templates/editor_component.rb.tt +22 -0
  257. data/lib/generators/alchemy/page_layouts/templates/layout.html.erb +1 -1
  258. data/lib/generators/alchemy/site_layouts/templates/layout.html.erb +1 -1
  259. data/lib/tasks/alchemy/upgrade.rake +21 -7
  260. data/vendor/javascript/shoelace.min.js +713 -31
  261. data/vendor/javascript/tinymce.min.js +1 -1
  262. metadata +103 -83
  263. data/app/decorators/alchemy/element_editor.rb +0 -90
  264. data/app/helpers/alchemy/admin/pictures_helper.rb +0 -14
  265. data/app/javascript/alchemy_admin/file_editors.js +0 -28
  266. data/app/javascript/alchemy_admin/image_loader.js +0 -54
  267. data/app/javascript/alchemy_admin/page_sorter.js +0 -71
  268. data/app/javascript/alchemy_admin/sitemap.js +0 -154
  269. data/app/javascript/alchemy_admin/templates/page_folder.hbs +0 -3
  270. data/app/views/alchemy/admin/attachments/archive_overlay.js.erb +0 -4
  271. data/app/views/alchemy/admin/pages/_page.html.erb +0 -163
  272. data/app/views/alchemy/admin/pages/_sitemap.html.erb +0 -30
  273. data/app/views/alchemy/admin/pages/flush.js.erb +0 -2
  274. data/app/views/alchemy/admin/pictures/archive_overlay.js.erb +0 -5
  275. data/app/views/alchemy/admin/pictures/index.js.erb +0 -2
  276. data/app/views/alchemy/ingredients/_audio_editor.html.erb +0 -5
  277. data/app/views/alchemy/ingredients/_boolean_editor.html.erb +0 -11
  278. data/app/views/alchemy/ingredients/_datetime_editor.html.erb +0 -20
  279. data/app/views/alchemy/ingredients/_file_editor.html.erb +0 -52
  280. data/app/views/alchemy/ingredients/_headline_editor.html.erb +0 -44
  281. data/app/views/alchemy/ingredients/_html_editor.html.erb +0 -8
  282. data/app/views/alchemy/ingredients/_link_editor.html.erb +0 -30
  283. data/app/views/alchemy/ingredients/_node_editor.html.erb +0 -13
  284. data/app/views/alchemy/ingredients/_number_editor.html.erb +0 -24
  285. data/app/views/alchemy/ingredients/_page_editor.html.erb +0 -13
  286. data/app/views/alchemy/ingredients/_picture_editor.html.erb +0 -59
  287. data/app/views/alchemy/ingredients/_richtext_editor.html.erb +0 -15
  288. data/app/views/alchemy/ingredients/_select_editor.html.erb +0 -31
  289. data/app/views/alchemy/ingredients/_text_editor.html.erb +0 -29
  290. data/app/views/alchemy/ingredients/_video_editor.html.erb +0 -5
  291. data/lib/generators/alchemy/ingredient/templates/editor.html.erb +0 -14
  292. /data/{lib → app/models}/alchemy/permissions.rb +0 -0
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ module Ingredients
5
+ class PictureEditor < BaseEditor
6
+ delegate :allow_image_cropping?,
7
+ :css_class,
8
+ :image_file_width,
9
+ :image_file_height,
10
+ :picture,
11
+ :thumbnail_url_options,
12
+ to: :ingredient
13
+
14
+ def input_field
15
+ content_tag("alchemy-picture-editor") do
16
+ concat(
17
+ tag.div(class: "picture_thumbnail",
18
+ data: {
19
+ target_size: settings[:size] || [
20
+ image_file_width.to_i,
21
+ image_file_height.to_i
22
+ ].join("x"),
23
+ image_cropper: thumbnail_url_options[:crop]
24
+ }) do
25
+ if editable?
26
+ concat tag.button(
27
+ render_icon("close"),
28
+ type: "button",
29
+ class: "picture_tool delete"
30
+ )
31
+ end
32
+ concat(
33
+ tag.div(class: "picture_image") do
34
+ render Alchemy::Admin::PictureThumbnail.new(
35
+ ingredient,
36
+ css_class: "img_paddingtop",
37
+ placeholder: render_icon(:image, size: "xl")
38
+ )
39
+ end
40
+ )
41
+ if css_class.present?
42
+ concat render(
43
+ "alchemy/ingredients/shared/picture_css_class",
44
+ css_class: css_class
45
+ )
46
+ end
47
+ concat(
48
+ tag.div(class: "edit_images_bottom") do
49
+ render(
50
+ "alchemy/ingredients/shared/picture_tools",
51
+ picture_editor: self
52
+ )
53
+ end
54
+ )
55
+ end
56
+ )
57
+ concat hidden_field_tag(form_field_name(:picture_id),
58
+ picture&.id,
59
+ id: form_field_id(:picture_id),
60
+ data: {
61
+ picture_id: true,
62
+ image_file_width: image_file_width,
63
+ image_file_height: image_file_height
64
+ })
65
+ concat hidden_field_tag(form_field_name(:link), ingredient.link, data: {link_value: true}, id: nil)
66
+ concat hidden_field_tag(form_field_name(:link_title), ingredient.link_title, data: {link_title: true}, id: nil)
67
+ concat hidden_field_tag(form_field_name(:link_class_name), ingredient.link_class_name, data: {link_class: true}, id: nil)
68
+ concat hidden_field_tag(form_field_name(:link_target), ingredient.link_target, data: {link_target: true}, id: nil)
69
+ concat hidden_field_tag(form_field_name(:crop_from), ingredient.crop_from, data: {crop_from: true}, id: form_field_id(:crop_from))
70
+ concat hidden_field_tag(form_field_name(:crop_size), ingredient.crop_size, data: {crop_size: true}, id: form_field_id(:crop_size))
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ def ingredient_label(*)
77
+ super(:picture_id)
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ module Ingredients
5
+ class RichtextEditor < BaseEditor
6
+ def input_field
7
+ content_tag("alchemy-tinymce", tinymce_config) do
8
+ text_area_tag form_field_name,
9
+ value,
10
+ minlength: length_validation&.fetch(:minimum, nil),
11
+ maxlength: length_validation&.fetch(:maximum, nil),
12
+ id: form_field_id(:value)
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def tinymce_config
19
+ config = custom_tinymce_config
20
+ config["readonly"] = true.to_json if !editable?
21
+ config
22
+ end
23
+
24
+ def custom_tinymce_config
25
+ ingredient.custom_tinymce_config.each_with_object({}) do |(k, v), obj|
26
+ obj[k.to_s.dasherize] = v.to_json
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ module Ingredients
5
+ class SelectEditor < BaseEditor
6
+ def input_field
7
+ if select_values.nil?
8
+ warning(":select_values is nil",
9
+ <<-MSG.strip_heredoc
10
+ <strong>No select values given.</strong>
11
+ <br>Please provide <code>select_values</code> on the
12
+ ingredient definition <code>settings</code> in
13
+ <code>elements.yml</code>.
14
+ MSG
15
+ )
16
+ else
17
+ options_tags = if select_values.is_a?(Hash)
18
+ grouped_options_for_select(select_values, value)
19
+ else
20
+ options_for_select(select_values, value)
21
+ end
22
+ select_tag form_field_name, options_tags, {
23
+ id: form_field_id,
24
+ class: ["ingredient-editor-select"],
25
+ is: "alchemy-select",
26
+ multiple: settings[:multiple],
27
+ disabled: !editable?
28
+ }
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def select_values = settings[:select_values]
35
+ end
36
+ end
37
+ end
@@ -1,6 +1,13 @@
1
1
  module Alchemy
2
2
  module Ingredients
3
3
  class SelectView < BaseView
4
+ def call
5
+ if ingredient.multiple? && value.is_a?(Array)
6
+ value.to_sentence
7
+ else
8
+ super
9
+ end
10
+ end
4
11
  end
5
12
  end
6
13
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ module Ingredients
5
+ class TextEditor < BaseEditor
6
+ def input_field
7
+ tag.div(class: "input-field") do
8
+ concat text_field_tag(form_field_name,
9
+ value,
10
+ class: settings[:linkable] ? "text_with_icon" : "",
11
+ id: form_field_id,
12
+ minlength: length_validation&.fetch(:minimum, nil),
13
+ maxlength: length_validation&.fetch(:maximum, nil),
14
+ required: presence_validation?,
15
+ pattern: format_validation,
16
+ readonly: !editable?,
17
+ type: settings[:input_type] || "text")
18
+
19
+ if settings[:anchor]
20
+ concat render(
21
+ "alchemy/ingredients/shared/anchor",
22
+ ingredient:
23
+ )
24
+ end
25
+
26
+ if settings[:linkable]
27
+ concat hidden_field_tag(form_field_name(:link), ingredient.link, "data-link-value": true, id: nil)
28
+ concat hidden_field_tag(form_field_name(:link_title), ingredient.link_title, "data-link-title": true, id: nil)
29
+ concat hidden_field_tag(form_field_name(:link_class_name), ingredient.link_class_name, "data-link-class": true, id: nil)
30
+ concat hidden_field_tag(form_field_name(:link_target), ingredient.link_target, "data-link-target": true, id: nil)
31
+ concat render(
32
+ "alchemy/ingredients/shared/link_tools",
33
+ ingredient:,
34
+ wrapper_class: "ingredient_link_buttons"
35
+ )
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ module Ingredients
5
+ class VideoEditor < FileEditor
6
+ end
7
+ end
8
+ end
@@ -22,15 +22,13 @@ module Alchemy
22
22
  add_alchemy_filter :without_tag, type: :checkbox
23
23
  add_alchemy_filter :deletable, type: :checkbox
24
24
 
25
- helper "alchemy/admin/tags"
26
-
27
25
  before_action(only: :assign) do
28
26
  @attachment = Attachment.find(params[:id])
29
27
  end
30
28
 
31
29
  def index
32
30
  @query = Attachment.ransack(search_filter_params[:q])
33
- @query.sorts = "name asc" if @query.sorts.empty?
31
+ @query.sorts = default_sort_order if @query.sorts.empty?
34
32
  @attachments = @query.result
35
33
 
36
34
  if search_filter_params[:tagged_with].present?
@@ -48,19 +46,19 @@ module Alchemy
48
46
 
49
47
  # The resources controller renders the edit form as default for show actions.
50
48
  def show
51
- @assignments = @attachment.related_ingredients.joins(element: :page).merge(PageVersion.drafts)
49
+ @assignments = @attachment.related_ingredients.joins(element: :page).merge(PageVersion.draft)
52
50
  render :show
53
51
  end
54
52
 
55
53
  def create
56
54
  @attachment = Attachment.create(attachment_attributes)
57
- handle_uploader_response(status: :created)
55
+ handle_uploader_response(status: 201)
58
56
  end
59
57
 
60
58
  def update
61
59
  @attachment.update(attachment_attributes)
62
60
  if attachment_attributes["file"].present?
63
- handle_uploader_response(status: :accepted)
61
+ handle_uploader_response(status: 202)
64
62
  else
65
63
  render_errors_or_redirect(
66
64
  @attachment,
@@ -78,6 +76,10 @@ module Alchemy
78
76
 
79
77
  private
80
78
 
79
+ def default_sort_order
80
+ "created_at desc"
81
+ end
82
+
81
83
  def search_filter_params
82
84
  @_search_filter_params ||= begin
83
85
  params[:q] ||= ActionController::Parameters.new
@@ -8,7 +8,7 @@ module Alchemy
8
8
 
9
9
  before_action :load_locked_pages
10
10
 
11
- helper_method :clipboard_empty?, :get_clipboard, :is_admin?
11
+ helper_method :is_admin?
12
12
 
13
13
  check_authorization
14
14
 
@@ -79,26 +79,15 @@ module Alchemy
79
79
  end
80
80
  end
81
81
 
82
- # Returns clipboard items for given category
83
- def get_clipboard(category)
84
- session[:alchemy_clipboard] ||= {}
85
- session[:alchemy_clipboard][category.to_s] ||= []
86
- end
87
-
88
- # Checks if clipboard for given category is blank
89
- def clipboard_empty?(category)
90
- get_clipboard(category).blank?
91
- end
92
-
93
82
  def set_stamper
94
- if Alchemy.user_class < ActiveRecord::Base
95
- Alchemy.user_class.stamper = current_alchemy_user
83
+ if Alchemy.config.user_class.respond_to?(:stamper=)
84
+ Alchemy.config.user_class.stamper = current_alchemy_user
96
85
  end
97
86
  end
98
87
 
99
88
  def reset_stamper
100
- if Alchemy.user_class < ActiveRecord::Base
101
- Alchemy.user_class.reset_stamper
89
+ if Alchemy.config.user_class.respond_to?(:reset_stamper)
90
+ Alchemy.config.user_class.reset_stamper
102
91
  end
103
92
  end
104
93
 
@@ -122,7 +111,7 @@ module Alchemy
122
111
  do_redirect_to redirect_url
123
112
  else
124
113
  render action: ((params[:action] == "update") ? "edit" : "new"),
125
- status: :unprocessable_entity
114
+ status: 422
126
115
  end
127
116
  end
128
117
 
@@ -178,7 +167,7 @@ module Alchemy
178
167
  if ::Alchemy::ErrorTracking.notification_handler.respond_to?(:call)
179
168
  ::Alchemy::ErrorTracking.notification_handler.call(exception)
180
169
  else
181
- Rails.logger.warn("To use the Alchemy::ErrorTracking.notification_handler, it must respond to #call.")
170
+ Logger.warn("To use the Alchemy::ErrorTracking.notification_handler, it must respond to #call.")
182
171
  end
183
172
  end
184
173
  end
@@ -3,15 +3,19 @@
3
3
  module Alchemy
4
4
  module Admin
5
5
  class ClipboardController < Alchemy::Admin::BaseController
6
- REMARKABLE_TYPES = %w[elements pages]
6
+ include Alchemy::Admin::Clipboard
7
+
8
+ REMARKABLE_TYPES = %w[elements pages nodes]
7
9
 
8
10
  authorize_resource class: :alchemy_admin_clipboard
9
- before_action :set_clipboard
10
11
 
11
12
  helper_method :remarkable_type
12
13
 
13
14
  def index
14
- @clipboard_items = model_class.all_from_clipboard(@clipboard)
15
+ raise ActionController::BadRequest unless remarkable_type
16
+
17
+ @clipboard_items = clipboard_items
18
+
15
19
  respond_to do |format|
16
20
  format.html
17
21
  end
@@ -19,8 +23,8 @@ module Alchemy
19
23
 
20
24
  def insert
21
25
  @item = model_class.find(remarkable_params[:remarkable_id])
22
- unless @clipboard.detect { |item| item["id"] == remarkable_params[:remarkable_id] }
23
- @clipboard << {
26
+ unless clipboard.detect { |item| item["id"] == remarkable_params[:remarkable_id] }
27
+ clipboard << {
24
28
  "id" => remarkable_params[:remarkable_id],
25
29
  "action" => params[:remove] ? "cut" : "copy"
26
30
  }
@@ -29,25 +33,25 @@ module Alchemy
29
33
 
30
34
  def remove
31
35
  @item = model_class.find(remarkable_params[:remarkable_id])
32
- @clipboard.delete_if { |item| item["id"] == remarkable_params[:remarkable_id] }
36
+ remove_resource_from_clipboard(@item)
33
37
  end
34
38
 
35
39
  def clear
36
- @clipboard.clear
40
+ clipboard.clear
37
41
  end
38
42
 
39
43
  private
40
44
 
41
- def set_clipboard
42
- @clipboard = get_clipboard(remarkable_type)
43
- end
44
-
45
45
  def model_class
46
46
  raise ActionController::BadRequest unless remarkable_type
47
47
 
48
48
  "alchemy/#{remarkable_type}".classify.constantize
49
49
  end
50
50
 
51
+ def clipboard_type
52
+ remarkable_type
53
+ end
54
+
51
55
  def remarkable_params
52
56
  params.permit(:remarkable_type, :remarkable_id)
53
57
  end
@@ -11,8 +11,8 @@ module Alchemy
11
11
  def index
12
12
  @last_edited_pages = Page.all_last_edited_from(current_alchemy_user)
13
13
  @all_locked_pages = Page.locked
14
- if Alchemy.user_class.respond_to?(:logged_in)
15
- @online_users = Alchemy.user_class.logged_in.to_a - [current_alchemy_user]
14
+ if Alchemy.config.user_class.respond_to?(:logged_in)
15
+ @online_users = Alchemy.config.user_class.logged_in.to_a - [current_alchemy_user]
16
16
  end
17
17
  if current_alchemy_user.respond_to?(:sign_in_count) && current_alchemy_user.respond_to?(:last_sign_in_at)
18
18
  @last_sign_at = current_alchemy_user.last_sign_in_at
@@ -5,25 +5,23 @@ module Alchemy
5
5
  class ElementsController < Alchemy::Admin::BaseController
6
6
  helper Alchemy::Admin::IngredientsHelper
7
7
 
8
+ before_action :load_page_and_version, only: [:index, :new]
9
+ include Alchemy::Admin::Clipboard
10
+
8
11
  before_action :load_element, only: [:update, :destroy, :collapse, :expand, :publish]
9
12
  authorize_resource class: Alchemy::Element
10
13
 
11
14
  def index
12
- @page_version = PageVersion.find(params[:page_version_id])
13
- @page = @page_version.page
14
15
  elements = @page_version.elements.order(:position).includes(*element_includes)
15
16
  @elements = elements.not_nested.unfixed
16
17
  @fixed_elements = elements.not_nested.fixed
17
- load_clipboard_items
18
18
  end
19
19
 
20
20
  def new
21
- @page_version = PageVersion.find(params[:page_version_id])
22
- @page = @page_version.page
23
21
  @parent_element = Element.find_by(id: params[:parent_element_id])
24
22
  @elements = @page.available_elements_within_current_scope(@parent_element)
25
23
  @element = @page_version.elements.build
26
- load_clipboard_items
24
+ clipboard_items
27
25
  end
28
26
 
29
27
  # Creates a element as discribed in config/alchemy/elements.yml on page via AJAX.
@@ -43,12 +41,11 @@ module Alchemy
43
41
  end
44
42
  end
45
43
  if @element.save
46
- render :create, status: :created
44
+ render :create, status: 201
47
45
  else
48
46
  @element.page_version = @page_version
49
47
  @elements = @page.available_element_definitions
50
- load_clipboard_items
51
- render :new, status: :unprocessable_entity
48
+ render :new, status: 422
52
49
  end
53
50
  end
54
51
 
@@ -59,13 +56,15 @@ module Alchemy
59
56
  render json: {
60
57
  notice: Alchemy.t(:element_saved),
61
58
  previewText: Rails::Html::SafeListSanitizer.new.sanitize(@element.preview_text),
62
- ingredientAnchors: @element.ingredients.select { |i| i.settings[:anchor] }.map do |ingredient|
63
- {
64
- ingredientId: ingredient.id,
65
- active: ingredient.dom_id.present?
66
- }
59
+ ingredientAnchors: @element.ingredients.filter_map do |ingredient|
60
+ if ingredient.settings[:anchor]
61
+ {
62
+ ingredientId: ingredient.id,
63
+ active: ingredient.dom_id.present?
64
+ }
65
+ end
67
66
  end
68
- }
67
+ }.merge(pagePublicationData(@element.page))
69
68
  else
70
69
  @warning = Alchemy.t("Validation failed")
71
70
  render json: {
@@ -77,7 +76,7 @@ module Alchemy
77
76
  errorMessage: ingredient.errors.messages[:value].to_sentence
78
77
  }
79
78
  end
80
- }, status: :unprocessable_entity
79
+ }, status: 422
81
80
  end
82
81
  end
83
82
 
@@ -86,7 +85,7 @@ module Alchemy
86
85
 
87
86
  render json: {
88
87
  message: Alchemy.t("Successfully deleted element") % {element: @element.display_name}
89
- }
88
+ }.merge(pagePublicationData(@element.page))
90
89
  end
91
90
 
92
91
  def publish
@@ -95,7 +94,7 @@ module Alchemy
95
94
  render json: {
96
95
  public: @element.public?,
97
96
  label: @element.public? ? Alchemy.t(:hide_element) : Alchemy.t(:show_element)
98
- }
97
+ }.merge(pagePublicationData(@element.page))
99
98
  end
100
99
 
101
100
  def order
@@ -112,7 +111,7 @@ module Alchemy
112
111
  render json: {
113
112
  message: Alchemy.t(:successfully_saved_element_position),
114
113
  preview_text: @element.preview_text
115
- }
114
+ }.merge(pagePublicationData(@element.page))
116
115
  end
117
116
 
118
117
  # Collapses the element, all nested elements and persists the state in the db
@@ -137,7 +136,7 @@ module Alchemy
137
136
  @element.update_columns(folded: false)
138
137
  # We want to expand the upper most parent first in order to prevent
139
138
  # re-painting issues in the browser
140
- parent_element_ids = @element.parent_element_ids.reverse
139
+ parent_element_ids = @element.folded_parent_element_ids.reverse
141
140
  Alchemy::Element.where(id: parent_element_ids).update_all(folded: false)
142
141
 
143
142
  render json: {
@@ -148,6 +147,11 @@ module Alchemy
148
147
 
149
148
  private
150
149
 
150
+ def load_page_and_version
151
+ @page_version = PageVersion.find(params[:page_version_id])
152
+ @page = @page_version.page
153
+ end
154
+
151
155
  def collapse_nested_elements_ids(element)
152
156
  ids = []
153
157
  element.all_nested_elements.includes(:all_nested_elements).reject(&:compact?).each do |nested_element|
@@ -178,20 +182,19 @@ module Alchemy
178
182
  @element = Element.find(params[:id])
179
183
  end
180
184
 
181
- def load_clipboard_items
182
- @clipboard = get_clipboard("elements")
183
- @clipboard_items = Element.all_from_clipboard_for_page(@clipboard, @page)
185
+ def clipboard_items
186
+ @clipboard_items = Element.all_from_clipboard_for_page(clipboard, @page)
184
187
  end
185
188
 
186
- def element_from_clipboard
187
- @element_from_clipboard ||= begin
188
- @clipboard = get_clipboard("elements")
189
- @clipboard.detect { |item| item["id"].to_i == params[:paste_from_clipboard].to_i }
190
- end
189
+ def pagePublicationData(page)
190
+ {
191
+ pageHasUnpublishedChanges: page.has_unpublished_changes?,
192
+ publishButtonTooltip: Alchemy.t(:explain_publishing)
193
+ }
191
194
  end
192
195
 
193
196
  def paste_element_from_clipboard
194
- @source_element = Element.find(element_from_clipboard["id"])
197
+ @source_element = Element.find(item_from_clipboard["id"])
195
198
  element = Element.copy(
196
199
  @source_element,
197
200
  {
@@ -199,10 +202,9 @@ module Alchemy
199
202
  page_version_id: @page_version.id
200
203
  }
201
204
  )
202
- if element_from_clipboard["action"] == "cut"
205
+ if item_from_clipboard["action"] == "cut"
203
206
  @cut_element_id = @source_element.id
204
- @clipboard.delete_if { |item| item["id"] == @source_element.id.to_s }
205
- @source_element.destroy
207
+ remove_resource_from_clipboard(@source_element)
206
208
  end
207
209
  element
208
210
  end
@@ -15,6 +15,7 @@ module Alchemy
15
15
  end
16
16
 
17
17
  def update
18
+ @page = @ingredient.page # necessary to a render picture ingredient component
18
19
  @ingredient.update(ingredient_params)
19
20
  end
20
21
 
@@ -6,6 +6,7 @@ module Alchemy
6
6
  authorize_resource class: :alchemy_admin_layoutpages
7
7
 
8
8
  include Alchemy::Admin::CurrentLanguage
9
+ include Alchemy::Admin::Clipboard
9
10
 
10
11
  helper Alchemy::Admin::PagesHelper
11
12
 
@@ -25,7 +26,7 @@ module Alchemy
25
26
  @while_page_edit = request.referer.include?("edit")
26
27
  render "alchemy/admin/pages/update"
27
28
  else
28
- render :edit, status: :unprocessable_entity
29
+ render :edit, status: 422
29
30
  end
30
31
  end
31
32
 
@@ -22,7 +22,7 @@ module Alchemy
22
22
  @message = message_for_resource_action
23
23
  render :update
24
24
  else
25
- render :edit, status: :unprocessable_entity
25
+ render :edit, status: 422
26
26
  end
27
27
  end
28
28
 
@@ -4,6 +4,7 @@ module Alchemy
4
4
  module Admin
5
5
  class NodesController < Admin::ResourcesController
6
6
  include Alchemy::Admin::CurrentLanguage
7
+ include Alchemy::Admin::Clipboard
7
8
 
8
9
  def index
9
10
  @root_nodes = Node.language_root_nodes
@@ -26,8 +27,28 @@ module Alchemy
26
27
  else
27
28
  flash[:error] = @node.errors.full_messages.join(", ")
28
29
  end
30
+ elsif params[:paste_from_clipboard]
31
+ begin
32
+ @node = paste_from_clipboard
33
+ if @node&.persisted?
34
+ flash_notice_for_resource_action(:create)
35
+ do_redirect_to(admin_nodes_path)
36
+ else
37
+ render :new, status: 422
38
+ end
39
+ rescue => e
40
+ flash[:error] = e.message
41
+ new # Reinitialize instance variables like @node
42
+ render :new, status: 422
43
+ end
29
44
  else
30
- super
45
+ @node = Node.new(resource_params)
46
+ if @node.save
47
+ flash_notice_for_resource_action(:create)
48
+ do_redirect_to(admin_nodes_path)
49
+ else
50
+ render :new, status: 422
51
+ end
31
52
  end
32
53
  end
33
54
 
@@ -36,6 +57,8 @@ module Alchemy
36
57
  @node = Alchemy::Node.find(params[:id])
37
58
  @page = @node.page
38
59
  @page.nodes.destroy(@node)
60
+ # Remove node from clipboard
61
+ remove_resource_from_clipboard(@node)
39
62
  flash_notice_for_resource_action(:destroy)
40
63
  else
41
64
  super