spree_admin 5.4.2 → 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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/spree/admin/assets_controller.rb +7 -1
  3. data/app/controllers/spree/admin/bulk_operations_controller.rb +4 -0
  4. data/app/controllers/spree/admin/channels_controller.rb +13 -0
  5. data/app/controllers/spree/admin/invitations_controller.rb +1 -2
  6. data/app/controllers/spree/admin/products_controller.rb +8 -8
  7. data/app/controllers/spree/admin/resource_controller.rb +11 -2
  8. data/app/controllers/spree/admin/states_controller.rb +13 -0
  9. data/app/controllers/spree/admin/zones_controller.rb +0 -1
  10. data/app/helpers/spree/admin/assets_helper.rb +5 -1
  11. data/app/helpers/spree/admin/base_helper.rb +0 -8
  12. data/app/helpers/spree/admin/channels_helper.rb +14 -0
  13. data/app/helpers/spree/admin/json_preview_helper.rb +5 -0
  14. data/app/helpers/spree/admin/orders_helper.rb +0 -29
  15. data/app/helpers/spree/admin/publishing_helper.rb +73 -0
  16. data/app/helpers/spree/admin/turbo_helper.rb +0 -18
  17. data/app/helpers/spree/admin/webhook_endpoints_helper.rb +1 -0
  18. data/app/javascript/spree/admin/application.js +4 -0
  19. data/app/javascript/spree/admin/controllers/product_form_controller.js +0 -26
  20. data/app/javascript/spree/admin/controllers/product_publishing_controller.js +11 -0
  21. data/app/javascript/spree/admin/controllers/slug_form_controller.js +18 -1
  22. data/app/javascript/spree/admin/controllers/zone_state_select_controller.js +31 -0
  23. data/app/presenters/spree/admin/order_summary_presenter.rb +12 -0
  24. data/app/views/spree/admin/api_keys/_form.html.erb +18 -0
  25. data/app/views/spree/admin/assets/edit.html.erb +64 -9
  26. data/app/views/spree/admin/channels/_form.html.erb +19 -0
  27. data/app/views/spree/admin/channels/edit.html.erb +1 -0
  28. data/app/views/spree/admin/channels/index.html.erb +12 -0
  29. data/app/views/spree/admin/channels/new.html.erb +1 -0
  30. data/app/views/spree/admin/gift_cards/_form.html.erb +1 -1
  31. data/app/views/spree/admin/integrations/_integration.html.erb +7 -9
  32. data/app/views/spree/admin/integrations/edit.html.erb +1 -1
  33. data/app/views/spree/admin/invitations/expired.html.erb +4 -0
  34. data/app/views/spree/admin/orders/_line_item.html.erb +3 -3
  35. data/app/views/spree/admin/orders/_shipment.html.erb +1 -1
  36. data/app/views/spree/admin/products/_form.html.erb +2 -1
  37. data/app/views/spree/admin/products/form/_publishing.html.erb +125 -0
  38. data/app/views/spree/admin/products/form/_status.html.erb +3 -47
  39. data/app/views/spree/admin/shared/_media_asset.html.erb +1 -1
  40. data/app/views/spree/admin/variants/edit.html.erb +0 -1
  41. data/app/views/spree/admin/zones/_state_members.html.erb +14 -4
  42. data/config/initializers/spree_admin_navigation.rb +9 -0
  43. data/config/initializers/spree_admin_tables.rb +62 -2
  44. data/config/locales/en.yml +32 -3
  45. data/config/routes.rb +11 -1
  46. data/lib/spree/admin/engine.rb +2 -0
  47. data/lib/spree/admin/tailwind_helper.rb +1 -1
  48. metadata +21 -11
  49. data/app/helpers/spree/admin/modal_helper.rb +0 -31
  50. data/app/views/spree/admin/shared/_modal.html.erb +0 -27
  51. /data/app/views/spree/admin/promotion_rules/forms/{_taxon.html.erb → _category.html.erb} +0 -0
  52. /data/app/views/spree/admin/promotion_rules/forms/{_user.html.erb → _customer.html.erb} +0 -0
  53. /data/app/views/spree/admin/promotion_rules/forms/{_user_logged_in.html.erb → _customer_logged_in.html.erb} +0 -0
