diff --git a/assets/images/arrow-down-long.svg b/assets/images/arrow-down-long.svg
new file mode 100644
index 000000000000..cbf6e7e5ad2f
--- /dev/null
+++ b/assets/images/arrow-down-long.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/images/arrow-up-long.svg b/assets/images/arrow-up-long.svg
new file mode 100644
index 000000000000..13d7a0c2d67e
--- /dev/null
+++ b/assets/images/arrow-up-long.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/CONST.ts b/src/CONST.ts
index 4f622cc0b3bf..4665ec94625a 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -932,6 +932,7 @@ const CONST = {
RECEIPT: 'receipt',
DATE: 'date',
MERCHANT: 'merchant',
+ DESCRIPTION: 'description',
FROM: 'from',
TO: 'to',
CATEGORY: 'category',
@@ -4777,6 +4778,11 @@ const CONST = {
REFERRER: {
NOTIFICATION: 'notification',
},
+
+ SORT_ORDER: {
+ ASC: 'asc',
+ DESC: 'desc',
+ },
} as const;
type Country = keyof typeof CONST.ALL_COUNTRIES;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 0302faf7e92b..ed6e1cd3ce38 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -2,6 +2,8 @@ import type {ValueOf} from 'type-fest';
import type CONST from './CONST';
import type {IOUAction, IOUType} from './CONST';
import type {IOURequestType} from './libs/actions/IOU';
+import type {CentralPaneNavigatorParamList} from './libs/Navigation/types';
+import type {SearchQuery} from './types/onyx/SearchResults';
import type AssertTypesNotEqual from './types/utils/AssertTypesNotEqual';
// This is a file containing constants for all the routes we want to be able to go to
@@ -35,7 +37,15 @@ const ROUTES = {
SEARCH: {
route: '/search/:query',
- getRoute: (query: string) => `search/${query}` as const,
+ getRoute: (searchQuery: SearchQuery, queryParams?: CentralPaneNavigatorParamList['Search_Central_Pane']) => {
+ const {sortBy, sortOrder} = queryParams ?? {};
+
+ if (!sortBy && !sortOrder) {
+ return `search/${searchQuery}` as const;
+ }
+
+ return `search/${searchQuery}?sortBy=${sortBy}&sortOrder=${sortOrder}` as const;
+ },
},
SEARCH_REPORT: {
diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts
index c127fbda1bd5..bcc40947a83a 100644
--- a/src/components/Icon/Expensicons.ts
+++ b/src/components/Icon/Expensicons.ts
@@ -2,8 +2,10 @@ import AddReaction from '@assets/images/add-reaction.svg';
import All from '@assets/images/all.svg';
import Android from '@assets/images/android.svg';
import Apple from '@assets/images/apple.svg';
+import ArrowDownLong from '@assets/images/arrow-down-long.svg';
import ArrowRightLong from '@assets/images/arrow-right-long.svg';
import ArrowRight from '@assets/images/arrow-right.svg';
+import ArrowUpLong from '@assets/images/arrow-up-long.svg';
import UpArrow from '@assets/images/arrow-up.svg';
import ArrowsUpDown from '@assets/images/arrows-updown.svg';
import AdminRoomAvatar from '@assets/images/avatars/admin-room.svg';
@@ -182,6 +184,8 @@ export {
ArrowRight,
ArrowRightLong,
ArrowsUpDown,
+ ArrowUpLong,
+ ArrowDownLong,
Wrench,
BackArrow,
Bank,
diff --git a/src/components/Search.tsx b/src/components/Search.tsx
index 699ae0907f31..fcb915379181 100644
--- a/src/components/Search.tsx
+++ b/src/components/Search.tsx
@@ -1,4 +1,7 @@
-import React, {useEffect} from 'react';
+import {useNavigation} from '@react-navigation/native';
+import type {StackNavigationProp} from '@react-navigation/stack';
+import React, {useEffect, useRef} from 'react';
+import type {OnyxEntry} from 'react-native-onyx';
import {useOnyx} from 'react-native-onyx';
import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -6,11 +9,15 @@ import * as SearchActions from '@libs/actions/Search';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import Log from '@libs/Log';
import * as SearchUtils from '@libs/SearchUtils';
+import type {SearchColumnType, SortOrder} from '@libs/SearchUtils';
import Navigation from '@navigation/Navigation';
+import type {CentralPaneNavigatorParamList} from '@navigation/types';
import EmptySearchView from '@pages/Search/EmptySearchView';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
+import type {SearchQuery} from '@src/types/onyx/SearchResults';
+import type SearchResults from '@src/types/onyx/SearchResults';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
import SelectionList from './SelectionList';
@@ -19,8 +26,10 @@ import type {ReportListItemType, TransactionListItemType} from './SelectionList/
import TableListItemSkeleton from './Skeletons/TableListItemSkeleton';
type SearchProps = {
- query: string;
+ query: SearchQuery;
policyIDs?: string;
+ sortBy?: SearchColumnType;
+ sortOrder?: SortOrder;
};
function isReportListItemType(item: TransactionListItemType | ReportListItemType): item is ReportListItemType {
@@ -28,27 +37,36 @@ function isReportListItemType(item: TransactionListItemType | ReportListItemType
return reportListItem.transactions !== undefined;
}
-function Search({query, policyIDs}: SearchProps) {
+function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) {
const {isOffline} = useNetwork();
const styles = useThemeStyles();
+ const navigation = useNavigation>();
+ const lastSearchResultsRef = useRef>();
- const hash = SearchUtils.getQueryHash(query, policyIDs);
- const [searchResults, searchResultsMeta] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`);
+ const hash = SearchUtils.getQueryHash(query, policyIDs, sortBy, sortOrder);
+ const [currentSearchResults, searchResultsMeta] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`);
+
+ // save last non-empty search results to avoid ugly flash of loading screen when hash changes and onyx returns empty data
+ if (currentSearchResults?.data && currentSearchResults !== lastSearchResultsRef.current) {
+ lastSearchResultsRef.current = currentSearchResults;
+ }
+
+ const searchResults = currentSearchResults?.data ? currentSearchResults : lastSearchResultsRef.current;
useEffect(() => {
if (isOffline) {
return;
}
- SearchActions.search(hash, query, 0, policyIDs);
+ SearchActions.search({hash, query, policyIDs, offset: 0, sortBy, sortOrder});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hash, isOffline]);
- const isLoadingInitialItems = (!isOffline && isLoadingOnyxValue(searchResultsMeta)) || searchResults?.data === undefined;
- const isLoadingMoreItems = !isLoadingInitialItems && searchResults?.search?.isLoading;
- const shouldShowEmptyState = !isLoadingInitialItems && isEmptyObject(searchResults?.data);
+ const isLoadingItems = (!isOffline && isLoadingOnyxValue(searchResultsMeta)) || searchResults?.data === undefined;
+ const isLoadingMoreItems = !isLoadingItems && searchResults?.search?.isLoading;
+ const shouldShowEmptyState = !isLoadingItems && isEmptyObject(searchResults?.data);
- if (isLoadingInitialItems) {
+ if (isLoadingItems) {
return ;
}
@@ -65,11 +83,11 @@ function Search({query, policyIDs}: SearchProps) {
};
const fetchMoreResults = () => {
- if (!searchResults?.search?.hasMoreResults || isLoadingInitialItems || isLoadingMoreItems) {
+ if (!searchResults?.search?.hasMoreResults || isLoadingItems || isLoadingMoreItems) {
return;
}
const currentOffset = searchResults?.search?.offset ?? 0;
- SearchActions.search(hash, query, currentOffset + CONST.SEARCH_RESULTS_PAGE_SIZE);
+ SearchActions.search({hash, query, offset: currentOffset + CONST.SEARCH_RESULTS_PAGE_SIZE, sortBy, sortOrder});
};
const type = SearchUtils.getSearchType(searchResults?.search);
@@ -83,9 +101,25 @@ function Search({query, policyIDs}: SearchProps) {
const data = SearchUtils.getSections(searchResults?.data ?? {}, type);
+ const onSortPress = (column: SearchColumnType, order: SortOrder) => {
+ navigation.setParams({
+ sortBy: column,
+ sortOrder: order,
+ });
+ };
+
+ const sortedData = SearchUtils.getSortedSections(type, data, sortBy, sortOrder);
+
return (
- customListHeader={}
+ customListHeader={
+
+ }
// To enhance the smoothness of scrolling and minimize the risk of encountering blank spaces during scrolling,
// we have configured a larger windowSize and a longer delay between batch renders.
// The windowSize determines the number of items rendered before and after the currently visible items.
@@ -98,7 +132,7 @@ function Search({query, policyIDs}: SearchProps) {
windowSize={111}
updateCellsBatchingPeriod={200}
ListItem={ListItem}
- sections={[{data, isDisabled: false}]}
+ sections={[{data: sortedData, isDisabled: false}]}
onSelectRow={(item) => {
const reportID = isReportListItemType(item) ? item.reportID : item.transactionThreadReportID;
diff --git a/src/components/SelectionList/Search/ExpenseItemHeaderNarrow.tsx b/src/components/SelectionList/Search/ExpenseItemHeaderNarrow.tsx
index 9b2188c659e5..cea9c174dda5 100644
--- a/src/components/SelectionList/Search/ExpenseItemHeaderNarrow.tsx
+++ b/src/components/SelectionList/Search/ExpenseItemHeaderNarrow.tsx
@@ -13,11 +13,13 @@ import UserInfoCell from './UserInfoCell';
type ExpenseItemHeaderNarrowProps = {
participantFrom: SearchAccountDetails;
participantTo: SearchAccountDetails;
+ participantFromDisplayName: string;
+ participantToDisplayName: string;
buttonText: string;
onButtonPress: () => void;
};
-function ExpenseItemHeaderNarrow({participantFrom, participantTo, buttonText, onButtonPress}: ExpenseItemHeaderNarrowProps) {
+function ExpenseItemHeaderNarrow({participantFrom, participantFromDisplayName, participantTo, participantToDisplayName, buttonText, onButtonPress}: ExpenseItemHeaderNarrowProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const theme = useTheme();
@@ -26,7 +28,10 @@ function ExpenseItemHeaderNarrow({participantFrom, participantTo, buttonText, on
-
+
-
+
diff --git a/src/components/SelectionList/Search/ReportListItem.tsx b/src/components/SelectionList/Search/ReportListItem.tsx
index ecab5a3053df..c1a38cf6e7cd 100644
--- a/src/components/SelectionList/Search/ReportListItem.tsx
+++ b/src/components/SelectionList/Search/ReportListItem.tsx
@@ -74,6 +74,11 @@ function ReportListItem({
const participantFrom = reportItem.transactions[0].from;
const participantTo = reportItem.transactions[0].to;
+ // These values should come as part of the item via SearchUtils.getSections() but ReportListItem is not yet 100% handled
+ // This will be simplified in future once sorting of ReportListItem is done
+ const participantFromDisplayName = participantFrom?.name ?? participantFrom?.displayName ?? participantFrom?.login ?? '';
+ const participantToDisplayName = participantTo?.name ?? participantTo?.displayName ?? participantTo?.login ?? '';
+
if (reportItem.transactions.length === 1) {
const transactionItem = reportItem.transactions[0];
@@ -118,7 +123,9 @@ function ReportListItem({
{!isLargeScreenWidth && (
diff --git a/src/components/SelectionList/Search/TransactionListItemRow.tsx b/src/components/SelectionList/Search/TransactionListItemRow.tsx
index 71912c2d3c7a..cb1ef3fdc6e1 100644
--- a/src/components/SelectionList/Search/TransactionListItemRow.tsx
+++ b/src/components/SelectionList/Search/TransactionListItemRow.tsx
@@ -1,25 +1,11 @@
import React, {memo} from 'react';
import type {StyleProp, ViewStyle} from 'react-native';
import {View} from 'react-native';
-import type {OnyxEntry} from 'react-native-onyx';
-import Avatar from '@components/Avatar';
import Button from '@components/Button';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import ReceiptImage from '@components/ReceiptImage';
-import type {
- ActionCellProps,
- CellProps,
- CurrencyCellProps,
- DateCellProps,
- MerchantCellProps,
- ReceiptCellProps,
- TransactionCellProps,
- TransactionListItemType,
- TypeCellProps,
- UserCellProps,
-} from '@components/SelectionList/types';
-import Text from '@components/Text';
+import type {TransactionListItemType} from '@components/SelectionList/types';
import TextWithTooltip from '@components/TextWithTooltip';
import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
@@ -31,10 +17,31 @@ import * as TransactionUtils from '@libs/TransactionUtils';
import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot';
import variables from '@styles/variables';
import CONST from '@src/CONST';
-import type {Transaction} from '@src/types/onyx';
import type {SearchTransactionType} from '@src/types/onyx/SearchResults';
import ExpenseItemHeaderNarrow from './ExpenseItemHeaderNarrow';
import TextWithIconCell from './TextWithIconCell';
+import UserInfoCell from './UserInfoCell';
+
+type CellProps = {
+ // eslint-disable-next-line react/no-unused-prop-types
+ showTooltip: boolean;
+ // eslint-disable-next-line react/no-unused-prop-types
+ keyForList: string;
+ // eslint-disable-next-line react/no-unused-prop-types
+ isLargeScreenWidth: boolean;
+};
+
+type TransactionCellProps = {
+ transactionItem: TransactionListItemType;
+} & CellProps;
+
+type ReceiptCellProps = {
+ isHovered?: boolean;
+} & TransactionCellProps;
+
+type ActionCellProps = {
+ onButtonPress: () => void;
+} & CellProps;
type TransactionListItemRowProps = {
item: TransactionListItemType;
@@ -89,71 +96,49 @@ const ReceiptCell = memo(({transactionItem, isHovered = false}: ReceiptCellProps
);
}, areReceiptPropsEqual);
-const DateCell = memo(({showTooltip, date, isLargeScreenWidth}: DateCellProps) => {
+const DateCell = memo(({transactionItem, showTooltip, isLargeScreenWidth}: TransactionCellProps) => {
const styles = useThemeStyles();
+ const date = TransactionUtils.getCreated(transactionItem, CONST.DATE.MONTH_DAY_ABBR_FORMAT);
return (
);
}, arePropsEqual);
-const MerchantCell = memo(({showTooltip, transactionItem, merchant, description}: MerchantCellProps) => {
+const MerchantCell = memo(({transactionItem, showTooltip}: TransactionCellProps) => {
const styles = useThemeStyles();
+ const description = TransactionUtils.getDescription(transactionItem);
+
return (
);
}, arePropsEqual);
-const UserCell = memo(({participant}: UserCellProps) => {
- const styles = useThemeStyles();
-
- const displayName = participant?.name ?? participant?.displayName ?? participant?.login;
- const avatarURL = participant?.avatarURL ?? participant?.avatar;
- const isWorkspace = participant?.avatarURL !== undefined;
- const iconType = isWorkspace ? CONST.ICON_TYPE_WORKSPACE : CONST.ICON_TYPE_AVATAR;
-
- return (
-
-
-
- {displayName}
-
-
- );
-}, arePropsEqual);
-
-const TotalCell = memo(({showTooltip, amount, currency, isLargeScreenWidth}: CurrencyCellProps) => {
+const TotalCell = memo(({showTooltip, isLargeScreenWidth, transactionItem}: TransactionCellProps) => {
const styles = useThemeStyles();
+ const currency = TransactionUtils.getCurrency(transactionItem);
return (
);
}, arePropsEqual);
-const TypeCell = memo(({typeIcon, isLargeScreenWidth}: TypeCellProps) => {
+const TypeCell = memo(({transactionItem, isLargeScreenWidth}: TransactionCellProps) => {
const theme = useTheme();
+ const typeIcon = getTypeIcon(transactionItem.type);
+
return (
{
);
}, arePropsEqual);
-const ActionCell = memo(({item, onSelectRow}: ActionCellProps) => {
+const ActionCell = memo(({onButtonPress}: ActionCellProps) => {
const {translate} = useLocalize();
const styles = useThemeStyles();
return (