spree_admin 5.4.3 → 5.5.0.rc2

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/spree/admin/auth_rate_limiting.rb +58 -0
  3. data/app/controllers/spree/admin/assets_controller.rb +7 -1
  4. data/app/controllers/spree/admin/channels_controller.rb +13 -0
  5. data/app/controllers/spree/admin/products_controller.rb +8 -8
  6. data/app/controllers/spree/admin/user_passwords_controller.rb +4 -0
  7. data/app/controllers/spree/admin/user_sessions_controller.rb +4 -0
  8. data/app/helpers/spree/admin/assets_helper.rb +5 -1
  9. data/app/helpers/spree/admin/base_helper.rb +0 -8
  10. data/app/helpers/spree/admin/channels_helper.rb +14 -0
  11. data/app/helpers/spree/admin/json_preview_helper.rb +5 -0
  12. data/app/helpers/spree/admin/orders_helper.rb +0 -29
  13. data/app/helpers/spree/admin/publishing_helper.rb +73 -0
  14. data/app/helpers/spree/admin/turbo_helper.rb +0 -18
  15. data/app/helpers/spree/admin/webhook_endpoints_helper.rb +1 -0
  16. data/app/javascript/spree/admin/application.js +2 -0
  17. data/app/javascript/spree/admin/controllers/product_form_controller.js +0 -26
  18. data/app/javascript/spree/admin/controllers/product_publishing_controller.js +11 -0
  19. data/app/javascript/spree/admin/controllers/slug_form_controller.js +18 -1
  20. data/app/presenters/spree/admin/order_summary_presenter.rb +12 -0
  21. data/app/views/spree/admin/api_keys/_form.html.erb +18 -0
  22. data/app/views/spree/admin/assets/edit.html.erb +64 -9
  23. data/app/views/spree/admin/channels/_form.html.erb +19 -0
  24. data/app/views/spree/admin/channels/edit.html.erb +1 -0
  25. data/app/views/spree/admin/channels/index.html.erb +12 -0
  26. data/app/views/spree/admin/channels/new.html.erb +1 -0
  27. data/app/views/spree/admin/integrations/_integration.html.erb +7 -9
  28. data/app/views/spree/admin/integrations/edit.html.erb +1 -1
  29. data/app/views/spree/admin/product_translations/index.html.erb +1 -1
  30. data/app/views/spree/admin/products/_form.html.erb +2 -1
  31. data/app/views/spree/admin/products/form/_publishing.html.erb +125 -0
  32. data/app/views/spree/admin/products/form/_status.html.erb +3 -47
  33. data/app/views/spree/admin/shared/_media_asset.html.erb +1 -1
  34. data/app/views/spree/admin/variants/edit.html.erb +0 -1
  35. data/config/initializers/spree_admin_navigation.rb +9 -0
  36. data/config/initializers/spree_admin_tables.rb +60 -0
  37. data/config/locales/en.yml +32 -3
  38. data/config/routes.rb +1 -0
  39. data/lib/spree/admin/engine.rb +2 -0
  40. data/lib/spree/admin/runtime_configuration.rb +5 -0
  41. metadata +19 -11
  42. data/app/helpers/spree/admin/modal_helper.rb +0 -31
  43. data/app/views/spree/admin/shared/_modal.html.erb +0 -27
  44. /data/app/views/spree/admin/promotion_rules/forms/{_taxon.html.erb → _category.html.erb} +0 -0
  45. /data/app/views/spree/admin/promotion_rules/forms/{_user.html.erb → _customer.html.erb} +0 -0
  46. /data/app/views/spree/admin/promotion_rules/forms/{_user_logged_in.html.erb → _customer_logged_in.html.erb} +0 -0
@@ -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 %>
@@ -101,7 +101,7 @@
101
101
  <%= render 'spree/admin/shared/index_table_options', collection: @products %>
102
102
  </div>
103
103
  <% else %>
104
- <%= render 'spree/admin/shared/no_resource_found', new_object_url: nil %>
104
+ <%= render 'spree/admin/shared/no_resource_found', new_object_url: nil, model_class: Spree::Product %>
105
105
  <% end %>
106
106
  </div>
