diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductStatusStoreViewScopeTest/AdminMassUpdateProductStatusStoreViewScopeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductStatusStoreViewScopeTest/AdminMassUpdateProductStatusStoreViewScopeTest.xml index 014104380bf5c..17361ff651486 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductStatusStoreViewScopeTest/AdminMassUpdateProductStatusStoreViewScopeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductStatusStoreViewScopeTest/AdminMassUpdateProductStatusStoreViewScopeTest.xml @@ -5,8 +5,7 @@ * See COPYING.txt for license details. */ --> - + @@ -140,5 +139,5 @@ - + diff --git a/app/code/Magento/Customer/etc/adminhtml/di.xml b/app/code/Magento/Customer/etc/adminhtml/di.xml index 8d7fc668bb61c..e9884cf09bedb 100644 --- a/app/code/Magento/Customer/etc/adminhtml/di.xml +++ b/app/code/Magento/Customer/etc/adminhtml/di.xml @@ -6,13 +6,13 @@ */ --> - + Magento\Customer\Model\Backend\Customer - + diff --git a/app/code/Magento/Customer/view/adminhtml/ui_component/customer_address_listing.xml b/app/code/Magento/Customer/view/adminhtml/ui_component/customer_address_listing.xml index fb42a2c5a0787..09775e11f63e2 100644 --- a/app/code/Magento/Customer/view/adminhtml/ui_component/customer_address_listing.xml +++ b/app/code/Magento/Customer/view/adminhtml/ui_component/customer_address_listing.xml @@ -38,13 +38,9 @@ - + - - customer_address_listing.customer_address_listing.listing_top.bookmarks - current.filters - customer_address_listing.customer_address_listing.listing_top.listing_filters diff --git a/app/code/Magento/Customer/view/adminhtml/web/js/grid/filters/chips.js b/app/code/Magento/Customer/view/adminhtml/web/js/grid/filters/chips.js index 5dbf62a209b3b..18f4f541d6f40 100644 --- a/app/code/Magento/Customer/view/adminhtml/web/js/grid/filters/chips.js +++ b/app/code/Magento/Customer/view/adminhtml/web/js/grid/filters/chips.js @@ -3,6 +3,9 @@ * See COPYING.txt for license details. */ +/** + * @deprecated Unused file. Filters don't store and share between customers + */ define([ 'Magento_Ui/js/grid/filters/chips' ], function (Chips) { diff --git a/app/code/Magento/Customer/view/adminhtml/web/js/grid/filters/filters.js b/app/code/Magento/Customer/view/adminhtml/web/js/grid/filters/filters.js index f8bf7d3d7ec46..034aa6ad7bcab 100644 --- a/app/code/Magento/Customer/view/adminhtml/web/js/grid/filters/filters.js +++ b/app/code/Magento/Customer/view/adminhtml/web/js/grid/filters/filters.js @@ -10,10 +10,8 @@ define([ return Filters.extend({ defaults: { - chipsConfig: { - name: '${ $.name }_chips', - provider: '${ $.chipsConfig.name }', - component: 'Magento_Customer/js/grid/filters/chips' + statefull: { + applied: false } } }); diff --git a/app/code/Magento/Customer/view/adminhtml/web/js/grid/search/search.js b/app/code/Magento/Customer/view/adminhtml/web/js/grid/search/search.js new file mode 100644 index 0000000000000..8b14b1633a5bb --- /dev/null +++ b/app/code/Magento/Customer/view/adminhtml/web/js/grid/search/search.js @@ -0,0 +1,18 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'Magento_Ui/js/grid/search/search' +], function (Search) { + 'use strict'; + + return Search.extend({ + defaults: { + statefull: { + value: false + } + } + }); +}); diff --git a/app/code/Magento/Customer/view/base/ui_component/customer_form.xml b/app/code/Magento/Customer/view/base/ui_component/customer_form.xml index 0a9df3016df0b..8f3eb982070a1 100644 --- a/app/code/Magento/Customer/view/base/ui_component/customer_form.xml +++ b/app/code/Magento/Customer/view/base/ui_component/customer_form.xml @@ -522,10 +522,10 @@ customer_address_listing customer_address_listing - ${ $.externalProvider }:params.parent_id + ${ $.externalProvider }:params.parent_id - ${ $.provider }:data.customer.entity_id + ${ $.provider }:data.customer.entity_id ns = ${ $.ns }, index = actions:action ns = ${ $.ns }, index = listing_massaction:massaction diff --git a/app/code/Magento/Ui/Component/Filters/Type/AbstractFilter.php b/app/code/Magento/Ui/Component/Filters/Type/AbstractFilter.php index f5d3af44f71f4..3883867e1a0a0 100644 --- a/app/code/Magento/Ui/Component/Filters/Type/AbstractFilter.php +++ b/app/code/Magento/Ui/Component/Filters/Type/AbstractFilter.php @@ -3,60 +3,70 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Ui\Component\Filters\Type; +use Magento\Framework\App\ObjectManager; use Magento\Ui\Component\AbstractComponent; use Magento\Framework\View\Element\UiComponentFactory; use Magento\Framework\View\Element\UiComponent\ContextInterface; use Magento\Framework\Api\FilterBuilder; use Magento\Ui\Component\Filters\FilterModifier; +use Magento\Ui\View\Element\BookmarkContextInterface; +use Magento\Ui\View\Element\BookmarkContextProviderInterface; /** * Abstract class AbstractFilter - * @api + * @api phpcs:ignore Magento2.Classes.AbstractApi.AbstractApi -- Legacy declaration for abstract class * @since 100.0.2 + * phpcs:disable Magento2.Classes.AbstractApi */ abstract class AbstractFilter extends AbstractComponent { /** * Component name */ - const NAME = 'filter'; + public const NAME = 'filter'; /** * Filter variable name + * + * @deprecated Use ContextInterface for retrieve filters + * @see ContextInterface */ - const FILTER_VAR = 'filters'; + public const FILTER_VAR = 'filters'; /** - * Filter data - * * @var array */ - protected $filterData; + protected array $filterData; /** * @var UiComponentFactory */ - protected $uiComponentFactory; + protected UiComponentFactory $uiComponentFactory; /** * @var FilterBuilder */ - protected $filterBuilder; + protected FilterBuilder $filterBuilder; /** * @var FilterModifier */ - protected $filterModifier; + protected FilterModifier $filterModifier; /** + * AbstractFilter constructor + * * @param ContextInterface $context * @param UiComponentFactory $uiComponentFactory * @param FilterBuilder $filterBuilder * @param FilterModifier $filterModifier * @param array $components * @param array $data + * @param BookmarkContextProviderInterface|null $bookmarkContextProvider */ public function __construct( ContextInterface $context, @@ -64,13 +74,18 @@ public function __construct( FilterBuilder $filterBuilder, FilterModifier $filterModifier, array $components = [], - array $data = [] + array $data = [], + BookmarkContextProviderInterface $bookmarkContextProvider = null ) { $this->uiComponentFactory = $uiComponentFactory; $this->filterBuilder = $filterBuilder; parent::__construct($context, $components, $data); - $this->filterData = $this->getContext()->getFiltersParams(); $this->filterModifier = $filterModifier; + + $bookmarkContextProvider = $bookmarkContextProvider ?: ObjectManager::getInstance() + ->get(BookmarkContextProviderInterface::class); + $bookmarkContext = $bookmarkContextProvider->getByUiContext($context); + $this->filterData = $bookmarkContext->getFilterData(); } /** @@ -84,7 +99,9 @@ public function getComponentName() } /** - * {@inheritdoc} + * Prepare filter component + * + * @inheridoc */ public function prepare() { diff --git a/app/code/Magento/Ui/Component/Filters/Type/Select.php b/app/code/Magento/Ui/Component/Filters/Type/Select.php index e560b15c35856..8393aa6515e50 100644 --- a/app/code/Magento/Ui/Component/Filters/Type/Select.php +++ b/app/code/Magento/Ui/Component/Filters/Type/Select.php @@ -3,13 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Ui\Component\Filters\Type; +use Magento\Framework\Api\FilterBuilder; use Magento\Framework\Data\OptionSourceInterface; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\View\Element\UiComponentFactory; use Magento\Framework\View\Element\UiComponent\ContextInterface; use Magento\Ui\Component\Form\Element\Select as ElementSelect; use Magento\Ui\Component\Filters\FilterModifier; +use Magento\Ui\View\Element\BookmarkContextProviderInterface; /** * @api @@ -36,29 +40,40 @@ class Select extends AbstractFilter /** * @param ContextInterface $context * @param UiComponentFactory $uiComponentFactory - * @param \Magento\Framework\Api\FilterBuilder $filterBuilder + * @param FilterBuilder $filterBuilder * @param FilterModifier $filterModifier * @param OptionSourceInterface|null $optionsProvider * @param array $components * @param array $data + * @param BookmarkContextProviderInterface|null $bookmarkContextProvider */ public function __construct( ContextInterface $context, UiComponentFactory $uiComponentFactory, - \Magento\Framework\Api\FilterBuilder $filterBuilder, + FilterBuilder $filterBuilder, FilterModifier $filterModifier, OptionSourceInterface $optionsProvider = null, array $components = [], - array $data = [] + array $data = [], + BookmarkContextProviderInterface $bookmarkContextProvider = null ) { $this->optionsProvider = $optionsProvider; - parent::__construct($context, $uiComponentFactory, $filterBuilder, $filterModifier, $components, $data); + parent::__construct( + $context, + $uiComponentFactory, + $filterBuilder, + $filterModifier, + $components, + $data, + $bookmarkContextProvider + ); } /** * Prepare component configuration * * @return void + * @throws LocalizedException */ public function prepare() { diff --git a/app/code/Magento/Ui/Component/Plugin/Filters/PrepareErrorHandler.php b/app/code/Magento/Ui/Component/Plugin/Filters/PrepareErrorHandler.php new file mode 100644 index 0000000000000..9c4b29e0fb760 --- /dev/null +++ b/app/code/Magento/Ui/Component/Plugin/Filters/PrepareErrorHandler.php @@ -0,0 +1,73 @@ +bookmarkContextProvider = $bookmarkContextProvider; + $this->sanitizer = $sanitizer; + $this->logger = $logger; + } + + /** + * Add error config if filter prepare throw exception + * + * @param AbstractFilter $subject + * @param callable $proceed + */ + public function aroundPrepare(AbstractFilter $subject, callable $proceed) + { + try { + $proceed(); + } catch (Throwable $e) { + $this->logger->critical($e->getMessage()); + + $dataProvider = $subject->getContext()->getDataProvider(); + $config = array_replace( + $dataProvider->getConfigData(), + [ + 'lastError' => true + ] + ); + $dataProvider->setConfigData($this->sanitizer->sanitize($config)); + } + } +} diff --git a/app/code/Magento/Ui/Component/Plugin/Filters/SetFirstLoadFlag.php b/app/code/Magento/Ui/Component/Plugin/Filters/SetFirstLoadFlag.php new file mode 100644 index 0000000000000..4b6744f4e348c --- /dev/null +++ b/app/code/Magento/Ui/Component/Plugin/Filters/SetFirstLoadFlag.php @@ -0,0 +1,57 @@ +bookmarkContextProvider = $bookmarkContextProvider; + $this->sanitizer = $sanitizer; + } + + /** + * Set first load flag for prevent trigger grid ajax reload if bookmarks available + * + * @param AbstractFilter $subject + */ + public function beforePrepare(AbstractFilter $subject): void + { + $bookmarkContext = $this->bookmarkContextProvider->getByUiContext($subject->getContext()); + if ($bookmarkContext->isBookmarkAvailable()) { + $dataProvider = $subject->getContext()->getDataProvider(); + $config = array_replace( + $dataProvider->getConfigData(), + [ + 'firstLoad' => false + ] + ); + $dataProvider->setConfigData($this->sanitizer->sanitize($config)); + } + } +} diff --git a/app/code/Magento/Ui/Model/BookmarkManagement.php b/app/code/Magento/Ui/Model/BookmarkManagement.php index 0659ce2ded7bc..061ed43e23581 100644 --- a/app/code/Magento/Ui/Model/BookmarkManagement.php +++ b/app/code/Magento/Ui/Model/BookmarkManagement.php @@ -3,42 +3,61 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Ui\Model; -class BookmarkManagement implements \Magento\Ui\Api\BookmarkManagementInterface +use Magento\Authorization\Model\UserContextInterface; +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Exception\LocalizedException; +use Magento\Ui\Api\BookmarkManagementInterface; +use Magento\Ui\Api\BookmarkRepositoryInterface; +use Magento\Ui\Api\Data\BookmarkInterface; + +/** + * Bookmark Management class provide functional for retrieving bookmarks by params + * + * @SuppressWarnings(PHPMD.LongVariableName) + */ +class BookmarkManagement implements BookmarkManagementInterface { /** - * @var \Magento\Ui\Api\BookmarkRepositoryInterface + * @var BookmarkRepositoryInterface */ protected $bookmarkRepository; /** - * @var \Magento\Framework\Api\SearchCriteriaBuilder + * @var SearchCriteriaBuilder */ protected $searchCriteriaBuilder; /** - * @var \Magento\Framework\Api\FilterBuilder + * @var FilterBuilder */ protected $filterBuilder; /** - * @var \Magento\Authorization\Model\UserContextInterface + * @var UserContextInterface */ protected $userContext; /** - * @param \Magento\Ui\Api\BookmarkRepositoryInterface $bookmarkRepository - * @param \Magento\Framework\Api\FilterBuilder $filterBuilder - * @param \Magento\Framework\Api\SearchCriteriaBuilder $searchCriteriaBuilder - * @param \Magento\Authorization\Model\UserContextInterface $userContext + * @var array + */ + private $bookmarkRegistry = []; + + /** + * @param BookmarkRepositoryInterface $bookmarkRepository + * @param FilterBuilder $filterBuilder + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param UserContextInterface $userContext */ public function __construct( - \Magento\Ui\Api\BookmarkRepositoryInterface $bookmarkRepository, - \Magento\Framework\Api\FilterBuilder $filterBuilder, - \Magento\Framework\Api\SearchCriteriaBuilder $searchCriteriaBuilder, - \Magento\Authorization\Model\UserContextInterface $userContext + BookmarkRepositoryInterface $bookmarkRepository, + FilterBuilder $filterBuilder, + SearchCriteriaBuilder $searchCriteriaBuilder, + UserContextInterface $userContext ) { $this->bookmarkRepository = $bookmarkRepository; $this->searchCriteriaBuilder = $searchCriteriaBuilder; @@ -47,9 +66,12 @@ public function __construct( } /** - * {@inheritdoc} + * Create search criteria builder with namespace and user filters + * + * @param string $namespace + * @return void */ - public function loadByNamespace($namespace) + private function prepareSearchCriteriaBuilderByNamespace(string $namespace): void { $userIdFilter = $this->filterBuilder ->setField('user_id') @@ -64,47 +86,44 @@ public function loadByNamespace($namespace) $this->searchCriteriaBuilder->addFilters([$userIdFilter]); $this->searchCriteriaBuilder->addFilters([$namespaceFilter]); + } + /** + * @inheritdoc + */ + public function loadByNamespace($namespace) + { + $this->prepareSearchCriteriaBuilderByNamespace($namespace); $searchCriteria = $this->searchCriteriaBuilder->create(); - $searchResults = $this->bookmarkRepository->getList($searchCriteria); - - return $searchResults; + return $this->bookmarkRepository->getList($searchCriteria); } /** - * {@inheritdoc} + * @inheritdoc + * @return BookmarkInterface|null + * @throws LocalizedException */ public function getByIdentifierNamespace($identifier, $namespace) { - $userIdFilter = $this->filterBuilder - ->setField('user_id') - ->setConditionType('eq') - ->setValue($this->userContext->getUserId()) - ->create(); - $identifierFilter = $this->filterBuilder - ->setField('identifier') - ->setConditionType('eq') - ->setValue($identifier) - ->create(); - $namespaceFilter = $this->filterBuilder - ->setField('namespace') - ->setConditionType('eq') - ->setValue($namespace) - ->create(); + if (!isset($this->bookmarkRegistry[$identifier . $namespace])) { + $this->prepareSearchCriteriaBuilderByNamespace($namespace); + $identifierFilter = $this->filterBuilder + ->setField('identifier') + ->setConditionType('eq') + ->setValue($identifier) + ->create(); + $this->searchCriteriaBuilder->addFilters([$identifierFilter]); - $this->searchCriteriaBuilder->addFilters([$userIdFilter]); - $this->searchCriteriaBuilder->addFilters([$identifierFilter]); - $this->searchCriteriaBuilder->addFilters([$namespaceFilter]); - - $searchCriteria = $this->searchCriteriaBuilder->create(); - $searchResults = $this->bookmarkRepository->getList($searchCriteria); - if ($searchResults->getTotalCount() > 0) { - foreach ($searchResults->getItems() as $searchResult) { - $bookmark = $this->bookmarkRepository->getById($searchResult->getId()); - return $bookmark; + $searchCriteria = $this->searchCriteriaBuilder->create(); + $searchResults = $this->bookmarkRepository->getList($searchCriteria); + if ($searchResults->getTotalCount() > 0) { + $items = $searchResults->getItems(); + $this->bookmarkRegistry[$identifier . $namespace] = array_shift($items); + } else { + $this->bookmarkRegistry[$identifier . $namespace] = null; } } - return null; + return $this->bookmarkRegistry[$identifier . $namespace]; } } diff --git a/app/code/Magento/Ui/Test/Unit/Component/Filters/Type/DateRangeTest.php b/app/code/Magento/Ui/Test/Unit/Component/Filters/Type/DateRangeTest.php index dd8456460d6b3..73952cf236794 100644 --- a/app/code/Magento/Ui/Test/Unit/Component/Filters/Type/DateRangeTest.php +++ b/app/code/Magento/Ui/Test/Unit/Component/Filters/Type/DateRangeTest.php @@ -7,8 +7,8 @@ namespace Magento\Ui\Test\Unit\Component\Filters\Type; +use Magento\Framework\Api\Filter; use Magento\Framework\Api\FilterBuilder; -use Magento\Framework\View\Element\UiComponent\ContextInterface; use Magento\Framework\View\Element\UiComponent\ContextInterface as UiContext; use Magento\Framework\View\Element\UiComponent\DataProvider\DataProviderInterface; use Magento\Framework\View\Element\UiComponent\Processor; @@ -16,13 +16,15 @@ use Magento\Ui\Component\Filters\FilterModifier; use Magento\Ui\Component\Filters\Type\DateRange; use Magento\Ui\Component\Form\Element\DataType\Date as FormDate; +use Magento\Ui\View\Element\BookmarkContextInterface; +use Magento\Ui\View\Element\BookmarkContextProviderInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class DateRangeTest extends TestCase { /** - * @var ContextInterface|MockObject + * @var UiContext|MockObject */ protected $contextMock; @@ -41,13 +43,23 @@ class DateRangeTest extends TestCase */ protected $filterModifierMock; + /** + * @var BookmarkContextInterface|MockObject + */ + private $bookmarkContextMock; + + /** + * @var BookmarkContextProviderInterface|MockObject + */ + private $bookmarkContextProviderMock; + /** * Set up */ protected function setUp(): void { $this->contextMock = $this->getMockForAbstractClass( - \Magento\Framework\View\Element\UiComponent\ContextInterface::class, + UiContext::class, [], '', false @@ -61,6 +73,15 @@ protected function setUp(): void FilterModifier::class, ['applyFilterModifier'] ); + $this->bookmarkContextProviderMock = $this->getMockForAbstractClass( + BookmarkContextProviderInterface::class + ); + $this->bookmarkContextMock = $this->getMockForAbstractClass( + BookmarkContextInterface::class + ); + $this->bookmarkContextProviderMock->expects($this->once()) + ->method('getByUiContext') + ->willReturn($this->bookmarkContextMock); } /** @@ -71,12 +92,16 @@ protected function setUp(): void public function testGetComponentName() { $this->contextMock->expects($this->never())->method('getProcessor'); + $this->bookmarkContextMock->expects($this->once()) + ->method('getFilterData'); $dateRange = new DateRange( $this->contextMock, $this->uiComponentFactory, $this->filterBuilderMock, $this->filterModifierMock, - [] + [], + [], + $this->bookmarkContextProviderMock ); $this->assertSame(DateRange::NAME, $dateRange->getComponentName()); } @@ -96,6 +121,7 @@ public function testPrepare($name, $filterData, $expectedCondition) ->disableOriginalConstructor() ->getMock(); $this->contextMock->expects($this->atLeastOnce())->method('getProcessor')->willReturn($processor); + /** @var FormDate $uiComponent */ $uiComponent = $this->createMock(\Magento\Ui\Component\Form\Element\DataType\Date::class); @@ -109,34 +135,51 @@ public function testPrepare($name, $filterData, $expectedCondition) $this->contextMock->expects($this->any()) ->method('addComponentDefinition') ->with(DateRange::NAME, ['extends' => DateRange::NAME]); - $this->contextMock->expects($this->any()) - ->method('getRequestParam') - ->with(UiContext::FILTER_VAR) - ->willReturn($filterData); $dataProvider = $this->getMockForAbstractClass( - DataProviderInterface::class, - [], - '', - false + DataProviderInterface::class ); $this->contextMock->expects($this->any()) ->method('getDataProvider') ->willReturn($dataProvider); if ($expectedCondition !== null) { + $filterMock = $this->createMock(Filter::class); + $this->filterBuilderMock->expects($this->any()) + ->method('setConditionType') + ->willReturnSelf(); + $this->filterBuilderMock->expects($this->any()) + ->method('setField') + ->willReturnSelf(); + $this->filterBuilderMock->expects($this->any()) + ->method('setValue') + ->willReturnSelf(); + $this->filterBuilderMock->expects($this->any()) + ->method('create') + ->willReturn($filterMock); + /** @var DataProviderInterface $dataProvider */ $dataProvider->expects($this->any()) ->method('addFilter') - ->with($expectedCondition, $name); + ->with($filterMock); $uiComponent->expects($this->any()) ->method('getLocale') ->willReturn($expectedCondition['locale']); $uiComponent->expects($this->any()) ->method('convertDate') - ->willReturnArgument(0); + ->willReturnCallback(function ($date) { + return new \DateTime($date); + }); } + $this->bookmarkContextMock->expects($this->once()) + ->method('getFilterData') + ->willReturn($filterData); + $this->contextMock->expects($this->any()) + ->method('getRequestParam') + ->with(UiContext::FILTER_VAR) + ->willReturn($filterData); + $this->uiComponentFactory->expects($this->any()) ->method('create') ->with($name, DateRange::COMPONENT, ['context' => $this->contextMock]) @@ -148,7 +191,8 @@ public function testPrepare($name, $filterData, $expectedCondition) $this->filterBuilderMock, $this->filterModifierMock, [], - ['name' => $name] + ['name' => $name], + $this->bookmarkContextProviderMock ); $dateRange->prepare(); diff --git a/app/code/Magento/Ui/Test/Unit/Component/Filters/Type/DateTest.php b/app/code/Magento/Ui/Test/Unit/Component/Filters/Type/DateTest.php index f89bf6e36dda8..929f0b2ee2908 100644 --- a/app/code/Magento/Ui/Test/Unit/Component/Filters/Type/DateTest.php +++ b/app/code/Magento/Ui/Test/Unit/Component/Filters/Type/DateTest.php @@ -9,13 +9,15 @@ use Magento\Framework\Api\Filter; use Magento\Framework\Api\FilterBuilder; -use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponent\ContextInterface as UiContext; use Magento\Framework\View\Element\UiComponent\DataProvider\DataProviderInterface; use Magento\Framework\View\Element\UiComponent\Processor; use Magento\Framework\View\Element\UiComponentFactory; use Magento\Ui\Component\Filters\FilterModifier; use Magento\Ui\Component\Filters\Type\Date; use Magento\Ui\Component\Form\Element\DataType\Date as FormDate; +use Magento\Ui\View\Element\BookmarkContextInterface; +use Magento\Ui\View\Element\BookmarkContextProviderInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -25,7 +27,7 @@ class DateTest extends TestCase { /** - * @var ContextInterface|MockObject + * @var UiContext|MockObject */ private $contextMock; @@ -49,12 +51,22 @@ class DateTest extends TestCase */ private $dataProviderMock; + /** + * @var BookmarkContextInterface|MockObject + */ + private $bookmarkContextMock; + + /** + * @var BookmarkContextProviderInterface|MockObject + */ + private $bookmarkContextProviderMock; + /** * @inheritDoc */ protected function setUp(): void { - $this->contextMock = $this->getMockForAbstractClass(ContextInterface::class); + $this->contextMock = $this->getMockForAbstractClass(UiContext::class); $this->uiComponentFactory = $this->getMockBuilder(UiComponentFactory::class) ->onlyMethods(['create']) ->disableOriginalConstructor() @@ -69,6 +81,16 @@ protected function setUp(): void ->getMock(); $this->dataProviderMock = $this->getMockForAbstractClass(DataProviderInterface::class); + + $this->bookmarkContextProviderMock = $this->getMockForAbstractClass( + BookmarkContextProviderInterface::class + ); + $this->bookmarkContextMock = $this->getMockForAbstractClass( + BookmarkContextInterface::class + ); + $this->bookmarkContextProviderMock->expects($this->once()) + ->method('getByUiContext') + ->willReturn($this->bookmarkContextMock); } /** @@ -79,12 +101,16 @@ protected function setUp(): void public function testGetComponentName(): void { $this->contextMock->expects(static::never())->method('getProcessor'); + $this->bookmarkContextMock->expects($this->once()) + ->method('getFilterData'); $date = new Date( $this->contextMock, $this->uiComponentFactory, $this->filterBuilderMock, $this->filterModifierMock, - [] + [], + [], + $this->bookmarkContextProviderMock ); static::assertSame(Date::NAME, $date->getComponentName()); @@ -123,14 +149,18 @@ public function testPrepare(string $name, bool $showsTime, array $filterData, ?a ->method('addComponentDefinition') ->with(Date::NAME, ['extends' => Date::NAME]); - $this->contextMock->expects($this->any()) - ->method('getFiltersParams') - ->willReturn($filterData); - $this->contextMock->expects($this->any()) ->method('getDataProvider') ->willReturn($this->dataProviderMock); + $this->bookmarkContextMock->expects($this->once()) + ->method('getFilterData') + ->willReturn($filterData); + $this->contextMock->expects($this->any()) + ->method('getRequestParam') + ->with(UiContext::FILTER_VAR) + ->willReturn($filterData); + if ($expectedCondition !== null) { $this->processFilters($name, $showsTime, $filterData, $expectedCondition, $uiComponent); } @@ -149,7 +179,8 @@ public function testPrepare(string $name, bool $showsTime, array $filterData, ?a [ 'name' => $name, 'config' => ['options' => ['showsTime' => $showsTime]], - ] + ], + $this->bookmarkContextProviderMock ); $date->prepare(); } @@ -157,7 +188,7 @@ public function testPrepare(string $name, bool $showsTime, array $filterData, ?a /** * @return array */ - public function getPrepareDataProvider(): array + public function getPrepareDataProvider() { return [ [ @@ -177,8 +208,10 @@ public function getPrepareDataProvider(): array 'showsTime' => false, 'filterData' => ['test_date' => ['from' => '11-05-2015', 'to' => '11-05-2015']], 'expectedCondition' => [ - 'date_from' => '2015-05-11 00:00:00', 'type_from' => 'gteq', - 'date_to' => '2015-05-11 23:59:59', 'type_to' => 'lteq' + 'date_from' => '2015-05-11 00:00:00', + 'type_from' => 'gteq', + 'date_to' => '2015-05-11 23:59:59', + 'type_to' => 'lteq' ] ], [ @@ -198,8 +231,10 @@ public function getPrepareDataProvider(): array 'showsTime' => true, 'filterData' => ['test_date' => ['from' => '11-05-2015 10:20:00', 'to' => '11-05-2015 18:25:00']], 'expectedCondition' => [ - 'date_from' => '2015-05-11 10:20:00', 'type_from' => 'gteq', - 'date_to' => '2015-05-11 18:25:00', 'type_to' => 'lteq' + 'date_from' => '2015-05-11 10:20:00', + 'type_from' => 'gteq', + 'date_to' => '2015-05-11 18:25:00', + 'type_to' => 'lteq' ] ] ]; diff --git a/app/code/Magento/Ui/Test/Unit/Component/Filters/Type/InputTest.php b/app/code/Magento/Ui/Test/Unit/Component/Filters/Type/InputTest.php index 757ff04eb1cc2..36be56756bb7e 100644 --- a/app/code/Magento/Ui/Test/Unit/Component/Filters/Type/InputTest.php +++ b/app/code/Magento/Ui/Test/Unit/Component/Filters/Type/InputTest.php @@ -9,20 +9,22 @@ use Magento\Framework\Api\Filter; use Magento\Framework\Api\FilterBuilder; -use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponent\ContextInterface as UiContext; use Magento\Framework\View\Element\UiComponent\DataProvider\DataProviderInterface; use Magento\Framework\View\Element\UiComponent\Processor; use Magento\Framework\View\Element\UiComponentFactory; use Magento\Framework\View\Element\UiComponentInterface; use Magento\Ui\Component\Filters\FilterModifier; use Magento\Ui\Component\Filters\Type\Input; +use Magento\Ui\View\Element\BookmarkContextInterface; +use Magento\Ui\View\Element\BookmarkContextProviderInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class InputTest extends TestCase { /** - * @var ContextInterface|MockObject + * @var UiContext|MockObject */ protected $contextMock; @@ -41,13 +43,23 @@ class InputTest extends TestCase */ protected $filterModifierMock; + /** + * @var BookmarkContextInterface|MockObject + */ + private $bookmarkContextMock; + + /** + * @var BookmarkContextProviderInterface|MockObject + */ + private $bookmarkContextProviderMock; + /** * @inheritdoc */ protected function setUp(): void { $this->contextMock = $this->getMockForAbstractClass( - ContextInterface::class, + UiContext::class, [], '', false @@ -61,6 +73,16 @@ protected function setUp(): void FilterModifier::class, ['applyFilterModifier'] ); + + $this->bookmarkContextProviderMock = $this->getMockForAbstractClass( + BookmarkContextProviderInterface::class + ); + $this->bookmarkContextMock = $this->getMockForAbstractClass( + BookmarkContextInterface::class + ); + $this->bookmarkContextProviderMock->expects($this->once()) + ->method('getByUiContext') + ->willReturn($this->bookmarkContextMock); } /** @@ -71,12 +93,16 @@ protected function setUp(): void public function testGetComponentName(): void { $this->contextMock->expects($this->never())->method('getProcessor'); + $this->bookmarkContextMock->expects($this->once()) + ->method('getFilterData'); $date = new Input( $this->contextMock, $this->uiComponentFactory, $this->filterBuilderMock, $this->filterModifierMock, - [] + [], + [], + $this->bookmarkContextProviderMock ); $this->assertSame(Input::NAME, $date->getComponentName()); @@ -115,9 +141,6 @@ public function testPrepare(array $data, array $filterData, ?array $expectedCond $this->contextMock->expects($this->any()) ->method('addComponentDefinition') ->with(Input::NAME, ['extends' => Input::NAME]); - $this->contextMock->expects($this->any()) - ->method('getFiltersParams') - ->willReturn($filterData); $dataProvider = $this->getMockForAbstractClass( DataProviderInterface::class, [], @@ -129,6 +152,14 @@ public function testPrepare(array $data, array $filterData, ?array $expectedCond ->method('getDataProvider') ->willReturn($dataProvider); + $this->bookmarkContextMock->expects($this->once()) + ->method('getFilterData') + ->willReturn($filterData); + $this->contextMock->expects($this->any()) + ->method('getRequestParam') + ->with(UiContext::FILTER_VAR) + ->willReturn($filterData); + $this->uiComponentFactory->expects($this->any()) ->method('create') ->with($data['name'], Input::COMPONENT, ['context' => $this->contextMock]) @@ -165,7 +196,8 @@ public function testPrepare(array $data, array $filterData, ?array $expectedCond $this->filterBuilderMock, $this->filterModifierMock, [], - $data + $data, + $this->bookmarkContextProviderMock ); $date->prepare(); diff --git a/app/code/Magento/Ui/Test/Unit/Component/Filters/Type/RangeTest.php b/app/code/Magento/Ui/Test/Unit/Component/Filters/Type/RangeTest.php index e0f140b3ae901..298af8fba656c 100644 --- a/app/code/Magento/Ui/Test/Unit/Component/Filters/Type/RangeTest.php +++ b/app/code/Magento/Ui/Test/Unit/Component/Filters/Type/RangeTest.php @@ -9,19 +9,21 @@ use Magento\Framework\Api\Filter; use Magento\Framework\Api\FilterBuilder; -use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponent\ContextInterface as UiContext; use Magento\Framework\View\Element\UiComponent\DataProvider\DataProviderInterface; use Magento\Framework\View\Element\UiComponent\Processor; use Magento\Framework\View\Element\UiComponentFactory; use Magento\Ui\Component\Filters\FilterModifier; use Magento\Ui\Component\Filters\Type\Range; +use Magento\Ui\View\Element\BookmarkContextInterface; +use Magento\Ui\View\Element\BookmarkContextProviderInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class RangeTest extends TestCase { /** - * @var ContextInterface|MockObject + * @var UiContext|MockObject */ protected $contextMock; @@ -40,13 +42,23 @@ class RangeTest extends TestCase */ protected $filterModifierMock; + /** + * @var BookmarkContextInterface|MockObject + */ + private $bookmarkContextMock; + + /** + * @var BookmarkContextProviderInterface|MockObject + */ + private $bookmarkContextProviderMock; + /** * Set up */ protected function setUp(): void { $this->contextMock = $this->getMockForAbstractClass( - ContextInterface::class, + UiContext::class, [], '', false @@ -57,6 +69,15 @@ protected function setUp(): void FilterModifier::class, ['applyFilterModifier'] ); + $this->bookmarkContextProviderMock = $this->getMockForAbstractClass( + BookmarkContextProviderInterface::class + ); + $this->bookmarkContextMock = $this->getMockForAbstractClass( + BookmarkContextInterface::class + ); + $this->bookmarkContextProviderMock->expects($this->once()) + ->method('getByUiContext') + ->willReturn($this->bookmarkContextMock); } /** @@ -67,12 +88,16 @@ protected function setUp(): void public function testGetComponentName() { $this->contextMock->expects($this->never())->method('getProcessor'); + $this->bookmarkContextMock->expects($this->once()) + ->method('getFilterData'); $range = new Range( $this->contextMock, $this->uiComponentFactory, $this->filterBuilderMock, $this->filterModifierMock, - [] + [], + [], + $this->bookmarkContextProviderMock ); $this->assertSame(Range::NAME, $range->getComponentName()); @@ -113,9 +138,6 @@ public function testPrepare($name, $filterData, $expectedCalls) $this->contextMock->expects($this->any()) ->method('addComponentDefinition') ->with(Range::NAME, ['extends' => Range::NAME]); - $this->contextMock->expects($this->any()) - ->method('getFiltersParams') - ->willReturn($filterData); /** @var DataProviderInterface $dataProvider */ $dataProvider = $this->getMockForAbstractClass( @@ -133,13 +155,22 @@ public function testPrepare($name, $filterData, $expectedCalls) ->method('addFilter') ->with($filter); + $this->bookmarkContextMock->expects($this->once()) + ->method('getFilterData') + ->willReturn($filterData); + $this->contextMock->expects($this->any()) + ->method('getRequestParam') + ->with(UiContext::FILTER_VAR) + ->willReturn($filterData); + $range = new Range( $this->contextMock, $this->uiComponentFactory, $this->filterBuilderMock, $this->filterModifierMock, [], - ['name' => $name] + ['name' => $name], + $this->bookmarkContextProviderMock ); $range->prepare(); } diff --git a/app/code/Magento/Ui/Test/Unit/Component/Filters/Type/SelectTest.php b/app/code/Magento/Ui/Test/Unit/Component/Filters/Type/SelectTest.php index 83de1a87a981c..163055d8f8f66 100644 --- a/app/code/Magento/Ui/Test/Unit/Component/Filters/Type/SelectTest.php +++ b/app/code/Magento/Ui/Test/Unit/Component/Filters/Type/SelectTest.php @@ -10,20 +10,22 @@ use Magento\Framework\Api\Filter; use Magento\Framework\Api\FilterBuilder; use Magento\Framework\Data\OptionSourceInterface; -use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponent\ContextInterface as UiContext; use Magento\Framework\View\Element\UiComponent\DataProvider\DataProviderInterface; use Magento\Framework\View\Element\UiComponent\Processor; use Magento\Framework\View\Element\UiComponentFactory; use Magento\Framework\View\Element\UiComponentInterface; use Magento\Ui\Component\Filters\FilterModifier; use Magento\Ui\Component\Filters\Type\Select; +use Magento\Ui\View\Element\BookmarkContextInterface; +use Magento\Ui\View\Element\BookmarkContextProviderInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class SelectTest extends TestCase { /** - * @var ContextInterface|MockObject + * @var UiContext|MockObject */ protected $contextMock; @@ -42,13 +44,23 @@ class SelectTest extends TestCase */ protected $filterModifierMock; + /** + * @var BookmarkContextInterface|MockObject + */ + private $bookmarkContextMock; + + /** + * @var BookmarkContextProviderInterface|MockObject + */ + private $bookmarkContextProviderMock; + /** * Set up */ protected function setUp(): void { $this->contextMock = $this->getMockForAbstractClass( - ContextInterface::class, + UiContext::class, [], '', false @@ -62,6 +74,16 @@ protected function setUp(): void FilterModifier::class, ['applyFilterModifier'] ); + + $this->bookmarkContextProviderMock = $this->getMockForAbstractClass( + BookmarkContextProviderInterface::class + ); + $this->bookmarkContextMock = $this->getMockForAbstractClass( + BookmarkContextInterface::class + ); + $this->bookmarkContextProviderMock->expects($this->once()) + ->method('getByUiContext') + ->willReturn($this->bookmarkContextMock); } /** @@ -72,13 +94,17 @@ protected function setUp(): void public function testGetComponentName() { $this->contextMock->expects($this->never())->method('getProcessor'); + $this->bookmarkContextMock->expects($this->once()) + ->method('getFilterData'); $date = new Select( $this->contextMock, $this->uiComponentFactory, $this->filterBuilderMock, $this->filterModifierMock, null, - [] + [], + [], + $this->bookmarkContextProviderMock ); $this->assertSame(Select::NAME, $date->getComponentName()); @@ -118,9 +144,6 @@ public function testPrepare($data, $filterData, $expectedCondition) $this->contextMock->expects($this->any()) ->method('addComponentDefinition') ->with(Select::NAME, ['extends' => Select::NAME]); - $this->contextMock->expects($this->any()) - ->method('getFiltersParams') - ->willReturn($filterData); /** @var DataProviderInterface $dataProvider */ $dataProvider = $this->getMockForAbstractClass( DataProviderInterface::class, @@ -132,6 +155,14 @@ public function testPrepare($data, $filterData, $expectedCondition) ->method('getDataProvider') ->willReturn($dataProvider); + $this->bookmarkContextMock->expects($this->once()) + ->method('getFilterData') + ->willReturn($filterData); + $this->contextMock->expects($this->any()) + ->method('getRequestParam') + ->with(UiContext::FILTER_VAR) + ->willReturn($filterData); + if ($expectedCondition !== null) { $filterMock = $this->createMock(Filter::class); $this->filterBuilderMock->expects($this->any()) @@ -173,7 +204,8 @@ public function testPrepare($data, $filterData, $expectedCondition) $this->filterModifierMock, $selectOptions, [], - $data + $data, + $this->bookmarkContextProviderMock ); $date->prepare(); diff --git a/app/code/Magento/Ui/Test/Unit/Model/BookmarkManagementTest.php b/app/code/Magento/Ui/Test/Unit/Model/BookmarkManagementTest.php index 0f3dc0f5d5395..ad26cac2279e9 100644 --- a/app/code/Magento/Ui/Test/Unit/Model/BookmarkManagementTest.php +++ b/app/code/Magento/Ui/Test/Unit/Model/BookmarkManagementTest.php @@ -146,10 +146,8 @@ public function testGetByIdentifierNamespace(): void Filter::KEY_CONDITION_TYPE => 'eq' ] ); - $bookmarkId = 1; $bookmark = $this->getMockBuilder(BookmarkInterface::class) ->getMockForAbstractClass(); - $bookmark->expects($this->once())->method('getId')->willReturn($bookmarkId); $searchCriteria = $this->getMockBuilder(SearchCriteriaInterface::class) ->getMockForAbstractClass(); $this->filterBuilder @@ -169,10 +167,6 @@ public function testGetByIdentifierNamespace(): void ->method('getList') ->with($searchCriteria) ->willReturn($searchResult); - $this->bookmarkRepository->expects($this->once()) - ->method('getById') - ->with($bookmarkId) - ->willReturn($bookmark); $this->assertEquals( $bookmark, $this->bookmarkManagement->getByIdentifierNamespace($identifier, $namespace) diff --git a/app/code/Magento/Ui/View/Element/BookmarkContext.php b/app/code/Magento/Ui/View/Element/BookmarkContext.php new file mode 100644 index 0000000000000..63cfc822d9732 --- /dev/null +++ b/app/code/Magento/Ui/View/Element/BookmarkContext.php @@ -0,0 +1,145 @@ +context = $context; + $this->bookmarkManagement = $bookmarkManagement; + $this->request = $request; + } + + /** + * Prepare filter data from bookmarks + * + * @return array + */ + private function getFilterDataFromBookmark(): array + { + if ($this->bookmarkFilterData === null) { + $this->bookmarkFilterData = []; + $bookmark = $this->bookmarkManagement->getByIdentifierNamespace( + 'current', + $this->context->getNamespace() + ); + + if ($bookmark !== null) { + $this->bookmarkAvailable = true; + $bookmarkConfig = $bookmark->getConfig(); + $this->bookmarkFilterData = $bookmarkConfig['current']['filters']['applied'] ?? []; + + $this->preparePagingParams($bookmarkConfig) + ->prepareSoringParams($bookmarkConfig); + } + } + + return $this->bookmarkFilterData; + } + + /** + * Prepare paging params + * + * @param array $bookmarkConfig + * @return BookmarkContext + */ + private function preparePagingParams(array $bookmarkConfig): BookmarkContext + { + $this->request->setParams( + [ + 'paging' => $bookmarkConfig['current']['paging'] ?? [], + 'search' => $bookmarkConfig['current']['search']['value'] ?? '' + ] + ); + return $this; + } + + /** + * Prepare sorting params + * + * @param array $bookmarkConfig + * @return BookmarkContext + */ + private function prepareSoringParams(array $bookmarkConfig): BookmarkContext + { + $columns = $bookmarkConfig['current']['columns'] ?? []; + foreach ($columns as $columnName => $columnConfig) { + if (isset($columnConfig['sorting']) && $columnConfig['sorting'] !== false) { + $this->request->setParams([ + 'sorting' => [ + 'field' => $columnName, + 'direction' => $columnConfig['sorting'] + ] + ]); + break; + } + } + return $this; + } + + /** + * @inheritDoc + */ + public function getFilterData(): array + { + $contextFilterData = $this->context->getRequestParam(ContextInterface::FILTER_VAR); + if ($contextFilterData !== null) { + return $contextFilterData; + } + + return $this->getFilterDataFromBookmark(); + } + + /** + * @inheritDoc + */ + public function isBookmarkAvailable(): bool + { + return $this->bookmarkAvailable; + } +} diff --git a/app/code/Magento/Ui/View/Element/BookmarkContextInterface.php b/app/code/Magento/Ui/View/Element/BookmarkContextInterface.php new file mode 100644 index 0000000000000..d8343a1d67153 --- /dev/null +++ b/app/code/Magento/Ui/View/Element/BookmarkContextInterface.php @@ -0,0 +1,25 @@ +objectManager = $objectManager; + } + + /** + * @inheritDoc + */ + public function getByUiContext(ContextInterface $context): BookmarkContextInterface + { + $key = $context->getNamespace(); + if (!isset($this->sharedContext[$key])) { + $this->sharedContext[$key] = $this->objectManager->create( + BookmarkContextInterface::class, + ['context' => $context] + ); + } + + return $this->sharedContext[$key]; + } +} diff --git a/app/code/Magento/Ui/View/Element/BookmarkContextProviderInterface.php b/app/code/Magento/Ui/View/Element/BookmarkContextProviderInterface.php new file mode 100644 index 0000000000000..567876f2db6d9 --- /dev/null +++ b/app/code/Magento/Ui/View/Element/BookmarkContextProviderInterface.php @@ -0,0 +1,21 @@ + - - - - - - + + + + + + - + @@ -24,8 +24,10 @@ + + - + @@ -436,4 +438,8 @@ + + + + diff --git a/app/code/Magento/Ui/view/base/web/js/grid/columns/image-preview.js b/app/code/Magento/Ui/view/base/web/js/grid/columns/image-preview.js index 2d7ec9151d9b4..e1eaeb6d74e69 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/columns/image-preview.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/columns/image-preview.js @@ -67,7 +67,7 @@ define([ return; } - this.show(this.masonry().rows()[newValue]); + this.show(this.masonry().rows[newValue]); }.bind(this)); return this; @@ -97,12 +97,12 @@ define([ next: function (record) { var recordToShow; - if (record._rowIndex + 1 === this.masonry().rows().length) { + if (record._rowIndex + 1 === this.masonry().rows.length) { return; } recordToShow = this.getRecord(record._rowIndex + 1); - recordToShow.rowNumber = record.lastInRow ? record.rowNumber + 1 : record.rowNumber; + this.show(recordToShow); }, @@ -119,7 +119,6 @@ define([ } recordToShow = this.getRecord(record._rowIndex - 1); - recordToShow.rowNumber = record.firstInRow ? record.rowNumber - 1 : record.rowNumber; this.show(recordToShow); }, @@ -131,7 +130,7 @@ define([ * @return {Object} */ getRecord: function (recordIndex) { - return this.masonry().rows()[recordIndex]; + return this.masonry().rows[recordIndex]; }, /** diff --git a/app/code/Magento/Ui/view/base/web/js/grid/filters/filters.js b/app/code/Magento/Ui/view/base/web/js/grid/filters/filters.js index c0d09c2824019..30a3b2e05edcf 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/filters/filters.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/filters/filters.js @@ -110,8 +110,8 @@ define([ }, imports: { onColumnsUpdate: '${ $.columnsProvider }:elems', - onBackendError: '${ $.provider }:lastError', - bookmarksActiveIndex: '${ $.bookmarksProvider }:activeIndex' + bookmarksActiveIndex: '${ $.bookmarksProvider }:activeIndex', + onBackendError: '${ $.provider }:lastError' }, modules: { columns: '${ $.columnsProvider }', @@ -246,8 +246,8 @@ define([ * @returns {Filters} Chainable. */ addFilter: function (column) { - var index = column.index, - processed = this._processed, + var index = column.index, + processed = this._processed, filter; if (!column.filter || _.contains(processed, index)) { @@ -271,8 +271,8 @@ define([ */ buildFilter: function (column) { var filters = this.templates.filters, - filter = column.filter, - type = filters[filter.filterType]; + filter = column.filter, + type = filters[filter.filterType]; if (_.isObject(filter) && type) { filter = utils.extend({}, type, filter); @@ -405,8 +405,8 @@ define([ */ onBackendError: function (isError) { var defaultMessage = 'Something went wrong with processing the default view and we have restored the ' + - 'filter to its original state.', - customMessage = 'Something went wrong with processing current custom view and filters have been ' + + 'filter to its original state.', + customMessage = 'Something went wrong with processing current custom view and filters have been ' + 'reset to its original state. Please edit filters then click apply.'; if (isError) { diff --git a/app/code/Magento/Ui/view/base/web/js/grid/masonry.js b/app/code/Magento/Ui/view/base/web/js/grid/masonry.js index ac17c7fb565e1..5f3e69bbf3c4d 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/masonry.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/masonry.js @@ -14,6 +14,7 @@ define([ return Listing.extend({ defaults: { template: 'ui/grid/masonry', + imageRows: {}, imports: { rows: '${ $.provider }:data.items', errorMessage: '${ $.provider }:data.errorMessage' @@ -82,7 +83,6 @@ define([ initObservable: function () { this._super() .observe([ - 'rows', 'errorMessage' ]); @@ -91,22 +91,94 @@ define([ /** * Init component handler - * @param {Object} rows + * + * @param {Object} images * @return {Object} */ - initComponent: function (rows) { - if (!rows.length) { + initComponent: function (images) { + if (!images.length) { return; } this.imageMargin = parseInt(this.imageMargin, 10); - this.container = $('[data-id="' + this.containerId + '"]')[0]; - this.setLayoutStyles(); + this.setMinRatio(); + this.clearImageRows(); + this.initRows(); + this.setLayoutStylesWhenLoaded(); this.setEventListener(); return this; }, + /** + * Initialize rows + */ + initRows: function () { + var ratio = 0, + rowNumber = 1; + + this.rows.forEach(function (image, index) { + this.initRow(rowNumber); + + image.styles = ko.observable({}); + image.ratio = parseFloat((image.width / image.height).toFixed(2)); + image.rowNumber = rowNumber; + ratio += image.ratio; + this.assignImageToRow(image, rowNumber); + + if (ratio < this.minRatio && index + 1 !== this.rows.length) { + // Row has more space for images and the image is not the last one - proceed to the next iteration + return; + } + + this.assignRatioToRow(ratio, rowNumber); + + ratio = 0; + rowNumber++; + }.bind(this)); + }, + + /** + * Initialize row list by row number + * + * @param {Number} rowNumber + */ + initRow: function (rowNumber) { + if (!this.imageRows.hasOwnProperty(rowNumber)) { + this.imageRows[rowNumber] = { + ratio: 0, + images: [] + }; + } + }, + + /** + * Clear image rows before initialize + */ + clearImageRows: function () { + this.imageRows = {}; + }, + + /** + * Assign image to row + * + * @param {Object} image + * @param {Number} rowNumber + */ + assignImageToRow: function (image, rowNumber) { + this.imageRows[rowNumber].images.push(image); + }, + + /** + * Assign image to row + * + * @param {Number} ratio + * @param {Number} rowNumber + */ + assignRatioToRow: function (ratio, rowNumber) { + this.imageRows[rowNumber].ratio = ratio; + }, + /** * Set event listener to track resize event */ @@ -122,7 +194,7 @@ define([ updateStyles: function () { raf(function () { this.containerWidth = window.innerWidth; - this.setLayoutStyles(); + this.setLayoutStylesWhenLoaded(); }.bind(this), this.refreshFPS); }, @@ -131,34 +203,18 @@ define([ */ setLayoutStyles: function () { var containerWidth = parseInt(this.container.clientWidth, 10), - rowImages = [], - ratio = 0, - rowHeight = 0, - calcHeight = 0, isLastRow = false, - rowNumber = 1; - - this.setMinRatio(); - - this.rows().forEach(function (image, index) { - ratio += parseFloat((image.width / image.height).toFixed(2)); - rowImages.push(image); - - if (ratio < this.minRatio && index + 1 !== this.rows().length) { - // Row has more space for images and the image is not the last one - proceed to the next iteration - return; - } + ratio, + rowHeight, + calcHeight; - ratio = Math.max(ratio, this.minRatio); - calcHeight = (containerWidth - this.imageMargin * rowImages.length) / ratio; + _.each(this.imageRows, function (row, rowNumber) { + ratio = Math.max(row.ratio, this.minRatio); + calcHeight = (containerWidth - this.imageMargin * row.images.length) / ratio; rowHeight = calcHeight < this.maxImageHeight ? calcHeight : this.maxImageHeight; - isLastRow = index + 1 === this.rows().length; - - this.assignImagesToRow(rowImages, rowNumber, rowHeight, isLastRow); + isLastRow = parseInt(rowNumber, 10) === Object.keys(this.imageRows).length; - rowImages = []; - ratio = 0; - rowNumber++; + this.updateImagesInRow(row.images, rowHeight, isLastRow); }.bind(this)); }, @@ -167,11 +223,10 @@ define([ * Apply styles, css classes and add properties for images in the row * * @param {Object[]} images - * @param {Number} rowNumber * @param {Number} rowHeight * @param {Boolean} isLastRow */ - assignImagesToRow: function (images, rowNumber, rowHeight, isLastRow) { + updateImagesInRow: function (images, rowHeight, isLastRow) { var imageWidth; images.forEach(function (img) { @@ -180,7 +235,6 @@ define([ this.setImageClass(img, { bottom: isLastRow }); - img.rowNumber = rowNumber; }.bind(this)); images[0].firstInRow = true; @@ -191,6 +245,8 @@ define([ * Wait for container to initialize */ waitForContainer: function (callback) { + this.container = $('[data-id="' + this.containerId + '"]')[0]; + if (typeof this.container === 'undefined') { setTimeout(function () { this.waitForContainer(callback); @@ -217,13 +273,13 @@ define([ * @param {Number} height */ setImageStyles: function (img, width, height) { - if (!img.styles) { - img.styles = ko.observable(); - } - img.styles({ - width: parseInt(width, 10) + 'px', - height: parseInt(height, 10) + 'px' + var styles = img.styles(); + + styles = _.extend(styles, { + width: parseInt(width, 10).toString() + 'px', + height: parseInt(height, 10).toString() + 'px' }); + img.styles(styles); }, /** @@ -262,15 +318,6 @@ define([ this.minRatio = minRatio ? minRatio : this.defaultMinRatio; }, - /** - * Checks if grid has data. - * - * @returns {Boolean} - */ - hasData: function () { - return !!this.rows() && !!this.rows().length; - }, - /** * Returns error message returned by the data provider * diff --git a/app/code/Magento/Ui/view/base/web/js/grid/provider.js b/app/code/Magento/Ui/view/base/web/js/grid/provider.js index cf22318ee838a..b2d3675238f15 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/provider.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/provider.js @@ -21,8 +21,10 @@ define([ return Element.extend({ defaults: { + initialized: false, firstLoad: true, lastError: false, + bindListensDelay: 200, storageConfig: { component: 'Magento_Ui/js/grid/data-storage', provider: '${ $.storageConfig.name }', @@ -30,13 +32,15 @@ define([ updateUrl: '${ $.update_url }' }, listens: { - params: 'onParamsChange', - requestConfig: 'updateRequestConfig' + requestConfig: 'updateRequestConfig', + lastError: 'showAlert' + }, + delayedListens: { + params: 'onParamsChange' }, ignoreTmpls: { data: true - }, - triggerDataReload: false + } }, /** @@ -50,15 +54,47 @@ define([ this._super() .initStorage() - .clearData(); + // Invoke showAlert if error on page load + .showAlert(this.lastError); + + if (this.firstLoad) { + this.clearData(); + resolver(this.reload, this); + } else { + this.processData(this.data); + resolver(this.triggerReloaded, this); + } - // Load data when there will - // be no more pending assets. - resolver(this.reload, this); + this.delayedBindListens(); return this; }, + /** + * Trigger reloaded event on first load with predefined data + */ + triggerReloaded: function () { + var diff; + + // Invoke subscribers for predefined data + diff = utils.compare({}, this.data, 'data'); + this._notifyChanges(diff); + + this.initialized = true; + this.trigger('reloaded'); + }, + + /** + * Delayed listens binding + */ + delayedBindListens: function () { + // Let's wait before binding params listener + // for first page load due to params export from child components + _.delay(() => { + this.setListeners(this.delayedListens); + }, this.bindListensDelay, this); + }, + /** * Initializes storage component. * @@ -138,10 +174,8 @@ define([ onParamsChange: function () { // It's necessary to make a reload only // after the initial loading has been made. - if (!this.firstLoad) { + if (this.initialized) { this.reload(); - } else { - this.triggerDataReload = true; } }, @@ -152,15 +186,8 @@ define([ if (xhr.statusText === 'abort') { return; } - this.set('lastError', true); - - this.firstLoad = false; - this.triggerDataReload = false; - - alert({ - content: $t('Something went wrong.') - }); + this.initialized = true; }, /** @@ -169,15 +196,10 @@ define([ * @param {Object} data - Retrieved data object. */ onReload: function (data) { - this.firstLoad = false; + this.initialized = true; this.set('lastError', false); this.setData(data) .trigger('reloaded'); - - if (this.triggerDataReload) { - this.triggerDataReload = false; - this.reload(); - } }, /** @@ -189,6 +211,19 @@ define([ if (this.storage()) { _.extend(this.storage().requestConfig, requestConfig); } + }, + + /** + * Show alert notification + * + * @param {Boolean} isError + */ + showAlert: function (isError) { + if (isError) { + alert({ + content: $t('Something went wrong.') + }); + } } }); }); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/Data/AssociatedProductsTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/Data/AssociatedProductsTest.php index ea96514ebde32..a28df3edbce04 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/Data/AssociatedProductsTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/Data/AssociatedProductsTest.php @@ -103,7 +103,10 @@ public function testAddManuallyConfigurationsWithNotFilterableInGridAttribute(): 'test_configurable' ], ]); - $context = $this->objectManager->create(ContextInterface::class, ['request' => $request]); + $context = $this->objectManager->create(ContextInterface::class, [ + 'request' => $request, + 'namespace' => ConfigurablePanel::ASSOCIATED_PRODUCT_LISTING + ]); /** @var UiComponentFactory $uiComponentFactory */ $uiComponentFactory = $this->objectManager->get(UiComponentFactory::class); $uiComponent = $uiComponentFactory->create( diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/columns/image-preview.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/columns/image-preview.test.js index a5b434d956097..c7680a17f606f 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/columns/image-preview.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/columns/image-preview.test.js @@ -20,6 +20,10 @@ define([ recordMock = { _rowIndex: 2 }, + firstRecordMock = { + _rowIndex: 0, + rowNumber: 0 + }, secondRecordMock = { _rowIndex: 1, rowNumber: 1 @@ -29,9 +33,7 @@ define([ }, masonryMock = { shows: jasmine.createSpy().and.returnValue([]), - rows: jasmine.createSpy().and.returnValue({ - 1: secondRecordMock - }) + rows: [firstRecordMock, secondRecordMock] }; beforeEach(function () { diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/masonry.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/masonry.test.js index c32e0f78bcbfa..5dc563385039e 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/masonry.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/masonry.test.js @@ -61,20 +61,20 @@ define([ describe('check initComponent', function () { it('verify setLayoutstyles called and grid iniztilized', function () { - var setlayoutStyles = spyOn(Component, 'setLayoutStyles'); + var setlayoutStyles = spyOn(Component, 'setLayoutStylesWhenLoaded'); Object.defineProperty(Component.container, 'clientWidth', {value: '', configurable: true}); expect(Component).toBeDefined(); Component.containerId = 'masonry_grid'; Component.initComponent(rows); - Component.rows().forEach(function (image) { + Component.rows.forEach(function (image) { expect(image.styles).toBeDefined(); expect(image.css).toBeDefined(); }); expect(setlayoutStyles).toHaveBeenCalled(); }); it('verify events triggered', function () { - var setLayoutStyles = spyOn(Component, 'setLayoutStyles'); + var setLayoutStyles = spyOn(Component, 'setLayoutStylesWhenLoaded'); Component.initComponent(rows); Component.container = {}; diff --git a/lib/internal/Magento/Framework/View/Element/UiComponentFactory.php b/lib/internal/Magento/Framework/View/Element/UiComponentFactory.php index 5e1bc93b9c033..d0e3290400e21 100644 --- a/lib/internal/Magento/Framework/View/Element/UiComponentFactory.php +++ b/lib/internal/Magento/Framework/View/Element/UiComponentFactory.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Framework\View\Element; @@ -19,26 +20,24 @@ use Magento\Framework\View\Element\UiComponent\DataProvider\DataProviderInterface; use Magento\Framework\View\Element\UiComponent\DataProvider\Sanitizer; use Magento\Framework\View\Element\UiComponent\Factory\ComponentFactoryInterface; +use Magento\Ui\Config\Reader\Definition\Data; /** * Factory that creates UI component descriptor based on configuration provided. * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.LongVariableName) * @since 100.0.2 */ class UiComponentFactory extends DataObject { /** - * Object manager - * * @var ObjectManagerInterface */ protected $objectManager; /** - * Argument interpreter - * * @var InterpreterInterface */ protected $argumentInterpreter; @@ -50,11 +49,14 @@ class UiComponentFactory extends DataObject /** * UI component manager + * TODO: Add deprecated description * * @deprecated 101.0.0 * @var ManagerInterface + * phpcs:disable Magento2.Commenting.ClassPropertyPHPDocFormatting.InvalidDeprecatedTagUsage */ protected $componentManager; + //phpcs:enable Magento2.Commenting.ClassPropertyPHPDocFormatting.InvalidDeprecatedTagUsage /** * @var ComponentFactoryInterface[] @@ -67,7 +69,7 @@ class UiComponentFactory extends DataObject private $configFactory; /** - * @var \Magento\Ui\Config\Reader\Definition\Data + * @var Data */ private $definitionData; @@ -83,8 +85,8 @@ class UiComponentFactory extends DataObject * @param ContextFactory $contextFactory * @param array $data * @param array $componentChildFactories - * @param DataInterface $definitionData - * @param DataInterfaceFactory $configFactory + * @param DataInterface|null $definitionData + * @param DataInterfaceFactory|null $configFactory * @param Sanitizer|null $sanitizer */ public function __construct( @@ -159,9 +161,9 @@ protected function createChildComponent( $components = array_filter($components); $componentArguments['components'] = $components; - /** - * Prevent passing ACL restricted blocks to htmlContent constructor - */ + /** + * Prevent passing ACL restricted blocks to htmlContent constructor + */ if (isset($componentArguments['block']) && !$componentArguments['block']) { return null; } @@ -235,7 +237,7 @@ public function create($identifier, $name = null, array $arguments = []) list($className, $componentArguments) = $this->argumentsResolver($identifier, $rawComponentData); $componentArguments = array_replace_recursive($componentArguments, $arguments); $children = isset($componentArguments['data']['config']['children']) ? - $componentArguments['data']['config']['children'] : []; + $componentArguments['data']['config']['children'] : []; $children = $this->getBundleChildren($children); } @@ -280,21 +282,13 @@ protected function getBundleChildren(array $children = []) foreach ($children as $identifier => $config) { if (!isset($config['componentType'])) { throw new LocalizedException( - new Phrase( + __( 'The "componentType" configuration parameter is required for the "%1" component.', $identifier ) ); } - if (!isset($componentArguments['context'])) { - throw new LocalizedException( - new \Magento\Framework\Phrase( - 'An error occurred with the UI component. Each component needs context. Verify and try again.' - ) - ); - } - $rawComponentData = $this->definitionData->get($config['componentType']); list(, $componentArguments) = $this->argumentsResolver($identifier, $rawComponentData); $arguments = array_replace_recursive($componentArguments, ['data' => ['config' => $config]]); @@ -327,6 +321,12 @@ protected function mergeMetadata($identifier, array $bundleComponents, $reverseM $dataProvider = $this->getDataProvider($identifier, $bundleComponents); if ($dataProvider instanceof DataProviderInterface) { //Dynamic meta from data providers should not contain templates. + $dataProvider->setConfigData($this->sanitizer->sanitize(array_merge( + $dataProvider->getConfigData(), + [ + 'namespace' => $identifier + ] + ))); $metadata = $dataProvider->getMeta(); $metadata = [ $identifier => $this->sanitizer->sanitizeComponentMetadata(['children' => $metadata]) diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Element/UiComponent/ContextTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Element/UiComponent/ContextTest.php index 7fce88ec4a8ad..eb9622fc11003 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Element/UiComponent/ContextTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Element/UiComponent/ContextTest.php @@ -8,6 +8,7 @@ /** * Test for view Messages model */ + namespace Magento\Framework\View\Test\Unit\Element\UiComponent; use Magento\Framework\App\Request\Http; @@ -27,6 +28,7 @@ /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.LongVariableName) */ class ContextTest extends TestCase { @@ -121,15 +123,15 @@ protected function setUp(): void $this->context = $objectManagerHelper->getObject( Context::class, [ - 'pageLayout' => $this->pageLayout, - 'request' => $request, + 'pageLayout' => $this->pageLayout, + 'request' => $request, 'buttonProviderFactory' => $this->buttonProviderFactory, - 'actionPoolFactory' => $this->actionPoolFactory, - 'contentTypeFactory' => $this->contentTypeFactory, - 'urlBuilder' => $this->urlBuilder, - 'processor' => $this->processor, - 'uiComponentFactory' => $this->uiComponentFactory, - 'authorization' => $this->authorization, + 'actionPoolFactory' => $this->actionPoolFactory, + 'contentTypeFactory' => $this->contentTypeFactory, + 'urlBuilder' => $this->urlBuilder, + 'processor' => $this->processor, + 'uiComponentFactory' => $this->uiComponentFactory, + 'authorization' => $this->authorization, ] ); }