create-instantsearch-app 7.1.0 → 7.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,7 +16,6 @@
16
16
  <!-- START doctoc generated TOC please keep comment here to allow auto update -->
17
17
  <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
18
18
 
19
-
20
19
  - [Get started](#get-started)
21
20
  - [Usage](#usage)
22
21
  - [API](#api)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-instantsearch-app",
3
- "version": "7.1.0",
3
+ "version": "7.2.1",
4
4
  "license": "MIT",
5
5
  "description": "⚡️ Build InstantSearch apps at the speed of thought",
6
6
  "keywords": [
@@ -49,9 +49,9 @@
49
49
  "validate-npm-package-name": "3.0.0"
50
50
  },
51
51
  "devDependencies": {
52
- "@instantsearch/testutils": "1.15.0",
52
+ "@instantsearch/testutils": "1.17.0",
53
53
  "jest-image-snapshot": "2.12.0",
54
54
  "walk-sync": "2.0.2"
55
55
  },
56
- "gitHead": "b2196f01f03c6bea37a3aa048e4e5b45a9bbf33d"
56
+ "gitHead": "8fb537f2301a5e133e9e5df20c7c208cae16b3aa"
57
57
  }
@@ -185,4 +185,60 @@ describe('flags', () => {
185
185
  ).toEqual(expect.objectContaining({ insights: false }));
186
186
  });
187
187
  });
188
+
189
+ describe('autocomplete', () => {
190
+ test('with usage of autocomplete in searchInputType', async () => {
191
+ expect(
192
+ await postProcessAnswers({
193
+ configuration: {},
194
+ templateConfig: {
195
+ libraryName: 'instantsearch.js',
196
+ flags: {
197
+ autocomplete: '>= 4.52',
198
+ },
199
+ },
200
+ optionsFromArguments: {},
201
+ answers: { searchInputType: 'autocomplete' },
202
+ })
203
+ ).toEqual(
204
+ expect.objectContaining({
205
+ searchInputType: 'autocomplete',
206
+ flags: expect.objectContaining({ autocomplete: true }),
207
+ })
208
+ );
209
+ });
210
+
211
+ test('without usage of autocomplete in searchInputType', async () => {
212
+ expect(
213
+ await postProcessAnswers({
214
+ configuration: {},
215
+ templateConfig: {
216
+ libraryName: 'instantsearch.js',
217
+ flags: {
218
+ autocomplete: '>= 4.52',
219
+ },
220
+ },
221
+ optionsFromArguments: {},
222
+ answers: { searchInputType: 'searchbox' },
223
+ })
224
+ ).toEqual(
225
+ expect.objectContaining({
226
+ searchInputType: 'searchbox',
227
+ flags: expect.objectContaining({ autocomplete: false }),
228
+ })
229
+ );
230
+ });
231
+
232
+ test('without config', async () => {
233
+ expect(
234
+ (
235
+ await postProcessAnswers({
236
+ configuration: {},
237
+ templateConfig: {},
238
+ optionsFromArguments: {},
239
+ })
240
+ ).flags
241
+ ).toEqual(expect.objectContaining({ autocomplete: false }));
242
+ });
243
+ });
188
244
  });
