diff --git a/.gitignore b/.gitignore index 4abf887f0..6319cde4d 100755 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ composer.lock vendor/ .php_cs.cache .vscode +.idea diff --git a/CHANGELOG.md b/CHANGELOG.md index afc103d4f..f28ebc83b 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # CHANGE LOG +## 3.13.3 + +### Updates +- Updated CSP whitelist +- Updated code to show current version of AlgoliaSearch extension in Magento admin +- Updated code to make compatible with PHP 7 +- Updated code and merged community submitted PRs +- Updated code for consistent auth tokens +- Updated code for Autocomplete highlights + +### Bug Fixes +- Fixed issues with Recommend items in mobile view +- Fixed issue related to decompoundedAttributes admin config error +- Fixed issue with Analytics Overview date format bug + + ## 3.13.2 ### Updates diff --git a/Helper/AlgoliaHelper.php b/Helper/AlgoliaHelper.php index f288564bd..7536e6bc6 100755 --- a/Helper/AlgoliaHelper.php +++ b/Helper/AlgoliaHelper.php @@ -289,7 +289,7 @@ public function mergeSettings($indexName, $settings, $mergeSettingsFrom = '') } catch (\Exception $e) { } - $removes = ['slaves', 'replicas']; + $removes = ['slaves', 'replicas', 'decompoundedAttributes']; if (isset($settings['attributesToIndex'])) { $settings['searchableAttributes'] = $settings['attributesToIndex']; @@ -963,4 +963,4 @@ protected function getAlgoliaFiltersArrayWithoutCurrentRefinement($filters, $nee return $filters; } -} \ No newline at end of file +} diff --git a/Helper/AnalyticsHelper.php b/Helper/AnalyticsHelper.php index a08c96ef6..5987da55e 100644 --- a/Helper/AnalyticsHelper.php +++ b/Helper/AnalyticsHelper.php @@ -6,6 +6,7 @@ use Algolia\AlgoliaSearch\Config\AnalyticsConfig; use Algolia\AlgoliaSearch\DataProvider\Analytics\IndexEntityDataProvider; use Algolia\AlgoliaSearch\RequestOptions\RequestOptionsFactory; +use Magento\Framework\Locale\ResolverInterface; class AnalyticsHelper { @@ -14,9 +15,6 @@ class AnalyticsHelper public const ANALYTICS_FILTER_PATH = '/2/filters'; public const ANALYTICS_CLICKS_PATH = '/2/clicks'; - /** @var AlgoliaHelper */ - private $algoliaHelper; - /** @var ConfigHelper */ private $configHelper; @@ -26,6 +24,12 @@ class AnalyticsHelper /** @var Logger */ private $logger; + /** @var ResolverInterface */ + private $localeResolver; + + public const DATE_FORMAT_PICKER = 'dd MMM yyyy'; + public const DATE_FORMAT_API = 'Y-m-d'; + private $searches; private $users; private $rateOfNoResults; @@ -58,25 +62,21 @@ class AnalyticsHelper protected $region; /** - * @param AlgoliaHelper $algoliaHelper * @param ConfigHelper $configHelper * @param IndexEntityDataProvider $entityHelper * @param Logger $logger - * @param string $region + * @param ResolverInterface $localeResolver */ public function __construct( - AlgoliaHelper $algoliaHelper, ConfigHelper $configHelper, IndexEntityDataProvider $entityHelper, Logger $logger, - string $region = 'us' + ResolverInterface $localeResolver ) { - $this->algoliaHelper = $algoliaHelper; $this->configHelper = $configHelper; - $this->entityHelper = $entityHelper; - $this->logger = $logger; + $this->localeResolver = $localeResolver; $this->region = $this->configHelper->getAnalyticsRegion(); } @@ -371,4 +371,17 @@ public function getErrors() { return $this->errors; } + + /** + * @param string $timezone + * @return \IntlDateFormatter + */ + public function getAnalyticsDatePickerFormatter(string $timezone): \IntlDateFormatter + { + $locale = $this->localeResolver->getLocale(); + $dateFormatter = new \IntlDateFormatter($locale, \IntlDateFormatter::NONE, \IntlDateFormatter::NONE, $timezone); + $dateFormatter->setPattern(self::DATE_FORMAT_PICKER); + return $dateFormatter; + } + } diff --git a/Helper/Configuration/NoticeHelper.php b/Helper/Configuration/NoticeHelper.php index aa154aa6a..ea9f075c9 100644 --- a/Helper/Configuration/NoticeHelper.php +++ b/Helper/Configuration/NoticeHelper.php @@ -44,7 +44,7 @@ class NoticeHelper extends \Magento\Framework\App\Helper\AbstractHelper 'getClickAnalyticsNotice', 'getPersonalizationNotice', 'getRecommendNotice', - 'getCookieConfigurationNotice', + 'getCookieConfigurationNotice', ]; /** @var array[] */ @@ -157,23 +157,33 @@ protected function getMsiNotice() ]; } - protected function getVersionNotice() + protected function getVersionNotice(): void { + $currentVersion = $this->configHelper->getExtensionVersion(); $newVersion = $this->getNewVersionNotification(); - if ($newVersion === null) { + if (!$currentVersion && !$newVersion) { return; } - $noticeTitle = 'Algolia Extension update'; - $noticeContent = 'You are using old version of Algolia extension. Latest version of the extension is v ' . $newVersion['version'] . '
+ $notice = [ + 'selector' => '.entry-edit', + 'method' => 'before' + ]; + + if ($newVersion) { + $noticeTitle = 'Algolia extension update'; + $noticeContent = 'You are using an old version of Algolia extension. Latest version of the extension is v ' . $newVersion['version'] . '
It is highly recommended to update your version to avoid any unexpected issues and to get new features.
See details on our Github repository.'; + $notice['message'] = $this->formatNotice($noticeTitle, $noticeContent); + } + else { + $noticeTitle = 'Algolia extension version'; + $noticeContent = "You are using version $currentVersion of the Algolia Magento integration."; + $notice['message'] = $this->formatNotice($noticeTitle, $noticeContent, 'icon-bulb'); + } - $this->notices[] = [ - 'selector' => '.entry-edit', - 'method' => 'before', - 'message' => $this->formatNotice($noticeTitle, $noticeContent), - ]; + $this->notices[] = $notice; } protected function getClickAnalyticsNotice() @@ -208,7 +218,7 @@ protected function getClickAnalyticsNotice() 'message' => $noticeContent, ]; } - + protected function getCookieConfigurationNotice() { $noticeContent = ''; diff --git a/Helper/Entity/Product/PriceManager/ProductWithoutChildren.php b/Helper/Entity/Product/PriceManager/ProductWithoutChildren.php index 4ea16b285..fd4379f5a 100755 --- a/Helper/Entity/Product/PriceManager/ProductWithoutChildren.php +++ b/Helper/Entity/Product/PriceManager/ProductWithoutChildren.php @@ -92,7 +92,7 @@ public function __construct( Rule $rule, ProductFactory $productloader, ScopedProductTierPriceManagementInterface $productTierPrice, - Logger $logger, + Logger $logger ) { $this->configHelper = $configHelper; $this->customerGroupCollectionFactory = $customerGroupCollectionFactory; diff --git a/Helper/InsightsHelper.php b/Helper/InsightsHelper.php index 7eda60be5..830f7aeaf 100644 --- a/Helper/InsightsHelper.php +++ b/Helper/InsightsHelper.php @@ -17,6 +17,13 @@ class InsightsHelper public const ALGOLIA_CUSTOMER_USER_TOKEN_COOKIE_NAME = 'aa-search'; + /** + * Up to 129 chars per https://www.algolia.com/doc/rest-api/insights/#method-param-usertoken + * But capping at legacy 64 chars for backward compat + */ + /** @var int */ + public const ALGOLIA_USER_TOKEN_MAX_LENGTH = 64; + /** @var ConfigHelper */ private $configHelper; @@ -25,7 +32,7 @@ class InsightsHelper /** @var SessionManagerInterface */ private $sessionManager; - + /** @var CookieManagerInterface */ private $cookieManager; diff --git a/README.md b/README.md index db9d6dd18..625037641 100755 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ Algolia Search & Discovery extension for Magento 2 ================================================== -![Latest version](https://img.shields.io/badge/latest-3.13.2-green) +![Latest version](https://img.shields.io/badge/latest-3.13.3-green) ![Magento 2](https://img.shields.io/badge/Magento-2.4.x-orange) ![PHP](https://img.shields.io/badge/PHP-8.2%2C8.1%2C7.4-blue) diff --git a/ViewModel/Adminhtml/Analytics/Overview.php b/ViewModel/Adminhtml/Analytics/Overview.php index 103b40e55..f03b0168e 100644 --- a/ViewModel/Adminhtml/Analytics/Overview.php +++ b/ViewModel/Adminhtml/Analytics/Overview.php @@ -7,6 +7,7 @@ use Algolia\AlgoliaSearch\ViewModel\Adminhtml\BackendView; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Locale\ResolverInterface; use Magento\Store\Api\Data\StoreInterface; class Overview implements \Magento\Framework\View\Element\Block\ArgumentInterface @@ -26,24 +27,30 @@ class Overview implements \Magento\Framework\View\Element\Block\ArgumentInterfac /** @var IndexEntityDataProvider */ private $indexEntityDataProvider; + /** @var ResolverInterface */ + private $localeResolver; + /** @var array */ private $analyticsParams = []; /** - * Index constructor. + * Index constructor. * * @param BackendView $backendView * @param AnalyticsHelper $analyticsHelper * @param IndexEntityDataProvider $indexEntityDataProvider + * @param ResolverInterface $localeResolver */ public function __construct( BackendView $backendView, AnalyticsHelper $analyticsHelper, - IndexEntityDataProvider $indexEntityDataProvider + IndexEntityDataProvider $indexEntityDataProvider, + ResolverInterface $localeResolver ) { $this->backendView = $backendView; $this->analyticsHelper = $analyticsHelper; $this->indexEntityDataProvider = $indexEntityDataProvider; + $this->localeResolver = $localeResolver; } /** @@ -54,9 +61,13 @@ public function getBackendView() return $this->backendView; } - public function getTimeZone() + /** + * @return string + * @throws NoSuchEntityException + */ + public function getTimeZone(): string { - return $this->backendView->getDateTime()->getConfigTimezone( + return (string) $this->backendView->getDateTime()->getConfigTimezone( \Magento\Store\Model\ScopeInterface::SCOPE_STORE, $this->getStore()->getId() ); @@ -81,18 +92,16 @@ public function getIndexName() * * @return array */ - public function getAnalyticsParams($additional = []) + public function getAnalyticsParams(array $additional = []): array { if (empty($this->analyticsParams)) { $params = ['index' => $this->getIndexName()]; if ($formData = $this->getBackendView()->getBackendSession()->getAlgoliaAnalyticsFormData()) { - $dateTime = $this->getBackendView()->getDateTime(); - $timeZone = $this->getTimeZone(); - if (isset($formData['from']) && $formData['from'] !== '') { - $params['startDate'] = $dateTime->date($formData['from'], $timeZone, true, false)->format('Y-m-d'); + if (!empty($formData['from'])) { + $params['startDate'] = $this->formatFormSubmittedDate($formData['from']); } - if (isset($formData['to']) && $formData['to'] !== '') { - $params['endDate'] = $dateTime->date($formData['to'], $timeZone, true, false)->format('Y-m-d'); + if (!empty($formData['to'])) { + $params['endDate'] = $this->formatFormSubmittedDate($formData['to']); } } @@ -102,6 +111,39 @@ public function getAnalyticsParams($additional = []) return array_merge($this->analyticsParams, $additional); } + /** + * @param string $dateString + * @return string + * @throws NoSuchEntityException + */ + protected function formatFormSubmittedDate(string $dateString): string + { + $timezone = $this->getTimeZone(); + $dateTime = $this->parseFormSubmittedDate($dateString, $timezone); + return $dateTime->format(AnalyticsHelper::DATE_FORMAT_API); + } + + /** + * @param string|null $dateString + * @param string|null $timezone + * @return \DateTime + * @throws NoSuchEntityException + */ + protected function parseFormSubmittedDate(string $dateString = null, string $timezone = null): \DateTime + { + if (empty($timezone)) { + $timezone = $this->getTimeZone(); + } + + if (empty($dateString)) { + return new \DateTime('now', new \DateTimeZone($timezone)); + } + + $dateFormatter = $this->analyticsHelper->getAnalyticsDatePickerFormatter($timezone); + $parsedDate = $dateFormatter->parse($dateString); + return (new \DateTime('now', new \DateTimeZone($timezone)))->setTimestamp($parsedDate); + } + public function getTotalCountOfSearches() { return $this->analyticsHelper->getTotalCountOfSearches($this->getAnalyticsParams()); @@ -306,14 +348,15 @@ public function getNoResultSearches() * * @return bool */ - public function checkIsValidDateRange() + public function checkIsValidDateRange(): bool { if ($formData = $this->getBackendView()->getBackendSession()->getAlgoliaAnalyticsFormData()) { - if (isset($formData['from']) && !empty($formData['from'])) { - $dateTime = $this->getBackendView()->getDateTime(); - $timeZone = $this->getTimeZone(); - $startDate = $dateTime->date($formData['from'], $timeZone, true, false); - $diff = date_diff($startDate, $dateTime->date(null, $timeZone, true, null)); + if (!empty($formData['from'])) { + $timezone = $this->getTimeZone(); + + $startDate = $this->parseFormSubmittedDate($formData['from'], $timezone); + $now = $this->parseFormSubmittedDate(null, $timezone); + $diff = date_diff($startDate, $now); if ($diff->days > $this->getAnalyticRetentionDays()) { return false; diff --git a/composer.json b/composer.json index bc18202ab..87cb10d46 100755 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "description": "Algolia Search & Discovery extension for Magento 2", "type": "magento2-module", "license": ["MIT"], - "version": "3.13.2", + "version": "3.13.3", "require": { "magento/framework": "~102.0|~103.0", "algolia/algoliasearch-client-php": "3.3.2", diff --git a/etc/csp_whitelist.xml b/etc/csp_whitelist.xml index c533d4a43..ac29534de 100644 --- a/etc/csp_whitelist.xml +++ b/etc/csp_whitelist.xml @@ -13,6 +13,7 @@ *.algolia.com *.algolianet.com *.insights.algolia.io + insights.algolia.io diff --git a/etc/module.xml b/etc/module.xml index 78806a7f3..b515de603 100755 --- a/etc/module.xml +++ b/etc/module.xml @@ -1,6 +1,6 @@ - + diff --git a/view/adminhtml/web/js/config.js b/view/adminhtml/web/js/config.js index 11ff9d794..d9c7af323 100644 --- a/view/adminhtml/web/js/config.js +++ b/view/adminhtml/web/js/config.js @@ -38,7 +38,7 @@ require( pageWarning += ''; var pageWarningSynonyms = '
'; - pageWarningSynonyms += '

Configurations related to Synonyms have been deprecated from the Magento dashboard. We advise you to configure synonyms from the Algolia dashboard.

'; + pageWarningSynonyms += '

Configurations related to Synonyms have been removed from the Magento dashboard. We advise you to configure synonyms from the Algolia dashboard.

'; pageWarningSynonyms += '
'; for (var i=0; i < pageIds.length; i++) { diff --git a/view/frontend/templates/recommend/cart/recommend_items.phtml b/view/frontend/templates/recommend/cart/recommend_items.phtml index b2f3ea9cc..ba4c90a0d 100644 --- a/view/frontend/templates/recommend/cart/recommend_items.phtml +++ b/view/frontend/templates/recommend/cart/recommend_items.phtml @@ -2,7 +2,7 @@ /** @var \Algolia\AlgoliaSearch\ViewModel\Recommend\Cart $block */ $viewModel = $block->getViewModel(); $recommendConfig = $viewModel->getAlgoliaRecommendConfiguration(); -if (isset($recommendConfig['enabledRelatedInCart']) || isset($recommendConfig['enabledFBTInCart']) || isset($recommendConfig['isTrendItemsEnabledInCartPage'])): +if (!empty($recommendConfig['enabledRelatedInCart']) || !empty($recommendConfig['enabledFBTInCart']) || !empty($recommendConfig['isTrendItemsEnabledInCartPage'])): $cartItems = $viewModel->getAllCartItems(); ?>
diff --git a/view/frontend/templates/recommend/products.phtml b/view/frontend/templates/recommend/products.phtml index bb5705bdb..b5c7d97fc 100644 --- a/view/frontend/templates/recommend/products.phtml +++ b/view/frontend/templates/recommend/products.phtml @@ -3,7 +3,7 @@ $viewModel = $block->getViewModel(); $recommendConfig = $viewModel->getAlgoliaRecommendConfiguration(); -if (isset($recommendConfig['enabledFBT']) || isset($recommendConfig['enabledRelated']) || isset($recommendConfig['isTrendItemsEnabledInPDP'])): +if (!empty($recommendConfig['enabledFBT']) || !empty($recommendConfig['enabledRelated']) || !empty($recommendConfig['isTrendItemsEnabledInPDP'])): $product = $viewModel->getProduct(); ?>
@@ -24,4 +24,4 @@ if (isset($recommendConfig['enabledFBT']) || isset($recommendConfig['enabledRela } } - \ No newline at end of file + diff --git a/view/frontend/web/insights.js b/view/frontend/web/insights.js index decc580ea..e2de304ae 100755 --- a/view/frontend/web/insights.js +++ b/view/frontend/web/insights.js @@ -16,6 +16,11 @@ define( hasAddedParameters: false, useCookie: false, + // Although events can accept both auth and anon tokens, queries can only accept a single token + determineUserToken() { + return algoliaAnalytics.getAuthenticatedUserToken() ?? algoliaAnalytics.getUserToken(); + }, + track: function (algoliaConfig, partial = false) { this.config = algoliaConfig; this.defaultIndexName = algoliaConfig.indexName + '_products'; @@ -68,34 +73,34 @@ define( } }, + applyInsightsToSearchParams(params = {}) { + if (algoliaConfig.ccAnalytics.enabled) { + params.clickAnalytics = true; + } + + if (algoliaConfig.personalization.enabled) { + params.enablePersonalization = true; + params.userToken = this.determineUserToken(); + } + + return params; + }, + addSearchParameters: function () { if (this.hasAddedParameters) { return; } - algolia.registerHook('beforeWidgetInitialization', function (allWidgetConfiguration) { - allWidgetConfiguration.configure = allWidgetConfiguration.configure || {}; - if (algoliaConfig.ccAnalytics.enabled) { - allWidgetConfiguration.configure.clickAnalytics = true; - } - - if (algoliaConfig.personalization.enabled) { - allWidgetConfiguration.configure.enablePersonalization = true; - allWidgetConfiguration.configure.userToken = algoliaAnalytics.getUserToken(); - } + algolia.registerHook('beforeWidgetInitialization', (allWidgetConfiguration) => { + allWidgetConfiguration.configure = algoliaInsights.applyInsightsToSearchParams( + allWidgetConfiguration.configure + ); return allWidgetConfiguration; }); - algolia.registerHook('afterAutocompleteProductSourceOptions', function (options) { - if (algoliaConfig.ccAnalytics.enabled) { - options.clickAnalytics = true; - } - if (algoliaConfig.personalization.enabled) { - options.enablePersonalization = true; - options.userToken = algoliaAnalytics.getUserToken(); - } - return options; + algolia.registerHook('afterAutocompleteProductSourceOptions', (options) => { + return algoliaInsights.applyInsightsToSearchParams(options); }); this.hasAddedParameters = true; diff --git a/view/frontend/web/internals/recommend.css b/view/frontend/web/internals/recommend.css index 34a5072ab..815246bf9 100644 --- a/view/frontend/web/internals/recommend.css +++ b/view/frontend/web/internals/recommend.css @@ -1,4 +1,4 @@ -.product-img { +.recommend-item .product-img { width: 180px; } .auc-Recommend-list { @@ -6,7 +6,7 @@ justify-content: space-evenly; list-style: none; } -.product-name { +.recommend-item .product-name { height: 50px; width: 110px; margin: 0 auto; @@ -50,23 +50,23 @@ flex-wrap: wrap; justify-content: flex-start; } -.product-details .action.primary, .action-primary{ +.product-details .recommend-item .action.primary, .action-primary{ background: #f4f4f4; border: 1px solid #f4f4f4; color: #666666; } -.product-details .action.primary:hover, .action-primary:hover { +.product-details .recommend-item .action.primary:hover, .action-primary:hover { border-color: #1979c3; background: #1979c3; color: #FFFFFF; } @media (min-width: 768px) and (max-width: 1023px) { - #relatedProducts li, #frequentlyBoughtTogether li { + #relatedProducts li, #frequentlyBoughtTogether li, #trendItems li { width: 33.33333333%; } } @media (max-width: 767px) { - #relatedProducts li, #frequentlyBoughtTogether li { + #relatedProducts li, #frequentlyBoughtTogether li, #trendItems li { width: 50%; } } diff --git a/view/frontend/web/internals/template/autocomplete/categories.js b/view/frontend/web/internals/template/autocomplete/categories.js index 3cc9bde97..a10fb663b 100644 --- a/view/frontend/web/internals/template/autocomplete/categories.js +++ b/view/frontend/web/internals/template/autocomplete/categories.js @@ -15,12 +15,24 @@ define([], function () { data-position="${item.position}" data-index="${item.__autocomplete_indexName}" data-queryId="${item.__autocomplete_queryID}"> - ${components.Highlight({ hit: item, attribute: 'path' })} (${item.product_count}) + ${this.safeHighlight(components, item, "path")} (${item.product_count}) `; }, getFooterHtml: function () { return ""; }, + + safeHighlight: function(components, hit, attribute) { + const highlightResult = hit._highlightResult[attribute]; + + if (!highlightResult) return ''; + + try { + return components.Highlight({ hit, attribute }); + } catch (e) { + return ''; + } + } }; }); diff --git a/view/frontend/web/internals/template/autocomplete/pages.js b/view/frontend/web/internals/template/autocomplete/pages.js index ca2de4eed..eba9400ea 100644 --- a/view/frontend/web/internals/template/autocomplete/pages.js +++ b/view/frontend/web/internals/template/autocomplete/pages.js @@ -16,9 +16,9 @@ define([], function () { data-index="${item.__autocomplete_indexName}" data-queryId="${item.__autocomplete_queryID}">
- ${components.Highlight({hit: item, attribute: 'name'})} + ${this.safeHighlight(components, item, "name")}
- ${components.Highlight({hit: item, attribute: 'content'})} + ${this.safeHighlight(components, item, "content")}
@@ -27,6 +27,19 @@ define([], function () { getFooterHtml: function () { return ""; + }, + + safeHighlight: function(components, hit, attribute) { + const highlightResult = hit._highlightResult[attribute]; + + if (!highlightResult) return ''; + + try { + return components.Highlight({ hit, attribute }); + } catch (e) { + return ''; + } } + }; }); diff --git a/view/frontend/web/internals/template/autocomplete/products.js b/view/frontend/web/internals/template/autocomplete/products.js index e1f79caf6..31a47d295 100644 --- a/view/frontend/web/internals/template/autocomplete/products.js +++ b/view/frontend/web/internals/template/autocomplete/products.js @@ -22,7 +22,7 @@ define([], function () { data-queryId="${item.__autocomplete_queryID}">
${item.name || ''}
- ${components.Highlight({hit: item, attribute: 'name'})} + ${this.safeHighlight(components, item, "name")}
${this.getColorHtml(item, components, html)} ${this.getCategoriesHtml(item, components, html)} @@ -43,16 +43,20 @@ define([], function () { // Helper methods // //////////////////// - getColorHtml: (item, components, html) => { - if (item._highlightResult.color == undefined || item._highlightResult.color.value == "") return ""; - - return html`color: ${components.Highlight({ hit: item, attribute: "color" })}`; + getColorHtml: function(item, components, html) { + const highlight = this.safeHighlight(components, item, "color"); + + return highlight + ? html`color: ${highlight}` + : ""; }, - getCategoriesHtml: (item, components, html) => { - if (item.categories_without_path == undefined || item.categories_without_path.length == 0) return ""; + getCategoriesHtml: function(item, components, html) { + const highlight = this.safeHighlight(components, item, "categories_without_path", false); - return html`in ${components.Highlight({ hit: item, attribute: "categories_without_path",})}`; + return highlight + ? html`in ${highlight}` + : ""; }, getOriginalPriceHtml: (item, html, priceGroup) => { @@ -98,6 +102,30 @@ define([], function () { return html`${algoliaConfig.translations.seeIn} ${algoliaConfig.translations.allDepartments} (${resultDetails.nbHits}) ${this.getFooterSearchCategoryLinks(html, resultDetails)} `; + }, + + // TODO: Refactor to external lib + safeHighlight: function(components, hit, attribute, strict = true) { + const highlightResult = hit._highlightResult[attribute]; + + if (!highlightResult) return ''; + + if (strict + && + ( + (Array.isArray(highlightResult)) && !highlightResult.find(hit => hit.matchLevel !== 'none') + || + highlightResult.value === '' + ) + ) { + return ''; + } + + try { + return components.Highlight({ hit, attribute }); + } catch (e) { + return ''; + } } };