-
Notifications
You must be signed in to change notification settings - Fork 329
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(core): introduce Reshape API #647
Changes from all commits
a8505cd
8e4ef0f
12ae2db
c36da9c
d197428
13a1608
f61f432
74d5a4e
785936e
b6db429
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 dedupeAndLimitSuggestions = 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 ( | ||
<Fragment> | ||
<span className="aa-SourceHeaderTitle">{name}</span> | ||
<div className="aa-SourceHeaderLine" /> | ||
</Fragment> | ||
); | ||
}, | ||
}, | ||
}; | ||
}, | ||
}); | ||
|
||
autocomplete({ | ||
container: '#autocomplete', | ||
placeholder: 'Search', | ||
debug: true, | ||
openOnFocus: true, | ||
plugins: [recentSearchesPlugin, querySuggestionsPlugin, productsPlugin], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why is products a plugin and not a regular source? could indicate more "regular" usage? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this gives me the idea if longer term plugins and sources maybe could be merged into a single argument? plugins could have no source, and it would allow you to order before using reshape already There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I wanted to focus the example on the reshape API, and not pollute the file with a huge source. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
You mean something like this? autocomplete({
// ...
getSources() {
return [
recentSearchesPlugin,
querySuggestionsPlugin,
{
// custom source
}
]
}
}) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was more thinking of I was just wondering if we really need to distinguish between the two |
||
reshape({ sourcesBySourceId }) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. reasonable name! |
||
const { | ||
recentSearchesPlugin, | ||
querySuggestionsPlugin, | ||
products, | ||
...rest | ||
} = sourcesBySourceId; | ||
|
||
return [ | ||
dedupeAndLimitSuggestions(recentSearchesPlugin, querySuggestionsPlugin), | ||
groupByCategory(products), | ||
Object.values(rest), | ||
]; | ||
}, | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { | ||
AutocompleteReshapeSource, | ||
BaseItem, | ||
} from '@algolia/autocomplete-core'; | ||
|
||
export type AutocompleteReshapeFunction<TParams = any> = < | ||
Haroenv marked this conversation as resolved.
Show resolved
Hide resolved
|
||
TItem extends BaseItem | ||
>( | ||
...params: TParams[] | ||
) => ( | ||
...expressions: Array<AutocompleteReshapeSource<TItem>> | ||
) => Array<AutocompleteReshapeSource<TItem>>; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
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<TItem> | ||
> = { | ||
getSource(params: { name: string; items: TItem[] }): Partial<TSource>; | ||
}; | ||
|
||
export const groupBy: AutocompleteReshapeFunction = < | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since we've pulled Ramda already in this example, I'd use it there instead of rolling our own There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See #647 (comment). |
||
TItem extends BaseItem, | ||
TSource extends AutocompleteSource<TItem> = AutocompleteSource<TItem> | ||
>( | ||
predicate: (value: TItem) => string, | ||
options: GroupByOptions<TItem, TSource> | ||
) => { | ||
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<Record<string, TItem[]>>((acc, item) => { | ||
const key = predicate(item as TItem); | ||
|
||
if (!acc.hasOwnProperty(key)) { | ||
acc[key] = []; | ||
} | ||
|
||
acc[key].push(item as TItem); | ||
|
||
return acc; | ||
}, {}); | ||
|
||
return Object.entries(groupedItems).map(([groupName, groupItems]) => { | ||
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, | ||
}, | ||
}; | ||
}); | ||
}; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export * from './groupBy'; | ||
export * from './limit'; | ||
export * from './uniqBy'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { AutocompleteReshapeFunction } from './AutocompleteReshapeFunction'; | ||
import { normalizeReshapeSources } from './normalizeReshapeSources'; | ||
|
||
export const limit: AutocompleteReshapeFunction<number> = (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 sourceLimit = isLastSource | ||
? sharedLimitRemaining | ||
: Math.min(limitPerSource, sharedLimitRemaining); | ||
const items = source.getItems().slice(0, sourceLimit); | ||
sharedLimitRemaining = Math.max(sharedLimitRemaining - items.length, 0); | ||
|
||
return { | ||
...source, | ||
getItems() { | ||
return items; | ||
}, | ||
}; | ||
}); | ||
}; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<TItem extends BaseItem>( | ||
sarahdayan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
sources: Array<AutocompleteReshapeSource<TItem>> | ||
) { | ||
return flatten(sources).filter(Boolean); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import { | ||
AutocompleteReshapeSource, | ||
BaseItem, | ||
} from '@algolia/autocomplete-core'; | ||
|
||
import { AutocompleteReshapeFunction } from './AutocompleteReshapeFunction'; | ||
import { normalizeReshapeSources } from './normalizeReshapeSources'; | ||
|
||
type UniqByPredicate<TItem extends BaseItem> = (params: { | ||
source: AutocompleteReshapeSource<TItem>; | ||
item: TItem; | ||
}) => TItem; | ||
|
||
export const uniqBy: AutocompleteReshapeFunction<UniqByPredicate<any>> = < | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since we've pulled Ramda already in this example, I'd use it there instead of rolling our own There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not really a fan of using ramda in the examples, I don't think it's so common, and might cause people to include it just because our example does so There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Besides, it will not make it possible for them to copy/paste. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @francoischalifour You recommended that I go with Ramda's Anyway it's no biggie and not blocking. We can discuss that later. |
||
TItem extends BaseItem | ||
>( | ||
predicate | ||
) => { | ||
return function runUniqBy(...rawSources) { | ||
const sources = normalizeReshapeSources(rawSources); | ||
const seen = new Set<TItem>(); | ||
|
||
return sources.map((source) => { | ||
const items = source.getItems().filter((item) => { | ||
const appliedItem = predicate({ source, item }); | ||
const hasSeen = seen.has(appliedItem); | ||
|
||
seen.add(appliedItem); | ||
|
||
return !hasSeen; | ||
}); | ||
|
||
return { | ||
...source, | ||
getItems() { | ||
return items; | ||
}, | ||
}; | ||
}); | ||
}; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
<html lang="en"> | ||
<head> | ||
<meta charset="UTF-8" /> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
|
||
<link rel="shortcut icon" href="favicon.png" type="image/x-icon" /> | ||
<link rel="stylesheet" href="style.css" /> | ||
|
||
<title>Reshape API | Autocomplete</title> | ||
</head> | ||
|
||
<body> | ||
<div class="container"> | ||
<div id="autocomplete"></div> | ||
</div> | ||
|
||
<script src="env.ts"></script> | ||
<script src="app.tsx"></script> | ||
</body> | ||
</html> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
] | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like that it's not needed to have pipe builtin