From 0c84a1902f8418595f559085ad4a94ed5ae0e34d Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Mon, 28 Jul 2025 15:24:57 +0100 Subject: [PATCH 1/4] Revert "Merge pull request #67149 from Expensify/revert-66830-search-total-footer" This reverts commit fb2ea302b739f9ea227395a23e1d4afcdc647a67. --- src/CONST/index.ts | 14 -- src/components/Search/SearchContext.tsx | 36 ++--- src/components/Search/SearchPageFooter.tsx | 41 ++++++ src/components/Search/index.tsx | 43 ++++-- src/components/Search/types.ts | 8 +- src/hooks/useSearchHighlightAndScroll.ts | 32 ++++- src/languages/de.ts | 2 +- src/languages/en.ts | 2 +- src/languages/es.ts | 2 +- src/languages/fr.ts | 2 +- src/languages/it.ts | 2 +- src/languages/ja.ts | 2 +- src/languages/nl.ts | 2 +- src/languages/pl.ts | 2 +- src/languages/pt-BR.ts | 2 +- src/languages/zh-hans.ts | 2 +- src/libs/SearchQueryUtils.ts | 8 +- src/libs/SearchUIUtils.ts | 134 ++++++------------ src/libs/actions/IOU.ts | 4 +- src/libs/actions/Search.ts | 27 ++-- src/pages/Search/SearchPage.tsx | 22 ++- src/pages/Search/SearchPageNarrow.tsx | 4 + src/types/onyx/SearchResults.ts | 10 ++ tests/ui/ReportListItemHeaderTest.tsx | 2 +- tests/unit/useSearchHighlightAndScrollTest.ts | 6 +- 25 files changed, 229 insertions(+), 182 deletions(-) create mode 100644 src/components/Search/SearchPageFooter.tsx diff --git a/src/CONST/index.ts b/src/CONST/index.ts index e8bd897d88f5..9551d53dd06f 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -6427,20 +6427,6 @@ const CONST = { ONYXKEYS.PERSONAL_DETAILS_LIST, ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS, ], - SEARCH_LIST: { - EXPENSES: 'expenses', - REPORTS: 'reports', - CHATS: 'chats', - SUBMIT: 'submit', - APPROVE: 'approve', - PAY: 'pay', - EXPORT: 'export', - STATEMENTS: 'statements', - UNAPPROVED_CASH: 'unapprovedCash', - UNAPPROVED_COMPANY_CARDS: 'unapprovedCompanyCards', - UNAPPROVED_CASH_ONLY: 'unapprovedCashOnly', - UNAPPROVED_COMPANY_CARDS_ONLY: 'unapprovedCompanyCardsOnly', - }, SEARCH_KEYS: { EXPENSES: 'expenses', REPORTS: 'reports', diff --git a/src/components/Search/SearchContext.tsx b/src/components/Search/SearchContext.tsx index bef62f7c8349..6b01895420ee 100644 --- a/src/components/Search/SearchContext.tsx +++ b/src/components/Search/SearchContext.tsx @@ -1,22 +1,15 @@ import React, {useCallback, useContext, useMemo, useRef, useState} from 'react'; -import useCardFeedsForDisplay from '@hooks/useCardFeedsForDisplay'; -import useOnyx from '@hooks/useOnyx'; import {isMoneyRequestReport} from '@libs/ReportUtils'; -import { - getSuggestedSearches, - isTransactionCardGroupListItemType, - isTransactionListItemType, - isTransactionMemberGroupListItemType, - isTransactionReportGroupListItemType, -} from '@libs/SearchUIUtils'; +import {isTransactionCardGroupListItemType, isTransactionListItemType, isTransactionMemberGroupListItemType, isTransactionReportGroupListItemType} from '@libs/SearchUIUtils'; +import type {SearchKey} from '@libs/SearchUIUtils'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {SearchContext, SearchContextData, SelectedTransactions} from './types'; const defaultSearchContextData: SearchContextData = { currentSearchHash: -1, + currentSearchKey: undefined, selectedTransactions: {}, selectedTransactionIDs: [], selectedReports: [], @@ -26,13 +19,12 @@ const defaultSearchContextData: SearchContextData = { const defaultSearchContext: SearchContext = { ...defaultSearchContextData, - currentSearchKey: undefined, lastSearchType: undefined, isExportMode: false, shouldShowExportModeOption: false, shouldShowFiltersBarLoading: false, setLastSearchType: () => {}, - setCurrentSearchHash: () => {}, + setCurrentSearchHashAndKey: () => {}, setSelectedTransactions: () => {}, removeTransaction: () => {}, clearSelectedTransactions: () => {}, @@ -50,25 +42,17 @@ function SearchContextProvider({children}: ChildrenProps) { const [lastSearchType, setLastSearchType] = useState(undefined); const [searchContextData, setSearchContextData] = useState(defaultSearchContextData); const areTransactionsEmpty = useRef(true); - const {defaultCardFeed} = useCardFeedsForDisplay(); - - const [accountID] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false, selector: (s) => s?.accountID}); - const suggestedSearches = useMemo(() => getSuggestedSearches(defaultCardFeed?.id, accountID), [defaultCardFeed?.id, accountID]); - - const currentSearchKey = useMemo(() => { - const currentSearch = Object.values(suggestedSearches).find((search) => search.hash === searchContextData.currentSearchHash); - return currentSearch?.key; - }, [suggestedSearches, searchContextData.currentSearchHash]); - const setCurrentSearchHash = useCallback((searchHash: number) => { + const setCurrentSearchHashAndKey = useCallback((searchHash: number, searchKey: SearchKey | undefined) => { setSearchContextData((prevState) => { - if (searchHash === prevState.currentSearchHash) { + if (searchHash === prevState.currentSearchHash && searchKey === prevState.currentSearchKey) { return prevState; } return { ...prevState, currentSearchHash: searchHash, + currentSearchKey: searchKey, }; }); }, []); @@ -190,9 +174,8 @@ function SearchContextProvider({children}: ChildrenProps) { const searchContext = useMemo( () => ({ ...searchContextData, - currentSearchKey, removeTransaction, - setCurrentSearchHash, + setCurrentSearchHashAndKey, setSelectedTransactions, clearSelectedTransactions, shouldShowFiltersBarLoading, @@ -206,9 +189,8 @@ function SearchContextProvider({children}: ChildrenProps) { }), [ searchContextData, - currentSearchKey, removeTransaction, - setCurrentSearchHash, + setCurrentSearchHashAndKey, setSelectedTransactions, clearSelectedTransactions, shouldShowFiltersBarLoading, diff --git a/src/components/Search/SearchPageFooter.tsx b/src/components/Search/SearchPageFooter.tsx new file mode 100644 index 000000000000..bb7d7b65616b --- /dev/null +++ b/src/components/Search/SearchPageFooter.tsx @@ -0,0 +1,41 @@ +import React, {useMemo} from 'react'; +import {View} from 'react-native'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {convertToDisplayString} from '@libs/CurrencyUtils'; +import type {SearchResultsInfo} from '@src/types/onyx/SearchResults'; + +type SearchPageFooterProps = { + metadata: SearchResultsInfo; +}; + +function SearchPageFooter({metadata}: SearchPageFooterProps) { + const theme = useTheme(); + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const {translate} = useLocalize(); + const {isOffline} = useNetwork(); + + const valueTextStyle = useMemo(() => (isOffline ? [styles.textLabelSupporting, styles.labelStrong] : [styles.labelStrong]), [isOffline, styles]); + + return ( + + + {`${translate('common.expenses')}:`} + {metadata.count} + + + {`${translate('common.totalSpend')}:`} + {convertToDisplayString(metadata.total, metadata.currency)} + + + ); +} + +SearchPageFooter.displayName = 'SearchPageFooter'; + +export default SearchPageFooter; diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index a90652a369fe..8baaa47143ef 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -8,6 +8,7 @@ import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOffli import SearchTableHeader from '@components/SelectionList/SearchTableHeader'; import type {ReportActionListItemType, SearchListItem, SelectionListHandle, TransactionGroupListItemType, TransactionListItemType} from '@components/SelectionList/types'; import SearchRowSkeleton from '@components/Skeletons/SearchRowSkeleton'; +import useCardFeedsForDisplay from '@hooks/useCardFeedsForDisplay'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; @@ -30,6 +31,7 @@ import { getListItem, getSections, getSortedSections, + getSuggestedSearches, getWideAmountIndicators, isReportActionListItemType, isSearchDataLoaded, @@ -40,6 +42,7 @@ import { shouldShowEmptyState, shouldShowYear as shouldShowYearUtil, } from '@libs/SearchUIUtils'; +import type {SearchKey} from '@libs/SearchUIUtils'; import {isOnHold, isTransactionPendingDelete} from '@libs/TransactionUtils'; import Navigation, {navigationRef} from '@navigation/Navigation'; import type {SearchFullscreenNavigatorParamList} from '@navigation/types'; @@ -149,8 +152,7 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS const navigation = useNavigation>(); const isFocused = useIsFocused(); const { - currentSearchKey, - setCurrentSearchHash, + setCurrentSearchHashAndKey, setSelectedTransactions, selectedTransactions, clearSelectedTransactions, @@ -163,8 +165,6 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS } = useSearchContext(); const [offset, setOffset] = useState(0); - const {type, status, sortBy, sortOrder, hash, groupBy} = queryJSON; - const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {canBeMissing: true}); const previousTransactions = usePrevious(transactions); const [reportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS, {canBeMissing: true}); @@ -181,6 +181,23 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS ); }, }); + const {defaultCardFeed} = useCardFeedsForDisplay(); + const [accountID] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false, selector: (s) => s?.accountID}); + const suggestedSearches = useMemo(() => getSuggestedSearches(defaultCardFeed?.id, accountID), [defaultCardFeed?.id, accountID]); + + const {type, status, sortBy, sortOrder, hash, groupBy} = queryJSON; + const searchKey = useMemo(() => Object.values(suggestedSearches).find((search) => search.hash === hash)?.key, [suggestedSearches, hash]); + + const shouldCalculateTotals = useMemo(() => { + if (offset !== 0) { + return false; + } + if (!searchKey) { + return false; + } + const eligibleSearchKeys: Partial = [CONST.SEARCH.SEARCH_KEYS.STATEMENTS, CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH, CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_COMPANY_CARDS]; + return eligibleSearchKeys.includes(searchKey); + }, [offset, searchKey]); const previousReportActions = usePrevious(reportActions); const reportActionsArray = useMemo( @@ -196,8 +213,8 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS useFocusEffect( useCallback(() => { clearSelectedTransactions(hash); - setCurrentSearchHash(hash); - }, [hash, clearSelectedTransactions, setCurrentSearchHash]), + setCurrentSearchHashAndKey(hash, searchKey); + }, [hash, searchKey, clearSelectedTransactions, setCurrentSearchHashAndKey]), ); const isSearchResultsEmpty = !searchResults?.data || isSearchResultsEmptyUtil(searchResults); @@ -207,7 +224,7 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS return; } - const selectedKeys = Object.keys(selectedTransactions).filter((key) => selectedTransactions[key]); + const selectedKeys = Object.keys(selectedTransactions).filter((transactionKey) => selectedTransactions[transactionKey]); if (selectedKeys.length === 0 && isMobileSelectionModeEnabled && shouldTurnOffSelectionMode) { turnOffMobileSelectionMode(); } @@ -222,7 +239,7 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS return; } - const selectedKeys = Object.keys(selectedTransactions).filter((key) => selectedTransactions[key]); + const selectedKeys = Object.keys(selectedTransactions).filter((transactionKey) => selectedTransactions[transactionKey]); if (!isSmallScreenWidth) { if (selectedKeys.length === 0 && isMobileSelectionModeEnabled) { turnOffMobileSelectionMode(); @@ -244,11 +261,11 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS return; } - handleSearch({queryJSON, offset}); + handleSearch({queryJSON, searchKey, offset, shouldCalculateTotals}); // We don't need to run the effect on change of isFocused. // eslint-disable-next-line react-compiler/react-compiler // eslint-disable-next-line react-hooks/exhaustive-deps - }, [handleSearch, isOffline, offset, queryJSON]); + }, [handleSearch, isOffline, offset, queryJSON, searchKey, shouldCalculateTotals]); useEffect(() => { openSearch(); @@ -259,7 +276,9 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS transactions, previousTransactions, queryJSON, + searchKey, offset, + shouldCalculateTotals, reportActions, previousReportActions, }); @@ -284,8 +303,8 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS return []; } - return getSections(type, searchResults.data, searchResults.search, groupBy, exportReportActions, currentSearchKey); - }, [currentSearchKey, exportReportActions, groupBy, isDataLoaded, searchResults, type]); + return getSections(type, searchResults.data, searchResults.search, groupBy, exportReportActions, searchKey); + }, [searchKey, exportReportActions, groupBy, isDataLoaded, searchResults, type]); useEffect(() => { /** We only want to display the skeleton for the status filters the first time we load them for a specific data type */ diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts index bb97a179b810..f86fd1848953 100644 --- a/src/components/Search/types.ts +++ b/src/components/Search/types.ts @@ -1,6 +1,6 @@ import type {ValueOf} from 'type-fest'; import type {ReportActionListItemType, TaskListItemType, TransactionGroupListItemType, TransactionListItemType} from '@components/SelectionList/types'; -import type {SuggestedSearchKey} from '@libs/SearchUIUtils'; +import type {SearchKey} from '@libs/SearchUIUtils'; import type CONST from '@src/CONST'; import type {SearchDataTypes} from '@src/types/onyx/SearchResults'; @@ -70,6 +70,7 @@ type SearchDatePreset = ValueOf; type SearchContextData = { currentSearchHash: number; + currentSearchKey: SearchKey | undefined; selectedTransactions: SelectedTransactions; selectedTransactionIDs: string[]; selectedReports: SelectedReports[]; @@ -78,8 +79,7 @@ type SearchContextData = { }; type SearchContext = SearchContextData & { - currentSearchKey: SuggestedSearchKey | undefined; - setCurrentSearchHash: (hash: number) => void; + setCurrentSearchHashAndKey: (hash: number, key: SearchKey | undefined) => void; /** If you want to set `selectedTransactionIDs`, pass an array as the first argument, object/record otherwise */ setSelectedTransactions: { (selectedTransactionIDs: string[], unused?: undefined): void; @@ -169,7 +169,9 @@ type SearchAutocompleteQueryRange = { type SearchParams = { queryJSON: SearchQueryJSON; + searchKey: SearchKey | undefined; offset: number; + shouldCalculateTotals: boolean; }; export type { diff --git a/src/hooks/useSearchHighlightAndScroll.ts b/src/hooks/useSearchHighlightAndScroll.ts index f1d6e81b926d..60d855fad7c4 100644 --- a/src/hooks/useSearchHighlightAndScroll.ts +++ b/src/hooks/useSearchHighlightAndScroll.ts @@ -5,6 +5,7 @@ import type {SearchQueryJSON} from '@components/Search/types'; import type {SearchListItem, SelectionListHandle, TransactionGroupListItemType, TransactionListItemType} from '@components/SelectionList/types'; import {search} from '@libs/actions/Search'; import {isReportActionEntry} from '@libs/SearchUIUtils'; +import type {SearchKey} from '@libs/SearchUIUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {ReportActions, SearchResults, Transaction} from '@src/types/onyx'; @@ -17,13 +18,25 @@ type UseSearchHighlightAndScroll = { reportActions: OnyxCollection; previousReportActions: OnyxCollection; queryJSON: SearchQueryJSON; + searchKey: SearchKey | undefined; offset: number; + shouldCalculateTotals: boolean; }; /** * Hook used to trigger a search when a new transaction or report action is added and handle highlighting and scrolling. */ -function useSearchHighlightAndScroll({searchResults, transactions, previousTransactions, reportActions, previousReportActions, queryJSON, offset}: UseSearchHighlightAndScroll) { +function useSearchHighlightAndScroll({ + searchResults, + transactions, + previousTransactions, + reportActions, + previousReportActions, + queryJSON, + searchKey, + offset, + shouldCalculateTotals, +}: UseSearchHighlightAndScroll) { const isFocused = useIsFocused(); // Ref to track if the search was triggered by this hook const triggeredByHookRef = useRef(false); @@ -97,12 +110,25 @@ function useSearchHighlightAndScroll({searchResults, transactions, previousTrans triggeredByHookRef.current = true; // Trigger the search - search({queryJSON, offset}); + search({queryJSON, searchKey, offset, shouldCalculateTotals}); // Set the ref to prevent further triggers until reset searchTriggeredRef.current = true; } - }, [isFocused, transactions, previousTransactions, queryJSON, offset, reportActions, previousReportActions, isChat, searchResults?.data, existingSearchResultIDs]); + }, [ + isFocused, + transactions, + previousTransactions, + queryJSON, + searchKey, + offset, + shouldCalculateTotals, + reportActions, + previousReportActions, + isChat, + searchResults?.data, + existingSearchResultIDs, + ]); // Initialize the set with existing IDs only once useEffect(() => { diff --git a/src/languages/de.ts b/src/languages/de.ts index 17648e94ed12..c28748410f10 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -547,6 +547,7 @@ const translations = { type: 'Typ', action: 'Aktion', expenses: 'Ausgaben', + totalSpend: 'Gesamtausgaben', tax: 'Steuer', shared: 'Geteilt', drafts: 'Entwürfe', @@ -5959,7 +5960,6 @@ const translations = { }, }, statements: 'Erklärungen', - unapproved: 'Nicht bewilligt', unapprovedCash: 'Nicht genehmigtes Bargeld', unapprovedCompanyCards: 'Nicht genehmigte Firmenkarten', saveSearch: 'Suche speichern', diff --git a/src/languages/en.ts b/src/languages/en.ts index 428aa7b2785b..84ef9d798103 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -538,6 +538,7 @@ const translations = { type: 'Type', action: 'Action', expenses: 'Expenses', + totalSpend: 'Total spend', tax: 'Tax', shared: 'Shared', drafts: 'Drafts', @@ -5931,7 +5932,6 @@ const translations = { }, }, statements: 'Statements', - unapproved: 'Unapproved', unapprovedCash: 'Unapproved cash', unapprovedCompanyCards: 'Unapproved company cards', saveSearch: 'Save search', diff --git a/src/languages/es.ts b/src/languages/es.ts index 354c2765cef4..cbb93bdbd387 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -531,6 +531,7 @@ const translations = { type: 'Tipo', action: 'Acción', expenses: 'Gastos', + totalSpend: 'Gasto total', tax: 'Impuesto', shared: 'Compartidos', drafts: 'Borradores', @@ -5952,7 +5953,6 @@ const translations = { }, }, statements: 'Extractos', - unapproved: 'No aprobado', unapprovedCash: 'Efectivo no aprobado', unapprovedCompanyCards: 'Tarjetas de empresa no aprobadas', saveSearch: 'Guardar búsqueda', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 46af1135f346..2c1769c3355f 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -547,6 +547,7 @@ const translations = { type: 'Type', action: 'Action', expenses: 'Dépenses', + totalSpend: 'Dépense totale', tax: 'Taxe', shared: 'Partagé', drafts: 'Brouillons', @@ -5970,7 +5971,6 @@ const translations = { }, }, statements: 'Relevés', - unapproved: 'Non approuvé', unapprovedCash: 'Espèces non approuvées', unapprovedCompanyCards: "Cartes d'entreprise non approuvées", saveSearch: 'Enregistrer la recherche', diff --git a/src/languages/it.ts b/src/languages/it.ts index 00ef1866b320..407d50035610 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -547,6 +547,7 @@ const translations = { type: 'Tipo', action: 'Azione', expenses: 'Spese', + totalSpend: 'Spesa totale', tax: 'Tassa', shared: 'Condiviso', drafts: 'Bozze', @@ -5974,7 +5975,6 @@ const translations = { }, }, statements: 'Dichiarazioni', - unapproved: 'Non approvato', unapprovedCash: 'Contanti non approvati', unapprovedCompanyCards: 'Carte aziendali non approvate', saveSearch: 'Salva ricerca', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 7a70878f1021..bdf554a10be7 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -547,6 +547,7 @@ const translations = { type: 'タイプ', action: 'アクション', expenses: '経費', + totalSpend: '総支出', tax: '税金', shared: '共有', drafts: '下書き', @@ -5932,7 +5933,6 @@ const translations = { }, }, statements: 'ステートメント', - unapproved: '未承認', unapprovedCash: '未承認現金', unapprovedCompanyCards: '未承認の社用カード', saveSearch: '検索を保存', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 3a4d668da3ca..40e70a9c69f2 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -547,6 +547,7 @@ const translations = { type: 'Type', action: 'Actie', expenses: 'Uitgaven', + totalSpend: 'Totale uitgaven', tax: 'Belasting', shared: 'Gedeeld', drafts: 'Concepten', @@ -5966,7 +5967,6 @@ const translations = { }, }, statements: 'Verklaringen', - unapproved: 'Niet goedgekeurd', unapprovedCash: 'Niet goedgekeurd contant geld', unapprovedCompanyCards: 'Ongoedgekeurde bedrijfskaarten', saveSearch: 'Zoekopdracht opslaan', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index cd0a1d390685..17bf104c5a91 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -547,6 +547,7 @@ const translations = { type: 'Rodzaj', action: 'Akcja', expenses: 'Wydatki', + totalSpend: 'Całkowite wydatki', tax: 'Podatek', shared: 'Udostępnione', drafts: 'Szkice', @@ -5951,7 +5952,6 @@ const translations = { }, }, statements: 'Oświadczenia', - unapproved: 'Niezatwierdzony', unapprovedCash: 'Niezatwierdzone środki pieniężne', unapprovedCompanyCards: 'Niezatwierdzone karty firmowe', saveSearch: 'Zapisz wyszukiwanie', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 9395e982a7fe..ee9135bb1b65 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -547,6 +547,7 @@ const translations = { type: 'Tipo', action: 'Ação', expenses: 'Despesas', + totalSpend: 'Gasto total', tax: 'Imposto', shared: 'Compartilhado', drafts: 'Rascunhos', @@ -5964,7 +5965,6 @@ const translations = { }, }, statements: 'Declarações', - unapproved: 'Não aprovado', unapprovedCash: 'Dinheiro não aprovado', unapprovedCompanyCards: 'Cartões corporativos não aprovados', saveSearch: 'Salvar pesquisa', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index fc6c49687044..2b8d9f4ecf05 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -547,6 +547,7 @@ const translations = { type: '类型', action: '操作', expenses: '费用', + totalSpend: '总支出', tax: '税务', shared: '共享', drafts: '草稿', @@ -5858,7 +5859,6 @@ const translations = { }, }, statements: '发言', - unapproved: '未经批准', unapprovedCash: '未经批准的现金', unapprovedCompanyCards: '未经批准的公司卡', saveSearch: '保存搜索', diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index ee2aa13f3774..904e7f3d6b5f 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -964,21 +964,21 @@ function getCurrentSearchQueryJSON() { function getTodoSearchQuery(action: TodoSearchType, userAccountID: number | undefined) { switch (action) { - case CONST.SEARCH.SEARCH_LIST.SUBMIT: + case CONST.SEARCH.SEARCH_KEYS.SUBMIT: return buildQueryStringFromFilterFormValues({ type: CONST.SEARCH.DATA_TYPES.EXPENSE, groupBy: CONST.SEARCH.GROUP_BY.REPORTS, status: CONST.SEARCH.STATUS.EXPENSE.DRAFTS, from: [`${userAccountID}`], }); - case CONST.SEARCH.SEARCH_LIST.APPROVE: + case CONST.SEARCH.SEARCH_KEYS.APPROVE: return buildQueryStringFromFilterFormValues({ type: CONST.SEARCH.DATA_TYPES.EXPENSE, groupBy: CONST.SEARCH.GROUP_BY.REPORTS, action: CONST.SEARCH.ACTION_FILTERS.APPROVE, to: [`${userAccountID}`], }); - case CONST.SEARCH.SEARCH_LIST.PAY: + case CONST.SEARCH.SEARCH_KEYS.PAY: return buildQueryStringFromFilterFormValues({ type: CONST.SEARCH.DATA_TYPES.EXPENSE, groupBy: CONST.SEARCH.GROUP_BY.REPORTS, @@ -986,7 +986,7 @@ function getTodoSearchQuery(action: TodoSearchType, userAccountID: number | unde reimbursable: CONST.SEARCH.BOOLEAN.YES, payer: userAccountID?.toString(), }); - case CONST.SEARCH.SEARCH_LIST.EXPORT: + case CONST.SEARCH.SEARCH_KEYS.EXPORT: return buildQueryStringFromFilterFormValues({ groupBy: CONST.SEARCH.GROUP_BY.REPORTS, action: CONST.SEARCH.ACTION_FILTERS.EXPORT, diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index aaf4d469cbc2..4cf370746263 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -231,9 +231,9 @@ type SearchDateModifierLower = Lowercase; * * These searches should be as static as possible, and should not contain conditionals, or any other logic */ -function getSuggestedSearches(defaultFeedID: string | undefined, accountID: number = CONST.DEFAULT_NUMBER_ID): Record, SearchTypeMenuItem> { +function getSuggestedSearches(defaultFeedID: string | undefined, accountID: number = CONST.DEFAULT_NUMBER_ID): Record, SearchTypeMenuItem> { return { - [CONST.SEARCH.SEARCH_LIST.EXPENSES]: { + [CONST.SEARCH.SEARCH_KEYS.EXPENSES]: { key: CONST.SEARCH.SEARCH_KEYS.EXPENSES, translationPath: 'common.expenses', type: CONST.SEARCH.DATA_TYPES.EXPENSE, @@ -243,7 +243,7 @@ function getSuggestedSearches(defaultFeedID: string | undefined, accountID: numb return buildSearchQueryJSON(this.searchQuery)?.hash ?? CONST.DEFAULT_NUMBER_ID; }, }, - [CONST.SEARCH.SEARCH_LIST.REPORTS]: { + [CONST.SEARCH.SEARCH_KEYS.REPORTS]: { key: CONST.SEARCH.SEARCH_KEYS.REPORTS, translationPath: 'common.reports', type: CONST.SEARCH.DATA_TYPES.EXPENSE, @@ -253,7 +253,7 @@ function getSuggestedSearches(defaultFeedID: string | undefined, accountID: numb return buildSearchQueryJSON(this.searchQuery)?.hash ?? CONST.DEFAULT_NUMBER_ID; }, }, - [CONST.SEARCH.SEARCH_LIST.CHATS]: { + [CONST.SEARCH.SEARCH_KEYS.CHATS]: { key: CONST.SEARCH.SEARCH_KEYS.CHATS, translationPath: 'common.chats', type: CONST.SEARCH.DATA_TYPES.CHAT, @@ -263,54 +263,53 @@ function getSuggestedSearches(defaultFeedID: string | undefined, accountID: numb return buildSearchQueryJSON(this.searchQuery)?.hash ?? CONST.DEFAULT_NUMBER_ID; }, }, - [CONST.SEARCH.SEARCH_LIST.SUBMIT]: { + [CONST.SEARCH.SEARCH_KEYS.SUBMIT]: { key: CONST.SEARCH.SEARCH_KEYS.SUBMIT, translationPath: 'common.submit', type: CONST.SEARCH.DATA_TYPES.EXPENSE, icon: Expensicons.Pencil, - searchQuery: getTodoSearchQuery(CONST.SEARCH.SEARCH_LIST.SUBMIT, accountID), + searchQuery: getTodoSearchQuery(CONST.SEARCH.SEARCH_KEYS.SUBMIT, accountID), get hash() { return buildSearchQueryJSON(this.searchQuery)?.hash ?? CONST.DEFAULT_NUMBER_ID; }, }, - [CONST.SEARCH.SEARCH_LIST.APPROVE]: { + [CONST.SEARCH.SEARCH_KEYS.APPROVE]: { key: CONST.SEARCH.SEARCH_KEYS.APPROVE, translationPath: 'search.bulkActions.approve', type: CONST.SEARCH.DATA_TYPES.EXPENSE, icon: Expensicons.ThumbsUp, - searchQuery: getTodoSearchQuery(CONST.SEARCH.SEARCH_LIST.APPROVE, accountID), + searchQuery: getTodoSearchQuery(CONST.SEARCH.SEARCH_KEYS.APPROVE, accountID), get hash() { return buildSearchQueryJSON(this.searchQuery)?.hash ?? CONST.DEFAULT_NUMBER_ID; }, }, - [CONST.SEARCH.SEARCH_LIST.PAY]: { + [CONST.SEARCH.SEARCH_KEYS.PAY]: { key: CONST.SEARCH.SEARCH_KEYS.PAY, translationPath: 'search.bulkActions.pay', type: CONST.SEARCH.DATA_TYPES.EXPENSE, icon: Expensicons.MoneyBag, - searchQuery: getTodoSearchQuery(CONST.SEARCH.SEARCH_LIST.PAY, accountID), + searchQuery: getTodoSearchQuery(CONST.SEARCH.SEARCH_KEYS.PAY, accountID), get hash() { return buildSearchQueryJSON(this.searchQuery)?.hash ?? CONST.DEFAULT_NUMBER_ID; }, }, - [CONST.SEARCH.SEARCH_LIST.EXPORT]: { + [CONST.SEARCH.SEARCH_KEYS.EXPORT]: { key: CONST.SEARCH.SEARCH_KEYS.EXPORT, translationPath: 'common.export', type: CONST.SEARCH.DATA_TYPES.EXPENSE, icon: Expensicons.CheckCircle, - searchQuery: getTodoSearchQuery(CONST.SEARCH.SEARCH_LIST.EXPORT, accountID), + searchQuery: getTodoSearchQuery(CONST.SEARCH.SEARCH_KEYS.EXPORT, accountID), get hash() { return buildSearchQueryJSON(this.searchQuery)?.hash ?? CONST.DEFAULT_NUMBER_ID; }, }, - [CONST.SEARCH.SEARCH_LIST.STATEMENTS]: { + [CONST.SEARCH.SEARCH_KEYS.STATEMENTS]: { key: CONST.SEARCH.SEARCH_KEYS.STATEMENTS, translationPath: 'search.statements', type: CONST.SEARCH.DATA_TYPES.EXPENSE, icon: Expensicons.CreditCard, searchQuery: buildQueryStringFromFilterFormValues({ type: CONST.SEARCH.DATA_TYPES.EXPENSE, - groupBy: CONST.SEARCH.GROUP_BY.CARDS, feed: defaultFeedID ? [defaultFeedID] : undefined, postedOn: CONST.SEARCH.DATE_PRESETS.LAST_STATEMENT, }), @@ -318,14 +317,13 @@ function getSuggestedSearches(defaultFeedID: string | undefined, accountID: numb return buildSearchQueryJSON(this.searchQuery)?.hash ?? CONST.DEFAULT_NUMBER_ID; }, }, - [CONST.SEARCH.SEARCH_LIST.UNAPPROVED_CASH]: { + [CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH]: { key: CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH, translationPath: 'search.unapprovedCash', type: CONST.SEARCH.DATA_TYPES.EXPENSE, icon: Expensicons.MoneyHourglass, searchQuery: buildQueryStringFromFilterFormValues({ type: CONST.SEARCH.DATA_TYPES.EXPENSE, - groupBy: CONST.SEARCH.GROUP_BY.REPORTS, status: [CONST.SEARCH.STATUS.EXPENSE.DRAFTS, CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING], reimbursable: CONST.SEARCH.BOOLEAN.YES, }), @@ -333,14 +331,13 @@ function getSuggestedSearches(defaultFeedID: string | undefined, accountID: numb return buildSearchQueryJSON(this.searchQuery)?.hash ?? CONST.DEFAULT_NUMBER_ID; }, }, - [CONST.SEARCH.SEARCH_LIST.UNAPPROVED_COMPANY_CARDS]: { + [CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_COMPANY_CARDS]: { key: CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_COMPANY_CARDS, translationPath: 'search.unapprovedCompanyCards', type: CONST.SEARCH.DATA_TYPES.EXPENSE, icon: Expensicons.CreditCardHourglass, searchQuery: buildQueryStringFromFilterFormValues({ type: CONST.SEARCH.DATA_TYPES.EXPENSE, - groupBy: CONST.SEARCH.GROUP_BY.MEMBERS, feed: defaultFeedID ? [defaultFeedID] : undefined, status: [CONST.SEARCH.STATUS.EXPENSE.DRAFTS, CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING], }), @@ -348,36 +345,6 @@ function getSuggestedSearches(defaultFeedID: string | undefined, accountID: numb return buildSearchQueryJSON(this.searchQuery)?.hash ?? CONST.DEFAULT_NUMBER_ID; }, }, - [CONST.SEARCH.SEARCH_LIST.UNAPPROVED_COMPANY_CARDS_ONLY]: { - key: CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_COMPANY_CARDS, - translationPath: 'search.unapproved', - type: CONST.SEARCH.DATA_TYPES.EXPENSE, - icon: Expensicons.Hourglass, - searchQuery: buildQueryStringFromFilterFormValues({ - type: CONST.SEARCH.DATA_TYPES.EXPENSE, - groupBy: CONST.SEARCH.GROUP_BY.MEMBERS, - feed: defaultFeedID ? [defaultFeedID] : undefined, - status: [CONST.SEARCH.STATUS.EXPENSE.DRAFTS, CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING], - }), - get hash() { - return buildSearchQueryJSON(this.searchQuery)?.hash ?? CONST.DEFAULT_NUMBER_ID; - }, - }, - [CONST.SEARCH.SEARCH_LIST.UNAPPROVED_CASH_ONLY]: { - key: CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH, - translationPath: 'search.unapproved', - type: CONST.SEARCH.DATA_TYPES.EXPENSE, - icon: Expensicons.Hourglass, - searchQuery: buildQueryStringFromFilterFormValues({ - type: CONST.SEARCH.DATA_TYPES.EXPENSE, - groupBy: CONST.SEARCH.GROUP_BY.REPORTS, - status: [CONST.SEARCH.STATUS.EXPENSE.DRAFTS, CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING], - reimbursable: CONST.SEARCH.BOOLEAN.YES, - }), - get hash() { - return buildSearchQueryJSON(this.searchQuery)?.hash ?? CONST.DEFAULT_NUMBER_ID; - }, - }, }; } @@ -443,16 +410,18 @@ function getSuggestedSearchesVisibility( }); return { - [CONST.SEARCH.SEARCH_LIST.EXPENSES]: true, - [CONST.SEARCH.SEARCH_LIST.REPORTS]: true, - [CONST.SEARCH.SEARCH_LIST.CHATS]: true, - [CONST.SEARCH.SEARCH_LIST.SUBMIT]: shouldShowSubmitSuggestion, - [CONST.SEARCH.SEARCH_LIST.PAY]: shouldShowPaySuggestion, - [CONST.SEARCH.SEARCH_LIST.APPROVE]: shouldShowApproveSuggestion, - [CONST.SEARCH.SEARCH_LIST.EXPORT]: shouldShowExportSuggestion, - [CONST.SEARCH.SEARCH_LIST.STATEMENTS]: shouldShowStatementsSuggestion, - [CONST.SEARCH.SEARCH_LIST.UNAPPROVED_CASH]: showShowUnapprovedCashSuggestion, - [CONST.SEARCH.SEARCH_LIST.UNAPPROVED_COMPANY_CARDS]: showShowUnapprovedCompanyCardsSuggestion, + [CONST.SEARCH.SEARCH_KEYS.EXPENSES]: true, + [CONST.SEARCH.SEARCH_KEYS.REPORTS]: true, + [CONST.SEARCH.SEARCH_KEYS.CHATS]: true, + [CONST.SEARCH.SEARCH_KEYS.SUBMIT]: shouldShowSubmitSuggestion, + [CONST.SEARCH.SEARCH_KEYS.PAY]: shouldShowPaySuggestion, + [CONST.SEARCH.SEARCH_KEYS.APPROVE]: shouldShowApproveSuggestion, + [CONST.SEARCH.SEARCH_KEYS.EXPORT]: shouldShowExportSuggestion, + // s77rt remove DEV lock + [CONST.SEARCH.SEARCH_KEYS.STATEMENTS]: shouldShowStatementsSuggestion && isDevelopment(), + [CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH]: showShowUnapprovedCashSuggestion, + // s77rt remove DEV lock + [CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_COMPANY_CARDS]: showShowUnapprovedCompanyCardsSuggestion && isDevelopment(), }; } @@ -1555,13 +1524,13 @@ function createTypeMenuSections( }; if (suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.EXPENSES]) { - exploreSection.menuItems.push(suggestedSearches[CONST.SEARCH.SEARCH_LIST.EXPENSES]); + exploreSection.menuItems.push(suggestedSearches[CONST.SEARCH.SEARCH_KEYS.EXPENSES]); } if (suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.REPORTS]) { - exploreSection.menuItems.push(suggestedSearches[CONST.SEARCH.SEARCH_LIST.REPORTS]); + exploreSection.menuItems.push(suggestedSearches[CONST.SEARCH.SEARCH_KEYS.REPORTS]); } if (suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.CHATS]) { - exploreSection.menuItems.push(suggestedSearches[CONST.SEARCH.SEARCH_LIST.CHATS]); + exploreSection.menuItems.push(suggestedSearches[CONST.SEARCH.SEARCH_KEYS.CHATS]); } if (exploreSection.menuItems.length > 0) { @@ -1579,7 +1548,7 @@ function createTypeMenuSections( if (suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.SUBMIT]) { const groupPoliciesWithChatEnabled = getGroupPaidPoliciesWithExpenseChatEnabled(policies); todoSection.menuItems.push({ - ...suggestedSearches[CONST.SEARCH.SEARCH_LIST.SUBMIT], + ...suggestedSearches[CONST.SEARCH.SEARCH_KEYS.SUBMIT], emptyState: { headerMedia: DotLottieAnimations.Fireworks, title: 'search.searchResults.emptySubmitResults.title', @@ -1624,7 +1593,7 @@ function createTypeMenuSections( } if (suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.APPROVE]) { todoSection.menuItems.push({ - ...suggestedSearches[CONST.SEARCH.SEARCH_LIST.APPROVE], + ...suggestedSearches[CONST.SEARCH.SEARCH_KEYS.APPROVE], emptyState: { headerMedia: DotLottieAnimations.Fireworks, title: 'search.searchResults.emptyApproveResults.title', @@ -1634,7 +1603,7 @@ function createTypeMenuSections( } if (suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.PAY]) { todoSection.menuItems.push({ - ...suggestedSearches[CONST.SEARCH.SEARCH_LIST.PAY], + ...suggestedSearches[CONST.SEARCH.SEARCH_KEYS.PAY], emptyState: { headerMedia: DotLottieAnimations.Fireworks, title: 'search.searchResults.emptyPayResults.title', @@ -1644,7 +1613,7 @@ function createTypeMenuSections( } if (suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.EXPORT]) { todoSection.menuItems.push({ - ...suggestedSearches[CONST.SEARCH.SEARCH_LIST.EXPORT], + ...suggestedSearches[CONST.SEARCH.SEARCH_KEYS.EXPORT], emptyState: { headerMedia: DotLottieAnimations.Fireworks, title: 'search.searchResults.emptyExportResults.title', @@ -1667,7 +1636,7 @@ function createTypeMenuSections( if (suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.STATEMENTS]) { accountingSection.menuItems.push({ - ...suggestedSearches[CONST.SEARCH.SEARCH_LIST.STATEMENTS], + ...suggestedSearches[CONST.SEARCH.SEARCH_KEYS.STATEMENTS], emptyState: { headerMedia: DotLottieAnimations.GenericEmptyState, title: 'search.searchResults.emptyStatementsResults.title', @@ -1675,37 +1644,19 @@ function createTypeMenuSections( }, }); } - if (suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH] && suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_COMPANY_CARDS]) { - accountingSection.menuItems.push( - { - ...suggestedSearches[CONST.SEARCH.SEARCH_LIST.UNAPPROVED_CASH], - emptyState: { - headerMedia: DotLottieAnimations.Fireworks, - title: 'search.searchResults.emptyUnapprovedResults.title', - subtitle: 'search.searchResults.emptyUnapprovedResults.subtitle', - }, - }, - { - ...suggestedSearches[CONST.SEARCH.SEARCH_LIST.UNAPPROVED_COMPANY_CARDS], - emptyState: { - headerMedia: DotLottieAnimations.Fireworks, - title: 'search.searchResults.emptyUnapprovedResults.title', - subtitle: 'search.searchResults.emptyUnapprovedResults.subtitle', - }, - }, - ); - } else if (suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH]) { + if (suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH]) { accountingSection.menuItems.push({ - ...suggestedSearches[CONST.SEARCH.SEARCH_LIST.UNAPPROVED_CASH_ONLY], + ...suggestedSearches[CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH], emptyState: { headerMedia: DotLottieAnimations.Fireworks, title: 'search.searchResults.emptyUnapprovedResults.title', subtitle: 'search.searchResults.emptyUnapprovedResults.subtitle', }, }); - } else if (suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_COMPANY_CARDS]) { + } + if (suggestedSearchesVisibility[CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_COMPANY_CARDS]) { accountingSection.menuItems.push({ - ...suggestedSearches[CONST.SEARCH.SEARCH_LIST.UNAPPROVED_COMPANY_CARDS_ONLY], + ...suggestedSearches[CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_COMPANY_CARDS], emptyState: { headerMedia: DotLottieAnimations.Fireworks, title: 'search.searchResults.emptyUnapprovedResults.title', @@ -1714,8 +1665,7 @@ function createTypeMenuSections( }); } - // s77rt remove DEV lock - if (accountingSection.menuItems.length > 0 && isDevelopment()) { + if (accountingSection.menuItems.length > 0) { typeMenuSections.push(accountingSection); } } @@ -1829,4 +1779,4 @@ export { isTransactionAmountTooLong, isTransactionTaxAmountTooLong, }; -export type {SavedSearchMenuItem, SearchTypeMenuSection, SearchTypeMenuItem, SearchDateModifier, SearchDateModifierLower, SearchKey as SuggestedSearchKey}; +export type {SavedSearchMenuItem, SearchTypeMenuSection, SearchTypeMenuItem, SearchDateModifier, SearchDateModifierLower, SearchKey}; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 7eaa22d158a3..4a39b875e993 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -11543,11 +11543,11 @@ function shouldOptimisticallyUpdateSearch(currentSearchQueryJSON: SearchQueryJSO return false; } - const submitQueryString = getTodoSearchQuery(CONST.SEARCH.SEARCH_LIST.SUBMIT, userAccountID); + const submitQueryString = getTodoSearchQuery(CONST.SEARCH.SEARCH_KEYS.SUBMIT, userAccountID); const submitQueryJSON = buildSearchQueryJSON(submitQueryString); - const approveQueryString = getTodoSearchQuery(CONST.SEARCH.SEARCH_LIST.APPROVE, userAccountID); + const approveQueryString = getTodoSearchQuery(CONST.SEARCH.SEARCH_KEYS.APPROVE, userAccountID); const approveQueryJSON = buildSearchQueryJSON(approveQueryString); const validSearchTypes = diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index d2b27aff7621..97f95744c0bc 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -15,7 +15,7 @@ import {rand64} from '@libs/NumberUtils'; import {getSubmitToAccountID, getValidConnectedIntegration} from '@libs/PolicyUtils'; import type {OptimisticExportIntegrationAction} from '@libs/ReportUtils'; import {buildOptimisticExportIntegrationAction, hasHeldExpenses} from '@libs/ReportUtils'; -import type {SuggestedSearchKey} from '@libs/SearchUIUtils'; +import type {SearchKey} from '@libs/SearchUIUtils'; import {isTransactionGroupListItemType, isTransactionListItemType} from '@libs/SearchUIUtils'; import playSound, {SOUNDS} from '@libs/Sound'; import CONST from '@src/CONST'; @@ -49,7 +49,7 @@ function handleActionButtonPress( item: TransactionListItemType | TransactionReportGroupListItemType, goToItem: () => void, isInMobileSelectionMode: boolean, - currentSearchKey?: SuggestedSearchKey, + currentSearchKey?: SearchKey, ) { // The transactionIDList is needed to handle actions taken on `status:""` where transactions on single expense reports can be approved/paid. // We need the transactionID to display the loading indicator for that list item's action. @@ -108,7 +108,7 @@ function getLastPolicyPaymentMethod(policyID: string | undefined, lastPaymentMet return lastPolicyPaymentMethod; } -function getPayActionCallback(hash: number, item: TransactionListItemType | TransactionReportGroupListItemType, goToItem: () => void, currentSearchKey?: SuggestedSearchKey) { +function getPayActionCallback(hash: number, item: TransactionListItemType | TransactionReportGroupListItemType, goToItem: () => void, currentSearchKey?: SearchKey) { const lastPolicyPaymentMethod = getLastPolicyPaymentMethod(item.policyID, lastPaymentMethod); if (!lastPolicyPaymentMethod) { @@ -276,11 +276,22 @@ function openSearchPage() { API.read(READ_COMMANDS.OPEN_SEARCH_PAGE, null); } -function search({queryJSON, offset, shouldCalculateTotals = false}: {queryJSON: SearchQueryJSON; offset?: number; shouldCalculateTotals?: boolean}) { +function search({ + queryJSON, + searchKey, + offset, + shouldCalculateTotals = false, +}: { + queryJSON: SearchQueryJSON; + searchKey: SearchKey | undefined; + offset?: number; + shouldCalculateTotals?: boolean; +}) { const {optimisticData, finallyData, failureData} = getOnyxLoadingData(queryJSON.hash, queryJSON); const {flatFilters, ...queryJSONWithoutFlatFilters} = queryJSON; const query = { ...queryJSONWithoutFlatFilters, + searchKey, offset, shouldCalculateTotals, }; @@ -310,7 +321,7 @@ function holdMoneyRequestOnSearch(hash: number, transactionIDList: string[], com API.write(WRITE_COMMANDS.HOLD_MONEY_REQUEST_ON_SEARCH, {hash, transactionIDList, comment}, {optimisticData, finallyData}); } -function submitMoneyRequestOnSearch(hash: number, reportList: SearchReport[], policy: SearchPolicy[], transactionIDList?: string[], currentSearchKey?: SuggestedSearchKey) { +function submitMoneyRequestOnSearch(hash: number, reportList: SearchReport[], policy: SearchPolicy[], transactionIDList?: string[], currentSearchKey?: SearchKey) { const createOnyxData = (update: Partial | Partial | null): OnyxUpdate[] => [ { onyxMethod: Onyx.METHOD.MERGE, @@ -340,7 +351,7 @@ function submitMoneyRequestOnSearch(hash: number, reportList: SearchReport[], po API.write(WRITE_COMMANDS.SUBMIT_REPORT, parameters, {optimisticData, successData, failureData}); } -function approveMoneyRequestOnSearch(hash: number, reportIDList: string[], transactionIDList?: string[], currentSearchKey?: SuggestedSearchKey) { +function approveMoneyRequestOnSearch(hash: number, reportIDList: string[], transactionIDList?: string[], currentSearchKey?: SearchKey) { const createOnyxData = (update: Partial | Partial | null): OnyxUpdate[] => [ { onyxMethod: Onyx.METHOD.MERGE, @@ -362,7 +373,7 @@ function approveMoneyRequestOnSearch(hash: number, reportIDList: string[], trans API.write(WRITE_COMMANDS.APPROVE_MONEY_REQUEST_ON_SEARCH, {hash, reportIDList}, {optimisticData, failureData, successData}); } -function exportToIntegrationOnSearch(hash: number, reportID: string, connectionName: ConnectionName, currentSearchKey?: SuggestedSearchKey) { +function exportToIntegrationOnSearch(hash: number, reportID: string, connectionName: ConnectionName, currentSearchKey?: SearchKey) { const optimisticAction = buildOptimisticExportIntegrationAction(connectionName); const successAction: OptimisticExportIntegrationAction = {...optimisticAction, pendingAction: null}; const optimisticReportActionID = optimisticAction.reportActionID; @@ -403,7 +414,7 @@ function exportToIntegrationOnSearch(hash: number, reportID: string, connectionN API.write(WRITE_COMMANDS.REPORT_EXPORT, params, {optimisticData, failureData, successData}); } -function payMoneyRequestOnSearch(hash: number, paymentData: PaymentData[], transactionIDList?: string[], currentSearchKey?: SuggestedSearchKey) { +function payMoneyRequestOnSearch(hash: number, paymentData: PaymentData[], transactionIDList?: string[], currentSearchKey?: SearchKey) { const createOnyxData = (update: Partial | Partial | null): OnyxUpdate[] => [ { onyxMethod: Onyx.METHOD.MERGE, diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 3f748195e886..875e1eade777 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -11,6 +11,7 @@ import * as Expensicons from '@components/Icon/Expensicons'; import ScreenWrapper from '@components/ScreenWrapper'; import Search from '@components/Search'; import {useSearchContext} from '@components/Search/SearchContext'; +import SearchPageFooter from '@components/Search/SearchPageFooter'; import SearchFiltersBar from '@components/Search/SearchPageHeader/SearchFiltersBar'; import type {SearchHeaderOptionValue} from '@components/Search/SearchPageHeader/SearchPageHeader'; import SearchPageHeader from '@components/Search/SearchPageHeader/SearchPageHeader'; @@ -472,7 +473,19 @@ function SearchPage({route}: SearchPageProps) { const handleOnBackButtonPress = () => Navigation.goBack(ROUTES.SEARCH_ROOT.getRoute({query: buildCannedSearchQuery()})); const {resetVideoPlayerData} = usePlaybackContext(); - const shouldShowOfflineIndicator = currentSearchResults?.data ?? lastNonEmptySearchResults.current; + + const searchResults = currentSearchResults?.data ? currentSearchResults : lastNonEmptySearchResults.current; + const metadata = searchResults?.search; + const shouldShowOfflineIndicator = !!searchResults?.data; + const shouldShowFooter = !!metadata?.count; + + const offlineIndicatorStyle = useMemo(() => { + if (shouldShowFooter) { + return [styles.mtAuto, styles.pAbsolute, styles.h10, styles.b0]; + } + + return [styles.mtAuto]; + }, [shouldShowFooter, styles]); // Handles video player cleanup: // 1. On mount: Resets player if navigating from report screen @@ -509,7 +522,7 @@ function SearchPage({route}: SearchPageProps) { @@ -579,7 +592,7 @@ function SearchPage({route}: SearchPageProps) { {PDFValidationComponent} @@ -597,10 +610,11 @@ function SearchPage({route}: SearchPageProps) { + {shouldShowFooter && } )} + {shouldShowFooter && } ); diff --git a/src/types/onyx/SearchResults.ts b/src/types/onyx/SearchResults.ts index e20e038b80ff..d370b62706e2 100644 --- a/src/types/onyx/SearchResults.ts +++ b/src/types/onyx/SearchResults.ts @@ -68,6 +68,15 @@ type SearchResultsInfo = { /** The optional columns that should be shown according to policy settings */ columnsToShow: ColumnsToShow; + + /** The number of results */ + count?: number; + + /** The total spend */ + total?: number; + + /** The currency of the total spend */ + currency?: string; }; /** Model of personal details search result */ @@ -515,4 +524,5 @@ export type { SearchReportAction, SearchPolicy, SearchCard, + SearchResultsInfo, }; diff --git a/tests/ui/ReportListItemHeaderTest.tsx b/tests/ui/ReportListItemHeaderTest.tsx index 5499ea3ac563..7040569ab3d7 100644 --- a/tests/ui/ReportListItemHeaderTest.tsx +++ b/tests/ui/ReportListItemHeaderTest.tsx @@ -29,7 +29,7 @@ const mockSearchContext = { setSelectedReports: jest.fn(), clearSelectedTransactions: jest.fn(), setLastSearchType: jest.fn(), - setCurrentSearchHash: jest.fn(), + setCurrentSearchHashAndKey: jest.fn(), setSelectedTransactions: jest.fn(), setShouldShowFiltersBarLoading: jest.fn(), setShouldShowExportModeOption: jest.fn(), diff --git a/tests/unit/useSearchHighlightAndScrollTest.ts b/tests/unit/useSearchHighlightAndScrollTest.ts index 248f901a657d..e42d23c65f3c 100644 --- a/tests/unit/useSearchHighlightAndScrollTest.ts +++ b/tests/unit/useSearchHighlightAndScrollTest.ts @@ -60,6 +60,8 @@ describe('useSearchHighlightAndScroll', () => { hash: 123, recentSearchHash: 456, }, + searchKey: undefined, + shouldCalculateTotals: false, offset: 0, }; @@ -93,7 +95,7 @@ describe('useSearchHighlightAndScroll', () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error rerender(updatedProps); - expect(search).toHaveBeenCalledWith({queryJSON: baseProps.queryJSON, offset: 0}); + expect(search).toHaveBeenCalledWith({queryJSON: baseProps.queryJSON, searchKey: undefined, offset: 0, shouldCalculateTotals: false}); }); it('should not trigger search when not focused', () => { @@ -151,7 +153,7 @@ describe('useSearchHighlightAndScroll', () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error rerender(updatedProps); - expect(search).toHaveBeenCalledWith({queryJSON: chatProps.queryJSON, offset: 0}); + expect(search).toHaveBeenCalledWith({queryJSON: chatProps.queryJSON, searchKey: undefined, offset: 0, shouldCalculateTotals: false}); }); it('should not trigger search when new transaction removed and focused', () => { From 13ad4fdfd6aa30d19f9aa3cc5d95d261dd1fb3b0 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Mon, 28 Jul 2025 15:57:20 +0100 Subject: [PATCH 2/4] remove approved report if approved in Unapproved suggestion search --- src/libs/actions/Search.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index 97f95744c0bc..a21bb49ac518 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -366,8 +366,15 @@ function approveMoneyRequestOnSearch(hash: number, reportIDList: string[], trans const optimisticData: OnyxUpdate[] = createOnyxData({isActionLoading: true}); const failureData: OnyxUpdate[] = createOnyxData({isActionLoading: false, errors: getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage')}); - // If we are on the 'Approve' suggested search, remove the report from the view once the action is taken, don't wait for the view to be re-fetched via Search - const successData: OnyxUpdate[] = currentSearchKey === CONST.SEARCH.SEARCH_KEYS.APPROVE ? createOnyxData(null) : createOnyxData({isActionLoading: false}); + + // If we are on the 'Approve', `Unapproved cash` or the `Unapproved company cards` suggested search, remove the report from the view once the action is taken, don't wait for the view to be re-fetched via Search + const approveActionSuggestedSearches: Partial = [ + CONST.SEARCH.SEARCH_KEYS.APPROVE, + CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH, + CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_COMPANY_CARDS, + ]; + + const successData: OnyxUpdate[] = approveActionSuggestedSearches.includes(currentSearchKey) ? createOnyxData(null) : createOnyxData({isActionLoading: false}); playSound(SOUNDS.SUCCESS); API.write(WRITE_COMMANDS.APPROVE_MONEY_REQUEST_ON_SEARCH, {hash, reportIDList}, {optimisticData, failureData, successData}); From e4e6918d13a69b1ecff270a26e63693071d52863 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Mon, 28 Jul 2025 16:59:55 +0100 Subject: [PATCH 3/4] trigger search if length of transactions or report actions changes --- src/hooks/useSearchHighlightAndScroll.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/useSearchHighlightAndScroll.ts b/src/hooks/useSearchHighlightAndScroll.ts index 60d855fad7c4..cefb572a51c0 100644 --- a/src/hooks/useSearchHighlightAndScroll.ts +++ b/src/hooks/useSearchHighlightAndScroll.ts @@ -76,8 +76,8 @@ function useSearchHighlightAndScroll({ const previousTransactionsIDsSet = new Set(previousTransactionsIDs); const previousReportActionsIDsSet = new Set(previousReportActionsIDs); - const hasTransactionsIDsChange = transactionsIDs.some((id) => !previousTransactionsIDsSet.has(id)); - const hasReportActionsIDsChange = reportActionsIDs.some((id) => !previousReportActionsIDsSet.has(id)); + const hasTransactionsIDsChange = transactionsIDs.length !== previousTransactionsIDs.length || transactionsIDs.some((id) => !previousTransactionsIDsSet.has(id)); + const hasReportActionsIDsChange = reportActionsIDs.length !== previousReportActionsIDs.length || reportActionsIDs.some((id) => !previousReportActionsIDsSet.has(id)); // Check if there is a change in the transactions or report actions list if ((!isChat && hasTransactionsIDsChange) || hasReportActionsIDsChange || hasPendingSearchRef.current) { From 881088d2c6df874827004d9a24d0615b2233f7e3 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Mon, 28 Jul 2025 17:24:31 +0100 Subject: [PATCH 4/4] don't trigger search if report actions are removed --- src/hooks/useSearchHighlightAndScroll.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useSearchHighlightAndScroll.ts b/src/hooks/useSearchHighlightAndScroll.ts index cefb572a51c0..fc421f62067f 100644 --- a/src/hooks/useSearchHighlightAndScroll.ts +++ b/src/hooks/useSearchHighlightAndScroll.ts @@ -77,7 +77,7 @@ function useSearchHighlightAndScroll({ const previousTransactionsIDsSet = new Set(previousTransactionsIDs); const previousReportActionsIDsSet = new Set(previousReportActionsIDs); const hasTransactionsIDsChange = transactionsIDs.length !== previousTransactionsIDs.length || transactionsIDs.some((id) => !previousTransactionsIDsSet.has(id)); - const hasReportActionsIDsChange = reportActionsIDs.length !== previousReportActionsIDs.length || reportActionsIDs.some((id) => !previousReportActionsIDsSet.has(id)); + const hasReportActionsIDsChange = reportActionsIDs.some((id) => !previousReportActionsIDsSet.has(id)); // Check if there is a change in the transactions or report actions list if ((!isChat && hasTransactionsIDsChange) || hasReportActionsIDsChange || hasPendingSearchRef.current) {