spree_admin 5.2.0.rc2 → 5.2.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 (188) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/stylesheets/spree/admin/application.scss +1 -1
  3. data/app/assets/stylesheets/spree/admin/components/_alerts.scss +1 -1
  4. data/app/assets/stylesheets/spree/admin/components/_buttons.scss +5 -4
  5. data/app/assets/stylesheets/spree/admin/components/_dialogs.scss +0 -1
  6. data/app/assets/stylesheets/spree/admin/components/_dropdowns.scss +6 -0
  7. data/app/assets/stylesheets/spree/admin/components/_main.scss +2 -233
  8. data/app/assets/stylesheets/spree/admin/components/_sidebar.scss +693 -0
  9. data/app/assets/stylesheets/spree/admin/components/_tables.scss +2 -2
  10. data/app/assets/stylesheets/spree/admin/components/_variants_form.scss +1 -2
  11. data/app/assets/stylesheets/spree/admin/global/_variables.scss +15 -12
  12. data/app/assets/stylesheets/spree/admin/shared/_base.scss +9 -3
  13. data/app/assets/stylesheets/spree/admin/shared/_forms.scss +5 -6
  14. data/app/assets/stylesheets/spree/admin/views/_dashboard.scss +14 -0
  15. data/app/controllers/spree/admin/admin_users_controller.rb +0 -2
  16. data/app/controllers/spree/admin/checkouts_controller.rb +1 -4
  17. data/app/controllers/spree/admin/coupon_codes_controller.rb +0 -14
  18. data/app/controllers/spree/admin/customer_returns_controller.rb +0 -13
  19. data/app/controllers/spree/admin/digital_assets_controller.rb +2 -2
  20. data/app/controllers/spree/admin/exports_controller.rb +2 -9
  21. data/app/controllers/spree/admin/gift_cards_controller.rb +7 -14
  22. data/app/controllers/spree/admin/integrations_controller.rb +1 -1
  23. data/app/controllers/spree/admin/invitations_controller.rb +0 -2
  24. data/app/controllers/spree/admin/metafields_controller.rb +1 -1
  25. data/app/controllers/spree/admin/oauth_applications_controller.rb +0 -10
  26. data/app/controllers/spree/admin/option_types_controller.rb +0 -10
  27. data/app/controllers/spree/admin/orders_controller.rb +1 -1
  28. data/app/controllers/spree/admin/page_blocks_controller.rb +1 -1
  29. data/app/controllers/spree/admin/pages_controller.rb +1 -1
  30. data/app/controllers/spree/admin/payment_methods_controller.rb +1 -11
  31. data/app/controllers/spree/admin/policies_controller.rb +4 -0
  32. data/app/controllers/spree/admin/posts_controller.rb +2 -10
  33. data/app/controllers/spree/admin/promotion_actions_controller.rb +1 -1
  34. data/app/controllers/spree/admin/promotion_rules_controller.rb +1 -1
  35. data/app/controllers/spree/admin/promotions_controller.rb +1 -1
  36. data/app/controllers/spree/admin/properties_controller.rb +0 -12
  37. data/app/controllers/spree/admin/reports_controller.rb +1 -1
  38. data/app/controllers/spree/admin/resource_controller.rb +27 -17
  39. data/app/controllers/spree/admin/return_authorizations_controller.rb +0 -10
  40. data/app/controllers/spree/admin/shipping_methods_controller.rb +4 -0
  41. data/app/controllers/spree/admin/stock_items_controller.rb +8 -11
  42. data/app/controllers/spree/admin/stock_locations_controller.rb +1 -1
  43. data/app/controllers/spree/admin/stock_transfers_controller.rb +0 -10
  44. data/app/controllers/spree/admin/store_credits_controller.rb +35 -35
  45. data/app/controllers/spree/admin/taxonomies_controller.rb +0 -10
  46. data/app/controllers/spree/admin/taxons_controller.rb +1 -1
  47. data/app/controllers/spree/admin/themes_controller.rb +6 -2
  48. data/app/controllers/spree/admin/translations_controller.rb +1 -1
  49. data/app/controllers/spree/admin/users_controller.rb +7 -17
  50. data/app/controllers/spree/admin/webhooks_subscribers_controller.rb +0 -10
  51. data/app/controllers/spree/admin/zones_controller.rb +0 -7
  52. data/app/helpers/spree/admin/base_helper.rb +1 -1
  53. data/app/helpers/spree/admin/drawer_helper.rb +6 -6
  54. data/app/helpers/spree/admin/dropdown_helper.rb +26 -16
  55. data/app/helpers/spree/admin/modal_helper.rb +2 -0
  56. data/app/helpers/spree/admin/navigation_helper.rb +47 -4
  57. data/app/helpers/spree/admin/orders_filters_helper.rb +3 -0
  58. data/app/helpers/spree/admin/promotion_actions_helper.rb +1 -1
  59. data/app/helpers/spree/admin/promotion_rules_helper.rb +1 -1
  60. data/app/helpers/spree/admin/translations_helper.rb +1 -1
  61. data/app/javascript/spree/admin/application.js +2 -1
  62. data/app/javascript/spree/admin/controllers/dropdown_controller.js +85 -14
  63. data/app/javascript/spree/admin/controllers/sidebar_controller.js +231 -0
  64. data/app/javascript/spree/admin/controllers/tooltip_controller.js +84 -31
  65. data/app/models/spree/admin/form_builder.rb +76 -17
  66. data/app/models/spree/admin/navigation/builder.rb +82 -0
  67. data/app/models/spree/admin/navigation/item.rb +177 -0
  68. data/app/models/spree/admin/navigation.rb +193 -0
  69. data/app/views/layouts/spree/admin.html.erb +1 -1
  70. data/app/views/spree/admin/assets/edit.html.erb +1 -1
  71. data/app/views/spree/admin/custom_domains/_custom_domains.html.erb +1 -1
  72. data/app/views/spree/admin/custom_domains/_form.html.erb +2 -14
  73. data/app/views/spree/admin/digital_assets/_table.html.erb +1 -1
  74. data/app/views/spree/admin/gift_cards/_filters.html.erb +25 -16
  75. data/app/views/spree/admin/gift_cards/index.html.erb +1 -1
  76. data/app/views/spree/admin/integrations/index.html.erb +20 -8
  77. data/app/views/spree/admin/invitations/new.html.erb +2 -1
  78. data/app/views/spree/admin/metafield_definitions/_filters.html.erb +1 -1
  79. data/app/views/spree/admin/newsletter_subscribers/_filters.html.erb +1 -1
  80. data/app/views/spree/admin/newsletter_subscribers/_table_header.html.erb +2 -2
  81. data/app/views/spree/admin/oauth_applications/_table_header.html.erb +1 -1
  82. data/app/views/spree/admin/orders/_customer.html.erb +3 -3
  83. data/app/views/spree/admin/orders/_filters.html.erb +33 -25
  84. data/app/views/spree/admin/orders/_header.html.erb +0 -5
  85. data/app/views/spree/admin/orders/_list.html.erb +3 -3
  86. data/app/views/spree/admin/orders/_table_filter_dropdown.html.erb +1 -1
  87. data/app/views/spree/admin/page_blocks/edit.html.erb +3 -3
  88. data/app/views/spree/admin/page_blocks/forms/_image.html.erb +2 -5
  89. data/app/views/spree/admin/page_builder/_add_block.html.erb +1 -1
  90. data/app/views/spree/admin/page_builder/_header.html.erb +1 -1
  91. data/app/views/spree/admin/page_builder/_pages_dropdown.html.erb +2 -2
  92. data/app/views/spree/admin/page_builder/_sidebar_block.html.erb +1 -1
  93. data/app/views/spree/admin/page_builder/_sidebar_colors.html.erb +2 -2
  94. data/app/views/spree/admin/page_builder/_sidebar_fonts.html.erb +3 -3
  95. data/app/views/spree/admin/page_builder/_sidebar_section.html.erb +1 -1
  96. data/app/views/spree/admin/page_links/_form.html.erb +4 -13
  97. data/app/views/spree/admin/page_links/_list.html.erb +1 -1
  98. data/app/views/spree/admin/page_links/edit.html.erb +1 -1
  99. data/app/views/spree/admin/page_sections/edit.html.erb +3 -3
  100. data/app/views/spree/admin/page_sections/forms/_header.html.erb +0 -2
  101. data/app/views/spree/admin/page_sections/new.html.erb +1 -1
  102. data/app/views/spree/admin/pages/_table_header.html.erb +3 -3
  103. data/app/views/spree/admin/payment_methods/index.html.erb +5 -1
  104. data/app/views/spree/admin/payments/_payment.html.erb +7 -0
  105. data/app/views/spree/admin/policies/_filters.html.erb +1 -1
  106. data/app/views/spree/admin/posts/_form.html.erb +1 -4
  107. data/app/views/spree/admin/posts/filters.html.erb +18 -8
  108. data/app/views/spree/admin/products/_bulk_operations.html.erb +2 -2
  109. data/app/views/spree/admin/products/_filters.html.erb +17 -6
  110. data/app/views/spree/admin/products/_table_filter_dropdown.html.erb +1 -1
  111. data/app/views/spree/admin/products/edit.html.erb +0 -2
  112. data/app/views/spree/admin/products/form/_status.html.erb +0 -3
  113. data/app/views/spree/admin/products/form/_variants.html.erb +1 -1
  114. data/app/views/spree/admin/profile/edit.html.erb +9 -61
  115. data/app/views/spree/admin/promotions/_filters.html.erb +23 -13
  116. data/app/views/spree/admin/promotions/_table_filter_dropdown.html.erb +1 -1
  117. data/app/views/spree/admin/promotions/_table_header.html.erb +1 -1
  118. data/app/views/spree/admin/promotions/form/_kind.html.erb +4 -4
  119. data/app/views/spree/admin/promotions/form/_settings.html.erb +2 -13
  120. data/app/views/spree/admin/refund_reasons/_table_header.html.erb +1 -1
  121. data/app/views/spree/admin/refunds/_form.html.erb +1 -9
  122. data/app/views/spree/admin/reimbursement_types/_table_header.html.erb +1 -1
  123. data/app/views/spree/admin/return_authorization_reasons/_table_header.html.erb +1 -1
  124. data/app/views/spree/admin/return_authorizations/filters.html.erb +1 -1
  125. data/app/views/spree/admin/roles/index.html.erb +1 -1
  126. data/app/views/spree/admin/shared/_audit_nav.html.erb +2 -0
  127. data/app/views/spree/admin/shared/_calendar_range_picker.html.erb +2 -2
  128. data/app/views/spree/admin/shared/_content_header.html.erb +5 -2
  129. data/app/views/spree/admin/shared/_developers_nav.html.erb +2 -4
  130. data/app/views/spree/admin/shared/_header.html.erb +5 -7
  131. data/app/views/spree/admin/shared/_index_table.html.erb +5 -4
  132. data/app/views/spree/admin/shared/_index_table_options.html.erb +1 -1
  133. data/app/views/spree/admin/shared/_navigation.html.erb +5 -0
  134. data/app/views/spree/admin/shared/_navigation_item.html.erb +64 -0
  135. data/app/views/spree/admin/shared/_new_item_dropdown.html.erb +2 -2
  136. data/app/views/spree/admin/shared/_page_section_image.html.erb +2 -5
  137. data/app/views/spree/admin/shared/_page_section_logo.html.erb +1 -1
  138. data/app/views/spree/admin/shared/_returns_and_refunds_nav.html.erb +2 -3
  139. data/app/views/spree/admin/shared/_shipping_nav.html.erb +3 -2
  140. data/app/views/spree/admin/shared/_sidebar.html.erb +33 -7
  141. data/app/views/spree/admin/shared/_stock_nav.html.erb +6 -3
  142. data/app/views/spree/admin/shared/_tax_nav.html.erb +1 -2
  143. data/app/views/spree/admin/shared/_team_nav.html.erb +2 -3
  144. data/app/views/spree/admin/shared/_user_dropdown.html.erb +29 -19
  145. data/app/views/spree/admin/shared/sidebar/_customers_nav.html.erb +7 -0
  146. data/app/views/spree/admin/shared/sidebar/_orders_nav.html.erb +22 -2
  147. data/app/views/spree/admin/shared/sidebar/_products_nav.html.erb +21 -0
  148. data/app/views/spree/admin/shared/sidebar/_promotions_nav.html.erb +8 -0
  149. data/app/views/spree/admin/shared/sidebar/_returns_nav.html.erb +12 -0
  150. data/app/views/spree/admin/shared/sidebar/_store_dropdown.html.erb +4 -2
  151. data/app/views/spree/admin/shared/sidebar/_store_nav.html.erb +15 -1
  152. data/app/views/spree/admin/shared/sidebar/_storefront_nav.html.erb +25 -3
  153. data/app/views/spree/admin/shared/sortable_tree/_taxonomy.html.erb +1 -1
  154. data/app/views/spree/admin/shipping_categories/_table_header.html.erb +1 -1
  155. data/app/views/spree/admin/shipping_methods/_table_header.html.erb +1 -1
  156. data/app/views/spree/admin/stock_items/_filters.html.erb +18 -8
  157. data/app/views/spree/admin/stock_locations/_table_header.html.erb +2 -2
  158. data/app/views/spree/admin/stock_locations/_table_row.html.erb +1 -1
  159. data/app/views/spree/admin/stock_transfers/_filters.html.erb +19 -9
  160. data/app/views/spree/admin/store_credit_categories/index.html.erb +1 -1
  161. data/app/views/spree/admin/store_credits/_list.html.erb +3 -3
  162. data/app/views/spree/admin/storefront/edit.html.erb +2 -14
  163. data/app/views/spree/admin/stores/form/_basic.html.erb +2 -8
  164. data/app/views/spree/admin/stores/form/_checkout.html.erb +2 -2
  165. data/app/views/spree/admin/stores/form/_checkout_links.html.erb +1 -1
  166. data/app/views/spree/admin/stores/form/_emails.html.erb +1 -1
  167. data/app/views/spree/admin/tax_categories/_table_header.html.erb +2 -2
  168. data/app/views/spree/admin/tax_rates/_form.html.erb +1 -10
  169. data/app/views/spree/admin/tax_rates/_table_header.html.erb +2 -2
  170. data/app/views/spree/admin/taxonomies/_table_header.html.erb +1 -1
  171. data/app/views/spree/admin/taxons/_form.html.erb +2 -9
  172. data/app/views/spree/admin/themes/_theme.html.erb +1 -1
  173. data/app/views/spree/admin/translations/translation_rows/_permalink_field_row.html.erb +1 -12
  174. data/app/views/spree/admin/users/_filters.html.erb +26 -17
  175. data/app/views/spree/admin/users/index.html.erb +1 -1
  176. data/config/initializers/spree_admin_navigation.rb +510 -0
  177. data/config/locales/en.yml +6 -0
  178. data/lib/generators/spree/admin/scaffold/templates/controller.rb.tt +3 -1
  179. data/lib/generators/spree/admin/scaffold/templates/views/_filters.html.erb.tt +1 -1
  180. data/lib/generators/spree/admin/scaffold/templates/views/_table_header.html.erb.tt +2 -2
  181. data/lib/generators/spree/admin/scaffold/templates/views/_table_row.html.erb.tt +8 -6
  182. data/lib/spree/admin/engine.rb +64 -2
  183. data/lib/spree/admin/runtime_configuration.rb +1 -0
  184. data/lib/spree/admin.rb +20 -0
  185. metadata +17 -15
  186. data/app/assets/stylesheets/spree/admin/components/_offcanvas.scss +0 -26
  187. data/app/javascript/spree/admin/helpers/canvas.js +0 -29
  188. data/app/views/spree/admin/shared/_offcanvas_nav.html.erb +0 -12
