Skip to content

Commit

Permalink
Merge branch 'master' into feat/insights-dashboard-support
Browse files Browse the repository at this point in the history
  • Loading branch information
dhayab authored Oct 9, 2023
2 parents b9ba0b7 + 2258d89 commit 6be8e14
Show file tree
Hide file tree
Showing 12 changed files with 272 additions and 37 deletions.
4 changes: 2 additions & 2 deletions packages/instantsearch.js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@
],
"dependencies": {
"@algolia/events": "^4.0.1",
"@algolia/ui-components-highlight-vdom": "^1.2.1",
"@algolia/ui-components-shared": "^1.2.1",
"@algolia/ui-components-highlight-vdom": "^1.2.2",
"@algolia/ui-components-shared": "^1.2.2",
"@types/dom-speech-recognition": "^0.0.1",
"@types/google.maps": "^3.45.3",
"@types/hogan.js": "^3.0.0",
Expand Down
36 changes: 36 additions & 0 deletions packages/instantsearch.js/src/lib/InstantSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,27 @@ export type InstantSearchOptions<
* @deprecated This property will be still supported in 4.x releases, but not further. It is replaced by the `insights` middleware. For more information, visit https://www.algolia.com/doc/guides/getting-insights-and-analytics/search-analytics/click-through-and-conversions/how-to/send-click-and-conversion-events-with-instantsearch/js/
*/
insightsClient?: AlgoliaInsightsClient;
future?: {
/**
* Changes the way `dispose` is used in InstantSearch lifecycle.
*
* If `false` (by default), each widget unmounting will remove its state as well, even if there are multiple widgets reading that UI State.
*
* If `true`, each widget unmounting will only remove its own state if it's the last of its type. This allows for dynamically adding and removing widgets without losing their state.
*
* @default false
*/
// @MAJOR: Remove legacy behaviour
preserveSharedStateOnUnmount?: boolean;
};
};

export type InstantSearchStatus = 'idle' | 'loading' | 'stalled' | 'error';

export const INSTANTSEARCH_FUTURE_DEFAULTS: Required<
InstantSearchOptions['future']
> = { preserveSharedStateOnUnmount: false };

