alchemy_cms 7.4.6 → 8.0.0.a

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.

Potentially problematic release.


This version of alchemy_cms might be problematic. Click here for more details.

Files changed (349) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +86 -0
  3. data/Gemfile +13 -6
  4. data/README.md +13 -5
  5. data/alchemy_cms.gemspec +14 -5
  6. data/app/assets/builds/alchemy/admin/page-select.css +1 -1
  7. data/app/assets/builds/alchemy/admin/print.css +1 -1
  8. data/app/assets/builds/alchemy/admin.css +2 -2
  9. data/app/assets/builds/alchemy/custom-properties.css +1 -1
  10. data/app/assets/builds/alchemy/welcome.css +1 -1
  11. data/app/assets/builds/tinymce/skins/content/alchemy/content.min.css +1 -1
  12. data/app/assets/builds/tinymce/skins/ui/alchemy/content.min.css +1 -0
  13. data/app/assets/builds/tinymce/skins/ui/alchemy/skin.min.css +1 -1
  14. data/app/assets/config/alchemy_manifest.js +0 -2
  15. data/app/assets/images/alchemy/icons-sprite.svg +1 -0
  16. data/app/components/alchemy/admin/resource/applied_filter.rb +29 -0
  17. data/app/components/alchemy/admin/resource/checkbox_filter.rb +36 -0
  18. data/app/components/alchemy/admin/resource/datepicker_filter.rb +42 -0
  19. data/app/components/alchemy/admin/resource/select_filter.rb +43 -0
  20. data/app/components/alchemy/admin/toolbar_button.rb +5 -2
  21. data/app/components/alchemy/ingredients/number_view.rb +18 -0
  22. data/app/controllers/alchemy/admin/attachments_controller.rb +8 -15
  23. data/app/controllers/alchemy/admin/clipboard_controller.rb +2 -6
  24. data/app/controllers/alchemy/admin/elements_controller.rb +1 -1
  25. data/app/controllers/alchemy/admin/languages_controller.rb +1 -1
  26. data/app/controllers/alchemy/admin/pages_controller.rb +15 -15
  27. data/app/controllers/alchemy/admin/pictures_controller.rb +9 -5
  28. data/app/controllers/alchemy/admin/resources_controller.rb +16 -106
  29. data/app/controllers/alchemy/attachments_controller.rb +43 -14
  30. data/app/controllers/alchemy/messages_controller.rb +1 -1
  31. data/app/controllers/alchemy/pages_controller.rb +7 -2
  32. data/app/controllers/concerns/alchemy/admin/resource_filter.rb +92 -0
  33. data/app/decorators/alchemy/element_editor.rb +5 -48
  34. data/app/decorators/alchemy/ingredient_editor.rb +3 -53
  35. data/app/helpers/alchemy/admin/base_helper.rb +14 -84
  36. data/app/helpers/alchemy/admin/elements_helper.rb +4 -4
  37. data/app/helpers/alchemy/admin/pages_helper.rb +1 -1
  38. data/app/helpers/alchemy/base_helper.rb +0 -30
  39. data/app/helpers/alchemy/elements_block_helper.rb +0 -14
  40. data/app/helpers/alchemy/pages_helper.rb +1 -1
  41. data/{lib → app/helpers}/alchemy/resources_helper.rb +5 -45
  42. data/app/javascript/alchemy_admin/components/action.js +2 -0
  43. data/app/javascript/alchemy_admin/components/alchemy_html_element.js +3 -3
  44. data/app/javascript/alchemy_admin/components/datepicker.js +10 -2
  45. data/app/javascript/alchemy_admin/components/element_editor/delete_element_button.js +7 -7
  46. data/app/javascript/alchemy_admin/components/element_editor.js +1 -1
  47. data/app/javascript/alchemy_admin/components/index.js +1 -0
  48. data/app/javascript/alchemy_admin/components/remote_select.js +4 -1
  49. data/app/javascript/alchemy_admin/components/tags_autocomplete.js +5 -1
  50. data/app/javascript/alchemy_admin/components/tinymce.js +4 -2
  51. data/app/javascript/alchemy_admin/components/update_check.js +42 -0
  52. data/app/javascript/alchemy_admin/components/uploader/file_upload.js +15 -8
  53. data/app/javascript/alchemy_admin/components/uploader/progress.js +12 -6
  54. data/app/javascript/alchemy_admin/components/uploader.js +4 -2
  55. data/app/javascript/alchemy_admin/confirm_dialog.js +27 -57
  56. data/app/javascript/alchemy_admin/dirty.js +3 -2
  57. data/app/javascript/alchemy_admin/i18n.js +15 -16
  58. data/app/javascript/alchemy_admin/initializer.js +1 -49
  59. data/app/javascript/alchemy_admin/utils/ajax.js +51 -44
  60. data/app/javascript/alchemy_admin.js +3 -8
  61. data/app/models/alchemy/admin/filters/base.rb +38 -0
  62. data/app/models/alchemy/admin/filters/checkbox.rb +24 -0
  63. data/app/models/alchemy/admin/filters/datepicker.rb +53 -0
  64. data/app/models/alchemy/admin/filters/select.rb +70 -0
  65. data/app/models/alchemy/admin/resource_name.rb +27 -0
  66. data/app/models/alchemy/attachment.rb +51 -34
  67. data/app/models/alchemy/base_record.rb +2 -0
  68. data/app/models/alchemy/element/definitions.rb +1 -1
  69. data/app/models/alchemy/element/element_ingredients.rb +6 -6
  70. data/app/models/alchemy/element/presenters.rb +3 -12
  71. data/app/models/alchemy/element.rb +9 -27
  72. data/app/models/alchemy/element_definition.rb +160 -0
  73. data/app/models/alchemy/ingredient.rb +10 -43
  74. data/app/models/alchemy/ingredient_definition.rb +134 -0
  75. data/app/models/alchemy/ingredient_validator.rb +7 -3
  76. data/app/models/alchemy/ingredients/number.rb +19 -0
  77. data/app/models/alchemy/language.rb +0 -14
  78. data/app/models/alchemy/message.rb +3 -7
  79. data/app/models/alchemy/node.rb +1 -1
  80. data/app/models/alchemy/page/{page_layouts.rb → definitions.rb} +12 -19
  81. data/app/models/alchemy/page/fixed_attributes.rb +1 -1
  82. data/app/models/alchemy/page/page_elements.rb +13 -14
  83. data/app/models/alchemy/page/page_natures.rb +7 -7
  84. data/app/models/alchemy/page/page_scopes.rb +1 -1
  85. data/app/models/alchemy/page.rb +11 -33
  86. data/app/models/alchemy/page_definition.rb +115 -0
  87. data/app/models/alchemy/picture.rb +69 -79
  88. data/app/models/alchemy/picture_variant.rb +115 -5
  89. data/{lib → app/models}/alchemy/resource.rb +4 -18
  90. data/{lib → app/models}/alchemy/searchable_resource.rb +15 -0
  91. data/app/models/alchemy/site/layout.rb +5 -5
  92. data/app/models/alchemy/site.rb +0 -15
  93. data/app/models/alchemy/storage_adapter/active_storage/attachment_url.rb +41 -0
  94. data/app/models/alchemy/storage_adapter/active_storage/picture_url.rb +55 -0
  95. data/app/models/alchemy/storage_adapter/active_storage/preprocessor.rb +40 -0
  96. data/app/models/alchemy/storage_adapter/active_storage.rb +173 -0
  97. data/app/models/alchemy/{attachment/url.rb → storage_adapter/dragonfly/attachment_url.rb} +12 -12
  98. data/app/models/alchemy/{picture/url.rb → storage_adapter/dragonfly/picture_url.rb} +28 -12
  99. data/app/models/alchemy/{picture → storage_adapter/dragonfly}/preprocessor.rb +4 -4
  100. data/app/models/alchemy/storage_adapter/dragonfly.rb +183 -0
  101. data/app/models/alchemy/storage_adapter.rb +74 -0
  102. data/app/models/concerns/alchemy/picture_thumbnails.rb +19 -6
  103. data/app/serializers/alchemy/element_serializer.rb +0 -1
  104. data/app/services/alchemy/dragonfly_to_image_processing.rb +100 -0
  105. data/app/stylesheets/alchemy/_defaults.scss +3 -0
  106. data/app/stylesheets/alchemy/_extends.scss +69 -0
  107. data/app/{assets/stylesheets → stylesheets}/alchemy/_mixins.scss +36 -49
  108. data/app/stylesheets/alchemy/_variables.scss +5 -0
  109. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/archive.scss +20 -37
  110. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/base.scss +16 -14
  111. data/app/stylesheets/alchemy/admin/buttons.scss +160 -0
  112. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/clipboard.scss +2 -2
  113. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/dashboard.scss +13 -16
  114. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/dialogs.scss +23 -16
  115. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/elements.scss +150 -105
  116. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/errors.scss +5 -5
  117. data/app/stylesheets/alchemy/admin/filters.scss +58 -0
  118. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/flatpickr.scss +53 -60
  119. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/form_fields.scss +21 -7
  120. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/forms.scss +31 -19
  121. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/frame.scss +20 -16
  122. data/app/stylesheets/alchemy/admin/hints.scss +5 -0
  123. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/icons.scss +10 -1
  124. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/image_library.scss +10 -8
  125. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/images.scss +1 -1
  126. data/app/stylesheets/alchemy/admin/labels.scss +5 -0
  127. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/lists.scss +3 -3
  128. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/navigation.scss +61 -55
  129. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/nodes.scss +21 -18
  130. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/notices.scss +18 -18
  131. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/page-select.scss +2 -2
  132. data/app/stylesheets/alchemy/admin/pagination.scss +144 -0
  133. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/preview_window.scss +8 -6
  134. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/print.scss +1 -1
  135. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/resource_info.scss +8 -5
  136. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/search.scss +9 -6
  137. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/selects.scss +49 -37
  138. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/shoelace.scss +5 -6
  139. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/sitemap.scss +38 -33
  140. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/tables.scss +6 -4
  141. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/tags.scss +6 -4
  142. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/toolbar.scss +12 -6
  143. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/typography.scss +2 -2
  144. data/app/{assets/stylesheets → stylesheets}/alchemy/admin/upload.scss +7 -5
  145. data/app/stylesheets/alchemy/admin.scss +44 -0
  146. data/app/stylesheets/alchemy/custom-properties.css +244 -0
  147. data/app/stylesheets/alchemy/welcome.scss +75 -0
  148. data/app/{assets/stylesheets → stylesheets}/tinymce/skins/content/alchemy/content.scss +8 -9
  149. data/app/stylesheets/tinymce/skins/ui/alchemy/content.scss +1 -0
  150. data/app/{assets/stylesheets → stylesheets}/tinymce/skins/ui/alchemy/skin.scss +133 -136
  151. data/app/views/alchemy/admin/attachments/_files_list.html.erb +2 -2
  152. data/app/views/alchemy/admin/attachments/_overlay_file_list.html.erb +1 -1
  153. data/app/views/alchemy/admin/{elements/_clipboard_button.html.erb → clipboard/_button.html.erb} +3 -5
  154. data/app/views/alchemy/admin/clipboard/_update_nested_element_button.turbo_stream.erb +11 -0
  155. data/app/views/alchemy/admin/clipboard/clear.turbo_stream.erb +4 -0
  156. data/app/views/alchemy/admin/clipboard/index.html.erb +15 -13
  157. data/app/views/alchemy/admin/clipboard/insert.turbo_stream.erb +18 -0
  158. data/app/views/alchemy/admin/clipboard/remove.turbo_stream.erb +9 -0
  159. data/app/views/alchemy/admin/dashboard/info.html.erb +17 -31
  160. data/app/views/alchemy/admin/elements/_element.html.erb +4 -8
  161. data/app/views/alchemy/admin/elements/_form.html.erb +1 -1
  162. data/app/views/alchemy/admin/elements/_header.html.erb +1 -0
  163. data/app/views/alchemy/admin/elements/_toolbar.html.erb +4 -6
  164. data/app/views/alchemy/admin/elements/create.turbo_stream.erb +2 -1
  165. data/app/views/alchemy/admin/elements/index.html.erb +2 -2
  166. data/app/views/alchemy/admin/ingredients/_file_fields.html.erb +3 -16
  167. data/app/views/alchemy/admin/ingredients/_picture_fields.html.erb +0 -9
  168. data/app/views/alchemy/admin/languages/_form.html.erb +1 -1
  169. data/app/views/alchemy/admin/languages/_table.html.erb +1 -1
  170. data/app/views/alchemy/admin/languages/index.html.erb +5 -2
  171. data/app/views/alchemy/admin/layoutpages/index.html.erb +1 -12
  172. data/app/views/alchemy/admin/pages/_form.html.erb +2 -2
  173. data/app/views/alchemy/admin/pages/_page.html.erb +2 -3
  174. data/app/views/alchemy/admin/pages/_toolbar.html.erb +1 -15
  175. data/app/views/alchemy/admin/pages/index.html.erb +1 -1
  176. data/app/views/alchemy/admin/partials/_remote_search_form.html.erb +9 -12
  177. data/app/views/alchemy/admin/partials/_search_form.html.erb +4 -9
  178. data/app/views/alchemy/admin/pictures/_archive.html.erb +4 -7
  179. data/app/views/alchemy/admin/pictures/_archive_overlay.html.erb +2 -1
  180. data/app/views/alchemy/admin/pictures/_filter_and_size_bar.html.erb +1 -1
  181. data/app/views/alchemy/admin/pictures/index.html.erb +2 -7
  182. data/app/views/alchemy/admin/resources/_applied_filters.html.erb +8 -0
  183. data/app/views/alchemy/admin/resources/_filter_bar.html.erb +11 -21
  184. data/app/views/alchemy/admin/resources/_pagination.html.erb +6 -0
  185. data/app/views/alchemy/admin/resources/_per_page_select.html.erb +4 -2
  186. data/app/views/alchemy/admin/resources/_resource_table.html.erb +1 -1
  187. data/app/views/alchemy/admin/resources/_table_header.html.erb +1 -15
  188. data/app/views/alchemy/admin/sites/index.html.erb +5 -1
  189. data/app/views/alchemy/admin/styleguide/index.html.erb +8 -0
  190. data/app/views/alchemy/admin/tags/index.html.erb +1 -1
  191. data/app/views/alchemy/admin/tinymce/_setup.html.erb +7 -7
  192. data/app/{javascript/alchemy_admin/locales/en.js → views/alchemy/admin/translations/_en.js} +5 -2
  193. data/app/views/alchemy/admin/uploader/_button.html.erb +1 -1
  194. data/app/views/alchemy/admin/uploader/_setup.html.erb +4 -4
  195. data/app/views/alchemy/base/redirect.js.erb +1 -1
  196. data/app/views/alchemy/ingredients/_number_editor.html.erb +24 -0
  197. data/app/views/alchemy/no_index.html.erb +31 -0
  198. data/app/views/alchemy/welcome.html.erb +12 -10
  199. data/app/views/kaminari/alchemy/_first_page.html.erb +5 -3
  200. data/app/views/kaminari/alchemy/_last_page.html.erb +5 -3
  201. data/app/views/kaminari/alchemy/_next_page.html.erb +5 -3
  202. data/app/views/kaminari/alchemy/_paginator.html.erb +18 -13
  203. data/app/views/kaminari/alchemy/_prev_page.html.erb +5 -3
  204. data/app/views/layouts/alchemy/admin.html.erb +5 -9
  205. data/bun.lockb +0 -0
  206. data/bundles/remixicon.mjs +153 -0
  207. data/config/alchemy/config.yml +3 -2
  208. data/config/initializers/dragonfly.rb +0 -1
  209. data/config/initializers/mime_types.rb +1 -0
  210. data/config/locales/alchemy.en.yml +32 -14
  211. data/config/routes.rb +0 -2
  212. data/eslint.config.js +2 -1
  213. data/lib/alchemy/admin/preview_url.rb +4 -5
  214. data/lib/alchemy/cache_digests/template_tracker.rb +6 -9
  215. data/lib/alchemy/config_missing.rb +14 -0
  216. data/lib/alchemy/configuration/base_option.rb +24 -0
  217. data/lib/alchemy/configuration/boolean_option.rb +16 -0
  218. data/lib/alchemy/configuration/class_option.rb +15 -0
  219. data/lib/alchemy/configuration/class_set_option.rb +46 -0
  220. data/lib/alchemy/configuration/integer_list_option.rb +13 -0
  221. data/lib/alchemy/configuration/integer_option.rb +12 -0
  222. data/lib/alchemy/configuration/list_option.rb +22 -0
  223. data/lib/alchemy/configuration/regexp_option.rb +11 -0
  224. data/lib/alchemy/configuration/string_list_option.rb +13 -0
  225. data/lib/alchemy/configuration/string_option.rb +11 -0
  226. data/lib/alchemy/configuration.rb +115 -0
  227. data/lib/alchemy/configuration_methods.rb +3 -1
  228. data/lib/alchemy/configurations/default_language.rb +12 -0
  229. data/lib/alchemy/configurations/default_site.rb +10 -0
  230. data/lib/alchemy/configurations/format_matchers.rb +11 -0
  231. data/lib/alchemy/configurations/mailer.rb +16 -0
  232. data/lib/alchemy/configurations/main.rb +216 -0
  233. data/lib/alchemy/configurations/preview.rb +32 -0
  234. data/lib/alchemy/configurations/sitemap.rb +10 -0
  235. data/lib/alchemy/configurations/uploader.rb +34 -0
  236. data/lib/alchemy/engine.rb +65 -17
  237. data/lib/alchemy/hints.rb +3 -7
  238. data/lib/alchemy/on_page_layout.rb +2 -2
  239. data/lib/alchemy/propshaft/tinymce_asset.rb +15 -0
  240. data/lib/alchemy/seeder.rb +2 -2
  241. data/lib/alchemy/tasks/usage.rb +4 -4
  242. data/lib/alchemy/test_support/config_stubbing.rb +1 -7
  243. data/lib/alchemy/test_support/factories/attachment_factory.rb +13 -2
  244. data/lib/alchemy/test_support/factories/language_factory.rb +1 -1
  245. data/lib/alchemy/test_support/factories/page_factory.rb +2 -3
  246. data/lib/alchemy/test_support/factories/picture_factory.rb +30 -2
  247. data/lib/alchemy/test_support/factories/site_factory.rb +2 -2
  248. data/lib/alchemy/test_support/having_crop_action_examples.rb +2 -2
  249. data/lib/alchemy/test_support/having_picture_thumbnails_examples.rb +80 -26
  250. data/lib/alchemy/test_support/shared_ingredient_examples.rb +5 -5
  251. data/lib/alchemy/upgrader/.keep +0 -0
  252. data/lib/alchemy/upgrader/eight_zero.rb +14 -0
  253. data/lib/alchemy/upgrader.rb +33 -20
  254. data/lib/alchemy/version.rb +1 -1
  255. data/lib/alchemy.rb +192 -170
  256. data/lib/alchemy_cms.rb +1 -7
  257. data/lib/generators/alchemy/ingredient/ingredient_generator.rb +0 -3
  258. data/lib/generators/alchemy/install/files/_article.html.erb +6 -4
  259. data/lib/generators/alchemy/install/files/alchemy.en.yml +22 -3
  260. data/lib/generators/alchemy/install/files/application.html.erb +5 -0
  261. data/lib/generators/alchemy/install/install_generator.rb +5 -14
  262. data/lib/generators/alchemy/install/templates/alchemy.rb.tt +196 -0
  263. data/lib/generators/alchemy/install/templates/dragonfly.rb.tt +0 -1
  264. data/lib/generators/alchemy/install/templates/elements.yml.tt +3 -1
  265. data/lib/generators/alchemy/install/templates/menus.yml.tt +1 -1
  266. data/lib/generators/alchemy/install/templates/page_layouts.yml.tt +2 -2
  267. data/lib/generators/alchemy/page_layouts/page_layouts_generator.rb +2 -2
  268. data/lib/tasks/alchemy/assets.rake +14 -0
  269. data/lib/tasks/alchemy/upgrade.rake +12 -47
  270. data/vendor/javascript/tinymce.min.js +1 -1
  271. data/vitest.config.js +21 -0
  272. metadata +181 -180
  273. data/app/assets/builds/alchemy/admin/page-select.css.map +0 -1
  274. data/app/assets/builds/alchemy/admin/print.css.map +0 -1
  275. data/app/assets/builds/alchemy/admin.css.map +0 -1
  276. data/app/assets/builds/alchemy/custom-properties.css.map +0 -1
  277. data/app/assets/builds/alchemy/welcome.css.map +0 -1
  278. data/app/assets/builds/tinymce/skins/content/alchemy/content.min.css.map +0 -1
  279. data/app/assets/builds/tinymce/skins/ui/alchemy/skin.min.css.map +0 -1
  280. data/app/assets/javascripts/alchemy/admin.js +0 -10
  281. data/app/assets/stylesheets/alchemy/_defaults.scss +0 -3
  282. data/app/assets/stylesheets/alchemy/_deprecated_variables.scss +0 -45
  283. data/app/assets/stylesheets/alchemy/_deprecation.scss +0 -17
  284. data/app/assets/stylesheets/alchemy/_extends.scss +0 -62
  285. data/app/assets/stylesheets/alchemy/_variables.scss +0 -201
  286. data/app/assets/stylesheets/alchemy/admin/buttons.scss +0 -124
  287. data/app/assets/stylesheets/alchemy/admin/hints.scss +0 -5
  288. data/app/assets/stylesheets/alchemy/admin/labels.scss +0 -3
  289. data/app/assets/stylesheets/alchemy/admin/pagination.scss +0 -92
  290. data/app/assets/stylesheets/alchemy/admin.scss +0 -42
  291. data/app/assets/stylesheets/alchemy/custom-properties.css +0 -98
  292. data/app/assets/stylesheets/alchemy/welcome.scss +0 -57
  293. data/app/assets/stylesheets/tinymce/skins/ui/alchemy/content.css +0 -711
  294. data/app/assets/stylesheets/tinymce/skins/ui/alchemy/content.inline.css +0 -705
  295. data/app/assets/stylesheets/tinymce/skins/ui/alchemy/content.inline.min.css +0 -7
  296. data/app/assets/stylesheets/tinymce/skins/ui/alchemy/content.min.css +0 -7
  297. data/app/assets/stylesheets/tinymce/skins/ui/alchemy/content.mobile.css +0 -29
  298. data/app/assets/stylesheets/tinymce/skins/ui/alchemy/content.mobile.min.css +0 -7
  299. data/app/assets/stylesheets/tinymce/skins/ui/alchemy/skin.mobile.css +0 -677
  300. data/app/assets/stylesheets/tinymce/skins/ui/alchemy/skin.mobile.min.css +0 -7
  301. data/app/controllers/alchemy/elements_controller.rb +0 -32
  302. data/app/models/alchemy/element/dom_id.rb +0 -31
  303. data/app/models/alchemy/picture/calculations.rb +0 -49
  304. data/app/models/alchemy/picture/transformations.rb +0 -115
  305. data/app/views/alchemy/admin/attachments/destroy.js.erb +0 -1
  306. data/app/views/alchemy/admin/clipboard/clear.js.erb +0 -3
  307. data/app/views/alchemy/admin/clipboard/insert.js.erb +0 -29
  308. data/app/views/alchemy/admin/clipboard/remove.js.erb +0 -10
  309. data/app/views/alchemy/admin/resources/_filter.html.erb +0 -12
  310. data/app/views/alchemy/admin/resources/_resource.html.erb +0 -34
  311. data/app/views/alchemy/admin/resources/_table.html.erb +0 -29
  312. data/app/views/alchemy/elements/show.html.erb +0 -1
  313. data/app/views/alchemy/elements/show.js.erb +0 -1
  314. data/app/views/alchemy/ingredients/_audio_view.html.erb +0 -1
  315. data/app/views/alchemy/ingredients/_boolean_view.html.erb +0 -1
  316. data/app/views/alchemy/ingredients/_datetime_view.html.erb +0 -3
  317. data/app/views/alchemy/ingredients/_file_view.html.erb +0 -4
  318. data/app/views/alchemy/ingredients/_headline_view.html.erb +0 -4
  319. data/app/views/alchemy/ingredients/_html_view.html.erb +0 -1
  320. data/app/views/alchemy/ingredients/_link_view.html.erb +0 -4
  321. data/app/views/alchemy/ingredients/_node_view.html.erb +0 -1
  322. data/app/views/alchemy/ingredients/_page_view.html.erb +0 -1
  323. data/app/views/alchemy/ingredients/_picture_view.html.erb +0 -4
  324. data/app/views/alchemy/ingredients/_richtext_view.html.erb +0 -3
  325. data/app/views/alchemy/ingredients/_select_view.html.erb +0 -1
  326. data/app/views/alchemy/ingredients/_text_view.html.erb +0 -4
  327. data/app/views/alchemy/ingredients/_video_view.html.erb +0 -3
  328. data/babel.config.js +0 -12
  329. data/config/initializers/assets.rb +0 -4
  330. data/lib/alchemy/config.rb +0 -114
  331. data/lib/alchemy/element_definition.rb +0 -73
  332. data/lib/alchemy/page_layout.rb +0 -73
  333. data/lib/alchemy/resource_filter.rb +0 -40
  334. data/lib/alchemy/upgrader/seven_point_four.rb +0 -26
  335. data/lib/alchemy/upgrader/seven_point_three.rb +0 -52
  336. data/lib/generators/alchemy/ingredient/templates/view.html.erb +0 -1
  337. data/lib/generators/alchemy/install/files/alchemy_admin.js +0 -1
  338. data/lib/generators/alchemy/install/files/all.js +0 -11
  339. data/lib/generators/alchemy/install/files/article.css +0 -25
  340. data/vendor/assets/images/remixicon.symbol.svg +0 -11
  341. /data/app/{assets/stylesheets → stylesheets}/alchemy/_fonts.scss +0 -0
  342. /data/app/{assets/stylesheets → stylesheets}/alchemy/admin/attachment-select.scss +0 -0
  343. /data/app/{assets/stylesheets → stylesheets}/alchemy/admin/attachments.scss +0 -0
  344. /data/app/{assets/stylesheets → stylesheets}/alchemy/admin/flash.scss +0 -0
  345. /data/app/{assets/stylesheets → stylesheets}/alchemy/admin/list_filter.scss +0 -0
  346. /data/app/{assets/stylesheets → stylesheets}/alchemy/admin/node-select.scss +0 -0
  347. /data/app/{assets/stylesheets → stylesheets}/alchemy/admin/spinner.scss +0 -0
  348. /data/app/{assets/stylesheets → stylesheets}/tinymce/skins/skintool.json +0 -0
  349. /data/app/{assets/stylesheets → stylesheets}/tinymce/skins/ui/alchemy/fonts/tinymce-mobile.woff +0 -0