@@ -24,13 +24,18 @@ module Spree
24
24
  # - :required [Boolean] whether field is required
25
25
  # - :help [String] help text to display below the field
26
26
  # - :help_bubble [String] help bubble text
27
+ # - :prepend [String] text to prepend before the input field
28
+ # - :append [String] text to append after the input field
27
29
  # @return [String] HTML string containing the complete form group
28
30
  def spree_text_field(method, options = {})
29
31
  options[:class] ||= 'form-control'
32
+ prepend = options.delete(:prepend)
33
+ append = options.delete(:append)
34
+
30
35
  @template.content_tag(:div, class: 'form-group') do
31
36
  @template.label(@object_name, method, get_label(method, options)) +
32
- @template.text_field(@object_name, method, objectify_options(options)) +
33
- @template.error_message_on(@object_name, method) + field_help(method, options)
37
+ wrap_with_input_group(@template.text_field(@object_name, method, objectify_options(options)), prepend, append) +
38
+ @template.error_message_on(@object_name, method) + spree_field_help(method, options.merge(class: 'form-text mt-2'))
34
39
  end
35
40
  end
36
41
 
@@ -41,10 +46,13 @@ module Spree
41
46
  # @return [String] HTML string containing the complete form group
42
47
  def spree_number_field(method, options = {})
