Skip to content
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(recommend): introduce connectRelatedProducts connector #6142

Merged
merged 64 commits into from
Apr 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
440e9cc
chore: update algoliasearch version
dhayab Mar 28, 2024
81b4400
feat(helper): add methods to request and retrieve recommendations
dhayab Mar 28, 2024
0eac184
add tests
dhayab Mar 29, 2024
98018d8
emit placeholder change event and test events better
dhayab Mar 29, 2024
84937b3
Merge branch 'master' into feat/helper-recommend-methods
dhayab Mar 29, 2024
6f064f8
bump bundle sizes
dhayab Mar 29, 2024
9469f26
Merge branch 'master' into feat/helper-recommend-methods
dhayab Apr 2, 2024
2cc4743
retrieve recommend queries on all index widgets through derived helpers
dhayab Apr 3, 2024
2a70204
add tests for derived helper
dhayab Apr 3, 2024
2518d91
bump bundlesize
dhayab Apr 3, 2024
7ab1436
feat(recommend): add FBT to instantsearch-ui-components
raed667 Apr 3, 2024
d326cab
fix: split component files to fix type errors
raed667 Apr 4, 2024
60f8c9c
fix babel spread issue
raed667 Apr 4, 2024
ab5dd33
replace classes auc with ais
raed667 Apr 4, 2024
e8544c6
fix: address feedback
raed667 Apr 4, 2024
7ade025
fix: address feedback by adding tests and TS types
raed667 Apr 4, 2024
e041a2f
wip: map recommend results to widgets
aymeric-giraudet Apr 4, 2024
8813c64
remove comment
raed667 Apr 4, 2024
60a9ef6
Merge branch 'feat/map-recommend-results' into feat-recommend-fbt-ui-…
raed667 Apr 5, 2024
60f052b
fix: results
aymeric-giraudet Apr 5, 2024
c90ef99
fix: typescript
aymeric-giraudet Apr 5, 2024
418cffb
fix types
aymeric-giraudet Apr 5, 2024
5c69418
bundlesize
aymeric-giraudet Apr 5, 2024
46c2531
Merge branch 'master' into feat/map-recommend-results
aymeric-giraudet Apr 5, 2024
deab535
Merge branch 'feat/map-recommend-results' into feat-recommend-fbt-ui-…
raed667 Apr 5, 2024
c65c703
wip: map recommend results to widgets
aymeric-giraudet Apr 4, 2024
859c5ba
fix: results
aymeric-giraudet Apr 5, 2024
3785724
fix: typescript
aymeric-giraudet Apr 5, 2024
ea2259d
fix types
aymeric-giraudet Apr 5, 2024
6eab8a3
bundlesize
aymeric-giraudet Apr 5, 2024
61ba925
Merge branch 'master' into feat/map-recommend-results
aymeric-giraudet Apr 5, 2024
49f9efe
fix
aymeric-giraudet Apr 5, 2024
6c1dc58
Merge branch 'feat/map-recommend-results' into feat-recommend-fbt-ui-…
raed667 Apr 5, 2024
f1eb001
fix linit issue
raed667 Apr 5, 2024
8375968
wip
sarahdayan Apr 8, 2024
b37546e
wip
sarahdayan Apr 8, 2024
cb00cee
fix: fix build
sarahdayan Apr 8, 2024
44b40fe
fix: add __position
sarahdayan Apr 8, 2024
853d96a
Update packages/algoliasearch-helper/src/algoliasearch.helper.js
raed667 Apr 8, 2024
f9c44d2
add support and types for sendEvent
dhayab Apr 9, 2024
4dbc64c
fix tests
dhayab Apr 9, 2024
a62a979
fix: fix issues with sendEvent and test it
sarahdayan Apr 9, 2024
30d0f35
fix: stop passing createElement and Fragment to ItemComponent
sarahdayan Apr 9, 2024
087f1f3
style: lint
sarahdayan Apr 10, 2024
f0d5299
feat(instantsearch-ui-components): introduce `RelatedProducts` component
sarahdayan Apr 10, 2024
a9b846b
feat: remove default click event
sarahdayan Apr 15, 2024
5a7ed9e
feat(instantsearch-ui-components): introduce `FrequentlyBoughtTogethe…
raed667 Apr 15, 2024
3d8c469
feat(instantsearch-ui-components): introduce `RelatedProducts` compon…
sarahdayan Apr 15, 2024
af7670f
feat(recommend): introduce `connectRelatedProducts` connector
sarahdayan Apr 17, 2024
bf90f6d
Merge branch 'feat/map-recommend-results' into feat/recommend-rp-conn…
sarahdayan Apr 17, 2024
c7a0117
fix: fix build
sarahdayan Apr 17, 2024
619599a
chore: increase bundle size
sarahdayan Apr 17, 2024
708da27
test: fix tests
sarahdayan Apr 18, 2024
aa02fa2
chore: update bundlesize
sarahdayan Apr 18, 2024
135ddd4
fix: fix types
sarahdayan Apr 18, 2024
b12219a
fix: fix type
sarahdayan Apr 18, 2024
e06aeeb
test: fix import
sarahdayan Apr 18, 2024
c027f15
chore: update bundle size
sarahdayan Apr 19, 2024
c41de5a
Merge branch 'feat/map-recommend-results' into feat/recommend-rp-conn…
sarahdayan Apr 19, 2024
4a15d5f
fix: fix merge conflict
sarahdayan Apr 19, 2024
0b4c876
fix: fix type issue
sarahdayan Apr 22, 2024
402f8c1
fix: handle review comments
sarahdayan Apr 22, 2024
54ef18f
feat: switch to objectIDs
sarahdayan Apr 22, 2024
fdf0f7d
fix: fix things
sarahdayan Apr 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions bundlesize.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,19 @@
},
{
"path": "./packages/instantsearch.js/dist/instantsearch.production.min.js",
"maxSize": "77.75 kB"
"maxSize": "78 kB"
},
{
"path": "./packages/instantsearch.js/dist/instantsearch.development.js",
"maxSize": "171 kB"
"maxSize": "171.5 kB"
},
{
"path": "packages/react-instantsearch-core/dist/umd/ReactInstantSearchCore.min.js",
"maxSize": "47.5 kB"
"maxSize": "47.75 kB"
},
{
"path": "packages/react-instantsearch/dist/umd/ReactInstantSearch.min.js",
"maxSize": "59.25 kB"
"maxSize": "59.5 kB"
},
{
"path": "packages/vue-instantsearch/vue2/umd/index.js",
Expand Down
27 changes: 27 additions & 0 deletions packages/instantsearch.js/src/__tests__/common-connectors.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
connectRatingMenu,
connectRefinementList,
connectToggleRefinement,
connectRelatedProducts,
} from '../connectors';
import instantsearch from '../index.es';
import { refinementList } from '../widgets';
Expand Down Expand Up @@ -392,6 +393,31 @@ const testSetups: TestSetupsMap<TestSuites> = {
})
.start();
},
createRelatedProductsConnectorTests({ instantSearchOptions, widgetParams }) {
const customRelatedProducts = connectRelatedProducts<{
container: HTMLElement;
}>((renderOptions) => {
renderOptions.widgetParams.container.innerHTML = `
<ul>${renderOptions.recommendations
.map((recommendation) => `<li>${recommendation.objectID}</li>`)
.join('')}</ul>`;
});

instantsearch(instantSearchOptions)
.addWidgets([
customRelatedProducts({
container: document.body.appendChild(document.createElement('div')),
...widgetParams,
}),
])
.on('error', () => {
/*
* prevent rethrowing InstantSearch errors, so tests can be asserted.
* IRL this isn't needed, as the error doesn't stop execution.
*/
})
.start();
},
};

const testOptions: TestOptionsMap<TestSuites> = {
Expand All @@ -405,6 +431,7 @@ const testOptions: TestOptionsMap<TestSuites> = {
createNumericMenuConnectorTests: undefined,
createRatingMenuConnectorTests: undefined,
createToggleRefinementConnectorTests: undefined,
createRelatedProductsConnectorTests: undefined,
};

describe('Common connector tests (InstantSearch.js)', () => {
Expand Down
1 change: 1 addition & 0 deletions packages/instantsearch.js/src/connectors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export { default as connectNumericMenu } from './numeric-menu/connectNumericMenu
export { default as connectPagination } from './pagination/connectPagination';
export { default as connectRange } from './range/connectRange';
export { default as connectRefinementList } from './refinement-list/connectRefinementList';
export { default as connectRelatedProducts } from './related-products/connectRelatedProducts';
sarahdayan marked this conversation as resolved.
Show resolved Hide resolved
export { default as connectSearchBox } from './search-box/connectSearchBox';
export { default as connectSortBy } from './sort-by/connectSortBy';
export { default as connectRatingMenu } from './rating-menu/connectRatingMenu';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import {
createDocumentationMessageGenerator,
checkRendering,
noop,
} from '../../lib/utils';

import type { Connector, TransformItems, Hit, BaseHit } from '../../types';
import type {
PlainSearchParameters,
RecommendResultItem,
} from 'algoliasearch-helper';

const withUsage = createDocumentationMessageGenerator({
name: 'related-products',
connector: true,
});

export type RelatedProductsRenderState<THit extends BaseHit = BaseHit> = {
/**
* The matched recommendations from the Algolia API.
*/
recommendations: Array<Hit<THit>>;
sarahdayan marked this conversation as resolved.
Show resolved Hide resolved
};

export type RelatedProductsConnectorParams<THit extends BaseHit = BaseHit> = {
/**
* The `objectIDs` of the items to get related products from.
*/
objectIDs: string[];
/**
* The number of recommendations to retrieve.
*/
maxRecommendations?: number;
/**
* The threshold for the recommendations confidence score (between 0 and 100).
*/
threshold?: number;
/**
* List of search parameters to send.
*/
fallbackParameters?: Omit<
PlainSearchParameters,
'page' | 'hitsPerPage' | 'offset' | 'length'
>;
/**
* List of search parameters to send.
*/
queryParameters?: Omit<
PlainSearchParameters,
'page' | 'hitsPerPage' | 'offset' | 'length'
>;
/**
* Function to transform the items passed to the templates.
*/
transformItems?: TransformItems<Hit<THit>, { results: RecommendResultItem }>;
};

export type RelatedProductsWidgetDescription<THit extends BaseHit = BaseHit> = {
$$type: 'ais.relatedProducts';
renderState: RelatedProductsRenderState<THit>;
};

export type RelatedProductsConnector<THit extends BaseHit = BaseHit> =
Connector<
RelatedProductsWidgetDescription<THit>,
RelatedProductsConnectorParams<THit>
>;

const connectRelatedProducts: RelatedProductsConnector =
function connectRelatedProducts(renderFn, unmountFn = noop) {
checkRendering(renderFn, withUsage());

return function relatedProducts(widgetParams) {
const {
objectIDs,
maxRecommendations,
threshold,
fallbackParameters,
queryParameters,
transformItems = ((items) => items) as NonNullable<
RelatedProductsConnectorParams['transformItems']
>,
} = widgetParams || {};

if (!objectIDs || objectIDs.length === 0) {
throw new Error(withUsage('The `objectIDs` option is required.'));
}

return {
dependsOn: 'recommend',
$$type: 'ais.relatedProducts',

init(initOptions) {
renderFn(
{
...this.getWidgetRenderState(initOptions),
instantSearchInstance: initOptions.instantSearchInstance,
},
true
);
},

render(renderOptions) {
const renderState = this.getWidgetRenderState(renderOptions);

renderFn(
{
...renderState,
instantSearchInstance: renderOptions.instantSearchInstance,
},
false
);
},

getRenderState(renderState) {
return renderState;
},

getWidgetRenderState({ results }) {
if (results === null || results === undefined) {
return { recommendations: [], widgetParams };
}

return {
recommendations: transformItems(results.hits, {
results: results as RecommendResultItem,
}),
widgetParams,
};
},

dispose({ state }) {
unmountFn();

return state;
},

getWidgetParameters(state) {
// We only use the first `objectID` to get the recommendations for
// until we implement support for multiple `objectIDs` in the helper.
const objectID = objectIDs[0];

return state.addRelatedProducts({
objectID,
maxRecommendations,
threshold,
fallbackParameters,
queryParameters,
$$id: this.$$id!,
Haroenv marked this conversation as resolved.
Show resolved Hide resolved
});
},
};
};
};

export default connectRelatedProducts;
11 changes: 11 additions & 0 deletions packages/instantsearch.js/src/lib/utils/addWidgetId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { Widget } from '../../types';

let id = 0;

export function addWidgetId(widget: Widget) {
sarahdayan marked this conversation as resolved.
Show resolved Hide resolved
if (widget.dependsOn !== 'recommend') {
return;
}

widget.$$id = id++;
}
2 changes: 1 addition & 1 deletion packages/instantsearch.js/src/types/widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ type RecommendWidget<
TWidgetDescription extends WidgetDescription & WidgetParams
> = {
dependsOn: 'recommend';
$$id: number;
$$id?: number;
getWidgetParameters: (
state: RecommendParameters,
widgetParametersOptions: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,17 +109,19 @@ describe('index', () => {
...args,
});

const createFrequentlyBoughtTogether = (args: Partial<Widget> = {}): Widget =>
const createFrequentlyBoughtTogether = (
args: Partial<Widget> = {}
): Widget & { $$id: number } =>
createWidget({
dependsOn: 'recommend',
getWidgetParameters: jest.fn((parameters) => {
getWidgetParameters(parameters) {
return parameters.addFrequentlyBoughtTogether({
$$id: 1,
$$id: this.$$id!,
objectID: 'abc',
});
}),
},
...args,
} as unknown as Widget);
} as Widget) as unknown as Widget & { $$id: number };

const virtualSearchBox = connectSearchBox(() => {});
const virtualPagination = connectPagination(() => {});
Expand Down Expand Up @@ -226,6 +228,17 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/index-widge
`);
});