package/src/cli/index.js CHANGED
@@ -201,6 +201,91 @@ const getQuestions = ({ appName }) => ({
201
201
  when: ({ appId, apiKey, indexName }) =>
202
202
  attributesForFaceting.length === 0 && appId && apiKey && indexName,
203
203
  },
204
+ {
205
+ type: 'list',
206
+ name: 'searchInputType',
207
+ message: 'Type of search input',
208
+ choices: [
209
+ {
210
+ name: 'Autocomplete with suggested and recent searches',
211
+ value: 'autocomplete',
212
+ },
213
+ { name: 'Regular search box', value: 'searchbox' },
214
+ ],
215
+ default: 'autocomplete',
216
+ when: ({ libraryVersion, template }) => {
217
+ const templatePath = getTemplatePath(template);
218
+ const templateConfig = getAppTemplateConfig(templatePath);
219
+
220
+ const selectedLibraryVersion = libraryVersion;
221
+ const requiredLibraryVersion =
222
+ templateConfig.flags && templateConfig.flags.autocomplete;
223
+ const supportsAutocomplete =
224
+ selectedLibraryVersion &&
225
+ requiredLibraryVersion &&
226
+ semver.satisfies(selectedLibraryVersion, requiredLibraryVersion, {
227
+ includePrerelease: true,
228
+ });
229
+
230
+ return supportsAutocomplete;
231
+ },
232
+ },
233
+ {
234
+ type: 'input',
235
+ name: 'querySuggestionsIndexName',
236
+ message: 'Index name for suggested searches',
237
+ suffix: `\n ${chalk.gray('This must be a Query Suggestions index')}`,
238
+ default: 'instant_search_demo_query_suggestions',
239
+ when: ({ searchInputType }) => searchInputType === 'autocomplete',
240
+ },
241
+ {
242
+ type: 'list',
243
+ name: 'autocompleteLibraryVersion',
244
+ message: () => `Autocomplete version`,
245
+ choices: async () => {
246
+ const libraryName = '@algolia/autocomplete-js';
247
+
248
+ try {
249
+ const versions = await fetchLibraryVersions(libraryName);
250
+ const latestStableVersion = semver.maxSatisfying(versions, '1', {
251
+ includePrerelease: false,
252
+ });
253
+
254
+ if (!latestStableVersion) {
255
+ return versions;
256
+ }
257
+
258
+ return [
259
+ new inquirer.Separator('Latest stable version (recommended)'),
260
+ latestStableVersion,
261
+ new inquirer.Separator('All versions'),
262
+ ...versions,
263
+ ];
264
+ } catch (err) {
265
+ const fallbackLibraryVersion = '1.11.0';
266
+
267
+ console.log();
268
+ console.error(
269
+ chalk.red(
270
+ `Cannot fetch versions for library "${chalk.cyan(libraryName)}".`
271
+ )
272
+ );
273
+ console.log();
274
+ console.log(
275
+ `Fallback to ${chalk.cyan(
276
+ fallbackLibraryVersion
277
+ )}, please upgrade the dependency after generating the app.`
278
+ );
279
+ console.log();
280
+
281
+ return [
282
+ new inquirer.Separator('Available versions'),
283
+ fallbackLibraryVersion,
284
+ ];
285
+ }
286
+ },
287
+ when: ({ searchInputType }) => searchInputType === 'autocomplete',
288
+ },
204
289
  ],