43
48
  options[:class] ||= 'form-control'
49
+ prepend = options.delete(:prepend)
50
+ append = options.delete(:append)
51
+
44
52
  @template.content_tag(:div, class: 'form-group') do
45
53
  @template.label(@object_name, method, get_label(method, options)) +
46
- @template.number_field(@object_name, method, objectify_options(options)) +
47
- @template.error_message_on(@object_name, method) + field_help(method, options)
54
+ wrap_with_input_group(@template.number_field(@object_name, method, objectify_options(options)), prepend, append) +
55
+ @template.error_message_on(@object_name, method) + spree_field_help(method, options.merge(class: 'form-text mt-2'))
48
56
  end
49
57
  end
50
58
 
@@ -58,7 +66,7 @@ module Spree
58
66
  @template.content_tag(:div, class: 'form-group') do
59
67
  @template.label(@object_name, method, get_label(method, options)) +
60
68
  @template.email_field(@object_name, method, objectify_options(options)) +
61
- @template.error_message_on(@object_name, method) + field_help(method, options)
69
+ @template.error_message_on(@object_name, method) + spree_field_help(method, options.merge(class: 'form-text mt-2'))
62
70
  end
63
71
  end
64
72
 
@@ -72,7 +80,7 @@ module Spree
72
80
  @template.content_tag(:div, class: 'form-group') do
