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.
Files changed (200) 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/admin/resource/action.rb +6 -4
  50. data/app/components/alchemy/admin/resource/cell.rb +6 -4
  51. data/app/components/alchemy/admin/resource/table.rb +2 -14
  52. data/app/components/alchemy/ingredients/base_view.rb +1 -1
  53. data/app/components/alchemy/ingredients/headline_editor.rb +1 -1
  54. data/app/components/alchemy/ingredients/picture_view.rb +1 -1
  55. data/app/components/alchemy/ingredients/text_view.rb +2 -2
  56. data/app/controllers/alchemy/admin/dashboard/widgets_controller.rb +21 -0
  57. data/app/controllers/alchemy/admin/dashboard_controller.rb +3 -12
  58. data/app/controllers/alchemy/pages_controller.rb +5 -4
  59. data/app/helpers/alchemy/elements_block_helper.rb +1 -0
  60. data/app/javascript/alchemy_admin/components/attachment_select.js +1 -1
  61. data/app/javascript/alchemy_admin/components/auto_submit.js +15 -9
  62. data/app/javascript/alchemy_admin/components/char_counter.js +17 -7
  63. data/app/javascript/alchemy_admin/components/clipboard_button.js +2 -6
  64. data/app/javascript/alchemy_admin/components/color_select.js +13 -4
  65. data/app/javascript/alchemy_admin/components/datepicker.js +11 -14
  66. data/app/javascript/alchemy_admin/components/dialog_link.js +5 -2
  67. data/app/javascript/alchemy_admin/components/element_editor/delete_element_button.js +6 -3
  68. data/app/javascript/alchemy_admin/components/element_editor.js +45 -28
  69. data/app/javascript/alchemy_admin/components/element_select.js +8 -5
  70. data/app/javascript/alchemy_admin/components/elements_window.js +38 -31
  71. data/app/javascript/alchemy_admin/components/elements_window_handle.js +7 -3
  72. data/app/javascript/alchemy_admin/components/file_editor.js +5 -2
  73. data/app/javascript/alchemy_admin/components/ingredient_group.js +6 -4
  74. data/app/javascript/alchemy_admin/components/link_buttons/link_button.js +1 -2
  75. data/app/javascript/alchemy_admin/components/link_buttons/unlink_button.js +1 -2
  76. data/app/javascript/alchemy_admin/components/link_buttons.js +6 -2
  77. data/app/javascript/alchemy_admin/components/list_filter.js +44 -29
  78. data/app/javascript/alchemy_admin/components/message.js +22 -15
  79. data/app/javascript/alchemy_admin/components/overlay.js +5 -7
  80. data/app/javascript/alchemy_admin/components/page_publication_fields.js +38 -25
  81. data/app/javascript/alchemy_admin/components/page_select.js +1 -1
  82. data/app/javascript/alchemy_admin/components/picture_description_select.js +5 -2
  83. data/app/javascript/alchemy_admin/components/picture_editor.js +5 -10
  84. data/app/javascript/alchemy_admin/components/picture_thumbnail.js +4 -5
  85. data/app/javascript/alchemy_admin/components/preview_window.js +5 -10
  86. data/app/javascript/alchemy_admin/components/publish_page_button.js +2 -5
  87. data/app/javascript/alchemy_admin/components/remote_select.js +53 -23
  88. data/app/javascript/alchemy_admin/components/select.js +169 -26
  89. data/app/javascript/alchemy_admin/components/sortable_elements.js +1 -1
  90. data/app/javascript/alchemy_admin/components/spinner.js +11 -11
  91. data/app/javascript/alchemy_admin/components/tags_autocomplete.js +10 -2
  92. data/app/javascript/alchemy_admin/components/tinymce.js +16 -22
  93. data/app/javascript/alchemy_admin/components/uploader/file_upload.js +48 -45
  94. data/app/javascript/alchemy_admin/components/uploader/progress.js +70 -84
  95. data/app/javascript/alchemy_admin/components/uploader.js +71 -46
  96. data/app/javascript/alchemy_admin/dialog.js +3 -0
  97. data/app/javascript/alchemy_admin/hotkeys.js +0 -18
  98. data/app/javascript/alchemy_admin/image_cropper.js +7 -9
  99. data/app/javascript/alchemy_admin/initializer.js +21 -0
  100. data/app/javascript/alchemy_admin/utils/dispatch_page_dirty_event.js +7 -0
  101. data/app/javascript/alchemy_admin.js +0 -5
  102. data/app/javascript/tinymce/plugins/alchemy_link/index.js +9 -0
  103. data/app/jobs/alchemy/base_job.rb +2 -2
  104. data/app/jobs/alchemy/invalidate_elements_cache_job.rb +33 -0
  105. data/app/models/alchemy/page/page_naming.rb +28 -5
  106. data/app/models/alchemy/page/page_natures.rb +7 -2
  107. data/app/models/alchemy/page/page_scopes.rb +2 -2
  108. data/app/models/alchemy/page/url_path.rb +7 -2
  109. data/app/models/alchemy/page.rb +2 -2
  110. data/app/models/alchemy/page_definition.rb +1 -0
  111. data/app/models/alchemy/permissions.rb +1 -1
  112. data/app/models/concerns/alchemy/publishable.rb +3 -1
  113. data/app/models/concerns/alchemy/relatable_resource.rb +8 -0
  114. data/app/services/alchemy/page_finder.rb +88 -0
  115. data/app/stylesheets/alchemy/_custom-properties.scss +6 -4
  116. data/app/stylesheets/alchemy/_mixins.scss +1 -7
  117. data/app/stylesheets/alchemy/_themes.scss +13 -1
  118. data/app/stylesheets/alchemy/admin/_tom-select.scss +240 -0
  119. data/app/stylesheets/alchemy/admin/archive.scss +0 -1
  120. data/app/stylesheets/alchemy/admin/base.scss +0 -19
  121. data/app/stylesheets/alchemy/admin/dashboard.scss +395 -28
  122. data/app/stylesheets/alchemy/admin/elements.scss +14 -17
  123. data/app/stylesheets/alchemy/admin/form_fields.scss +3 -3
  124. data/app/stylesheets/alchemy/admin/forms.scss +107 -93
  125. data/app/stylesheets/alchemy/admin/icons.scss +28 -0
  126. data/app/stylesheets/alchemy/admin/image_library.scss +20 -10
  127. data/app/stylesheets/alchemy/admin/navigation.scss +4 -1
  128. data/app/stylesheets/alchemy/admin/popover.scss +3 -5
  129. data/app/stylesheets/alchemy/admin/resource_info.scss +11 -17
  130. data/app/stylesheets/alchemy/admin/shoelace.scss +8 -0
  131. data/app/stylesheets/alchemy/admin/sitemap.scss +5 -0
  132. data/app/stylesheets/alchemy/admin/tables.scss +32 -3
  133. data/app/stylesheets/alchemy/admin/toolbar.scss +0 -1
  134. data/app/stylesheets/alchemy/admin.scss +1 -0
  135. data/app/stylesheets/tinymce/skins/ui/alchemy/skin.scss +0 -4
  136. data/app/stylesheets/tinymce/skins/ui/alchemy-dark/skin.scss +0 -4
  137. data/app/types/alchemy/wildcard_url_type.rb +48 -0
  138. data/app/views/alchemy/_menubar.html.erb +1 -5
  139. data/app/views/alchemy/admin/attachments/edit.html.erb +6 -3
  140. data/app/views/alchemy/admin/dashboard/_dashboard.html.erb +3 -2
  141. data/app/views/alchemy/admin/dashboard/_footer.html.erb +22 -0
  142. data/app/views/alchemy/admin/dashboard/_stats.html.erb +7 -0
  143. data/app/views/alchemy/admin/dashboard/_top.html.erb +4 -12
  144. data/app/views/alchemy/admin/dashboard/_widgets.html.erb +7 -0
  145. data/app/views/alchemy/admin/dashboard/index.html.erb +0 -17
  146. data/app/views/alchemy/admin/dashboard/info.html.erb +1 -62
  147. data/app/views/alchemy/admin/dashboard/widgets/show.html.erb +3 -0
  148. data/app/views/alchemy/admin/elements/_form.html.erb +2 -1
  149. data/app/views/alchemy/admin/elements/_schedule.html.erb +2 -15
  150. data/app/views/alchemy/admin/elements/_schedule_fields.html.erb +2 -0
  151. data/app/views/alchemy/admin/layoutpages/edit.html.erb +6 -3
  152. data/app/views/alchemy/admin/nodes/_page_nodes.html.erb +10 -8
  153. data/app/views/alchemy/admin/pages/_form.html.erb +25 -19
  154. data/app/views/alchemy/admin/pages/_publication_fields.html.erb +2 -32
  155. data/app/views/alchemy/admin/pages/_table.html.erb +1 -18
  156. data/app/views/alchemy/admin/pages/configure.html.erb +2 -2
  157. data/app/views/alchemy/admin/pages/info.html.erb +6 -0
  158. data/app/views/alchemy/admin/resources/_form.html.erb +7 -4
  159. data/app/views/alchemy/admin/resources/edit.html.erb +3 -1
  160. data/app/views/alchemy/admin/resources/new.html.erb +3 -1
  161. data/app/views/alchemy/admin/styleguide/index.html.erb +52 -30
  162. data/app/views/alchemy/admin/translations/_en.js +4 -0
  163. data/app/views/layouts/alchemy/admin.html.erb +3 -3
  164. data/config/importmap.rb +3 -12
  165. data/config/locales/alchemy.en.yml +15 -0
  166. data/config/routes.rb +1 -0
  167. data/lib/alchemy/configuration/class_option.rb +46 -3
  168. data/lib/alchemy/configuration/collection_option.rb +4 -0
  169. data/lib/alchemy/configurations/dashboard.rb +79 -0
  170. data/lib/alchemy/configurations/main.rb +15 -0
  171. data/lib/alchemy/engine.rb +9 -3
  172. data/lib/alchemy/sprockets/skip_builds_compression.rb +33 -0
  173. data/lib/alchemy/test_support/capybara_helpers.rb +17 -0
  174. data/lib/alchemy/test_support/relatable_resource_examples.rb +20 -0
  175. data/lib/alchemy/test_support/rspec_matchers.rb +8 -0
  176. data/lib/alchemy/test_support/shared_publishable_examples.rb +38 -41
  177. data/lib/alchemy/tinymce.rb +1 -1
  178. data/lib/alchemy/version.rb +17 -3
  179. data/vendor/javascript/cropperjs.min.js +1 -1
  180. data/vendor/javascript/flatpickr.min.js +1 -1
  181. data/vendor/javascript/floating-ui.min.js +1 -0
  182. data/vendor/javascript/keymaster.min.js +1 -1
  183. data/vendor/javascript/rails-ujs.min.js +1 -1
  184. data/vendor/javascript/shoelace.min.js +93 -93
  185. data/vendor/javascript/sortable.min.js +1 -1
  186. data/vendor/javascript/tinymce.min.js +5 -1
  187. data/vendor/javascript/tom-select.min.js +1 -0
  188. metadata +58 -20
  189. data/app/javascript/alchemy_admin/components/alchemy_html_element.js +0 -129
  190. data/app/views/alchemy/admin/dashboard/_left_column.html.erb +0 -4
  191. data/app/views/alchemy/admin/dashboard/_right_column.html.erb +0 -9
  192. data/app/views/alchemy/admin/dashboard/widgets/_locked_pages.html.erb +0 -52
  193. data/app/views/alchemy/admin/dashboard/widgets/_recent_pages.html.erb +0 -34
  194. data/app/views/alchemy/admin/dashboard/widgets/_sites.html.erb +0 -25
  195. data/app/views/alchemy/admin/dashboard/widgets/_users.html.erb +0 -21
  196. data/app/views/alchemy/admin/languages/edit.html.erb +0 -1
  197. data/app/views/alchemy/admin/languages/new.html.erb +0 -1
  198. data/app/views/alchemy/admin/sites/edit.html.erb +0 -1
  199. data/app/views/alchemy/admin/sites/new.html.erb +0 -1
  200. data/db/migrate/20260702090038_add_index_to_elements_pages_join_table.rb +0 -14
