Magento – Magento2 : How to enable inline edit in catalog grid without changing source code

catalogmagento2product-griduicomponent

I try to turn on inline edit in catalog grid as described in DevDocs:
http://devdocs.magento.com/guides/v2.0/ui-components/ui-secondary-inline.html

The main problem is how to override this block of configuration in

\vendor\magento\module-catalog\view\adminhtml\ui_component\product_listing.xml

            <item name="childDefaults" xsi:type="array">
                <item name="fieldAction" xsi:type="array">
                    <item name="provider" xsi:type="string">product_listing.product_listing.product_columns.actions</item>
                    <item name="target" xsi:type="string">applyAction</item>
                    <item name="params" xsi:type="array">
                        <item name="0" xsi:type="string">edit</item>
                        <item name="1" xsi:type="string">${ $.$data.rowIndex }</item>                            
                    </item>
                </item>
            </item>

with custom "params" required for inline edit in

\app\code\MyVendor\MyModule\view\adminhtml\ui_component\product_listing.xml

                    <item name="params" xsi:type="array">
                        <item name="0" xsi:type="string">${ $.$data.rowIndex }</item>
                        <item name="1" xsi:type="boolean">true</item>
                    </item>   

It throws error while loading configuration by the core framework function

**Magento\Ui\Model\Manager->evaluateComponentArguments()**

I don't know how to cure it except overriding framework components such as

Magento\Ui\Model\Manager or Magento\Framework\View\Layout\Generator\UiComponent,

but it's a bad idea

Another decision is to comment "fieldAction" item in the original Magento product_listing.xml

With this modification inline edit works.

Here is the full sample code :

\app\code\MyVendor\MyModule\etc\adminhtml\di.xml

<preference for="Magento\Catalog\Ui\Component\Listing\Columns" type="MyVendor\MyModule\Magento\Catalog\Ui\Component\Listing\Columns" />

\app\code\MyVendor\MyModule\etc\adminhtml\routes.xml

<router id="admin">
    <route id="my_product" frontName="my_product">
        <module name="MyVendor_MyModule"/>
    </route>
</router>

Modified original magento config:

\vendor\magento\module-catalog\view\adminhtml\ui_component\product_listing.xml

             ------
             <item name="childDefaults" xsi:type="array">
                <!--<item name="fieldAction" xsi:type="array">
                    <item name="provider" xsi:type="string">product_listing.product_listing.product_columns.actions</item>
                    <item name="target" xsi:type="string">applyAction</item>
                    <item name="params" xsi:type="array">
                        <item name="0" xsi:type="string">edit</item>
                        <item name="1" xsi:type="string">${ $.$data.rowIndex }</item>                           
                    </item>
                </item>--> 
            </item>
            --------

Custom config:

\app\code\MyVendor\MyModule\view\adminhtml\ui_component\product_listing.xml

<dataSource name="product_listing_data_source"></dataSource>
<listingToolbar name="listing_top"></listingToolbar>
<columns name="product_columns" class="Magento\Customer\Ui\Component\Listing\Columns">
    <argument name="data" xsi:type="array">
        <item name="config" xsi:type="array">
            <item name="editorConfig" xsi:type="array">
                <item name="selectProvider" xsi:type="string">product_listing.product_listing.product_columns.ids</item>
                <item name="enabled" xsi:type="boolean">true</item>
                <item name="indexField" xsi:type="string">entity_id</item>
                <item name="clientConfig" xsi:type="array">
                    <item name="saveUrl" xsi:type="url" path="my_product/product/inlineEdit"/>
                    <item name="validateBeforeSave" xsi:type="boolean">false</item>
                </item>
            </item>
            <item name="childDefaults" xsi:type="array">
                <item name="fieldAction" xsi:type="array">
                    <item name="provider" xsi:type="string">product_listing.product_listing.product_columns_editor</item>
                    <item name="target" xsi:type="string">startEdit</item>
                    <item name="params" xsi:type="array">
                        <item name="0" xsi:type="string">${ $.$data.rowIndex }</item>
                        <item name="1" xsi:type="boolean">true</item>
                    </item>                        
                </item>
            </item>
        </item>
    </argument>
    <column name="name">
        <argument name="data" xsi:type="array">
            <item name="config" xsi:type="array">
                <item name="editor" xsi:type="string">text</item>
            </item>
        </argument>
    </column>
    <column name="my_pku">
        <argument name="data" xsi:type="array">
            <item name="config" xsi:type="array">
                <item name="editor" xsi:type="string">text</item>                    
            </item>
        </argument>
    </column>
    ----- other columns ----
 </columns>