73
81
  @template.label(@object_name, method, get_label(method, options)) +
74
82
  @template.date_field(@object_name, method, objectify_options(options)) +
75
- @template.error_message_on(@object_name, method) + field_help(method, options)
83
+ @template.error_message_on(@object_name, method) + spree_field_help(method, options.merge(class: 'form-text mt-2'))
76
84
  end
77
85
  end
78
86
 
@@ -86,7 +94,7 @@ module Spree
86
94
  @template.content_tag(:div, class: 'form-group') do
87
95
  @template.label(@object_name, method, get_label(method, options)) +
88
96
  @template.datetime_field(@object_name, method, objectify_options(options)) +
89
- @template.error_message_on(@object_name, method) + field_help(method, options)
97
+ @template.error_message_on(@object_name, method) + spree_field_help(method, options.merge(class: 'form-text mt-2'))
90
98
  end
91
99
  end
92
100
 
@@ -107,7 +115,7 @@ module Spree
107
115
 
108
116
  @template.label(@object_name, method, get_label(method, options)) +
109
117
  @template.text_area(@object_name, method, objectify_options(options)) +
110
- @template.error_message_on(@object_name, method) + field_help(method, options)
118
+ @template.error_message_on(@object_name, method) + spree_field_help(method, options.merge(class: 'form-text mt-2'))
111
119
  end
112
120
  end
113
121
 
@@ -122,7 +130,7 @@ module Spree
122
130
  @template.content_tag(:div, class: 'trix-container') do
123
131
  @template.rich_text_area(@object_name, method, objectify_options(options))
124
132
  end +
125
- @template.error_message_on(@object_name, method) + field_help(method, options)
133
+ @template.error_message_on(@object_name, method) + spree_field_help(method, options.merge(class: 'form-text mt-2'))
126
134
  end
127
135
  end
128
136
 
@@ -147,7 +155,7 @@ module Spree
147
155
  @template.content_tag(:div, class: 'form-group') do
148
156
  @template.label(@object_name, method, get_label(method, options)) +