@@ -0,0 +1,42 @@
1
+ import Spinner from "alchemy_admin/spinner"
2
+
3
+ class UpdateCheck extends HTMLElement {
4
+ async connectedCallback() {
5
+ const spinner = new Spinner("small")
6
+ spinner.spin(this)
7
+
8
+ try {
9
+ const response = await fetch(this.url)
10
+ const responseText = await response.text()
11
+
12
+ if (response.ok) {
13
+ this.showStatus(responseText)
14
+ } else {
15
+ this.showError(response)
16
+ }
17
+ } catch (error) {
18
+ this.showError(error)
19
+ } finally {
20
+ spinner.stop()
21
+ }
22
+ }
23
+
24
+ get url() {
25
+ return this.getAttribute("url")
26
+ }
27
+
28
+ showStatus(responseText) {
29
+ if (responseText == "true") {
30
+ this.querySelector(".update_available").classList.remove("hidden")
31
+ } else {
32
+ this.querySelector(".up_to_date").classList.remove("hidden")
33
+ }
34
+ }
35
+
36
+ showError(error) {
37
+ this.querySelector(".error").classList.remove("hidden")
38
+ console.error("[alchemy] Error fetching update status", error)
39
+ }
40
+ }
41
+
42
+ customElements.define("alchemy-update-check", UpdateCheck)
@@ -4,21 +4,28 @@ import { translate } from "alchemy_admin/i18n"
4
4
  import { growl } from "alchemy_admin/growler"