it('adds generated `$$id` to widgets that depend on `recommend`', () => {
const instance = index({ indexName: 'indexName' });
const fbt1 = createFrequentlyBoughtTogether({});
const fbt2 = createFrequentlyBoughtTogether({});

instance.addWidgets([fbt1, fbt2]);

expect(fbt1.$$id).toBe(0);
expect(fbt2.$$id).toBe(1);
});

describe('with a started instance', () => {
it('updates the internal state with added widgets', () => {
const instance = index({ indexName: 'indexName' });
Expand Down Expand Up @@ -270,9 +283,10 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/index-widge
instance.init(createIndexInitOptions({ parent: null }));

const fbt = createFrequentlyBoughtTogether({});
const getWidgetParameters = jest.spyOn(fbt, 'getWidgetParameters');
instance.addWidgets([fbt]);

expect(fbt.getWidgetParameters).toHaveBeenCalledTimes(1);
expect(getWidgetParameters).toHaveBeenCalledTimes(1);
});

it('calls getWidgetParameters after init on widgets that depend on search and implement the function', () => {
Expand Down Expand Up @@ -1216,11 +1230,12 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/index-widge
const instance = index({ indexName: 'indexName' });

const fbt = createFrequentlyBoughtTogether({});
const getWidgetParameters = jest.spyOn(fbt, 'getWidgetParameters');
instance.addWidgets([fbt]);

instance.init(createIndexInitOptions({ parent: null }));

expect(fbt.getWidgetParameters).toHaveBeenCalledTimes(1);
expect(getWidgetParameters).toHaveBeenCalledTimes(1);
});

it('calls getWidgetParameters on widgets that depend on search and implement the function', () => {
Expand Down Expand Up @@ -2978,9 +2993,9 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/index-widge
});

const fbt = createFrequentlyBoughtTogether({
$$id: 1,
dependsOn: 'recommend',
shouldRender: () => true,
render: jest.fn(),
});
instance.addWidgets([fbt]);

Expand Down
11 changes: 10 additions & 1 deletion packages/instantsearch.js/src/widgets/index/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
createInitArgs,
createRenderArgs,
} from '../../lib/utils';
import { addWidgetId } from '../../lib/utils/addWidgetId';

import type {
InstantSearch,
Expand Down Expand Up @@ -306,7 +307,7 @@ const index = (widgetParams: IndexWidgetParams): IndexWidget => {
if (
widget.dependsOn !== 'recommend' ||
isIndexWidget(widget) ||
!widget.$$id
widget.$$id === undefined
) {
return this.getResults();
}
Expand Down Expand Up @@ -376,6 +377,14 @@ const index = (widgetParams: IndexWidgetParams): IndexWidget => {
);
}

widgets.forEach((widget) => {
if (isIndexWidget(widget)) {
return;
}

addWidgetId(widget);
});

localWidgets = localWidgets.concat(widgets);

if (localInstantSearchInstance && Boolean(widgets.length)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ const testSetups: TestSetupsMap<TestSuites> = {
</InstantSearch>
);
},
createRelatedProductsConnectorTests: () => {},
};

const testOptions: TestOptionsMap<TestSuites> = {
Expand All @@ -332,6 +333,12 @@ const testOptions: TestOptionsMap<TestSuites> = {
createNumericMenuConnectorTests: { act },
createRatingMenuConnectorTests: { act },
createToggleRefinementConnectorTests: { act },
createRelatedProductsConnectorTests: {
act,
skippedTests: {
options: true,
},
},
};

describe('Common connector tests (React InstantSearch)', () => {
Expand Down
Loading