alchemy_cms 8.2.7 → 8.3.1

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 (202) 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/link_dialog/anchor_tab.rb +1 -1
  44. data/app/components/alchemy/admin/link_dialog/file_tab.rb +1 -1
  45. data/app/components/alchemy/admin/page_node.html.erb +1 -20
  46. data/app/components/alchemy/admin/page_publication_fields.html.erb +30 -0
  47. data/app/components/alchemy/admin/page_publication_fields.rb +18 -0
  48. data/app/components/alchemy/admin/page_status_indicators.html.erb +29 -0
  49. data/app/components/alchemy/admin/page_status_indicators.rb +9 -0
  50. data/app/components/alchemy/admin/publish_element_button.html.erb +12 -4
  51. data/app/components/alchemy/ingredients/headline_editor.rb +1 -1
  52. data/app/controllers/alchemy/admin/dashboard/widgets_controller.rb +21 -0
  53. data/app/controllers/alchemy/admin/dashboard_controller.rb +3 -12
  54. data/app/controllers/alchemy/admin/pages_controller.rb +6 -5
  55. data/app/controllers/alchemy/pages_controller.rb +5 -4
  56. data/app/helpers/alchemy/elements_block_helper.rb +1 -0
  57. data/app/javascript/alchemy_admin/components/auto_submit.js +15 -9
  58. data/app/javascript/alchemy_admin/components/char_counter.js +17 -7
  59. data/app/javascript/alchemy_admin/components/clipboard_button.js +2 -6
  60. data/app/javascript/alchemy_admin/components/color_select.js +13 -4
  61. data/app/javascript/alchemy_admin/components/datepicker.js +11 -14
  62. data/app/javascript/alchemy_admin/components/dialog_link.js +5 -2
  63. data/app/javascript/alchemy_admin/components/element_editor/delete_element_button.js +6 -3
  64. data/app/javascript/alchemy_admin/components/element_editor.js +45 -28
  65. data/app/javascript/alchemy_admin/components/element_select.js +7 -4
  66. data/app/javascript/alchemy_admin/components/elements_window.js +38 -31
  67. data/app/javascript/alchemy_admin/components/elements_window_handle.js +7 -3
  68. data/app/javascript/alchemy_admin/components/file_editor.js +5 -2
  69. data/app/javascript/alchemy_admin/components/ingredient_group.js +6 -4
  70. data/app/javascript/alchemy_admin/components/link_buttons/link_button.js +1 -2
  71. data/app/javascript/alchemy_admin/components/link_buttons/unlink_button.js +1 -2
  72. data/app/javascript/alchemy_admin/components/link_buttons.js +6 -2
  73. data/app/javascript/alchemy_admin/components/list_filter.js +44 -29
  74. data/app/javascript/alchemy_admin/components/message.js +22 -15
  75. data/app/javascript/alchemy_admin/components/overlay.js +5 -7
  76. data/app/javascript/alchemy_admin/components/page_publication_fields.js +38 -25
  77. data/app/javascript/alchemy_admin/components/picture_description_select.js +5 -2
  78. data/app/javascript/alchemy_admin/components/picture_editor.js +5 -10
  79. data/app/javascript/alchemy_admin/components/picture_thumbnail.js +4 -5
  80. data/app/javascript/alchemy_admin/components/preview_window.js +5 -10
  81. data/app/javascript/alchemy_admin/components/publish_page_button.js +2 -5
  82. data/app/javascript/alchemy_admin/components/remote_select.js +53 -23
  83. data/app/javascript/alchemy_admin/components/select.js +169 -26
  84. data/app/javascript/alchemy_admin/components/sortable_elements.js +1 -1
  85. data/app/javascript/alchemy_admin/components/spinner.js +11 -11
  86. data/app/javascript/alchemy_admin/components/tags_autocomplete.js +9 -1
  87. data/app/javascript/alchemy_admin/components/tinymce.js +16 -22
  88. data/app/javascript/alchemy_admin/components/uploader/file_upload.js +48 -45
  89. data/app/javascript/alchemy_admin/components/uploader/progress.js +70 -84
  90. data/app/javascript/alchemy_admin/components/uploader.js +71 -46
  91. data/app/javascript/alchemy_admin/dialog.js +3 -0
  92. data/app/javascript/alchemy_admin/hotkeys.js +0 -18
  93. data/app/javascript/alchemy_admin/image_cropper.js +7 -9
  94. data/app/javascript/alchemy_admin/initializer.js +21 -0
  95. data/app/javascript/alchemy_admin/utils/dispatch_page_dirty_event.js +7 -0
  96. data/app/javascript/tinymce/plugins/alchemy_link/index.js +9 -0
  97. data/app/jobs/alchemy/base_job.rb +2 -2
  98. data/app/jobs/alchemy/invalidate_elements_cache_job.rb +33 -0
  99. data/app/models/alchemy/page/page_naming.rb +28 -5
  100. data/app/models/alchemy/page/page_natures.rb +7 -2
  101. data/app/models/alchemy/page/page_scopes.rb +2 -2
  102. data/app/models/alchemy/page/url_path.rb +7 -2
  103. data/app/models/alchemy/page.rb +2 -2
  104. data/app/models/alchemy/page_definition.rb +1 -0
  105. data/app/models/alchemy/permissions.rb +1 -1
  106. data/app/models/concerns/alchemy/relatable_resource.rb +8 -0
  107. data/app/services/alchemy/page_finder.rb +88 -0
  108. data/app/stylesheets/alchemy/_custom-properties.scss +6 -4
  109. data/app/stylesheets/alchemy/_mixins.scss +1 -7
  110. data/app/stylesheets/alchemy/_themes.scss +15 -3
  111. data/app/stylesheets/alchemy/admin/_tom-select.scss +241 -0
  112. data/app/stylesheets/alchemy/admin/archive.scss +0 -1
  113. data/app/stylesheets/alchemy/admin/base.scss +0 -19
  114. data/app/stylesheets/alchemy/admin/buttons.scss +1 -0
  115. data/app/stylesheets/alchemy/admin/clipboard.scss +2 -0
  116. data/app/stylesheets/alchemy/admin/dashboard.scss +395 -28
  117. data/app/stylesheets/alchemy/admin/elements.scss +16 -23
  118. data/app/stylesheets/alchemy/admin/filters.scss +4 -1
  119. data/app/stylesheets/alchemy/admin/form_fields.scss +3 -3
  120. data/app/stylesheets/alchemy/admin/forms.scss +126 -94
  121. data/app/stylesheets/alchemy/admin/icons.scss +28 -0
  122. data/app/stylesheets/alchemy/admin/image_library.scss +30 -10
  123. data/app/stylesheets/alchemy/admin/navigation.scss +4 -1
  124. data/app/stylesheets/alchemy/admin/popover.scss +3 -5
  125. data/app/stylesheets/alchemy/admin/resource_info.scss +11 -17
  126. data/app/stylesheets/alchemy/admin/shoelace.scss +8 -0
  127. data/app/stylesheets/alchemy/admin/sitemap.scss +13 -0
  128. data/app/stylesheets/alchemy/admin/tables.scss +32 -3
  129. data/app/stylesheets/alchemy/admin/tags.scss +1 -6
  130. data/app/stylesheets/alchemy/admin/toolbar.scss +0 -1
  131. data/app/stylesheets/alchemy/admin.scss +1 -0
  132. data/app/stylesheets/tinymce/skins/ui/alchemy/skin.scss +0 -4
  133. data/app/stylesheets/tinymce/skins/ui/alchemy-dark/skin.scss +0 -4
  134. data/app/types/alchemy/wildcard_url_type.rb +48 -0
  135. data/app/views/alchemy/_menubar.html.erb +1 -5
  136. data/app/views/alchemy/admin/attachments/edit.html.erb +6 -3
  137. data/app/views/alchemy/admin/dashboard/_dashboard.html.erb +3 -2
  138. data/app/views/alchemy/admin/dashboard/_footer.html.erb +22 -0
  139. data/app/views/alchemy/admin/dashboard/_stats.html.erb +7 -0
  140. data/app/views/alchemy/admin/dashboard/_top.html.erb +4 -12
  141. data/app/views/alchemy/admin/dashboard/_widgets.html.erb +7 -0
  142. data/app/views/alchemy/admin/dashboard/index.html.erb +0 -17
  143. data/app/views/alchemy/admin/dashboard/info.html.erb +1 -62
  144. data/app/views/alchemy/admin/dashboard/widgets/show.html.erb +3 -0
  145. data/app/views/alchemy/admin/elements/_form.html.erb +2 -1
  146. data/app/views/alchemy/admin/elements/_schedule.html.erb +2 -15
  147. data/app/views/alchemy/admin/elements/_schedule_fields.html.erb +2 -0
  148. data/app/views/alchemy/admin/layoutpages/edit.html.erb +6 -3
  149. data/app/views/alchemy/admin/legacy_page_urls/_table.html.erb +0 -1
  150. data/app/views/alchemy/admin/nodes/_page_nodes.html.erb +11 -9
  151. data/app/views/alchemy/admin/pages/_form.html.erb +25 -19
  152. data/app/views/alchemy/admin/pages/_locked_page.html.erb +1 -1
  153. data/app/views/alchemy/admin/pages/_publication_fields.html.erb +2 -32
  154. data/app/views/alchemy/admin/pages/_table.html.erb +1 -18
  155. data/app/views/alchemy/admin/pages/configure.html.erb +2 -2
  156. data/app/views/alchemy/admin/pages/info.html.erb +6 -0
  157. data/app/views/alchemy/admin/pages/unlock.turbo_stream.erb +20 -0
  158. data/app/views/alchemy/admin/pictures/_form.html.erb +1 -1
  159. data/app/views/alchemy/admin/pictures/edit_multiple.html.erb +1 -1
  160. data/app/views/alchemy/admin/resources/_form.html.erb +7 -4
  161. data/app/views/alchemy/admin/resources/edit.html.erb +3 -1
  162. data/app/views/alchemy/admin/resources/new.html.erb +3 -1
  163. data/app/views/alchemy/admin/styleguide/index.html.erb +52 -30
  164. data/app/views/alchemy/admin/translations/_en.js +4 -0
  165. data/app/views/layouts/alchemy/admin.html.erb +3 -3
  166. data/config/importmap.rb +2 -0
  167. data/config/locales/alchemy.en.yml +17 -2
  168. data/config/routes.rb +1 -0
  169. data/lib/alchemy/configuration/class_option.rb +46 -3
  170. data/lib/alchemy/configuration/collection_option.rb +4 -0
  171. data/lib/alchemy/configurations/dashboard.rb +79 -0
  172. data/lib/alchemy/configurations/main.rb +15 -0
  173. data/lib/alchemy/engine.rb +9 -3
  174. data/lib/alchemy/sprockets/skip_builds_compression.rb +33 -0
  175. data/lib/alchemy/test_support/capybara_helpers.rb +17 -0
  176. data/lib/alchemy/test_support/relatable_resource_examples.rb +20 -0
  177. data/lib/alchemy/test_support/rspec_matchers.rb +8 -0
  178. data/lib/alchemy/test_support/shared_publishable_examples.rb +38 -31
  179. data/lib/alchemy/tinymce.rb +1 -1
  180. data/lib/alchemy/version.rb +17 -3
  181. data/vendor/javascript/cropperjs.min.js +1 -1
  182. data/vendor/javascript/flatpickr.min.js +1 -1
  183. data/vendor/javascript/floating-ui.min.js +1 -0
  184. data/vendor/javascript/keymaster.min.js +1 -1
  185. data/vendor/javascript/rails-ujs.min.js +1 -1
  186. data/vendor/javascript/shoelace.min.js +93 -93
  187. data/vendor/javascript/sortable.min.js +1 -1
  188. data/vendor/javascript/tinymce.min.js +5 -1
  189. data/vendor/javascript/tom-select.min.js +1 -0
  190. metadata +58 -19
  191. data/app/javascript/alchemy_admin/components/alchemy_html_element.js +0 -129
  192. data/app/views/alchemy/admin/dashboard/_left_column.html.erb +0 -4
  193. data/app/views/alchemy/admin/dashboard/_right_column.html.erb +0 -9
  194. data/app/views/alchemy/admin/dashboard/widgets/_locked_pages.html.erb +0 -52
  195. data/app/views/alchemy/admin/dashboard/widgets/_recent_pages.html.erb +0 -34
  196. data/app/views/alchemy/admin/dashboard/widgets/_sites.html.erb +0 -25
  197. data/app/views/alchemy/admin/dashboard/widgets/_users.html.erb +0 -21
  198. data/app/views/alchemy/admin/languages/edit.html.erb +0 -1
  199. data/app/views/alchemy/admin/languages/new.html.erb +0 -1
  200. data/app/views/alchemy/admin/pages/unlock.js.erb +0 -20
  201. data/app/views/alchemy/admin/sites/edit.html.erb +0 -1
  202. data/app/views/alchemy/admin/sites/new.html.erb +0 -1
