From c553e4b3b7dbb444070339637b364acce605c3a4 Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Mon, 13 May 2024 09:47:01 +0200 Subject: [PATCH 01/12] Refactor SearchTableHeader to allow for sorting by column --- src/CONST.ts | 1 + .../SelectionList/SearchTableHeader.tsx | 141 +++++++++++------- .../SelectionList/SearchTableHeaderColumn.tsx | 31 ---- .../SelectionList/SortableHeaderText.tsx | 60 ++++++++ src/styles/index.ts | 4 + 5 files changed, 156 insertions(+), 81 deletions(-) delete mode 100644 src/components/SelectionList/SearchTableHeaderColumn.tsx create mode 100644 src/components/SelectionList/SortableHeaderText.tsx diff --git a/src/CONST.ts b/src/CONST.ts index 6517ece4276d..5da39e49e984 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -936,6 +936,7 @@ const CONST = { SEARCH_TABLE_COLUMNS: { DATE: 'date', MERCHANT: 'merchant', + DESCRIPTION: 'description', FROM: 'from', TO: 'to', CATEGORY: 'category', diff --git a/src/components/SelectionList/SearchTableHeader.tsx b/src/components/SelectionList/SearchTableHeader.tsx index ec0267d20c04..ac653ded9767 100644 --- a/src/components/SelectionList/SearchTableHeader.tsx +++ b/src/components/SelectionList/SearchTableHeader.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useState} from 'react'; import {View} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -6,8 +6,75 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; 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 SearchTableHeaderColumn from './SearchTableHeaderColumn'; +import SortableHeaderText from './SortableHeaderText'; + +type SearchColumnConfig = { + columnName: (typeof CONST.SEARCH_TABLE_COLUMNS)[keyof typeof CONST.SEARCH_TABLE_COLUMNS]; + translationKey: TranslationPaths; + isSortable?: boolean; + shouldShowFn: (data: OnyxTypes.SearchResults['data']) => boolean; +}; + +const SearchColumns: SearchColumnConfig[] = [ + { + columnName: CONST.SEARCH_TABLE_COLUMNS.DATE, + translationKey: 'common.date', + shouldShowFn: () => true, + }, + { + columnName: CONST.SEARCH_TABLE_COLUMNS.MERCHANT, + translationKey: 'common.merchant', + shouldShowFn: (data: OnyxTypes.SearchResults['data']) => SearchUtils.getShouldShowMerchant(data), + }, + { + columnName: CONST.SEARCH_TABLE_COLUMNS.DESCRIPTION, + translationKey: 'common.description', + shouldShowFn: (data: OnyxTypes.SearchResults['data']) => !SearchUtils.getShouldShowMerchant(data), + }, + { + columnName: CONST.SEARCH_TABLE_COLUMNS.FROM, + translationKey: 'common.from', + shouldShowFn: () => true, + }, + { + columnName: CONST.SEARCH_TABLE_COLUMNS.TO, + translationKey: 'common.to', + shouldShowFn: () => true, + }, + { + columnName: CONST.SEARCH_TABLE_COLUMNS.CATEGORY, + translationKey: 'common.category', + shouldShowFn: (data: OnyxTypes.SearchResults['data']) => SearchUtils.getShouldShowColumn(data, CONST.SEARCH_TABLE_COLUMNS.CATEGORY), + }, + { + columnName: CONST.SEARCH_TABLE_COLUMNS.TAG, + translationKey: 'common.tag', + shouldShowFn: (data: OnyxTypes.SearchResults['data']) => SearchUtils.getShouldShowColumn(data, CONST.SEARCH_TABLE_COLUMNS.TAG), + }, + { + columnName: CONST.SEARCH_TABLE_COLUMNS.TAX_AMOUNT, + translationKey: 'common.tax', + shouldShowFn: (data: OnyxTypes.SearchResults['data']) => SearchUtils.getShouldShowColumn(data, CONST.SEARCH_TABLE_COLUMNS.TAX_AMOUNT), + }, + { + columnName: CONST.SEARCH_TABLE_COLUMNS.TOTAL, + translationKey: 'common.total', + shouldShowFn: () => true, + }, + { + columnName: CONST.SEARCH_TABLE_COLUMNS.TYPE, + translationKey: 'common.type', + shouldShowFn: () => true, + }, + { + columnName: CONST.SEARCH_TABLE_COLUMNS.ACTION, + translationKey: 'common.action', + isSortable: false, + shouldShowFn: () => true, + }, +]; type SearchTableHeaderProps = { data: OnyxTypes.SearchResults['data']; @@ -20,62 +87,36 @@ function SearchTableHeader({data}: SearchTableHeaderProps) { const {translate} = useLocalize(); const displayNarrowVersion = isMediumScreenWidth || isSmallScreenWidth; - const shouldShowCategoryColumn = SearchUtils.getShouldShowColumn(data, CONST.SEARCH_TABLE_COLUMNS.CATEGORY); - const shouldShowTagColumn = SearchUtils.getShouldShowColumn(data, CONST.SEARCH_TABLE_COLUMNS.TAG); - const shouldShowTaxColumn = SearchUtils.getShouldShowColumn(data, CONST.SEARCH_TABLE_COLUMNS.TAX_AMOUNT); - const shouldShowMerchant = SearchUtils.getShouldShowMerchant(data); + const [sortColumn, setSortColumn] = useState(); + const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc'); if (displayNarrowVersion) { return; } + const onSortPress = (columnName: SearchColumnConfig['columnName'], order: 'asc' | 'desc') => { + setSortColumn(columnName); + setSortOrder(order); + }; + return ( - - - - - - - - - - + {SearchColumns.map(({columnName, translationKey, shouldShowFn, isSortable}) => { + const isActive = sortColumn === columnName; + + return ( + onSortPress(columnName, order)} + /> + ); + })} ); diff --git a/src/components/SelectionList/SearchTableHeaderColumn.tsx b/src/components/SelectionList/SearchTableHeaderColumn.tsx deleted file mode 100644 index 92b59f702cb1..000000000000 --- a/src/components/SelectionList/SearchTableHeaderColumn.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; -import {View} from 'react-native'; -import type {StyleProp, TextStyle, ViewStyle} from 'react-native'; -import Text from '@components/Text'; -import useThemeStyles from '@hooks/useThemeStyles'; - -type SearchTableHeaderColumnProps = { - shouldShow?: boolean; - containerStyle?: StyleProp; - text: string; - textStyle?: StyleProp; -}; - -export default function SearchTableHeaderColumn({containerStyle, text, textStyle, shouldShow = true}: SearchTableHeaderColumnProps) { - const styles = useThemeStyles(); - - if (!shouldShow) { - return null; - } - - return ( - - - {text} - - - ); -} diff --git a/src/components/SelectionList/SortableHeaderText.tsx b/src/components/SelectionList/SortableHeaderText.tsx new file mode 100644 index 000000000000..9de64528738d --- /dev/null +++ b/src/components/SelectionList/SortableHeaderText.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import {View} from 'react-native'; +import type {StyleProp, TextStyle, ViewStyle} from 'react-native'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; +import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; +import Text from '@components/Text'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; + +type SearchTableHeaderColumnProps = { + text: string; + isActive: boolean; + sortOrder: 'asc' | 'desc'; + shouldShow?: boolean; + isSortable?: boolean; + containerStyle?: StyleProp; + textStyle?: StyleProp; + onPress: (order: 'asc' | 'desc') => void; +}; + +export default function SortableHeaderText({text, sortOrder, isActive, textStyle, containerStyle, shouldShow = true, isSortable = true, onPress}: SearchTableHeaderColumnProps) { + const styles = useThemeStyles(); + const theme = useTheme(); + + if (!shouldShow) { + return null; + } + + const icon = sortOrder === 'asc' ? Expensicons.UpArrow : Expensicons.DownArrow; + const iconStyles = isActive ? [] : [styles.visibilityHidden]; + + const newSortOrder = isActive && sortOrder === 'asc' ? 'desc' : 'asc'; + + return ( + + onPress(newSortOrder)} + accessibilityLabel={''} + disabled={!isSortable} + > + + + {text} + + + + + + ); +} diff --git a/src/styles/index.ts b/src/styles/index.ts index 45e11dbe6cb9..e942bfdd4788 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -3501,6 +3501,10 @@ const styles = (theme: ThemeColors) => lineHeight: 16, }, + searchTableHeaderActive: { + fontWeight: 'bold', + }, + threeDotsPopoverOffset: (windowWidth: number) => ({ ...getPopOverVerticalOffset(60), From 48abb831f40be37d4a4d6471bbf1aa673ee60a55 Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Thu, 16 May 2024 09:29:09 +0200 Subject: [PATCH 02/12] Implement search sorting and update page params --- src/CONST.ts | 4 +- src/ROUTES.ts | 12 ++- src/components/Search.tsx | 84 +++++++++++++++++-- .../SelectionList/SearchTableHeader.tsx | 22 ++--- .../SelectionList/SortableHeaderText.tsx | 7 +- src/libs/API/parameters/Search.ts | 4 +- src/libs/Navigation/types.ts | 6 +- src/libs/actions/Search.ts | 4 +- src/pages/Search/SearchPage.tsx | 8 +- src/types/onyx/SearchResults.ts | 3 +- 10 files changed, 123 insertions(+), 31 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 5da39e49e984..db699525a66b 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -4786,6 +4786,8 @@ type IOUType = ValueOf; type IOUAction = ValueOf; type IOURequestType = ValueOf; -export type {Country, IOUAction, IOUType, RateAndUnit, OnboardingPurposeType, IOURequestType}; +type SearchColumnType = (typeof CONST.SEARCH_TABLE_COLUMNS)[keyof typeof CONST.SEARCH_TABLE_COLUMNS]; + +export type {Country, IOUAction, IOUType, RateAndUnit, OnboardingPurposeType, IOURequestType, SearchColumnType}; export default CONST; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 2bc04c4a99ea..8d11c889abd9 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 @@ -25,7 +27,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/Search.tsx b/src/components/Search.tsx index 27e87017bfee..73ab399fcc45 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -10,20 +10,69 @@ import Navigation from '@navigation/Navigation'; import EmptySearchView from '@pages/Search/EmptySearchView'; import useCustomBackHandler from '@pages/Search/useCustomBackHandler'; import CONST from '@src/CONST'; +import type {SearchColumnType} from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {SearchQuery} from '@src/types/onyx/SearchResults'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; import SelectionList from './SelectionList'; import SearchTableHeader from './SelectionList/SearchTableHeader'; +import type {TransactionListItemType} from './SelectionList/types'; import TableListItemSkeleton from './Skeletons/TableListItemSkeleton'; +const columnNamesToPropertyMap = { + [CONST.SEARCH_TABLE_COLUMNS.TO]: 'to', + [CONST.SEARCH_TABLE_COLUMNS.FROM]: 'from', + [CONST.SEARCH_TABLE_COLUMNS.DATE]: 'created', + [CONST.SEARCH_TABLE_COLUMNS.TAG]: '', + [CONST.SEARCH_TABLE_COLUMNS.MERCHANT]: 'merchant', + [CONST.SEARCH_TABLE_COLUMNS.TOTAL]: 'amount', + [CONST.SEARCH_TABLE_COLUMNS.CATEGORY]: 'category', + [CONST.SEARCH_TABLE_COLUMNS.TYPE]: null, + [CONST.SEARCH_TABLE_COLUMNS.ACTION]: null, + [CONST.SEARCH_TABLE_COLUMNS.DESCRIPTION]: null, + [CONST.SEARCH_TABLE_COLUMNS.TAX_AMOUNT]: null, +}; + +function getSortedData(data: TransactionListItemType[], sortBy?: SearchColumnType, sortOrder?: 'asc' | 'desc') { + if (!sortBy || !sortOrder) { + return data; + } + + const sortingProp = columnNamesToPropertyMap[sortBy] as keyof TransactionListItemType; + + if (!sortingProp) { + return data; + } + + // Todo sorting needs more work + return data.sort((a, b) => { + const aValue = a[sortingProp]; + const bValue = b[sortingProp]; + + if (!aValue || !bValue) { + return 0; + } + + if (typeof aValue === 'string' && typeof bValue === 'string') { + return sortOrder === 'asc' ? aValue.toLowerCase().localeCompare(bValue) : bValue.toLowerCase().localeCompare(aValue); + } + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + return sortOrder === 'asc' ? aValue - bValue : bValue - aValue; + }); +} + type SearchProps = { - query: string; + query: SearchQuery; policyIDs?: string; + sortBy?: SearchColumnType; + sortOrder?: 'asc' | 'desc'; }; -function Search({query, policyIDs}: SearchProps) { +function Search({query, policyIDs, sortOrder, sortBy}: SearchProps) { const {isOffline} = useNetwork(); const styles = useThemeStyles(); useCustomBackHandler(); @@ -31,14 +80,16 @@ function Search({query, policyIDs}: SearchProps) { const hash = SearchUtils.getQueryHash(query, policyIDs); const [searchResults, searchResultsMeta] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`); + const offset = 0; + useEffect(() => { if (isOffline) { return; } - SearchActions.search(hash, query, 0, policyIDs); + SearchActions.search({hash, query, policyIDs, offset, sortBy, sortOrder}); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [hash, isOffline]); + }, [hash, isOffline, sortBy, sortOrder]); const isLoadingInitialItems = (!isOffline && isLoadingOnyxValue(searchResultsMeta)) || searchResults?.data === undefined; const isLoadingMoreItems = !isLoadingInitialItems && searchResults?.search?.isLoading; @@ -65,7 +116,7 @@ function Search({query, policyIDs}: SearchProps) { 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}); }; const type = SearchUtils.getSearchType(searchResults?.search); @@ -78,11 +129,30 @@ function Search({query, policyIDs}: SearchProps) { const ListItem = SearchUtils.getListItem(type); const data = SearchUtils.getSections(searchResults?.data ?? {}, type); + const onSortPress = (column: SearchColumnType, order: 'asc' | 'desc') => { + const newRoute = ROUTES.SEARCH.getRoute(query, { + query, + sortBy: column, + sortOrder: order, + }); + + Navigation.navigate(newRoute); + }; + + const sortedData = getSortedData(data, sortBy, sortOrder); + return ( } + customListHeader={ + + } ListItem={ListItem} - sections={[{data, isDisabled: false}]} + sections={[{data: sortedData, isDisabled: false}]} onSelectRow={(item) => { openReport(item.transactionThreadReportID); }} diff --git a/src/components/SelectionList/SearchTableHeader.tsx b/src/components/SelectionList/SearchTableHeader.tsx index ac653ded9767..3bce8af15f19 100644 --- a/src/components/SelectionList/SearchTableHeader.tsx +++ b/src/components/SelectionList/SearchTableHeader.tsx @@ -1,4 +1,4 @@ -import React, {useState} from 'react'; +import React from 'react'; import {View} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -6,12 +6,13 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as SearchUtils from '@libs/SearchUtils'; import CONST from '@src/CONST'; +import type {SearchColumnType} from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import type * as OnyxTypes from '@src/types/onyx'; import SortableHeaderText from './SortableHeaderText'; type SearchColumnConfig = { - columnName: (typeof CONST.SEARCH_TABLE_COLUMNS)[keyof typeof CONST.SEARCH_TABLE_COLUMNS]; + columnName: SearchColumnType; translationKey: TranslationPaths; isSortable?: boolean; shouldShowFn: (data: OnyxTypes.SearchResults['data']) => boolean; @@ -78,37 +79,32 @@ const SearchColumns: SearchColumnConfig[] = [ type SearchTableHeaderProps = { data: OnyxTypes.SearchResults['data']; + sortBy?: SearchColumnType; + sortOrder?: 'asc' | 'desc'; + onSortPress: (column: SearchColumnType, order: 'asc' | 'desc') => void; }; -function SearchTableHeader({data}: SearchTableHeaderProps) { +function SearchTableHeader({data, sortBy, sortOrder, onSortPress}: SearchTableHeaderProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {isSmallScreenWidth, isMediumScreenWidth} = useWindowDimensions(); const {translate} = useLocalize(); const displayNarrowVersion = isMediumScreenWidth || isSmallScreenWidth; - const [sortColumn, setSortColumn] = useState(); - const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc'); - if (displayNarrowVersion) { return; } - const onSortPress = (columnName: SearchColumnConfig['columnName'], order: 'asc' | 'desc') => { - setSortColumn(columnName); - setSortOrder(order); - }; - return ( {SearchColumns.map(({columnName, translationKey, shouldShowFn, isSortable}) => { - const isActive = sortColumn === columnName; + const isActive = sortBy === columnName; return ( onPress(newSortOrder)} - accessibilityLabel={''} + role={CONST.ROLE.BUTTON} + accessibilityLabel={CONST.ROLE.BUTTON} + accessible disabled={!isSortable} > @@ -47,9 +50,9 @@ export default function SortableHeaderText({text, sortOrder, isActive, textStyle {text} diff --git a/src/libs/API/parameters/Search.ts b/src/libs/API/parameters/Search.ts index e9d18b3c4541..55a56dcd2543 100644 --- a/src/libs/API/parameters/Search.ts +++ b/src/libs/API/parameters/Search.ts @@ -1,7 +1,9 @@ type SearchParams = { + hash: number; query: string; policyIDs?: string; - hash: number; + sortBy?: string; + sortOrder?: 'asc' | 'desc'; offset: number; }; diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index e4820df10df9..9bfa490d4582 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -13,7 +13,7 @@ import type { import type {ValueOf} from 'type-fest'; import type {IOURequestType} from '@libs/actions/IOU'; import type CONST from '@src/CONST'; -import type {Country, IOUAction, IOUType} from '@src/CONST'; +import type {Country, IOUAction, IOUType, SearchColumnType} from '@src/CONST'; import type NAVIGATORS from '@src/NAVIGATORS'; import type {HybridAppRoute, Route as Routes} from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; @@ -64,6 +64,10 @@ type CentralPaneNavigatorParamList = { [SCREENS.SETTINGS.WORKSPACES]: undefined; [SCREENS.SEARCH.CENTRAL_PANE]: { query: string; + policyIDs?: string; + offset?: number; + sortBy?: SearchColumnType; + sortOrder?: 'asc' | 'desc'; }; [SCREENS.SETTINGS.SAVE_THE_WORLD]: undefined; }; diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index fae9a427f4fa..fb93fc78481b 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -1,10 +1,11 @@ import Onyx from 'react-native-onyx'; import type {OnyxUpdate} from 'react-native-onyx'; import * as API from '@libs/API'; +import type {SearchParams} from '@libs/API/parameters'; import {READ_COMMANDS} from '@libs/API/types'; import ONYXKEYS from '@src/ONYXKEYS'; -function search(hash: number, query: string, offset = 0, policyIDs?: string) { +function search({hash, query, policyIDs, offset, sortBy, sortOrder}: SearchParams) { const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -31,7 +32,6 @@ function search(hash: number, query: string, offset = 0, policyIDs?: string) { API.read(READ_COMMANDS.SEARCH, {hash, query, offset, policyIDs}, {optimisticData, finallyData}); } - export { // eslint-disable-next-line import/prefer-default-export search, diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index bc36b7bd2d2f..3857cb263784 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -21,8 +21,10 @@ type SearchPageProps = StackScreenProps diff --git a/src/types/onyx/SearchResults.ts b/src/types/onyx/SearchResults.ts index 5bce5be12f30..f05af28d63d6 100644 --- a/src/types/onyx/SearchResults.ts +++ b/src/types/onyx/SearchResults.ts @@ -1,12 +1,13 @@ import type {ValueOf} from 'type-fest'; import type TransactionListItem from '@components/SelectionList/TransactionListItem'; +import type {TransactionListItemType} from '@components/SelectionList/types'; import type CONST from '@src/CONST'; type SearchDataTypes = ValueOf; type ListItemType = T extends typeof CONST.SEARCH_DATA_TYPES.TRANSACTION ? typeof TransactionListItem : never; -type SectionsType = T extends typeof CONST.SEARCH_DATA_TYPES.TRANSACTION ? SearchTransaction[] : never; +type SectionsType = T extends typeof CONST.SEARCH_DATA_TYPES.TRANSACTION ? TransactionListItemType[] : never; type SearchTypeToItemMap = { [K in SearchDataTypes]: { From b18bfa18d07d70020dd641bcddca733af8058c2c Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Thu, 16 May 2024 16:11:46 +0200 Subject: [PATCH 03/12] Add small improvements to Search sorting --- assets/images/arrow-down-long.svg | 1 + assets/images/arrow-up-long.svg | 1 + src/CONST.ts | 4 +--- src/components/Icon/Expensicons.ts | 4 ++++ src/components/Search.tsx | 8 ++++---- src/components/SelectionList/SearchTableHeader.tsx | 8 ++++---- src/components/SelectionList/SortableHeaderText.tsx | 11 ++++++----- src/libs/API/parameters/Search.ts | 4 +++- src/libs/Navigation/types.ts | 5 +++-- src/libs/SearchUtils.ts | 5 +++++ 10 files changed, 32 insertions(+), 19 deletions(-) create mode 100644 assets/images/arrow-down-long.svg create mode 100644 assets/images/arrow-up-long.svg 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 db699525a66b..5da39e49e984 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -4786,8 +4786,6 @@ type IOUType = ValueOf; type IOUAction = ValueOf; type IOURequestType = ValueOf; -type SearchColumnType = (typeof CONST.SEARCH_TABLE_COLUMNS)[keyof typeof CONST.SEARCH_TABLE_COLUMNS]; - -export type {Country, IOUAction, IOUType, RateAndUnit, OnboardingPurposeType, IOURequestType, SearchColumnType}; +export type {Country, IOUAction, IOUType, RateAndUnit, OnboardingPurposeType, IOURequestType}; export default CONST; diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index c147c3735e96..fc880be93f6c 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'; @@ -180,6 +182,8 @@ export { ArrowRight, ArrowRightLong, ArrowsUpDown, + ArrowUpLong, + ArrowDownLong, Wrench, BackArrow, Bank, diff --git a/src/components/Search.tsx b/src/components/Search.tsx index 73ab399fcc45..4f34c7cf745e 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -6,11 +6,11 @@ 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 EmptySearchView from '@pages/Search/EmptySearchView'; import useCustomBackHandler from '@pages/Search/useCustomBackHandler'; import CONST from '@src/CONST'; -import type {SearchColumnType} from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {SearchQuery} from '@src/types/onyx/SearchResults'; @@ -35,7 +35,7 @@ const columnNamesToPropertyMap = { [CONST.SEARCH_TABLE_COLUMNS.TAX_AMOUNT]: null, }; -function getSortedData(data: TransactionListItemType[], sortBy?: SearchColumnType, sortOrder?: 'asc' | 'desc') { +function getSortedData(data: TransactionListItemType[], sortBy?: SearchColumnType, sortOrder?: SortOrder) { if (!sortBy || !sortOrder) { return data; } @@ -69,7 +69,7 @@ type SearchProps = { query: SearchQuery; policyIDs?: string; sortBy?: SearchColumnType; - sortOrder?: 'asc' | 'desc'; + sortOrder?: SortOrder; }; function Search({query, policyIDs, sortOrder, sortBy}: SearchProps) { @@ -129,7 +129,7 @@ function Search({query, policyIDs, sortOrder, sortBy}: SearchProps) { const ListItem = SearchUtils.getListItem(type); const data = SearchUtils.getSections(searchResults?.data ?? {}, type); - const onSortPress = (column: SearchColumnType, order: 'asc' | 'desc') => { + const onSortPress = (column: SearchColumnType, order: SortOrder) => { const newRoute = ROUTES.SEARCH.getRoute(query, { query, sortBy: column, diff --git a/src/components/SelectionList/SearchTableHeader.tsx b/src/components/SelectionList/SearchTableHeader.tsx index 3bce8af15f19..31a5c032ce86 100644 --- a/src/components/SelectionList/SearchTableHeader.tsx +++ b/src/components/SelectionList/SearchTableHeader.tsx @@ -5,8 +5,8 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as SearchUtils from '@libs/SearchUtils'; +import type {SearchColumnType, SortOrder} from '@libs/SearchUtils'; import CONST from '@src/CONST'; -import type {SearchColumnType} from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import type * as OnyxTypes from '@src/types/onyx'; import SortableHeaderText from './SortableHeaderText'; @@ -80,8 +80,8 @@ const SearchColumns: SearchColumnConfig[] = [ type SearchTableHeaderProps = { data: OnyxTypes.SearchResults['data']; sortBy?: SearchColumnType; - sortOrder?: 'asc' | 'desc'; - onSortPress: (column: SearchColumnType, order: 'asc' | 'desc') => void; + sortOrder?: SortOrder; + onSortPress: (column: SearchColumnType, order: SortOrder) => void; }; function SearchTableHeader({data, sortBy, sortOrder, onSortPress}: SearchTableHeaderProps) { @@ -109,7 +109,7 @@ function SearchTableHeader({data, sortBy, sortOrder, onSortPress}: SearchTableHe containerStyle={[StyleUtils.getSearchTableColumnStyles(columnName)]} shouldShow={shouldShowFn(data)} isSortable={isSortable} - onPress={(order: 'asc' | 'desc') => onSortPress(columnName, order)} + onPress={(order: SortOrder) => onSortPress(columnName, order)} /> ); })} diff --git a/src/components/SelectionList/SortableHeaderText.tsx b/src/components/SelectionList/SortableHeaderText.tsx index 8a9389b35f65..088984b48f0e 100644 --- a/src/components/SelectionList/SortableHeaderText.tsx +++ b/src/components/SelectionList/SortableHeaderText.tsx @@ -7,17 +7,18 @@ import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import Text from '@components/Text'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import type {SortOrder} from '@libs/SearchUtils'; import CONST from '@src/CONST'; type SearchTableHeaderColumnProps = { text: string; isActive: boolean; - sortOrder: 'asc' | 'desc'; + sortOrder: SortOrder; shouldShow?: boolean; isSortable?: boolean; containerStyle?: StyleProp; textStyle?: StyleProp; - onPress: (order: 'asc' | 'desc') => void; + onPress: (order: SortOrder) => void; }; export default function SortableHeaderText({text, sortOrder, isActive, textStyle, containerStyle, shouldShow = true, isSortable = true, onPress}: SearchTableHeaderColumnProps) { @@ -28,15 +29,15 @@ export default function SortableHeaderText({text, sortOrder, isActive, textStyle return null; } - const icon = sortOrder === 'asc' ? Expensicons.UpArrow : Expensicons.DownArrow; + const icon = sortOrder === 'asc' ? Expensicons.ArrowUpLong : Expensicons.ArrowDownLong; const iconStyles = isActive ? [] : [styles.visibilityHidden]; - const newSortOrder = isActive && sortOrder === 'asc' ? 'desc' : 'asc'; + const nextSortOrder = isActive && sortOrder === 'asc' ? 'desc' : 'asc'; return ( onPress(newSortOrder)} + onPress={() => onPress(nextSortOrder)} role={CONST.ROLE.BUTTON} accessibilityLabel={CONST.ROLE.BUTTON} accessible diff --git a/src/libs/API/parameters/Search.ts b/src/libs/API/parameters/Search.ts index 55a56dcd2543..0a8345b0b7e0 100644 --- a/src/libs/API/parameters/Search.ts +++ b/src/libs/API/parameters/Search.ts @@ -1,9 +1,11 @@ +import type {SortOrder} from '@libs/SearchUtils'; + type SearchParams = { hash: number; query: string; policyIDs?: string; sortBy?: string; - sortOrder?: 'asc' | 'desc'; + sortOrder?: SortOrder; offset: number; }; diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 9bfa490d4582..7112ddca607c 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -12,8 +12,9 @@ import type { } from '@react-navigation/native'; import type {ValueOf} from 'type-fest'; import type {IOURequestType} from '@libs/actions/IOU'; +import type {SearchColumnType, SortOrder} from '@libs/SearchUtils'; import type CONST from '@src/CONST'; -import type {Country, IOUAction, IOUType, SearchColumnType} from '@src/CONST'; +import type {Country, IOUAction, IOUType} from '@src/CONST'; import type NAVIGATORS from '@src/NAVIGATORS'; import type {HybridAppRoute, Route as Routes} from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; @@ -67,7 +68,7 @@ type CentralPaneNavigatorParamList = { policyIDs?: string; offset?: number; sortBy?: SearchColumnType; - sortOrder?: 'asc' | 'desc'; + sortOrder?: SortOrder; }; [SCREENS.SETTINGS.SAVE_THE_WORLD]: undefined; }; diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts index fb07990237f9..af75479cc313 100644 --- a/src/libs/SearchUtils.ts +++ b/src/libs/SearchUtils.ts @@ -7,6 +7,10 @@ import type * as OnyxTypes from '@src/types/onyx'; import type {SearchDataTypes, SearchTypeToItemMap} from '@src/types/onyx/SearchResults'; import * as UserUtils from './UserUtils'; +type SortOrder = 'asc' | 'desc'; + +type SearchColumnType = (typeof CONST.SEARCH_TABLE_COLUMNS)[keyof typeof CONST.SEARCH_TABLE_COLUMNS]; + function getSearchType(search: OnyxTypes.SearchResults['search']): SearchDataTypes | undefined { switch (search.type) { case CONST.SEARCH_DATA_TYPES.TRANSACTION: @@ -75,3 +79,4 @@ function getQueryHash(query: string, policyID?: string): number { } export {getListItem, getQueryHash, getSections, getShouldShowColumn, getShouldShowMerchant, getSearchType}; +export type {SearchColumnType, SortOrder}; From b48978354e1b1bc51d8e8bd896b0a2f8e926ee1c Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Thu, 16 May 2024 16:25:57 +0200 Subject: [PATCH 04/12] Fix text not aligned in TransactionListItem --- src/components/SelectionList/TransactionListItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SelectionList/TransactionListItem.tsx b/src/components/SelectionList/TransactionListItem.tsx index bb0918d7569b..5af32e05f2ac 100644 --- a/src/components/SelectionList/TransactionListItem.tsx +++ b/src/components/SelectionList/TransactionListItem.tsx @@ -80,7 +80,7 @@ function TransactionListItem({ ); From 62dc109ad81735c450143f901dd682dd632a7790 Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Fri, 17 May 2024 09:40:29 +0200 Subject: [PATCH 05/12] Update search to work with pagination --- src/components/Search.tsx | 14 ++++++-------- src/libs/actions/Search.ts | 2 +- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/components/Search.tsx b/src/components/Search.tsx index 4f34c7cf745e..a6729510aa4e 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -1,3 +1,4 @@ +import {useNavigation} from '@react-navigation/native'; import React, {useEffect} from 'react'; import {useOnyx} from 'react-native-onyx'; import useNetwork from '@hooks/useNetwork'; @@ -75,19 +76,19 @@ type SearchProps = { function Search({query, policyIDs, sortOrder, sortBy}: SearchProps) { const {isOffline} = useNetwork(); const styles = useThemeStyles(); + const navigation = useNavigation(); + useCustomBackHandler(); const hash = SearchUtils.getQueryHash(query, policyIDs); const [searchResults, searchResultsMeta] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`); - const offset = 0; - useEffect(() => { if (isOffline) { return; } - SearchActions.search({hash, query, policyIDs, offset, sortBy, sortOrder}); + SearchActions.search({hash, query, policyIDs, offset: 0, sortBy, sortOrder}); // eslint-disable-next-line react-hooks/exhaustive-deps }, [hash, isOffline, sortBy, sortOrder]); @@ -116,7 +117,7 @@ function Search({query, policyIDs, sortOrder, sortBy}: SearchProps) { return; } const currentOffset = searchResults?.search?.offset ?? 0; - SearchActions.search({hash, query, offset: 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); @@ -130,13 +131,10 @@ function Search({query, policyIDs, sortOrder, sortBy}: SearchProps) { const data = SearchUtils.getSections(searchResults?.data ?? {}, type); const onSortPress = (column: SearchColumnType, order: SortOrder) => { - const newRoute = ROUTES.SEARCH.getRoute(query, { - query, + navigation.setParams({ sortBy: column, sortOrder: order, }); - - Navigation.navigate(newRoute); }; const sortedData = getSortedData(data, sortBy, sortOrder); diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index fb93fc78481b..43f3e2697e8a 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -30,7 +30,7 @@ function search({hash, query, policyIDs, offset, sortBy, sortOrder}: SearchParam }, ]; - API.read(READ_COMMANDS.SEARCH, {hash, query, offset, policyIDs}, {optimisticData, finallyData}); + API.read(READ_COMMANDS.SEARCH, {hash, query, offset, policyIDs, sortBy, sortOrder}, {optimisticData, finallyData}); } export { // eslint-disable-next-line import/prefer-default-export From 031fe03b016aefeda3694690c2b01083b9f6e3c0 Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Fri, 17 May 2024 14:34:39 +0200 Subject: [PATCH 06/12] Fix navigation types --- src/components/Search.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/Search.tsx b/src/components/Search.tsx index a6729510aa4e..a1ca673d3890 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -1,4 +1,5 @@ import {useNavigation} from '@react-navigation/native'; +import type {StackNavigationProp} from '@react-navigation/stack'; import React, {useEffect} from 'react'; import {useOnyx} from 'react-native-onyx'; import useNetwork from '@hooks/useNetwork'; @@ -9,6 +10,7 @@ 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 useCustomBackHandler from '@pages/Search/useCustomBackHandler'; import CONST from '@src/CONST'; @@ -76,7 +78,7 @@ type SearchProps = { function Search({query, policyIDs, sortOrder, sortBy}: SearchProps) { const {isOffline} = useNetwork(); const styles = useThemeStyles(); - const navigation = useNavigation(); + const navigation = useNavigation>(); useCustomBackHandler(); From 6e379da421885c4306cfd2d208bc94aed8137ef3 Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Mon, 20 May 2024 14:00:45 +0200 Subject: [PATCH 07/12] Make sorting work for every Search column --- src/components/Search.tsx | 25 ++++++++-------- .../SelectionList/SearchTableHeader.tsx | 1 + .../SelectionList/TransactionListItem.tsx | 22 +++++++------- src/components/SelectionList/types.ts | 12 ++++++++ src/libs/SearchUtils.ts | 30 ++++++++++++++----- 5 files changed, 59 insertions(+), 31 deletions(-) diff --git a/src/components/Search.tsx b/src/components/Search.tsx index a1ca673d3890..43a0428ecd45 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -25,13 +25,13 @@ import type {TransactionListItemType} from './SelectionList/types'; import TableListItemSkeleton from './Skeletons/TableListItemSkeleton'; const columnNamesToPropertyMap = { - [CONST.SEARCH_TABLE_COLUMNS.TO]: 'to', - [CONST.SEARCH_TABLE_COLUMNS.FROM]: 'from', - [CONST.SEARCH_TABLE_COLUMNS.DATE]: 'created', - [CONST.SEARCH_TABLE_COLUMNS.TAG]: '', - [CONST.SEARCH_TABLE_COLUMNS.MERCHANT]: 'merchant', - [CONST.SEARCH_TABLE_COLUMNS.TOTAL]: 'amount', - [CONST.SEARCH_TABLE_COLUMNS.CATEGORY]: 'category', + [CONST.SEARCH_TABLE_COLUMNS.TO]: 'formattedTo' as const, + [CONST.SEARCH_TABLE_COLUMNS.FROM]: 'formattedFrom' as const, + [CONST.SEARCH_TABLE_COLUMNS.DATE]: 'date' as const, + [CONST.SEARCH_TABLE_COLUMNS.TAG]: 'tag' as const, + [CONST.SEARCH_TABLE_COLUMNS.MERCHANT]: 'merchant' as const, + [CONST.SEARCH_TABLE_COLUMNS.TOTAL]: 'formattedTotal' as const, + [CONST.SEARCH_TABLE_COLUMNS.CATEGORY]: 'category' as const, [CONST.SEARCH_TABLE_COLUMNS.TYPE]: null, [CONST.SEARCH_TABLE_COLUMNS.ACTION]: null, [CONST.SEARCH_TABLE_COLUMNS.DESCRIPTION]: null, @@ -43,13 +43,12 @@ function getSortedData(data: TransactionListItemType[], sortBy?: SearchColumnTyp return data; } - const sortingProp = columnNamesToPropertyMap[sortBy] as keyof TransactionListItemType; + const sortingProp = columnNamesToPropertyMap[sortBy]; if (!sortingProp) { return data; } - // Todo sorting needs more work return data.sort((a, b) => { const aValue = a[sortingProp]; const bValue = b[sortingProp]; @@ -58,13 +57,15 @@ function getSortedData(data: TransactionListItemType[], sortBy?: SearchColumnTyp return 0; } + // We are guaranteed that both a and b will be string or number at the same time if (typeof aValue === 'string' && typeof bValue === 'string') { return sortOrder === 'asc' ? aValue.toLowerCase().localeCompare(bValue) : bValue.toLowerCase().localeCompare(aValue); } - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - return sortOrder === 'asc' ? aValue - bValue : bValue - aValue; + const aNum = aValue as number; + const bNum = bValue as number; + + return sortOrder === 'asc' ? aNum - bNum : bNum - aNum; }); } diff --git a/src/components/SelectionList/SearchTableHeader.tsx b/src/components/SelectionList/SearchTableHeader.tsx index 31a5c032ce86..9cd44e6716fa 100644 --- a/src/components/SelectionList/SearchTableHeader.tsx +++ b/src/components/SelectionList/SearchTableHeader.tsx @@ -67,6 +67,7 @@ const SearchColumns: SearchColumnConfig[] = [ { columnName: CONST.SEARCH_TABLE_COLUMNS.TYPE, translationKey: 'common.type', + isSortable: false, shouldShowFn: () => true, }, { diff --git a/src/components/SelectionList/TransactionListItem.tsx b/src/components/SelectionList/TransactionListItem.tsx index 5af32e05f2ac..1612216816cc 100644 --- a/src/components/SelectionList/TransactionListItem.tsx +++ b/src/components/SelectionList/TransactionListItem.tsx @@ -60,11 +60,10 @@ function TransactionListItem({ } const isFromExpenseReport = transactionItem.reportType === CONST.REPORT.TYPE.EXPENSE; - const date = TransactionUtils.getCreated(transactionItem as OnyxEntry, CONST.DATE.MONTH_DAY_ABBR_FORMAT); - const amount = TransactionUtils.getAmount(transactionItem as OnyxEntry, isFromExpenseReport); - const taxAmount = TransactionUtils.getTaxAmount(transactionItem as OnyxEntry, isFromExpenseReport); - const currency = TransactionUtils.getCurrency(transactionItem as OnyxEntry); - const description = TransactionUtils.getDescription(transactionItem as OnyxEntry); + const date = TransactionUtils.getCreated(transactionItem, CONST.DATE.MONTH_DAY_ABBR_FORMAT); + const taxAmount = TransactionUtils.getTaxAmount(transactionItem, isFromExpenseReport); + const currency = TransactionUtils.getCurrency(transactionItem); + const description = TransactionUtils.getDescription(transactionItem); const merchant = getMerchant(); const typeIcon = getTypeIcon(transactionItem.type); @@ -84,8 +83,7 @@ function TransactionListItem({ /> ); - const userCell = (participant: SearchAccountDetails) => { - const displayName = participant?.name ?? participant?.displayName ?? participant?.login; + const userCell = (participant: SearchAccountDetails, displayName: string) => { const avatarURL = participant?.avatarURL ?? participant?.avatar; const isWorkspace = participant?.avatarURL !== undefined; const iconType = isWorkspace ? CONST.ICON_TYPE_WORKSPACE : CONST.ICON_TYPE_AVATAR; @@ -149,7 +147,7 @@ function TransactionListItem({ const totalCell = ( ); @@ -202,14 +200,14 @@ function TransactionListItem({ <> - {userCell(transactionItem.from)} + {userCell(transactionItem.from, transactionItem.formattedFrom)} - {userCell(transactionItem.to)} + {userCell(transactionItem.to, transactionItem.formattedTo)} {actionCell} @@ -259,8 +257,8 @@ function TransactionListItem({ {dateCell} {merchantCell} - {userCell(transactionItem.from)} - {userCell(transactionItem.to)} + {userCell(transactionItem.from, transactionItem.formattedFrom)} + {userCell(transactionItem.to, transactionItem.formattedTo)} {transactionItem.shouldShowCategory && {categoryCell}} {transactionItem.shouldShowTag && {tagCell}} {transactionItem.shouldShowTax && {taxCell}} diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 0a4b0532b581..ad2a2fe96bce 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -135,6 +135,18 @@ type TransactionListItemType = ListItem & /** The personal details of the user paying the request */ to: SearchAccountDetails; + /** final and formatted "from" value used for displaying and sorting */ + formattedFrom: string; + + /** final and formatted "to" value used for displaying and sorting */ + formattedTo: string; + + /** final and formatted "total" value used for displaying and sorting */ + formattedTotal: number; + + /** final "date" value used for sorting */ + date: string; + /** Whether we should show the merchant column */ shouldShowMerchant: boolean; diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts index af75479cc313..2d480f00f98d 100644 --- a/src/libs/SearchUtils.ts +++ b/src/libs/SearchUtils.ts @@ -4,7 +4,8 @@ import type {TransactionListItemType} from '@components/SelectionList/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; -import type {SearchDataTypes, SearchTypeToItemMap} from '@src/types/onyx/SearchResults'; +import type {SearchAccountDetails, SearchDataTypes, SearchTypeToItemMap} from '@src/types/onyx/SearchResults'; +import * as TransactionUtils from './TransactionUtils'; import * as UserUtils from './UserUtils'; type SortOrder = 'asc' | 'desc'; @@ -36,19 +37,34 @@ function getTransactionsSections(data: OnyxTypes.SearchResults['data']): Transac const shouldShowCategory = getShouldShowColumn(data, CONST.SEARCH_TABLE_COLUMNS.CATEGORY); const shouldShowTag = getShouldShowColumn(data, CONST.SEARCH_TABLE_COLUMNS.TAG); const shouldShowTax = getShouldShowColumn(data, CONST.SEARCH_TABLE_COLUMNS.TAX_AMOUNT); + return Object.entries(data) .filter(([key]) => key.startsWith(ONYXKEYS.COLLECTION.TRANSACTION)) - .map(([, value]) => { - const isExpenseReport = value.reportType === CONST.REPORT.TYPE.EXPENSE; + .map(([, transactionItem]) => { + const isExpenseReport = transactionItem.reportType === CONST.REPORT.TYPE.EXPENSE; + const from = data.personalDetailsList?.[transactionItem.accountID]; + const to = isExpenseReport + ? (data[`${ONYXKEYS.COLLECTION.POLICY}${transactionItem.policyID}`] as SearchAccountDetails) + : (data.personalDetailsList?.[transactionItem.managerID] as SearchAccountDetails); + + const formattedFrom = from.displayName ?? from.login ?? ''; + const formattedTo = to.name ?? to.displayName ?? to.login ?? ''; + const formattedTotal = TransactionUtils.getAmount(transactionItem, isExpenseReport); + const date = transactionItem?.modifiedCreated ? transactionItem.modifiedCreated : transactionItem?.created; + return { - ...value, - from: data.personalDetailsList?.[value.accountID], - to: isExpenseReport ? data[`${ONYXKEYS.COLLECTION.POLICY}${value.policyID}`] : data.personalDetailsList?.[value.managerID], + ...transactionItem, + from, + to, + formattedFrom, + formattedTo, + date, + formattedTotal, shouldShowMerchant, shouldShowCategory, shouldShowTag, shouldShowTax, - keyForList: value.transactionID, + keyForList: transactionItem.transactionID, }; }) .sort((a, b) => { From 7dab1722da808526c99f4ed8937f40a6b4f4978c Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Tue, 21 May 2024 10:37:39 +0200 Subject: [PATCH 08/12] Allow sorting by more columns --- src/components/Search.tsx | 4 ++-- src/components/SelectionList/SearchTableHeader.tsx | 3 +-- .../CentralPaneNavigator/BaseCentralPaneNavigator.tsx | 3 ++- src/libs/SearchUtils.ts | 5 ----- src/styles/utils/index.ts | 2 +- 5 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/components/Search.tsx b/src/components/Search.tsx index 43a0428ecd45..1ccfc154cead 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -32,8 +32,8 @@ const columnNamesToPropertyMap = { [CONST.SEARCH_TABLE_COLUMNS.MERCHANT]: 'merchant' as const, [CONST.SEARCH_TABLE_COLUMNS.TOTAL]: 'formattedTotal' as const, [CONST.SEARCH_TABLE_COLUMNS.CATEGORY]: 'category' as const, - [CONST.SEARCH_TABLE_COLUMNS.TYPE]: null, - [CONST.SEARCH_TABLE_COLUMNS.ACTION]: null, + [CONST.SEARCH_TABLE_COLUMNS.TYPE]: 'type' as const, + [CONST.SEARCH_TABLE_COLUMNS.ACTION]: 'action' as const, [CONST.SEARCH_TABLE_COLUMNS.DESCRIPTION]: null, [CONST.SEARCH_TABLE_COLUMNS.TAX_AMOUNT]: null, }; diff --git a/src/components/SelectionList/SearchTableHeader.tsx b/src/components/SelectionList/SearchTableHeader.tsx index 9cd44e6716fa..a5ebe43de5ac 100644 --- a/src/components/SelectionList/SearchTableHeader.tsx +++ b/src/components/SelectionList/SearchTableHeader.tsx @@ -67,13 +67,11 @@ const SearchColumns: SearchColumnConfig[] = [ { columnName: CONST.SEARCH_TABLE_COLUMNS.TYPE, translationKey: 'common.type', - isSortable: false, shouldShowFn: () => true, }, { columnName: CONST.SEARCH_TABLE_COLUMNS.ACTION, translationKey: 'common.action', - isSortable: false, shouldShowFn: () => true, }, ]; @@ -104,6 +102,7 @@ function SearchTableHeader({data, sortBy, sortOrder, onSortPress}: SearchTableHe return ( (); @@ -44,7 +45,7 @@ function BaseCentralPaneNavigator() { /> diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts index 2d480f00f98d..b6c271d0409f 100644 --- a/src/libs/SearchUtils.ts +++ b/src/libs/SearchUtils.ts @@ -66,11 +66,6 @@ function getTransactionsSections(data: OnyxTypes.SearchResults['data']): Transac shouldShowTax, keyForList: transactionItem.transactionID, }; - }) - .sort((a, b) => { - const createdA = a.modifiedCreated ? a.modifiedCreated : a.created; - const createdB = b.modifiedCreated ? b.modifiedCreated : b.created; - return createdB > createdA ? 1 : -1; }); } diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 4c2f9a180518..46bbba99e37e 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -1582,7 +1582,7 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ columnWidth = {...getWidthStyle(variables.w28), ...styles.alignItemsCenter}; break; case CONST.SEARCH_TABLE_COLUMNS.ACTION: - columnWidth = getWidthStyle(variables.w80); + columnWidth = {...getWidthStyle(variables.w80), ...styles.alignItemsCenter}; break; default: columnWidth = styles.flex1; From 782a42d11be08b8f2993b3ef7265a098ac1c75bf Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Wed, 22 May 2024 12:39:52 +0200 Subject: [PATCH 09/12] Refactor sorting props and update sorting hash generation --- src/CONST.ts | 5 ++ src/components/Search.tsx | 76 +++++-------------- .../SelectionList/SearchTableHeader.tsx | 30 ++++---- .../SelectionList/SortableHeaderText.tsx | 4 +- .../BaseCentralPaneNavigator.tsx | 2 +- src/libs/SearchUtils.ts | 54 +++++++++++-- 6 files changed, 92 insertions(+), 79 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 5da39e49e984..a6b6e0a2e651 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -4778,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/components/Search.tsx b/src/components/Search.tsx index 1ccfc154cead..688a58601e91 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -1,6 +1,7 @@ import {useNavigation} from '@react-navigation/native'; import type {StackNavigationProp} from '@react-navigation/stack'; -import React, {useEffect} from 'react'; +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'; @@ -17,58 +18,13 @@ 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'; import SearchTableHeader from './SelectionList/SearchTableHeader'; -import type {TransactionListItemType} from './SelectionList/types'; import TableListItemSkeleton from './Skeletons/TableListItemSkeleton'; -const columnNamesToPropertyMap = { - [CONST.SEARCH_TABLE_COLUMNS.TO]: 'formattedTo' as const, - [CONST.SEARCH_TABLE_COLUMNS.FROM]: 'formattedFrom' as const, - [CONST.SEARCH_TABLE_COLUMNS.DATE]: 'date' as const, - [CONST.SEARCH_TABLE_COLUMNS.TAG]: 'tag' as const, - [CONST.SEARCH_TABLE_COLUMNS.MERCHANT]: 'merchant' as const, - [CONST.SEARCH_TABLE_COLUMNS.TOTAL]: 'formattedTotal' as const, - [CONST.SEARCH_TABLE_COLUMNS.CATEGORY]: 'category' as const, - [CONST.SEARCH_TABLE_COLUMNS.TYPE]: 'type' as const, - [CONST.SEARCH_TABLE_COLUMNS.ACTION]: 'action' as const, - [CONST.SEARCH_TABLE_COLUMNS.DESCRIPTION]: null, - [CONST.SEARCH_TABLE_COLUMNS.TAX_AMOUNT]: null, -}; - -function getSortedData(data: TransactionListItemType[], sortBy?: SearchColumnType, sortOrder?: SortOrder) { - if (!sortBy || !sortOrder) { - return data; - } - - const sortingProp = columnNamesToPropertyMap[sortBy]; - - if (!sortingProp) { - return data; - } - - return data.sort((a, b) => { - const aValue = a[sortingProp]; - const bValue = b[sortingProp]; - - if (!aValue || !bValue) { - return 0; - } - - // We are guaranteed that both a and b will be string or number at the same time - if (typeof aValue === 'string' && typeof bValue === 'string') { - return sortOrder === 'asc' ? aValue.toLowerCase().localeCompare(bValue) : bValue.toLowerCase().localeCompare(aValue); - } - - const aNum = aValue as number; - const bNum = bValue as number; - - return sortOrder === 'asc' ? aNum - bNum : bNum - aNum; - }); -} - type SearchProps = { query: SearchQuery; policyIDs?: string; @@ -80,11 +36,19 @@ function Search({query, policyIDs, sortOrder, sortBy}: SearchProps) { const {isOffline} = useNetwork(); const styles = useThemeStyles(); const navigation = useNavigation>(); + const lastSearchResultsRef = useRef>(); useCustomBackHandler(); - const hash = SearchUtils.getQueryHash(query, policyIDs); - const [searchResults, searchResultsMeta] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`); + const hash = SearchUtils.getQueryHash(query, policyIDs, sortOrder, sortBy); + 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) { @@ -93,13 +57,13 @@ function Search({query, policyIDs, sortOrder, sortBy}: SearchProps) { SearchActions.search({hash, query, policyIDs, offset: 0, sortBy, sortOrder}); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [hash, isOffline, sortBy, sortOrder]); + }, [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 ; } @@ -116,7 +80,7 @@ function Search({query, policyIDs, sortOrder, sortBy}: SearchProps) { }; const fetchMoreResults = () => { - if (!searchResults?.search?.hasMoreResults || isLoadingInitialItems || isLoadingMoreItems) { + if (!searchResults?.search?.hasMoreResults || isLoadingItems || isLoadingMoreItems) { return; } const currentOffset = searchResults?.search?.offset ?? 0; @@ -140,7 +104,7 @@ function Search({query, policyIDs, sortOrder, sortBy}: SearchProps) { }); }; - const sortedData = getSortedData(data, sortBy, sortOrder); + const sortedData = SearchUtils.getSortedData(data, sortBy, sortOrder); return ( boolean; + shouldShow: (data: OnyxTypes.SearchResults['data']) => boolean; }; const SearchColumns: SearchColumnConfig[] = [ { columnName: CONST.SEARCH_TABLE_COLUMNS.DATE, translationKey: 'common.date', - shouldShowFn: () => true, + shouldShow: () => true, }, { columnName: CONST.SEARCH_TABLE_COLUMNS.MERCHANT, translationKey: 'common.merchant', - shouldShowFn: (data: OnyxTypes.SearchResults['data']) => SearchUtils.getShouldShowMerchant(data), + shouldShow: (data: OnyxTypes.SearchResults['data']) => SearchUtils.getShouldShowMerchant(data), }, { columnName: CONST.SEARCH_TABLE_COLUMNS.DESCRIPTION, translationKey: 'common.description', - shouldShowFn: (data: OnyxTypes.SearchResults['data']) => !SearchUtils.getShouldShowMerchant(data), + shouldShow: (data: OnyxTypes.SearchResults['data']) => !SearchUtils.getShouldShowMerchant(data), }, { columnName: CONST.SEARCH_TABLE_COLUMNS.FROM, translationKey: 'common.from', - shouldShowFn: () => true, + shouldShow: () => true, }, { columnName: CONST.SEARCH_TABLE_COLUMNS.TO, translationKey: 'common.to', - shouldShowFn: () => true, + shouldShow: () => true, }, { columnName: CONST.SEARCH_TABLE_COLUMNS.CATEGORY, translationKey: 'common.category', - shouldShowFn: (data: OnyxTypes.SearchResults['data']) => SearchUtils.getShouldShowColumn(data, CONST.SEARCH_TABLE_COLUMNS.CATEGORY), + shouldShow: (data: OnyxTypes.SearchResults['data']) => SearchUtils.getShouldShowColumn(data, CONST.SEARCH_TABLE_COLUMNS.CATEGORY), }, { columnName: CONST.SEARCH_TABLE_COLUMNS.TAG, translationKey: 'common.tag', - shouldShowFn: (data: OnyxTypes.SearchResults['data']) => SearchUtils.getShouldShowColumn(data, CONST.SEARCH_TABLE_COLUMNS.TAG), + shouldShow: (data: OnyxTypes.SearchResults['data']) => SearchUtils.getShouldShowColumn(data, CONST.SEARCH_TABLE_COLUMNS.TAG), }, { columnName: CONST.SEARCH_TABLE_COLUMNS.TAX_AMOUNT, translationKey: 'common.tax', - shouldShowFn: (data: OnyxTypes.SearchResults['data']) => SearchUtils.getShouldShowColumn(data, CONST.SEARCH_TABLE_COLUMNS.TAX_AMOUNT), + shouldShow: (data: OnyxTypes.SearchResults['data']) => SearchUtils.getShouldShowColumn(data, CONST.SEARCH_TABLE_COLUMNS.TAX_AMOUNT), }, { columnName: CONST.SEARCH_TABLE_COLUMNS.TOTAL, translationKey: 'common.total', - shouldShowFn: () => true, + shouldShow: () => true, }, { columnName: CONST.SEARCH_TABLE_COLUMNS.TYPE, translationKey: 'common.type', - shouldShowFn: () => true, + shouldShow: () => true, }, { columnName: CONST.SEARCH_TABLE_COLUMNS.ACTION, translationKey: 'common.action', - shouldShowFn: () => true, + shouldShow: () => true, }, ]; @@ -97,17 +97,17 @@ function SearchTableHeader({data, sortBy, sortOrder, onSortPress}: SearchTableHe return ( - {SearchColumns.map(({columnName, translationKey, shouldShowFn, isSortable}) => { + {SearchColumns.map(({columnName, translationKey, shouldShow, isSortable}) => { const isActive = sortBy === columnName; return ( onSortPress(columnName, order)} /> diff --git a/src/components/SelectionList/SortableHeaderText.tsx b/src/components/SelectionList/SortableHeaderText.tsx index 088984b48f0e..5a837dc548c4 100644 --- a/src/components/SelectionList/SortableHeaderText.tsx +++ b/src/components/SelectionList/SortableHeaderText.tsx @@ -29,10 +29,10 @@ export default function SortableHeaderText({text, sortOrder, isActive, textStyle return null; } - const icon = sortOrder === 'asc' ? Expensicons.ArrowUpLong : Expensicons.ArrowDownLong; + const icon = sortOrder === CONST.SORT_ORDER.ASC ? Expensicons.ArrowUpLong : Expensicons.ArrowDownLong; const iconStyles = isActive ? [] : [styles.visibilityHidden]; - const nextSortOrder = isActive && sortOrder === 'asc' ? 'desc' : 'asc'; + const nextSortOrder = isActive && sortOrder === CONST.SORT_ORDER.DESC ? CONST.SORT_ORDER.ASC : CONST.SORT_ORDER.DESC; return ( diff --git a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx index b4b563bb6fac..1e257732cd91 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx @@ -45,7 +45,7 @@ function BaseCentralPaneNavigator() { /> diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts index b6c271d0409f..1306368f195c 100644 --- a/src/libs/SearchUtils.ts +++ b/src/libs/SearchUtils.ts @@ -8,10 +8,23 @@ import type {SearchAccountDetails, SearchDataTypes, SearchTypeToItemMap} from '@ import * as TransactionUtils from './TransactionUtils'; import * as UserUtils from './UserUtils'; -type SortOrder = 'asc' | 'desc'; - +type SortOrder = (typeof CONST.SORT_ORDER)[keyof typeof CONST.SORT_ORDER]; type SearchColumnType = (typeof CONST.SEARCH_TABLE_COLUMNS)[keyof typeof CONST.SEARCH_TABLE_COLUMNS]; +const columnNamesToSortingProperty = { + [CONST.SEARCH_TABLE_COLUMNS.TO]: 'formattedTo' as const, + [CONST.SEARCH_TABLE_COLUMNS.FROM]: 'formattedFrom' as const, + [CONST.SEARCH_TABLE_COLUMNS.DATE]: 'date' as const, + [CONST.SEARCH_TABLE_COLUMNS.TAG]: 'tag' as const, + [CONST.SEARCH_TABLE_COLUMNS.MERCHANT]: 'merchant' as const, + [CONST.SEARCH_TABLE_COLUMNS.TOTAL]: 'formattedTotal' as const, + [CONST.SEARCH_TABLE_COLUMNS.CATEGORY]: 'category' as const, + [CONST.SEARCH_TABLE_COLUMNS.TYPE]: 'type' as const, + [CONST.SEARCH_TABLE_COLUMNS.ACTION]: 'action' as const, + [CONST.SEARCH_TABLE_COLUMNS.DESCRIPTION]: null, + [CONST.SEARCH_TABLE_COLUMNS.TAX_AMOUNT]: null, +}; + function getSearchType(search: OnyxTypes.SearchResults['search']): SearchDataTypes | undefined { switch (search.type) { case CONST.SEARCH_DATA_TYPES.TRANSACTION: @@ -84,10 +97,41 @@ function getSections(data: OnyxTypes.Search return searchTypeToItemMap[type].getSections(data) as ReturnType; } -function getQueryHash(query: string, policyID?: string): number { - const textToHash = [query, policyID].filter(Boolean).join('_'); +function getQueryHash(query: string, policyID?: string, sortBy?: string, sortOrder?: string): number { + const textToHash = [query, policyID, sortOrder, sortBy].filter(Boolean).join('_'); return UserUtils.hashText(textToHash, 2 ** 32); } -export {getListItem, getQueryHash, getSections, getShouldShowColumn, getShouldShowMerchant, getSearchType}; +function getSortedData(data: TransactionListItemType[], sortBy?: SearchColumnType, sortOrder?: SortOrder) { + if (!sortBy || !sortOrder) { + return data; + } + + const sortingProperty = columnNamesToSortingProperty[sortBy]; + + if (!sortingProperty) { + return data; + } + + return data.sort((a, b) => { + const aValue = a[sortingProperty]; + const bValue = b[sortingProperty]; + + if (!aValue || !bValue) { + return 0; + } + + // We are guaranteed that both a and b will be string or number at the same time + if (typeof aValue === 'string' && typeof bValue === 'string') { + return sortOrder === CONST.SORT_ORDER.ASC ? aValue.toLowerCase().localeCompare(bValue) : bValue.toLowerCase().localeCompare(aValue); + } + + const aNum = aValue as number; + const bNum = bValue as number; + + return sortOrder === CONST.SORT_ORDER.ASC ? aNum - bNum : bNum - aNum; + }); +} + +export {getListItem, getQueryHash, getSections, getShouldShowColumn, getShouldShowMerchant, getSearchType, getSortedData}; export type {SearchColumnType, SortOrder}; From 5a71c3f092ac71633efd68e92818bf02f44f4932 Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Thu, 23 May 2024 10:01:45 +0200 Subject: [PATCH 10/12] Update styling of SearchTableHeader column --- .../SelectionList/SortableHeaderText.tsx | 17 +++++++++-------- src/styles/utils/index.ts | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/components/SelectionList/SortableHeaderText.tsx b/src/components/SelectionList/SortableHeaderText.tsx index 5a837dc548c4..8ec30610a9d4 100644 --- a/src/components/SelectionList/SortableHeaderText.tsx +++ b/src/components/SelectionList/SortableHeaderText.tsx @@ -30,7 +30,7 @@ export default function SortableHeaderText({text, sortOrder, isActive, textStyle } const icon = sortOrder === CONST.SORT_ORDER.ASC ? Expensicons.ArrowUpLong : Expensicons.ArrowDownLong; - const iconStyles = isActive ? [] : [styles.visibilityHidden]; + const displayIcon = isActive; const nextSortOrder = isActive && sortOrder === CONST.SORT_ORDER.DESC ? CONST.SORT_ORDER.ASC : CONST.SORT_ORDER.DESC; @@ -50,13 +50,14 @@ export default function SortableHeaderText({text, sortOrder, isActive, textStyle > {text} - + {displayIcon && ( + + )} diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 46bbba99e37e..324ee80dc8e0 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -1579,7 +1579,7 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ columnWidth = {...getWidthStyle(variables.w96), ...styles.alignItemsEnd}; break; case CONST.SEARCH_TABLE_COLUMNS.TYPE: - columnWidth = {...getWidthStyle(variables.w28), ...styles.alignItemsCenter}; + columnWidth = {...getWidthStyle(variables.w44), ...styles.alignItemsCenter}; break; case CONST.SEARCH_TABLE_COLUMNS.ACTION: columnWidth = {...getWidthStyle(variables.w80), ...styles.alignItemsCenter}; From fea65222cf183a7c59fb47bed2f97ccbfe29722e Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Tue, 28 May 2024 12:17:00 +0200 Subject: [PATCH 11/12] Fix sorting by merchant column --- src/components/SelectionList/TransactionListItem.tsx | 10 +--------- src/components/SelectionList/types.ts | 3 +++ src/libs/SearchUtils.ts | 9 ++++++--- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/components/SelectionList/TransactionListItem.tsx b/src/components/SelectionList/TransactionListItem.tsx index 1612216816cc..db058302db21 100644 --- a/src/components/SelectionList/TransactionListItem.tsx +++ b/src/components/SelectionList/TransactionListItem.tsx @@ -1,6 +1,5 @@ import React from 'react'; 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'; @@ -16,7 +15,6 @@ import * as CurrencyUtils from '@libs/CurrencyUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; -import type {Transaction} from '@src/types/onyx'; import type {SearchAccountDetails, SearchTransactionType} from '@src/types/onyx/SearchResults'; import BaseListItem from './BaseListItem'; import TextWithIconCell from './TextWithIconCell'; @@ -54,17 +52,11 @@ function TransactionListItem({ const {isLargeScreenWidth} = useWindowDimensions(); const StyleUtils = useStyleUtils(); - function getMerchant() { - const merchant = TransactionUtils.getMerchant(transactionItem as OnyxEntry); - return merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT || merchant === CONST.TRANSACTION.DEFAULT_MERCHANT ? '' : merchant; - } - const isFromExpenseReport = transactionItem.reportType === CONST.REPORT.TYPE.EXPENSE; const date = TransactionUtils.getCreated(transactionItem, CONST.DATE.MONTH_DAY_ABBR_FORMAT); const taxAmount = TransactionUtils.getTaxAmount(transactionItem, isFromExpenseReport); const currency = TransactionUtils.getCurrency(transactionItem); const description = TransactionUtils.getDescription(transactionItem); - const merchant = getMerchant(); const typeIcon = getTypeIcon(transactionItem.type); const dateCell = ( @@ -78,7 +70,7 @@ function TransactionListItem({ const merchantCell = ( ); diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index ad2a2fe96bce..f97a6f883c69 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -144,6 +144,9 @@ type TransactionListItemType = ListItem & /** final and formatted "total" value used for displaying and sorting */ formattedTotal: number; + /** final and formatted "merchant" value used for displaying and sorting */ + formattedMerchant: string; + /** final "date" value used for sorting */ date: string; diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts index 1306368f195c..2b814190747e 100644 --- a/src/libs/SearchUtils.ts +++ b/src/libs/SearchUtils.ts @@ -16,7 +16,7 @@ const columnNamesToSortingProperty = { [CONST.SEARCH_TABLE_COLUMNS.FROM]: 'formattedFrom' as const, [CONST.SEARCH_TABLE_COLUMNS.DATE]: 'date' as const, [CONST.SEARCH_TABLE_COLUMNS.TAG]: 'tag' as const, - [CONST.SEARCH_TABLE_COLUMNS.MERCHANT]: 'merchant' as const, + [CONST.SEARCH_TABLE_COLUMNS.MERCHANT]: 'formattedMerchant' as const, [CONST.SEARCH_TABLE_COLUMNS.TOTAL]: 'formattedTotal' as const, [CONST.SEARCH_TABLE_COLUMNS.CATEGORY]: 'category' as const, [CONST.SEARCH_TABLE_COLUMNS.TYPE]: 'type' as const, @@ -61,9 +61,11 @@ function getTransactionsSections(data: OnyxTypes.SearchResults['data']): Transac : (data.personalDetailsList?.[transactionItem.managerID] as SearchAccountDetails); const formattedFrom = from.displayName ?? from.login ?? ''; - const formattedTo = to.name ?? to.displayName ?? to.login ?? ''; + const formattedTo = to?.name ?? to?.displayName ?? to?.login ?? ''; const formattedTotal = TransactionUtils.getAmount(transactionItem, isExpenseReport); const date = transactionItem?.modifiedCreated ? transactionItem.modifiedCreated : transactionItem?.created; + const merchant = TransactionUtils.getMerchant(transactionItem); + const formattedMerchant = merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT || merchant === CONST.TRANSACTION.DEFAULT_MERCHANT ? '' : merchant; return { ...transactionItem, @@ -73,6 +75,7 @@ function getTransactionsSections(data: OnyxTypes.SearchResults['data']): Transac formattedTo, date, formattedTotal, + formattedMerchant, shouldShowMerchant, shouldShowCategory, shouldShowTag, @@ -117,7 +120,7 @@ function getSortedData(data: TransactionListItemType[], sortBy?: SearchColumnTyp const aValue = a[sortingProperty]; const bValue = b[sortingProperty]; - if (!aValue || !bValue) { + if (aValue === undefined || bValue === undefined) { return 0; } From 2fd1c571b83eb478f589d0ee2d9ba7082d5d2632 Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Wed, 29 May 2024 16:12:58 +0200 Subject: [PATCH 12/12] Fix receipt header missing after merge conflicts --- .../SelectionList/SearchTableHeader.tsx | 16 ++++++++-------- src/libs/SearchUtils.ts | 1 - 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/components/SelectionList/SearchTableHeader.tsx b/src/components/SelectionList/SearchTableHeader.tsx index 9d2a5cd6d707..ab930936ec9f 100644 --- a/src/components/SelectionList/SearchTableHeader.tsx +++ b/src/components/SelectionList/SearchTableHeader.tsx @@ -19,6 +19,12 @@ type SearchColumnConfig = { }; const SearchColumns: SearchColumnConfig[] = [ + { + columnName: CONST.SEARCH_TABLE_COLUMNS.RECEIPT, + translationKey: 'common.receipt', + shouldShow: () => true, + isSortable: false, + }, { columnName: CONST.SEARCH_TABLE_COLUMNS.DATE, translationKey: 'common.date', @@ -94,24 +100,18 @@ function SearchTableHeader({data, sortBy, sortOrder, onSortPress}: SearchTableHe return; } - // Todo add textStyle - // - return ( - {SearchColumns.map(({columnName, translationKey, shouldShow, isSortable}) => { const isActive = sortBy === columnName; + const textStyle = columnName === CONST.SEARCH_TABLE_COLUMNS.RECEIPT ? StyleUtils.getTextOverflowStyle('clip') : null; return ( ; type SearchColumnType = ValueOf;