Skip to content

Commit 564aa6b

Browse files
Merge remote-tracking branch 'remotes/github/MAGETWO-99443' into EPAM-PR-59
2 parents bfa0fd4 + 3f3b2b5 commit 564aa6b

File tree

6 files changed

+276
-45
lines changed

6 files changed

+276
-45
lines changed

app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/CreateOptions.php

Lines changed: 119 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,20 @@
44
* Copyright © Magento, Inc. All rights reserved.
55
* See COPYING.txt for license details.
66
*/
7+
declare(strict_types=1);
8+
79
namespace Magento\ConfigurableProduct\Controller\Adminhtml\Product\Attribute;
810

9-
use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface;
1011
use Magento\Backend\App\Action;
12+
use Magento\Catalog\Api\Data\ProductAttributeInterface;
1113
use Magento\Catalog\Model\ResourceModel\Eav\AttributeFactory;
14+
use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface;
15+
use Magento\Framework\Exception\LocalizedException;
16+
use Magento\Framework\Json\Helper\Data;
1217

18+
/**
19+
* Creates options for product attributes
20+
*/
1321
class CreateOptions extends Action implements HttpPostActionInterface
1422
{
1523
/**
@@ -20,28 +28,33 @@ class CreateOptions extends Action implements HttpPostActionInterface
2028
const ADMIN_RESOURCE = 'Magento_Catalog::products';
2129

2230
/**
23-
* @var \Magento\Framework\Json\Helper\Data
31+
* @var Data
2432
*/
2533
protected $jsonHelper;
2634

2735
/**
28-
* @var \Magento\Catalog\Model\ResourceModel\Eav\AttributeFactory
36+
* @var AttributeFactory
2937
*/
3038
protected $attributeFactory;
3139

40+
/**
41+
* @var ProductAttributeInterface[]
42+
*/
43+
private $attributes;
44+
3245
/**
3346
* @param Action\Context $context
34-
* @param \Magento\Framework\Json\Helper\Data $jsonHelper
47+
* @param Data $jsonHelper
3548
* @param AttributeFactory $attributeFactory
3649
*/
3750
public function __construct(
3851
Action\Context $context,
39-
\Magento\Framework\Json\Helper\Data $jsonHelper,
52+
Data $jsonHelper,
4053
AttributeFactory $attributeFactory
4154
) {
55+
parent::__construct($context);
4256
$this->jsonHelper = $jsonHelper;
4357
$this->attributeFactory = $attributeFactory;
44-
parent::__construct($context);
4558
}
4659

4760
/**
@@ -51,7 +64,15 @@ public function __construct(
5164
*/
5265
public function execute()
5366
{
54-
$this->getResponse()->representJson($this->jsonHelper->jsonEncode($this->saveAttributeOptions()));
67+
try {
68+
$output = $this->saveAttributeOptions();
69+
} catch (LocalizedException $e) {
70+
$output = [
71+
'error' => true,
72+
'message' => $e->getMessage(),
73+
];
74+
}
75+
$this->getResponse()->representJson($this->jsonHelper->jsonEncode($output));
5576
}
5677

5778
/**
@@ -61,31 +82,103 @@ public function execute()
6182
* @TODO Move this logic to configurable product type model
6283
* when full set of operations for attribute options during
6384
* product creation will be implemented: edit labels, remove, reorder.
64-
* Currently only addition of options to end and removal of just added option is supported.
85+
* Currently only addition of options is supported.
86+
* @throws LocalizedException
6587
*/
6688
protected function saveAttributeOptions()
6789
{
68-
$options = (array)$this->getRequest()->getParam('options');
90+
$attributeIds = $this->getUpdatedAttributeIds();
6991
$savedOptions = [];
70-
foreach ($options as $option) {
71-
if (isset($option['label']) && isset($option['is_new'])) {
72-
$attribute = $this->attributeFactory->create();
73-
$attribute->load($option['attribute_id']);
74-
$optionsBefore = $attribute->getSource()->getAllOptions(false);
75-
$attribute->setOption(
76-
[
77-
'value' => ['option_0' => [$option['label']]],
78-
'order' => ['option_0' => count($optionsBefore) + 1],
79-
]
80-
);
81-
$attribute->save();
82-
$attribute = $this->attributeFactory->create();
83-
$attribute->load($option['attribute_id']);
84-
$optionsAfter = $attribute->getSource()->getAllOptions(false);
85-
$newOption = array_pop($optionsAfter);
86-
$savedOptions[$option['id']] = $newOption['value'];
92+
foreach ($attributeIds as $attributeId => $newOptions) {
93+
$attribute = $this->getAttribute($attributeId);
94+
$this->checkUnique($attribute, $newOptions);
95+
foreach ($newOptions as $newOption) {
96+
$lastAddedOption = $this->saveOption($attribute, $newOption);
97+
$savedOptions[$newOption['id']] = $lastAddedOption['value'];
8798
}
8899
}
100+
89101
return $savedOptions;
90102
}
103+
104+
/**
105+
* Checks unique values
106+
*
107+
* @param ProductAttributeInterface $attribute
108+
* @param array $newOptions
109+
* @return void
110+
* @throws LocalizedException
111+
*/
112+
private function checkUnique(ProductAttributeInterface $attribute, array $newOptions)
113+
{
114+
$originalOptions = $attribute->getSource()->getAllOptions(false);
115+
$allOptions = array_merge($originalOptions, $newOptions);
116+
$optionValues = array_map(
117+
function ($option) {
118+
return $option['label'] ?? null;
119+
},
120+
$allOptions
121+
);
122+
123+
$uniqueValues = array_unique(array_filter($optionValues));
124+
$duplicates = array_diff_assoc($optionValues, $uniqueValues);
125+
if ($duplicates) {
126+
throw new LocalizedException(__('The value of attribute ""%1"" must be unique', $attribute->getName()));
127+
}
128+
}
129+
130+
/**
131+
* Loads the product attribute by the id
132+
*
133+
* @param int $attributeId
134+
* @return ProductAttributeInterface
135+
*/
136+
private function getAttribute(int $attributeId)
137+
{
138+
if (!isset($this->attributes[$attributeId])) {
139+
$attribute = $this->attributeFactory->create();
140+
$this->attributes[$attributeId] = $attribute->load($attributeId);
141+
}
142+
143+
return $this->attributes[$attributeId];
144+
}
145+
146+
/**
147+
* Retrieve updated attribute ids with new options
148+
*
149+
* @return array
150+
*/
151+
private function getUpdatedAttributeIds()
152+
{
153+
$options = (array)$this->getRequest()->getParam('options');
154+
$updatedAttributeIds = [];
155+
foreach ($options as $option) {
156+
if (isset($option['label'], $option['is_new'], $option['attribute_id'])) {
157+
$updatedAttributeIds[$option['attribute_id']][] = $option;
158+
}
159+
}
160+
161+
return $updatedAttributeIds;
162+
}
163+
164+
/**
165+
* Saves the option
166+
*
167+
* @param ProductAttributeInterface $attribute
168+
* @param array $newOption
169+
* @return array
170+
*/
171+
private function saveOption(ProductAttributeInterface $attribute, array $newOption)
172+
{
173+
$optionsBefore = $attribute->getSource()->getAllOptions(false);
174+
$attribute->setOption(
175+
[
176+
'value' => ['option_0' => [$newOption['label']]],
177+
'order' => ['option_0' => count($optionsBefore) + 1],
178+
]
179+
);
180+
$attribute->save();
181+
$optionsAfter = $attribute->getSource()->getAllOptions(false);
182+
return array_pop($optionsAfter);
183+
}
91184
}

app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminConfigurableProductActionGroup.xml

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,23 @@
205205
<click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnThirdNextButton"/>
206206
<click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnFourthNextButton"/>
207207
</actionGroup>
208-
208+
<actionGroup name="selectCreatedAttributeAndCreateTwoOptions" extends="addNewProductConfigurationAttribute">
209+
<remove keyForRemoval="clickOnNewAttribute"/>
210+
<remove keyForRemoval="waitForIFrame"/>
211+
<remove keyForRemoval="switchToNewAttributeIFrame"/>
212+
<remove keyForRemoval="fillDefaultLabel"/>
213+
<remove keyForRemoval="clickOnNewAttributePanel"/>
214+
<remove keyForRemoval="waitForSaveAttribute"/>
215+
<remove keyForRemoval="switchOutOfIFrame"/>
216+
<remove keyForRemoval="waitForFilters"/>
217+
<fillField userInput="{{attribute.attribute_code}}" selector="{{AdminCreateProductConfigurationsPanel.attributeCode}}" stepKey="fillFilterAttributeCodeField"/>
218+
<fillField userInput="{{firstOption.label}}" selector="{{AdminCreateProductConfigurationsPanel.attributeName}}" stepKey="fillFieldForNewFirstOption"/>
219+
<fillField userInput="{{secondOption.label}}" selector="{{AdminCreateProductConfigurationsPanel.attributeName}}" stepKey="fillFieldForNewSecondOption"/>
220+
<remove keyForRemoval="clickOnSelectAll"/>
221+
<remove keyForRemoval="clickOnSecondNextButton"/>
222+
<remove keyForRemoval="clickOnThirdNextButton"/>
223+
<remove keyForRemoval="clickOnFourthNextButton"/>
224+
</actionGroup>
209225
<actionGroup name="changeProductConfigurationsInGrid">
210226
<arguments>
211227
<argument name="firstOption" type="entity"/>

app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminCreateProductConfigurationsPanelSection.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@
2222
<element name="selectAll" type="button" selector=".action-select-all"/>
2323
<element name="selectAllByAttribute" type="button" selector="//div[@data-attribute-title='{{attr}}']//button[contains(@class, 'action-select-all')]" parameterized="true"/>
2424
<element name="createNewValue" type="input" selector=".action-create-new" timeout="30"/>
25+
<element name="attributeNameInTitle" type="input" selector="//div[contains(@class,'attribute-entity-title-block')]/div[contains(@class,'attribute-entity-title')]"/>
2526
<element name="attributeName" type="input" selector="li[data-attribute-option-title=''] .admin__field-create-new .admin__control-text"/>
27+
<element name="attributeNameWithError" type="text" selector="//li[@data-attribute-option-title='']/div[contains(@class,'admin__field admin__field-create-new _error')]"/>
2628
<element name="saveAttribute" type="button" selector="li[data-attribute-option-title=''] .action-save" timeout="30"/>
2729
<element name="attributeCheckboxByIndex" type="input" selector="li.attribute-option:nth-of-type({{var1}}) input" parameterized="true"/>
2830

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
/**
4+
* Copyright © Magento, Inc. All rights reserved.
5+
* See COPYING.txt for license details.
6+
*/
7+
-->
8+
9+
<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
10+
xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd">
11+
<test name="AdminCheckConfigurableProductAttributeValueUniquenessTest">
12+
<annotations>
13+
<features value="ConfigurableProduct"/>
14+
<title value="Attribute value validation (check for uniqueness) in configurable products"/>
15+
<description value="Attribute value validation (check for uniqueness) in configurable products"/>
16+
<severity value="MAJOR"/>
17+
<testCaseId value="MC-17450"/>
18+
<useCaseId value="MAGETWO-99443"/>
19+
<group value="ConfigurableProduct"/>
20+
</annotations>
21+
<before>
22+
<actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/>
23+
<createData entity="dropdownProductAttribute" stepKey="createProductAttribute"/>
24+
</before>
25+
<after>
26+
<!--Delete created data-->
27+
<comment userInput="Delete created data" stepKey="deleteCreatedData"/>
28+
<actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteConfigurableProductAndOptions">
29+
<argument name="product" value="$$createConfigProduct$$"/>
30+
</actionGroup>
31+
<waitForPageLoad stepKey="waitForProductIndexPage"/>
32+
<actionGroup ref="resetProductGridToDefaultView" stepKey="resetProductGridColumnsInitial"/>
33+
<deleteData createDataKey="createCategory" stepKey="deleteCategory"/>
34+
<deleteData createDataKey="createProductAttribute" stepKey="deleteAttribute"/>
35+
<actionGroup ref="logout" stepKey="logOut"/>
36+
</after>
37+
<!--Create configurable product-->
38+
<comment userInput="Create configurable product" stepKey="createConfProd"/>
39+
<createData entity="ApiCategory" stepKey="createCategory"/>
40+
<createData entity="ApiConfigurableProduct" stepKey="createConfigProduct">
41+
<requiredEntity createDataKey="createCategory"/>
42+
</createData>
43+
<!--Go to created product page-->
44+
<comment userInput="Go to created product page" stepKey="goToProdPage"/>
45+
<amOnPage url="{{ProductCatalogPage.url}}" stepKey="goToProductGrid"/>
46+
<waitForPageLoad stepKey="waitForProductPage1"/>
47+
<actionGroup ref="filterProductGridByName2" stepKey="filterByName">
48+
<argument name="name" value="$$createConfigProduct.name$$"/>
49+
</actionGroup>
50+
<click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductName"/>
51+
<waitForPageLoad stepKey="waitForProductEditPageToLoad"/>
52+
<!--Create configurations for the product-->
53+
<comment userInput="Create configurations for the product" stepKey="createConfigurations"/>
54+
<conditionalClick selector="{{AdminProductFormConfigurationsSection.sectionHeader}}" dependentSelector="{{AdminProductFormConfigurationsSection.createConfigurations}}" visible="false" stepKey="expandConfigurationsTab1"/>
55+
<click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickOnCreateConfigurations1"/>
56+
<waitForPageLoad stepKey="waitForSelectAttributesPage1"/>
57+
<actionGroup ref="selectCreatedAttributeAndCreateTwoOptions" stepKey="selectCreatedAttributeAndCreateOptions">
58+
<argument name="attribute" value="dropdownProductAttribute"/>
59+
<argument name="firstOption" value="productAttributeOption1"/>
60+
<argument name="secondOption" value="productAttributeOption1"/>
61+
</actionGroup>
62+
<!--Check that system does not allow to save 2 options with same name-->
63+
<comment userInput="Check that system does not allow to save 2 options with same name" stepKey="checkOptionNameUniqueness"/>
64+
<seeElement selector="{{AdminCreateProductConfigurationsPanel.attributeNameWithError}}" stepKey="seeThatOptionWithSameNameIsNotSaved"/>
65+
<!--Click next and assert error message-->
66+
<comment userInput="Click next and assert error message" stepKey="clickNextAndAssertErrMssg"/>
67+
<click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickNext"/>
68+
<waitForPageLoad time="10" stepKey="waitForPageLoad"/>
69+
<grabTextFrom selector="{{AdminCreateProductConfigurationsPanel.attributeNameInTitle}}" stepKey="grabErrMsg"/>
70+
<see userInput='The value of attribute "$grabErrMsg" must be unique' stepKey="verifyAttributesValueUniqueness"/>
71+
</test>
72+
</tests>

app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/attributes_values.phtml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
<label data-bind="text: label, visible: label, attr:{for:id}"
7373
class="admin__field-label"></label>
7474
</div>
75-
<div class="admin__field admin__field-create-new" data-bind="visible: !label">
75+
<div class="admin__field admin__field-create-new" data-bind="attr:{'data-role':id}, visible: !label">
7676
<div class="admin__field-control">
7777
<input class="admin__control-text"
7878
name="label"

0 commit comments

Comments
 (0)