From a8505cd95e246ece4534f20ce6a02616ddb628c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Chalifour?= Date: Fri, 6 Aug 2021 12:06:21 +0200 Subject: [PATCH 01/10] feat(core): introduce Reshape API --- examples/reshape/README.md | 0 examples/reshape/app.tsx | 74 ++++++ examples/reshape/env.ts | 10 + examples/reshape/favicon.png | Bin 0 -> 44137 bytes .../functions/AutocompleteReshapeFunction.ts | 12 + examples/reshape/functions/groupBy.ts | 68 +++++ examples/reshape/functions/index.ts | 3 + examples/reshape/functions/limit.ts | 30 +++ .../functions/normalizeReshapeSources.ts | 13 + examples/reshape/functions/uniqBy.ts | 41 +++ examples/reshape/index.html | 20 ++ examples/reshape/package.json | 33 +++ examples/reshape/productsPlugin.tsx | 249 ++++++++++++++++++ examples/reshape/searchClient.ts | 6 + examples/reshape/style.css | 25 ++ examples/reshape/types/Highlighted.ts | 5 + examples/reshape/types/ProductHit.ts | 35 +++ examples/reshape/types/index.ts | 2 + .../src/__tests__/reshape.test.ts | 213 +++++++++++++++ .../src/getAutocompleteSetters.ts | 3 +- .../autocomplete-core/src/getDefaultProps.ts | 4 +- packages/autocomplete-core/src/onInput.ts | 4 + packages/autocomplete-core/src/reshape.ts | 55 ++++ packages/autocomplete-core/src/resolve.ts | 4 +- .../src/types/AutocompleteOptions.ts | 8 + .../src/types/AutocompleteReshape.ts | 22 ++ packages/autocomplete-core/src/types/index.ts | 1 + packages/autocomplete-core/src/utils/index.ts | 1 - .../src}/__tests__/flatten.test.ts | 0 .../src}/flatten.ts | 0 packages/autocomplete-shared/src/index.ts | 1 + yarn.lock | 5 + 32 files changed, 942 insertions(+), 5 deletions(-) create mode 100644 examples/reshape/README.md create mode 100644 examples/reshape/app.tsx create mode 100644 examples/reshape/env.ts create mode 100644 examples/reshape/favicon.png create mode 100644 examples/reshape/functions/AutocompleteReshapeFunction.ts create mode 100644 examples/reshape/functions/groupBy.ts create mode 100644 examples/reshape/functions/index.ts create mode 100644 examples/reshape/functions/limit.ts create mode 100644 examples/reshape/functions/normalizeReshapeSources.ts create mode 100644 examples/reshape/functions/uniqBy.ts create mode 100644 examples/reshape/index.html create mode 100644 examples/reshape/package.json create mode 100644 examples/reshape/productsPlugin.tsx create mode 100644 examples/reshape/searchClient.ts create mode 100644 examples/reshape/style.css create mode 100644 examples/reshape/types/Highlighted.ts create mode 100644 examples/reshape/types/ProductHit.ts create mode 100644 examples/reshape/types/index.ts create mode 100644 packages/autocomplete-core/src/__tests__/reshape.test.ts create mode 100644 packages/autocomplete-core/src/reshape.ts create mode 100644 packages/autocomplete-core/src/types/AutocompleteReshape.ts rename packages/{autocomplete-core/src/utils => autocomplete-shared/src}/__tests__/flatten.test.ts (100%) rename packages/{autocomplete-core/src/utils => autocomplete-shared/src}/flatten.ts (100%) diff --git a/examples/reshape/README.md b/examples/reshape/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/examples/reshape/app.tsx b/examples/reshape/app.tsx new file mode 100644 index 000000000..340d23f77 --- /dev/null +++ b/examples/reshape/app.tsx @@ -0,0 +1,74 @@ +/** @jsx h */ +import { autocomplete } from '@algolia/autocomplete-js'; +import { createQuerySuggestionsPlugin } from '@algolia/autocomplete-plugin-query-suggestions'; +import { createLocalStorageRecentSearchesPlugin } from '@algolia/autocomplete-plugin-recent-searches'; +import { h, Fragment } from 'preact'; +import { pipe } from 'ramda'; + +import '@algolia/autocomplete-theme-classic'; + +import { groupBy, limit, uniqBy } from './functions'; +import { productsPlugin } from './productsPlugin'; +import { searchClient } from './searchClient'; + +const recentSearchesPlugin = createLocalStorageRecentSearchesPlugin({ + key: 'search', + limit: 10, +}); +const querySuggestionsPlugin = createQuerySuggestionsPlugin({ + searchClient, + indexName: 'instant_search_demo_query_suggestions', + getSearchParams() { + return { + hitsPerPage: 10, + }; + }, +}); + +const combineSuggestions = pipe( + uniqBy(({ source, item }) => + source.sourceId === 'querySuggestionsPlugin' ? item.query : item.label + ), + limit(4) +); +const groupByCategory = groupBy((hit) => hit.categories[0], { + getSource({ name, items }) { + return { + getItems() { + return items.slice(0, 3); + }, + templates: { + header() { + return ( + + {name} +
+ + ); + }, + }, + }; + }, +}); + +autocomplete({ + container: '#autocomplete', + placeholder: 'Search', + debug: true, + openOnFocus: true, + plugins: [recentSearchesPlugin, querySuggestionsPlugin, productsPlugin], + reshape({ sourcesBySourceId }) { + const { + recentSearchesPlugin, + querySuggestionsPlugin, + products, + ...rest + } = sourcesBySourceId; + + return [ + combineSuggestions(recentSearchesPlugin, querySuggestionsPlugin), + groupByCategory(products), + Object.values(rest), + ]; + }, +}); diff --git a/examples/reshape/env.ts b/examples/reshape/env.ts new file mode 100644 index 000000000..6eef24529 --- /dev/null +++ b/examples/reshape/env.ts @@ -0,0 +1,10 @@ +import * as preact from 'preact'; + +// Parcel picks the `source` field of the monorepo packages and thus doesn't +// apply the Babel config. We therefore need to manually override the constants +// in the app, as well as the React pragmas. +// See https://twitter.com/devongovett/status/1134231234605830144 +(global as any).__DEV__ = process.env.NODE_ENV !== 'production'; +(global as any).__TEST__ = false; +(global as any).h = preact.h; +(global as any).React = preact; diff --git a/examples/reshape/favicon.png b/examples/reshape/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..084fdfdfc2dff34a510e79b2f8ee8c19a7db96b4 GIT binary patch literal 44137 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 = < + TItem extends BaseItem +>( + ...params: TParams[] +) => ( + ...expressions: Array> +) => Array>; diff --git a/examples/reshape/functions/groupBy.ts b/examples/reshape/functions/groupBy.ts new file mode 100644 index 000000000..39cfc2af4 --- /dev/null +++ b/examples/reshape/functions/groupBy.ts @@ -0,0 +1,68 @@ +import { BaseItem } from '@algolia/autocomplete-core'; +import { AutocompleteSource } from '@algolia/autocomplete-js'; +import { flatten } from '@algolia/autocomplete-shared'; + +import { AutocompleteReshapeFunction } from './AutocompleteReshapeFunction'; +import { normalizeReshapeSources } from './normalizeReshapeSources'; + +export type GroupByOptions< + TItem extends BaseItem, + TSource extends AutocompleteSource +> = { + getSource(params: { name: string; items: TItem[] }): Partial; +}; + +export const groupBy: AutocompleteReshapeFunction = < + TItem extends BaseItem, + TSource extends AutocompleteSource = AutocompleteSource +>( + predicate: (value: TItem) => string, + options: GroupByOptions +) => { + return function runGroupBy(...rawSources) { + const sources = normalizeReshapeSources(rawSources); + + if (sources.length === 0) { + return []; + } + + // Since we create multiple sources from a single one, we take the first one + // as reference to create the new sources from. + const referenceSource = sources[0]; + const items = flatten(sources.map((source) => source.getItems())); + const groupedItems = items.reduce>((acc, item) => { + const key = predicate(item as TItem); + + if (!acc.hasOwnProperty(key)) { + acc[key] = []; + } + + acc[key].push(item as TItem); + + return acc; + }, {}); + + const groupNames = Object.keys(groupedItems); + + return groupNames.map((groupName) => { + const groupItems = groupedItems[groupName]; + const userSource = options.getSource({ + name: groupName, + items: groupItems, + }); + + return { + ...referenceSource, + sourceId: groupName, + getItems() { + return groupItems; + }, + ...userSource, + templates: { + ...((referenceSource as any).templates as any), + ...(userSource as any).templates, + }, + }; + }); + }; +}; diff --git a/examples/reshape/functions/index.ts b/examples/reshape/functions/index.ts new file mode 100644 index 000000000..2af93dae4 --- /dev/null +++ b/examples/reshape/functions/index.ts @@ -0,0 +1,3 @@ +export * from './groupBy'; +export * from './limit'; +export * from './uniqBy'; diff --git a/examples/reshape/functions/limit.ts b/examples/reshape/functions/limit.ts new file mode 100644 index 000000000..059b9eb77 --- /dev/null +++ b/examples/reshape/functions/limit.ts @@ -0,0 +1,30 @@ +import { AutocompleteReshapeFunction } from './AutocompleteReshapeFunction'; +import { normalizeReshapeSources } from './normalizeReshapeSources'; + +export const limit: AutocompleteReshapeFunction = (value) => { + return function runLimit(...rawSources) { + const sources = normalizeReshapeSources(rawSources); + const limitPerSource = Math.ceil(value / sources.length); + let sharedLimitRemaining = value; + + return sources.map((source, index) => { + const isLastSource = index === sources.length - 1; + const items = source + .getItems() + .slice( + 0, + isLastSource + ? sharedLimitRemaining + : Math.min(limitPerSource, sharedLimitRemaining) + ); + sharedLimitRemaining = Math.max(sharedLimitRemaining - items.length, 0); + + return { + ...source, + getItems() { + return items; + }, + }; + }); + }; +}; diff --git a/examples/reshape/functions/normalizeReshapeSources.ts b/examples/reshape/functions/normalizeReshapeSources.ts new file mode 100644 index 000000000..b9e179475 --- /dev/null +++ b/examples/reshape/functions/normalizeReshapeSources.ts @@ -0,0 +1,13 @@ +import { + AutocompleteReshapeSource, + BaseItem, +} from '@algolia/autocomplete-core'; +import { flatten } from '@algolia/autocomplete-shared'; + +// We filter out falsy values because dynamic sources may not exist at every render. +// We flatten to support pipe operators from functional libraries like Ramda. +export function normalizeReshapeSources( + sources: Array> +) { + return flatten(sources).filter(Boolean); +} diff --git a/examples/reshape/functions/uniqBy.ts b/examples/reshape/functions/uniqBy.ts new file mode 100644 index 000000000..39e80a9dc --- /dev/null +++ b/examples/reshape/functions/uniqBy.ts @@ -0,0 +1,41 @@ +import { + AutocompleteReshapeSource, + BaseItem, +} from '@algolia/autocomplete-core'; + +import { AutocompleteReshapeFunction } from './AutocompleteReshapeFunction'; +import { normalizeReshapeSources } from './normalizeReshapeSources'; + +type UniqByPredicate = (params: { + source: AutocompleteReshapeSource; + item: TItem; +}) => TItem; + +export const uniqBy: AutocompleteReshapeFunction> = < + TItem extends BaseItem +>( + predicate +) => { + return function runUniqBy(...rawSources) { + const sources = normalizeReshapeSources(rawSources); + const seen: TItem[] = []; + + return sources.map((source) => { + const items = source.getItems().filter((item) => { + const appliedItem = predicate({ source, item }); + const hasSeen = seen.includes(appliedItem); + + seen.push(appliedItem); + + return !hasSeen; + }); + + return { + ...source, + getItems() { + return items; + }, + }; + }); + }; +}; diff --git a/examples/reshape/index.html b/examples/reshape/index.html new file mode 100644 index 000000000..44c246a45 --- /dev/null +++ b/examples/reshape/index.html @@ -0,0 +1,20 @@ + + + + + + + + + Reshape API | Autocomplete + + + +
+
+
+ + + + + diff --git a/examples/reshape/package.json b/examples/reshape/package.json new file mode 100644 index 000000000..c8b9d4eaf --- /dev/null +++ b/examples/reshape/package.json @@ -0,0 +1,33 @@ +{ + "name": "@algolia/autocomplete-example-reshape", + "description": "Autocomplete example with the Reshape API", + "version": "1.2.2", + "private": true, + "license": "MIT", + "scripts": { + "build": "parcel build index.html", + "start": "parcel index.html" + }, + "dependencies": { + "@algolia/autocomplete-js": "1.2.2", + "@algolia/autocomplete-plugin-query-suggestions": "1.2.2", + "@algolia/autocomplete-plugin-recent-searches": "1.2.2", + "@algolia/autocomplete-preset-algolia": "1.2.2", + "@algolia/autocomplete-shared": "1.2.2", + "@algolia/autocomplete-theme-classic": "1.2.2", + "@algolia/client-search": "4.9.1", + "algoliasearch": "4.9.1", + "preact": "10.5.13", + "ramda": "0.27.1", + "search-insights": "1.7.1" + }, + "devDependencies": { + "@algolia/autocomplete-core": "1.2.2", + "parcel": "2.0.0-beta.2" + }, + "keywords": [ + "algolia", + "autocomplete", + "javascript" + ] +} diff --git a/examples/reshape/productsPlugin.tsx b/examples/reshape/productsPlugin.tsx new file mode 100644 index 000000000..20f616ede --- /dev/null +++ b/examples/reshape/productsPlugin.tsx @@ -0,0 +1,249 @@ +/** @jsx h */ +import { + AutocompleteComponents, + getAlgoliaResults, +} from '@algolia/autocomplete-js'; +import { h, Fragment } from 'preact'; + +import { searchClient } from './searchClient'; +import { ProductRecord } from './types'; + +export const productsPlugin = { + getSources({ query }) { + if (!query) { + return []; + } + + return [ + { + sourceId: 'products', + getItems() { + return getAlgoliaResults({ + searchClient, + queries: [ + { + indexName: 'instant_search', + query, + params: { + clickAnalytics: true, + attributesToSnippet: ['name:10', 'description:35'], + snippetEllipsisText: '…', + hitsPerPage: 15, + }, + }, + ], + transformResponse({ hits }) { + const [bestBuyHits] = hits; + + return bestBuyHits.map((hit) => ({ + ...hit, + comments: hit.popularity % 100, + sale: hit.free_shipping, + // eslint-disable-next-line @typescript-eslint/camelcase + sale_price: hit.free_shipping + ? (hit.price - hit.price / 10).toFixed(2) + : hit.price.toString(), + })); + }, + }); + }, + templates: { + header() { + return ( + + Products +
+ + ); + }, + item({ item, components }) { + return ; + }, + noResults() { + return 'No products for this query.'; + }, + }, + }, + ]; + }, +}; + +type ProductItemProps = { + hit: ProductHit; + components: AutocompleteComponents; +}; + +function ProductItem({ hit, components }: ProductItemProps) { + return ( + +
+
+ {hit.name} +
+ +
+
+ +
+
+ By {hit.brand} in{' '} + {hit.categories[0]} +
+ +
+ {hit.rating > 0 && ( +
+
+ {Array.from({ length: 5 }, (_value, index) => { + const isFilled = hit.rating >= index + 1; + + return ( + + + + ); + })} +
+
+ )} +
+ + + + {hit.comments.toLocaleString()} +
+
+ +
+
+
+ + ${hit.sale_price.toLocaleString()} + {' '} + {hit.sale && ( + + ${hit.price.toLocaleString()} + + )} +
+ {hit.sale && ( + + On sale + + )} +
+
+
+
+ +
+ + +
+
+ ); +} diff --git a/examples/reshape/searchClient.ts b/examples/reshape/searchClient.ts new file mode 100644 index 000000000..8d0663310 --- /dev/null +++ b/examples/reshape/searchClient.ts @@ -0,0 +1,6 @@ +import algoliasearch from 'algoliasearch'; + +const appId = 'latency'; +const apiKey = '6be0576ff61c053d5f9a3225e2a90f76'; + +export const searchClient = algoliasearch(appId, apiKey); diff --git a/examples/reshape/style.css b/examples/reshape/style.css new file mode 100644 index 000000000..46a9ccf94 --- /dev/null +++ b/examples/reshape/style.css @@ -0,0 +1,25 @@ +* { + box-sizing: border-box; +} + +body { + background-color: rgb(244, 244, 249); + color: rgb(65, 65, 65); + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + padding: 1rem; +} + +.container { + margin: 0 auto; + max-width: 640px; + width: 100%; +} + +body[data-theme='dark'] { + background-color: rgb(0, 0, 0); + color: rgb(183, 192, 199); +} diff --git a/examples/reshape/types/Highlighted.ts b/examples/reshape/types/Highlighted.ts new file mode 100644 index 000000000..a543e461e --- /dev/null +++ b/examples/reshape/types/Highlighted.ts @@ -0,0 +1,5 @@ +import { HighlightResult } from '@algolia/client-search'; + +export type Highlighted = TRecord & { + _highlightResult: HighlightResult; +}; diff --git a/examples/reshape/types/ProductHit.ts b/examples/reshape/types/ProductHit.ts new file mode 100644 index 000000000..74d7bd40f --- /dev/null +++ b/examples/reshape/types/ProductHit.ts @@ -0,0 +1,35 @@ +import { Hit } from '@algolia/client-search'; + +export type ProductRecord = { + brand: string; + categories: string[]; + comments: number; + description: string; + free_shipping: boolean; + hierarchicalCategories: { + lvl0: string; + lvl1?: string; + lvl2?: string; + lvl3?: string; + lvl4?: string; + lvl5?: string; + lvl6?: string; + }; + image: string; + name: string; + popularity: number; + price: number; + prince_range: string; + rating: number; + sale: boolean; + sale_price: string; + type: string; + url: string; +}; + +type WithAutocompleteAnalytics = THit & { + __autocomplete_indexName: string; + __autocomplete_queryID: string; +}; + +export type ProductHit = WithAutocompleteAnalytics>; diff --git a/examples/reshape/types/index.ts b/examples/reshape/types/index.ts new file mode 100644 index 000000000..5b77dc77b --- /dev/null +++ b/examples/reshape/types/index.ts @@ -0,0 +1,2 @@ +export * from './Highlighted'; +export * from './ProductHit'; diff --git a/packages/autocomplete-core/src/__tests__/reshape.test.ts b/packages/autocomplete-core/src/__tests__/reshape.test.ts new file mode 100644 index 000000000..92cf8fdcd --- /dev/null +++ b/packages/autocomplete-core/src/__tests__/reshape.test.ts @@ -0,0 +1,213 @@ +import { createPlayground, runAllMicroTasks } from '../../../../test/utils'; +import { createAutocomplete } from '../createAutocomplete'; +import { AutocompleteReshapeSource } from '../types'; + +const recentSearchesPlugin = { + getSources() { + return [ + { + sourceId: 'recentSearchesPlugin', + getItems() { + return [ + { label: 'macbook' }, + { label: 'macbook pro' }, + { label: 'iphone' }, + ]; + }, + }, + ]; + }, +}; + +const querySuggestionsPlugin = { + getSources() { + return [ + { + sourceId: 'querySuggestionsPlugin', + getItems() { + return [ + { query: 'macbook' }, + { query: 'macbook air' }, + { query: 'macbook pro' }, + ]; + }, + }, + ]; + }, +}; + +const customLimit = (value: number) => { + return function runCustomLimit( + ...sources: Array> + ) { + return sources.map((source) => { + const items = source.getItems(); + + return { + ...source, + getItems() { + return items.slice(0, value); + }, + }; + }); + }; +}; + +const limitToOnePerSource = customLimit(1); + +describe('reshape', () => { + test('gets called with sourcesBySourceId, sources and state', async () => { + const reshape = jest.fn(({ sources }) => sources); + const { inputElement } = createPlayground(createAutocomplete, { + openOnFocus: true, + plugins: [recentSearchesPlugin, querySuggestionsPlugin], + reshape, + }); + + inputElement.focus(); + await runAllMicroTasks(); + + expect(reshape).toHaveBeenCalledTimes(1); + expect(reshape).toHaveBeenCalledWith({ + sourcesBySourceId: { + recentSearchesPlugin: expect.objectContaining({ + sourceId: 'recentSearchesPlugin', + }), + querySuggestionsPlugin: expect.objectContaining({ + sourceId: 'querySuggestionsPlugin', + }), + }, + sources: expect.arrayContaining([ + expect.objectContaining({ sourceId: 'recentSearchesPlugin' }), + expect.objectContaining({ sourceId: 'querySuggestionsPlugin' }), + ]), + state: expect.any(Object), + }); + }); + + test('provides resolved items in sources', async () => { + const reshape = jest.fn(({ sources }) => sources); + const asyncPlugin = { + getSources() { + return [ + { + sourceId: 'asyncPlugin', + getItems() { + return Promise.resolve([{ label: 'macbook' }]); + }, + }, + ]; + }, + }; + const { inputElement } = createPlayground(createAutocomplete, { + openOnFocus: true, + plugins: [asyncPlugin], + reshape, + }); + + inputElement.focus(); + await runAllMicroTasks(); + + expect(reshape).toHaveBeenCalledTimes(1); + + const call = reshape.mock.calls[0][0]; + const source: AutocompleteReshapeSource = call.sources[0]; + const sourcesBySourceId = call.sourcesBySourceId; + + expect(source.getItems()).toEqual([{ label: 'macbook' }]); + expect(sourcesBySourceId.asyncPlugin.getItems()).toEqual([ + { label: 'macbook' }, + ]); + }); + + test('supports a reshape function in return', async () => { + const onStateChange = jest.fn(); + const { inputElement } = createPlayground(createAutocomplete, { + openOnFocus: true, + onStateChange, + plugins: [recentSearchesPlugin, querySuggestionsPlugin], + reshape({ sources }) { + return limitToOnePerSource(...sources); + }, + }); + + inputElement.focus(); + await runAllMicroTasks(); + + expect(onStateChange).toHaveBeenLastCalledWith( + expect.objectContaining({ + state: expect.objectContaining({ + collections: [ + expect.objectContaining({ + items: [{ __autocomplete_id: 0, label: 'macbook' }], + }), + expect.objectContaining({ + items: [{ __autocomplete_id: 1, query: 'macbook' }], + }), + ], + }), + }) + ); + }); + + test('supports an array of reshape functions in return', async () => { + const onStateChange = jest.fn(); + + const { inputElement } = createPlayground(createAutocomplete, { + openOnFocus: true, + onStateChange, + plugins: [recentSearchesPlugin, querySuggestionsPlugin], + reshape({ sources }) { + return [limitToOnePerSource(...sources)]; + }, + }); + + inputElement.focus(); + await runAllMicroTasks(); + + expect(onStateChange).toHaveBeenLastCalledWith( + expect.objectContaining({ + state: expect.objectContaining({ + collections: [ + expect.objectContaining({ + items: [{ __autocomplete_id: 0, label: 'macbook' }], + }), + expect.objectContaining({ + items: [{ __autocomplete_id: 1, query: 'macbook' }], + }), + ], + }), + }) + ); + }); + + test('ignores undefined sources', async () => { + const onStateChange = jest.fn(); + const { inputElement } = createPlayground(createAutocomplete, { + openOnFocus: true, + onStateChange, + plugins: [recentSearchesPlugin, querySuggestionsPlugin], + reshape({ sources }) { + return [limitToOnePerSource(...sources), undefined]; + }, + }); + + inputElement.focus(); + await runAllMicroTasks(); + + expect(onStateChange).toHaveBeenLastCalledWith( + expect.objectContaining({ + state: expect.objectContaining({ + collections: [ + expect.objectContaining({ + items: [{ __autocomplete_id: 0, label: 'macbook' }], + }), + expect.objectContaining({ + items: [{ __autocomplete_id: 1, query: 'macbook' }], + }), + ], + }), + }) + ); + }); +}); diff --git a/packages/autocomplete-core/src/getAutocompleteSetters.ts b/packages/autocomplete-core/src/getAutocompleteSetters.ts index 4a698129a..2ffd7473e 100644 --- a/packages/autocomplete-core/src/getAutocompleteSetters.ts +++ b/packages/autocomplete-core/src/getAutocompleteSetters.ts @@ -1,10 +1,11 @@ +import { flatten } from '@algolia/autocomplete-shared'; + import { AutocompleteApi, AutocompleteCollection, AutocompleteStore, BaseItem, } from './types'; -import { flatten } from './utils'; interface GetAutocompleteSettersOptions { store: AutocompleteStore; diff --git a/packages/autocomplete-core/src/getDefaultProps.ts b/packages/autocomplete-core/src/getDefaultProps.ts index bc0bc7a1f..dc8a46ef2 100644 --- a/packages/autocomplete-core/src/getDefaultProps.ts +++ b/packages/autocomplete-core/src/getDefaultProps.ts @@ -1,6 +1,7 @@ import { getItemsCount, generateAutocompleteId, + flatten, } from '@algolia/autocomplete-shared'; import { @@ -10,7 +11,7 @@ import { BaseItem, InternalAutocompleteOptions, } from './types'; -import { getNormalizedSources, flatten } from './utils'; +import { getNormalizedSources } from './utils'; export function getDefaultProps( props: AutocompleteOptions, @@ -32,6 +33,7 @@ export function getDefaultProps( stallThreshold: 300, environment, shouldPanelOpen: ({ state }) => getItemsCount(state) > 0, + reshape: ({ sources }) => sources, ...props, // Since `generateAutocompleteId` triggers a side effect (it increments // an internal counter), we don't want to execute it if unnecessary. diff --git a/packages/autocomplete-core/src/onInput.ts b/packages/autocomplete-core/src/onInput.ts index 7cdce3e70..451160329 100644 --- a/packages/autocomplete-core/src/onInput.ts +++ b/packages/autocomplete-core/src/onInput.ts @@ -1,3 +1,4 @@ +import { reshape } from './reshape'; import { preResolve, resolve, postResolve } from './resolve'; import { AutocompleteScopeApi, @@ -97,6 +98,9 @@ export function onInput({ ) .then(resolve) .then((responses) => postResolve(responses, sources)) + .then((collections) => + reshape({ collections, props, state: store.getState() }) + ) .then((collections) => { setStatus('idle'); setCollections(collections as any); diff --git a/packages/autocomplete-core/src/reshape.ts b/packages/autocomplete-core/src/reshape.ts new file mode 100644 index 000000000..d6e7f4fba --- /dev/null +++ b/packages/autocomplete-core/src/reshape.ts @@ -0,0 +1,55 @@ +import { flatten } from '@algolia/autocomplete-shared'; + +import { + AutocompleteCollection, + AutocompleteReshapeSourcesBySourceId, + AutocompleteState, + BaseItem, + InternalAutocompleteOptions, +} from './types'; + +type ReshapeParams = { + collections: Array>; + props: InternalAutocompleteOptions; + state: AutocompleteState; +}; + +export function reshape({ + collections, + props, + state, +}: ReshapeParams) { + // Sources are grouped by `sourceId` to conveniently pick them via destructuring. + // Example: `const { recentSearchesPlugin } = sourcesBySourceId` + const sourcesBySourceId = collections.reduce< + AutocompleteReshapeSourcesBySourceId + >( + (acc, collection) => ({ + ...acc, + [collection.source.sourceId]: { + ...collection.source, + getItems() { + // We provide the resolved items from the collection to the `reshape` prop. + return flatten(collection.items); + }, + }, + }), + {} + ); + + const reshapeSources = props.reshape({ + sources: Object.values(sourcesBySourceId), + sourcesBySourceId, + state, + }); + + // We reconstruct the collections with the items modified by the `reshape` prop. + return flatten(reshapeSources) + .filter(Boolean) + .map((source) => { + return { + source, + items: source.getItems(), + }; + }); +} diff --git a/packages/autocomplete-core/src/resolve.ts b/packages/autocomplete-core/src/resolve.ts index 68bb97a5b..0b732b7ef 100644 --- a/packages/autocomplete-core/src/resolve.ts +++ b/packages/autocomplete-core/src/resolve.ts @@ -4,7 +4,7 @@ import type { RequesterDescription, TransformResponse, } from '@algolia/autocomplete-preset-algolia'; -import { decycle, invariant } from '@algolia/autocomplete-shared'; +import { decycle, flatten, invariant } from '@algolia/autocomplete-shared'; import { MultipleQueriesQuery, SearchForFacetValuesResponse, @@ -13,7 +13,7 @@ import { import type { SearchClient } from 'algoliasearch/lite'; import { BaseItem, InternalAutocompleteSource } from './types'; -import { flatten, mapToAlgoliaResponse } from './utils'; +import { mapToAlgoliaResponse } from './utils'; function isDescription( item: diff --git a/packages/autocomplete-core/src/types/AutocompleteOptions.ts b/packages/autocomplete-core/src/types/AutocompleteOptions.ts index d5ceec0be..a1038fff1 100644 --- a/packages/autocomplete-core/src/types/AutocompleteOptions.ts +++ b/packages/autocomplete-core/src/types/AutocompleteOptions.ts @@ -4,6 +4,7 @@ import { AutocompleteScopeApi, BaseItem } from './AutocompleteApi'; import { AutocompleteEnvironment } from './AutocompleteEnvironment'; import { AutocompleteNavigator } from './AutocompleteNavigator'; import { AutocompletePlugin } from './AutocompletePlugin'; +import { Reshape } from './AutocompleteReshape'; import { AutocompleteSource, InternalAutocompleteSource, @@ -165,6 +166,12 @@ export interface AutocompleteOptions { * @link https://www.algolia.com/doc/ui-libraries/autocomplete/api-reference/autocomplete-js/autocomplete/#param-plugins */ plugins?: Array>; + /** + * The function called to reshape the sources after they're resolved. + * + * @link https://www.algolia.com/doc/ui-libraries/autocomplete/api-reference/autocomplete-js/autocomplete/#param-reshape + */ + reshape?: Reshape; } // Props manipulated internally with default values. @@ -186,4 +193,5 @@ export interface InternalAutocompleteOptions shouldPanelOpen(params: { state: AutocompleteState }): boolean; onSubmit(params: OnSubmitParams): void; onReset(params: OnResetParams): void; + reshape: Reshape; } diff --git a/packages/autocomplete-core/src/types/AutocompleteReshape.ts b/packages/autocomplete-core/src/types/AutocompleteReshape.ts new file mode 100644 index 000000000..be7881c34 --- /dev/null +++ b/packages/autocomplete-core/src/types/AutocompleteReshape.ts @@ -0,0 +1,22 @@ +import { BaseItem } from './AutocompleteApi'; +import { AutocompleteSource } from './AutocompleteSource'; +import { AutocompleteState } from './AutocompleteState'; + +export type AutocompleteReshapeSource< + TItem extends BaseItem +> = AutocompleteSource & { + getItems(): TItem[]; +}; + +export type AutocompleteReshapeSourcesBySourceId< + TItem extends BaseItem +> = Record>; + +export type Reshape< + TItem extends BaseItem, + TState extends AutocompleteState = AutocompleteState +> = (params: { + sources: Array>; + sourcesBySourceId: AutocompleteReshapeSourcesBySourceId; + state: TState; +}) => Array>; diff --git a/packages/autocomplete-core/src/types/index.ts b/packages/autocomplete-core/src/types/index.ts index 2b2fb4f7c..02ce13328 100644 --- a/packages/autocomplete-core/src/types/index.ts +++ b/packages/autocomplete-core/src/types/index.ts @@ -6,6 +6,7 @@ export * from './AutocompleteOptions'; export * from './AutocompleteSource'; export * from './AutocompletePropGetters'; export * from './AutocompletePlugin'; +export * from './AutocompleteReshape'; export * from './AutocompleteSetters'; export * from './AutocompleteState'; export * from './AutocompleteStore'; diff --git a/packages/autocomplete-core/src/utils/index.ts b/packages/autocomplete-core/src/utils/index.ts index d0998991d..78bfba092 100644 --- a/packages/autocomplete-core/src/utils/index.ts +++ b/packages/autocomplete-core/src/utils/index.ts @@ -1,5 +1,4 @@ export * from './createConcurrentSafePromise'; -export * from './flatten'; export * from './getNextActiveItemId'; export * from './getNormalizedSources'; export * from './getActiveItem'; diff --git a/packages/autocomplete-core/src/utils/__tests__/flatten.test.ts b/packages/autocomplete-shared/src/__tests__/flatten.test.ts similarity index 100% rename from packages/autocomplete-core/src/utils/__tests__/flatten.test.ts rename to packages/autocomplete-shared/src/__tests__/flatten.test.ts diff --git a/packages/autocomplete-core/src/utils/flatten.ts b/packages/autocomplete-shared/src/flatten.ts similarity index 100% rename from packages/autocomplete-core/src/utils/flatten.ts rename to packages/autocomplete-shared/src/flatten.ts diff --git a/packages/autocomplete-shared/src/index.ts b/packages/autocomplete-shared/src/index.ts index 6a4a7de39..cdcd3b7ff 100644 --- a/packages/autocomplete-shared/src/index.ts +++ b/packages/autocomplete-shared/src/index.ts @@ -1,6 +1,7 @@ export * from './createRef'; export * from './debounce'; export * from './decycle'; +export * from './flatten'; export * from './generateAutocompleteId'; export * from './getAttributeValueByPath'; export * from './getItemsCount'; diff --git a/yarn.lock b/yarn.lock index 3f9b76276..83ae3f662 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15482,6 +15482,11 @@ raf@^3.4.1: dependencies: performance-now "^2.1.0" +ramda@0.27.1: + version "0.27.1" + resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.27.1.tgz#66fc2df3ef873874ffc2da6aa8984658abacf5c9" + integrity sha512-PgIdVpn5y5Yns8vqb8FzBUEYn98V3xcPgawAkkgj0YJ0qDsnHCiNmZYfOGMgOvoB0eWFLpYbhxUR3mxfDIMvpw== + ramda@~0.26.1: version "0.26.1" resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.26.1.tgz#8d41351eb8111c55353617fc3bbffad8e4d35d06" From 8e4ef0f3824850ac23eea30dc772fa9ca6347aaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Chalifour?= Date: Fri, 6 Aug 2021 14:56:12 +0200 Subject: [PATCH 02/10] chore(docs): deploy reshape example to CodeSandbox --- .codesandbox/ci.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.codesandbox/ci.json b/.codesandbox/ci.json index a45200f69..2d25b221c 100644 --- a/.codesandbox/ci.json +++ b/.codesandbox/ci.json @@ -10,7 +10,7 @@ "/examples/react-renderer", "/examples/starter-algolia", "/examples/starter", - "/examples/voice-search", + "/examples/reshape", "/examples/vue" ], "node": "14" From 12ae2dbdc49e963a1a578329dc660b60e2aca729 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Chalifour?= Date: Mon, 16 Aug 2021 10:31:36 +0200 Subject: [PATCH 03/10] test(core): update fixtures --- .../src/__tests__/reshape.test.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/autocomplete-core/src/__tests__/reshape.test.ts b/packages/autocomplete-core/src/__tests__/reshape.test.ts index 92cf8fdcd..6f6197509 100644 --- a/packages/autocomplete-core/src/__tests__/reshape.test.ts +++ b/packages/autocomplete-core/src/__tests__/reshape.test.ts @@ -11,7 +11,7 @@ const recentSearchesPlugin = { return [ { label: 'macbook' }, { label: 'macbook pro' }, - { label: 'iphone' }, + { label: 'macbook air' }, ]; }, }, @@ -26,9 +26,9 @@ const querySuggestionsPlugin = { sourceId: 'querySuggestionsPlugin', getItems() { return [ - { query: 'macbook' }, - { query: 'macbook air' }, - { query: 'macbook pro' }, + { query: 'iphone' }, + { query: 'iphone pro' }, + { query: 'iphone pro max' }, ]; }, }, @@ -142,7 +142,7 @@ describe('reshape', () => { items: [{ __autocomplete_id: 0, label: 'macbook' }], }), expect.objectContaining({ - items: [{ __autocomplete_id: 1, query: 'macbook' }], + items: [{ __autocomplete_id: 1, query: 'iphone' }], }), ], }), @@ -173,7 +173,7 @@ describe('reshape', () => { items: [{ __autocomplete_id: 0, label: 'macbook' }], }), expect.objectContaining({ - items: [{ __autocomplete_id: 1, query: 'macbook' }], + items: [{ __autocomplete_id: 1, query: 'iphone' }], }), ], }), @@ -203,7 +203,7 @@ describe('reshape', () => { items: [{ __autocomplete_id: 0, label: 'macbook' }], }), expect.objectContaining({ - items: [{ __autocomplete_id: 1, query: 'macbook' }], + items: [{ __autocomplete_id: 1, query: 'iphone' }], }), ], }), From c36da9c733803d4c0736e8e6baf6ac7002fbb64b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Chalifour?= Date: Mon, 16 Aug 2021 10:36:39 +0200 Subject: [PATCH 04/10] docs(examples): use sets for `uniqBy` --- examples/reshape/functions/uniqBy.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/reshape/functions/uniqBy.ts b/examples/reshape/functions/uniqBy.ts index 39e80a9dc..331af3873 100644 --- a/examples/reshape/functions/uniqBy.ts +++ b/examples/reshape/functions/uniqBy.ts @@ -18,14 +18,14 @@ export const uniqBy: AutocompleteReshapeFunction> = < ) => { return function runUniqBy(...rawSources) { const sources = normalizeReshapeSources(rawSources); - const seen: TItem[] = []; + const seen = new Set(); return sources.map((source) => { const items = source.getItems().filter((item) => { const appliedItem = predicate({ source, item }); - const hasSeen = seen.includes(appliedItem); + const hasSeen = seen.has(appliedItem); - seen.push(appliedItem); + seen.add(appliedItem); return !hasSeen; }); From d197428990b31f4f159e84037a7ba4fd457ed302 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Chalifour?= Date: Thu, 26 Aug 2021 10:49:43 +0200 Subject: [PATCH 05/10] refactor: use `Object.entries` --- examples/reshape/functions/groupBy.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/examples/reshape/functions/groupBy.ts b/examples/reshape/functions/groupBy.ts index 39cfc2af4..66df8754d 100644 --- a/examples/reshape/functions/groupBy.ts +++ b/examples/reshape/functions/groupBy.ts @@ -42,10 +42,7 @@ export const groupBy: AutocompleteReshapeFunction = < return acc; }, {}); - const groupNames = Object.keys(groupedItems); - - return groupNames.map((groupName) => { - const groupItems = groupedItems[groupName]; + return Object.entries(groupedItems).map(([groupName, groupItems]) => { const userSource = options.getSource({ name: groupName, items: groupItems, From 13a160831524db98cb561d1a3374de47d1b54224 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Chalifour?= Date: Thu, 26 Aug 2021 10:50:14 +0200 Subject: [PATCH 06/10] refactor: rename `dedupeAndLimitSuggestions` --- examples/reshape/app.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/reshape/app.tsx b/examples/reshape/app.tsx index 340d23f77..5edc4e5db 100644 --- a/examples/reshape/app.tsx +++ b/examples/reshape/app.tsx @@ -25,7 +25,7 @@ const querySuggestionsPlugin = createQuerySuggestionsPlugin({ }, }); -const combineSuggestions = pipe( +const dedupeAndLimitSuggestions = pipe( uniqBy(({ source, item }) => source.sourceId === 'querySuggestionsPlugin' ? item.query : item.label ), @@ -66,7 +66,7 @@ autocomplete({ } = sourcesBySourceId; return [ - combineSuggestions(recentSearchesPlugin, querySuggestionsPlugin), + dedupeAndLimitSuggestions(recentSearchesPlugin, querySuggestionsPlugin), groupByCategory(products), Object.values(rest), ]; From f61f4327ee74cb7b36ab2405d0d0d82d18b89fde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Chalifour?= Date: Thu, 26 Aug 2021 10:52:17 +0200 Subject: [PATCH 07/10] refactor: create `sourceLimit` variable --- examples/reshape/functions/limit.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/examples/reshape/functions/limit.ts b/examples/reshape/functions/limit.ts index 059b9eb77..58e0e1242 100644 --- a/examples/reshape/functions/limit.ts +++ b/examples/reshape/functions/limit.ts @@ -9,14 +9,10 @@ export const limit: AutocompleteReshapeFunction = (value) => { return sources.map((source, index) => { const isLastSource = index === sources.length - 1; - const items = source - .getItems() - .slice( - 0, - isLastSource - ? sharedLimitRemaining - : Math.min(limitPerSource, sharedLimitRemaining) - ); + const sourceLimit = isLastSource + ? sharedLimitRemaining + : Math.min(limitPerSource, sharedLimitRemaining); + const items = source.getItems().slice(0, sourceLimit); sharedLimitRemaining = Math.max(sharedLimitRemaining - items.length, 0); return { From 74d5a4e71a130f39123fdc8cf7056564521acf51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Chalifour?= Date: Thu, 26 Aug 2021 10:54:38 +0200 Subject: [PATCH 08/10] test: update test names --- packages/autocomplete-core/src/__tests__/reshape.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/autocomplete-core/src/__tests__/reshape.test.ts b/packages/autocomplete-core/src/__tests__/reshape.test.ts index 6f6197509..4ead2c01d 100644 --- a/packages/autocomplete-core/src/__tests__/reshape.test.ts +++ b/packages/autocomplete-core/src/__tests__/reshape.test.ts @@ -120,7 +120,7 @@ describe('reshape', () => { ]); }); - test('supports a reshape function in return', async () => { + test('supports a reshaped source in return', async () => { const onStateChange = jest.fn(); const { inputElement } = createPlayground(createAutocomplete, { openOnFocus: true, @@ -150,7 +150,7 @@ describe('reshape', () => { ); }); - test('supports an array of reshape functions in return', async () => { + test('supports an array of reshaped sources in return', async () => { const onStateChange = jest.fn(); const { inputElement } = createPlayground(createAutocomplete, { From 785936e623bfb145fbce461eb84f944e579502bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Chalifour?= Date: Thu, 26 Aug 2021 10:58:47 +0200 Subject: [PATCH 09/10] docs: add link to guide --- packages/autocomplete-core/src/types/AutocompleteOptions.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/autocomplete-core/src/types/AutocompleteOptions.ts b/packages/autocomplete-core/src/types/AutocompleteOptions.ts index a1038fff1..98f795f79 100644 --- a/packages/autocomplete-core/src/types/AutocompleteOptions.ts +++ b/packages/autocomplete-core/src/types/AutocompleteOptions.ts @@ -169,6 +169,10 @@ export interface AutocompleteOptions { /** * The function called to reshape the sources after they're resolved. * + * This is useful to transform sources before rendering them. You can group sources by attribute, remove duplicates, create shared limits between sources, etc. + * + * See [**Reshaping sources**](https://www.algolia.com/doc/ui-libraries/autocomplete/guides/reshaping-sources/) for more information. + * * @link https://www.algolia.com/doc/ui-libraries/autocomplete/api-reference/autocomplete-js/autocomplete/#param-reshape */ reshape?: Reshape; From b6db429fa2856a7141822c62bd3922aaa215c041 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Chalifour?= Date: Thu, 26 Aug 2021 11:02:00 +0200 Subject: [PATCH 10/10] docs: add Reshape params TSDoc --- .../src/types/AutocompleteReshape.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/autocomplete-core/src/types/AutocompleteReshape.ts b/packages/autocomplete-core/src/types/AutocompleteReshape.ts index be7881c34..ceb7e7d0c 100644 --- a/packages/autocomplete-core/src/types/AutocompleteReshape.ts +++ b/packages/autocomplete-core/src/types/AutocompleteReshape.ts @@ -16,7 +16,18 @@ export type Reshape< TItem extends BaseItem, TState extends AutocompleteState = AutocompleteState > = (params: { + /** + * The resolved sources provided by [`getSources`](https://www.algolia.com/doc/ui-libraries/autocomplete/core-concepts/sources/#param-getsources) + */ sources: Array>; + /** + * The resolved sources grouped by [`sourceId`](https://www.algolia.com/doc/ui-libraries/autocomplete/core-concepts/sources/#param-sourceid)s + */ sourcesBySourceId: AutocompleteReshapeSourcesBySourceId; + /** + * The current Autocomplete state. + * + * @link https://www.algolia.com/doc/ui-libraries/autocomplete/core-concepts/state + */ state: TState; }) => Array>;