spree_admin 5.5.3 → 5.6.0.rc1

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 (146) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/images/vercel-deploy-button.svg +14 -0
  3. data/app/assets/tailwind/spree/admin/components/_alerts.css +5 -1
  4. data/app/assets/tailwind/spree/admin/components/_badges.css +1 -1
  5. data/app/assets/tailwind/spree/admin/components/_breadcrumbs.css +1 -1
  6. data/app/assets/tailwind/spree/admin/components/_buttons.css +1 -1
  7. data/app/assets/tailwind/spree/admin/components/_cards.css +1 -1
  8. data/app/assets/tailwind/spree/admin/components/_dialogs.css +3 -1
  9. data/app/assets/tailwind/spree/admin/components/_dropdowns.css +2 -2
  10. data/app/assets/tailwind/spree/admin/components/_filters.css +1 -1
  11. data/app/assets/tailwind/spree/admin/components/_forms.css +10 -6
  12. data/app/assets/tailwind/spree/admin/components/_layout.css +30 -23
  13. data/app/assets/tailwind/spree/admin/components/_rtl.css +53 -0
  14. data/app/assets/tailwind/spree/admin/components/_search-picker.css +2 -2
  15. data/app/assets/tailwind/spree/admin/components/_tables.css +1 -1
  16. data/app/assets/tailwind/spree/admin/index.css +1 -0
  17. data/app/assets/tailwind/spree/admin/plugins/_tom-select.css +6 -6
  18. data/app/assets/tailwind/spree/admin/plugins/_trix.css +1 -1
  19. data/app/assets/tailwind/spree/admin/plugins/_uppy.css +1 -1
  20. data/app/assets/tailwind/spree/admin/views/_page-builder.css +8 -5
  21. data/app/controllers/concerns/spree/admin/locale_concern.rb +76 -0
  22. data/app/controllers/spree/admin/base_controller.rb +38 -2
  23. data/app/controllers/spree/admin/countries_controller.rb +18 -4
  24. data/app/controllers/spree/admin/exports_controller.rb +7 -0
  25. data/app/controllers/spree/admin/imports_controller.rb +8 -0
  26. data/app/controllers/spree/admin/products_controller.rb +1 -0
  27. data/app/controllers/spree/admin/storefront_controller.rb +100 -0
  28. data/app/controllers/spree/admin/user_sessions_controller.rb +10 -0
  29. data/app/controllers/spree/admin/variants_controller.rb +3 -1
  30. data/app/helpers/spree/admin/base_helper.rb +10 -0
  31. data/app/helpers/spree/admin/channels_helper.rb +17 -0
  32. data/app/helpers/spree/admin/dropdown_helper.rb +14 -0
  33. data/app/helpers/spree/admin/navigation_helper.rb +1 -1
  34. data/app/helpers/spree/admin/onboarding_helper.rb +1 -1
  35. data/app/helpers/spree/admin/orders_helper.rb +9 -34
  36. data/app/helpers/spree/admin/products_helper.rb +1 -1
  37. data/app/helpers/spree/admin/rtl_helper.rb +15 -0
  38. data/app/helpers/spree/admin/shipment_helper.rb +1 -1
  39. data/app/helpers/spree/admin/storefront_helper.rb +46 -0
  40. data/app/helpers/spree/admin/stores_helper.rb +2 -1
  41. data/app/javascript/spree/admin/application.js +2 -0
  42. data/app/javascript/spree/admin/controllers/autocomplete_select_controller.js +4 -0
  43. data/app/javascript/spree/admin/controllers/calendar_range_controller.js +7 -3
  44. data/app/javascript/spree/admin/controllers/display_name_controller.js +18 -0
  45. data/app/javascript/spree/admin/controllers/query_builder_controller.js +39 -28
  46. data/app/javascript/spree/admin/controllers/variants_form_controller.js +49 -1
  47. data/app/javascript/spree/admin/helpers/display_names.js +54 -0
  48. data/app/models/spree/admin/table/column.rb +15 -1
  49. data/app/models/spree/admin/table/filter.rb +23 -16
  50. data/app/models/spree/admin/table/query_builder.rb +11 -2
  51. data/app/views/active_storage/_upload_form.html.erb +1 -1
  52. data/app/views/layouts/spree/admin.html.erb +1 -1
  53. data/app/views/layouts/spree/admin_settings.html.erb +1 -1
  54. data/app/views/layouts/spree/admin_wizard.html.erb +2 -2
  55. data/app/views/layouts/spree/minimal.html.erb +1 -1
  56. data/app/views/spree/admin/admin_users/_audit_log.html.erb +2 -2
  57. data/app/views/spree/admin/admin_users/new.html.erb +1 -1
  58. data/app/views/spree/admin/assets/edit.html.erb +1 -1
  59. data/app/views/spree/admin/channels/_form.html.erb +10 -0
  60. data/app/views/spree/admin/dashboard/_setup_progress.html.erb +4 -4
  61. data/app/views/spree/admin/dashboard/_setup_tasks.html.erb +5 -4
  62. data/app/views/spree/admin/dashboard/_updater.html.erb +4 -4
  63. data/app/views/spree/admin/dashboard/_visits.html.erb +12 -12
  64. data/app/views/spree/admin/dashboard/analytics.html.erb +5 -5
  65. data/app/views/spree/admin/dashboard/setup_tasks/_add_products.html.erb +4 -4
  66. data/app/views/spree/admin/dashboard/setup_tasks/_set_customer_support_email.html.erb +3 -4
  67. data/app/views/spree/admin/dashboard/setup_tasks/_setup_storefront.html.erb +18 -0
  68. data/app/views/spree/admin/gift_card_batches/_form.html.erb +1 -1
  69. data/app/views/spree/admin/import_rows/show.html.erb +2 -2
  70. data/app/views/spree/admin/imports/_footer.html.erb +1 -1
  71. data/app/views/spree/admin/imports/_mapping_form.html.erb +1 -1
  72. data/app/views/spree/admin/imports/show.html.erb +3 -3
  73. data/app/views/spree/admin/integrations/_integration.html.erb +1 -1
  74. data/app/views/spree/admin/integrations/index.html.erb +2 -4
  75. data/app/views/spree/admin/invitations/_invitation.html.erb +1 -1
  76. data/app/views/spree/admin/invitations/new.html.erb +1 -1
  77. data/app/views/spree/admin/invitations/show.html.erb +1 -1
  78. data/app/views/spree/admin/line_items/new.html.erb +1 -1
  79. data/app/views/spree/admin/markets/_form.html.erb +8 -6
  80. data/app/views/spree/admin/metafields/edit.html.erb +1 -1
  81. data/app/views/spree/admin/option_types/_form.html.erb +9 -6
  82. data/app/views/spree/admin/option_types/index.html.erb +2 -3
  83. data/app/views/spree/admin/orders/_customer.html.erb +1 -1
  84. data/app/views/spree/admin/orders/_customer_summary.html.erb +1 -1
  85. data/app/views/spree/admin/orders/_payments.html.erb +1 -1
  86. data/app/views/spree/admin/orders/_shipment.html.erb +7 -2
  87. data/app/views/spree/admin/orders/_shipments.html.erb +1 -1
  88. data/app/views/spree/admin/orders/_user_overview.html.erb +9 -6
  89. data/app/views/spree/admin/orders/return_authorizations/edit.html.erb +1 -1
  90. data/app/views/spree/admin/payment_methods/_form.html.erb +1 -1
  91. data/app/views/spree/admin/payment_methods/index.html.erb +2 -2
  92. data/app/views/spree/admin/payments/_payment.html.erb +1 -1
  93. data/app/views/spree/admin/price_lists/index.html.erb +2 -2
  94. data/app/views/spree/admin/products/form/_inventory.html.erb +19 -0
  95. data/app/views/spree/admin/products/form/_publishing.html.erb +2 -3
  96. data/app/views/spree/admin/products/index.html.erb +1 -2
  97. data/app/views/spree/admin/profile/edit.html.erb +3 -0
  98. data/app/views/spree/admin/promotion_rules/forms/_channel.html.erb +7 -0
  99. data/app/views/spree/admin/promotion_rules/forms/_country.html.erb +1 -1
  100. data/app/views/spree/admin/promotion_rules/forms/_item_total.html.erb +1 -1
  101. data/app/views/spree/admin/promotion_rules/forms/_market.html.erb +7 -0
  102. data/app/views/spree/admin/promotions/_form.html.erb +2 -2
  103. data/app/views/spree/admin/promotions/_sidebar.html.erb +1 -1
  104. data/app/views/spree/admin/promotions/edit.html.erb +2 -2
  105. data/app/views/spree/admin/promotions/form/_kind.html.erb +11 -11
  106. data/app/views/spree/admin/promotions/form/_settings.html.erb +4 -5
  107. data/app/views/spree/admin/reports/new.html.erb +2 -2
  108. data/app/views/spree/admin/shared/_address.html.erb +1 -1
  109. data/app/views/spree/admin/shared/_analytics_loader_skeleton.html.erb +5 -5
  110. data/app/views/spree/admin/shared/_calendar_range_picker.html.erb +11 -1
  111. data/app/views/spree/admin/shared/_content_header.html.erb +2 -2
  112. data/app/views/spree/admin/shared/_edit_resource_links.html.erb +1 -1
  113. data/app/views/spree/admin/shared/_filters_search_bar.html.erb +1 -1
  114. data/app/views/spree/admin/shared/_growth_rate_badge.html.erb +1 -1
  115. data/app/views/spree/admin/shared/_head.html.erb +4 -1
  116. data/app/views/spree/admin/shared/_index_table_options.html.erb +2 -2
  117. data/app/views/spree/admin/shared/_media_asset.html.erb +1 -1
  118. data/app/views/spree/admin/shared/_media_form.html.erb +1 -1
  119. data/app/views/spree/admin/shared/_seo.html.erb +1 -1
  120. data/app/views/spree/admin/shared/_sidebar.html.erb +1 -1
  121. data/app/views/spree/admin/shared/sidebar/_enterprise_edition_notice.html.erb +2 -2
  122. data/app/views/spree/admin/shared/sortable_tree/_taxonomy.html.erb +1 -1
  123. data/app/views/spree/admin/shipping_methods/form/_display.html.erb +2 -2
  124. data/app/views/spree/admin/shipping_methods/form/_estimated_transit_business_days.html.erb +1 -2
  125. data/app/views/spree/admin/shipping_methods/form/_tracking_url.html.erb +1 -1
  126. data/app/views/spree/admin/shipping_methods/form/_zones.html.erb +1 -1
  127. data/app/views/spree/admin/shipping_methods/index.html.erb +1 -1
  128. data/app/views/spree/admin/storefront/show.html.erb +150 -0
  129. data/app/views/spree/admin/stores/form/_basic.html.erb +2 -2
  130. data/app/views/spree/admin/stores/form/_checkout.html.erb +4 -0
  131. data/app/views/spree/admin/stores/form/_emails.html.erb +4 -6
  132. data/app/views/spree/admin/tables/_table.html.erb +3 -2
  133. data/app/views/spree/admin/taxons/_form.html.erb +2 -2
  134. data/app/views/spree/admin/user_sessions/new.html.erb +11 -2
  135. data/app/views/spree/admin/users/_tabs.html.erb +1 -1
  136. data/app/views/spree/admin/variants/_search_result.html.erb +1 -1
  137. data/app/views/spree/admin/variants/_variant.html.erb +1 -1
  138. data/app/views/spree/admin/variants/form/_inventory.html.erb +17 -0
  139. data/app/views/spree/admin/webhook_endpoints/_form.html.erb +2 -2
  140. data/app/views/spree/admin/zones/index.html.erb +1 -1
  141. data/config/i18n-tasks.yml +3 -0
  142. data/config/initializers/spree_admin_navigation.rb +9 -0
  143. data/config/initializers/spree_admin_tables.rb +20 -18
  144. data/config/locales/en.yml +647 -0
  145. data/config/routes.rb +5 -0
  146. metadata +18 -6
