spree_admin 5.6.0.rc3 → 5.6.0.rc4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c16da621df0bbae6fd763c5182dfdbbf5523a64957abfe850630a2c3d074971f
4
- data.tar.gz: '076780665f46d75d1d342c7d478c1fdf6ecf230e30ee60912bf18cf603a27334'
3
+ metadata.gz: 8d4b0b49721fed24adb001b68fa29aff946374186d3de5a2d3eac7500d13dc1d
4
+ data.tar.gz: c44a54d9688235ac082e358b267446c565fdab88d91a0f23519df95bd0a95c4c
5
5
  SHA512:
6
- metadata.gz: ed88f940e388b9c6aae42ddf7b433a42dc692f6672b58ea29ece15e9481989c3613c1395e0ad4480d83bccd610328d3660ae2e3eddf261306939679c57b20638
7
- data.tar.gz: 8aec0d68de620b06ec498600eed1c09b1a02e20af0614fad427429db8f97f8ee92f13fbee16531d3638b6258425709b4130766d0923a288ba1b5146bd972bfb1
6
+ metadata.gz: b021c5154b33e6600b2ae72c8cf92d505dcda0486a012fe3959b5509c7cb5285e3966fc7690b7c9ed45d010a4d2a1ef5602249f5e62f012fbcbc2c03a8f943fe
7
+ data.tar.gz: 2372e776ff93374313cde1d2749e6bf9d05947e640a1e4d1a2f1a66240bc109e9ffb5838d3df3dc82c5d80a6a443a05f6657ef82186a68d5b5879b46b491ad91
@@ -9,6 +9,10 @@ module Spree
9
9
  end
10
10
  end
11
11
 
12
+ def api_key_channel_options
13
+ current_store.channels.active.pluck(:name, :id)
14
+ end
15
+
12
16
  def api_key_status_badge(api_key)
13
17
  if api_key.active?
14
18
  content_tag(:span, icon('check') + Spree.t('admin.api_keys.statuses.active'), class: 'badge badge-success')
@@ -47,6 +47,7 @@ import AutocompleteSelectController from 'spree/admin/controllers/autocomplete_s
47
47
  import AutoScrollController from 'spree/admin/controllers/auto_scroll_controller'
48
48
  import BetterSliderController from 'spree/admin/controllers/better_slider_controller'
49
49
  import BlockFormController from 'spree/admin/controllers/block_form_controller'
50
+ import ApiKeyFormController from 'spree/admin/controllers/api_key_form_controller'
50
51
  import BulkOperationController from 'spree/admin/controllers/bulk_operation_controller'
51
52
  import CalculatorFieldsController from 'spree/admin/controllers/calculator_fields_controller'
52
53
  import CalendarRangeController from 'spree/admin/controllers/calendar_range_controller'
@@ -103,6 +104,7 @@ import EnableButtonController from 'spree/core/controllers/enable_button_control
103
104
 
104
105
  application.register('active-storage-upload', ActiveStorageUpload)
105
106
  application.register('address-autocomplete', AddressAutocompleteController)
107
+ application.register('api-key-form', ApiKeyFormController)
106
108
  application.register('address-form', AddressFormController)
107
109
  application.register('admin', AdminController)
108
110
  application.register('asset-uploader', AssetUploaderController)