\app\code\MyVendor\MyModule\Controller\Adminhtml\Product.php

namespace MyVendor\MyModule\Controller\Adminhtml;
use Magento\Backend\App\Action;
abstract class Product extends \Magento\Backend\App\Action {
    const ADMIN_RESOURCE = 'Magento_Catalog::products';

    public function __construct(
        \Magento\Backend\App\Action\Context $context
    ) {
        parent::__construct($context);
    }
}

\app\code\MyVendor\MyModule\Controller\Adminhtml\Product\InlineEdit.php

namespace MyVendor\MyModule\Controller\Adminhtml\Product;

use Magento\Backend\App\Action;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Api\ProductRepositoryInterface;

class InlineEdit extends \Magento\Backend\App\Action {

    const ADMIN_RESOURCE = 'Magento_Catalog::products';

    private $product;
    protected $productRepository;
    protected $resultJsonFactory;
    protected $dataObjectHelper;
    protected $logger;

    public function __construct(
    Action\Context $context, ProductRepositoryInterface $productRepository,
            \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory,
            \Magento\Framework\Api\DataObjectHelper $dataObjectHelper,
            \Psr\Log\LoggerInterface $logger
    ) {
        $this->productRepository = $productRepository;
        $this->resultJsonFactory = $resultJsonFactory;
        $this->dataObjectHelper = $dataObjectHelper;
        $this->logger = $logger;
        parent::__construct($context);
    }

    public function execute() {
        $resultJson = $this->resultJsonFactory->create();

        $postItems = $this->getRequest()->getParam('items', []);
        if (!($this->getRequest()->getParam('isAjax') && count($postItems))) {
            return $resultJson->setData([
                        'messages' => [__('Please correct the data sent.')],
                        'error' => true,
            ]);
        }

        foreach ($postItems as $productId => $productRow) {
            $this->setProduct($this->productRepository->getById($productId));

            $data = $this->product->getData();
            // ---- formatting and validating
            // $this->my_server_side_validator($data);
            // $this->my_server_side_formatter($data);

            // saving
            $dataUpdated = array_replace($data, $productRow);
            $this->product->setData($dataUpdated);
            $this->product->save();
        }

        return $resultJson->setData([
                    'messages' => $this->getErrorMessages(),
                    'error' => $this->isErrorExists()
        ]);
    }

    protected function getErrorMessages() {
        $messages = [];
        foreach ($this->getMessageManager()->getMessages()->getItems() as $error) {
            $messages[] = $error->getText();
        }
        return $messages;
    }

    protected function isErrorExists() {
        return (bool) $this->getMessageManager()->getMessages(true)->getCount();
    }

    protected function setProduct(ProductInterface $product) {
        $this->product = $product;
        return $this;
    }

    protected function getProduct() {
        return $this->product;
    }
}

\app\code\MyVendor\MyModule\Magento\Catalog\Ui\Component\Listing\Columns.php

namespace MyVendor\MyModule\Magento\Catalog\Ui\Component\Listing;
class Columns extends \Magento\Catalog\Ui\Component\Listing\Columns {
    // Array of attributes not included in
    //  \vendor\magento\module-catalog\view\adminhtml\ui_component\product_listing.xml
    protected $additional_fields = [
        'my_pku'
    ];
    public function __construct(
    \Magento\Framework\View\Element\UiComponent\ContextInterface $context,
            \Magento\Catalog\Ui\Component\ColumnFactory $columnFactory,
            \Magento\Catalog\Ui\Component\Listing\Attribute\RepositoryInterface $attributeRepository,
            array $components = [], array $data = []
    ) {
        parent::__construct($context, $columnFactory, $attributeRepository,
                $components, $data);
    }
    /**
     * {@inheritdoc}
     */
    public function prepare() {
        $columnSortOrder = self::DEFAULT_COLUMNS_MAX_ORDER;
        foreach ($this->attributeRepository->getList() as $attribute) {
            $attr_code = $attribute->getAttributeCode();
            $config = [];
            if (!isset($this->components[$attr_code]) || in_array($attr_code,
                            $this->additional_fields)) {
                $config['sortOrder'] = ++$columnSortOrder;
                if ($attribute->getIsFilterableInGrid()) {
                    $config['filter'] = $this->getFilterType($attribute->getFrontendInput());
                }
                // Copy editor configuration for additional attributes
                if (isset($this->components[$attr_code]->_data['config']['editor'])) {
                    $config['editor'] = $this->components[$attr_code]->_data['config']['editor'];
                }
                $column = $this->columnFactory->create($attribute,
                        $this->getContext(), $config);
                $column->prepare();
                $this->addComponent($attribute->getAttributeCode(), $column);
            }
        }
        parent::prepare();
    }
}

