Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { IndexUiState, SearchToolInput } from '../../types';

export function generateIndexUiState(
input: SearchToolInput,
currentUiState: IndexUiState
): IndexUiState {
const indexUiState: IndexUiState = { ...currentUiState };

if (input.query) {
indexUiState.query = input.query;
}

if (input.facet_filters) {
let uiStateToUpdate: 'refinementList' | 'menu' | null = null;

if (indexUiState.refinementList) {
uiStateToUpdate = 'refinementList';
} else if (indexUiState.menu) {
uiStateToUpdate = 'menu';
}
Comment on lines +13 to +20
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

have you considered something like index.getWidgetSearchParameters( currentUiState)?


if (uiStateToUpdate) {
input.facet_filters.forEach((facetFilter) => {
facetFilter.forEach((filter) => {
const [facet, value] = filter.split(':');

if (indexUiState[uiStateToUpdate]?.[facet]) {
indexUiState[uiStateToUpdate]![facet] = [];

Check warning on line 28 in packages/instantsearch.js/src/lib/utils/getStateFromSearchToolInput.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

packages/instantsearch.js/src/lib/utils/getStateFromSearchToolInput.ts#L28

Bracket object notation with user input is present, this might allow an attacker to access all properties of the object and even it's prototype.
(indexUiState[uiStateToUpdate]![facet] as string[]).push(value);
}
});
});
}
}

return indexUiState;
}
1 change: 1 addition & 0 deletions packages/instantsearch.js/src/lib/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,4 @@ export * from './safelyRunOnBrowser';
export * from './serializer';
export * from './toArray';
export * from './uniq';
export * from './getStateFromSearchToolInput';
6 changes: 6 additions & 0 deletions packages/instantsearch.js/src/types/widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,3 +377,9 @@ export type SortBy<TItem> =
* Creates the URL for the given value.
*/
export type CreateURL<TValue> = (value: TValue) => string;

export type SearchToolInput = {
query: string;
number_of_results?: number;
facet_filters?: string[][];
};
205 changes: 4 additions & 201 deletions packages/instantsearch.js/src/widgets/chat/chat.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
/** @jsx h */

import {
ArrowRightIcon,
ChevronLeftIcon,
ChevronRightIcon,
createButtonComponent,
createChatComponent,
} from 'instantsearch-ui-components';
import { createChatComponent } from 'instantsearch-ui-components';
import { Fragment, h, render } from 'preact';
import { useMemo } from 'preact/hooks';

import TemplateComponent from '../../components/Template/Template';
import connectChat from '../../connectors/chat/connectChat';
Expand All @@ -19,7 +12,8 @@ import {
getContainerNode,
createDocumentationMessageGenerator,
} from '../../lib/utils';
import { carousel } from '../../templates';

import { createCarouselTool } from './search-index-tool';

import type {
ChatRenderState,
Expand Down Expand Up @@ -70,197 +64,6 @@ function getDefinedProperties<T extends object>(obj: T): Partial<T> {
) as Partial<T>;
}

