Skip to content

Commit 0ca1c05

Browse files
authored
Merge pull request Expensify#68241 from s77rt/search-similarSearchHash
Search: Use `similarSearchHash` to highlight correct item
2 parents f899258 + 9146aef commit 0ca1c05

8 files changed

Lines changed: 121 additions & 57 deletions

File tree

src/components/Search/index.tsx

Lines changed: 7 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ import {
4242
shouldShowEmptyState,
4343
shouldShowYear as shouldShowYearUtil,
4444
} from '@libs/SearchUIUtils';
45-
import type {ArchivedReportsIDSet} from '@libs/SearchUIUtils';
45+
import type {ArchivedReportsIDSet, SearchKey} from '@libs/SearchUIUtils';
4646
import {isOnHold, isTransactionPendingDelete} from '@libs/TransactionUtils';
4747
import Navigation, {navigationRef} from '@navigation/Navigation';
4848
import type {SearchFullscreenNavigatorParamList} from '@navigation/types';
@@ -227,49 +227,19 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS
227227
const [accountID] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false, selector: (s) => s?.accountID});
228228
const suggestedSearches = useMemo(() => getSuggestedSearches(defaultCardFeed?.id, accountID), [defaultCardFeed?.id, accountID]);
229229

230-
const {type, status, sortBy, sortOrder, hash, groupBy} = queryJSON;
231-
const searchKey = useMemo(() => Object.values(suggestedSearches).find((search) => search.hash === hash)?.key, [suggestedSearches, hash]);
230+
const {type, status, sortBy, sortOrder, hash, similarSearchHash, groupBy} = queryJSON;
231+
const searchKey = useMemo(() => Object.values(suggestedSearches).find((search) => search.similarSearchHash === similarSearchHash)?.key, [suggestedSearches, similarSearchHash]);
232232

233233
const shouldCalculateTotals = useMemo(() => {
234234
if (offset !== 0) {
235235
return false;
236236
}
237-
if (queryJSON.type !== CONST.SEARCH.DATA_TYPES.EXPENSE) {
237+
if (!searchKey) {
238238
return false;
239239
}
240-
241-
let hasFeedFilter = false;
242-
let hasPostedFilter = false;
243-
let isReimbursable = false;
244-
245-
queryJSON.flatFilters.forEach((filter) => {
246-
if (filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.FEED) {
247-
hasFeedFilter = true;
248-
} else if (filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.POSTED) {
249-
hasPostedFilter = true;
250-
} else if (filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.REIMBURSABLE && filter.filters.at(0)?.value === CONST.SEARCH.BOOLEAN.YES) {
251-
isReimbursable = true;
252-
}
253-
});
254-
255-
/**
256-
* The total should be calculated for all accounting queries (statements, unapprovedCash and unapprovedCard)
257-
* We can't use `searchKey` directly because we want to also match similar queries e.g. the statements suggested search query with a custom feed should be matched too.
258-
*/
259-
const isStatementsLikeQuery = queryJSON.flatFilters.length === 2 && hasFeedFilter && hasPostedFilter;
260-
const isUnapprovedCashLikeQuery =
261-
queryJSON.flatFilters.length === 1 &&
262-
isReimbursable &&
263-
queryJSON.status[0] === CONST.SEARCH.STATUS.EXPENSE.DRAFTS &&
264-
queryJSON.status[1] === CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING;
265-
const isUnapprovedCardLikeQuery =
266-
queryJSON.flatFilters.length === 1 &&
267-
hasFeedFilter &&
268-
queryJSON.status[0] === CONST.SEARCH.STATUS.EXPENSE.DRAFTS &&
269-
queryJSON.status[1] === CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING;
270-
271-
return isStatementsLikeQuery || isUnapprovedCashLikeQuery || isUnapprovedCardLikeQuery;
272-
}, [offset, queryJSON.flatFilters, queryJSON.type, queryJSON.status]);
240+
const eligibleSearchKeys: Partial<SearchKey[]> = [CONST.SEARCH.SEARCH_KEYS.STATEMENTS, CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH, CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CARD];
241+
return eligibleSearchKeys.includes(searchKey);
242+
}, [offset, searchKey]);
273243