@@ -2,14 +2,17 @@ module Spree
2
2
  module Admin
3
3
  class BaseController < Spree::BaseController
4
4
  include Spree::Admin::BreadcrumbConcern
5
+ include Spree::Admin::LocaleConcern
5
6
 
6
7
  layout :choose_layout
7
8
  default_form_builder Spree::Admin::FormBuilder
8
9
 
9
10
  helper 'spree/base'
11
+ helper 'spree/admin/rtl'
10
12
  helper 'spree/admin/navigation'
11
13
  helper 'spree/locale'
12
14
  helper 'spree/currency'
15
+ helper 'spree/localized_names'
13
16
  helper 'spree/addresses'
14
17
  helper 'spree/integrations'
15
18
 
@@ -84,14 +87,47 @@ module Spree
84
87
  @default_locale ||= current_store&.preferred_admin_locale.presence || super
85
88
  end
86
89
 
90
+ # Set the UI language (`I18n.locale`, via super) while keeping content
91
+ # reads on the store's locale — see Spree::Admin::LocaleConcern. `super`
92
+ # records the content locale in `Spree::Current` (the admin overrides
93
+ # `default_locale` to the UI language, but `content_locale` stays on the
94
+ # store's default), and `pin_content_locale!` aligns Mobility with it.
95
+ def set_locale
96
+ super
97
+ pin_content_locale!
98
+ sync_pagy_locale!
99
+ end
100
+
101
+ # Pagy ships its own i18n dictionaries and resolves them through `Pagy::I18n`
102
+ # (a thread-local that defaults to `:en`), not Rails `I18n`. Align it to the
103
+ # admin UI locale so pagination strings (e.g. "Displaying N items") localize.
104
+ def sync_pagy_locale!
105
+ Pagy::I18n.locale = I18n.locale if defined?(Pagy::I18n)
106
+ end
107
+
108
+ # Resolve the admin UI locale for the current request. Unlike the
109
+ # storefront, the admin prefers the signed-in staff member's own saved
110
+ # `selected_locale` (the language of the back-office chrome), then the
111
+ # `spree_admin_locale` cookie (the per-browser choice made on the login
112
+ # screen, which carries from the pre-auth page into the session), then
113
+ # the store-wide `preferred_admin_locale` via `default_locale`.
114
+ def current_locale
115
+ @current_locale ||= [admin_user_selected_locale, admin_locale_cookie].
116
+ detect { |locale| supported_admin_locale?(locale) } || default_locale
117
+ end
118
+
119
+ # The store's timezone resolved to an +ActiveSupport::TimeZone+, falling
120
+ # back to +Time.zone+ when the preference holds an unknown zone name.
121
+ # @return [ActiveSupport::TimeZone]
87
122
  def current_timezone