@@ -0,0 +1,24 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+
3
+ // Progressive disclosure for the API key form: scopes apply only to secret
4
+ // keys, channel binding only to publishable keys. Hidden sections get their
5
+ // inputs disabled so the irrelevant params are never submitted.
6
+ export default class extends Controller {
7
+ static targets = ['typeSelect', 'section']
8
+
9
+ connect() {
10
+ this.toggle()
11
+ }
12
+
13
+ toggle() {
14
+ const type = this.typeSelectTarget.value
15
+
16
+ this.sectionTargets.forEach((section) => {
17
+ const match = section.dataset.showWhenKeyType === type
18
+ section.hidden = !match
19
+ section.querySelectorAll('select, input').forEach((input) => {
20
+ input.disabled = !match
21
+ })
22
+ })
23
+ }
24
+ }
@@ -0,0 +1,265 @@
1
+ module Spree
2
+ module Products
3
+ # Prepares nested attributes for product updates, handling multi-store scenarios
4
+ # and permissions.
5
+ #
6
+ # This service ensures that when editing a product in one store, taxon associations
7
+ # from other stores are preserved. This prevents accidental data loss when a store
8
+ # admin updates product categories in their store without affecting other stores.
9
+ #
10
+ # Variant removal is opt-in: only variants explicitly listed in the
11
+ # `removed_variant_ids` param are marked for destruction (or collected for
12
+ # discontinuation when they have completed orders). Variants merely absent from
13
+ # `variants_attributes` are left untouched, so a partially rendered or broken
14
+ # form can never silently mass-delete variants.
15
+ #
16
+ # @example
17
+ # service = Spree::Products::PrepareNestedAttributes.new(
18
+ # product,
19
+ # current_store,
20
+ # params,
21
+ # current_ability
22
+ # )
23
+ # prepared_params = service.call
24
+ #
25
+ class PrepareNestedAttributes
26
+ attr_reader :variants_to_discontinue
27
+
28
+ def initialize(product, store, params, ability)
29
+ @product = product
30
+ @store = store
31
+ @params = params
32
+ @ability = ability
33
+ @variants_to_discontinue = []
34
+ end
35
+
36
+ def call
37
+ extract_removed_variant_ids
38
+
39
+ if params[:variants_attributes]
40
+ params[:variants_attributes].each do |key, variant_params|
41
+ existing_variant = variant_params[:id].presence && @product.variants.find_by(id: variant_params[:id])
42
+ # a re-submitted variant always wins over a removal request
43
+ variants_to_remove.delete(variant_params[:id].to_s) if variant_params[:id].present?
44
+
45
+ variant_params.delete(:price) # remove legacy price param
46
+
47
+ if can_update_prices?
48
+ backfill_price_ids!(variant_params, existing_variant)
49
+
50
+ variant_params[:prices_attributes]&.each do |price_key, price_params|
51
+ variant_params[:prices_attributes][price_key]['_destroy'] = '1' if price_params[:amount].blank?
52
+ end
53
+ else
54
+ variant_params.delete(:prices_attributes)
55
+ end
56
+
57
+ variant_params[:option_value_variants_attributes] = update_option_value_variants(variant_params.delete(:options), existing_variant)
58
+
59
+ variant_params.delete(:stock_items_attributes) unless can_update_stock_items?
60
+
61
+ params[:variants_attributes].delete(key) if variant_params.blank?
62
+ end
63
+ params[:variants_attributes] = params[:variants_attributes].merge(removed_variants_attributes)
64
+
65
+ params[:product_option_types_attributes] = product_option_types_params.merge(removed_product_option_types_attributes)
66
+ elsif params[:master_attributes]
67
+ params[:master_attributes].delete(:stock_items_attributes) unless can_update_stock_items?
68
+
69
+ if can_update_prices?
70
+ # If the master price is nil then mark it for destruction
71
+ params.dig(:master_attributes, :prices_attributes)&.each do |price_key, price_params|
72
+ params[:master_attributes][:prices_attributes][price_key]['_destroy'] = '1' if price_params[:amount].blank?
73
+ end
74
+ else
75
+ params[:master_attributes].delete(:prices_attributes)
76
+ end
77
+ end
78
+
79
+ params.delete(:legacy_product_publications_attributes) unless can?(:manage, Spree::ProductPublication)
80
+
81
+ # ensure the product is owned by a store
82
+ params[:store_id] = store.id if params[:store_id].blank? && product.store_id.blank?
83
+
84
+ # The variants matrix was emptied: no variant rows re-submitted, removals sent instead.
85
+ # Option types are detached only when the removal list covers every variant, so a
86
+ # partial (possibly broken) submission never turns the product into a simple one.
87
+ # Variants kept alive by discontinuation still count as removed from the matrix,
88
+ # so the detachment can't hinge on any of them yielding a `_destroy` row.
89
+ if params[:variants_attributes].blank? && variants_to_remove.any? && can_remove_variants?
90
+ attributes = removed_variants_attributes
91
+
92
+ params[:option_type_ids] = [] if removing_all_variants? && !params.key?(:option_type_ids)
93
+
94
+ if attributes.any?
95
+ params[:variants_attributes] = attributes
96
+ params[:variants_attributes].permit!
97
+ end
98
+ end
99
+
100
+ params
101
+ end
102
+
103
+ private
104
+
105
+ attr_reader :product, :store, :params, :ability
106
+
107
+ delegate :can?, :cannot?, to: :ability
108
+
109
+ # Backfill IDs for prices_attributes entries that reference existing prices
110
+ # so that ActiveRecord updates them instead of inserting duplicates
111
+ def backfill_price_ids!(variant_params, existing_variant)
112
+ return unless existing_variant && variant_params[:prices_attributes]
113
+
114
+ variant_params[:prices_attributes].each do |_key, price_params|
115
+ next if price_params[:id].present?
116
+ next if price_params[:currency].blank?
117
+
118
+ existing_price = existing_variant.prices.base_prices.find_by(currency: price_params[:currency])
119
+ price_params[:id] = existing_price.id if existing_price
120
+ end
121
+ end
122
+
123
+ def product_option_types_params
124
+ @product_option_types_params ||= {}
125
+ end
126
+
127
+ def product_option_types_to_remove
128
+ @product_option_types_to_remove ||= product.product_option_type_ids
129
+ end
130
+
131
+ # Pulls `removed_variant_ids` out of the params so it never reaches Product#update.
132
+ # Must run before any `variants_to_remove` access.
133
+ def extract_removed_variant_ids
134
+ @removed_variant_ids = Array(params.delete(:removed_variant_ids)).map(&:to_s)
135
+ end
136
+
137
+ # Only variants the client explicitly asked to remove, and only ones that
138
+ # actually belong to this product.
139
+ def variants_to_remove
140
+ @variants_to_remove ||= (@removed_variant_ids || []).uniq & all_variant_ids
141
+ end
142
+
143
+ def all_variant_ids
144
+ @all_variant_ids ||= product.variant_ids.map(&:to_s)
145
+ end
146
+
147
+ def removing_all_variants?
148
+ (all_variant_ids - variants_to_remove).empty?
149
+ end
150
+
151
+ def can_update_prices?
152
+ @can_update_prices ||= product.new_record? || can?(:manage, Spree::Price.new(variant_id: product.default_variant.id))
153
+ end
154
+
155
+ def can_manage_option_types?
156
+ @can_manage_option_types ||= product.new_record? || can?(:manage_option_types, product)
157
+ end
158
+
159
+ def can_update_stock_items?
160
+ @can_update_stock_items ||= product.new_record? || can?(:manage, Spree::StockItem.new(variant_id: product.default_variant.id))
161
+ end
162
+
163
+ def can_remove_variants?
164
+ @can_remove_variants ||= product.persisted? && can?(:destroy, product.default_variant)
165
+ end
166
+
167
+ def removed_variants_attributes
168
+ return {} unless can_remove_variants?
169
+
170
+ populate_variants_to_discontinue
171
+
172
+ attributes = {}
173
+ last_index = params[:variants_attributes].presence&.keys&.map(&:to_i)&.max || -1
174
+ variant_ids_to_destroy.each_with_index do |variant_id, index|
175
+ attributes[(last_index + 1 + index).to_s] = { id: variant_id, _destroy: '1' }
176
+ end
177
+
178
+ attributes
179
+ end
180
+
181
+ def populate_variants_to_discontinue
182
+ ids = variants_to_remove.select { |vid| variant_ids_with_completed_orders.include?(vid) }
183
+ @variants_to_discontinue = product.variants.where(id: ids).to_a if ids.any?
184
+ end
185
+
186
+ def variant_ids_to_destroy
187
+ variants_to_remove - variant_ids_with_completed_orders
188
+ end
189
+
190
+ def variant_ids_with_completed_orders
191
+ @variant_ids_with_completed_orders ||=
192
+ product.variants
193
+ .joins(:orders)
194
+ .merge(Spree::Order.complete)
195
+ .reorder(nil)
196
+ .distinct
197
+ .pluck(:id)
198
+ .map(&:to_s)
199
+ end
200
+
201
+ def removed_product_option_types_attributes
202
+ return {} unless can_manage_option_types?
203
+
204
+ attributes = {}
205
+ last_index = product_option_types_params.keys.map(&:to_i).max
206
+ product_option_types_to_remove.each_with_index do |product_option_type_id, index|
207
+ attributes[(last_index + index + 1).to_s] = { id: product_option_type_id, _destroy: '1' }
208
+ end
209
+
210
+ attributes
211
+ end
212
+
213
+ def update_option_value_variants(option_value_params, existing_variant)
214
+ return {} unless option_value_params.present?
215
+ return {} unless can_manage_option_types?
216
+
217
+ option_value_variant_params = {}
218
+
219
+ option_value_params.each_with_index do |opt, index|
220
+ option_type = Spree::OptionType.find_by_param(opt[:id]) if opt.fetch(:id)
221
+ option_type ||= Spree::OptionType.where(name: opt[:name].parameterize).first_or_initialize do |o|
222
+ o.name = o.presentation = opt[:name]
223
+ o.position = opt[:position]
224
+ o.save!
225
+ end
226
+
227
+ option_value_identificator = if opt[:option_value_name].present?
228
+ opt[:option_value_name]
229
+ else
230
+ opt[:option_value_presentation]
231
+ end.parameterize.strip
232
+
233
+ option_value = option_type.option_values.where(name: option_value_identificator).first_or_initialize do |o|
234
+ o.presentation = opt[:option_value_presentation]
235
+ o.save!
236
+ end
237
+
238
+ existing_option_value_variant = existing_variant&.option_value_variants&.find { |ovv| ovv.option_value_id == option_value.id }
239
+
240
+ option_value_variant_params[index.to_s] = { id: existing_option_value_variant&.id, option_value_id: option_value.id }.compact_blank
241
+
242
+ next if product_option_types_params.find { |_i, v| v[:option_type_id] == option_type.id }
243
+
244
+ existing_product_option_type = @product.product_option_types.find { |pot| pot.option_type_id == option_type.id }
245
+
246
+ if existing_product_option_type
247
+ product_option_types_to_remove.delete(existing_product_option_type.id)
248
+ product_option_types_params[opt[:position]] = {
249
+ id: existing_product_option_type.id,
250
+ position: opt[:position],
251
+ option_type_id: option_type.id
252
+ }
253
+ else
254
+ product_option_types_params[opt[:position]] = {
255
+ option_type_id: option_type.id,
256
+ position: opt[:position]
257
+ }
258
+ end
259
+ end
260
+
261
+ option_value_variant_params
262
+ end
263
+ end
264
+ end
265
+ end
@@ -14,6 +14,13 @@
14
14
  <dd class="mt-1"><%= api_key_type_badge(@api_key) %></dd>
