alchemy_cms 8.0.10 → 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 (283) 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 +214 -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 +17 -13
  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 +36 -42
  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/uploader.js +30 -0
  85. data/app/javascript/alchemy_admin/image_overlay.js +0 -2
  86. data/app/javascript/alchemy_admin/initializer.js +0 -3
  87. data/app/javascript/alchemy_admin/link_dialog.js +1 -6
  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 +13 -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/services/alchemy/update_checks/alchemy_app.rb +1 -1
  126. data/app/stylesheets/alchemy/_extends.scss +3 -9
  127. data/app/stylesheets/alchemy/_mixins.scss +3 -1
  128. data/app/stylesheets/alchemy/_themes.scss +19 -10
  129. data/app/stylesheets/alchemy/admin/archive.scss +1 -0
  130. data/app/stylesheets/alchemy/admin/base.scss +5 -2
  131. data/app/stylesheets/alchemy/admin/buttons.scss +3 -3
  132. data/app/stylesheets/alchemy/admin/element-select.scss +18 -0
  133. data/app/stylesheets/alchemy/admin/elements.scss +123 -23
  134. data/app/stylesheets/alchemy/admin/errors.scss +1 -1
  135. data/app/stylesheets/alchemy/admin/flash.scss +6 -4
  136. data/app/stylesheets/alchemy/admin/images.scss +9 -5
  137. data/app/stylesheets/alchemy/admin/list_filter.scss +4 -4
  138. data/app/stylesheets/alchemy/admin/navigation.scss +1 -1
  139. data/app/stylesheets/alchemy/admin/notices.scss +1 -2
  140. data/app/stylesheets/alchemy/admin/selects.scss +36 -21
  141. data/app/stylesheets/alchemy/admin/shoelace.scss +14 -1
  142. data/app/stylesheets/alchemy/admin/sitemap.scss +11 -3
  143. data/app/stylesheets/alchemy/admin/tags.scss +3 -1
  144. data/app/stylesheets/alchemy/admin/toolbar.scss +1 -1
  145. data/app/views/alchemy/_edit_mode.html.erb +1 -1
  146. data/app/views/alchemy/_menubar.html.erb +1 -1
  147. data/app/views/alchemy/admin/attachments/_archive_overlay.html.erb +35 -31
  148. data/app/views/alchemy/admin/attachments/_library_sidebar.html.erb +6 -0
  149. data/app/views/alchemy/admin/attachments/_overlay_file_list.html.erb +1 -1
  150. data/app/views/alchemy/admin/attachments/_replace_button.html.erb +1 -8
  151. data/app/views/alchemy/admin/attachments/_sorting_select.html.erb +13 -0
  152. data/app/views/alchemy/admin/attachments/_tag_list.html.erb +2 -3
  153. data/app/views/alchemy/admin/attachments/index.html.erb +5 -11
  154. data/app/views/alchemy/admin/attachments/show.html.erb +1 -1
  155. data/app/views/alchemy/admin/clipboard/_button.html.erb +1 -0
  156. data/app/views/alchemy/admin/clipboard/index.html.erb +4 -5
  157. data/app/views/alchemy/admin/clipboard/insert.turbo_stream.erb +1 -1
  158. data/app/views/alchemy/admin/crop.html.erb +5 -7
  159. data/app/views/alchemy/admin/dashboard/widgets/_locked_pages.html.erb +1 -1
  160. data/app/views/alchemy/admin/elements/_add_nested_element_form.html.erb +6 -6
  161. data/app/views/alchemy/admin/elements/_fixed_element.html.erb +1 -1
  162. data/app/views/alchemy/admin/elements/_footer.html.erb +7 -1
  163. data/app/views/alchemy/admin/elements/_header.html.erb +5 -5
  164. data/app/views/alchemy/admin/elements/_toolbar.html.erb +33 -8
  165. data/app/views/alchemy/admin/elements/create.turbo_stream.erb +10 -10
  166. data/app/views/alchemy/admin/elements/index.html.erb +29 -16
  167. data/app/views/alchemy/admin/elements/new.html.erb +2 -2
  168. data/app/views/alchemy/admin/ingredients/update.turbo_stream.erb +3 -5
  169. data/app/views/alchemy/admin/leave.html.erb +1 -1
  170. data/app/views/alchemy/admin/nodes/_node.html.erb +19 -0
  171. data/app/views/alchemy/admin/nodes/edit.html.erb +1 -1
  172. data/app/views/alchemy/admin/nodes/index.html.erb +3 -1
  173. data/app/views/alchemy/admin/nodes/new.html.erb +14 -1
  174. data/app/views/alchemy/admin/pages/_current_page.html.erb +3 -1
  175. data/app/views/alchemy/admin/pages/_form.html.erb +21 -9
  176. data/app/views/alchemy/admin/pages/_page_status.html.erb +1 -1
  177. data/app/views/alchemy/admin/pages/_publication_fields.html.erb +28 -26
  178. data/app/views/alchemy/admin/pages/_table.html.erb +0 -7
  179. data/app/views/alchemy/admin/pages/_toolbar.html.erb +3 -6
  180. data/app/views/alchemy/admin/pages/edit.html.erb +5 -11
  181. data/app/views/alchemy/admin/pages/flush.turbo_stream.erb +2 -0
  182. data/app/views/alchemy/admin/pages/fold.turbo_stream.erb +5 -0
  183. data/app/views/alchemy/admin/pages/index.html.erb +5 -3
  184. data/app/views/alchemy/admin/pages/new.html.erb +2 -12
  185. data/app/views/alchemy/admin/pages/publish.turbo_stream.erb +12 -0
  186. data/app/views/alchemy/admin/pages/tree.html.erb +13 -0
  187. data/app/views/alchemy/admin/pages/update.turbo_stream.erb +5 -16
  188. data/app/views/alchemy/admin/partials/_flash_notices.html.erb +1 -1
  189. data/app/views/alchemy/admin/partials/{_remote_search_form.html.erb → _overlay_search_form.html.erb} +1 -2
  190. data/app/views/alchemy/admin/partials/_paste_from_clipboard_form.html.erb +12 -0
  191. data/app/views/alchemy/admin/pictures/_archive_overlay.html.erb +24 -21
  192. data/app/views/alchemy/admin/pictures/_filter_and_size_bar.html.erb +18 -26
  193. data/app/views/alchemy/admin/pictures/_picture.html.erb +11 -15
  194. data/app/views/alchemy/admin/pictures/_picture_to_assign.html.erb +3 -6
  195. data/app/views/alchemy/admin/pictures/_tag_list.html.erb +2 -3
  196. data/app/views/alchemy/admin/pictures/index.html.erb +0 -1
  197. data/app/views/alchemy/admin/pictures/update.turbo_stream.erb +1 -1
  198. data/app/views/alchemy/admin/resources/_resource_usage_info.html.erb +1 -1
  199. data/app/views/alchemy/admin/resources/_tag_list.html.erb +2 -3
  200. data/app/views/alchemy/admin/styleguide/index.html.erb +25 -20
  201. data/app/views/alchemy/admin/tags/edit.html.erb +1 -1
  202. data/app/views/alchemy/admin/tinymce/_setup.html.erb +2 -2
  203. data/app/views/alchemy/admin/uploader/_button.html.erb +1 -15
  204. data/app/views/alchemy/attachments/show.html.erb +1 -1
  205. data/app/views/alchemy/base/permission_denied.js.erb +1 -1
  206. data/app/views/alchemy/ingredients/shared/_anchor.html.erb +9 -7
  207. data/app/views/alchemy/ingredients/shared/_link_tools.html.erb +12 -5
  208. data/app/views/alchemy/ingredients/shared/_picture_tools.html.erb +10 -11
  209. data/app/views/alchemy/language_links/_spacer.html.erb +1 -1
  210. data/app/views/alchemy/messages_mailer/new.html.erb +1 -1
  211. data/app/views/alchemy/welcome.html.erb +1 -1
  212. data/config/locales/alchemy.en.yml +12 -3
  213. data/config/routes.rb +2 -2
  214. data/db/migrate/20230123112425_add_searchable_to_alchemy_pages.rb +1 -1
  215. data/db/migrate/20230505132743_add_indexes_to_alchemy_pictures.rb +1 -1
  216. data/db/migrate/20231113104432_create_page_mutexes.rb +1 -1
  217. data/db/migrate/20240314105244_create_alchemy_picture_descriptions.rb +1 -1
  218. data/db/migrate/20250626160259_add_unique_index_to_picture_descriptions.rb +1 -1
  219. data/db/migrate/20250905140323_add_created_at_index_to_pictures_and_attachments.rb +1 -1
  220. data/db/migrate/20251106150010_convert_select_value_for_multiple.rb +11 -0
  221. data/db/migrate/20260102121232_add_metadata_to_page_versions.rb +9 -0
  222. data/db/migrate/20260115164704_add_publication_timestamps_to_alchemy_elements.rb +30 -0
  223. data/db/migrate/20260115164705_add_index_to_element_publication_timestamps.rb +13 -0
  224. data/lib/alchemy/ability_helper.rb +1 -3
  225. data/lib/alchemy/auth_accessors.rb +51 -117
  226. data/lib/alchemy/configuration.rb +1 -0
  227. data/lib/alchemy/configurations/main.rb +63 -0
  228. data/lib/alchemy/controller_actions.rb +1 -1
  229. data/lib/alchemy/engine.rb +9 -12
  230. data/lib/alchemy/error_tracking/error_logger.rb +1 -1
  231. data/lib/alchemy/errors.rb +1 -1
  232. data/lib/alchemy/logger.rb +34 -4
  233. data/lib/alchemy/name_conversions.rb +0 -6
  234. data/lib/alchemy/seeder.rb +2 -2
  235. data/lib/alchemy/tasks/usage.rb +4 -4
  236. data/lib/alchemy/test_support/factories/page_version_factory.rb +3 -0
  237. data/lib/alchemy/test_support/having_picture_thumbnails_examples.rb +30 -0
  238. data/lib/alchemy/test_support/shared_ingredient_editor_examples.rb +26 -6
  239. data/lib/alchemy/test_support/shared_publishable_examples.rb +114 -0
  240. data/lib/alchemy/upgrader/eight_one.rb +56 -0
  241. data/lib/alchemy/upgrader.rb +9 -1
  242. data/lib/alchemy/version.rb +1 -1
  243. data/lib/alchemy.rb +1 -4
  244. data/lib/alchemy_cms.rb +0 -1
  245. data/lib/generators/alchemy/elements/templates/view.html.erb +3 -3
  246. data/lib/generators/alchemy/ingredient/ingredient_generator.rb +6 -8
  247. data/lib/generators/alchemy/ingredient/templates/editor_component.rb.tt +22 -0
  248. data/lib/generators/alchemy/page_layouts/templates/layout.html.erb +1 -1
  249. data/lib/generators/alchemy/site_layouts/templates/layout.html.erb +1 -1
  250. data/lib/tasks/alchemy/upgrade.rake +21 -7
  251. data/vendor/javascript/shoelace.min.js +713 -31
  252. data/vendor/javascript/tinymce.min.js +1 -1
  253. metadata +104 -84
  254. data/app/decorators/alchemy/element_editor.rb +0 -90
  255. data/app/helpers/alchemy/admin/pictures_helper.rb +0 -14
  256. data/app/javascript/alchemy_admin/file_editors.js +0 -28
  257. data/app/javascript/alchemy_admin/image_loader.js +0 -54
  258. data/app/javascript/alchemy_admin/page_sorter.js +0 -71
  259. data/app/javascript/alchemy_admin/sitemap.js +0 -154
  260. data/app/javascript/alchemy_admin/templates/page_folder.hbs +0 -3
  261. data/app/views/alchemy/admin/attachments/archive_overlay.js.erb +0 -4
  262. data/app/views/alchemy/admin/pages/_page.html.erb +0 -163
  263. data/app/views/alchemy/admin/pages/_sitemap.html.erb +0 -30
  264. data/app/views/alchemy/admin/pages/flush.js.erb +0 -2
  265. data/app/views/alchemy/admin/pictures/archive_overlay.js.erb +0 -5
  266. data/app/views/alchemy/admin/pictures/index.js.erb +0 -2
  267. data/app/views/alchemy/ingredients/_audio_editor.html.erb +0 -5
  268. data/app/views/alchemy/ingredients/_boolean_editor.html.erb +0 -11
  269. data/app/views/alchemy/ingredients/_datetime_editor.html.erb +0 -20
  270. data/app/views/alchemy/ingredients/_file_editor.html.erb +0 -52
  271. data/app/views/alchemy/ingredients/_headline_editor.html.erb +0 -44
  272. data/app/views/alchemy/ingredients/_html_editor.html.erb +0 -8
  273. data/app/views/alchemy/ingredients/_link_editor.html.erb +0 -30
  274. data/app/views/alchemy/ingredients/_node_editor.html.erb +0 -13
  275. data/app/views/alchemy/ingredients/_number_editor.html.erb +0 -24
  276. data/app/views/alchemy/ingredients/_page_editor.html.erb +0 -13
  277. data/app/views/alchemy/ingredients/_picture_editor.html.erb +0 -59
  278. data/app/views/alchemy/ingredients/_richtext_editor.html.erb +0 -15
  279. data/app/views/alchemy/ingredients/_select_editor.html.erb +0 -31
  280. data/app/views/alchemy/ingredients/_text_editor.html.erb +0 -29
  281. data/app/views/alchemy/ingredients/_video_editor.html.erb +0 -5
  282. data/lib/generators/alchemy/ingredient/templates/editor.html.erb +0 -14
  283. /data/{lib → app/models}/alchemy/permissions.rb +0 -0
