alchemy_cms 8.1.11 → 8.2.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.
- checksums.yaml +4 -4
- data/README.md +12 -1
- data/app/assets/builds/alchemy/admin.css +1 -1
- data/app/assets/builds/alchemy/alchemy_admin.min.js +1 -1
- data/app/assets/builds/alchemy/alchemy_admin.min.js.map +1 -1
- data/app/assets/builds/alchemy/dark-theme.css +1 -1
- data/app/assets/builds/alchemy/light-theme.css +1 -1
- data/app/assets/builds/alchemy/theme.css +1 -1
- data/app/assets/builds/alchemy/welcome.css +1 -1
- data/app/assets/builds/tinymce/skins/content/alchemy/content.min.css +1 -1
- data/app/assets/builds/tinymce/skins/content/alchemy-dark/content.min.css +1 -1
- data/app/assets/images/alchemy/icons-sprite.svg +1 -1
- data/app/components/alchemy/admin/current_user_name.rb +34 -0
- data/app/components/alchemy/admin/locale_select.rb +12 -8
- data/app/components/alchemy/admin/page_node.html.erb +3 -2
- data/app/components/alchemy/admin/picture_thumbnail.rb +1 -1
- data/app/components/alchemy/admin/preview_time_select.rb +55 -0
- data/app/components/alchemy/admin/publish_element_button.html.erb +41 -0
- data/app/components/alchemy/admin/publish_element_button.rb +13 -0
- data/app/components/alchemy/admin/timezone_select.rb +47 -0
- data/app/components/alchemy/ingredients/select_editor.rb +6 -1
- data/app/controllers/alchemy/admin/attachments_controller.rb +0 -9
- data/app/controllers/alchemy/admin/base_controller.rb +1 -0
- data/app/controllers/alchemy/admin/elements_controller.rb +54 -34
- data/app/controllers/alchemy/admin/pages_controller.rb +1 -0
- data/app/controllers/alchemy/admin/pictures_controller.rb +2 -23
- data/app/controllers/alchemy/admin/resources_controller.rb +11 -6
- data/app/controllers/alchemy/pages_controller.rb +1 -2
- data/app/helpers/alchemy/admin/base_helper.rb +4 -7
- data/app/helpers/alchemy/url_helper.rb +2 -10
- data/app/javascript/alchemy_admin/components/element_editor/publish_element_button.js +28 -27
- data/app/javascript/alchemy_admin/components/element_editor.js +11 -2
- data/app/javascript/alchemy_admin/components/message.js +5 -1
- data/app/javascript/alchemy_admin/components/picture_thumbnail.js +1 -0
- data/app/javascript/alchemy_admin/components/uploader/file_upload.js +5 -5
- data/app/javascript/alchemy_admin/image_cropper.js +10 -6
- data/app/javascript/alchemy_admin/initializer.js +6 -33
- data/app/javascript/alchemy_admin/shoelace_theme.js +6 -2
- data/app/javascript/alchemy_admin/templates/compiled.js +1 -1
- data/app/javascript/alchemy_admin.js +12 -2
- data/app/models/alchemy/attachment.rb +1 -41
- data/app/models/alchemy/current.rb +5 -1
- data/app/models/alchemy/element/element_ingredients.rb +11 -3
- data/app/models/alchemy/element.rb +10 -0
- data/app/models/alchemy/ingredient.rb +2 -0
- data/app/models/alchemy/ingredients/select.rb +1 -2
- data/app/models/alchemy/page/etag_generator.rb +21 -0
- data/app/models/alchemy/page/url_path.rb +11 -2
- data/app/models/alchemy/page.rb +12 -2
- data/app/models/alchemy/page_version.rb +5 -5
- data/app/models/alchemy/picture.rb +19 -2
- data/app/models/alchemy/storage_adapter/active_storage.rb +9 -0
- data/app/models/alchemy/storage_adapter/dragonfly.rb +9 -0
- data/app/models/alchemy/storage_adapter.rb +1 -0
- data/app/models/concerns/alchemy/publishable.rb +20 -12
- data/app/models/concerns/alchemy/relatable_resource.rb +19 -15
- data/app/models/concerns/alchemy/touch_elements.rb +3 -3
- data/app/services/alchemy/element_preloader.rb +107 -0
- data/app/stylesheets/alchemy/_custom-properties.scss +1 -0
- data/app/stylesheets/alchemy/_mixins.scss +1 -1
- data/app/stylesheets/alchemy/_themes.scss +2 -0
- data/app/stylesheets/alchemy/admin/archive.scss +2 -2
- data/app/stylesheets/alchemy/admin/base.scss +2 -1
- data/app/stylesheets/alchemy/admin/elements.scss +22 -19
- data/app/stylesheets/alchemy/admin/form_fields.scss +3 -0
- data/app/stylesheets/alchemy/admin/forms.scss +14 -1
- data/app/stylesheets/alchemy/admin/frame.scss +9 -8
- data/app/stylesheets/alchemy/admin/images.scss +2 -2
- data/app/stylesheets/alchemy/admin/notices.scss +1 -1
- data/app/stylesheets/alchemy/admin/popover.scss +37 -0
- data/app/stylesheets/alchemy/admin/selects.scss +4 -0
- data/app/stylesheets/alchemy/admin/shoelace.scss +16 -4
- data/app/stylesheets/alchemy/admin/toolbar.scss +8 -0
- data/app/stylesheets/alchemy/admin.scss +1 -0
- data/app/views/alchemy/admin/_header.html.erb +4 -0
- data/app/views/alchemy/admin/_left_menu.html.erb +24 -0
- data/app/views/alchemy/admin/_main_navi.html.erb +6 -0
- data/app/views/alchemy/admin/_top_menu.html.erb +6 -0
- data/app/views/alchemy/admin/_user_info.html.erb +5 -0
- data/app/views/alchemy/admin/attachments/_files_list.html.erb +1 -1
- data/app/views/alchemy/admin/crop.html.erb +6 -11
- data/app/views/alchemy/admin/elements/_header.html.erb +16 -6
- data/app/views/alchemy/admin/elements/_schedule.html.erb +62 -0
- data/app/views/alchemy/admin/elements/_toolbar.html.erb +1 -15
- data/app/views/alchemy/admin/elements/publish.turbo_stream.erb +28 -0
- data/app/views/alchemy/admin/nodes/index.html.erb +1 -1
- data/app/views/alchemy/admin/pages/_locked_pages.html.erb +5 -0
- data/app/views/alchemy/admin/pages/_publication_fields.html.erb +4 -4
- data/app/views/alchemy/admin/pages/_table.html.erb +1 -1
- data/app/views/alchemy/admin/pages/edit.html.erb +6 -2
- data/app/views/alchemy/admin/partials/_language_tree_select.html.erb +10 -10
- data/app/views/alchemy/admin/partials/_site_select.html.erb +6 -3
- data/app/views/alchemy/admin/pictures/_filter_and_size_bar.html.erb +3 -3
- data/app/views/alchemy/admin/pictures/_picture.html.erb +1 -1
- data/app/views/alchemy/admin/pictures/_picture_to_assign.html.erb +1 -1
- data/app/views/alchemy/admin/pictures/index.html.erb +2 -2
- data/app/views/alchemy/admin/tinymce/_setup.html.erb +9 -16
- data/app/views/alchemy/admin/uploader/_setup.html.erb +1 -6
- data/app/views/alchemy/language_links/_language.html.erb +1 -2
- data/app/views/layouts/alchemy/admin.html.erb +2 -45
- data/config/importmap.rb +7 -2
- data/config/locales/alchemy.en.yml +35 -5
- data/lib/alchemy/admin/preview_time.rb +23 -0
- data/lib/alchemy/admin/preview_url.rb +13 -2
- data/lib/alchemy/admin/timezone.rb +56 -0
- data/lib/alchemy/configurations/main.rb +13 -1
- data/lib/alchemy/test_support/factories/element_factory.rb +2 -2
- data/lib/alchemy/test_support/relatable_resource_examples.rb +2 -2
- data/lib/alchemy/test_support/shared_publishable_examples.rb +44 -2
- data/lib/alchemy/upgrader.rb +3 -1
- data/lib/alchemy/version.rb +1 -1
- data/lib/alchemy_cms.rb +2 -0
- data/lib/generators/alchemy/install/install_generator.rb +2 -1
- data/vendor/javascript/handlebars.min.js +4 -4
- data/vendor/javascript/shoelace.min.js +1419 -1323
- data/vendor/javascript/sortable.min.js +2 -2
- data/vendor/javascript/tinymce.min.js +1 -1
- metadata +33 -1
|
@@ -7,14 +7,18 @@ module Alchemy
|
|
|
7
7
|
|
|
8
8
|
before_action :load_page_and_version, only: [:index, :new]
|
|
9
9
|
include Alchemy::Admin::Clipboard
|
|
10
|
+
include Alchemy::Admin::PreviewTime
|
|
10
11
|
|
|
11
12
|
before_action :load_element, only: [:update, :destroy, :collapse, :expand, :publish]
|
|
12
13
|
authorize_resource class: Alchemy::Element
|
|
13
14
|
|
|
14
15
|
def index
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
preloaded = Alchemy::ElementPreloader
|
|
17
|
+
.new(page_version: @page_version)
|
|
18
|
+
.call
|
|
19
|
+
|
|
20
|
+
@elements = preloaded.reject(&:fixed?)
|
|
21
|
+
@fixed_elements = preloaded.select(&:fixed?)
|
|
18
22
|
end
|
|
19
23
|
|
|
20
24
|
def new
|
|
@@ -89,12 +93,23 @@ module Alchemy
|
|
|
89
93
|
end
|
|
90
94
|
|
|
91
95
|
def publish
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
if schedule_element_params.present?
|
|
97
|
+
@element.assign_attributes(schedule_element_params)
|
|
98
|
+
@element.skip_ingredient_validations = true
|
|
99
|
+
@element.save
|
|
100
|
+
status = @element.valid? ? 200 : 422
|
|
101
|
+
else
|
|
102
|
+
@element.public = !@element.public?
|
|
103
|
+
@element.save(validate: false)
|
|
104
|
+
status = 200
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
respond_to do |format|
|
|
108
|
+
format.turbo_stream do
|
|
109
|
+
@page = @element.page
|
|
110
|
+
render status:
|
|
111
|
+
end
|
|
112
|
+
end
|
|
98
113
|
end
|
|
99
114
|
|
|
100
115
|
def order
|
|
@@ -119,12 +134,12 @@ module Alchemy
|
|
|
119
134
|
def collapse
|
|
120
135
|
# We do not want to trigger the touch callback or any validations
|
|
121
136
|
@element.update_columns(folded: true)
|
|
122
|
-
# Collapse all nested elements
|
|
123
|
-
|
|
124
|
-
Alchemy::Element.where(id:
|
|
137
|
+
# Collapse all nested elements (except compact ones which stay as-is)
|
|
138
|
+
nested_element_ids = nested_element_ids_to_collapse(@element)
|
|
139
|
+
Alchemy::Element.where(id: nested_element_ids).update_all(folded: true)
|
|
125
140
|
|
|
126
141
|
render json: {
|
|
127
|
-
nestedElementIds:
|
|
142
|
+
nestedElementIds: nested_element_ids,
|
|
128
143
|
title: Alchemy.t(@element.folded? ? :show_element_content : :hide_element_content)
|
|
129
144
|
}
|
|
130
145
|
end
|
|
@@ -152,30 +167,31 @@ module Alchemy
|
|
|
152
167
|
@page = @page_version.page
|
|
153
168
|
end
|
|
154
169
|
|
|
155
|
-
|
|
170
|
+
# Collects IDs of nested elements that should be collapsed.
|
|
171
|
+
# Skips compact elements (they retain their fold state).
|
|
172
|
+
# Optimized to use a single query instead of N queries for N levels.
|
|
173
|
+
def nested_element_ids_to_collapse(element)
|
|
174
|
+
all_elements = Element
|
|
175
|
+
.where(page_version_id: element.page_version_id)
|
|
176
|
+
.select(:id, :parent_element_id, :folded, :name)
|
|
177
|
+
.to_a
|
|
178
|
+
|
|
179
|
+
children_by_parent = all_elements.group_by(&:parent_element_id)
|
|
180
|
+
|
|
156
181
|
ids = []
|
|
157
|
-
element.
|
|
158
|
-
|
|
159
|
-
|
|
182
|
+
stack = [element.id]
|
|
183
|
+
|
|
184
|
+
while stack.any?
|
|
185
|
+
current_id = stack.pop
|
|
186
|
+
children = children_by_parent[current_id] || []
|
|
187
|
+
|
|
188
|
+
children.reject(&:compact?).each do |child|
|
|
189
|
+
ids << child.id if child.expanded?
|
|
190
|
+
stack << child.id
|
|
191
|
+
end
|
|
160
192
|
end
|
|
161
|
-
ids
|
|
162
|
-
end
|
|
163
193
|
|
|
164
|
-
|
|
165
|
-
[
|
|
166
|
-
{
|
|
167
|
-
ingredients: :related_object
|
|
168
|
-
},
|
|
169
|
-
:tags,
|
|
170
|
-
{
|
|
171
|
-
all_nested_elements: [
|
|
172
|
-
{
|
|
173
|
-
ingredients: :related_object
|
|
174
|
-
},
|
|
175
|
-
:tags
|
|
176
|
-
]
|
|
177
|
-
}
|
|
178
|
-
]
|
|
194
|
+
ids
|
|
179
195
|
end
|
|
180
196
|
|
|
181
197
|
def load_element
|
|
@@ -216,6 +232,10 @@ module Alchemy
|
|
|
216
232
|
def create_element_params
|
|
217
233
|
params.require(:element).permit(:name, :page_version_id, :parent_element_id)
|
|
218
234
|
end
|
|
235
|
+
|
|
236
|
+
def schedule_element_params
|
|
237
|
+
params[:element]&.permit(:public_on, :public_until)
|
|
238
|
+
end
|
|
219
239
|
end
|
|
220
240
|
end
|
|
221
241
|
end
|
|
@@ -8,8 +8,6 @@ module Alchemy
|
|
|
8
8
|
include CurrentLanguage
|
|
9
9
|
include PictureDescriptionsFormHelper
|
|
10
10
|
|
|
11
|
-
before_action :load_pictures, only: :index
|
|
12
|
-
|
|
13
11
|
before_action :load_resource,
|
|
14
12
|
only: [:edit, :update, :url, :destroy]
|
|
15
13
|
|
|
@@ -21,12 +19,6 @@ module Alchemy
|
|
|
21
19
|
@picture = Picture.find(params[:id])
|
|
22
20
|
end
|
|
23
21
|
|
|
24
|
-
# Preload deletable ids for the current page (index) or the current
|
|
25
|
-
# resource (update, which re-renders the +_picture+ partial via a
|
|
26
|
-
# turbo stream). One query replaces the per-row +deletable?+ check
|
|
27
|
-
# that would otherwise fire for every picture the view renders.
|
|
28
|
-
before_action :load_deletable_picture_ids, only: [:index, :update]
|
|
29
|
-
|
|
30
22
|
add_alchemy_filter :by_file_format, type: :select, options: ->(query) do
|
|
31
23
|
Alchemy::Picture.file_formats(query.result)
|
|
32
24
|
end
|
|
@@ -38,6 +30,8 @@ module Alchemy
|
|
|
38
30
|
helper_method :picture_offset
|
|
39
31
|
|
|
40
32
|
def index
|
|
33
|
+
@pictures = filtered_pictures(page: params[:page])
|
|
34
|
+
|
|
41
35
|
if in_overlay?
|
|
42
36
|
archive_overlay
|
|
43
37
|
end
|
|
@@ -165,21 +159,6 @@ module Alchemy
|
|
|
165
159
|
|
|
166
160
|
private
|
|
167
161
|
|
|
168
|
-
def load_pictures
|
|
169
|
-
@pictures = filtered_pictures(page: params[:page])
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
# Preload deletable ids in a single query so the view can decide which
|
|
173
|
-
# delete buttons to enable without calling +deletable?+ (a two-query
|
|
174
|
-
# check) per row. We pass already-loaded ids (via +map(&:id)+) rather
|
|
175
|
-
# than the relation itself, because passing a paginated relation
|
|
176
|
-
# produces +IN (SELECT ... LIMIT n)+, which older MariaDB versions
|
|
177
|
-
# reject.
|
|
178
|
-
def load_deletable_picture_ids
|
|
179
|
-
ids = @pictures ? @pictures.map(&:id) : [@picture.id]
|
|
180
|
-
@deletable_picture_ids = Picture.where(id: ids).deletable.pluck(:id).to_set
|
|
181
|
-
end
|
|
182
|
-
|
|
183
162
|
def picture_offset
|
|
184
163
|
((params[:page] || 1).to_i - 1) * items_per_page
|
|
185
164
|
end
|
|
@@ -18,12 +18,7 @@ module Alchemy
|
|
|
18
18
|
before_action :authorize_resource
|
|
19
19
|
|
|
20
20
|
def index
|
|
21
|
-
|
|
22
|
-
@query.sorts = default_sort_order if @query.sorts.empty?
|
|
23
|
-
items = @query.result
|
|
24
|
-
|
|
25
|
-
items = items.includes(*resource_relations_names) if contains_relations?
|
|
26
|
-
items = items.tagged_with(search_filter_params[:tagged_with]) if search_filter_params[:tagged_with].present?
|
|
21
|
+
items = collection
|
|
27
22
|
|
|
28
23
|
respond_to do |format|
|
|
29
24
|
format.html do
|
|
@@ -121,6 +116,16 @@ module Alchemy
|
|
|
121
116
|
authorize!(action_name.to_sym, resource_instance_variable || resource_handler.model)
|
|
122
117
|
end
|
|
123
118
|
|
|
119
|
+
def collection
|
|
120
|
+
@query = resource_handler.model.ransack(search_filter_params[:q])
|
|
121
|
+
@query.sorts = default_sort_order if @query.sorts.empty?
|
|
122
|
+
items = @query.result(distinct: true)
|
|
123
|
+
|
|
124
|
+
items = items.includes(*resource_relations_names) if contains_relations?
|
|
125
|
+
items = items.tagged_with(search_filter_params[:tagged_with]) if search_filter_params[:tagged_with].present?
|
|
126
|
+
items
|
|
127
|
+
end
|
|
128
|
+
|
|
124
129
|
# Permits all editable resource attributes as default.
|
|
125
130
|
#
|
|
126
131
|
# Define this method in your inheriting controller if you want to permit additional attributes.
|
|
@@ -230,8 +230,7 @@ module Alchemy
|
|
|
230
230
|
# Otherwise all users will see the same cached page, regardless of user's state.
|
|
231
231
|
#
|
|
232
232
|
def page_etag
|
|
233
|
-
|
|
234
|
-
[@page, elements_cache_key, current_alchemy_user]
|
|
233
|
+
Alchemy::Page::EtagGenerator.new(@page).call(current_alchemy_user)
|
|
235
234
|
end
|
|
236
235
|
|
|
237
236
|
# We only render the page if either the cache is disabled for this page
|
|
@@ -17,15 +17,12 @@ module Alchemy
|
|
|
17
17
|
#
|
|
18
18
|
# In order to represent your own +User+'s class instance,
|
|
19
19
|
# you should add a +alchemy_display_name+ method to your +User+ class
|
|
20
|
-
#
|
|
20
|
+
# @deprecated Use the +CurrentAlchemyUserName+ component instead.
|
|
21
21
|
def current_alchemy_user_name
|
|
22
|
-
|
|
23
|
-
if name.present?
|
|
24
|
-
content_tag :span, class: "current-user-name" do
|
|
25
|
-
"#{render_icon(:user, size: "1x")} #{name}".html_safe
|
|
26
|
-
end
|
|
27
|
-
end
|
|
22
|
+
render Alchemy::Admin::CurrentUserName.new(user: current_alchemy_user)
|
|
28
23
|
end
|
|
24
|
+
deprecate current_alchemy_user_name: "Use the Alchemy::Admin::CurrentUserName component instead.",
|
|
25
|
+
deprecator: Alchemy::Deprecation
|
|
29
26
|
|
|
30
27
|
# This helper renders the link to an dialog.
|
|
31
28
|
#
|
|
@@ -8,20 +8,12 @@ module Alchemy
|
|
|
8
8
|
module UrlHelper
|
|
9
9
|
# Returns the path for rendering an alchemy page
|
|
10
10
|
def show_alchemy_page_path(page, optional_params = {})
|
|
11
|
-
|
|
11
|
+
page.url_path(optional_params)
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
# Returns the url for rendering an alchemy page
|
|
15
15
|
def show_alchemy_page_url(page, optional_params = {})
|
|
16
|
-
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
# Returns the correct params-hash for passing to show_page_path
|
|
20
|
-
def show_page_path_params(page, optional_params = {})
|
|
21
|
-
raise ArgumentError, "Page is nil" if page.nil?
|
|
22
|
-
|
|
23
|
-
url_params = {urlname: page.urlname}.update(optional_params)
|
|
24
|
-
prefix_locale?(page.language_code) ? url_params.update(locale: page.language_code) : url_params
|
|
16
|
+
"#{request.base_url}#{page.url_path(optional_params)}"
|
|
25
17
|
end
|
|
26
18
|
|
|
27
19
|
# Returns the path for downloading an alchemy attachment
|
|
@@ -1,42 +1,43 @@
|
|
|
1
|
-
import { patch } from "alchemy_admin/utils/ajax"
|
|
2
|
-
import { reloadPreview } from "alchemy_admin/components/preview_window"
|
|
3
|
-
import { growl } from "alchemy_admin/growler"
|
|
4
|
-
import { dispatchPageDirtyEvent } from "alchemy_admin/components/element_editor"
|
|
5
|
-
|
|
6
1
|
export class PublishElementButton extends HTMLElement {
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
#scheduleButtonVariant
|
|
3
|
+
|
|
4
|
+
connectedCallback() {
|
|
5
|
+
this.#scheduleButtonVariant = this.scheduleButton.getAttribute("variant")
|
|
6
|
+
this.publishButton.addEventListener("click", this)
|
|
7
|
+
this.dropdown.addEventListener("sl-show", this)
|
|
8
|
+
this.dropdown.addEventListener("sl-hide", this)
|
|
9
|
+
}
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
disconnectedCallback() {
|
|
12
|
+
this.publishButton.removeEventListener("click", this)
|
|
13
|
+
this.dropdown.removeEventListener("sl-show", this)
|
|
14
|
+
this.dropdown.removeEventListener("sl-hide", this)
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
handleEvent(event) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
})
|
|
26
|
-
.catch((error) => growl(error.message, "error"))
|
|
18
|
+
switch (event.type) {
|
|
19
|
+
case "click":
|
|
20
|
+
this.publishButton.loading = true
|
|
21
|
+
break
|
|
22
|
+
case "sl-show":
|
|
23
|
+
this.scheduleButton.setAttribute("variant", "primary")
|
|
24
|
+
break
|
|
25
|
+
case "sl-hide":
|
|
26
|
+
this.scheduleButton.setAttribute("variant", this.#scheduleButtonVariant)
|
|
27
|
+
break
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
get
|
|
31
|
-
return this.
|
|
31
|
+
get publishButton() {
|
|
32
|
+
return this.querySelector("sl-button[type='submit']")
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
get
|
|
35
|
-
return this.
|
|
35
|
+
get dropdown() {
|
|
36
|
+
return this.querySelector("sl-dropdown")
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
get
|
|
39
|
-
return this.
|
|
39
|
+
get scheduleButton() {
|
|
40
|
+
return this.querySelector("sl-button[slot='trigger']")
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
43
|
|
|
@@ -27,7 +27,7 @@ export class ElementEditor extends HTMLElement {
|
|
|
27
27
|
|
|
28
28
|
// Dirty observer still needs to be jQuery
|
|
29
29
|
// in order to support select2.
|
|
30
|
-
$(this).on("change", this.onChange)
|
|
30
|
+
$(this.form).on("change", this.onChange)
|
|
31
31
|
|
|
32
32
|
this.header?.addEventListener("dblclick", () => {
|
|
33
33
|
this.toggle()
|
|
@@ -87,7 +87,7 @@ export class ElementEditor extends HTMLElement {
|
|
|
87
87
|
if (target.classList.contains("nested-elements")) {
|
|
88
88
|
return
|
|
89
89
|
}
|
|
90
|
-
this.setDirty(target)
|
|
90
|
+
this.closest("alchemy-element-editor").setDirty(target)
|
|
91
91
|
event.stopPropagation()
|
|
92
92
|
return false
|
|
93
93
|
}
|
|
@@ -555,6 +555,15 @@ export class ElementEditor extends HTMLElement {
|
|
|
555
555
|
return this.querySelector("alchemy-element-editor")
|
|
556
556
|
}
|
|
557
557
|
|
|
558
|
+
/**
|
|
559
|
+
* The form element if present
|
|
560
|
+
*
|
|
561
|
+
* @returns {HTMLFormElement|undefined}
|
|
562
|
+
*/
|
|
563
|
+
get form() {
|
|
564
|
+
return this.querySelector("form.element-body")
|
|
565
|
+
}
|
|
566
|
+
|
|
558
567
|
/**
|
|
559
568
|
* The parent element editor if present
|
|
560
569
|
*
|
|
@@ -39,6 +39,10 @@ class Message extends HTMLElement {
|
|
|
39
39
|
return this.hasAttribute("dismissable")
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
get icon() {
|
|
43
|
+
return this.getAttribute("icon")
|
|
44
|
+
}
|
|
45
|
+
|
|
42
46
|
get type() {
|
|
43
47
|
return this.getAttribute("type") || "notice"
|
|
44
48
|
}
|
|
@@ -50,7 +54,7 @@ class Message extends HTMLElement {
|
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
get iconName() {
|
|
53
|
-
switch (this.type) {
|
|
57
|
+
switch (this.icon || this.type) {
|
|
54
58
|
case "warning":
|
|
55
59
|
case "warn":
|
|
56
60
|
case "alert":
|
|
@@ -85,13 +85,13 @@ export class FileUpload extends AlchemyHTMLElement {
|
|
|
85
85
|
errorMessage = translate("Uploaded bytes exceed file size")
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
const
|
|
89
|
-
?
|
|
90
|
-
:
|
|
88
|
+
const allowedFiletypes = this.file?.type.includes("image")
|
|
89
|
+
? config.allowed_filetypes.alchemy_pictures
|
|
90
|
+
: config.allowed_filetypes.alchemy_attachments
|
|
91
91
|
|
|
92
92
|
const isFileFormatSupported =
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
allowedFiletypes.includes("*") ||
|
|
94
|
+
allowedFiletypes.includes(
|
|
95
95
|
this.file?.type.replace(/^\w+\/(\w+)(\+\w+)?/i, "$1")
|
|
96
96
|
)
|
|
97
97
|
|
|
@@ -6,13 +6,17 @@ export default class ImageCropper {
|
|
|
6
6
|
#cropFromField = null
|
|
7
7
|
#cropSizeField = null
|
|
8
8
|
|
|
9
|
-
constructor(image,
|
|
9
|
+
constructor(image, settings) {
|
|
10
10
|
this.image = image
|
|
11
|
-
this.defaultBox =
|
|
12
|
-
this.aspectRatio =
|
|
13
|
-
this.#cropFromField = document.getElementById(
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
this.defaultBox = settings.default_box
|
|
12
|
+
this.aspectRatio = settings.ratio
|
|
13
|
+
this.#cropFromField = document.getElementById(
|
|
14
|
+
settings.crop_from_form_field_id
|
|
15
|
+
)
|
|
16
|
+
this.#cropSizeField = document.getElementById(
|
|
17
|
+
settings.crop_size_form_field_id
|
|
18
|
+
)
|
|
19
|
+
this.elementId = settings.element_id
|
|
16
20
|
this.dialog = Alchemy.currentDialog()
|
|
17
21
|
if (this.dialog) {
|
|
18
22
|
this.dialog.options.closed = () => this.destroy()
|
|
@@ -1,48 +1,21 @@
|
|
|
1
1
|
import Hotkeys from "alchemy_admin/hotkeys"
|
|
2
2
|
import pleaseWaitOverlay from "alchemy_admin/please_wait_overlay"
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
* add change listener to select to redirect the user after selecting another locale or site
|
|
6
|
-
* @param {string} selectId
|
|
7
|
-
* @param {string} parameterName
|
|
8
|
-
* @param {boolean} forcedReload
|
|
9
|
-
*/
|
|
10
|
-
function selectHandler(selectId, parameterName, forcedReload = false) {
|
|
11
|
-
$(`select#${selectId}`).on("change", function (e) {
|
|
12
|
-
let url = window.location.pathname
|
|
13
|
-
let delimiter = url.match(/\?/) ? "&" : "?"
|
|
14
|
-
const location = `${url}${delimiter}${parameterName}=${$(this).val()}`
|
|
15
|
-
|
|
16
|
-
if (forcedReload) {
|
|
17
|
-
window.location.href = location
|
|
18
|
-
} else {
|
|
19
|
-
Turbo.visit(location, {})
|
|
20
|
-
}
|
|
21
|
-
})
|
|
22
|
-
}
|
|
23
|
-
|
|
24
4
|
export default function Initializer() {
|
|
25
5
|
// We obviously have javascript enabled.
|
|
26
|
-
|
|
6
|
+
document.documentElement.classList.remove("no-js")
|
|
27
7
|
|
|
28
8
|
// Initialize hotkeys.
|
|
29
9
|
Hotkeys()
|
|
30
10
|
|
|
31
11
|
// Add observer for please wait overlay.
|
|
32
|
-
|
|
12
|
+
document.querySelectorAll(".please_wait").forEach((element) => {
|
|
13
|
+
element.addEventListener("click", pleaseWaitOverlay)
|
|
14
|
+
})
|
|
33
15
|
|
|
34
16
|
// Hack for enabling tab focus for <a>'s styled as button.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
// Locale select handler
|
|
38
|
-
selectHandler("change_locale", "admin_locale", true)
|
|
39
|
-
|
|
40
|
-
// Site select handler
|
|
41
|
-
selectHandler("change_site", "site_id")
|
|
42
|
-
|
|
43
|
-
// Submit forms of selects with `data-autosubmit="true"`
|
|
44
|
-
$('select[data-auto-submit="true"]').on("change", function () {
|
|
45
|
-
$(this.form).submit()
|
|
17
|
+
document.querySelectorAll("a.button").forEach((button) => {
|
|
18
|
+
button.setAttribute("tabindex", 0)
|
|
46
19
|
})
|
|
47
20
|
|
|
48
21
|
// Override the filter of keymaster.js so we can blur the fields on esc key.
|
|
@@ -47,12 +47,16 @@ const spriteUrl = document
|
|
|
47
47
|
.getAttribute("href")
|
|
48
48
|
|
|
49
49
|
const iconMap = {
|
|
50
|
-
"x-lg": "close"
|
|
50
|
+
"x-lg": "close",
|
|
51
|
+
caret: "arrow-down-s"
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
const options = {
|
|
54
55
|
resolver: (name) => `${spriteUrl}#ri-${iconMap[name] || name}-line`,
|
|
55
|
-
mutator: (svg) =>
|
|
56
|
+
mutator: (svg) => {
|
|
57
|
+
svg.setAttribute("fill", "currentColor")
|
|
58
|
+
svg.setAttribute("viewBox", "0 0 24 24")
|
|
59
|
+
},
|
|
56
60
|
spriteSheet: true
|
|
57
61
|
}
|
|
58
62
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
(()=>{var n=Handlebars.template;(Handlebars.templates=Handlebars.templates||{})["node_folder.hbs"]=n({
|
|
1
|
+
(()=>{var n=Handlebars.template;(Handlebars.templates=Handlebars.templates||{})["node_folder.hbs"]=n({0:function(n,e,l,a,r){return"right"},1:function(n,e,l,a,r){return"down"},compiler:[8,">= 4.3.0"],main:function(n,e,l,a,r){var o,t=n.lambda,d=n.escapeExpression,u=n.lookupProperty||function(n,e){if(Object.prototype.hasOwnProperty.call(n,e))return n[e]};return'<a class="node_folder" data-record-id="'+d(t(null!=(o=null!=e?u(e,"node"):e)?u(o,"id"):o,e))+'" data-record-type="'+d(t(null!=(o=null!=e?u(e,"node"):e)?u(o,"type"):o,e))+'">\n <alchemy-icon name="arrow-'+(null!=(o=u(l,"if").call(null!=e?e:n.nullContext||{},null!=(o=null!=e?u(e,"node"):e)?u(o,"folded"):o,{name:"if",hash:{},fn:n.program(0,r,0),inverse:n.program(1,r,0),data:r,loc:{start:{line:2,column:28},end:{line:2,column:72}}}))?o:"")+'-s"></alchemy-icon>\n</a>\n'},useData:!0})})();
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import "handlebars"
|
|
3
3
|
import "jquery"
|
|
4
4
|
import "@ungap/custom-elements"
|
|
5
|
-
import "@hotwired/turbo-rails"
|
|
5
|
+
import { Turbo } from "@hotwired/turbo-rails"
|
|
6
6
|
import "select2"
|
|
7
7
|
|
|
8
8
|
import Rails from "@rails/ujs"
|
|
@@ -49,4 +49,14 @@ Object.assign(Alchemy, {
|
|
|
49
49
|
|
|
50
50
|
Rails.start()
|
|
51
51
|
Turbo.config.forms.confirm = openConfirmDialog
|
|
52
|
-
|
|
52
|
+
document.addEventListener("turbo:load", Initializer)
|
|
53
|
+
|
|
54
|
+
// Public API for extensions
|
|
55
|
+
export { RemoteSelect } from "alchemy_admin/components/remote_select"
|
|
56
|
+
export { on } from "alchemy_admin/utils/events"
|
|
57
|
+
|
|
58
|
+
// Page-specific modules - bundled to avoid dual-loading
|
|
59
|
+
export { default as ImageCropper } from "alchemy_admin/image_cropper"
|
|
60
|
+
export { default as ImageOverlay } from "alchemy_admin/image_overlay"
|
|
61
|
+
export { default as pictureSelector } from "alchemy_admin/picture_selector"
|
|
62
|
+
export { default as NodeTree } from "alchemy_admin/node_tree"
|
|
@@ -42,33 +42,6 @@ module Alchemy
|
|
|
42
42
|
scope :recent, -> { where("#{table_name}.created_at > ?", Time.current - 24.hours).order(:created_at) }
|
|
43
43
|
scope :without_tag, -> { left_outer_joins(:taggings).where(gutentag_taggings: {id: nil}) }
|
|
44
44
|
|
|
45
|
-
# Override +Alchemy::RelatableResource#deletable+ to also exclude
|
|
46
|
-
# attachments referenced from +/attachment/:id/download+ URLs inside
|
|
47
|
-
# ingredient values (e.g. Richtext markup, Link ingredients, raw Html).
|
|
48
|
-
# Those URLs are written by the file tab of the link dialog and are
|
|
49
|
-
# not tracked via the polymorphic +related_object+ association, so the
|
|
50
|
-
# base scope cannot see them.
|
|
51
|
-
#
|
|
52
|
-
# Uses a correlated +NOT EXISTS+ subquery that builds the per-row LIKE
|
|
53
|
-
# pattern with +Arel::Nodes::Concat+, which compiles to +||+ on
|
|
54
|
-
# SQLite/PostgreSQL and +CONCAT()+ on MySQL.
|
|
55
|
-
scope :deletable, -> do
|
|
56
|
-
ingredients = Alchemy::Ingredient.arel_table
|
|
57
|
-
pattern = Arel::Nodes::Concat.new(
|
|
58
|
-
Arel::Nodes::Concat.new(
|
|
59
|
-
Arel::Nodes.build_quoted("%/attachment/"),
|
|
60
|
-
arel_table[:id]
|
|
61
|
-
),
|
|
62
|
-
Arel::Nodes.build_quoted("/download%")
|
|
63
|
-
)
|
|
64
|
-
referenced = ingredients
|
|
65
|
-
.project(1)
|
|
66
|
-
.where(ingredients[:value].matches(pattern))
|
|
67
|
-
|
|
68
|
-
where("#{table_name}.id NOT IN (#{RelatableResource::RELATED_INGREDIENTS_SUBQUERY})", type: name)
|
|
69
|
-
.where.not(referenced.exists)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
45
|
# We need to define this method here to have it available in the validations below.
|
|
73
46
|
class << self
|
|
74
47
|
# The class used to generate URLs for attachments
|
|
@@ -139,16 +112,9 @@ module Alchemy
|
|
|
139
112
|
CGI.escape(file_name.gsub(/\.#{extension}$/, "").tr(".", " "))
|
|
140
113
|
end
|
|
141
114
|
|
|
142
|
-
# Override +Alchemy::RelatableResource#deletable?+ to also consider
|
|
143
|
-
# +/attachment/:id/download+ links inside ingredient values (e.g.
|
|
144
|
-
# Richtext markup, Link ingredients, raw Html).
|
|
145
|
-
def deletable?
|
|
146
|
-
super && !referenced_in_ingredient_value?
|
|
147
|
-
end
|
|
148
|
-
|
|
149
115
|
# Checks if the attachment is restricted, because it is attached on restricted pages only
|
|
150
116
|
def restricted?
|
|
151
|
-
|
|
117
|
+
related_pages.any? && related_pages.not_restricted.blank?
|
|
152
118
|
end
|
|
153
119
|
|
|
154
120
|
# File name
|
|
@@ -212,12 +178,6 @@ module Alchemy
|
|
|
212
178
|
end
|
|
213
179
|
end
|
|
214
180
|
|
|
215
|
-
def referenced_in_ingredient_value?
|
|
216
|
-
Alchemy::Ingredient
|
|
217
|
-
.where("value LIKE ?", "%/attachment/#{id}/download%")
|
|
218
|
-
.exists?
|
|
219
|
-
end
|
|
220
|
-
|
|
221
181
|
def set_name
|
|
222
182
|
self.name ||= Alchemy.storage_adapter.file_basename(self).humanize
|
|
223
183
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module Alchemy
|
|
2
2
|
class Current < ActiveSupport::CurrentAttributes
|
|
3
|
-
attribute :preview_page, :page, :language, :site
|
|
3
|
+
attribute :preview_page, :preview_time, :page, :language, :site
|
|
4
4
|
|
|
5
5
|
def language
|
|
6
6
|
super || Language.default
|
|
@@ -10,6 +10,10 @@ module Alchemy
|
|
|
10
10
|
super || Site.first
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
+
def preview_time
|
|
14
|
+
super || Time.current
|
|
15
|
+
end
|
|
16
|
+
|
|
13
17
|
def preview_page=(page)
|
|
14
18
|
super
|
|
15
19
|
|