@@ -0,0 +1,25 @@
1
+ module Alchemy
2
+ module Admin
3
+ module Dashboard
4
+ module Widgets
5
+ class UserCounts < StatWidget
6
+ private
7
+
8
+ def link = Alchemy.config.admin_users_path
9
+ def icon = "group"
10
+ def title = Alchemy.config.user_class.model_name.human(count: :many)
11
+ def count = Alchemy.config.user_class.count
12
+
13
+ def infos
14
+ if Alchemy.config.user_class.respond_to?(:logged_in)
15
+ safe_join([
16
+ number_with_delimiter(Alchemy.config.user_class.logged_in.length),
17
+ t(".online")
18
+ ], " ")
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -4,9 +4,15 @@
4
4
  data-element-name="<%= name %>"
5
5
  filter-text="<%= filter_text %>"
6
6
  class="<%= css_classes.join(" ") %>"
7
- <%= compact? ? "compact" : nil %>
8
- <%= created ? "created" : nil %>
9
- <%= fixed? ? "fixed" : nil %>
7
+ <% if compact? %>
8
+ compact
9
+ <% end %>
10
+ <% if created %>
11
+ created
12
+ <% end %>
13
+ <% if fixed? %>
14
+ fixed
15
+ <% end %>
10
16
  >
11
17
  <% unless fixed? %>
