alchemy_cms 7.2.6 → 7.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (184) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +97 -9
  3. data/Gemfile +4 -3
  4. data/Rakefile +1 -0
  5. data/alchemy_cms.gemspec +7 -7
  6. data/app/assets/builds/alchemy/admin/print.css +1 -0
  7. data/app/assets/builds/alchemy/admin/print.css.map +1 -0
  8. data/app/assets/builds/alchemy/admin.css +1 -0
  9. data/app/assets/builds/alchemy/admin.css.map +1 -0
  10. data/app/assets/builds/alchemy/welcome.css +1 -0
  11. data/app/assets/builds/alchemy/welcome.css.map +1 -0
  12. data/app/assets/builds/tinymce/skins/content/alchemy/content.css +1 -0
  13. data/app/assets/builds/tinymce/skins/content/alchemy/content.css.map +1 -0
  14. data/app/assets/builds/tinymce/skins/content/alchemy/content.min.css +1 -0
  15. data/app/assets/builds/tinymce/skins/content/alchemy/content.min.css.map +1 -0
  16. data/app/assets/builds/tinymce/skins/ui/alchemy/skin.css +1 -0
  17. data/app/assets/builds/tinymce/skins/ui/alchemy/skin.css.map +1 -0
  18. data/app/assets/builds/tinymce/skins/ui/alchemy/skin.min.css +1 -0
  19. data/app/assets/builds/tinymce/skins/ui/alchemy/skin.min.css.map +1 -0
  20. data/app/assets/config/alchemy_manifest.js +1 -5
  21. data/app/assets/javascripts/alchemy/alchemy.dialog.js.coffee +4 -0
  22. data/app/assets/stylesheets/alchemy/{_custom-properties.scss → _custom-properties.css} +28 -25
  23. data/app/assets/stylesheets/alchemy/_deprecated_variables.scss +41 -0
  24. data/app/assets/stylesheets/alchemy/_deprecation.scss +17 -0
  25. data/app/assets/stylesheets/alchemy/_extends.scss +1 -1
  26. data/app/assets/stylesheets/alchemy/_mixins.scss +20 -23
  27. data/app/assets/stylesheets/alchemy/_variables.scss +98 -94
  28. data/app/assets/stylesheets/alchemy/{archive.scss → admin/archive.scss} +23 -23
  29. data/app/assets/stylesheets/alchemy/{attachment-select.scss → admin/attachment-select.scss} +2 -2
  30. data/app/assets/stylesheets/alchemy/{attachments.scss → admin/attachments.scss} +4 -4
  31. data/app/assets/stylesheets/alchemy/{base.scss → admin/base.scss} +9 -9
  32. data/app/assets/stylesheets/alchemy/{buttons.scss → admin/buttons.scss} +3 -3
  33. data/app/assets/stylesheets/alchemy/{clipboard.scss → admin/clipboard.scss} +9 -6
  34. data/app/assets/stylesheets/alchemy/{dashboard.scss → admin/dashboard.scss} +8 -8
  35. data/app/assets/stylesheets/alchemy/{dialogs.scss → admin/dialogs.scss} +20 -20
  36. data/app/assets/stylesheets/alchemy/{elements.scss → admin/elements.scss} +128 -88
  37. data/app/assets/stylesheets/alchemy/{errors.scss → admin/errors.scss} +22 -6
  38. data/app/assets/stylesheets/alchemy/{flash.scss → admin/flash.scss} +3 -3
  39. data/app/assets/stylesheets/alchemy/{flatpickr.scss → admin/flatpickr.scss} +55 -35
  40. data/app/assets/stylesheets/alchemy/{form_fields.scss → admin/form_fields.scss} +8 -6
  41. data/app/assets/stylesheets/alchemy/{forms.scss → admin/forms.scss} +20 -16
  42. data/app/assets/stylesheets/alchemy/{frame.scss → admin/frame.scss} +9 -9
  43. data/app/assets/stylesheets/alchemy/{image_library.scss → admin/image_library.scss} +34 -33
  44. data/app/assets/stylesheets/alchemy/admin/labels.scss +3 -0
  45. data/app/assets/stylesheets/alchemy/{list_filter.scss → admin/list_filter.scss} +4 -4
  46. data/app/assets/stylesheets/alchemy/{lists.scss → admin/lists.scss} +9 -7
  47. data/app/assets/stylesheets/alchemy/{navigation.scss → admin/navigation.scss} +17 -17
  48. data/app/assets/stylesheets/alchemy/{node-select.scss → admin/node-select.scss} +5 -5
  49. data/app/assets/stylesheets/alchemy/{nodes.scss → admin/nodes.scss} +11 -11
  50. data/app/assets/stylesheets/alchemy/{notices.scss → admin/notices.scss} +11 -7
  51. data/app/assets/stylesheets/alchemy/{page-select.scss → admin/page-select.scss} +10 -10
  52. data/app/assets/stylesheets/alchemy/{pagination.scss → admin/pagination.scss} +10 -10
  53. data/app/assets/stylesheets/alchemy/{print.scss → admin/print.scss} +2 -6
  54. data/app/assets/stylesheets/alchemy/{resource_info.scss → admin/resource_info.scss} +6 -7
  55. data/app/assets/stylesheets/alchemy/{search.scss → admin/search.scss} +6 -6
  56. data/app/assets/stylesheets/alchemy/{selects.scss → admin/selects.scss} +46 -39
  57. data/app/assets/stylesheets/alchemy/{shoelace.scss → admin/shoelace.scss} +10 -10
  58. data/app/assets/stylesheets/alchemy/{sitemap.scss → admin/sitemap.scss} +18 -19
  59. data/app/assets/stylesheets/alchemy/{tables.scss → admin/tables.scss} +26 -22
  60. data/app/assets/stylesheets/alchemy/admin/tags.scss +158 -0
  61. data/app/assets/stylesheets/alchemy/{toolbar.scss → admin/toolbar.scss} +10 -10
  62. data/app/assets/stylesheets/alchemy/{typography.scss → admin/typography.scss} +3 -3
  63. data/app/assets/stylesheets/alchemy/{upload.scss → admin/upload.scss} +1 -1
  64. data/app/assets/stylesheets/alchemy/admin.scss +40 -45
  65. data/app/assets/stylesheets/alchemy/welcome.scss +57 -0
  66. data/app/assets/stylesheets/tinymce/skins/content/alchemy/{content.min.scss → content.scss} +5 -4
  67. data/app/assets/stylesheets/tinymce/skins/ui/alchemy/{skin.min.scss → skin.scss} +40 -40
  68. data/app/components/alchemy/admin/resource/action.rb +46 -0
  69. data/app/components/alchemy/admin/resource/cell.rb +34 -0
  70. data/app/components/alchemy/admin/resource/header.rb +46 -0
  71. data/app/components/alchemy/admin/resource/table.rb +153 -0
  72. data/app/components/alchemy/ingredients/datetime_view.rb +2 -2
  73. data/app/controllers/alchemy/admin/base_controller.rb +2 -1
  74. data/app/controllers/alchemy/admin/elements_controller.rb +7 -3
  75. data/app/controllers/alchemy/admin/legacy_page_urls_controller.rb +1 -1
  76. data/app/controllers/alchemy/admin/pages_controller.rb +1 -1
  77. data/app/controllers/alchemy/admin/pictures_controller.rb +2 -2
  78. data/app/controllers/alchemy/admin/resources_controller.rb +1 -1
  79. data/app/controllers/alchemy/base_controller.rb +2 -0
  80. data/app/controllers/concerns/alchemy/admin/uploader_responses.rb +2 -11
  81. data/app/decorators/alchemy/ingredient_editor.rb +17 -0
  82. data/app/helpers/alchemy/admin/pages_helper.rb +6 -10
  83. data/app/helpers/alchemy/base_helper.rb +2 -2
  84. data/app/helpers/alchemy/elements_block_helper.rb +13 -1
  85. data/app/helpers/alchemy/pages_helper.rb +2 -2
  86. data/app/javascript/alchemy_admin/components/element_editor.js +23 -31
  87. data/app/javascript/alchemy_admin/components/preview_window.js +2 -3
  88. data/app/javascript/alchemy_admin/picture_selector.js +38 -10
  89. data/app/models/alchemy/attachment.rb +0 -8
  90. data/app/models/alchemy/element/dom_id.rb +1 -0
  91. data/app/models/alchemy/element/element_ingredients.rb +0 -73
  92. data/app/models/alchemy/element/presenters.rb +4 -1
  93. data/app/models/alchemy/element.rb +6 -0
  94. data/app/models/alchemy/elements_repository.rb +2 -2
  95. data/app/models/alchemy/ingredient_validator.rb +10 -0
  96. data/app/models/alchemy/page/page_scopes.rb +1 -1
  97. data/app/models/alchemy/page.rb +3 -3
  98. data/app/models/alchemy/picture.rb +0 -10
  99. data/app/views/alchemy/admin/attachments/_files_list.html.erb +74 -16
  100. data/app/views/alchemy/admin/clipboard/index.html.erb +38 -33
  101. data/app/views/alchemy/admin/dashboard/_dashboard.html.erb +3 -0
  102. data/app/views/alchemy/admin/dashboard/_left_column.html.erb +4 -0
  103. data/app/views/alchemy/admin/dashboard/_right_column.html.erb +9 -0
  104. data/app/views/alchemy/admin/dashboard/_top.html.erb +12 -0
  105. data/app/views/alchemy/admin/dashboard/index.html.erb +1 -25
  106. data/app/views/alchemy/admin/elements/_element.html.erb +1 -2
  107. data/app/views/alchemy/admin/elements/_form.html.erb +1 -1
  108. data/app/views/alchemy/admin/ingredients/_picture_fields.html.erb +10 -3
  109. data/app/views/alchemy/admin/ingredients/update.turbo_stream.erb +7 -0
  110. data/app/views/alchemy/admin/languages/_table.html.erb +16 -42
  111. data/app/views/alchemy/admin/nodes/_form.html.erb +1 -1
  112. data/app/views/alchemy/admin/pages/_table.html.erb +92 -27
  113. data/app/views/alchemy/admin/pages/edit.html.erb +6 -8
  114. data/app/views/alchemy/admin/pages/index.html.erb +0 -4
  115. data/app/views/alchemy/admin/pictures/_form.html.erb +14 -12
  116. data/app/views/alchemy/admin/pictures/index.html.erb +1 -11
  117. data/app/views/alchemy/admin/pictures/update.turbo_stream.erb +6 -0
  118. data/app/views/alchemy/admin/resources/_resource_table.html.erb +3 -0
  119. data/app/views/alchemy/admin/resources/_table.html.erb +2 -0
  120. data/app/views/alchemy/admin/resources/index.html.erb +1 -1
  121. data/app/views/alchemy/admin/sites/index.html.erb +1 -1
  122. data/app/views/alchemy/admin/styleguide/index.html.erb +0 -4
  123. data/app/views/alchemy/admin/tags/index.html.erb +15 -14
  124. data/app/views/alchemy/base/403.html.erb +6 -0
  125. data/app/views/alchemy/base/500.html.erb +14 -12
  126. data/app/views/alchemy/ingredients/_datetime_editor.html.erb +13 -11
  127. data/app/views/alchemy/ingredients/_headline_editor.html.erb +29 -22
  128. data/app/views/alchemy/ingredients/_link_editor.html.erb +17 -11
  129. data/app/views/alchemy/ingredients/_page_editor.html.erb +1 -0
  130. data/app/views/alchemy/ingredients/_picture_editor.html.erb +3 -4
  131. data/app/views/alchemy/ingredients/_richtext_editor.html.erb +5 -1
  132. data/app/views/alchemy/ingredients/_select_editor.html.erb +2 -1
  133. data/app/views/alchemy/ingredients/_text_editor.html.erb +20 -14
  134. data/app/views/alchemy/ingredients/shared/_picture_css_class.html.erb +6 -0
  135. data/app/views/layouts/alchemy/admin.html.erb +4 -2
  136. data/bin/setup +2 -0
  137. data/bin/start +1 -1
  138. data/bun.lockb +0 -0
  139. data/config/alchemy/config.yml +9 -0
  140. data/config/locales/alchemy.en.yml +8 -29
  141. data/config/routes.rb +22 -22
  142. data/lib/alchemy/config.rb +3 -3
  143. data/lib/alchemy/install/tasks.rb +5 -2
  144. data/lib/alchemy/resources_helper.rb +3 -1
  145. data/lib/alchemy/test_support/capybara_helpers.rb +8 -5
  146. data/lib/alchemy/test_support/shared_uploader_examples.rb +0 -1
  147. data/lib/alchemy/upgrader/seven_point_three.rb +52 -0
  148. data/lib/alchemy/version.rb +1 -1
  149. data/lib/alchemy_cms.rb +1 -1
  150. data/lib/generators/alchemy/install/files/article.css +25 -0
  151. data/lib/generators/alchemy/install/files/custom.css +4 -0
  152. data/lib/generators/alchemy/install/install_generator.rb +6 -6
  153. data/lib/tasks/alchemy/upgrade.rake +29 -1
  154. data/vendor/assets/stylesheets/alchemy_admin/select2.css +1 -0
  155. data/vendor/assets/stylesheets/jquery.Jcrop.min.css +2 -0
  156. data/vendor/javascript/shoelace.min.js +62 -63
  157. data/vendor/javascript/tinymce.min.js +1 -1
  158. metadata +132 -105
  159. data/app/assets/images/alchemy/lupe.cur +0 -0
  160. data/app/assets/stylesheets/alchemy/labels.scss +0 -3
  161. data/app/assets/stylesheets/alchemy/tags.scss +0 -155
  162. data/app/assets/stylesheets/alchemy/welcome.sass +0 -49
  163. data/app/views/alchemy/admin/attachments/_attachment.html.erb +0 -81
  164. data/app/views/alchemy/admin/languages/_language.html.erb +0 -50
  165. data/app/views/alchemy/admin/pages/_table_row.html.erb +0 -111
  166. data/app/views/alchemy/admin/pages/list/_table.html.erb +0 -31
  167. data/app/views/alchemy/admin/pictures/update.js.erb +0 -6
  168. data/app/views/alchemy/admin/tags/_tag.html.erb +0 -32
  169. data/app/views/alchemy/base/update.js.erb +0 -5
  170. data/lib/generators/alchemy/install/files/all.css +0 -11
  171. data/lib/generators/alchemy/install/files/article.scss +0 -30
  172. data/package.json +0 -52
  173. data/vendor/assets/stylesheets/alchemy_admin/select2.scss +0 -741
  174. data/vendor/assets/stylesheets/jquery.Jcrop.min.scss +0 -2
  175. /data/app/assets/stylesheets/alchemy/{fonts.scss → _fonts.scss} +0 -0
  176. /data/app/assets/stylesheets/alchemy/{hints.scss → admin/hints.scss} +0 -0
  177. /data/app/assets/stylesheets/alchemy/{icons.scss → admin/icons.scss} +0 -0
  178. /data/app/assets/stylesheets/alchemy/{images.scss → admin/images.scss} +0 -0
  179. /data/app/assets/stylesheets/alchemy/{preview_window.scss → admin/preview_window.scss} +0 -0
  180. /data/app/assets/stylesheets/alchemy/{spinner.scss → admin/spinner.scss} +0 -0
  181. /data/app/views/alchemy/admin/dashboard/{_locked_pages.html.erb → widgets/_locked_pages.html.erb} +0 -0
  182. /data/app/views/alchemy/admin/dashboard/{_recent_pages.html.erb → widgets/_recent_pages.html.erb} +0 -0
  183. /data/app/views/alchemy/admin/dashboard/{_sites.html.erb → widgets/_sites.html.erb} +0 -0
  184. /data/app/views/alchemy/admin/dashboard/{_users.html.erb → widgets/_users.html.erb} +0 -0
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ module Admin
5
+ module Resource
6
+ # Renders a table header tag
7
+ # the component is an internal component of the Table component
8
+ #
9
+ # @param [String] :name
10
+ # name of the sortable link or the text if not additional text is given
11
+ # @param [String] :query
12
+ # Ransack query
13
+ # @param [String] :css_classes ("")
14
+ # css class of the th - tag
15
+ # @param [String, nil] :text (nil)
16
+ # optional text of the header
17
+ # @param [Symbol] :type (:string)
18
+ # type of the column will be used to inverse the sorting order for data/time - objects
19
+ # @param [Boolean] :sortable (false)
20
+ # enable a sortable link
21
+ #
22
+ class Header < ViewComponent::Base
23
+ delegate :sort_link, to: :helpers
24
+
25
+ erb_template <<~ERB
26
+ <th class="<%= @css_classes %>">
27
+ <% if @sortable %>
28
+ <%= sort_link @query, @name, @text, default_order: @default_order %>
29
+ <% else %>
30
+ <%= @text %>
31
+ <% end %>
32
+ </th>
33
+ ERB
34
+
35
+ def initialize(name, query, css_classes: "", text: nil, type: :string, sortable: false)
36
+ @name = name
37
+ @query = query
38
+ @text = text || name
39
+ @css_classes = css_classes
40
+ @default_order = /date|time/.match?(type.to_s) ? "desc" : "asc"
41
+ @sortable = sortable
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ module Admin
5
+ module Resource
6
+ # Renders a resource table with columns and buttons
7
+ #
8
+ # == Example
9
+ #
10
+ # <%= render Alchemy::Admin::Resource::Table.new(@languages, query: @query) do |table| %>
11
+ # <% table.icon_column "translate-2", style: false %>
12
+ # <% table.column :name, sortable: true %>
13
+ # <% table.column :language_code, sortable: true %>
14
+ # <% table.column :page_layout do |language| %>
15
+ # <%= Alchemy::Page.human_layout_name(language.page_layout) %>
16
+ # <% end %>
17
+ # <% table.delete_button %>
18
+ # <% table.edit_button %>
19
+ # <% end %>
20
+ #
21
+ # @param [ActiveRecord::Relation] :collection
22
+ # a collection of Alchemy::Resource objects that are shown in the table
23
+ # @param [Ransack::Search] :query
24
+ # The ransack search object to allow sortable table columns
25
+ # @param [String] :nothing_found_label (Alchemy.t("Nothing found"))
26
+ # The message that will be shown, if the collection is empty
27
+ # @param [Hash] :search_filter_params ({})
28
+ # An additional hash that will attached to the delete and edit button to redirect back to
29
+ # the same page of the table
30
+ # @param [String] :icon (nil)
31
+ # a default icon, if the table is auto generated
32
+ class Table < ViewComponent::Base
33
+ delegate :render_attribute,
34
+ :resource_path,
35
+ :render_icon,
36
+ :edit_resource_path,
37
+ :resource_handler,
38
+ :resource_window_size,
39
+ to: :helpers
40
+
41
+ attr_reader :collection,
42
+ :nothing_found_label,
43
+ :search_filter_params
44
+
45
+ renders_many :headers, Header
46
+
47
+ renders_many :cells, ->(css_classes, &block) do
48
+ Cell.new(css_classes, &block)
49
+ end
50
+
51
+ renders_many :actions, ->(name, tooltip = nil, &block) do
52
+ Action.new(name, tooltip, &block)
53
+ end
54
+
55
+ erb_template <<~ERB
56
+ <% if collection.any? %>
57
+ <table class="list">
58
+ <thead>
59
+ <tr>
60
+ <% headers.each do |header| %>
61
+ <%= header %>
62
+ <% end %>
63
+ <% if actions? %>
64
+ <th class="tools"></th>
65
+ <% end %>
66
+ </tr>
67
+ </thead>
68
+ <tbody>
69
+ <% collection.each do |resource| %>
70
+ <tr class="<%= cycle('even', 'odd') %>">
71
+ <% cells.each do |cell| %>
72
+ <%= render cell.with_resource(resource) %>
73
+ <% end %>
74
+ <% if actions? %>
75
+ <td class="tools">
76
+ <% actions.each do |action| %>
77
+ <%= render action.with_resource(resource) %>
78
+ <% end %>
79
+ </td>
80
+ <% end %>
81
+ </tr>
82
+ <% end %>
83
+ </tbody>
84
+ </table>
85
+ <% else %>
86
+ <alchemy-message type="info">
87
+ <%= nothing_found_label %>
88
+ </alchemy-message>
89
+ <% end %>
90
+ ERB
91
+
92
+ def initialize(collection, query: nil, nothing_found_label: Alchemy.t("Nothing found"), search_filter_params: {}, icon: nil)
93
+ @collection = collection
94
+ @query = query
95
+ @nothing_found_label = nothing_found_label
96
+ @search_filter_params = search_filter_params
97
+ @icon = icon
98
+ end
99
+
100
+ def column(name, header: nil, sortable: false, type: nil, class_name: nil, &block)
101
+ header ||= resource_handler.model.human_attribute_name(name)
102
+ type ||= resource_handler.model.columns_hash[name.to_s]&.type
103
+ attribute = resource_handler.attributes.find { |item| item[:name] == name.to_s } || {name: name, type: type}
104
+ block ||= lambda { |item| render_attribute(item, attribute) }
105
+
106
+ css_classes = [name, type, class_name].compact.join(" ")
107
+ with_header(name, @query, css_classes: css_classes, text: header, type: type, sortable: sortable)
108
+ with_cell(css_classes, &block)
109
+ end
110
+
111
+ def icon_column(icon = nil, style: nil)
112
+ column(:icon, header: "") do |resource|
113
+ render_icon(icon || yield(resource), size: "xl", style: style)
114
+ end
115
+ end
116
+
117
+ def delete_button(tooltip: Alchemy.t("Delete"), confirm_message: Alchemy.t("Are you sure?"))
118
+ with_action(:destroy, tooltip) do |row|
119
+ helpers.delete_button(resource_path(row, search_filter_params), {message: confirm_message})
120
+ end
121
+ end
122
+
123
+ def edit_button(tooltip: Alchemy.t("Edit"), dialog_title: tooltip, dialog_size: resource_window_size)
124
+ with_action(:edit, tooltip) do |row|
125
+ helpers.link_to_dialog render_icon(:edit),
126
+ edit_resource_path(row, search_filter_params),
127
+ {
128
+ size: dialog_size,
129
+ title: dialog_title
130
+ },
131
+ class: "icon_button"
132
+ end
133
+ end
134
+
135
+ private
136
+
137
+ ##
138
+ # if no cells are available the resource_helper will be used, to generate the
139
+ # default attributes of the given resource
140
+ def before_render
141
+ unless cells?
142
+ icon_column(@icon) if @icon.present?
143
+ resource_handler.sorted_attributes.each do |attribute|
144
+ column(attribute[:name], sortable: true)
145
+ end
146
+ delete_button
147
+ edit_button
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
@@ -4,8 +4,8 @@ module Alchemy
4
4
  attr_reader :date_format