/**
* The actual implementation of the InstantSearch. This is
* created using the `instantsearch` factory function.
Expand All @@ -176,6 +193,7 @@ class InstantSearch<
public insightsClient: AlgoliaInsightsClient | null;
public onStateChange: InstantSearchOptions<TUiState>['onStateChange'] | null =
null;
public future: NonNullable<InstantSearchOptions<TUiState>['future']>;
public helper: AlgoliaSearchHelper | null;
public mainHelper: AlgoliaSearchHelper | null;
public mainIndex: IndexWidget;
Expand Down Expand Up @@ -236,6 +254,10 @@ Use \`InstantSearch.status === "stalled"\` instead.`
searchClient = null,
insightsClient = null,
onStateChange = null,
future = {
...INSTANTSEARCH_FUTURE_DEFAULTS,
...(options.future || {}),
},
} = options;

if (searchClient === null) {
Expand Down Expand Up @@ -284,7 +306,21 @@ See ${createDocumentationLink({
})}`
);

if (__DEV__ && options.future?.preserveSharedStateOnUnmount === undefined) {
// eslint-disable-next-line no-console
console.info(`Starting from the next major version, InstantSearch will change how widgets state is preserved when they are removed. InstantSearch will keep the state of unmounted widgets to be usable by other widgets with the same attribute.
We recommend setting \`future.preserveSharedStateOnUnmount\` to true to adopt this change today.
To stay with the current behaviour and remove this warning, set the option to false.
See documentation: ${createDocumentationLink({
name: 'instantsearch',
})}#widget-param-future
`);
}

this.client = searchClient;
this.future = future;
this.insightsClient = insightsClient;
this.indexName = indexName;
this.helper = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ describe('insights', () => {
}

const getUserToken = () =>
(instantSearchInstance.helper!.state as PlainSearchParameters).userToken;
(instantSearchInstance.mainHelper!.state as PlainSearchParameters)
.userToken;

return {
analytics,
Expand Down Expand Up @@ -94,10 +95,11 @@ describe('insights', () => {
});
instantSearchInstance.start();

const helper = instantSearchInstance.helper!;
const helper = instantSearchInstance.mainHelper!;

const getUserToken = () =>
(instantSearchInstance.helper!.state as PlainSearchParameters).userToken;
(instantSearchInstance.mainHelper!.state as PlainSearchParameters)
.userToken;

return {
analytics,
Expand Down Expand Up @@ -473,7 +475,7 @@ See documentation: https://www.algolia.com/doc/guides/building-search-ui/going-f
insightsClient,
})
);
expect(instantSearchInstance.helper!.state.clickAnalytics).toBe(true);
expect(instantSearchInstance.mainHelper!.state.clickAnalytics).toBe(true);
});

it('does not apply clickAnalytics if $$automatic: true', () => {
Expand All @@ -494,9 +496,9 @@ See documentation: https://www.algolia.com/doc/guides/building-search-ui/going-f
const middleware = createInsightsMiddleware({
insightsClient,
})({ instantSearchInstance });
instantSearchInstance.helper!.setPage(100);
instantSearchInstance.mainHelper!.setPage(100);
middleware.subscribe();
expect(instantSearchInstance.helper!.state.page).toBe(100);
expect(instantSearchInstance.mainHelper!.state.page).toBe(100);
});

it('adds user agent', () => {
Expand Down Expand Up @@ -689,15 +691,15 @@ See documentation: https://www.algolia.com/doc/guides/building-search-ui/going-f

insightsClient('setUserToken', 'abc');
instantSearchInstance.start();
instantSearchInstance.helper!.setPage(100);
instantSearchInstance.mainHelper!.setPage(100);

instantSearchInstance.use(
createInsightsMiddleware({
insightsClient,
})
);

expect(instantSearchInstance.helper!.state.page).toBe(100);
expect(instantSearchInstance.mainHelper!.state.page).toBe(100);
expect(getUserToken()).toEqual('abc');
});

Expand All @@ -721,7 +723,7 @@ See documentation: https://www.algolia.com/doc/guides/building-search-ui/going-f
createTestEnvironment({ started: false });

instantSearchInstance.start();
instantSearchInstance.helper!.setPage(100);
instantSearchInstance.mainHelper!.setPage(100);

instantSearchInstance.use(
createInsightsMiddleware({
Expand All @@ -733,7 +735,7 @@ See documentation: https://www.algolia.com/doc/guides/building-search-ui/going-f

await wait(0);

expect(instantSearchInstance.helper!.state.page).toEqual(100);
expect(instantSearchInstance.mainHelper!.state.page).toEqual(100);
expect(getUserToken()).toEqual('def');
});

Expand Down Expand Up @@ -793,7 +795,8 @@ See documentation: https://www.algolia.com/doc/guides/building-search-ui/going-f
await wait(0);

expect(
(instantSearchInstance.helper!.state as PlainSearchParameters).userToken
(instantSearchInstance.mainHelper!.state as PlainSearchParameters)
.userToken
).toEqual('def');
});

Expand Down Expand Up @@ -1202,7 +1205,7 @@ See documentation: https://www.algolia.com/doc/guides/building-search-ui/going-f

instantSearchInstance
.helper!.setState({
...instantSearchInstance.helper!.state,
...instantSearchInstance.mainHelper!.state,
query: 'test',
})
.search();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ export function createInsightsMiddleware<
started() {
insightsClient('addAlgoliaAgent', 'insights-middleware');

helper = instantSearchInstance.helper!;
helper = instantSearchInstance.mainHelper!;

initialParameters = {
userToken: (helper.state as PlainSearchParameters).userToken,
Expand Down
151 changes: 151 additions & 0 deletions packages/instantsearch.js/src/widgets/index/__tests__/index-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,157 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/index-widge
).toHaveBeenCalledTimes(2);
});

it('cleans shared refinements when `preserveSharedStateOnUnmount` is unset', () => {
const instance = index({ indexName: 'indexName' });
const instantSearchInstance = createInstantSearch();

const refinementList1 = virtualRefinementList({
attribute: 'brand',
});

const refinementList2 = virtualRefinementList({
attribute: 'brand',
});

instance.addWidgets([refinementList1, refinementList2]);

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

// Simulate a state change
instance.getHelper()!.addDisjunctiveFacetRefinement('brand', 'Apple');

expect(instance.getHelper()!.state).toEqual(
new SearchParameters({
index: 'indexName',
maxValuesPerFacet: 10,
disjunctiveFacets: ['brand'],
disjunctiveFacetsRefinements: {
brand: ['Apple'],
},
})
);

instance.removeWidgets([refinementList2]);

expect(instance.getHelper()!.state).toEqual(
new SearchParameters({
index: 'indexName',
maxValuesPerFacet: 10,
disjunctiveFacets: ['brand'],
disjunctiveFacetsRefinements: {
brand: [],
},
})
);
});

it('cleans shared refinements when `preserveSharedStateOnUnmount` is false', () => {
const instance = index({ indexName: 'indexName' });
const instantSearchInstance = createInstantSearch({
future: { preserveSharedStateOnUnmount: false },
});

const refinementList1 = virtualRefinementList({
attribute: 'brand',
});

const refinementList2 = virtualRefinementList({
attribute: 'brand',
});

instance.addWidgets([refinementList1, refinementList2]);

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

// Simulate a state change
instance.getHelper()!.addDisjunctiveFacetRefinement('brand', 'Apple');

expect(instance.getHelper()!.state).toEqual(
new SearchParameters({
index: 'indexName',
maxValuesPerFacet: 10,
disjunctiveFacets: ['brand'],
disjunctiveFacetsRefinements: {
brand: ['Apple'],
},
})
);

instance.removeWidgets([refinementList2]);

expect(instance.getHelper()!.state).toEqual(
new SearchParameters({
index: 'indexName',
maxValuesPerFacet: 10,
disjunctiveFacets: ['brand'],
disjunctiveFacetsRefinements: {
brand: [],
},
})
);
});

it('preserves shared refinements when `preserveSharedStateOnUnmount` is true', () => {
const instance = index({ indexName: 'indexName' });
const instantSearchInstance = createInstantSearch({
future: { preserveSharedStateOnUnmount: true },
});

const refinementList1 = virtualRefinementList({
attribute: 'brand',
});

const refinementList2 = virtualRefinementList({
attribute: 'brand',
});

instance.addWidgets([refinementList1, refinementList2]);

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

// Simulate a state change
instance.getHelper()!.addDisjunctiveFacetRefinement('brand', 'Apple');

expect(instance.getHelper()!.state).toEqual(
new SearchParameters({
index: 'indexName',
maxValuesPerFacet: 10,
disjunctiveFacets: ['brand'],
disjunctiveFacetsRefinements: {
brand: ['Apple'],
},
})
);

instance.removeWidgets([refinementList2]);

expect(instance.getHelper()!.state).toEqual(
new SearchParameters({
index: 'indexName',
maxValuesPerFacet: 10,
disjunctiveFacets: ['brand'],
disjunctiveFacetsRefinements: {
brand: ['Apple'],
},
})
);
});

it('calls `dispose` on the removed widgets', () => {
const instance = index({ indexName: 'indexName' });
const widgets = [
Expand Down
Loading

0 comments on commit 6be8e14

Please sign in to comment.