alchemy_cms 8.0.11 → 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 (284) 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/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 +2 -5
  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 -4
  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/serializers/alchemy/page_tree_serializer.rb +11 -31
  124. data/app/services/alchemy/copy_page.rb +17 -0
  125. data/app/services/alchemy/duplicate_element.rb +1 -1
  126. data/app/services/alchemy/page_tree_preloader.rb +105 -0
  127. data/app/stylesheets/alchemy/_extends.scss +3 -9
  128. data/app/stylesheets/alchemy/_mixins.scss +3 -1
  129. data/app/stylesheets/alchemy/_themes.scss +19 -10
  130. data/app/stylesheets/alchemy/admin/archive.scss +1 -0
  131. data/app/stylesheets/alchemy/admin/base.scss +5 -2
  132. data/app/stylesheets/alchemy/admin/buttons.scss +3 -3
  133. data/app/stylesheets/alchemy/admin/element-select.scss +18 -0
  134. data/app/stylesheets/alchemy/admin/elements.scss +123 -23
  135. data/app/stylesheets/alchemy/admin/errors.scss +1 -1
  136. data/app/stylesheets/alchemy/admin/flash.scss +6 -4
  137. data/app/stylesheets/alchemy/admin/images.scss +9 -5
  138. data/app/stylesheets/alchemy/admin/list_filter.scss +4 -4
  139. data/app/stylesheets/alchemy/admin/navigation.scss +1 -1
  140. data/app/stylesheets/alchemy/admin/notices.scss +1 -2
  141. data/app/stylesheets/alchemy/admin/selects.scss +36 -21
  142. data/app/stylesheets/alchemy/admin/shoelace.scss +14 -1
  143. data/app/stylesheets/alchemy/admin/sitemap.scss +11 -3
  144. data/app/stylesheets/alchemy/admin/tags.scss +3 -1
  145. data/app/stylesheets/alchemy/admin/toolbar.scss +1 -1
  146. data/app/views/alchemy/_edit_mode.html.erb +1 -1
  147. data/app/views/alchemy/_menubar.html.erb +1 -1
  148. data/app/views/alchemy/admin/attachments/_archive_overlay.html.erb +35 -31
  149. data/app/views/alchemy/admin/attachments/_library_sidebar.html.erb +6 -0
  150. data/app/views/alchemy/admin/attachments/_overlay_file_list.html.erb +1 -1
  151. data/app/views/alchemy/admin/attachments/_replace_button.html.erb +1 -8
  152. data/app/views/alchemy/admin/attachments/_sorting_select.html.erb +13 -0
  153. data/app/views/alchemy/admin/attachments/_tag_list.html.erb +2 -3
  154. data/app/views/alchemy/admin/attachments/index.html.erb +5 -11
  155. data/app/views/alchemy/admin/attachments/show.html.erb +1 -1
  156. data/app/views/alchemy/admin/clipboard/_button.html.erb +1 -0
  157. data/app/views/alchemy/admin/clipboard/index.html.erb +4 -5
  158. data/app/views/alchemy/admin/clipboard/insert.turbo_stream.erb +1 -1
  159. data/app/views/alchemy/admin/crop.html.erb +5 -7
  160. data/app/views/alchemy/admin/dashboard/widgets/_locked_pages.html.erb +1 -1
  161. data/app/views/alchemy/admin/elements/_add_nested_element_form.html.erb +6 -6
  162. data/app/views/alchemy/admin/elements/_fixed_element.html.erb +1 -1
  163. data/app/views/alchemy/admin/elements/_footer.html.erb +7 -1
  164. data/app/views/alchemy/admin/elements/_header.html.erb +5 -5
  165. data/app/views/alchemy/admin/elements/_toolbar.html.erb +33 -8
  166. data/app/views/alchemy/admin/elements/create.turbo_stream.erb +10 -10
  167. data/app/views/alchemy/admin/elements/index.html.erb +29 -16
  168. data/app/views/alchemy/admin/elements/new.html.erb +2 -2
  169. data/app/views/alchemy/admin/ingredients/update.turbo_stream.erb +3 -5
  170. data/app/views/alchemy/admin/leave.html.erb +1 -1
  171. data/app/views/alchemy/admin/nodes/_node.html.erb +19 -0
  172. data/app/views/alchemy/admin/nodes/edit.html.erb +1 -1
  173. data/app/views/alchemy/admin/nodes/index.html.erb +3 -1
  174. data/app/views/alchemy/admin/nodes/new.html.erb +14 -1
  175. data/app/views/alchemy/admin/pages/_current_page.html.erb +3 -1
  176. data/app/views/alchemy/admin/pages/_form.html.erb +21 -9
  177. data/app/views/alchemy/admin/pages/_page_status.html.erb +1 -1
  178. data/app/views/alchemy/admin/pages/_publication_fields.html.erb +28 -26
  179. data/app/views/alchemy/admin/pages/_table.html.erb +0 -7
  180. data/app/views/alchemy/admin/pages/_toolbar.html.erb +3 -6
  181. data/app/views/alchemy/admin/pages/edit.html.erb +5 -11
  182. data/app/views/alchemy/admin/pages/flush.turbo_stream.erb +2 -0
  183. data/app/views/alchemy/admin/pages/fold.turbo_stream.erb +5 -0
  184. data/app/views/alchemy/admin/pages/index.html.erb +5 -3
  185. data/app/views/alchemy/admin/pages/new.html.erb +2 -12
  186. data/app/views/alchemy/admin/pages/publish.turbo_stream.erb +12 -0
  187. data/app/views/alchemy/admin/pages/tree.html.erb +13 -0
  188. data/app/views/alchemy/admin/pages/update.turbo_stream.erb +5 -16
  189. data/app/views/alchemy/admin/partials/_flash_notices.html.erb +1 -1
  190. data/app/views/alchemy/admin/partials/{_remote_search_form.html.erb → _overlay_search_form.html.erb} +1 -2
  191. data/app/views/alchemy/admin/partials/_paste_from_clipboard_form.html.erb +12 -0
  192. data/app/views/alchemy/admin/pictures/_archive_overlay.html.erb +24 -21
  193. data/app/views/alchemy/admin/pictures/_filter_and_size_bar.html.erb +18 -26
  194. data/app/views/alchemy/admin/pictures/_picture.html.erb +11 -15
  195. data/app/views/alchemy/admin/pictures/_picture_to_assign.html.erb +3 -6
  196. data/app/views/alchemy/admin/pictures/_tag_list.html.erb +2 -3
  197. data/app/views/alchemy/admin/pictures/index.html.erb +0 -1
  198. data/app/views/alchemy/admin/pictures/update.turbo_stream.erb +1 -1
  199. data/app/views/alchemy/admin/resources/_resource_usage_info.html.erb +1 -1
  200. data/app/views/alchemy/admin/resources/_tag_list.html.erb +2 -3
  201. data/app/views/alchemy/admin/styleguide/index.html.erb +25 -20
  202. data/app/views/alchemy/admin/tags/edit.html.erb +1 -1
  203. data/app/views/alchemy/admin/tinymce/_setup.html.erb +2 -2
  204. data/app/views/alchemy/admin/uploader/_button.html.erb +1 -15
  205. data/app/views/alchemy/attachments/show.html.erb +1 -1
  206. data/app/views/alchemy/base/permission_denied.js.erb +1 -1
  207. data/app/views/alchemy/ingredients/shared/_anchor.html.erb +9 -7
  208. data/app/views/alchemy/ingredients/shared/_link_tools.html.erb +12 -5
  209. data/app/views/alchemy/ingredients/shared/_picture_tools.html.erb +10 -11
  210. data/app/views/alchemy/language_links/_spacer.html.erb +1 -1
  211. data/app/views/alchemy/messages_mailer/new.html.erb +1 -1
  212. data/app/views/alchemy/welcome.html.erb +1 -1
  213. data/config/locales/alchemy.en.yml +12 -3
  214. data/config/routes.rb +2 -2
  215. data/db/migrate/20230123112425_add_searchable_to_alchemy_pages.rb +1 -1
  216. data/db/migrate/20230505132743_add_indexes_to_alchemy_pictures.rb +1 -1
  217. data/db/migrate/20231113104432_create_page_mutexes.rb +1 -1
  218. data/db/migrate/20240314105244_create_alchemy_picture_descriptions.rb +1 -1
  219. data/db/migrate/20250626160259_add_unique_index_to_picture_descriptions.rb +1 -1
  220. data/db/migrate/20250905140323_add_created_at_index_to_pictures_and_attachments.rb +1 -1
  221. data/db/migrate/20251106150010_convert_select_value_for_multiple.rb +11 -0
  222. data/db/migrate/20260102121232_add_metadata_to_page_versions.rb +9 -0
  223. data/db/migrate/20260115164704_add_publication_timestamps_to_alchemy_elements.rb +30 -0
  224. data/db/migrate/20260115164705_add_index_to_element_publication_timestamps.rb +13 -0
  225. data/lib/alchemy/ability_helper.rb +1 -3
  226. data/lib/alchemy/auth_accessors.rb +51 -117
  227. data/lib/alchemy/configuration.rb +1 -0
  228. data/lib/alchemy/configurations/main.rb +63 -0
  229. data/lib/alchemy/controller_actions.rb +2 -3
  230. data/lib/alchemy/engine.rb +9 -12
  231. data/lib/alchemy/error_tracking/error_logger.rb +1 -1
  232. data/lib/alchemy/errors.rb +1 -1
  233. data/lib/alchemy/logger.rb +34 -4
  234. data/lib/alchemy/name_conversions.rb +0 -6
  235. data/lib/alchemy/seeder.rb +2 -2
  236. data/lib/alchemy/tasks/usage.rb +4 -4
  237. data/lib/alchemy/test_support/factories/page_version_factory.rb +3 -0
  238. data/lib/alchemy/test_support/having_picture_thumbnails_examples.rb +30 -0
  239. data/lib/alchemy/test_support/shared_ingredient_editor_examples.rb +26 -6
  240. data/lib/alchemy/test_support/shared_publishable_examples.rb +114 -0
  241. data/lib/alchemy/upgrader/eight_one.rb +56 -0
  242. data/lib/alchemy/upgrader.rb +9 -1
  243. data/lib/alchemy/version.rb +1 -1
  244. data/lib/alchemy.rb +1 -4
  245. data/lib/alchemy_cms.rb +0 -1
  246. data/lib/generators/alchemy/elements/templates/view.html.erb +3 -3
  247. data/lib/generators/alchemy/ingredient/ingredient_generator.rb +6 -8
  248. data/lib/generators/alchemy/ingredient/templates/editor_component.rb.tt +22 -0
  249. data/lib/generators/alchemy/page_layouts/templates/layout.html.erb +1 -1
  250. data/lib/generators/alchemy/site_layouts/templates/layout.html.erb +1 -1
  251. data/lib/tasks/alchemy/upgrade.rake +21 -7
  252. data/vendor/javascript/shoelace.min.js +713 -31
  253. data/vendor/javascript/tinymce.min.js +1 -1
  254. metadata +104 -84
  255. data/app/decorators/alchemy/element_editor.rb +0 -90
  256. data/app/helpers/alchemy/admin/pictures_helper.rb +0 -14
  257. data/app/javascript/alchemy_admin/file_editors.js +0 -28
  258. data/app/javascript/alchemy_admin/image_loader.js +0 -54
  259. data/app/javascript/alchemy_admin/page_sorter.js +0 -71
  260. data/app/javascript/alchemy_admin/sitemap.js +0 -154
  261. data/app/javascript/alchemy_admin/templates/page_folder.hbs +0 -3
  262. data/app/views/alchemy/admin/attachments/archive_overlay.js.erb +0 -4
  263. data/app/views/alchemy/admin/pages/_page.html.erb +0 -163
  264. data/app/views/alchemy/admin/pages/_sitemap.html.erb +0 -30
  265. data/app/views/alchemy/admin/pages/flush.js.erb +0 -2
  266. data/app/views/alchemy/admin/pictures/archive_overlay.js.erb +0 -5
  267. data/app/views/alchemy/admin/pictures/index.js.erb +0 -2
  268. data/app/views/alchemy/ingredients/_audio_editor.html.erb +0 -5
  269. data/app/views/alchemy/ingredients/_boolean_editor.html.erb +0 -11
  270. data/app/views/alchemy/ingredients/_datetime_editor.html.erb +0 -20
  271. data/app/views/alchemy/ingredients/_file_editor.html.erb +0 -52
  272. data/app/views/alchemy/ingredients/_headline_editor.html.erb +0 -44
  273. data/app/views/alchemy/ingredients/_html_editor.html.erb +0 -8
  274. data/app/views/alchemy/ingredients/_link_editor.html.erb +0 -30
  275. data/app/views/alchemy/ingredients/_node_editor.html.erb +0 -13
  276. data/app/views/alchemy/ingredients/_number_editor.html.erb +0 -24
  277. data/app/views/alchemy/ingredients/_page_editor.html.erb +0 -13
  278. data/app/views/alchemy/ingredients/_picture_editor.html.erb +0 -59
  279. data/app/views/alchemy/ingredients/_richtext_editor.html.erb +0 -15
  280. data/app/views/alchemy/ingredients/_select_editor.html.erb +0 -31
  281. data/app/views/alchemy/ingredients/_text_editor.html.erb +0 -29
  282. data/app/views/alchemy/ingredients/_video_editor.html.erb +0 -5
  283. data/lib/generators/alchemy/ingredient/templates/editor.html.erb +0 -14
  284. /data/{lib → app/models}/alchemy/permissions.rb +0 -0