5
5
 
6
6
  # @param ingredient [Alchemy::Ingredient]
7
- # @param date_format [String] The date format to use. Use either a strftime format string, a I18n format symbol or "rfc822".
8
- def initialize(ingredient, date_format: nil, html_options: {})
7
+ # @param date_format [String] The date format to use. Use either a strftime format string, a I18n format symbol or "rfc822". Defaults to "time.formats.alchemy.default".
8
+ def initialize(ingredient, date_format: :"alchemy.default", html_options: {})
9
9
  super(ingredient)
10
10
  @date_format = settings_value(:date_format, value: date_format)
11
11
  end
@@ -100,7 +100,8 @@ module Alchemy
100
100
  flash[:notice] = Alchemy.t(flash_notice)
101
101
  do_redirect_to redirect_url
102
102
  else
103
- render action: ((params[:action] == "update") ? "edit" : "new")
103
+ render action: ((params[:action] == "update") ? "edit" : "new"),
104
+ status: :unprocessable_entity
104
105
  end
105
106
  end
106
107
 
@@ -69,9 +69,13 @@ module Alchemy
69
69
  render json: {
70
70
  warning: @warning,
71
71
  errorMessage: Alchemy.t(:ingredient_validations_headline),
72
- ingredientsWithErrors: @element.ingredients_with_errors.map(&:id),
73
- errors: @element.ingredient_error_messages
74
- }
72
+ ingredientsWithErrors: @element.ingredients_with_errors.map do |ingredient|
73
+ {
74
+ id: ingredient.id,
75
+ errorMessage: ingredient.errors.messages[:value].to_sentence
76
+ }
77
+ end
78
+ }, status: :unprocessable_entity
75
79
  end
