diff --git a/assets/images/simple-illustrations/simple-illustration__envelopereceipt.svg b/assets/images/simple-illustrations/simple-illustration__envelopereceipt.svg
new file mode 100644
index 000000000000..eb2bad31620d
--- /dev/null
+++ b/assets/images/simple-illustrations/simple-illustration__envelopereceipt.svg
@@ -0,0 +1,49 @@
+
+
\ No newline at end of file
diff --git a/src/CONST.ts b/src/CONST.ts
index f1e4a3bb46d5..22eb8215c6f0 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -5243,8 +5243,6 @@ const CONST = {
SEARCH: {
RESULTS_PAGE_SIZE: 50,
DATA_TYPES: {
- TRANSACTION: 'transaction',
- REPORT: 'report',
EXPENSE: 'expense',
INVOICE: 'invoice',
TRIP: 'trip',
@@ -5273,9 +5271,10 @@ const CONST = {
STATUS: {
EXPENSE: {
ALL: 'all',
- SHARED: 'shared',
DRAFTS: 'drafts',
- FINISHED: 'finished',
+ OUTSTANDING: 'outstanding',
+ APPROVED: 'approved',
+ PAID: 'paid',
},
INVOICE: {
ALL: 'all',
@@ -5290,14 +5289,6 @@ const CONST = {
PAID: 'paid',
},
},
- TAB: {
- EXPENSE: {
- ALL: 'type:expense status:all',
- SHARED: 'type:expense status:shared',
- DRAFTS: 'type:expense status:drafts',
- FINISHED: 'type:expense status:finished',
- },
- },
TABLE_COLUMNS: {
RECEIPT: 'receipt',
DATE: 'date',
diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx
index 126c81961cee..1441266c76de 100644
--- a/src/components/Button/index.tsx
+++ b/src/components/Button/index.tsx
@@ -29,6 +29,9 @@ type ButtonProps = Partial & {
/** The fill color to pass into the icon. */
iconFill?: string;
+ /** The fill color to pass into the icon when the button is hovered. */
+ iconHoverFill?: string;
+
/** Any additional styles to pass to the left icon container. */
iconStyles?: StyleProp;
@@ -80,9 +83,15 @@ type ButtonProps = Partial & {
/** Additional text styles */
textStyles?: StyleProp;
+ /** Additional text styles when the button is hovered */
+ textHoverStyles?: StyleProp;
+
/** Whether we should use the default hover style */
shouldUseDefaultHover?: boolean;
+ /** Additional hover styles */
+ hoverStyles?: StyleProp;
+
/** Whether we should use the success theme color */
success?: boolean;
@@ -170,6 +179,7 @@ function Button(
iconRight = Expensicons.ArrowRight,
iconFill,
+ iconHoverFill,
icon = null,
iconStyles = [],
iconRightStyles = [],
@@ -194,8 +204,10 @@ function Button(
style = [],
innerStyles = [],
textStyles = [],
+ textHoverStyles = [],
shouldUseDefaultHover = true,
+ hoverStyles = undefined,
success = false,
danger = false,
@@ -238,6 +250,7 @@ function Button(
danger && styles.buttonDangerText,
!!icon && styles.textAlignLeft,
textStyles,
+ isHovered && textHoverStyles,
link && styles.link,
link && isHovered && StyleUtils.getColorStyle(theme.linkHover),
link && styles.fontWeightNormal,
@@ -259,7 +272,7 @@ function Button(
) : (
& {
subtitleStyles?: StyleProp;
@@ -100,13 +100,23 @@ type SearchPageHeaderProps = {
type SearchHeaderOptionValue = DeepValueOf | undefined;
-const headerContent: {[key in SearchStatus]: {icon: IconAsset; titleText: TranslationPaths}} = {
- all: {icon: Illustrations.MoneyReceipts, titleText: 'common.expenses'},
- shared: {icon: Illustrations.SendMoney, titleText: 'common.shared'},
- drafts: {icon: Illustrations.Pencil, titleText: 'common.drafts'},
- finished: {icon: Illustrations.CheckmarkCircle, titleText: 'common.finished'},
+type HeaderContent = {
+ icon: IconAsset;
+ titleText: TranslationPaths;
};
+function getHeaderContent(type: SearchDataTypes): HeaderContent {
+ switch (type) {
+ case CONST.SEARCH.DATA_TYPES.INVOICE:
+ return {icon: Illustrations.EnvelopeReceipt, titleText: 'workspace.common.invoices'};
+ case CONST.SEARCH.DATA_TYPES.TRIP:
+ return {icon: Illustrations.Luggage, titleText: 'travel.trips'};
+ case CONST.SEARCH.DATA_TYPES.EXPENSE:
+ default:
+ return {icon: Illustrations.MoneyReceipts, titleText: 'common.expenses'};
+ }
+}
+
function SearchPageHeader({queryJSON, hash, onSelectDeleteOption, setOfflineModalOpen, setDownloadErrorModalOpen, isCustomQuery, data}: SearchPageHeaderProps) {
const {translate} = useLocalize();
const theme = useTheme();
@@ -131,10 +141,10 @@ function SearchPageHeader({queryJSON, hash, onSelectDeleteOption, setOfflineModa
.map((item) => item.reportID),
[data, selectedTransactions],
);
- const {status} = queryJSON;
- const headerSubtitle = isCustomQuery ? SearchUtils.getSearchHeaderTitle(queryJSON) : translate(headerContent[status]?.titleText);
+ const {status, type} = queryJSON;
+ const headerSubtitle = isCustomQuery ? SearchUtils.getSearchHeaderTitle(queryJSON) : translate(getHeaderContent(type).titleText);
const headerTitle = isCustomQuery ? translate('search.filtersHeader') : '';
- const headerIcon = isCustomQuery ? Illustrations.Filters : headerContent[status]?.icon;
+ const headerIcon = isCustomQuery ? Illustrations.Filters : getHeaderContent(type).icon;
const subtitleStyles = isCustomQuery ? {} : styles.textHeadlineH2;
diff --git a/src/components/Search/SearchStatusBar.tsx b/src/components/Search/SearchStatusBar.tsx
new file mode 100644
index 000000000000..7c1ffeff1818
--- /dev/null
+++ b/src/components/Search/SearchStatusBar.tsx
@@ -0,0 +1,166 @@
+import React from 'react';
+import Button from '@components/Button';
+import * as Expensicons from '@components/Icon/Expensicons';
+import ScrollView from '@components/ScrollView';
+import useLocalize from '@hooks/useLocalize';
+import useSingleExecution from '@hooks/useSingleExecution';
+import useStyleUtils from '@hooks/useStyleUtils';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+import Navigation from '@libs/Navigation/Navigation';
+import * as SearchUtils from '@libs/SearchUtils';
+import CONST from '@src/CONST';
+import type {TranslationPaths} from '@src/languages/types';
+import type {SearchDataTypes} from '@src/types/onyx/SearchResults';
+import type IconAsset from '@src/types/utils/IconAsset';
+import type {ExpenseSearchStatus, InvoiceSearchStatus, SearchQueryString, SearchStatus, TripSearchStatus} from './types';
+
+type SearchStatusBarProps = {
+ type: SearchDataTypes;
+ status: SearchStatus;
+};
+
+const expenseOptions: Array<{key: ExpenseSearchStatus; icon: IconAsset; text: TranslationPaths; query: SearchQueryString}> = [
+ {
+ key: CONST.SEARCH.STATUS.EXPENSE.ALL,
+ icon: Expensicons.All,
+ text: 'common.all',
+ query: SearchUtils.buildCannedSearchQuery(CONST.SEARCH.DATA_TYPES.EXPENSE, CONST.SEARCH.STATUS.EXPENSE.ALL),
+ },
+ {
+ key: CONST.SEARCH.STATUS.EXPENSE.DRAFTS,
+ icon: Expensicons.Pencil,
+ text: 'common.drafts',
+ query: SearchUtils.buildCannedSearchQuery(CONST.SEARCH.DATA_TYPES.EXPENSE, CONST.SEARCH.STATUS.EXPENSE.DRAFTS),
+ },
+ {
+ key: CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING,
+ icon: Expensicons.Hourglass,
+ text: 'common.outstanding',
+ query: SearchUtils.buildCannedSearchQuery(CONST.SEARCH.DATA_TYPES.EXPENSE, CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING),
+ },
+ {
+ key: CONST.SEARCH.STATUS.EXPENSE.APPROVED,
+ icon: Expensicons.ThumbsUp,
+ text: 'iou.approved',
+ query: SearchUtils.buildCannedSearchQuery(CONST.SEARCH.DATA_TYPES.EXPENSE, CONST.SEARCH.STATUS.EXPENSE.APPROVED),
+ },
+ {
+ key: CONST.SEARCH.STATUS.EXPENSE.PAID,
+ icon: Expensicons.MoneyBag,
+ text: 'iou.settledExpensify',
+ query: SearchUtils.buildCannedSearchQuery(CONST.SEARCH.DATA_TYPES.EXPENSE, CONST.SEARCH.STATUS.EXPENSE.PAID),
+ },
+];
+
+const invoiceOptions: Array<{key: InvoiceSearchStatus; icon: IconAsset; text: TranslationPaths; query: SearchQueryString}> = [
+ {
+ key: CONST.SEARCH.STATUS.INVOICE.ALL,
+ icon: Expensicons.All,
+ text: 'common.all',
+ query: SearchUtils.buildCannedSearchQuery(CONST.SEARCH.DATA_TYPES.INVOICE, CONST.SEARCH.STATUS.INVOICE.ALL),
+ },
+ {
+ key: CONST.SEARCH.STATUS.INVOICE.OUTSTANDING,
+ icon: Expensicons.Hourglass,
+ text: 'common.outstanding',
+ query: SearchUtils.buildCannedSearchQuery(CONST.SEARCH.DATA_TYPES.INVOICE, CONST.SEARCH.STATUS.INVOICE.OUTSTANDING),
+ },
+ {
+ key: CONST.SEARCH.STATUS.INVOICE.PAID,
+ icon: Expensicons.MoneyBag,
+ text: 'iou.settledExpensify',
+ query: SearchUtils.buildCannedSearchQuery(CONST.SEARCH.DATA_TYPES.INVOICE, CONST.SEARCH.STATUS.INVOICE.PAID),
+ },
+];
+
+const tripOptions: Array<{key: TripSearchStatus; icon: IconAsset; text: TranslationPaths; query: SearchQueryString}> = [
+ {
+ key: CONST.SEARCH.STATUS.TRIP.ALL,
+ icon: Expensicons.All,
+ text: 'common.all',
+ query: SearchUtils.buildCannedSearchQuery(CONST.SEARCH.DATA_TYPES.TRIP, CONST.SEARCH.STATUS.TRIP.ALL),
+ },
+ {
+ key: CONST.SEARCH.STATUS.TRIP.DRAFTS,
+ icon: Expensicons.Pencil,
+ text: 'common.drafts',
+ query: SearchUtils.buildCannedSearchQuery(CONST.SEARCH.DATA_TYPES.TRIP, CONST.SEARCH.STATUS.TRIP.DRAFTS),
+ },
+ {
+ key: CONST.SEARCH.STATUS.TRIP.OUTSTANDING,
+ icon: Expensicons.Hourglass,
+ text: 'common.outstanding',
+ query: SearchUtils.buildCannedSearchQuery(CONST.SEARCH.DATA_TYPES.TRIP, CONST.SEARCH.STATUS.TRIP.OUTSTANDING),
+ },
+ {
+ key: CONST.SEARCH.STATUS.TRIP.APPROVED,
+ icon: Expensicons.ThumbsUp,
+ text: 'iou.approved',
+ query: SearchUtils.buildCannedSearchQuery(CONST.SEARCH.DATA_TYPES.TRIP, CONST.SEARCH.STATUS.TRIP.APPROVED),
+ },
+ {
+ key: CONST.SEARCH.STATUS.TRIP.PAID,
+ icon: Expensicons.MoneyBag,
+ text: 'iou.settledExpensify',
+ query: SearchUtils.buildCannedSearchQuery(CONST.SEARCH.DATA_TYPES.TRIP, CONST.SEARCH.STATUS.TRIP.PAID),
+ },
+];
+
+function getOptions(type: SearchDataTypes) {
+ switch (type) {
+ case CONST.SEARCH.DATA_TYPES.INVOICE:
+ return invoiceOptions;
+ case CONST.SEARCH.DATA_TYPES.TRIP:
+ return tripOptions;
+ case CONST.SEARCH.DATA_TYPES.EXPENSE:
+ default:
+ return expenseOptions;
+ }
+}
+
+function SearchStatusBar({type, status}: SearchStatusBarProps) {
+ const {singleExecution} = useSingleExecution();
+ const styles = useThemeStyles();
+ const StyleUtils = useStyleUtils();
+ const theme = useTheme();
+ const {translate} = useLocalize();
+ const options = getOptions(type);
+
+ return (
+
+ {options.map((item, index) => {
+ const onPress = singleExecution(() => Navigation.setParams({q: item.query}));
+ const isActive = status === item.key;
+ const isFirstItem = index === 0;
+ const isLastItem = index === options.length - 1;
+
+ return (
+
+ );
+ })}
+
+ );
+}
+
+SearchStatusBar.displayName = 'SearchStatusBar';
+
+export default SearchStatusBar;
diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx
index df5398bffffa..db729a9aa77d 100644
--- a/src/components/Search/index.tsx
+++ b/src/components/Search/index.tsx
@@ -31,6 +31,7 @@ import ROUTES from '@src/ROUTES';
import type SearchResults from '@src/types/onyx/SearchResults';
import {useSearchContext} from './SearchContext';
import SearchPageHeader from './SearchPageHeader';
+import SearchStatusBar from './SearchStatusBar';
import type {SearchColumnType, SearchQueryJSON, SearchStatus, SelectedTransactionInfo, SelectedTransactions, SortOrder} from './types';
type SearchProps = {
@@ -88,7 +89,7 @@ function Search({queryJSON, policyIDs, isCustomQuery}: SearchProps) {
const [selectedTransactionsToDelete, setSelectedTransactionsToDelete] = useState([]);
const [deleteExpensesConfirmModalVisible, setDeleteExpensesConfirmModalVisible] = useState(false);
const [downloadErrorModalVisible, setDownloadErrorModalVisible] = useState(false);
- const {status, sortBy, sortOrder, hash} = queryJSON;
+ const {type, status, sortBy, sortOrder, hash} = queryJSON;
const [currentSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`);
@@ -176,11 +177,12 @@ function Search({queryJSON, policyIDs, isCustomQuery}: SearchProps) {
const searchResults = currentSearchResults?.data ? currentSearchResults : lastSearchResultsRef.current;
- const isDataLoaded = searchResults?.data !== undefined;
+ // There's a race condition in Onyx which makes it return data from the previous Search, so in addition to checking that the data is loaded
+ // we also need to check that the searchResults matches the type and status of the current search
+ const isDataLoaded = searchResults?.data !== undefined && searchResults?.search?.type === type && searchResults?.search?.status === status;
const shouldShowLoadingState = !isOffline && !isDataLoaded;
const shouldShowLoadingMoreItems = !shouldShowLoadingState && searchResults?.search?.isLoading && searchResults?.search?.offset > 0;
-
- const isSearchResultsEmpty = !searchResults || SearchUtils.isSearchResultsEmpty(searchResults);
+ const isSearchResultsEmpty = !searchResults?.data || SearchUtils.isSearchResultsEmpty(searchResults);
const prevIsSearchResultEmpty = usePrevious(isSearchResultsEmpty);
useEffect(() => {
@@ -203,16 +205,14 @@ function Search({queryJSON, policyIDs, isCustomQuery}: SearchProps) {
);
}
- const type = SearchUtils.getSearchType(searchResults?.search);
-
- if (searchResults === undefined || type === undefined) {
+ if (searchResults === undefined) {
Log.alert('[Search] Undefined search type');
return null;
}
- const ListItem = SearchUtils.getListItem(type, status);
- const data = SearchUtils.getSections(type, status, searchResults.data, searchResults.search);
- const sortedData = SearchUtils.getSortedSections(type, status, data, sortBy, sortOrder);
+ const ListItem = SearchUtils.getListItem(status);
+ const data = SearchUtils.getSections(status, searchResults.data, searchResults.search);
+ const sortedData = SearchUtils.getSortedSections(status, data, sortBy, sortOrder);
const sortedSelectedData = sortedData.map((item) => mapToItemWithSelectionInfo(item, selectedTransactions, canSelectMultiple));
const shouldShowEmptyState = !isDataLoaded || data.length === 0;
@@ -225,6 +225,10 @@ function Search({queryJSON, policyIDs, isCustomQuery}: SearchProps) {
queryJSON={queryJSON}
hash={hash}
/>
+
>
);
@@ -281,7 +285,7 @@ function Search({queryJSON, policyIDs, isCustomQuery}: SearchProps) {
};
const toggleAllTransactions = () => {
- const areItemsOfReportType = searchResults?.search.type === CONST.SEARCH.DATA_TYPES.REPORT;
+ const areItemsOfReportType = status !== CONST.SEARCH.STATUS.EXPENSE.ALL;
const flattenedItems = areItemsOfReportType ? (data as ReportListItemType[]).flatMap((item) => item.transactions) : data;
const isAllSelected = flattenedItems.length === Object.keys(selectedTransactions).length;
@@ -318,7 +322,10 @@ function Search({queryJSON, policyIDs, isCustomQuery}: SearchProps) {
setOfflineModalOpen={() => setOfflineModalVisible(true)}
setDownloadErrorModalOpen={() => setDownloadErrorModalVisible(true)}
/>
-
+
sections={[{data: sortedSelectedData, isDisabled: false}]}
turnOnSelectionModeOnLongPress
diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts
index 54b26ea6bac4..c933deb8ee03 100644
--- a/src/components/Search/types.ts
+++ b/src/components/Search/types.ts
@@ -1,5 +1,6 @@
import type {ValueOf} from 'react-native-gesture-handler/lib/typescript/typeUtils';
import type CONST from '@src/CONST';
+import type {SearchDataTypes} from '@src/types/onyx/SearchResults';
/** Model of the selected transaction */
type SelectedTransactionInfo = {
@@ -25,7 +26,9 @@ type SelectedTransactions = Record;
type SortOrder = ValueOf;
type SearchColumnType = ValueOf;
type ExpenseSearchStatus = ValueOf;
-type SearchStatus = ExpenseSearchStatus;
+type InvoiceSearchStatus = ValueOf;
+type TripSearchStatus = ValueOf;
+type SearchStatus = ExpenseSearchStatus | InvoiceSearchStatus | TripSearchStatus;
type SearchContext = {
currentSearchHash: number;
@@ -55,7 +58,7 @@ type QueryFilters = {
type SearchQueryString = string;
type SearchQueryAST = {
- type: string;
+ type: SearchDataTypes;
status: SearchStatus;
sortBy: SearchColumnType;
sortOrder: SortOrder;
@@ -81,4 +84,7 @@ export type {
QueryFilter,
QueryFilters,
AdvancedFiltersKeys,
+ ExpenseSearchStatus,
+ InvoiceSearchStatus,
+ TripSearchStatus,
};
diff --git a/src/components/SelectionList/SearchTableHeader.tsx b/src/components/SelectionList/SearchTableHeader.tsx
index f68b3c6d17ff..a5547eb9e710 100644
--- a/src/components/SelectionList/SearchTableHeader.tsx
+++ b/src/components/SelectionList/SearchTableHeader.tsx
@@ -9,7 +9,6 @@ import * as SearchUtils from '@libs/SearchUtils';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import type * as OnyxTypes from '@src/types/onyx';
-import type {SearchDataTypes} from '@src/types/onyx/SearchResults';
import SortableHeaderText from './SortableHeaderText';
type SearchColumnConfig = {
@@ -86,19 +85,6 @@ const expenseHeaders: SearchColumnConfig[] = [
},
];
-function getSearchColumns(type: SearchDataTypes): SearchColumnConfig[] {
- switch (type) {
- case CONST.SEARCH.DATA_TYPES.TRANSACTION:
- case CONST.SEARCH.DATA_TYPES.REPORT:
- case CONST.SEARCH.DATA_TYPES.EXPENSE:
- case CONST.SEARCH.DATA_TYPES.INVOICE:
- case CONST.SEARCH.DATA_TYPES.TRIP:
- return expenseHeaders;
- default:
- return expenseHeaders;
- }
-}
-
type SearchTableHeaderProps = {
data: OnyxTypes.SearchResults['data'];
metadata: OnyxTypes.SearchResults['search'];
@@ -115,16 +101,15 @@ function SearchTableHeader({data, metadata, sortBy, sortOrder, onSortPress, shou
const {isSmallScreenWidth, isMediumScreenWidth} = useWindowDimensions();
const {translate} = useLocalize();
const displayNarrowVersion = isMediumScreenWidth || isSmallScreenWidth;
- const type = SearchUtils.getSearchType(metadata);
- if (displayNarrowVersion || !type) {
+ if (displayNarrowVersion) {
return;
}
return (
- {getSearchColumns(type).map(({columnName, translationKey, shouldShow, isColumnSortable}) => {
+ {expenseHeaders.map(({columnName, translationKey, shouldShow, isColumnSortable}) => {
if (!shouldShow(data, metadata)) {
return null;
}
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 1684ed3057da..5c9acf472dd7 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -375,6 +375,7 @@ export default {
filterLogs: 'Filter Logs',
network: 'Network',
reportID: 'Report ID',
+ outstanding: 'Outstanding',
},
location: {
useCurrent: 'Use current location',
@@ -2095,6 +2096,7 @@ export default {
viewTrip: 'View trip',
viewTripDetails: 'View trip details',
trip: 'Trip',
+ trips: 'Trips',
tripSummary: 'Trip summary',
departs: 'Departs',
errorMessage: 'Something went wrong. Please try again later.',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index b4d071ba4a08..cd6e799991ea 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -365,6 +365,7 @@ export default {
filterLogs: 'Registros de filtrado',
network: 'La red',
reportID: 'ID del informe',
+ outstanding: 'Pendiente',
},
connectionComplete: {
title: 'Conexión completa',
@@ -2130,6 +2131,7 @@ export default {
viewTrip: 'Ver viaje',
viewTripDetails: 'Ver detalles del viaje',
trip: 'Viaje',
+ trips: 'Viajes',
tripSummary: 'Resumen del viaje',
departs: 'Sale',
errorMessage: 'Ha ocurrido un error. Por favor, inténtalo mas tarde.',
diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx
index f023e94bec9b..a9df43ad5b64 100644
--- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx
+++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx
@@ -18,7 +18,7 @@ import Navigation, {navigationRef} from '@libs/Navigation/Navigation';
import type {RootStackParamList, State} from '@libs/Navigation/types';
import {isCentralPaneName} from '@libs/NavigationUtils';
import * as PolicyUtils from '@libs/PolicyUtils';
-import {getCurrentSearchParams} from '@libs/SearchUtils';
+import * as SearchUtils from '@libs/SearchUtils';
import type {BrickRoad} from '@libs/WorkspacesSettingsUtils';
import {getChatTabBrickRoad} from '@libs/WorkspacesSettingsUtils';
import BottomTabAvatar from '@pages/home/sidebar/BottomTabAvatar';
@@ -88,14 +88,14 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) {
return;
}
interceptAnonymousUser(() => {
- const currentSearchParams = getCurrentSearchParams();
+ const currentSearchParams = SearchUtils.getCurrentSearchParams();
if (currentSearchParams) {
const {q, ...rest} = currentSearchParams;
const policy = PolicyUtils.getPolicy(currentSearchParams?.policyIDs);
Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: q, ...rest, policyIDs: policy ? currentSearchParams?.policyIDs : undefined}));
return;
}
- Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: CONST.SEARCH.TAB.EXPENSE.ALL}));
+ Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: SearchUtils.buildCannedSearchQuery()}));
});
}, [selectedTab]);
diff --git a/src/libs/Navigation/switchPolicyID.ts b/src/libs/Navigation/switchPolicyID.ts
index 28de413b0904..a37ccb0c2506 100644
--- a/src/libs/Navigation/switchPolicyID.ts
+++ b/src/libs/Navigation/switchPolicyID.ts
@@ -4,6 +4,7 @@ import {getPathFromState} from '@react-navigation/native';
import type {Writable} from 'type-fest';
import getIsNarrowLayout from '@libs/getIsNarrowLayout';
import {isCentralPaneName} from '@libs/NavigationUtils';
+import * as SearchUtils from '@libs/SearchUtils';
import CONST from '@src/CONST';
import type {Route} from '@src/ROUTES';
import ROUTES from '@src/ROUTES';
@@ -84,7 +85,7 @@ export default function switchPolicyID(navigation: NavigationContainerRef>;
const action: StackNavigationAction = getActionFromState(stateFromPath, linkingConfig.config);
diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts
index bb49697a8345..48af780eab5b 100644
--- a/src/libs/SearchUtils.ts
+++ b/src/libs/SearchUtils.ts
@@ -76,23 +76,6 @@ function getTransactionItemCommonFormattedProperties(
};
}
-function isSearchDataType(type: string): type is SearchDataTypes {
- const searchDataTypes: string[] = Object.values(CONST.SEARCH.DATA_TYPES);
- return searchDataTypes.includes(type);
-}
-
-function getSearchType(search: OnyxTypes.SearchResults['search'] | undefined): SearchDataTypes | undefined {
- if (!search) {
- return undefined;
- }
-
- if (!isSearchDataType(search.type)) {
- return undefined;
- }
-
- return search.type;
-}
-
function getShouldShowMerchant(data: OnyxTypes.SearchResults['data']): boolean {
return Object.values(data).some((item) => {
const merchant = item.modifiedMerchant ? item.modifiedMerchant : item.merchant ?? '';
@@ -250,45 +233,16 @@ function getReportSections(data: OnyxTypes.SearchResults['data'], metadata: Onyx
return Object.values(reportIDToTransactions);
}
-function getListItem(type: SearchDataTypes, status: SearchStatus): ListItemType {
- switch (type) {
- case CONST.SEARCH.DATA_TYPES.TRANSACTION:
- case CONST.SEARCH.DATA_TYPES.EXPENSE:
- case CONST.SEARCH.DATA_TYPES.REPORT:
- case CONST.SEARCH.DATA_TYPES.INVOICE:
- case CONST.SEARCH.DATA_TYPES.TRIP:
- return status === CONST.SEARCH.STATUS.EXPENSE.ALL ? TransactionListItem : ReportListItem;
- default:
- return TransactionListItem;
- }
+function getListItem(status: SearchStatus): ListItemType {
+ return status === CONST.SEARCH.STATUS.EXPENSE.ALL ? TransactionListItem : ReportListItem;
}
-function getSections(type: SearchDataTypes, status: SearchStatus, data: OnyxTypes.SearchResults['data'], metadata: OnyxTypes.SearchResults['search']) {
- switch (type) {
- case CONST.SEARCH.DATA_TYPES.TRANSACTION:
- case CONST.SEARCH.DATA_TYPES.EXPENSE:
- case CONST.SEARCH.DATA_TYPES.REPORT:
- case CONST.SEARCH.DATA_TYPES.INVOICE:
- case CONST.SEARCH.DATA_TYPES.TRIP:
- return status === CONST.SEARCH.STATUS.EXPENSE.ALL ? getTransactionsSections(data, metadata) : getReportSections(data, metadata);
- default:
- return getTransactionsSections(data, metadata);
- }
+function getSections(status: SearchStatus, data: OnyxTypes.SearchResults['data'], metadata: OnyxTypes.SearchResults['search']) {
+ return status === CONST.SEARCH.STATUS.EXPENSE.ALL ? getTransactionsSections(data, metadata) : getReportSections(data, metadata);
}
-function getSortedSections(type: SearchDataTypes, status: SearchStatus, data: ListItemDataType, sortBy?: SearchColumnType, sortOrder?: SortOrder) {
- switch (type) {
- case CONST.SEARCH.DATA_TYPES.TRANSACTION:
- case CONST.SEARCH.DATA_TYPES.EXPENSE:
- case CONST.SEARCH.DATA_TYPES.REPORT:
- case CONST.SEARCH.DATA_TYPES.INVOICE:
- case CONST.SEARCH.DATA_TYPES.TRIP:
- return status === CONST.SEARCH.STATUS.EXPENSE.ALL
- ? getSortedTransactionData(data as TransactionListItemType[], sortBy, sortOrder)
- : getSortedReportData(data as ReportListItemType[]);
- default:
- return getSortedTransactionData(data as TransactionListItemType[], sortBy, sortOrder);
- }
+function getSortedSections(status: SearchStatus, data: ListItemDataType, sortBy?: SearchColumnType, sortOrder?: SortOrder) {
+ return status === CONST.SEARCH.STATUS.EXPENSE.ALL ? getSortedTransactionData(data as TransactionListItemType[], sortBy, sortOrder) : getSortedReportData(data as ReportListItemType[]);
}
function getQueryHash(query: string, policyID?: string, sortBy?: string, sortOrder?: string): number {
@@ -571,6 +525,10 @@ function getSearchHeaderTitle(queryJSON: SearchQueryJSON) {
return title;
}
+function buildCannedSearchQuery(type: SearchDataTypes = CONST.SEARCH.DATA_TYPES.EXPENSE, status: SearchStatus = CONST.SEARCH.STATUS.EXPENSE.ALL): SearchQueryString {
+ return normalizeQuery(`type:${type} status:${status}`);
+}
+
export {
buildQueryStringFromFilters,
buildSearchQueryJSON,
@@ -580,7 +538,6 @@ export {
getListItem,
getQueryHash,
getSearchHeaderTitle,
- getSearchType,
getSections,
getShouldShowMerchant,
getSortedSections,
@@ -589,5 +546,6 @@ export {
isTransactionListItemType,
normalizeQuery,
shouldShowYear,
+ buildCannedSearchQuery,
getExpenseTypeTranslationKey,
};
diff --git a/src/pages/Search/EmptySearchView.tsx b/src/pages/Search/EmptySearchView.tsx
index eaa6c13bcad6..88b2a38b3603 100644
--- a/src/pages/Search/EmptySearchView.tsx
+++ b/src/pages/Search/EmptySearchView.tsx
@@ -32,8 +32,6 @@ function EmptySearchView({type}: EmptySearchViewProps) {
buttonText: translate('search.searchResults.emptyTripResults.buttonText'),
buttonAction: () => Navigation.navigate(ROUTES.TRAVEL_MY_TRIPS),
};
- case CONST.SEARCH.DATA_TYPES.TRANSACTION:
- case CONST.SEARCH.DATA_TYPES.REPORT:
case CONST.SEARCH.DATA_TYPES.EXPENSE:
case CONST.SEARCH.DATA_TYPES.INVOICE:
default:
diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx
index e2495c02d44f..d5725c2d40a2 100644
--- a/src/pages/Search/SearchPage.tsx
+++ b/src/pages/Search/SearchPage.tsx
@@ -7,8 +7,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import type {AuthScreensParamList} from '@libs/Navigation/types';
-import {buildSearchQueryJSON} from '@libs/SearchUtils';
-import CONST from '@src/CONST';
+import * as SearchUtils from '@libs/SearchUtils';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
@@ -19,9 +18,8 @@ function SearchPage({route}: SearchPageProps) {
const styles = useThemeStyles();
const {policyIDs, q, isCustomQuery} = route.params;
- const queryJSON = useMemo(() => buildSearchQueryJSON(q, policyIDs), [q, policyIDs]);
-
- const handleOnBackButtonPress = () => Navigation.goBack(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: CONST.SEARCH.TAB.EXPENSE.ALL}));
+ const queryJSON = useMemo(() => SearchUtils.buildSearchQueryJSON(q, policyIDs), [q, policyIDs]);
+ const handleOnBackButtonPress = () => Navigation.goBack(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: SearchUtils.buildCannedSearchQuery()}));
// On small screens this page is not displayed, the configuration is in the file: src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.tsx
// To avoid calling hooks in the Search component when this page isn't visible, we return null here.
diff --git a/src/pages/Search/SearchPageBottomTab.tsx b/src/pages/Search/SearchPageBottomTab.tsx
index 1b9e81c8a035..029407026dc8 100644
--- a/src/pages/Search/SearchPageBottomTab.tsx
+++ b/src/pages/Search/SearchPageBottomTab.tsx
@@ -12,13 +12,12 @@ import useThemeStyles from '@hooks/useThemeStyles';
import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode';
import Navigation from '@libs/Navigation/Navigation';
import type {AuthScreensParamList} from '@libs/Navigation/types';
-import {buildSearchQueryJSON} from '@libs/SearchUtils';
+import * as SearchUtils from '@libs/SearchUtils';
import TopBar from '@navigation/AppNavigator/createCustomBottomTabNavigator/TopBar';
-import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import SCREENS from '@src/SCREENS';
-import SearchStatusMenu from './SearchStatusMenu';
+import SearchTypeMenu from './SearchTypeMenu';
function SearchPageBottomTab() {
const {translate} = useLocalize();
@@ -37,13 +36,13 @@ function SearchPageBottomTab() {
const searchParams = activeCentralPaneRoute.params as AuthScreensParamList[typeof SCREENS.SEARCH.CENTRAL_PANE];
return {
- queryJSON: buildSearchQueryJSON(searchParams.q, searchParams.policyIDs),
+ queryJSON: SearchUtils.buildSearchQueryJSON(searchParams.q, searchParams.policyIDs),
policyIDs: searchParams.policyIDs,
isCustomQuery: searchParams.isCustomQuery,
};
}, [activeCentralPaneRoute]);
- const handleOnBackButtonPress = () => Navigation.goBack(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: CONST.SEARCH.TAB.EXPENSE.ALL}));
+ const handleOnBackButtonPress = () => Navigation.goBack(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: SearchUtils.buildCannedSearchQuery()}));
return (
-
diff --git a/src/pages/Search/SearchStatusMenu.tsx b/src/pages/Search/SearchTypeMenu.tsx
similarity index 57%
rename from src/pages/Search/SearchStatusMenu.tsx
rename to src/pages/Search/SearchTypeMenu.tsx
index 9fd812ead9a1..a2d84209114b 100644
--- a/src/pages/Search/SearchStatusMenu.tsx
+++ b/src/pages/Search/SearchTypeMenu.tsx
@@ -1,75 +1,69 @@
import React from 'react';
import {View} from 'react-native';
import MenuItem from '@components/MenuItem';
-import type {SearchQueryJSON, SearchStatus} from '@components/Search/types';
+import type {SearchQueryJSON} from '@components/Search/types';
import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useSingleExecution from '@hooks/useSingleExecution';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
-import {normalizeQuery} from '@libs/SearchUtils';
import * as SearchUtils from '@libs/SearchUtils';
import variables from '@styles/variables';
import * as Expensicons from '@src/components/Icon/Expensicons';
import CONST from '@src/CONST';
import type {Route} from '@src/ROUTES';
import ROUTES from '@src/ROUTES';
+import type {SearchDataTypes} from '@src/types/onyx/SearchResults';
import type IconAsset from '@src/types/utils/IconAsset';
-import SearchStatusMenuNarrow from './SearchStatusMenuNarrow';
+import SearchTypeMenuNarrow from './SearchTypeMenuNarrow';
-type SearchStatusMenuProps = {
+type SearchTypeMenuProps = {
queryJSON: SearchQueryJSON;
isCustomQuery: boolean;
};
-type SearchStatusMenuItem = {
+type SearchTypeMenuItem = {
title: string;
- status: SearchStatus;
+ type: SearchDataTypes;
icon: IconAsset;
route?: Route;
};
-function SearchStatusMenu({queryJSON, isCustomQuery}: SearchStatusMenuProps) {
- const {status} = queryJSON;
+function SearchTypeMenu({queryJSON, isCustomQuery}: SearchTypeMenuProps) {
+ const {type} = queryJSON;
const styles = useThemeStyles();
const {shouldUseNarrowLayout} = useResponsiveLayout();
const {singleExecution} = useSingleExecution();
const {translate} = useLocalize();
- const statusMenuItems: SearchStatusMenuItem[] = [
+ const typeMenuItems: SearchTypeMenuItem[] = [
{
title: translate('common.expenses'),
- status: CONST.SEARCH.STATUS.EXPENSE.ALL,
+ type: CONST.SEARCH.DATA_TYPES.EXPENSE,
icon: Expensicons.Receipt,
- route: ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: normalizeQuery(CONST.SEARCH.TAB.EXPENSE.ALL)}),
+ route: ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: SearchUtils.buildCannedSearchQuery()}),
},
{
- title: translate('common.shared'),
- status: CONST.SEARCH.STATUS.EXPENSE.SHARED,
- icon: Expensicons.Send,
- route: ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: normalizeQuery(CONST.SEARCH.TAB.EXPENSE.SHARED)}),
+ title: translate('workspace.common.invoices'),
+ type: CONST.SEARCH.DATA_TYPES.INVOICE,
+ icon: Expensicons.InvoiceGeneric,
+ route: ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: SearchUtils.buildCannedSearchQuery(CONST.SEARCH.DATA_TYPES.INVOICE, CONST.SEARCH.STATUS.INVOICE.ALL)}),
},
{
- title: translate('common.drafts'),
- status: CONST.SEARCH.STATUS.EXPENSE.DRAFTS,
- icon: Expensicons.Pencil,
- route: ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: normalizeQuery(CONST.SEARCH.TAB.EXPENSE.DRAFTS)}),
- },
- {
- title: translate('common.finished'),
- status: CONST.SEARCH.STATUS.EXPENSE.FINISHED,
- icon: Expensicons.CheckCircle,
- route: ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: normalizeQuery(CONST.SEARCH.TAB.EXPENSE.FINISHED)}),
+ title: translate('travel.trips'),
+ type: CONST.SEARCH.DATA_TYPES.TRIP,
+ icon: Expensicons.Suitcase,
+ route: ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: SearchUtils.buildCannedSearchQuery(CONST.SEARCH.DATA_TYPES.TRIP, CONST.SEARCH.STATUS.TRIP.ALL)}),
},
];
- const activeItemIndex = statusMenuItems.findIndex((item) => item.status === status);
+ const activeItemIndex = typeMenuItems.findIndex((item) => item.type === type);
if (shouldUseNarrowLayout) {
const title = isCustomQuery ? SearchUtils.getSearchHeaderTitle(queryJSON) : undefined;
return (
-
@@ -78,7 +72,7 @@ function SearchStatusMenu({queryJSON, isCustomQuery}: SearchStatusMenuProps) {
return (
- {statusMenuItems.map((item, index) => {
+ {typeMenuItems.map((item, index) => {
const onPress = singleExecution(() => Navigation.navigate(item.route));
return (
@@ -102,7 +96,7 @@ function SearchStatusMenu({queryJSON, isCustomQuery}: SearchStatusMenuProps) {
);
}
-SearchStatusMenu.displayName = 'SearchStatusMenu';
+SearchTypeMenu.displayName = 'SearchTypeMenu';
-export default SearchStatusMenu;
-export type {SearchStatusMenuItem};
+export default SearchTypeMenu;
+export type {SearchTypeMenuItem};
diff --git a/src/pages/Search/SearchStatusMenuNarrow.tsx b/src/pages/Search/SearchTypeMenuNarrow.tsx
similarity index 91%
rename from src/pages/Search/SearchStatusMenuNarrow.tsx
rename to src/pages/Search/SearchTypeMenuNarrow.tsx
index 8850b0fb2b0f..08c7852cb037 100644
--- a/src/pages/Search/SearchStatusMenuNarrow.tsx
+++ b/src/pages/Search/SearchTypeMenuNarrow.tsx
@@ -10,15 +10,15 @@ import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import Navigation from '@libs/Navigation/Navigation';
import * as Expensicons from '@src/components/Icon/Expensicons';
-import type {SearchStatusMenuItem} from './SearchStatusMenu';
+import type {SearchTypeMenuItem} from './SearchTypeMenu';
-type SearchStatusMenuNarrowProps = {
- statusMenuItems: SearchStatusMenuItem[];
+type SearchTypeMenuNarrowProps = {
+ typeMenuItems: SearchTypeMenuItem[];
activeItemIndex: number;
title?: string;
};
-function SearchStatusMenuNarrow({statusMenuItems, activeItemIndex, title}: SearchStatusMenuNarrowProps) {
+function SearchTypeMenuNarrow({typeMenuItems, activeItemIndex, title}: SearchTypeMenuNarrowProps) {
const theme = useTheme();
const styles = useThemeStyles();
const {singleExecution} = useSingleExecution();
@@ -30,7 +30,7 @@ function SearchStatusMenuNarrow({statusMenuItems, activeItemIndex, title}: Searc
const openMenu = () => setIsPopoverVisible(true);
const closeMenu = () => setIsPopoverVisible(false);
- const popoverMenuItems = statusMenuItems.map((item, index) => {
+ const popoverMenuItems = typeMenuItems.map((item, index) => {
const isSelected = title ? false : index === activeItemIndex;
return {
@@ -105,6 +105,6 @@ function SearchStatusMenuNarrow({statusMenuItems, activeItemIndex, title}: Searc
);
}
-SearchStatusMenuNarrow.displayName = 'SearchStatusMenuNarrow';
+SearchTypeMenuNarrow.displayName = 'SearchTypeMenuNarrow';
-export default SearchStatusMenuNarrow;
+export default SearchTypeMenuNarrow;
diff --git a/src/pages/settings/Subscription/CardSection/CardSection.tsx b/src/pages/settings/Subscription/CardSection/CardSection.tsx
index 25a201bf6887..d90d3e839603 100644
--- a/src/pages/settings/Subscription/CardSection/CardSection.tsx
+++ b/src/pages/settings/Subscription/CardSection/CardSection.tsx
@@ -17,6 +17,7 @@ import * as User from '@libs/actions/User';
import DateUtils from '@libs/DateUtils';
import Navigation from '@libs/Navigation/Navigation';
import {getPaymentMethodDescription} from '@libs/PaymentUtils';
+import * as SearchUtils from '@libs/SearchUtils';
import * as SubscriptionUtils from '@libs/SubscriptionUtils';
import * as Subscription from '@userActions/Subscription';
import CONST from '@src/CONST';
@@ -148,7 +149,7 @@ function CardSection() {
wrapperStyle={styles.sectionMenuItemTopDescription}
title={translate('subscription.cardSection.viewPaymentHistory')}
titleStyle={styles.textStrong}
- onPress={() => Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: CONST.SEARCH.TAB.EXPENSE.ALL}))}
+ onPress={() => Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: SearchUtils.buildCannedSearchQuery()}))}
hoverAndPressStyle={styles.hoveredComponentBG}
/>
)}
diff --git a/src/styles/index.ts b/src/styles/index.ts
index fe65e48bc4a1..918c7927852b 100644
--- a/src/styles/index.ts
+++ b/src/styles/index.ts
@@ -4253,7 +4253,7 @@ const styles = (theme: ThemeColors) =>
tabText: (isSelected: boolean) =>
({
marginLeft: 8,
- ...(isSelected ? FontUtils.fontFamily.platform.EXP_NEUE_BOLD : FontUtils.fontFamily.platform.EXP_NEUE),
+ ...FontUtils.fontFamily.platform.EXP_NEUE_BOLD,
color: isSelected ? theme.text : theme.textSupporting,
lineHeight: variables.lineHeightNormal,
fontSize: variables.fontSizeNormal,
diff --git a/src/types/onyx/SearchResults.ts b/src/types/onyx/SearchResults.ts
index 228a49e263f7..e67b5b30f1d6 100644
--- a/src/types/onyx/SearchResults.ts
+++ b/src/types/onyx/SearchResults.ts
@@ -14,13 +14,6 @@ type ListItemType = T extends typeof CONST.SEARCH.STATUS
/** Model of search list item data type */
type ListItemDataType = T extends typeof CONST.SEARCH.STATUS.EXPENSE.ALL ? TransactionListItemType[] : ReportListItemType[];
-/** Model of search result section */
-type SectionsType = T extends typeof CONST.SEARCH.DATA_TYPES.TRANSACTION
- ? TransactionListItemType[]
- : T extends typeof CONST.SEARCH.DATA_TYPES.REPORT
- ? ReportListItemType[]
- : never;
-
/** Model of columns to show for search results */
type ColumnsToShow = {
/** Whether the category column should be shown */
@@ -39,7 +32,10 @@ type SearchResultsInfo = {
offset: number;
/** Type of search */
- type: string;
+ type: SearchDataTypes;
+
+ /** The status filter for the current search */
+ status: SearchStatus;
/** Whether the user can fetch more search results */
hasMoreResults: boolean;
@@ -230,4 +226,4 @@ type SearchResults = {
export default SearchResults;
-export type {ListItemType, ListItemDataType, SearchTransaction, SearchTransactionType, SearchTransactionAction, SearchPersonalDetails, SearchDataTypes, SearchReport, SectionsType};
+export type {ListItemType, ListItemDataType, SearchTransaction, SearchTransactionType, SearchTransactionAction, SearchPersonalDetails, SearchDataTypes, SearchReport};