@@ -17,26 +17,37 @@ module Alchemy
17
17
  #
18
18
  # @param [String] items_selector - A CSS selector string that represents the items to filter
19
19
  # @param [String] name_attribute - A name that represents the attribute on the items to get filtered by
20
+ # @param [String] placeholder - Inoput field placeholder
20
21
  #
21
22
  class ListFilter < ViewComponent::Base
22
23
  erb_template <<~ERB
23
- <alchemy-list-filter items-selector="<%= items_selector %>" name-attribute="<%= name_attribute %>">
24
- <input type="text" class="js_filter_field" />
25
- <alchemy-icon name="search"></alchemy-icon>
24
+ <alchemy-list-filter
25
+ items-selector="<%= items_selector %>"
26
+ name-attribute="<%= name_attribute %>"
27
+ <% if hotkey %>
28
+ hotkey="<%= hotkey %>"
29
+ <% end %>
30
+ >
31
+ <label>
32
+ <alchemy-icon name="search"></alchemy-icon>
33
+ <input type="text" placeholder="<%= placeholder %>" class="js_filter_field">
34
+ </label>
26
35
  <button type="button" class="js_filter_field_clear icon_button">
27
36
  <alchemy-icon name="close" size="1x"></alchemy-icon>
28
37
  </button>