76
80
  end
77
81
 
@@ -22,7 +22,7 @@ module Alchemy
22
22
  @message = message_for_resource_action
23
23
  render :update
24
24
  else
25
- render :edit
25
+ render :edit, status: :unprocessable_entity
26
26
  end
27
27
  end
28
28
 
@@ -135,7 +135,7 @@ module Alchemy
135
135
  @tree = serialized_page_tree
136
136
  end
137
137
  else
138
- render :configure
138
+ render :configure, status: :unprocessable_entity
139
139
  end
140
140
  end
141
141
 
@@ -12,7 +12,7 @@ module Alchemy
12
12
  before_action :load_resource,
13
13
  only: [:show, :edit, :update, :url, :destroy]
14
14
 
15
- before_action :set_size, only: [:index, :show, :edit_multiple]
15
+ before_action :set_size, only: [:index, :show, :edit_multiple, :update]
16
16
 
17
17
  authorize_resource class: Alchemy::Picture
18
18
 
@@ -79,7 +79,7 @@ module Alchemy
79
79
  type: "error"
80
80
  }
81
81
  end
82
- render :update
82
+ render :update, status: (@message[:type] == "notice") ? :ok : :unprocessable_entity
83
83
  end
84
84
 
85
85
  def update_multiple
@@ -170,7 +170,7 @@ module Alchemy
170
170
  end