107
107
  </div>
@@ -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,
@@ -17,6 +17,11 @@ module Spree
17
17
  preference :legacy_sidebar_navigation, :boolean, default: false
18
18
 
19
19
  preference :reports_line_items_limit, :integer, default: 1000
20
+
21
+ # Brute-force rate limiting for the admin dashboard auth endpoints (login / password reset).
22
+ preference :rate_limit_window, :integer, default: 60 # window in seconds
23
+ preference :rate_limit_login, :integer, default: 5 # per IP and per email
24
+ preference :rate_limit_password_reset, :integer, default: 3 # per IP and per email
20
25
  end
21
26
  end
22
27
  end
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.rc2
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-15 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.rc2
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.rc2
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: active_link_to
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -284,6 +284,7 @@ files:
284
284
  - app/assets/tailwind/spree/admin/views/_dashboard.css
285
285
  - app/assets/tailwind/spree/admin/views/_page-builder.css
286
286
  - app/controllers/concerns/spree/admin/analytics_concern.rb
287
+ - app/controllers/concerns/spree/admin/auth_rate_limiting.rb
287
288
  - app/controllers/concerns/spree/admin/breadcrumb_concern.rb
288
289
  - app/controllers/concerns/spree/admin/bulk_operations_concern.rb
289
290
  - app/controllers/concerns/spree/admin/order_breadcrumb_concern.rb
@@ -301,6 +302,7 @@ files:
301
302
  - app/controllers/spree/admin/assets_controller.rb
302
303
  - app/controllers/spree/admin/base_controller.rb
303
304
  - app/controllers/spree/admin/bulk_operations_controller.rb
305
+ - app/controllers/spree/admin/channels_controller.rb
304
306
  - app/controllers/spree/admin/checkouts_controller.rb
305
307
  - app/controllers/spree/admin/classifications_controller.rb
306
308
  - app/controllers/spree/admin/countries_controller.rb
@@ -392,6 +394,7 @@ files:
392
394
  - app/helpers/spree/admin/base_helper.rb
393
395
  - app/helpers/spree/admin/bulk_editor_helper.rb
394
396
  - app/helpers/spree/admin/bulk_operations_helper.rb
397
+ - app/helpers/spree/admin/channels_helper.rb
395
398
  - app/helpers/spree/admin/code_block_helper.rb
396
399
  - app/helpers/spree/admin/customer_returns_helper.rb
397
400
  - app/helpers/spree/admin/dialog_helper.rb
@@ -400,7 +403,6 @@ files:
400
403
  - app/helpers/spree/admin/flash_helper.rb
401
404
  - app/helpers/spree/admin/json_preview_helper.rb
402
405
  - app/helpers/spree/admin/metafields_helper.rb
403
- - app/helpers/spree/admin/modal_helper.rb
404
406
  - app/helpers/spree/admin/navigation_helper.rb
405
407
  - app/helpers/spree/admin/onboarding_helper.rb
406
408
  - app/helpers/spree/admin/orders_filters_helper.rb
@@ -411,6 +413,7 @@ files:
411
413
  - app/helpers/spree/admin/promotion_actions_helper.rb
412
414
  - app/helpers/spree/admin/promotion_rules_helper.rb
413
415
  - app/helpers/spree/admin/promotions_helper.rb
416
+ - app/helpers/spree/admin/publishing_helper.rb
414
417
  - app/helpers/spree/admin/reimbursement_type_helper.rb
415
418
  - app/helpers/spree/admin/shipment_helper.rb
416
419
  - app/helpers/spree/admin/sortable_tree_helper.rb
@@ -456,6 +459,7 @@ files:
456
459
  - app/javascript/spree/admin/controllers/page_builder_controller.js
457
460
  - app/javascript/spree/admin/controllers/password_toggle_controller.js
458
461
  - app/javascript/spree/admin/controllers/product_form_controller.js
462
+ - app/javascript/spree/admin/controllers/product_publishing_controller.js
459
463
  - app/javascript/spree/admin/controllers/query_builder_controller.js
460
464
  - app/javascript/spree/admin/controllers/range_input_controller.js
461
465
  - app/javascript/spree/admin/controllers/replace_controller.js
