spree_admin 5.4.3 → 5.5.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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/spree/admin/assets_controller.rb +7 -1
  3. data/app/controllers/spree/admin/channels_controller.rb +13 -0
  4. data/app/controllers/spree/admin/products_controller.rb +8 -8
  5. data/app/helpers/spree/admin/assets_helper.rb +5 -1
  6. data/app/helpers/spree/admin/base_helper.rb +0 -8
  7. data/app/helpers/spree/admin/channels_helper.rb +14 -0
  8. data/app/helpers/spree/admin/json_preview_helper.rb +5 -0
  9. data/app/helpers/spree/admin/orders_helper.rb +0 -29
  10. data/app/helpers/spree/admin/publishing_helper.rb +73 -0
  11. data/app/helpers/spree/admin/turbo_helper.rb +0 -18
  12. data/app/helpers/spree/admin/webhook_endpoints_helper.rb +1 -0
  13. data/app/javascript/spree/admin/application.js +2 -0
  14. data/app/javascript/spree/admin/controllers/product_form_controller.js +0 -26
  15. data/app/javascript/spree/admin/controllers/product_publishing_controller.js +11 -0
  16. data/app/javascript/spree/admin/controllers/slug_form_controller.js +18 -1
  17. data/app/presenters/spree/admin/order_summary_presenter.rb +12 -0
  18. data/app/views/spree/admin/api_keys/_form.html.erb +18 -0
  19. data/app/views/spree/admin/assets/edit.html.erb +64 -9
  20. data/app/views/spree/admin/channels/_form.html.erb +19 -0
  21. data/app/views/spree/admin/channels/edit.html.erb +1 -0
  22. data/app/views/spree/admin/channels/index.html.erb +12 -0
  23. data/app/views/spree/admin/channels/new.html.erb +1 -0
  24. data/app/views/spree/admin/integrations/_integration.html.erb +7 -9
  25. data/app/views/spree/admin/integrations/edit.html.erb +1 -1
  26. data/app/views/spree/admin/products/_form.html.erb +2 -1
  27. data/app/views/spree/admin/products/form/_publishing.html.erb +125 -0
  28. data/app/views/spree/admin/products/form/_status.html.erb +3 -47
  29. data/app/views/spree/admin/shared/_media_asset.html.erb +1 -1
  30. data/app/views/spree/admin/variants/edit.html.erb +0 -1
  31. data/config/initializers/spree_admin_navigation.rb +9 -0
  32. data/config/initializers/spree_admin_tables.rb +60 -0
  33. data/config/locales/en.yml +32 -3
  34. data/config/routes.rb +1 -0
  35. data/lib/spree/admin/engine.rb +2 -0
  36. metadata +18 -11
  37. data/app/helpers/spree/admin/modal_helper.rb +0 -31
  38. data/app/views/spree/admin/shared/_modal.html.erb +0 -27
  39. /data/app/views/spree/admin/promotion_rules/forms/{_taxon.html.erb → _category.html.erb} +0 -0
  40. /data/app/views/spree/admin/promotion_rules/forms/{_user.html.erb → _customer.html.erb} +0 -0
  41. /data/app/views/spree/admin/promotion_rules/forms/{_user_logged_in.html.erb → _customer_logged_in.html.erb} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9a5af2b78a13b13f570093bfe92c6d6c6b3ade67ec72e294fbf0f1ffd9c121d6
4
- data.tar.gz: 6dfa405d89717625d66b270fe16f7343bb8ec532f8ba673ee856361eb3682e92
3
+ metadata.gz: 2f4bbbb1dd33ca70b641c0c4c687f4a8a36731ec816c3324930b44139d911bd6
4
+ data.tar.gz: d402abe10df4c9fe520db4f9b97fb17e33172238ff7e58b14f71b2754a9c09a5
5
5
  SHA512:
6
- metadata.gz: 9172a34304882f931b1bf56502ffd09121ca3497d8e3b24b5520dc03cf6cebf5a70c6e284615098dc4c70462f153aa204899911f152902e75317b5130760222d
7
- data.tar.gz: 91f7065af97ecddea447d6add3cd60ab8d36ceb6590ced8ca3e74bbe65a26eeded78625209b38e5b5ad9f26e43605bdc05fb594664f0d1aad1fbe51af22f17b9
6
+ metadata.gz: 9931e85d7ade12bc71fd556f9014cb410ad03e205eedc6ba4c1bc03d3e5d38a32ddabd7a5b34114fe0cb1b58059d7b6ce36907c3515a866812609c9911989517
7
+ data.tar.gz: 8f9090d2f5d25c761f4f3ce74701fabd179b9947c21ce23ab64362ac67363ce77b63c624136a35bb02b2897d60fe72e716ac4e8539fdc032f5bc7c7cdbcab12c
@@ -38,8 +38,14 @@ module Spree
38
38
 
39
39
  private
40
40
 
41
+ # Includes :variant_ids so the variant-assignment checkboxes mass-assign
42
+ # straight into Spree::Asset#variant_ids= — the model resolves prefixed
43
+ # ids and rejects cross-product variants.
41
44
  def permitted_resource_params
42
- params.require(:asset).permit(Spree::PermittedAttributes.asset_attributes)
45
+ params.require(:asset).permit(
46
+ *Spree::PermittedAttributes.asset_attributes,
47
+ variant_ids: []
48
+ )
43
49
  end
44
50
 
45
51
  def create_turbo_stream_enabled?
@@ -0,0 +1,13 @@
1
+ module Spree
2
+ module Admin
3
+ class ChannelsController < ResourceController
4
+ include Spree::Admin::SettingsConcern
5
+
6
+ private
7
+
8
+ def permitted_resource_params
9
+ params.require(:channel).permit(permitted_channel_attributes)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -21,7 +21,7 @@ module Spree
21
21
  new_action.before :build_master_stock_items
22
22
  edit_action.before :build_master_prices
23
23
  edit_action.before :build_master_stock_items
24
- create.after :assign_master_images
24
+ create.after :assign_session_uploaded_assets
25
25
  update.before :skip_updating_status
26
26
  update.before :update_status
27
27
  update.before :remove_empty_params
@@ -185,7 +185,7 @@ module Spree
185
185
  @product_variant_ids[variant.human_name] = variant.id.to_s
186
186
  @product_variant_prefix_ids[variant.human_name] = variant.to_param
187
187
 
188
- image = variant.primary_media
188
+ image = variant.primary_media || @product.primary_media
189
189
  if image.present? && image.attached? && image.variable?
190
190
  @product_variant_images[variant.human_name] = helpers.spree_image_url(image, variant: :mini)
191
191
  end
@@ -287,15 +287,15 @@ module Spree
287
287
  {}
288
288
  end
289
289
 
290
- def assign_master_images
291
- return unless @product.master.persisted?
292
-
293
- uploaded_assets = session_uploaded_assets('Spree::Variant')
290
+ def assign_session_uploaded_assets
291
+ uploaded_assets = session_uploaded_assets('Spree::Product')
294
292
 
295
293
  return if uploaded_assets.empty?
296
294
 
297
- uploaded_assets.update_all(viewable_id: @product.master.id, viewable_type: 'Spree::Variant', updated_at: Time.current)
298
- clear_session_for_uploaded_assets('Spree::Variant')
295
+ uploaded_assets.update_all(viewable_id: @product.id, viewable_type: 'Spree::Product', updated_at: Time.current)
296
+ @product.update_thumbnail!
297
+
298
+ clear_session_for_uploaded_assets('Spree::Product')
299
299
  end
300
300
 
301
301
  def check_slug_availability
@@ -3,7 +3,11 @@ module Spree
3
3
  module AssetsHelper
4
4
  def media_form_assets(viewable, viewable_type)
5
5
  if viewable&.persisted?