88
- @current_timezone ||= current_store.preferred_timezone
123
+ @current_timezone ||= ActiveSupport::TimeZone[current_store.preferred_timezone] || Time.zone
89
124
  end
125
+ helper_method :current_timezone
90
126
 
91
127
  # datetime-local inputs submit values without timezone information. Interpret
92
128
  # them in the store's timezone so they match what the admin sees in the form.
93
129
  def parse_datetime_in_store_timezone(attrs, *fields)
94
- zone = ActiveSupport::TimeZone[current_timezone] || Time.zone
130
+ zone = current_timezone
95
131
 
96
132
  fields.each do |field|
97
133
  value = attrs[field]
@@ -1,12 +1,26 @@
1
1
  module Spree
2
2
  module Admin
3
3
  class CountriesController < Spree::Admin::BaseController
4
+ # Returns every accessible country, sorted by its localized name. The set
5
+ # is small and fixed, so the admin autocomplete loads the full list once
6
+ # and filters it client-side (in the admin UI locale) rather than querying
7
+ # the server per keystroke.
8
+ #
9
+ # `name` is the underlying value used for filtering (the stored English
10
+ # country name); `label` is the localized, flag-prefixed display string.
4
11
  def select_options
5
- q = params[:q]
6
- ransack_params = q.is_a?(String) ? { name_cont: q } : q
7
- countries = Spree::Country.accessible_by(current_ability).ransack(ransack_params).result.order(:name).limit(50)
12
+ countries = Spree::Country.accessible_by(current_ability, :show).
13
+ sort_by(&:localized_name)
8
14
 
