Skip to content

Commit

Permalink
Merge pull request #51237 from software-mansion-labs/@szymczak/autoco…
Browse files Browse the repository at this point in the history
…mplete-search-router

Provide autocomplete suggestions in SearchRouterList
  • Loading branch information
luacmartins authored Oct 29, 2024
2 parents 9f92c5e + f66dfee commit 94fd9b5
Show file tree
Hide file tree
Showing 13 changed files with 412 additions and 87 deletions.
5 changes: 5 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5804,6 +5804,11 @@ const CONST = {
IN: 'in',
},
EMPTY_VALUE: 'none',
SEARCH_ROUTER_ITEM_TYPE: {
CONTEXTUAL_SUGGESTION: 'contextualSuggestion',
AUTOCOMPLETE_SUGGESTION: 'autocompleteSuggestion',
SEARCH: 'searchItem',
},
},

REFERRER: {
Expand Down
3 changes: 1 addition & 2 deletions src/components/Search/SearchPageHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,8 @@ function HeaderWrapper({icon, children, text, value, isCannedQuery, onSubmit, se
<View style={styles.pr5}>
<SearchRouterInput
value={value}
setValue={setValue}
onSubmit={onSubmit}
updateSearch={() => {}}
updateSearch={setValue}
autoFocus={false}
isFullWidth
wrapperStyle={[styles.searchRouterInputResults, styles.br2]}
Expand Down
275 changes: 222 additions & 53 deletions src/components/Search/SearchRouter/SearchRouter.tsx

Large diffs are not rendered by default.

11 changes: 1 addition & 10 deletions src/components/Search/SearchRouter/SearchRouterInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@ type SearchRouterInputProps = {
/** Value of TextInput */
value: string;

/** Setter to TextInput value */
setValue: (searchTerm: string) => void;

/** Callback to update search in SearchRouter */
updateSearch: (searchTerm: string) => void;

Expand Down Expand Up @@ -58,7 +55,6 @@ type SearchRouterInputProps = {

function SearchRouterInput({
value,
setValue,
updateSearch,
onSubmit = () => {},
routerListRef,
Expand All @@ -78,11 +74,6 @@ function SearchRouterInput({
const {isOffline} = useNetwork();
const offlineMessage: string = isOffline && shouldShowOfflineMessage ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : '';

const onChangeText = (text: string) => {
setValue(text);
updateSearch(text);
};

const inputWidth = isFullWidth ? styles.w100 : {width: variables.popoverWidth};

return (
Expand All @@ -92,7 +83,7 @@ function SearchRouterInput({
<TextInput
testID="search-router-text-input"
value={value}
onChangeText={onChangeText}
onChangeText={updateSearch}
autoFocus={autoFocus}
shouldDelayFocus={shouldDelayFocus}
loadingSpinnerStyle={[styles.mt0, styles.mr2]}
Expand Down
90 changes: 69 additions & 21 deletions src/components/Search/SearchRouter/SearchRouterList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import Navigation from '@libs/Navigation/Navigation';
import Performance from '@libs/Performance';
import {getAllTaxRates} from '@libs/PolicyUtils';
import type {OptionData} from '@libs/ReportUtils';
import {trimSearchQueryForAutocomplete} from '@libs/SearchAutocompleteUtils';
import * as SearchQueryUtils from '@libs/SearchQueryUtils';
import * as Report from '@userActions/Report';
import Timing from '@userActions/Timing';
Expand All @@ -25,29 +26,37 @@ import ROUTES from '@src/ROUTES';

type ItemWithQuery = {
query: string;
id?: string;
text?: string;
};

type SearchRouterListProps = {
/** currentQuery value computed coming from parsed TextInput value */
currentQuery: SearchQueryJSON | undefined;
/** value of TextInput */
textInputValue: string;

/** Callback to update text input value along with autocomplete suggestions */
updateSearchValue: (newValue: string) => void;

/** Callback to update text input value */
setTextInputValue: (text: string) => void;

/** Recent searches */
recentSearches: Array<ItemWithQuery & {timestamp: string}> | undefined;

/** Recent reports */
recentReports: OptionData[];

/** Autocomplete items */
autocompleteItems: ItemWithQuery[] | undefined;

/** Callback to submit query when selecting a list item */
onSearchSubmit: (query: SearchQueryJSON | undefined) => void;

/** Context present when opening SearchRouter from a report, invoice or workspace page */
reportForContextualSearch?: OptionData;

/** Callback to update search query when selecting contextual suggestion */
updateUserSearchQuery: (newSearchQuery: string) => void;

/** Callback to close and clear SearchRouter */
closeAndClearRouter: () => void;
closeRouter: () => void;
};

const setPerformanceTimersEnd = () => {
Expand Down Expand Up @@ -91,7 +100,7 @@ function SearchRouterItem(props: UserListItemProps<OptionData> | SearchQueryList
}

function SearchRouterList(
{currentQuery, reportForContextualSearch, recentSearches, recentReports, onSearchSubmit, updateUserSearchQuery, closeAndClearRouter}: SearchRouterListProps,
{textInputValue, updateSearchValue, setTextInputValue, reportForContextualSearch, recentSearches, autocompleteItems, recentReports, onSearchSubmit, closeRouter}: SearchRouterListProps,
ref: ForwardedRef<SelectionListHandle>,
) {
const styles = useThemeStyles();
Expand All @@ -104,21 +113,22 @@ function SearchRouterList(
const [cardList = {}] = useOnyx(ONYXKEYS.CARD_LIST);
const sections: Array<SectionListDataType<OptionData | SearchQueryItem>> = [];

if (currentQuery?.inputQuery) {
if (textInputValue) {
sections.push({
data: [
{
text: currentQuery?.inputQuery,
text: textInputValue,
singleIcon: Expensicons.MagnifyingGlass,
query: currentQuery?.inputQuery,
query: textInputValue,
itemStyle: styles.activeComponentBG,
keyForList: 'findItem',
searchItemType: CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.SEARCH,
},
],
});
}

if (reportForContextualSearch && !currentQuery?.inputQuery) {
if (reportForContextualSearch && !textInputValue) {
sections.push({
data: [
{
Expand All @@ -127,23 +137,38 @@ function SearchRouterList(
query: getContextualSearchQuery(reportForContextualSearch.reportID),
itemStyle: styles.activeComponentBG,
keyForList: 'contextualSearch',
isContextualSearchItem: true,
searchItemType: CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.CONTEXTUAL_SUGGESTION,
},
],
});
}

const autocompleteData = autocompleteItems?.map(({text, query}) => {
return {
text,
singleIcon: Expensicons.MagnifyingGlass,
query,
keyForList: query,
searchItemType: CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.AUTOCOMPLETE_SUGGESTION,
};
});

if (autocompleteData && autocompleteData.length > 0) {
sections.push({title: translate('search.suggestions'), data: autocompleteData});
}

const recentSearchesData = recentSearches?.map(({query, timestamp}) => {
const searchQueryJSON = SearchQueryUtils.buildSearchQueryJSON(query);
return {
text: searchQueryJSON ? SearchQueryUtils.buildUserReadableQueryString(searchQueryJSON, personalDetails, cardList, reports, taxRates) : query,
singleIcon: Expensicons.History,
query,
keyForList: timestamp,
searchItemType: CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.SEARCH,
};
});

if (!currentQuery?.inputQuery && recentSearchesData && recentSearchesData.length > 0) {
if (!textInputValue && recentSearchesData && recentSearchesData.length > 0) {
sections.push({title: translate('search.recentSearches'), data: recentSearchesData});
}

Expand All @@ -153,30 +178,51 @@ function SearchRouterList(
const onSelectRow = useCallback(
(item: OptionData | SearchQueryItem) => {
if (isSearchQueryItem(item)) {
if (item.isContextualSearchItem) {
// Handle selection of "Contextual search suggestion"
updateUserSearchQuery(`${item?.query} ${currentQuery?.inputQuery ?? ''}`);
if (!item?.query) {
return;
}

// Handle selection of "Recent search"
if (!item?.query) {
if (item?.searchItemType === CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.CONTEXTUAL_SUGGESTION) {
updateSearchValue(`${item?.query} `);
return;
}
if (item?.searchItemType === CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.AUTOCOMPLETE_SUGGESTION && textInputValue) {
const trimmedUserSearchQuery = trimSearchQueryForAutocomplete(textInputValue);
updateSearchValue(`${trimmedUserSearchQuery}${item?.query} `);
return;
}

onSearchSubmit(SearchQueryUtils.buildSearchQueryJSON(item?.query));
}

// Handle selection of "Recent chat"
closeAndClearRouter();
closeRouter();
if ('reportID' in item && item?.reportID) {
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(item?.reportID));
} else if ('login' in item) {
Report.navigateToAndOpenReport(item.login ? [item.login] : [], false);
}
},
[closeAndClearRouter, onSearchSubmit, currentQuery, updateUserSearchQuery],
[closeRouter, textInputValue, onSearchSubmit, updateSearchValue],
);

const onArrowFocus = useCallback(
(focusedItem: OptionData | SearchQueryItem) => {
if (!isSearchQueryItem(focusedItem) || focusedItem?.searchItemType !== CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.AUTOCOMPLETE_SUGGESTION || !textInputValue) {
return;
}
const trimmedUserSearchQuery = trimSearchQueryForAutocomplete(textInputValue);
setTextInputValue(`${trimmedUserSearchQuery}${focusedItem?.query} `);
},
[setTextInputValue, textInputValue],
);

const getItemHeight = useCallback((item: OptionData | SearchQueryItem) => {
if (isSearchQueryItem(item)) {
return 44;
}
return 64;
}, []);

return (
<SelectionList<OptionData | SearchQueryItem>
sections={sections}
Expand All @@ -185,11 +231,13 @@ function SearchRouterList(
containerStyle={[styles.mh100]}
sectionListStyle={[shouldUseNarrowLayout ? styles.ph5 : styles.ph2, styles.pb2]}
listItemWrapperStyle={[styles.pr3, styles.pl3]}
getItemHeight={getItemHeight}
onLayout={setPerformanceTimersEnd}
ref={ref}
showScrollIndicator={!shouldUseNarrowLayout}
sectionTitleStyles={styles.mhn2}
shouldSingleExecuteRowSelect
onArrowFocus={onArrowFocus}
/>
);
}
Expand Down
14 changes: 14 additions & 0 deletions src/components/Search/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,18 @@ type SearchQueryJSON = {
flatFilters: QueryFilters;
} & SearchQueryAST;

type AutocompleteRange = {
key: ValueOf<typeof CONST.SEARCH.SYNTAX_FILTER_KEYS & typeof CONST.SEARCH.SYNTAX_ROOT_KEYS>;
length: number;
start: number;
value: string;
};

type SearchAutocompleteResult = {
autocomplete: AutocompleteRange | null;
ranges: AutocompleteRange[];
};

export type {
SelectedTransactionInfo,
SelectedTransactions,
Expand All @@ -98,4 +110,6 @@ export type {
InvoiceSearchStatus,
TripSearchStatus,
ChatSearchStatus,
SearchAutocompleteResult,
AutocompleteRange,
};
5 changes: 5 additions & 0 deletions src/components/SelectionList/BaseSelectionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ function BaseSelectionList<TItem extends ListItem>(
updateCellsBatchingPeriod = 50,
removeClippedSubviews = true,
shouldDelayFocus = true,
onArrowFocus = () => {},
shouldUpdateFocusedIndex = false,
onLongPressRow,
shouldShowTextInput = !!textInputLabel || !!textInputIconLeft,
Expand Down Expand Up @@ -281,6 +282,10 @@ function BaseSelectionList<TItem extends ListItem>(
disabledIndexes: disabledArrowKeyIndexes,
isActive: isFocused,
onFocusedIndexChange: (index: number) => {
const focusedItem = flattenedSections.allOptions.at(index);
if (focusedItem) {
onArrowFocus(focusedItem);
}
scrollToIndex(index, true);
},
isFocused,
Expand Down
4 changes: 3 additions & 1 deletion src/components/SelectionList/Search/SearchQueryListItem.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import React from 'react';
import {View} from 'react-native';
import type {ValueOf} from 'type-fest';
import Icon from '@components/Icon';
import BaseListItem from '@components/SelectionList/BaseListItem';
import type {ListItem} from '@components/SelectionList/types';
import TextWithTooltip from '@components/TextWithTooltip';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import type CONST from '@src/CONST';
import type IconAsset from '@src/types/utils/IconAsset';

type SearchQueryItem = ListItem & {
singleIcon?: IconAsset;
query?: string;
isContextualSearchItem?: boolean;
searchItemType?: ValueOf<typeof CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE>;
};

type SearchQueryListItemProps = {
Expand Down
3 changes: 3 additions & 0 deletions src/components/SelectionList/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,9 @@ type BaseSelectionListProps<TItem extends ListItem> = Partial<ChildrenProps> & {
/** Whether focus event should be delayed */
shouldDelayFocus?: boolean;

/** Callback to fire when the text input changes */
onArrowFocus?: (focusedItem: TItem) => void;

/** Whether to show the loading indicator for new options */
isLoadingNewOptions?: boolean;

Expand Down
1 change: 1 addition & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4355,6 +4355,7 @@ const translations = {
recentChats: 'Recent chats',
searchIn: 'Search in',
searchPlaceholder: 'Search for something',
suggestions: 'Suggestions',
},
genericErrorPage: {
title: 'Uh-oh, something went wrong!',
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4402,6 +4402,7 @@ const translations = {
recentChats: 'Chats recientes',
searchIn: 'Buscar en',
searchPlaceholder: 'Busca algo',
suggestions: 'Sugerencias',
},
genericErrorPage: {
title: '¡Oh-oh, algo salió mal!',
Expand Down
Loading

0 comments on commit 94fd9b5

Please sign in to comment.