12
18
  <%= render 'alchemy/admin/elements/header', element: element %>
@@ -38,28 +44,29 @@
38
44
  ungrouped_ingredients,
39
45
  element_form: f
40
46
  ) %>
47
+ </div>
41
48
 
42
- <!-- Each ingredient group -->
43
- <% grouped_ingredients.each do |group, ingredients| %>
44
- <%= content_tag :details, class: "ingredient-group", id: "element_#{id}_ingredient_group_#{group.parameterize.underscore}", is: "alchemy-ingredient-group" do %>
45
- <summary>
46
- <%= translated_group group %>
47
- <%= render_icon "arrow-left-s" %>
48
- </summary>
49
- <%= render Alchemy::Admin::IngredientEditor.with_collection(
50
- ingredients,
51
- element_form: f
52
- ) %>
53
- <% end %>
49
+ <% grouped_ingredients.each do |group, ingredients| %>
50
+ <%= content_tag :details, class: "ingredient-group", id: "element_#{id}_ingredient_group_#{group.parameterize.underscore}", is: "alchemy-ingredient-group" do %>
51
+ <summary>
52
+ <%= render_icon "arrow-right-s" %>
53
+ <%= translated_group group %>
54
+ </summary>
55
+ <%= render Alchemy::Admin::IngredientEditor.with_collection(
56
+ ingredients,
57
+ element_form: f
58
+ ) %>
54
59
  <% end %>