9
- render json: countries.pluck(:id, :name).map { |id, name| { id: id, name: name } }
15
+ options = countries.map do |country|
16
+ {
17
+ id: country.id,
18
+ name: country.name,
19
+ label: country.option_label
20
+ }
21
+ end
22
+
23
+ render json: options
10
24
  end
11
25
  end
12
26
  end
@@ -7,6 +7,7 @@ module Spree
7
7
 
8
8
  new_action.before :assign_params
9
9
  create.before :set_user
10
+ create.after :remember_results_url
10
11
 
11
12
  def show
12
13
  redirect_to @object.attachment.url, status: :see_other, allow_other_host: true
@@ -30,6 +31,12 @@ module Spree
30
31
  @object.user = try_spree_current_user
31
32
  end
32
33
 
34
+ # The export-done email links wherever the creating surface serves the
35
+ # file — for the legacy admin that is this export's download page.
36
+ def remember_results_url
37
+ @object.update!(results_url: spree.admin_export_url(@object))
38
+ end
39
+
33
40
  def location_after_save
34
41
  spree.admin_exports_path
35
42
  end
@@ -8,6 +8,7 @@ module Spree
8
8
  create.before :set_user
9
9
  create.before :set_owner
10
10
  create.after :start_mapping
11
+ create.after :remember_results_url
11
12
 
12
13
  # GET /admin/imports/:id
13
14
  def show
@@ -40,6 +41,13 @@ module Spree
40
41
  @object.start_mapping!
41
42
  end
42
43
 
44
+ # The import-done email links wherever the creating surface shows
45
+ # results — for the legacy admin that is this import's page. The
46
+ # dashboard passes its own URL through the Admin API instead.
47
+ def remember_results_url
48
+ @object.update!(results_url: spree.admin_import_url(@object))
49
+ end
50
+
43
51
  def location_after_save
44
52
  spree.admin_import_path(@object)
45
53
  end
@@ -311,6 +311,7 @@ module Spree
311
311
  params.require(:product).permit(permitted_product_attributes)
312
312
  end
313
313
  parse_datetime_in_store_timezone(attrs, :available_on, :discontinue_on, :make_active_at)
314
+ parse_datetime_in_store_timezone(attrs[:master_attributes], :preorder_ships_at) if attrs[:master_attributes].present?
314
315
  attrs
315
316
  end
316
317
  end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ module Admin