15
15
  </div>
16
16
 
17
+ <% if @api_key.channel.present? %>
18
+ <div>
19
+ <dt class="text-sm text-gray-500"><%= Spree.t('admin.api_keys.channel') %></dt>
20
+ <dd class="mt-1"><span class="badge badge-light"><%= @api_key.channel.name %></span></dd>
21
+ </div>
22
+ <% end %>
23
+
17
24
  <div>
18
25
  <dt class="text-sm text-gray-500"><%= Spree.t(:created_at) %></dt>
19
26
  <dd class="mt-1"><%= l(@api_key.created_at, format: :long) %></dd>
@@ -1,60 +1,90 @@
1
- <div class="card mb-6">
2
- <div class="card-body">
3
- <%= f.spree_text_field :name, required: true, autofocus: f.object.new_record?,
4
- placeholder: Spree.t('admin.api_keys.name_placeholder') %>
1
+ <div data-controller="api-key-form">
2
+ <div class="card mb-6">
3
+ <div class="card-body">
4
+ <%= f.spree_text_field :name, required: true, autofocus: f.object.new_record?,
5
+ placeholder: Spree.t('admin.api_keys.name_placeholder') %>
5
6
 
6
- <% if f.object.new_record? %>
7
- <%= f.spree_select :key_type, api_key_type_options,
8
- { prompt: Spree.t(:select_key_type), label: Spree.t(:type) },
9
- { required: true } %>
10
- <p class="text-sm text-gray-500 mt-2">
11
- <strong><%= Spree.t('admin.api_keys.key_types.publishable') %>:</strong>
12
- <%= Spree.t('admin.api_keys.key_type_descriptions.publishable') %>
13
- <br>
14
- <strong><%= Spree.t('admin.api_keys.key_types.secret') %>:</strong>
15
- <%= Spree.t('admin.api_keys.key_type_descriptions.secret') %>
16
- </p>
17
- <% else %>
18
- <div class="form-group">
19
- <label class="form-label"><%= Spree.t(:type) %></label>
20
- <div class="form-control-plaintext">
21
- <%= api_key_type_badge(@api_key) %>
7
+ <% if f.object.new_record? %>
8
+ <%= f.spree_select :key_type, api_key_type_options,
9
+ { prompt: Spree.t(:select_key_type), label: Spree.t(:type) },
10
+ { required: true,
11
+ data: { api_key_form_target: 'typeSelect', action: 'change->api-key-form#toggle' } } %>
12
+ <p class="text-sm text-gray-500 mt-2">
13
+ <strong><%= Spree.t('admin.api_keys.key_types.publishable') %>:</strong>
14
+ <%= Spree.t('admin.api_keys.key_type_descriptions.publishable') %>
15
+ <br>
16
+ <strong><%= Spree.t('admin.api_keys.key_types.secret') %>:</strong>
17
+ <%= Spree.t('admin.api_keys.key_type_descriptions.secret') %>
18
+ </p>
19
+ <% else %>
20
+ <div class="form-group">
21
+ <label class="form-label"><%= Spree.t(:type) %></label>
22
+ <div class="form-control-plaintext">
23
+ <%= api_key_type_badge(@api_key) %>
24
+ </div>
22
25
  </div>