function createCarouselTool<
THit extends RecordWithObjectID = RecordWithObjectID
>(
showViewAll: boolean,
templates: ChatTemplates<THit>,
getSearchPageURL?: (nextUiState: IndexUiState) => string
): UserClientSideToolWithTemplate {
const Button = createButtonComponent({
createElement: h,
});

function SearchLayoutComponent({
message,
indexUiState,
setIndexUiState,
onClose,
}: ClientSideToolComponentProps) {
const input = message?.input as
| {
query: string;
number_of_results?: number;
}
| undefined;

const output = message?.output as
| {
hits?: Array<RecordWithObjectID<THit>>;
nbHits?: number;
}
| undefined;

const items = output?.hits || [];

const MemoedHeaderComponent = useMemo(() => {
return (
props: Omit<
ComponentProps<typeof HeaderComponent>,
| 'nbHits'
| 'query'
| 'hitsPerPage'
| 'setIndexUiState'
| 'indexUiState'
| 'getSearchPageURL'
| 'onClose'
>
) => (
<HeaderComponent
nbHits={output?.nbHits}
query={input?.query}
hitsPerPage={items.length}
setIndexUiState={setIndexUiState}
indexUiState={indexUiState}
getSearchPageURL={getSearchPageURL}
onClose={onClose}
{...props}
/>
);
}, [
items.length,
input?.query,
output?.nbHits,
setIndexUiState,
indexUiState,
onClose,
]);

return carousel({
showNavigation: false,
templates: {
header: MemoedHeaderComponent,
},
})({
items,
templates: {
item: ({ item }) => (
<TemplateComponent
templates={templates}
templateKey="item"
data={item}
rootTagName="fragment"
/>
),
},
sendEvent: () => {},
});
}

function HeaderComponent({
canScrollLeft,
canScrollRight,
scrollLeft,
scrollRight,
nbHits,
query,
hitsPerPage,
setIndexUiState,
indexUiState,
onClose,
// eslint-disable-next-line no-shadow
getSearchPageURL,
}: {
canScrollLeft: boolean;
canScrollRight: boolean;
scrollLeft: () => void;
scrollRight: () => void;
nbHits?: number;
query?: string;
hitsPerPage?: number;
setIndexUiState: IndexWidget['setIndexUiState'];
indexUiState: IndexUiState;
onClose: () => void;
getSearchPageURL?: (nextUiState: IndexUiState) => string;
}) {
if ((hitsPerPage ?? 0) < 1) {
return null;
}

return (
<div className="ais-ChatToolSearchIndexCarouselHeader">
<div className="ais-ChatToolSearchIndexCarouselHeaderResults">
{nbHits && (
<div className="ais-ChatToolSearchIndexCarouselHeaderCount">
{hitsPerPage ?? 0} of {nbHits.toLocaleString()} result
{nbHits > 1 ? 's' : ''}
</div>
)}
{showViewAll && (
<Button
variant="ghost"
size="sm"
onClick={() => {
if (!query) return;

const nextUiState = { ...indexUiState, query };

// If no main search page URL or we are on the search page, just update the state
if (
!getSearchPageURL ||
(getSearchPageURL &&
new URL(getSearchPageURL(nextUiState)).pathname ===
window.location.pathname)
) {
setIndexUiState(nextUiState);
onClose();
return;
}

// Navigate to different page
window.location.href = getSearchPageURL(nextUiState);
}}
className="ais-ChatToolSearchIndexCarouselHeaderViewAll"
>
View all
<ArrowRightIcon createElement={h} />
</Button>
)}
</div>

{(hitsPerPage ?? 0) > 2 && (
<div className="ais-ChatToolSearchIndexCarouselHeaderScrollButtons">
<Button
variant="outline"
size="sm"
iconOnly
onClick={scrollLeft}
disabled={!canScrollLeft}
className="ais-ChatToolSearchIndexCarouselHeaderScrollButton"
>
<ChevronLeftIcon createElement={h} />
</Button>
<Button
variant="outline"
size="sm"
iconOnly
onClick={scrollRight}
disabled={!canScrollRight}
className="ais-ChatToolSearchIndexCarouselHeaderScrollButton"
>
<ChevronRightIcon createElement={h} />
</Button>
</div>
)}
</div>
);
}

return {
templates: { layout: SearchLayoutComponent },
};
}

function createDefaultTools<
THit extends RecordWithObjectID = RecordWithObjectID
>(
Expand Down Expand Up @@ -857,7 +660,7 @@ export type UserClientSideToolTemplates = Partial<{
layout: TemplateWithBindEvent<ClientSideToolComponentProps>;
}>;

type UserClientSideToolWithTemplate = Omit<
export type UserClientSideToolWithTemplate = Omit<
UserClientSideTool,
'layoutComponent'
> & {
Expand Down
Loading