29
38
  </alchemy-list-filter>
30
39
  ERB
31
40
 
32
- def initialize(items_selector, name_attribute: "name")
41
+ def initialize(items_selector, name_attribute: "name", placeholder: Alchemy.t(:filter), hotkey: nil)
33
42
  @items_selector = items_selector
34
43
  @name_attribute = name_attribute
44
+ @placeholder = placeholder
45
+ @hotkey = hotkey
35
46
  end
36
47
 
37
48
  private
38
49
 
39
- attr_reader :items_selector, :name_attribute
50
+ attr_reader :items_selector, :name_attribute, :placeholder, :hotkey
40
51
  end
41
52
  end
42
53
  end
@@ -0,0 +1,214 @@
1
+ <alchemy-page-node
2
+ page-id="<%= @page.id %>"
3
+ <%= @page.folded? ? 'folded' : nil %>
4
+ >
5
+ <li class="sitemap-item <%= @page.page_layout %>">
6
+ <div
7
+ class="sitemap_page<% @page.locked? ? ' locked' : '' %>"
8
+ name="<%= @page.name %>"
9
+ >
10
+ <div class="sitemap_left_images">
11
+ <% if @page.children.any? || @page.folded? %>
12
+ <button class="page_folder icon_button">
13
+ <alchemy-icon
14
+ name="<%= @page.folded? ? 'arrow-right-s' : 'arrow-down-s' %>"
15
+ ></alchemy-icon>
16
+ </button>
17
+ <% else %>
18
+ <span class="page_folder"></span>
19
+ <% end %>
20
+ <% if @page.definition.blank? %>
21
+ <%= page_layout_missing_warning %>
22
+ <% else %>
23
+ <% if can?(:edit_content) %>
24
+ <span class="page-icon<%= @page.root? ? '' : ' handle' %>">
25
+ <% if @page.locked? %>
26
+ <sl-tooltip
27
+ content="<%= Alchemy.t("This page is locked", name: @page.locker_name) %>"
28
+ class="like-hint-tooltip"
29
+ placement="bottom-start"
30
+ >
31
+ <alchemy-icon name="file-edit" size="xl"></alchemy-icon>
32
+ </sl-tooltip>
33
+ <% else %>
34
+ <alchemy-icon name="file" size="xl"></alchemy-icon>
35
+ <% end %>
36
+ </span>
37
+ <% else %>
38
+ <sl-tooltip
39
+ content="<%= t(:cannot_edit_page) %>"
40
+ class="like-hint-tooltip"
41
+ placement="bottom-start"
42
+ >
43
+ <alchemy-icon name="file-forbid" size="xl"></alchemy-icon>
44
+ </sl-tooltip>
45
+ <% end %>
46
+ <% end %>
47
+ </div>
48
+
49
+ <div class="sitemap_sitename">
50
+ <% if can?(:edit_content) %>
51
+ <%= link_to(
52
+ @page.name,
53
+ url(:edit_page),
54
+ title: t(:edit_page),
55
+ class: "sitemap_pagename_link"
56
+ ) %>
57
+ <% else %>
58
+ <%= content_tag("span", @page.name, class: "sitemap_pagename_link") %>
59
+ <% end %>
60
+ </div>
61
+
62
+ <div class="sitemap_url" title="<%= @page.url_path %>">
63
+ <%= @page.url_path %>
64
+ </div>
65
+
66
+ <div class="page_infos">
67
+ <% if @page.locked? %>
68
+ <span class="page_status locked">
69
+ <alchemy-icon name="edit" size="1x"></alchemy-icon>
70
+ <%= @page.status_title(:locked) %>
71
+ </span>
72
+ <% end %>
73
+
74
+ <% if @page.restricted? %>
75
+ <span class="page_status">
76
+ <alchemy-icon name="lock" size="1x"></alchemy-icon>
77
+ <%= @page.status_title(:restricted) %>
78
+ </span>
79
+ <% end %>
80
+
81
+ <% unless @page.public? %>
82
+ <span class="page_status">
83
+ <alchemy-icon name="cloud-off" size="1x"></alchemy-icon>
84
+ <%= @page.status_title(:public) %>
85
+ </span>
86
+ <% end %>
87
+ </div>
88
+
89
+ <div class="sitemap_right_tools">
90
+ <% if can?(:info) %>
91
+ <sl-tooltip content="<%= t(:page_infos) %>">
92
+ <%= link_to_dialog(
93
+ render_icon("information"),
94
+ url(:info_page),
95
+ {
96
+ title: t(:page_infos),
97
+ size: "520x290"
98
+ },
99
+ class: "icon_button"
100
+ ) %>
101
+ </sl-tooltip>
102
+ <% else %>
103
+ <div class="sitemap_tool disabled">
104
+ <sl-tooltip
105
+ content="<%= t(:cannot_edit_page) %>"
106
+ class="like-hint-tooltip"
107
+ placement="bottom-start"
108
+ >
109
+ <%= render_icon(:information) %>
110
+ </sl-tooltip>
111
+ </div>
112
+ <% end %>
113
+
114
+ <% if can?(:configure) %>
115
+ <sl-tooltip content="<%= t(:edit_page_properties) %>">
116
+ <%= link_to_dialog(
117
+ render_icon("settings-3"),
118
+ url(:configure_page),
119
+ {
120
+ title: t(:edit_page_properties),
121
+ size: "500x680"
122
+ },
123
+ class: "icon_button"
124
+ ) %>
125
+ </sl-tooltip>
126
+ <% else %>
127
+ <div class="sitemap_tool disabled">
128
+ <sl-tooltip
129
+ content="<%= t(:cannot_edit_page) %>"
130
+ class="like-hint-tooltip"
131
+ placement="bottom-start"
132
+ >
133
+ <%= render_icon("settings-3") %>
134
+ </sl-tooltip>
135
+ </div>
136
+ <% end %>
137
+
138
+ <% if can?(:copy) %>
139
+ <sl-tooltip content="<%= t(:copy_page) %>">
140
+ <%= button_to(
141
+ render_icon("file-copy"),
142
+ url(:clipboard_insert),
143
+ "data-turbo-method": :post,
144
+ class: "icon_button"
145
+ ) %>
146
+ </sl-tooltip>
147
+ <% else %>
148
+ <div class="sitemap_tool disabled">
149
+ <sl-tooltip
150
+ content="<%= t(:cannot_edit_page) %>"
151
+ class="like-hint-tooltip"
152
+ placement="bottom-start"
153
+ >
154
+ <%= render_icon("file-copy") %>
155
+ </sl-tooltip>
156
+ </div>
157
+ <% end %>
158
+
159
+ <% if can?(:destroy) %>
160
+ <sl-tooltip content="<%= t(:delete_page) %>">
161
+ <%= link_to_confirm_dialog(
162
+ render_icon("delete-bin-2"),
163
+ t(:confirm_to_delete_page),
164
+ url(:page),
165
+ class: "icon_button"
166
+ ) %>
167
+ </sl-tooltip>
168
+ <% else %>
169
+ <div class="sitemap_tool disabled">
170
+ <sl-tooltip
171
+ content="<%= t(:cannot_edit_page) %>"
172
+ class="like-hint-tooltip"
173
+ placement="bottom-start"
174
+ >
175
+ <%= render_icon("delete-bin-2") %>
176
+ </sl-tooltip>
177
+ </div>
178
+ <% end %>
179
+
180
+ <% if can?(:create) %>
181
+ <sl-tooltip content="<%= t(:create_page) %>">
182
+ <%= link_to_dialog(
183
+ render_icon(:add),
184
+ url(:new_child_page),
185
+ {
186
+ title: t(:create_page),
187
+ size: "340x165",
188
+ overflow: true
189
+ },
190
+ class: "icon_button"
191
+ ) %>
192
+ </sl-tooltip>
193
+ <% else %>
194
+ <div class="sitemap_tool disabled">
195
+ <sl-tooltip
196
+ content="<%= t(:cannot_edit_page) %>"
197
+ class="like-hint-tooltip"
198
+ placement="bottom-start"
199
+ >
200
+ <%= render_icon("file-add") %>
201
+ </sl-tooltip>
202
+ </div>
203
+ <% end %>
204
+ </div>
205
+ </div>
206
+ <ul
207
+ id="page_<%= @page.id %>_children"
208
+ class="children<%= ' hidden' if @page.folded? %>"
209
+ data-parent-id="<%= @page.id %>"
210
+ >
211
+ <%= render Alchemy::Admin::PageNode.with_collection(@page.children) %>
212
+ </ul>
213
+ </li>
214
+ </alchemy-page-node>
@@ -0,0 +1,70 @@
1
+ module Alchemy
2
+ module Admin
3
+ class PageNode < ViewComponent::Base
4
+ with_collection_parameter :page
5
+
6
+ attr_reader :page
7
+
8
+ delegate :current_alchemy_user,
9
+ :render_icon,
10
+ :link_to_dialog,
11
+ :link_to_confirm_dialog,
12
+ :page_layout_missing_warning,
13
+ to: :helpers
14
+
15
+ def initialize(page:)
16
+ @page = page
17
+ end
18
+
19
+ def can?(action)
20
+ helpers.can?(action, page.__getobj__)
21
+ end
22
+
23
+ # Memoized URL path
24
+ def url(key)
25
+ self.class.routes[key].sub(PAGE_ID, page.id.to_s)
26
+ end
27
+
28
+ # Memoized translation lookup
29
+ def t(key)
30
+ self.class.translations[key]
31
+ end
32
+
33
+ PAGE_ID = "__ID__"
34
+
35
+ class << self
36
+ # URL templates - computed once, reused for all pages
37
+ def routes
38
+ @_routes ||= begin
39
+ router = Alchemy::Engine.routes.url_helpers
40
+ {
41
+ edit_page: router.edit_admin_page_path(PAGE_ID),
42
+ info_page: router.info_admin_page_path(PAGE_ID),
43
+ configure_page: router.configure_admin_page_path(PAGE_ID),
44
+ page: router.admin_page_path(PAGE_ID),
45
+ new_child_page: router.new_admin_page_path(parent_id: PAGE_ID),
46
+ clipboard_insert: router.insert_admin_clipboard_path(
47
+ remarkable_type: :pages,
48
+ remarkable_id: PAGE_ID
49
+ )
50
+ }.freeze
51
+ end
52
+ end
53
+
54
+ # Translations - computed once, reused for all pages
55
+ def translations
56
+ @_translations ||= {
57
+ cannot_edit_page: Alchemy.t("Your user role does not allow you to edit this page"),
58
+ edit_page: Alchemy.t(:edit_page),
59
+ page_infos: Alchemy.t(:page_infos),
60
+ edit_page_properties: Alchemy.t(:edit_page_properties),
61
+ copy_page: Alchemy.t(:copy_page),
62
+ delete_page: Alchemy.t(:delete_page),
63
+ confirm_to_delete_page: Alchemy.t(:confirm_to_delete_page),
64
+ create_page: Alchemy.t(:create_page)
65
+ }.freeze
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,36 @@
1
+ module Alchemy
2
+ module Admin
3
+ class PictureThumbnail < ViewComponent::Base
4
+ attr_reader :picture, :size, :css_class, :placeholder
5
+
6
+ def initialize(picture, size: :medium, css_class: nil, placeholder: nil)
7
+ @picture = picture
8
+ @size = size
9
+ @css_class = css_class
10
+ @placeholder = placeholder
11
+ end
12
+
13
+ def call
14
+ thumbnail_url = picture&.thumbnail_url(size: preview_size)
15
+ if thumbnail_url || placeholder
16
+ content_tag "alchemy-picture-thumbnail", placeholder, {
17
+ src: thumbnail_url,
18
+ name: picture&.description_for(::Alchemy::Current.language) || picture&.name,
19
+ class: css_class
20
+ }
21
+ else
22
+ render Alchemy::Admin::Icon.new("file-damage", size: "xl")
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def preview_size
29
+ Alchemy::Picture::THUMBNAIL_SIZES.fetch(
30
+ size,
31
+ Alchemy::Picture::THUMBNAIL_SIZES[:medium]
32
+ )
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,15 @@
1
+ <alchemy-publish-page-button class="toolbar_button" id="publish_page_button">
2
+ <sl-tooltip content="<%= tooltip_content %>">
3
+ <%= form_tag publish_path, id: "publish_page_form", data: {turbo: true} do %>
4
+ <sl-button
5
+ type="submit"
6
+ outline
7
+ variant="<%= has_unpublished_changes? ? 'primary' : 'default' %>"
8
+ <%= "disabled" if disabled? %>
9
+ >
10
+ <sl-icon slot="prefix" name="upload-cloud-2" label="Publish" style="font-size: var(--icon-size-md)"></sl-icon>
11
+ <%= button_label %>
12
+ </sl-button>
13
+ <% end %>
14
+ </sl-tooltip>
15
+ </alchemy-publish-page-button>
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ module Admin
5
+ # The button publishes the page and indicates unpublished changes if any.
6
+ #
7
+ # == Example
8
+ #
9
+ # <%= render Alchemy::Admin::PublishPageButton.new(page: @page) %>
10
+ #
11
+ class PublishPageButton < ViewComponent::Base
12
+ delegate :cannot?, :render_icon, to: :helpers
13
+
14
+ attr_reader :page
15
+
16
+ def initialize(page:, published: false)
17
+ @page = page
18
+ @published = published
19
+ end
20
+
21
+ def disabled?
22
+ @published || cannot?(:publish, page) || !has_unpublished_changes?
23
+ end
24
+
25
+ def has_unpublished_changes?
26
+ !@published && page.has_unpublished_changes?
27
+ end
28
+
29
+ def tooltip_content
30
+ if !page.language.public?
31
+ Alchemy.t(:publish_page_language_not_public)
32
+ elsif cannot?(:publish, page)
33
+ Alchemy.t(:publish_page_not_allowed)
34
+ elsif @published || !has_unpublished_changes?
35
+ Alchemy.t(:no_unpublished_changes)
36
+ else
37
+ Alchemy.t(:explain_publishing)
38
+ end
39
+ end
40
+
41
+ def button_label
42
+ if page.public_version.nil?
43
+ t(".publish_page")
44
+ else
45
+ t(".publish_changes")
46
+ end
47
+ end
48
+
49
+ def publish_path
50
+ Alchemy::Engine.routes.url_helpers.publish_admin_page_path(page)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -2,33 +2,41 @@
2
2
 