@@ -546,6 +550,10 @@ files:
546
550
  - app/views/spree/admin/bulk_operations/forms/_tag_picker.html.erb
547
551
  - app/views/spree/admin/bulk_operations/forms/_taxon_picker.html.erb
548
552
  - app/views/spree/admin/bulk_operations/new.html.erb
553
+ - app/views/spree/admin/channels/_form.html.erb
554
+ - app/views/spree/admin/channels/edit.html.erb
555
+ - app/views/spree/admin/channels/index.html.erb
556
+ - app/views/spree/admin/channels/new.html.erb
549
557
  - app/views/spree/admin/checkouts/index.html.erb
550
558
  - app/views/spree/admin/classifications/_classification.html.erb
551
559
  - app/views/spree/admin/classifications/create.turbo_stream.erb
@@ -776,6 +784,7 @@ files:
776
784
  - app/views/spree/admin/products/form/_base.html.erb
777
785
  - app/views/spree/admin/products/form/_categorization.html.erb
778
786
  - app/views/spree/admin/products/form/_inventory.html.erb
787
+ - app/views/spree/admin/products/form/_publishing.html.erb
779
788
  - app/views/spree/admin/products/form/_shipping.html.erb
780
789
  - app/views/spree/admin/products/form/_status.html.erb
781
790
  - app/views/spree/admin/products/form/_tax.html.erb
@@ -795,17 +804,17 @@ files:
795
804
  - app/views/spree/admin/promotion_actions/new.html.erb
796
805
  - app/views/spree/admin/promotion_rules/_promotion_rule.html.erb
797
806
  - app/views/spree/admin/promotion_rules/edit.html.erb
807
+ - app/views/spree/admin/promotion_rules/forms/_category.html.erb
798
808
  - app/views/spree/admin/promotion_rules/forms/_country.html.erb
799
809
  - app/views/spree/admin/promotion_rules/forms/_currency.html.erb
810
+ - app/views/spree/admin/promotion_rules/forms/_customer.html.erb
800
811
  - app/views/spree/admin/promotion_rules/forms/_customer_group.html.erb
812
+ - app/views/spree/admin/promotion_rules/forms/_customer_logged_in.html.erb
801
813
  - app/views/spree/admin/promotion_rules/forms/_first_order.html.erb
802
814
  - app/views/spree/admin/promotion_rules/forms/_item_total.html.erb
803
815
  - app/views/spree/admin/promotion_rules/forms/_one_use_per_user.html.erb
804
816
  - app/views/spree/admin/promotion_rules/forms/_option_value.html.erb
805
817
  - 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
818
  - app/views/spree/admin/promotion_rules/new.html.erb
810
819
  - app/views/spree/admin/promotions/_actions.html.erb
811
820
  - app/views/spree/admin/promotions/_form.html.erb
@@ -881,7 +890,6 @@ files:
881
890
  - app/views/spree/admin/shared/_map.html.erb
882
891
  - app/views/spree/admin/shared/_media_asset.html.erb
883
892
  - app/views/spree/admin/shared/_media_form.html.erb
884
- - app/views/spree/admin/shared/_modal.html.erb
885
893
  - app/views/spree/admin/shared/_multi_product_picker.html.erb
886
894
  - app/views/spree/admin/shared/_new_item_dropdown.html.erb
887
895
  - app/views/spree/admin/shared/_new_resource.html.erb
@@ -1164,9 +1172,9 @@ licenses:
1164
1172
  - BSD-3-Clause
1165
1173
  metadata:
1166
1174
  bug_tracker_uri: https://github.com/spree/spree/issues
1167
- changelog_uri: https://github.com/spree/spree/releases/tag/v5.4.3
1175
+ changelog_uri: https://github.com/spree/spree/releases/tag/v5.5.0.rc2
1168
1176
  documentation_uri: https://docs.spreecommerce.org/
1169
- source_code_uri: https://github.com/spree/spree/tree/v5.4.3
1177
+ source_code_uri: https://github.com/spree/spree/tree/v5.5.0.rc2
1170
1178
  post_install_message:
1171
1179
  rdoc_options: []
1172
1180
  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