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
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemy
|
|
4
|
+
module Admin
|
|
5
|
+
module Dashboard
|
|
6
|
+
class Widget < ViewComponent::Base
|
|
7
|
+
delegate :alchemy, to: :helpers
|
|
8
|
+
|
|
9
|
+
erb_template <<~ERB
|
|
10
|
+
<div class="widget <%= @style %>">
|
|
11
|
+
<turbo-frame id="<%= @id %>" src="<%= url %>" loading="<%= @loading %>">
|
|
12
|
+
<alchemy-spinner size="small"></alchemy-spinner>
|
|
13
|
+
</turbo-frame>
|
|
14
|
+
</div>
|
|
15
|
+
ERB
|
|
16
|
+
|
|
17
|
+
def initialize(id:, loading: "eager", style: "default", condition: nil)
|
|
18
|
+
@id = id
|
|
19
|
+
@loading = loading
|
|
20
|
+
@style = style
|
|
21
|
+
@condition = condition
|
|
22
|
+
|
|
23
|
+
if condition && !condition.respond_to?(:call)
|
|
24
|
+
raise ArgumentError, ":condition argument must be a proc or lambda"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def render?
|
|
31
|
+
return true if @condition.nil?
|
|
32
|
+
|
|
33
|
+
instance_exec(&@condition)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def url = alchemy.admin_dashboard_widget_path(id: @id)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Alchemy
|
|
2
|
+
module Admin
|
|
3
|
+
module Dashboard
|
|
4
|
+
module Widgets
|
|
5
|
+
class AttachmentCounts < StatWidget
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def title = Alchemy::Attachment.model_name.human(count: :many)
|
|
9
|
+
def link = alchemy.admin_attachments_path
|
|
10
|
+
def icon = "file-copy-2"
|
|
11
|
+
def count = Alchemy::Attachment.count
|
|
12
|
+
def infos = number_to_human_size(Alchemy::Attachment.sum(&:file_size))
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Alchemy
|
|
2
|
+
module Admin
|
|
3
|
+
module Dashboard
|
|
4
|
+
module Widgets
|
|
5
|
+
class ElementUsage < UsageWidget
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def header_text(total:)
|
|
9
|
+
Alchemy.t(:element_usage, total:)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def definitions
|
|
13
|
+
Alchemy::ElementDefinition.all
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def public_counts
|
|
17
|
+
@public_counts ||= Alchemy::Element.published.group(:name).count
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def draft_counts
|
|
21
|
+
@draft_counts ||= Alchemy::Element
|
|
22
|
+
.where("alchemy_elements.public_on IS NULL OR alchemy_elements.public_until <= ?", Time.current)
|
|
23
|
+
.group(:name).count
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def entry_label(entry)
|
|
27
|
+
Alchemy::Element.display_name_for(entry.name)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def entry_icon(entry)
|
|
31
|
+
entry.definition.icon_file
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<div class="greeting">
|
|
2
|
+
<h1>
|
|
3
|
+
<%= Alchemy.t(@first_time ? :welcome_note : :welcome_back_note, name: "") %>
|
|
4
|
+
<span class="user-name">
|
|
5
|
+
<%= @user.alchemy_display_name %>
|
|
6
|
+
</span>
|
|
7
|
+
</h1>
|
|
8
|
+
<% if @last_sign_at %>
|
|
9
|
+
<p>
|
|
10
|
+
<%= Alchemy.t('Your last login was on', time: l(@last_sign_at, format: :'alchemy.default')) %>
|
|
11
|
+
</p>
|
|
12
|
+
<% end %>
|
|
13
|
+
</div>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Alchemy
|
|
2
|
+
module Admin
|
|
3
|
+
module Dashboard
|
|
4
|
+
module Widgets
|
|
5
|
+
class Greeting < ViewComponent::Base
|
|
6
|
+
def initialize(user:)
|
|
7
|
+
@user = user
|
|
8
|
+
if user.respond_to?(:sign_in_count) && user.respond_to?(:last_sign_in_at)
|
|
9
|
+
@last_sign_at = user.last_sign_in_at
|
|
10
|
+
@first_time = user.sign_in_count == 1 && @last_sign_at.nil?
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def render?
|
|
15
|
+
@user.present?
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<div class="widget-body">
|
|
2
|
+
<header>
|
|
3
|
+
<%= Alchemy.t('Currently locked pages') %>
|
|
4
|
+
</header>
|
|
5
|
+
<% if @all_locked_pages.blank? %>
|
|
6
|
+
<alchemy-message type="info">
|
|
7
|
+
<%= Alchemy.t('no pages') %>
|
|
8
|
+
</alchemy-message>
|
|
9
|
+
<% else %>
|
|
10
|
+
<div class="table-scroll">
|
|
11
|
+
<table class="list">
|
|
12
|
+
<% @all_locked_pages.each do |page| %>
|
|
13
|
+
<tr>
|
|
14
|
+
<td class="page_name">
|
|
15
|
+
<span class="label-with-icon">
|
|
16
|
+
<%= render_icon("file-edit") %>
|
|
17
|
+
<%= link_to_if current_alchemy_user.id == page.locked_by,
|
|
18
|
+
page.name,
|
|
19
|
+
alchemy.edit_admin_page_path(page),
|
|
20
|
+
data: {turbo_frame: "_top"} %>
|
|
21
|
+
</span>
|
|
22
|
+
</td>
|
|
23
|
+
<% if multi_site? %>
|
|
24
|
+
<td class="site_name">
|
|
25
|
+
<small><%= page.site_name %></small>
|
|
26
|
+
</td>
|
|
27
|
+
<% end %>
|
|
28
|
+
<% if page.site_languages.many? %>
|
|
29
|
+
<td class="locale">
|
|
30
|
+
<small><%= page.language.code.upcase %></small>
|
|
31
|
+
</td>
|
|
32
|
+
<% end %>
|
|
33
|
+
<td class="name">
|
|
34
|
+
<small>
|
|
35
|
+
<% if current_alchemy_user.id == page.locked_by %>
|
|
36
|
+
<%= Alchemy.t(:me) %>
|
|
37
|
+
<% else %>
|
|
38
|
+
<%= page.locker_name %>
|
|
39
|
+
<% end %>
|
|
40
|
+
</small>
|
|
41
|
+
</td>
|
|
42
|
+
<td class="button">
|
|
43
|
+
<% if (current_alchemy_user.id == page.locked_by) || can?(:manage, Alchemy::Current.site) %>
|
|
44
|
+
<%= button_to alchemy.unlock_admin_page_path(page, redirect_to: alchemy.admin_dashboard_path), class: "icon_button small", title: Alchemy.t(:explain_unlocking) do %>
|
|
45
|
+
<%= render_icon(:close, size: '1x') %>
|
|
46
|
+
<% end %>
|
|
47
|
+
<% end %>
|
|
48
|
+
</td>
|
|
49
|
+
</tr>
|
|
50
|
+
<% end %>
|
|
51
|
+
</table>
|
|
52
|
+
</div>
|
|
53
|
+
<% end %>
|
|
54
|
+
</div>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Alchemy
|
|
2
|
+
module Admin
|
|
3
|
+
module Dashboard
|
|
4
|
+
module Widgets
|
|
5
|
+
class LockedPages < ViewComponent::Base
|
|
6
|
+
delegate :alchemy, :can?, :current_alchemy_user, :render_icon, :multi_site?,
|
|
7
|
+
to: :helpers
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@all_locked_pages = Page.locked
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def colspan = multi_site? ? 5 : 4
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<div class="widget-body">
|
|
2
|
+
<header>
|
|
3
|
+
<%= Alchemy.t('Who else is online') %>
|
|
4
|
+
<aside><%= online_users&.count %></aside>
|
|
5
|
+
</header>
|
|
6
|
+
<% if online_users.blank? %>
|
|
7
|
+
<alchemy-message type="info">
|
|
8
|
+
<%= Alchemy.t('no users') %>
|
|
9
|
+
</alchemy-message>
|
|
10
|
+
<% else %>
|
|
11
|
+
<table class="list">
|
|
12
|
+
<% online_users.each do |user| %>
|
|
13
|
+
<tr>
|
|
14
|
+
<td class="name"><%= user.name %></td>
|
|
15
|
+
<td class="right">
|
|
16
|
+
<small><%= user.human_roles_string %></small>
|
|
17
|
+
</td>
|
|
18
|
+
</tr>
|
|
19
|
+
<% end %>
|
|
20
|
+
</table>
|
|
21
|
+
<% end %>
|
|
22
|
+
</div>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Alchemy
|
|
2
|
+
module Admin
|
|
3
|
+
module Dashboard
|
|
4
|
+
module Widgets
|
|
5
|
+
class OnlineUsers < ViewComponent::Base
|
|
6
|
+
delegate :current_alchemy_user, to: :helpers
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def online_users
|
|
11
|
+
if Alchemy.config.user_class.respond_to?(:logged_in)
|
|
12
|
+
Alchemy.config.user_class.logged_in.to_a - [current_alchemy_user]
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Alchemy
|
|
2
|
+
module Admin
|
|
3
|
+
module Dashboard
|
|
4
|
+
module Widgets
|
|
5
|
+
class PageCounts < StatWidget
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def link = alchemy.admin_pages_path
|
|
9
|
+
def icon = "pages"
|
|
10
|
+
def title = Alchemy::Page.model_name.human(count: :many)
|
|
11
|
+
def count = Alchemy::Page.count
|
|
12
|
+
|
|
13
|
+
def infos
|
|
14
|
+
safe_join([
|
|
15
|
+
number_with_delimiter(Alchemy::Page.published.count),
|
|
16
|
+
t(".published")
|
|
17
|
+
], " ")
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module Alchemy
|
|
2
|
+
module Admin
|
|
3
|
+
module Dashboard
|
|
4
|
+
module Widgets
|
|
5
|
+
class PageUsage < UsageWidget
|
|
6
|
+
delegate :alchemy, to: :helpers
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def header_text(total:)
|
|
11
|
+
Alchemy.t(:page_usage, total:)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def definitions
|
|
15
|
+
Alchemy::PageDefinition.all
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def public_counts
|
|
19
|
+
@public_counts ||= Alchemy::Page.published.group(:page_layout).count
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def draft_counts
|
|
23
|
+
@draft_counts ||= Alchemy::Page
|
|
24
|
+
.where.not(id: Alchemy::Page.published.select(:id))
|
|
25
|
+
.where.not(id: Alchemy::PageVersion.where("public_on > ?", Time.current).select(:page_id))
|
|
26
|
+
.group(:page_layout).count
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def entry_label(entry)
|
|
30
|
+
link_to entry.definition.human_name,
|
|
31
|
+
alchemy.admin_pages_path(view: "list", q: {by_page_layout: entry.name}),
|
|
32
|
+
data: {turbo_frame: "_top"}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def tooltip_content(entry)
|
|
36
|
+
"#{entry.definition.human_name}: #{entry.public_count} #{Alchemy.t(:published)}, #{entry.draft_count} #{Alchemy.t(:draft)}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def entry_icon(entry)
|
|
40
|
+
helpers.content_tag(:"alchemy-icon", "", name: "file")
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Alchemy
|
|
2
|
+
module Admin
|
|
3
|
+
module Dashboard
|
|
4
|
+
module Widgets
|
|
5
|
+
class PictureCounts < StatWidget
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def title = Alchemy::Picture.model_name.human(count: :many)
|
|
9
|
+
def link = alchemy.admin_pictures_path
|
|
10
|
+
def icon = "multi-image"
|
|
11
|
+
def count = Alchemy::Picture.count
|
|
12
|
+
def infos = number_to_human_size(Alchemy::Picture.sum(&:image_file_size))
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<div class="widget-body">
|
|
2
|
+
<header>
|
|
3
|
+
<%= Alchemy.t('Your last updated pages') %>
|
|
4
|
+
</header>
|
|
5
|
+
<% if last_edited_pages.blank? %>
|
|
6
|
+
<alchemy-message type="info">
|
|
7
|
+
<%= Alchemy.t('no pages') %>
|
|
8
|
+
</alchemy-message>
|
|
9
|
+
<% else %>
|
|
10
|
+
<div class="table-scroll">
|
|
11
|
+
<table class="list">
|
|
12
|
+
<% last_edited_pages.each do |page| %>
|
|
13
|
+
<tr>
|
|
14
|
+
<td class="page_name">
|
|
15
|
+
<span class="label-with-icon">
|
|
16
|
+
<%= render_icon(page.locked? ? "file-edit" : "file") %>
|
|
17
|
+
<%= link_to_if (page.locked_by.blank? || page.locked_by == user.id),
|
|
18
|
+
page.name,
|
|
19
|
+
alchemy.edit_admin_page_path(page),
|
|
20
|
+
data: {turbo_frame: "_top"} %>
|
|
21
|
+
</span>
|
|
22
|
+
</td>
|
|
23
|
+
<% if multi_site? %>
|
|
24
|
+
<td class="site_name">
|
|
25
|
+
<small><%= page.site_name %></small>
|
|
26
|
+
</td>
|
|
27
|
+
<% end %>
|
|
28
|
+
<% if page.site_languages.many? %>
|
|
29
|
+
<td class="locale">
|
|
30
|
+
<small><%= page.language.code.upcase %></small>
|
|
31
|
+
</td>
|
|
32
|
+
<% end %>
|
|
33
|
+
<td class="right">
|
|
34
|
+
<small><%= l(page.updated_at, format: :'alchemy.short_datetime') %></small>
|
|
35
|
+
</td>
|
|
36
|
+
</tr>
|
|
37
|
+
<% end %>
|
|
38
|
+
</table>
|
|
39
|
+
</div>
|
|
40
|
+
<% end %>
|
|
41
|
+
</div>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Alchemy
|
|
2
|
+
module Admin
|
|
3
|
+
module Dashboard
|
|
4
|
+
module Widgets
|
|
5
|
+
class RecentPages < ViewComponent::Base
|
|
6
|
+
delegate :alchemy, :current_alchemy_user, :render_icon, :multi_site?, to: :helpers
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def user = current_alchemy_user
|
|
11
|
+
def last_edited_pages = Page.all_last_edited_from(user).limit(5)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<div class="widget-body">
|
|
2
|
+
<header>
|
|
3
|
+
<%= Alchemy::Site.model_name.human(count: 2) %>
|
|
4
|
+
<aside><%= Alchemy::Site.count %></aside>
|
|
5
|
+
</header>
|
|
6
|
+
<table class="list">
|
|
7
|
+
<% sites.each do |site| %>
|
|
8
|
+
<tr>
|
|
9
|
+
<td>
|
|
10
|
+
<span class="label-with-icon">
|
|
11
|
+
<alchemy-icon name="global"></alchemy-icon>
|
|
12
|
+
<% if alchemy.respond_to?(:login_url) %>
|
|
13
|
+
<%= link_to_unless site == Alchemy::Current.site,
|
|
14
|
+
site.name,
|
|
15
|
+
alchemy.login_url(
|
|
16
|
+
protocol: 'http://',
|
|
17
|
+
host: site.host,
|
|
18
|
+
user: {login: current_alchemy_user.try(:login)}
|
|
19
|
+
), target: '_blank' %>
|
|
20
|
+
<% else %>
|
|
21
|
+
<%= site.name %>
|
|
22
|
+
<% end %>
|
|
23
|
+
</span>
|
|
24
|
+
</td>
|
|
25
|
+
<td class="right"><small><%= site.host %></small></td>
|
|
26
|
+
</tr>
|
|
27
|
+
<% end %>
|
|
28
|
+
</table>
|
|
29
|
+
</div>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<div class="widget-body">
|
|
2
|
+
<div class="name">
|
|
3
|
+
<% label = capture do %>
|
|
4
|
+
<% if icon %>
|
|
5
|
+
<alchemy-icon name="<%= icon %>"></alchemy-icon>
|
|
6
|
+
<% end %>
|
|
7
|
+
<%= title %>
|
|
8
|
+
<% end %>
|
|
9
|
+
<% if link %>
|
|
10
|
+
<%= link_to label, link, class: "label-with-icon", data: {turbo_frame: "_top"} %>
|
|
11
|
+
<% else %>
|
|
12
|
+
<span class="label-with-icon"><%= label %></span>
|
|
13
|
+
<% end %>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="count">
|
|
16
|
+
<%= number_with_delimiter(count) %>
|
|
17
|
+
</div>
|
|
18
|
+
<% if infos %>
|
|
19
|
+
<div class="infos">
|
|
20
|
+
<%= infos %>
|
|
21
|
+
</div>
|
|
22
|
+
<% end %>
|
|
23
|
+
</div>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Alchemy
|
|
2
|
+
module Admin
|
|
3
|
+
module Dashboard
|
|
4
|
+
module Widgets
|
|
5
|
+
class StatWidget < ViewComponent::Base
|
|
6
|
+
delegate :alchemy, to: :helpers
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def title = raise(NotImplementedError)
|
|
11
|
+
def count = raise(NotImplementedError)
|
|
12
|
+
def icon = nil
|
|
13
|
+
def link = nil
|
|
14
|
+
def infos = nil
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<div class="widget system-info">
|
|
2
|
+
<div class="logo">
|
|
3
|
+
<%= logo %>
|
|
4
|
+
</div>
|
|
5
|
+
<div class="version-info">
|
|
6
|
+
<span class="version">
|
|
7
|
+
<%= @alchemy_version %>
|
|
8
|
+
<%= git_source %>
|
|
9
|
+
</span>
|
|
10
|
+
|
|
11
|
+
<%= render Alchemy::Admin::UpdateCheck.new %>
|
|
12
|
+
|
|
13
|
+
<span class="ruby-rails-version">
|
|
14
|
+
<span class="label-with-icon">
|
|
15
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="icon">
|
|
16
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M1 7.19l6.64 6.64h.72L15 7.19v-.72l-3.32-3.32-.36-.15H4.68l-.36.15L1 6.47v.72zm7 5.56L2.08 6.83 4.89 4h6.22l2.81 2.83L8 12.75zm0-7.73h2.69l1.81 1.81-4.5 4.4V5.02z"/>
|
|
17
|
+
</svg>
|
|
18
|
+
Ruby <%= RUBY_VERSION %>
|
|
19
|
+
</span>
|
|
20
|
+
<span class="label-with-icon">
|
|
21
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="icon">
|
|
22
|
+
<path d="M.74,19.23s.32-3.12,2.53-6.82c2.22-3.69,5.99-6.49,9.06-6.76,6.11-.61,11.59,4.86,11.59,4.86,0,0-.18.13-.37.29-5.99-4.43-9.65-2.34-10.8-1.78-5.27,2.94-3.68,10.21-3.68,10.21H.74ZM18.1,6.24c-.34-.14-.67-.26-1.02-.38l.04-.7c.66.19.93.32,1.02.37l-.04.71h0ZM17.1,8.48c.34.02.69.06,1.04.14l-.04.65c-.35-.07-.7-.12-1.04-.13l.04-.66h0ZM13.2,5.18c-.31,0-.61.01-.92.04l-.2-.62c.35-.04.69-.06,1.01-.06l.21.64h-.11ZM13.7,9.07c.26-.12.57-.23.93-.34l.23.7c-.31.09-.61.22-.92.37l-.24-.73h0ZM8.92,6.29c-.27.14-.54.3-.81.47l-.49-.74c.29-.17.56-.31.82-.45l.47.73h0ZM11.08,11.04c.19-.24.41-.48.65-.71l.48.72c-.23.24-.44.49-.62.75l-.51-.76h0ZM9.55,14.44c.08-.38.18-.77.32-1.16l.76.6c-.11.41-.18.82-.22,1.23l-.86-.67h0ZM4.93,9.43c-.26.26-.5.55-.72.83l-.81-.7c.24-.26.51-.53.78-.79l.75.65h0ZM1.74,14.14c-.17.39-.39.91-.53,1.27l-1.21-.44c.11-.29.33-.81.53-1.27l1.21.44h0ZM9.39,17.12l1.14.41c.06.5.16.96.25,1.37l-1.27-.45c-.05-.32-.11-.77-.13-1.33h0Z"/>
|
|
23
|
+
</svg>
|
|
24
|
+
Rails <%= Rails.version %>
|
|
25
|
+
</span>
|
|
26
|
+
</span>
|
|
27
|
+
|
|
28
|
+
<a href="#legal_info" data-turbo="false">
|
|
29
|
+
<small><%= Alchemy.t(:legal_info) %></small>
|
|
30
|
+
</a>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
require "alchemy/version"
|
|
2
|
+
|
|
3
|
+
module Alchemy
|
|
4
|
+
module Admin
|
|
5
|
+
module Dashboard
|
|
6
|
+
module Widgets
|
|
7
|
+
class SystemInfo < ViewComponent::Base
|
|
8
|
+
delegate :alchemy, :render_message, to: :helpers
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@alchemy_version = Alchemy.version
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def logo
|
|
17
|
+
@_logo_file ||= File.read(logo_file_path).html_safe
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def git_source
|
|
21
|
+
git_info = Alchemy.git_revision_info
|
|
22
|
+
return unless git_info
|
|
23
|
+
|
|
24
|
+
branch = git_info[:branch]
|
|
25
|
+
revision = git_info[:revision]
|
|
26
|
+
|
|
27
|
+
"(#{[branch, revision[0, 7]].compact.join(" @ ")})"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def logo_file_path
|
|
31
|
+
Alchemy::Engine.root.join("app/assets/images/alchemy/admin/logo.svg")
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<div class="widget-body">
|
|
2
|
+
<header>
|
|
3
|
+
<%= header_text(total:) %>
|
|
4
|
+
<aside>
|
|
5
|
+
<%= number_with_delimiter(total) %>
|
|
6
|
+
</aside>
|
|
7
|
+
</header>
|
|
8
|
+
<div class="table-scroll">
|
|
9
|
+
<table class="list usage-stats">
|
|
10
|
+
<% stats.each_with_index do |entry, i| %>
|
|
11
|
+
<% row_total = entry.total %>
|
|
12
|
+
<% bar_pct = max.positive? ? (row_total / max * 100).round(2) : 0 %>
|
|
13
|
+
<% public_pct = row_total.positive? ? (entry.public_count / row_total.to_f * 100).round(2) : 0 %>
|
|
14
|
+
<% draft_pct = (100 - public_pct).round(2) %>
|
|
15
|
+
<% hue = (240 + i * 360.0 / stats.size).round % 360 %>
|
|
16
|
+
<% label = entry_label(entry) %>
|
|
17
|
+
<tr>
|
|
18
|
+
<td class="usage-stats--label">
|
|
19
|
+
<span class="label-with-icon">
|
|
20
|
+
<%= entry_icon(entry) %>
|
|
21
|
+
<%= label %>
|
|
22
|
+
</span>
|
|
23
|
+
</td>
|
|
24
|
+
<td class="usage-stats--bar">
|
|
25
|
+
<sl-tooltip content="<%= tooltip_content(entry) %>">
|
|
26
|
+
<span class="bar" style="width: <%= bar_pct %>%; --bar-hue: <%= hue %>">
|
|
27
|
+
<span class="bar__public" style="width: <%= public_pct %>%"></span>
|
|
28
|
+
<span class="bar__draft" style="width: <%= draft_pct %>%"></span>
|
|
29
|
+
</span>
|
|
30
|
+
</sl-tooltip>
|
|
31
|
+
</td>
|
|
32
|
+
<td class="usage-stats--value">
|
|
33
|
+
<%= number_to_percentage(total.positive? ? (row_total / total.to_f) * 100 : 0, precision: 2) %>
|
|
34
|
+
</td>
|
|
35
|
+
<td class="usage-stats--value">
|
|
36
|
+
<%= number_with_delimiter(row_total) %>
|
|
37
|
+
</td>
|
|
38
|
+
</tr>
|
|
39
|
+
<% end %>
|
|
40
|
+
</table>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
module Alchemy
|
|
2
|
+
module Admin
|
|
3
|
+
module Dashboard
|
|
4
|
+
module Widgets
|
|
5
|
+
class UsageWidget < ViewComponent::Base
|
|
6
|
+
Entry = Data.define(:name, :public_count, :draft_count, :definition) do
|
|
7
|
+
def total = public_count + draft_count
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def stats
|
|
13
|
+
@stats ||= definitions
|
|
14
|
+
.map do |definition|
|
|
15
|
+
Entry.new(
|
|
16
|
+
name: definition.name,
|
|
17
|
+
public_count: public_counts.fetch(definition.name, 0),
|
|
18
|
+
draft_count: draft_counts.fetch(definition.name, 0),
|
|
19
|
+
definition: definition
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
.sort_by { |entry| -entry.total }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def total
|
|
26
|
+
@total ||= stats.sum(&:total)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def max
|
|
30
|
+
@max ||= (stats.first&.total || 0).to_f
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Subclass hooks
|
|
34
|
+
|
|
35
|
+
def header_text(total:)
|
|
36
|
+
raise NotImplementedError
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def definitions
|
|
40
|
+
raise NotImplementedError
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def public_counts
|
|
44
|
+
raise NotImplementedError
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def draft_counts
|
|
48
|
+
raise NotImplementedError
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def entry_label(entry)
|
|
52
|
+
raise NotImplementedError
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def tooltip_content(entry)
|
|
56
|
+
"#{entry_label(entry)}: #{entry.public_count} #{Alchemy.t(:published)}, #{entry.draft_count} #{Alchemy.t(:draft)}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def entry_icon(entry)
|
|
60
|
+
raise NotImplementedError
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|