274244
const previousReportActions = usePrevious(reportActions);
275245
const reportActionsArray = useMemo(

src/components/Search/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,8 @@ type SearchQueryJSON = {
154154
hash: number;
155155
/** Hash used for putting queries in recent searches list. It ignores sortOrder and sortBy, because we want to treat queries differing only in sort params as the same query */
156156
recentSearchHash: number;
157+
/** Use similarSearchHash to test if two searchers are similar i.e. have same filters but not necessary same values */
158+
similarSearchHash: number;
157159
flatFilters: QueryFilters;
158160
} & SearchQueryAST;
159161

src/hooks/useSearchTypeMenu.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import useThemeStyles from './useThemeStyles';
2828
import useWindowDimensions from './useWindowDimensions';
2929

3030
export default function useSearchTypeMenu(queryJSON: SearchQueryJSON) {
31-
const {hash} = queryJSON;
31+
const {hash, similarSearchHash} = queryJSON;
3232

3333
const theme = useTheme();
3434
const styles = useThemeStyles();
@@ -130,8 +130,8 @@ export default function useSearchTypeMenu(queryJSON: SearchQueryJSON) {
130130
}
131131

132132
const flattenedMenuItems = typeMenuSections.map((section) => section.menuItems).flat();
133-
return flattenedMenuItems.findIndex((item) => item.hash === hash);
134-
}, [hash, isSavedSearchActive, typeMenuSections]);
133+
return flattenedMenuItems.findIndex((item) => item.similarSearchHash === similarSearchHash);
134+
}, [similarSearchHash, isSavedSearchActive, typeMenuSections]);
135135

