alchemy_cms 8.2.7 → 8.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +4 -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/preview.min.js +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/builds/tinymce/skins/ui/alchemy/skin.min.css +1 -1
- data/app/assets/builds/tinymce/skins/ui/alchemy-dark/skin.min.css +1 -1
- data/app/assets/images/alchemy/admin/logo.svg +27 -0
- data/app/assets/images/alchemy/icons-sprite.svg +1 -1
- data/app/components/alchemy/admin/dashboard/widget.rb +40 -0
- data/app/components/alchemy/admin/dashboard/widgets/attachment_counts.rb +17 -0
- data/app/components/alchemy/admin/dashboard/widgets/element_usage.rb +37 -0
- data/app/components/alchemy/admin/dashboard/widgets/greeting.html.erb +13 -0
- data/app/components/alchemy/admin/dashboard/widgets/greeting.rb +21 -0
- data/app/components/alchemy/admin/dashboard/widgets/locked_pages.html.erb +54 -0
- data/app/components/alchemy/admin/dashboard/widgets/locked_pages.rb +20 -0
- data/app/components/alchemy/admin/dashboard/widgets/online_users.html.erb +22 -0
- data/app/components/alchemy/admin/dashboard/widgets/online_users.rb +19 -0
- data/app/components/alchemy/admin/dashboard/widgets/page_counts.rb +23 -0
- data/app/components/alchemy/admin/dashboard/widgets/page_usage.rb +46 -0
- data/app/components/alchemy/admin/dashboard/widgets/picture_counts.rb +17 -0
- data/app/components/alchemy/admin/dashboard/widgets/recent_pages.html.erb +41 -0
- data/app/components/alchemy/admin/dashboard/widgets/recent_pages.rb +16 -0
- data/app/components/alchemy/admin/dashboard/widgets/sites.html.erb +29 -0
- data/app/components/alchemy/admin/dashboard/widgets/sites.rb +15 -0
- data/app/components/alchemy/admin/dashboard/widgets/stat_widget.html.erb +23 -0
- data/app/components/alchemy/admin/dashboard/widgets/stat_widget.rb +19 -0
- data/app/components/alchemy/admin/dashboard/widgets/system_info.html.erb +32 -0
- data/app/components/alchemy/admin/dashboard/widgets/system_info.rb +37 -0
- data/app/components/alchemy/admin/dashboard/widgets/usage_widget.html.erb +42 -0
- data/app/components/alchemy/admin/dashboard/widgets/usage_widget.rb +66 -0
- data/app/components/alchemy/admin/dashboard/widgets/user_counts.rb +25 -0
- data/app/components/alchemy/admin/element_editor.html.erb +27 -20
- data/app/components/alchemy/admin/element_schedule_timestamps.rb +33 -0
- data/app/components/alchemy/admin/element_select.rb +4 -3
- data/app/components/alchemy/admin/page_node.html.erb +1 -20
- data/app/components/alchemy/admin/page_publication_fields.html.erb +30 -0
- data/app/components/alchemy/admin/page_publication_fields.rb +18 -0
- data/app/components/alchemy/admin/page_status_indicators.html.erb +29 -0
- data/app/components/alchemy/admin/page_status_indicators.rb +9 -0
- data/app/components/alchemy/admin/publish_element_button.html.erb +12 -4
- data/app/components/alchemy/ingredients/headline_editor.rb +1 -1
- data/app/controllers/alchemy/admin/dashboard/widgets_controller.rb +21 -0
- data/app/controllers/alchemy/admin/dashboard_controller.rb +3 -12
- data/app/controllers/alchemy/pages_controller.rb +5 -4
- data/app/helpers/alchemy/elements_block_helper.rb +1 -0
- data/app/javascript/alchemy_admin/components/auto_submit.js +15 -9
- data/app/javascript/alchemy_admin/components/char_counter.js +17 -7
- data/app/javascript/alchemy_admin/components/clipboard_button.js +2 -6
- data/app/javascript/alchemy_admin/components/color_select.js +13 -4
- data/app/javascript/alchemy_admin/components/datepicker.js +11 -14
- data/app/javascript/alchemy_admin/components/dialog_link.js +5 -2
- data/app/javascript/alchemy_admin/components/element_editor/delete_element_button.js +6 -3
- data/app/javascript/alchemy_admin/components/element_editor.js +45 -28
- data/app/javascript/alchemy_admin/components/element_select.js +7 -4
- data/app/javascript/alchemy_admin/components/elements_window.js +38 -31
- data/app/javascript/alchemy_admin/components/elements_window_handle.js +7 -3
- data/app/javascript/alchemy_admin/components/file_editor.js +5 -2
- data/app/javascript/alchemy_admin/components/ingredient_group.js +6 -4
- data/app/javascript/alchemy_admin/components/link_buttons/link_button.js +1 -2
- data/app/javascript/alchemy_admin/components/link_buttons/unlink_button.js +1 -2
- data/app/javascript/alchemy_admin/components/link_buttons.js +6 -2
- data/app/javascript/alchemy_admin/components/list_filter.js +44 -29
- data/app/javascript/alchemy_admin/components/message.js +22 -15
- data/app/javascript/alchemy_admin/components/overlay.js +5 -7
- data/app/javascript/alchemy_admin/components/page_publication_fields.js +38 -25
- data/app/javascript/alchemy_admin/components/picture_description_select.js +5 -2
- data/app/javascript/alchemy_admin/components/picture_editor.js +5 -10
- data/app/javascript/alchemy_admin/components/picture_thumbnail.js +4 -5
- data/app/javascript/alchemy_admin/components/preview_window.js +5 -10
- data/app/javascript/alchemy_admin/components/publish_page_button.js +2 -5
- data/app/javascript/alchemy_admin/components/remote_select.js +53 -23
- data/app/javascript/alchemy_admin/components/select.js +169 -26
- data/app/javascript/alchemy_admin/components/sortable_elements.js +1 -1
- data/app/javascript/alchemy_admin/components/spinner.js +11 -11
- data/app/javascript/alchemy_admin/components/tags_autocomplete.js +9 -1
- data/app/javascript/alchemy_admin/components/tinymce.js +16 -22
- data/app/javascript/alchemy_admin/components/uploader/file_upload.js +48 -45
- data/app/javascript/alchemy_admin/components/uploader/progress.js +70 -84
- data/app/javascript/alchemy_admin/components/uploader.js +71 -46
- data/app/javascript/alchemy_admin/dialog.js +3 -0
- data/app/javascript/alchemy_admin/hotkeys.js +0 -18
- data/app/javascript/alchemy_admin/image_cropper.js +7 -9
- data/app/javascript/alchemy_admin/initializer.js +21 -0
- data/app/javascript/alchemy_admin/utils/dispatch_page_dirty_event.js +7 -0
- data/app/javascript/tinymce/plugins/alchemy_link/index.js +9 -0
- data/app/jobs/alchemy/base_job.rb +2 -2
- data/app/jobs/alchemy/invalidate_elements_cache_job.rb +33 -0
- data/app/models/alchemy/page/page_naming.rb +28 -5
- data/app/models/alchemy/page/page_natures.rb +7 -2
- data/app/models/alchemy/page/page_scopes.rb +2 -2
- data/app/models/alchemy/page/url_path.rb +7 -2
- data/app/models/alchemy/page.rb +2 -2
- data/app/models/alchemy/page_definition.rb +1 -0
- data/app/models/alchemy/permissions.rb +1 -1
- data/app/models/concerns/alchemy/relatable_resource.rb +8 -0
- data/app/services/alchemy/page_finder.rb +88 -0
- data/app/stylesheets/alchemy/_custom-properties.scss +6 -4
- data/app/stylesheets/alchemy/_mixins.scss +1 -7
- data/app/stylesheets/alchemy/_themes.scss +13 -1
- data/app/stylesheets/alchemy/admin/_tom-select.scss +240 -0
- data/app/stylesheets/alchemy/admin/archive.scss +0 -1
- data/app/stylesheets/alchemy/admin/base.scss +0 -19
- data/app/stylesheets/alchemy/admin/dashboard.scss +395 -28
- data/app/stylesheets/alchemy/admin/elements.scss +14 -17
- data/app/stylesheets/alchemy/admin/form_fields.scss +3 -3
- data/app/stylesheets/alchemy/admin/forms.scss +107 -93
- data/app/stylesheets/alchemy/admin/icons.scss +28 -0
- data/app/stylesheets/alchemy/admin/image_library.scss +20 -10
- data/app/stylesheets/alchemy/admin/navigation.scss +4 -1
- data/app/stylesheets/alchemy/admin/popover.scss +3 -5
- data/app/stylesheets/alchemy/admin/resource_info.scss +11 -17
- data/app/stylesheets/alchemy/admin/shoelace.scss +8 -0
- data/app/stylesheets/alchemy/admin/sitemap.scss +5 -0
- data/app/stylesheets/alchemy/admin/tables.scss +32 -3
- data/app/stylesheets/alchemy/admin/toolbar.scss +0 -1
- data/app/stylesheets/alchemy/admin.scss +1 -0
- data/app/stylesheets/tinymce/skins/ui/alchemy/skin.scss +0 -4
- data/app/stylesheets/tinymce/skins/ui/alchemy-dark/skin.scss +0 -4
- data/app/types/alchemy/wildcard_url_type.rb +48 -0
- data/app/views/alchemy/_menubar.html.erb +1 -5
- data/app/views/alchemy/admin/attachments/edit.html.erb +6 -3
- data/app/views/alchemy/admin/dashboard/_dashboard.html.erb +3 -2
- data/app/views/alchemy/admin/dashboard/_footer.html.erb +22 -0
- data/app/views/alchemy/admin/dashboard/_stats.html.erb +7 -0
- data/app/views/alchemy/admin/dashboard/_top.html.erb +4 -12
- data/app/views/alchemy/admin/dashboard/_widgets.html.erb +7 -0
- data/app/views/alchemy/admin/dashboard/index.html.erb +0 -17
- data/app/views/alchemy/admin/dashboard/info.html.erb +1 -62
- data/app/views/alchemy/admin/dashboard/widgets/show.html.erb +3 -0
- data/app/views/alchemy/admin/elements/_form.html.erb +2 -1
- data/app/views/alchemy/admin/elements/_schedule.html.erb +2 -15
- data/app/views/alchemy/admin/elements/_schedule_fields.html.erb +2 -0
- data/app/views/alchemy/admin/layoutpages/edit.html.erb +6 -3
- data/app/views/alchemy/admin/nodes/_page_nodes.html.erb +10 -8
- data/app/views/alchemy/admin/pages/_form.html.erb +25 -19
- data/app/views/alchemy/admin/pages/_publication_fields.html.erb +2 -32
- data/app/views/alchemy/admin/pages/_table.html.erb +1 -18
- data/app/views/alchemy/admin/pages/configure.html.erb +2 -2
- data/app/views/alchemy/admin/pages/info.html.erb +6 -0
- data/app/views/alchemy/admin/resources/_form.html.erb +7 -4
- data/app/views/alchemy/admin/resources/edit.html.erb +3 -1
- data/app/views/alchemy/admin/resources/new.html.erb +3 -1
- data/app/views/alchemy/admin/styleguide/index.html.erb +52 -30
- data/app/views/alchemy/admin/translations/_en.js +4 -0
- data/app/views/layouts/alchemy/admin.html.erb +3 -3
- data/config/importmap.rb +2 -0
- data/config/locales/alchemy.en.yml +15 -0
- data/config/routes.rb +1 -0
- data/lib/alchemy/configuration/class_option.rb +46 -3
- data/lib/alchemy/configuration/collection_option.rb +4 -0
- data/lib/alchemy/configurations/dashboard.rb +79 -0
- data/lib/alchemy/configurations/main.rb +15 -0
- data/lib/alchemy/engine.rb +9 -3
- data/lib/alchemy/sprockets/skip_builds_compression.rb +33 -0
- data/lib/alchemy/test_support/capybara_helpers.rb +17 -0
- data/lib/alchemy/test_support/relatable_resource_examples.rb +20 -0
- data/lib/alchemy/test_support/rspec_matchers.rb +8 -0
- data/lib/alchemy/test_support/shared_publishable_examples.rb +38 -31
- data/lib/alchemy/tinymce.rb +1 -1
- data/lib/alchemy/version.rb +17 -3
- data/vendor/javascript/cropperjs.min.js +1 -1
- data/vendor/javascript/flatpickr.min.js +1 -1
- data/vendor/javascript/floating-ui.min.js +1 -0
- data/vendor/javascript/keymaster.min.js +1 -1
- data/vendor/javascript/rails-ujs.min.js +1 -1
- data/vendor/javascript/shoelace.min.js +93 -93
- data/vendor/javascript/sortable.min.js +1 -1
- data/vendor/javascript/tinymce.min.js +5 -1
- data/vendor/javascript/tom-select.min.js +1 -0
- metadata +57 -18
- data/app/javascript/alchemy_admin/components/alchemy_html_element.js +0 -129
- data/app/views/alchemy/admin/dashboard/_left_column.html.erb +0 -4
- data/app/views/alchemy/admin/dashboard/_right_column.html.erb +0 -9
- data/app/views/alchemy/admin/dashboard/widgets/_locked_pages.html.erb +0 -52
- data/app/views/alchemy/admin/dashboard/widgets/_recent_pages.html.erb +0 -34
- data/app/views/alchemy/admin/dashboard/widgets/_sites.html.erb +0 -25
- data/app/views/alchemy/admin/dashboard/widgets/_users.html.erb +0 -21
- data/app/views/alchemy/admin/languages/edit.html.erb +0 -1
- data/app/views/alchemy/admin/languages/new.html.erb +0 -1
- data/app/views/alchemy/admin/sites/edit.html.erb +0 -1
- data/app/views/alchemy/admin/sites/new.html.erb +0 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
Alchemy.translations = {
|
|
2
|
+
Add: "Add",
|
|
2
3
|
allowed_chars: "of %{count} chars",
|
|
3
4
|
cancel: "Cancel",
|
|
4
5
|
cancelled: "Cancelled",
|
|
@@ -23,6 +24,9 @@ Alchemy.translations = {
|
|
|
23
24
|
"Uploaded bytes exceed file size": "Uploaded bytes exceed file size",
|
|
24
25
|
"Abort upload": "Abort upload",
|
|
25
26
|
"Cancel all uploads": "Cancel all uploads",
|
|
27
|
+
"Clear selection": "Clear selection",
|
|
28
|
+
Remove: "Remove",
|
|
29
|
+
"No results found": "No results found",
|
|
26
30
|
None: "None",
|
|
27
31
|
"No anchors found": "No anchors found",
|
|
28
32
|
"Select a page first": "Select a page first",
|
|
@@ -45,13 +45,13 @@
|
|
|
45
45
|
</noscript>
|
|
46
46
|
<alchemy-overlay text="<%= Alchemy.t(:please_wait) %>"></alchemy-overlay>
|
|
47
47
|
<%= render "alchemy/admin/left_menu" %>
|
|
48
|
-
<% if current_alchemy_user %>
|
|
49
|
-
<%= render "alchemy/admin/top_menu", locked_pages: @locked_pages %>
|
|
50
|
-
<% end %>
|
|
51
48
|
<%= render 'alchemy/admin/partials/flash_notices' %>
|
|
52
49
|
<div id="main_content">
|
|
53
50
|
<%= yield %>
|
|
54
51
|
</div>
|
|
52
|
+
<% if current_alchemy_user %>
|
|
53
|
+
<%= render "alchemy/admin/top_menu", locked_pages: @locked_pages %>
|
|
54
|
+
<% end %>
|
|
55
55
|
<%= render 'alchemy/admin/uploader/setup' %>
|
|
56
56
|
<%= yield(:javascripts) %>
|
|
57
57
|
<% end %>
|
data/config/importmap.rb
CHANGED
|
@@ -7,10 +7,12 @@ pin "jquery", to: "jquery.min.js", preload: true
|
|
|
7
7
|
pin "keymaster", to: "keymaster.min.js", preload: true
|
|
8
8
|
pin "select2", to: "select2.min.js", preload: true
|
|
9
9
|
pin "sortablejs", to: "sortable.min.js", preload: true # @1.15.1
|
|
10
|
+
pin "@floating-ui/dom", to: "floating-ui.min.js", preload: true
|
|
10
11
|
pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
|
|
11
12
|
pin "shoelace", to: "shoelace.min.js", preload: true
|
|
12
13
|
pin "@rails/ujs", to: "rails-ujs.min.js", preload: true # @7.1.2
|
|
13
14
|
pin "tinymce", to: "tinymce.min.js", preload: true
|
|
15
|
+
pin "tom-select", to: "tom-select.min.js", preload: true
|
|
14
16
|
|
|
15
17
|
pin "alchemy_admin", to: "alchemy/alchemy_admin.min.js", preload: true
|
|
16
18
|
pin "alchemy_admin/components/remote_select", to: "alchemy/alchemy_admin.min.js"
|
|
@@ -230,6 +230,12 @@ en:
|
|
|
230
230
|
label: "Sorting"
|
|
231
231
|
by_latest: "Latest"
|
|
232
232
|
alphabetical: "A-Z"
|
|
233
|
+
dashboard:
|
|
234
|
+
widgets:
|
|
235
|
+
page_counts:
|
|
236
|
+
published: "published"
|
|
237
|
+
user_counts:
|
|
238
|
+
online: "online"
|
|
233
239
|
elements:
|
|
234
240
|
toolbar:
|
|
235
241
|
hide: Hide
|
|
@@ -571,6 +577,10 @@ en:
|
|
|
571
577
|
restricted:
|
|
572
578
|
"true": "Page is only accessible by members."
|
|
573
579
|
"false": "Page is accessible by all visitors."
|
|
580
|
+
scheduled:
|
|
581
|
+
public_on: "Page is visible from %{public_on}"
|
|
582
|
+
public_until: "Page is visible until %{public_until}"
|
|
583
|
+
"false": ""
|
|
574
584
|
page_status_titles:
|
|
575
585
|
public:
|
|
576
586
|
"true": "online"
|
|
@@ -581,6 +591,9 @@ en:
|
|
|
581
591
|
restricted:
|
|
582
592
|
"true": "restricted"
|
|
583
593
|
"false": "accessible"
|
|
594
|
+
scheduled:
|
|
595
|
+
"true": "scheduled"
|
|
596
|
+
"false": ""
|
|
584
597
|
page_status: "Status"
|
|
585
598
|
page_title: "Title"
|
|
586
599
|
page_type: "Type"
|
|
@@ -834,6 +847,8 @@ en:
|
|
|
834
847
|
base:
|
|
835
848
|
restrict_dependent_destroy:
|
|
836
849
|
has_many: "There are still %{record} attached to this page. Please remove them first."
|
|
850
|
+
page_layout:
|
|
851
|
+
conflicting_wildcard_param_key: 'has a conflicting wildcard param "%{param}" already used by the "%{conflicting_layout}" page layout'
|
|
837
852
|
descendants:
|
|
838
853
|
still_attached_to_nodes: "The following descendant pages are still attached to menu nodes: %{page_names}. Please remove them first."
|
|
839
854
|
alchemy/element:
|
data/config/routes.rb
CHANGED
|
@@ -11,6 +11,7 @@ Alchemy::Engine.routes.draw do
|
|
|
11
11
|
get "/", to: redirect("#{Alchemy.admin_path}/dashboard"), as: :admin
|
|
12
12
|
get "/dashboard", to: "admin/dashboard#index", as: :admin_dashboard
|
|
13
13
|
get "/dashboard/info", to: "admin/dashboard#info", as: :dashboard_info
|
|
14
|
+
get "/dashboard/widgets/:id", to: "admin/dashboard/widgets#show", as: :admin_dashboard_widget
|
|
14
15
|
get "/help", to: "admin/dashboard#help", as: :help
|
|
15
16
|
get "/update_check" => "admin/update_checks#show", :as => :update_check
|
|
16
17
|
get "/leave", to: "admin/base#leave", as: :leave_admin
|
|
@@ -5,11 +5,54 @@ require "alchemy/configuration/base_option"
|
|
|
5
5
|
module Alchemy
|
|
6
6
|
class Configuration
|
|
7
7
|
class ClassOption < BaseOption
|
|
8
|
-
def
|
|
9
|
-
String
|
|
8
|
+
def allowed_classes
|
|
9
|
+
[String, Array]
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
def value
|
|
12
|
+
def validate(value)
|
|
13
|
+
super
|
|
14
|
+
|
|
15
|
+
if value.is_a?(Array)
|
|
16
|
+
validate_array!(value)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def value
|
|
21
|
+
@_cached_value ||= case @value
|
|
22
|
+
when Array
|
|
23
|
+
@value[0] = @value[0]&.constantize
|
|
24
|
+
@value
|
|
25
|
+
when String
|
|
26
|
+
@value&.constantize
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def validate_array!(value)
|
|
33
|
+
@array = value
|
|
34
|
+
has_length_two! && first_value_is_string! && second_value_is_hash!
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def has_length_two!
|
|
38
|
+
return true if @array.length == 2
|
|
39
|
+
|
|
40
|
+
raise(ConfigurationError.new(name, @array, [
|
|
41
|
+
Class.new(Array) { def self.name = "an Array of length two" }
|
|
42
|
+
]))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def first_value_is_string!
|
|
46
|
+
return true if @array[0].is_a?(String)
|
|
47
|
+
|
|
48
|
+
raise(ConfigurationError.new(name, @array[0], [String]))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def second_value_is_hash!
|
|
52
|
+
return true if @array[1].is_a?(Hash)
|
|
53
|
+
|
|
54
|
+
raise(ConfigurationError.new(name, @array[1], [Hash]))
|
|
55
|
+
end
|
|
13
56
|
end
|
|
14
57
|
end
|
|
15
58
|
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemy
|
|
4
|
+
module Configurations
|
|
5
|
+
class Dashboard < Alchemy::Configuration
|
|
6
|
+
option :stats, :collection, item_type: :class, default: [
|
|
7
|
+
[
|
|
8
|
+
"Alchemy::Admin::Dashboard::Widget", {
|
|
9
|
+
id: "PageCounts",
|
|
10
|
+
style: "stat"
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
[
|
|
14
|
+
"Alchemy::Admin::Dashboard::Widget", {
|
|
15
|
+
id: "UserCounts",
|
|
16
|
+
style: "stat"
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
[
|
|
20
|
+
"Alchemy::Admin::Dashboard::Widget", {
|
|
21
|
+
id: "PictureCounts",
|
|
22
|
+
style: "stat"
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
[
|
|
26
|
+
"Alchemy::Admin::Dashboard::Widget", {
|
|
27
|
+
id: "AttachmentCounts",
|
|
28
|
+
style: "stat"
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
option :widgets, :collection, item_type: :class, default: [
|
|
34
|
+
[
|
|
35
|
+
"Alchemy::Admin::Dashboard::Widget", {
|
|
36
|
+
id: "LockedPages",
|
|
37
|
+
style: "wide"
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
[
|
|
41
|
+
"Alchemy::Admin::Dashboard::Widget", {
|
|
42
|
+
id: "RecentPages",
|
|
43
|
+
style: "wide"
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
[
|
|
47
|
+
"Alchemy::Admin::Dashboard::Widget", {
|
|
48
|
+
id: "ElementUsage",
|
|
49
|
+
style: "usage",
|
|
50
|
+
loading: "lazy"
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
[
|
|
54
|
+
"Alchemy::Admin::Dashboard::Widget", {
|
|
55
|
+
id: "PageUsage",
|
|
56
|
+
style: "usage",
|
|
57
|
+
loading: "lazy"
|
|
58
|
+
}
|
|
59
|
+
],
|
|
60
|
+
[
|
|
61
|
+
"Alchemy::Admin::Dashboard::Widget", {
|
|
62
|
+
id: "Sites",
|
|
63
|
+
loading: "lazy",
|
|
64
|
+
condition: -> { helpers.multi_site? }
|
|
65
|
+
}
|
|
66
|
+
],
|
|
67
|
+
[
|
|
68
|
+
"Alchemy::Admin::Dashboard::Widget", {
|
|
69
|
+
id: "OnlineUsers",
|
|
70
|
+
loading: "lazy",
|
|
71
|
+
condition: -> {
|
|
72
|
+
Alchemy.config.user_class.respond_to?(:logged_in)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
]
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "alchemy/configuration"
|
|
4
|
+
require "alchemy/configurations/dashboard"
|
|
4
5
|
require "alchemy/configurations/default_language"
|
|
5
6
|
require "alchemy/configurations/default_site"
|
|
6
7
|
require "alchemy/configurations/importmap"
|
|
@@ -434,6 +435,16 @@ module Alchemy
|
|
|
434
435
|
# The path to the page showing the user they're unauthorized
|
|
435
436
|
option :unauthorized_path, :string, default: "/"
|
|
436
437
|
|
|
438
|
+
# === Admin Users Path
|
|
439
|
+
#
|
|
440
|
+
# The path to the admin users list.
|
|
441
|
+
#
|
|
442
|
+
# == Example
|
|
443
|
+
#
|
|
444
|
+
# "/admin/users"
|
|
445
|
+
#
|
|
446
|
+
option :admin_users_path, :string
|
|
447
|
+
|
|
437
448
|
# === Edit User Path
|
|
438
449
|
#
|
|
439
450
|
# The path to the edit user form.
|
|
@@ -453,6 +464,10 @@ module Alchemy
|
|
|
453
464
|
# == Example
|
|
454
465
|
# Alchemy.config.abilities.add("MyCustom::Ability")
|
|
455
466
|
option :abilities, :collection, item_type: :class
|
|
467
|
+
|
|
468
|
+
# === Dashboard configuration
|
|
469
|
+
#
|
|
470
|
+
configuration :dashboard, Dashboard
|
|
456
471
|
end
|
|
457
472
|
end
|
|
458
473
|
end
|
data/lib/alchemy/engine.rb
CHANGED
|
@@ -17,7 +17,8 @@ module Alchemy
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
initializer "alchemy.assets" do |app|
|
|
20
|
-
if defined?(Sprockets)
|
|
20
|
+
if defined?(::Sprockets)
|
|
21
|
+
require_relative "sprockets/skip_builds_compression"
|
|
21
22
|
require_relative "../non_stupid_digest_assets"
|
|
22
23
|
NonStupidDigestAssets.whitelist += [/^tinymce\//]
|
|
23
24
|
app.config.assets.precompile << "alchemy_manifest.js"
|
|
@@ -25,7 +26,7 @@ module Alchemy
|
|
|
25
26
|
end
|
|
26
27
|
|
|
27
28
|
initializer "alchemy.admin_stylesheets" do |app|
|
|
28
|
-
if defined?(Sprockets)
|
|
29
|
+
if defined?(::Sprockets)
|
|
29
30
|
Alchemy.config.admin_stylesheets.each do |stylesheet|
|
|
30
31
|
app.config.assets.precompile << stylesheet
|
|
31
32
|
end
|
|
@@ -33,7 +34,7 @@ module Alchemy
|
|
|
33
34
|
end
|
|
34
35
|
|
|
35
36
|
initializer "alchemy.propshaft" do |app|
|
|
36
|
-
if defined?(Propshaft)
|
|
37
|
+
if defined?(::Propshaft)
|
|
37
38
|
if app.config.assets.server
|
|
38
39
|
# Monkey-patch Propshaft::Asset to enable access
|
|
39
40
|
# of TinyMCE assets without a hash digest.
|
|
@@ -67,6 +68,11 @@ module Alchemy
|
|
|
67
68
|
end
|
|
68
69
|
|
|
69
70
|
if app.config.importmap.sweep_cache
|
|
71
|
+
# The importmap pins resolve to the bundled files in app/assets/builds,
|
|
72
|
+
# so the cache must be swept when those are rebuilt. Without this the
|
|
73
|
+
# cached importmap keeps emitting the previous digest after a rebuild
|
|
74
|
+
# and the asset 404s until the server is restarted.
|
|
75
|
+
watch_paths << Alchemy::Engine.root.join("app/assets/builds")
|
|
70
76
|
Alchemy.importmap.cache_sweeper(watches: watch_paths)
|
|
71
77
|
ActiveSupport.on_load(:action_controller_base) do
|
|
72
78
|
before_action { Alchemy.importmap.cache_sweeper.execute_if_updated }
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
require "sprockets/sass_compressor"
|
|
5
|
+
rescue LoadError
|
|
6
|
+
# Sprockets::SassCompressor is only defined if sassc-rails is present,
|
|
7
|
+
# which is not the case in all environments (for example, when using Propshaft).
|
|
8
|
+
# In that case, we can skip prepending our patch module.
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
module Alchemy
|
|
12
|
+
module Sprockets
|
|
13
|
+
# Alchemy ships pre-built, already-minified admin CSS in +app/assets/builds+
|
|
14
|
+
# that uses modern CSS syntax (relative colors, +oklch()+ and friends). The
|
|
15
|
+
# legacy SassC/libSass +css_compressor+ — the Sprockets default whenever
|
|
16
|
+
# +sassc-rails+ is present (for example through Solidus) — re-parses every
|
|
17
|
+
# +text/css+ asset as SCSS and raises on that syntax. These files are already
|
|
18
|
+
# minified, so leave them untouched; all other stylesheets still get
|
|
19
|
+
# compressed as before.
|
|
20
|
+
module SkipBuildsCompression
|
|
21
|
+
def call(input)
|
|
22
|
+
builds_path = Alchemy::Engine.root.join("app/assets/builds").to_s
|
|
23
|
+
if input[:filename].to_s.start_with?(builds_path)
|
|
24
|
+
{data: input[:data]}
|
|
25
|
+
else
|
|
26
|
+
super
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
::Sprockets::SassCompressor.singleton_class.prepend(self)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -25,6 +25,23 @@ module Alchemy
|
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
# Tom Select capybara helper
|
|
29
|
+
def tom_select(value, options)
|
|
30
|
+
label = find_label_by_text(options[:from])
|
|
31
|
+
|
|
32
|
+
within label.find(:xpath, "..") do
|
|
33
|
+
find(".ts-control").click
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The dropdown is appended to the body, so search the whole page for it.
|
|
37
|
+
within_entire_page do
|
|
38
|
+
find(
|
|
39
|
+
".ts-dropdown .option",
|
|
40
|
+
text: /#{Regexp.escape(value)}/i, match: :prefer_exact
|
|
41
|
+
).click
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
28
45
|
def select2_search(value, options)
|
|
29
46
|
if options[:from]
|
|
30
47
|
label = find_label_by_text(options[:from])
|
|
@@ -55,4 +55,24 @@ RSpec.shared_examples_for "a relatable resource" do |args|
|
|
|
55
55
|
it { is_expected.to be(true) }
|
|
56
56
|
end
|
|
57
57
|
end
|
|
58
|
+
|
|
59
|
+
describe "after_touch" do
|
|
60
|
+
let(:related_object) { create(:"alchemy_#{args[:resource_name]}") }
|
|
61
|
+
|
|
62
|
+
context "when related ingredients exist" do
|
|
63
|
+
let!(:ingredient) { create(:"alchemy_ingredient_#{args[:ingredient_type]}", related_object:) }
|
|
64
|
+
|
|
65
|
+
it "enqueues InvalidateElementsCacheJob" do
|
|
66
|
+
expect {
|
|
67
|
+
related_object.touch
|
|
68
|
+
}.to have_enqueued_job(Alchemy::InvalidateElementsCacheJob).with(described_class.name, related_object.id)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
context "when no related ingredients exist" do
|
|
73
|
+
it "does not enqueue InvalidateElementsCacheJob" do
|
|
74
|
+
expect { related_object.touch }.to_not have_enqueued_job(Alchemy::InvalidateElementsCacheJob)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
58
78
|
end
|
|
@@ -12,3 +12,11 @@ RSpec::Matchers.define :include_language_information_for do |expected|
|
|
|
12
12
|
actual[:alchemy_language_id] == expected.id
|
|
13
13
|
end
|
|
14
14
|
end
|
|
15
|
+
|
|
16
|
+
# This matcher checks for the presence of an alchemy-select component with a given label.
|
|
17
|
+
RSpec::Matchers.define :have_alchemy_select do |expected|
|
|
18
|
+
match do |session|
|
|
19
|
+
label = session.find(:css, "label", exact_text: expected)
|
|
20
|
+
label.has_sibling?(%(select[is="alchemy-select"]), visible: :all)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
RSpec.shared_examples_for "being publishable" do |factory_name|
|
|
4
4
|
describe "validations" do
|
|
5
5
|
context "when public_until is older than public_on" do
|
|
6
|
-
let(:
|
|
6
|
+
let(:publishable) do
|
|
7
7
|
build(
|
|
8
8
|
factory_name,
|
|
9
9
|
public_on: Time.current,
|
|
@@ -12,8 +12,8 @@ RSpec.shared_examples_for "being publishable" do |factory_name|
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
it "is not valid" do
|
|
15
|
-
expect(
|
|
16
|
-
expect(
|
|
15
|
+
expect(publishable).not_to be_valid
|
|
16
|
+
expect(publishable.errors[:public_until]).to include(
|
|
17
17
|
I18n.t("errors.attributes.public_until.must_be_after_public_on")
|
|
18
18
|
)
|
|
19
19
|
end
|
|
@@ -84,9 +84,9 @@ RSpec.shared_examples_for "being publishable" do |factory_name|
|
|
|
84
84
|
end
|
|
85
85
|
|
|
86
86
|
describe "#scheduled?" do
|
|
87
|
-
subject {
|
|
87
|
+
subject { publishable.scheduled? }
|
|
88
88
|
|
|
89
|
-
let(:
|
|
89
|
+
let(:publishable) { build(factory_name, public_on:, public_until:) }
|
|
90
90
|
|
|
91
91
|
context "when public_on is nil" do
|
|
92
92
|
let(:public_on) { nil }
|
|
@@ -143,17 +143,17 @@ RSpec.shared_examples_for "being publishable" do |factory_name|
|
|
|
143
143
|
end
|
|
144
144
|
|
|
145
145
|
describe "#public?" do
|
|
146
|
-
subject {
|
|
146
|
+
subject { publishable.public? }
|
|
147
147
|
|
|
148
148
|
context "when public_on is not set" do
|
|
149
|
-
let(:
|
|
149
|
+
let(:publishable) { build(factory_name, public_on: nil) }
|
|
150
150
|
|
|
151
151
|
it { is_expected.to be(false) }
|
|
152
152
|
end
|
|
153
153
|
|
|
154
154
|
context "when public_on is set to past date" do
|
|
155
155
|
context "and public_until is set to nil" do
|
|
156
|
-
let(:
|
|
156
|
+
let(:publishable) do
|
|
157
157
|
build(factory_name,
|
|
158
158
|
public_on: Time.current - 2.days,
|
|
159
159
|
public_until: nil)
|
|
@@ -163,7 +163,7 @@ RSpec.shared_examples_for "being publishable" do |factory_name|
|
|
|
163
163
|
end
|
|
164
164
|
|
|
165
165
|
context "and public_until is set to future date" do
|
|
166
|
-
let(:
|
|
166
|
+
let(:publishable) do
|
|
167
167
|
build(factory_name,
|
|
168
168
|
public_on: Time.current - 2.days,
|
|
169
169
|
public_until: Time.current + 2.days)
|
|
@@ -173,7 +173,7 @@ RSpec.shared_examples_for "being publishable" do |factory_name|
|
|
|
173
173
|
end
|
|
174
174
|
|
|
175
175
|
context "and public_until is set to past date" do
|
|
176
|
-
let(:
|
|
176
|
+
let(:publishable) do
|
|
177
177
|
build(factory_name,
|
|
178
178
|
public_on: Time.current - 2.days,
|
|
179
179
|
public_until: Time.current - 1.days)
|
|
@@ -184,13 +184,13 @@ RSpec.shared_examples_for "being publishable" do |factory_name|
|
|
|
184
184
|
end
|
|
185
185
|
|
|
186
186
|
context "when public_on is set to future date" do
|
|
187
|
-
let(:
|
|
187
|
+
let(:publishable) { build(factory_name, public_on: Time.current + 2.days) }
|
|
188
188
|
|
|
189
189
|
it { is_expected.to be(false) }
|
|
190
190
|
end
|
|
191
191
|
|
|
192
192
|
context "when Current.preview_time is set" do
|
|
193
|
-
let(:
|
|
193
|
+
let(:publishable) do
|
|
194
194
|
build(factory_name,
|
|
195
195
|
public_on: Time.zone.parse("2025-06-01 00:00:00"),
|
|
196
196
|
public_until: Time.zone.parse("2025-06-30 23:59:59"))
|
|
@@ -198,54 +198,61 @@ RSpec.shared_examples_for "being publishable" do |factory_name|
|
|
|
198
198
|
|
|
199
199
|
it "uses preview_time to determine visibility" do
|
|
200
200
|
Alchemy::Current.preview_time = Time.zone.parse("2025-06-15 12:00:00")
|
|
201
|
-
expect(
|
|
201
|
+
expect(publishable.public?).to be(true)
|
|
202
202
|
end
|
|
203
203
|
|
|
204
204
|
it "returns false when preview_time is outside the public range" do
|
|
205
205
|
Alchemy::Current.preview_time = Time.zone.parse("2025-07-15 12:00:00")
|
|
206
|
-
expect(
|
|
206
|
+
expect(publishable.public?).to be(false)
|
|
207
207
|
end
|
|
208
208
|
|
|
209
209
|
it "returns false when preview_time is before public_on" do
|
|
210
210
|
Alchemy::Current.preview_time = Time.zone.parse("2025-05-15 12:00:00")
|
|
211
|
-
expect(
|
|
211
|
+
expect(publishable.public?).to be(false)
|
|
212
212
|
end
|
|
213
213
|
end
|
|
214
214
|
end
|
|
215
215
|
|
|
216
216
|
describe "#publishable?" do
|
|
217
|
+
let(:publishable) { build(factory_name, public_on:, public_until:) }
|
|
218
|
+
let(:public_on) { nil }
|
|
219
|
+
let(:public_until) { nil }
|
|
220
|
+
|
|
221
|
+
subject { publishable.publishable? }
|
|
222
|
+
|
|
217
223
|
context "when public_on is nil" do
|
|
218
|
-
let(:
|
|
224
|
+
let(:public_on) { nil }
|
|
219
225
|
|
|
220
|
-
it {
|
|
226
|
+
it { is_expected.to be(false) }
|
|
221
227
|
end
|
|
222
228
|
|
|
223
229
|
context "when public_on is set and public_until is nil" do
|
|
224
|
-
let(:
|
|
230
|
+
let(:public_on) { Time.current }
|
|
231
|
+
|
|
232
|
+
it { is_expected.to be(true) }
|
|
233
|
+
end
|
|
225
234
|
|
|
226
|
-
|
|
235
|
+
context "when public_on is set and public_until is in the future" do
|
|
236
|
+
let(:public_on) { Time.current }
|
|
237
|
+
let(:public_until) { Time.current + 1.day }
|
|
238
|
+
|
|
239
|
+
it { is_expected.to be(true) }
|
|
227
240
|
end
|
|
228
241
|
|
|
229
242
|
context "when public_on is set and public_until is in the past" do
|
|
230
|
-
let(:
|
|
231
|
-
|
|
232
|
-
public_on: Time.current - 2.days,
|
|
233
|
-
public_until: Time.current - 1.day)
|
|
234
|
-
end
|
|
243
|
+
let(:public_on) { Time.current - 2.days }
|
|
244
|
+
let(:public_until) { Time.current - 1.day }
|
|
235
245
|
|
|
236
|
-
it {
|
|
246
|
+
it { is_expected.to be(false) }
|
|
237
247
|
end
|
|
238
248
|
|
|
239
249
|
context "when Current.preview_time is set to a future time" do
|
|
240
|
-
let(:
|
|
241
|
-
|
|
242
|
-
public_on: Time.current - 1.day,
|
|
243
|
-
public_until: Time.current + 1.day)
|
|
244
|
-
end
|
|
250
|
+
let(:public_on) { Time.current - 1.day }
|
|
251
|
+
let(:public_until) { Time.current + 1.day }
|
|
245
252
|
|
|
246
253
|
it "uses Time.current instead of the preview_time" do
|
|
247
254
|
Alchemy::Current.preview_time = Time.current + 1.week
|
|
248
|
-
|
|
255
|
+
is_expected.to be(true)
|
|
249
256
|
end
|
|
250
257
|
end
|
|
251
258
|
end
|
data/lib/alchemy/tinymce.rb
CHANGED
data/lib/alchemy/version.rb
CHANGED
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Alchemy
|
|
4
|
-
|
|
4
|
+
extend self
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
VERSION = "8.3.0"
|
|
7
|
+
|
|
8
|
+
def version
|
|
7
9
|
VERSION
|
|
8
10
|
end
|
|
9
11
|
|
|
10
|
-
def
|
|
12
|
+
def gem_version
|
|
11
13
|
Gem::Version.new(VERSION)
|
|
12
14
|
end
|
|
15
|
+
|
|
16
|
+
def git_revision_info
|
|
17
|
+
source = Bundler.locked_gems.sources.find { _1.name == "alchemy_cms" }
|
|
18
|
+
return unless source.respond_to?(:revision)
|
|
19
|
+
|
|
20
|
+
{
|
|
21
|
+
revision: source.revision,
|
|
22
|
+
branch: source.branch
|
|
23
|
+
}
|
|
24
|
+
rescue
|
|
25
|
+
nil
|
|
26
|
+
end
|
|
13
27
|
end
|