From 4a27e5bc3924bc289945c55a10da74dfda02fedb Mon Sep 17 00:00:00 2001 From: Sarah Dayan <5370675+sarahdayan@users.noreply.github.com> Date: Mon, 25 Sep 2023 11:27:15 +0200 Subject: [PATCH] feat(create-instantsearch-app): support Autocomplete in InstantSearch.js template (#5857) Co-authored-by: Dhaya <154633+dhayab@users.noreply.github.com> --- .../src/cli/__tests__/postProcessAnswers.js | 56 +++++++++++ .../create-instantsearch-app/src/cli/index.js | 85 +++++++++++++++++ .../src/cli/postProcessAnswers.js | 3 + .../templates/InstantSearch.js/.template.js | 1 + .../templates/InstantSearch.js/index.html.hbs | 8 ++ .../{package.json => package.json.hbs} | 5 + .../templates/InstantSearch.js/src/app.js.hbs | 95 +++++++++++++++++++ .../InstantSearch.js/src/global.d.ts.hbs | 16 ++++ 8 files changed, 269 insertions(+) rename packages/create-instantsearch-app/src/templates/InstantSearch.js/{package.json => package.json.hbs} (62%) diff --git a/packages/create-instantsearch-app/src/cli/__tests__/postProcessAnswers.js b/packages/create-instantsearch-app/src/cli/__tests__/postProcessAnswers.js index 5cc171d926..d96fd2654e 100644 --- a/packages/create-instantsearch-app/src/cli/__tests__/postProcessAnswers.js +++ b/packages/create-instantsearch-app/src/cli/__tests__/postProcessAnswers.js @@ -185,4 +185,60 @@ describe('flags', () => { ).toEqual(expect.objectContaining({ insights: false })); }); }); + + describe('autocomplete', () => { + test('with usage of autocomplete in searchInputType', async () => { + expect( + await postProcessAnswers({ + configuration: {}, + templateConfig: { + libraryName: 'instantsearch.js', + flags: { + autocomplete: '>= 4.52', + }, + }, + optionsFromArguments: {}, + answers: { searchInputType: 'autocomplete' }, + }) + ).toEqual( + expect.objectContaining({ + searchInputType: 'autocomplete', + flags: expect.objectContaining({ autocomplete: true }), + }) + ); + }); + + test('without usage of autocomplete in searchInputType', async () => { + expect( + await postProcessAnswers({ + configuration: {}, + templateConfig: { + libraryName: 'instantsearch.js', + flags: { + autocomplete: '>= 4.52', + }, + }, + optionsFromArguments: {}, + answers: { searchInputType: 'searchbox' }, + }) + ).toEqual( + expect.objectContaining({ + searchInputType: 'searchbox', + flags: expect.objectContaining({ autocomplete: false }), + }) + ); + }); + + test('without config', async () => { + expect( + ( + await postProcessAnswers({ + configuration: {}, + templateConfig: {}, + optionsFromArguments: {}, + }) + ).flags + ).toEqual(expect.objectContaining({ autocomplete: false })); + }); + }); }); diff --git a/packages/create-instantsearch-app/src/cli/index.js b/packages/create-instantsearch-app/src/cli/index.js index 217fc8ecce..6d90fb80bb 100755 --- a/packages/create-instantsearch-app/src/cli/index.js +++ b/packages/create-instantsearch-app/src/cli/index.js @@ -201,6 +201,91 @@ const getQuestions = ({ appName }) => ({ when: ({ appId, apiKey, indexName }) => attributesForFaceting.length === 0 && appId && apiKey && indexName, }, + { + type: 'list', + name: 'searchInputType', + message: 'Type of search input', + choices: [ + { + name: 'Autocomplete with suggested and recent searches', + value: 'autocomplete', + }, + { name: 'Regular search box', value: 'searchbox' }, + ], + default: 'autocomplete', + when: ({ libraryVersion, template }) => { + const templatePath = getTemplatePath(template); + const templateConfig = getAppTemplateConfig(templatePath); + + const selectedLibraryVersion = libraryVersion; + const requiredLibraryVersion = + templateConfig.flags && templateConfig.flags.autocomplete; + const supportsAutocomplete = + selectedLibraryVersion && + requiredLibraryVersion && + semver.satisfies(selectedLibraryVersion, requiredLibraryVersion, { + includePrerelease: true, + }); + + return supportsAutocomplete; + }, + }, + { + type: 'input', + name: 'querySuggestionsIndexName', + message: 'Index name for suggested searches', + suffix: `\n ${chalk.gray('This must be a Query Suggestions index')}`, + default: 'instant_search_demo_query_suggestions', + when: ({ searchInputType }) => searchInputType === 'autocomplete', + }, + { + type: 'list', + name: 'autocompleteLibraryVersion', + message: () => `Autocomplete version`, + choices: async () => { + const libraryName = '@algolia/autocomplete-js'; + + try { + const versions = await fetchLibraryVersions(libraryName); + const latestStableVersion = semver.maxSatisfying(versions, '1', { + includePrerelease: false, + }); + + if (!latestStableVersion) { + return versions; + } + + return [ + new inquirer.Separator('Latest stable version (recommended)'), + latestStableVersion, + new inquirer.Separator('All versions'), + ...versions, + ]; + } catch (err) { + const fallbackLibraryVersion = '1.11.0'; + + console.log(); + console.error( + chalk.red( + `Cannot fetch versions for library "${chalk.cyan(libraryName)}".` + ) + ); + console.log(); + console.log( + `Fallback to ${chalk.cyan( + fallbackLibraryVersion + )}, please upgrade the dependency after generating the app.` + ); + console.log(); + + return [ + new inquirer.Separator('Available versions'), + fallbackLibraryVersion, + ]; + } + }, + when: ({ searchInputType }) => searchInputType === 'autocomplete', + }, ], widget: [ { diff --git a/packages/create-instantsearch-app/src/cli/postProcessAnswers.js b/packages/create-instantsearch-app/src/cli/postProcessAnswers.js index 6538774e92..a647cc89ea 100644 --- a/packages/create-instantsearch-app/src/cli/postProcessAnswers.js +++ b/packages/create-instantsearch-app/src/cli/postProcessAnswers.js @@ -77,6 +77,9 @@ async function postProcessAnswers({ insights: Boolean(templateConfig.flags && templateConfig.flags.insights) && semver.satisfies(libraryVersion, templateConfig.flags.insights), + autocomplete: + Boolean(templateConfig.flags && templateConfig.flags.autocomplete) && + combinedAnswers.searchInputType === 'autocomplete', }, }; } diff --git a/packages/create-instantsearch-app/src/templates/InstantSearch.js/.template.js b/packages/create-instantsearch-app/src/templates/InstantSearch.js/.template.js index fe49c9f0ef..ffe93cd517 100644 --- a/packages/create-instantsearch-app/src/templates/InstantSearch.js/.template.js +++ b/packages/create-instantsearch-app/src/templates/InstantSearch.js/.template.js @@ -8,6 +8,7 @@ module.exports = { flags: { dynamicWidgets: '>= 4.30', insights: '>= 4.55', + autocomplete: '>= 4.52', }, templateName: 'instantsearch.js', appName: 'instantsearch.js-app', diff --git a/packages/create-instantsearch-app/src/templates/InstantSearch.js/index.html.hbs b/packages/create-instantsearch-app/src/templates/InstantSearch.js/index.html.hbs index af255d4f8c..1469369586 100644 --- a/packages/create-instantsearch-app/src/templates/InstantSearch.js/index.html.hbs +++ b/packages/create-instantsearch-app/src/templates/InstantSearch.js/index.html.hbs @@ -9,6 +9,9 @@ + {{#if flags.autocomplete}} + + {{/if}} @@ -50,6 +53,11 @@ + {{#if flags.autocomplete}} + + + + {{/if}} diff --git a/packages/create-instantsearch-app/src/templates/InstantSearch.js/package.json b/packages/create-instantsearch-app/src/templates/InstantSearch.js/package.json.hbs similarity index 62% rename from packages/create-instantsearch-app/src/templates/InstantSearch.js/package.json rename to packages/create-instantsearch-app/src/templates/InstantSearch.js/package.json.hbs index 43ab029a19..932caa5834 100644 --- a/packages/create-instantsearch-app/src/templates/InstantSearch.js/package.json +++ b/packages/create-instantsearch-app/src/templates/InstantSearch.js/package.json.hbs @@ -14,6 +14,11 @@ "prettier": "2.6.2" }, "dependencies": { + {{#if flags.autocomplete}} + "@algolia/autocomplete-js": "{{autocompleteLibraryVersion}}", + "@algolia/autocomplete-plugin-recent-searches": "{{autocompleteLibraryVersion}}", + "@algolia/autocomplete-plugin-query-suggestions": "{{autocompleteLibraryVersion}}", + {{/if}} "algoliasearch": "4", "instantsearch.js": "{{libraryVersion}}" } diff --git a/packages/create-instantsearch-app/src/templates/InstantSearch.js/src/app.js.hbs b/packages/create-instantsearch-app/src/templates/InstantSearch.js/src/app.js.hbs index 8835ead97b..32923ef9f2 100644 --- a/packages/create-instantsearch-app/src/templates/InstantSearch.js/src/app.js.hbs +++ b/packages/create-instantsearch-app/src/templates/InstantSearch.js/src/app.js.hbs @@ -1,4 +1,13 @@ const { algoliasearch, instantsearch } = window; +{{#if flags.autocomplete}} +const { autocomplete } = window['@algolia/autocomplete-js']; +const { createLocalStorageRecentSearchesPlugin } = window[ + '@algolia/autocomplete-plugin-recent-searches' +]; +const { createQuerySuggestionsPlugin } = window[ + '@algolia/autocomplete-plugin-query-suggestions' +]; +{{/if}} const searchClient = algoliasearch('{{appId}}', '{{apiKey}}'); @@ -8,13 +17,21 @@ const search = instantsearch({ {{#if flags.insights}}insights: true,{{/if}} }); +{{#if flags.autocomplete}} +const virtualSearchBox = instantsearch.connectors.connectSearchBox(() => {}); +{{/if}} + search.addWidgets([ + {{#unless flags.autocomplete}} instantsearch.widgets.searchBox({ container: '#searchbox', {{#if searchPlaceholder}} placeholder: '{{searchPlaceholder}}', {{/if}} }), + {{else}} + virtualSearchBox({}), + {{/unless}} instantsearch.widgets.hits({ container: '#hits', {{#if attributesToDisplay}} @@ -74,3 +91,81 @@ search.addWidgets([ ]); search.start(); + +{{#if flags.autocomplete}} +const recentSearchesPlugin = createLocalStorageRecentSearchesPlugin({ + key: 'instantsearch', + limit: 3, + transformSource({ source }) { + return { + ...source, + onSelect({ setIsOpen, setQuery, item, event }) { + onSelect({ setQuery, setIsOpen, event, query: item.label }); + }, + }; + }, +}); + +const querySuggestionsPlugin = createQuerySuggestionsPlugin({ + searchClient, + indexName: '{{querySuggestionsIndexName}}', + getSearchParams() { + return recentSearchesPlugin.data.getAlgoliaSearchParams({ hitsPerPage: 6 }); + }, + transformSource({ source }) { + return { + ...source, + sourceId: 'querySuggestionsPlugin', + onSelect({ setIsOpen, setQuery, event, item }) { + onSelect({ setQuery, setIsOpen, event, query: item.query }); + }, + getItems(params) { + if (!params.state.query) { + return []; + } + + return source.getItems(params); + }, + }; + }, +}); + +autocomplete({ + container: '#searchbox', + {{#if searchPlaceholder}} + placeholder: '{{searchPlaceholder}}', + {{/if}} + openOnFocus: true, + detachedMediaQuery: 'none', + onSubmit({ state }) { + setInstantSearchUiState({ query: state.query }); + }, + plugins: [recentSearchesPlugin, querySuggestionsPlugin], +}); + +function setInstantSearchUiState(indexUiState) { + search.mainIndex.setIndexUiState({ page: 1, ...indexUiState }); +} + +function onSelect({ setIsOpen, setQuery, event, query }) { + if (isModifierEvent(event)) { + return; + } + + setQuery(query); + setIsOpen(false); + setInstantSearchUiState({ query }); +} + +function isModifierEvent(event) { + const isMiddleClick = event.button === 1; + + return ( + isMiddleClick || + event.altKey || + event.ctrlKey || + event.metaKey || + event.shiftKey + ); +} +{{/if}} diff --git a/packages/create-instantsearch-app/src/templates/InstantSearch.js/src/global.d.ts.hbs b/packages/create-instantsearch-app/src/templates/InstantSearch.js/src/global.d.ts.hbs index 42b0d5d43d..1942f6c89d 100644 --- a/packages/create-instantsearch-app/src/templates/InstantSearch.js/src/global.d.ts.hbs +++ b/packages/create-instantsearch-app/src/templates/InstantSearch.js/src/global.d.ts.hbs @@ -1,3 +1,8 @@ +{{#if flags.autocomplete}} +import { autocomplete } from '@algolia/autocomplete-js'; +import { createLocalStorageRecentSearchesPlugin } from '@algolia/autocomplete-plugin-recent-searches'; +import { createQuerySuggestionsPlugin } from '@algolia/autocomplete-plugin-query-suggestions'; +{{/if}} import algoliasearch from 'algoliasearch/lite'; import instantsearch from 'instantsearch.js/dist/instantsearch.production.min'; @@ -5,5 +10,16 @@ declare global { interface Window { algoliasearch: typeof algoliasearch; instantsearch: typeof instantsearch; + {{#if flags.autocomplete}} + '@algolia/autocomplete-js': { + autocomplete: typeof autocomplete; + }; + '@algolia/autocomplete-plugin-recent-searches': { + createLocalStorageRecentSearchesPlugin: typeof createLocalStorageRecentSearchesPlugin; + }; + '@algolia/autocomplete-plugin-query-suggestions': { + createQuerySuggestionsPlugin: typeof createQuerySuggestionsPlugin; + }; + {{/if}} } }