136136
const popoverMenuItems = useMemo(() => {
137137
return typeMenuSections

src/libs/SearchQueryUtils.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -289,21 +289,26 @@ const customCollator = new Intl.Collator('en', {usage: 'sort', sensitivity: 'var
289289
* Computes and returns a numerical hash for a given queryJSON.
290290
* Sorts the query keys and values to ensure that hashes stay consistent.
291291
*/
292-
function getQueryHashes(query: SearchQueryJSON): {primaryHash: number; recentSearchHash: number} {
292+
function getQueryHashes(query: SearchQueryJSON): {primaryHash: number; recentSearchHash: number; similarSearchHash: number} {
293293
let orderedQuery = '';
294294
orderedQuery += `${CONST.SEARCH.SYNTAX_ROOT_KEYS.TYPE}:${query.type}`;
295295
orderedQuery += ` ${CONST.SEARCH.SYNTAX_ROOT_KEYS.STATUS}:${Array.isArray(query.status) ? query.status.join(',') : query.status}`;
296296
orderedQuery += ` ${CONST.SEARCH.SYNTAX_ROOT_KEYS.GROUP_BY}:${query.groupBy}`;
297297

298+
let similarSearchHashInput = orderedQuery;
299+
298300
query.flatFilters
299301
.map((filter) => {
302+
similarSearchHashInput += filter.key;
303+
300304
const filters = cloneDeep(filter.filters);
301305
filters.sort((a, b) => customCollator.compare(a.value.toString(), b.value.toString()));
302306
return buildFilterValuesString(filter.key, filters);
303307
})
304308
.sort()
305309
.forEach((filterString) => (orderedQuery += ` ${filterString}`));
306310

311+
const similarSearchHash = hashText(similarSearchHashInput, 2 ** 32);
307312
const recentSearchHash = hashText(orderedQuery, 2 ** 32);
308313

309314
orderedQuery += ` ${CONST.SEARCH.SYNTAX_ROOT_KEYS.SORT_BY}:${query.sortBy}`;
@@ -313,7 +318,7 @@ function getQueryHashes(query: SearchQueryJSON): {primaryHash: number; recentSea
313318
}
314319
const primaryHash = hashText(orderedQuery, 2 ** 32);
315320

316-
return {primaryHash, recentSearchHash};
321+
return {primaryHash, recentSearchHash, similarSearchHash};
317322
}
318323

319324
/**
@@ -344,9 +349,10 @@ function buildSearchQueryJSON(query: SearchQueryString) {
344349
// Add the full input and hash to the results
345350
result.inputQuery = query;
346351
result.flatFilters = flatFilters;
347-
const {primaryHash, recentSearchHash} = getQueryHashes(result);
352+
const {primaryHash, recentSearchHash, similarSearchHash} = getQueryHashes(result);
348353
result.hash = primaryHash;
349354
result.recentSearchHash = recentSearchHash;
355+
result.similarSearchHash = similarSearchHash;
350356

351357
if (result.policyID && typeof result.policyID === 'string') {
352358
// Ensure policyID is always an array for consistency

src/libs/SearchUIUtils.ts

Lines changed: 73 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -212,8 +212,10 @@ type SearchTypeMenuItem = {
212212
translationPath: TranslationPaths;
213213
type: SearchDataTypes;
214214
icon: IconAsset;
215-
hash: number;
216215
searchQuery: string;
216+
searchQueryJSON: SearchQueryJSON | undefined;
217+
hash: number;
218+
similarSearchHash: number;
217219
emptyState?: {
218220
headerMedia: DotLottieAnimation;
219221
title: TranslationPaths;
@@ -252,8 +254,14 @@ function getSuggestedSearches(defaultFeedID: string | undefined, accountID: numb
252254
type: CONST.SEARCH.DATA_TYPES.EXPENSE,
253255
icon: Expensicons.Receipt,
254256
searchQuery: buildCannedSearchQuery(),
257+
get searchQueryJSON() {
258+
return buildSearchQueryJSON(this.searchQuery);
259+
},
255260
get hash() {
256-
return buildSearchQueryJSON(this.searchQuery)?.hash ?? CONST.DEFAULT_NUMBER_ID;
261+
return this.searchQueryJSON?.hash ?? CONST.DEFAULT_NUMBER_ID;
262+
},
263+
get similarSearchHash() {
264+
return this.searchQueryJSON?.similarSearchHash ?? CONST.DEFAULT_NUMBER_ID;
257265
},
258266
},
259267
[CONST.SEARCH.SEARCH_KEYS.REPORTS]: {
@@ -262,8 +270,14 @@ function getSuggestedSearches(defaultFeedID: string | undefined, accountID: numb
262270
type: CONST.SEARCH.DATA_TYPES.EXPENSE,
263271
icon: Expensicons.Document,
264272
searchQuery: buildCannedSearchQuery({groupBy: CONST.SEARCH.GROUP_BY.REPORTS}),
273+
get searchQueryJSON() {
274+
return buildSearchQueryJSON(this.searchQuery);
275+
},
265276
get hash() {
266-
return buildSearchQueryJSON(this.searchQuery)?.hash ?? CONST.DEFAULT_NUMBER_ID;
277+
return this.searchQueryJSON?.hash ?? CONST.DEFAULT_NUMBER_ID;
278+
},
279+
get similarSearchHash() {
280+
return this.searchQueryJSON?.similarSearchHash ?? CONST.DEFAULT_NUMBER_ID;
267281
},
268282
},
269283
[CONST.SEARCH.SEARCH_KEYS.CHATS]: {
@@ -272,8 +286,14 @@ function getSuggestedSearches(defaultFeedID: string | undefined, accountID: numb
272286
type: CONST.SEARCH.DATA_TYPES.CHAT,
273287
icon: Expensicons.ChatBubbles,
274288
searchQuery: buildCannedSearchQuery({type: CONST.SEARCH.DATA_TYPES.CHAT}),
289+
get searchQueryJSON() {
290+
return buildSearchQueryJSON(this.searchQuery);
291+
},
275292
get hash() {
276-
return buildSearchQueryJSON(this.searchQuery)?.hash ?? CONST.DEFAULT_NUMBER_ID;
293+
return this.searchQueryJSON?.hash ?? CONST.DEFAULT_NUMBER_ID;
294+
},
295+
get similarSearchHash() {
296+
return this.searchQueryJSON?.similarSearchHash ?? CONST.DEFAULT_NUMBER_ID;
277297
},
278298
},
279299
[CONST.SEARCH.SEARCH_KEYS.SUBMIT]: {
@@ -282,8 +302,14 @@ function getSuggestedSearches(defaultFeedID: string | undefined, accountID: numb
282302
type: CONST.SEARCH.DATA_TYPES.EXPENSE,
283303
icon: Expensicons.Pencil,
284304
searchQuery: getTodoSearchQuery(CONST.SEARCH.SEARCH_KEYS.SUBMIT, accountID),
305+
get searchQueryJSON() {
306+
return buildSearchQueryJSON(this.searchQuery);
307+
},
285308
get hash() {
286-
return buildSearchQueryJSON(this.searchQuery)?.hash ?? CONST.DEFAULT_NUMBER_ID;
309+
return this.searchQueryJSON?.hash ?? CONST.DEFAULT_NUMBER_ID;
310+
},
311+
get similarSearchHash() {
312+
return this.searchQueryJSON?.similarSearchHash ?? CONST.DEFAULT_NUMBER_ID;
287313
},
288314
},
289315
[CONST.SEARCH.SEARCH_KEYS.APPROVE]: {
@@ -292,8 +318,14 @@ function getSuggestedSearches(defaultFeedID: string | undefined, accountID: numb
292318
type: CONST.SEARCH.DATA_TYPES.EXPENSE,
293319
icon: Expensicons.ThumbsUp,
294320
searchQuery: getTodoSearchQuery(CONST.SEARCH.SEARCH_KEYS.APPROVE, accountID),
321+
get searchQueryJSON() {
322+
return buildSearchQueryJSON(this.searchQuery);
323+
},
295324
get hash() {
296-
return buildSearchQueryJSON(this.searchQuery)?.hash ?? CONST.DEFAULT_NUMBER_ID;
325+
return this.searchQueryJSON?.hash ?? CONST.DEFAULT_NUMBER_ID;
326+
},
327+
get similarSearchHash() {
328+
return this.searchQueryJSON?.similarSearchHash ?? CONST.DEFAULT_NUMBER_ID;
297329
},
298330
},
299331
[CONST.SEARCH.SEARCH_KEYS.PAY]: {
@@ -302,8 +334,14 @@ function getSuggestedSearches(defaultFeedID: string | undefined, accountID: numb
302334
type: CONST.SEARCH.DATA_TYPES.EXPENSE,
303335
icon: Expensicons.MoneyBag,
304336
searchQuery: getTodoSearchQuery(CONST.SEARCH.SEARCH_KEYS.PAY, accountID),
337+
get searchQueryJSON() {
338+
return buildSearchQueryJSON(this.searchQuery);
339+
},
305340
get hash() {
306-
return buildSearchQueryJSON(this.searchQuery)?.hash ?? CONST.DEFAULT_NUMBER_ID;
341+
return this.searchQueryJSON?.hash ?? CONST.DEFAULT_NUMBER_ID;
342+
},
343+
get similarSearchHash() {
344+
return this.searchQueryJSON?.similarSearchHash ?? CONST.DEFAULT_NUMBER_ID;
307345
},
308346
},
309347
[CONST.SEARCH.SEARCH_KEYS.EXPORT]: {
@@ -312,8 +350,14 @@ function getSuggestedSearches(defaultFeedID: string | undefined, accountID: numb
312350
type: CONST.SEARCH.DATA_TYPES.EXPENSE,
313351
icon: Expensicons.CheckCircle,
314352
searchQuery: getTodoSearchQuery(CONST.SEARCH.SEARCH_KEYS.EXPORT, accountID),
353+
get searchQueryJSON() {
354+
return buildSearchQueryJSON(this.searchQuery);
355+
},
315356
get hash() {
316-
return buildSearchQueryJSON(this.searchQuery)?.hash ?? CONST.DEFAULT_NUMBER_ID;
357+
return this.searchQueryJSON?.hash ?? CONST.DEFAULT_NUMBER_ID;
358+
},
359+
get similarSearchHash() {
360+
return this.searchQueryJSON?.similarSearchHash ?? CONST.DEFAULT_NUMBER_ID;
317361
},
318362
},
319363
[CONST.SEARCH.SEARCH_KEYS.STATEMENTS]: {
@@ -326,8 +370,14 @@ function getSuggestedSearches(defaultFeedID: string | undefined, accountID: numb
326370
feed: defaultFeedID ? [defaultFeedID] : [''],
327371
postedOn: CONST.SEARCH.DATE_PRESETS.LAST_STATEMENT,
328372
}),
373+
get searchQueryJSON() {
374+
return buildSearchQueryJSON(this.searchQuery);
375+
},
329376
get hash() {
330-
return buildSearchQueryJSON(this.searchQuery)?.hash ?? CONST.DEFAULT_NUMBER_ID;
377+
return this.searchQueryJSON?.hash ?? CONST.DEFAULT_NUMBER_ID;
378+
},
379+
get similarSearchHash() {
380+
return this.searchQueryJSON?.similarSearchHash ?? CONST.DEFAULT_NUMBER_ID;
331381
},
332382
},
333383
[CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH]: {
@@ -340,8 +390,14 @@ function getSuggestedSearches(defaultFeedID: string | undefined, accountID: numb
340390
status: [CONST.SEARCH.STATUS.EXPENSE.DRAFTS, CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING],
341391
reimbursable: CONST.SEARCH.BOOLEAN.YES,
342392
}),
393+
get searchQueryJSON() {
394+
return buildSearchQueryJSON(this.searchQuery);
395+
},
343396
get hash() {
344-
return buildSearchQueryJSON(this.searchQuery)?.hash ?? CONST.DEFAULT_NUMBER_ID;
397+
return this.searchQueryJSON?.hash ?? CONST.DEFAULT_NUMBER_ID;
398+
},
399+
get similarSearchHash() {
400+
return this.searchQueryJSON?.similarSearchHash ?? CONST.DEFAULT_NUMBER_ID;
345401
},
346402
},
347403
[CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CARD]: {
@@ -354,8 +410,14 @@ function getSuggestedSearches(defaultFeedID: string | undefined, accountID: numb
354410
feed: defaultFeedID ? [defaultFeedID] : [''],
355411
status: [CONST.SEARCH.STATUS.EXPENSE.DRAFTS, CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING],
356412
}),
413+
get searchQueryJSON() {
414+
return buildSearchQueryJSON(this.searchQuery);
415+
},
357416
get hash() {
358-
return buildSearchQueryJSON(this.searchQuery)?.hash ?? CONST.DEFAULT_NUMBER_ID;
417+
return this.searchQueryJSON?.hash ?? CONST.DEFAULT_NUMBER_ID;
418+
},
419+
get similarSearchHash() {
420+
return this.searchQueryJSON?.similarSearchHash ?? CONST.DEFAULT_NUMBER_ID;
359421
},
360422
},
361423
};

src/pages/Search/SearchTypeMenu.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ type SearchTypeMenuProps = {
4040
};
4141

4242
function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) {
43-
const {hash} = queryJSON ?? {};
43+
const {hash, similarSearchHash} = queryJSON ?? {};
4444

4545
const styles = useThemeStyles();
4646
const {singleExecution} = useSingleExecution();
@@ -208,8 +208,8 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) {
208208
}
209209

210210
const flattenedMenuItems = typeMenuSections.map((section) => section.menuItems).flat();
211-
return flattenedMenuItems.findIndex((item) => item.hash === hash);
212-
}, [hash, isSavedSearchActive, typeMenuSections]);
211+
return flattenedMenuItems.findIndex((item) => item.similarSearchHash === similarSearchHash);
212+
}, [similarSearchHash, isSavedSearchActive, typeMenuSections]);
213213

214214
return (
215215
<ScrollView

tests/unit/Search/SearchQueryUtilsTest.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,4 +316,27 @@ describe('SearchQueryUtils', () => {
316316
expect(sortedOptions).toEqual(['A', 'B', 'C']);
317317
});
318318
});
319+
320+
describe('similarSearchHash', () => {
321+
it('should return same similarSearchHash for two queries that are the same but use different sorting', () => {
322+
const queryJSONa = buildSearchQueryJSON('sortBy:date sortOrder:desc type:expense category:none,Uncategorized,Maintenance');
323+
const queryJSONb = buildSearchQueryJSON('sortBy:date sortOrder:asc type:expense category:none,Uncategorized,Maintenance');
324+
325+
expect(queryJSONa?.similarSearchHash).toEqual(queryJSONb?.similarSearchHash);
326+
});
327+
328+
it('should return same similarSearchHash for two queries that have same filters but different values', () => {
329+
const queryJSONa = buildSearchQueryJSON('sortBy:date sortOrder:desc type:expense feed:"oauth.americanexpressfdx.com 1001" posted:last-statement');
330+
const queryJSONb = buildSearchQueryJSON('sortBy:date sortOrder:desc type:expense feed:"1234_stripe" posted:last-month');
331+
332+
expect(queryJSONa?.similarSearchHash).toEqual(queryJSONb?.similarSearchHash);
333+
});
334+
335+
it('should return different similarSearchHash for two queries that have different types', () => {
336+
const queryJSONa = buildSearchQueryJSON('sortBy:date sortOrder:desc type:expense feed:"oauth.americanexpressfdx.com 1001"');
337+
const queryJSONb = buildSearchQueryJSON('sortBy:date sortOrder:desc type:trip feed:"oauth.americanexpressfdx.com 1001"');
338+
339+
expect(queryJSONa?.similarSearchHash).not.toEqual(queryJSONb?.similarSearchHash);
340+
});
341+
});
319342
});

tests/unit/useSearchHighlightAndScrollTest.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ describe('useSearchHighlightAndScroll', () => {
5959
flatFilters: [],
6060
hash: 123,
6161
recentSearchHash: 456,
62+
similarSearchHash: 789,
6263
},
6364
searchKey: undefined,
6465
shouldCalculateTotals: false,

0 commit comments

Comments
 (0)