149
157
  @template.select(@object_name, method, choices, objectify_options(options), html_options, &block) +
150
- @template.error_message_on(@object_name, method) + field_help(method, options)
158
+ @template.error_message_on(@object_name, method) + spree_field_help(method, options.merge(class: 'form-text mt-2'))
151
159
  end
152
160
  end
153
161
 
@@ -173,7 +181,7 @@ module Spree
173
181
  @template.content_tag(:div, class: 'form-group') do
174
182
  @template.label(@object_name, method, get_label(method, options)) +
175
183
  @template.collection_select(@object_name, method, collection, value_method, text_method, objectify_options(options), html_options) +
176
- @template.error_message_on(@object_name, method) + field_help(method, options)
184
+ @template.error_message_on(@object_name, method) + spree_field_help(method, options.merge(class: 'form-text mt-2'))
177
185
  end
178
186
  end
179
187
 
@@ -187,7 +195,7 @@ module Spree
187
195
  @template.content_tag(:div, class: 'custom-control custom-checkbox') do
188
196
  @template.check_box(@object_name, method, objectify_options(options.merge(class: 'custom-control-input'))) +
189
197
  @template.label(@object_name, method, get_label(method, options), class: 'custom-control-label')
190
- end + @template.error_message_on(@object_name, method) + field_help(method, options)
198
+ end + @template.error_message_on(@object_name, method) + spree_field_help(method, options.merge!(class: 'form-text mt-2 ml-4'))
191
199
  end
192
200
  end
193
201
 
@@ -204,22 +212,43 @@ module Spree
204
212
  @template.content_tag(:div, class: 'custom-control custom-radio') do
205
213
  @template.radio_button(@object_name, method, tag_value, objectify_options(options.merge(class: 'custom-control-input'))) +
206
214
  @template.label(@object_name, method, get_label(method, options), class: 'custom-control-label', for: options[:id])
207
- end + @template.error_message_on(@object_name, method) + field_help(method, options)
215
+ end + @template.error_message_on(@object_name, method) + spree_field_help(method, options.merge(class: 'form-text mt-2'))
208
216
  end
209
217
  end
210
218
 
219
+ # Create a direct file upload field with Spree form styling
220
+ #
221
+ # @param method [Symbol] the field name
222
+ # @param options [Hash] field options
223
+ # @option options [Boolean] :crop whether to crop the image
224
+ # @option options [Boolean] :auto_submit whether to auto-submit the form when the file is uploaded
225
+ # @option options [Boolean] :can_delete whether to show the delete button
226
+ # @option options [Boolean] :inline whether to display the uploader inline
227
+ # @option options [Integer] :height the height of the uploader
228
+ # @option options [Integer] :width the width of the uploader
229
+ # @option options [Array] :allowed_file_types the allowed file types, defaults to image types
230
+ # @return [String] HTML string containing the complete form group with direct file upload field
231
+ def spree_file_field(method, options = {})
232
+ @template.content_tag(:div, class: 'form-group') do
233
+ @template.label(@object_name, method, get_label(method, options)) +
234
+ @template.render('active_storage/upload_form', form: self, field_name: method, **options) +
235
+ @template.error_message_on(@object_name, method) + spree_field_help(method, options.merge(class: 'form-text mt-2'))
236
+ end
237
+ end
238
+
239
+ private
240
+
211
241
  # Generate help text for a field
212
242
  #
213
243
  # @param _method [Symbol] the field name (unused but kept for consistency)
214
244
  # @param options [Hash] field options
215
245
  # @option options [String] :help help text to display
216
246
  # @return [String] HTML string containing the help text or empty string
217
- def field_help(_method, options = {})
218
- @template.content_tag(:span, options[:help], class: 'form-text mt-2')
247
+ def spree_field_help(_method, options = {})
248
+ options[:class] ||= 'form-text mt-2'
249
+ @template.content_tag(:span, options[:help], class: options[:class])
219
250
  end
220
251
 
221
- private
222
-
223
252
  # Generate the label for a field with required indicator and help bubble
224
253
  #
225
254
  # @param method [Symbol] the field name
@@ -245,6 +274,36 @@ module Spree
245
274
 
246
275
  @template.raw(translated_label + required_label + help_bubble)
247
276
  end
277
+
278
+ # Wrap a field with an input group if prepend or append is specified
279
+ #
280
+ # @param field_html [String] the HTML for the field
281
+ # @param prepend [String, nil] text to prepend before the input field
282
+ # @param append [String, nil] text to append after the input field
283
+ # @return [String] HTML string with input group wrapper or the original field
284
+ def wrap_with_input_group(field_html, prepend = nil, append = nil)
285
+ return field_html if prepend.nil? && append.nil?
286
+
287
+ @template.content_tag(:div, class: 'input-group') do
288
+ prepend_html = if prepend.present?
289
+ @template.content_tag(:div, class: 'input-group-prepend') do
290
+ @template.content_tag(:span, prepend, class: 'input-group-text')
291
+ end
292
+ else
293
+ ''.html_safe
294
+ end
295
+
296
+ append_html = if append.present?
297
+ @template.content_tag(:div, class: 'input-group-append') do
298
+ @template.content_tag(:span, append, class: 'input-group-text')
299
+ end
300
+ else
301
+ ''.html_safe
302
+ end
303
+
304
+ prepend_html + field_html + append_html
305
+ end
306
+ end
248
307
  end