@@ -1,17 +1,17 @@
1
1
  <% content_for :title do %>
2
- <%= "Styleguide" %>
2
+ Styleguide
3
3
  <% end %>
4
4
 
5
5
  <% content_for(:toolbar) do %>
6
6
  <%= render Alchemy::Admin::ToolbarButton.new(
7
7
  icon: :info,
8
8
  label: 'Info',
9
- url: alchemy.dashboard_info_path,
9
+ url: alchemy.admin_styleguide_path,
10
10
  title: 'Info',
11
11
  hotkey: 'alt+i',
12
12
  dialog_options: {
13
13
  title: 'Info',
14
- size: "430x400"
14
+ size: "650x400"
15
15
  },
16
16
  if_permitted_to: [:index, :alchemy_admin_styleguide]
17
17
  ) %>
@@ -29,8 +29,7 @@
29
29
  <form class="simple_form alchemy" action="#" accept-charset="UTF-8" method="post">
30
30
  <div class="input select">
31
31
  <label class="select control-label" for="select">Select</label>
32
- <select class="select" id="select" is="alchemy-select">
33
- <option value="">Please choose</option>
32
+ <select class="select" id="select" is="alchemy-select" placeholder="Please choose">
34
33
  <option value="1">Option 1</option>
35
34
  <option selected value="2">Option 2</option>