5
+ class StorefrontController < BaseController
6
+ include Spree::Admin::SettingsConcern
7
+
8
+ # GET /admin/storefront
9
+ def show
10
+ @breadcrumb_icon = 'building-store'
11
+ add_breadcrumb Spree.t('admin.storefront_setup.title'), spree.admin_storefront_path
12
+
13
+ @publishable_key = find_or_create_publishable_key
14
+ @deployment_origin = normalize_origin(params[:'deployment-url'])
15
+ @deployment_origin_allowed = @deployment_origin.present? && current_store.allowed_origin?(@deployment_origin)
16
+ @vercel_dashboard_url = vercel_dashboard_url
17
+ @storefront_origins = current_store.allowed_origins.order(:created_at).reject(&:loopback?)
18
+ end
19
+
20
+ # PATCH /admin/storefront
21
+ #
22
+ # Saves the storefront URL preference (used as the base for links in
23
+ # customer emails and as the setup-task completion signal) and adds it
24
+ # as an allowed origin so browser-based storefronts can call the Store API.
25
+ def update
26
+ origin = normalize_origin(params[:storefront_url])
27
+
28
+ if origin.nil?
29
+ flash[:error] = Spree.t('admin.storefront_setup.invalid_origin')
30
+ elsif current_store.update(preferred_storefront_url: origin)
31
+ current_store.allowed_origins.find_or_create_by(origin: origin)
32
+ flash[:success] = Spree.t('admin.storefront_setup.storefront_url_saved', url: origin)
33
+ else
34
+ flash[:error] = current_store.errors.full_messages.to_sentence
35
+ end
36
+
37
+ redirect_to spree.admin_storefront_path, status: :see_other
38
+ end
39
+
40
+ # POST /admin/storefront/allow_origin
41
+ def allow_origin
42
+ origin = normalize_origin(params[:origin])
43
+
44
+ if origin.nil?
45
+ flash[:error] = Spree.t('admin.storefront_setup.invalid_origin')
46
+ else
47
+ allowed_origin = current_store.allowed_origins.find_or_initialize_by(origin: origin)
48
+
49
+ if allowed_origin.persisted? || allowed_origin.save
50
+ current_store.update(preferred_storefront_url: origin) if current_store.preferred_storefront_url.blank?
51
+ flash[:success] = Spree.t('admin.storefront_setup.origin_allowed', origin: origin)
52
+ else
53
+ flash[:error] = allowed_origin.errors.full_messages.to_sentence
54
+ end
55
+ end
56
+
57
+ redirect_to spree.admin_storefront_path, status: :see_other
58
+ end
59
+
60
+ private
61
+
62
+ # Every action here is store configuration (publishable keys, allowed
63
+ # origins, store preferences), so gate the whole page behind store
64
+ # management — granted by e.g. Spree::PermissionSets::ConfigurationManagement.
65
+ def authorize_admin
66
+ authorize! :admin, Spree::Store
67
+ authorize! :update, current_store
68
+ end
69
+
70
+ # The storefront needs a publishable key; reuse the oldest active one
71
+ # (usually the seeded "Default") and mint one for stores that have none.
72
+ def find_or_create_publishable_key
73
+ current_store.api_keys.active.publishable.order(:created_at).first ||
74
+ current_store.api_keys.create!(
75
+ name: 'Storefront',
76
+ key_type: 'publishable',
77
+ created_by: try_spree_current_user
78
+ )
79
+ end
80
+
81
+ # Normalizes user or Vercel-callback input to a canonical origin string,
82
+ # or nil when it's not a valid http(s) URL — pre-normalized here (rather
83
+ # than leaning on the Store validation) so invalid input gets the
84
+ # dedicated invalid_origin flash.
85
+ def normalize_origin(raw)
86
+ Spree::AllowedOrigin.normalize_origin(raw)
87
+ end
88
+
89
+ # Only link back to the Vercel dashboard when the callback param actually
90
+ # points at vercel.com — never reflect an arbitrary URL as a link.
91
+ def vercel_dashboard_url
92
+ url = params[:'project-dashboard-url'].to_s
93
+ parsed = Spree::AllowedOrigin.parse_origin(url)
94
+ return unless parsed && parsed[:scheme] == 'https'
95
+
96
+ url if parsed[:host] == 'vercel.com' || parsed[:host].end_with?('.vercel.com')
97
+ end
98
+ end
99
+ end
100
+ end
@@ -2,9 +2,19 @@ module Spree
2
2
  module Admin
3
3
  class UserSessionsController < defined?(Devise::SessionsController) ? Devise::SessionsController : Spree::Admin::BaseController
4
4
  include Spree::Admin::AuthRateLimiting
5
+ include Spree::Admin::LocaleConcern
6
+
7
+ helper 'spree/locale'
8
+ helper 'spree/admin/rtl'
5
9
 
6
10
  layout 'spree/minimal'
7
11
 
12
+ # This controller inherits from Devise::SessionsController, so the
13
+ # `set_locale` before_action from Spree::Core::ControllerHelpers::Locale
14
+ # (mixed into Spree::Admin::BaseController) never runs here — opt into
15
+ # the concern's pre-auth locale handling instead (see LocaleConcern).
16
+ before_action :set_login_locale
17
+
8
18
  auth_rate_limit :rate_limit_login, redirect_to: -> { new_session_path(resource_name) }
9
19
 
10
20
  # We need to overwrite this action because `return_to` url may be in a different domain
@@ -96,7 +96,9 @@ module Spree
96
96
  end
97
97
 
98
98
  def permitted_resource_params
99
- params.require(:variant).permit(permitted_variant_attributes)
99
+ attrs = params.require(:variant).permit(permitted_variant_attributes)
100
+ parse_datetime_in_store_timezone(attrs, :preorder_ships_at)
101
+ attrs
100
102
  end
101
103
  end
102
104
  end
@@ -41,6 +41,11 @@ module Spree
41
41
  @available_countries_iso ||= current_store.countries_available_for_checkout.pluck(:iso)
42
42
  end
43
43
 
44
+ # @return [Array<Spree::Country>] the available countries for checkout
45
+ def available_countries
46
+ @available_countries ||= available_countries_iso.map { |iso| Spree::Country.new(iso: iso) }
47
+ end
48
+
44
49
  # returns the available display on options, eg backend, frontend, both
