alchemy_cms 7.2.7 → 7.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +97 -14
  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 +2 -2
  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/picture.rb +0 -10
  98. data/app/views/alchemy/admin/attachments/_files_list.html.erb +74 -16
  99. data/app/views/alchemy/admin/clipboard/index.html.erb +38 -33
  100. data/app/views/alchemy/admin/dashboard/_dashboard.html.erb +3 -0
  101. data/app/views/alchemy/admin/dashboard/_left_column.html.erb +4 -0
  102. data/app/views/alchemy/admin/dashboard/_right_column.html.erb +9 -0
  103. data/app/views/alchemy/admin/dashboard/_top.html.erb +12 -0
  104. data/app/views/alchemy/admin/dashboard/index.html.erb +1 -25
  105. data/app/views/alchemy/admin/elements/_element.html.erb +1 -2
  106. data/app/views/alchemy/admin/elements/_form.html.erb +1 -1
  107. data/app/views/alchemy/admin/ingredients/_picture_fields.html.erb +10 -3
  108. data/app/views/alchemy/admin/ingredients/update.turbo_stream.erb +7 -0
  109. data/app/views/alchemy/admin/languages/_table.html.erb +16 -42
  110. data/app/views/alchemy/admin/nodes/_form.html.erb +1 -1
  111. data/app/views/alchemy/admin/pages/_table.html.erb +92 -27
  112. data/app/views/alchemy/admin/pages/edit.html.erb +6 -8
  113. data/app/views/alchemy/admin/pages/index.html.erb +0 -4
  114. data/app/views/alchemy/admin/pictures/_form.html.erb +14 -12
  115. data/app/views/alchemy/admin/pictures/index.html.erb +1 -11
  116. data/app/views/alchemy/admin/pictures/update.turbo_stream.erb +6 -0
  117. data/app/views/alchemy/admin/resources/_resource_table.html.erb +3 -0
  118. data/app/views/alchemy/admin/resources/_table.html.erb +2 -0
  119. data/app/views/alchemy/admin/resources/index.html.erb +1 -1
  120. data/app/views/alchemy/admin/sites/index.html.erb +1 -1
  121. data/app/views/alchemy/admin/styleguide/index.html.erb +0 -4
  122. data/app/views/alchemy/admin/tags/index.html.erb +15 -14
  123. data/app/views/alchemy/base/403.html.erb +6 -0
  124. data/app/views/alchemy/base/500.html.erb +14 -12
  125. data/app/views/alchemy/ingredients/_datetime_editor.html.erb +13 -11
  126. data/app/views/alchemy/ingredients/_headline_editor.html.erb +29 -22
  127. data/app/views/alchemy/ingredients/_link_editor.html.erb +17 -11
  128. data/app/views/alchemy/ingredients/_page_editor.html.erb +1 -0
  129. data/app/views/alchemy/ingredients/_picture_editor.html.erb +3 -4
  130. data/app/views/alchemy/ingredients/_richtext_editor.html.erb +5 -1
  131. data/app/views/alchemy/ingredients/_select_editor.html.erb +2 -1
  132. data/app/views/alchemy/ingredients/_text_editor.html.erb +20 -14
  133. data/app/views/alchemy/ingredients/shared/_picture_css_class.html.erb +6 -0
  134. data/app/views/layouts/alchemy/admin.html.erb +4 -2
  135. data/bin/setup +2 -0
  136. data/bin/start +1 -1
  137. data/bun.lockb +0 -0
  138. data/config/alchemy/config.yml +9 -0
  139. data/config/locales/alchemy.en.yml +8 -29
  140. data/config/routes.rb +22 -22
  141. data/lib/alchemy/config.rb +3 -3
  142. data/lib/alchemy/install/tasks.rb +5 -2
  143. data/lib/alchemy/resources_helper.rb +3 -1
  144. data/lib/alchemy/test_support/capybara_helpers.rb +8 -5
  145. data/lib/alchemy/test_support/shared_uploader_examples.rb +0 -1
  146. data/lib/alchemy/upgrader/seven_point_three.rb +52 -0
  147. data/lib/alchemy/version.rb +1 -1
  148. data/lib/alchemy_cms.rb +1 -1
  149. data/lib/generators/alchemy/install/files/article.css +25 -0
  150. data/lib/generators/alchemy/install/files/custom.css +4 -0
  151. data/lib/generators/alchemy/install/install_generator.rb +6 -6
  152. data/lib/tasks/alchemy/upgrade.rake +29 -1
  153. data/vendor/assets/stylesheets/alchemy_admin/select2.css +1 -0
  154. data/vendor/assets/stylesheets/jquery.Jcrop.min.css +2 -0
  155. data/vendor/javascript/shoelace.min.js +62 -63
  156. data/vendor/javascript/tinymce.min.js +1 -1
  157. metadata +132 -105
  158. data/app/assets/images/alchemy/lupe.cur +0 -0
  159. data/app/assets/stylesheets/alchemy/labels.scss +0 -3
  160. data/app/assets/stylesheets/alchemy/tags.scss +0 -155
  161. data/app/assets/stylesheets/alchemy/welcome.sass +0 -49
  162. data/app/views/alchemy/admin/attachments/_attachment.html.erb +0 -81
  163. data/app/views/alchemy/admin/languages/_language.html.erb +0 -50
  164. data/app/views/alchemy/admin/pages/_table_row.html.erb +0 -111
  165. data/app/views/alchemy/admin/pages/list/_table.html.erb +0 -31
  166. data/app/views/alchemy/admin/pictures/update.js.erb +0 -6
  167. data/app/views/alchemy/admin/tags/_tag.html.erb +0 -32
  168. data/app/views/alchemy/base/update.js.erb +0 -5
  169. data/lib/generators/alchemy/install/files/all.css +0 -11
  170. data/lib/generators/alchemy/install/files/article.scss +0 -30
  171. data/package.json +0 -52
  172. data/vendor/assets/stylesheets/alchemy_admin/select2.scss +0 -741
  173. data/vendor/assets/stylesheets/jquery.Jcrop.min.scss +0 -2
  174. /data/app/assets/stylesheets/alchemy/{fonts.scss → _fonts.scss} +0 -0
  175. /data/app/assets/stylesheets/alchemy/{hints.scss → admin/hints.scss} +0 -0
  176. /data/app/assets/stylesheets/alchemy/{icons.scss → admin/icons.scss} +0 -0
  177. /data/app/assets/stylesheets/alchemy/{images.scss → admin/images.scss} +0 -0
  178. /data/app/assets/stylesheets/alchemy/{preview_window.scss → admin/preview_window.scss} +0 -0
  179. /data/app/assets/stylesheets/alchemy/{spinner.scss → admin/spinner.scss} +0 -0
  180. /data/app/views/alchemy/admin/dashboard/{_locked_pages.html.erb → widgets/_locked_pages.html.erb} +0 -0
  181. /data/app/views/alchemy/admin/dashboard/{_recent_pages.html.erb → widgets/_recent_pages.html.erb} +0 -0
  182. /data/app/views/alchemy/admin/dashboard/{_sites.html.erb → widgets/_sites.html.erb} +0 -0
  183. /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
@@ -138,7 +138,7 @@ module Alchemy
138
138
  end
139
139
 
140
140
  def eligible_resource_filter_values
141
- resource_filters.map(&:values).flatten!.map!(&:to_s)
141
+ resource_filters.map(&:values).flatten
142
142
  end
143
143
 
144
144
  # Returns a translated +flash[:notice]+ for current controller action.
@@ -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)