alchemy_cms 8.0.7 → 8.1.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 (280) 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/{views/alchemy/admin/elements/_element.html.erb → components/alchemy/admin/element_editor.html.erb} +34 -29
  9. data/app/components/alchemy/admin/element_editor.rb +115 -0
  10. data/app/components/alchemy/admin/element_select.rb +12 -9
  11. data/app/components/alchemy/admin/ingredient_editor.rb +54 -0
  12. data/app/components/alchemy/admin/list_filter.rb +16 -5
  13. data/app/components/alchemy/admin/page_node.html.erb +215 -0
  14. data/app/components/alchemy/admin/page_node.rb +70 -0
  15. data/app/components/alchemy/admin/picture_thumbnail.rb +36 -0
  16. data/app/components/alchemy/admin/publish_page_button.html.erb +15 -0
  17. data/app/components/alchemy/admin/publish_page_button.rb +54 -0
  18. data/app/{helpers/alchemy/admin/tags_helper.rb → components/alchemy/admin/tags_list.rb} +19 -11
  19. data/app/components/alchemy/admin/toolbar_button.rb +16 -12
  20. data/app/components/alchemy/ingredients/audio_editor.rb +8 -0
  21. data/app/components/alchemy/ingredients/base_editor.rb +222 -0
  22. data/app/components/alchemy/ingredients/boolean_editor.rb +21 -0
  23. data/app/components/alchemy/ingredients/color_editor.rb +80 -0
  24. data/app/components/alchemy/ingredients/color_view.rb +13 -0
  25. data/app/components/alchemy/ingredients/datetime_editor.rb +28 -0
  26. data/app/components/alchemy/ingredients/file_editor.rb +69 -0
  27. data/app/components/alchemy/ingredients/headline_editor.rb +88 -0
  28. data/app/components/alchemy/ingredients/html_editor.rb +11 -0
  29. data/app/components/alchemy/ingredients/link_editor.rb +29 -0
  30. data/app/components/alchemy/ingredients/node_editor.rb +23 -0
  31. data/app/components/alchemy/ingredients/number_editor.rb +28 -0
  32. data/app/components/alchemy/ingredients/page_editor.rb +19 -0
  33. data/app/components/alchemy/ingredients/picture_editor.rb +81 -0
  34. data/app/components/alchemy/ingredients/richtext_editor.rb +31 -0
  35. data/app/components/alchemy/ingredients/select_editor.rb +37 -0
  36. data/app/components/alchemy/ingredients/select_view.rb +7 -0
  37. data/app/components/alchemy/ingredients/text_editor.rb +41 -0
  38. data/app/components/alchemy/ingredients/video_editor.rb +8 -0
  39. data/app/controllers/alchemy/admin/attachments_controller.rb +8 -6
  40. data/app/controllers/alchemy/admin/base_controller.rb +7 -18
  41. data/app/controllers/alchemy/admin/clipboard_controller.rb +15 -11
  42. data/app/controllers/alchemy/admin/dashboard_controller.rb +2 -2
  43. data/app/controllers/alchemy/admin/elements_controller.rb +34 -32
  44. data/app/controllers/alchemy/admin/ingredients_controller.rb +1 -0
  45. data/app/controllers/alchemy/admin/layoutpages_controller.rb +2 -1
  46. data/app/controllers/alchemy/admin/legacy_page_urls_controller.rb +1 -1
  47. data/app/controllers/alchemy/admin/nodes_controller.rb +24 -1
  48. data/app/controllers/alchemy/admin/pages_controller.rb +31 -41
  49. data/app/controllers/alchemy/admin/pictures_controller.rb +2 -5
  50. data/app/controllers/alchemy/admin/resources_controller.rb +1 -1
  51. data/app/controllers/alchemy/api/ingredients_controller.rb +1 -1
  52. data/app/controllers/alchemy/api/pages_controller.rb +5 -3
  53. data/app/controllers/alchemy/base_controller.rb +6 -6
  54. data/app/controllers/alchemy/pages_controller.rb +12 -6
  55. data/app/controllers/concerns/alchemy/admin/archive_overlay.rb +0 -1
  56. data/app/controllers/concerns/alchemy/admin/clipboard.rb +57 -0
  57. data/app/controllers/concerns/alchemy/admin/uploader_responses.rb +2 -2
  58. data/app/controllers/concerns/alchemy/site_redirects.rb +1 -1
  59. data/app/decorators/alchemy/ingredient_editor.rb +37 -4
  60. data/app/helpers/alchemy/admin/base_helper.rb +10 -6
  61. data/app/helpers/alchemy/admin/ingredients_helper.rb +6 -3
  62. data/app/helpers/alchemy/base_helper.rb +1 -1
  63. data/app/helpers/alchemy/pages_helper.rb +1 -1
  64. data/app/javascript/alchemy_admin/components/action.js +5 -1
  65. data/app/javascript/alchemy_admin/components/color_select.js +73 -0
  66. data/app/javascript/alchemy_admin/components/element_editor/delete_element_button.js +11 -3
  67. data/app/javascript/alchemy_admin/components/element_editor/publish_element_button.js +7 -2
  68. data/app/javascript/alchemy_admin/components/element_editor.js +11 -12
  69. data/app/javascript/alchemy_admin/components/element_select.js +39 -17
  70. data/app/javascript/alchemy_admin/components/elements_window.js +0 -2
  71. data/app/javascript/alchemy_admin/components/file_editor.js +26 -0
  72. data/app/javascript/alchemy_admin/components/index.js +9 -0
  73. data/app/javascript/alchemy_admin/components/list_filter.js +57 -8
  74. data/app/javascript/alchemy_admin/components/message.js +9 -3
  75. data/app/javascript/alchemy_admin/components/page_node.js +119 -0
  76. data/app/javascript/alchemy_admin/{page_publication_fields.js → components/page_publication_fields.js} +9 -8
  77. data/app/javascript/alchemy_admin/{picture_editors.js → components/picture_editor.js} +30 -45
  78. data/app/javascript/alchemy_admin/components/picture_thumbnail.js +107 -0
  79. data/app/javascript/alchemy_admin/components/publish_page_button.js +41 -0
  80. data/app/javascript/alchemy_admin/components/select.js +3 -1
  81. data/app/javascript/alchemy_admin/components/sitemap.js +210 -0
  82. data/app/javascript/alchemy_admin/{sortable_elements.js → components/sortable_elements.js} +22 -25
  83. data/app/javascript/alchemy_admin/components/tinymce.js +10 -5
  84. data/app/javascript/alchemy_admin/components/update_check.js +1 -1
  85. data/app/javascript/alchemy_admin/components/uploader.js +30 -0
  86. data/app/javascript/alchemy_admin/image_overlay.js +0 -2
  87. data/app/javascript/alchemy_admin/initializer.js +0 -3
  88. data/app/javascript/alchemy_admin/templates/compiled.js +1 -1
  89. data/app/javascript/alchemy_admin/utils/ajax.js +15 -3
  90. data/app/javascript/alchemy_admin.js +0 -6
  91. data/app/models/alchemy/attachment.rb +4 -4
  92. data/app/models/alchemy/element/definitions.rb +1 -2
  93. data/app/models/alchemy/element/element_ingredients.rb +6 -2
  94. data/app/models/alchemy/element.rb +54 -13
  95. data/app/models/alchemy/element_definition.rb +4 -1
  96. data/app/models/alchemy/elements_repository.rb +6 -0
  97. data/app/models/alchemy/folded_page.rb +2 -2
  98. data/app/models/alchemy/ingredient.rb +38 -1
  99. data/app/models/alchemy/ingredient_definition.rb +4 -1
  100. data/app/models/alchemy/ingredient_validator.rb +6 -2
  101. data/app/models/alchemy/ingredients/color.rb +10 -0
  102. data/app/models/alchemy/ingredients/headline.rb +2 -17
  103. data/app/models/alchemy/ingredients/picture.rb +4 -4
  104. data/app/models/alchemy/ingredients/select.rb +19 -0
  105. data/app/models/alchemy/node.rb +28 -1
  106. data/app/models/alchemy/page/page_naming.rb +0 -7
  107. data/app/models/alchemy/page/page_natures.rb +7 -3
  108. data/app/models/alchemy/page/page_scopes.rb +1 -1
  109. data/app/models/alchemy/page/publisher.rb +14 -2
  110. data/app/models/alchemy/page.rb +102 -23
  111. data/app/models/alchemy/page_definition.rb +4 -1
  112. data/app/models/alchemy/page_version.rb +22 -6
  113. data/app/models/alchemy/picture.rb +10 -11
  114. data/app/models/alchemy/picture_variant.rb +1 -3
  115. data/app/models/alchemy/resource.rb +1 -1
  116. data/app/models/alchemy/storage_adapter/active_storage.rb +14 -2
  117. data/app/models/alchemy/storage_adapter/dragonfly.rb +12 -0
  118. data/app/models/alchemy/storage_adapter.rb +2 -0
  119. data/app/models/concerns/alchemy/picture_thumbnails.rb +4 -4
  120. data/app/models/concerns/alchemy/publishable.rb +54 -0
  121. data/app/serializers/alchemy/page_tree_serializer.rb +11 -31
  122. data/app/services/alchemy/copy_page.rb +17 -0
  123. data/app/services/alchemy/duplicate_element.rb +1 -1
  124. data/app/services/alchemy/page_tree_preloader.rb +105 -0
  125. data/app/stylesheets/alchemy/_extends.scss +3 -9
  126. data/app/stylesheets/alchemy/_mixins.scss +3 -1
  127. data/app/stylesheets/alchemy/_themes.scss +19 -10
  128. data/app/stylesheets/alchemy/admin/archive.scss +1 -0
  129. data/app/stylesheets/alchemy/admin/base.scss +5 -2
  130. data/app/stylesheets/alchemy/admin/buttons.scss +3 -3
  131. data/app/stylesheets/alchemy/admin/element-select.scss +18 -0
  132. data/app/stylesheets/alchemy/admin/elements.scss +123 -23
  133. data/app/stylesheets/alchemy/admin/errors.scss +17 -9
  134. data/app/stylesheets/alchemy/admin/flash.scss +6 -4
  135. data/app/stylesheets/alchemy/admin/images.scss +9 -5
  136. data/app/stylesheets/alchemy/admin/list_filter.scss +4 -4
  137. data/app/stylesheets/alchemy/admin/notices.scss +1 -2
  138. data/app/stylesheets/alchemy/admin/selects.scss +36 -21
  139. data/app/stylesheets/alchemy/admin/shoelace.scss +14 -1
  140. data/app/stylesheets/alchemy/admin/sitemap.scss +11 -3
  141. data/app/stylesheets/alchemy/admin/tags.scss +3 -1
  142. data/app/stylesheets/alchemy/admin/toolbar.scss +1 -1
  143. data/app/views/alchemy/_edit_mode.html.erb +1 -1
  144. data/app/views/alchemy/_menubar.html.erb +1 -1
  145. data/app/views/alchemy/admin/attachments/_archive_overlay.html.erb +35 -31
  146. data/app/views/alchemy/admin/attachments/_library_sidebar.html.erb +6 -0
  147. data/app/views/alchemy/admin/attachments/_overlay_file_list.html.erb +1 -1
  148. data/app/views/alchemy/admin/attachments/_replace_button.html.erb +1 -8
  149. data/app/views/alchemy/admin/attachments/_sorting_select.html.erb +13 -0
  150. data/app/views/alchemy/admin/attachments/_tag_list.html.erb +2 -3
  151. data/app/views/alchemy/admin/attachments/index.html.erb +5 -11
  152. data/app/views/alchemy/admin/attachments/show.html.erb +1 -1
  153. data/app/views/alchemy/admin/clipboard/_button.html.erb +1 -0
  154. data/app/views/alchemy/admin/clipboard/index.html.erb +4 -5
  155. data/app/views/alchemy/admin/clipboard/insert.turbo_stream.erb +1 -1
  156. data/app/views/alchemy/admin/crop.html.erb +5 -7
  157. data/app/views/alchemy/admin/elements/_add_nested_element_form.html.erb +6 -6
  158. data/app/views/alchemy/admin/elements/_fixed_element.html.erb +1 -1
  159. data/app/views/alchemy/admin/elements/_footer.html.erb +7 -1
  160. data/app/views/alchemy/admin/elements/_header.html.erb +5 -5
  161. data/app/views/alchemy/admin/elements/_toolbar.html.erb +33 -8
  162. data/app/views/alchemy/admin/elements/create.turbo_stream.erb +10 -10
  163. data/app/views/alchemy/admin/elements/index.html.erb +29 -16
  164. data/app/views/alchemy/admin/elements/new.html.erb +2 -2
  165. data/app/views/alchemy/admin/ingredients/update.turbo_stream.erb +3 -5
  166. data/app/views/alchemy/admin/leave.html.erb +1 -1
  167. data/app/views/alchemy/admin/nodes/_node.html.erb +19 -0
  168. data/app/views/alchemy/admin/nodes/edit.html.erb +1 -1
  169. data/app/views/alchemy/admin/nodes/index.html.erb +3 -1
  170. data/app/views/alchemy/admin/nodes/new.html.erb +14 -1
  171. data/app/views/alchemy/admin/pages/_current_page.html.erb +3 -1
  172. data/app/views/alchemy/admin/pages/_form.html.erb +21 -9
  173. data/app/views/alchemy/admin/pages/_page_status.html.erb +1 -1
  174. data/app/views/alchemy/admin/pages/_publication_fields.html.erb +28 -26
  175. data/app/views/alchemy/admin/pages/_table.html.erb +0 -7
  176. data/app/views/alchemy/admin/pages/_toolbar.html.erb +3 -5
  177. data/app/views/alchemy/admin/pages/edit.html.erb +5 -11
  178. data/app/views/alchemy/admin/pages/flush.turbo_stream.erb +2 -0
  179. data/app/views/alchemy/admin/pages/fold.turbo_stream.erb +5 -0
  180. data/app/views/alchemy/admin/pages/index.html.erb +5 -3
  181. data/app/views/alchemy/admin/pages/new.html.erb +2 -12
  182. data/app/views/alchemy/admin/pages/publish.turbo_stream.erb +12 -0
  183. data/app/views/alchemy/admin/pages/tree.html.erb +13 -0
  184. data/app/views/alchemy/admin/pages/update.turbo_stream.erb +5 -16
  185. data/app/views/alchemy/admin/partials/_flash_notices.html.erb +1 -1
  186. data/app/views/alchemy/admin/partials/{_remote_search_form.html.erb → _overlay_search_form.html.erb} +1 -2
  187. data/app/views/alchemy/admin/partials/_paste_from_clipboard_form.html.erb +12 -0
  188. data/app/views/alchemy/admin/pictures/_archive_overlay.html.erb +24 -21
  189. data/app/views/alchemy/admin/pictures/_filter_and_size_bar.html.erb +18 -26
  190. data/app/views/alchemy/admin/pictures/_picture.html.erb +11 -15
  191. data/app/views/alchemy/admin/pictures/_picture_to_assign.html.erb +3 -6
  192. data/app/views/alchemy/admin/pictures/_tag_list.html.erb +2 -3
  193. data/app/views/alchemy/admin/pictures/index.html.erb +0 -1
  194. data/app/views/alchemy/admin/pictures/update.turbo_stream.erb +1 -1
  195. data/app/views/alchemy/admin/resources/_resource_usage_info.html.erb +1 -1
  196. data/app/views/alchemy/admin/resources/_tag_list.html.erb +2 -3
  197. data/app/views/alchemy/admin/styleguide/index.html.erb +25 -20
  198. data/app/views/alchemy/admin/tags/edit.html.erb +1 -1
  199. data/app/views/alchemy/admin/tinymce/_setup.html.erb +2 -2
  200. data/app/views/alchemy/admin/uploader/_button.html.erb +1 -15
  201. data/app/views/alchemy/attachments/show.html.erb +1 -1
  202. data/app/views/alchemy/base/permission_denied.js.erb +1 -1
  203. data/app/views/alchemy/ingredients/shared/_anchor.html.erb +9 -7
  204. data/app/views/alchemy/ingredients/shared/_link_tools.html.erb +12 -5
  205. data/app/views/alchemy/ingredients/shared/_picture_tools.html.erb +10 -11
  206. data/app/views/alchemy/language_links/_spacer.html.erb +1 -1
  207. data/app/views/alchemy/messages_mailer/new.html.erb +1 -1
  208. data/app/views/alchemy/welcome.html.erb +1 -1
  209. data/config/locales/alchemy.en.yml +12 -2
  210. data/config/routes.rb +2 -2
  211. data/db/migrate/20230123112425_add_searchable_to_alchemy_pages.rb +1 -1
  212. data/db/migrate/20230505132743_add_indexes_to_alchemy_pictures.rb +1 -1
  213. data/db/migrate/20231113104432_create_page_mutexes.rb +1 -1
  214. data/db/migrate/20240314105244_create_alchemy_picture_descriptions.rb +1 -1
  215. data/db/migrate/20250626160259_add_unique_index_to_picture_descriptions.rb +1 -1
  216. data/db/migrate/20250905140323_add_created_at_index_to_pictures_and_attachments.rb +1 -1
  217. data/db/migrate/20251106150010_convert_select_value_for_multiple.rb +11 -0
  218. data/db/migrate/20260102121232_add_metadata_to_page_versions.rb +9 -0
  219. data/db/migrate/20260115164704_add_publication_timestamps_to_alchemy_elements.rb +30 -0
  220. data/db/migrate/20260115164705_add_index_to_element_publication_timestamps.rb +13 -0
  221. data/lib/alchemy/ability_helper.rb +1 -3
  222. data/lib/alchemy/auth_accessors.rb +51 -117
  223. data/lib/alchemy/configuration.rb +1 -0
  224. data/lib/alchemy/configurations/main.rb +63 -0
  225. data/lib/alchemy/controller_actions.rb +1 -1
  226. data/lib/alchemy/engine.rb +9 -12
  227. data/lib/alchemy/error_tracking/error_logger.rb +1 -1
  228. data/lib/alchemy/errors.rb +1 -1
  229. data/lib/alchemy/logger.rb +34 -4
  230. data/lib/alchemy/name_conversions.rb +0 -6
  231. data/lib/alchemy/seeder.rb +2 -2
  232. data/lib/alchemy/tasks/usage.rb +4 -4
  233. data/lib/alchemy/test_support/factories/page_version_factory.rb +3 -0
  234. data/lib/alchemy/test_support/having_picture_thumbnails_examples.rb +30 -0
  235. data/lib/alchemy/test_support/shared_ingredient_editor_examples.rb +26 -6
  236. data/lib/alchemy/test_support/shared_publishable_examples.rb +114 -0
  237. data/lib/alchemy/upgrader/eight_one.rb +56 -0
  238. data/lib/alchemy/upgrader.rb +9 -1
  239. data/lib/alchemy/version.rb +1 -1
  240. data/lib/alchemy.rb +1 -4
  241. data/lib/alchemy_cms.rb +0 -1
  242. data/lib/generators/alchemy/elements/templates/view.html.erb +3 -3
  243. data/lib/generators/alchemy/ingredient/ingredient_generator.rb +6 -8
  244. data/lib/generators/alchemy/ingredient/templates/editor_component.rb.tt +22 -0
  245. data/lib/generators/alchemy/page_layouts/templates/layout.html.erb +1 -1
  246. data/lib/generators/alchemy/site_layouts/templates/layout.html.erb +1 -1
  247. data/lib/tasks/alchemy/upgrade.rake +21 -7
  248. data/vendor/javascript/shoelace.min.js +713 -31
  249. data/vendor/javascript/tinymce.min.js +1 -1
  250. metadata +103 -83
  251. data/app/decorators/alchemy/element_editor.rb +0 -90
  252. data/app/helpers/alchemy/admin/pictures_helper.rb +0 -14
  253. data/app/javascript/alchemy_admin/file_editors.js +0 -28
  254. data/app/javascript/alchemy_admin/image_loader.js +0 -54
  255. data/app/javascript/alchemy_admin/page_sorter.js +0 -71
  256. data/app/javascript/alchemy_admin/sitemap.js +0 -154
  257. data/app/javascript/alchemy_admin/templates/page_folder.hbs +0 -3
  258. data/app/views/alchemy/admin/attachments/archive_overlay.js.erb +0 -4
  259. data/app/views/alchemy/admin/pages/_page.html.erb +0 -163
  260. data/app/views/alchemy/admin/pages/_sitemap.html.erb +0 -30
  261. data/app/views/alchemy/admin/pages/flush.js.erb +0 -2
  262. data/app/views/alchemy/admin/pictures/archive_overlay.js.erb +0 -5
  263. data/app/views/alchemy/admin/pictures/index.js.erb +0 -2
  264. data/app/views/alchemy/ingredients/_audio_editor.html.erb +0 -5
  265. data/app/views/alchemy/ingredients/_boolean_editor.html.erb +0 -11
  266. data/app/views/alchemy/ingredients/_datetime_editor.html.erb +0 -20
  267. data/app/views/alchemy/ingredients/_file_editor.html.erb +0 -52
  268. data/app/views/alchemy/ingredients/_headline_editor.html.erb +0 -44
  269. data/app/views/alchemy/ingredients/_html_editor.html.erb +0 -8
  270. data/app/views/alchemy/ingredients/_link_editor.html.erb +0 -30
  271. data/app/views/alchemy/ingredients/_node_editor.html.erb +0 -13
  272. data/app/views/alchemy/ingredients/_number_editor.html.erb +0 -24
  273. data/app/views/alchemy/ingredients/_page_editor.html.erb +0 -13
  274. data/app/views/alchemy/ingredients/_picture_editor.html.erb +0 -59
  275. data/app/views/alchemy/ingredients/_richtext_editor.html.erb +0 -15
  276. data/app/views/alchemy/ingredients/_select_editor.html.erb +0 -31
  277. data/app/views/alchemy/ingredients/_text_editor.html.erb +0 -29
  278. data/app/views/alchemy/ingredients/_video_editor.html.erb +0 -5
  279. data/lib/generators/alchemy/ingredient/templates/editor.html.erb +0 -14
  280. /data/{lib → app/models}/alchemy/permissions.rb +0 -0
