I’ve created a custom product chooser for a client that uses the modal popup and product grid.
(the example data was added manually to the database).
The “Add Products” button works and opens up the modal window so the user can select products. When the “Add Selected Products” button is pressed the modal window closes but the rows aren’t updated.
“Remove” works as expected.
Here’s my form modifer (I’ve stripped out the vendor/module names):
<?php
namespace VendorModuleUiDataProviderProductFormModifier;
use MagentoCatalogModelLocatorLocatorInterface;
use MagentoCatalogUiDataProviderProductFormModifierAbstractModifier;
use MagentoFrameworkPhrase;
use MagentoUiComponentModal;
use MagentoUiComponentForm;
use MagentoFrameworkUrlInterface;
use MagentoUiComponentDynamicRows;
use MagentoCatalogApiProductLinkRepositoryInterface;
use MagentoCatalogApiProductRepositoryInterface;
use MagentoCatalogHelperImage as ImageHelper;
use MagentoEavApiAttributeSetRepositoryInterface;
use MagentoCatalogModelProductAttributeSourceStatus;
use MagentoFrameworkLocaleCurrencyInterface;
use MagentoFrameworkAppObjectManager;
use MagentoCatalogApiDataProductLinkInterfaceFactory;
use MagentoTestFrameworkUtilityChildrenClassesSearchD;
/**
* Data provider for Grouped products
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class SamplesToCart extends AbstractModifier
{
const SAMPLES_INCLUSIVE = 'samples_product_listing';
const SAMPLES_CONTENT = 'samples_content';
const DATA_SCOPE_SAMPLE = 'samples_product_listing';
const SORT_ORDER = 20;
const LINK_TYPE = 'samples';
/**
* @var LocatorInterface
*/
protected $locator;
/**
* @var UrlInterface
*/
protected $urlBuilder;
/**
* @var ProductLinkRepositoryInterface
*/
protected $productLinkRepository;
/**
* @var ProductRepositoryInterface
*/
protected $productRepository;
/**
* @var Status
*/
protected $status;
/**
* @var AttributeSetRepositoryInterface
*/
protected $attributeSetRepository;
/**
* @var ImageHelper
*/
protected $imageHelper;
/**
* @var CurrencyInterface
*/
protected $localeCurrency;
/**
* @var ProductCollectionFactory
*/
protected $_productCollectionFactory;
/**
* @var array
*/
protected $uiComponentsConfig = [
'button_set' => 'samples_button_set',
'modal' => 'samples_modal',
'listing' => 'samples_listing',
'form' => 'product_form',
];
/**
* @var string
*/
private static $codeQuantityAndStockStatus = 'quantity_and_stock_status';
/**
* @var string
*/
private static $codeQtyContainer = 'quantity_and_stock_status_qty';
/**
* @var string
*/
private static $codeQty = 'qty';
/**
* @var ProductLinkInterfaceFactory
*/
private $productLinkFactory;
/**
* @param LocatorInterface $locator
* @param UrlInterface $urlBuilder
* @param ProductLinkRepositoryInterface $productLinkRepository
* @param ProductRepositoryInterface $productRepository
* @param ImageHelper $imageHelper
* @param Status $status
* @param AttributeSetRepositoryInterface $attributeSetRepository
* @param CurrencyInterface $localeCurrency
* @param array $uiComponentsConfig
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
MagentoCatalogModelResourceModelProductCollectionFactory $productCollectionFactory,
LocatorInterface $locator,
UrlInterface $urlBuilder,
ProductLinkRepositoryInterface $productLinkRepository,
ProductRepositoryInterface $productRepository,
ImageHelper $imageHelper,
Status $status,
AttributeSetRepositoryInterface $attributeSetRepository,
CurrencyInterface $localeCurrency,
array $uiComponentsConfig = []
) {
$this->locator = $locator;
$this->urlBuilder = $urlBuilder;
$this->productLinkRepository = $productLinkRepository;
$this->productRepository = $productRepository;
$this->imageHelper = $imageHelper;
$this->attributeSetRepository = $attributeSetRepository;
$this->status = $status;
$this->localeCurrency = $localeCurrency;
$this->uiComponentsConfig = array_replace_recursive($this->uiComponentsConfig, $uiComponentsConfig);
$this->_productCollectionFactory = $productCollectionFactory;
}
public function modifyData(array $data) {
$product = $this->locator->getProduct();
$modelID = $product->getId();
$storeId = $this->locator->getStore()->getId();
$incArray = [];
$data[$product->getId()]['links'][self::LINK_TYPE] = [];
$includedProducts = ($product->getSampleFor()) ?: null;
if ($includedProducts) {
$prodIds = json_decode($includedProducts,true);
$prodCollection = $this->_productCollectionFactory->create();
$prodCollection->addAttributeToSelect('*')->addFieldToFilter('entity_id',['in'=>[$prodIds]]);
/* Just to ensure the products are displayed in the correct order */
$prodCollection->getSelect()->order(new Zend_Db_Expr('FIELD(e.entity_id,'.implode(",",$prodIds).')'));
foreach ( $prodCollection as $item):
$data[$modelID]['links'][self::LINK_TYPE][] = $this->fillData($item,$incArray);
endforeach;
}
$data[$modelID][self::DATA_SOURCE_DEFAULT]['current_store_id'] = $storeId;
return $data;
}
protected function fillData($linkedProduct,$data) {
$data = [
'id' => $linkedProduct->getId(),
'thumbnail' => $this->imageHelper
->init($linkedProduct, 'product_listing_thumbnail')
->setImageFile($linkedProduct->getImage())
->getUrl(),
'name' => $linkedProduct->getName(),
'sku' => $linkedProduct->getSku()
];
return $data;
}
/**
* @inheritdoc
*/
public function modifyMeta(array $meta)
{
$meta = array_replace_recursive(
$meta,
[
static::SAMPLES_INCLUSIVE => [
'children' => $this->getChildren(),
'arguments' => [
'data' => [
'config' => [
'label' => __('Sample For'),
'collapsible' => true,
'opened' => true,
'componentType' => FormFieldset::NAME,
'sortOrder' => $this->getNextGroupSortOrder(
$meta,
static::SAMPLES_CONTENT,
static::SORT_ORDER
)
],
],
],
],
]
);
return $meta;
}
/**
* Disable Qty and Stock status fields
*
* @param array $meta
* @return array
*/
protected function modifyQtyAndStockStatus(array $meta)
{
if ($groupCode = $this->getGroupCodeByField($meta, 'container_' . self::$codeQuantityAndStockStatus)) {
$parentChildren = &$meta[$groupCode]['children'];
if (!empty($parentChildren['container_' . self::$codeQuantityAndStockStatus])) {
$parentChildren['container_' . self::$codeQuantityAndStockStatus] = array_replace_recursive(
$parentChildren['container_' . self::$codeQuantityAndStockStatus],
[
'children' => [
self::$codeQuantityAndStockStatus => [
'arguments' => [
'data' => [
'config' => ['disabled' => false],
],
],
],
]
]
);
}
}
if ($groupCode = $this->getGroupCodeByField($meta, self::$codeQtyContainer)) {
$parentChildren = &$meta[$groupCode]['children'];
if (!empty($parentChildren[self::$codeQtyContainer])) {
$parentChildren[self::$codeQtyContainer] = array_replace_recursive(
$parentChildren[self::$codeQtyContainer],
[
'children' => [
self::$codeQty => [
'arguments' => [
'data' => [
'config' => ['disabled' => true],
],
],
],
],
]
);
}
}
return $meta;
}
/**
* Retrieve child meta configuration
*
* @return array
*/
protected function getChildren()
{
$children = [
$this->uiComponentsConfig['button_set'] => $this->getButtonSet(),
$this->uiComponentsConfig['modal'] => $this->getModal(),
self::LINK_TYPE => $this->getGrid(),
];
return $children;
}
/**Add Included Products
* Returns Modal configuration
*
* @return array
*/
protected function getModal()
{
return [
'arguments' => [
'data' => [
'config' => [
'componentType' => Modal::NAME,
'dataScope' => '',
'provider' =>
$this->uiComponentsConfig['form']
. '.'
. $this->uiComponentsConfig['form']
. '_data_source',
'options' => [
'title' => __('Add Products'),
'buttons' => [
[
'text' => __('Cancel'),
'actions' => ['closeModal']
],
[
'text' => __('Add Selected Products'),
'class' => 'action-primary',
'actions' => [
[
'targetName' => 'index = ' . $this->uiComponentsConfig['listing'],
'actionName' => 'save'
],
'closeModal'
],
],
],
],
],
],
],
'children' => [$this->uiComponentsConfig['listing'] => $this->getListing()],
];
}
/**
* Returns Listing configuration
*
* @return array
*/
protected function getListing()
{
return [
'arguments' => [
'data' => [
'config' => [
'autoRender' => false,
'componentType' => 'insertListing',
'dataScope' => $this->uiComponentsConfig['listing'],
'externalProvider' =>
$this->uiComponentsConfig['listing']
. '.'
. $this->uiComponentsConfig['listing']
. '_data_source',
'selectionsProvider' =>
$this->uiComponentsConfig['listing']
. '.'
. $this->uiComponentsConfig['listing']
. '.product_columns.ids',
'ns' => $this->uiComponentsConfig['listing'],
'render_url' => $this->urlBuilder->getUrl('mui/index/render'),
'realTimeLink' => true,
'provider' =>
$this->uiComponentsConfig['form']
. '.'
. $this->uiComponentsConfig['form']
. '_data_source',
'dataLinks' => ['imports' => false, 'exports' => true],
'behaviourType' => 'simple',
'externalFilterMode' => true,
'imports' => [
'productId' => '${ $.provider }:data.product.current_product_id',
'storeId' => '${ $.provider }:data.product.current_store_id',
],
'exports' => [
'productId' => '${ $.provider }:data.product.current_product_id',
'storeId' => '${ $.externalProvider }:params.current_store_id',
],
],
],
],
];
}
/**
* Returns Buttons Set configuration
*
* @return array
*/
protected function getButtonSet()
{
return [
'arguments' => [
'data' => [
'config' => [
'formElement' => 'container',
'componentType' => 'container',
'label' => false,
'content' => __(
'If this product is an orderable sample for another product, then select which product(s) here.'
),
'template' => 'ui/form/components/complex',
],
],
],
'children' => [
'sample_for_button' => [
'arguments' => [
'data' => [
'config' => [
'formElement' => 'container',
'componentType' => 'container',
'component' => 'Magento_Ui/js/form/components/button',
'actions' => [
[
'targetName' => $this->uiComponentsConfig['form'] .
'.' . $this->uiComponentsConfig['form']
. '.'
. static::SAMPLES_INCLUSIVE
. '.'
. $this->uiComponentsConfig['modal'],
'actionName' => 'toggleModal',
],
[
'targetName' => $this->uiComponentsConfig['form'] .
'.' . $this->uiComponentsConfig['form']
. '.'
. static::SAMPLES_INCLUSIVE
. '.'
. $this->uiComponentsConfig['modal']
. '.'
. $this->uiComponentsConfig['listing'],
'actionName' => 'render',
],
],
'title' => __('Add Products'),
'provider' => null,
],
],
],
],
],
];
}
/**
* Returns dynamic rows configuration
*
* @return array
*/
protected function getGrid()
{
$grid = [
'arguments' => [
'data' => [
'config' => [
'additionalClasses' => 'admin__field-wide',
'componentType' => DynamicRows::NAME,
'dndConfig'=> [
'enabled'=>true
],
'label' => null,
'columnsHeader' => true,
'columnsHeaderAfterRender' => true,
'renderDefaultRecord' => false,
'template' => 'ui/dynamic-rows/templates/grid',
'addButton' => false,
'itemTemplate' => 'record',
'dataScope' => 'data.links',
'deleteButtonLabel' => __('Remove'),
'dataProvider' => $this->uiComponentsConfig['listing'],
'map' => [
'id' => 'entity_id',
'name' => 'name',
'sku' => 'sku',
'thumbnail' => 'thumbnail_src'
],
'links' => ['insertData' => '${ $.provider }:${ $.dataProvider }'],
'sortOrder' => 20,
'columnsHeader' => false,
'columnsHeaderAfterRender' => true,
],
]
],
'children' => $this->getRows(),
];
return $grid;
}
/**
* Returns Dynamic rows records configuration
*
* @return array
*/
protected function getRows()
{
return [
'record' => [
'arguments' => [
'data' => [
'config' => [
'componentType' => 'container',
'isTemplate' => true,
'is_collection' => true,
'component' => 'Magento_Ui/js/dynamic-rows/record',
'dataScope' => '',
],
],
],
'children' => $this->fillMeta(),
],
];
}
/**
* Fill meta columns
*
* @return array
*/
protected function fillMeta()
{
return [
'id' => $this->getTextColumn('id', true, __('ID'), 10),
'thumbnail' => [
'arguments' => [
'data' => [
'config' => [
'componentType' => FormField::NAME,
'formElement' => FormElementInput::NAME,
'elementTmpl' => 'ui/dynamic-rows/cells/thumbnail',
'dataType' => FormElementDataTypeText::NAME,
'dataScope' => 'thumbnail',
'fit' => true,
'label' => __('Thumbnail'),
'sortOrder' => 20,
],
],
],
],
'name' => $this->getTextColumn('name', false, __('Name'), 30),
'sku' => $this->getTextColumn('sku', false, __('SKU'), 40),
'actionDelete' => [
'arguments' => [
'data' => [
'config' => [
'additionalClasses' => 'data-grid-actions-cell',
'componentType' => 'actionDelete',
'dataType' => FormElementDataTypeText::NAME,
'label' => __('Actions'),
'sortOrder' => 100,
'fit' => true,
],
],
],
]
];
}
/**
* Returns text column configuration for the dynamic grid
*
* @param string $dataScope
* @param bool $fit
* @param Phrase $label
* @param int $sortOrder
* @return array
*/
protected function getTextColumn($dataScope, $fit, Phrase $label, $sortOrder)
{
$column = [
'arguments' => [
'data' => [
'config' => [
'componentType' => FormField::NAME,
'formElement' => FormElementInput::NAME,
'elementTmpl' => 'ui/dynamic-rows/cells/text',
'dataType' => FormElementDataTypeText::NAME,
'dataScope' => $dataScope,
'fit' => $fit,
'label' => $label,
'sortOrder' => $sortOrder,
],
],
],
];
return $column;
}
}
and ui_component XML:
<?xml version="1.0" encoding="UTF-8"?>
<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
<argument name="data" xsi:type="array">
<item name="js_config" xsi:type="array">
<item name="provider" xsi:type="string">samples_listing.samples_listing_data_source</item>
</item>
</argument>
<settings>
<spinner>product_columns</spinner>
<deps>
<dep>samples_listing.samples_listing_data_source</dep>
</deps>
</settings>
<dataSource name="samples_listing_data_source" component="Magento_Ui/js/grid/provider">
<settings>
<storageConfig>
<param name="cacheRequests" xsi:type="boolean">false</param>
</storageConfig>
<updateUrl path="mui/index/render"/>
</settings>
<aclResource>Magento_Catalog::products</aclResource>
<dataProvider class="TwentyOneDigitalSamplesToCartUiDataProviderProductSamplesToCartBundleDataProvider"
name="samples_listing_data_source">
<settings>
<requestFieldName>id</requestFieldName>
<primaryFieldName>entity_id</primaryFieldName>
</settings>
</dataProvider>
</dataSource>
<listingToolbar name="listing_top">
<filters name="listing_filters">
<argument name="data" xsi:type="array">
<item name="config" xsi:type="array">
<item name="statefull" xsi:type="array">
<item name="applied" xsi:type="boolean">false</item>
</item>
<item name="params" xsi:type="array">
<item name="filters_modifier" xsi:type="array"/>
</item>
<item name="observers" xsi:type="array">
<item name="filters" xsi:type="object">MagentoCatalogUiComponentListingFilters</item>
</item>
</item>
</argument>
<settings>
<statefull>
<property name="applied" xsi:type="boolean">false</property>
</statefull>
</settings>
</filters>
<paging name="listing_paging"/>
</listingToolbar>
<columns name="product_columns" class="MagentoUiComponentListingColumns">
<settings>
<childDefaults>
<param name="fieldAction" xsi:type="array">
<item name="provider" xsi:type="string">dynamicRowsGrid</item>
<item name="target" xsi:type="string">selectProduct</item>
<item name="params" xsi:type="array">
<item name="0" xsi:type="string">${ $.$data.rowIndex }</item>
</item>
</param>
</childDefaults>
</settings>
<selectionsColumn name="ids" sortOrder="0">
<settings>
<indexField>entity_id</indexField>
<preserveSelectionsOnFilter>true</preserveSelectionsOnFilter>
</settings>
</selectionsColumn>
<column name="entity_id" sortOrder="10">
<settings>
<filter>textRange</filter>
<label translate="true">ID</label>
<sorting>asc</sorting>
</settings>
</column>
<column name="thumbnail" class="MagentoCatalogUiComponentListingColumnsThumbnail" component="Magento_Ui/js/grid/columns/thumbnail" sortOrder="20">
<settings>
<altField>name</altField>
<hasPreview>1</hasPreview>
<addField>true</addField>
<label translate="true">Thumbnail</label>
<sortable>false</sortable>
</settings>
</column>
<column name="sku" sortOrder="30">
<settings>
<filter>text</filter>
<label translate="true">SKU</label>
</settings>
</column>
<column name="name" sortOrder="80">
<settings>
<filter>text</filter>
<label translate="true">Name</label>
</settings>
</column>
</columns>
</listing>
I know I’m close to resolving this, but I could really use some input as to what I’m missing.
The frustrating part is that the code is being reused from an early project that worked perfectly!!
Thanks in advance for any help.