├── .github └── FUNDING.yml ├── Api └── ClientInterface.php ├── Controller └── Adminhtml │ └── Index │ └── Index.php ├── LICENSE.txt ├── Model ├── Client.php ├── Config.php ├── Indexer.php ├── Indexer │ ├── Block.php │ ├── CartPriceRule.php │ ├── CatalogPriceRule.php │ ├── Category.php │ ├── Customer.php │ ├── Order.php │ ├── Page.php │ └── Product.php ├── IndexerConfig.php └── Search.php ├── Observer └── Reindex.php ├── README.md ├── composer.json ├── docs └── assets │ └── showcase.gif ├── etc ├── adminhtml │ ├── routes.xml │ └── system.xml ├── config.xml ├── di.xml ├── events.xml ├── indexer.xml ├── module.xml └── mview.xml ├── registration.php └── view └── adminhtml ├── layout ├── admin_login.xml ├── default.xml ├── popup.xml └── searchbar_removed.xml ├── templates └── searchbar.phtml └── web ├── css └── source │ └── _module.less ├── js └── view │ └── search.js └── template ├── hit ├── block.html ├── cart-price-rule.html ├── catalog-price-rule.html ├── category.html ├── customer.html ├── order.html ├── page.html └── product.html └── search.html /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | buy_me_a_coffee: vasileuski 4 | -------------------------------------------------------------------------------- /Api/ClientInterface.php: -------------------------------------------------------------------------------- 1 | getRequest()->getParam('query'); 35 | $result = $this->index->search($query); 36 | 37 | return $this->resultFactory->create(ResultFactory::TYPE_JSON)->setData($result); 38 | } 39 | 40 | /** 41 | * @inheritDoc 42 | */ 43 | protected function _validateSecretKey(): bool 44 | { 45 | return true; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Dzmitry Vasileuski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Model/Client.php: -------------------------------------------------------------------------------- 1 | engine = $clientResolver->getCurrentEngine(); 32 | 33 | $this->client = match ($this->engine) { 34 | 'elasticsuite' => $objectManager->get(\Smile\ElasticsuiteCore\Api\Client\ClientInterface::class), 35 | default => $clientResolver->create(), 36 | }; 37 | } 38 | 39 | /** 40 | * @inheritDoc 41 | */ 42 | public function search(array $query): array 43 | { 44 | return match ($this->engine) { 45 | 'elasticsuite' => $this->client->search($query), 46 | default => $this->client->query($query), 47 | }; 48 | } 49 | 50 | /** 51 | * @inheritDoc 52 | */ 53 | public function bulk(array $query): void 54 | { 55 | match ($this->engine) { 56 | 'elasticsuite' => $this->client->bulk($query), 57 | default => $this->client->bulkQuery($query), 58 | }; 59 | } 60 | 61 | /** 62 | * @inheritDoc 63 | */ 64 | public function deleteIndex(string $index): void 65 | { 66 | $this->client->deleteIndex($index); 67 | } 68 | 69 | /** 70 | * @inheritDoc 71 | */ 72 | public function createIndex(string $index, array $params = []): void 73 | { 74 | $this->client->createIndex($index, $params); 75 | } 76 | 77 | /** 78 | * @inheritDoc 79 | */ 80 | public function indexExists(string $index): bool 81 | { 82 | return $this->client->indexExists($index); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Model/Config.php: -------------------------------------------------------------------------------- 1 | scopeConfig->isSetFlag(self::XML_PATH_ADMIN_SEARCH_STICKY_HEADER); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Model/Indexer.php: -------------------------------------------------------------------------------- 1 | client->indexExists($this->getIndexName())) { 62 | $this->client->deleteIndex($this->getIndexName()); 63 | } 64 | 65 | if (!$this->client->indexExists($this->getIndexName())) { 66 | $this->client->createIndex( 67 | $this->getIndexName(), 68 | [ 69 | 'mappings' => [ 70 | 'properties' => $this->indexerConfig->get( 71 | static::INDEXER_ID, 72 | 'properties' 73 | ) 74 | ] 75 | ] 76 | ); 77 | } 78 | 79 | $documentsCount = $this->getDocumentsCount($ids); 80 | $totalPages = max(1, ceil($documentsCount / static::BATCH_SIZE)); 81 | $page = 1; 82 | 83 | while ($page <= $totalPages) { 84 | $documents = $this->getDocuments($ids, $page, self::BATCH_SIZE); 85 | $documentsToDelete = array_diff($ids, array_keys($documents)); 86 | 87 | $bulk = [ 88 | 'index' => $this->getIndexName(), 89 | 'body' => [], 90 | ]; 91 | 92 | foreach ($documents as $id => $document) { 93 | $bulk['body'][] = [ 94 | 'index' => [ 95 | '_id' => $id, 96 | ] 97 | ]; 98 | 99 | $bulk['body'][] = $document; 100 | } 101 | 102 | foreach ($documentsToDelete as $id) { 103 | $bulk['body'][] = [ 104 | 'delete' => [ 105 | '_id' => $id, 106 | ], 107 | ]; 108 | } 109 | 110 | if ($bulk['body']) { 111 | $this->client->bulk($bulk); 112 | } 113 | 114 | $page++; 115 | } 116 | } 117 | 118 | /** 119 | * @inheritDoc 120 | */ 121 | public function executeFull() 122 | { 123 | $this->execute([]); 124 | } 125 | 126 | /** 127 | * @inheritDoc 128 | */ 129 | public function executeList(array $ids) 130 | { 131 | $this->execute($ids); 132 | } 133 | 134 | /** 135 | * @inheritDoc 136 | */ 137 | public function executeRow($id) 138 | { 139 | $this->execute([$id]); 140 | } 141 | 142 | /** 143 | * @inheritDoc 144 | */ 145 | protected function getIndexName(): string 146 | { 147 | return static::INDEXER_ID; 148 | } 149 | 150 | /** 151 | * Format date according to locale and timezone settings. 152 | * 153 | * @param string|null $date 154 | * @param string|null $timezone 155 | * @return string 156 | * 157 | * @throws \DateInvalidTimeZoneException 158 | * @throws \DateMalformedStringException 159 | */ 160 | protected function getFormattedDate(?string $date, ?string $timezone = null): string 161 | { 162 | if (!$date) { 163 | return ''; 164 | } 165 | 166 | if ($timezone) { 167 | $date = new \DateTime($date, new \DateTimeZone($timezone)); 168 | } else { 169 | $date = new \DateTime($date, new \DateTimeZone($this->timezone->getConfigTimezone())); 170 | } 171 | 172 | return $this->timezone->formatDateTime( 173 | $date, 174 | \IntlDateFormatter::MEDIUM, 175 | \IntlDateFormatter::NONE, 176 | $this->localeResolver->getDefaultLocale(), 177 | ); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /Model/Indexer/Block.php: -------------------------------------------------------------------------------- 1 | resource->getConnection(); 19 | $select = $connection->select(); 20 | 21 | $select->from( 22 | $this->resource->getTableName('cms_block'), 23 | ['block_id', 'title', 'identifier', 'is_active'] 24 | ); 25 | 26 | if ($ids) { 27 | $select->where('block_id IN (?)', $ids); 28 | } 29 | 30 | $select->limitPage($page, $pageSize); 31 | 32 | $entities = $connection->fetchAll($select); 33 | $documents = []; 34 | 35 | foreach ($entities as $entity) { 36 | $documents[$entity['block_id']] = [ 37 | 'block_title' => $entity['title'], 38 | 'block_identifier' => $entity['identifier'], 39 | 'block_status' => $entity['is_active'] ? __('Enabled') : __('Disabled'), 40 | ]; 41 | } 42 | 43 | return $documents; 44 | } 45 | 46 | /** 47 | * @inheritDoc 48 | */ 49 | protected function getDocumentsCount(array $ids): int 50 | { 51 | $connection = $this->resource->getConnection(); 52 | $select = $connection->select(); 53 | 54 | $select->from( 55 | $this->resource->getTableName('cms_block'), 56 | ['count' => new \Zend_Db_Expr('COUNT(*)')] 57 | ); 58 | 59 | if ($ids) { 60 | $select->where('block_id IN (?)', $ids); 61 | } 62 | 63 | return (int) $connection->fetchOne($select); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Model/Indexer/CartPriceRule.php: -------------------------------------------------------------------------------- 1 | resource->getConnection(); 20 | $select = $connection->select(); 21 | 22 | $select->from( 23 | $this->resource->getTableName('salesrule'), 24 | [ 25 | 'rule_id', 26 | 'name', 27 | 'from_date', 28 | 'to_date', 29 | 'is_active', 30 | ] 31 | ); 32 | 33 | if ($ids) { 34 | $select->where('rule_id IN (?)', $ids); 35 | } 36 | 37 | $select->limitPage($page, $pageSize); 38 | 39 | $entities = $connection->fetchAll($select); 40 | $documents = []; 41 | 42 | foreach ($entities as $entity) { 43 | $documents[$entity['rule_id']] = [ 44 | 'cart_price_rule_name' => $entity['name'], 45 | 'cart_price_rule_from' => $this->getFormattedDate($entity['from_date']), 46 | 'cart_price_rule_to' => $this->getFormattedDate($entity['to_date']), 47 | 'cart_price_rule_is_active' => (int) $entity['is_active'], 48 | ]; 49 | } 50 | 51 | return $documents; 52 | } 53 | 54 | /** 55 | * @inheritDoc 56 | */ 57 | protected function getDocumentsCount(array $ids): int 58 | { 59 | $connection = $this->resource->getConnection(); 60 | $select = $connection->select(); 61 | 62 | $select->from( 63 | $this->resource->getTableName('salesrule'), 64 | ['count' => new \Zend_Db_Expr('COUNT(*)')] 65 | ); 66 | 67 | if ($ids) { 68 | $select->where('rule_id IN (?)', $ids); 69 | } 70 | 71 | return (int) $connection->fetchOne($select); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Model/Indexer/CatalogPriceRule.php: -------------------------------------------------------------------------------- 1 | resource->getConnection(); 19 | $select = $connection->select(); 20 | 21 | $select->from( 22 | $this->resource->getTableName('catalogrule'), 23 | [ 24 | 'rule_id', 25 | 'name', 26 | 'from_date', 27 | 'to_date', 28 | 'is_active', 29 | ] 30 | ); 31 | 32 | if ($ids) { 33 | $select->where('rule_id IN (?)', $ids); 34 | } 35 | 36 | $select->limitPage($page, $pageSize); 37 | 38 | $entities = $connection->fetchAll($select); 39 | $documents = []; 40 | 41 | foreach ($entities as $entity) { 42 | $documents[$entity['rule_id']] = [ 43 | 'catalog_price_rule_name' => $entity['name'], 44 | 'catalog_price_rule_from' => $this->getFormattedDate($entity['from_date']), 45 | 'catalog_price_rule_to' => $this->getFormattedDate($entity['to_date']), 46 | 'catalog_price_rule_is_active' => (int) $entity['is_active'], 47 | ]; 48 | } 49 | 50 | return $documents; 51 | } 52 | 53 | /** 54 | * @inheritDoc 55 | */ 56 | protected function getDocumentsCount(array $ids): int 57 | { 58 | $connection = $this->resource->getConnection(); 59 | $select = $connection->select(); 60 | 61 | $select->from( 62 | $this->resource->getTableName('catalogrule'), 63 | ['count' => new \Zend_Db_Expr('COUNT(*)')] 64 | ); 65 | 66 | if ($ids) { 67 | $select->where('rule_id IN (?)', $ids); 68 | } 69 | 70 | return (int) $connection->fetchOne($select); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Model/Indexer/Category.php: -------------------------------------------------------------------------------- 1 | getCategories($ids, $page, $pageSize); 56 | 57 | $parentEntityIds = []; 58 | 59 | foreach ($entities as $entity) { 60 | // phpcs:ignore 61 | $parentEntityIds = array_merge( 62 | $parentEntityIds, 63 | array_slice(explode('/', $entity['path']), 1) 64 | ); 65 | } 66 | 67 | $parentEntities = $this->getCategories(array_unique($parentEntityIds)); 68 | 69 | $parentEntitiesMap = array_combine( 70 | array_column($parentEntities, 'entity_id'), 71 | $parentEntities 72 | ); 73 | 74 | $documents = []; 75 | 76 | foreach ($entities as $entity) { 77 | $parents = []; 78 | 79 | foreach (array_slice(explode('/', $entity['path']), 1) as $categoryId) { 80 | if (isset($parentEntities[$categoryId]['name'])) { 81 | $parents[] = $parentEntitiesMap[$categoryId]['name']; 82 | } 83 | } 84 | 85 | $documents[$entity['entity_id']] = [ 86 | 'category_name' => $entity['name'], 87 | 'category_path' => implode(' / ', $parents), 88 | ]; 89 | } 90 | 91 | return $documents; 92 | } 93 | 94 | /** 95 | * Get categories by IDs 96 | * 97 | * @param array $ids 98 | * @param int|null $page 99 | * @param int|null $pageSize 100 | * 101 | * @return array 102 | * 103 | * @throws LocalizedException 104 | * @throws NoSuchEntityException 105 | */ 106 | private function getCategories(array $ids, ?int $page = null, ?int $pageSize = null): array 107 | { 108 | $connection = $this->resource->getConnection(); 109 | $select = $connection->select(); 110 | 111 | $select->from( 112 | ['c' => $this->resource->getTableName('catalog_category_entity')], 113 | ['entity_id', 'path'] 114 | ); 115 | 116 | $select->join( 117 | ['cv' => $this->resource->getTableName('catalog_category_entity_varchar')], 118 | 'c.entity_id = cv.entity_id', 119 | ['name' => 'value'] 120 | ); 121 | 122 | $select->join( 123 | ['a' => $this->resource->getTableName('eav_attribute')], 124 | 'a.attribute_id = cv.attribute_id', 125 | [] 126 | ); 127 | 128 | $entityTypeId = $this->eavConfig->getEntityType(CategoryModel::ENTITY)->getId(); 129 | $storeId = $this->storeManager->getStore('admin')->getId(); 130 | 131 | $select->where('cv.store_id = ?', $storeId); 132 | $select->where('a.attribute_code = ?', 'name'); 133 | $select->where('a.entity_type_id = ?', $entityTypeId); 134 | 135 | if ($ids) { 136 | $select->where('c.entity_id IN (?)', $ids); 137 | } 138 | 139 | if ($page && $pageSize) { 140 | $select->limitPage($page, $pageSize); 141 | } 142 | 143 | return $connection->fetchAll($select); 144 | } 145 | 146 | /** 147 | * @inheritDoc 148 | */ 149 | protected function getDocumentsCount(array $ids): int 150 | { 151 | $connection = $this->resource->getConnection(); 152 | $select = $connection->select(); 153 | 154 | $select->from( 155 | ['c' => $this->resource->getTableName('catalog_category_entity')], 156 | ['count' => new \Zend_Db_Expr('COUNT(*)')] 157 | ); 158 | 159 | if ($ids) { 160 | $select->where('c.entity_id IN (?)', $ids); 161 | } 162 | 163 | return (int) $connection->fetchOne($select); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /Model/Indexer/Customer.php: -------------------------------------------------------------------------------- 1 | resource->getConnection(); 19 | $select = $connection->select(); 20 | 21 | $select->from( 22 | $this->resource->getTableName('customer_entity'), 23 | [ 24 | 'entity_id', 25 | 'email', 26 | 'firstname', 27 | 'lastname', 28 | ] 29 | ); 30 | 31 | if ($ids) { 32 | $select->where('entity_id IN (?)', $ids); 33 | } 34 | 35 | $select->limitPage($page, $pageSize); 36 | 37 | $entities = $connection->fetchAll($select); 38 | $documents = []; 39 | 40 | foreach ($entities as $entity) { 41 | $documents[$entity['entity_id']] = [ 42 | 'customer_email' => $entity['email'], 43 | 'customer_firstname' => $entity['firstname'], 44 | 'customer_lastname' => $entity['lastname'], 45 | ]; 46 | } 47 | 48 | return $documents; 49 | } 50 | 51 | /** 52 | * @inheritDoc 53 | */ 54 | protected function getDocumentsCount(array $ids): int 55 | { 56 | $connection = $this->resource->getConnection(); 57 | $select = $connection->select(); 58 | 59 | $select->from( 60 | $this->resource->getTableName('customer_entity'), 61 | ['count' => new \Zend_Db_Expr('COUNT(*)')] 62 | ); 63 | 64 | if ($ids) { 65 | $select->where('entity_id IN (?)', $ids); 66 | } 67 | 68 | return (int) $connection->fetchOne($select); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Model/Indexer/Order.php: -------------------------------------------------------------------------------- 1 | resource->getConnection(); 50 | $select = $connection->select(); 51 | 52 | $select->from( 53 | $this->resource->getTableName('sales_order'), 54 | [ 55 | 'entity_id', 56 | 'increment_id', 57 | 'status', 58 | 'customer_firstname', 59 | 'customer_lastname', 60 | 'customer_email', 61 | 'created_at', 62 | ] 63 | ); 64 | 65 | if ($ids) { 66 | $select->where('entity_id IN (?)', $ids); 67 | } 68 | 69 | $select->where('created_at > DATE_SUB(NOW(), INTERVAL 1 YEAR)'); 70 | 71 | $select->limitPage($page, $pageSize); 72 | 73 | $entities = $connection->fetchAll($select); 74 | $statuses = $this->orderConfig->getStatuses(); 75 | $documents = []; 76 | 77 | foreach ($entities as $entity) { 78 | $documents[$entity['entity_id']] = [ 79 | 'order_increment_id' => $entity['increment_id'], 80 | 'order_status' => $statuses[$entity['status']] ?? null, 81 | 'order_customer_firstname' => $entity['customer_firstname'], 82 | 'order_customer_lastname' => $entity['customer_lastname'], 83 | 'order_customer_email' => $entity['customer_email'], 84 | 'order_created_at' => $this->getFormattedDate($entity['created_at'], 'UTC'), 85 | ]; 86 | } 87 | 88 | return $documents; 89 | } 90 | 91 | /** 92 | * @inheritDoc 93 | */ 94 | protected function getDocumentsCount(array $ids): int 95 | { 96 | $connection = $this->resource->getConnection(); 97 | $select = $connection->select(); 98 | 99 | $select->from( 100 | $this->resource->getTableName('sales_order'), 101 | ['count' => new \Zend_Db_Expr('COUNT(*)')] 102 | ); 103 | 104 | if ($ids) { 105 | $select->where('entity_id IN (?)', $ids); 106 | } 107 | 108 | $select->where('created_at > DATE_SUB(NOW(), INTERVAL 1 YEAR)'); 109 | 110 | return (int) $connection->fetchOne($select); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Model/Indexer/Page.php: -------------------------------------------------------------------------------- 1 | resource->getConnection(); 19 | $select = $connection->select(); 20 | 21 | $select->from( 22 | $this->resource->getTableName('cms_page'), 23 | ['page_id', 'title', 'identifier', 'is_active'] 24 | ); 25 | 26 | if ($ids) { 27 | $select->where('page_id IN (?)', $ids); 28 | } 29 | 30 | $select->limitPage($page, $pageSize); 31 | 32 | $entities = $connection->fetchAll($select); 33 | $documents = []; 34 | 35 | foreach ($entities as $entity) { 36 | $documents[$entity['page_id']] = [ 37 | 'page_title' => $entity['title'], 38 | 'page_identifier' => $entity['identifier'], 39 | 'page_status' => $entity['is_active'] ? __('Enabled') : __('Disabled'), 40 | ]; 41 | } 42 | 43 | return $documents; 44 | } 45 | 46 | /** 47 | * @inheritDoc 48 | */ 49 | protected function getDocumentsCount(array $ids): int 50 | { 51 | $connection = $this->resource->getConnection(); 52 | $select = $connection->select(); 53 | 54 | $select->from( 55 | $this->resource->getTableName('cms_page'), 56 | ['count' => new \Zend_Db_Expr('COUNT(*)')] 57 | ); 58 | 59 | if ($ids) { 60 | $select->where('page_id IN (?)', $ids); 61 | } 62 | 63 | return (int) $connection->fetchOne($select); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Model/Indexer/Product.php: -------------------------------------------------------------------------------- 1 | resource->getConnection(); 54 | $select = $connection->select(); 55 | 56 | $select->from( 57 | ['p' => $this->resource->getTableName('catalog_product_entity')], 58 | ['entity_id', 'sku', 'type_id'] 59 | ); 60 | 61 | $select->join( 62 | ['pv' => $this->resource->getTableName('catalog_product_entity_varchar')], 63 | 'p.entity_id = pv.entity_id', 64 | ['name' => 'value'] 65 | ); 66 | 67 | $select->join( 68 | ['a' => $this->resource->getTableName('eav_attribute')], 69 | 'a.attribute_id = pv.attribute_id', 70 | [] 71 | ); 72 | 73 | $entityTypeId = $this->eavConfig->getEntityType(ProductModel::ENTITY)->getId(); 74 | $storeId = $this->storeManager->getStore('admin')->getId(); 75 | 76 | $select->where('pv.store_id = ?', $storeId); 77 | $select->where('a.attribute_code = ?', 'name'); 78 | $select->where('a.entity_type_id = ?', $entityTypeId); 79 | 80 | if ($ids) { 81 | $select->where('p.entity_id IN (?)', $ids); 82 | } 83 | 84 | $select->limitPage($page, $pageSize); 85 | 86 | $entities = $connection->fetchAll($select); 87 | $documents = []; 88 | 89 | foreach ($entities as $entity) { 90 | $documents[$entity['entity_id']] = [ 91 | 'product_name' => $entity['name'], 92 | 'product_sku' => $entity['sku'], 93 | 'product_type' => __(ucfirst($entity['type_id'])), 94 | ]; 95 | } 96 | 97 | return $documents; 98 | } 99 | 100 | /** 101 | * @inheritDoc 102 | */ 103 | protected function getDocumentsCount(array $ids): int 104 | { 105 | $connection = $this->resource->getConnection(); 106 | $select = $connection->select(); 107 | 108 | $select->from( 109 | ['p' => $this->resource->getTableName('catalog_product_entity')], 110 | ['count' => new \Zend_Db_Expr('COUNT(*)')] 111 | ); 112 | 113 | if ($ids) { 114 | $select->where('p.entity_id IN (?)', $ids); 115 | } 116 | 117 | return (int) $connection->fetchOne($select); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Model/IndexerConfig.php: -------------------------------------------------------------------------------- 1 | config[$indexId] ?? null; 34 | 35 | if (!$config) { 36 | throw new \InvalidArgumentException(sprintf('Indexer "%s" not found in configuration', $indexId)); 37 | } 38 | 39 | if ($path) { 40 | $config = $this->arrayManager->get("{$indexId}/$path", $this->config, $default); 41 | } 42 | 43 | return $config; 44 | } 45 | 46 | /** 47 | * Return all index IDs. 48 | * 49 | * @return array 50 | */ 51 | public function list(): array 52 | { 53 | return array_keys($this->config); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Model/Search.php: -------------------------------------------------------------------------------- 1 | getAllowedIndices(); 39 | 40 | if (!$allowedIndices) { 41 | return []; 42 | } 43 | 44 | $request = [ 45 | 'index' => implode(',', $allowedIndices), 46 | 'body' => [ 47 | 'query' => [ 48 | 'bool' => [ 49 | 'should' => [ 50 | 51 | ], 52 | 'minimum_should_match' => 1, 53 | ], 54 | ], 55 | 'sort' => [ 56 | '_score' => 'desc', 57 | '_id' => 'desc', 58 | ], 59 | ], 60 | ]; 61 | 62 | foreach ($allowedIndices as $index) { 63 | $properties = $this->indexerConfig->get($index, 'properties'); 64 | 65 | $indexedProperties = array_filter( 66 | $properties, 67 | fn($property) => $property['index'] ?? true 68 | ); 69 | 70 | foreach ($indexedProperties as $property => $propertyConfig) { 71 | $request['body']['query']['bool']['should'][] = [ 72 | 'match_bool_prefix' => [ 73 | $property => [ 74 | 'query' => $query, 75 | 'boost' => 2, 76 | ], 77 | ], 78 | ]; 79 | 80 | $request['body']['query']['bool']['should'][] =[ 81 | 'match' => [ 82 | $property => [ 83 | 'query' => $query, 84 | 'boost' => 4, 85 | 'operator' => 'or', 86 | ], 87 | ], 88 | ]; 89 | 90 | $request['body']['query']['bool']['should'][] =[ 91 | 'match' => [ 92 | $property => [ 93 | 'query' => $query, 94 | 'boost' => 6, 95 | 'operator' => 'and', 96 | ], 97 | ], 98 | ]; 99 | 100 | $request['body']['query']['bool']['should'][] = [ 101 | 'match_phrase_prefix' => [ 102 | $property => [ 103 | 'query' => $query, 104 | 'boost' => 8, 105 | ], 106 | ], 107 | ]; 108 | 109 | $request['body']['query']['bool']['should'][] = [ 110 | 'match_phrase' => [ 111 | $property => [ 112 | 'query' => $query, 113 | 'boost' => 10, 114 | ] 115 | ] 116 | ]; 117 | } 118 | } 119 | 120 | $result = $this->client->search($request); 121 | $hits = $result['hits']['hits']; 122 | 123 | return array_map(function ($hit) { 124 | $type = $this->indexerConfig->get($hit['_index'], 'type'); 125 | $url = $this->indexerConfig->get($hit['_index'], 'url'); 126 | 127 | $hit['_source']['_type'] = $type; 128 | $hit['_source']['_url'] = $this->url->getUrl($url['path'], [$url['param'] => $hit['_id']]); 129 | 130 | return $hit['_source']; 131 | }, $hits); 132 | } 133 | 134 | /** 135 | * Get allowed indices for search. 136 | * 137 | * @return array 138 | */ 139 | public function getAllowedIndices(): array 140 | { 141 | $allowedIndices = []; 142 | 143 | if (!$this->session->isAllowed('Magento_Backend::global_search')) { 144 | return $allowedIndices; 145 | } 146 | 147 | foreach ($this->indexerConfig->list() as $indexId) { 148 | $resource = $this->indexerConfig->get($indexId, 'resource'); 149 | 150 | if ($this->session->isAllowed($resource)) { 151 | $allowedIndices[] = $indexId; 152 | } 153 | } 154 | 155 | return $allowedIndices; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /Observer/Reindex.php: -------------------------------------------------------------------------------- 1 | indexerRegistry->get($this->indexerId); 36 | 37 | if ($indexer->isScheduled()) { 38 | return; 39 | } 40 | 41 | $object = $observer->getDataObject(); 42 | $hasChanges = false; 43 | 44 | foreach ($this->fields as $field) { 45 | if ($object->dataHasChangedFor($field)) { 46 | $hasChanges = true; 47 | break; 48 | } 49 | } 50 | 51 | if ($hasChanges || $object->isDeleted()) { 52 | $indexer->reindexRow($object->getId()); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Magento 2 Admin Search Module 2 | 3 | ![Magento](https://img.shields.io/badge/magento-2.4.x-blue) 4 | ![Version](https://img.shields.io/badge/version-1.2.3-blue) 5 | ![License](https://img.shields.io/badge/license-MIT-green) 6 | 7 | ## Overview 🚀 8 | 9 | The Magento 2 Admin Search Module enhances the global search functionality in the admin panel by enabling fast 10 | and efficient searches through the main entities. It addresses the limitations of the default Magento global search, 11 | offering a more noticeable and performant solution. 12 | 13 | ![Show Case](docs/assets/showcase.gif) 14 | 15 | ## Benefits 🎉 16 | 17 | - ⚡ **Performance**: The module leverages ElasticSearch/OpenSearch for search operations, ensuring fast and efficient results even with large datasets. 18 | - 👀 **Visibility**: Unlike the default search, the enhanced search is more noticeable and user-friendly, streamlining admin workflows. 19 | - 🔒 **Access Control**: The module respects Magento ACL permissions, ensuring users only see results they are authorized to access, maintaining security and compliance. 20 | 21 | ## Searchable Entities 🔍 22 | 23 | - Orders 24 | - Customers 25 | - Products 26 | - Categories 27 | - CMS Pages 28 | - CMS Blocks 29 | - Catalog Price Rules 30 | - Cart Price Rules 31 | 32 | ## Installation 🔧 33 | 34 | ```shell 35 | composer require vasileuski/magento2-module-admin-search 36 | ``` 37 | 38 | ## Configuration ⚙️ 39 | 40 | The module settings can be found in the Magento admin panel under the following path: 41 | **Stores → Configuration → Advanced → Admin → Admin Search** 42 | 43 | Available Options: 44 | - **Sticky Header**: Customers can toggle the sticky header feature. 45 | 46 | ## Troubleshooting 🛠️ 47 | 48 | If no results are displayed in the search bar, execute the following CLI command to ensure proper indexing: 49 | 50 | ```shell 51 | bin/magento indexer:reindex admin_search_blocks admin_search_catalog_price_rules admin_search_cart_price_rules admin_search_categories admin_search_customers admin_search_orders admin_search_pages admin_search_products 52 | ``` 53 | 54 | ## License 📄 55 | 56 | This module is open-source and licensed under the [MIT License](LICENSE.txt). 57 | 58 | ## Support and Contact 💬 59 | 60 | For any issues, feature requests, or contributions, please open an issue on the GitHub repository. 61 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vasileuski/magento2-module-admin-search", 3 | "type": "magento2-module", 4 | "version": "1.2.3", 5 | "description": "Magento 2 module that enhances the admin panel's global search by enabling fast and efficient searches through main entities with improved visibility and ACL support.", 6 | "keywords": ["magento", "magento2", "admin", "search"], 7 | "minimum-stability": "stable", 8 | "license": "MIT", 9 | "authors": [ 10 | { 11 | "name": "Dzmitry Vasileuski", 12 | "email": "vasileuski_dzmitry@outlook.com" 13 | } 14 | ], 15 | "require": { 16 | "php": "~8.1.0||~8.2.0||~8.3.0", 17 | "ext-intl": "*", 18 | "magento/module-catalog": "104.0.*", 19 | "magento/module-catalog-rule": "101.2.*", 20 | "magento/module-cms": "104.0.*", 21 | "magento/module-customer": "103.0.*", 22 | "magento/module-elasticsearch": "101.0.*", 23 | "magento/module-sales": "103.0.*", 24 | "magento/module-sales-rule": "101.2.*", 25 | "magento/module-store": "101.1.*", 26 | "magento/framework": "103.0.*" 27 | }, 28 | "autoload": { 29 | "files": [ 30 | "registration.php" 31 | ], 32 | "psr-4": { 33 | "Vasileuski\\AdminSearch\\": "" 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /docs/assets/showcase.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dzmitry-vasileuski/magento2-module-admin-search/b16d226de47fce074a1977bec9ae3bbc92e84bf9/docs/assets/showcase.gif -------------------------------------------------------------------------------- /etc/adminhtml/routes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /etc/adminhtml/system.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 | 9 | 10 | Magento\Config\Model\Config\Source\Yesno 11 | 12 | 13 |
14 |
15 |
16 | -------------------------------------------------------------------------------- /etc/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 1 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /etc/di.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Magento_Cms::block 10 | block 11 | 12 | cms/block/edit 13 | block_id 14 | 15 | 16 | 17 | text 18 | whitespace 19 | 20 | 21 | text 22 | 23 | 24 | text 25 | false 26 | 27 | 28 | 29 | 30 | Magento_SalesRule::quote 31 | cart-price-rule 32 | 33 | sales_rule/promo_quote/edit 34 | id 35 | 36 | 37 | 38 | text 39 | whitespace 40 | 41 | 42 | text 43 | false 44 | 45 | 46 | text 47 | false 48 | 49 | 50 | integer 51 | false 52 | 53 | 54 | 55 | 56 | Magento_CatalogRule::promo_catalog 57 | catalog-price-rule 58 | 59 | catalog_rule/promo_catalog/edit 60 | id 61 | 62 | 63 | 64 | text 65 | whitespace 66 | 67 | 68 | text 69 | false 70 | 71 | 72 | text 73 | false 74 | 75 | 76 | integer 77 | false 78 | 79 | 80 | 81 | 82 | Magento_Catalog::categories 83 | category 84 | 85 | catalog/category/edit 86 | id 87 | 88 | 89 | 90 | text 91 | whitespace 92 | 93 | 94 | text 95 | false 96 | 97 | 98 | 99 | 100 | Magento_Customer::manage 101 | customer 102 | 103 | customer/index/edit 104 | id 105 | 106 | 107 | 108 | text 109 | whitespace 110 | 111 | 112 | text 113 | whitespace 114 | 115 | 116 | text 117 | 118 | 119 | 120 | 121 | Magento_Sales::actions_view 122 | order 123 | 124 | sales/order/view 125 | order_id 126 | 127 | 128 | 129 | text 130 | whitespace 131 | 132 | 133 | text 134 | whitespace 135 | 136 | 137 | text 138 | whitespace 139 | 140 | 141 | text 142 | false 143 | 144 | 145 | text 146 | false 147 | 148 | 149 | 150 | 151 | Magento_Cms::page 152 | page 153 | 154 | cms/page/edit 155 | page_id 156 | 157 | 158 | 159 | text 160 | whitespace 161 | 162 | 163 | text 164 | 165 | 166 | text 167 | false 168 | 169 | 170 | 171 | 172 | Magento_Catalog::products 173 | product 174 | 175 | catalog/product/edit 176 | id 177 | 178 | 179 | 180 | text 181 | whitespace 182 | 183 | 184 | text 185 | 186 | 187 | text 188 | false 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | Vasileuski\AdminSearch\Model\Indexer\Block::INDEXER_ID 199 | 200 | title 201 | identifier 202 | is_active 203 | 204 | 205 | 206 | 207 | 208 | 209 | Vasileuski\AdminSearch\Model\Indexer\CatalogPriceRule::INDEXER_ID 210 | 211 | name 212 | from_date 213 | to_date 214 | is_active 215 | 216 | 217 | 218 | 219 | 220 | 221 | Vasileuski\AdminSearch\Model\Indexer\CartPriceRule::INDEXER_ID 222 | 223 | name 224 | from_date 225 | to_date 226 | is_active 227 | 228 | 229 | 230 | 231 | 232 | 233 | Vasileuski\AdminSearch\Model\Indexer\Category::INDEXER_ID 234 | 235 | name 236 | path 237 | 238 | 239 | 240 | 241 | 242 | 243 | Vasileuski\AdminSearch\Model\Indexer\Customer::INDEXER_ID 244 | 245 | firstname 246 | lastname 247 | email 248 | 249 | 250 | 251 | 252 | 253 | 254 | Vasileuski\AdminSearch\Model\Indexer\Order::INDEXER_ID 255 | 256 | increment_id 257 | status 258 | customer_firstname 259 | customer_lastname 260 | customer_email 261 | created_at 262 | 263 | 264 | 265 | 266 | 267 | 268 | Vasileuski\AdminSearch\Model\Indexer\Page::INDEXER_ID 269 | 270 | title 271 | identifier 272 | is_active 273 | 274 | 275 | 276 | 277 | 278 | 279 | Vasileuski\AdminSearch\Model\Indexer\Product::INDEXER_ID 280 | 281 | name 282 | sku 283 | 284 | 285 | 286 | 287 | -------------------------------------------------------------------------------- /etc/events.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /etc/indexer.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Admin Search Blocks 5 | 6 | 7 | Admin Search Catalog Price Rules 8 | 9 | 10 | Admin Search Cart Price Rules 11 | 12 | 13 | Admin Search Categories 14 | 15 | 16 | Admin Search Customers 17 | 18 | 19 | Admin Search Orders 20 | 21 | 22 | Admin Search Pages 23 | 24 | 25 | Admin Search Products 26 | 27 | 28 | -------------------------------------------------------------------------------- /etc/module.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /etc/mview.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 |
21 |
22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 |
32 | 33 | 34 | 35 | 36 |
37 | 38 | 39 | 40 | 41 |
42 |
43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /registration.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /view/adminhtml/layout/default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vasileuski\AdminSearch\Model\Config 8 | Vasileuski\AdminSearch\Model\Search 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | user 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /view/adminhtml/layout/popup.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /view/adminhtml/layout/searchbar_removed.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /view/adminhtml/templates/searchbar.phtml: -------------------------------------------------------------------------------- 1 | getConfig(); 17 | /** @var Search $search */ 18 | $search = $block->getSearch(); 19 | ?> 20 | 21 | 49 | 50 | getAllowedIndices()): ?> 51 | 67 | 68 | -------------------------------------------------------------------------------- /view/adminhtml/web/css/source/_module.less: -------------------------------------------------------------------------------- 1 | // 2 | // Variables 3 | // --------------------------------------------- 4 | 5 | @header__background-color: @menu__background-color; 6 | @header__color: @color-white; 7 | @header__z-index: @menu__z-index - 1; 8 | 9 | @search__width: 400px; 10 | 11 | // 12 | // Common 13 | // _____________________________________________ 14 | 15 | & when (@media-common = true) { 16 | .searchbar { 17 | align-items: center; 18 | background: @header__background-color; 19 | color: @header__color; 20 | display: grid; 21 | gap: 2rem; 22 | grid-template-columns: 1fr @search__width 1fr; 23 | height: 6rem; 24 | justify-content: center; 25 | padding: 1rem 1rem 1rem 9.8rem; 26 | width: 100%; 27 | z-index: @header__z-index; 28 | 29 | &._sticky { 30 | left: 0; 31 | position: fixed; 32 | top: 0; 33 | } 34 | 35 | &__right { 36 | align-items: center; 37 | display: flex; 38 | justify-content: flex-end; 39 | 40 | .admin__action-dropdown { 41 | background: @header__background-color; 42 | border: none !important; 43 | box-shadow: none !important; 44 | color: @header__color; 45 | } 46 | 47 | .admin__action-dropdown:after, 48 | .admin__action-dropdown:before { 49 | color: @header__color !important; 50 | } 51 | 52 | .admin-user .admin__action-dropdown:after { 53 | border-color: @header__color transparent transparent transparent !important; 54 | } 55 | 56 | .admin__action-dropdown-menu { 57 | border: none !important; 58 | z-index: 2; 59 | } 60 | } 61 | } 62 | 63 | .search { 64 | color: @color-black; 65 | position: relative; 66 | 67 | svg { 68 | display: block; 69 | max-width: 100%; 70 | } 71 | 72 | &__icon { 73 | left: .751rem; 74 | position: absolute; 75 | top: 50%; 76 | transform: translateY(-50%); 77 | transition: opacity .2s ease-in-out; 78 | width: 16px; 79 | } 80 | 81 | &.loading { 82 | .search { 83 | &__icon { 84 | opacity: .5; 85 | } 86 | } 87 | } 88 | 89 | &__input { 90 | border: none; 91 | border-radius: .5rem; 92 | color: @color-black; 93 | min-width: @search__width; 94 | padding: .5rem 6.5rem .5rem ~'calc(1.5rem + 16px)'; 95 | } 96 | 97 | &__hotkey { 98 | background: @header__background-color; 99 | border-radius: .275rem; 100 | color: @header__color; 101 | font-size: 1rem; 102 | height: 2rem; 103 | line-height: 1; 104 | padding: .5rem; 105 | position: absolute; 106 | right: .425rem; 107 | top: 50%; 108 | transform: translateY(-50%); 109 | } 110 | 111 | &__results { 112 | background: @color-white; 113 | border-radius: 5px; 114 | bottom: -.5rem; 115 | box-shadow: 0 4px 8px rgba(0, 0, 0, .1), 0 2px 4px rgba(0, 0, 0, .06); 116 | color: @color-black; 117 | left: 0; 118 | padding: 1rem; 119 | position: absolute; 120 | transform: translateY(100%); 121 | width: 100%; 122 | z-index: @z-index-7; 123 | 124 | &__empty { 125 | padding: 1rem; 126 | } 127 | } 128 | 129 | &__hits { 130 | margin: 0; 131 | } 132 | 133 | &__hit { 134 | align-items: center; 135 | border-radius: .5rem; 136 | cursor: pointer; 137 | display: flex; 138 | gap: 1rem; 139 | overflow: hidden; 140 | padding: .75rem 1rem; 141 | 142 | &:focus, 143 | &:hover { 144 | background: @color-lighter-gray; 145 | box-shadow: none; 146 | } 147 | 148 | &__icon { 149 | display: flex; 150 | justify-content: center; 151 | width: 18px; 152 | } 153 | 154 | &__content { 155 | max-width: ~'calc(100% - 28px)'; 156 | 157 | &__secondary { 158 | display: flex; 159 | flex-wrap: nowrap; 160 | gap: 5px; 161 | 162 | span { 163 | color: @color-very-dark-gray2; 164 | font-size: 1.2rem; 165 | text-wrap: nowrap; 166 | } 167 | } 168 | } 169 | } 170 | 171 | ._ellipsis { 172 | max-width: 100%; 173 | overflow: hidden; 174 | text-overflow: ellipsis; 175 | text-wrap: nowrap; 176 | } 177 | } 178 | 179 | .menu-wrapper .logo { 180 | height: 6rem !important; 181 | 182 | .logo-img { 183 | height: 2.75rem !important; 184 | } 185 | } 186 | 187 | body:has(.header._sticky) { 188 | .page-wrapper { 189 | margin-top: 6rem !important; 190 | } 191 | 192 | .page-main-actions .page-actions._fixed { 193 | top: 6rem !important; 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /view/adminhtml/web/js/view/search.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'jquery', 3 | 'ko', 4 | 'underscore', 5 | 'uiComponent' 6 | ], function ($, ko, _, Component) { 7 | 'use strict'; 8 | 9 | return Component.extend({ 10 | query: ko.observable(''), 11 | hits: ko.observableArray([]), 12 | isActive: ko.observable(false), 13 | isLoading: ko.observable(false), 14 | 15 | defaults: { 16 | template: 'Vasileuski_AdminSearch/search', 17 | searchUrl: '', 18 | selectors: { 19 | search: '.search', 20 | input: '.search__input', 21 | results: '.search__results', 22 | placeholder: '.search._placeholder', 23 | hit: '.search__hit', 24 | }, 25 | templates: { 26 | block: 'Vasileuski_AdminSearch/hit/block', 27 | 'cart-price-rule': 'Vasileuski_AdminSearch/hit/cart-price-rule', 28 | 'catalog-price-rule': 'Vasileuski_AdminSearch/hit/catalog-price-rule', 29 | category: 'Vasileuski_AdminSearch/hit/category', 30 | customer: 'Vasileuski_AdminSearch/hit/customer', 31 | order: 'Vasileuski_AdminSearch/hit/order', 32 | page: 'Vasileuski_AdminSearch/hit/page', 33 | product: 'Vasileuski_AdminSearch/hit/product', 34 | } 35 | }, 36 | 37 | initialize() { 38 | this._super(); 39 | this.initObservers(); 40 | }, 41 | 42 | initObservable() { 43 | this._super(); 44 | 45 | this.query.subscribe(_.debounce(this.search.bind(this), 300)); 46 | this.hits.subscribe((hits) => { 47 | if (hits) { 48 | setTimeout(() => $(this.selectors.hit).first().focus(), 100); 49 | } 50 | }); 51 | this.isActive.subscribe((isActive) => { 52 | if (!isActive) { 53 | this.query(''); 54 | $(this.selectors.input).blur(); 55 | } 56 | }); 57 | 58 | return this; 59 | }, 60 | 61 | initObservers() { 62 | $(document).on('keydown', (event) => { 63 | if ((event.ctrlKey || this.isMacOS() && event.metaKey) && event.key === 'k') { 64 | this.handleCtrlKPress(event); 65 | } else if (event.key === 'Escape') { 66 | this.handleEscapePress(event); 67 | } else if (event.key === 'Enter') { 68 | this.handleEnterPress(event); 69 | } else if (event.key === 'ArrowDown') { 70 | this.handleArrowDownPress(event); 71 | } else if (event.key === 'ArrowUp') { 72 | this.handleArrowUpPress(event); 73 | } else { 74 | if (this.isActive() && !$(event.target).is(this.selectors.input)) { 75 | $(this.selectors.input).focus(); 76 | $(this.selectors.input).trigger('keydown', event); 77 | } 78 | } 79 | }); 80 | 81 | $(document).on('click', this.handleClickOutside.bind(this)); 82 | $(document).on('click', this.selectors.hit, this.handleHitClick.bind(this)); 83 | $(document).on('focus', this.selectors.input, this.handleInputFocus.bind(this)); 84 | }, 85 | 86 | handleCtrlKPress(event) { 87 | event.preventDefault(); 88 | $(this.selectors.input).focus(); 89 | }, 90 | 91 | handleEscapePress() { 92 | this.isActive(false); 93 | }, 94 | 95 | handleEnterPress(event) { 96 | this.handleHitClick(event); 97 | }, 98 | 99 | handleArrowDownPress(event) { 100 | if (!this.isActive()) { 101 | return; 102 | } 103 | 104 | event.preventDefault(); 105 | 106 | const $activeElement = $(document.activeElement); 107 | 108 | if ($activeElement.is(this.selectors.hit)) { 109 | $activeElement.next(this.selectors.hit).focus(); 110 | } else if ($activeElement.is(this.selectors.input)) { 111 | $(this.selectors.hit).first().focus(); 112 | } 113 | }, 114 | 115 | handleArrowUpPress(event) { 116 | if (!this.isActive()) { 117 | return; 118 | } 119 | 120 | event.preventDefault(); 121 | 122 | const $activeElement = $(document.activeElement); 123 | 124 | if ($activeElement.is(this.selectors.hit)) { 125 | const $prev = $activeElement.prev(this.selectors.hit); 126 | 127 | if ($prev.length) { 128 | $prev.focus(); 129 | } else { 130 | $(this.selectors.input).focus(); 131 | } 132 | } 133 | }, 134 | 135 | handleClickOutside(event) { 136 | if (!this.isActive()) { 137 | return; 138 | } 139 | 140 | const $target = $(event.target); 141 | 142 | if ($target.closest(this.selectors.search).length === 0) { 143 | this.isActive(false); 144 | } 145 | }, 146 | 147 | handleHitClick(event) { 148 | const $target = $(event.target); 149 | let $hit; 150 | 151 | if ($target.is(this.selectors.hit)) { 152 | $hit = $target; 153 | } else if ($target.closest(this.selectors.hit)) { 154 | $hit = $target.closest(this.selectors.hit); 155 | } 156 | 157 | if (!$hit.length) { 158 | return; 159 | } 160 | 161 | window.location.href = $hit.data('href'); 162 | this.isActive(false); 163 | $('body').trigger('processStart'); 164 | }, 165 | 166 | handleInputFocus() { 167 | this.isActive(true); 168 | }, 169 | 170 | afterRender() { 171 | $(this.selectors.placeholder).remove(); 172 | }, 173 | 174 | search() { 175 | const query = this.query(); 176 | 177 | if (query.trim().length === 0) { 178 | this.hits([]); 179 | return; 180 | } 181 | 182 | this.isLoading(true); 183 | 184 | $.post(this.searchUrl, { query }, (response) => { 185 | this.hits(response); 186 | }).always(() => this.isLoading(false)); 187 | }, 188 | 189 | getHitTemplate(hit) { 190 | return this.templates[hit._type]; 191 | }, 192 | 193 | isMacOS() { 194 | return /Mac OS/.test(navigator.userAgent); 195 | }, 196 | }); 197 | }); 198 | -------------------------------------------------------------------------------- /view/adminhtml/web/template/hit/block.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | 4 | 5 | 6 | 7 |
    8 |
    9 |
    10 |
    11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
    19 |
    20 |
  • 21 | -------------------------------------------------------------------------------- /view/adminhtml/web/template/hit/cart-price-rule.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | 4 | 5 | 6 | 7 | 8 |
    9 |
    10 |
    11 |
    12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
    27 |
    28 |
  • 29 | -------------------------------------------------------------------------------- /view/adminhtml/web/template/hit/catalog-price-rule.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | 4 | 5 | 6 | 7 | 8 |
    9 |
    10 |
    11 |
    12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
    27 |
    28 |
  • 29 | -------------------------------------------------------------------------------- /view/adminhtml/web/template/hit/category.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | 4 | 5 | 6 | 7 | 8 | 9 |
    10 |
    11 |
    12 |
    13 | 14 | 15 | 16 |
    17 |
    18 |
  • 19 | -------------------------------------------------------------------------------- /view/adminhtml/web/template/hit/customer.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | 4 | 5 | 6 | 7 |
    8 |
    9 |
    10 |
    11 | 12 | 13 | 14 |
    15 |
    16 |
  • 17 | -------------------------------------------------------------------------------- /view/adminhtml/web/template/hit/order.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | 4 | 5 | 6 | 7 | 8 | 9 |
    10 |
    11 |
    12 |
    13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
    25 |
    26 |
  • 27 | -------------------------------------------------------------------------------- /view/adminhtml/web/template/hit/page.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | 4 | 5 | 6 |
    7 |
    8 |
    9 |
    10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
    18 |
    19 |
  • 20 | -------------------------------------------------------------------------------- /view/adminhtml/web/template/hit/product.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | 4 | 5 | 6 | 7 | 8 | 9 |
    10 |
    11 |
    12 |
    13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
    21 |
    22 |
  • 23 | -------------------------------------------------------------------------------- /view/adminhtml/web/template/search.html: -------------------------------------------------------------------------------- 1 | 32 | --------------------------------------------------------------------------------