36
35
  <option value="3">Option 3</option>
@@ -53,18 +52,16 @@
53
52
 
54
53
  <div class="input">
55
54
  <label class="control-label">Date & Time Pickers</label>
56
- <div class="control_group">
57
- <div class="input-row">
58
- <div class="input-column">
59
- <alchemy-datepicker input-type="date">
60
- <input type="text" id="date_picker" autocomplete="off">
61
- </alchemy-datepicker>
62
- </div>
63
- <div class="input-column">
64
- <alchemy-datepicker input-type="time">
65
- <input type="text" id="time_picker" autocomplete="off">
66
- </alchemy-datepicker>
67
- </div>
55
+ <div class="input-row">
56
+ <div class="input-column">
57
+ <alchemy-datepicker input-type="date">
58
+ <input type="text" id="date_picker" autocomplete="off">
59
+ </alchemy-datepicker>
60
+ </div>
61
+ <div class="input-column">
62
+ <alchemy-datepicker input-type="time">
63
+ <input type="text" id="time_picker" autocomplete="off">
64
+ </alchemy-datepicker>
68
65
  </div>
69
66
  </div>
70
67
  </div>
@@ -139,10 +136,19 @@
139
136
 
140
137
  <h2>Selects</h2>
141
138
 
142
- <div style="display: flex; gap: 1em;">
139
+ <div style="display: flex; gap: 1em; flex-wrap: wrap">
140
+ <div style="width: 20%">
141
+ <h3>Native Select</h3>
142
+ <select class="full_width" autofocus>
143
+ <option value="1">Option 1</option>
144
+ <option value="2">Option 2</option>
145
+ <option value="3">Option 3</option>
146
+ </select>
147
+ </div>
148
+
143
149
  <div style="width: 20%">