45
50
  # @return [Array<Array<String, String>>] the available display on options
46
51
  def display_on_options(model = nil)
@@ -360,6 +365,11 @@ module Spree
360
365
  return Spree.t(:none) if market_ids.empty?
361
366
 
362
367
  Spree::Market.where(id: market_ids).pluck(:name).join(', ')
368
+ when :channel_ids
369
+ channel_ids = Array(value).reject(&:blank?).map(&:to_i)
370
+ return Spree.t(:none) if channel_ids.empty?
371
+
372
+ Spree::Channel.where(id: channel_ids).pluck(:name).join(', ')
363
373
  when :user_ids
364
374
  user_ids = Array(value).reject(&:blank?).map(&:to_i)
365
375
  return Spree.t(:none) if user_ids.empty?
@@ -9,6 +9,23 @@ module Spree
9
9
  def channel_order_routing_strategy_options
10
10
  Spree.order_routing.strategies.map { |strategy| [strategy.display_name, strategy.to_s] }
11
11
  end
12
+
13
+ # Storefront access levels for the channel edit form. A blank selection
14
+ # clears the channel override and falls back to +Store#preferred_storefront_access+.
15
+ def channel_storefront_access_options
16
+ Spree::Channel::Gating::STOREFRONT_ACCESS.map do |value|
17
+ [Spree.t("admin.channels.storefront_access_options.#{value}"), value]
18
+ end
19
+ end
20
+
21
+ # Tri-state guest-checkout override for the channel edit form. A blank
22
+ # selection clears the override and falls back to +Store#preferred_guest_checkout+.
23
+ def channel_guest_checkout_options
24
+ [
25
+ [Spree.t('admin.channels.guest_checkout_allowed'), 'true'],
26
+ [Spree.t('admin.channels.guest_checkout_blocked'), 'false']
27
+ ]
28
+ end
12
29
  end
13
30
  end
14
31
  end
@@ -16,6 +16,8 @@ module Spree
16
16
  options[:placement] || 'bottom-start'
17
17
  end
18
18
 
19
+ placement = flip_placement_for_rtl(placement)
20
+
19
21
  # Extract portal option
20
22
  portal = options.delete(:portal)
21
23
 
@@ -49,6 +51,18 @@ module Spree
49
51
 
50
52
  content_tag(:div, options, &block)
51
53
  end
54
+
55
+ def flip_placement_for_rtl(placement)
56
+ return placement unless rtl_locale?
57
+
58
+ case placement
59
+ when 'bottom-start' then 'bottom-end'
60
+ when 'bottom-end' then 'bottom-start'
61
+ when 'top-start' then 'top-end'
62
+ when 'top-end' then 'top-start'
63
+ else placement
64
+ end
65
+ end
52
66
  end
53
67
  end
54
68
  end
@@ -404,7 +404,7 @@ module Spree
404
404
  result = label.to_s
405
405
  if badge_value.present?
406
406
  css_class = badge_class.presence || 'badge-light'
407
- result += content_tag(:span, badge_value, class: "badge ml-auto #{css_class}")
407
+ result += content_tag(:span, badge_value, class: "badge ms-auto #{css_class}")
408
408
  end
409
409
  if tooltip_text.present?
410
410
  result += tooltip(tooltip_text)
@@ -10,7 +10,7 @@ module Spree
10
10
  if condition == true
11
11
  icon('check', class: 'rounded-full inline-flex items-center bg-green-200 text-green-600 border-green-200 p-1')
12
12
  else
13
- icon('check', class: 'rounded-full inline-flex items-center border-dashed text-gray-300 p-1')
13
+ icon('circle-dashed-check', class: 'rounded-full inline-flex items-center border-dashed text-gray-300 p-1')
14
14
  end.html_safe
15
15
  end
16
16
  end
@@ -82,44 +82,19 @@ module Spree
82
82
  end
83
83
 
84
84
  def avs_response_code
85
- {
86
- 'A' => 'Street address matches, but 5-digit and 9-digit postal code do not match.',
87
- 'B' => 'Street address matches, but postal code not verified.',
88
- 'C' => 'Street address and postal code do not match.',
89
- 'D' => 'Street address and postal code match. ',
90
- 'E' => 'AVS data is invalid or AVS is not allowed for this card type.',
91
- 'F' => "Card member's name does not match, but billing postal code matches.",
92
- 'G' => 'Non-U.S. issuing bank does not support AVS.',
93
- 'H' => "Card member's name does not match. Street address and postal code match.",
94
- 'I' => 'Address not verified.',
95
- 'J' => "Card member's name, billing address, and postal code match.",
96
- 'K' => "Card member's name matches but billing address and billing postal code do not match.",
97
- 'L' => "Card member's name and billing postal code match, but billing address does not match.",
98
- 'M' => 'Street address and postal code match. ',
99
- 'N' => 'Street address and postal code do not match.',
100
- 'O' => "Card member's name and billing address match, but billing postal code does not match.",
101
- 'P' => 'Postal code matches, but street address not verified.',
102
- 'Q' => "Card member's name, billing address, and postal code match.",
103
- 'R' => 'System unavailable.',
104
- 'S' => 'Bank does not support AVS.',
105
- 'T' => "Card member's name does not match, but street address matches.",
106
- 'U' => 'Address information unavailable. Returned if the U.S. bank does not support non-U.S. AVS or if the AVS in a U.S. bank is not functioning properly.',
107
- 'V' => "Card member's name, billing address, and billing postal code match.",
108
- 'W' => 'Street address does not match, but 9-digit postal code matches.',
109
- 'X' => 'Street address and 9-digit postal code match.',
110
- 'Y' => 'Street address and 5-digit postal code match.',
111
- 'Z' => 'Street address does not match, but 5-digit postal code matches.'
112
- }
85
+ ('A'..'Z').each_with_object({}) do |code, codes|
86
+ codes[code] = Spree.t("admin.orders.avs_responses.#{code.downcase}")
87
+ end
113
88
  end