171
171
 
172
172
  def alchemy_module
173
- @alchemy_module ||= module_definition_for(controller: params[:controller], action: "index")
173
+ @alchemy_module ||= module_definition_for(controller: controller_path, action: "index")
174
174
  end
175
175
 
176
176
  def load_resource
@@ -82,6 +82,8 @@ module Alchemy
82
82
  locals: {message: flash[:warning], flash_type: "warning"}
83
83
  end
84
84
  end
85
+ elsif turbo_frame_request?
86
+ render "403", status: 403
85
87
  else
86
88
  redirect_to(alchemy.admin_dashboard_path)
87
89
  end
@@ -11,7 +11,7 @@ module Alchemy
11
11
  name: file.name)
12
12
 
13
13
  {
14
- json: uploader_response(file: file, message: message),
14
+ json: {message: message},
15
15
  status: status
16
16
  }
17
17
  end
@@ -23,19 +23,10 @@ module Alchemy
23
23
  name: file.name)
24
24
 
25
25
  {
26
- json: uploader_response(file: file, message: message),
26
+ json: {message: message},
27
27
  status: :unprocessable_entity
28
28
  }
29
29
  end
30
-
31
- private
32
-
33
- def uploader_response(file:, message:)
34
- {
35
- files: [file.to_jq_upload],
36
- message: message
37
- }
38
- end
39
30
  end