@@ -0,0 +1,222 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ module Ingredients
5
+ class BaseEditor < ViewComponent::Base
6
+ delegate :definition,
7
+ :element,
8
+ :id,
9
+ :linked?,
10
+ :partial_name,
11
+ :role,
12
+ :settings,
13
+ :value,
14
+ to: :ingredient
15
+
16
+ delegate :alchemy,
17
+ :can?,
18
+ :cannot?,
19
+ :dom_id,
20
+ :hint_with_tooltip,
21
+ :render_hint_for,
22
+ :render_icon,
23
+ :warning,
24
+ to: :helpers
25
+
26
+ attr_reader :ingredient, :html_options
27
+
28
+ def initialize(ingredient, html_options: {})
29
+ raise ArgumentError, "Ingredient missing!" if ingredient.nil?
30
+
31
+ @ingredient = ingredient
32
+ @html_options = html_options
33
+ end
34
+
35
+ def call
36
+ tag.div(class: css_classes, data: data_attributes, id: dom_id(ingredient)) do
37
+ concat ingredient_id_field
38
+ concat ingredient_label
39
+ concat input_field
40
+ end
41
+ end
42
+
43
+ # Returns a string to be passed to Rails form field helpers.
44
+ #
45
+ # === Example:
46
+ #
47
+ # <%= text_field_tag text_editor.form_field_name, text_editor.value %>
48
+ #
49
+ # === Options:
50
+ #
51
+ # You can pass an Ingredient column_name. Default is 'value'
52
+ #
53
+ # ==== Example:
54
+ #
55
+ # <%= text_field_tag text_editor.form_field_name(:link), text_editor.value %>
56
+ #
57
+ def form_field_name(column = "value")
58
+ "element[ingredients_attributes][#{form_field_counter}][#{column}]"
59
+ end
60
+
61
+ # Returns a unique string to be passed to a form field id.
62
+ #
63
+ # @param column [String] A Ingredient column_name. Default is 'value'
64
+ #
65
+ def form_field_id(column = "value")
66
+ "element_#{element.id}_ingredient_#{ingredient.id}_#{column}"
67
+ end
68
+
69
+ private
70
+
71
+ def editable?
72
+ @_editable ||= can?(:edit, ingredient)
73
+ end
74
+
75
+ # Returns the translated role for displaying in labels
76
+ #
77
+ # Translate it in your locale yml file:
78
+ #
79
+ # alchemy:
80
+ # ingredient_roles:
81
+ # foo: Bar
82
+ #
83
+ # Optionally you can scope your ingredient role to an element:
84
+ #
85
+ # alchemy:
86
+ # ingredient_roles:
87
+ # article:
88
+ # foo: Baz
89
+ #
90
+ def translated_role
91
+ Alchemy.t(
92
+ role,
93
+ scope: "ingredient_roles.#{element.name}",
94
+ default: Alchemy.t("ingredient_roles.#{role}", default: role.humanize)
95
+ )
96
+ end
97
+
98
+ def css_classes
99
+ [
100
+ "ingredient-editor",
101
+ partial_name,
102
+ ingredient.deprecated? ? "deprecated" : nil,
103
+ settings[:linkable] ? "linkable" : nil,
104
+ settings[:anchor] ? "with-anchor" : nil
105
+ ].compact
106
+ end
107
+
108
+ def data_attributes
109
+ {
110
+ ingredient_id: ingredient.id,
111
+ ingredient_role: role
112
+ }
113
+ end
114
+
115
+ def has_warnings?
116
+ definition.blank? || ingredient.deprecated?
117
+ end
118
+
119
+ def warnings
120
+ return unless has_warnings?
121
+
122
+ if definition.blank?
123
+ Logger.warn("ingredient '#{role}' is missing its definition! Please check your element definitions.")
124
+ Alchemy.t(:ingredient_definition_missing)
125
+ else
126
+ definition.deprecation_notice(element_name: element&.name)
127
+ end
128
+ end
129
+
130
+ def validations
131
+ definition.validate
132
+ end
133
+
134
+ def format_validation
135
+ format = validations.select { _1.is_a?(Hash) }.find { _1[:format] }&.fetch(:format)
136
+ return nil unless format
137
+
138
+ # If format is a string or symbol, resolve it from config format_matchers
139
+ if format.is_a?(String) || format.is_a?(Symbol)
140
+ Alchemy.config.format_matchers.get(format)
141
+ else
142
+ format
143
+ end
144
+ end
145
+
146
+ def length_validation
147
+ validations.select { _1.is_a?(Hash) }.find { _1[:length] }&.fetch(:length)
148
+ end
149
+
150
+ def presence_validation?
151
+ validations.any? do |validation|
152
+ case validation
153
+ when :presence, "presence"
154
+ true
155
+ when Hash
156
+ validation[:presence] == true || validation["presence"] == true
157
+ else
158
+ false
159
+ end
160
+ end
161
+ end
162
+
163
+ def form_field_counter
164
+ element.definition.ingredients.index { |i| i.role == role }
165
+ end
166
+
167
+ # Renders the translated role of ingredient.
168
+ #
169
+ # Displays a warning icon if ingredient is missing its definition.
170
+ #
171
+ # Displays a mandatory field indicator, if the ingredient has validations.
172
+ #
173
+ def ingredient_role
174
+ content = translated_role
175
+
176
+ if has_warnings?
177
+ icon = hint_with_tooltip(warnings)
178
+ content = "#{icon} #{content}".html_safe
179
+ end
180
+
181
+ if ingredient.has_validations?
182
+ "#{content}<span class='validation_indicator'>*</span>".html_safe
183
+ else
184
+ content
185
+ end
186
+ end
187
+
188
+ # Renders the label and hint for a ingredient.
189
+ def ingredient_label(column = :value)
190
+ label_tag form_field_id(column) do
191
+ concat ingredient_role
192
+ concat render_hint_for(ingredient, size: "1x", fixed_width: false)
193
+ end
194
+ end
195
+
196
+ # Renders a hidden field with the ingredient's ID.
197
+ # This allows Rails to identify which ingredient to update
198
+ # when processing nested attributes.
199
+ #
200
+ def ingredient_id_field
201
+ hidden_field_tag(form_field_name(:id), ingredient.id, id: nil)
202
+ end
203
+
204
+ # Renders the input field for the ingredient.
205
+ # Override this method in subclasses to provide custom input fields.
206
+ # For example a text area or a select box.
207
+ #
208
+ def input_field
209
+ tag.div(class: "input-field") do
210
+ text_field_tag(form_field_name,
211
+ value,
212
+ class: "full_width",
213
+ id: form_field_id,
214
+ minlength: length_validation&.fetch(:minimum, nil),
215
+ maxlength: length_validation&.fetch(:maximum, nil),
216
+ required: presence_validation?,
217
+ pattern: format_validation)
218
+ end
219
+ end
220
+ end
221
+ end
222
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ module Ingredients
5
+ class BooleanEditor < BaseEditor
6
+ def call
7
+ tag.div(class: css_classes, data: data_attributes, id: dom_id(ingredient)) do
8
+ concat ingredient_id_field
9
+ concat label_tag(nil, for: form_field_id) {
10
+ safe_join([
11
+ hidden_field_tag(form_field_name, "0", id: nil),
12
+ check_box_tag(form_field_name, "1", value, id: form_field_id, disabled: !editable?),
13
+ ingredient_role,
14
+ render_hint_for(ingredient, size: "1x", fixed_width: false)
15
+ ])
16
+ }
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ module Ingredients
5
+ class ColorEditor < BaseEditor
6
+ def input_field
7
+ content_tag("alchemy-color-select") do
8
+ if colors.none?
9
+ concat text_field_tag(
10
+ form_field_name,
11
+ value,
12
+ disabled: !editable?
13
+ )
14
+ concat color_field_tag(nil, value, disabled: disabled?)
15
+ else
16
+ concat select_tag(
17
+ form_field_name,
18
+ color_options.join.html_safe,
19
+ disabled: !editable?
20
+ )
21
+ concat color_field_tag(form_field_name, value, disabled: disabled?)
22
+ end
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def color_value
29
+ @_color_value ||= begin
30
+ current_value = value.presence || settings[:default]
31
+ if current_value.present?
32
+ unknown_color = with_custom_color? && colors.none? { color_settings(_1)[:value] == current_value }
33
+ unknown_color ? "custom_color" : current_value
34
+ elsif with_custom_color?
35
+ "custom_color"
36
+ end
37
+ end
38
+ end
39
+
40
+ def color_options
41
+ colors.map do |color|
42
+ name, value, swatch = color_settings(color).values_at(:name, :value, :swatch)
43
+ selected = value == color_value
44
+ tag.option(name, value:, selected:, data: {swatch:})
45
+ end.tap do |option_tags|
46
+ if with_custom_color?
47
+ option_tags << tag.option(
48
+ Alchemy.t(:custom_color),
49
+ value: "custom_color",
50
+ selected: color_value == "custom_color"
51
+ )
52
+ end
53
+ end
54
+ end
55
+
56
+ def color_settings(color)
57
+ case color
58
+ when Hash
59
+ color.symbolize_keys.tap { |c| c[:swatch] ||= c[:value] }
60
+ when Array
61
+ {name: color[0], value: color[1], swatch: color[2] || color[1]}
62
+ else
63
+ {name: color, value: color, swatch: color}
64
+ end
65
+ end
66
+
67
+ def colors
68
+ @_colors ||= settings.fetch(:colors, [])
69
+ end
70
+
71
+ def with_custom_color?
72
+ settings[:custom_color]
73
+ end
74
+
75
+ def disabled?
76
+ !editable? || (color_value != "custom_color" && colors.present?)
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,13 @@
1
+ module Alchemy
2
+ module Ingredients
3
+ class ColorView < BaseView
4
+ def call
5
+ value
6
+ end
7
+
8
+ def render?
9
+ !value.nil?
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ module Ingredients
5
+ class DatetimeEditor < BaseEditor
6
+ delegate :alchemy_datepicker, to: :helpers
7
+
8
+ def input_field
9
+ tag.div(class: "input-field") do
10
+ concat alchemy_datepicker(
11
+ ingredient, :value, {
12
+ name: form_field_name,
13
+ id: form_field_id,
14
+ value: value,
15
+ type: settings[:input_type],
16
+ disabled: !editable?
17
+ }
18
+ )
19
+ concat tag.label(
20
+ render_icon(:calendar),
21
+ for: form_field_id,
22
+ class: "ingredient-date--label"
23
+ )
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ module Ingredients
5
+ class FileEditor < BaseEditor
6
+ delegate :attachment, to: :ingredient
7
+ delegate :link_to_dialog, to: :helpers
8
+
9
+ def input_field
10
+ content_tag("alchemy-file-editor", class: "file") do
11
+ concat tag.div(
12
+ render_icon(attachment&.icon_css_class),
13
+ class: "file_icon"
14
+ )
15
+ concat tag.div(attachment&.name, class: "file_name")
16
+ if editable?
17
+ concat link_to(
18
+ render_icon(:times), "#",
19
+ class: [
20
+ "remove_file_link",
21
+ attachment ? nil : "hidden"
22
+ ],
23
+ data: {
24
+ form_field_id: form_field_id(:attachment_id)
25
+ }
26
+ )
27
+ end
28
+ concat(
29
+ tag.div(class: "file_tools") do
30
+ concat dialog_link
31
+ concat link_to_dialog(
32
+ render_icon(:edit),
33
+ editable? ? alchemy.edit_admin_ingredient_path(ingredient) : nil,
34
+ {
35
+ title: Alchemy.t(:edit_file_properties),
36
+ size: "400x215"
37
+ },
38
+ title: editable? ? Alchemy.t(:edit_file_properties) : nil
39
+ )
40
+ end
41
+ )
42
+ concat hidden_field_tag(form_field_name(:attachment_id),
43
+ attachment&.id,
44
+ id: form_field_id(:attachment_id))
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def dialog_link
51
+ link_to_dialog(
52
+ render_icon("file-add"),
53
+ editable? ? alchemy.admin_attachments_path(
54
+ form_field_id: form_field_id(:attachment_id),
55
+ only: Array(settings[:only]),
56
+ except: Array(settings[:except])
57
+ ) : nil,
58
+ {
59
+ title: Alchemy.t(:assign_file),
60
+ size: "780x585",
61
+ padding: false
62
+ },
63
+ class: "file_icon",
64
+ title: editable? ? Alchemy.t(:assign_file) : nil
65
+ )
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ module Ingredients
5
+ class HeadlineEditor < BaseEditor
6
+ delegate :level, :levels, :size, :sizes, to: :ingredient
7
+
8
+ def input_field
9
+ tag.div(class: "input-field") do
10
+ concat text_field_tag(form_field_name,
11
+ value,
12
+ minlength: length_validation&.fetch(:minimum, nil),
13
+ maxlength: length_validation&.fetch(:maximum, nil),
14
+ readonly: !editable?,
15
+ required: presence_validation?,
16
+ pattern: format_validation,
17
+ id: form_field_id)
18
+
19
+ if settings[:anchor]
20
+ concat render(
21
+ "alchemy/ingredients/shared/anchor",
22
+ ingredient:
23
+ )
24
+ end
25
+
26
+ concat(
27
+ tag.div(class: ["input-addon", "right", has_size_select? ? "second" : nil].compact) do
28
+ content_tag("sl-tooltip",
29
+ content: ingredient.class.human_attribute_name(:level),
30
+ disabled: !editable?) do
31
+ select_tag(
32
+ form_field_name(:level),
33
+ options_for_select(level_options, level),
34
+ class: "custom-select",
35
+ disabled: !has_level_select? || !editable?
36
+ )
37
+ end
38
+ end
39
+ )
40
+
41
+ if has_size_select?
42
+ concat(
43
+ tag.div(class: "input-addon right") do
44
+ content_tag("sl-tooltip",
45
+ content: ingredient.class.human_attribute_name(:size),
46
+ disabled: !editable?) do
47
+ select_tag(
48
+ form_field_name(:size),
49
+ options_for_select(size_options, size),
50
+ class: "custom-select",
51
+ disabled: !editable?
52
+ )
53
+ end
54
+ end
55
+ )
56
+ end
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ def css_classes
63
+ super + [
64
+ has_level_select? ? "with-level-select" : nil,
65
+ has_size_select? ? "with-size-select" : nil
66
+ ].compact
67
+ end
68
+
69
+ def level_options
70
+ levels.map { |level| ["H#{level}", level] }
71
+ end
72
+
73
+ def size_options
74
+ sizes.map do |size|
75
+ case size
76
+ when Array
77
+ size
78
+ else
79
+ [".h#{size}", size]
80
+ end
81
+ end
82
+ end
83
+
84
+ def has_level_select? = level_options.many?
85
+ def has_size_select? = size_options.many?
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ module Ingredients
5
+ class HtmlEditor < BaseEditor
6
+ def input_field
7
+ text_area_tag(form_field_name, value, id: form_field_id, readonly: !editable?)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ module Ingredients
5
+ class LinkEditor < BaseEditor
6
+ def input_field
7
+ tag.div(class: "input-field") do
8
+ concat text_field_tag(form_field_name,
9
+ value,
10
+ class: "thin_border text_with_icon readonly",
11
+ id: form_field_id,
12
+ "data-link-value": true,
13
+ minlength: length_validation&.fetch(:minimum, nil),
14
+ maxlength: length_validation&.fetch(:maximum, nil),
15
+ required: presence_validation?,
16
+ pattern: format_validation,
17
+ readonly: true,
18
+ tabindex: -1)
19
+ concat hidden_field_tag(form_field_name(:link_title), ingredient.link_title, "data-link-title": true, id: nil)
20
+ concat hidden_field_tag(form_field_name(:link_class_name), ingredient.link_class_name, "data-link-class": true, id: nil)
21
+ concat hidden_field_tag(form_field_name(:link_target), ingredient.link_target, "data-link-target": true, id: nil)
22
+ concat render("alchemy/ingredients/shared/link_tools",
23
+ ingredient:,
24
+ wrapper_class: "ingredient_link_buttons")
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ module Ingredients
5
+ class NodeEditor < BaseEditor
6
+ delegate :node, :page, to: :ingredient
7
+
8
+ def input_field
9
+ render Alchemy::Admin::NodeSelect.new(
10
+ node,
11
+ url: alchemy.api_nodes_path(language_id: page&.language_id, include: :ancestors),
12
+ query_params: settings.fetch(:query_params, {})
13
+ ) do
14
+ text_field_tag form_field_name(:node_id),
15
+ node&.id,
16
+ id: form_field_id(:node_id),
17
+ class: "alchemy_selectbox full_width",
18
+ disabled: !editable?
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ module Ingredients
5
+ class NumberEditor < BaseEditor
6
+ def input_field
7
+ tag.div(class: "input-field") do
8
+ concat text_field_tag(form_field_name,
9
+ value,
10
+ type: settings[:input_type] || "number",
11
+ required: presence_validation?,
12
+ step: settings[:step],
13
+ min: settings[:min],
14
+ max: settings[:max],
15
+ id: form_field_id,
16
+ readonly: !editable?,
17
+ oninput: (settings[:input_type] == "range") ? "this.nextElementSibling.value = this.value" : nil)
18
+ if settings[:input_type] == "range"
19
+ concat tag.output(value)
20
+ end
21
+ if settings[:unit]
22
+ concat tag.div(settings[:unit], class: "input-addon right")
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ module Ingredients
5
+ class PageEditor < BaseEditor
6
+ delegate :page, to: :ingredient
7
+
8
+ def input_field
9
+ render Alchemy::Admin::PageSelect.new(page, allow_clear: true, query_params: settings[:query_params]) do
10
+ text_field_tag form_field_name(:page_id),
11
+ page&.id,
12
+ id: form_field_id(:page_id),
13
+ class: "full_width",
14
+ disabled: !editable?
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end