Skip to content

Commit

Permalink
Fix autocomplete for contextual search item
Browse files Browse the repository at this point in the history
  • Loading branch information
Kicu committed Oct 31, 2024
1 parent 217d562 commit e3e559b
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 21 deletions.
8 changes: 4 additions & 4 deletions src/components/Search/SearchRouter/SearchRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -356,8 +356,8 @@ function SearchRouter({onRouterClose}: SearchRouterProps) {
[autocompleteSubstitutions, onRouterClose, setTextInputValue],
);

const updateSubstitutionsMap = (key: string, value: string) => {
const substitutions = {...autocompleteSubstitutions, [key]: {value}};
const onAutocompleteSuggestionClick = (autocompleteKey: string, autocompleteId: string) => {
const substitutions = {...autocompleteSubstitutions, [autocompleteKey]: {value: autocompleteId}};

setAutocompleteSubstitutions(substitutions);
};
Expand Down Expand Up @@ -400,10 +400,10 @@ function SearchRouter({onRouterClose}: SearchRouterProps) {
reportForContextualSearch={contextualReportData}
recentSearches={sortedRecentSearches?.slice(0, 5)}
recentReports={recentReports}
autocompleteItems={autocompleteSuggestions}
autocompleteSuggestions={autocompleteSuggestions}
onSearchSubmit={onSearchSubmit}
closeRouter={onRouterClose}
onAutocompleteSuggestionClick={updateSubstitutionsMap}
onAutocompleteSuggestionClick={onAutocompleteSuggestionClick}
ref={listRef}
/>
</View>
Expand Down
42 changes: 25 additions & 17 deletions src/components/Search/SearchRouter/SearchRouterList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,28 +53,28 @@ type SearchRouterListProps = {
recentReports: OptionData[];

/** Autocomplete items */
autocompleteItems: AutocompleteItemData[] | undefined;
autocompleteSuggestions: AutocompleteItemData[] | undefined;

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

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

/** Callback to run when user clicks a suggestion item that contains autocomplete data */
onAutocompleteSuggestionClick: (autocompleteKey: string, autocompleteId: string) => void;

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

/** Callback WIP */
onAutocompleteSuggestionClick: (id: string, value: string) => void;
};

const setPerformanceTimersEnd = () => {
Timing.end(CONST.TIMING.SEARCH_ROUTER_RENDER);
Performance.markEnd(CONST.TIMING.SEARCH_ROUTER_RENDER);
};

function getContextualSearchQuery(reportID: string) {
return `${CONST.SEARCH.SYNTAX_ROOT_KEYS.TYPE}:${CONST.SEARCH.DATA_TYPES.CHAT} in:${reportID}`;
function getContextualSearchQuery(reportName: string) {
return `${CONST.SEARCH.SYNTAX_ROOT_KEYS.TYPE}:${CONST.SEARCH.DATA_TYPES.CHAT} ${CONST.SEARCH.SYNTAX_FILTER_KEYS.IN}:${SearchQueryUtils.sanitizeSearchValue(reportName)}`;
}

function isSearchQueryItem(item: OptionData | SearchQueryItem): item is SearchQueryItem {
Expand All @@ -85,6 +85,13 @@ function isSearchQueryListItem(listItem: UserListItemProps<OptionData> | SearchQ
return isSearchQueryItem(listItem.item);
}

function getItemHeight(item: OptionData | SearchQueryItem) {
if (isSearchQueryItem(item)) {
return 44;
}
return 64;
}

function SearchRouterItem(props: UserListItemProps<OptionData> | SearchQueryListItemProps) {
const styles = useThemeStyles();

Expand Down Expand Up @@ -112,7 +119,7 @@ function SearchRouterList(
setTextInputValue,
reportForContextualSearch,
recentSearches,
autocompleteItems,
autocompleteSuggestions,
recentReports,
onSearchSubmit,
onAutocompleteSuggestionClick,
Expand Down Expand Up @@ -146,12 +153,14 @@ function SearchRouterList(
}

if (reportForContextualSearch && !textInputValue) {
const reportQueryValue = reportForContextualSearch.text ?? reportForContextualSearch.alternateText ?? reportForContextualSearch.reportID;
sections.push({
data: [
{
text: `${translate('search.searchIn')} ${reportForContextualSearch.text ?? reportForContextualSearch.alternateText}`,
singleIcon: Expensicons.MagnifyingGlass,
searchQuery: getContextualSearchQuery(reportForContextualSearch.reportID),
searchQuery: reportQueryValue,
autocompleteID: reportForContextualSearch.reportID,
itemStyle: styles.activeComponentBG,
keyForList: 'contextualSearch',
searchItemType: CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.CONTEXTUAL_SUGGESTION,
Expand All @@ -160,7 +169,7 @@ function SearchRouterList(
});
}

const autocompleteData = autocompleteItems?.map(({filterKey, text, autocompleteID}) => {
const autocompleteData = autocompleteSuggestions?.map(({filterKey, text, autocompleteID}) => {
return {
text: getSubstitutionMapKey(filterKey, text),
singleIcon: Expensicons.MagnifyingGlass,
Expand Down Expand Up @@ -200,7 +209,13 @@ function SearchRouterList(
return;
}
if (item.searchItemType === CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.CONTEXTUAL_SUGGESTION) {
updateSearchValue(`${item.searchQuery} `);
const searchQuery = getContextualSearchQuery(item.searchQuery);
updateSearchValue(`${searchQuery} `);

if (item.autocompleteID) {
const autocompleteKey = `${CONST.SEARCH.SYNTAX_FILTER_KEYS.IN}:${item.searchQuery}`;
onAutocompleteSuggestionClick(autocompleteKey, item.autocompleteID);
}
return;
}
if (item.searchItemType === CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.AUTOCOMPLETE_SUGGESTION && textInputValue) {
Expand Down Expand Up @@ -243,13 +258,6 @@ function SearchRouterList(
[setTextInputValue, textInputValue, onAutocompleteSuggestionClick],
);

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

return (
<SelectionList<OptionData | SearchQueryItem>
sections={sections}
Expand Down
12 changes: 12 additions & 0 deletions src/components/Search/SearchRouter/getQueryWithSubstitutions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@ type SubstitutionMap = Record<string, SubstitutionEntry>;

const getSubstitutionMapKey = (filterName: string, value: string) => `${filterName}:${value}`;

/**
* Given a plaintext query and a SubstitutionMap object, this function will return a transformed query where:
* - any autocomplete mention in the original query will be substituted with an id taken from `substitutions` object
* - anything that does not match will stay as is
*
* Ex:
* query: `A from:@johndoe A`
* substitutions: {
* from:@johndoe: 9876
* }
* return: `A from:9876 A`
*/
function getQueryWithSubstitutions(changedQuery: string, substitutions: SubstitutionMap) {
const parsed = parser.parse(changedQuery) as {ranges: SearchAutocompleteQueryRange[]};

Expand Down
11 changes: 11 additions & 0 deletions src/components/Search/SearchRouter/getUpdatedSubstitutionsMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ import type {SubstitutionMap} from './getQueryWithSubstitutions';

const getSubstitutionsKey = (filterName: string, value: string) => `${filterName}:${value}`;

/**
* Given a plaintext query and a SubstitutionMap object,
* this function will remove any substitution keys that do not appear in the query and return an updated object
*
* Ex:
* query: `Test from:John1`
* substitutions: {
* from:SomeOtherJohn: 12345
* }
* return: {}
*/
function getUpdatedSubstitutionsMap(query: string, substitutions: SubstitutionMap): SubstitutionMap {
const parsedQuery = parser.parse(query) as {ranges: SearchAutocompleteQueryRange[]};

Expand Down
23 changes: 23 additions & 0 deletions src/libs/SearchAutocompleteUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import type {Policy, PolicyCategories, PolicyTagLists, RecentlyUsedCategories, R
import {getTagNamesFromTagsLists} from './PolicyUtils';
import * as autocompleteParser from './SearchParser/autocompleteParser';

/**
* Parses given query using the autocomplete parser.
* This is a smaller and simpler version of search parser used for autocomplete displaying logic.
*/
function parseForAutocomplete(text: string) {
try {
const parsedAutocomplete = autocompleteParser.parse(text) as SearchAutocompleteResult;
Expand All @@ -14,6 +18,9 @@ function parseForAutocomplete(text: string) {
}
}

/**
* Returns data for computing the `Tag` filter autocomplete list.
*/
function getAutocompleteTags(allPoliciesTagsLists: OnyxCollection<PolicyTagLists>, policyID?: string) {
const singlePolicyTagsList: PolicyTagLists | undefined = allPoliciesTagsLists?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`];
if (!singlePolicyTagsList) {
Expand All @@ -28,6 +35,9 @@ function getAutocompleteTags(allPoliciesTagsLists: OnyxCollection<PolicyTagLists
return getTagNamesFromTagsLists(singlePolicyTagsList);
}

/**
* Returns data for computing the recent tags autocomplete list.
*/
function getAutocompleteRecentTags(allRecentTags: OnyxCollection<RecentlyUsedTags>, policyID?: string) {
const singlePolicyRecentTags: RecentlyUsedTags | undefined = allRecentTags?.[`${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS}${policyID}`];
if (!singlePolicyRecentTags) {
Expand All @@ -41,6 +51,9 @@ function getAutocompleteRecentTags(allRecentTags: OnyxCollection<RecentlyUsedTag
return Object.values(singlePolicyRecentTags ?? {}).flat(2);
}

/**
* Returns data for computing the `Category` filter autocomplete list.
*/
function getAutocompleteCategories(allPolicyCategories: OnyxCollection<PolicyCategories>, policyID?: string) {
const singlePolicyCategories = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`];
if (!singlePolicyCategories) {
Expand All @@ -51,6 +64,9 @@ function getAutocompleteCategories(allPolicyCategories: OnyxCollection<PolicyCat
return Object.values(singlePolicyCategories ?? {}).map((category) => category.name);
}

/**
* Returns data for computing the recent categories autocomplete list.
*/
function getAutocompleteRecentCategories(allRecentCategories: OnyxCollection<RecentlyUsedCategories>, policyID?: string) {
const singlePolicyRecentCategories = allRecentCategories?.[`${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES}${policyID}`];
if (!singlePolicyRecentCategories) {
Expand All @@ -61,6 +77,13 @@ function getAutocompleteRecentCategories(allRecentCategories: OnyxCollection<Rec
return Object.values(singlePolicyRecentCategories ?? {}).map((category) => category);
}

/**
* Returns data for computing the `Tax` filter autocomplete list
*
* Please note: taxes are stored in a quite convoluted and non-obvious way, and there can be multiple taxes with the same id
* because tax ids are generated based on a tax name, so they look like this: `id_My_Tax` and are not numeric.
* That is why this function may seem a bit complex.
*/
function getAutocompleteTaxList(taxRates: Record<string, string[]>, policy?: OnyxEntry<Policy>) {
if (policy) {
const policyTaxes = policy?.taxRates?.taxes ?? {};
Expand Down

0 comments on commit e3e559b

Please sign in to comment.