6
- viewable.images
6
+ # Product#images delegates to the master variant (legacy alias), so
7
+ # 5.5+ product-level uploads wouldn't show. gallery_media returns
8
+ # product-level media when present and falls back to legacy
9
+ # variant-pinned images during the transition.
10
+ viewable.respond_to?(:gallery_media) ? viewable.gallery_media : viewable.images
7
11
  elsif session_uploaded_assets(viewable_type).any?
8
12
  Spree::Asset.accessible_by(current_ability, :manage).where(id: session_uploaded_assets(viewable_type))
9
13
  else
@@ -36,14 +36,6 @@ module Spree
36
36
  @settings_area.present?
37
37
  end
38
38
 
39
- def settings_active?
40
- Spree::Deprecation.warn('settings_active? is deprecated and will be removed in Spree 5.5. Please use settings_area? instead')
41
- @settings_active || %w[admin_users audits custom_domains exports invitations oauth_applications
42
- payment_methods refund_reasons reimbursement_types return_authorization_reasons roles
43
- shipping_categories shipping_methods stock_locations store_credit_categories
44
- stores tax_categories tax_rates webhooks webhooks_subscribers zones policies metafield_definitions].include?(controller_name) || settings_area?
45
- end
46
-
47
39
  # @return [Array<String>] the available countries for checkout
48
40
  def available_countries_iso
49
41
  @available_countries_iso ||= current_store.countries_available_for_checkout.pluck(:iso)
@@ -0,0 +1,14 @@
1
+ module Spree
2
+ module Admin
3
+ module ChannelsHelper
4
+ # Registered +Spree::OrderRouting::Strategy::Base+ subclasses presented in
5
+ # the channel edit form, sourced from +Spree.order_routing.strategies+ so the
6
+ # picker can never drift from what the model accepts. A blank value clears the
7
+ # channel-level override and falls back to
8
+ # +Store#preferred_order_routing_strategy+.
9
+ def channel_order_routing_strategy_options
10
+ Spree.order_routing.strategies.map { |strategy| [strategy.display_name, strategy.to_s] }
11
+ end
12
+ end
13
+ end
14
+ end
@@ -69,6 +69,11 @@ module Spree
69
69
  return Spree.api.public_send(method_name)
70
70
  end
71
71
 
72
+ if namespace == 'Spree::Api::V3::Admin'
73
+ method_name = "admin_#{class_name.underscore}_serializer"
74
+ return Spree.api.public_send(method_name) if Spree.api.respond_to?(method_name)
75
+ end
76
+
72
77
  # Fall back to direct constant lookup
73
78
  serializer_class_name = "#{namespace}::#{class_name}Serializer"
74
79
  serializer_class_name.safe_constantize
@@ -1,24 +1,6 @@
1
1
  module Spree
2
2
  module Admin
3
3
  module OrdersHelper
4
- TaxLine = Struct.new(:label, :display_amount, :item, :for_shipment, keyword_init: true) do
5
- def name
6
- Spree::Deprecation.warn("TaxLine is deprecated and will be removed in Spree 5.5")
7
- item_name = item.name
8
- item_name += " #{Spree.t(:shipment).downcase}" if for_shipment
9
-
10
- "#{label} (#{item_name})"
11
- end
12
- end
13
-
14
- def order_summary_tax_lines_additional(order)
15
- Spree::Deprecation.warn("order_summary_tax_lines_additional is deprecated and will be removed in Spree 5.5")
16
- line_item_taxes = order.line_item_adjustments.tax.map { |tax_adjustment| map_to_tax_line(tax_adjustment) }
17
- shipment_taxes = order.shipment_adjustments.tax.map { |tax_adjustment| map_to_tax_line(tax_adjustment, for_shipment: true) }
18
-
19
- line_item_taxes + shipment_taxes
20
- end
21
-
22
4
  def order_payment_state(order, options = {})
23
5
  return if order.payment_state.blank?
24
6
 
@@ -140,17 +122,6 @@ module Spree
140
122
  '' => 'Transaction failed because wrong CVV2 number was entered or no CVV2 number was entered'
141
123
  }
142
124
  end
143
-
144
- private
145
-
146
- def map_to_tax_line(tax_adjustment, for_shipment: false)
147
- TaxLine.new(
148
- label: tax_adjustment.label,
149
- display_amount: tax_adjustment.display_amount,
150
- item: tax_adjustment.adjustable,
151
- for_shipment: for_shipment
152
- )
153
- end
154
125
  end
155
126
  end
156
127
  end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ module Admin
5
+ # Helpers for the product Publishing card in the Rails admin. Mirrors the
6
+ # SPA's publishing card (see packages/dashboard/src/components/spree/products/publishing-card.tsx):
7
+ # per-channel status is gated by product status — Draft/Archived products
8
+ # render every publication as +not_available+ regardless of window.
9
+ module PublishingHelper
10
+ # @param product_status [String] the product's status (draft/active/archived)
11
+ # @param publication [Spree::ProductPublication]
12
+ # @return [Symbol] one of :live, :scheduled, :hidden, :not_available
13
+ def publication_schedule_status(product_status, publication)
14
+ return :not_available unless product_status == 'active'
15
+
16
+ now = Time.current
17
+ return :hidden if publication.unpublished_at && publication.unpublished_at <= now
18
+ return :scheduled if publication.published_at && publication.published_at > now
19
+
20
+ :live
21
+ end
22
+
23
+ # Renders a colored dot + status label for a publication row.
24
+ def publication_status_badge(product_status, publication)
25
+ status = publication_schedule_status(product_status, publication)
26
+ dot_class = case status
27
+ when :live then 'bg-success'
28
+ when :scheduled then 'bg-warning'
29
+ else 'bg-secondary'
30
+ end
31
+
32
+ label = Spree.t("admin.publishing.status_#{status}")
33
+
34
+ content_tag(:span, class: 'd-inline-flex align-items-center gap-1 text-muted small') do
35
+ content_tag(:span, '', class: "publication-dot rounded-circle #{dot_class}",
36
+ style: 'display:inline-block; width:0.5rem; height:0.5rem;') +
37
+ content_tag(:span, label)
38
+ end
39
+ end
40
+
41
+ # One-line summary of the publication window in the store's timezone.
42
+ def publication_caption(product_status, publication, store)
43
+ status = publication_schedule_status(product_status, publication)
44
+ tz = ActiveSupport::TimeZone[store.preferred_timezone] || Time.zone
45
+
46
+ case status
47
+ when :not_available
48
+ Spree.t('admin.publishing.caption_not_available',
49
+ product_status: Spree.t("admin.products.status_options.#{product_status}", default: product_status.to_s.humanize))
50
+ when :hidden
51
+ Spree.t('admin.publishing.caption_unpublished',
52
+ date: l(publication.unpublished_at.in_time_zone(tz), format: :short))
53
+ when :scheduled
54
+ if publication.unpublished_at
55
+ Spree.t('admin.publishing.caption_window',
56
+ start: l(publication.published_at.in_time_zone(tz), format: :short),
57
+ end: l(publication.unpublished_at.in_time_zone(tz), format: :short))
58
+ else
59
+ Spree.t('admin.publishing.caption_scheduled',
60
+ date: l(publication.published_at.in_time_zone(tz), format: :short))
61
+ end
62
+ else # :live
63
+ if publication.unpublished_at
64
+ Spree.t('admin.publishing.caption_hidden_after',
65
+ date: l(publication.unpublished_at.in_time_zone(tz), format: :short))
66
+ else
67
+ Spree.t('admin.publishing.caption_live')
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -1,24 +1,6 @@
1
1
  module Spree
2
2
  module Admin
3
3
  module TurboHelper
4
- def turbo_close_modal(modal_id = nil)
5
- Spree::Deprecation.warn('turbo_close_modal is deprecated and will be removed in Spree 5.5. Use turbo_close_dialog instead.')
6
-
7
- modal_id ||= 'modal'
8
-
9
- turbo_stream.replace :modal_scripts do
10
- turbo_frame_tag :modal_scripts do
11
- javascript_tag do
12
- raw <<~JS
13
- if (document.querySelector('##{modal_id}')) {
14
- window.$("##{modal_id}").modal('hide');
15
- }
16
- JS
17
- end
18
- end
19
- end
20
- end
21
-
22
4
  def turbo_close_dialog
