Skip to content

Commit dfad59b

Browse files
authored
Merge pull request Expensify#82784 from abbasifaizan70/82304
82304: Search - The workspace with the same name is not displayed in the autocomplete
2 parents d501480 + bbb732d commit dfad59b

16 files changed

Lines changed: 264 additions & 33 deletions

src/components/Search/SearchAutocompleteList.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {getEmptyObject} from '@src/types/utils/EmptyObject';
3939
import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
4040
import type {SearchQueryItem, SearchQueryListItemProps} from './SearchList/ListItem/SearchQueryListItem';
4141
import SearchQueryListItem, {isSearchQueryItem} from './SearchList/ListItem/SearchQueryListItem';
42+
import type {SubstitutionMap} from './SearchRouter/getQueryWithSubstitutions';
4243
import {getSubstitutionMapKey} from './SearchRouter/getQueryWithSubstitutions';
4344
import type {UserFriendlyKey} from './types';
4445

@@ -71,6 +72,8 @@ type SearchAutocompleteListProps = {
7172
/** Ref for the external text input */
7273
textInputRef?: RefObject<AnimatedTextInputRef | null>;
7374

75+
/** Map of display values to actual IDs for filters (e.g. workspace name -> policy ID). Used to exclude by ID when multiple options share the same name. */
76+
autocompleteSubstitutions?: SubstitutionMap;
7477
/** Reference to the outer element */
7578
ref?: ForwardedRef<SelectionListWithSectionsHandle>;
7679
};
@@ -133,6 +136,7 @@ function SearchAutocompleteList({
133136
shouldSubscribeToArrowKeyEvents = true,
134137
onHighlightFirstItem,
135138
textInputRef,
139+
autocompleteSubstitutions,
136140
ref,
137141
}: SearchAutocompleteListProps) {
138142
const styles = useThemeStyles();
@@ -300,6 +304,7 @@ function SearchAutocompleteList({
300304
personalDetails,
301305
feedKeysWithCards,
302306
translate,
307+
autocompleteSubstitutions,
303308
});
304309

305310
const autocompleteQueryWithoutFilters = getQueryWithoutFilters(autocompleteQueryValue);

src/components/Search/SearchPageHeader/SearchPageInputNarrow.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ function SearchPageInputNarrow({queryJSON, searchRouterListVisible, hideSearchRo
8383
searchQueryItem={searchQueryItem}
8484
onListItemPress={onListItemPress}
8585
textInputRef={textInputRef}
86+
autocompleteSubstitutions={autocompleteSubstitutions}
8687
/>
8788
)}
8889
</View>

src/components/Search/SearchPageHeader/SearchPageInputWide.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ function SearchPageInputWide({queryJSON, handleSearch}: SearchPageInputWideProps
102102
ref={listRef}
103103
shouldSubscribeToArrowKeyEvents={isAutocompleteListVisible}
104104
textInputRef={textInputRef}
105+
autocompleteSubstitutions={autocompleteSubstitutions}
105106
/>
106107
</View>
107108
)}