40
31
  end
41
32
  end
@@ -153,6 +153,23 @@ module Alchemy
153
153
  end
154
154
  end
155
155
 
156
+ def validations
157
+ definition.fetch(:validate, [])
158
+ end
159
+
160
+ def format_validation
161
+ validations.select { _1.is_a?(Hash) }.find { _1[:format] }&.fetch(:format)
162
+ end
163
+
164
+ def length_validation
165
+ validations.select { _1.is_a?(Hash) }.find { _1[:length] }&.fetch(:length)
166
+ end
167
+
168
+ def presence_validation?
169
+ validations.include?("presence") ||
170
+ validations.any? { _1.is_a?(Hash) && _1[:presence] == true }
171
+ end
172
+
156
173
  private
157
174
 
158
175
  def form_field_counter
@@ -5,18 +5,14 @@ module Alchemy
5
5
  module PagesHelper
6
6
  include Alchemy::Admin::BaseHelper
7
7
 
8
- # Returns options tags for the screen sizes select in page edit view.
8
+ # Returns screen sizes for the preview size select in page edit view.
9
+ #
10
+ # You can configure the screen sizes in your +config/alchemy/config.yml+.
9
11
  #
10
12
  def preview_sizes_for_select
11
- options_for_select([
12
- "auto",
13
- [Alchemy.t("240", scope: "preview_sizes"), 240],
14
- [Alchemy.t("320", scope: "preview_sizes"), 320],
15
- [Alchemy.t("480", scope: "preview_sizes"), 480],
16
- [Alchemy.t("768", scope: "preview_sizes"), 768],
17
- [Alchemy.t("1024", scope: "preview_sizes"), 1024],
18
- [Alchemy.t("1280", scope: "preview_sizes"), 1280]
19
- ])
13
+ Alchemy::Config.get(:page_preview_sizes).map do |size|
14
+ [Alchemy.t(size, scope: "preview_sizes"), size]
15
+ end
20
16
  end