@@ -1,90 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Alchemy
4
- class ElementEditor < SimpleDelegator
5
- alias_method :element, :__getobj__
6
-
7
- def to_partial_path
8
- "alchemy/admin/elements/element"
9
- end
10
-
11
- # Returns ingredient editor instances for defined ingredients
12
- #
13
- # Creates ingredient on demand if the ingredient is not yet present on the element
14
- #
15
- # @return Array<Alchemy::IngredientEditor>
16
- def ingredients
17
- ingredient_definitions.map do |ingredient|
18
- Alchemy::IngredientEditor.new(find_or_create_ingredient(ingredient))
19
- end
20
- end
21
-
22
- # Are any ingredients defined?
23
- # @return [Boolean]
24
- def has_ingredients_defined?
25
- ingredient_definitions.any?
26
- end
27
-
28
- # Returns the translated ingredient group for displaying in admin editor group headings
29
- #
30
- # Translate it in your locale yml file:
31
- #
32
- # alchemy:
33
- # element_groups:
34
- # foo: Bar
35
- #
36
- # Optionally you can scope your ingredient role to an element:
37
- #
38
- # alchemy:
39
- # element_groups:
40
- # article:
41
- # foo: Baz
42
- #
43
- def translated_group(group)
44
- Alchemy.t(
45
- group,
46
- scope: "element_groups.#{element.name}",
47
- default: Alchemy.t("element_groups.#{group}", default: group.humanize)
48
- )
49
- end
50
-
51
- # CSS classes for the element editor partial.
52
- def css_classes
53
- [
54
- "element-editor",
55
- ingredient_definitions.present? ? "with-ingredients" : "without-ingredients",
56
- nestable_elements.any? ? "nestable" : "not-nestable",
57
- taggable? ? "taggable" : "not-taggable",
58
- folded ? "folded" : "expanded",
59
- compact? ? "compact" : nil,
60
- deprecated? ? "deprecated" : nil,
61
- fixed? ? "is-fixed" : "not-fixed",
62
- public? ? nil : "element-hidden"
63
- ]
64
- end
65
-
66
- # Tells us, if we should show the element footer and form inputs.
67
- def editable?
68
- ingredient_definitions.any? || taggable?
69
- end
70
-
71
- # Fixes Rails partial renderer calling to_model on the object
72
- # which reveals the delegated element instead of this decorator.
73
- def respond_to?(*args, **kwargs)
74
- method_name = args.first
75
- return false if method_name == :to_model
76
-
77
- super
78
- end
79
-
80
- private
81
-
82
- def find_or_create_ingredient(definition)
83
- element.ingredients.detect { _1.role == definition.role } ||
84
- element.ingredients.create!(
85
- role: definition.role,
86
- type: Alchemy::Ingredient.normalize_type(definition.type)
87
- )
88
- end
89
- end
90
- end
@@ -1,14 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Alchemy
4
- module Admin
5
- module PicturesHelper
6
- def preview_size(size)
7
- Alchemy::Picture::THUMBNAIL_SIZES.fetch(
8
- size,
9
- Alchemy::Picture::THUMBNAIL_SIZES[:medium]
10
- )
11
- end
12
- end
13
- end
14
- end
@@ -1,28 +0,0 @@
1
- class FileEditor {
2
- constructor(container) {
3
- this.container = container
4
- this.deleteLink = container.querySelector(".remove_file_link")
5
- this.fileIcon = container.querySelector(".file_icon")
6
- this.fileName = container.querySelector(".file_name")
7
- this.deleteLink.addEventListener("click", this.removeFile.bind(this))
8
- this.formFieldId = this.deleteLink.dataset.formFieldId
9
- this.formField = container.querySelector(`#${this.formFieldId}`)
10
- this.assignFileText = this.deleteLink.dataset.assignFileText
11
- }
12
-
13
- removeFile(event) {
14
- event.stopPropagation()
15
- this.formField.value = ""
16
- this.fileIcon.innerHTML = ""
17
- this.fileName.innerHTML = ""
18
- this.deleteLink.classList.add("hidden")
19
- this.container.closest("alchemy-element-editor").setDirty(this.formField)
20
- return false
21
- }
22
- }
23
-
24
- export default function init(selector) {
25
- document.querySelectorAll(selector).forEach((node) => {
26
- new FileEditor(node)
27
- })
28
- }
@@ -1,54 +0,0 @@
1
- // Shows spinner while loading images and
2
- // fades the image after its been loaded
3
-
4
- import Spinner from "alchemy_admin/spinner"
5
-
6
- export default class ImageLoader {
7
- static init(scope = document) {
8
- if (typeof scope === "string") {
9
- scope = document.querySelector(scope)
10
- }
11
- scope.querySelectorAll("img").forEach((image) => {
12
- const loader = new ImageLoader(image)
13
- loader.load()
14
- })
15
- }
16
-
17
- constructor(image) {
18
- this.image = image
19
- this.parent = image.parentNode
20
- this.spinner = new Spinner("small")
21
- this.bind()
22
- }
23
-
24
- bind() {
25
- this.image.addEventListener("load", this.onLoaded.bind(this))
26
- this.image.addEventListener("error", this.onError.bind(this))
27
- }
28
-
29
- load(force = false) {
30
- if (!force && this.image.complete) return
31
-
32
- this.image.classList.add("loading")
33
- this.spinner.spin(this.parent)
34
- }
35
-
36
- onLoaded() {
37
- this.spinner.stop()
38
- this.image.classList.remove("loading")
39
- this.unbind()
40
- }
41
-
42
- onError(evt) {
43
- const message = `Could not load ${this.image.src}`
44
- this.spinner.stop()
45
- this.parent.innerHTML = `<alchemy-icon name="alert" class="error" title="${message}" />`
46
- console.error(message, evt)
47
- this.unbind()
48
- }
49
-
50
- unbind() {
51
- this.image.removeEventListener("load", this.onLoaded)
52
- this.image.removeEventListener("error", this.onError)
53
- }
54
- }
@@ -1,71 +0,0 @@
1
- import Sortable from "sortablejs"
2
- import { growl } from "alchemy_admin/growler"
3
- import { patch } from "alchemy_admin/utils/ajax"
4
- import pleaseWaitOverlay from "alchemy_admin/please_wait_overlay"
5
-
6
- function onSort(evt) {
7
- const pageId = evt.item.dataset.pageId
8
- const url = Alchemy.routes.move_admin_page_path(pageId)
9
- const data = {
10
- target_parent_id: evt.to.dataset.parentId,
11
- new_position: evt.newIndex
12
- }
13
-
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")
21
-
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
- }
34
- }
35
-
36
- export function displayPageFolders() {
37
- document.querySelectorAll("li.sitemap-item").forEach((el) => {
38
- const pageFolderEl = el.querySelector(".page_folder")
39
- const list = el.querySelector(".children")
40
- const page = {
41
- folded: el.dataset.folded === "true",
42
- id: el.dataset.pageId,
43
- type: el.dataset.type
44
- }
45
-
46
- if (list.children.length > 0 || page.folded) {
47
- pageFolderEl.outerHTML = Handlebars.templates["page_folder.hbs"]({ page })
48
- } else {
49
- pageFolderEl.innerHTML = ""
50
- }
51
- })
52
- }
53
-
54
- export function createSortables(sortables) {
55
- sortables.forEach((el) => {
56
- new Sortable(el, {
57
- group: "pages",
58
- animation: 150,
59
- fallbackOnBody: true,
60
- swapThreshold: 0.65,
61
- handle: ".handle",
62
- onSort
63
- })
64
- })
65
- }
66
-
67
- export default function () {
68
- const sortables = document.querySelectorAll("ul.children")
69
- displayPageFolders()
70
- createSortables(sortables)
71
- }
@@ -1,154 +0,0 @@
1
- // The admin sitemap Alchemy class
2
- import PageSorter from "alchemy_admin/page_sorter"
3
- import { on } from "alchemy_admin/utils/events"
4
- import { get, patch } from "alchemy_admin/utils/ajax"
5
- import { growl } from "alchemy_admin/growler"
6
- import { createSortables, displayPageFolders } from "alchemy_admin/page_sorter"
7
-
8
- export default class Sitemap {
9
- // Storing some objects.
10
- constructor(options) {
11
- const list_template_html = document
12
- .getElementById("sitemap-list")
13
- .innerHTML.replace(/__ID__/g, "{{id}}")
14
- this.search_field = document.querySelector(".search_input_field")
15
- this.filter_field_clear = document.querySelector(".search_field_clear")
16
- this.filter_field_clear.removeAttribute("href")
17
- this.display = document.getElementById("page_filter_result")
18
- this.sitemap_wrapper = document.getElementById("sitemap-wrapper")
19
- this.template = Handlebars.compile(
20
- document.getElementById("sitemap-template").innerHTML
21
- )
22
- this.list_template = Handlebars.compile(list_template_html)
23
- this.items = null
24
- this.options = options
25
- Handlebars.registerPartial("list", list_template_html)
26
- this.load(options.page_root_id)
27
- }
28
-
29
- // Loads the sitemap
30
- load(pageId) {
31
- const spinner = new Alchemy.Spinner("medium")
32
- const spinTarget = this.sitemap_wrapper
33
- spinTarget.innerHTML = ""
34
- spinner.spin(spinTarget)
35
- get(this.options.url, { id: pageId })
36
- .then(async (response) => {
37
- this.render(await response.data)
38
- this.handlePageFolders()
39
- spinner.stop()
40
- })
41
- .catch(this.errorHandler)
42
- }
43
-
44
- // Reloads the sitemap
45
- reload() {
46
- this.load(this.options.page_root_id)
47
- }
48
-
49
- // Watch page folder clicks and re-render the page branch
50
- handlePageFolders() {
51
- on(
52
- "click",
53
- "#sitemap",
54
- ".page_folder",
55
- function (evt) {
56
- const spinner = new Alchemy.Spinner("small")
57
- const pageFolder = evt.target.closest(".page_folder")
58
- const pageId = pageFolder.dataset.pageId
59
- pageFolder.innerHTML = ""
60
- spinner.spin(pageFolder)
61
-
62
- patch(Alchemy.routes.fold_admin_page_path(pageId))
63
- .then(async (response) => {
64
- this.reRender(pageId, await response.data)
65
- spinner.stop()
66
- })
67
- .catch(this.errorHandler)
68
- }.bind(this)
69
- )
70
- }
71
-
72
- // Renders the sitemap
73
- render(data) {
74
- const renderTarget = this.sitemap_wrapper
75
- const renderTemplate = this.template
76
-
77
- renderTarget.innerHTML = renderTemplate({ children: data.pages })
78
- this.items = document
79
- .getElementById("sitemap")
80
- .querySelectorAll(".sitemap_page")
81
- this.sitemap_wrapper = document.getElementById("sitemap-wrapper")
82
- this._observe()
83
- PageSorter()
84
- }
85
-
86
- reRender(pageId, data) {
87
- let pageEl = document.getElementById(`page_${pageId}`)
88
- pageEl.outerHTML = this.list_template({ children: data.pages })
89
- pageEl = document.getElementById(`page_${pageId}`)
90
- const sortables = pageEl.querySelectorAll("ul.children")
91
- createSortables(sortables)
92
- displayPageFolders()
93
- }
94
-
95
- // Filters the sitemap
96
- filter(term) {
97
- const results = []
98
-
99
- this.items.forEach(function (item) {
100
- if (
101
- term !== "" &&
102
- item.getAttribute("name").toLowerCase().indexOf(term) !== -1
103
- ) {
104
- item.classList.add("highlight")
105
- item.classList.remove("no-match")
106
- results.push(item)
107
- } else {
108
- item.classList.add("no-match")
109
- item.classList.remove("highlight")
110
- }
111
- })
112
- const { length } = results
113
-
114
- if (length === 1) {
115
- this.display.style.display = "block"
116
- this.display.innerText = `1 ${Alchemy.t("page_found")}`
117
- results[0].scrollIntoView({ behavior: "smooth", block: "center" })
118
- } else if (length > 1) {
119
- this.display.style.display = "block"
120
- this.display.innerText = `${length} ${Alchemy.t("pages_found")}`
121
- } else {
122
- this.items.forEach((item) =>
123
- item.classList.remove("no-match", "highlight")
124
- )
125
- this.display.style.display = "none"
126
- window.scrollTo({
127
- top: 0,
128
- left: 0,
129
- behavior: "smooth"
130
- })
131
- }
132
- }
133
-
134
- // Adds onkey up observer to search field
135
- _observe() {
136
- this.search_field.addEventListener("keyup", (evt) => {
137
- const term = evt.target.value
138
- this.filter_field_clear.style.visibility =
139
- term == "" ? "hidden" : "visible"
140
- this.filter(term.toLowerCase())
141
- })
142
- this.search_field.addEventListener("focus", () => key.setScope("search"))
143
- this.filter_field_clear.addEventListener("click", () => {
144
- this.search_field.value = ""
145
- this.filter("")
146
- return false
147
- })
148
- }
149
-
150
- errorHandler(error) {
151
- growl(error.message || error, "error")
152
- console.error(error)
153
- }
154
- }
@@ -1,3 +0,0 @@
1
- <a class="page_folder icon_button" data-page-id="{{ page.id }}">
2
- <alchemy-icon name="arrow-{{#if page.folded }}right{{else}}down{{/if}}-s"></alchemy-icon>
3
- </a>
@@ -1,4 +0,0 @@
1
- (function() {
2
- var dialog = Alchemy.currentDialog();
3
- dialog.dialog_body.html('<%= j render("archive_overlay") %>');
4
- })();
@@ -1,163 +0,0 @@
1
- <li id="page_{{id}}" class="sitemap-item {{page_layout}}" data-slug="{{slug}}" data-restricted="{{restricted}}" data-page-id="{{id}}" data-folded="{{folded}}">
2
- <div class="sitemap_page{{#if locked}} locked{{/if}}" name="{{name}}">
3
- <div class="sitemap_left_images">
4
- <span class="page_folder"></span>
5
- {{#if definition_missing}}
6
- <%= page_layout_missing_warning %>
7
- {{else}}
8
- {{#if permissions.edit_content}}
9
- <span class="page-icon {{#unless root}}handle{{/unless}}">
10
- {{#if locked}}
11
- <sl-tooltip content="{{locked_notice}}" class="like-hint-tooltip" placement="bottom-start">
12
- <alchemy-icon name="file-edit" size="xl"></alchemy-icon>
13
- </sl-tooltip>
14
- {{else}}
15
- <alchemy-icon name="file" size="xl"></alchemy-icon>
16
- {{/if}}
17
- </span>
18
- {{else}}
19
- <sl-tooltip content="<%= Alchemy.t("Your user role does not allow you to edit this page") %>" class="like-hint-tooltip" placement="bottom-start">
20
- <alchemy-icon name="file-forbid" size="xl"></alchemy-icon>
21
- </sl-tooltip>
22
- {{/if}}
23
- {{/if}}
24
- </div>
25
- <div class="sitemap_sitename">
26
- {{#if permissions.edit_content}}
27
- <%= link_to(
28
- "{{name}}",
29
- alchemy.edit_admin_page_path(id: "__ID__"),
30
- title: Alchemy.t(:edit_page),
31
- class: "sitemap_pagename_link"
32
- ) -%>
33
- {{else}}
34
- <%= content_tag("span", "{{name}}", class: "sitemap_pagename_link") %>
35
- {{/if}}
36
- </div>
37
- <div class="sitemap_url" title="{{url_path}}">
38
- {{ url_path }}
39
- </div>
40
- <div class="page_infos">
41
- {{#if locked}}
42
- <span class="page_status locked">
43
- <alchemy-icon name="edit" size="1x"></alchemy-icon>
44
- {{status_titles.locked}}
45
- </span>
46
- {{/if}}
47
- {{#if restricted}}
48
- <span class="page_status">
49
- <alchemy-icon name="lock" size="1x"></alchemy-icon>
50
- {{status_titles.restricted}}
51
- </span>
52
- {{/if}}
53
- {{#unless public}}
54
- <span class="page_status">
55
- <alchemy-icon name="cloud-off" size="1x"></alchemy-icon>
56
- {{status_titles.public}}
57
- </span>
58
- {{/unless}}
59
- </div>
60
- <div class="sitemap_right_tools">
61
- {{#if permissions.info}}
62
- <sl-tooltip content="<%= Alchemy.t(:page_infos) %>">
63
- <%= link_to_dialog(
64
- render_icon("information"),
65
- alchemy.info_admin_page_path(id: "__ID__"),
66
- {
67
- title: Alchemy.t(:page_infos),
68
- size: "520x290"
69
- },
70
- class: "icon_button"
71
- ) %>
72
- </sl-tooltip>
73
- {{else}}
74
- <div class="sitemap_tool disabled">
75
- <sl-tooltip content="<%= Alchemy.t("Your user role does not allow you to edit this page") %>" class="like-hint-tooltip" placement="bottom-start">
76
- <%= render_icon("information") %>
77
- </sl-tooltip>
78
- </div>
79
- {{/if}}
80
- {{#if permissions.configure}}
81
- <sl-tooltip content="<%= Alchemy.t(:edit_page_properties) %>">
82
- <%= link_to_dialog(
83
- render_icon("settings-3"),
84
- alchemy.configure_admin_page_path(id: "__ID__"),
85
- {
86
- title: Alchemy.t(:edit_page_properties),
87
- size: "500x680"
88
- },
89
- class: "icon_button"
90
- ) -%>
91
- </sl-tooltip>
92
- {{else}}
93
- <div class="sitemap_tool disabled">
94
- <sl-tooltip content="<%= Alchemy.t("Your user role does not allow you to edit this page") %>" class="like-hint-tooltip" placement="bottom-start">
95
- <%= render_icon("settings-3") %>
96
- </sl-tooltip>
97
- </div>
98
- {{/if}}
99
- {{#if permissions.copy}}
100
- <sl-tooltip content="<%= Alchemy.t(:copy_page) %>">
101
- <%= button_to(
102
- render_icon("file-copy"),
103
- alchemy.insert_admin_clipboard_path(
104
- remarkable_type: :pages,
105
- remarkable_id: "__ID__",
106
- ),
107
- "data-turbo-method": :post,
108
- class: "icon_button"
109
- ) %>
110
- </sl-tooltip>
111
- {{else}}
112
- <div class="sitemap_tool disabled">
113
- <sl-tooltip content="<%= Alchemy.t("Your user role does not allow you to edit this page") %>" class="like-hint-tooltip" placement="bottom-start">
114
- <%= render_icon("file-copy") %>
115
- </sl-tooltip>
116
- </div>
117
- {{/if}}
118
- {{#if permissions.destroy}}
119
- <sl-tooltip content="<%= Alchemy.t(:delete_page) %>">
120
- <%= link_to_confirm_dialog(
121
- render_icon("delete-bin-2"),
122
- Alchemy.t(:confirm_to_delete_page),
123
- url_for(
124
- controller: "pages",
125
- action: "destroy",
126
- id: "__ID__"
127
- ),
128
- class: "icon_button"
129
- ) -%>
130
- </sl-tooltip>
131
- {{else}}
132
- <div class="sitemap_tool disabled">
133
- <sl-tooltip content="<%= Alchemy.t("Your user role does not allow you to edit this page") %>" class="like-hint-tooltip" placement="bottom-start">
134
- <%= render_icon("delete-bin-2") %>
135
- </sl-tooltip>
136
- </div>
137
- {{/if}}
138
- {{#if permissions.create}}
139
- <sl-tooltip content="<%= Alchemy.t(:create_page) %>">
140
- <%= link_to_dialog(
141
- render_icon(:add),
142
- alchemy.new_admin_page_path(parent_id: "__ID__"),
143
- {
144
- title: Alchemy.t(:create_page),
145
- size: "340x165",
146
- overflow: true
147
- },
148
- class: "icon_button"
149
- ) -%>
150
- </sl-tooltip>
151
- {{else}}
152
- <div class="sitemap_tool disabled">
153
- <sl-tooltip content="<%= Alchemy.t("Your user role does not allow you to edit this page") %>" class="like-hint-tooltip" placement="bottom-start">
154
- <%= render_icon("file-add") %>
155
- </sl-tooltip>
156
- </div>
157
- {{/if}}
158
- </div>
159
- </div>
160
- <ul id="page_{{id}}_children" class="children" data-parent-id="{{id}}">
161
- {{> list}}
162
- </ul>
163
- </li>
@@ -1,30 +0,0 @@
1
- <h4 id="sitemap_heading">
2
- <span class="page_name"><%= Alchemy::Page.human_attribute_name(:name) %></span>
3
- <span class="page_urlname"><%= Alchemy::Page.human_attribute_name(:urlname) %></span>
4
- <span class="page_status"><%= Alchemy.t(:page_status) %></span>
5
- </h4>
6
-
7
- <div id="sitemap-wrapper">
8
- </div>
9
-
10
- <script id="sitemap-template" type="text/x-handlebars-template">
11
- <ul id="sitemap" class="list">
12
- {{> list}}
13
- </ul>
14
- </script>
15
-
16
- <script id="sitemap-list" type="text/x-handlebars-template">
17
- {{#each children}}
18
- <%= render partial: page_partial, object: @page_root %>
19
- {{/each}}
20
- </script>
21
-
22
- <script type="module">
23
- $(function() {
24
- Alchemy.currentSitemap = new Alchemy.Sitemap({
25
- url: '<%= alchemy.tree_admin_pages_path %>',
26
- page_root_id: <%= @page_root.id %>
27
- });
28
- Alchemy.PagePublicationFields();
29
- });
30
- </script>
@@ -1,2 +0,0 @@
1
- Alchemy.growl('<%= Alchemy.t("Page cache flushed") -%>');
2
- Alchemy.pleaseWaitOverlay(false);
@@ -1,5 +0,0 @@
1
- (function() {
2
- var dialog = Alchemy.currentDialog();
3
- dialog.dialog_body.html('<%= j render("archive_overlay") %>');
4
- Alchemy.ImageLoader('#assign_image_list');
5
- })();
@@ -1,2 +0,0 @@
1
- $('#archive_all').html('<%= j render("archive") %>');
2
- $('#filter_bar').replaceWith('<%= j render("filter_bar") %>');
@@ -1,5 +0,0 @@
1
- <%= render(
2
- "alchemy/ingredients/file_editor",
3
- element_form: element_form,
4
- file_editor: audio_editor
5
- ) %>
@@ -1,11 +0,0 @@
1
- <%= content_tag :div,
2
- class: boolean_editor.css_classes,
3
- data: boolean_editor.data_attributes do %>
4
- <%= element_form.fields_for(:ingredients, boolean_editor.ingredient) do |f| %>
5
- <%= f.label :value, style: "display: inline-block", for: nil do %>
6
- <%= f.check_box :value, id: nil %>
7
- <%= render_ingredient_role(boolean_editor) %>
8
- <%= render_hint_for(boolean_editor, size: "1x", fixed_width: false) %>
9
- <% end %>
10
- <% end %>
11
- <% end %>
@@ -1,20 +0,0 @@
1
- <%= content_tag :div,
2
- class: datetime_editor.css_classes,
3
- data: datetime_editor.data_attributes do %>
4
- <%= element_form.fields_for(:ingredients, datetime_editor.ingredient) do |f| %>
5
- <%= ingredient_label(datetime_editor) %>
6
- <div class="input-field">
7
- <%= alchemy_datepicker(
8
- datetime_editor, :value, {
9
- name: datetime_editor.form_field_name,
10
- id: datetime_editor.form_field_id,
11
- value: datetime_editor.value,
12
- type: datetime_editor.settings[:input_type]
13
- }
14
- ) %>
15
- <label for="<%= datetime_editor.form_field_id %>" class="ingredient-date--label">
16
- <%= render_icon "calendar" %>
17
- </label>
18
- </div>
19
- <% end %>
20
- <% end %>