I need to extend Column.php because custom product_listing.xml also can't add editor config for additional product attributes like "my_pku" code, that not included in original Magento product_listing.xml

Without this, I can inline edit only predefined fields in original Magento product_listing.xml

So, QUESTIONS are:

  1. How to enable catalog inline edit without modifying \vendor\magento\module-catalog\view\adminhtml\ui_component\product_listing.xml ?

  2. What is an acceptable method to replace original ui_component config XML files safely, when item types are different?

UPDATE:

to Pramod Kumar Sharma

In 2.1.6 I had to comment out this block of code in

\vendor\magento\module-catalog\view\adminhtml\ui_component\product_listing.xml

             ------
         <item name="childDefaults" xsi:type="array">
            <!--<item name="fieldAction" xsi:type="array">
                <item name="provider" xsi:type="string">product_listing.product_listing.product_columns.actions</item>
                <item name="target" xsi:type="string">applyAction</item>
                <item name="params" xsi:type="array">
                    <item name="0" xsi:type="string">edit</item>
                    <item name="1" xsi:type="string">${ $.$data.rowIndex }</item>                           
                </item>
            </item>--> 
        </item>
        --------

In 2.2.1 it works without commenting out.
I have no separate extension for this, it's part of my module with many other improvements

Just do as described above

Here it is

Best Answer

I have found the way for it. I have overridden all the files.I have done inline edit for below attributes.

-- brand, sku, name, price, visibility, status

Also i have done it for below custom dropdown attributes which are

-- supplier , show on home page or not(boolean)

Please follow below steps

Step 1 Show attributes in grid

PackageName/ProductInlineEdit/view/adminhtml/ui_component/product_listing.xml

<?xml version="1.0" encoding="UTF-8"?>
<!--
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */
-->
<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
    <listingToolbar name="listing_top">
        <massaction name="listing_massaction" component="Magento_Ui/js/grid/tree-massactions" class="\Magento\Catalog\Ui\Component\Product\MassAction">
            <action name="edit">
                <settings>
                    <callback>
                        <target>editSelected</target>
                        <provider>product_listing.product_listing.product_columns_editor</provider>
                    </callback>
                    <type>edit</type>
                    <label translate="true">Edit</label>
                </settings>
            </action>
        </massaction>
    </listingToolbar>
    <columns name="product_columns" class="Magento\Catalog\Ui\Component\Listing\Columns">
        <settings>
            <editorConfig>
                <param name="clientConfig" xsi:type="array">
                    <item name="saveUrl" xsi:type="url" path="productinlineedit/product/inlineEdit"/>
                    <item name="validateBeforeSave" xsi:type="boolean">false</item>
                </param>
                <param name="indexField" xsi:type="string">entity_id</param>
                <param name="enabled" xsi:type="boolean">true</param>
                <param name="selectProvider" xsi:type="string">product_listing.product_listing.product_columns.ids</param>
            </editorConfig>
            <childDefaults>
                <param name="fieldAction" xsi:type="array">
                    <item name="provider" xsi:type="string">product_listing.product_listing.product_columns_editor</item>
                    <item name="target" xsi:type="string">startEdit</item>
                    <item name="params" xsi:type="array">
                        <item name="0" xsi:type="string">${ $.$data.rowIndex }</item>
                        <item name="1" xsi:type="boolean">true</item>
                    </item>
                </param>
            </childDefaults>
        </settings>
        <column name="name" sortOrder="30">
            <settings>
                <editor>
                    <editorType>text</editorType>
                </editor>
            </settings>
        </column>
        <column name="sku" sortOrder="60">
            <settings>
                <editor>
                    <editorType>text</editorType>
                </editor>
            </settings>
        </column>
        <column name="price" class="Magento\Catalog\Ui\Component\Listing\Columns\Price" sortOrder="70">
            <settings>
                <editor>
                    <editorType>text</editorType>
                </editor>
            </settings>
        </column>
        <column name="visibility" component="Magento_Ui/js/grid/columns/select" sortOrder="80">
            <settings>
                <editor>
                    <editorType>select</editorType>
                </editor>
            </settings>
        </column>
        <column name="status" component="Magento_Ui/js/grid/columns/select" sortOrder="90">
            <settings>
                <editor>
                    <editorType>select</editorType>
                </editor>
            </settings>
        </column>
    </columns>