21
17
 
22
18
  # Renders a label for page's page layout
@@ -42,8 +42,8 @@ module Alchemy
42
42
  # <p>Caution! This is a warning!</p>
43
43
  # <% end %>
44
44
  #
45
- def render_message(type = :info, msg = nil, &blk)
46
- render Alchemy::Admin::Message.new(msg || capture(&blk), type: type)
45
+ def render_message(type = :info, msg = nil, &)
46
+ render Alchemy::Admin::Message.new(msg || capture(&), type: type)
47
47
  end
48
48
 
49
49
  # Checks if the given argument is a String or a Page object.
@@ -100,9 +100,21 @@ module Alchemy
100
100
  # A lambda used for formatting the element's tags (see Alchemy::ElementsHelper::element_tags_attributes). Set to +false+ to not include tags in the wrapper element.
101
101
  #
102
102
  def element_view_for(element, options = {})
103
+ if options[:id].nil?
104
+ Alchemy::Deprecation.warn <<~WARN
105
+ Relying on an implicit DOM id in `element_view_for` is deprecated. Please provide an explicit `id` if you actually want to render an `id` attribute on the #{element.name} element wrapper tag.
106
+ WARN
107
+ end
108
+
109
+ if options[:class].nil?
110
+ Alchemy::Deprecation.warn <<~WARN
111
+ Relying on an implicit CSS class in `element_view_for` is deprecated. Please provide an explicit `class` for the #{element.name} element wrapper tag.
112
+ WARN
113
+ end
114
+
103
115
  options = {
104
116
  tag: :div,
105
- id: element.dom_id,
117
+ id: (!!options[:id]) ? options[:id] : element.dom_id,
106
118
  class: element.name,
107
119
  tags_formatter: ->(tags) { tags.join(" ") }
108
120
  }.merge(options)
