From 37359e15b20fce5756fd048b35d677b3ec9e4d5a Mon Sep 17 00:00:00 2001 From: Nathan McBride Date: Tue, 28 Feb 2023 21:32:01 +1100 Subject: [PATCH 01/13] Add TypeSenses' JS Adapter to Algolia instantsearch --- Observer/AddConfigurationToAlgoliaBundle.php | 55 +++++++++++++++++++ .../Helper/AlgoliaHelperPlugin.php | 4 +- Services/ConfigService.php | 8 +-- etc/di.xml | 5 +- etc/frontend/events.xml | 6 ++ view/frontend/layout/default.xml | 7 +++ view/frontend/web/js/magento-adapter.js | 36 ++++++++++++ view/frontend/web/js/typesense-adapter.js | 2 + 8 files changed, 116 insertions(+), 7 deletions(-) create mode 100644 Observer/AddConfigurationToAlgoliaBundle.php create mode 100644 etc/frontend/events.xml create mode 100644 view/frontend/layout/default.xml create mode 100644 view/frontend/web/js/magento-adapter.js create mode 100644 view/frontend/web/js/typesense-adapter.js diff --git a/Observer/AddConfigurationToAlgoliaBundle.php b/Observer/AddConfigurationToAlgoliaBundle.php new file mode 100644 index 0000000..61fa6c5 --- /dev/null +++ b/Observer/AddConfigurationToAlgoliaBundle.php @@ -0,0 +1,55 @@ +configService = $configService; + } + + public function execute(Observer $observer) + { + $configuration = $observer->getData('configuration'); + + if (!$configuration instanceof DataObject) { + return; + } + + $typesenseConfig = [ + 'isEnabled' => $this->configService->isEnabled(), + 'config' => [ + 'apiKey' => $this->configService->getSearchOnlyKey(), + 'nodes' => [ + [ + 'host' => $this->configService->getNodes(), + 'path' => '', // @todo add this + 'port' => $this->configService->getPort(), + 'protocol' => $this->configService->getProtocol() + ] + ], + 'cacheSearchResultsForSeconds' => '2 * 60' + ] + + ]; + + $configuration->setData('typesense', $typesenseConfig); + } +} diff --git a/Plugin/Backend/Algolia/AlgoliaSearch/Helper/AlgoliaHelperPlugin.php b/Plugin/Backend/Algolia/AlgoliaSearch/Helper/AlgoliaHelperPlugin.php index a30bb4f..42a0749 100644 --- a/Plugin/Backend/Algolia/AlgoliaSearch/Helper/AlgoliaHelperPlugin.php +++ b/Plugin/Backend/Algolia/AlgoliaSearch/Helper/AlgoliaHelperPlugin.php @@ -17,12 +17,12 @@ class AlgoliaHelperPlugin /** * @var ConfigService */ - protected ConfigService $configService; + protected $configService; /** * @var Client $typesenseClient */ - protected Client $typesenseClient; + protected $typesenseClient; /** * @param ConfigService $configService diff --git a/Services/ConfigService.php b/Services/ConfigService.php index cf8bbe9..83ec085 100644 --- a/Services/ConfigService.php +++ b/Services/ConfigService.php @@ -24,7 +24,7 @@ class ConfigService /** * @var ScopeConfigInterface $scopeConfig */ - protected ScopeConfigInterface $scopeConfig; + protected $scopeConfig; /** * @param EncryptorInterface $encryptor @@ -60,7 +60,8 @@ public function getCloudId(): ?string */ public function getApiKey(): ?string { - $value = $this->scopeConfig->getValue(SELF::TYPESENSE_API_KEY, ScopeConfig::SCOPE_STORE); + $value = $this->scopeConfig->getValue(self::TYPESENSE_API_KEY, ScopeConfig::SCOPE_STORE); + return $this->encryptor->decrypt($value); } @@ -69,8 +70,7 @@ public function getApiKey(): ?string */ public function getSearchOnlyKey(): ?string { - $value = $this->scopeConfig->getValue(SELF::TYPESENSE_SEARCH_ONLY_KEY_KEY, ScopeConfig::SCOPE_STORE); - return $this->encryptor->decrypt($value); + return $this->scopeConfig->getValue(self::TYPESENSE_SEARCH_ONLY_KEY_KEY, ScopeConfig::SCOPE_STORE); } /** diff --git a/etc/di.xml b/etc/di.xml index 12efa73..81dc653 100644 --- a/etc/di.xml +++ b/etc/di.xml @@ -2,6 +2,9 @@ - + diff --git a/etc/frontend/events.xml b/etc/frontend/events.xml new file mode 100644 index 0000000..c1c6136 --- /dev/null +++ b/etc/frontend/events.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/view/frontend/layout/default.xml b/view/frontend/layout/default.xml new file mode 100644 index 0000000..91f0b20 --- /dev/null +++ b/view/frontend/layout/default.xml @@ -0,0 +1,7 @@ + + + + +
+
+ getChildHtml('autocomplete'); ?> +
+
+ + From ae9d0a2511fbe1d0a97fc514c631df90cda9dd72 Mon Sep 17 00:00:00 2001 From: Nathan McBride Date: Sun, 26 Mar 2023 12:40:41 +1100 Subject: [PATCH 04/13] Make module compatible with Hyva --- Observer/AddConfigurationToAlgoliaBundle.php | 2 +- Services/ConfigService.php | 9 ++++ ViewModel/Form.php | 47 ++++++++++++++++++++ etc/adminhtml/system.xml | 6 ++- etc/module.xml | 3 +- view/frontend/layout/default.xml | 2 +- view/frontend/layout/hyva_algolia.xml | 14 ++++++ view/frontend/templates/algolia-form.phtml | 3 +- view/frontend/web/js/hyva-adapter.js | 37 +++++++++++++++ view/frontend/web/js/magento-adapter.js | 6 ++- 10 files changed, 122 insertions(+), 7 deletions(-) create mode 100644 ViewModel/Form.php create mode 100644 view/frontend/layout/hyva_algolia.xml create mode 100644 view/frontend/web/js/hyva-adapter.js diff --git a/Observer/AddConfigurationToAlgoliaBundle.php b/Observer/AddConfigurationToAlgoliaBundle.php index 61fa6c5..ded7097 100644 --- a/Observer/AddConfigurationToAlgoliaBundle.php +++ b/Observer/AddConfigurationToAlgoliaBundle.php @@ -40,7 +40,7 @@ public function execute(Observer $observer) 'nodes' => [ [ 'host' => $this->configService->getNodes(), - 'path' => '', // @todo add this + 'path' => $this->configService->getPath() ?? '', 'port' => $this->configService->getPort(), 'protocol' => $this->configService->getProtocol() ] diff --git a/Services/ConfigService.php b/Services/ConfigService.php index fbdbb6a..bf5e58f 100644 --- a/Services/ConfigService.php +++ b/Services/ConfigService.php @@ -17,6 +17,7 @@ class ConfigService private const TYPESENSE_API_KEY = 'typesense_general/settings/admin_api_key'; private const TYPESENSE_SEARCH_ONLY_KEY_KEY = 'typesense_general/settings/search_only_key'; private const TYPESENSE_NODES = 'typesense_general/settings/nodes'; + private const TYPESENSE_PATH = 'typesense_general/settings/path'; private const TYPESENSE_PORT = 'typesense_general/settings/port'; private const TYPESENSE_PROTOCOL = 'typesense_general/settings/protocol'; private const TYPESENSE_INDEX_METHOD = 'typesense_general/settings/index_method'; @@ -81,6 +82,14 @@ public function getNodes(): ?string return $this->scopeConfig->getValue(self::TYPESENSE_NODES, ScopeConfig::SCOPE_STORE); } + /** + * @return string|null + */ + public function getPath(): ?string + { + return $this->scopeConfig->getValue(self::TYPESENSE_PATH, ScopeConfig::SCOPE_STORE); + } + /** * @return string|null */ diff --git a/ViewModel/Form.php b/ViewModel/Form.php new file mode 100644 index 0000000..1864ded --- /dev/null +++ b/ViewModel/Form.php @@ -0,0 +1,47 @@ +json = $json; + } + + public function getInstantsearchScripts() + { + return $this->json->serialize( + [ + $this->getAssetUrl('Hyva_AlgoliaSearch::js/internals/algoliaBundle.min.js'), + $this->getAssetUrl('Hyva_AlgoliaSearch::js/internals/common.js'), + $this->getAssetUrl('Develo_Typesense::js/typesense-adapter.js'), + $this->getAssetUrl('Develo_Typesense::js/hyva-adapter.js'), + $this->getAssetUrl('Hyva_AlgoliaSearch::js/internals/instantsearch.js') + ] + ); + } +} diff --git a/etc/adminhtml/system.xml b/etc/adminhtml/system.xml index 3a32151..abf51c1 100644 --- a/etc/adminhtml/system.xml +++ b/etc/adminhtml/system.xml @@ -34,7 +34,11 @@ - + + + + + diff --git a/etc/module.xml b/etc/module.xml index e9aa6f0..6a33384 100644 --- a/etc/module.xml +++ b/etc/module.xml @@ -2,8 +2,9 @@ + - \ No newline at end of file + diff --git a/view/frontend/layout/default.xml b/view/frontend/layout/default.xml index 91f0b20..ad6a857 100644 --- a/view/frontend/layout/default.xml +++ b/view/frontend/layout/default.xml @@ -3,5 +3,5 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> diff --git a/view/frontend/web/js/autocomplete-adapter.js b/view/frontend/web/js/autocomplete-adapter.js deleted file mode 100644 index e69de29..0000000 diff --git a/view/frontend/web/js/hyva-adapter.js b/view/frontend/web/js/hyva-adapter.js index 9cbc2db..388e4a1 100644 --- a/view/frontend/web/js/hyva-adapter.js +++ b/view/frontend/web/js/hyva-adapter.js @@ -14,22 +14,25 @@ const initHyvaAdapter = () => { return; } - var typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter({ + var query_by = 'name,categories'; + if (typeof algoliaConfig.typesense_searchable !== 'undefined') { + query_by = algoliaConfig.typesense_searchable.products; + } + + window.typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter({ server: algoliaConfig.typesense.config, additionalSearchParameters: { - query_by: 'name,categories' + query_by } }); - var searchClient = typesenseInstantsearchAdapter.searchClient; - algolia.registerHook('beforeInstantsearchInit', function (instantsearchOptions) { - searchClient.addAlgoliaAgent = function () { + window.typesenseInstantsearchAdapter.searchClient.addAlgoliaAgent = function () { // do nothing, function is required. } - instantsearchOptions.searchClient = searchClient; + instantsearchOptions.searchClient = window.typesenseInstantsearchAdapter.searchClient; return instantsearchOptions; }); diff --git a/view/frontend/web/js/internals/autocompleteConfig.js b/view/frontend/web/js/internals/autocompleteConfig.js index d9fbc15..a90c0ac 100644 --- a/view/frontend/web/js/internals/autocompleteConfig.js +++ b/view/frontend/web/js/internals/autocompleteConfig.js @@ -1,380 +1,192 @@ -const { autocomplete, getAlgoliaResults, getAlgoliaFacets } = window['@algolia/autocomplete-js']; -const { applicationId, apiKey, indexName, resultURL } = algoliaConfig; -const { createQuerySuggestionsPlugin } = window['@algolia/autocomplete-plugin-query-suggestions']; +const initAutoComplete = () => { + algoliaBundle.$(function ($) { -const searchClient = algoliasearch(applicationId, apiKey); + const {autocomplete} = window['@algolia/autocomplete-js']; + const {resultURL} = algoliaConfig; -if (algoliaConfig.autocomplete.nbOfProductsSuggestions > 0) { - algoliaConfig.autocomplete.sections.unshift({ hitsPerPage: algoliaConfig.autocomplete.nbOfProductsSuggestions, label: algoliaConfig.translations.products, name: "products"}); -} + if (algoliaConfig.autocomplete.nbOfProductsSuggestions > 0) { + algoliaConfig.autocomplete.sections.unshift({ + hitsPerPage: algoliaConfig.autocomplete.nbOfProductsSuggestions, + label: algoliaConfig.translations.products, + name: "products" + }); + } -if (algoliaConfig.autocomplete.nbOfCategoriesSuggestions > 0) { - algoliaConfig.autocomplete.sections.unshift({ hitsPerPage: algoliaConfig.autocomplete.nbOfCategoriesSuggestions, label: algoliaConfig.translations.categories, name: "categories"}); -} + if (algoliaConfig.autocomplete.nbOfCategoriesSuggestions > 0) { + algoliaConfig.autocomplete.sections.unshift({ + hitsPerPage: algoliaConfig.autocomplete.nbOfCategoriesSuggestions, + label: algoliaConfig.translations.categories, + name: "categories" + }); + } -if (algoliaConfig.autocomplete.nbOfQueriesSuggestions > 0) { - algoliaConfig.autocomplete.sections.unshift({ hitsPerPage: algoliaConfig.autocomplete.nbOfQueriesSuggestions, label: '', name: "suggestions"}); -} + if (algoliaConfig.autocomplete.nbOfQueriesSuggestions > 0) { + algoliaConfig.autocomplete.sections.unshift({ + hitsPerPage: algoliaConfig.autocomplete.nbOfQueriesSuggestions, + label: '', + name: "suggestions" + }); + } -// taken from common.js (autocomplete v0) and adopted to autocomplete v1 -const getAutocompleteSource = function ({ section, setContext }) { - if (section.hitsPerPage <= 0) - return null; - - var options = { - hitsPerPage: section.hitsPerPage, - analyticsTags: 'autocomplete', - clickAnalytics: true - }; - - var source; - - if (section.name === "products") { - options.numericFilters = 'visibility_search=1'; - options.ruleContexts = ['magento_filters', '']; // Empty context to keep BC for already create rules in dashboard - - source = Object.assign({ - source: searchClient.initIndex(algoliaConfig.indexName + "_" + section.name), - }, options, { - name: section.name, - transformResponse({ results, hits }) { - setContext({ - nbProducts: results[0].nbHits, - }); - - return hits; - }, - templates: { - footer({ state, html }) { - const categoryLinks = [] - const endcodedQuery = encodeURIComponent(state.query) - - const firstCategories = state.context.productsFacetHits - .sort((a, b) => b.count - a.count) - .slice(0, 2) - - if (firstCategories) { - for (var i = 0; i { - const key = facetHit.label - return { - key, - url: algoliaConfig.baseUrl + '/catalogsearch/result/?q=' + endcodedQuery + '#q=' + endcodedQuery + '&hFR[categories.level0][0]=' + encodeURIComponent(key) + '&idx=' + algoliaConfig.indexName + '_products' - } - }) - ) - } - } - } + algoliaConfig.autocomplete.templates = { + products: algoliaBundle.Hogan.compile($('#autocomplete_products_template').html()), + categories: algoliaBundle.Hogan.compile($('#autocomplete_categories_template').html()), + pages: algoliaBundle.Hogan.compile($('#autocomplete_pages_template').html()) + }; - var allUrl = algoliaConfig.baseUrl + '/catalogsearch/result/?q=' + endcodedQuery; - - return html`` - }, - header({ html }) { - return html`${algoliaConfig.translations.products}`; - }, - item({ item, components, html }) { - return html` - -
- ${item.name} -
-
-
- ${components.Highlight({ - hit: item, - attribute: 'name', - tagName: 'em' - })} -
-
${item.categories_without_path[0]}
-
- ${item.price[algoliaConfig.currencyCode].default_formated} - ${item.price[algoliaConfig.currencyCode].default_original_formated ? item.price[algoliaConfig.currencyCode].default_original_formated:""} -
-
-
` - } + const getQueryBy = function (name) { + if ( + typeof algoliaConfig.typesense_searchable !== 'undefined' && + typeof algoliaConfig.typesense_searchable[name] !== 'undefined' + ) { + return algoliaConfig.typesense_searchable[name]; } - }); - } - else if (section.name === "categories" || section.name === "pages") - { - if (section.name === "categories" && algoliaConfig.showCatsNotIncludedInNavigation === false) { - options.numericFilters = 'include_in_menu=1'; - } - let templates + return 'name' + } - if (section.name === 'categories') { - templates = { - header({ items, html }) { - if (items.length === 0) { - return null; +// taken from common.js (autocomplete v0) and adopted to autocomplete v1 + const getAutocompleteSource = function ({section, setContext}) { + if (section.hitsPerPage <= 0) + return null; + + var options = { + hitsPerPage: section.hitsPerPage, + analyticsTags: 'autocomplete', + clickAnalytics: true + }; + + var source = {}; + + var templates = {}; + + switch (section.name) { + case 'products': + options.numericFilters = 'visibility_search=1'; + options.ruleContexts = ['magento_filters', '']; // Empty context to keep BC for already create rules in dashboard + break; + case 'categories': + if (algoliaConfig.showCatsNotIncludedInNavigation === false) { + options.numericFilters = 'include_in_menu=1'; } - return html`${algoliaConfig.translations.categories}`; - }, - item({ item, components, html }) { - return html` - - - ${components.Highlight({ - hit: item, - attribute: 'path', - tagName: 'em' - })} - - ` - } + break; } - } else { + templates = { - item({ item, components, html }) { - return html` - - ${components.Highlight({ - hit: item, - attribute: 'name', - tagName: 'em' - })} - - - ${components.Highlight({ - hit: item, - attribute: 'content', - tagName: 'em' - })} - - ` + header({html}) { + return html`

${section.label}

`; + }, + item({item, html}) { + const innerHtml = algoliaConfig.autocomplete.templates[section.name].render(item); + + return html`
` } } - } - source = { - ...options, - source: searchClient.initIndex(algoliaConfig.indexName + "_" + section.name), - name: section.name, - templates + source = { + ...options, + indexName: algoliaConfig.indexName + "_" + section.name, + name: section.name, + templates + }; + + return source; }; - } - else if (section.name === "suggestions") - { - /// popular queries/suggestions - // todo adopt to autocomplete v1 - var suggestions_index = searchClient.initIndex(algoliaConfig.indexName + "_suggestions"); - var products_index = searchClient.initIndex(algoliaConfig.indexName + "_products"); - - source = { - source: 'query', - source: products_index, - facets: ['categories.level0'], - hitsPerPage: 0, - typoTolerance: false, - maxValuesPerFacet: 1, - analytics: false, - displayKey: 'query', - name: section.name, - templates: { - suggestion: function (hit, payload) { - if (hit.facet) { - hit.category = hit.facet.value; - } - if (hit.facet && hit.facet.value !== algoliaConfig.translations.allDepartments) { - hit.url = algoliaConfig.baseUrl + '/catalogsearch/result/?q=' + hit.query + '#q=' + hit.query + '&hFR[categories.level0][0]=' + encodeURIComponent(hit.category) + '&idx=' + algoliaConfig.indexName + '_products'; - } else { - hit.url = algoliaConfig.baseUrl + '/catalogsearch/result/?q=' + hit.query; - } + const plugins = [] + + if (window.initExtraAlgoliaConfiguration) { + const {plugins: extraPlugins} = initExtraAlgoliaConfiguration(algoliaConfig) + plugins.push(...extraPlugins) + } - var toEscape = hit._highlightResult.query.value; - hit._highlightResult.query.value = algoliaBundle.autocomplete.escapeHighlightedString(toEscape); - hit.__indexName = algoliaConfig.indexName + "_" + section.name; - hit.__queryID = payload.queryID; - hit.__position = payload.hits.indexOf(hit) + 1; + autocomplete({ + container: '#algolia-autocomplete-container', + placeholder: algoliaConfig.placeholder, + debug: algoliaConfig.autocomplete.isDebugEnabled, + plugins, + detachedMediaQuery: 'none', + onSubmit: (params) => { + window.location.href = `${resultURL}?q=${params.state.query}` + }, + classNames: { + list: 'w-full flex flex-wrap', + item: 'w-full lg:w-1/2 flex-grow p-2 hover:bg-gray-200', + sourceHeader: 'px-2 py-4 uppercase tracking-widest text-blue-500', + source: 'flex flex-col', + panel: 'mx-4 absolute w-full bg-white z-50 border border-gray-300', + input: 'w-full p-2 text-base lg:text-lg leading-7 tracking-wider border border-gray-300', + form: 'w-full relative flex items-center', + inputWrapper: 'flex-grow px-4', + inputWrapperPrefix: 'hidden', + inputWrapperSuffix: 'hidden', + label: 'm-0 leading-none', + submitButton: 'leading-none' + }, + async getSources({query, setContext}) { + /** Setup autocomplete data sources **/ + var sources = []; + for (let i = 0; i < algoliaConfig.autocomplete.sections.length; i++) { - return algoliaConfig.autocomplete.templates.suggestions.render(hit); - } - } - }; - } else { - /** If is not products, categories, pages or suggestions, it's additional section **/ - var index = searchClient.initIndex(algoliaConfig.indexName + "_section_" + section.name); - - source = { - source: index, - displayKey: 'value', - ...options, - name: section.name, - templates: { - item: function (hit, payload) { - console.warn(`Missing renderer for ${section.name}, add one inside autocompleteConfig.js`) - } - } - }; - } + let section = algoliaConfig.autocomplete.sections[i]; - if (section.name !== 'suggestions' && section.name !== 'products') { - source.templates.header = ({ html }) => html`
${section.label ? section.label : section.name}
`; - } + var source = getAutocompleteSource({section, setContext}); - return source; -}; + // autocomplete v1 adapter + if (source) { -const plugins = [] - -if (window.initExtraAlgoliaConfiguration) { - const { plugins: extraPlugins } = initExtraAlgoliaConfiguration(algoliaConfig) - plugins.push(...extraPlugins) -} - - -autocomplete({ - container:'#algolia-autocomplete-container', - placeholder: algoliaConfig.placeholder, - debug: algoliaConfig.autocomplete.isDebugEnabled, - plugins, - detachedMediaQuery: 'none', - onSubmit: (params)=>{ - window.location.href =`${resultURL}?q=${params.state.query}` - }, - classNames:{ - list:'w-full flex flex-wrap', - item:'w-full lg:w-1/2 flex-grow p-2 hover:bg-gray-200', - sourceHeader:'px-2 py-4 uppercase tracking-widest text-blue-500', - source:'flex flex-col', - panel:'mx-4 absolute w-full bg-white z-50 border border-gray-300', - input:'w-full p-2 text-base lg:text-lg leading-7 tracking-wider border border-gray-300', - form:'w-full relative flex items-center', - inputWrapper:'flex-grow px-4', - inputWrapperPrefix:'hidden', - inputWrapperSuffix:'hidden', - label:'m-0 leading-none', - submitButton:'leading-none' - }, - getSources({ query, setContext }) { - /** Setup autocomplete data sources **/ - var sources = []; - algoliaConfig.autocomplete.sections.forEach(function (section) { - var source = getAutocompleteSource({ section, setContext }); - - // autocomplete v1 adapter - if (source) { - sources.push({ - sourceId: source.name, - query, - getItems() { - const params = { - hitsPerPage: source.hitsPerPage - } - if (source.numericFilters) { - params.numericFilters = source.numericFilters - } - if (source.ruleContexts) { - params.ruleContexts = source.ruleContexts - } - if (source.clickAnalytics) { - params.clickAnalytics = source.clickAnalytics - } - const resultsConfig = { - searchClient, - queries: [ - { - indexName: source.source.indexName, - query, - params - }, - ], - } - - if (source.transformResponse) { - resultsConfig.transformResponse = source.transformResponse; - } - return getAlgoliaResults(resultsConfig); - }, - templates: source.templates - }); - } - }); + let typesenseClient = new Typesense.Client(algoliaConfig.typesense.config) + + const results = await typesenseClient.collections(source.indexName).documents().search({ + q: query, + query_by: getQueryBy(source.name), + per_page: source.hitsPerPage + }) - sources.push({ - sourceId: 'productCategories', - getItems() { - return getAlgoliaFacets({ - searchClient, - transformResponse({ facetHits }) { - setContext({ productsFacetHits: facetHits[0] }); - return [] - }, - queries: [ - { - indexName: algoliaConfig.indexName + "_products", - facet: 'categories.level0', + sources.push({ + sourceId: source.name, query, - params: { - maxFacetHits: 10, - } - }, - ] - }); + getItems() { + return results.hits.map(hit => ( + hit.document + )); + }, + templates: source.templates + }); + } + } + + return sources; }, - templates: { - item: () => {} - } - }) - - return sources; - }, - render({ elements, render, html }, root) { - const { categories, pages, products, querySuggestionsPlugin } = elements; - render( - html`
- ${querySuggestionsPlugin} + + render({elements, render, html}, root) { + const {categories, pages, products} = elements; + + render( + html`
${products}
${categories} ${pages}
`, - root - ); - }, - renderNoResults({ state, render,html }, root) { - const suggestions = []; - - if (algoliaConfig.showSuggestionsOnNoResultsPage && algoliaConfig.popularQueries.length > 0) { - algoliaConfig.popularQueries - .slice(0, Math.min(3, algoliaConfig.popularQueries.length)) - .forEach(function (query) { - suggestions.push({ url: algoliaConfig.baseUrl + '/catalogsearch/result/?q=' + encodeURIComponent(query), query }); - }); - } + root + ); + }, - render(html` + renderNoResults({state, render, html}, root) { + const suggestions = []; + + if (algoliaConfig.showSuggestionsOnNoResultsPage && algoliaConfig.popularQueries.length > 0) { + algoliaConfig.popularQueries + .slice(0, Math.min(3, algoliaConfig.popularQueries.length)) + .forEach(function (query) { + suggestions.push({ + url: algoliaConfig.baseUrl + '/catalogsearch/result/?q=' + encodeURIComponent(query), + query + }); + }); + } + + render(html`
${algoliaConfig.translations.noProducts} @@ -382,14 +194,16 @@ autocomplete({
${(algoliaConfig.showSuggestionsOnNoResultsPage && suggestions.length > 0 ? - html`
+ html`
${algoliaConfig.translations.popularQueries} - ${suggestions.map(({ url, query}) => html`${query}`)} + ${suggestions.map(({url, query}) => html`${query}`)}
` : '') - } + } ${algoliaConfig.translations.seeAll}
`, root); - }, - } -); + }, + } + ); + }) +}; diff --git a/view/frontend/web/js/magento-adapter.js b/view/frontend/web/js/magento-adapter.js index 8cb63e7..801ebff 100644 --- a/view/frontend/web/js/magento-adapter.js +++ b/view/frontend/web/js/magento-adapter.js @@ -16,22 +16,25 @@ requirejs([ return; } - var typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter({ + var query_by = 'name,categories'; + if (typeof algoliaConfig.typesense_searchable !== 'undefined') { + query_by = algoliaConfig.typesense_searchable.products; + } + + window.typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter({ server: algoliaConfig.typesense.config, additionalSearchParameters: { - query_by: 'name,categories' + query_by } }); - var searchClient = typesenseInstantsearchAdapter.searchClient; - algolia.registerHook('beforeInstantsearchInit', function (instantsearchOptions) { - searchClient.addAlgoliaAgent = function () { + window.typesenseInstantsearchAdapter.searchClient.addAlgoliaAgent = function () { // do nothing, function is required. } - instantsearchOptions.searchClient = searchClient; + instantsearchOptions.searchClient = window.typesenseInstantsearchAdapter.searchClient; return instantsearchOptions; }) From 44d7584e06f75cd7dbb396d27e5a0a91b5ca3625 Mon Sep 17 00:00:00 2001 From: Nathan McBride Date: Tue, 4 Apr 2023 18:55:02 +1000 Subject: [PATCH 10/13] Allow user to sort attributes in Typesense --- Helper/ConfigChangeHelper.php | 10 ++++- Observer/AddConfigurationToAlgoliaBundle.php | 18 ++++++++- Observer/AddDefaultPriceObserver.php | 42 ++++++++++++++++++++ 3 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 Observer/AddDefaultPriceObserver.php diff --git a/Helper/ConfigChangeHelper.php b/Helper/ConfigChangeHelper.php index a5805aa..633e501 100644 --- a/Helper/ConfigChangeHelper.php +++ b/Helper/ConfigChangeHelper.php @@ -28,6 +28,8 @@ class ConfigChangeHelper self::INDEX_PAGES ]; + const SORTABLE_ATTRIBUTES = ['float', 'int64', 'string']; + /** * @var RequestInterface */ @@ -236,6 +238,12 @@ public function getFields(array $facets, array $sortingAttributes, string $index 'type' => 'object' ]; + $fields[] = [ + 'name' => 'price_default', + 'type' => 'float', + 'sort' => true + ]; + continue; } @@ -255,7 +263,7 @@ public function getFields(array $facets, array $sortingAttributes, string $index 'type' => $backendTypes[$attribute->getBackendType()], 'facet' => in_array($attribute->getAttributeCode(), $facets), 'sort' => in_array($attribute->getAttributeCode(), $sortingAttributes) && - in_array($backendTypes[$attribute->getBackendType()], ['float', 'int64']), + in_array($backendTypes[$attribute->getBackendType()], self::SORTABLE_ATTRIBUTES), ]; } diff --git a/Observer/AddConfigurationToAlgoliaBundle.php b/Observer/AddConfigurationToAlgoliaBundle.php index ed7ecd4..7e40524 100644 --- a/Observer/AddConfigurationToAlgoliaBundle.php +++ b/Observer/AddConfigurationToAlgoliaBundle.php @@ -36,7 +36,7 @@ public function __construct(ConfigService $configService, ConfigChangeHelper $co /** * @param Observer $observer * - * @event develo_typesense_add_additional_config + * @event algolia_after_create_configuration */ public function execute(Observer $observer) { @@ -62,6 +62,22 @@ public function execute(Observer $observer) ] ]; + $items = []; + $indexName = $configuration->getData('indexName').'_products'; + foreach ($configuration->getData('sortingIndices') as &$sorting) { + + if ($sorting['attribute'] === 'price') { + $sorting['attribute'] = 'price_default'; + } + + $items[] = [ + 'label' => $sorting['label'], + 'name' => sprintf('%s/sort/%s:%s', $indexName, $sorting['attribute'], $sorting['sort']) + ]; + } + + $configuration->setData('sortingIndices', $items); + $configuration->setData('typesense', $typesenseConfig); $configuration->setData('typesense_searchable', [ 'products' => $this->configChangeHelper->getSearchableAttributes(), diff --git a/Observer/AddDefaultPriceObserver.php b/Observer/AddDefaultPriceObserver.php new file mode 100644 index 0000000..694a42c --- /dev/null +++ b/Observer/AddDefaultPriceObserver.php @@ -0,0 +1,42 @@ +getData('custom_data'); + + if (!$transport instanceof DataObject) { + return; + } + + $price = $transport->getData('price'); + + if (!count($price)) { + return; + } + + $defaultPrice = array_values($price)[0]; + + if (!isset($defaultPrice['default'])) { + return; + } + + $transport->setData('price_default', $defaultPrice['default']); + } +} From 3d055ca703f40b45037c1ac215bcdc2174c6b809 Mon Sep 17 00:00:00 2001 From: Nathan McBride Date: Thu, 6 Apr 2023 20:16:31 +1000 Subject: [PATCH 11/13] Make compatible with the newest version of Algolia --- etc/events.xml | 6 ++ .../frontend/layout/algolia_search_handle.xml | 10 +++ .../templates/autocomplete/category.phtml | 31 ++++++++++ .../templates/autocomplete/product.phtml | 61 +++++++++++++++++++ 4 files changed, 108 insertions(+) create mode 100644 etc/events.xml create mode 100644 view/frontend/layout/algolia_search_handle.xml create mode 100644 view/frontend/templates/autocomplete/category.phtml create mode 100644 view/frontend/templates/autocomplete/product.phtml diff --git a/etc/events.xml b/etc/events.xml new file mode 100644 index 0000000..e88a234 --- /dev/null +++ b/etc/events.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/view/frontend/layout/algolia_search_handle.xml b/view/frontend/layout/algolia_search_handle.xml new file mode 100644 index 0000000..325a510 --- /dev/null +++ b/view/frontend/layout/algolia_search_handle.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/view/frontend/templates/autocomplete/category.phtml b/view/frontend/templates/autocomplete/category.phtml new file mode 100644 index 0000000..c24149e --- /dev/null +++ b/view/frontend/templates/autocomplete/category.phtml @@ -0,0 +1,31 @@ + + diff --git a/view/frontend/templates/autocomplete/product.phtml b/view/frontend/templates/autocomplete/product.phtml new file mode 100644 index 0000000..a25b6a8 --- /dev/null +++ b/view/frontend/templates/autocomplete/product.phtml @@ -0,0 +1,61 @@ +getPriceKey(); + +$origFormatedVar = 'price' . $priceKey . '_original_formated'; +$tierFormatedVar = 'price' . $priceKey . '_tier_formated' + +?> + + + From c26b6ab3486aa6ee6ac99f3cc4d5f373c0d2e96f Mon Sep 17 00:00:00 2001 From: Nathan McBride Date: Thu, 6 Apr 2023 20:21:43 +1000 Subject: [PATCH 12/13] always initiate Algolia as its used for autocomplete --- view/frontend/templates/algolia-form.phtml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/view/frontend/templates/algolia-form.phtml b/view/frontend/templates/algolia-form.phtml index 257ccb5..4599716 100644 --- a/view/frontend/templates/algolia-form.phtml +++ b/view/frontend/templates/algolia-form.phtml @@ -59,6 +59,7 @@ if ($config->isDefaultSelector() && ($config->isAutoCompleteEnabled() || $config initAutoComplete(); }); } + function initInstantSearch() { loadScripts(instantsearchScripts).then(() => { algoliaCommon() @@ -75,7 +76,7 @@ if ($config->isDefaultSelector() && ($config->isAutoCompleteEnabled() || $config } } - if (algoliaConfig.instant.enabled && algoliaConfig.isSearchPage) { + if (algoliaConfig.instant.enabled) { initInstantSearch() } From de5882d12257cb62dd663ba3d2b33c87fa2e39ae Mon Sep 17 00:00:00 2001 From: Luke Collymore Date: Wed, 12 Apr 2023 09:45:32 +0000 Subject: [PATCH 13/13] frontend fixes for Hyva --- Observer/RegisterModuleForHyvaConfig.php | 34 +++++++++++++++++++ etc/frontend/events.xml | 3 ++ view/frontend/tailwind/tailwind.config.js | 7 ++++ .../templates/autocomplete/product.phtml | 20 ++++++----- .../web/js/internals/autocompleteConfig.js | 8 ++--- 5 files changed, 59 insertions(+), 13 deletions(-) create mode 100644 Observer/RegisterModuleForHyvaConfig.php create mode 100644 view/frontend/tailwind/tailwind.config.js diff --git a/Observer/RegisterModuleForHyvaConfig.php b/Observer/RegisterModuleForHyvaConfig.php new file mode 100644 index 0000000..79fc17f --- /dev/null +++ b/Observer/RegisterModuleForHyvaConfig.php @@ -0,0 +1,34 @@ +componentRegistrar = $componentRegistrar; + } + + public function execute(Observer $event) + { + $config = $event->getData('config'); + $extensions = $config->hasData('extensions') ? $config->getData('extensions') : []; + + $moduleName = implode('_', array_slice(explode('\\', __CLASS__), 0, 2)); + + $path = $this->componentRegistrar->getPath(ComponentRegistrar::MODULE, $moduleName); + + // Only use the path relative to the Magento base dir + $extensions[] = ['src' => substr($path, strlen(BP) + 1)]; + + $config->setData('extensions', $extensions); + } +} \ No newline at end of file diff --git a/etc/frontend/events.xml b/etc/frontend/events.xml index c1c6136..45b968f 100644 --- a/etc/frontend/events.xml +++ b/etc/frontend/events.xml @@ -1,5 +1,8 @@ + + + diff --git a/view/frontend/tailwind/tailwind.config.js b/view/frontend/tailwind/tailwind.config.js new file mode 100644 index 0000000..7009c95 --- /dev/null +++ b/view/frontend/tailwind/tailwind.config.js @@ -0,0 +1,7 @@ +module.exports = { + purge: { + content: [ + '../templates/**/*.phtml', + ] + } + } \ No newline at end of file diff --git a/view/frontend/templates/autocomplete/product.phtml b/view/frontend/templates/autocomplete/product.phtml index a25b6a8..d503c33 100644 --- a/view/frontend/templates/autocomplete/product.phtml +++ b/view/frontend/templates/autocomplete/product.phtml @@ -10,7 +10,7 @@ $tierFormatedVar = 'price' . $priceKey . '_tier_formated'