diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx index 51f6c68c11ae..cd1b912e2475 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader.tsx @@ -137,7 +137,7 @@ function SearchPageHeader({queryJSON, hash}: SearchPageHeaderProps) { const {status, type} = queryJSON; const isCannedQuery = SearchQueryUtils.isCannedSearchQuery(queryJSON); - const headerText = isCannedQuery ? translate(getHeaderContent(type).titleText) : SearchQueryUtils.getSearchHeaderTitle(queryJSON, personalDetails, cardList, reports, taxRates); + const headerText = isCannedQuery ? translate(getHeaderContent(type).titleText) : SearchQueryUtils.buildUserReadableQueryString(queryJSON, personalDetails, cardList, reports, taxRates); const [inputValue, setInputValue] = useState(headerText); useEffect(() => { diff --git a/src/components/Search/SearchRouter/SearchRouterList.tsx b/src/components/Search/SearchRouter/SearchRouterList.tsx index 832af8168ab4..5b89bd08ab4b 100644 --- a/src/components/Search/SearchRouter/SearchRouterList.tsx +++ b/src/components/Search/SearchRouter/SearchRouterList.tsx @@ -55,6 +55,10 @@ const setPerformanceTimersEnd = () => { 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 isSearchQueryItem(item: OptionData | SearchQueryItem): item is SearchQueryItem { if ('singleIcon' in item && item.singleIcon && 'query' in item && item.query) { return true; @@ -120,7 +124,7 @@ function SearchRouterList( { text: `${translate('search.searchIn')} ${reportForContextualSearch.text ?? reportForContextualSearch.alternateText}`, singleIcon: Expensicons.MagnifyingGlass, - query: SearchQueryUtils.getContextualSuggestionQuery(reportForContextualSearch.reportID), + query: getContextualSearchQuery(reportForContextualSearch.reportID), itemStyle: styles.activeComponentBG, keyForList: 'contextualSearch', isContextualSearchItem: true, @@ -132,7 +136,7 @@ function SearchRouterList( const recentSearchesData = recentSearches?.map(({query}) => { const searchQueryJSON = SearchQueryUtils.buildSearchQueryJSON(query); return { - text: searchQueryJSON ? SearchQueryUtils.getSearchHeaderTitle(searchQueryJSON, personalDetails, cardList, reports, taxRates) : query, + text: searchQueryJSON ? SearchQueryUtils.buildUserReadableQueryString(searchQueryJSON, personalDetails, cardList, reports, taxRates) : query, singleIcon: Expensicons.History, query, keyForList: query, diff --git a/src/libs/Navigation/AppNavigator/createCustomPlatformStackBottomTabNavigator/TopBar.tsx b/src/libs/Navigation/AppNavigator/createCustomPlatformStackBottomTabNavigator/TopBar.tsx index cef1266d2d26..eba7a7448ad0 100644 --- a/src/libs/Navigation/AppNavigator/createCustomPlatformStackBottomTabNavigator/TopBar.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomPlatformStackBottomTabNavigator/TopBar.tsx @@ -10,7 +10,6 @@ import useLocalize from '@hooks/useLocalize'; import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; -import Performance from '@libs/Performance'; import * as SearchQueryUtils from '@libs/SearchQueryUtils'; import SignInButton from '@pages/home/sidebar/SignInButton'; import * as Session from '@userActions/Session'; diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index b79cc254924f..f7e18d205b16 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -32,7 +32,19 @@ const operatorToCharMap = { /** * @private - * returns Date filter query string part, which needs special logic + * Returns string value wrapped in quotes "", if the value contains special characters. + */ +function sanitizeSearchValue(str: string) { + const regexp = /[^A-Za-z0-9_@./#&+\-\\';,"]/g; + if (regexp.test(str)) { + return `"${str}"`; + } + return str; +} + +/** + * @private + * Returns date filter value for QueryString. */ function buildDateFilterQuery(filterValues: Partial) { const dateBefore = filterValues[FILTER_KEYS.DATE_BEFORE]; @@ -54,7 +66,7 @@ function buildDateFilterQuery(filterValues: Partial) /** * @private - * returns Date filter query string part, which needs special logic + * Returns amount filter value for QueryString. */ function buildAmountFilterQuery(filterValues: Partial) { const lessThan = filterValues[FILTER_KEYS.LESS_THAN]; @@ -74,17 +86,33 @@ function buildAmountFilterQuery(filterValues: Partial return amountFilter; } -function sanitizeString(str: string) { - const regexp = /[^A-Za-z0-9_@./#&+\-\\';,"]/g; - if (regexp.test(str)) { - return `"${str}"`; - } - return str; +/** + * @private + * Returns string of correctly formatted filter values from QueryFilters object. + */ +function buildFilterValuesString(filterName: string, queryFilters: QueryFilter[]) { + const delimiter = filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.KEYWORD ? ' ' : ','; + let filterValueString = ''; + queryFilters.forEach((queryFilter, index) => { + // If the previous queryFilter has the same operator (this rule applies only to eq and neq operators) then append the current value + if ( + index !== 0 && + ((queryFilter.operator === 'eq' && queryFilters?.at(index - 1)?.operator === 'eq') || (queryFilter.operator === 'neq' && queryFilters.at(index - 1)?.operator === 'neq')) + ) { + filterValueString += `${delimiter}${sanitizeSearchValue(queryFilter.value.toString())}`; + } else if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.KEYWORD) { + filterValueString += `${delimiter}${sanitizeSearchValue(queryFilter.value.toString())}`; + } else { + filterValueString += ` ${filterName}${operatorToCharMap[queryFilter.operator]}${sanitizeSearchValue(queryFilter.value.toString())}`; + } + }); + + return filterValueString; } /** * @private - * traverses the AST and returns filters as a QueryFilters object + * Traverses the AST and returns filters as a QueryFilters object. */ function getFilters(queryJSON: SearchQueryJSON) { const filters = {} as QueryFilters; @@ -136,6 +164,51 @@ function getFilters(queryJSON: SearchQueryJSON) { return filters; } +/** + * @private + * Given a filter name and its value, this function returns the corresponding ID found in Onyx data. + */ +function findIDFromDisplayValue(filterName: ValueOf, filter: string | string[], cardList: OnyxTypes.CardList, taxRates: Record) { + if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM || filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.TO) { + if (typeof filter === 'string') { + const email = filter; + return PersonalDetailsUtils.getPersonalDetailByEmail(email)?.accountID.toString() ?? filter; + } + const emails = filter; + return emails.map((email) => PersonalDetailsUtils.getPersonalDetailByEmail(email)?.accountID.toString() ?? email); + } + if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.TAX_RATE) { + const names = Array.isArray(filter) ? filter : ([filter] as string[]); + return names.map((name) => taxRates[name] ?? name).flat(); + } + if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.CARD_ID) { + if (typeof filter === 'string') { + const bank = filter; + const ids = + Object.values(cardList) + .filter((card) => card.bank === bank) + .map((card) => card.cardID.toString()) ?? filter; + return ids.length > 0 ? ids : bank; + } + const banks = filter; + return banks + .map( + (bank) => + Object.values(cardList) + .filter((card) => card.bank === bank) + .map((card) => card.cardID.toString()) ?? bank, + ) + .flat(); + } + return filter; +} + +/** + * Parses a given search query string into a structured `SearchQueryJSON` format. + * This format of query is most commonly shared between components and also sent to backend to retrieve search results. + * + * In a way this is the reverse of buildSearchQueryString() + */ function buildSearchQueryJSON(query: SearchQueryString) { try { const result = searchParser.parse(query) as SearchQueryJSON; @@ -154,6 +227,12 @@ function buildSearchQueryJSON(query: SearchQueryString) { } } +/** + * Formats a given `SearchQueryJSON` object into the string version of query. + * This format of query is the most basic string format and is used as the query param `q` in search URLs. + * + * In a way this is the reverse of buildSearchQueryJSON() + */ function buildSearchQueryString(queryJSON?: SearchQueryJSON) { const queryParts: string[] = []; const defaultQueryJSON = buildSearchQueryJSON(''); @@ -177,7 +256,7 @@ function buildSearchQueryString(queryJSON?: SearchQueryJSON) { const queryFilter = filters[filterKey]; if (queryFilter) { - const filterValueString = buildFilterString(filterKey, queryFilter); + const filterValueString = buildFilterValuesString(filterKey, queryFilter); queryParts.push(filterValueString); } } @@ -186,7 +265,10 @@ function buildSearchQueryString(queryJSON?: SearchQueryJSON) { } /** - * Given object with chosen search filters builds correct query string from them + * Formats a given object with search filter values into the string version of the query. + * Main usage is to consume data format that comes from AdvancedFilters Onyx Form Data, and generate query string. + * + * Reverse operation of buildFilterFormValuesFromQuery() */ function buildQueryStringFromFilterFormValues(filterValues: Partial) { // We separate type and status filters from other filters to maintain hashes consistency for saved searches @@ -194,17 +276,17 @@ function buildQueryStringFromFilterFormValues(filterValues: Partial CONST.SEARCH.SYNTAX_FILTER_KEYS[key] === filterKey); if (keyInCorrectForm) { - return `${CONST.SEARCH.SYNTAX_FILTER_KEYS[keyInCorrectForm]}:${sanitizeString(filterValue as string)}`; + return `${CONST.SEARCH.SYNTAX_FILTER_KEYS[keyInCorrectForm]}:${sanitizeSearchValue(filterValue as string)}`; } } if (filterKey === FILTER_KEYS.KEYWORD && filterValue) { - const value = (filterValue as string).split(' ').map(sanitizeString).join(' '); + const value = (filterValue as string).split(' ').map(sanitizeSearchValue).join(' '); return `${value}`; } @@ -238,7 +320,7 @@ function buildQueryStringFromFilterFormValues(filterValues: Partial(filterValue)]; const keyInCorrectForm = (Object.keys(CONST.SEARCH.SYNTAX_FILTER_KEYS) as FilterKeys[]).find((key) => CONST.SEARCH.SYNTAX_FILTER_KEYS[key] === filterKey); if (keyInCorrectForm) { - return `${CONST.SEARCH.SYNTAX_FILTER_KEYS[keyInCorrectForm]}:${filterValueArray.map(sanitizeString).join(',')}`; + return `${CONST.SEARCH.SYNTAX_FILTER_KEYS[keyInCorrectForm]}:${filterValueArray.map(sanitizeSearchValue).join(',')}`; } } @@ -258,7 +340,10 @@ function buildQueryStringFromFilterFormValues(filterValues: Partial) { if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM || filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.TO) { // login can be an empty string @@ -383,27 +472,13 @@ function getDisplayValue(filterName: string, filter: string, personalDetails: On return filter; } -function buildFilterString(filterName: string, queryFilters: QueryFilter[]) { - const delimiter = filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.KEYWORD ? ' ' : ','; - let filterValueString = ''; - queryFilters.forEach((queryFilter, index) => { - // If the previous queryFilter has the same operator (this rule applies only to eq and neq operators) then append the current value - if ( - index !== 0 && - ((queryFilter.operator === 'eq' && queryFilters?.at(index - 1)?.operator === 'eq') || (queryFilter.operator === 'neq' && queryFilters.at(index - 1)?.operator === 'neq')) - ) { - filterValueString += `${delimiter}${sanitizeString(queryFilter.value.toString())}`; - } else if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.KEYWORD) { - filterValueString += `${delimiter}${sanitizeString(queryFilter.value.toString())}`; - } else { - filterValueString += ` ${filterName}${operatorToCharMap[queryFilter.operator]}${sanitizeString(queryFilter.value.toString())}`; - } - }); - - return filterValueString; -} - -function getSearchHeaderTitle( +/** + * Formats a given `SearchQueryJSON` object into the human-readable string version of query. + * This format of query is the one which we want to display to users. + * We try to replace every numeric id value with a display version of this value, + * So: user IDs get turned into emails, report ids into report names etc. + */ +function buildUserReadableQueryString( queryJSON: SearchQueryJSON, PersonalDetails: OnyxTypes.PersonalDetailsList, cardList: OnyxTypes.CardList, @@ -439,12 +514,15 @@ function getSearchHeaderTitle( value: getDisplayValue(key, filter.value.toString(), PersonalDetails, cardList, reports), })); } - title += buildFilterString(key, displayQueryFilters); + title += buildFilterValuesString(key, displayQueryFilters); }); return title; } +/** + * Returns properly built QueryString for a canned query, with the optional policyID. + */ function buildCannedSearchQuery({ type = CONST.SEARCH.DATA_TYPES.EXPENSE, status = CONST.SEARCH.STATUS.EXPENSE.ALL, @@ -462,42 +540,14 @@ function buildCannedSearchQuery({ } /** - * @private - * Given a filter name and its value, this function will try to find the corresponding ID. + * Returns whether a given search query is a Canned query. + * + * Canned queries are simple predefined queries, that are defined only using type and status and no additional filters. + * In addition, they can contain an optional policyID. + * For example: "type:trip status:all" is a canned query. */ -function findIDFromDisplayValue(filterName: ValueOf, filter: string | string[], cardList: OnyxTypes.CardList, taxRates: Record) { - if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.FROM || filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.TO) { - if (typeof filter === 'string') { - const email = filter; - return PersonalDetailsUtils.getPersonalDetailByEmail(email)?.accountID.toString() ?? filter; - } - const emails = filter; - return emails.map((email) => PersonalDetailsUtils.getPersonalDetailByEmail(email)?.accountID.toString() ?? email); - } - if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.TAX_RATE) { - const names = Array.isArray(filter) ? filter : ([filter] as string[]); - return names.map((name) => taxRates[name] ?? name).flat(); - } - if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.CARD_ID) { - if (typeof filter === 'string') { - const bank = filter; - const ids = - Object.values(cardList) - .filter((card) => card.bank === bank) - .map((card) => card.cardID.toString()) ?? filter; - return ids.length > 0 ? ids : bank; - } - const banks = filter; - return banks - .map( - (bank) => - Object.values(cardList) - .filter((card) => card.bank === bank) - .map((card) => card.cardID.toString()) ?? bank, - ) - .flat(); - } - return filter; +function isCannedSearchQuery(queryJSON: SearchQueryJSON) { + return !queryJSON.filters; } /** @@ -531,29 +581,14 @@ function standardizeQueryJSON(queryJSON: SearchQueryJSON, cardList: OnyxTypes.Ca return standardQuery; } -/** - * Returns whether a given search query is a Canned query. - * - * Canned queries are simple predefined queries, that are defined only using type and status and no additional filters. - * For example: "type:trip status:all" is a canned query. - */ -function isCannedSearchQuery(queryJSON: SearchQueryJSON) { - return !queryJSON.filters; -} - -function getContextualSuggestionQuery(reportID: string) { - return `type:chat in:${reportID}`; -} - export { buildSearchQueryJSON, buildSearchQueryString, + buildUserReadableQueryString, buildQueryStringFromFilterFormValues, buildFilterFormValuesFromQuery, getPolicyIDFromSearchQuery, - getSearchHeaderTitle, buildCannedSearchQuery, isCannedSearchQuery, standardizeQueryJSON, - getContextualSuggestionQuery, }; diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 89c28eb6c39f..b648d31418b3 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -40,10 +40,17 @@ const emptyPersonalDetails = { displayName: undefined, login: undefined, }; -/* Search list and results related */ + +type ReportKey = `${typeof ONYXKEYS.COLLECTION.REPORT}${string}`; + +type TransactionKey = `${typeof ONYXKEYS.COLLECTION.TRANSACTION}${string}`; + +type ReportActionKey = `${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS}${string}`; /** * @private + * + * Returns a list of properties that are common to every Search ListItem */ function getTransactionItemCommonFormattedProperties( transactionItem: SearchTransaction, @@ -68,24 +75,30 @@ function getTransactionItemCommonFormattedProperties( }; } -type ReportKey = `${typeof ONYXKEYS.COLLECTION.REPORT}${string}`; - -type TransactionKey = `${typeof ONYXKEYS.COLLECTION.TRANSACTION}${string}`; - -type ReportActionKey = `${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS}${string}`; - +/** + * @private + */ function isReportEntry(key: string): key is ReportKey { return key.startsWith(ONYXKEYS.COLLECTION.REPORT); } +/** + * @private + */ function isTransactionEntry(key: string): key is TransactionKey { return key.startsWith(ONYXKEYS.COLLECTION.TRANSACTION); } +/** + * @private + */ function isReportActionEntry(key: string): key is ReportActionKey { return key.startsWith(ONYXKEYS.COLLECTION.REPORT_ACTIONS); } +/** + * Determines whether to display the merchant field based on the transactions in the search results. + */ function getShouldShowMerchant(data: OnyxTypes.SearchResults['data']): boolean { return Object.keys(data).some((key) => { if (isTransactionEntry(key)) { @@ -99,20 +112,32 @@ function getShouldShowMerchant(data: OnyxTypes.SearchResults['data']): boolean { const currentYear = new Date().getFullYear(); +/** + * Type guard that checks if something is a ReportListItemType + */ function isReportListItemType(item: ListItem): item is ReportListItemType { return 'transactions' in item; } +/** + * Type guard that checks if something is a TransactionListItemType + */ function isTransactionListItemType(item: TransactionListItemType | ReportListItemType | ReportActionListItemType): item is TransactionListItemType { const transactionListItem = item as TransactionListItemType; return transactionListItem.transactionID !== undefined; } +/** + * Type guard that checks if something is a ReportActionListItemType + */ function isReportActionListItemType(item: TransactionListItemType | ReportListItemType | ReportActionListItemType): item is ReportActionListItemType { const reportActionListItem = item as ReportActionListItemType; return reportActionListItem.reportActionID !== undefined; } +/** + * Checks if the date of transactions or reports indicate the need to display the year because they are from a past year. + */ function shouldShowYear(data: TransactionListItemType[] | ReportListItemType[] | OnyxTypes.SearchResults['data']): boolean { if (Array.isArray(data)) { return data.some((item: TransactionListItemType | ReportListItemType) => { @@ -151,6 +176,37 @@ function shouldShowYear(data: TransactionListItemType[] | ReportListItemType[] | return false; } +/** + * @private + * Generates a display name for IOU reports considering the personal details of the payer and the transaction details. + */ +function getIOUReportName(data: OnyxTypes.SearchResults['data'], reportItem: SearchReport) { + const payerPersonalDetails = reportItem.managerID ? data.personalDetailsList?.[reportItem.managerID] : emptyPersonalDetails; + const payerName = payerPersonalDetails?.displayName ?? payerPersonalDetails?.login ?? translateLocal('common.hidden'); + const formattedAmount = CurrencyUtils.convertToDisplayString(reportItem.total ?? 0, reportItem.currency ?? CONST.CURRENCY.USD); + if (reportItem.action === CONST.SEARCH.ACTION_TYPES.VIEW) { + return translateLocal('iou.payerOwesAmount', { + payer: payerName, + amount: formattedAmount, + }); + } + + if (reportItem.action === CONST.SEARCH.ACTION_TYPES.PAID) { + return translateLocal('iou.payerPaidAmount', { + payer: payerName, + amount: formattedAmount, + }); + } + + return reportItem.reportName; +} + +/** + * @private + * Organizes data into List Sections for display, for the TransactionListItemType of Search Results. + * + * Do not use directly, use only via `getSections()` facade. + */ function getTransactionsSections(data: OnyxTypes.SearchResults['data'], metadata: OnyxTypes.SearchResults['search']): TransactionListItemType[] { const shouldShowMerchant = getShouldShowMerchant(data); @@ -184,6 +240,12 @@ function getTransactionsSections(data: OnyxTypes.SearchResults['data'], metadata }); } +/** + * @private + * Organizes data into List Sections for display, for the ReportActionListItemType of Search Results. + * + * Do not use directly, use only via `getSections()` facade. + */ function getReportActionsSections(data: OnyxTypes.SearchResults['data']): ReportActionListItemType[] { const reportActionItems: ReportActionListItemType[] = []; for (const key in data) { @@ -208,27 +270,12 @@ function getReportActionsSections(data: OnyxTypes.SearchResults['data']): Report return reportActionItems; } -function getIOUReportName(data: OnyxTypes.SearchResults['data'], reportItem: SearchReport) { - const payerPersonalDetails = reportItem.managerID ? data.personalDetailsList?.[reportItem.managerID] : emptyPersonalDetails; - const payerName = payerPersonalDetails?.displayName ?? payerPersonalDetails?.login ?? translateLocal('common.hidden'); - const formattedAmount = CurrencyUtils.convertToDisplayString(reportItem.total ?? 0, reportItem.currency ?? CONST.CURRENCY.USD); - if (reportItem.action === CONST.SEARCH.ACTION_TYPES.VIEW) { - return translateLocal('iou.payerOwesAmount', { - payer: payerName, - amount: formattedAmount, - }); - } - - if (reportItem.action === CONST.SEARCH.ACTION_TYPES.PAID) { - return translateLocal('iou.payerPaidAmount', { - payer: payerName, - amount: formattedAmount, - }); - } - - return reportItem.reportName; -} - +/** + * @private + * Organizes data into List Sections for display, for the ReportListItemType of Search Results. + * + * Do not use directly, use only via `getSections()` facade. + */ function getReportSections(data: OnyxTypes.SearchResults['data'], metadata: OnyxTypes.SearchResults['search']): ReportListItemType[] { const shouldShowMerchant = getShouldShowMerchant(data); @@ -286,6 +333,9 @@ function getReportSections(data: OnyxTypes.SearchResults['data'], metadata: Onyx return Object.values(reportIDToTransactions); } +/** + * Returns the appropriate list item component based on the type and status of the search data. + */ function getListItem(type: SearchDataTypes, status: SearchStatus): ListItemType { if (type === CONST.SEARCH.DATA_TYPES.CHAT) { return ChatListItem; @@ -296,6 +346,9 @@ function getListItem(type: SearchDataTypes, status: SearchStatus): ListItemType< return ReportListItem; } +/** + * Organizes data into appropriate list sections for display based on the type of search results. + */ function getSections(type: SearchDataTypes, status: SearchStatus, data: OnyxTypes.SearchResults['data'], metadata: OnyxTypes.SearchResults['search']) { if (type === CONST.SEARCH.DATA_TYPES.CHAT) { return getReportActionsSections(data); @@ -306,6 +359,9 @@ function getSections(type: SearchDataTypes, status: SearchStatus, data: OnyxType return getReportSections(data, metadata); } +/** + * Sorts sections of data based on a specified column and sort order for displaying sorted results. + */ function getSortedSections(type: SearchDataTypes, status: SearchStatus, data: ListItemDataType, sortBy?: SearchColumnType, sortOrder?: SortOrder) { if (type === CONST.SEARCH.DATA_TYPES.CHAT) { return getSortedReportActionData(data as ReportActionListItemType[]); @@ -316,6 +372,10 @@ function getSortedSections(type: SearchDataTypes, status: SearchStatus, data: Li return getSortedReportData(data as ReportListItemType[]); } +/** + * @private + * Sorts transaction sections based on a specified column and sort order. + */ function getSortedTransactionData(data: TransactionListItemType[], sortBy?: SearchColumnType, sortOrder?: SortOrder) { if (!sortBy || !sortOrder) { return data; @@ -347,10 +407,18 @@ function getSortedTransactionData(data: TransactionListItemType[], sortBy?: Sear }); } +/** + * @private + * Determines the date of the newest transaction within a report for sorting purposes. + */ function getReportNewestTransactionDate(report: ReportListItemType) { return report.transactions?.reduce((max, curr) => (curr.modifiedCreated ?? curr.created > (max?.created ?? '') ? curr : max), report.transactions.at(0))?.created; } +/** + * @private + * Sorts report sections based on a specified column and sort order. + */ function getSortedReportData(data: ReportListItemType[]) { return data.sort((a, b) => { const aNewestTransaction = getReportNewestTransactionDate(a); @@ -364,6 +432,10 @@ function getSortedReportData(data: ReportListItemType[]) { }); } +/** + * @private + * Sorts report actions sections based on a specified column and sort order. + */ function getSortedReportActionData(data: ReportActionListItemType[]) { return data.sort((a, b) => { const aValue = a?.created; @@ -377,10 +449,16 @@ function getSortedReportActionData(data: ReportActionListItemType[]) { }); } +/** + * Checks if the search results contain any data, useful for determining if the search results are empty. + */ function isSearchResultsEmpty(searchResults: SearchResults) { return !Object.keys(searchResults?.data).some((key) => key.startsWith(ONYXKEYS.COLLECTION.TRANSACTION)); } +/** + * Returns the corresponding translation key for expense type + */ function getExpenseTypeTranslationKey(expenseType: ValueOf): TranslationPaths { // eslint-disable-next-line default-case switch (expenseType) { @@ -393,6 +471,9 @@ function getExpenseTypeTranslationKey(expenseType: ValueOf void, isMobileMenu?: boolean, closeMenu?: () => void) { return [ { @@ -420,6 +501,9 @@ function getOverflowMenu(itemName: string, hash: number, inputQuery: string, sho ]; } +/** + * Checks if the passed username is a correct standard username, and not a placeholder + */ function isCorrectSearchUserName(displayName?: string) { return displayName && displayName.toUpperCase() !== CONST.REPORT.OWNER_EMAIL_FAKE; } diff --git a/src/pages/Search/SearchTypeMenu.tsx b/src/pages/Search/SearchTypeMenu.tsx index 4aec50986f84..44d83cffc196 100644 --- a/src/pages/Search/SearchTypeMenu.tsx +++ b/src/pages/Search/SearchTypeMenu.tsx @@ -116,7 +116,7 @@ function SearchTypeMenu({queryJSON, searchName}: SearchTypeMenuProps) { let title = item.name; if (title === item.query) { const jsonQuery = SearchQueryUtils.buildSearchQueryJSON(item.query) ?? ({} as SearchQueryJSON); - title = SearchQueryUtils.getSearchHeaderTitle(jsonQuery, personalDetails, cardList, reports, taxRates); + title = SearchQueryUtils.buildUserReadableQueryString(jsonQuery, personalDetails, cardList, reports, taxRates); } const baseMenuItem: SavedSearchMenuItem = { @@ -221,7 +221,7 @@ function SearchTypeMenu({queryJSON, searchName}: SearchTypeMenuProps) { const activeItemIndex = isCannedQuery ? typeMenuItems.findIndex((item) => item.type === type) : -1; if (shouldUseNarrowLayout) { - const title = searchName ?? (isCannedQuery ? undefined : SearchQueryUtils.getSearchHeaderTitle(queryJSON, personalDetails, cardList, reports, taxRates)); + const title = searchName ?? (isCannedQuery ? undefined : SearchQueryUtils.buildUserReadableQueryString(queryJSON, personalDetails, cardList, reports, taxRates)); return (