23
5
  turbo_stream.replace 'main-dialog' do
24
6
  render 'spree/admin/shared/dialog'
@@ -93,6 +93,7 @@ module Spree
93
93
  def custom_webhook_events
94
94
  %w[
95
95
  customer.password_reset_requested
96
+ newsletter_subscriber.subscription_requested
96
97
  order.completed
97
98
  order.paid
98
99
  order.canceled
@@ -68,6 +68,7 @@ import OrderBillingAddressController from 'spree/admin/controllers/order_billing
68
68
  import PageBuilderController from 'spree/admin/controllers/page_builder_controller'
69
69
  import PasswordToggle from 'spree/admin/controllers/password_toggle_controller'
70
70
  import ProductFormController from 'spree/admin/controllers/product_form_controller'
71
+ import ProductPublishingController from 'spree/admin/controllers/product_publishing_controller'
71
72
  import QueryBuilderController from 'spree/admin/controllers/query_builder_controller'
72
73
  import RangeInputController from 'spree/admin/controllers/range_input_controller'
73
74
  import TableController from 'spree/admin/controllers/table_controller'
@@ -140,6 +141,7 @@ application.register('page-builder', PageBuilderController)
140
141
  application.register('password-toggle', PasswordToggle)
141
142
  application.register('password-visibility', PasswordVisibility)
142
143
  application.register('product-form', ProductFormController)
144
+ application.register('product-publishing', ProductPublishingController)
143
145
  application.register('query-builder', QueryBuilderController)
144
146
  application.register('range-input', RangeInputController)
145
147
  application.register('table', TableController)
@@ -4,10 +4,6 @@ export default class extends Controller {
4
4
  static targets = [
5
5
  'trackInventoryCheckbox',
6
6
  'quantityForm',
7
- 'availableOn',
8
- 'makeActiveAt',
9
- 'discontinueOn',
10
- 'status',
11
7
  'pricesForm'
12
8
  ]
13
9
 
@@ -26,28 +22,6 @@ export default class extends Controller {
26
22
  this.togglePricesFormVisibility()
27
23
  }
28
24
 
29
- switchAvailabilityDatesFields(event) {
30
- let status = event.target.value
31
- if (status === 'draft') {
32
- this.show(this.availableOnTarget)
33
- this.show(this.makeActiveAtTarget)
34
- } else if (status === 'active') {
35
- this.show(this.availableOnTarget)
36
- this.hide(this.makeActiveAtTarget)
37
- } else {
38
- this.hide(this.availableOnTarget)
39
- this.hide(this.makeActiveAtTarget)
40
- }
41
- }
42
-
43
- show(element) {
44
- element.classList.remove('hidden', 'd-none')
45
- }
46
-
47
- hide(element) {
48
- element.classList.add('hidden')
49
- }
50
-
51
25
  toggleQuantityFormVisibility() {
52
26
  if (this.hasQuantityFormTarget) {
53
27
  if (!this.hasVariantsValue && this.trackInventoryCheckboxTarget.checked) {
@@ -0,0 +1,11 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+
3
+ // Toggles the "Manage" channel-checkbox panel on the product Publishing card.
4
+ // Pairs with spree/admin/app/views/spree/admin/products/form/_publishing.html.erb.
5
+ export default class extends Controller {
6
+ static targets = ['manage']
7
+
8
+ toggleManage() {
9
+ this.manageTarget.classList.toggle('d-none')
10
+ }
11
+ }
@@ -6,9 +6,26 @@ export default class extends Controller {
6
6
  'url',
7
7
  ]
8
8
 
9
+ connect() {
10
+ this.urlTouched = this.hasUrlTarget && this.urlTarget.value.length > 0
11
+ if (this.hasUrlTarget) {
12
+ this.urlTarget.addEventListener('input', () => { this.urlTouched = true })
13
+ }
14
+ }
15
+
9
16
  updateUrlFromName() {
17
+ if (this.urlTouched) return
10
18
  const name = this.nameTarget.value
11
- const url = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)+/g, '')
19
+ // Mirrors ActiveSupport's +String#parameterize+: NFKD decompose, drop
20
+ // combining marks (accents), lowercase, hyphenate the rest. Keeps the
21
+ // legacy admin's auto-fill aligned with what +normalizes :code+ stores
22
+ // server-side — without NFKD, "Café" rendered "caf-" instead of "cafe".
23
+ const url = name
24
+ .normalize('NFKD')
25
+ .replace(/\p{M}/gu, '')
26
+ .toLowerCase()
27
+ .replace(/[^a-z0-9]+/g, '-')
28
+ .replace(/(^-|-$)+/g, '')
12
29
  this.urlTarget.value = url
13
30
  }
14
31
  }
@@ -13,6 +13,7 @@ module Spree
13
13
  [
14
14
  *metadata_rows,
15
15
  :separator,
16
+ channel_row,
16
17
  market_row,
17
18
  locale_row,
18
19
  currency_row,
@@ -73,6 +74,17 @@ module Spree
73
74
  rows
74
75
  end
75
76
 
77
+ def channel_row
78
+ return nil if order.channel.blank?
79
+
80
+ {
81
+ label: Spree.t(:channel),
82
+ value: order.channel.name,
83
+ id: 'channel',
84
+ link: Spree::Core::Engine.routes.url_helpers.edit_admin_channel_path(order.channel)
85
+ }
86
+ end
87
+
76
88
  def market_row
77
89
  return nil if order.market.blank?
78
90
 
@@ -24,3 +24,21 @@
24
24
  <% end %>
25
25
  </div>
26
26
  </div>
27
+
28
+ <div class="card mb-6" id="api-key-scopes" data-secret-only data-show-when-key-type="secret">
29
+ <div class="card-header">
30
+ <h5 class="card-title"><%= Spree.t('admin.api_keys.scopes', default: 'Scopes') %></h5>
31
+ <p class="text-sm text-gray-500 mt-1">
32
+ <%= Spree.t('admin.api_keys.scopes_description', default: 'Required for secret keys. Pick the narrowest set of scopes your integration needs.') %>
33
+ </p>
34
+ </div>
35
+ <div class="card-body">
36
+ <% Spree::ApiKey::SCOPES.each do |scope| %>
37
+ <div class="custom-control form-checkbox mb-1">
38
+ <%= f.check_box :scopes, { multiple: true, class: 'custom-control-input', id: "api_key_scopes_#{scope}" }, scope, nil %>
39
+ <%= f.label "scopes_#{scope}", scope, class: 'custom-control-label font-mono text-sm' %>
40
+ </div>
41
+ <% end %>
42
+ <%= f.hidden_field :scopes, value: '', multiple: true %>
43
+ </div>
44
+ </div>
@@ -1,17 +1,72 @@
1
- <%= turbo_frame_tag :dialog do %>
2
- <%= dialog_header(Spree.t(:edit) + ' ' + Spree.t(:image)) %>
1
+ <%= turbo_frame_tag :drawer do %>
3
2
  <%= form_with model: @asset, url: spree.admin_asset_path(@asset), method: :put, scope: :asset do |f| %>
4
- <div class="dialog-body pb-0" data-turbo-permanent id="asset-<%= @asset.key.parameterize %>">
5
- <%= f.spree_file_field :attachment, width: 200, height: 200, can_delete: false, label: Spree.t(:image) %>
3
+ <%= drawer_header(Spree.t(:edit) + ' ' + Spree.t(:image)) %>
4
+
5
+ <div class="drawer-body">
6
+ <% if @asset.attachment.attached? %>
7
+ <% blob_url_opts = { host: request.host_with_port, protocol: (request.ssl? ? 'https' : 'http') } %>
8
+ <% download_url = Rails.application.routes.url_helpers.rails_blob_url(@asset.attachment.blob, **blob_url_opts, disposition: 'attachment') %>
9
+ <% original_url = Rails.application.routes.url_helpers.rails_blob_url(@asset.attachment.blob, **blob_url_opts) %>
10
+ <div class="mb-4 rounded-lg border bg-light overflow-hidden">
11
+ <a href="<%= original_url %>"
12
+ target="_blank"
13
+ rel="noopener"
14
+ title="<%= Spree.t(:view_full_size) %>"
15
+ style="display: block; position: relative; cursor: zoom-in;">
16
+ <%= spree_image_tag(@asset.attachment, variant: :large, alt: @asset.alt, class: 'w-full max-h-96 object-contain') %>
17
+ <span style="position: absolute; top: 8px; right: 8px; display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 6px; background: rgba(255,255,255,0.95); color: #4b5563; box-shadow: 0 1px 2px rgba(0,0,0,0.1); pointer-events: none;">
18
+ <%= icon('zoom-in', class: 'mr-0') %>
19
+ </span>
20
+ </a>
21
+ </div>
22
+ <div class="flex gap-2 mb-4">
23
+ <%= link_to original_url, target: '_blank', rel: 'noopener', class: 'btn btn-light btn-sm' do %>
24
+ <%= icon('external-link', class: 'mr-1') %><%= Spree.t(:view_full_size) %>
25
+ <% end %>
26
+ <%= link_to download_url, target: '_blank', rel: 'noopener', class: 'btn btn-light btn-sm' do %>
27
+ <%= icon('download', class: 'mr-1') %><%= Spree.t(:download) %>
28
+ <% end %>
29
+ </div>
30
+ <% else %>
31
+ <%= f.spree_file_field :attachment, width: 200, height: 200, can_delete: false, label: Spree.t(:image) %>
32
+ <% end %>
33
+
34
+ <%= f.spree_text_area :alt, label: Spree.t(:alt_text), rows: 3 %>
35
+
36
+ <%# Variant assignment shows only for product-level assets — variant-pinned assets
37
+ already live on a single variant via the polymorphic viewable, so the
38
+ join-table UI doesn't apply. Empty submission (no boxes ticked) clears
39
+ all links thanks to the hidden field below. %>
40
+ <% if @asset.viewable_type == 'Spree::Product' && @asset.viewable.present? && @asset.viewable.variants.any? %>
41
+ <% product = @asset.viewable %>
42
+ <% linked_ids = @asset.variant_media.pluck(:variant_id).to_set %>
43
+ <div class="mt-4 pt-4 border-t">
44
+ <label class="form-label"><%= Spree.t(:assigned_variants) %></label>
45
+ <p class="text-muted small mb-2">
46
+ <%= Spree.t(:assigned_variants_help) %>
47
+ </p>
48
+ <%= hidden_field_tag 'asset[variant_ids][]', '' %>
49
+ <div class="flex flex-col gap-1">
50
+ <% product.variants.each do |variant| %>
51
+ <% chip_id = "asset_variant_#{variant.id}" %>
52
+ <div class="custom-control form-checkbox rounded-lg hover:bg-gray-100 p-2 transition-colors duration-100 ease-linear">
53
+ <%= check_box_tag('asset[variant_ids][]', variant.to_param, linked_ids.include?(variant.id), id: chip_id, class: 'custom-control-input') %>
54
+ <%= label_tag chip_id, class: 'custom-control-label' do %>
55
+ <%= variant.descriptive_name.presence || variant.sku %>
56
+ <% end %>
57
+ </div>
58
+ <% end %>
59
+ </div>
60
+ </div>
61
+ <% end %>
6
62
  </div>
7
- <div class="dialog-body" style="min-height: 200px">
8
- <%= f.spree_text_area :alt, label: Spree.t(:alt_text), rows: 4 %>
9
- </div>
10
- <div class="dialog-footer">
63
+
64
+ <div class="drawer-footer gap-3">
11
65
  <%= turbo_save_button_tag %>
66
+ <%= drawer_discard_button %>
12
67
  <%= link_to Spree.t('actions.destroy'), object_url(@asset),
13
68
  data: { turbo_method: :delete, turbo_confirm: Spree.t(:are_you_sure_delete), turbo_frame: '_top' },
14
69
  class: 'btn btn-danger ml-auto' if can?(:destroy, @asset) %>
15
70
  </div>
16
71
  <% end %>
17
- <% end %>
72
+ <% end %>
@@ -0,0 +1,19 @@
1
+ <div class="card mb-6">
2
+ <div class="card-body" data-controller="slug-form">
3
+ <%= f.spree_text_field :name,
4
+ autofocus: f.object.new_record?,
5
+ required: true,
6
+ data: { slug_form_target: :name, action: 'slug-form#updateUrlFromName' } %>
7
+ <%= f.spree_text_field :code,
8
+ required: true,
9
+ hint: Spree.t('admin.channels.code_hint'),
10
+ data: { slug_form_target: :url } %>
11
+ <%= f.spree_check_box :active, label: Spree.t(:active) %>
12
+ <%= f.spree_check_box :default, label: Spree.t('admin.channels.default'), hint: Spree.t('admin.channels.default_hint') %>
13
+ <%= f.spree_select :preferred_order_routing_strategy,
14
+ options_for_select(channel_order_routing_strategy_options, f.object.preferred_order_routing_strategy),
15
+ { label: Spree.t('admin.channels.order_routing_strategy'),
16
+ help_bubble: Spree.t('admin.channels.order_routing_strategy_hint'),
17
+ include_blank: Spree.t('admin.channels.order_routing_strategy_inherit') } %>
18
+ </div>
19
+ </div>
@@ -0,0 +1 @@
1
+ <%= render 'spree/admin/shared/edit_resource' %>
@@ -0,0 +1,12 @@
1
+ <% content_for :page_title do %>
2
+ <%= Spree.t(:channels) %>
3
+ <% end %>
4
+
5
+ <% content_for :page_actions do %>
6
+ <%= render_admin_partials(:channels_actions_partials) %>
7
+ <%= link_to_with_icon 'plus', Spree.t(:new_channel), new_object_url, class: "btn btn-primary" if can? :create, Spree::Channel %>
8
+ <% end %>
9
+
10
+ <%= render_admin_partials(:channels_header_partials) %>
11
+
12
+ <%= render_table @collection, :channels %>
@@ -0,0 +1 @@
1
+ <%= render 'spree/admin/shared/new_resource' %>
@@ -5,29 +5,27 @@
5
5
 
6
6
  <div id="integration-<%= integration_class.integration_key %>" class="col mb-6 flex items-stretch">
7
7
  <div class="card w-full">
8
- <div class="card-header border-b flex flex-col gap-4 items-center justify-between h-auto pr-4 <% if integration.present? %>bg-green-50<% end %>">
8
+ <div class="card-header border-b flex items-center justify-center h-auto pr-4 <% if integration.present? %>bg-green-50<% end %>">
9
9
  <div class="flex items-center py-1">
10
10
  <%= image_tag integration_class.icon_path, alt: integration_name, height: 36 if integration_class.icon_path.present? %>
11
11
  </div>
12
-
13
- <% if integration.present? %>
14
- <span class="badge badge-success small">
15
- <%= icon('check', class: 'mr-1') %>
16
- <%= Spree.t(:active) %>
17
- </span>
18
- <% end %>
19
12
  </div>
20
13
 
21
14
  <div class="card-body">
22
15
  <p><%= integration_description %></p>
23
16
  </div>
24
17
 
25
- <div class="card-footer flex <% if integration.present? %>items-baseline justify-between<% else %>flex-col<% end %>">
18
+ <div class="card-footer flex <% if integration.present? %>items-center gap-3<% else %>flex-col<% end %>">
26
19
  <% if integration.present? %>
27
20
  <%= link_to spree.edit_admin_integration_path(integration), class: 'btn btn-sm btn-light' do %>
28
21
  <%= icon 'settings' %>
29
22
  <%= Spree.t(:settings) %>
30
23
  <% end if can?(:update, integration) %>
24
+
25
+ <span class="ml-auto inline-flex items-center text-xs font-medium text-green-700">
26
+ <span class="w-1.5 h-1.5 rounded-full bg-green-500 mr-1"></span>
27
+ <%= Spree.t(:active) %>
28
+ </span>
31
29
  <% else %>
32
30
  <%= link_to_with_icon 'plus', "#{Spree.t('actions.connect')} #{integration_class.integration_name}", spree.new_admin_integration_path(integration: { type: integration_class.name }), class: 'btn btn-primary truncate w-full' if can?(:create, Spree::Integration) %>
33
31
  <% end %>
@@ -5,7 +5,7 @@
5
5
 
6
6
  <%= render partial: 'spree/admin/shared/error_messages', locals: { target: @integration } %>
7
7
 
8
- <%= form_for @integration, as: :integration, url: spree.admin_integration_path(@integration) do |form| %>
8
+ <%= form_for @integration, as: :integration, url: spree.admin_integration_path(@integration), html: { id: "edit_integration_#{@integration.id}" } do |form| %>
9
9
  <div class="grid grid-cols-12 gap-6">
10
10
  <div class="col-span-12 lg:col-span-6 lg:col-start-4">
11
11
  <%= render "spree/admin/integrations/forms/#{@integration.key}", form: form %>
@@ -1,7 +1,7 @@
1
1
  <div class="grid grid-cols-12 gap-6" data-controller="product-form slug-form seo-form" data-seo-form-editor-value="tinymce">
2
2
  <div class="col-span-12 md:col-span-8">
3
3
  <%= render 'spree/admin/products/form/base', f: f %>
4
- <%= render 'spree/admin/shared/media_form', viewable: @product.master, viewable_type: 'Spree::Variant' %>
4
+ <%= render 'spree/admin/shared/media_form', viewable: @product, viewable_type: 'Spree::Product' %>
5
5
  <%= f.fields_for :master, @product.master do |vf| %>
6
6
  <%= render 'spree/admin/variants/form/pricing', f: vf %>
7
7
  <% end unless @product.has_variants? %>
@@ -11,6 +11,7 @@
11
11
  </div>
12
12
  <div class="col-span-12 md:col-span-4">
13
13
  <%= render 'spree/admin/products/form/status', f: f %>
14
+ <%= render 'spree/admin/products/form/publishing', f: f %>
14
15
  <%= render 'spree/admin/products/form/categorization', f: f %>
15
16
  <%= render 'spree/admin/products/form/shipping', f: f %>
16
17
  <%= render 'spree/admin/products/form/tax', f: f %>
@@ -0,0 +1,125 @@
1
+ <%#
2
+ Publishing card — mirrors the SPA's <PublishingCard> in
3
+ packages/dashboard/src/components/spree/products/publishing-card.tsx.
4
+
5
+ All channel attach/detach AND per-channel scheduling submit through the
6
+ same nested-attributes form (+legacy_product_publications_attributes+). Each row
7
+ in the Manage panel toggles the +_destroy+ flag on the corresponding
8
+ publication; unchecked rows for unattached channels send blank attrs that
9
+ the model's +reject_if+ discards.
10
+
11
+ Datetime inputs render values in the store's timezone since datetime-local
12
+ has no TZ awareness (same pattern as the legacy form/_status partial).
13
+ %>
14
+
15
+ <%
16
+ store_timezone = ActiveSupport::TimeZone[current_store.preferred_timezone] || Time.zone
17
+ to_store_local = ->(value) { value&.in_time_zone(store_timezone)&.strftime('%Y-%m-%dT%H:%M') }
18
+ tz_help = Spree.t('admin.datetime_field_timezone_help', zone: store_timezone.name)
19
+ store_channels = current_store.channels.order(:name).to_a
20
+ publications_by_channel = @product.product_publications.includes(:channel).index_by(&:channel_id)
21
+
22
+ # Build one record per store channel: the existing publication when
23
+ # attached, or a fresh ProductPublication when not. Stable channel-keyed
24
+ # indices keep the form params deterministic so checkbox toggles always
25
+ # reference the right slot.
26
+ channel_rows = store_channels.map do |channel|
27
+ publication = publications_by_channel[channel.id] ||
28
+ @product.product_publications.build(channel: channel)
29
+ [channel, publication]
30
+ end
31
+ %>
32
+
33
+ <div class="card mb-6" data-controller="product-publishing">
34
+ <div class="card-header d-flex align-items-center justify-content-between">
35
+ <h6 class="card-title mb-0"><%= Spree.t('admin.publishing.title') %></h6>
36
+ <% if store_channels.any? %>
37
+ <button type="button"
38
+ class="btn btn-sm btn-outline-secondary"
39
+ data-action="product-publishing#toggleManage">
40
+ <%= Spree.t('admin.publishing.manage_cta') %>
41
+ </button>
42
+ <% end %>
43
+ </div>
44
+
45
+ <div class="card-body">
46
+ <% if store_channels.empty? %>
47
+ <p class="text-xs text-muted mb-0"><%= Spree.t('admin.publishing.no_channels') %></p>
48
+ <% else %>
49
+ <%# ---- Manage panel (hidden by default) ---- %>
50
+ <div class="d-none flex flex-col gap-2 mb-3" data-product-publishing-target="manage">
51
+ <p class="text-xs text-muted mb-1"><%= Spree.t('admin.publishing.manage_description') %></p>
52
+ <% channel_rows.each_with_index do |(channel, publication), index| %>
53
+ <%= f.fields_for :legacy_product_publications, publication, child_index: index do |pf| %>
54
+ <%= pf.hidden_field :id if publication.persisted? %>
55
+ <%= pf.hidden_field :channel_id, value: channel.id %>
56
+ <% checked_by_default = publication.persisted? || (@product.new_record? && channel.default?) %>
57
+ <label class="flex items-center gap-3 rounded border px-3 py-2 cursor-pointer hover:bg-gray-50 text-sm">
58
+ <%# The +_destroy+ hidden field defaults to 1 (= destroy/skip);
59
+ the checkbox flips it to 0 (= keep/upsert) when checked.
60
+ +include_hidden: false+ on the checkbox keeps Rails from
61
+ emitting its own competing hidden field. %>
62
+ <%= pf.hidden_field :_destroy, value: '1', id: nil %>
63
+ <%= pf.check_box :_destroy,
64
+ { checked: checked_by_default,
65
+ class: 'form-check-input m-0',
66
+ include_hidden: false },
67
+ '0', '1' %>
68
+ <span class="flex-1"><%= channel.name %></span>
69
+ <% unless channel.active? %>
70
+ <span class="text-xs text-muted"><%= Spree.t('admin.publishing.inactive_marker') %></span>
71
+ <% end %>
72
+ </label>
73
+ <% end %>
74
+ <% end %>
75
+ </div>
76
+
77
+ <%# ---- Per-publication scheduling rows ---- %>
78
+ <% if publications_by_channel.empty? %>
79
+ <p class="text-xs text-muted mb-0"><%= Spree.t('admin.publishing.empty') %></p>
80
+ <% else %>
81
+ <% channel_rows.each_with_index do |(channel, publication), index| %>
82
+ <% next unless publication.persisted? %>
83
+ <%= f.fields_for :legacy_product_publications, publication, child_index: index do |pf| %>
84
+ <details class="publication-row group rounded -mx-2 px-2 hover:bg-gray-50">
85
+ <summary class="flex items-start justify-between gap-3 py-2 cursor-pointer" style="list-style: none;">
86
+ <div class="flex min-w-0 flex-1 flex-col gap-0.5">
87
+ <div class="flex items-center gap-2">
88
+ <span class="truncate text-sm font-medium"><%= channel.name %></span>
89
+ <%= publication_status_badge(@product.status, publication) %>
90
+ </div>
91
+ <span class="text-xs text-muted">
92
+ <%= publication_caption(@product.status, publication, current_store) %>
93
+ </span>
94
+ </div>
95
+ <span class="shrink-0 mt-1 opacity-0 transition-opacity group-hover:opacity-100 text-muted">
96
+ <%= icon('pencil', class: 'h-3.5 w-3.5') %>
97
+ </span>
98
+ </summary>
99
+ <div class="mt-2 ps-2 pb-2 flex flex-col gap-3">
100
+ <div>
101
+ <%= pf.label :published_at,
102
+ Spree.t('admin.publishing.published_at_label'),
103
+ class: 'form-label text-xs mb-1' %>
104
+ <%= pf.datetime_field :published_at,
105
+ value: to_store_local.call(publication.published_at),
106
+ class: 'form-control form-control-sm' %>
107
+ <small class="text-muted text-xs"><%= tz_help %></small>
108
+ </div>
109
+ <div>
110
+ <%= pf.label :unpublished_at,
111
+ Spree.t('admin.publishing.unpublished_at_label'),
112
+ class: 'form-label text-xs mb-1' %>
113
+ <%= pf.datetime_field :unpublished_at,
114
+ value: to_store_local.call(publication.unpublished_at),
115
+ class: 'form-control form-control-sm' %>
116
+ <small class="text-muted text-xs"><%= tz_help %></small>
117
+ </div>
118
+ </div>
119
+ </details>
120
+ <% end %>
121
+ <% end %>
122
+ <% end %>
123
+ <% end %>
124
+ </div>
125
+ </div>
@@ -1,51 +1,7 @@
1
- <%# datetime-local inputs have no timezone; render values in the store's timezone so admins see and submit values in the store's local time. %>
2
- <%
3
- store_timezone = ActiveSupport::TimeZone[current_store.preferred_timezone] || Time.zone
4
- to_store_local = ->(value) { value&.in_time_zone(store_timezone)&.strftime('%Y-%m-%dT%H:%M') }
5
- make_active_at_local = to_store_local.call(@product.make_active_at)
6
- available_on_local = to_store_local.call(@product.available_on)
7
- discontinue_on_local = to_store_local.call(@product.discontinue_on)
8
- tz_help = Spree.t('admin.datetime_field_timezone_help', zone: store_timezone.name)
9
- %>
10
-
11
1
  <div class="card mb-6">
12
2
  <div class="card-body">
13
- <div class="grid grid-cols-12 gap-6 mb-6">
14
- <div class="col-span-6">
15
- <%= f.spree_select :status,
16
- available_status_options(@product),
17
- { selected: @product.status, help_bubble: show_product_status_help_bubble? ? Spree.t('admin.products.status_form.status') : nil },
18
- { data: {
19
- action: 'change->product-form#switchAvailabilityDatesFields',
20
- 'product-form-target': 'status'
21
- }
22
- } %>
23
- </div>
24
- <div class="col-span-6 <%= 'hidden' if @product.active? %>" data-product-form-target="makeActiveAt">
25
- <% if can?(:activate, @product) %>
26
- <%= f.spree_datetime_field :make_active_at,
27
- value: make_active_at_local,
28
- help: tz_help,
29
- help_bubble: Spree.t('admin.products.status_form.make_active_at'),
30
- max: discontinue_on_local %>
31
- <% end %>
32
- </div>
33
- </div>
34
- <div class="grid grid-cols-12 gap-6">
35
- <div data-product-form-target="availableOn" class="col-span-6">
36
- <%= f.spree_datetime_field :available_on,
37
- value: available_on_local,
38
- help: tz_help,
39
- help_bubble: Spree.t('admin.products.status_form.available_on'),
40
- max: discontinue_on_local %>
41
- </div>
42
- <div data-product-form-target="discontinueOn" class="col-span-6">
43
- <%= f.spree_datetime_field :discontinue_on,
44
- value: discontinue_on_local,
45
- help: tz_help,
46
- help_bubble: Spree.t('admin.products.status_form.discontinue_on'),
47
- min: make_active_at_local %>
48
- </div>
49
- </div>
3
+ <%= f.spree_select :status,
4
+ available_status_options(@product),
5
+ { selected: @product.status, help_bubble: show_product_status_help_bubble? ? Spree.t('admin.products.status_form.status') : nil } %>
50
6
  </div>
51
7
  </div>
@@ -7,7 +7,7 @@
7
7
  <button class="btn btn-light btn-sm px-1"><%= icon('grip-vertical', class: 'mr-0') %></button>
8
8
  </div>
9
9
 
10
- <%= link_to spree.edit_admin_asset_path(asset), data: { turbo_frame: 'dialog', action: 'dialog#open' } do %>
10
+ <%= link_to spree.edit_admin_asset_path(asset), data: { turbo_frame: 'drawer', action: 'drawer#open' } do %>
11
11
  <%= spree_image_tag(asset.attachment, width: 200, height: 200, alt: asset.alt, class: 'w-full block h-full absolute') %>
12
12
  <% end %>
13
13
  </div>
@@ -10,7 +10,6 @@
10
10
  <%= render 'spree/admin/variants/form/basic', f: f %>
11
11
  <%= render 'spree/admin/variants/form/pricing', f: f %>
12
12
  <%= render 'spree/admin/variants/form/inventory', f: f %>
13
- <%= render 'spree/admin/shared/media_form', viewable: @variant, viewable_type: 'Spree::Variant' %>
14
13
  <%= render 'spree/admin/shared/edit_resource_links', f: f %>
15
14
  </div>
16
15
  </div>
@@ -251,6 +251,15 @@ Rails.application.config.after_initialize do
251
251
  active: -> { controller_name == 'policies' },
252
252
  if: -> { can?(:manage, Spree::Policy) }
253
253
 
254
+ # Channels
255
+ settings_nav.add :channels,
256
+ label: :channels,
257
+ url: :admin_channels_path,
258
+ icon: 'broadcast',
259
+ position: 65,
260
+ active: -> { controller_name == 'channels' },
261
+ if: -> { can?(:manage, Spree::Channel) }
262
+
254
263
  # Payment Methods
255
264
  settings_nav.add :payment_methods,
256
265
  label: :payments,
@@ -128,6 +128,19 @@ Rails.application.config.after_initialize do
128
128
  search_url: ->(view_context) { view_context.spree.admin_tags_select_options_path(format: :json, taggable_type: 'Spree::Product') },
129
129
  method: ->(product) { product.tag_list.to_sentence }
130
130
 
131
+ Spree.admin.tables.products.add :channels,
132
+ label: :channels,
133
+ type: :association,
134
+ filter_type: :select,
135
+ sortable: false,
136
+ filterable: true,
137
+ default: false,
138
+ position: 90,
139
+ ransack_attribute: 'channels_id',
140
+ operators: %i[in not_in],
141
+ value_options: -> { Spree::Current.store&.channels&.order(:name)&.pluck(:name, :id)&.map { |name, id| { value: id, label: name } } || [] },
142
+ method: ->(product) { product.channels.pluck(:name).to_sentence }
143
+
131
144
  # Products bulk actions
132
145
  Spree.admin.tables.products.add_bulk_action :set_active,
133
146
  label: 'admin.bulk_ops.products.title.set_active',
@@ -350,6 +363,19 @@ Rails.application.config.after_initialize do
350
363
  operators: %i[in],
351
364
  search_url: ->(view_context) { view_context.spree.select_options_admin_promotions_path(format: :json) }
352
365
 
366
+ Spree.admin.tables.orders.add :channel,
367
+ label: :channel,
368
+ type: :association,
369
+ filter_type: :select,
370
+ sortable: true,
371
+ filterable: true,
372
+ default: true,
373
+ position: 35,
374
+ ransack_attribute: 'channel_id',
375
+ operators: %i[eq not_eq in not_in],
376
+ value_options: -> { Spree::Current.store&.channels&.order(:name)&.pluck(:name, :id)&.map { |name, id| { value: id, label: name } } || [] },
377
+ method: ->(order) { order.channel&.name }
378
+
353
379
  # Register Checkouts table (draft orders)
354
380
  Spree.admin.tables.register(:checkouts, model_class: Spree::Order, search_param: :search, date_range_param: :created_at, new_resource: false)
355
381
 
@@ -1744,6 +1770,40 @@ Rails.application.config.after_initialize do
1744
1770
  default: true,
1745
1771
  position: 10
1746
1772
 
1773
+ # ==========================================
1774
+ # Register Channels table
1775
+ # ==========================================
1776
+ Spree.admin.tables.register(:channels, model_class: Spree::Channel, search_param: :name_cont)
1777
+
1778
+ Spree.admin.tables.channels.add :name,
1779
+ label: :name,
1780
+ type: :link,
1781
+ sortable: true,
1782
+ filterable: true,
1783
+ default: true,
1784
+ position: 10
1785
+
1786
+ Spree.admin.tables.channels.add :code,
1787
+ label: :code,
1788
+ sortable: true,
1789
+ default: true,
1790
+ position: 20
1791
+
1792
+ Spree.admin.tables.channels.add :active,
1793
+ label: :status,
1794
+ type: :status,
1795
+ sortable: false,
1796
+ default: true,
1797
+ position: 30,
1798
+ method: ->(c) { c.active? ? 'active' : 'inactive' }
1799
+
1800
+ Spree.admin.tables.channels.add :default,
1801
+ label: :default,
1802
+ type: :boolean,
1803
+ sortable: true,
1804
+ default: true,
1805
+ position: 40
1806
+
1747
1807
  # ==========================================
1748
1808
  # Register Refund Reasons table
1749
1809
  # ==========================================
@@ -26,6 +26,8 @@ en:
26
26
  revoked: API key has been revoked
27
27
  revoked_at: Revoked at
28
28
  revoked_by: Revoked by
29
+ scopes: Scopes
30
+ scopes_description: Required for secret keys. Pick the narrowest set of scopes your integration needs.
29
31
  secret_key_hidden: This secret key cannot be revealed. If you've lost it, revoke this key and create a new one.
30
32
  statuses:
31
33
  active: Active
@@ -73,6 +75,13 @@ en:
73
75
  title:
74
76
  add_tags: Add tags
75
77
  remove_tags: Remove tags
78
+ channels:
79
+ code_hint: A short, stable identifier (e.g. "online", "pos", "wholesale"). Used by API clients via the X-Spree-Channel header and for order attribution.
80
+ default: Default channel
81
+ default_hint: Exactly one channel per store is the default. Used as the fallback when no channel is selected and as the auto-publish target for new products.
82
+ order_routing_strategy: Order routing strategy
83
+ order_routing_strategy_hint: Overrides the store-level routing strategy for orders attributed to this channel.
84
+ order_routing_strategy_inherit: Inherit from store
76
85
  checkout_settings:
77
86
  checkout_links:
78
87
  description: Links to these pages will be displayed in the checkout footer.
@@ -278,14 +287,34 @@ en:
278
287
  seo:
279
288
  placeholder: Add a title and description to see how this product might appear in a search engine listing
280
289
  status_form:
281
- available_on: Marks when the product will be released, put a future date to indicate that this is a pre-order
282
- discontinue_on: Marks when the product should be automatically taken off from your site
283
- make_active_at: Marks when the product should be automatically promoted to "active" state
284
290
  status: Draft products aren't available for purchase. Active products are live.
291
+ status_options:
292
+ active: Active
293
+ archived: Archived
294
+ draft: Draft
285
295
  stores:
286
296
  choose_stores: Choose which stores this product should be available in
287
297
  variants:
288
298
  option_types_link: To add more option types please go to <a href="%{link}">Option Types</a>
299
+ publishing:
300
+ caption_hidden_after: Comes down %{date}
301
+ caption_live: Visible to customers now
302
+ caption_not_available: Hidden — product is %{product_status}
303
+ caption_scheduled: Goes live %{date}
304
+ caption_unpublished: Was hidden on %{date}
305
+ caption_window: Live %{start} → %{end}
306
+ empty: Not listed on any sales channel yet.
307
+ inactive_marker: Inactive
308
+ manage_cta: Manage
309
+ manage_description: Pick which sales channels list this product. Toggle a channel off to unlist it everywhere.
310
+ no_channels: No sales channels available. Create one in Settings → Sales channels.
311
+ published_at_label: Publish from
312
+ status_hidden: Hidden
313
+ status_live: Live
314
+ status_not_available: Not available
315
+ status_scheduled: Scheduled
316
+ title: Publishing
317
+ unpublished_at_label: Unpublish on
289
318
  recommended_size: Recommended size
290
319
  report_created: Your report is being generated. You will receive an email with a download link when it is ready!
291
320
  reset_digital_link_download_limits: Reset download limits
data/config/routes.rb CHANGED
@@ -226,6 +226,7 @@ Spree::Core::Engine.add_routes do
226
226
  resources :payment_methods, except: :show
227
227
  resources :shipping_methods, except: :show
228
228
  resources :shipping_categories, except: :show
229
+ resources :channels, except: :show
229
230
  resources :store_credit_categories
230
231
  resources :tax_rates, except: :show
231
232
  resources :tax_categories, except: :show
@@ -9,6 +9,8 @@ module Spree
9
9
  :admin_users_header_partials,
10
10
  :body_end_partials,
11
11
  :body_start_partials,
12
+ :channels_actions_partials,
13
+ :channels_header_partials,
12
14
  :classifications_actions_partials,
13
15
  :classifications_header_partials,
14
16
  :coupon_codes_actions_partials,
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spree_admin
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.4.3
4
+ version: 5.5.0.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vendo Connect Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-05-19 00:00:00.000000000 Z
11
+ date: 2026-06-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: spree
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 5.4.3
19
+ version: 5.5.0.rc1
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 5.4.3
26
+ version: 5.5.0.rc1
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: active_link_to
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -301,6 +301,7 @@ files:
301
301
  - app/controllers/spree/admin/assets_controller.rb
302
302
  - app/controllers/spree/admin/base_controller.rb
303
303
  - app/controllers/spree/admin/bulk_operations_controller.rb
304
+ - app/controllers/spree/admin/channels_controller.rb
304
305
  - app/controllers/spree/admin/checkouts_controller.rb
305
306
  - app/controllers/spree/admin/classifications_controller.rb
306
307
  - app/controllers/spree/admin/countries_controller.rb
@@ -392,6 +393,7 @@ files:
392
393
  - app/helpers/spree/admin/base_helper.rb
393
394
  - app/helpers/spree/admin/bulk_editor_helper.rb
394
395
  - app/helpers/spree/admin/bulk_operations_helper.rb
396
+ - app/helpers/spree/admin/channels_helper.rb
395
397
  - app/helpers/spree/admin/code_block_helper.rb
396
398
  - app/helpers/spree/admin/customer_returns_helper.rb
397
399
  - app/helpers/spree/admin/dialog_helper.rb
@@ -400,7 +402,6 @@ files:
400
402
  - app/helpers/spree/admin/flash_helper.rb
401
403
  - app/helpers/spree/admin/json_preview_helper.rb
402
404
  - app/helpers/spree/admin/metafields_helper.rb
403
- - app/helpers/spree/admin/modal_helper.rb
404
405
  - app/helpers/spree/admin/navigation_helper.rb
405
406
  - app/helpers/spree/admin/onboarding_helper.rb
406
407
  - app/helpers/spree/admin/orders_filters_helper.rb
@@ -411,6 +412,7 @@ files:
411
412
  - app/helpers/spree/admin/promotion_actions_helper.rb
412
413
  - app/helpers/spree/admin/promotion_rules_helper.rb
413
414
  - app/helpers/spree/admin/promotions_helper.rb
415
+ - app/helpers/spree/admin/publishing_helper.rb
414
416
  - app/helpers/spree/admin/reimbursement_type_helper.rb
415
417
  - app/helpers/spree/admin/shipment_helper.rb
416
418
  - app/helpers/spree/admin/sortable_tree_helper.rb
@@ -456,6 +458,7 @@ files:
456
458
  - app/javascript/spree/admin/controllers/page_builder_controller.js
457
459
  - app/javascript/spree/admin/controllers/password_toggle_controller.js
458
460
  - app/javascript/spree/admin/controllers/product_form_controller.js
461
+ - app/javascript/spree/admin/controllers/product_publishing_controller.js
459
462
  - app/javascript/spree/admin/controllers/query_builder_controller.js
460
463
  - app/javascript/spree/admin/controllers/range_input_controller.js
461
464
  - app/javascript/spree/admin/controllers/replace_controller.js
@@ -546,6 +549,10 @@ files:
546
549
  - app/views/spree/admin/bulk_operations/forms/_tag_picker.html.erb
547
550
  - app/views/spree/admin/bulk_operations/forms/_taxon_picker.html.erb
548
551
  - app/views/spree/admin/bulk_operations/new.html.erb
552
+ - app/views/spree/admin/channels/_form.html.erb
553
+ - app/views/spree/admin/channels/edit.html.erb
554
+ - app/views/spree/admin/channels/index.html.erb
555
+ - app/views/spree/admin/channels/new.html.erb
549
556
  - app/views/spree/admin/checkouts/index.html.erb
550
557
  - app/views/spree/admin/classifications/_classification.html.erb
551
558
  - app/views/spree/admin/classifications/create.turbo_stream.erb
@@ -776,6 +783,7 @@ files:
776
783
  - app/views/spree/admin/products/form/_base.html.erb
777
784
  - app/views/spree/admin/products/form/_categorization.html.erb
778
785
  - app/views/spree/admin/products/form/_inventory.html.erb
786
+ - app/views/spree/admin/products/form/_publishing.html.erb
779
787
  - app/views/spree/admin/products/form/_shipping.html.erb
780
788
  - app/views/spree/admin/products/form/_status.html.erb
781
789
  - app/views/spree/admin/products/form/_tax.html.erb
@@ -795,17 +803,17 @@ files:
795
803
  - app/views/spree/admin/promotion_actions/new.html.erb
796
804
  - app/views/spree/admin/promotion_rules/_promotion_rule.html.erb
797
805
  - app/views/spree/admin/promotion_rules/edit.html.erb
806
+ - app/views/spree/admin/promotion_rules/forms/_category.html.erb
798
807
  - app/views/spree/admin/promotion_rules/forms/_country.html.erb
799
808
  - app/views/spree/admin/promotion_rules/forms/_currency.html.erb
809
+ - app/views/spree/admin/promotion_rules/forms/_customer.html.erb
800
810
  - app/views/spree/admin/promotion_rules/forms/_customer_group.html.erb
811
+ - app/views/spree/admin/promotion_rules/forms/_customer_logged_in.html.erb
801
812
  - app/views/spree/admin/promotion_rules/forms/_first_order.html.erb
802
813
  - app/views/spree/admin/promotion_rules/forms/_item_total.html.erb
803
814
  - app/views/spree/admin/promotion_rules/forms/_one_use_per_user.html.erb
804
815
  - app/views/spree/admin/promotion_rules/forms/_option_value.html.erb
805
816
  - app/views/spree/admin/promotion_rules/forms/_product.html.erb
806
- - app/views/spree/admin/promotion_rules/forms/_taxon.html.erb
807
- - app/views/spree/admin/promotion_rules/forms/_user.html.erb
808
- - app/views/spree/admin/promotion_rules/forms/_user_logged_in.html.erb
809
817
  - app/views/spree/admin/promotion_rules/new.html.erb
810
818
  - app/views/spree/admin/promotions/_actions.html.erb
811
819
  - app/views/spree/admin/promotions/_form.html.erb
@@ -881,7 +889,6 @@ files:
881
889
  - app/views/spree/admin/shared/_map.html.erb
882
890
  - app/views/spree/admin/shared/_media_asset.html.erb
883
891
  - app/views/spree/admin/shared/_media_form.html.erb
884
- - app/views/spree/admin/shared/_modal.html.erb
885
892
  - app/views/spree/admin/shared/_multi_product_picker.html.erb
886
893
  - app/views/spree/admin/shared/_new_item_dropdown.html.erb
887
894
  - app/views/spree/admin/shared/_new_resource.html.erb
@@ -1164,9 +1171,9 @@ licenses:
1164
1171
  - BSD-3-Clause
1165
1172
  metadata:
1166
1173
  bug_tracker_uri: https://github.com/spree/spree/issues
1167
- changelog_uri: https://github.com/spree/spree/releases/tag/v5.4.3
1174
+ changelog_uri: https://github.com/spree/spree/releases/tag/v5.5.0.rc1
1168
1175
  documentation_uri: https://docs.spreecommerce.org/
1169
- source_code_uri: https://github.com/spree/spree/tree/v5.4.3
1176
+ source_code_uri: https://github.com/spree/spree/tree/v5.5.0.rc1
1170
1177
  post_install_message:
1171
1178
  rdoc_options: []
1172
1179
  require_paths:
@@ -1,31 +0,0 @@
1
- module Spree
2
- module Admin
3
- module ModalHelper
4
- # render a header for the modal
5
- # @param title [String, Proc] the title of the modal
6
- # @return [String]
7
- def modal_header(title)
8
- Spree::Deprecation.warn('Bootstrap modals are deprecated and will be removed in Spree 5.5. Please use native dialogs with `dialog_header` helper.')
9
-
10
- title = capture(&title) if block_given?
11
- content_tag(:div, class: 'modal-header') do
12
- content_tag(:h5, title, class: 'modal-title') + modal_close_button
13
- end.html_safe
14
- end
15
-
16
- # render a close button for the modal
17
- # @return [String]
18
- def modal_close_button
19
- button_tag('', type: 'button', class: 'btn-close', data: { dismiss: 'modal', aria_label: Spree.t(:close) }).html_safe
20
- end
21
-
22
- # render a discard button for the modal
23
- # @return [String]
24
- def modal_discard_button
25
- button_tag(type: 'button', class: 'btn btn-light mr-auto', data: { dismiss: 'modal' }) do
26
- Spree.t('actions.discard')
27
- end.html_safe
28
- end
29
- end
30
- end
31
- end
@@ -1,27 +0,0 @@
1
- <div class="modal fade" id="modal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
2
- <div class="modal-dialog modal-dialog-centered">
3
- <%= turbo_frame_tag :dialog_modal do %>
4
- <div class="modal-content">
5
- <%= modal_header(Spree.t(:loading)) %>
6
- <div class="modal-body text-center min-h-[300px]">
7
- <%= render 'spree/admin/shared/spinner' %>
8
- </div>
9
- </div>
10
- <% end %>
11
- </div>
12
- </div>
13
-
14
- <div class="modal fade" id="modal-lg" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
15
- <div class="modal-dialog modal-dialog-centered modal-lg">
16
- <%= turbo_frame_tag :dialog_modal_lg do %>
17
- <div class="modal-content">
18
- <%= modal_header(Spree.t(:loading)) %>
19
- <div class="modal-body text-center min-h-[300px]">
20
- <%= render 'spree/admin/shared/spinner' %>
21
- </div>
22
- </div>
23
- <% end %>
24
- </div>
25
- </div>
26
-
27
- <%= turbo_frame_tag :modal_scripts %>