src/components/Search/SearchPageHeader/useSearchPageInput.tsx

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import {deepEqual} from 'fast-equals';
2-
import isEmpty from 'lodash/isEmpty';
32
import {useEffect, useRef, useState} from 'react';
43
import type {TextInputKeyPressEvent} from 'react-native';
54
import {usePersonalDetails} from '@components/OnyxListItemProvider';
@@ -10,6 +9,7 @@ import {buildSubstitutionsMap} from '@components/Search/SearchRouter/buildSubsti
109
import {getQueryWithSubstitutions} from '@components/Search/SearchRouter/getQueryWithSubstitutions';
1110
import type {SubstitutionMap} from '@components/Search/SearchRouter/getQueryWithSubstitutions';
1211
import {getUpdatedSubstitutionsMap} from '@components/Search/SearchRouter/getUpdatedSubstitutionsMap';
12+
import updateAutocompleteSubstitutionsForSelection from '@components/Search/SearchRouter/updateAutocompleteSubstitutionsForSelection';
1313
import type {SearchQueryJSON, SearchQueryString} from '@components/Search/types';
1414
import useFeedKeysWithAssignedCards from '@hooks/useFeedKeysWithAssignedCards';
1515
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
@@ -155,7 +155,7 @@ function useSearchPageInput({queryJSON, onSearch, onSubmit}: UseSearchPageInputP
155155
setAutocompleteQueryValue(updatedUserQuery);
156156

157157
const updatedSubstitutionsMap = getUpdatedSubstitutionsMap(singleLineUserQuery, autocompleteSubstitutions);
158-
if (!deepEqual(autocompleteSubstitutions, updatedSubstitutionsMap) && !isEmpty(updatedSubstitutionsMap)) {
158+
if (!deepEqual(autocompleteSubstitutions, updatedSubstitutionsMap)) {
159159
setAutocompleteSubstitutions(updatedSubstitutionsMap);
160160
}
161161
}
@@ -174,10 +174,15 @@ function useSearchPageInput({queryJSON, onSearch, onSubmit}: UseSearchPageInputP
174174
onSearchQueryChange(newSearchQuery);
175175
setSelection({start: newSearchQuery.length, end: newSearchQuery.length});
176176

177-
if (item.mapKey && item.autocompleteID) {
178-
const substitutions = {...autocompleteSubstitutions, [item.mapKey]: item.autocompleteID};
179-
setAutocompleteSubstitutions(substitutions);
180-
}
177+
updateAutocompleteSubstitutionsForSelection({
178+
newSearchQuery,
179+
fieldKey,
180+
mapKey: item.mapKey,
181+
searchQuery: item.searchQuery,
182+
autocompleteID: item.autocompleteID,
183+
substitutions: autocompleteSubstitutions,
184+
setAutocompleteSubstitutions,
185+
});
181186
} else if (item.searchItemType === CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.SEARCH) {
182187
submitSearch(item.searchQuery, item.keyForList !== CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.FIND_ITEM);
183188
}

src/components/Search/SearchRouter/SearchRouter.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import type {SubstitutionMap} from './getQueryWithSubstitutions';
4747
import {getQueryWithSubstitutions} from './getQueryWithSubstitutions';
4848
import {getUpdatedSubstitutionsMap} from './getUpdatedSubstitutionsMap';
4949
import {getContextualReportData, getContextualSearchAutocompleteKey, getContextualSearchQuery} from './SearchRouterUtils';
50+
import updateAutocompleteSubstitutionsForSelection from './updateAutocompleteSubstitutionsForSelection';
5051

5152
const privateIsArchivedSelector = (nvp: {private_isArchived?: string} | undefined): boolean | undefined => !!nvp?.private_isArchived;
5253

@@ -296,10 +297,15 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla
296297
onSearchQueryChange(newSearchQuery, true);
297298
setSelection({start: newSearchQuery.length, end: newSearchQuery.length});
298299

299-
if (item.mapKey && item.autocompleteID) {
300-
const substitutions = {...autocompleteSubstitutions, [item.mapKey]: item.autocompleteID};
301-
setAutocompleteSubstitutions(substitutions);
302-
}
300+
updateAutocompleteSubstitutionsForSelection({
301+
newSearchQuery,
302+
fieldKey,
303+
mapKey: item.mapKey,
304+
searchQuery: item.searchQuery,
305+
autocompleteID: item.autocompleteID,
306+
substitutions: autocompleteSubstitutions,
307+
setAutocompleteSubstitutions,
308+
});
303309
setFocusAndScrollToRight();
304310
} else {
305311
submitSearch(item.searchQuery, item.keyForList !== CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.FIND_ITEM);
@@ -385,6 +391,7 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla
385391
onHighlightFirstItem={updateAndScrollToFocusedIndex}
386392
ref={listRef}
387393
textInputRef={textInputRef}
394+
autocompleteSubstitutions={autocompleteSubstitutions}
388395
/>
389396
</View>
390397
);