@@ -62,8 +62,8 @@ module Alchemy
62
62
  #
63
63
  # renders +app/views/alchemy/site_layouts/_default_site.html.erb+ for the site named "Default Site".
64
64
  #
65
- def render_site_layout(&block)
66
- render current_alchemy_site, &block
65
+ def render_site_layout(&)
66
+ render(current_alchemy_site, &)
67
67
  rescue ActionView::MissingTemplate => error
68
68
  error_or_warning(error, "Site layout for #{current_alchemy_site.try(:name)} not found. Please run `rails g alchemy:site_layouts`")
69
69
  end
@@ -19,7 +19,7 @@ export class ElementEditor extends HTMLElement {
19
19
  // Triggered by child elements
20
20
  this.addEventListener("alchemy:element-update-title", this)
21
21
  // We use of @rails/ujs for Rails remote forms
22
- this.addEventListener("ajax:success", this)
22
+ this.addEventListener("ajax:complete", this)
23
23
  // Dirty observer
24
24
  this.addEventListener("change", this)
25
25
 
@@ -57,11 +57,11 @@ export class ElementEditor extends HTMLElement {
57
57
  this.onClickElement()
58
58
  }
59
59
  break
60
- case "ajax:success":
60
+ case "ajax:complete":
61
61
  if (event.target === this.body) {
62
- const responseJSON = event.detail[0]
62
+ const xhr = event.detail[0]
63
63
  event.stopPropagation()
64
- this.onSaveElement(responseJSON)
64
+ this.onSaveElement(xhr)
65
65
  }
66
66
  break
67
67
  case "alchemy:element-update-title":
@@ -115,30 +115,28 @@ export class ElementEditor extends HTMLElement {
115
115
  /**
116
116
  * Sets the element to saved state
117
117
  * Updates title
118
+ * JS event bubbling will also update the parents element quote.
118
119
  * Shows error messages if ingredient validations fail
119
- * @argument {JSON} data
120
+ * @argument {XMLHttpRequest} xhr
120
121
  */
121
- onSaveElement(data) {
122
- // JS event bubbling will also update the parents element quote.
123
- this.setClean()
122
+ onSaveElement(xhr) {
123
+ const data = JSON.parse(xhr.responseText)
124
124
  // Reset errors that might be visible from last save attempt
125
- this.errorsDisplay.innerHTML = ""
126
- this.elementErrors.classList.add("hidden")
127
- this.body
128
- .querySelectorAll(".ingredient-editor")
129
- .forEach((el) => el.classList.remove("validation_failed"))
125
+ this.setClean()
130
126
  // If validation failed
131
- if (data.errors) {
127
+ if (xhr.status === 422) {
132
128
  const warning = data.warning
133
129
  // Create error messages
134
- data.errors.forEach((message) => {
135
- this.errorsDisplay.append(createHtmlElement(`<li>${message}</li>`))
136
- })
137
130
  // Mark ingredients as failed
138
- data.ingredientsWithErrors.forEach((id) => {
139
- this.querySelector(`[data-ingredient-id="${id}"]`)?.classList.add(
140
- "validation_failed"
131
+ data.ingredientsWithErrors.forEach((ingredient) => {
132
+ const ingredientEditor = this.querySelector(
133
+ `[data-ingredient-id="${ingredient.id}"]`
141
134
  )
135
+ const errorDisplay = createHtmlElement(
136
+ `<small class="error">${ingredient.errorMessage}</small>`
137
+ )
138
+ ingredientEditor?.appendChild(errorDisplay)
139
+ ingredientEditor?.classList.add("validation_failed")
142
140
  })
143
141
  // Show message
144
142
  growl(warning, "warn")
@@ -208,9 +206,12 @@ export class ElementEditor extends HTMLElement {
208
206
  setClean() {
209
207
  this.dirty = false
210
208
  window.onbeforeunload = null
209
+ this.elementErrors.classList.add("hidden")
210
+
211
211
  if (this.hasEditors) {
212
- this.body.querySelectorAll(".dirty").forEach((el) => {
213
- el.classList.remove("dirty")
212
+ this.body.querySelectorAll(".ingredient-editor").forEach((el) => {
213
+ el.classList.remove("dirty", "validation_failed")
214
+ el.querySelectorAll("small.error").forEach((e) => e.remove())
214
215
  })
215
216
  }
216
217
  }
@@ -482,15 +483,6 @@ export class ElementEditor extends HTMLElement {
482
483
  return this.toggleButton?.querySelector("alchemy-icon")
483
484
  }
484
485
 
485
- /**
486
- * The error messages container
487
- *
488
- * @returns {HTMLElement}
489
- */
490
- get errorsDisplay() {
491
- return this.body.querySelector(".error-messages")
492
- }
493
-
494
486
  /**
495
487
  * The validation messages list container
496
488
  *
@@ -66,12 +66,11 @@ class PreviewWindow extends HTMLIFrameElement {
66
66
 
67
67
  key("alt+r", () => this.refresh())
68
68
 
69
- // Need to listen with jQuery here because select2 does not emit native events.
70
- $(this.sizeSelect).on("change", (evt) => {
69
+ this.sizeSelect.addEventListener("change", (evt) => {
71
70
  const select = evt.target
72
71
  const width = select.value
73
72
 
74
- if (width === "auto") {
73
+ if (width === "") {
75
74
  this.style.width = null
76
75
  } else {
77
76
  this.resize(width)
@@ -1,34 +1,62 @@
1
1
  import { on } from "alchemy_admin/utils/events"
2
2
 
3
+ function toggleCheckboxes(state) {
4
+ document
5
+ .querySelectorAll(".picture_tool.select input[type='checkbox']")
6
+ .forEach((checkbox) => {
7
+ checkbox.checked = state
8
+ checkbox.closest(".picture_thumbnail").classList.toggle("active", state)
9
+ })
10
+ }
11
+
12
+ function checkedInputs() {
13
+ return document.querySelectorAll("#picture_archive input:checked")
14
+ }
15
+
16
+ function editMultiplePicturesUrl(href) {
17
+ const searchParameters = new URLSearchParams()
18
+ checkedInputs().forEach((entry) =>
19
+ searchParameters.append(entry.name, entry.value)
20
+ )
21
+ const url = href + "?" + searchParameters.toString()
22
+
23
+ return url
24
+ }
25
+
3
26
  /**
4
27
  * Multiple picture select handler for the picture archive.
5
28
  */
6
29
  export default function PictureSelector() {
30
+ const selectAllButton = document.querySelector("#select_all_pictures")
7
31
  const selectedItemTools = document.querySelector(".selected_item_tools")
8
- const checkedInputs = () =>
9
- document.querySelectorAll("#picture_archive input:checked")
32
+
33
+ on("click", ".toolbar_buttons", "a#select_all_pictures", (event) => {
34
+ event.preventDefault()
35
+
36
+ selectAllButton.classList.toggle("active")
37
+
38
+ const state = selectAllButton.classList.contains("active")
39
+
40
+ toggleCheckboxes(state)
41
+
42
+ selectedItemTools.classList.toggle("hidden", !state)
43
+ })
10
44
 
11
45
  // make the item toolbar visible and show the checkbox also if it is not hovered anymore
12
46
  on("change", ".picture_tool.select", "input", (event) => {
13
- selectedItemTools.style.display =
14
- checkedInputs().length > 0 ? "block" : "none"
47
+ selectedItemTools.classList.toggle("hidden", checkedInputs().length === 0)
15
48
 
16
49
  const parentElementClassList = event.target.parentElement.classList
17
50
  const checked = event.target.checked
18
51
 
19
52
  parentElementClassList.toggle("visible", checked)
20
- parentElementClassList.toggle("hidden", !checked)
21
53
  })
22
54
 
23
55
  // open the edit view in a dialog modal
24
56
  on("click", ".selected_item_tools", "a#edit_multiple_pictures", (event) => {
25
57
  event.preventDefault()
26
58
 
27
- const searchParameters = new URLSearchParams()
28
- checkedInputs().forEach((entry) =>
29
- searchParameters.append(entry.name, entry.value)
30
- )
31
- const url = event.target.href + "?" + searchParameters.toString()
59
+ const url = editMultiplePicturesUrl(event.target.href)
32
60
 
33
61
  Alchemy.openDialog(url, {
34
62
  title: event.target.title,
@@ -102,14 +102,6 @@ module Alchemy
102
102
 
103
103
  # Instance methods
104
104
 
105
- def to_jq_upload
106
- {
107
- "name" => read_attribute(:file_name),
108
- "size" => read_attribute(:file_size),
109
- "error" => errors[:file].join
110
- }
111
- end
112
-
113
105
  def url(options = {})
114
106
  if file
115
107
  self.class.url_class.new(self).call(options)
@@ -11,6 +11,7 @@ module Alchemy
11
11
  #
12
12
  # Alchemy::Element.dom_id_class = MyDomIdClass
13
13
  #
14
+ # @deprecated Use a headline ingredient with anchor setting instead.
14
15
  class Element < BaseRecord
15
16
  class DomId
16
17
  def initialize(element)