23
- </div>
24
- <% end %>
26
+ <% end %>
27
+ </div>
25
28
  </div>
26
- </div>
27
29
 
28
- <% if f.object.new_record? %>
29
- <div class="card mb-6" id="api-key-scopes" data-secret-only data-show-when-key-type="secret">
30
- <div class="card-header">
31
- <h5 class="card-title"><%= Spree.t('admin.api_keys.scopes', default: 'Scopes') %></h5>
32
- <p class="text-sm text-gray-500 mt-1">
33
- <%= Spree.t('admin.api_keys.scopes_description', default: 'Required for secret keys. Pick the narrowest set of scopes your integration needs.') %>
34
- </p>
30
+ <% if f.object.new_record? %>
31
+ <div class="card mb-6" id="api-key-scopes" hidden
32
+ data-api-key-form-target="section" data-show-when-key-type="secret">
33
+ <div class="card-header">
34
+ <h5 class="card-title"><%= Spree.t('admin.api_keys.scopes') %></h5>
35
+ </div>
36
+ <div class="card-body">
37
+ <p class="text-sm text-gray-500 mb-4">
38
+ <%= Spree.t('admin.api_keys.scopes_description') %>
39
+ </p>
40
+ <% Spree::ApiKey::SCOPES.each do |scope| %>
41
+ <div class="custom-control form-checkbox mb-1">
42
+ <%= f.check_box :scopes, { multiple: true, class: 'custom-control-input', id: "api_key_scopes_#{scope}" }, scope, nil %>
43
+ <%= f.label "scopes_#{scope}", scope, class: 'custom-control-label font-mono text-sm' %>
44
+ </div>
45
+ <% end %>
46
+ <%= f.hidden_field :scopes, value: '', multiple: true %>
47
+ </div>
35
48
  </div>