@@ -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' %>
@@ -1,7 +1,7 @@
1
1
  <div class="card mb-6">
2
2
  <div class="card-body">
3
3
  <% unless f.object.persisted? %>
4
- <%= f.spree_text_field :code, required: true, help_bubble: Spree.t('admin.gift_cards.code_help_text'), autofocus: true %>
4
+ <%= f.spree_text_field :code, help_bubble: Spree.t('admin.gift_cards.code_help_text'), autofocus: true %>
5
5
  <% end %>
6
6
 
7
7
  <%= f.spree_money_field :amount, currency: f.object.currency, required: true %>
@@ -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 %>
@@ -0,0 +1,4 @@
1
+ <div class="text-center">
2
+ <h1 class="text-2xl font-semibold mb-2"><%= Spree.t('invitation_expired.heading') %></h1>
3
+ <p class="text-gray-500"><%= Spree.t('invitation_expired.body') %></p>
4
+ </div>
@@ -30,12 +30,12 @@
30
30
  <%= dropdown_menu do %>
31
31
  <%= link_to_with_icon "edit",
32
32
  Spree.t(:edit),
33
- spree.edit_admin_order_line_item_path(@order, line_item),
33
+ spree.edit_admin_order_line_item_path(line_item.order, line_item),
34
34
  class: "dropdown-item" %>
35
35
  <% if line_item.digital_links.any? %>
36
36
  <%= link_to_with_icon "refresh",
37
37
  Spree.t("admin.reset_digital_link_download_limits"),
