alchemy_cms 8.1.9 → 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 +2 -0
- data/app/assets/builds/alchemy/alchemy_admin.min.js.map +1 -0
- 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/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/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/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 -1
- 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 +16 -2
- 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/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/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/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/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/configuration.rb +2 -0
- data/lib/alchemy/configurations/main.rb +13 -1
- data/lib/alchemy/tasks/tidy.rb +6 -7
- 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 +35 -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
|
|
@@ -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"
|
|
@@ -114,7 +114,7 @@ module Alchemy
|
|
|
114
114
|
|
|
115
115
|
# Checks if the attachment is restricted, because it is attached on restricted pages only
|
|
116
116
|
def restricted?
|
|
117
|
-
|
|
117
|
+
related_pages.any? && related_pages.not_restricted.blank?
|
|
118
118
|
end
|
|
119
119
|
|
|
120
120
|
# File name
|
|
@@ -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
|
|
|
@@ -75,11 +75,19 @@ module Alchemy
|
|
|
75
75
|
# This is used to re-initialize the TinyMCE editor in the element editor.
|
|
76
76
|
#
|
|
77
77
|
def richtext_ingredients_ids
|
|
78
|
-
ids = ingredients.
|
|
79
|
-
|
|
78
|
+
ids = ingredients.filter_map { |i| i.id if i.has_tinymce? }
|
|
79
|
+
|
|
80
|
+
# Use preloaded association if available, otherwise query
|
|
81
|
+
expanded_nested_elements = if association(:all_nested_elements).loaded?
|
|
82
|
+
all_nested_elements.select(&:expanded?)
|
|
83
|
+
else
|
|
84
|
+
nested_elements.expanded
|
|
85
|
+
end
|
|
86
|
+
|
|
80
87
|
if expanded_nested_elements.present?
|
|
81
|
-
ids += expanded_nested_elements.
|
|
88
|
+
ids += expanded_nested_elements.map(&:richtext_ingredients_ids)
|
|
82
89
|
end
|
|
90
|
+
|
|
83
91
|
ids.flatten
|
|
84
92
|
end
|
|
85
93
|
|
|
@@ -37,6 +37,8 @@ module Alchemy
|
|
|
37
37
|
include Alchemy::Taggable
|
|
38
38
|
include Publishable
|
|
39
39
|
|
|
40
|
+
attr_accessor :skip_ingredient_validations
|
|
41
|
+
|
|
40
42
|
FORBIDDEN_DEFINITION_ATTRIBUTES = [
|
|
41
43
|
"amount",
|
|
42
44
|
"autogenerate",
|
|
@@ -96,6 +98,7 @@ module Alchemy
|
|
|
96
98
|
|
|
97
99
|
validates_presence_of :name, on: :create
|
|
98
100
|
validates_format_of :name, on: :create, with: NAME_REGEXP
|
|
101
|
+
validate :validate_same_page_version_as_parent
|
|
99
102
|
|
|
100
103
|
after_initialize :set_default_public_on, if: :new_record?
|
|
101
104
|
|
|
@@ -326,6 +329,13 @@ module Alchemy
|
|
|
326
329
|
self.public_on ||= Time.current
|
|
327
330
|
end
|
|
328
331
|
|
|
332
|
+
def validate_same_page_version_as_parent
|
|
333
|
+
return unless parent_element
|
|
334
|
+
return if page_version_id == parent_element.page_version_id
|
|
335
|
+
|
|
336
|
+
errors.add(:page_version_id, :must_match_parent)
|
|
337
|
+
end
|
|
338
|
+
|
|
329
339
|
def generate_nested_elements
|
|
330
340
|
definition.autogenerate.each do |nestable_element|
|
|
331
341
|
if nestable_elements.include?(nestable_element)
|
|
@@ -5,8 +5,7 @@ module Alchemy
|
|
|
5
5
|
# A text value from a select box
|
|
6
6
|
#
|
|
7
7
|
class Select < Alchemy::Ingredient
|
|
8
|
-
allow_settings %i[display_inline select_values]
|
|
9
|
-
allow_settings %i[display_inline select_values multiple]
|
|
8
|
+
allow_settings %i[allow_clear display_inline select_values multiple]
|
|
10
9
|
|
|
11
10
|
serialize :value, coder: JSON
|
|
12
11
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Alchemy
|
|
2
|
+
# Generates an ETag for a page.
|
|
3
|
+
#
|
|
4
|
+
# By default, it uses the page's id and the ids of its published elements.
|
|
5
|
+
# You can customize this by providing your own generator in the configuration.
|
|
6
|
+
class Page::EtagGenerator
|
|
7
|
+
attr_reader :page
|
|
8
|
+
|
|
9
|
+
# @param page [Alchemy::Page] The page for which to generate the ETag.
|
|
10
|
+
def initialize(page)
|
|
11
|
+
@page = page
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @return [Array<Object>]
|
|
15
|
+
# @param args [Array<Object>] Additional arguments that can be used in the ETag generation.
|
|
16
|
+
def call(*args)
|
|
17
|
+
elements_cache_key = page.public_version&.elements&.published&.order(:id)&.pluck(:id)
|
|
18
|
+
[page, elements_cache_key, *args]
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|