205
290
  widget: [
206
291
  {
@@ -77,6 +77,9 @@ async function postProcessAnswers({
77
77
  insights:
78
78
  Boolean(templateConfig.flags && templateConfig.flags.insights) &&
79
79
  semver.satisfies(libraryVersion, templateConfig.flags.insights),
80
+ autocomplete:
81
+ Boolean(templateConfig.flags && templateConfig.flags.autocomplete) &&
82
+ combinedAnswers.searchInputType === 'autocomplete',
80
83
  },
81
84
  };
82
85
  }
@@ -8,6 +8,7 @@ module.exports = {
8
8
  flags: {
9
9
  dynamicWidgets: '>= 4.30',
10
10
  insights: '>= 4.55',
11
+ autocomplete: '>= 4.52',
11
12
  },
12
13
  templateName: 'instantsearch.js',
13
14
  appName: 'instantsearch.js-app',
@@ -9,6 +9,9 @@
9
9
  <link rel="shortcut icon" href="./favicon.png">
10
10
 
11
11
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/instantsearch.css@7/themes/satellite-min.css">
12
+ {{#if flags.autocomplete}}
13
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@algolia/autocomplete-theme-classic@1.11.0/dist/theme.min.css">
14
+ {{/if}}
12
15
  <link rel="stylesheet" href="./src/index.css">
13
16
  <link rel="stylesheet" href="./src/app.css">
14
17
 
@@ -50,6 +53,11 @@
50
53
 
51
54
  <script src="https://cdn.jsdelivr.net/npm/algoliasearch@4.10.5/dist/algoliasearch-lite.umd.js"></script>
52
55
  <script src="https://cdn.jsdelivr.net/npm/instantsearch.js@{{libraryVersion}}"></script>
56
+ {{#if flags.autocomplete}}
57
+ <script src="https://cdn.jsdelivr.net/npm/@algolia/autocomplete-js@{{autocompleteLibraryVersion}}/dist/umd/index.production.min.js"></script>
58
+ <script src="https://cdn.jsdelivr.net/npm/@algolia/autocomplete-plugin-query-suggestions@{{autocompleteLibraryVersion}}/dist/umd/index.production.min.js"></script>
59
+ <script src="https://cdn.jsdelivr.net/npm/@algolia/autocomplete-plugin-recent-searches@{{autocompleteLibraryVersion}}/dist/umd/index.production.min.js"></script>
60
+ {{/if}}
53
61
  <script src="./src/app.js"></script>
54
62
  </body>
55
63
 
@@ -14,6 +14,11 @@
14
14
  "prettier": "2.6.2"
15
15
  },
16
16
  "dependencies": {
17
+ {{#if flags.autocomplete}}
18
+ "@algolia/autocomplete-js": "{{autocompleteLibraryVersion}}",
19
+ "@algolia/autocomplete-plugin-recent-searches": "{{autocompleteLibraryVersion}}",
20
+ "@algolia/autocomplete-plugin-query-suggestions": "{{autocompleteLibraryVersion}}",
21
+ {{/if}}
17
22
  "algoliasearch": "4",
18
23
  "instantsearch.js": "{{libraryVersion}}"
19
24
  }
@@ -1,20 +1,38 @@
1
1
  const { algoliasearch, instantsearch } = window;
2
+ {{#if flags.autocomplete}}
3
+ const { autocomplete } = window['@algolia/autocomplete-js'];
4
+ const { createLocalStorageRecentSearchesPlugin } = window[
5
+ '@algolia/autocomplete-plugin-recent-searches'
6
+ ];
7
+ const { createQuerySuggestionsPlugin } = window[
8
+ '@algolia/autocomplete-plugin-query-suggestions'
9
+ ];
10
+ {{/if}}
2
11
 
3
12
  const searchClient = algoliasearch('{{appId}}', '{{apiKey}}');
4
13
 
5
14
  const search = instantsearch({
6
15
  indexName: '{{indexName}}',
7
16
  searchClient,
17
+ future: { preserveSharedStateOnUnmount: true },
8
18
  {{#if flags.insights}}insights: true,{{/if}}
9
19
  });
10
20
 
21
+ {{#if flags.autocomplete}}
22
+ const virtualSearchBox = instantsearch.connectors.connectSearchBox(() => {});
23
+ {{/if}}
24
+
11
25
  search.addWidgets([
26
+ {{#unless flags.autocomplete}}
12
27
  instantsearch.widgets.searchBox({
13
28
  container: '#searchbox',
14
29
  {{#if searchPlaceholder}}
15
30
  placeholder: '{{searchPlaceholder}}',
16
31
  {{/if}}
17
32
  }),
33
+ {{else}}
34
+ virtualSearchBox({}),
35
+ {{/unless}}
18
36
  instantsearch.widgets.hits({
19
37
  container: '#hits',
20
38
  {{#if attributesToDisplay}}
@@ -74,3 +92,81 @@ search.addWidgets([
74
92
  ]);
75
93
 
76
94
  search.start();
95
+
96
+ {{#if flags.autocomplete}}
97
+ const recentSearchesPlugin = createLocalStorageRecentSearchesPlugin({
98
+ key: 'instantsearch',
99
+ limit: 3,
100
+ transformSource({ source }) {
101
+ return {
102
+ ...source,
103
+ onSelect({ setIsOpen, setQuery, item, event }) {
104
+ onSelect({ setQuery, setIsOpen, event, query: item.label });
105
+ },
106
+ };
107
+ },
108
+ });
109
+
110
+ const querySuggestionsPlugin = createQuerySuggestionsPlugin({
111
+ searchClient,
112
+ indexName: '{{querySuggestionsIndexName}}',
113
+ getSearchParams() {
114
+ return recentSearchesPlugin.data.getAlgoliaSearchParams({ hitsPerPage: 6 });
115
+ },
116
+ transformSource({ source }) {
117
+ return {
118
+ ...source,
119
+ sourceId: 'querySuggestionsPlugin',
120
+ onSelect({ setIsOpen, setQuery, event, item }) {
121
+ onSelect({ setQuery, setIsOpen, event, query: item.query });
122
+ },
123
+ getItems(params) {
124
+ if (!params.state.query) {
125
+ return [];
126
+ }
127
+
128
+ return source.getItems(params);
129
+ },
130
+ };
131
+ },
132
+ });
133
+
134
+ autocomplete({
135
+ container: '#searchbox',
136
+ {{#if searchPlaceholder}}
137
+ placeholder: '{{searchPlaceholder}}',
138
+ {{/if}}
139
+ openOnFocus: true,
140
+ detachedMediaQuery: 'none',
141
+ onSubmit({ state }) {
142
+ setInstantSearchUiState({ query: state.query });
143
+ },
144
+ plugins: [recentSearchesPlugin, querySuggestionsPlugin],
145
+ });
146
+
147
+ function setInstantSearchUiState(indexUiState) {
148
+ search.mainIndex.setIndexUiState({ page: 1, ...indexUiState });
149
+ }
150
+
151
+ function onSelect({ setIsOpen, setQuery, event, query }) {
152
+ if (isModifierEvent(event)) {
153
+ return;
154
+ }
155
+
156
+ setQuery(query);
157
+ setIsOpen(false);
158
+ setInstantSearchUiState({ query });
159
+ }
160
+
161
+ function isModifierEvent(event) {
162
+ const isMiddleClick = event.button === 1;
163
+
164
+ return (
165
+ isMiddleClick ||
166
+ event.altKey ||
167
+ event.ctrlKey ||
168
+ event.metaKey ||
169
+ event.shiftKey
170
+ );
171
+ }
172
+ {{/if}}
@@ -1,3 +1,8 @@
1
+ {{#if flags.autocomplete}}
2
+ import { autocomplete } from '@algolia/autocomplete-js';
3
+ import { createLocalStorageRecentSearchesPlugin } from '@algolia/autocomplete-plugin-recent-searches';
4
+ import { createQuerySuggestionsPlugin } from '@algolia/autocomplete-plugin-query-suggestions';
5
+ {{/if}}
1
6
  import algoliasearch from 'algoliasearch/lite';
2
7
  import instantsearch from 'instantsearch.js/dist/instantsearch.production.min';
3
8
 
@@ -5,5 +10,16 @@ declare global {
5
10
  interface Window {
6
11
  algoliasearch: typeof algoliasearch;
7
12
  instantsearch: typeof instantsearch;
13
+ {{#if flags.autocomplete}}
14
+ '@algolia/autocomplete-js': {
15
+ autocomplete: typeof autocomplete;
16
+ };
17
+ '@algolia/autocomplete-plugin-recent-searches': {
18
+ createLocalStorageRecentSearchesPlugin: typeof createLocalStorageRecentSearchesPlugin;
19
+ };
20
+ '@algolia/autocomplete-plugin-query-suggestions': {
21
+ createQuerySuggestionsPlugin: typeof createQuerySuggestionsPlugin;
22
+ };
23
+ {{/if}}
8
24
  }
9
25
  }
@@ -31,6 +31,8 @@ const searchClient = algoliasearch(
31
31
  '{{apiKey}}'
32
32
  );
33
33
 
34
+ const future = { preserveSharedStateOnUnmount: true };
35
+
34
36
  export function App() {
35
37
  return (
36
38
  <div>
@@ -47,7 +49,7 @@ export function App() {
47
49
  </header>
48
50
 
49
51
  <div className="container">
50
- <InstantSearch searchClient={searchClient} indexName="{{indexName}}" {{#if flags.insights}}insights{{/if}}>
52
+ <InstantSearch searchClient={searchClient} indexName="{{indexName}}" future={future} {{#if flags.insights}}insights{{/if}}>
51
53
  <Configure hitsPerPage={8} />
52
54
  <div className="search-panel">
53
55
  <div className="search-panel__filters">
@@ -16,6 +16,7 @@
16
16
  <ais-instant-search
17
17
  :search-client="searchClient"
18
18
  index-name="{{indexName}}"
19
+ :future="future"
19
20
  {{#if flags.insights}}insights{{/if}}
20
21
  >
21
22
  <ais-configure :hits-per-page.camel="8" />
@@ -85,6 +86,7 @@ export default {
85
86
  data() {
86
87
  return {
87
88
  searchClient: algoliasearch('{{appId}}', '{{apiKey}}'),
89
+ future: { preserveSharedStateOnUnmount: true },
88
90
  };
89
91
  },
90
92
  };