55
- </div>
60
+ <% end %>
56
61
  <% end %>
57
62
 
58
63
  <% if taggable? %>
59
- <%= render Alchemy::Admin::TagsAutocomplete.new do %>
60
- <%= f.label :tag_list %>
61
- <%= f.text_field :tag_list, value: f.object.tag_list.join(","), disabled: cannot?(:edit, element) %>
62
- <% end %>
64
+ <div class="element-tags">
65
+ <%= render Alchemy::Admin::TagsAutocomplete.new do %>
66
+ <%= f.label :tag_list %>
67
+ <%= f.text_field :tag_list, value: f.object.tag_list.join(","), disabled: cannot?(:edit, element) %>
68
+ <% end %>
69
+ </div>
63
70
  <% end %>
64
71
  <% end %>
65
72
 
@@ -0,0 +1,33 @@
1
+ module Alchemy
2
+ module Admin
3
+ class ElementScheduleTimestamps < ViewComponent::Base
4
+ erb_template <<~HTML
5
+ <div class="input-row">
6
+ <div class="input-column">
7
+ <label for="element_public_on" class="control-label"><%= Alchemy::Element.human_attribute_name(:public_on) %></label>
8
+ <%= datetime_local_field :element, :public_on, include_seconds: false %>
9
+ <%= error_for(:public_on) %>
10
+ </div>
11
+ <div class="input-column">
12
+ <label for="element_public_until" class="control-label"><%= Alchemy::Element.human_attribute_name(:public_until) %></label>
13
+ <%= datetime_local_field :element, :public_until, include_seconds: false %>
14
+ <%= error_for(:public_until) %>
15
+ </div>
16
+ </div>
17
+ HTML
18
+
19
+ def initialize(element:)
20
+ @element = element
21
+ end
22
+
23
+ private
24
+
25
+ def error_for(attribute)
26
+ errors = @element.errors[attribute]
27
+ return unless errors.present?
28
+
29
+ tag.span(errors.to_sentence, class: "error")
30
+ end
31
+ end
32
+ end
33
+ end
@@ -3,11 +3,12 @@ module Alchemy
3
3
  class ElementSelect < ViewComponent::Base
4
4
  delegate :alchemy, to: :helpers
5
5
 
6
- attr_reader :elements, :field_name
6
+ attr_reader :elements, :field_name, :autofocus
7
7
 
8
- def initialize(elements, field_name: "element[name]")
8
+ def initialize(elements, field_name: "element[name]", autofocus: false)
9
9
  @field_name = field_name
10
10
  @elements = elements
11
+ @autofocus = autofocus
11
12
  end
12
13
 
13
14
  def call
@@ -15,7 +16,7 @@ module Alchemy
15
16
  options: elements_options.to_json,
16
17
  placeholder: Alchemy.t(:select_element) do