249
308
  end
250
309
  end
@@ -0,0 +1,82 @@
1
+ module Spree
2
+ module Admin
3
+ class Navigation
4
+ class Builder
5
+ attr_reader :registry, :parent_item
6
+
7
+ def initialize(registry, parent_item = nil)
8
+ @registry = registry
9
+ @parent_item = parent_item
10
+ end
11
+
12
+ # Add a navigation item
13
+ # If parent_item is set, the item becomes a child
14
+ def add(key, **options, &block)
15
+ # If we have a parent item, set it in the options
16
+ if parent_item
17
+ options[:parent] = parent_item.key
18
+ # Adjust position to be relative to parent
19
+ options[:position] ||= parent_item.children.size * 10
20
+ end
21
+
22
+ item = registry.add(key, **options, &block)
23
+ item
24
+ end
25
+
26
+ # Add a section (group of items)
27
+ def section(key, label: nil, &block)
28
+ section_item = add(key, section_label: label || key.to_s.humanize)
29
+
30
+ if block_given?
31
+ builder = self.class.new(registry, section_item)
32
+ # Support both block styles: |nav| nav.add or just add
33
+ if block.arity > 0
34
+ # Block expects parameter: do |nav| nav.add ... end
35
+ block.call(builder)
36
+ else
37
+ # Block uses implicit self: do add ... end
38
+ builder.instance_eval(&block)
39
+ end
40
+ end
41
+
42
+ section_item
43
+ end
44
+
45
+ # Remove an item
46
+ def remove(key)
47
+ registry.remove(key)
48
+ end
49
+
50
+ # Update an item
51
+ def update(key, **options)
52
+ registry.update(key, **options)
53
+ end
54
+
55
+ # Insert before another item
56
+ def insert_before(target_key, new_key, **options)
57
+ registry.insert_before(target_key, new_key, **options)
58
+ end
59
+
60
+ # Insert after another item
61
+ def insert_after(target_key, new_key, **options)
62
+ registry.insert_after(target_key, new_key, **options)
63
+ end
64
+
65
+ # Move an item
66
+ def move(key, **position_options)
67
+ registry.move(key, **position_options)
68
+ end
69
+
70
+ # Replace an item
71
+ def replace(key, **options, &block)
72
+ registry.replace(key, **options, &block)
73
+ end
74
+
75
+ # Reorder items with a block
76
+ def reorder(&block)
77
+ instance_eval(&block) if block_given?
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,177 @@
1
+ module Spree
2
+ module Admin
3
+ class Navigation
4
+ class Item
5
+ attr_accessor :key, :label, :url, :icon, :position, :parent_key,
6
+ :condition, :badge, :badge_class, :tooltip, :target, :data_attributes, :children, :section_label, :active_condition
7
+
8
+ def initialize(key, **options)
9
+ @key = key.to_sym
10
+ @label = options[:label]
11
+ @url = options[:url]
12
+ @icon = options[:icon]
13
+ @position = options[:position] || 999
14
+ @parent_key = options[:parent]
15
+ @active_condition = options[:active]
16
+ @condition = options.key?(:if) ? options[:if] : options[:condition]
17
+ @badge = options[:badge]
18
+ @badge_class = options[:badge_class]
19
+ @tooltip = options[:tooltip]
20
+ @target = options[:target]
21
+ @data_attributes = options[:data_attributes] || {}
22
+ @section_label = options[:section_label]
23
+ @children = []
24
+ end
25
+
26
+ # Check if this item should be visible for the given user/context
27
+ # @param user_or_context [Object] Either a user object or a view context
28
+ def visible?(user_or_context = nil)
29
+ return true if condition.nil?
30
+
31
+ if condition.respond_to?(:call)
32
+ # If we have a view context with instance_exec, use it to evaluate the condition
33
+ # This allows access to can? and other helper methods
34
+ if user_or_context.respond_to?(:instance_exec)
35
+ user_or_context.instance_exec(&condition)
36
+ else
37
+ # Otherwise, call with the user object
38
+ condition.call(user_or_context)
39
+ end
40
+ else
41
+ condition
42
+ end
43
+ end
44
+
45
+ # Check if this item is active based on current path
46
+ # @param current_path [String] The current request path
47
+ # @param context [Object] View context with access to route helpers
48
+ def active?(current_path, context = nil)
49
+ # Use custom active condition if provided (most flexible)
50
+ if active_condition.respond_to?(:call)
51
+ if context&.respond_to?(:instance_exec)
52
+ return context.instance_exec(&active_condition)
53
+ else
54
+ return active_condition.call
55
+ end
56
+ end
57
+
58
+ # Match exact path
59
+ item_url = resolve_url(context)
60
+ return true if item_url && current_path == item_url
61
+
62
+ # Check if any child item is active
63
+ return true if children.any? { |child| child.active?(current_path, context) }
64
+
65
+ # Default: match if path starts with url (handled by active_link_to)
66
+ if item_url
67
+ current_path.start_with?(item_url)
68
+ else
69
+ false
70
+ end
71
+ end
72
+
73
+ # Resolve URL (handles symbols, procs, and strings)
74
+ # @param context [Object] View context with access to route helpers
75
+ def resolve_url(context = nil)
76
+ case url
77
+ when Symbol
78
+ # Try to call the route helper on the context (which has spree routes)
79
+ if context&.respond_to?(url)
80
+ context.send(url)
81
+ elsif context&.respond_to?(:spree)
82
+ context.spree.send(url) rescue url.to_s
83
+ else
84
+ url.to_s
85
+ end
86
+ when Proc
87
+ # Evaluate proc in the context where route helpers are available
88
+ if context&.respond_to?(:instance_exec)
89
+ context.instance_exec(&url)
90
+ else
91
+ url.call
92
+ end
93
+ else
94
+ url
95
+ end
96
+ end
97
+
98
+ # Resolve label (handles i18n keys)
99
+ def resolve_label
100
+ return label unless label.is_a?(String) || label.is_a?(Symbol)
101
+
102
+ # Use Spree.t for translation which handles the spree namespace
103
+ Spree.t(label, default: label.to_s.humanize)
104
+ end
105
+
106
+ # Compute badge value
107
+ # @param view_context [Object] View context with access to helper methods
108
+ def badge_value(view_context = nil)
109
+ return nil unless badge
110
+
111
+ if badge.respond_to?(:call)
112
+ # Evaluate badge in view context if available (for access to helpers)
113
+ if view_context&.respond_to?(:instance_exec)
114
+ view_context.instance_exec(&badge)
115
+ else
116
+ badge.call
117
+ end
118
+ else
119
+ badge
120
+ end
121
+ end
122
+
123
+ # Check if this is a section header
124
+ def section?
125
+ section_label.present?
126
+ end
127
+
128
+ # Add a child item
129
+ def add_child(item)
130
+ children << item
131
+ item.parent_key = key
132
+ sort_children!
133
+ end
134
+
135
+ # Remove a child item
136
+ def remove_child(key)
137
+ children.reject! { |child| child.key == key }
138
+ end
139
+
140
+ # Sort children by position
141
+ def sort_children!
142
+ children.sort_by! { |child| [child.position, child.key.to_s] }
143
+ end
144
+
145
+ # Deep clone for modifications
146
+ def deep_clone
147
+ cloned = self.class.new(key, to_h)
148
+ cloned.children = children.map(&:deep_clone)
149
+ cloned
150
+ end
151
+
152
+ # Convert to hash
153
+ def to_h
154
+ {
155
+ label: label,
156
+ url: url,
157
+ icon: icon,
158
+ position: position,
159
+ parent: parent_key,
160
+ active: active_condition,
161
+ condition: condition,
162
+ badge: badge,
163
+ badge_class: badge_class,
164
+ tooltip: tooltip,
165
+ target: target,
166
+ data_attributes: data_attributes,
167
+ section_label: section_label
168
+ }
169
+ end
170
+
171
+ def inspect
172
+ "#<Spree::Admin::Navigation::Item key=#{key} label=#{label} children=#{children.size}>"
173
+ end
174
+ end
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,193 @@
1
+ module Spree
2
+ module Admin
3
+ class Navigation
4
+ attr_reader :items, :context
5
+
6
+ def initialize(context)
7
+ @context = context
8
+ @items = {}
9
+ end
10
+
11
+ # Add a navigation item
12
+ def add(key, **options, &block)
13
+ key = key.to_sym
14
+ item = Item.new(key, **options)
15
+
16
+ @items[key] = item
17
+
18
+ # If block provided, it's for children
19
+ if block_given?
20
+ builder = Builder.new(self, item)
21
+ # Support both block styles: |nav| nav.add or just add
22
+ if block.arity > 0
23
+ # Block expects parameter: do |nav| nav.add ... end
24
+ block.call(builder)
25
+ else
26
+ # Block uses implicit self: do add ... end
27
+ builder.instance_eval(&block)
28
+ end
29
+ end
30
+
31
+ sort_items!
32
+ item
33
+ end
34
+
35
+ # Remove a navigation item
36
+ def remove(key)
37
+ key = key.to_sym
38
+ removed = @items.delete(key)
39
+
40
+ # Also remove from any parent's children
41
+ @items.each_value do |item|
42
+ item.remove_child(key)
43
+ end
44
+
45
+ removed
46
+ end
47
+
48
+ # Update an existing navigation item
49
+ def update(key, **options)
50
+ key = key.to_sym
51
+ item = @items[key]
52
+
53
+ return nil unless item
54
+
55
+ options.each do |attr, value|
56
+ item.send("#{attr}=", value) if item.respond_to?("#{attr}=")
57
+ end
58
+
59
+ sort_items!
60
+ item
61
+ end
62
+
63
+ # Find a navigation item
64
+ def find(key)
65
+ @items[key.to_sym]
66
+ end
67
+
68
+ # Check if item exists
69
+ def exists?(key)
70
+ @items.key?(key.to_sym)
71
+ end
72
+
73
+ # Insert item before another item
74
+ def insert_before(target_key, new_key, **options)
75
+ target = find(target_key)
76
+ return nil unless target
77
+
78
+ new_position = target.position - 1
79
+ add(new_key, **options.merge(position: new_position))
80
+ end
81
+
82
+ # Insert item after another item
83
+ def insert_after(target_key, new_key, **options)
84
+ target = find(target_key)
85
+ return nil unless target
86
+
87
+ new_position = target.position + 1
88
+ add(new_key, **options.merge(position: new_position))
89
+ end
90
+
91
+ # Move item to a new position
92
+ def move(key, position: nil, before: nil, after: nil)
93
+ item = find(key)
94
+ return nil unless item
95
+
96
+ if before
97
+ target = find(before)
98
+ item.position = target.position - 1 if target
99
+ elsif after
100
+ target = find(after)
101
+ item.position = target.position + 1 if target
102
+ elsif position == :first
103
+ item.position = -999
104
+ elsif position == :last
105
+ item.position = 999
106
+ elsif position.is_a?(Integer)
107
+ item.position = position
108
+ end
109
+
110
+ sort_items!
111
+ item
112
+ end
113
+
114
+ # Replace an item
115
+ def replace(key, **options, &block)
116
+ remove(key)
117
+ add(key, **options, &block)
118
+ end
119
+
120
+ # Get all root items (items without a parent)
121
+ def root_items
122
+ @items.values.select { |item| item.parent_key.nil? }.sort_by { |item| [item.position, item.key.to_s] }
123
+ end
124
+
125
+ # Get all items that are visible to the user
126
+ def visible_items(user = nil, parent_key = nil)
127
+ items_to_filter = if parent_key
128
+ find(parent_key)&.children || []
129
+ else
130
+ root_items
131
+ end
132
+
133
+ items_to_filter.select { |item| item.visible?(user) }
134
+ end
135
+
136
+ # Build tree structure
137
+ def build_tree
138
+ # First, clear all children
139
+ @items.each_value { |item| item.children.clear }
140
+
141
+ # Then rebuild the tree
142
+ @items.each_value do |item|
143
+ if item.parent_key && (parent = @items[item.parent_key])
144
+ parent.add_child(item)
145
+ end
146
+ end
147
+
148
+ root_items
149
+ end
150
+
151
+ # Clear all items
152
+ def clear
153
+ @items.clear
154
+ end
155
+
156
+ # Get all registered paths (for settings_area? detection)
157
+ def registered_paths(context = nil)
158
+ @items.values.map { |item| item.resolve_url(context) }.compact
159
+ end
160
+
161
+ # Add a section
162
+ def section(key, label: nil, &block)
163
+ # Create a section header item
164
+ section_item = add(key, section_label: label || key.to_s.humanize, position: @items.size * 100)
165
+
166
+ if block_given?
167
+ builder = Builder.new(self, section_item)
168
+ builder.instance_eval(&block)
169
+ end
170
+
171
+ section_item
172
+ end
173
+
174
+ # Deep clone the registry
175
+ def deep_clone
176
+ cloned = self.class.new(context)
177
+ @items.each do |key, item|
178
+ cloned.items[key] = item.deep_clone
179
+ end
180
+ cloned.build_tree
181
+ cloned
182
+ end
183
+
184
+ private
185
+
186
+ def sort_items!
187
+ # Sort items by position, then rebuild tree
188
+ @items = @items.sort_by { |_key, item| [item.position, item.key.to_s] }.to_h
189
+ build_tree
190
+ end
191
+ end
192
+ end
193
+ end
@@ -6,7 +6,7 @@
6
6
  </head>
