Skip to content

Commit f8b3385

Browse files
authored
Merge pull request #2673 from magento-earl-grey/MAGETWO-91570
[earl] MAGETWO-90803 & MAGETWO-91570
2 parents 919dd8c + 1a22d95 commit f8b3385

File tree

8 files changed

+448
-6
lines changed

8 files changed

+448
-6
lines changed

app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ class Save extends Attribute
6969
* @var LayoutFactory
7070
*/
7171
private $layoutFactory;
72+
7273
/**
7374
* @var Presentation
7475
*/
@@ -124,6 +125,7 @@ public function execute()
124125
{
125126
$data = $this->getRequest()->getPostValue();
126127
if ($data) {
128+
$this->preprocessOptionsData($data);
127129
$setId = $this->getRequest()->getParam('set');
128130

129131
$attributeSet = null;
@@ -313,6 +315,28 @@ public function execute()
313315
return $this->returnResult('catalog/*/', [], ['error' => true]);
314316
}
315317

318+
/**
319+
* Extract options data from serialized options field and append to data array.
320+
*
321+
* This logic is required to overcome max_input_vars php limit
322+
* that may vary and/or be inaccessible to change on different instances.
323+
*
324+
* @param array $data
325+
* @return void
326+
*/
327+
private function preprocessOptionsData(&$data)
328+
{
329+
if (isset($data['serialized_options'])) {
330+
$serializedOptions = json_decode($data['serialized_options'], JSON_OBJECT_AS_ARRAY);
331+
foreach ($serializedOptions as $serializedOption) {
332+
$option = [];
333+
parse_str($serializedOption, $option);
334+
$data = array_replace_recursive($data, $option);
335+
}
336+
}
337+
unset($data['serialized_options']);
338+
}
339+
316340
/**
317341
* @param string $path
318342
* @param array $params

app/code/Magento/Catalog/view/adminhtml/web/js/options.js

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,16 @@ define([
1313
'jquery/ui',
1414
'prototype',
1515
'form',
16-
'validation'
16+
'validation',
17+
'mage/translate'
1718
], function (jQuery, mageTemplate, rg) {
1819
'use strict';
1920

2021
return function (config) {
21-
var attributeOption = {
22+
var optionPanel = jQuery('#manage-options-panel'),
23+
optionsValues = [],
24+
editForm = jQuery('#edit_form'),
25+
attributeOption = {
2226
table: $('attribute-options-table'),
2327
itemCount: 0,
2428
totalItems: 0,
@@ -150,7 +154,7 @@ define([
150154
attributeOption.remove(event);
151155
});
152156

153-
jQuery('#manage-options-panel').on('render', function () {
157+
optionPanel.on('render', function () {
154158
attributeOption.ignoreValidate();
155159

156160
if (attributeOption.rendered) {
@@ -176,7 +180,31 @@ define([
176180
});
177181
});
178182
}
183+
editForm.on('submit', function () {
184+
optionPanel.find('input')
185+
.each(function () {
186+
if (this.disabled) {
187+
return;
188+
}
179189

190+
if (this.type === 'checkbox' || this.type === 'radio') {
191+
if (this.checked) {
192+
optionsValues.push(this.name + '=' + jQuery(this).val());
193+
}
194+
} else {
195+
optionsValues.push(this.name + '=' + jQuery(this).val());
196+
}
197+
});
198+
jQuery('<input>')
199+
.attr({
200+
type: 'hidden',
201+
name: 'serialized_options'
202+
})
203+
.val(JSON.stringify(optionsValues))
204+
.prependTo(editForm);
205+
optionPanel.find('table')
206+
.replaceWith(jQuery('<div>').text(jQuery.mage.__('Sending attribute values as package.')));
207+
});
180208
window.attributeOption = attributeOption;
181209
window.optionDefaultInputType = attributeOption.getOptionInputType();
182210

app/code/Magento/Swatches/Controller/Adminhtml/Product/Attribute/Plugin/Save.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
use Magento\Swatches\Model\Swatch;
1212

1313
/**
14-
* Class Save
14+
* Plugin for product attribute save controller.
1515
*/
1616
class Save
1717
{
@@ -24,7 +24,17 @@ class Save
2424
public function beforeDispatch(Attribute\Save $subject, RequestInterface $request)
2525
{
2626
$data = $request->getPostValue();
27+
2728
if (isset($data['frontend_input'])) {
29+
//Data is serialized to overcome issues caused by max_input_vars value if it's modification is unavailable.
30+
//See subject controller code and comments for more info.
31+
if (isset($data['serialized_swatch_values'])
32+
&& in_array($data['frontend_input'], ['swatch_visual', 'swatch_text'])
33+
) {
34+
$data['serialized_options'] = $data['serialized_swatch_values'];
35+
unset($data['serialized_swatch_values']);
36+
}
37+
2838
switch ($data['frontend_input']) {
2939
case 'swatch_visual':
3040
$data[Swatch::SWATCH_INPUT_TYPE_KEY] = Swatch::SWATCH_INPUT_TYPE_VISUAL;

app/code/Magento/Swatches/view/adminhtml/web/js/product-attributes.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,8 @@ define([
414414
};
415415

416416
$(function () {
417+
var editForm = $('#edit_form');
418+
417419
$('#frontend_input').bind('change', function () {
418420
swatchProductAttributes.bindAttributeInputType();
419421
});
@@ -427,6 +429,34 @@ define([
427429
$('.attribute-popup .collapse, [data-role="advanced_fieldset-content"]')
428430
.collapsable()
429431
.collapse('hide');
432+
433+
editForm.on('submit', function () {
434+
var activePanel,
435+
swatchValues = [],
436+
swatchVisualPanel = $('#swatch-visual-options-panel'),
437+
swatchTextPanel = $('#swatch-text-options-panel');
438+
439+
activePanel = swatchTextPanel.is(':visible') ? swatchTextPanel : swatchVisualPanel;
440+
441+
activePanel
442+
.find('table input')
443+
.each(function () {
444+
swatchValues.push(this.name + '=' + $(this).val());
445+
});
446+
447+
$('<input>')
448+
.attr({
449+
type: 'hidden',
450+
name: 'serialized_swatch_values'
451+
})
452+
.val(JSON.stringify(swatchValues))
453+
.prependTo(editForm);
454+
455+
[swatchVisualPanel, swatchTextPanel].forEach(function (el) {
456+
$(el).find('table')
457+
.replaceWith($('<div>').text($.mage.__('Sending swatch values as package.')));
458+
});
459+
});
430460
});
431461

432462
window.saveAttributeInNewSet = swatchProductAttributes.saveAttributeInNewSet;

dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
*/
66
namespace Magento\Catalog\Controller\Adminhtml\Product;
77

8+
use Magento\Framework\Exception\LocalizedException;
9+
810
/**
911
* @magentoAppArea adminhtml
1012
* @magentoDbIsolation enabled
@@ -224,6 +226,109 @@ public function testSaveActionCleanAttributeLabelCache()
224226
$this->assertEquals('new string translation', $this->_translate('string to translate'));
225227
}
226228

229+
/**
230+
* Get attribute data preset.
231+
*
232+
* @return array
233+
*/
234+
private function getLargeOptionsSetAttributeData()
235+
{
236+
return [
237+
'frontend_label' => [
238+
0 => 'testdrop1',
239+
1 => '',
240+
2 => '',
241+
],
242+
'frontend_input' => 'select',
243+
'is_required' => '0',
244+
'update_product_preview_image' => '0',
245+
'use_product_image_for_swatch' => '0',
246+
'visual_swatch_validation' => '',
247+
'visual_swatch_validation_unique' => '',
248+
'text_swatch_validation' => '',
249+
'text_swatch_validation_unique' => '',
250+
'attribute_code' => 'test_many_options',
251+
'is_global' => '0',
252+
'default_value_text' => '',
253+
'default_value_yesno' => '0',
254+
'default_value_date' => '',
255+
'default_value_textarea' => '',
256+
'is_unique' => '0',
257+
'is_used_in_grid' => '1',
258+
'is_visible_in_grid' => '1',
259+
'is_filterable_in_grid' => '1',
260+
'is_searchable' => '0',
261+
'is_comparable' => '0',
262+
'is_filterable' => '0',
263+
'is_filterable_in_search' => '0',
264+
'is_used_for_promo_rules' => '0',
265+
'is_html_allowed_on_front' => '1',
266+
'is_visible_on_front' => '0',
267+
'used_in_product_listing' => '0',
268+
'used_for_sort_by' => '0',
269+
'swatch_input_type' => 'dropdown',
270+
];
271+
}
272+
273+
/**
274+
* Test attribute saving with large amount of options exceeding maximum allowed by max_input_vars limit.
275+
* @return void
276+
*/
277+
public function testLargeOptionsDataSet()
278+
{
279+
$maxInputVars = ini_get('max_input_vars');
280+
// Each option is at least 4 variables array (order, admin value, first store view value, delete flag).
281+
// Set options count to exceed max_input_vars by 100 options (400 variables).
282+
$optionsCount = floor($maxInputVars / 4) + 100;
283+
$attributeData = $this->getLargeOptionsSetAttributeData();
284+
$optionsData = [];
285+
$expectedOptionsLabels = [];
286+
for ($i = 0; $i < $optionsCount; $i++) {
287+
$order = $i + 1;
288+
$expectedOptionLabelOnStoreView = "value_{$i}_store_1";
289+
$expectedOptionsLabels[$i+1] = $expectedOptionLabelOnStoreView;
290+
$optionsData []= "option[order][option_{$i}]={$order}";
291+
$optionsData []= "option[value][option_{$i}][0]=value_{$i}_admin";
292+
$optionsData []= "option[value][option_{$i}][1]={$expectedOptionLabelOnStoreView}";
293+
$optionsData []= "option[delete][option_{$i}=";
294+
}
295+
$attributeData['serialized_options'] = json_encode($optionsData);
296+
$this->getRequest()->setPostValue($attributeData);
297+
$this->dispatch('backend/catalog/product_attribute/save');
298+
$entityTypeId = $this->_objectManager->create(
299+
\Magento\Eav\Model\Entity::class
300+
)->setType(
301+
\Magento\Catalog\Model\Product::ENTITY
302+
)->getTypeId();
303+
304+
/** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */
305+
$attribute = $this->_objectManager->create(
306+
\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class
307+
)->setEntityTypeId(
308+
$entityTypeId
309+
);
310+
try {
311+
$attribute->loadByCode($entityTypeId, 'test_many_options');
312+
$options = $attribute->getOptions();
313+
// assert that all options are saved without truncation
314+
$this->assertEquals(
315+
$optionsCount + 1,
316+
count($options),
317+
'Expected options count does not match (regarding first empty option for non-required attribute)'
318+
);
319+
320+
foreach ($expectedOptionsLabels as $optionOrderNum => $label) {
321+
$this->assertEquals(
322+
$label,
323+
$options[$optionOrderNum]->getLabel(),
324+
"Label for option #{$optionOrderNum} does not match expected."
325+
);
326+
}
327+
} catch (LocalizedException $e) {
328+
$this->fail('Test failed with exception on attribute model load: ' . $e);
329+
}
330+
}
331+
227332
/**
228333
* Return translation for a string literal belonging to backend area
229334
*

0 commit comments

Comments
 (0)