5
5
 
6
6
  export class FileUpload extends AlchemyHTMLElement {
7
+ constructor() {
8
+ super()
9
+
10
+ this.file = null
11
+ this.request = null
12
+
13
+ this.progressEventLoaded = 0
14
+ this.progressEventTotal = 0
15
+ this.className = "in-progress"
16
+ this.valid = true
17
+ this.value = 0
18
+ }
19
+
7
20
  /**
21
+ * Initialize the component with file and request
8
22
  * @param {File} file
9
23
  * @param {XMLHttpRequest} request
10
24
  */
11
- constructor(file, request) {
12
- super({})
13
-
25
+ initialize(file, request) {
14
26
  this.file = file
15
27
  this.request = request
16
-
17
- this.progressEventLoaded = 0
18
28
  this.progressEventTotal = file ? file.size : 0
19
- this.className = "in-progress"
20
- this.valid = true
21
- this.value = 0
22
29
 
23
30
  this._validateFile()
24
31
  this._addRequestEventListener()
@@ -6,19 +6,25 @@ import { translate } from "alchemy_admin/i18n"
6
6
  export class Progress extends AlchemyHTMLElement {
7
7
  #visible = false
8
8
 
9
- /**
10
- * @param {FileUpload[]} fileUploads
11
- */
12
- constructor(fileUploads = []) {
9
+ constructor() {
13
10
  super()
14
11
  this.buttonLabel = translate("Cancel all uploads")
15
- this.fileUploads = fileUploads
16
- this.fileCount = fileUploads.length
12
+ this.fileUploads = []
13
+ this.fileCount = 0
17
14
  this.className = "in-progress"
18
15
  this.visible = true
19
16
  this.handleFileChange = () => this._updateView()
20
17
  }
21
18
 
19
+ /**
20
+ * Initialize the component with file uploads
21
+ * @param {FileUpload[]} fileUploads
22
+ */
23
+ initialize(fileUploads = []) {
24
+ this.fileUploads = fileUploads
25
+ this.fileCount = fileUploads.length
26
+ }
27
+
22
28
  /**
23
29
  * append file progress - components for each file
24
30
  */
@@ -68,7 +68,8 @@ export class Uploader extends AlchemyHTMLElement {
68
68
 
69
69
  const fileUploads = files.map((file) => {
70
70
  const request = new XMLHttpRequest()
71
- const fileUpload = new FileUpload(file, request)
71
+ const fileUpload = new FileUpload()
72
+ fileUpload.initialize(file, request)
72
73
 
73
74
  if (Alchemy.uploader_defaults.upload_limit - 1 < fileUploadCount) {
74
75
  fileUpload.valid = false
@@ -110,7 +111,8 @@ export class Uploader extends AlchemyHTMLElement {
110
111
  this.uploadProgress.cancel()
111
112
  document.body.removeChild(this.uploadProgress)
112
113
  }
113
- this.uploadProgress = new Progress(fileUploads)
114
+ this.uploadProgress = new Progress()
115
+ this.uploadProgress.initialize(fileUploads)
114
116
  this.uploadProgress.onComplete = (status) => {
115
117
  this.dispatchCustomEvent(`upload.${status}`)
116
118
  }
@@ -1,20 +1,19 @@
1
- import { growl } from "alchemy_admin/growler"
2
- import pleaseWaitOverlay from "alchemy_admin/please_wait_overlay"
3
1
  import { createHtmlElement } from "alchemy_admin/utils/dom_helpers"
4
2
  import { translate } from "alchemy_admin/i18n"
5
3
 
6
- const DEFAULTS = {
4
+ const getDefaults = () => ({
5
+ // The default size of the dialog
7
6
  size: "300x100",
8
7
  title: translate("Please confirm"),
9
8
  ok_label: translate("Yes"),
10
9
  cancel_label: translate("No"),
11
10
  on_ok() {}
12
- }
11
+ })
13
12
 
14
13
  class ConfirmDialog {
15
14
  constructor(message, options = {}) {
16
15
  this.message = message
17
- this.options = { ...DEFAULTS, ...options }
16
+ this.options = { ...getDefaults(), ...options }
18
17
  this.#build()
19
18
  this.#bindEvents()
20
19
  }
@@ -44,6 +43,7 @@ class ConfirmDialog {
44
43
  #bindEvents() {
45
44
  this.cancelButton.addEventListener("click", (evt) => {
46
45
  evt.preventDefault()
46
+ this.options.on_cancel()
47
47
  this.dialog.hide()
48
48
  })
49
49
  this.okButton.addEventListener("click", (evt) => {
@@ -54,6 +54,7 @@ class ConfirmDialog {
54
54
  // Prevent the dialog from closing when the user clicks on the overlay
55
55
  this.dialog.addEventListener("sl-request-close", (event) => {
56
56
  if (event.detail.source === "overlay") {
57
+ this.options.on_cancel()
57
58
  event.preventDefault()
58
59
  }
59
60
  })
@@ -72,60 +73,29 @@ class ConfirmDialog {
72
73
  }
73
74
  }
74
75
 
75
- // Opens a confirm dialog
76
- //
77
- // Arguments:
78
- //
79
- // message - The message that will be displayed to the user (String)
80
- //
81
- // Options:
82
- //
83
- // title: '' - The title of the overlay window (String)
84
- // cancel_label: '' - The label of the cancel button (String)
85
- // ok_label: '' - The label of the ok button (String)
86
- // on_ok: null - The function to invoke on confirmation (Function)
87
- //
76
+ /* Opens a confirm dialog
77
+ *
78
+ * @param {string} message - The message that will be displayed to the user
79
+ * @param {Object} [options={}] - Configuration options for the dialog
80
+ * @param {string} [options.title="Please confirm"] - The title of the overlay window
81
+ * @param {string} [options.cancel_label="No"] - The label of the cancel button
82
+ * @param {string} [options.ok_label="Yes"] - The label of the ok button
83
+ *
84
+ * @returns {Promise<void>} A promise that resolves to true when the OK button is clicked and
85
+ * resolves to false when the cancel button is clicked. Works as confirm dialog replacement
86
+ * for Turbo.confirm.
87
+ */
88
88
  export function openConfirmDialog(message, options = {}) {
89
- const dialog = new ConfirmDialog(message, options)
90
- dialog.open()
91
- return dialog
92
- }
93
-
94
- // Opens a confirm to delete dialog
95
- //
96
- // Arguments:
97
- //
98
- // url - The url to the server delete action. Uses DELETE as HTTP method. (String)
99
- // opts - An options object (Object)
100
- //
101
- // Options:
102
- //
103
- // title: '' - The title of the confirmation window (String)
104
- // message: '' - The message that will be displayed to the user (String)
105
- // ok_label: '' - The label for the ok button (String)
106
- // cancel_label: '' - The label for the cancel button (String)
107
- //
108
- export function confirmToDeleteDialog(url, opts = {}) {
109
- return new Promise((resolve, reject) => {
110
- const options = {
89
+ return new Promise((resolve) => {
90
+ const dialog = new ConfirmDialog(message, {
91
+ ...options,
111
92
  on_ok() {
112
- pleaseWaitOverlay()
113
- $.ajax({
114
- url,
115
- type: "DELETE",
116
- error(xhr, _status, error) {
117
- const type = xhr.status === 403 ? "warning" : "error"
118
- growl(xhr.responseText || error, type)
119
- reject(error)
120
- },
121
- complete(response) {
122
- pleaseWaitOverlay(false)
123
- resolve(response)
124
- }
125
- })
93
+ resolve(true)
94
+ },
95
+ on_cancel() {
96
+ resolve(false)
126
97
  }
127
- }
128
-
129
- openConfirmDialog(opts.message, { ...options, ...opts })
98
+ })
99
+ dialog.open()
130
100
  })
131
101
  }
@@ -27,8 +27,9 @@ function checkPageDirtyness(element) {
27
27
  openConfirmDialog(translate("page_dirty_notice"), {
28
28
  title: translate("warning"),
29
29
  ok_label: translate("ok"),
30
- cancel_label: translate("cancel"),
31
- on_ok: function () {
30
+ cancel_label: translate("cancel")
31
+ }).then((proceed) => {
32
+ if (proceed) {
32
33
  window.onbeforeunload = void 0
33
34
  callback()
34
35
  }
@@ -1,20 +1,5 @@
1
- import { en } from "alchemy_admin/locales/en"
2
-
3
- Alchemy.translations = Object.assign(Alchemy.translations || {}, { en })
4
-
5
1
  const KEY_SEPARATOR = /\./
6
2
 
7
- function getTranslations() {
8
- const locale = currentLocale()
9
- const translations = Alchemy.translations && Alchemy.translations[locale]
10
-
11
- if (translations) {
12
- return translations
13
- }
14
- console.warn(`Translations for locale ${locale} not found!`)
15
- return {}
16
- }
17
-
18
3
  function nestedTranslation(translations, key) {
19
4
  const keys = key.split(KEY_SEPARATOR)
20
5
  const group = translations[keys[0]]
@@ -25,7 +10,13 @@ function nestedTranslation(translations, key) {
25
10
  }
26
11
 
27
12
  function getTranslation(key) {
28
- const translations = getTranslations()
13
+ const locale = currentLocale()
14
+ const translations = Alchemy.translations
15
+
16
+ if (!translations) {
17
+ console.warn(`Translations for locale ${locale} not found!`)
18
+ return key
19
+ }
29
20
 
30
21
  if (KEY_SEPARATOR.test(key)) {
31
22
  return nestedTranslation(translations, key)
@@ -48,3 +39,11 @@ export function translate(key, replacement = undefined) {
48
39
  }
49
40
  return translation
50
41
  }
42
+
43
+ export async function setupSelectLocale() {
44
+ const locale = currentLocale()
45
+ if (locale === "en") return
46
+
47
+ await import(`select2/${locale}.js`)
48
+ $.extend($.fn.select2.defaults, $.fn.select2.locales[locale])
49
+ }
@@ -1,8 +1,3 @@
1
- import {
2
- confirmToDeleteDialog,
3
- openConfirmDialog
4
- } from "alchemy_admin/confirm_dialog"
5
-
6
1
  import Hotkeys from "alchemy_admin/hotkeys"
7
2
  import pleaseWaitOverlay from "alchemy_admin/please_wait_overlay"
8
3
 
@@ -26,44 +21,6 @@ function selectHandler(selectId, parameterName, forcedReload = false) {
26
21
  })
27
22
  }
28
23
 
29
- // Watches elements for Alchemy Dialogs
30
- //
31
- // Links having a data-alchemy-confirm-delete
32
- // and input/buttons having a data-alchemy-confirm attribute get watched.
33
- //
34
- // You can pass a scope so that only elements inside this scope are queried.
35
- //
36
- // The href attribute of the link is the url for the overlay window.
37
- //
38
- // See Dialog for further options you can add to the data attribute.
39
- //
40
- function watchForConfirmDialogs(scope) {
41
- if (scope == null) {
42
- scope = "#alchemy"
43
- }
44
- $(scope).on("click", "[data-alchemy-confirm-delete]", function (event) {
45
- const $this = $(this)
46
- const options = $this.data("alchemy-confirm-delete")
47
- confirmToDeleteDialog($this.attr("href"), options)
48
- event.preventDefault()
49
- })
50
- $(scope).on("click", "[data-alchemy-confirm]", function (event) {
51
- const options = $(this).data("alchemy-confirm")
52
- openConfirmDialog(
53
- options.message,
54
- $.extend(options, {
55
- ok_label: options.ok_label,
56
- cancel_label: options.cancel_label,
57
- on_ok: () => {
58
- pleaseWaitOverlay()
59
- this.form.submit()
60
- }
61
- })
62
- )
63
- event.preventDefault()
64
- })
65
- }
66
-
67
24
  export default function Initializer() {
68
25
  // We obviously have javascript enabled.
69
26
  $("html").removeClass("no-js")
@@ -71,13 +28,8 @@ export default function Initializer() {
71
28
  // Initialize hotkeys.
72
29
  Hotkeys()
73
30
 
74
- // Watch for click on confirm dialog links.
75
- watchForConfirmDialogs()
76
-
77
31
  // Add observer for please wait overlay.
78
- $(".please_wait")
79
- .not("*[data-alchemy-confirm]")
80
- .on("click", Alchemy.pleaseWaitOverlay)
32
+ $(".please_wait").on("click", pleaseWaitOverlay)
81
33
 
82
34
  // Hack for enabling tab focus for <a>'s styled as button.
83
35
  $("a.button").attr({ tabindex: 0 })
@@ -1,27 +1,37 @@
1
- function buildPromise(xhr) {
2
- return new Promise((resolve, reject) => {
3
- xhr.onload = () => {
4
- if (xhr.status >= 200 && xhr.status < 400) {
5
- try {
6
- resolve({
7
- data: JSON.parse(xhr.responseText),
8
- status: xhr.status
9
- })
10
- } catch (error) {
11
- reject(error)
12
- }
13
- } else {
14
- try {
15
- reject(JSON.parse(xhr.responseText))
16
- } catch (error) {
17
- reject(error)
18
- }
19
- }
20
- }
21
- xhr.onerror = () => {
22
- reject(new Error("An error occurred during the transaction"))
23
- }
24
- })
1
+ const JSON_CONTENT_TYPE = "application/json"
2
+
3
+ function isGetRequest(method) {
4
+ return method.toLowerCase() === "get"
5
+ }
6
+
7
+ function prepareURL(path, data, method) {
8
+ const url = new URL(window.location.origin + path)
9
+
10
+ if (data && isGetRequest(method)) {
11
+ url.search = new URLSearchParams(data).toString()
12
+ }
13
+
14
+ return url.toString()
15
+ }
16
+
17
+ function prepareHeaders(accept) {
18
+ return {
19
+ "Content-Type": "application/json; charset=utf-8",
20
+ Accept: accept,
21
+ "X-Requested-With": "XMLHttpRequest",
22
+ "X-CSRF-Token": getToken()
23
+ }
24
+ }
25
+
26
+ function prepareOptions(method, data, accept) {
27
+ const headers = prepareHeaders(accept)
28
+ const options = { method, headers }
29
+
30
+ if (data && !isGetRequest(method)) {
31
+ options.body = JSON.stringify(data)
32
+ }
33
+
34
+ return options
25
35
  }
26
36
 
27
37
  export function getToken() {
@@ -37,30 +47,27 @@ export function patch(url, data) {
37
47
  return ajax("PATCH", url, data)
38
48
  }
39
49
 
40
- export function post(url, data, accept = "application/json") {
50
+ export function post(url, data, accept = JSON_CONTENT_TYPE) {
41
51
  return ajax("POST", url, data, accept)
42
52
  }
43
53
 
44
- export default function ajax(method, path, data, accept = "application/json") {
45
- const xhr = new XMLHttpRequest()
46
- const promise = buildPromise(xhr)
47
- const url = new URL(window.location.origin + path)
54
+ export default async function ajax(
55
+ method,
56
+ path,
57
+ data,
58
+ accept = JSON_CONTENT_TYPE
59
+ ) {
60
+ const response = await fetch(
61
+ prepareURL(path, data, method),
62
+ prepareOptions(method, data, accept)
63
+ )
64
+ const contentType = response.headers.get("content-type")
65
+ const isJson = contentType?.includes(JSON_CONTENT_TYPE)
66
+ const responseData = isJson ? await response.json() : null
48
67
 
49
- if (data && method.toLowerCase() === "get") {
50
- url.search = new URLSearchParams(data).toString()
51
- }
52
-
53
- xhr.open(method, url.toString())
54
- xhr.setRequestHeader("Content-type", "application/json; charset=utf-8")
55
- xhr.setRequestHeader("Accept", accept)
56
- xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest")
57
- xhr.setRequestHeader("X-CSRF-Token", getToken())
58
-
59
- if (data && method.toLowerCase() !== "get") {
60
- xhr.send(JSON.stringify(data))
68
+ if (response.ok) {
69
+ return { data: responseData, status: response.status }
61
70
  } else {
62
- xhr.send()
71
+ throw responseData || new Error("An error occurred during the transaction")
63
72
  }
64
-
65
- return promise
66
73
  }
@@ -20,10 +20,7 @@ import Sitemap from "alchemy_admin/sitemap"
20
20
  import Spinner from "alchemy_admin/spinner"
21
21
  import PagePublicationFields from "alchemy_admin/page_publication_fields"
22
22
  import { reloadPreview } from "alchemy_admin/components/preview_window"
23
- import {
24
- openConfirmDialog,
25
- confirmToDeleteDialog
26
- } from "alchemy_admin/confirm_dialog"
23
+ import { openConfirmDialog } from "alchemy_admin/confirm_dialog"
27
24
 
28
25
  // Web Components
29
26
  import "alchemy_admin/components"
@@ -53,11 +50,9 @@ Object.assign(Alchemy, {
53
50
  Sitemap,
54
51
  Spinner,
55
52
  PagePublicationFields,
56
- reloadPreview,
57
- openConfirmDialog,
58
- confirmToDeleteDialog
53
+ reloadPreview
59
54
  })
60
55
 
61
56
  Rails.start()
62
-
57
+ Turbo.config.forms.confirm = openConfirmDialog
63
58
  $(document).on("turbo:load", Initializer)
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ module Admin
5
+ module Filters
6
+ class Base
7
+ attr_reader :name, :resource_name
8
+
9
+ def initialize(name:, resource_name:)
10
+ @name = name
11
+ @resource_name = resource_name
12
+ end
13
+
14
+ def applied_filter_component(search_filter_params:, resource_url_proxy:, query:)
15
+ Alchemy::Admin::Resource::AppliedFilter.new(
16
+ link: dismiss_filter_url(search_filter_params, resource_url_proxy),
17
+ applied_filter_label: translated_name,
18
+ applied_filter_value: translated_value(search_filter_params[:q][name], query)
19
+ )
20
+ end
21
+
22
+ private
23
+
24
+ def translated_name
25
+ Alchemy.t(:name, scope: [:filters, resource_name, name], default: name.to_s.humanize)
26
+ end
27
+
28
+ def dismiss_filter_url(search_filter_params, resource_url_proxy)
29
+ tmp_params = search_filter_params.dup
30
+ tmp_params[:q] = tmp_params[:q].except(name)
31
+ resource_url_proxy.url_for(
32
+ {action: "index"}.merge(tmp_params.except(:page))
33
+ )
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ module Admin
5
+ module Filters
6
+ class Checkbox < Base
7
+ # Renders a checkbox filter input.
8
+ # @param [Hash] params The controller params.
9
+ # @param [Ransack::Search] _query The current search query.
10
+ # @return [Alchemy::Admin::FilterInputs::Checkbox] The checkbox filter input component.
11
+ def input_component(params, _query)
12
+ Alchemy::Admin::Resource::CheckboxFilter.new(name:, label: translated_name, params:)
13
+ end
14
+
15
+ def applied_filter_component(search_filter_params:, resource_url_proxy:, query:)
16
+ Alchemy::Admin::Resource::AppliedFilter.new(
17
+ link: dismiss_filter_url(search_filter_params, resource_url_proxy),
18
+ applied_filter_label: translated_name
19
+ )
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ module Admin
5
+ module Filters
6
+ class Datepicker < Base
7
+ attr_reader :input_type
8
+ # Creates a resource filter that displays as a datepicker.
9
+ # @param name [String] The name of the filter.
10
+ # @param resource_name [String] The name of the resource.
11
+ # @param input_type [Symbol] The input type of the datepicker. Can be :date, :datetime, or :time.
12
+ # @example
13
+ # Alchemy::Admin::Filters::Datepicker.new(
14
+ # name: :created_at_lt,
15
+ # resource_name: :events,
16
+ # mode: :single,
17
+ # default: "2023-01-01"
18
+ def initialize(name:, resource_name:, input_type: :datetime)
19
+ super(name:, resource_name:)
20
+ @input_type = input_type
21
+ end
22
+
23
+ # Returns a datepicker filter component.
24
+ # @param params [Hash] The search filter params.
25
+ # @param query [Ransack::Search] The current search query.
26
+ # @return [Alchemy::Admin::Resource::DatepickerFilter] The datepicker filter component.
27
+ def input_component(params, _query)
28
+ Alchemy::Admin::Resource::DatepickerFilter.new(
29
+ name:,
30
+ label: translated_name,
31
+ input_type: @input_type,
32
+ params:
33
+ )
34
+ end
35
+
36
+ private
37
+
38
+ def translated_value(value, query)
39
+ date = Time.zone.parse(value) if value.is_a?(String)
40
+ format = case input_type
41
+ when :date
42
+ ::I18n.t(:default, scope: [:date, :formats, :alchemy])
43
+ when :datetime
44
+ ::I18n.t(:default, scope: [:time, :formats, :alchemy])
45
+ when :time
46
+ ::I18n.t(:time, scope: [:time, :formats, :alchemy])
47
+ end
48
+ ::I18n.l(date, format: format)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end