alchemy_cms 8.0.12 → 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 (286) 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 -15
  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/languages_controller.rb +0 -3
  46. data/app/controllers/alchemy/admin/layoutpages_controller.rb +2 -1
  47. data/app/controllers/alchemy/admin/legacy_page_urls_controller.rb +1 -1
  48. data/app/controllers/alchemy/admin/nodes_controller.rb +24 -1
  49. data/app/controllers/alchemy/admin/pages_controller.rb +36 -42
  50. data/app/controllers/alchemy/admin/pictures_controller.rb +4 -28
  51. data/app/controllers/alchemy/admin/resources_controller.rb +1 -1
  52. data/app/controllers/alchemy/api/ingredients_controller.rb +1 -1
  53. data/app/controllers/alchemy/api/pages_controller.rb +5 -3
  54. data/app/controllers/alchemy/base_controller.rb +6 -6
  55. data/app/controllers/alchemy/pages_controller.rb +12 -6
  56. data/app/controllers/concerns/alchemy/admin/archive_overlay.rb +0 -1
  57. data/app/controllers/concerns/alchemy/admin/clipboard.rb +57 -0
  58. data/app/controllers/concerns/alchemy/admin/uploader_responses.rb +2 -2
  59. data/app/controllers/concerns/alchemy/site_redirects.rb +1 -1
  60. data/app/decorators/alchemy/ingredient_editor.rb +37 -4
  61. data/app/helpers/alchemy/admin/base_helper.rb +10 -6
  62. data/app/helpers/alchemy/admin/ingredients_helper.rb +6 -3
  63. data/app/helpers/alchemy/base_helper.rb +1 -1
  64. data/app/helpers/alchemy/pages_helper.rb +1 -1
  65. data/app/javascript/alchemy_admin/components/action.js +5 -1
  66. data/app/javascript/alchemy_admin/components/color_select.js +73 -0
  67. data/app/javascript/alchemy_admin/components/element_editor/delete_element_button.js +11 -3
  68. data/app/javascript/alchemy_admin/components/element_editor/publish_element_button.js +7 -2
  69. data/app/javascript/alchemy_admin/components/element_editor.js +11 -12
  70. data/app/javascript/alchemy_admin/components/element_select.js +39 -17
  71. data/app/javascript/alchemy_admin/components/elements_window.js +0 -2
  72. data/app/javascript/alchemy_admin/components/file_editor.js +26 -0
  73. data/app/javascript/alchemy_admin/components/index.js +9 -0
  74. data/app/javascript/alchemy_admin/components/list_filter.js +57 -8
  75. data/app/javascript/alchemy_admin/components/message.js +9 -3
  76. data/app/javascript/alchemy_admin/components/page_node.js +119 -0
  77. data/app/javascript/alchemy_admin/{page_publication_fields.js → components/page_publication_fields.js} +9 -8
  78. data/app/javascript/alchemy_admin/{picture_editors.js → components/picture_editor.js} +30 -45
  79. data/app/javascript/alchemy_admin/components/picture_thumbnail.js +107 -0
  80. data/app/javascript/alchemy_admin/components/publish_page_button.js +41 -0
  81. data/app/javascript/alchemy_admin/components/select.js +3 -1
  82. data/app/javascript/alchemy_admin/components/sitemap.js +210 -0
  83. data/app/javascript/alchemy_admin/{sortable_elements.js → components/sortable_elements.js} +22 -25
  84. data/app/javascript/alchemy_admin/components/tinymce.js +10 -5
  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/link_dialog.js +1 -6
  89. data/app/javascript/alchemy_admin/templates/compiled.js +1 -1
  90. data/app/javascript/alchemy_admin/utils/ajax.js +15 -3
  91. data/app/javascript/alchemy_admin.js +0 -6
  92. data/app/models/alchemy/attachment.rb +4 -44
  93. data/app/models/alchemy/element/definitions.rb +1 -2
  94. data/app/models/alchemy/element/element_ingredients.rb +6 -2
  95. data/app/models/alchemy/element.rb +54 -13
  96. data/app/models/alchemy/element_definition.rb +4 -1
  97. data/app/models/alchemy/elements_repository.rb +6 -0
  98. data/app/models/alchemy/folded_page.rb +2 -2
  99. data/app/models/alchemy/ingredient.rb +38 -1
  100. data/app/models/alchemy/ingredient_definition.rb +4 -1
  101. data/app/models/alchemy/ingredient_validator.rb +6 -2
  102. data/app/models/alchemy/ingredients/color.rb +10 -0
  103. data/app/models/alchemy/ingredients/headline.rb +2 -17
  104. data/app/models/alchemy/ingredients/picture.rb +4 -4
  105. data/app/models/alchemy/ingredients/select.rb +19 -0
  106. data/app/models/alchemy/language/code.rb +0 -1
  107. data/app/models/alchemy/node.rb +28 -1
  108. data/app/models/alchemy/page/page_naming.rb +0 -7
  109. data/app/models/alchemy/page/page_natures.rb +7 -3
  110. data/app/models/alchemy/page/page_scopes.rb +13 -1
  111. data/app/models/alchemy/page/publisher.rb +14 -2
  112. data/app/models/alchemy/page.rb +102 -23
  113. data/app/models/alchemy/page_definition.rb +4 -1
  114. data/app/models/alchemy/page_version.rb +22 -6
  115. data/app/models/alchemy/picture.rb +10 -11
  116. data/app/models/alchemy/picture_variant.rb +1 -3
  117. data/app/models/alchemy/resource.rb +1 -1
  118. data/app/models/alchemy/storage_adapter/active_storage.rb +14 -2
  119. data/app/models/alchemy/storage_adapter/dragonfly.rb +12 -0
  120. data/app/models/alchemy/storage_adapter.rb +2 -0
  121. data/app/models/concerns/alchemy/picture_thumbnails.rb +4 -4
  122. data/app/models/concerns/alchemy/publishable.rb +54 -0
  123. data/app/models/concerns/alchemy/relatable_resource.rb +4 -14
  124. data/app/serializers/alchemy/page_tree_serializer.rb +11 -31
  125. data/app/services/alchemy/copy_page.rb +17 -0
  126. data/app/services/alchemy/duplicate_element.rb +1 -1
  127. data/app/services/alchemy/page_tree_preloader.rb +105 -0
  128. data/app/stylesheets/alchemy/_extends.scss +3 -9
  129. data/app/stylesheets/alchemy/_mixins.scss +3 -1
  130. data/app/stylesheets/alchemy/_themes.scss +19 -10
  131. data/app/stylesheets/alchemy/admin/archive.scss +1 -0
  132. data/app/stylesheets/alchemy/admin/base.scss +5 -2
  133. data/app/stylesheets/alchemy/admin/buttons.scss +3 -3
  134. data/app/stylesheets/alchemy/admin/element-select.scss +18 -0
  135. data/app/stylesheets/alchemy/admin/elements.scss +123 -23
  136. data/app/stylesheets/alchemy/admin/errors.scss +1 -1
  137. data/app/stylesheets/alchemy/admin/flash.scss +6 -4
  138. data/app/stylesheets/alchemy/admin/images.scss +9 -5
  139. data/app/stylesheets/alchemy/admin/list_filter.scss +4 -4
  140. data/app/stylesheets/alchemy/admin/navigation.scss +1 -1
  141. data/app/stylesheets/alchemy/admin/notices.scss +1 -2
  142. data/app/stylesheets/alchemy/admin/selects.scss +36 -21
  143. data/app/stylesheets/alchemy/admin/shoelace.scss +14 -1
  144. data/app/stylesheets/alchemy/admin/sitemap.scss +11 -3
  145. data/app/stylesheets/alchemy/admin/tags.scss +3 -1
  146. data/app/stylesheets/alchemy/admin/toolbar.scss +1 -1
  147. data/app/views/alchemy/_edit_mode.html.erb +1 -1
  148. data/app/views/alchemy/_menubar.html.erb +1 -1
  149. data/app/views/alchemy/admin/attachments/_archive_overlay.html.erb +35 -31
  150. data/app/views/alchemy/admin/attachments/_files_list.html.erb +1 -1
  151. data/app/views/alchemy/admin/attachments/_library_sidebar.html.erb +6 -0
  152. data/app/views/alchemy/admin/attachments/_overlay_file_list.html.erb +1 -1
  153. data/app/views/alchemy/admin/attachments/_replace_button.html.erb +1 -8
  154. data/app/views/alchemy/admin/attachments/_sorting_select.html.erb +13 -0
  155. data/app/views/alchemy/admin/attachments/_tag_list.html.erb +2 -3
  156. data/app/views/alchemy/admin/attachments/index.html.erb +5 -11
  157. data/app/views/alchemy/admin/attachments/show.html.erb +1 -1
  158. data/app/views/alchemy/admin/clipboard/_button.html.erb +1 -0
  159. data/app/views/alchemy/admin/clipboard/index.html.erb +4 -5
  160. data/app/views/alchemy/admin/clipboard/insert.turbo_stream.erb +1 -1
  161. data/app/views/alchemy/admin/crop.html.erb +5 -7
  162. data/app/views/alchemy/admin/dashboard/widgets/_locked_pages.html.erb +1 -1
  163. data/app/views/alchemy/admin/elements/_add_nested_element_form.html.erb +6 -6
  164. data/app/views/alchemy/admin/elements/_fixed_element.html.erb +1 -1
  165. data/app/views/alchemy/admin/elements/_footer.html.erb +7 -1
  166. data/app/views/alchemy/admin/elements/_header.html.erb +5 -5
  167. data/app/views/alchemy/admin/elements/_toolbar.html.erb +33 -8
  168. data/app/views/alchemy/admin/elements/create.turbo_stream.erb +10 -10
  169. data/app/views/alchemy/admin/elements/index.html.erb +29 -16
  170. data/app/views/alchemy/admin/elements/new.html.erb +2 -2
  171. data/app/views/alchemy/admin/ingredients/update.turbo_stream.erb +3 -5
  172. data/app/views/alchemy/admin/leave.html.erb +1 -1
  173. data/app/views/alchemy/admin/nodes/_node.html.erb +19 -0
  174. data/app/views/alchemy/admin/nodes/edit.html.erb +1 -1
  175. data/app/views/alchemy/admin/nodes/index.html.erb +3 -1
  176. data/app/views/alchemy/admin/nodes/new.html.erb +14 -1
  177. data/app/views/alchemy/admin/pages/_current_page.html.erb +3 -1
  178. data/app/views/alchemy/admin/pages/_form.html.erb +21 -9
  179. data/app/views/alchemy/admin/pages/_page_status.html.erb +1 -1
  180. data/app/views/alchemy/admin/pages/_publication_fields.html.erb +28 -26
  181. data/app/views/alchemy/admin/pages/_table.html.erb +0 -7
  182. data/app/views/alchemy/admin/pages/_toolbar.html.erb +3 -6
  183. data/app/views/alchemy/admin/pages/edit.html.erb +5 -11
  184. data/app/views/alchemy/admin/pages/flush.turbo_stream.erb +2 -0
  185. data/app/views/alchemy/admin/pages/fold.turbo_stream.erb +5 -0
  186. data/app/views/alchemy/admin/pages/index.html.erb +5 -3
  187. data/app/views/alchemy/admin/pages/new.html.erb +2 -12
  188. data/app/views/alchemy/admin/pages/publish.turbo_stream.erb +12 -0
  189. data/app/views/alchemy/admin/pages/tree.html.erb +13 -0
  190. data/app/views/alchemy/admin/pages/update.turbo_stream.erb +5 -16
  191. data/app/views/alchemy/admin/partials/_flash_notices.html.erb +1 -1
  192. data/app/views/alchemy/admin/partials/{_remote_search_form.html.erb → _overlay_search_form.html.erb} +1 -2
  193. data/app/views/alchemy/admin/partials/_paste_from_clipboard_form.html.erb +12 -0
  194. data/app/views/alchemy/admin/pictures/_archive_overlay.html.erb +24 -21
  195. data/app/views/alchemy/admin/pictures/_filter_and_size_bar.html.erb +18 -26
  196. data/app/views/alchemy/admin/pictures/_picture.html.erb +12 -16
  197. data/app/views/alchemy/admin/pictures/_picture_to_assign.html.erb +3 -6
  198. data/app/views/alchemy/admin/pictures/_tag_list.html.erb +2 -3
  199. data/app/views/alchemy/admin/pictures/index.html.erb +0 -1
  200. data/app/views/alchemy/admin/pictures/update.turbo_stream.erb +1 -1
  201. data/app/views/alchemy/admin/resources/_resource_usage_info.html.erb +1 -1
  202. data/app/views/alchemy/admin/resources/_tag_list.html.erb +2 -3
  203. data/app/views/alchemy/admin/styleguide/index.html.erb +25 -20
  204. data/app/views/alchemy/admin/tags/edit.html.erb +1 -1
  205. data/app/views/alchemy/admin/tinymce/_setup.html.erb +2 -2
  206. data/app/views/alchemy/admin/uploader/_button.html.erb +1 -15
  207. data/app/views/alchemy/attachments/show.html.erb +1 -1
  208. data/app/views/alchemy/base/permission_denied.js.erb +1 -1
  209. data/app/views/alchemy/ingredients/shared/_anchor.html.erb +9 -7
  210. data/app/views/alchemy/ingredients/shared/_link_tools.html.erb +12 -5
  211. data/app/views/alchemy/ingredients/shared/_picture_tools.html.erb +10 -11
  212. data/app/views/alchemy/language_links/_spacer.html.erb +1 -1
  213. data/app/views/alchemy/messages_mailer/new.html.erb +1 -1
  214. data/app/views/alchemy/welcome.html.erb +1 -1
  215. data/config/locales/alchemy.en.yml +12 -3
  216. data/config/routes.rb +2 -2
  217. data/db/migrate/20230123112425_add_searchable_to_alchemy_pages.rb +1 -1
  218. data/db/migrate/20230505132743_add_indexes_to_alchemy_pictures.rb +1 -1
  219. data/db/migrate/20231113104432_create_page_mutexes.rb +1 -1
  220. data/db/migrate/20240314105244_create_alchemy_picture_descriptions.rb +1 -1
  221. data/db/migrate/20250626160259_add_unique_index_to_picture_descriptions.rb +1 -1
  222. data/db/migrate/20250905140323_add_created_at_index_to_pictures_and_attachments.rb +1 -1
  223. data/db/migrate/20251106150010_convert_select_value_for_multiple.rb +11 -0
  224. data/db/migrate/20260102121232_add_metadata_to_page_versions.rb +9 -0
  225. data/db/migrate/20260115164704_add_publication_timestamps_to_alchemy_elements.rb +30 -0
  226. data/db/migrate/20260115164705_add_index_to_element_publication_timestamps.rb +13 -0
  227. data/lib/alchemy/ability_helper.rb +1 -3
  228. data/lib/alchemy/auth_accessors.rb +51 -117
  229. data/lib/alchemy/configuration.rb +1 -2
  230. data/lib/alchemy/configurations/main.rb +63 -0
  231. data/lib/alchemy/controller_actions.rb +2 -3
  232. data/lib/alchemy/engine.rb +9 -12
  233. data/lib/alchemy/error_tracking/error_logger.rb +1 -1
  234. data/lib/alchemy/errors.rb +1 -1
  235. data/lib/alchemy/logger.rb +34 -4
  236. data/lib/alchemy/name_conversions.rb +0 -6
  237. data/lib/alchemy/seeder.rb +2 -2
  238. data/lib/alchemy/tasks/usage.rb +4 -4
  239. data/lib/alchemy/test_support/factories/page_version_factory.rb +3 -0
  240. data/lib/alchemy/test_support/having_picture_thumbnails_examples.rb +30 -0
  241. data/lib/alchemy/test_support/shared_ingredient_editor_examples.rb +26 -6
  242. data/lib/alchemy/test_support/shared_publishable_examples.rb +114 -0
  243. data/lib/alchemy/upgrader/eight_one.rb +56 -0
  244. data/lib/alchemy/upgrader.rb +9 -1
  245. data/lib/alchemy/version.rb +1 -1
  246. data/lib/alchemy.rb +1 -4
  247. data/lib/alchemy_cms.rb +0 -1
  248. data/lib/generators/alchemy/elements/templates/view.html.erb +3 -3
  249. data/lib/generators/alchemy/ingredient/ingredient_generator.rb +6 -8
  250. data/lib/generators/alchemy/ingredient/templates/editor_component.rb.tt +22 -0
  251. data/lib/generators/alchemy/page_layouts/templates/layout.html.erb +1 -1
  252. data/lib/generators/alchemy/site_layouts/templates/layout.html.erb +1 -1
  253. data/lib/tasks/alchemy/upgrade.rake +21 -7
  254. data/vendor/javascript/shoelace.min.js +713 -31
  255. data/vendor/javascript/tinymce.min.js +1 -1
  256. metadata +104 -84
  257. data/app/decorators/alchemy/element_editor.rb +0 -90
  258. data/app/helpers/alchemy/admin/pictures_helper.rb +0 -14
  259. data/app/javascript/alchemy_admin/file_editors.js +0 -28
  260. data/app/javascript/alchemy_admin/image_loader.js +0 -54
  261. data/app/javascript/alchemy_admin/page_sorter.js +0 -71
  262. data/app/javascript/alchemy_admin/sitemap.js +0 -154
  263. data/app/javascript/alchemy_admin/templates/page_folder.hbs +0 -3
  264. data/app/views/alchemy/admin/attachments/archive_overlay.js.erb +0 -4
  265. data/app/views/alchemy/admin/pages/_page.html.erb +0 -163
  266. data/app/views/alchemy/admin/pages/_sitemap.html.erb +0 -30
  267. data/app/views/alchemy/admin/pages/flush.js.erb +0 -2
  268. data/app/views/alchemy/admin/pictures/archive_overlay.js.erb +0 -5
  269. data/app/views/alchemy/admin/pictures/index.js.erb +0 -2
  270. data/app/views/alchemy/ingredients/_audio_editor.html.erb +0 -5
  271. data/app/views/alchemy/ingredients/_boolean_editor.html.erb +0 -11
  272. data/app/views/alchemy/ingredients/_datetime_editor.html.erb +0 -20
  273. data/app/views/alchemy/ingredients/_file_editor.html.erb +0 -52
  274. data/app/views/alchemy/ingredients/_headline_editor.html.erb +0 -44
  275. data/app/views/alchemy/ingredients/_html_editor.html.erb +0 -8
  276. data/app/views/alchemy/ingredients/_link_editor.html.erb +0 -30
  277. data/app/views/alchemy/ingredients/_node_editor.html.erb +0 -13
  278. data/app/views/alchemy/ingredients/_number_editor.html.erb +0 -24
  279. data/app/views/alchemy/ingredients/_page_editor.html.erb +0 -13
  280. data/app/views/alchemy/ingredients/_picture_editor.html.erb +0 -59
  281. data/app/views/alchemy/ingredients/_richtext_editor.html.erb +0 -15
  282. data/app/views/alchemy/ingredients/_select_editor.html.erb +0 -31
  283. data/app/views/alchemy/ingredients/_text_editor.html.erb +0 -29
  284. data/app/views/alchemy/ingredients/_video_editor.html.erb +0 -5
  285. data/lib/generators/alchemy/ingredient/templates/editor.html.erb +0 -14
  286. /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 %>