3
3
  module Alchemy
4
4
  module Admin
5
- module TagsHelper
6
- # Renders tags list items for given class name
7
- #
5
+ # Renders tags list items for given class name
6
+ #
7
+ class TagsList < ViewComponent::Base
8
+ attr_reader :class_name
9
+
10
+ delegate :search_filter_params, to: :helpers
11
+
8
12
  # @param class_name [String]
9
13
  # The class_name representing a tagged class
10
14
  #
15
+ def initialize(class_name)
16
+ raise ArgumentError, "Please provide a String as class_name" if class_name.nil?
17
+ @class_name = class_name
18
+ end
19
+
11
20
  # @return [String]
12
21
  # A HTML string containing <tt><li></tt> tags
13
22
  #
14
- def render_tag_list(class_name)
15
- raise ArgumentError, "Please provide a String as class_name" if class_name.nil?
16
-
17
- sorted_tags_from(class_name: class_name).map do |tag|
23
+ def call
24
+ sorted_tags.map do |tag|
18
25
  content_tag("li", name: tag.name, class: filtered_by_tag?(tag) ? "active" : nil) do
19
26
  link_to(
20
27
  "#{tag.name} (#{tag.taggings_count})",
21
- url_for(
28
+ helpers.url_for(
22
29
  search_filter_params.except(:page, :tagged_with).merge(
23
30
  tagged_with: tags_for_filter(current: tag).presence
24
31
  )
25
- ),
26
- remote: request.xhr?
32
+ )
27
33
  )