17
18
  text_field_tag(field_name, nil, {
18
- autofocus: true,
19
+ autofocus:,
19
20
  required: true,
20
21
  value: elements.many? ? nil : elements.first&.name,
21
22
  class: "alchemy_selectbox full_width"
@@ -65,26 +65,7 @@
65
65
  </div>
66
66
 
67
67
  <div class="page_infos">
68
- <% if @page.locked? %>
69
- <span class="page_status locked">
70
- <alchemy-icon name="edit" size="1x"></alchemy-icon>
71
- <%= @page.status_title(:locked) %>
72
- </span>
73
- <% end %>
74
-
75
- <% if @page.restricted? %>
76
- <span class="page_status">
77
- <alchemy-icon name="lock" size="1x"></alchemy-icon>
78
- <%= @page.status_title(:restricted) %>
79
- </span>
80
- <% end %>
81
-
82
- <% unless @page.public? %>
83
- <span class="page_status">
84
- <alchemy-icon name="cloud-off" size="1x"></alchemy-icon>
85
- <%= @page.status_title(:public) %>
86
- </span>
87
- <% end %>
68
+ <%= render Alchemy::Admin::PageStatusIndicators.new(page: @page) %>
88
69
  </div>
89
70
 
90
71
  <div class="sitemap_right_tools">
@@ -0,0 +1,30 @@
1
+ <alchemy-page-publication-fields>
2
+ <label class="checkbox">
3
+ <% if @page.attribute_fixed?(:public_on) || @page.attribute_fixed?(:public_until) %>
4
+ <sl-tooltip class="like-hint-tooltip" content="<%= Alchemy.t(:attribute_fixed) %>" placement="bottom-start">
5
+ <%= checkbox %>
6
+ <%= Alchemy::Page.human_attribute_name :public %>
7
+ </sl-tooltip>
8
+ <% else %>
9
+ <%= checkbox %>
10
+ <%= Alchemy::Page.human_attribute_name :public %>
11
+ <% end %>
12
+ </label>
13
+
14
+ <%= content_tag :div, class: [
15
+ @page.public_on.present? || @page.public_until.present? ? nil : 'hidden',
16
+ 'page-publication-date-fields',
17
+ 'input-row'
18
+ ] do %>
19
+ <div class="input-column">
20
+ <%= label :page, :public_on %>
21
+ <%= datetime_local_field :page, :public_on, include_seconds: false,
22
+ disabled: @page.attribute_fixed?(:public_on) %>
23
+ </div>
24
+ <div class="input-column">
25
+ <%= label :page, :public_until %>
26
+ <%= datetime_local_field :page, :public_until, include_seconds: false,
27
+ disabled: @page.attribute_fixed?(:public_until) %>
28
+ </div>
29
+ <% end %>
30
+ </alchemy-page-publication-fields>
@@ -0,0 +1,18 @@
1
+ module Alchemy
2
+ module Admin
3
+ class PagePublicationFields < ViewComponent::Base
4
+ def initialize(page:)
5
+ @page = page
6
+ end
7
+
8
+ private
9
+
10
+ def checkbox
11
+ check_box_tag :page_public, nil,
12
+ @page.public? || @page.scheduled?,
13
+ name: nil,
14
+ disabled: @page.attribute_fixed?(:public_on)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,29 @@
1
+ <% if @page.locked? %>
2
+ <span class="page_status locked">
3
+ <alchemy-icon name="edit" size="1x"></alchemy-icon>
4
+ <%= @page.status_title(:locked) %>
5
+ </span>
6
+ <% end %>
7
+
8
+ <% if @page.restricted? %>
9
+ <span class="page_status">
10
+ <alchemy-icon name="lock" size="1x"></alchemy-icon>
11
+ <%= @page.status_title(:restricted) %>
12
+ </span>
13
+ <% end %>
14
+
15
+ <% if !@page.public? && !@page.scheduled? %>
16
+ <span class="page_status">
17
+ <alchemy-icon name="cloud-off" size="1x"></alchemy-icon>
18
+ <%= @page.status_title(:public) %>
19
+ </span>
20
+ <% end %>
21
+
22
+ <% if @page.scheduled? %>
23
+ <span class="page_status">
24
+ <sl-tooltip content="<%= @page.status_message(:scheduled) %>">
25
+ <alchemy-icon name="calendar-schedule" size="1x"></alchemy-icon>
26
+ <%= @page.status_title(:scheduled) %>
27
+ </sl-tooltip>
28
+ </span>
29
+ <% end %>
@@ -0,0 +1,9 @@
1
+ module Alchemy
2
+ module Admin
3
+ class PageStatusIndicators < ViewComponent::Base
4
+ def initialize(page:)
5
+ @page = page
6
+ end
7
+ end
8
+ end
9
+ end
@@ -2,14 +2,18 @@
2
2
  <sl-button-group label="Example Button Group">
3
3
  <sl-tooltip
4
4
  content="<%= element.public? ? Alchemy.t(:hide_element) : Alchemy.t(:show_element) %>"
5
- <%= "disabled" if element.scheduled? || cannot?(:update, element) %>
5
+ <% if element.scheduled? || cannot?(:update, element) %>
6
+ disabled
7
+ <% end %>
6
8
  >
7
9
  <%= form_tag(alchemy.publish_admin_element_path(element), method: "patch") do %>
8
10
  <sl-button
9
11
  variant="<%= element.public? ? "default" : "primary" %>"
10
12
  type="submit"
11
13
  size="small"
12
- <%= "disabled" if element.scheduled? || cannot?(:update, element) %>
14
+ <% if element.scheduled? || cannot?(:update, element) %>
15
+ disabled
16
+ <% end %>
13
17
  outline
14
18
  pill
15
19
  >
@@ -19,14 +23,18 @@
19
23
  </sl-tooltip>
20
24
  <sl-tooltip
21
25
  content="<%= element.scheduled? ? Alchemy.t(:edit_element_schedule) : Alchemy.t(:schedule_element) %>"
22
- <%= "disabled" if cannot?(:update, element) %>
26
+ <% if cannot?(:update, element) %>
27
+ disabled
28
+ <% end %>
23
29
  >
24
30
  <sl-dropdown distance="5">
25
31
  <sl-button
26
32
  slot="trigger"
27
33
  variant="<%= element.scheduled? ? "primary" : "default" %>"
28
34
  size="small"
29
- <%= "disabled" if cannot?(:update, element) %>
35
+ <% if cannot?(:update, element) %>
36
+ disabled
37
+ <% end %>
30
38
  outline
31
39
  pill
32
40
  >
@@ -10,8 +10,6 @@ module Alchemy
10
10
  # name of an action to evaluate if the user can perform these action on the given object
11
11
  # @param [String, nil] :tooltip
12
12
  # show a tooltip around the button
13
- # @param [Object, nil] :resource
14
- # the resource of the row the action belongs to
15
13
  # @param [Lambda] :block
16
14
  # a block to include a button or a link
17
15
  #
@@ -32,12 +30,16 @@ module Alchemy
32
30
  <% end %>
33
31
  ERB
34
32
 
35
- def initialize(name = nil, tooltip = nil, resource = nil, &block)
33
+ def initialize(name = nil, tooltip = nil, &block)
36
34
  @name = name
37
35
  @tooltip = tooltip
38
- @resource = resource
39
36
  @block = block
40
37
  end
38
+
39
+ def with_resource(resource)
40
+ @resource = resource
41
+ self
42
+ end
41
43
  end
42
44
  end
43
45
  end
@@ -7,8 +7,6 @@ module Alchemy
7
7
  #
8
8
  # @param [String, nil] :css_classes
9
9
  # css classes that are show at the table cell
10
- # @param [Object, nil] :resource
11
- # the resource of the row the cell belongs to
12
10
  # @param [Lambda] :block
13
11
  # a block to include a button or a link
14
12
  #
@@ -21,11 +19,15 @@ module Alchemy
21
19
  </td>
22
20
  ERB
23
21
 
24
- def initialize(css_classes, resource = nil, &block)
22
+ def initialize(css_classes, &block)
25
23
  @css_classes = css_classes
26
- @resource = resource
27
24
  @block = block
28
25
  end
26
+
27
+ def with_resource(resource)
28
+ @resource = resource
29
+ self
30
+ end
29
31
  end
30
32
  end
31
33
  end
@@ -69,12 +69,12 @@ module Alchemy
69
69
  <% collection.each do |resource| %>
70
70
  <tr class="<%= cycle('even', 'odd') %>">
71
71
  <% cells.each do |cell| %>
72
- <%= render cell_for(cell, resource) %>
72
+ <%= render cell.with_resource(resource) %>
73
73
  <% end %>
74
74
  <% if actions? %>
75
75
  <td class="tools">
76
76
  <% actions.each do |action| %>
77
- <%= render action_for(action, resource) %>
77
+ <%= render action.with_resource(resource) %>
78
78
  <% end %>
79
79
  </td>
80
80
  <% end %>
@@ -134,18 +134,6 @@ module Alchemy
134
134
 
135
135
  private
136
136
 
137
- # A fresh cell instance is built for every resource because ViewComponent
138
- # instances are single-use and may not be rendered more than once.
139
- def cell_for(cell, resource)
140
- Cell.new(cell.css_classes, resource, &cell.block)
141
- end
142
-
143
- # A fresh action instance is built for every resource because ViewComponent
144
- # instances are single-use and may not be rendered more than once.
145
- def action_for(action, resource)
146
- Action.new(action.name, action.tooltip, resource, &action.block)
147
- end
148
-
149
137
  ##
150
138
  # if no cells are available the resource_helper will be used, to generate the
151
139
  # default attributes of the given resource
@@ -16,7 +16,7 @@ module Alchemy
16
16
  end
17
17
 
18
18
  def call
19
- h(value)
19
+ value.html_safe
20
20
  end
21
21
 
22
22
  def render?
@@ -61,7 +61,7 @@ module Alchemy
61
61
 
62
62
  def css_classes
63
63
  super + [
64
- has_level_select? ? "with-level-select" : nil,
64
+ level_options.any? ? "with-level-select" : nil,
65
65
  has_size_select? ? "with-size-select" : nil
66
66
  ].compact
67
67
  end
@@ -68,7 +68,7 @@ module Alchemy
68
68
  def caption
69
69
  return unless show_caption?
70
70
 
71
- @_caption ||= content_tag(:figcaption, sanitize(ingredient.caption))
71
+ @_caption ||= content_tag(:figcaption, ingredient.caption.html_safe)
72
72
  end
73
73
 
74
74
  def src
@@ -18,7 +18,7 @@ module Alchemy
18
18
 
19
19
  def call
20
20
  if disable_link?
21
- dom_id.present? ? anchor : h(value)
21
+ dom_id.present? ? anchor : value
22
22
  else
23
23
  link_to(value, url_for(link), {
24
24
  id: dom_id.presence,
@@ -26,7 +26,7 @@ module Alchemy
26
26
  target: link_target_value(link_target),
27
27
  rel: link_rel_value(link_target)
28
28
  }.merge(html_options))
29
- end
29
+ end.html_safe
30
30
  end
31
31
 
32
32
  private
@@ -0,0 +1,21 @@
1
+ module Alchemy
2
+ module Admin
3
+ class Dashboard::WidgetsController < DashboardController
4
+ MODULE_NAMESPACE = "Alchemy::Admin::Dashboard::Widgets"
5
+
6
+ def show
7
+ @id = params[:id]
8
+ @widget = get_widget(@id)
9
+ end
10
+
11
+ private
12
+
13
+ def get_widget(id)
14
+ "#{MODULE_NAMESPACE}::#{id.camelcase}".constantize
15
+ rescue NameError => e
16
+ Alchemy::Logger.error "No dashboard widget found for id: #{id}"
17
+ raise ActionController::RoutingError, e.message
18
+ end
19
+ end
20
+ end
21
+ end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "net/http"
4
3
  require "alchemy/version"
5
4
 
6
5
  module Alchemy
@@ -9,20 +8,12 @@ module Alchemy
9
8
  authorize_resource class: :alchemy_admin_dashboard
10
9
 
11
10
  def index
12
- @last_edited_pages = Page.all_last_edited_from(current_alchemy_user)
13
- @all_locked_pages = Page.locked
14
- if Alchemy.config.user_class.respond_to?(:logged_in)
15
- @online_users = Alchemy.config.user_class.logged_in.to_a - [current_alchemy_user]
16
- end
17
- if current_alchemy_user.respond_to?(:sign_in_count) && current_alchemy_user.respond_to?(:last_sign_in_at)
18
- @last_sign_at = current_alchemy_user.last_sign_in_at
19
- @first_time = current_alchemy_user.sign_in_count == 1 && @last_sign_at.nil?
20
- end
21
- @sites = Site.all
22
11
  end
23
12
 
24
13
  def info
25
- @alchemy_version = Alchemy.version
14
+ Alchemy::Deprecation.warn <<~WARN
15
+ Requesting `admin/dashboard/info` is deprecated. Please render Alchemy::Admin::Dashboard::Widgets::SystemInfo instead.
16
+ WARN
26
17
  end
27
18
  end
28
19
  end
@@ -126,10 +126,11 @@ module Alchemy
126
126
  def load_page
127
127
  page_not_found! unless Current.language
128
128
 
129
- @page ||= Current.language.pages.contentpages.find_by(
130
- urlname: params[:urlname],
131
- language_code: params[:locale] || Current.language.code
132
- )
129
+ result = PageFinder.new(params[:urlname]).call
130
+ if result
131
+ @page ||= result.page
132
+ params.merge!(result.extracted_params)
133
+ end
133
134
  Current.page = @page
134
135
  end
135
136
 
@@ -57,6 +57,7 @@ module Alchemy
57
57
  def ingredient_by_role(role)
58
58
  element.ingredient_by_role(role)
59
59
  end
60
+ alias_method :ingredient, :ingredient_by_role
60
61
  end
61
62
 
62
63
  # Block-level helper for element views. Constructs a DOM element wrapping
@@ -1,6 +1,6 @@
1
1
  import { RemoteSelect } from "alchemy_admin/components/remote_select"
2
2
 
3
- export class AttachmentSelect extends RemoteSelect {
3
+ class AttachmentSelect extends RemoteSelect {
4
4
  _renderResult(item) {
5
5
  return this._renderListEntry(item)
6
6
  }
@@ -4,16 +4,22 @@ class AutoSubmit extends HTMLElement {
4
4
  connectedCallback() {
5
5
  // Still using jQuery here, because select2 does not emit
6
6
  // the event from the original select element.
7
- $(this).on("change", function (event) {
8
- // We need to dispatch a submit event, so that Turbo that listens
9
- // to it submits the search form us.
10
- const submitEvent = new Event("submit", {
11
- bubbles: true,
12
- cancelable: true
13
- })
14
- event.target.form.dispatchEvent(submitEvent)
15
- return false
7
+ $(this).on("change", this.#onChange)
8
+ }
9
+
10
+ disconnectedCallback() {
11
+ $(this).off("change", this.#onChange)
12
+ }
13
+
14
+ #onChange = (event) => {
15
+ // We need to dispatch a submit event, so that Turbo that listens
16
+ // to it submits the search form us.
17
+ const submitEvent = new Event("submit", {
18
+ bubbles: true,
19
+ cancelable: true
16
20
  })
21
+ event.target.form.dispatchEvent(submitEvent)
22
+ return false
17
23
  }
18
24
  }
19
25
 
@@ -1,30 +1,36 @@
1
1
  /**
2
2
  * Show the character counter below input fields and textareas
3
3
  */
4
- import { AlchemyHTMLElement } from "alchemy_admin/components/alchemy_html_element"
5
4
  import { translate } from "alchemy_admin/i18n"
6
5
 
7
- class CharCounter extends AlchemyHTMLElement {
8
- static properties = {
9
- maxChars: { default: 60 }
10
- }
11
- connected() {
6
+ class CharCounter extends HTMLElement {
7
+ connectedCallback() {
12
8
  this.translation = translate("allowed_chars", this.maxChars)
13
9
  this.formField = this.getFormField()
14
10
 
15
11
  if (this.formField) {
16
12
  this.createDisplayElement()
17
13
  this.countCharacters()
18
- this.formField.addEventListener("keyup", () => this.countCharacters()) // add arrow function to get a implicit this - binding
14
+ this.formField.addEventListener("keyup", this)
19
15
  }
20
16
  }
21
17
 
18
+ disconnectedCallback() {
19
+ this.formField?.removeEventListener("keyup", this)
20
+ }
21
+
22
+ handleEvent(event) {
23
+ if (event.type === "keyup") this.countCharacters()
24
+ }
25
+
22
26
  getFormField() {
23
27
  const formFields = this.querySelectorAll("input, textarea")
24
28
  return formFields.length > 0 ? formFields[0] : undefined
25
29
  }
26
30
 
27
31
  createDisplayElement() {
32
+ this.display = this.querySelector(":scope > .alchemy-char-counter")
33
+ if (this.display) return
28
34
  this.display = document.createElement("small")
29
35
  this.display.className = "alchemy-char-counter"
30
36
  this.formField.after(this.display)
@@ -35,6 +41,10 @@ class CharCounter extends AlchemyHTMLElement {
35
41
  this.display.textContent = `${charLength} ${this.translation}`
36
42
  this.display.classList.toggle("too-long", charLength > this.maxChars)
37
43
  }
44
+
45
+ get maxChars() {
46
+ return this.getAttribute("max-chars") ?? 60
47
+ }
38
48
  }
39
49
 
40
50
  customElements.define("alchemy-char-counter", CharCounter)
@@ -2,12 +2,8 @@ import "clipboard"
2
2
  import { growl } from "alchemy_admin/growler"
3
3
 
4
4
  class ClipboardButton extends HTMLElement {
5
- constructor() {
6
- super()
7
-
8
- this.innerHTML = `
9
- <alchemy-icon name="clipboard"></alchemy-icon>
10
- `
5
+ connectedCallback() {
6
+ this.innerHTML = '<alchemy-icon name="clipboard"></alchemy-icon>'
11
7
 
12
8
  this.clipboard = new ClipboardJS(this, {
13
9
  text: () => {