7
7
 
8
8
  <body class="admin min-vh-100 <%= controller_name %> <%= action_name %>"
9
- data-controller="admin"
9
+ data-controller="admin sidebar"
10
10
  data-action="keydown.ctrl+s->admin#save keydown.meta+s->admin#save"
11
11
  >
12
12
  <%= render_admin_partials(:body_start_partials) %>
@@ -2,7 +2,7 @@
2
2
  <%= dialog_header(Spree.t(:edit) + ' ' + Spree.t(:image)) %>
3
3
  <%= form_with model: @asset, url: spree.admin_asset_path(@asset), method: :put, scope: :asset do |f| %>
4
4
  <div class="dialog-body pb-0" data-turbo-permanent id="asset-<%= @asset.key.parameterize %>">
5
- <%= render 'active_storage/upload_form', form: f, field_name: :attachment, width: 200, height: 200, can_delete: false %>
5
+ <%= f.spree_file_field :attachment, width: 200, height: 200, can_delete: false, label: Spree.t(:image) %>
6
6
  </div>
7
7
  <div class="dialog-body" style="min-height: 200px">
8
8
  <%= f.spree_text_area :alt, label: Spree.t(:alt_text), rows: 4 %>
@@ -3,7 +3,7 @@
3
3
  <table class="table">
4
4
  <thead>
5
5
  <tr>
6
- <th scope="col"><%= sort_link search_collection, :name, Spree.t(:name) %></th>
6
+ <th scope="col"><%= sort_link @search, :name, Spree.t(:name) %></th>
7
7
  <th scope="col"><%= Spree.t(:active) %>?</th>
8
8
  <th scope="col"><%= Spree.t(:default) %>?</th>
9
9
  <th scope="col"></th>