</listing>

Step 2 Remove cursor over the fields

PackageName/ProductInlineEdit/view/adminhtml/layout/default.xml

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="admin-1column" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
   <head>
     <css src="PackageName_ProductInlineEdit::css/inlineedit.css"/>
   </head>
</page>

Add css file in below path:-

PackageName/ProductInlineEdit/view/adminhtml/web/css/inlineedit.css

.catalog-product-index .admin__data-grid-wrap .data-row{cursor: default !important;}

Step 3 Inline edit fields with the use of plugin method

PackageName/ProductInlineEdit/etc/adminhtml/di.xml

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../lib/internal/Magento/Framework/ObjectManager/etc/config.xsd">
    <type name="Magento\Catalog\Ui\Component\ColumnFactory">
        <plugin name="packagename.product.inlineedit" type="PackageName\ProductInlineEdit\Plugin\Component\Listing\ColumnFactory" sortOrder="10" disabled="false"  />
    </type>
</config>

Create plugin file in path :-

PackageName\ProductInlineEdit\Plugin\Component\Listing\ColumnFactory

<?php

namespace PackageName\ProductInlineEdit\Plugin\Component\Listing;
/**
 * {@inheritdoc}
 */
class ColumnFactory {

    /**
     * @var array
     */
    protected $editorMap = [
        'default' => 'text',
        'select' => 'select',
        'boolean' => 'select',
        'multiselect' => 'select',
        'date' => 'dateRange',
    ];

    /**
     * Add Inline Edit for custom Attributes
     * 
     * @param $subject
     * @param $attribute
     * @param $context
     * @param $config
     */
    public function beforeCreate(\Magento\Catalog\Ui\Component\ColumnFactory $subject, $attribute, $context, array $config = [])
    {
        $editorType = $attribute->getFrontendInput();
        if(isset($this->editorMap[$editorType])){
            $editorType = $this->editorMap[$editorType];
        }

        $config['editor'] = ['editorType'=> $editorType];
        return [$attribute, $context, $config];
    }
}

Step 4 Create js file to control columns

PackageName/ProductInlineEdit/view/adminhtml/requirejs-config.js

var config = 
{
    map: 
    {
        '*': 
        {
            'Magento_Ui/js/grid/controls/columns':'PackageName_ProductInlineEdit/js/grid/controls/columns'
        }
    }
};

PackageName/ProductInlineEdit/view/adminhtml/web/js/grid/controls/columns.js

/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */

/**
 * @api
 */
define([
    'underscore',
    'mageUtils',
    'mage/translate',
    'uiCollection'
], function (_, utils, $t, Collection) {
    'use strict';

    return Collection.extend({
        defaults: {
            template: 'ui/grid/controls/columns',
            minVisible: 1,
            maxVisible: 30,
            viewportSize: 18,
            displayArea: 'dataGridActions',
            columnsProvider: 'ns = ${ $.ns }, componentType = columns',
            imports: {
                addColumns: '${ $.columnsProvider }:elems'
            },
            templates: {
                headerMsg: $t('${ $.visible } out of ${ $.total } visible')
            }
        },

        /**
         * Resets columns visibility to theirs default state.
         *
         * @returns {Columns} Chainable.
         */
        reset: function () {
            this.elems.each('applyState', 'default', 'visible');

            return this;
        },

        /**
         * Applies last saved state of columns visibility.
         *
         * @returns {Columns} Chainable.
         */
        cancel: function () {
            //this.elems.each('applyState', '', 'visible');

            return this;
        },

        /**
         * Adds columns whose visibility can be controlled to the component.
         *
         * @param {Array} columns - Elements array that will be added to component.
         * @returns {Columns} Chainable.
         */
        addColumns: function (columns) {
            columns = _.where(columns, {
                controlVisibility: true
            });

            this.insertChild(columns);

            return this;
        },

        /**
         * Defines whether child elements array length
         * is greater than the 'viewportSize' property.
         *
         * @returns {Boolean}
         */
        hasOverflow: function () {
            return this.elems().length > this.viewportSize;
        },

        /**
         * Helper, checks
         *  - if less than one item choosen
         *  - if more then viewportMaxSize choosen
         *
         * @param {Object} elem
         * @returns {Boolean}
         */
        isDisabled: function (elem) {
            var visible = this.countVisible();

            return elem.visible ?
                    visible === this.minVisible :
                    visible === this.maxVisible;
        },

        /**
         * Counts number of visible columns.
         *
         * @returns {Number}
         */
        countVisible: function () {
            return this.elems.filter('visible').length;
        },

        /**
         * Compile header message from headerMessage setting.
         *
         * @returns {String}
         */
        getHeaderMessage: function () {
            return utils.template(this.templates.headerMsg, {
                visible: this.countVisible(),
                total: this.elems().length
            });
        }
    });
});

