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.
- checksums.yaml +4 -4
- data/app/controllers/spree/admin/assets_controller.rb +7 -1
- data/app/controllers/spree/admin/bulk_operations_controller.rb +4 -0
- data/app/controllers/spree/admin/channels_controller.rb +13 -0
- data/app/controllers/spree/admin/invitations_controller.rb +1 -2
- data/app/controllers/spree/admin/products_controller.rb +8 -8
- data/app/controllers/spree/admin/resource_controller.rb +11 -2
- data/app/controllers/spree/admin/states_controller.rb +13 -0
- data/app/controllers/spree/admin/zones_controller.rb +0 -1
- data/app/helpers/spree/admin/assets_helper.rb +5 -1
- data/app/helpers/spree/admin/base_helper.rb +0 -8
- data/app/helpers/spree/admin/channels_helper.rb +14 -0
- data/app/helpers/spree/admin/json_preview_helper.rb +5 -0
- data/app/helpers/spree/admin/orders_helper.rb +0 -29
- data/app/helpers/spree/admin/publishing_helper.rb +73 -0
- data/app/helpers/spree/admin/turbo_helper.rb +0 -18
- data/app/helpers/spree/admin/webhook_endpoints_helper.rb +1 -0
- data/app/javascript/spree/admin/application.js +4 -0
- data/app/javascript/spree/admin/controllers/product_form_controller.js +0 -26
- data/app/javascript/spree/admin/controllers/product_publishing_controller.js +11 -0
- data/app/javascript/spree/admin/controllers/slug_form_controller.js +18 -1
- data/app/javascript/spree/admin/controllers/zone_state_select_controller.js +31 -0
- data/app/presenters/spree/admin/order_summary_presenter.rb +12 -0
- data/app/views/spree/admin/api_keys/_form.html.erb +18 -0
- data/app/views/spree/admin/assets/edit.html.erb +64 -9
- data/app/views/spree/admin/channels/_form.html.erb +19 -0
- data/app/views/spree/admin/channels/edit.html.erb +1 -0
- data/app/views/spree/admin/channels/index.html.erb +12 -0
- data/app/views/spree/admin/channels/new.html.erb +1 -0
- data/app/views/spree/admin/gift_cards/_form.html.erb +1 -1
- data/app/views/spree/admin/integrations/_integration.html.erb +7 -9
- data/app/views/spree/admin/integrations/edit.html.erb +1 -1
- data/app/views/spree/admin/invitations/expired.html.erb +4 -0
- data/app/views/spree/admin/orders/_line_item.html.erb +3 -3
- data/app/views/spree/admin/orders/_shipment.html.erb +1 -1
- data/app/views/spree/admin/products/_form.html.erb +2 -1
- data/app/views/spree/admin/products/form/_publishing.html.erb +125 -0
- data/app/views/spree/admin/products/form/_status.html.erb +3 -47
- data/app/views/spree/admin/shared/_media_asset.html.erb +1 -1
- data/app/views/spree/admin/variants/edit.html.erb +0 -1
- data/app/views/spree/admin/zones/_state_members.html.erb +14 -4
- data/config/initializers/spree_admin_navigation.rb +9 -0
- data/config/initializers/spree_admin_tables.rb +62 -2
- data/config/locales/en.yml +32 -3
- data/config/routes.rb +11 -1
- data/lib/spree/admin/engine.rb +2 -0
- data/lib/spree/admin/tailwind_helper.rb +1 -1
- metadata +21 -11
- data/app/helpers/spree/admin/modal_helper.rb +0 -31
- data/app/views/spree/admin/shared/_modal.html.erb +0 -27
- /data/app/views/spree/admin/promotion_rules/forms/{_taxon.html.erb → _category.html.erb} +0 -0
- /data/app/views/spree/admin/promotion_rules/forms/{_user.html.erb → _customer.html.erb} +0 -0
- /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,
|
|
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
|
|
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-
|
|
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 %>
|
|
@@ -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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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: '
|
|
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,
|
|
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]',
|
|
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?(:
|
|
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.
|
|
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
|
# ==========================================
|
data/config/locales/en.yml
CHANGED
|
@@ -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
|
data/lib/spree/admin/engine.rb
CHANGED
|
@@ -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,
|