36
- <div class="card-body">
37
- <% Spree::ApiKey::SCOPES.each do |scope| %>
38
- <div class="custom-control form-checkbox mb-1">
39
- <%= f.check_box :scopes, { multiple: true, class: 'custom-control-input', id: "api_key_scopes_#{scope}" }, scope, nil %>
40
- <%= f.label "scopes_#{scope}", scope, class: 'custom-control-label font-mono text-sm' %>
41
- </div>
42
- <% end %>
43
- <%= f.hidden_field :scopes, value: '', multiple: true %>
49
+
50
+ <div class="card mb-6" id="api-key-channel" hidden
51
+ data-api-key-form-target="section" data-show-when-key-type="publishable">
52
+ <div class="card-header">
53
+ <h5 class="card-title"><%= Spree.t('admin.api_keys.channel') %></h5>
54
+ </div>
55
+ <div class="card-body">
56
+ <p class="text-sm text-gray-500 mb-4">
57
+ <%= Spree.t('admin.api_keys.channel_description') %>
58
+ </p>
59
+ <%= f.spree_select :channel_id, api_key_channel_options,
60
+ { include_blank: Spree.t('admin.api_keys.all_channels'), label: false } %>
61
+ </div>
44
62
  </div>
45
- </div>
46
- <% elsif f.object.secret? %>
47
- <div class="card mb-6">
48
- <div class="card-header">
49
- <h5 class="card-title"><%= Spree.t('admin.api_keys.scopes', default: 'Scopes') %></h5>
50
- <p class="text-sm text-gray-500 mt-1">
51
- <%= Spree.t('admin.api_keys.scopes_immutable', default: 'Scopes are fixed when the key is created. To change them, create a new key with the scopes you need and revoke this one.') %>
52
- </p>
63
+ <% elsif f.object.secret? %>
64
+ <div class="card mb-6">
65
+ <div class="card-header">
66
+ <h5 class="card-title"><%= Spree.t('admin.api_keys.scopes') %></h5>
67
+ </div>
68
+ <div class="card-body">
69
+ <p class="text-sm text-gray-500 mb-4">
70
+ <%= Spree.t('admin.api_keys.scopes_immutable') %>
71
+ </p>
72
+ <% f.object.scopes.each do |scope| %>
73
+ <span class="badge badge-light font-mono text-sm me-1 mb-1"><%= scope %></span>
74
+ <% end %>
75
+ </div>
53
76
  </div>