28
34
  end
29
35
  end.join.html_safe
30
36
  end
31
37
 
38
+ private
39
+
32
40
  # Returns true if the given tag is in +params[:tagged_with]+
33
41
  #
34
42
  def filtered_by_tag?(tag)
@@ -53,7 +61,7 @@ module Alchemy
53
61
  search_filter_params[:tagged_with].to_s.split(",")
54
62
  end
55
63
 
56
- def sorted_tags_from(class_name:)
64
+ def sorted_tags
57
65
  class_name.constantize.tag_counts.sort { |x, y| x.name.downcase <=> y.name.downcase }
58
66
  end
59
67
  end
@@ -34,18 +34,22 @@ module Alchemy
34
34
  class ToolbarButton < ViewComponent::Base
35
35
  erb_template <<-ERB
36
36
  <div class="toolbar_button" id="<%= id %>">
37
- <sl-tooltip content="<%= label %>" placement="<%= tooltip_placement %>">
38
- <%= link_to(render_icon(icon, style: icon_style), url, {
39
- class: css_classes,
40
- "data-dialog-options" => dialog ? dialog_options.to_json : nil,
41
- "data-alchemy-hotkey" => hotkey,
42
- :is => dialog ? "alchemy-dialog-link" : nil
43
- }.merge(link_options)) %>
37
+ <sl-tooltip content="<%= label %>" placement="<%= tooltip_placement %>" <%= "disabled" if disabled? %>>
38
+ <% if disabled? %>
39
+ <%= tag.a(render_icon(icon, style: icon_style), class: css_classes + ["disabled"], tabindex: "-1") %>
40
+ <% else %>
41
+ <%= link_to(render_icon(icon, style: icon_style), url, {
42
+ class: css_classes,
43
+ "data-dialog-options" => dialog ? dialog_options.to_json : nil,
44
+ "data-alchemy-hotkey" => hotkey,
45
+ :is => dialog ? "alchemy-dialog-link" : nil
46
+ }.merge(link_options)) %>
47
+ <% end %>
44
48
  </sl-tooltip>