Step 5 Save inline data with the use of custom controller. We have already defined controller path in product_listing.xml file

Create router file to define controller name:-

PackageName/ProductInlineEdit/etc/adminhtml/routes.xml

<?xml version="1.0"?>

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
    <router id="admin">
        <route id="productinlineedit" frontName="productinlineedit">
            <module name="PackageName_ProductInlineEdit" />
        </route>
    </router>
</config>

Create controller file in below path :-

PackageName/ProductInlineEdit/Controller/Adminhtml/Product/InlineEdit.php

<?php

namespace PackageName\ProductInlineEdit\Controller\Adminhtml\Product;

class InlineEdit extends \Magento\Backend\App\Action
{
    /**
     * @var \Magento\Framework\Controller\Result\JsonFactory
     */
    protected $jsonFactory;

    /**
     * @var \Magento\Catalog\Model\ProductFactory $productFactory
     */
    protected $productFactory;

    /**
    * @param \Magento\Backend\App\Action\Context $context
    * @param \Magento\Framework\Controller\Result\JsonFactory $jsonFactory
    * @param \Magento\Catalog\Model\ProductFactory $productFactory
    */

    public function __construct(
        \Magento\Backend\App\Action\Context $context,
        \Magento\Framework\Controller\Result\JsonFactory $jsonFactory,
        \Magento\Catalog\Model\ProductFactory $productFactory
    ) {
        parent::__construct($context);
        $this->jsonFactory = $jsonFactory;
        $this->_productFactory = $productFactory;
    }

    /**
     * @return \Magento\Framework\Controller\Result\JsonFactory
     */
    public function execute()
    {
        $resultJson = $this->jsonFactory->create();
        $error = false;
        $messages = [];

        $postItems = $this->getRequest()->getParam('items', []);
        if (!($this->getRequest()->getParam('isAjax') && count($postItems))) {
            return $resultJson->setData([
                'messages' => [__('Please correct the data sent.')],
                'error' => true,
            ]);
        }
        foreach ($postItems as $value)
        {
            if ($value) {
                $productObj = $this->_productFactory->create();
                if ($value['entity_id']) {
                    $productObj->load($value['entity_id']);
                }
                $productObj->setStoreId(0);
                if(array_key_exists('name', $value)) {
                    $productObj->setName($value['name']);
                }

                if(array_key_exists('sku', $value)) {
                    $productObj->setSku($value['sku']);
                }

                if(array_key_exists('price', $value)) {
                    $productObj->setPrice($value['price']);
                }

                if(array_key_exists('visibility', $value)) {
                    $productObj->setVisibility($value['visibility']);
                }

                if(array_key_exists('status', $value)) {
                    $productObj->setStatus($value['status']);
                }
                if(array_key_exists('brand', $value)) {
                    $productObj->setBrand($value['brand']);
                }

                if(array_key_exists('showinhome', $value)) {
                    $productObj->setShowinhome($value['showinhome']);
                }
                if(array_key_exists('supplier_id', $value)) {
                    $productObj->setSupplierId($value['supplier_id']);
                }

                try {
                    $productObj->save();
                } catch (\Magento\Framework\Model\Exception $e) {
                    $this->messageManager->addError($e->getMessage());
                }
            }
        }
        return $resultJson->setData([
            'messages' => $messages,
            'error' => $error
        ]);
    }
}
Related Topic