38
- spree.reset_digital_links_limit_admin_order_line_item_path(@order, line_item),
38
+ spree.reset_digital_links_limit_admin_order_line_item_path(line_item.order, line_item),
39
39
  data: {
40
40
  turbo_method: :post,
41
41
  turbo_confirm: Spree.t(:are_you_sure),
@@ -46,7 +46,7 @@
46
46
 
47
47
  <% if can?(:destroy, line_item) %>
48
48
  <div class="dropdown-divider"></div>
49
- <%= link_to_with_icon "trash", Spree.t('actions.destroy'), spree.admin_order_line_item_path(@order, line_item), data: { turbo_method: :delete, turbo_frame: '_top', turbo_confirm: Spree.t(:are_you_sure )}, class: 'dropdown-item text-red-600 hover:bg-red-100' %>
49
+ <%= link_to_with_icon "trash", Spree.t('actions.destroy'), spree.admin_order_line_item_path(line_item.order, line_item), data: { turbo_method: :delete, turbo_frame: '_top', turbo_confirm: Spree.t(:are_you_sure )}, class: 'dropdown-item text-red-600 hover:bg-red-100' %>
50
50
  <% end %>
51
51
  <% end %>
52
52
  <% end %>
@@ -87,7 +87,7 @@
87
87
  <% if can_ship?(shipment) %>
88
88
  <div class="card-footer flex justify-end border-t bg-gray-25">
89
89
  <% if shipment.tracked? %>
90
- <%= link_to_with_icon 'send.svg', Spree.t(:ship), spree.ship_admin_order_shipment_path(order, shipment), class: 'ml-auto btn btn-primary mb-0', data: {turbo_method: :post, turbo_confirm: Spree.t(:are_you_sure)} %>
90
+ <%= link_to_with_icon 'send.svg', Spree.t(:ship), spree.ship_admin_order_shipment_path(order, shipment), class: 'ml-auto btn btn-primary mb-0', data: {turbo_method: :post, turbo_confirm: Spree.t(:are_you_sure), turbo_frame: '_top'} %>
91
91
  <% elsif !shipment.digital? %>
92
92
  <span class="ship ml-auto btn btn-primary disabled mb-0" data-controller="tooltip" data-tooltip-placement-value="left">
93
93
  <%= icon 'send' %>
@@ -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>
@@ -1,4 +1,4 @@
1
- <div id="state_members">
1
+ <div id="state_members" data-controller="zone-state-select" data-zone-state-select-url-value="<%= spree.select_options_admin_country_states_path(':country_id') %>">
2
2
  <div class="card mb-6">
3
3
  <div class="card-header">
4
4
  <h5 class="card-title">
@@ -9,12 +9,22 @@
9
9
  <div class="card-body">
10
10
  <div class="form-group">
11
11
  <%= label_tag :states_country_id, Spree.t(:country) %>
12
- <%= tom_select_tag :states_country_id, class: 'w-full', options: Spree::Country.joins(:states).to_tom_select_json, active_option: @selected_country&.id, select_data: {action: "auto-submit#submit"} %>
12
+ <%= tom_select_tag :states_country_id,
13
+ class: 'w-full',
14
+ options: Spree::Country.joins(:states).distinct.to_tom_select_json,
15
+ active_option: @selected_country&.id,
16
+ select_data: {action: "change->zone-state-select#countryChanged"}
17
+ %>
13
18
  </div>
14
19
 
15
- <div class="form-group">
20
+ <div class="form-group" data-zone-state-select-target="statesSelect">
16
21
  <%= zone_form.label :state_ids, Spree.t(:states) %>
17
- <%= tom_select_tag 'zone[state_ids]', multiple: true, class: 'w-full', options: @states.to_tom_select_json, active_option: @zone.state_ids %>
22
+ <%= tom_select_tag 'zone[state_ids]',
23
+ multiple: true,
24
+ class: 'w-full',
25
+ url: @selected_country ? spree.select_options_admin_country_states_path(@selected_country) : nil,
26
+ active_option: @zone.state_ids
27
+ %>
18
28
  </div>
19
29
  </div>
20
30
  </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',
@@ -135,7 +148,7 @@ Rails.application.config.after_initialize do
135
148
  action_path: ->(view_context) { view_context.spree.bulk_status_update_admin_products_path(status: 'active') },
136
149
  body: 'admin.bulk_ops.products.body.set_active',
137
150
  position: 10,
138
- condition: -> { can?(:activate, Spree::Product) }
151
+ condition: -> { can?(:bulk_activate, Spree::Product) }
139
152
 
140
153
  Spree.admin.tables.products.add_bulk_action :set_draft,
141
154
  label: 'admin.bulk_ops.products.title.set_draft',
@@ -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
 
@@ -462,7 +488,7 @@ Rails.application.config.after_initialize do
462
488
  position: 30,
463
489
  ransack_attribute: 'addresses_country_name',
464
490
  operators: %i[eq],
465
- search_url: ->(view_context) { view_context.spree.admin_countries_select_options_path(format: :json) },
491
+ search_url: ->(view_context) { view_context.spree.select_options_admin_countries_path(format: :json) },
466
492
  partial: 'spree/admin/tables/columns/user_location'
467
493
 
468
494
  # Number of orders
@@ -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
@@ -62,9 +62,18 @@ Spree::Core::Engine.add_routes do
62
62
  end
63
63
  get '/taxons/select_options' => 'taxons#select_options', as: :taxons_select_options, defaults: { format: :json }
64
64
  get '/tags/select_options' => 'tags#select_options', as: :tags_select_options, defaults: { format: :json }
65
- get '/countries/select_options' => 'countries#select_options', as: :countries_select_options, defaults: { format: :json }
66
65
  get '/users/select_options' => 'users#select_options', as: :users_select_options, defaults: { format: :json }
67
66
  get '/stock_locations/select_options' => 'stock_locations#select_options', as: :stock_locations_select_options, defaults: { format: :json }
67
+ resources :countries, only: [] do
68
+ collection do
69
+ get :select_options, defaults: { format: :json }
70
+ end
71
+ resources :states, only: [] do
72
+ collection do
73
+ get :select_options, defaults: { format: :json }
74
+ end
75
+ end
76
+ end
68
77
 
69
78
  # media library
70
79
  resources :assets, only: [:create, :edit, :update, :destroy] do
@@ -217,6 +226,7 @@ Spree::Core::Engine.add_routes do
217
226
  resources :payment_methods, except: :show
218
227
  resources :shipping_methods, except: :show
219
228
  resources :shipping_categories, except: :show
229
+ resources :channels, except: :show
220
230
  resources :store_credit_categories
221
231
  resources :tax_rates, except: :show
222
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,
@@ -43,7 +43,7 @@ module Spree
43
43
 
44
44
  def spree_engines
45
45
  Rails::Engine.subclasses.select do |engine|
46
- engine.name&.start_with?("Spree::")
46
+ engine.name&.start_with?("Spree")
47
47
  end
48
48
  end
49
49