54
- <div class="card-body">
55
- <% f.object.scopes.each do |scope| %>
56
- <span class="badge badge-light font-mono text-sm me-1 mb-1"><%= scope %></span>
57
- <% end %>
77
+ <% elsif f.object.channel.present? %>
78
+ <div class="card mb-6">
79
+ <div class="card-header">
80
+ <h5 class="card-title"><%= Spree.t('admin.api_keys.channel') %></h5>
81
+ </div>
82
+ <div class="card-body">
83
+ <p class="text-sm text-gray-500 mb-4">
84
+ <%= Spree.t('admin.api_keys.channel_immutable') %>
85
+ </p>
86
+ <span class="badge badge-light"><%= f.object.channel.name %></span>
87
+ </div>
58
88
  </div>
59
- </div>
60
- <% end %>
89
+ <% end %>
90
+ </div>
@@ -46,6 +46,10 @@ en:
46
46
  origin_settings: Origin Settings
47
47
  amount_spent: Amount spent
48
48
  api_keys:
49
+ all_channels: All channels (store-wide)
50
+ channel: Channel
51
+ channel_description: Optional. Bind this key to a single channel — a bound key always resolves that channel and rejects requests naming any other. Leave on "All channels" for a store-wide key.
52
+ channel_immutable: The channel binding is fixed when the key is created. To change it, create a new key bound to the channel you need and revoke this one.
49
53
  example_request: Example Request
50
54
  key_revoked_message: This API key has been revoked and can no longer be used.
51
55
  key_type_descriptions:
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spree_admin
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.6.0.rc3
4
+ version: 5.6.0.rc4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vendo Connect Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-07-19 00:00:00.000000000 Z
11
+ date: 2026-07-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: spree
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 5.6.0.rc3
19
+ version: 5.6.0.rc4
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 5.6.0.rc3
26
+ version: 5.6.0.rc4
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: active_link_to
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -438,6 +438,7 @@ files:
438
438
  - app/javascript/spree/admin/controllers/active_storage_upload_controller.js
439
439
  - app/javascript/spree/admin/controllers/address_form_controller.js
440
440
  - app/javascript/spree/admin/controllers/admin_controller.js
441
+ - app/javascript/spree/admin/controllers/api_key_form_controller.js
441
442
  - app/javascript/spree/admin/controllers/asset_uploader_controller.js
442
443
  - app/javascript/spree/admin/controllers/auto_scroll_controller.js
443
444
  - app/javascript/spree/admin/controllers/autocomplete_select_controller.js
@@ -514,6 +515,7 @@ files:
514
515
  - app/models/spree/admin/table/visibility.rb
515
516
  - app/models/spree/admin/updater.rb
516
517
  - app/presenters/spree/admin/order_summary_presenter.rb
518
+ - app/services/spree/products/prepare_nested_attributes.rb
517
519
  - app/subscribers/spree/admin/import_row_subscriber.rb
518
520
  - app/subscribers/spree/admin/import_subscriber.rb
519
521
  - app/views/action_text/video_embeds/_thumbnail.html.erb
@@ -1185,9 +1187,9 @@ licenses:
1185
1187
  - BSD-3-Clause
1186
1188
  metadata:
1187
1189
  bug_tracker_uri: https://github.com/spree/spree/issues
1188
- changelog_uri: https://github.com/spree/spree/releases/tag/v5.6.0.rc3
1190
+ changelog_uri: https://github.com/spree/spree/releases/tag/v5.6.0.rc4
1189
1191
  documentation_uri: https://docs.spreecommerce.org/
1190
- source_code_uri: https://github.com/spree/spree/tree/v5.6.0.rc3
1192
+ source_code_uri: https://github.com/spree/spree/tree/v5.6.0.rc4
1191
1193
  post_install_message:
1192
1194
  rdoc_options: []
1193
1195
  require_paths: