alchemy_cms 5.2.0 → 6.0.0.b3

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 (289) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +6 -14
  3. data/.gitignore +0 -1
  4. data/.hound.yml +1 -1
  5. data/.rubocop.yml +46 -4
  6. data/CHANGELOG.md +114 -5
  7. data/Gemfile +8 -1
  8. data/README.md +5 -2
  9. data/alchemy_cms.gemspec +78 -65
  10. data/app/assets/javascripts/alchemy/admin.js +0 -2
  11. data/app/assets/javascripts/alchemy/alchemy.base.js.coffee +0 -27
  12. data/app/assets/javascripts/alchemy/alchemy.confirm_dialog.js.coffee +2 -1
  13. data/app/assets/javascripts/alchemy/alchemy.dialog.js.coffee +1 -1
  14. data/app/assets/javascripts/alchemy/alchemy.dragndrop.js.coffee +0 -25
  15. data/app/assets/javascripts/alchemy/alchemy.element_editors.js.coffee +1 -1
  16. data/app/assets/javascripts/alchemy/alchemy.elements_window.js.coffee +2 -0
  17. data/app/assets/javascripts/alchemy/alchemy.fixed_elements.js +1 -1
  18. data/app/assets/javascripts/alchemy/alchemy.gui.js.coffee +3 -1
  19. data/app/assets/javascripts/alchemy/alchemy.image_overlay.coffee +1 -1
  20. data/app/assets/javascripts/alchemy/alchemy.link_dialog.js.coffee +40 -27
  21. data/app/assets/javascripts/alchemy/templates/node_folder.hbs +1 -1
  22. data/app/assets/stylesheets/alchemy/_extends.scss +15 -2
  23. data/app/assets/stylesheets/alchemy/admin.scss +1 -1
  24. data/app/assets/stylesheets/alchemy/archive.scss +20 -5
  25. data/app/assets/stylesheets/alchemy/buttons.scss +0 -4
  26. data/app/assets/stylesheets/alchemy/elements.scss +73 -61
  27. data/app/assets/stylesheets/alchemy/images.scss +8 -0
  28. data/app/assets/stylesheets/alchemy/node-select.scss +4 -3
  29. data/app/assets/stylesheets/alchemy/page-select.scss +1 -0
  30. data/app/controllers/alchemy/admin/attachments_controller.rb +8 -4
  31. data/app/controllers/alchemy/admin/base_controller.rb +5 -7
  32. data/app/controllers/alchemy/admin/elements_controller.rb +59 -34
  33. data/app/controllers/alchemy/admin/essence_audios_controller.rb +30 -0
  34. data/app/controllers/alchemy/admin/essence_files_controller.rb +0 -14
  35. data/app/controllers/alchemy/admin/essence_pictures_controller.rb +8 -79
  36. data/app/controllers/alchemy/admin/essence_videos_controller.rb +33 -0
  37. data/app/controllers/alchemy/admin/ingredients_controller.rb +30 -0
  38. data/app/controllers/alchemy/admin/layoutpages_controller.rb +0 -1
  39. data/app/controllers/alchemy/admin/pages_controller.rb +7 -22
  40. data/app/controllers/alchemy/admin/pictures_controller.rb +56 -17
  41. data/app/controllers/alchemy/admin/resources_controller.rb +84 -10
  42. data/app/controllers/alchemy/api/elements_controller.rb +13 -4
  43. data/app/controllers/alchemy/api/pages_controller.rb +4 -3
  44. data/app/controllers/concerns/alchemy/admin/archive_overlay.rb +13 -3
  45. data/app/controllers/concerns/alchemy/admin/crop_action.rb +26 -0
  46. data/app/decorators/alchemy/element_editor.rb +26 -1
  47. data/app/decorators/alchemy/ingredient_editor.rb +158 -0
  48. data/app/helpers/alchemy/admin/elements_helper.rb +1 -0
  49. data/app/helpers/alchemy/admin/essences_helper.rb +1 -1
  50. data/app/helpers/alchemy/admin/ingredients_helper.rb +42 -0
  51. data/app/helpers/alchemy/elements_block_helper.rb +23 -6
  52. data/app/helpers/alchemy/elements_helper.rb +12 -5
  53. data/app/helpers/alchemy/pages_helper.rb +3 -11
  54. data/app/jobs/alchemy/base_job.rb +11 -0
  55. data/app/jobs/alchemy/publish_page_job.rb +11 -0
  56. data/app/models/alchemy/attachment.rb +24 -7
  57. data/app/models/alchemy/content.rb +1 -6
  58. data/app/models/alchemy/content/factory.rb +23 -27
  59. data/app/models/alchemy/element.rb +39 -72
  60. data/app/models/alchemy/element/definitions.rb +29 -27
  61. data/app/models/alchemy/element/element_contents.rb +131 -122
  62. data/app/models/alchemy/element/element_essences.rb +111 -98
  63. data/app/models/alchemy/element/element_ingredients.rb +184 -0
  64. data/app/models/alchemy/element/presenters.rb +104 -85
  65. data/app/models/alchemy/elements_repository.rb +126 -0
  66. data/app/models/alchemy/essence_audio.rb +12 -0
  67. data/app/models/alchemy/essence_headline.rb +40 -0
  68. data/app/models/alchemy/essence_picture.rb +4 -116
  69. data/app/models/alchemy/essence_richtext.rb +12 -0
  70. data/app/models/alchemy/essence_video.rb +12 -0
  71. data/app/models/alchemy/image_cropper_settings.rb +87 -0
  72. data/app/models/alchemy/ingredient.rb +183 -0
  73. data/app/models/alchemy/ingredient_validator.rb +97 -0
  74. data/app/models/alchemy/ingredients/audio.rb +29 -0
  75. data/app/models/alchemy/ingredients/boolean.rb +21 -0
  76. data/app/models/alchemy/ingredients/datetime.rb +20 -0
  77. data/app/models/alchemy/ingredients/file.rb +30 -0
  78. data/app/models/alchemy/ingredients/headline.rb +42 -0
  79. data/app/models/alchemy/ingredients/html.rb +19 -0
  80. data/app/models/alchemy/ingredients/link.rb +16 -0
  81. data/app/models/alchemy/ingredients/node.rb +23 -0
  82. data/app/models/alchemy/ingredients/page.rb +23 -0
  83. data/app/models/alchemy/ingredients/picture.rb +41 -0
  84. data/app/models/alchemy/ingredients/richtext.rb +57 -0
  85. data/app/models/alchemy/ingredients/select.rb +10 -0
  86. data/app/models/alchemy/ingredients/text.rb +17 -0
  87. data/app/models/alchemy/ingredients/video.rb +33 -0
  88. data/app/models/alchemy/language.rb +0 -11
  89. data/app/models/alchemy/page.rb +76 -33
  90. data/app/models/alchemy/page/fixed_attributes.rb +53 -51
  91. data/app/models/alchemy/page/page_elements.rb +186 -205
  92. data/app/models/alchemy/page/page_naming.rb +66 -64
  93. data/app/models/alchemy/page/page_natures.rb +139 -142
  94. data/app/models/alchemy/page/page_scopes.rb +117 -102
  95. data/app/models/alchemy/page/publisher.rb +50 -0
  96. data/app/models/alchemy/page/url_path.rb +1 -1
  97. data/app/models/alchemy/page_version.rb +58 -0
  98. data/app/models/alchemy/picture.rb +18 -40
  99. data/app/models/alchemy/picture/calculations.rb +2 -8
  100. data/app/models/alchemy/picture/preprocessor.rb +2 -0
  101. data/app/models/alchemy/picture/transformations.rb +24 -96
  102. data/app/models/concerns/alchemy/picture_thumbnails.rb +181 -0
  103. data/app/models/concerns/alchemy/touch_elements.rb +2 -2
  104. data/app/presenters/alchemy/picture_view.rb +88 -0
  105. data/app/serializers/alchemy/element_serializer.rb +5 -0
  106. data/app/serializers/alchemy/page_tree_serializer.rb +3 -2
  107. data/app/services/alchemy/delete_elements.rb +44 -0
  108. data/app/services/alchemy/duplicate_element.rb +56 -0
  109. data/app/views/alchemy/admin/attachments/_archive_overlay.html.erb +2 -3
  110. data/app/views/alchemy/admin/attachments/_file_to_assign.html.erb +3 -3
  111. data/app/views/alchemy/admin/attachments/assign.js.erb +11 -0
  112. data/app/views/alchemy/admin/attachments/index.html.erb +2 -3
  113. data/app/views/alchemy/admin/crop.html.erb +36 -0
  114. data/app/views/alchemy/admin/elements/_element.html.erb +14 -10
  115. data/app/views/alchemy/admin/elements/{_element_footer.html.erb → _footer.html.erb} +0 -0
  116. data/app/views/alchemy/admin/elements/{_new_element_form.html.erb → _form.html.erb} +1 -1
  117. data/app/views/alchemy/admin/elements/{_element_header.html.erb → _header.html.erb} +1 -1
  118. data/app/views/alchemy/admin/elements/{_element_toolbar.html.erb → _toolbar.html.erb} +5 -6
  119. data/app/views/alchemy/admin/elements/create.js.erb +1 -1
  120. data/app/views/alchemy/admin/elements/{trash.js.erb → destroy.js.erb} +2 -6
  121. data/app/views/alchemy/admin/elements/fold.js.erb +2 -2
  122. data/app/views/alchemy/admin/elements/new.html.erb +3 -3
  123. data/app/views/alchemy/admin/elements/order.js.erb +0 -17
  124. data/app/views/alchemy/admin/elements/update.js.erb +3 -2
  125. data/app/views/alchemy/admin/essence_audios/edit.html.erb +7 -0
  126. data/app/views/alchemy/admin/essence_pictures/update.js.erb +0 -1
  127. data/app/views/alchemy/admin/essence_videos/edit.html.erb +11 -0
  128. data/app/views/alchemy/admin/ingredients/_audio_fields.html.erb +4 -0
  129. data/app/views/alchemy/admin/ingredients/_file_fields.html.erb +18 -0
  130. data/app/views/alchemy/admin/ingredients/_picture_fields.html.erb +25 -0
  131. data/app/views/alchemy/admin/ingredients/_video_fields.html.erb +8 -0
  132. data/app/views/alchemy/admin/ingredients/edit.html.erb +4 -0
  133. data/app/views/alchemy/admin/layoutpages/edit.html.erb +0 -5
  134. data/app/views/alchemy/admin/nodes/_node.html.erb +2 -2
  135. data/app/views/alchemy/admin/pages/_anchor_link.html.erb +1 -1
  136. data/app/views/alchemy/admin/pages/_external_link.html.erb +1 -1
  137. data/app/views/alchemy/admin/pages/_file_link.html.erb +1 -1
  138. data/app/views/alchemy/admin/pages/_form.html.erb +0 -6
  139. data/app/views/alchemy/admin/pages/_internal_link.html.erb +1 -1
  140. data/app/views/alchemy/admin/pages/_tinymce_custom_config.html.erb +5 -2
  141. data/app/views/alchemy/admin/pages/_toolbar.html.erb +1 -1
  142. data/app/views/alchemy/admin/pages/edit.html.erb +36 -24
  143. data/app/views/alchemy/admin/pages/index.html.erb +2 -9
  144. data/app/views/alchemy/admin/partials/_remote_search_form.html.erb +2 -4
  145. data/app/views/alchemy/admin/partials/_routes.html.erb +7 -11
  146. data/app/views/alchemy/admin/partials/_search_form.html.erb +9 -0
  147. data/app/views/alchemy/admin/pictures/_archive.html.erb +1 -1
  148. data/app/views/alchemy/admin/pictures/_archive_overlay.html.erb +1 -1
  149. data/app/views/alchemy/admin/pictures/_filter_and_size_bar.html.erb +5 -7
  150. data/app/views/alchemy/admin/pictures/_infos.html.erb +0 -1
  151. data/app/views/alchemy/admin/pictures/_picture_to_assign.html.erb +4 -4
  152. data/app/views/alchemy/admin/pictures/assign.js.erb +10 -0
  153. data/app/views/alchemy/admin/pictures/index.html.erb +8 -3
  154. data/app/views/alchemy/admin/resources/_filter.html.erb +12 -0
  155. data/app/views/alchemy/admin/resources/_filter_bar.html.erb +14 -17
  156. data/app/views/alchemy/admin/resources/_form.html.erb +3 -0
  157. data/app/views/alchemy/admin/resources/_table_header.html.erb +15 -0
  158. data/app/views/alchemy/admin/resources/index.html.erb +3 -11
  159. data/app/views/alchemy/essences/_essence_audio_editor.html.erb +4 -0
  160. data/app/views/alchemy/essences/_essence_audio_view.html.erb +15 -0
  161. data/app/views/alchemy/essences/_essence_file_editor.html.erb +15 -6
  162. data/app/views/alchemy/essences/_essence_headline_editor.html.erb +36 -0
  163. data/app/views/alchemy/essences/_essence_headline_view.html.erb +10 -0
  164. data/app/views/alchemy/essences/_essence_link_editor.html.erb +8 -4
  165. data/app/views/alchemy/essences/_essence_picture_editor.html.erb +27 -12
  166. data/app/views/alchemy/essences/_essence_picture_view.html.erb +3 -3
  167. data/app/views/alchemy/essences/_essence_text_editor.html.erb +12 -4
  168. data/app/views/alchemy/essences/_essence_video_editor.html.erb +4 -0
  169. data/app/views/alchemy/essences/_essence_video_view.html.erb +18 -0
  170. data/app/views/alchemy/essences/shared/_essence_picture_tools.html.erb +21 -16
  171. data/app/views/alchemy/essences/shared/_linkable_essence_tools.html.erb +2 -2
  172. data/app/views/alchemy/ingredients/_audio_editor.html.erb +5 -0
  173. data/app/views/alchemy/ingredients/_audio_view.html.erb +14 -0
  174. data/app/views/alchemy/ingredients/_boolean_editor.html.erb +11 -0
  175. data/app/views/alchemy/ingredients/_boolean_view.html.erb +1 -0
  176. data/app/views/alchemy/ingredients/_datetime_editor.html.erb +17 -0
  177. data/app/views/alchemy/ingredients/_datetime_view.html.erb +9 -0
  178. data/app/views/alchemy/ingredients/_file_editor.html.erb +52 -0
  179. data/app/views/alchemy/ingredients/_file_view.html.erb +17 -0
  180. data/app/views/alchemy/ingredients/_headline_editor.html.erb +30 -0
  181. data/app/views/alchemy/ingredients/_headline_view.html.erb +9 -0
  182. data/app/views/alchemy/ingredients/_html_editor.html.erb +8 -0
  183. data/app/views/alchemy/ingredients/_html_view.html.erb +1 -0
  184. data/app/views/alchemy/ingredients/_link_editor.html.erb +24 -0
  185. data/app/views/alchemy/ingredients/_link_view.html.erb +9 -0
  186. data/app/views/alchemy/ingredients/_node_editor.html.erb +26 -0
  187. data/app/views/alchemy/ingredients/_node_view.html.erb +1 -0
  188. data/app/views/alchemy/ingredients/_page_editor.html.erb +25 -0
  189. data/app/views/alchemy/ingredients/_page_view.html.erb +4 -0
  190. data/app/views/alchemy/ingredients/_picture_editor.html.erb +60 -0
  191. data/app/views/alchemy/ingredients/_picture_view.html.erb +5 -0
  192. data/app/views/alchemy/ingredients/_richtext_editor.html.erb +12 -0
  193. data/app/views/alchemy/ingredients/_richtext_view.html.erb +3 -0
  194. data/app/views/alchemy/ingredients/_select_editor.html.erb +30 -0
  195. data/app/views/alchemy/ingredients/_select_view.html.erb +1 -0
  196. data/app/views/alchemy/ingredients/_text_editor.html.erb +20 -0
  197. data/app/views/alchemy/ingredients/_text_view.html.erb +16 -0
  198. data/app/views/alchemy/ingredients/_video_editor.html.erb +5 -0
  199. data/app/views/alchemy/ingredients/_video_view.html.erb +17 -0
  200. data/app/views/alchemy/ingredients/shared/_link_tools.html.erb +20 -0
  201. data/app/views/alchemy/ingredients/shared/_picture_tools.html.erb +57 -0
  202. data/config/brakeman.ignore +66 -159
  203. data/config/initializers/dragonfly.rb +10 -0
  204. data/config/locales/alchemy.en.yml +108 -64
  205. data/config/routes.rb +17 -22
  206. data/db/migrate/20201207131309_create_page_versions.rb +19 -0
  207. data/db/migrate/20201207135820_add_page_version_id_to_alchemy_elements.rb +76 -0
  208. data/db/migrate/20210205143548_rename_public_on_and_public_until_on_alchemy_pages.rb +10 -0
  209. data/db/migrate/20210326105046_add_sanitized_body_to_alchemy_essence_richtexts.rb +7 -0
  210. data/db/migrate/20210406093436_add_alchemy_essence_headlines.rb +12 -0
  211. data/db/migrate/20210506135919_create_essence_audios.rb +19 -0
  212. data/db/migrate/20210506140258_create_essence_videos.rb +23 -0
  213. data/db/migrate/20210508091432_create_alchemy_ingredients.rb +22 -0
  214. data/lib/alchemy/admin/preview_url.rb +2 -0
  215. data/lib/alchemy/deprecation.rb +1 -1
  216. data/lib/alchemy/dragonfly/processors/auto_orient.rb +18 -0
  217. data/lib/alchemy/dragonfly/processors/crop_resize.rb +35 -0
  218. data/lib/alchemy/elements_finder.rb +14 -60
  219. data/lib/alchemy/essence.rb +1 -2
  220. data/lib/alchemy/forms/builder.rb +21 -1
  221. data/lib/alchemy/hints.rb +8 -4
  222. data/lib/alchemy/page_layout.rb +0 -13
  223. data/lib/alchemy/permissions.rb +30 -29
  224. data/lib/alchemy/resource.rb +13 -3
  225. data/lib/alchemy/resource_filter.rb +40 -0
  226. data/lib/alchemy/resources_helper.rb +1 -16
  227. data/lib/alchemy/tasks/tidy.rb +29 -0
  228. data/lib/alchemy/test_support.rb +2 -11
  229. data/lib/alchemy/test_support/essence_shared_examples.rb +0 -1
  230. data/lib/alchemy/test_support/factories/element_factory.rb +8 -8
  231. data/lib/alchemy/test_support/factories/essence_audio_factory.rb +7 -0
  232. data/lib/alchemy/test_support/factories/essence_video_factory.rb +7 -0
  233. data/lib/alchemy/test_support/factories/ingredient_factory.rb +25 -0
  234. data/lib/alchemy/test_support/factories/page_factory.rb +20 -1
  235. data/lib/alchemy/test_support/factories/page_version_factory.rb +23 -0
  236. data/lib/alchemy/test_support/having_crop_action_examples.rb +170 -0
  237. data/lib/alchemy/test_support/having_picture_thumbnails_examples.rb +646 -0
  238. data/lib/alchemy/test_support/shared_ingredient_editor_examples.rb +21 -0
  239. data/lib/alchemy/test_support/shared_ingredient_examples.rb +75 -0
  240. data/lib/alchemy/tinymce.rb +17 -0
  241. data/lib/alchemy/upgrader/six_point_zero.rb +21 -0
  242. data/lib/alchemy/upgrader/tasks/add_page_versions.rb +33 -0
  243. data/lib/alchemy/upgrader/tasks/ingredients_migrator.rb +62 -0
  244. data/lib/alchemy/version.rb +1 -1
  245. data/lib/alchemy_cms.rb +1 -0
  246. data/lib/generators/alchemy/elements/elements_generator.rb +1 -0
  247. data/lib/generators/alchemy/elements/templates/view.html.erb +9 -0
  248. data/lib/generators/alchemy/elements/templates/view.html.haml +9 -0
  249. data/lib/generators/alchemy/elements/templates/view.html.slim +9 -0
  250. data/lib/generators/alchemy/ingredient/ingredient_generator.rb +38 -0
  251. data/lib/generators/alchemy/ingredient/templates/editor.html.erb +14 -0
  252. data/lib/generators/alchemy/ingredient/templates/model.rb.tt +13 -0
  253. data/lib/generators/alchemy/ingredient/templates/view.html.erb +1 -0
  254. data/lib/generators/alchemy/install/templates/dragonfly.rb.tt +1 -1
  255. data/lib/generators/alchemy/menus/templates/node.html.erb +1 -1
  256. data/lib/generators/alchemy/menus/templates/node.html.haml +1 -1
  257. data/lib/generators/alchemy/menus/templates/node.html.slim +1 -1
  258. data/lib/generators/alchemy/menus/templates/wrapper.html.erb +1 -1
  259. data/lib/generators/alchemy/menus/templates/wrapper.html.haml +1 -1
  260. data/lib/generators/alchemy/menus/templates/wrapper.html.slim +1 -1
  261. data/lib/tasks/alchemy/thumbnails.rake +4 -2
  262. data/lib/tasks/alchemy/tidy.rake +12 -0
  263. data/lib/tasks/alchemy/upgrade.rake +26 -0
  264. data/package.json +3 -2
  265. data/package/admin.js +11 -1
  266. data/package/src/__tests__/i18n.spec.js +23 -0
  267. data/package/src/file_editors.js +28 -0
  268. data/package/src/i18n.js +1 -3
  269. data/package/src/image_cropper.js +103 -0
  270. data/package/src/image_loader.js +58 -0
  271. data/package/src/node_tree.js +5 -5
  272. data/package/src/picture_editors.js +169 -0
  273. data/package/src/utils/__tests__/ajax.spec.js +20 -12
  274. data/package/src/utils/ajax.js +8 -3
  275. data/vendor/assets/javascripts/jquery_plugins/jquery.Jcrop.min.js +3 -18
  276. data/vendor/assets/stylesheets/jquery.Jcrop.min.scss +2 -28
  277. metadata +292 -55
  278. data/app/assets/javascripts/alchemy/alchemy.image_cropper.js.coffee +0 -44
  279. data/app/assets/javascripts/alchemy/alchemy.trash_window.js.coffee +0 -30
  280. data/app/assets/stylesheets/alchemy/trash.scss +0 -8
  281. data/app/controllers/alchemy/admin/trash_controller.rb +0 -44
  282. data/app/views/alchemy/admin/attachments/_filter_bar.html.erb +0 -29
  283. data/app/views/alchemy/admin/essence_files/assign.js.erb +0 -3
  284. data/app/views/alchemy/admin/essence_pictures/assign.js.erb +0 -4
  285. data/app/views/alchemy/admin/essence_pictures/crop.html.erb +0 -48
  286. data/app/views/alchemy/admin/pictures/_filter_bar.html.erb +0 -30
  287. data/app/views/alchemy/admin/trash/clear.js.erb +0 -4
  288. data/app/views/alchemy/admin/trash/index.html.erb +0 -31
  289. data/lib/alchemy/test_support/factories.rb +0 -16
@@ -1,4 +1,4 @@
1
- - cache menu do
1
+ - cache [menu, @page, @preview_mode] do
2
2
  ul.nav
3
3
  = render partial: menu.to_partial_path,
4
4
  collection: menu.children.includes(:page, :children),
@@ -4,8 +4,8 @@ namespace :alchemy do
4
4
  namespace :generate do
5
5
  desc "Generates all thumbnails for Alchemy Pictures and EssencePictures."
6
6
  task thumbnails: [
7
- "alchemy_dragonfly_s3:generate:picture_thumbnails",
8
- "alchemy_dragonfly_s3:generate:essence_picture_thumbnails",
7
+ "alchemy:generate:picture_thumbnails",
8
+ "alchemy:generate:essence_picture_thumbnails",
9
9
  ]
10
10
 
11
11
  desc "Generates thumbnails for Alchemy Pictures."
@@ -14,6 +14,8 @@ namespace :alchemy do
14
14
  puts "Please wait..."
15
15
 
16
16
  Alchemy::Picture.find_each do |picture|
17
+ next unless picture.has_convertible_format?
18
+
17
19
  puts Alchemy::PictureThumb.generate_thumbs!(picture)
18
20
  end
19
21
 
@@ -8,6 +8,8 @@ namespace :alchemy do
8
8
  Rake::Task["alchemy:tidy:element_positions"].invoke
9
9
  Rake::Task["alchemy:tidy:content_positions"].invoke
10
10
  Rake::Task["alchemy:tidy:remove_orphaned_records"].invoke
11
+ Rake::Task["alchemy:tidy:remove_trashed_elements"].invoke
12
+ Rake::Task["alchemy:tidy:remove_duplicate_legacy_urls"].invoke
11
13
  end
12
14
 
13
15
  desc "Fixes element positions."
@@ -36,6 +38,16 @@ namespace :alchemy do
36
38
  Alchemy::Tidy.remove_orphaned_contents
37
39
  end
38
40
 
41
+ desc "Remove trashed elements."
42
+ task remove_trashed_elements: [:environment] do
43
+ Alchemy::Tidy.remove_trashed_elements
44
+ end
45
+
46
+ desc "Remove duplicate legacy URLs"
47
+ task remove_duplicate_legacy_urls: [:environment] do
48
+ Alchemy::Tidy.remove_duplicate_legacy_urls
49
+ end
50
+
39
51
  desc "List Alchemy elements usage"
40
52
  task elements_usage: :environment do
41
53
  puts "\n"
@@ -7,6 +7,7 @@ namespace :alchemy do
7
7
  task upgrade: [
8
8
  "alchemy:upgrade:prepare",
9
9
  "alchemy:upgrade:5.0:run",
10
+ "alchemy:upgrade:6.0:run",
10
11
  ] do
