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.
Files changed (189) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -1
  3. data/app/assets/builds/alchemy/admin.css +1 -1
  4. data/app/assets/builds/alchemy/alchemy_admin.min.js +1 -1
  5. data/app/assets/builds/alchemy/alchemy_admin.min.js.map +1 -1
  6. data/app/assets/builds/alchemy/dark-theme.css +1 -1
  7. data/app/assets/builds/alchemy/light-theme.css +1 -1
  8. data/app/assets/builds/alchemy/preview.min.js +1 -1
  9. data/app/assets/builds/alchemy/theme.css +1 -1
  10. data/app/assets/builds/alchemy/welcome.css +1 -1
  11. data/app/assets/builds/tinymce/skins/content/alchemy/content.min.css +1 -1
  12. data/app/assets/builds/tinymce/skins/content/alchemy-dark/content.min.css +1 -1
  13. data/app/assets/builds/tinymce/skins/ui/alchemy/skin.min.css +1 -1
  14. data/app/assets/builds/tinymce/skins/ui/alchemy-dark/skin.min.css +1 -1
  15. data/app/assets/images/alchemy/admin/logo.svg +27 -0
  16. data/app/assets/images/alchemy/icons-sprite.svg +1 -1
  17. data/app/components/alchemy/admin/dashboard/widget.rb +40 -0
  18. data/app/components/alchemy/admin/dashboard/widgets/attachment_counts.rb +17 -0
  19. data/app/components/alchemy/admin/dashboard/widgets/element_usage.rb +37 -0
  20. data/app/components/alchemy/admin/dashboard/widgets/greeting.html.erb +13 -0
  21. data/app/components/alchemy/admin/dashboard/widgets/greeting.rb +21 -0
  22. data/app/components/alchemy/admin/dashboard/widgets/locked_pages.html.erb +54 -0
  23. data/app/components/alchemy/admin/dashboard/widgets/locked_pages.rb +20 -0
  24. data/app/components/alchemy/admin/dashboard/widgets/online_users.html.erb +22 -0
  25. data/app/components/alchemy/admin/dashboard/widgets/online_users.rb +19 -0
  26. data/app/components/alchemy/admin/dashboard/widgets/page_counts.rb +23 -0
  27. data/app/components/alchemy/admin/dashboard/widgets/page_usage.rb +46 -0
  28. data/app/components/alchemy/admin/dashboard/widgets/picture_counts.rb +17 -0
  29. data/app/components/alchemy/admin/dashboard/widgets/recent_pages.html.erb +41 -0
  30. data/app/components/alchemy/admin/dashboard/widgets/recent_pages.rb +16 -0
  31. data/app/components/alchemy/admin/dashboard/widgets/sites.html.erb +29 -0
  32. data/app/components/alchemy/admin/dashboard/widgets/sites.rb +15 -0
  33. data/app/components/alchemy/admin/dashboard/widgets/stat_widget.html.erb +23 -0
  34. data/app/components/alchemy/admin/dashboard/widgets/stat_widget.rb +19 -0
  35. data/app/components/alchemy/admin/dashboard/widgets/system_info.html.erb +32 -0
  36. data/app/components/alchemy/admin/dashboard/widgets/system_info.rb +37 -0
  37. data/app/components/alchemy/admin/dashboard/widgets/usage_widget.html.erb +42 -0
  38. data/app/components/alchemy/admin/dashboard/widgets/usage_widget.rb +66 -0
  39. data/app/components/alchemy/admin/dashboard/widgets/user_counts.rb +25 -0
  40. data/app/components/alchemy/admin/element_editor.html.erb +27 -20
  41. data/app/components/alchemy/admin/element_schedule_timestamps.rb +33 -0
  42. data/app/components/alchemy/admin/element_select.rb +4 -3
  43. data/app/components/alchemy/admin/page_node.html.erb +1 -20
  44. data/app/components/alchemy/admin/page_publication_fields.html.erb +30 -0
  45. data/app/components/alchemy/admin/page_publication_fields.rb +18 -0
  46. data/app/components/alchemy/admin/page_status_indicators.html.erb +29 -0
  47. data/app/components/alchemy/admin/page_status_indicators.rb +9 -0
  48. data/app/components/alchemy/admin/publish_element_button.html.erb +12 -4
  49. data/app/components/alchemy/ingredients/headline_editor.rb +1 -1
  50. data/app/controllers/alchemy/admin/dashboard/widgets_controller.rb +21 -0
  51. data/app/controllers/alchemy/admin/dashboard_controller.rb +3 -12
  52. data/app/controllers/alchemy/pages_controller.rb +5 -4
  53. data/app/helpers/alchemy/elements_block_helper.rb +1 -0
  54. data/app/javascript/alchemy_admin/components/auto_submit.js +15 -9
  55. data/app/javascript/alchemy_admin/components/char_counter.js +17 -7
  56. data/app/javascript/alchemy_admin/components/clipboard_button.js +2 -6
  57. data/app/javascript/alchemy_admin/components/color_select.js +13 -4
  58. data/app/javascript/alchemy_admin/components/datepicker.js +11 -14
  59. data/app/javascript/alchemy_admin/components/dialog_link.js +5 -2
  60. data/app/javascript/alchemy_admin/components/element_editor/delete_element_button.js +6 -3
  61. data/app/javascript/alchemy_admin/components/element_editor.js +45 -28
  62. data/app/javascript/alchemy_admin/components/element_select.js +7 -4
  63. data/app/javascript/alchemy_admin/components/elements_window.js +38 -31
  64. data/app/javascript/alchemy_admin/components/elements_window_handle.js +7 -3
  65. data/app/javascript/alchemy_admin/components/file_editor.js +5 -2
  66. data/app/javascript/alchemy_admin/components/ingredient_group.js +6 -4
  67. data/app/javascript/alchemy_admin/components/link_buttons/link_button.js +1 -2
  68. data/app/javascript/alchemy_admin/components/link_buttons/unlink_button.js +1 -2
  69. data/app/javascript/alchemy_admin/components/link_buttons.js +6 -2
  70. data/app/javascript/alchemy_admin/components/list_filter.js +44 -29
  71. data/app/javascript/alchemy_admin/components/message.js +22 -15
  72. data/app/javascript/alchemy_admin/components/overlay.js +5 -7
  73. data/app/javascript/alchemy_admin/components/page_publication_fields.js +38 -25
  74. data/app/javascript/alchemy_admin/components/picture_description_select.js +5 -2
  75. data/app/javascript/alchemy_admin/components/picture_editor.js +5 -10
  76. data/app/javascript/alchemy_admin/components/picture_thumbnail.js +4 -5
  77. data/app/javascript/alchemy_admin/components/preview_window.js +5 -10
  78. data/app/javascript/alchemy_admin/components/publish_page_button.js +2 -5
  79. data/app/javascript/alchemy_admin/components/remote_select.js +53 -23
  80. data/app/javascript/alchemy_admin/components/select.js +169 -26
  81. data/app/javascript/alchemy_admin/components/sortable_elements.js +1 -1
  82. data/app/javascript/alchemy_admin/components/spinner.js +11 -11
  83. data/app/javascript/alchemy_admin/components/tags_autocomplete.js +9 -1
  84. data/app/javascript/alchemy_admin/components/tinymce.js +16 -22
  85. data/app/javascript/alchemy_admin/components/uploader/file_upload.js +48 -45
  86. data/app/javascript/alchemy_admin/components/uploader/progress.js +70 -84
  87. data/app/javascript/alchemy_admin/components/uploader.js +71 -46
  88. data/app/javascript/alchemy_admin/dialog.js +3 -0
  89. data/app/javascript/alchemy_admin/hotkeys.js +0 -18
  90. data/app/javascript/alchemy_admin/image_cropper.js +7 -9
  91. data/app/javascript/alchemy_admin/initializer.js +21 -0
  92. data/app/javascript/alchemy_admin/utils/dispatch_page_dirty_event.js +7 -0
  93. data/app/javascript/tinymce/plugins/alchemy_link/index.js +9 -0
  94. data/app/jobs/alchemy/base_job.rb +2 -2
  95. data/app/jobs/alchemy/invalidate_elements_cache_job.rb +33 -0
  96. data/app/models/alchemy/page/page_naming.rb +28 -5
  97. data/app/models/alchemy/page/page_natures.rb +7 -2
  98. data/app/models/alchemy/page/page_scopes.rb +2 -2
  99. data/app/models/alchemy/page/url_path.rb +7 -2
  100. data/app/models/alchemy/page.rb +2 -2
  101. data/app/models/alchemy/page_definition.rb +1 -0
  102. data/app/models/alchemy/permissions.rb +1 -1
  103. data/app/models/concerns/alchemy/relatable_resource.rb +8 -0
  104. data/app/services/alchemy/page_finder.rb +88 -0
  105. data/app/stylesheets/alchemy/_custom-properties.scss +6 -4
  106. data/app/stylesheets/alchemy/_mixins.scss +1 -7
  107. data/app/stylesheets/alchemy/_themes.scss +13 -1
  108. data/app/stylesheets/alchemy/admin/_tom-select.scss +240 -0
  109. data/app/stylesheets/alchemy/admin/archive.scss +0 -1
  110. data/app/stylesheets/alchemy/admin/base.scss +0 -19
  111. data/app/stylesheets/alchemy/admin/dashboard.scss +395 -28
  112. data/app/stylesheets/alchemy/admin/elements.scss +14 -17
  113. data/app/stylesheets/alchemy/admin/form_fields.scss +3 -3
  114. data/app/stylesheets/alchemy/admin/forms.scss +107 -93
  115. data/app/stylesheets/alchemy/admin/icons.scss +28 -0
  116. data/app/stylesheets/alchemy/admin/image_library.scss +20 -10
  117. data/app/stylesheets/alchemy/admin/navigation.scss +4 -1
  118. data/app/stylesheets/alchemy/admin/popover.scss +3 -5
  119. data/app/stylesheets/alchemy/admin/resource_info.scss +11 -17
  120. data/app/stylesheets/alchemy/admin/shoelace.scss +8 -0
  121. data/app/stylesheets/alchemy/admin/sitemap.scss +5 -0
  122. data/app/stylesheets/alchemy/admin/tables.scss +32 -3
  123. data/app/stylesheets/alchemy/admin/toolbar.scss +0 -1
  124. data/app/stylesheets/alchemy/admin.scss +1 -0
  125. data/app/stylesheets/tinymce/skins/ui/alchemy/skin.scss +0 -4
  126. data/app/stylesheets/tinymce/skins/ui/alchemy-dark/skin.scss +0 -4
  127. data/app/types/alchemy/wildcard_url_type.rb +48 -0
  128. data/app/views/alchemy/_menubar.html.erb +1 -5
  129. data/app/views/alchemy/admin/attachments/edit.html.erb +6 -3
  130. data/app/views/alchemy/admin/dashboard/_dashboard.html.erb +3 -2
  131. data/app/views/alchemy/admin/dashboard/_footer.html.erb +22 -0
  132. data/app/views/alchemy/admin/dashboard/_stats.html.erb +7 -0
  133. data/app/views/alchemy/admin/dashboard/_top.html.erb +4 -12
  134. data/app/views/alchemy/admin/dashboard/_widgets.html.erb +7 -0
  135. data/app/views/alchemy/admin/dashboard/index.html.erb +0 -17
  136. data/app/views/alchemy/admin/dashboard/info.html.erb +1 -62
  137. data/app/views/alchemy/admin/dashboard/widgets/show.html.erb +3 -0
  138. data/app/views/alchemy/admin/elements/_form.html.erb +2 -1
  139. data/app/views/alchemy/admin/elements/_schedule.html.erb +2 -15
  140. data/app/views/alchemy/admin/elements/_schedule_fields.html.erb +2 -0
  141. data/app/views/alchemy/admin/layoutpages/edit.html.erb +6 -3
  142. data/app/views/alchemy/admin/nodes/_page_nodes.html.erb +10 -8
  143. data/app/views/alchemy/admin/pages/_form.html.erb +25 -19
  144. data/app/views/alchemy/admin/pages/_publication_fields.html.erb +2 -32
  145. data/app/views/alchemy/admin/pages/_table.html.erb +1 -18
  146. data/app/views/alchemy/admin/pages/configure.html.erb +2 -2
  147. data/app/views/alchemy/admin/pages/info.html.erb +6 -0
  148. data/app/views/alchemy/admin/resources/_form.html.erb +7 -4
  149. data/app/views/alchemy/admin/resources/edit.html.erb +3 -1
  150. data/app/views/alchemy/admin/resources/new.html.erb +3 -1
  151. data/app/views/alchemy/admin/styleguide/index.html.erb +52 -30
  152. data/app/views/alchemy/admin/translations/_en.js +4 -0
  153. data/app/views/layouts/alchemy/admin.html.erb +3 -3
  154. data/config/importmap.rb +2 -0
  155. data/config/locales/alchemy.en.yml +15 -0
  156. data/config/routes.rb +1 -0
  157. data/lib/alchemy/configuration/class_option.rb +46 -3
  158. data/lib/alchemy/configuration/collection_option.rb +4 -0
  159. data/lib/alchemy/configurations/dashboard.rb +79 -0
  160. data/lib/alchemy/configurations/main.rb +15 -0
  161. data/lib/alchemy/engine.rb +9 -3
  162. data/lib/alchemy/sprockets/skip_builds_compression.rb +33 -0
  163. data/lib/alchemy/test_support/capybara_helpers.rb +17 -0
  164. data/lib/alchemy/test_support/relatable_resource_examples.rb +20 -0
  165. data/lib/alchemy/test_support/rspec_matchers.rb +8 -0
  166. data/lib/alchemy/test_support/shared_publishable_examples.rb +38 -31
  167. data/lib/alchemy/tinymce.rb +1 -1
  168. data/lib/alchemy/version.rb +17 -3
  169. data/vendor/javascript/cropperjs.min.js +1 -1
  170. data/vendor/javascript/flatpickr.min.js +1 -1
  171. data/vendor/javascript/floating-ui.min.js +1 -0
  172. data/vendor/javascript/keymaster.min.js +1 -1
  173. data/vendor/javascript/rails-ujs.min.js +1 -1
  174. data/vendor/javascript/shoelace.min.js +93 -93
  175. data/vendor/javascript/sortable.min.js +1 -1
  176. data/vendor/javascript/tinymce.min.js +5 -1
  177. data/vendor/javascript/tom-select.min.js +1 -0
  178. metadata +57 -18
  179. data/app/javascript/alchemy_admin/components/alchemy_html_element.js +0 -129
  180. data/app/views/alchemy/admin/dashboard/_left_column.html.erb +0 -4
  181. data/app/views/alchemy/admin/dashboard/_right_column.html.erb +0 -9
  182. data/app/views/alchemy/admin/dashboard/widgets/_locked_pages.html.erb +0 -52
  183. data/app/views/alchemy/admin/dashboard/widgets/_recent_pages.html.erb +0 -34
  184. data/app/views/alchemy/admin/dashboard/widgets/_sites.html.erb +0 -25
  185. data/app/views/alchemy/admin/dashboard/widgets/_users.html.erb +0 -21
  186. data/app/views/alchemy/admin/languages/edit.html.erb +0 -1
  187. data/app/views/alchemy/admin/languages/new.html.erb +0 -1
  188. data/app/views/alchemy/admin/sites/edit.html.erb +0 -1
  189. 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,15 @@
1
+ module Alchemy
2
+ module Admin
3
+ module Dashboard
4
+ module Widgets
5
+ class Sites < ViewComponent::Base
6
+ delegate :alchemy, :current_alchemy_user, to: :helpers
7
+
8
+ private
9
+
10
+ def sites = Site.all
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -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