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.
- checksums.yaml +4 -4
- data/app/controllers/concerns/spree/admin/auth_rate_limiting.rb +58 -0
- data/app/controllers/spree/admin/assets_controller.rb +7 -1
- data/app/controllers/spree/admin/channels_controller.rb +13 -0
- data/app/controllers/spree/admin/products_controller.rb +8 -8
- data/app/controllers/spree/admin/user_passwords_controller.rb +4 -0
- data/app/controllers/spree/admin/user_sessions_controller.rb +4 -0
- 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 +2 -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/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/integrations/_integration.html.erb +7 -9
- data/app/views/spree/admin/integrations/edit.html.erb +1 -1
- data/app/views/spree/admin/product_translations/index.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/config/initializers/spree_admin_navigation.rb +9 -0
- data/config/initializers/spree_admin_tables.rb +60 -0
- data/config/locales/en.yml +32 -3
- data/config/routes.rb +1 -0
- data/lib/spree/admin/engine.rb +2 -0
- data/lib/spree/admin/runtime_configuration.rb +5 -0
- metadata +19 -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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cf50ce30e9bb9efb5d77bfc984d87f921c997520125e78b375c375d391999aec
|
|
4
|
+
data.tar.gz: 8bbc5de5e700786db47adc040e6e048901503bb9572d382302336d33a7531866
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3dd8c8013f072e443fb99b2c70450f005904f0d738870dcf6182e4edd39810fb462e6869eea08cc0ebc235d2d55895750469eaff710b4958fd86880952108aff
|
|
7
|
+
data.tar.gz: d032bec7f6a7f7eee57054a1b4af6e36e19fca1b80ef276c9a95eb71d6451151c629e05446426893cf1fae120b52a97e9863eafc2644ef2b1a2ac65475030d59
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Admin
|
|
3
|
+
module AuthRateLimiting
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
class_methods do
|
|
7
|
+
# @param limit_preference [Symbol] e.g. :rate_limit_login / :rate_limit_password_reset
|
|
8
|
+
# @param redirect_to [Proc] evaluated in controller context to the path to bounce
|
|
9
|
+
# back to when rate limited, e.g. `-> { new_session_path(resource_name) }`.
|
|
10
|
+
# @return [void]
|
|
11
|
+
def auth_rate_limit(limit_preference, redirect_to:)
|
|
12
|
+
limit = Spree::Admin::RuntimeConfig[limit_preference] || 5
|
|
13
|
+
window = (Spree::Admin::RuntimeConfig[:rate_limit_window] || 60).seconds
|
|
14
|
+
prefix = limit_preference.to_s # unique namespace per controller/action
|
|
15
|
+
|
|
16
|
+
# By IP — always present; backstops blank-email floods.
|
|
17
|
+
rate_limit(
|
|
18
|
+
to: limit,
|
|
19
|
+
within: window,
|
|
20
|
+
by: -> { "#{prefix}-ip:#{request.remote_ip}" },
|
|
21
|
+
with: -> { admin_auth_rate_limit_response(redirect_to) },
|
|
22
|
+
store: Rails.cache,
|
|
23
|
+
only: :create
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# By submitted email (case-insensitive). Falls back to per-IP bucketing when
|
|
27
|
+
# the email is blank, so blank submissions don't all share one global bucket.
|
|
28
|
+
rate_limit(
|
|
29
|
+
to: limit,
|
|
30
|
+
within: window,
|
|
31
|
+
by: lambda {
|
|
32
|
+
email = admin_auth_rate_limit_email
|
|
33
|
+
email.present? ? "#{prefix}-email:#{email}" : "#{prefix}-email-ip:#{request.remote_ip}"
|
|
34
|
+
},
|
|
35
|
+
with: -> { admin_auth_rate_limit_response(redirect_to) },
|
|
36
|
+
store: Rails.cache,
|
|
37
|
+
only: :create
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
# Email is read via Devise's `resource_params` so it works regardless of the
|
|
45
|
+
# configured admin user class / resource param key.
|
|
46
|
+
def admin_auth_rate_limit_email
|
|
47
|
+
resource_params[:email].to_s.strip.downcase.presence
|
|
48
|
+
rescue StandardError
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def admin_auth_rate_limit_response(redirect_path)
|
|
53
|
+
flash[:alert] = I18n.t('devise.failure.too_many_attempts')
|
|
54
|
+
redirect_to instance_exec(&redirect_path)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -38,8 +38,14 @@ module Spree
|
|
|
38
38
|
|
|
39
39
|
private
|
|
40
40
|
|
|
41
|
+
# Includes :variant_ids so the variant-assignment checkboxes mass-assign
|
|
42
|
+
# straight into Spree::Asset#variant_ids= — the model resolves prefixed
|
|
43
|
+
# ids and rejects cross-product variants.
|
|
41
44
|
def permitted_resource_params
|
|
42
|
-
params.require(:asset).permit(
|
|
45
|
+
params.require(:asset).permit(
|
|
46
|
+
*Spree::PermittedAttributes.asset_attributes,
|
|
47
|
+
variant_ids: []
|
|
48
|
+
)
|
|
43
49
|
end
|
|
44
50
|
|
|
45
51
|
def create_turbo_stream_enabled?
|
|
@@ -21,7 +21,7 @@ module Spree
|
|
|
21
21
|
new_action.before :build_master_stock_items
|
|
22
22
|
edit_action.before :build_master_prices
|
|
23
23
|
edit_action.before :build_master_stock_items
|
|
24
|
-
create.after :
|
|
24
|
+
create.after :assign_session_uploaded_assets
|
|
25
25
|
update.before :skip_updating_status
|
|
26
26
|
update.before :update_status
|
|
27
27
|
update.before :remove_empty_params
|
|
@@ -185,7 +185,7 @@ module Spree
|
|
|
185
185
|
@product_variant_ids[variant.human_name] = variant.id.to_s
|
|
186
186
|
@product_variant_prefix_ids[variant.human_name] = variant.to_param
|
|
187
187
|
|
|
188
|
-
image = variant.primary_media
|
|
188
|
+
image = variant.primary_media || @product.primary_media
|
|
189
189
|
if image.present? && image.attached? && image.variable?
|
|
190
190
|
@product_variant_images[variant.human_name] = helpers.spree_image_url(image, variant: :mini)
|
|
191
191
|
end
|
|
@@ -287,15 +287,15 @@ module Spree
|
|
|
287
287
|
{}
|
|
288
288
|
end
|
|
289
289
|
|
|
290
|
-
def
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
uploaded_assets = session_uploaded_assets('Spree::Variant')
|
|
290
|
+
def assign_session_uploaded_assets
|
|
291
|
+
uploaded_assets = session_uploaded_assets('Spree::Product')
|
|
294
292
|
|
|
295
293
|
return if uploaded_assets.empty?
|
|
296
294
|
|
|
297
|
-
uploaded_assets.update_all(viewable_id: @product.
|
|
298
|
-
|
|
295
|
+
uploaded_assets.update_all(viewable_id: @product.id, viewable_type: 'Spree::Product', updated_at: Time.current)
|
|
296
|
+
@product.update_thumbnail!
|
|
297
|
+
|
|
298
|
+
clear_session_for_uploaded_assets('Spree::Product')
|
|
299
299
|
end
|
|
300
300
|
|
|
301
301
|
def check_slug_availability
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
module Spree
|
|
2
2
|
module Admin
|
|
3
3
|
class UserPasswordsController < defined?(Devise::PasswordsController) ? Devise::PasswordsController : Spree::Admin::BaseController
|
|
4
|
+
include Spree::Admin::AuthRateLimiting
|
|
5
|
+
|
|
4
6
|
layout 'spree/minimal'
|
|
5
7
|
|
|
8
|
+
auth_rate_limit :rate_limit_password_reset, redirect_to: -> { new_password_path(resource_name) }
|
|
9
|
+
|
|
6
10
|
def create
|
|
7
11
|
self.resource = resource_class.send_reset_password_instructions(resource_params)
|
|
8
12
|
yield resource if block_given?
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
module Spree
|
|
2
2
|
module Admin
|
|
3
3
|
class UserSessionsController < defined?(Devise::SessionsController) ? Devise::SessionsController : Spree::Admin::BaseController
|
|
4
|
+
include Spree::Admin::AuthRateLimiting
|
|
5
|
+
|
|
4
6
|
layout 'spree/minimal'
|
|
5
7
|
|
|
8
|
+
auth_rate_limit :rate_limit_login, redirect_to: -> { new_session_path(resource_name) }
|
|
9
|
+
|
|
6
10
|
# We need to overwrite this action because `return_to` url may be in a different domain
|
|
7
11
|
# So we need to pass `allow_other_host` option to `redirect_to` method
|
|
8
12
|
def create
|
|
@@ -3,7 +3,11 @@ module Spree
|
|
|
3
3
|
module AssetsHelper
|
|
4
4
|
def media_form_assets(viewable, viewable_type)
|
|
5
5
|
if viewable&.persisted?
|
|
6
|
-
|
|
6
|
+
# Product#images delegates to the master variant (legacy alias), so
|
|
7
|
+
# 5.5+ product-level uploads wouldn't show. gallery_media returns
|
|
8
|
+
# product-level media when present and falls back to legacy
|
|
9
|
+
# variant-pinned images during the transition.
|
|
10
|
+
viewable.respond_to?(:gallery_media) ? viewable.gallery_media : viewable.images
|
|
7
11
|
elsif session_uploaded_assets(viewable_type).any?
|
|
8
12
|
Spree::Asset.accessible_by(current_ability, :manage).where(id: session_uploaded_assets(viewable_type))
|
|
9
13
|
else
|
|
@@ -36,14 +36,6 @@ module Spree
|
|
|
36
36
|
@settings_area.present?
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
def settings_active?
|
|
40
|
-
Spree::Deprecation.warn('settings_active? is deprecated and will be removed in Spree 5.5. Please use settings_area? instead')
|
|
41
|
-
@settings_active || %w[admin_users audits custom_domains exports invitations oauth_applications
|
|
42
|
-
payment_methods refund_reasons reimbursement_types return_authorization_reasons roles
|
|
43
|
-
shipping_categories shipping_methods stock_locations store_credit_categories
|
|
44
|
-
stores tax_categories tax_rates webhooks webhooks_subscribers zones policies metafield_definitions].include?(controller_name) || settings_area?
|
|
45
|
-
end
|
|
46
|
-
|
|
47
39
|
# @return [Array<String>] the available countries for checkout
|
|
48
40
|
def available_countries_iso
|
|
49
41
|
@available_countries_iso ||= current_store.countries_available_for_checkout.pluck(:iso)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Admin
|
|
3
|
+
module ChannelsHelper
|
|
4
|
+
# Registered +Spree::OrderRouting::Strategy::Base+ subclasses presented in
|
|
5
|
+
# the channel edit form, sourced from +Spree.order_routing.strategies+ so the
|
|
6
|
+
# picker can never drift from what the model accepts. A blank value clears the
|
|
7
|
+
# channel-level override and falls back to
|
|
8
|
+
# +Store#preferred_order_routing_strategy+.
|
|
9
|
+
def channel_order_routing_strategy_options
|
|
10
|
+
Spree.order_routing.strategies.map { |strategy| [strategy.display_name, strategy.to_s] }
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -69,6 +69,11 @@ module Spree
|
|
|
69
69
|
return Spree.api.public_send(method_name)
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
+
if namespace == 'Spree::Api::V3::Admin'
|
|
73
|
+
method_name = "admin_#{class_name.underscore}_serializer"
|
|
74
|
+
return Spree.api.public_send(method_name) if Spree.api.respond_to?(method_name)
|
|
75
|
+
end
|
|
76
|
+
|
|
72
77
|
# Fall back to direct constant lookup
|
|
73
78
|
serializer_class_name = "#{namespace}::#{class_name}Serializer"
|
|
74
79
|
serializer_class_name.safe_constantize
|
|
@@ -1,24 +1,6 @@
|
|
|
1
1
|
module Spree
|
|
2
2
|
module Admin
|
|
3
3
|
module OrdersHelper
|
|
4
|
-
TaxLine = Struct.new(:label, :display_amount, :item, :for_shipment, keyword_init: true) do
|
|
5
|
-
def name
|
|
6
|
-
Spree::Deprecation.warn("TaxLine is deprecated and will be removed in Spree 5.5")
|
|
7
|
-
item_name = item.name
|
|
8
|
-
item_name += " #{Spree.t(:shipment).downcase}" if for_shipment
|
|
9
|
-
|
|
10
|
-
"#{label} (#{item_name})"
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def order_summary_tax_lines_additional(order)
|
|
15
|
-
Spree::Deprecation.warn("order_summary_tax_lines_additional is deprecated and will be removed in Spree 5.5")
|
|
16
|
-
line_item_taxes = order.line_item_adjustments.tax.map { |tax_adjustment| map_to_tax_line(tax_adjustment) }
|
|
17
|
-
shipment_taxes = order.shipment_adjustments.tax.map { |tax_adjustment| map_to_tax_line(tax_adjustment, for_shipment: true) }
|
|
18
|
-
|
|
19
|
-
line_item_taxes + shipment_taxes
|
|
20
|
-
end
|
|
21
|
-
|
|
22
4
|
def order_payment_state(order, options = {})
|
|
23
5
|
return if order.payment_state.blank?
|
|
24
6
|
|
|
@@ -140,17 +122,6 @@ module Spree
|
|
|
140
122
|
'' => 'Transaction failed because wrong CVV2 number was entered or no CVV2 number was entered'
|
|
141
123
|
}
|
|
142
124
|
end
|
|
143
|
-
|
|
144
|
-
private
|
|
145
|
-
|
|
146
|
-
def map_to_tax_line(tax_adjustment, for_shipment: false)
|
|
147
|
-
TaxLine.new(
|
|
148
|
-
label: tax_adjustment.label,
|
|
149
|
-
display_amount: tax_adjustment.display_amount,
|
|
150
|
-
item: tax_adjustment.adjustable,
|
|
151
|
-
for_shipment: for_shipment
|
|
152
|
-
)
|
|
153
|
-
end
|
|
154
125
|
end
|
|
155
126
|
end
|
|
156
127
|
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Spree
|
|
4
|
+
module Admin
|
|
5
|
+
# Helpers for the product Publishing card in the Rails admin. Mirrors the
|
|
6
|
+
# SPA's publishing card (see packages/dashboard/src/components/spree/products/publishing-card.tsx):
|
|
7
|
+
# per-channel status is gated by product status — Draft/Archived products
|
|
8
|
+
# render every publication as +not_available+ regardless of window.
|
|
9
|
+
module PublishingHelper
|
|
10
|
+
# @param product_status [String] the product's status (draft/active/archived)
|
|
11
|
+
# @param publication [Spree::ProductPublication]
|
|
12
|
+
# @return [Symbol] one of :live, :scheduled, :hidden, :not_available
|
|
13
|
+
def publication_schedule_status(product_status, publication)
|
|
14
|
+
return :not_available unless product_status == 'active'
|
|
15
|
+
|
|
16
|
+
now = Time.current
|
|
17
|
+
return :hidden if publication.unpublished_at && publication.unpublished_at <= now
|
|
18
|
+
return :scheduled if publication.published_at && publication.published_at > now
|
|
19
|
+
|
|
20
|
+
:live
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Renders a colored dot + status label for a publication row.
|
|
24
|
+
def publication_status_badge(product_status, publication)
|
|
25
|
+
status = publication_schedule_status(product_status, publication)
|
|
26
|
+
dot_class = case status
|
|
27
|
+
when :live then 'bg-success'
|
|
28
|
+
when :scheduled then 'bg-warning'
|
|
29
|
+
else 'bg-secondary'
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
label = Spree.t("admin.publishing.status_#{status}")
|
|
33
|
+
|
|
34
|
+
content_tag(:span, class: 'd-inline-flex align-items-center gap-1 text-muted small') do
|
|
35
|
+
content_tag(:span, '', class: "publication-dot rounded-circle #{dot_class}",
|
|
36
|
+
style: 'display:inline-block; width:0.5rem; height:0.5rem;') +
|
|
37
|
+
content_tag(:span, label)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# One-line summary of the publication window in the store's timezone.
|
|
42
|
+
def publication_caption(product_status, publication, store)
|
|
43
|
+
status = publication_schedule_status(product_status, publication)
|
|
44
|
+
tz = ActiveSupport::TimeZone[store.preferred_timezone] || Time.zone
|
|
45
|
+
|
|
46
|
+
case status
|
|
47
|
+
when :not_available
|
|
48
|
+
Spree.t('admin.publishing.caption_not_available',
|
|
49
|
+
product_status: Spree.t("admin.products.status_options.#{product_status}", default: product_status.to_s.humanize))
|
|
50
|
+
when :hidden
|
|
51
|
+
Spree.t('admin.publishing.caption_unpublished',
|
|
52
|
+
date: l(publication.unpublished_at.in_time_zone(tz), format: :short))
|
|
53
|
+
when :scheduled
|
|
54
|
+
if publication.unpublished_at
|
|
55
|
+
Spree.t('admin.publishing.caption_window',
|
|
56
|
+
start: l(publication.published_at.in_time_zone(tz), format: :short),
|
|
57
|
+
end: l(publication.unpublished_at.in_time_zone(tz), format: :short))
|
|
58
|
+
else
|
|
59
|
+
Spree.t('admin.publishing.caption_scheduled',
|
|
60
|
+
date: l(publication.published_at.in_time_zone(tz), format: :short))
|
|
61
|
+
end
|
|
62
|
+
else # :live
|
|
63
|
+
if publication.unpublished_at
|
|
64
|
+
Spree.t('admin.publishing.caption_hidden_after',
|
|
65
|
+
date: l(publication.unpublished_at.in_time_zone(tz), format: :short))
|
|
66
|
+
else
|
|
67
|
+
Spree.t('admin.publishing.caption_live')
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -1,24 +1,6 @@
|
|
|
1
1
|
module Spree
|
|
2
2
|
module Admin
|
|
3
3
|
module TurboHelper
|
|
4
|
-
def turbo_close_modal(modal_id = nil)
|
|
5
|
-
Spree::Deprecation.warn('turbo_close_modal is deprecated and will be removed in Spree 5.5. Use turbo_close_dialog instead.')
|
|
6
|
-
|
|
7
|
-
modal_id ||= 'modal'
|
|
8
|
-
|
|
9
|
-
turbo_stream.replace :modal_scripts do
|
|
10
|
-
turbo_frame_tag :modal_scripts do
|
|
11
|
-
javascript_tag do
|
|
12
|
-
raw <<~JS
|
|
13
|
-
if (document.querySelector('##{modal_id}')) {
|
|
14
|
-
window.$("##{modal_id}").modal('hide');
|
|
15
|
-
}
|
|
16
|
-
JS
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
|
|
22
4
|
def turbo_close_dialog
|
|
23
5
|
turbo_stream.replace 'main-dialog' do
|
|
24
6
|
render 'spree/admin/shared/dialog'
|
|
@@ -68,6 +68,7 @@ import OrderBillingAddressController from 'spree/admin/controllers/order_billing
|
|
|
68
68
|
import PageBuilderController from 'spree/admin/controllers/page_builder_controller'
|
|
69
69
|
import PasswordToggle from 'spree/admin/controllers/password_toggle_controller'
|
|
70
70
|
import ProductFormController from 'spree/admin/controllers/product_form_controller'
|
|
71
|
+
import ProductPublishingController from 'spree/admin/controllers/product_publishing_controller'
|
|
71
72
|
import QueryBuilderController from 'spree/admin/controllers/query_builder_controller'
|
|
72
73
|
import RangeInputController from 'spree/admin/controllers/range_input_controller'
|
|
73
74
|
import TableController from 'spree/admin/controllers/table_controller'
|
|
@@ -140,6 +141,7 @@ application.register('page-builder', PageBuilderController)
|
|
|
140
141
|
application.register('password-toggle', PasswordToggle)
|
|
141
142
|
application.register('password-visibility', PasswordVisibility)
|
|
142
143
|
application.register('product-form', ProductFormController)
|
|
144
|
+
application.register('product-publishing', ProductPublishingController)
|
|
143
145
|
application.register('query-builder', QueryBuilderController)
|
|
144
146
|
application.register('range-input', RangeInputController)
|
|
145
147
|
application.register('table', TableController)
|
|
@@ -4,10 +4,6 @@ export default class extends Controller {
|
|
|
4
4
|
static targets = [
|
|
5
5
|
'trackInventoryCheckbox',
|
|
6
6
|
'quantityForm',
|
|
7
|
-
'availableOn',
|
|
8
|
-
'makeActiveAt',
|
|
9
|
-
'discontinueOn',
|
|
10
|
-
'status',
|
|
11
7
|
'pricesForm'
|
|
12
8
|
]
|
|
13
9
|
|
|
@@ -26,28 +22,6 @@ export default class extends Controller {
|
|
|
26
22
|
this.togglePricesFormVisibility()
|
|
27
23
|
}
|
|
28
24
|
|
|
29
|
-
switchAvailabilityDatesFields(event) {
|
|
30
|
-
let status = event.target.value
|
|
31
|
-
if (status === 'draft') {
|
|
32
|
-
this.show(this.availableOnTarget)
|
|
33
|
-
this.show(this.makeActiveAtTarget)
|
|
34
|
-
} else if (status === 'active') {
|
|
35
|
-
this.show(this.availableOnTarget)
|
|
36
|
-
this.hide(this.makeActiveAtTarget)
|
|
37
|
-
} else {
|
|
38
|
-
this.hide(this.availableOnTarget)
|
|
39
|
-
this.hide(this.makeActiveAtTarget)
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
show(element) {
|
|
44
|
-
element.classList.remove('hidden', 'd-none')
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
hide(element) {
|
|
48
|
-
element.classList.add('hidden')
|
|
49
|
-
}
|
|
50
|
-
|
|
51
25
|
toggleQuantityFormVisibility() {
|
|
52
26
|
if (this.hasQuantityFormTarget) {
|
|
53
27
|
if (!this.hasVariantsValue && this.trackInventoryCheckboxTarget.checked) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus'
|
|
2
|
+
|
|
3
|
+
// Toggles the "Manage" channel-checkbox panel on the product Publishing card.
|
|
4
|
+
// Pairs with spree/admin/app/views/spree/admin/products/form/_publishing.html.erb.
|
|
5
|
+
export default class extends Controller {
|
|
6
|
+
static targets = ['manage']
|
|
7
|
+
|
|
8
|
+
toggleManage() {
|
|
9
|
+
this.manageTarget.classList.toggle('d-none')
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -6,9 +6,26 @@ export default class extends Controller {
|
|
|
6
6
|
'url',
|
|
7
7
|
]
|
|
8
8
|
|
|
9
|
+
connect() {
|
|
10
|
+
this.urlTouched = this.hasUrlTarget && this.urlTarget.value.length > 0
|
|
11
|
+
if (this.hasUrlTarget) {
|
|
12
|
+
this.urlTarget.addEventListener('input', () => { this.urlTouched = true })
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
9
16
|
updateUrlFromName() {
|
|
17
|
+
if (this.urlTouched) return
|
|
10
18
|
const name = this.nameTarget.value
|
|
11
|
-
|
|
19
|
+
// Mirrors ActiveSupport's +String#parameterize+: NFKD decompose, drop
|
|
20
|
+
// combining marks (accents), lowercase, hyphenate the rest. Keeps the
|
|
21
|
+
// legacy admin's auto-fill aligned with what +normalizes :code+ stores
|
|
22
|
+
// server-side — without NFKD, "Café" rendered "caf-" instead of "cafe".
|
|
23
|
+
const url = name
|
|
24
|
+
.normalize('NFKD')
|
|
25
|
+
.replace(/\p{M}/gu, '')
|
|
26
|
+
.toLowerCase()
|
|
27
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
28
|
+
.replace(/(^-|-$)+/g, '')
|
|
12
29
|
this.urlTarget.value = url
|
|
13
30
|
}
|
|
14
31
|
}
|
|
@@ -13,6 +13,7 @@ module Spree
|
|
|
13
13
|
[
|
|
14
14
|
*metadata_rows,
|
|
15
15
|
:separator,
|
|
16
|
+
channel_row,
|
|
16
17
|
market_row,
|
|
17
18
|
locale_row,
|
|
18
19
|
currency_row,
|
|
@@ -73,6 +74,17 @@ module Spree
|
|
|
73
74
|
rows
|
|
74
75
|
end
|
|
75
76
|
|
|
77
|
+
def channel_row
|
|
78
|
+
return nil if order.channel.blank?
|
|
79
|
+
|
|
80
|
+
{
|
|
81
|
+
label: Spree.t(:channel),
|
|
82
|
+
value: order.channel.name,
|
|
83
|
+
id: 'channel',
|
|
84
|
+
link: Spree::Core::Engine.routes.url_helpers.edit_admin_channel_path(order.channel)
|
|
85
|
+
}
|
|
86
|
+
end
|
|
87
|
+
|
|
76
88
|
def market_row
|
|
77
89
|
return nil if order.market.blank?
|
|
78
90
|
|
|
@@ -24,3 +24,21 @@
|
|
|
24
24
|
<% end %>
|
|
25
25
|
</div>
|
|
26
26
|
</div>
|
|
27
|
+
|
|
28
|
+
<div class="card mb-6" id="api-key-scopes" data-secret-only data-show-when-key-type="secret">
|
|
29
|
+
<div class="card-header">
|
|
30
|
+
<h5 class="card-title"><%= Spree.t('admin.api_keys.scopes', default: 'Scopes') %></h5>
|
|
31
|
+
<p class="text-sm text-gray-500 mt-1">
|
|
32
|
+
<%= Spree.t('admin.api_keys.scopes_description', default: 'Required for secret keys. Pick the narrowest set of scopes your integration needs.') %>
|
|
33
|
+
</p>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="card-body">
|
|
36
|
+
<% Spree::ApiKey::SCOPES.each do |scope| %>
|
|
37
|
+
<div class="custom-control form-checkbox mb-1">
|
|
38
|
+
<%= f.check_box :scopes, { multiple: true, class: 'custom-control-input', id: "api_key_scopes_#{scope}" }, scope, nil %>
|
|
39
|
+
<%= f.label "scopes_#{scope}", scope, class: 'custom-control-label font-mono text-sm' %>
|
|
40
|
+
</div>
|
|
41
|
+
<% end %>
|
|
42
|
+
<%= f.hidden_field :scopes, value: '', multiple: true %>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
@@ -1,17 +1,72 @@
|
|
|
1
|
-
<%= turbo_frame_tag :
|
|
2
|
-
<%= dialog_header(Spree.t(:edit) + ' ' + Spree.t(:image)) %>
|
|
1
|
+
<%= turbo_frame_tag :drawer do %>
|
|
3
2
|
<%= form_with model: @asset, url: spree.admin_asset_path(@asset), method: :put, scope: :asset do |f| %>
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
<%= drawer_header(Spree.t(:edit) + ' ' + Spree.t(:image)) %>
|
|
4
|
+
|
|
5
|
+
<div class="drawer-body">
|
|
6
|
+
<% if @asset.attachment.attached? %>
|
|
7
|
+
<% blob_url_opts = { host: request.host_with_port, protocol: (request.ssl? ? 'https' : 'http') } %>
|
|
8
|
+
<% download_url = Rails.application.routes.url_helpers.rails_blob_url(@asset.attachment.blob, **blob_url_opts, disposition: 'attachment') %>
|
|
9
|
+
<% original_url = Rails.application.routes.url_helpers.rails_blob_url(@asset.attachment.blob, **blob_url_opts) %>
|
|
10
|
+
<div class="mb-4 rounded-lg border bg-light overflow-hidden">
|
|
11
|
+
<a href="<%= original_url %>"
|
|
12
|
+
target="_blank"
|
|
13
|
+
rel="noopener"
|
|
14
|
+
title="<%= Spree.t(:view_full_size) %>"
|
|
15
|
+
style="display: block; position: relative; cursor: zoom-in;">
|
|
16
|
+
<%= spree_image_tag(@asset.attachment, variant: :large, alt: @asset.alt, class: 'w-full max-h-96 object-contain') %>
|
|
17
|
+
<span style="position: absolute; top: 8px; right: 8px; display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 6px; background: rgba(255,255,255,0.95); color: #4b5563; box-shadow: 0 1px 2px rgba(0,0,0,0.1); pointer-events: none;">
|
|
18
|
+
<%= icon('zoom-in', class: 'mr-0') %>
|
|
19
|
+
</span>
|
|
20
|
+
</a>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="flex gap-2 mb-4">
|
|
23
|
+
<%= link_to original_url, target: '_blank', rel: 'noopener', class: 'btn btn-light btn-sm' do %>
|
|
24
|
+
<%= icon('external-link', class: 'mr-1') %><%= Spree.t(:view_full_size) %>
|
|
25
|
+
<% end %>
|
|
26
|
+
<%= link_to download_url, target: '_blank', rel: 'noopener', class: 'btn btn-light btn-sm' do %>
|
|
27
|
+
<%= icon('download', class: 'mr-1') %><%= Spree.t(:download) %>
|
|
28
|
+
<% end %>
|
|
29
|
+
</div>
|
|
30
|
+
<% else %>
|
|
31
|
+
<%= f.spree_file_field :attachment, width: 200, height: 200, can_delete: false, label: Spree.t(:image) %>
|
|
32
|
+
<% end %>
|
|
33
|
+
|
|
34
|
+
<%= f.spree_text_area :alt, label: Spree.t(:alt_text), rows: 3 %>
|
|
35
|
+
|
|
36
|
+
<%# Variant assignment shows only for product-level assets — variant-pinned assets
|
|
37
|
+
already live on a single variant via the polymorphic viewable, so the
|
|
38
|
+
join-table UI doesn't apply. Empty submission (no boxes ticked) clears
|
|
39
|
+
all links thanks to the hidden field below. %>
|
|
40
|
+
<% if @asset.viewable_type == 'Spree::Product' && @asset.viewable.present? && @asset.viewable.variants.any? %>
|
|
41
|
+
<% product = @asset.viewable %>
|
|
42
|
+
<% linked_ids = @asset.variant_media.pluck(:variant_id).to_set %>
|
|
43
|
+
<div class="mt-4 pt-4 border-t">
|
|
44
|
+
<label class="form-label"><%= Spree.t(:assigned_variants) %></label>
|
|
45
|
+
<p class="text-muted small mb-2">
|
|
46
|
+
<%= Spree.t(:assigned_variants_help) %>
|
|
47
|
+
</p>
|
|
48
|
+
<%= hidden_field_tag 'asset[variant_ids][]', '' %>
|
|
49
|
+
<div class="flex flex-col gap-1">
|
|
50
|
+
<% product.variants.each do |variant| %>
|
|
51
|
+
<% chip_id = "asset_variant_#{variant.id}" %>
|
|
52
|
+
<div class="custom-control form-checkbox rounded-lg hover:bg-gray-100 p-2 transition-colors duration-100 ease-linear">
|
|
53
|
+
<%= check_box_tag('asset[variant_ids][]', variant.to_param, linked_ids.include?(variant.id), id: chip_id, class: 'custom-control-input') %>
|
|
54
|
+
<%= label_tag chip_id, class: 'custom-control-label' do %>
|
|
55
|
+
<%= variant.descriptive_name.presence || variant.sku %>
|
|
56
|
+
<% end %>
|
|
57
|
+
</div>
|
|
58
|
+
<% end %>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
<% end %>
|
|
6
62
|
</div>
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
</div>
|
|
10
|
-
<div class="dialog-footer">
|
|
63
|
+
|
|
64
|
+
<div class="drawer-footer gap-3">
|
|
11
65
|
<%= turbo_save_button_tag %>
|
|
66
|
+
<%= drawer_discard_button %>
|
|
12
67
|
<%= link_to Spree.t('actions.destroy'), object_url(@asset),
|
|
13
68
|
data: { turbo_method: :delete, turbo_confirm: Spree.t(:are_you_sure_delete), turbo_frame: '_top' },
|
|
14
69
|
class: 'btn btn-danger ml-auto' if can?(:destroy, @asset) %>
|
|
15
70
|
</div>
|
|
16
71
|
<% end %>
|
|
17
|
-
<% end %>
|
|
72
|
+
<% end %>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<div class="card mb-6">
|
|
2
|
+
<div class="card-body" data-controller="slug-form">
|
|
3
|
+
<%= f.spree_text_field :name,
|
|
4
|
+
autofocus: f.object.new_record?,
|
|
5
|
+
required: true,
|
|
6
|
+
data: { slug_form_target: :name, action: 'slug-form#updateUrlFromName' } %>
|
|
7
|
+
<%= f.spree_text_field :code,
|
|
8
|
+
required: true,
|
|
9
|
+
hint: Spree.t('admin.channels.code_hint'),
|
|
10
|
+
data: { slug_form_target: :url } %>
|
|
11
|
+
<%= f.spree_check_box :active, label: Spree.t(:active) %>
|
|
12
|
+
<%= f.spree_check_box :default, label: Spree.t('admin.channels.default'), hint: Spree.t('admin.channels.default_hint') %>
|
|
13
|
+
<%= f.spree_select :preferred_order_routing_strategy,
|
|
14
|
+
options_for_select(channel_order_routing_strategy_options, f.object.preferred_order_routing_strategy),
|
|
15
|
+
{ label: Spree.t('admin.channels.order_routing_strategy'),
|
|
16
|
+
help_bubble: Spree.t('admin.channels.order_routing_strategy_hint'),
|
|
17
|
+
include_blank: Spree.t('admin.channels.order_routing_strategy_inherit') } %>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= render 'spree/admin/shared/edit_resource' %>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<% content_for :page_title do %>
|
|
2
|
+
<%= Spree.t(:channels) %>
|
|
3
|
+
<% end %>
|
|
4
|
+
|
|
5
|
+
<% content_for :page_actions do %>
|
|
6
|
+
<%= render_admin_partials(:channels_actions_partials) %>
|
|
7
|
+
<%= link_to_with_icon 'plus', Spree.t(:new_channel), new_object_url, class: "btn btn-primary" if can? :create, Spree::Channel %>
|
|
8
|
+
<% end %>
|
|
9
|
+
|
|
10
|
+
<%= render_admin_partials(:channels_header_partials) %>
|
|
11
|
+
|
|
12
|
+
<%= render_table @collection, :channels %>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= render 'spree/admin/shared/new_resource' %>
|