├── .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 | 
4 | 
5 | 
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 | 
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 |
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 |
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 |
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 |
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 |
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 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/view/adminhtml/web/template/hit/customer.html:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/view/adminhtml/web/template/hit/order.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | •
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/view/adminhtml/web/template/hit/product.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | •
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/view/adminhtml/web/template/search.html:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
⌘ + K
11 |
12 |
13 |
CTRL + K
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------