11
12
  Alchemy::Upgrader.display_todos
12
13
  end
@@ -59,5 +60,30 @@ namespace :alchemy do
59
60
  Alchemy::Upgrader::FivePointZero.remove_root_page
60
61
  end
61
62
  end
63
+
64
+ desc "Upgrade Alchemy to v6.0"
65
+ task "6.0" => [
66
+ "alchemy:upgrade:prepare",
67
+ "alchemy:upgrade:6.0:run",
68
+ ] do
69
+ Alchemy::Upgrader.display_todos
70
+ end
71
+
72
+ namespace "6.0" do
73
+ task "run" => [
74
+ "alchemy:upgrade:6.0:create_public_page_versions",
75
+ "alchemy:upgrade:6.0:create_ingredients",
76
+ ]
77
+
78
+ desc "Create public page versions"
79
+ task create_public_page_versions: [:environment] do
80
+ Alchemy::Upgrader::SixPointZero.create_public_page_versions
81
+ end
82
+
83
+ desc "Create ingredients for elements with ingredients defined"
84
+ task create_ingredients: [:environment] do
85
+ Alchemy::Upgrader::SixPointZero.create_ingredients
86
+ end
87
+ end
62
88
  end
63
89
  end
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alchemy_cms/admin",
3
- "version": "0.2.0",
3
+ "version": "6.0.0-b2",
4
4
  "description": "AlchemyCMS",
