alchemy_cms 8.2.10 → 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/admin/resource/action.rb +6 -4
- data/app/components/alchemy/admin/resource/cell.rb +6 -4
- data/app/components/alchemy/admin/resource/table.rb +2 -14
- data/app/components/alchemy/ingredients/base_view.rb +1 -1
- data/app/components/alchemy/ingredients/headline_editor.rb +1 -1
- data/app/components/alchemy/ingredients/picture_view.rb +1 -1
- data/app/components/alchemy/ingredients/text_view.rb +2 -2
- 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/attachment_select.js +1 -1
- 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 +8 -5
- 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/page_select.js +1 -1
- 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 +10 -2
- 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/alchemy_admin.js +0 -5
- 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/publishable.rb +3 -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 +3 -12
- 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 -41
- 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 +58 -20
- 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
- data/db/migrate/20260702090038_add_index_to_elements_pages_join_table.rb +0 -14
|
@@ -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,24 +7,15 @@ 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
|
-
|
|
17
|
-
# files (not the bundle) so host engines can import a single component without
|
|
18
|
-
# loading and executing the whole admin bundle. Not preloaded, so they add no
|
|
19
|
-
# fetches to normal admin page loads (which import the bundle instead).
|
|
20
|
-
pin "alchemy_admin/components/page_select"
|
|
21
|
-
pin "alchemy_admin/components/attachment_select"
|
|
22
|
-
pin "alchemy_admin/components/element_select"
|
|
23
|
-
pin "alchemy_admin/components/tags_autocomplete"
|
|
24
|
-
pin "alchemy_admin/components/tinymce"
|
|
25
|
-
pin "alchemy_admin/components/remote_select"
|
|
26
|
-
pin "alchemy_admin/components/select"
|
|
27
|
-
pin "alchemy_admin/i18n"
|
|
18
|
+
pin "alchemy_admin/components/remote_select", to: "alchemy/alchemy_admin.min.js"
|
|
28
19
|
pin "alchemy_admin/image_cropper", to: "alchemy/alchemy_admin.min.js"
|
|
29
20
|
pin "alchemy_admin/image_overlay", to: "alchemy/alchemy_admin.min.js"
|
|
30
21
|
pin "alchemy_admin/picture_selector", 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
|
|
@@ -81,22 +81,12 @@ RSpec.shared_examples_for "being publishable" do |factory_name|
|
|
|
81
81
|
])
|
|
82
82
|
end
|
|
83
83
|
end
|
|
84
|
-
|
|
85
|
-
context "with a publication window that has not started yet" do
|
|
86
|
-
let!(:future_window) do
|
|
87
|
-
create(factory_name, public_on: Date.tomorrow, public_until: Date.tomorrow + 2.days)
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
it "does not return the record" do
|
|
91
|
-
expect(described_class.published.where(id: future_window.id)).to be_empty
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
84
|
end
|
|
95
85
|
|
|
96
86
|
describe "#scheduled?" do
|
|
97
|
-
subject {
|
|
87
|
+
subject { publishable.scheduled? }
|
|
98
88
|
|
|
99
|
-
let(:
|
|
89
|
+
let(:publishable) { build(factory_name, public_on:, public_until:) }
|
|
100
90
|
|
|
101
91
|
context "when public_on is nil" do
|
|
102
92
|
let(:public_on) { nil }
|
|
@@ -153,17 +143,17 @@ RSpec.shared_examples_for "being publishable" do |factory_name|
|
|
|
153
143
|
end
|
|
154
144
|
|
|
155
145
|
describe "#public?" do
|
|
156
|
-
subject {
|
|
146
|
+
subject { publishable.public? }
|
|
157
147
|
|
|
158
148
|
context "when public_on is not set" do
|
|
159
|
-
let(:
|
|
149
|
+
let(:publishable) { build(factory_name, public_on: nil) }
|
|
160
150
|
|
|
161
151
|
it { is_expected.to be(false) }
|
|
162
152
|
end
|
|
163
153
|
|
|
164
154
|
context "when public_on is set to past date" do
|
|
165
155
|
context "and public_until is set to nil" do
|
|
166
|
-
let(:
|
|
156
|
+
let(:publishable) do
|
|
167
157
|
build(factory_name,
|
|
168
158
|
public_on: Time.current - 2.days,
|
|
169
159
|
public_until: nil)
|
|
@@ -173,7 +163,7 @@ RSpec.shared_examples_for "being publishable" do |factory_name|
|
|
|
173
163
|
end
|
|
174
164
|
|
|
175
165
|
context "and public_until is set to future date" do
|
|
176
|
-
let(:
|
|
166
|
+
let(:publishable) do
|
|
177
167
|
build(factory_name,
|
|
178
168
|
public_on: Time.current - 2.days,
|
|
179
169
|
public_until: Time.current + 2.days)
|
|
@@ -183,7 +173,7 @@ RSpec.shared_examples_for "being publishable" do |factory_name|
|
|
|
183
173
|
end
|
|
184
174
|
|
|
185
175
|
context "and public_until is set to past date" do
|
|
186
|
-
let(:
|
|
176
|
+
let(:publishable) do
|
|
187
177
|
build(factory_name,
|
|
188
178
|
public_on: Time.current - 2.days,
|
|
189
179
|
public_until: Time.current - 1.days)
|
|
@@ -194,13 +184,13 @@ RSpec.shared_examples_for "being publishable" do |factory_name|
|
|
|
194
184
|
end
|
|
195
185
|
|
|
196
186
|
context "when public_on is set to future date" do
|
|
197
|
-
let(:
|
|
187
|
+
let(:publishable) { build(factory_name, public_on: Time.current + 2.days) }
|
|
198
188
|
|
|
199
189
|
it { is_expected.to be(false) }
|
|
200
190
|
end
|
|
201
191
|
|
|
202
192
|
context "when Current.preview_time is set" do
|
|
203
|
-
let(:
|
|
193
|
+
let(:publishable) do
|
|
204
194
|
build(factory_name,
|
|
205
195
|
public_on: Time.zone.parse("2025-06-01 00:00:00"),
|
|
206
196
|
public_until: Time.zone.parse("2025-06-30 23:59:59"))
|
|
@@ -208,54 +198,61 @@ RSpec.shared_examples_for "being publishable" do |factory_name|
|
|
|
208
198
|
|
|
209
199
|
it "uses preview_time to determine visibility" do
|
|
210
200
|
Alchemy::Current.preview_time = Time.zone.parse("2025-06-15 12:00:00")
|
|
211
|
-
expect(
|
|
201
|
+
expect(publishable.public?).to be(true)
|
|
212
202
|
end
|
|
213
203
|
|
|
214
204
|
it "returns false when preview_time is outside the public range" do
|
|
215
205
|
Alchemy::Current.preview_time = Time.zone.parse("2025-07-15 12:00:00")
|
|
216
|
-
expect(
|
|
206
|
+
expect(publishable.public?).to be(false)
|
|
217
207
|
end
|
|
218
208
|
|
|
219
209
|
it "returns false when preview_time is before public_on" do
|
|
220
210
|
Alchemy::Current.preview_time = Time.zone.parse("2025-05-15 12:00:00")
|
|
221
|
-
expect(
|
|
211
|
+
expect(publishable.public?).to be(false)
|
|
222
212
|
end
|
|
223
213
|
end
|
|
224
214
|
end
|
|
225
215
|
|
|
226
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
|
+
|
|
227
223
|
context "when public_on is nil" do
|
|
228
|
-
let(:
|
|
224
|
+
let(:public_on) { nil }
|
|
229
225
|
|
|
230
|
-
it {
|
|
226
|
+
it { is_expected.to be(false) }
|
|
231
227
|
end
|
|
232
228
|
|
|
233
229
|
context "when public_on is set and public_until is nil" do
|
|
234
|
-
let(:
|
|
230
|
+
let(:public_on) { Time.current }
|
|
235
231
|
|
|
236
|
-
it {
|
|
232
|
+
it { is_expected.to be(true) }
|
|
233
|
+
end
|
|
234
|
+
|
|
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) }
|
|
237
240
|
end
|
|
238
241
|
|
|
239
242
|
context "when public_on is set and public_until is in the past" do
|
|
240
|
-
let(:
|
|
241
|
-
|
|
242
|
-
public_on: Time.current - 2.days,
|
|
243
|
-
public_until: Time.current - 1.day)
|
|
244
|
-
end
|
|
243
|
+
let(:public_on) { Time.current - 2.days }
|
|
244
|
+
let(:public_until) { Time.current - 1.day }
|
|
245
245
|
|
|
246
|
-
it {
|
|
246
|
+
it { is_expected.to be(false) }
|
|
247
247
|
end
|
|
248
248
|
|
|
249
249
|
context "when Current.preview_time is set to a future time" do
|
|
250
|
-
let(:
|
|
251
|
-
|
|
252
|
-
public_on: Time.current - 1.day,
|
|
253
|
-
public_until: Time.current + 1.day)
|
|
254
|
-
end
|
|
250
|
+
let(:public_on) { Time.current - 1.day }
|
|
251
|
+
let(:public_until) { Time.current + 1.day }
|
|
255
252
|
|
|
256
253
|
it "uses Time.current instead of the preview_time" do
|
|
257
254
|
Alchemy::Current.preview_time = Time.current + 1.week
|
|
258
|
-
|
|
255
|
+
is_expected.to be(true)
|
|
259
256
|
end
|
|
260
257
|
end
|
|
261
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
|