45
49
  </div>
46
50
  ERB
47
51
 
48
- delegate :can?, :link_to, :link_to_dialog, :render_icon, to: :helpers
52
+ delegate :cannot?, :link_to, :link_to_dialog, :render_icon, to: :helpers
49
53
 
50
54
  attr_reader :url,
51
55
  :icon,
@@ -92,12 +96,12 @@ module Alchemy
92
96
  @tooltip_placement = tooltip_placement
93
97
  end
94
98
 
95
- def render?
96
- skip_permission_check || can?(*permission_options)
97
- end
98
-
99
99
  private
100
100
 
101
+ def disabled?
102
+ @_disabled ||= !skip_permission_check && cannot?(*permission_options)
103
+ end
104
+
101
105
  def css_classes = ["icon_button", active && "active"].compact
102
106
 
103
107
  def permission_options = if_permitted_to.presence || permissions_from_url
@@ -106,7 +110,7 @@ module Alchemy
106
110
  action_controller = url.delete_prefix("/").split("/")
107
111
  [
108
112
  action_controller.last.to_sym,
109
- action_controller[0..-2].join("_").to_sym
113
+ action_controller[0..action_controller.length - 2].join("_").to_sym
110
114
  ]
111
115
  end
112
116
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ module Ingredients
5
+ class AudioEditor < FileEditor
6
+ end
7
+ end
8
+ end