144
150
  <h3>Select with Search</h3>
145
- <select is="alchemy-select" class="full_width">
151
+ <select is="alchemy-select" class="full_width" data-allow-clear placeholder="Please choose">
146
152
  <option value="1">Option 1</option>
147
153
  <option value="2">Option 2</option>
148
154
  <option value="3">Option 3</option>
@@ -154,16 +160,33 @@
154
160
  </div>
155
161
 
156
162
  <div style="width: 20%">
157
- <h3>Page Select</h3>
158
- <%= render Alchemy::Admin::PageSelect.new do %>
159
- <input name="page" class="full_width">
160
- <% end %>
163
+ <h3>Multi Select</h3>
164
+ <select is="alchemy-select" multiple class="full_width" placeholder="Pick Options" data-allow-clear>
165
+ <option value="1">Option 1</option>
166
+ <option value="2">Option 2</option>
167
+ <option value="3">Option 3</option>
168
+ <option value="4">Option 4</option>
169
+ <option value="5">Option 5</option>
170
+ <option value="6">Option 6</option>
171
+ <option value="7">Option 7</option>
172
+ </select>
161
173
  </div>
162
174
 
175
+
163
176
  <div style="width: 20%">
164
177
  <h3>Element Select</h3>
165
178
  <%= render Alchemy::Admin::ElementSelect.new(Alchemy::Element.definitions) %>
166
179
  </div>
180
+ </div>
181
+
182
+ <h2>Remote Selects</h2>
183
+ <div style="display: flex; gap: 1em; flex-wrap: wrap">
184
+ <div style="width: 20%">
185
+ <h3>Page Select</h3>
186
+ <%= render Alchemy::Admin::PageSelect.new do %>
187
+ <input name="page" class="full_width">
188
+ <% end %>
189
+ </div>
167
190
 
