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,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
  >
@@ -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
@@ -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
@@ -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: () => {
@@ -14,12 +14,12 @@ const formatItem = (object) => {
14
14
  }
15
15
 
16
16
  class ColorSelect extends HTMLElement {
17
+ #select2 = null
18
+
17
19
  connectedCallback() {
18
20
  if (this.select) {
19
21
  this.#initializeSelect2()
20
- $(this.select).on("change", (event) =>
21
- this.#toggleColorPicker(event.val === "custom_color")
22
- )
22
+ this.#select2.on("change", this.#onSelectChange)
23
23
  } else {
24
24
  this.colorInput?.addEventListener("input", this)
25
25
  this.textInput?.addEventListener("input", this)
@@ -41,6 +41,15 @@ class ColorSelect extends HTMLElement {
41
41
  disconnectedCallback() {
42
42
  this.colorInput?.removeEventListener("input", this)
43
43
  this.textInput?.removeEventListener("input", this)
44
+ if (this.#select2) {
45
+ this.#select2.off("change", this.#onSelectChange)
46
+ this.#select2.select2("destroy")
47
+ this.#select2 = null
48
+ }
49
+ }
50
+
51
+ #onSelectChange = (event) => {
52
+ this.#toggleColorPicker(event.val === "custom_color")
44
53
  }
45
54
 
46
55
  #initializeSelect2() {
@@ -50,7 +59,7 @@ class ColorSelect extends HTMLElement {
50
59
  formatResult: formatItem,
51
60
  formatSelection: formatItem
52
61
  }
53
- $(this.select).select2(options)
62
+ this.#select2 = $(this.select).select2(options)
54
63
  }
55
64
 
56
65
  #toggleColorPicker(enabled = true) {
@@ -1,31 +1,24 @@
1
- import { AlchemyHTMLElement } from "alchemy_admin/components/alchemy_html_element"
2
1
  import { translate, currentLocale } from "alchemy_admin/i18n"
3
2
  import flatpickr from "flatpickr"
4
3
 
5
4
  const locale = currentLocale()
6
5
 
7
- class Datepicker extends AlchemyHTMLElement {
8
- static properties = {
9
- inputType: { default: "date" }
10
- }
11
-
12
- constructor() {
13
- super()
14
- this.flatpickr = undefined
15
- }
16
-
6
+ class Datepicker extends HTMLElement {
17
7
  // Load the locales for flatpickr before setting it up.
18
- async connected() {
8
+ async connectedCallback() {
19
9
  // English is the default locale for flatpickr, so we don't need to load it
20
10
  if (locale !== "en") {
21
11
  await import(`flatpickr/${locale}.js`)
22
12
  }
13
+ // Bail out if the element was disconnected while the locale was loading.
14
+ // Otherwise flatpickr would leak a calendar onto a detached input.
15
+ if (!this.isConnected) return
23
16
 
24
17
  this.flatpickr = flatpickr(this.inputField, this.flatpickrOptions)
25
18
  }
26
19
 
27
- disconnected() {
28
- this.flatpickr.destroy()
20
+ disconnectedCallback() {
21
+ this.flatpickr?.destroy()
29
22
  }
30
23
 
31
24
  get flatpickrOptions() {
@@ -56,6 +49,10 @@ class Datepicker extends AlchemyHTMLElement {
56
49
  get inputField() {
57
50
  return this.querySelector("input")
58
51
  }
52
+
53
+ get inputType() {
54
+ return this.getAttribute("input-type") || "date"
55
+ }
59
56
  }
60
57
 
61
58
  customElements.define("alchemy-datepicker", Datepicker)
@@ -1,11 +1,14 @@
1
1
  import { Dialog } from "alchemy_admin/dialog"
2
2
 
3
3
  export class DialogLink extends HTMLAnchorElement {
4
- constructor() {
5
- super()
4
+ connectedCallback() {
6
5
  this.addEventListener("click", this)
7
6
  }
8
7
 
8
+ disconnectedCallback() {
9
+ this.removeEventListener("click", this)
10
+ }
11
+
9
12
  handleEvent(evt) {
10
13
  if (!this.disabled) {
11
14
  this.openDialog()
@@ -3,14 +3,17 @@ import { removeTab } from "alchemy_admin/fixed_elements"
3
3
  import { growl } from "alchemy_admin/growler"
4
4
  import { reloadPreview } from "alchemy_admin/components/preview_window"
5
5
  import { openConfirmDialog } from "alchemy_admin/confirm_dialog"
6
- import { dispatchPageDirtyEvent } from "alchemy_admin/components/element_editor"
6
+ import { dispatchPageDirtyEvent } from "alchemy_admin/utils/dispatch_page_dirty_event"
7
7
 
8
8
  export class DeleteElementButton extends HTMLElement {
9
- constructor() {
10
- super()
9
+ connectedCallback() {
11
10
  this.button?.addEventListener("click", this)
12
11
  }
13
12
 
13
+ disconnectedCallback() {
14
+ this.button?.removeEventListener("click", this)
15
+ }
16
+
14
17
  async handleEvent() {
15
18
  const confirmed = await openConfirmDialog(this.message)
16
19
  if (confirmed) {