114
89
 
115
90
  def cvv_response_code
116
91
  {
117
- 'M' => 'CVV2 Match',
118
- 'N' => 'CVV2 No Match',
119
- 'P' => 'Not Processed',
120
- 'S' => 'Issuer indicates that CVV2 data should be present on the card, but the merchant has indicated data is not present on the card',
121
- 'U' => 'Issuer has not certified for CVV2 or Issuer has not provided Visa with the CVV2 encryption keys',
122
- '' => 'Transaction failed because wrong CVV2 number was entered or no CVV2 number was entered'
92
+ 'M' => Spree.t('admin.orders.cvv_responses.m'),
93
+ 'N' => Spree.t('admin.orders.cvv_responses.n'),
94
+ 'P' => Spree.t('admin.orders.cvv_responses.p'),
95
+ 'S' => Spree.t('admin.orders.cvv_responses.s'),
96
+ 'U' => Spree.t('admin.orders.cvv_responses.u'),
97
+ '' => Spree.t('admin.orders.cvv_responses.blank')
123
98
  }
124
99
  end
125
100
  end
@@ -29,7 +29,7 @@ module Spree
29
29
 
30
30
  def product_status(product)
31
31
  if product.deleted?
32
- content_tag(:span, 'Deleted', class: 'badge badge-danger')
32
+ content_tag(:span, Spree.t('admin.products.deleted'), class: 'badge badge-danger')
33
33
  else
34
34
  content_tag(:span, class: "badge badge-#{product.status}") do
35
35
  if product.active?
@@ -0,0 +1,15 @@
1
+ module Spree
2
+ module Admin
3
+ # View helpers for the legacy admin's layout direction (`<html dir>`).
4
+ # RTL detection itself lives on Spree::Locale (the single source of truth).
5
+ module RtlHelper
6
+ def rtl_locale?(locale = I18n.locale)
7
+ Spree::Locale.new(code: locale).rtl?
8
+ end
9
+
10
+ def html_dir(locale = I18n.locale)
11
+ Spree::Locale.new(code: locale).direction
12
+ end
13
+ end
14
+ end
15
+ end
@@ -12,7 +12,7 @@ module Spree::Admin
12
12
  where(spree_stock_items: { variant_id: variant.id }).
13
13
  group(:id).
14
14
  pluck(:name, "sum(spree_stock_items.count_on_hand)", :id).
15
- map { |name, count, id| ["#{name.capitalize} (#{count} on hand)", "stock-location_#{id}"] }
15
+ map { |name, count, id| [Spree.t('admin.shipments.stock_location_option', name: name, count: count), "stock-location_#{id}"] }
16
16
  end
17
17
 
18
18
  def shipments_for_transfer(current_shipment)
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ module Admin
5
+ module StorefrontHelper
6
+ STOREFRONT_REPOSITORY_URL = 'https://github.com/spree/storefront'
7
+
8
+ # Builds the Vercel deploy-button URL for the storefront starter, prefilled
9
+ # with the store's API URL and publishable key (both non-secret), and
10
+ # redirecting back to the admin storefront page after a successful deploy
11
+ # so the deployed domain can be added as an allowed origin.
12
+ #
13
+ # @param store [Spree::Store]
14
+ # @param api_key [Spree::ApiKey] an active publishable key
15
+ # @return [String]
16
+ def vercel_deploy_url(store, api_key)
17
+ query = {
18
+ 'repository-url' => STOREFRONT_REPOSITORY_URL,
19
+ 'project-name' => "#{store.code}-storefront",
20
+ 'repository-name' => "#{store.code}-storefront",
21
+ 'env' => 'SPREE_API_URL,SPREE_PUBLISHABLE_KEY',
22
+ 'envDefaults' => {
23
+ 'SPREE_API_URL' => store.formatted_url,
24
+ 'SPREE_PUBLISHABLE_KEY' => api_key.token
25
+ }.to_json,
26
+ 'envDescription' => Spree.t('admin.storefront_setup.env_description'),
27
+ 'envLink' => "#{STOREFRONT_REPOSITORY_URL}#readme",
28
+ 'redirect-url' => spree.admin_storefront_url
29
+ }
30
+
31
+ "https://vercel.com/new/clone?#{query.to_query}"
32
+ end
33
+
34
+ # True when the store's public URL points at a loopback host, which
35
+ # Vercel's build servers cannot reach.
36
+ #
37
+ # @param store [Spree::Store]
38
+ # @return [Boolean]
39
+ def store_url_loopback?(store = current_store)
40
+ Spree::AllowedOrigin::LOOPBACK_HOSTS.include?(
41
+ Spree::AllowedOrigin.parse_origin(store.formatted_url)&.dig(:host)
42
+ )
43
+ end
44
+ end
45
+ end
46
+ end
@@ -30,7 +30,8 @@ module Spree
30
30
  opts[:width] ||= DEFAULT_ICON_SIZE