src/components/Search/SearchRouter/buildSubstitutionsMap.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import type {OnyxCollection} from 'react-native-onyx';
22
import type {LocalizedTranslate} from '@components/LocaleContextProvider';
3-
import type {SearchAutocompleteQueryRange, SearchFilterKey} from '@components/Search/types';
3+
import type {SearchAutocompleteQueryRange} from '@components/Search/types';
44
import {parse} from '@libs/SearchParser/autocompleteParser';
55
import {getFilterDisplayValue} from '@libs/SearchQueryUtils';
66
import CONST from '@src/CONST';
77
import type {CardFeeds, CardList, PersonalDetailsList, Policy, Report, ReportAttributesDerivedValue} from '@src/types/onyx';
88
import type {SubstitutionMap} from './getQueryWithSubstitutions';
9-
10-
const getSubstitutionsKey = (filterKey: SearchFilterKey, value: string) => `${filterKey}:${value}`;
9+
import {getSubstitutionMapKey, getSubstitutionMapKeyWithIndex} from './getQueryWithSubstitutions';
1110

1211
/**
1312
* Given a plaintext query and specific entities data,
@@ -44,6 +43,8 @@ function buildSubstitutionsMap(
4443
return {};
4544
}
4645

46+
const substitutionKeyOccurrences = new Map<string, number>();
47+
4748
const substitutionsMap = searchAutocompleteQueryRanges.reduce((map, range) => {
4849
const {key: filterKey, value: filterValue} = range;
4950

@@ -56,7 +57,10 @@ function buildSubstitutionsMap(
5657
const taxRateNames = taxRates.length > 0 ? taxRates : [taxRateID];
5758
const uniqueTaxRateNames = [...new Set(taxRateNames)];
5859
for (const taxRateName of uniqueTaxRateNames) {
59-
const substitutionKey = getSubstitutionsKey(filterKey, taxRateName);
60+
const substitutionBaseKey = getSubstitutionMapKey(filterKey, taxRateName);
61+
const occurrenceIndex = substitutionKeyOccurrences.get(substitutionBaseKey) ?? 0;
62+
substitutionKeyOccurrences.set(substitutionBaseKey, occurrenceIndex + 1);
63+
const substitutionKey = getSubstitutionMapKeyWithIndex(filterKey, taxRateName, occurrenceIndex);
6064

6165
// eslint-disable-next-line no-param-reassign
6266
map[substitutionKey] = taxRateID;
@@ -89,7 +93,10 @@ function buildSubstitutionsMap(
8993

9094
// If displayValue === filterValue, then it means there is nothing to substitute, so we don't add any key to map
9195
if (displayValue !== filterValue) {
92-
const substitutionKey = getSubstitutionsKey(filterKey, displayValue);
96+
const substitutionBaseKey = getSubstitutionMapKey(filterKey, displayValue);
97+
const occurrenceIndex = substitutionKeyOccurrences.get(substitutionBaseKey) ?? 0;
98+
substitutionKeyOccurrences.set(substitutionBaseKey, occurrenceIndex + 1);
99+
const substitutionKey = getSubstitutionMapKeyWithIndex(filterKey, displayValue, occurrenceIndex);
93100
// eslint-disable-next-line no-param-reassign
94101
map[substitutionKey] = filterValue;
95102
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type {SearchAutocompleteResult} from '@components/Search/types';
2+
import {parse as parseSearchQuery} from '@libs/SearchParser/autocompleteParser';
3+
4+
function getAutocompleteSelectionSubstitutionKey(newSearchQuery: string, fieldKey: string, fallbackMapKey: string, fallbackSearchQuery: string): string {
5+
const parsed = parseSearchQuery(newSearchQuery) as SearchAutocompleteResult;
6+
const sameKeyRanges = parsed.ranges?.filter((range) => range.key === fieldKey) ?? [];
7+
const lastRange = sameKeyRanges.at(-1);
8+
const rangeValue = lastRange?.value ?? fallbackSearchQuery;
9+
const index = sameKeyRanges.filter((range) => range.value === rangeValue).length - 1;
10+
const substitutionBaseKey = `${fieldKey}:${rangeValue}`;
11+
return index <= 0 ? fallbackMapKey : `${substitutionBaseKey}:${index}`;
12+
}
13+
14+
export default getAutocompleteSelectionSubstitutionKey;

src/components/Search/SearchRouter/getQueryWithSubstitutions.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,19 @@ type SubstitutionMap = Record<string, string>;
66

77
const getSubstitutionMapKey = (filterKey: SearchFilterKey, value: string) => `${filterKey}:${value}`;
88

9+
/**
10+
* Key for the Nth occurrence of the same filter+value (e.g. multiple workspaces with the same name).
11+
* Index 0 uses the base key for backward compatibility; index > 0 uses baseKey:index.
12+
*/
13+
const getSubstitutionMapKeyWithIndex = (filterKey: SearchFilterKey, value: string, index: number) =>
14+
index === 0 ? getSubstitutionMapKey(filterKey, value) : `${getSubstitutionMapKey(filterKey, value)}:${index}`;
15+
916
/**
1017
* Given a plaintext query and a SubstitutionMap object, this function will return a transformed query where:
1118
* - any autocomplete mention in the original query will be substituted with an id taken from `substitutions` object
1219
* - anything that does not match will stay as is
20+
* - when the same filter+value appears multiple times (e.g. workspace:"A's Workspace" three times), each occurrence
21+
* is looked up with an index so multiple different IDs can be stored (baseKey for first, baseKey:1, baseKey:2, ...)
1322
*
1423
* Ex:
1524
* query: `A from:@johndoe A`
@@ -27,12 +36,26 @@ function getQueryWithSubstitutions(changedQuery: string, substitutions: Substitu
2736
return changedQuery;
2837
}
2938

39+
// Count occurrence index per (key, value) so we can look up indexed keys for duplicates (e.g. same workspace name)
40+
const keyValueCount = new Map<string, number>();
41+
const rangeIndices = searchAutocompleteQueryRanges.map((range) => {
42+
const baseKey = getSubstitutionMapKey(range.key, range.value);
43+
const index = keyValueCount.get(baseKey) ?? 0;
44+
keyValueCount.set(baseKey, index + 1);
45+
return index;
46+
});
47+
3048
let resultQuery = changedQuery;
3149
let lengthDiff = 0;
3250

33-
for (const range of searchAutocompleteQueryRanges) {
34-
const itemKey = getSubstitutionMapKey(range.key, range.value);
35-
let substitutionEntry = substitutions[itemKey];
51+
for (let i = 0; i < searchAutocompleteQueryRanges.length; i++) {
52+
const range = searchAutocompleteQueryRanges.at(i);
53+
const index = rangeIndices.at(i);
54+
if (range === undefined || index === undefined) {
55+
continue;
56+
}
57+
const itemKey = getSubstitutionMapKeyWithIndex(range.key, range.value, index);
58+
let substitutionEntry = substitutions[itemKey] ?? (index === 0 ? substitutions[getSubstitutionMapKey(range.key, range.value)] : undefined);
3659

3760
if (substitutionEntry) {
3861
const substitutionStart = range.start + lengthDiff;
@@ -48,5 +71,5 @@ function getQueryWithSubstitutions(changedQuery: string, substitutions: Substitu
4871
return resultQuery;
4972
}
5073

51-
export {getQueryWithSubstitutions, getSubstitutionMapKey};
74+
export {getQueryWithSubstitutions, getSubstitutionMapKey, getSubstitutionMapKeyWithIndex};
5275
export type {SubstitutionMap};

src/components/Search/SearchRouter/getUpdatedSubstitutionsMap.ts

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
import type {SearchAutocompleteQueryRange, SearchFilterKey} from '@components/Search/types';
1+
import type {SearchAutocompleteQueryRange} from '@components/Search/types';
22
import {parse} from '@libs/SearchParser/autocompleteParser';
33
import type {SubstitutionMap} from './getQueryWithSubstitutions';
4-
5-
const getSubstitutionsKey = (filterKey: SearchFilterKey, value: string) => `${filterKey}:${value}`;
4+
import {getSubstitutionMapKeyWithIndex} from './getQueryWithSubstitutions';
65

76
/**
87
* Given a plaintext query and a SubstitutionMap object,
9-
* this function will remove any substitution keys that do not appear in the query and return an updated object
8+
* this function will remove any substitution keys that do not appear in the query and return an updated object.
9+
* When the same filter+value appears multiple times (e.g. workspace:"A's Workspace" three times), each occurrence
10+
* is assigned an index and we preserve keys baseKey (index 0), baseKey:1, baseKey:2, ... so multiple IDs are kept.
1011
*
1112
* Ex:
1213
* query: `Test from:John1`
@@ -24,17 +25,21 @@ function getUpdatedSubstitutionsMap(query: string, substitutions: SubstitutionMa
2425
return {};
2526
}
2627

27-
const autocompleteQueryKeys = searchAutocompleteQueryRanges.map((range) => getSubstitutionsKey(range.key, range.value));
28+
// Assign occurrence index per (key, value) so we preserve indexed keys for duplicates (e.g. same workspace name)
29+
const keyValueCount = new Map<string, number>();
30+
const updatedSubstitutionMap: SubstitutionMap = {};
2831

29-
// Build a new substitutions map consisting of only the keys from old map, that appear in query
30-
const updatedSubstitutionMap = autocompleteQueryKeys.reduce((map, key) => {
31-
if (substitutions[key]) {
32-
// eslint-disable-next-line no-param-reassign
33-
map[key] = substitutions[key];
34-
}
32+
for (const range of searchAutocompleteQueryRanges) {
33+
const baseKey = `${range.key}:${range.value}`;
34+
const index = keyValueCount.get(baseKey) ?? 0;
35+
keyValueCount.set(baseKey, index + 1);
3536

36-
return map;
37-
}, {} as SubstitutionMap);
37+
const fullKey = getSubstitutionMapKeyWithIndex(range.key, range.value, index);
38+
const value = substitutions[fullKey] ?? (index === 0 ? substitutions[baseKey] : undefined);
39+
if (value) {
40+
updatedSubstitutionMap[fullKey] = value;
41+
}
42+
}
3843

3944
return updatedSubstitutionMap;
4045
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import getAutocompleteSelectionSubstitutionKey from './getAutocompleteSelectionSubstitutionKey';
2+
import type {SubstitutionMap} from './getQueryWithSubstitutions';
3+
4+
type UpdateAutocompleteSubstitutionsParams = {
5+
newSearchQuery: string;
6+
fieldKey: string | undefined;
7+
mapKey: string | undefined;
8+
searchQuery: string;
9+
autocompleteID: string | undefined;
10+
substitutions: SubstitutionMap;
11+
setAutocompleteSubstitutions: (value: SubstitutionMap) => void;
12+
};
13+
14+
function updateAutocompleteSubstitutionsForSelection({
15+
newSearchQuery,
16+
fieldKey,
17+
mapKey,
18+
searchQuery,
19+
autocompleteID,
20+
substitutions,
21+
setAutocompleteSubstitutions,
22+
}: UpdateAutocompleteSubstitutionsParams) {
23+
if (!mapKey || !autocompleteID || !fieldKey) {
24+
return;
25+
}
26+
27+
const substitutionKey = getAutocompleteSelectionSubstitutionKey(newSearchQuery, fieldKey, mapKey, searchQuery);
28+
setAutocompleteSubstitutions({...substitutions, [substitutionKey]: autocompleteID});
29+
}
30+
31+
export default updateAutocompleteSubstitutionsForSelection;

0 commit comments

Comments
 (0)