168
191
  <div style="width: 20%">
169
192
  <h3>Node Select</h3>
@@ -178,14 +201,13 @@
178
201
  <input name="attachment" class="full_width">
179
202
  <% end %>
180
203
  </div>
181
- </div>
182
204
 
183
- <h3>Tags Autocomplete</h3>
184
-
185
- <div style="width: 25%">
186
- <alchemy-tags-autocomplete>
187
- <input type="text" class="full_width" autocomplete="off">
188
- </alchemy-tags-autocomplete>
205
+ <div style="width: 20%">
206
+ <h3>Tags Autocomplete</h3>
207
+ <%= render Alchemy::Admin::TagsAutocomplete.new do %>
208
+ <input type="text" class="full_width" autocomplete="off">
209
+ <% end %>
210
+ </div>
189
211
  </div>
190
212
 
191
213
  <h2>Typography</h2>
@@ -1,4 +1,5 @@
1
1
  Alchemy.translations = {
2
+ Add: "Add",
2
3
  allowed_chars: "of %{count} chars",
3
4
  cancel: "Cancel",
4
5
  cancelled: "Cancelled",
@@ -23,6 +24,9 @@ Alchemy.translations = {
23
24
  "Uploaded bytes exceed file size": "Uploaded bytes exceed file size",
24
25
  "Abort upload": "Abort upload",
25
26
  "Cancel all uploads": "Cancel all uploads",
27
+ "Clear selection": "Clear selection",
28
+ Remove: "Remove",
29
+ "No results found": "No results found",
26
30
  None: "None",
27
31
  "No anchors found": "No anchors found",
28
32
  "Select a page first": "Select a page first",
@@ -45,13 +45,13 @@
45
45
  </noscript>
46
46
  <alchemy-overlay text="<%= Alchemy.t(:please_wait) %>"></alchemy-overlay>
47
47
  <%= render "alchemy/admin/left_menu" %>
48
- <% if current_alchemy_user %>
49
- <%= render "alchemy/admin/top_menu", locked_pages: @locked_pages %>
50
- <% end %>
51
48
  <%= render 'alchemy/admin/partials/flash_notices' %>
52
49
  <div id="main_content">
53
50
  <%= yield %>
54
51
  </div>
52
+ <% if current_alchemy_user %>
53
+ <%= render "alchemy/admin/top_menu", locked_pages: @locked_pages %>
54
+ <% end %>
55
55
  <%= render 'alchemy/admin/uploader/setup' %>
56
56
  <%= yield(:javascripts) %>
57
57
  <% end %>
data/config/importmap.rb CHANGED
@@ -7,10 +7,12 @@ pin "jquery", to: "jquery.min.js", preload: true
7
7
  pin "keymaster", to: "keymaster.min.js", preload: true
8
8
  pin "select2", to: "select2.min.js", preload: true
9
9
  pin "sortablejs", to: "sortable.min.js", preload: true # @1.15.1
10
+ pin "@floating-ui/dom", to: "floating-ui.min.js", preload: true
10
11
  pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
11
12
  pin "shoelace", to: "shoelace.min.js", preload: true
12
13
  pin "@rails/ujs", to: "rails-ujs.min.js", preload: true # @7.1.2
13
14
  pin "tinymce", to: "tinymce.min.js", preload: true
15
+ pin "tom-select", to: "tom-select.min.js", preload: true
14
16
 
15
17
  pin "alchemy_admin", to: "alchemy/alchemy_admin.min.js", preload: true
16
18
  pin "alchemy_admin/components/remote_select", to: "alchemy/alchemy_admin.min.js"
@@ -230,6 +230,12 @@ en:
230
230
  label: "Sorting"
231
231
  by_latest: "Latest"
232
232
  alphabetical: "A-Z"
233
+ dashboard:
234
+ widgets:
235
+ page_counts:
236
+ published: "published"
237
+ user_counts:
238
+ online: "online"
233
239
  elements:
234
240
  toolbar:
235
241
  hide: Hide
@@ -271,7 +277,7 @@ en:
271
277
  "Confirm new password": "Confirm new password"
272
278
  "Copy": "Copy"
273
279
  "Could not load Adobe Flash® Plugin!": "Could not load Adobe Flash® Plugin!"
274
- "Create node on parent:": "Create node on parent:"
280
+ "Create node on parent:": "Attach to menu"
275
281
  "Currently locked pages": "Currently locked pages"
276
282
  "Default language has to be public": "Default language has to be public"
277
283
  "Delete image": "Delete image"
@@ -423,7 +429,7 @@ en:
423
429
  "Create site": "Create a new site"
424
430
  create_language_tree_heading: "Create new homepage"
425
431
  create_menu: "Add a menu"
426
- create_node: "Add a menu node"
432
+ create_node: "Add to menu"
427
433
  create_page: "Create a new subpage"
428
434
  currently_edited_by: "This page is locked by"
429
435
  custom_color: "Custom color"
@@ -571,6 +577,10 @@ en:
571
577
  restricted:
572
578
  "true": "Page is only accessible by members."
573
579
  "false": "Page is accessible by all visitors."
580
+ scheduled:
581
+ public_on: "Page is visible from %{public_on}"
582
+ public_until: "Page is visible until %{public_until}"
583
+ "false": ""
574
584
  page_status_titles:
575
585
  public:
576
586
  "true": "online"
@@ -581,6 +591,9 @@ en:
581
591
  restricted:
582
592
  "true": "restricted"
583
593
  "false": "accessible"
594
+ scheduled:
595
+ "true": "scheduled"
596
+ "false": ""
584
597
  page_status: "Status"
585
598
  page_title: "Title"
586
599
  page_type: "Type"
@@ -834,6 +847,8 @@ en:
834
847
  base:
835
848
  restrict_dependent_destroy:
836
849
  has_many: "There are still %{record} attached to this page. Please remove them first."
850
+ page_layout:
851
+ conflicting_wildcard_param_key: 'has a conflicting wildcard param "%{param}" already used by the "%{conflicting_layout}" page layout'
837
852
  descendants:
838
853
  still_attached_to_nodes: "The following descendant pages are still attached to menu nodes: %{page_names}. Please remove them first."
839
854
  alchemy/element:
data/config/routes.rb CHANGED
@@ -11,6 +11,7 @@ Alchemy::Engine.routes.draw do
11
11
  get "/", to: redirect("#{Alchemy.admin_path}/dashboard"), as: :admin
12
12
  get "/dashboard", to: "admin/dashboard#index", as: :admin_dashboard
13
13
  get "/dashboard/info", to: "admin/dashboard#info", as: :dashboard_info
14
+ get "/dashboard/widgets/:id", to: "admin/dashboard/widgets#show", as: :admin_dashboard_widget
14
15
  get "/help", to: "admin/dashboard#help", as: :help
15
16
  get "/update_check" => "admin/update_checks#show", :as => :update_check
16
17
  get "/leave", to: "admin/base#leave", as: :leave_admin
@@ -5,11 +5,54 @@ require "alchemy/configuration/base_option"
5
5
  module Alchemy
6
6
  class Configuration
7
7
  class ClassOption < BaseOption
8
- def self.value_class
9
- String
8
+ def allowed_classes
9
+ [String, Array]
10
10
  end
11
11
 
12
- def value = @value&.constantize
12
+ def validate(value)
13
+ super
14
+
15
+ if value.is_a?(Array)
16
+ validate_array!(value)
17
+ end
18
+ end
19
+
20
+ def value
21
+ @_cached_value ||= case @value
22
+ when Array
23
+ @value[0] = @value[0]&.constantize
24
+ @value
25
+ when String
26
+ @value&.constantize
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def validate_array!(value)
33
+ @array = value
34
+ has_length_two! && first_value_is_string! && second_value_is_hash!
35
+ end
36
+
37
+ def has_length_two!
38
+ return true if @array.length == 2
39
+
40
+ raise(ConfigurationError.new(name, @array, [
41
+ Class.new(Array) { def self.name = "an Array of length two" }
42
+ ]))
43
+ end
44
+
45
+ def first_value_is_string!
46
+ return true if @array[0].is_a?(String)
47
+
48
+ raise(ConfigurationError.new(name, @array[0], [String]))
49
+ end
50
+
51
+ def second_value_is_hash!
52
+ return true if @array[1].is_a?(Hash)
53
+
54
+ raise(ConfigurationError.new(name, @array[1], [Hash]))
55
+ end
13
56
  end
14
57
  end
15
58
  end
@@ -43,6 +43,10 @@ module Alchemy
43
43
  @value.delete to_item(value)
44
44
  end
45
45
 
46
+ def insert(index, value)
47
+ @value.insert(index, to_item(value))
48
+ end
49
+
46
50
  delegate :join, :[], to: :to_a
47
51
 
48
52
  delegate :clear, :empty?, to: :@value
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ module Configurations
5
+ class Dashboard < Alchemy::Configuration
6
+ option :stats, :collection, item_type: :class, default: [
7
+ [
8
+ "Alchemy::Admin::Dashboard::Widget", {
9
+ id: "PageCounts",
10
+ style: "stat"
11
+ }
12
+ ],
13
+ [
14
+ "Alchemy::Admin::Dashboard::Widget", {
15
+ id: "UserCounts",
16
+ style: "stat"
17
+ }
18
+ ],
19
+ [
20
+ "Alchemy::Admin::Dashboard::Widget", {
21
+ id: "PictureCounts",
22
+ style: "stat"
23
+ }
24
+ ],
25
+ [
26
+ "Alchemy::Admin::Dashboard::Widget", {
27
+ id: "AttachmentCounts",
28
+ style: "stat"
29
+ }
30
+ ]
31
+ ]
32
+
33
+ option :widgets, :collection, item_type: :class, default: [
34
+ [
35
+ "Alchemy::Admin::Dashboard::Widget", {
36
+ id: "LockedPages",
37
+ style: "wide"
38
+ }
39
+ ],
40
+ [
41
+ "Alchemy::Admin::Dashboard::Widget", {
42
+ id: "RecentPages",
43
+ style: "wide"
44
+ }
45
+ ],
46
+ [
47
+ "Alchemy::Admin::Dashboard::Widget", {
48
+ id: "ElementUsage",
49
+ style: "usage",
50
+ loading: "lazy"
51
+ }
52
+ ],
53
+ [
54
+ "Alchemy::Admin::Dashboard::Widget", {
55
+ id: "PageUsage",
56
+ style: "usage",
57
+ loading: "lazy"
58
+ }
59
+ ],
60
+ [
61
+ "Alchemy::Admin::Dashboard::Widget", {
62
+ id: "Sites",
63
+ loading: "lazy",
64
+ condition: -> { helpers.multi_site? }
65
+ }
66
+ ],
67
+ [
68
+ "Alchemy::Admin::Dashboard::Widget", {
69
+ id: "OnlineUsers",
70
+ loading: "lazy",
71
+ condition: -> {
72
+ Alchemy.config.user_class.respond_to?(:logged_in)
73
+ }
74
+ }
75
+ ]
76
+ ]
77
+ end
78
+ end
79
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "alchemy/configuration"
4
+ require "alchemy/configurations/dashboard"
4
5
  require "alchemy/configurations/default_language"
5
6
  require "alchemy/configurations/default_site"
6
7
  require "alchemy/configurations/importmap"
@@ -434,6 +435,16 @@ module Alchemy
434
435
  # The path to the page showing the user they're unauthorized
435
436
  option :unauthorized_path, :string, default: "/"
436
437
 
438
+ # === Admin Users Path
439
+ #
440
+ # The path to the admin users list.
441
+ #
442
+ # == Example
443
+ #
444
+ # "/admin/users"
445
+ #
446
+ option :admin_users_path, :string
447
+
437
448
  # === Edit User Path
438
449
  #
439
450
  # The path to the edit user form.
@@ -453,6 +464,10 @@ module Alchemy
453
464
  # == Example
454
465
  # Alchemy.config.abilities.add("MyCustom::Ability")
455
466
  option :abilities, :collection, item_type: :class
467
+
468
+ # === Dashboard configuration
469
+ #
470
+ configuration :dashboard, Dashboard
456
471
  end
457
472
  end
458
473
  end
@@ -17,7 +17,8 @@ module Alchemy
17
17
  end
18
18
 
19
19
  initializer "alchemy.assets" do |app|
20
- if defined?(Sprockets)
20
+ if defined?(::Sprockets)
21
+ require_relative "sprockets/skip_builds_compression"
21
22
  require_relative "../non_stupid_digest_assets"
22
23
  NonStupidDigestAssets.whitelist += [/^tinymce\//]
23
24
  app.config.assets.precompile << "alchemy_manifest.js"
@@ -25,7 +26,7 @@ module Alchemy
25
26
  end
26
27
 
27
28
  initializer "alchemy.admin_stylesheets" do |app|
28
- if defined?(Sprockets)
29
+ if defined?(::Sprockets)
29
30
  Alchemy.config.admin_stylesheets.each do |stylesheet|
30
31
  app.config.assets.precompile << stylesheet
31
32
  end
@@ -33,7 +34,7 @@ module Alchemy
33
34
  end
34
35
 
35
36
  initializer "alchemy.propshaft" do |app|
36
- if defined?(Propshaft)
37
+ if defined?(::Propshaft)
37
38
  if app.config.assets.server
38
39
  # Monkey-patch Propshaft::Asset to enable access
39
40
  # of TinyMCE assets without a hash digest.
@@ -67,6 +68,11 @@ module Alchemy
67
68
  end
68
69
 
69
70
  if app.config.importmap.sweep_cache
71
+ # The importmap pins resolve to the bundled files in app/assets/builds,
72
+ # so the cache must be swept when those are rebuilt. Without this the
73
+ # cached importmap keeps emitting the previous digest after a rebuild
74
+ # and the asset 404s until the server is restarted.
75
+ watch_paths << Alchemy::Engine.root.join("app/assets/builds")
70
76
  Alchemy.importmap.cache_sweeper(watches: watch_paths)
71
77
  ActiveSupport.on_load(:action_controller_base) do
72
78
  before_action { Alchemy.importmap.cache_sweeper.execute_if_updated }
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "sprockets/sass_compressor"
5
+ rescue LoadError
6
+ # Sprockets::SassCompressor is only defined if sassc-rails is present,
7
+ # which is not the case in all environments (for example, when using Propshaft).
8
+ # In that case, we can skip prepending our patch module.
9
+ end
10
+
11
+ module Alchemy
12
+ module Sprockets
13
+ # Alchemy ships pre-built, already-minified admin CSS in +app/assets/builds+
14
+ # that uses modern CSS syntax (relative colors, +oklch()+ and friends). The
15
+ # legacy SassC/libSass +css_compressor+ — the Sprockets default whenever
16
+ # +sassc-rails+ is present (for example through Solidus) — re-parses every
17
+ # +text/css+ asset as SCSS and raises on that syntax. These files are already
18
+ # minified, so leave them untouched; all other stylesheets still get
19
+ # compressed as before.
20
+ module SkipBuildsCompression
21
+ def call(input)
22
+ builds_path = Alchemy::Engine.root.join("app/assets/builds").to_s
23
+ if input[:filename].to_s.start_with?(builds_path)
24
+ {data: input[:data]}
25
+ else
26
+ super
27
+ end
28
+ end
29
+
30
+ ::Sprockets::SassCompressor.singleton_class.prepend(self)
31
+ end
32
+ end
33
+ end
@@ -25,6 +25,23 @@ module Alchemy
25
25
  end
26
26
  end
27
27
 
28
+ # Tom Select capybara helper
29
+ def tom_select(value, options)
30
+ label = find_label_by_text(options[:from])
31
+
32
+ within label.find(:xpath, "..") do
33
+ find(".ts-control").click
34
+ end
35
+
36
+ # The dropdown is appended to the body, so search the whole page for it.
37
+ within_entire_page do
38
+ find(
39
+ ".ts-dropdown .option",
40
+ text: /#{Regexp.escape(value)}/i, match: :prefer_exact
41
+ ).click
42
+ end
43
+ end
44
+
28
45
  def select2_search(value, options)
29
46
  if options[:from]
30
47
  label = find_label_by_text(options[:from])
@@ -55,4 +55,24 @@ RSpec.shared_examples_for "a relatable resource" do |args|
55
55
  it { is_expected.to be(true) }
56
56
  end
57
57
  end
58
+
59
+ describe "after_touch" do
60
+ let(:related_object) { create(:"alchemy_#{args[:resource_name]}") }
61
+
62
+ context "when related ingredients exist" do
63
+ let!(:ingredient) { create(:"alchemy_ingredient_#{args[:ingredient_type]}", related_object:) }
64
+
65
+ it "enqueues InvalidateElementsCacheJob" do
66
+ expect {
67
+ related_object.touch
68
+ }.to have_enqueued_job(Alchemy::InvalidateElementsCacheJob).with(described_class.name, related_object.id)
69
+ end
70
+ end
71
+
72
+ context "when no related ingredients exist" do
73
+ it "does not enqueue InvalidateElementsCacheJob" do
74
+ expect { related_object.touch }.to_not have_enqueued_job(Alchemy::InvalidateElementsCacheJob)
75
+ end
76
+ end
77
+ end
58
78
  end
@@ -12,3 +12,11 @@ RSpec::Matchers.define :include_language_information_for do |expected|
12
12
  actual[:alchemy_language_id] == expected.id
13
13
  end
14
14
  end
15
+
16
+ # This matcher checks for the presence of an alchemy-select component with a given label.
17
+ RSpec::Matchers.define :have_alchemy_select do |expected|
18
+ match do |session|
19
+ label = session.find(:css, "label", exact_text: expected)
20
+ label.has_sibling?(%(select[is="alchemy-select"]), visible: :all)
21
+ end
22
+ end