alchemy_cms 7.2.5 → 7.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +97 -4
- data/Gemfile +4 -3
- data/Rakefile +1 -0
- data/alchemy_cms.gemspec +7 -7
- data/app/assets/builds/alchemy/admin/print.css +1 -0
- data/app/assets/builds/alchemy/admin/print.css.map +1 -0
- data/app/assets/builds/alchemy/admin.css +1 -0
- data/app/assets/builds/alchemy/admin.css.map +1 -0
- data/app/assets/builds/alchemy/welcome.css +1 -0
- data/app/assets/builds/alchemy/welcome.css.map +1 -0
- data/app/assets/builds/tinymce/skins/content/alchemy/content.css +1 -0
- data/app/assets/builds/tinymce/skins/content/alchemy/content.css.map +1 -0
- data/app/assets/builds/tinymce/skins/content/alchemy/content.min.css +1 -0
- data/app/assets/builds/tinymce/skins/content/alchemy/content.min.css.map +1 -0
- data/app/assets/builds/tinymce/skins/ui/alchemy/skin.css +1 -0
- data/app/assets/builds/tinymce/skins/ui/alchemy/skin.css.map +1 -0
- data/app/assets/builds/tinymce/skins/ui/alchemy/skin.min.css +1 -0
- data/app/assets/builds/tinymce/skins/ui/alchemy/skin.min.css.map +1 -0
- data/app/assets/config/alchemy_manifest.js +1 -5
- data/app/assets/javascripts/alchemy/alchemy.dialog.js.coffee +4 -0
- data/app/assets/stylesheets/alchemy/{_custom-properties.scss → _custom-properties.css} +28 -25
- data/app/assets/stylesheets/alchemy/_deprecated_variables.scss +41 -0
- data/app/assets/stylesheets/alchemy/_deprecation.scss +17 -0
- data/app/assets/stylesheets/alchemy/_extends.scss +1 -1
- data/app/assets/stylesheets/alchemy/_mixins.scss +20 -23
- data/app/assets/stylesheets/alchemy/_variables.scss +98 -94
- data/app/assets/stylesheets/alchemy/{archive.scss → admin/archive.scss} +23 -23
- data/app/assets/stylesheets/alchemy/{attachment-select.scss → admin/attachment-select.scss} +2 -2
- data/app/assets/stylesheets/alchemy/{attachments.scss → admin/attachments.scss} +4 -4
- data/app/assets/stylesheets/alchemy/{base.scss → admin/base.scss} +9 -9
- data/app/assets/stylesheets/alchemy/{buttons.scss → admin/buttons.scss} +3 -3
- data/app/assets/stylesheets/alchemy/{clipboard.scss → admin/clipboard.scss} +9 -6
- data/app/assets/stylesheets/alchemy/{dashboard.scss → admin/dashboard.scss} +8 -8
- data/app/assets/stylesheets/alchemy/{dialogs.scss → admin/dialogs.scss} +20 -20
- data/app/assets/stylesheets/alchemy/{elements.scss → admin/elements.scss} +128 -88
- data/app/assets/stylesheets/alchemy/{errors.scss → admin/errors.scss} +22 -6
- data/app/assets/stylesheets/alchemy/{flash.scss → admin/flash.scss} +3 -3
- data/app/assets/stylesheets/alchemy/{flatpickr.scss → admin/flatpickr.scss} +55 -35
- data/app/assets/stylesheets/alchemy/{form_fields.scss → admin/form_fields.scss} +8 -6
- data/app/assets/stylesheets/alchemy/{forms.scss → admin/forms.scss} +20 -16
- data/app/assets/stylesheets/alchemy/{frame.scss → admin/frame.scss} +9 -9
- data/app/assets/stylesheets/alchemy/{image_library.scss → admin/image_library.scss} +34 -33
- data/app/assets/stylesheets/alchemy/admin/labels.scss +3 -0
- data/app/assets/stylesheets/alchemy/{list_filter.scss → admin/list_filter.scss} +4 -4
- data/app/assets/stylesheets/alchemy/{lists.scss → admin/lists.scss} +9 -7
- data/app/assets/stylesheets/alchemy/{navigation.scss → admin/navigation.scss} +17 -17
- data/app/assets/stylesheets/alchemy/{node-select.scss → admin/node-select.scss} +5 -5
- data/app/assets/stylesheets/alchemy/{nodes.scss → admin/nodes.scss} +11 -11
- data/app/assets/stylesheets/alchemy/{notices.scss → admin/notices.scss} +11 -7
- data/app/assets/stylesheets/alchemy/{page-select.scss → admin/page-select.scss} +10 -10
- data/app/assets/stylesheets/alchemy/{pagination.scss → admin/pagination.scss} +10 -10
- data/app/assets/stylesheets/alchemy/{print.scss → admin/print.scss} +2 -6
- data/app/assets/stylesheets/alchemy/{resource_info.scss → admin/resource_info.scss} +6 -7
- data/app/assets/stylesheets/alchemy/{search.scss → admin/search.scss} +6 -6
- data/app/assets/stylesheets/alchemy/{selects.scss → admin/selects.scss} +46 -39
- data/app/assets/stylesheets/alchemy/{shoelace.scss → admin/shoelace.scss} +10 -10
- data/app/assets/stylesheets/alchemy/{sitemap.scss → admin/sitemap.scss} +18 -19
- data/app/assets/stylesheets/alchemy/{tables.scss → admin/tables.scss} +26 -22
- data/app/assets/stylesheets/alchemy/admin/tags.scss +158 -0
- data/app/assets/stylesheets/alchemy/{toolbar.scss → admin/toolbar.scss} +10 -10
- data/app/assets/stylesheets/alchemy/{typography.scss → admin/typography.scss} +3 -3
- data/app/assets/stylesheets/alchemy/{upload.scss → admin/upload.scss} +1 -1
- data/app/assets/stylesheets/alchemy/admin.scss +40 -45
- data/app/assets/stylesheets/alchemy/welcome.scss +57 -0
- data/app/assets/stylesheets/tinymce/skins/content/alchemy/{content.min.scss → content.scss} +5 -4
- data/app/assets/stylesheets/tinymce/skins/ui/alchemy/{skin.min.scss → skin.scss} +40 -40
- data/app/components/alchemy/admin/resource/action.rb +46 -0
- data/app/components/alchemy/admin/resource/cell.rb +34 -0
- data/app/components/alchemy/admin/resource/header.rb +46 -0
- data/app/components/alchemy/admin/resource/table.rb +153 -0
- data/app/components/alchemy/ingredients/datetime_view.rb +2 -2
- data/app/controllers/alchemy/admin/base_controller.rb +2 -1
- data/app/controllers/alchemy/admin/elements_controller.rb +7 -3
- data/app/controllers/alchemy/admin/legacy_page_urls_controller.rb +1 -1
- data/app/controllers/alchemy/admin/pages_controller.rb +1 -1
- data/app/controllers/alchemy/admin/pictures_controller.rb +2 -2
- data/app/controllers/alchemy/admin/resources_controller.rb +1 -1
- data/app/controllers/alchemy/base_controller.rb +2 -0
- data/app/controllers/alchemy/messages_controller.rb +1 -0
- data/app/controllers/concerns/alchemy/admin/uploader_responses.rb +2 -11
- data/app/decorators/alchemy/ingredient_editor.rb +17 -0
- data/app/helpers/alchemy/admin/pages_helper.rb +6 -10
- data/app/helpers/alchemy/base_helper.rb +2 -2
- data/app/helpers/alchemy/elements_block_helper.rb +13 -1
- data/app/helpers/alchemy/elements_helper.rb +1 -1
- data/app/helpers/alchemy/pages_helper.rb +2 -2
- data/app/javascript/alchemy_admin/components/element_editor.js +23 -31
- data/app/javascript/alchemy_admin/components/preview_window.js +2 -3
- data/app/javascript/alchemy_admin/picture_selector.js +38 -10
- data/app/models/alchemy/attachment.rb +0 -8
- data/app/models/alchemy/element/dom_id.rb +1 -0
- data/app/models/alchemy/element/element_ingredients.rb +0 -73
- data/app/models/alchemy/element/presenters.rb +4 -1
- data/app/models/alchemy/element.rb +6 -0
- data/app/models/alchemy/elements_repository.rb +2 -2
- data/app/models/alchemy/ingredient_validator.rb +10 -0
- data/app/models/alchemy/page/page_scopes.rb +1 -1
- data/app/models/alchemy/page.rb +3 -3
- data/app/models/alchemy/picture.rb +0 -10
- data/app/views/alchemy/admin/attachments/_files_list.html.erb +74 -16
- data/app/views/alchemy/admin/clipboard/index.html.erb +38 -33
- data/app/views/alchemy/admin/dashboard/_dashboard.html.erb +3 -0
- data/app/views/alchemy/admin/dashboard/_left_column.html.erb +4 -0
- data/app/views/alchemy/admin/dashboard/_right_column.html.erb +9 -0
- data/app/views/alchemy/admin/dashboard/_top.html.erb +12 -0
- data/app/views/alchemy/admin/dashboard/index.html.erb +1 -25
- data/app/views/alchemy/admin/elements/_element.html.erb +1 -2
- data/app/views/alchemy/admin/elements/_form.html.erb +1 -1
- data/app/views/alchemy/admin/ingredients/_picture_fields.html.erb +10 -3
- data/app/views/alchemy/admin/ingredients/update.turbo_stream.erb +7 -0
- data/app/views/alchemy/admin/languages/_table.html.erb +16 -42
- data/app/views/alchemy/admin/nodes/_form.html.erb +1 -1
- data/app/views/alchemy/admin/pages/_table.html.erb +92 -27
- data/app/views/alchemy/admin/pages/edit.html.erb +6 -8
- data/app/views/alchemy/admin/pages/index.html.erb +0 -4
- data/app/views/alchemy/admin/pictures/_form.html.erb +14 -12
- data/app/views/alchemy/admin/pictures/index.html.erb +1 -11
- data/app/views/alchemy/admin/pictures/update.turbo_stream.erb +6 -0
- data/app/views/alchemy/admin/resources/_resource_table.html.erb +3 -0
- data/app/views/alchemy/admin/resources/_table.html.erb +2 -0
- data/app/views/alchemy/admin/resources/index.html.erb +1 -1
- data/app/views/alchemy/admin/sites/index.html.erb +1 -1
- data/app/views/alchemy/admin/styleguide/index.html.erb +0 -4
- data/app/views/alchemy/admin/tags/index.html.erb +15 -14
- data/app/views/alchemy/base/403.html.erb +6 -0
- data/app/views/alchemy/base/500.html.erb +14 -12
- data/app/views/alchemy/ingredients/_datetime_editor.html.erb +13 -11
- data/app/views/alchemy/ingredients/_headline_editor.html.erb +29 -22
- data/app/views/alchemy/ingredients/_link_editor.html.erb +17 -11
- data/app/views/alchemy/ingredients/_page_editor.html.erb +1 -0
- data/app/views/alchemy/ingredients/_picture_editor.html.erb +3 -4
- data/app/views/alchemy/ingredients/_richtext_editor.html.erb +5 -1
- data/app/views/alchemy/ingredients/_select_editor.html.erb +2 -1
- data/app/views/alchemy/ingredients/_text_editor.html.erb +20 -14
- data/app/views/alchemy/ingredients/shared/_picture_css_class.html.erb +6 -0
- data/app/views/layouts/alchemy/admin.html.erb +4 -2
- data/bin/setup +2 -0
- data/bin/start +1 -1
- data/bun.lockb +0 -0
- data/config/alchemy/config.yml +9 -0
- data/config/locales/alchemy.en.yml +8 -29
- data/config/routes.rb +22 -22
- data/lib/alchemy/config.rb +3 -3
- data/lib/alchemy/install/tasks.rb +5 -2
- data/lib/alchemy/resources_helper.rb +3 -1
- data/lib/alchemy/test_support/capybara_helpers.rb +8 -5
- data/lib/alchemy/test_support/shared_uploader_examples.rb +0 -1
- data/lib/alchemy/upgrader/seven_point_three.rb +52 -0
- data/lib/alchemy/version.rb +1 -1
- data/lib/alchemy_cms.rb +1 -1
- data/lib/generators/alchemy/install/files/article.css +25 -0
- data/lib/generators/alchemy/install/files/custom.css +4 -0
- data/lib/generators/alchemy/install/install_generator.rb +6 -6
- data/lib/tasks/alchemy/upgrade.rake +29 -1
- data/vendor/assets/stylesheets/alchemy_admin/select2.css +1 -0
- data/vendor/assets/stylesheets/jquery.Jcrop.min.css +2 -0
- data/vendor/javascript/shoelace.min.js +62 -63
- data/vendor/javascript/tinymce.min.js +1 -1
- metadata +132 -105
- data/app/assets/images/alchemy/lupe.cur +0 -0
- data/app/assets/stylesheets/alchemy/labels.scss +0 -3
- data/app/assets/stylesheets/alchemy/tags.scss +0 -155
- data/app/assets/stylesheets/alchemy/welcome.sass +0 -49
- data/app/views/alchemy/admin/attachments/_attachment.html.erb +0 -81
- data/app/views/alchemy/admin/languages/_language.html.erb +0 -50
- data/app/views/alchemy/admin/pages/_table_row.html.erb +0 -111
- data/app/views/alchemy/admin/pages/list/_table.html.erb +0 -31
- data/app/views/alchemy/admin/pictures/update.js.erb +0 -6
- data/app/views/alchemy/admin/tags/_tag.html.erb +0 -32
- data/app/views/alchemy/base/update.js.erb +0 -5
- data/lib/generators/alchemy/install/files/all.css +0 -11
- data/lib/generators/alchemy/install/files/article.scss +0 -30
- data/package.json +0 -52
- data/vendor/assets/stylesheets/alchemy_admin/select2.scss +0 -741
- data/vendor/assets/stylesheets/jquery.Jcrop.min.scss +0 -2
- /data/app/assets/stylesheets/alchemy/{fonts.scss → _fonts.scss} +0 -0
- /data/app/assets/stylesheets/alchemy/{hints.scss → admin/hints.scss} +0 -0
- /data/app/assets/stylesheets/alchemy/{icons.scss → admin/icons.scss} +0 -0
- /data/app/assets/stylesheets/alchemy/{images.scss → admin/images.scss} +0 -0
- /data/app/assets/stylesheets/alchemy/{preview_window.scss → admin/preview_window.scss} +0 -0
- /data/app/assets/stylesheets/alchemy/{spinner.scss → admin/spinner.scss} +0 -0
- /data/app/views/alchemy/admin/dashboard/{_locked_pages.html.erb → widgets/_locked_pages.html.erb} +0 -0
- /data/app/views/alchemy/admin/dashboard/{_recent_pages.html.erb → widgets/_recent_pages.html.erb} +0 -0
- /data/app/views/alchemy/admin/dashboard/{_sites.html.erb → widgets/_sites.html.erb} +0 -0
- /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:
|
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
|
73
|
-
|
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
|
|
@@ -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:
|
173
|
+
@alchemy_module ||= module_definition_for(controller: controller_path, action: "index")
|
174
174
|
end
|
175
175
|
|
176
176
|
def load_resource
|
@@ -11,7 +11,7 @@ module Alchemy
|
|
11
11
|
name: file.name)
|
12
12
|
|
13
13
|
{
|
14
|
-
json:
|
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:
|
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
|
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
|
-
|
12
|
-
"
|
13
|
-
|
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, &
|
46
|
-
render Alchemy::Admin::Message.new(msg || capture(&
|
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(&
|
66
|
-
render
|
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:
|
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:
|
60
|
+
case "ajax:complete":
|
61
61
|
if (event.target === this.body) {
|
62
|
-
const
|
62
|
+
const xhr = event.detail[0]
|
63
63
|
event.stopPropagation()
|
64
|
-
this.onSaveElement(
|
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 {
|
120
|
+
* @argument {XMLHttpRequest} xhr
|
120
121
|
*/
|
121
|
-
onSaveElement(
|
122
|
-
|
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.
|
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 (
|
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((
|
139
|
-
this.querySelector(
|
140
|
-
"
|
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(".
|
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
|
-
|
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 === "
|
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
|
-
|
9
|
-
|
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.
|
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
|
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)
|