5
5
  "browser": "package/admin.js",
6
6
  "files": [
@@ -24,12 +24,13 @@
24
24
  },
25
25
  "homepage": "https://github.com/AlchemyCMS/alchemy_cms#readme",
26
26
  "dependencies": {
27
+ "lodash-es": "^4.17.21",
27
28
  "sortablejs": "^1.10.2"
28
29
  },
29
30
  "devDependencies": {
30
31
  "@babel/core": "^7.9.6",
31
32
  "@babel/preset-env": "^7.9.6",
32
- "babel-jest": "^26.0.1",
33
+ "babel-jest": "^27.0.1",
33
34
  "jest": "^25.2.7",
34
35
  "prettier": "^2.0.2",
35
36
  "xhr-mock": "^2.5.1"
data/package/admin.js CHANGED
@@ -1,5 +1,10 @@
1
1
  import translate from "./src/i18n"
2
+ import translationData from "./src/translations"
2
3
  import NodeTree from "./src/node_tree"
4
+ import fileEditors from "./src/file_editors"
5
+ import pictureEditors from "./src/picture_editors"
6
+ import ImageLoader from "./src/image_loader"
7
+ import ImageCropper from "./src/image_cropper"
3
8
 
4
9
  // Global Alchemy object
5
10
  if (typeof window.Alchemy === "undefined") {
@@ -10,5 +15,10 @@ if (typeof window.Alchemy === "undefined") {
10
15
  Object.assign(Alchemy, {
11
16
  // Global utility method for translating a given string
12
17
  t: translate,
13
- NodeTree
18
+ translations: Object.assign(Alchemy.translations || {}, translationData),
19
+ NodeTree,
20
+ fileEditors,
21
+ pictureEditors,
22
+ ImageLoader: ImageLoader.init,
23
+ ImageCropper
14
24
  })
@@ -15,12 +15,20 @@ describe("translate", () => {
15
15
  })
16
16
 
17
17
  describe("if translation is present", () => {
18
+ beforeEach(() => {
19
+ Alchemy.translations = { en: { help: "Help" } }
20
+ })
21
+
18
22
  it("Returns translated string", () => {
19
23
  expect(translate("help")).toEqual("Help")
20
24
  })
21
25
 
22
26
  describe("if key includes a period", () => {
23
27
  describe("that is translated", () => {
28
+ beforeEach(() => {
29
+ Alchemy.translations = { en: { formats: { date: "Y-m-d" } } }
30
+ })
31
+
24
32
  it("splits into group", () => {
25
33
  expect(translate("formats.date")).toEqual("Y-m-d")
26
34
  })
@@ -40,6 +48,10 @@ describe("translate", () => {
40
48
  })
41
49
 
42
50
  describe("if replacement is given", () => {
51
+ beforeEach(() => {
52
+ Alchemy.translations = { en: { allowed_chars: "of %{number} chars" } }
53
+ })
54
+
43
55
  it("replaces it", () => {
44
56
  expect(translate("allowed_chars", 5)).toEqual("of 5 chars")
45
57
  })
@@ -67,4 +79,15 @@ describe("translate", () => {
67
79
  spy.mockRestore()
68
80
  })
69
81
  })
82
+
83
+ describe("if Alchemy.translations is not set", () => {
84
+ it("Returns passed string and logs a warning", () => {
85
+ const spy = jest.spyOn(console, "warn").mockImplementation(() => {})
86
+ expect(translate("help")).toEqual("help")
87
+ expect(spy.mock.calls).toEqual([
88
+ ["Translations for locale kl not found!"]
89
+ ])
90
+ spy.mockRestore()
91
+ })
92
+ })
70
93
  })
@@ -0,0 +1,28 @@
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
+ Alchemy.setElementDirty(this.container.closest(".element-editor"))
20
+ return false
21
+ }
22
+ }
23
+
24
+ export default function init(selector) {
25
+ document.querySelectorAll(selector).forEach((node) => {
26
+ new FileEditor(node)
27
+ })
28
+ }
data/package/src/i18n.js CHANGED
@@ -1,5 +1,3 @@
1
- import translationData from "./translations"
2
-
3
1
  const KEY_SEPARATOR = /\./
4
2
 
5
3
  function currentLocale() {
@@ -11,7 +9,7 @@ function currentLocale() {
11
9
 
12
10
  function getTranslations() {
13
11
  const locale = currentLocale()
14
- const translations = translationData[locale]
12
+ const translations = Alchemy.translations && Alchemy.translations[locale]
15
13
 
16
14
  if (translations) {
17
15
  return translations
@@ -0,0 +1,103 @@
1
+ export default class ImageCropper {
2
+ constructor(
3
+ minSize,
4
+ defaultBox,
5
+ aspectRatio,
6
+ trueSize,
7
+ formFieldIds,
8
+ elementId
9
+ ) {
10
+ this.initialized = false
11
+
12
+ this.minSize = minSize
13
+ this.defaultBox = defaultBox
14
+ this.aspectRatio = aspectRatio
15
+ this.trueSize = trueSize
16
+ this.cropFromField = document.getElementById(formFieldIds[0])
17
+ this.cropSizeField = document.getElementById(formFieldIds[1])
18
+ this.elementId = elementId
19
+ this.dialog = Alchemy.currentDialog()
20
+ this.dialog.options.closed = this.destroy
21
+
22
+ this.init()
23
+ this.bind()
24
+ }
25
+
26
+ get jcropOptions() {
27
+ return {
28
+ onSelect: this.update.bind(this),
29
+ setSelect: this.box,
30
+ aspectRatio: this.aspectRatio,
31
+ minSize: this.minSize,
32
+ boxWidth: 800,
33
+ boxHeight: 600,
34
+ trueSize: this.trueSize,
35
+ closed: this.destroy.bind(this)
36
+ }
37
+ }
38
+
39
+ get cropFrom() {
40
+ if (this.cropFromField.value) {
41
+ return this.cropFromField.value.split("x").map((v) => parseInt(v))
42
+ }
43
+ }
44
+
45
+ get cropSize() {
46
+ if (this.cropSizeField.value) {
47
+ return this.cropSizeField.value.split("x").map((v) => parseInt(v))
48
+ }
49
+ }
50
+
51
+ get box() {
52
+ if (this.cropFrom && this.cropSize) {
53
+ return [
54
+ this.cropFrom[0],
55
+ this.cropFrom[1],
56
+ this.cropFrom[0] + this.cropSize[0],
57
+ this.cropFrom[1] + this.cropSize[1]
58
+ ]
59
+ } else {
60
+ return this.defaultBox
61
+ }
62
+ }
63
+
64
+ init() {
65
+ if (!this.initialized) {
66
+ this.api = $.Jcrop("#imageToCrop", this.jcropOptions)
67
+ this.initialized = true
68
+ }
69
+ }
70
+
71
+ update(coords) {
72
+ this.cropFromField.value = Math.round(coords.x) + "x" + Math.round(coords.y)
73
+ this.cropFromField.dispatchEvent(new Event("change"))
74
+ this.cropSizeField.value = Math.round(coords.w) + "x" + Math.round(coords.h)
75
+ this.cropFromField.dispatchEvent(new Event("change"))
76
+ }
77
+
78
+ reset() {
79
+ this.api.setSelect(this.defaultBox)
80
+ this.cropFromField.value = `${this.box[0]}x${this.box[1]}`
81
+ this.cropSizeField.value = `${this.box[2]}x${this.box[3] - this.box[1]}`
82
+ }
83
+
84
+ destroy() {
85
+ if (this.api) {
86
+ this.api.destroy()
87
+ }
88
+ this.initialized = false
89
+ return true
90
+ }
91
+
92
+ bind() {
93
+ this.dialog.dialog_body.find('button[type="submit"]').click(() => {
94
+ Alchemy.setElementDirty(`[data-element-id='${this.elementId}']`)
95
+ this.dialog.close()
96
+ return false
97
+ })
98
+ this.dialog.dialog_body.find('button[type="reset"]').click(() => {
99
+ this.reset()
100
+ return false
101
+ })
102
+ }
103
+ }
@@ -0,0 +1,58 @@
1
+ // Shows spinner while loading images and
2
+ // fades the image after its been loaded
3
+
4
+ const DEFAULT_SPINNER_OPTIONS = { fill: "#fff" }
5
+
6
+ export default class ImageLoader {
7
+ static init(scope = document, spinnerOptions = DEFAULT_SPINNER_OPTIONS) {
8
+ if (typeof scope === "string") {
9
+ scope = document.querySelector(scope)
10
+ }
11
+ scope.querySelectorAll("img").forEach((image) => {
12
+ const loader = new ImageLoader(image, spinnerOptions)
13
+ loader.load()
14
+ })
15
+ }
16
+
17
+ constructor(image, spinnerOptions = DEFAULT_SPINNER_OPTIONS) {
18
+ this.image = image
19
+ this.parent = image.parentNode
20
+ this.spinner = new Alchemy.Spinner("small", spinnerOptions)
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.image.parentElement)
34
+ }
35
+
36
+ onLoaded() {
37
+ this.removeSpinner()
38
+ this.image.classList.remove("loading")
39
+ this.unbind()
40
+ }
41
+
42
+ onError() {
43
+ this.removeSpinner()
44
+ this.parent.innerHtml = '<span class="icon warn"></span>'
45
+ this.unbind()
46
+ }
47
+
48
+ unbind() {
49
+ this.image.removeEventListener("load", this.onLoaded)
50
+ this.image.removeEventListener("error", this.onError)
51
+ }
52
+
53
+ removeSpinner() {
54
+ this.parent.querySelectorAll(".spinner").forEach((spinner) => {
55
+ spinner.remove()
56
+ })
57
+ }
58
+ }
@@ -6,7 +6,7 @@ function displayNodeFolders() {
6
6
  document.querySelectorAll("li.menu-item").forEach((el) => {
7
7
  const leftIconArea = el.querySelector(".nodes_tree-left_images")
8
8
  const list = el.querySelector(".children")
9
- const node = { folded: el.dataset.folded === "true", id: el.dataset.id }
9
+ const node = { folded: el.dataset.folded === "true", id: el.dataset.id, type: el.dataset.type }
10
10
 
11
11
  if (list.children.length > 0 || node.folded) {
12
12
  leftIconArea.innerHTML = HandlebarsTemplates.node_folder({ node: node })
@@ -17,9 +17,9 @@ function displayNodeFolders() {
17
17
  }
18
18
 
19
19
  function onFinishDragging(evt) {
20
- const url = Alchemy.routes.move_api_node_path(evt.item.dataset.id)
20
+ const url = Alchemy.routes[evt.item.dataset.type].move_api_path(evt.item.dataset.id)
21
21
  const data = {
22
- target_parent_id: evt.to.dataset.nodeId,
22
+ target_parent_id: evt.to.dataset.recordId,
23
23
  new_position: evt.newIndex
24
24
  }
25
25
 
@@ -36,9 +36,9 @@ function onFinishDragging(evt) {
36
36
 
37
37
  function handleNodeFolders() {
38
38
  on("click", ".nodes_tree", ".node_folder", function () {
39
- const nodeId = this.dataset.nodeId
39
+ const nodeId = this.dataset.recordId
40
40
  const menu_item = this.closest("li.menu-item")
41
- const url = Alchemy.routes.toggle_folded_api_node_path(nodeId)
41
+ const url = Alchemy.routes[this.dataset.recordType].toggle_folded_api_path(nodeId)
42
42
  const list = menu_item.querySelector(".children")
43
43
 
44
44
  ajax("PATCH", url)
@@ -0,0 +1,169 @@
1
+ import debounce from "lodash/debounce"
2
+ import max from "lodash/max"
3
+ import ajax from "./utils/ajax"
4
+ import ImageLoader from "./image_loader"
5
+
6
+ const UPDATE_DELAY = 125
7
+ const IMAGE_PLACEHOLDER = '<i class="icon far fa-image fa-fw"></i>'
8
+ const EMPTY_IMAGE = '<img src="" class="img_paddingtop" />'
9
+ const THUMBNAIL_SIZE = "160x120"
10
+
11
+ class PictureEditor {
12
+ constructor(container) {
13
+ this.container = container
14
+ this.cropFromField = container.querySelector("[data-crop-from]")
15
+ this.cropSizeField = container.querySelector("[data-crop-size]")
16
+ this.pictureIdField = container.querySelector("[data-picture-id]")
17
+ this.targetSizeField = container.querySelector("[data-target-size]")
18
+ this.imageCropperField = container.querySelector("[data-image-cropper]")
19
+ this.image = container.querySelector("img")
20
+ this.thumbnailBackground = container.querySelector(".thumbnail_background")
21
+ this.deleteButton = container.querySelector(".picture_tool.delete")
22
+ this.cropLink = container.querySelector(".crop_link")
23
+
24
+ this.targetSize = this.targetSizeField.dataset.targetSize
25
+ this.pictureId = this.pictureIdField.value
26
+
27
+ if (this.image) {
28
+ this.imageLoader = new ImageLoader(this.image)
29
+ }
30
+
31
+ this.update = debounce(() => {
32
+ this.updateImage()
33
+ this.updateCropLink()
34
+ }, UPDATE_DELAY)
35
+
36
+ this.deleteButton.addEventListener("click", this.removeImage.bind(this))
37
+ }
38
+
39
+ observe() {
40
+ const observer = new MutationObserver(this.mutationCallback.bind(this))
41
+
42
+ observer.observe(this.cropFromField, { attributes: true })
43
+ observer.observe(this.cropSizeField, { attributes: true })
44
+ observer.observe(this.pictureIdField, { attributes: true })
45
+ }
46
+
47
+ mutationCallback(mutationsList) {
48
+ for (const mutation of mutationsList) {
49
+ if ("pictureId" in mutation.target.dataset) {
50
+ this.cropFromField.value = ""
51
+ this.cropSizeField.value = ""
52
+ this.pictureId = mutation.target.value
53
+ }
54
+ this.update()
55
+ }
56
+ }
57
+
58
+ updateImage() {
59
+ if (!this.pictureId) return
60
+
61
+ this.ensureImage()
62
+ this.image.removeAttribute("alt")
63
+ this.image.removeAttribute("src")
64
+ this.imageLoader.load(true)
65
+ ajax("GET", `/admin/pictures/${this.pictureId}/url`, {
66
+ crop: this.imageCropperEnabled,
67
+ crop_from: this.cropFrom,
68
+ crop_size: this.cropSize,
69
+ flatten: true,
70
+ size: THUMBNAIL_SIZE
71
+ })
72
+ .then(({ data }) => {
73
+ this.image.src = data.url
74
+ this.image.alt = data.alt
75
+ this.image.title = data.title
76
+ })
77
+ .catch((error) => {
78
+ console.error(error.message || error)
79
+ Alchemy.growl(error.message || error, "error")
80
+ })
81
+ }
82
+
83
+ ensureImage() {
84
+ if (this.image) return
85
+
86
+ this.thumbnailBackground.innerHTML = EMPTY_IMAGE
87
+ this.image = this.container.querySelector("img")
88
+ this.imageLoader = new ImageLoader(this.image)
89
+ }
90
+
91
+ removeImage() {
92
+ this.thumbnailBackground.innerHTML = IMAGE_PLACEHOLDER
93
+ this.pictureIdField.value = ""
94
+ this.image = null
95
+ this.cropLink.classList.add("disabled")
96
+ Alchemy.setElementDirty(this.container.closest(".element-editor"))
97
+ }
98
+
99
+ updateCropLink() {
100
+ if (!this.pictureId || !this.imageCropperEnabled) return
101
+
102
+ this.cropLink.classList.remove("disabled")
103
+
104
+ if (this.cropLink.href.match(/(picture_id=)\d+/)) {
105
+ this.cropLink.href = this.cropLink.href.replace(
106
+ /(picture_id=)\d+/,
107
+ "$1" + this.pictureId
108
+ )
109
+ } else {
110
+ this.cropLink.href = this.cropLink.href + `&picture_id=${this.pictureId}`
111
+ }
112
+ }
113
+
114
+ get cropFrom() {
115
+ if (this.cropFromField.value === "") {
116
+ return this.defaultCropFrom.join("x")
117
+ }
118
+ return this.cropFromField.value
119
+ }
120
+
121
+ get cropSize() {
122
+ if (this.cropSizeField.value === "") {
123
+ return this.defaultCropSize.join("x")
124
+ }
125
+ return this.cropSizeField.value
126
+ }
127
+
128
+ get defaultCropSize() {
129
+ if (!this.imageCropperEnabled) return []
130
+
131
+ const mask = this.targetSize.split("x").map((n) => parseInt(n))
132
+ const zoom = max([
133
+ mask[0] / this.imageFileWidth,
134
+ mask[1] / this.imageFileHeight
135
+ ])
136
+
137
+ return [Math.round(mask[0] / zoom), Math.round(mask[1] / zoom)]
138
+ }
139
+
140
+ get defaultCropFrom() {
141
+ if (!this.imageCropperEnabled) return []
142
+
143
+ const dimensions = this.defaultCropSize
144
+
145
+ return [
146
+ Math.round((this.imageFileWidth - dimensions[0]) / 2),
147
+ Math.round((this.imageFileHeight - dimensions[1]) / 2)
148
+ ]
149
+ }
150
+
151
+ get imageFileWidth() {
152
+ return parseInt(this.pictureIdField.dataset.imageFileWidth)
153
+ }
154
+
155
+ get imageFileHeight() {
156
+ return parseInt(this.pictureIdField.dataset.imageFileHeight)
157
+ }
158
+
159
+ get imageCropperEnabled() {
160
+ return this.targetSizeField.dataset.imageCropper === "true"
161
+ }
162
+ }
163
+
164
+ export default function init(selector) {
165
+ document.querySelectorAll(selector).forEach((node) => {
166
+ const thumbnail = new PictureEditor(node)
167
+ thumbnail.observe()
168
+ })
169
+ }