31
31
  opts[:class] ||= ''
32
32
  opts[:class] += ' store-icon rounded-lg text-gray-900 flex items-center justify-center'
33
- content_tag(:span, name[0].upcase, class: opts[:class], style: "height: #{opts[:height]}px; width: #{opts[:width]}px;")
33
+ letter = name.to_s.first&.upcase
34
+ content_tag(:span, letter, class: opts[:class], style: "height: #{opts[:height]}px; width: #{opts[:width]}px;")
34
35
  end
35
36
  end
36
37
  end
@@ -55,6 +55,7 @@ import CodeMirrorController from 'spree/admin/controllers/codemirror_controller'
55
55
  import ColorPaletteController from 'spree/admin/controllers/color_palette_controller'
56
56
  import ColumnSelectorController from 'spree/admin/controllers/column_selector_controller'
57
57
  import ColorPickerController from 'spree/admin/controllers/color_picker_controller'
58
+ import DisplayNameController from 'spree/admin/controllers/display_name_controller'
58
59
  import DropdownController from 'spree/admin/controllers/dropdown_controller'
59
60
  import FiltersController from 'spree/admin/controllers/filters_controller'
60
61
  import FontPickerController from 'spree/admin/controllers/font_picker_controller'
@@ -120,6 +121,7 @@ application.register('codemirror', CodeMirrorController)
120
121
  application.register('color-palette', ColorPaletteController)
121
122
  application.register('color-picker', ColorPickerController)
122
123
  application.register('column-selector', ColumnSelectorController)
124
+ application.register('display-name', DisplayNameController)
123
125
  application.register('dialog', Dialog)
124
126
  application.register('drawer', Dialog)
125
127
  application.register('disable-submit-button', DisableSubmitButtonController)
@@ -1,11 +1,15 @@
1
1
  import { Controller } from "@hotwired/stimulus"
2
2
  import TomSelect from 'tom-select/dist/esm/tom-select.complete.js';
3
+ import { localizeSelectOptions } from 'spree/admin/helpers/display_names'
3
4
 
4
5
  // this is a very simple autocomplete select controller that uses TomSelect
5
6
  // by default it does not support remote data fetching or adding new options
6
7
  // that scenarios are handled in the more robust select_controller.js
7
8
  export default class extends Controller {
8
9
  connect() {
10
+ const displayNamesType = this.element.dataset.displayNamesType
11
+ if (displayNamesType) localizeSelectOptions(this.element, displayNamesType)
12
+
9
13
  let selectedValue = this.element.value
10
14
 
11
15
  let plugins = []
@@ -5,6 +5,12 @@ import { PresetPlugin } from '@easepick/preset-plugin';
5
5
 
6
6
  export default class extends Controller {
7
7
  static targets = [ "picker", "dateFrom", "dateTo", "label" ]
8
+ static values = {
9
+ presetLabels: {
10
+ type: Array,
11
+ default: ['Today', 'Yesterday', 'Last 7 Days', 'Last 30 Days', 'This Month', 'Last Month']
12
+ }
13
+ }
8
14
 
9
15
  connect() {
10
16
  this.picker = new easepick.create({
@@ -23,9 +29,7 @@ export default class extends Controller {
23
29
  endDate: this.dateToTarget.value,
24
30
  },
25
31
  PresetPlugin: {
26
- customLabels: ['Today', 'Yesterday',
27
- 'Last 7 Days', 'Last 30 Days',
28
- 'This Month', 'Last Month'],
32
+ customLabels: this.presetLabelsValue,
29
33
  position: 'left'
30
34
  },
31
35
  lang: Spree.locale
@@ -0,0 +1,18 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+ import { adminUiLocale, formatCodeName, intlDisplayName } from 'spree/admin/helpers/display_names'
3
+
4
+ /**
5
+ * Upgrades a server-rendered code/name label to its `Intl.DisplayNames`
6
+ * equivalent in the admin UI language once JS is available.
7
+ */
8
+ export default class extends Controller {
9
+ static values = {
10
+ type: String,
11
+ code: String
12
+ }
13
+
14
+ connect() {
15
+ const name = intlDisplayName(this.typeValue, this.codeValue, adminUiLocale())
16
+ if (name) this.element.textContent = formatCodeName(this.codeValue, name)
17
+ }
18
+ }