From 8ebee6ae2fe89e8745225b0e68ff6546b5f82243 Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Tue, 23 Apr 2024 17:50:15 +0200 Subject: [PATCH 1/4] Add initial version of SearchWidget --- src/ONYXKEYS.ts | 4 + src/components/Search.tsx | 116 ++++++++++-------- .../TemporaryExpenseListItem.tsx | 16 +++ src/libs/API/types.ts | 1 + src/libs/SearchUtils.ts | 24 ++++ src/libs/actions/Search.ts | 26 ++++ src/pages/Search/SearchPage.tsx | 3 +- src/stories/Search.stories.tsx | 43 ------- src/types/onyx/SearchResults.ts | 40 ++++++ src/types/onyx/index.ts | 2 + 10 files changed, 178 insertions(+), 97 deletions(-) create mode 100644 src/components/SelectionList/TemporaryExpenseListItem.tsx create mode 100644 src/libs/SearchUtils.ts create mode 100644 src/libs/actions/Search.ts delete mode 100644 src/stories/Search.stories.tsx create mode 100644 src/types/onyx/SearchResults.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 5a765c93ca03..6f8e1c21284d 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -356,6 +356,9 @@ const ONYXKEYS = { /** This is deprecated, but needed for a migration, so we still need to include it here so that it will be initialized in Onyx.init */ DEPRECATED_POLICY_MEMBER_LIST: 'policyMemberList_', + + // Search Page related + SEARCH: 'search_' }, /** List of Form ids */ @@ -560,6 +563,7 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.NEXT_STEP]: OnyxTypes.ReportNextStep; [ONYXKEYS.COLLECTION.POLICY_JOIN_MEMBER]: OnyxTypes.PolicyJoinMember; [ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS]: OnyxTypes.PolicyConnectionSyncProgress; + [ONYXKEYS.COLLECTION.SEARCH]: OnyxTypes.SearchResults; }; type OnyxValuesMapping = { diff --git a/src/components/Search.tsx b/src/components/Search.tsx index 38135fd2631e..37d712957fab 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -1,66 +1,76 @@ -import React from 'react'; -import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native'; +import React, {useEffect} from 'react'; import {View} from 'react-native'; -import useLocalize from '@hooks/useLocalize'; -import useTheme from '@hooks/useTheme'; -import useThemeStyles from '@hooks/useThemeStyles'; -import variables from '@styles/variables'; -import CONST from '@src/CONST'; -import Icon from './Icon'; -import * as Expensicons from './Icon/Expensicons'; -import {PressableWithFeedback} from './Pressable'; +import {useOnyx} from 'react-native-onyx'; +import useNetwork from '@hooks/useNetwork'; +import * as SearchActions from '@libs/actions/Search'; +import * as SearchUtils from '@libs/SearchUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import Text from './Text'; -import Tooltip from './Tooltip'; -type SearchProps = { - // Callback fired when component is pressed - onPress: (event?: GestureResponderEvent | KeyboardEvent) => void; +/** + * For testing run this code in browser console to insert fake data: + * + * Onyx.set(`${ONYXKEYS.COLLECTION.SEARCH}${query}`, { + * search: { + * offset: 0, + * type: 'transaction', + * hasMoreResults: false, + * }, + * data: { + * transactions_1234: { + * receipt: {source: 'http...'}, + * hasEReceipt: false, + * created: '2024-04-11 00:00:00', + * amount: 12500, + * type: 'cash', + * reportID: '1', + * transactionThreadReportID: '2', + * transactionID: '1234', + * }, + * transactions_5555: { + * receipt: {source: 'http...'}, + * hasEReceipt: false, + * created: '2024-04-11 00:00:00', + * amount: 12500, + * type: 'cash', // not present in live data (data outside of snapshot_) + * reportID: '1', + * transactionThreadReportID: '2', + * transactionID: '5555', + * }, + * }, + * }) + */ - // Text explaining what the user can search for - placeholder?: string; +type SearchProps = { + query: string; +}; - // Text showing up in a tooltip when component is hovered - tooltip?: string; +function Search({query}: SearchProps) { + const {isOffline} = useNetwork(); + const [searchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SEARCH}${query}`); - // Styles to apply on the outer element - style?: StyleProp; + useEffect(() => { + SearchActions.search(query); + }, [query]); - /** Styles to apply to the outermost element */ - containerStyle?: StyleProp; -}; + const isLoading = !isOffline && searchResults === undefined; + const shouldShowEmptyState = isEmptyObject(searchResults); + const shouldShowResults = !isEmptyObject(searchResults); -function Search({onPress, placeholder, tooltip, style, containerStyle}: SearchProps) { - const styles = useThemeStyles(); - const theme = useTheme(); - const {translate} = useLocalize(); + const ListItem = SearchUtils.getListItem(); return ( - - - - {({hovered}) => ( - - - - {placeholder ?? translate('common.searchWithThreeDots')} - - - )} - - + + {isLoading && Loading data...} + {shouldShowEmptyState && Empty skeleton goes here} + {shouldShowResults && + SearchUtils.getTransactionsSections(searchResults?.data).map((item) => ( + + ))} ); } diff --git a/src/components/SelectionList/TemporaryExpenseListItem.tsx b/src/components/SelectionList/TemporaryExpenseListItem.tsx new file mode 100644 index 000000000000..dcfeb7c05542 --- /dev/null +++ b/src/components/SelectionList/TemporaryExpenseListItem.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import {View} from 'react-native'; +import Text from '@components/Text'; +import type {SearchTransaction} from '@src/types/onyx/SearchResults'; + +// NOTE: This is a completely temporary mock item so that something can be displayed in SearchWidget +// This should be removed and implement properly in: https://github.com/Expensify/App/issues/39877 +function ExpenseListItem({item}: {item: SearchTransaction}) { + return ( + + Item: {item.transactionID} + + ); +} + +export default ExpenseListItem; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 7a40400b3826..5a4000ec6cbc 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -465,6 +465,7 @@ const READ_COMMANDS = { OPEN_POLICY_DISTANCE_RATES_PAGE: 'OpenPolicyDistanceRatesPage', OPEN_POLICY_MORE_FEATURES_PAGE: 'OpenPolicyMoreFeaturesPage', OPEN_POLICY_ACCOUNTING_PAGE: 'OpenPolicyAccountingPage', + SEARCH: 'Search' } as const; type ReadCommand = ValueOf; diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts new file mode 100644 index 000000000000..6d2b427feaa9 --- /dev/null +++ b/src/libs/SearchUtils.ts @@ -0,0 +1,24 @@ +import type React from 'react'; +import ExpenseListItem from '@components/SelectionList/TemporaryExpenseListItem'; +import type * as OnyxTypes from '@src/types/onyx'; + +const searchTypeToItemMap = { + transaction: { + listItem: ExpenseListItem, + }, +}; + +const getTransactionsSections = (data: OnyxTypes.SearchResults['data']) => + Object.entries(data) + .filter(([key]) => key.startsWith('transactions_')) + .map(([, value]) => value); + +/** + * TODO: in future make this function generic and return specific item component based on type + * For now only 1 search item type exists in the app so this function is simplified + */ +function getListItem(type?: string): typeof ExpenseListItem { + return searchTypeToItemMap.transaction.listItem; +} + +export {getTransactionsSections, getListItem}; diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts new file mode 100644 index 000000000000..de0114f49552 --- /dev/null +++ b/src/libs/actions/Search.ts @@ -0,0 +1,26 @@ +import Onyx from 'react-native-onyx'; +import * as API from '@libs/API'; +import * as UserUtils from '@libs/UserUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; + +let isNetworkOffline = false; +Onyx.connect({ + key: ONYXKEYS.NETWORK, + callback: (value) => { + isNetworkOffline = value?.isOffline ?? false; + }, +}); + +function search(query: string) { + if (isNetworkOffline) { + return; + } + + const hash = UserUtils.hashText(query, 2 ** 32); + API.read('Search', {query, hash}); +} + +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 634dbe0572ec..7a2b905fc1bb 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -1,9 +1,9 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React from 'react'; import ScreenWrapper from '@components/ScreenWrapper'; +import Search from '@components/Search'; import type {CentralPaneNavigatorParamList} from '@libs/Navigation/types'; import type SCREENS from '@src/SCREENS'; -// import EmptySearchView from './EmptySearchView'; import SearchResults from './SearchResults'; import useCustomBackHandler from './useCustomBackHandler'; @@ -15,6 +15,7 @@ function SearchPage({route}: SearchPageProps) { return ( + {/* */} ); diff --git a/src/stories/Search.stories.tsx b/src/stories/Search.stories.tsx deleted file mode 100644 index 58c3eb2561a2..000000000000 --- a/src/stories/Search.stories.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react'; -import type {SearchProps} from '@components/Search'; -import Search from '@components/Search'; - -/** - * We use the Component Story Format for writing stories. Follow the docs here: - * - * https://storybook.js.org/docs/react/writing-stories/introduction#component-story-format - */ -const story = { - title: 'Components/Search', - component: Search, -}; - -type StoryType = typeof Template & {args?: Partial}; - -function Template(args: SearchProps) { - // eslint-disable-next-line react/jsx-props-no-spreading - return ; -} - -// Arguments can be passed to the component by binding -// See: https://storybook.js.org/docs/react/writing-stories/introduction#using-args -const Default: StoryType = Template.bind({}); -Default.args = { - onPress: () => alert('Pressed'), -}; - -const CustomPlaceholderAndTooltip: StoryType = Template.bind({}); -CustomPlaceholderAndTooltip.args = { - placeholder: 'Search for a specific thing...', - tooltip: 'Custom tooltip text', - onPress: () => alert('This component has custom placeholder text. Also custom tooltip text when hovered.'), -}; - -const CustomBackground: StoryType = Template.bind({}); -CustomBackground.args = { - onPress: () => alert('This component has custom styles applied'), - style: {backgroundColor: 'darkgreen'}, -}; - -export default story; -export {Default, CustomPlaceholderAndTooltip, CustomBackground}; diff --git a/src/types/onyx/SearchResults.ts b/src/types/onyx/SearchResults.ts new file mode 100644 index 000000000000..3e8fdd191ee2 --- /dev/null +++ b/src/types/onyx/SearchResults.ts @@ -0,0 +1,40 @@ +import type {Receipt} from './Transaction'; + +type SearchResultsInfo = { + offset: number; + type: string; + hasMoreResults: boolean; +}; + +type SearchTransaction = { + transactionID: string; + parentTransactionID?: string; + receipt?: Receipt; + hasEReceipt?: boolean; + created: string; + merchant: string; + modifiedCreated?: string; + modifiedMerchant?: string; + description: string; + from: {displayName: string; avatarURL: string}; + to: {displayName: string; avatarURL: string}; + amount: number; + modifiedAmount?: number; + category?: string; + tag?: string; + type: string; + hasViolation: boolean; + taxAmount?: number; + reportID: string; + transactionThreadReportID: string; // Not present in live transactions_ + action: string; +}; + +type SearchResults = { + search: SearchResultsInfo; + data: Record; +}; + +export default SearchResults; + +export type {SearchTransaction}; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index ea0870a7b8c6..2b4cf4c87584 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -80,6 +80,7 @@ import type WalletStatement from './WalletStatement'; import type WalletTerms from './WalletTerms'; import type WalletTransfer from './WalletTransfer'; import type WorkspaceRateAndUnit from './WorkspaceRateAndUnit'; +import type SearchResults from './SearchResults' export type { Account, @@ -177,4 +178,5 @@ export type { Log, PolicyJoinMember, CapturedLogs, + SearchResults }; From 9980f663aebb3163eabaf1c81bb28d441210801a Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Wed, 24 Apr 2024 15:55:38 +0200 Subject: [PATCH 2/4] Add SearchWidget handling basic search data logic --- src/components/Search.tsx | 39 ++++++++++++++++------------ src/languages/en.ts | 6 +++++ src/languages/es.ts | 6 +++++ src/libs/SearchUtils.ts | 3 +-- src/pages/Search/EmptySearchView.tsx | 19 ++++++++++++-- src/pages/Search/SearchPage.tsx | 3 --- src/pages/Search/SearchResults.tsx | 17 ------------ 7 files changed, 52 insertions(+), 41 deletions(-) delete mode 100644 src/pages/Search/SearchResults.tsx diff --git a/src/components/Search.tsx b/src/components/Search.tsx index 37d712957fab..d656e9f02174 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -1,17 +1,17 @@ import React, {useEffect} from 'react'; -import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import useNetwork from '@hooks/useNetwork'; import * as SearchActions from '@libs/actions/Search'; import * as SearchUtils from '@libs/SearchUtils'; +import EmptySearchView from '@pages/Search/EmptySearchView'; import ONYXKEYS from '@src/ONYXKEYS'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import Text from './Text'; +import TableListItemSkeleton from './Skeletons/TableListItemSkeleton'; /** - * For testing run this code in browser console to insert fake data: - * - * Onyx.set(`${ONYXKEYS.COLLECTION.SEARCH}${query}`, { + * For testing purposes run this code in browser console to insert fake data: + * query is the param from URL, by default it will be "all" + * Onyx.set(`search_${query}`, { * search: { * offset: 0, * type: 'transaction', @@ -58,20 +58,25 @@ function Search({query}: SearchProps) { const shouldShowEmptyState = isEmptyObject(searchResults); const shouldShowResults = !isEmptyObject(searchResults); - const ListItem = SearchUtils.getListItem(); + let resultsToDisplay = null; + + if (shouldShowResults) { + const ListItem = SearchUtils.getListItem(); + + resultsToDisplay = SearchUtils.getTransactionsSections(searchResults.data).map((item) => ( + + )); + } return ( - - {isLoading && Loading data...} - {shouldShowEmptyState && Empty skeleton goes here} - {shouldShowResults && - SearchUtils.getTransactionsSections(searchResults?.data).map((item) => ( - - ))} - + <> + {isLoading && } + {shouldShowEmptyState && } + {shouldShowResults && resultsToDisplay} + ); } diff --git a/src/languages/en.ts b/src/languages/en.ts index 9738be63911a..4bf45a608b48 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2452,6 +2452,12 @@ export default { }, search: { resultsAreLimited: 'Search results are limited.', + searchResults: { + emptyResults:{ + title: 'Nothing to show', + subtitle: 'Try creating something using the green + button.', + } + } }, genericErrorPage: { title: 'Uh-oh, something went wrong!', diff --git a/src/languages/es.ts b/src/languages/es.ts index 94b1ab030fb4..406313631049 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2483,6 +2483,12 @@ export default { }, search: { resultsAreLimited: 'Los resultados de búsqueda están limitados.', + searchResults: { + emptyResults: { + title: 'No hay nada que ver aquí', + subtitle: 'Por favor intenta crear algo usando el botón verde.', + }, + }, }, genericErrorPage: { title: '¡Oh-oh, algo salió mal!', diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts index 6d2b427feaa9..3d9ebeea5d50 100644 --- a/src/libs/SearchUtils.ts +++ b/src/libs/SearchUtils.ts @@ -1,4 +1,3 @@ -import type React from 'react'; import ExpenseListItem from '@components/SelectionList/TemporaryExpenseListItem'; import type * as OnyxTypes from '@src/types/onyx'; @@ -17,7 +16,7 @@ const getTransactionsSections = (data: OnyxTypes.SearchResults['data']) => * TODO: in future make this function generic and return specific item component based on type * For now only 1 search item type exists in the app so this function is simplified */ -function getListItem(type?: string): typeof ExpenseListItem { +function getListItem(): typeof ExpenseListItem { return searchTypeToItemMap.transaction.listItem; } diff --git a/src/pages/Search/EmptySearchView.tsx b/src/pages/Search/EmptySearchView.tsx index 7109b1faa1d4..e4e7d6f8eb1b 100644 --- a/src/pages/Search/EmptySearchView.tsx +++ b/src/pages/Search/EmptySearchView.tsx @@ -1,8 +1,23 @@ import React from 'react'; -import TableListItemSkeleton from '@components/Skeletons/TableListItemSkeleton'; +import {View} from 'react-native'; +import * as Illustrations from '@components/Icon/Illustrations'; +import WorkspaceEmptyStateSection from '@components/WorkspaceEmptyStateSection'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; function EmptySearchView() { - return ; + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + return ( + + + + ); } EmptySearchView.displayName = 'EmptySearchView'; diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 7a2b905fc1bb..8facbbba9446 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -4,7 +4,6 @@ import ScreenWrapper from '@components/ScreenWrapper'; import Search from '@components/Search'; import type {CentralPaneNavigatorParamList} from '@libs/Navigation/types'; import type SCREENS from '@src/SCREENS'; -import SearchResults from './SearchResults'; import useCustomBackHandler from './useCustomBackHandler'; type SearchPageProps = StackScreenProps; @@ -14,9 +13,7 @@ function SearchPage({route}: SearchPageProps) { return ( - - {/* */} ); } diff --git a/src/pages/Search/SearchResults.tsx b/src/pages/Search/SearchResults.tsx deleted file mode 100644 index 261484bc13ec..000000000000 --- a/src/pages/Search/SearchResults.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; -import Text from '@components/Text'; -import useThemeStyles from '@hooks/useThemeStyles'; - -type SearchResultsProps = { - query: string; -}; - -function SearchResults({query}: SearchResultsProps) { - const styles = useThemeStyles(); - - return Search results for: |{query}| filter; -} - -SearchResults.displayName = 'SearchResults'; - -export default SearchResults; From bb4195b29f1217baa617b0c4a49779e5b71b8a40 Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Thu, 25 Apr 2024 15:22:49 +0200 Subject: [PATCH 3/4] Improve SearchWidget layout and behavior --- src/components/Search.tsx | 101 +++++++++--------- .../TemporaryExpenseListItem.tsx | 4 +- src/libs/SearchUtils.ts | 3 +- src/libs/actions/Search.ts | 3 +- src/pages/Search/EmptySearchView.tsx | 15 +-- src/pages/Search/SearchFilters.tsx | 18 ++-- src/pages/Search/SearchFiltersNarrow.tsx | 11 +- src/pages/Search/SearchPageBottomTab.tsx | 15 ++- 8 files changed, 83 insertions(+), 87 deletions(-) diff --git a/src/components/Search.tsx b/src/components/Search.tsx index d656e9f02174..611c1498e264 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -6,41 +6,40 @@ import * as SearchUtils from '@libs/SearchUtils'; import EmptySearchView from '@pages/Search/EmptySearchView'; import ONYXKEYS from '@src/ONYXKEYS'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; import TableListItemSkeleton from './Skeletons/TableListItemSkeleton'; -/** - * For testing purposes run this code in browser console to insert fake data: - * query is the param from URL, by default it will be "all" - * Onyx.set(`search_${query}`, { - * search: { - * offset: 0, - * type: 'transaction', - * hasMoreResults: false, - * }, - * data: { - * transactions_1234: { - * receipt: {source: 'http...'}, - * hasEReceipt: false, - * created: '2024-04-11 00:00:00', - * amount: 12500, - * type: 'cash', - * reportID: '1', - * transactionThreadReportID: '2', - * transactionID: '1234', - * }, - * transactions_5555: { - * receipt: {source: 'http...'}, - * hasEReceipt: false, - * created: '2024-04-11 00:00:00', - * amount: 12500, - * type: 'cash', // not present in live data (data outside of snapshot_) - * reportID: '1', - * transactionThreadReportID: '2', - * transactionID: '5555', - * }, - * }, - * }) - */ +// For testing purposes run this code in browser console to insert fake data: +// query is the param from URL, by default it will be "all" +// Onyx.set(`search_${query}`, { +// search: { +// offset: 0, +// type: 'transaction', +// hasMoreResults: false, +// }, +// data: { +// transactions_1234: { +// receipt: {source: 'http...'}, +// hasEReceipt: false, +// created: '2024-04-11 00:00:00', +// amount: 12500, +// type: 'cash', +// reportID: '1', +// transactionThreadReportID: '2', +// transactionID: '1234', +// }, +// transactions_5555: { +// receipt: {source: 'http...'}, +// hasEReceipt: false, +// created: '2024-04-11 00:00:00', +// amount: 12500, +// type: 'cash', // not present in live data (data outside of snapshot_) +// reportID: '1', +// transactionThreadReportID: '2', +// transactionID: '5555', +// }, +// }, +// }) type SearchProps = { query: string; @@ -48,36 +47,32 @@ type SearchProps = { function Search({query}: SearchProps) { const {isOffline} = useNetwork(); - const [searchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SEARCH}${query}`); + const [searchResults, searchResultsMeta] = useOnyx(`${ONYXKEYS.COLLECTION.SEARCH}${query}`); useEffect(() => { SearchActions.search(query); }, [query]); - const isLoading = !isOffline && searchResults === undefined; + const isLoading = !isOffline && isLoadingOnyxValue(searchResultsMeta); const shouldShowEmptyState = isEmptyObject(searchResults); - const shouldShowResults = !isEmptyObject(searchResults); - let resultsToDisplay = null; - - if (shouldShowResults) { - const ListItem = SearchUtils.getListItem(); + if (isLoading) { + return ; + } - resultsToDisplay = SearchUtils.getTransactionsSections(searchResults.data).map((item) => ( - - )); + if (shouldShowEmptyState) { + return ; } - return ( - <> - {isLoading && } - {shouldShowEmptyState && } - {shouldShowResults && resultsToDisplay} - - ); + const ListItem = SearchUtils.getListItem(); + + // This will be updated with the proper List component in another PR + return SearchUtils.getTransactionsSections(searchResults.data).map((item) => ( + + )); } Search.displayName = 'Search'; diff --git a/src/components/SelectionList/TemporaryExpenseListItem.tsx b/src/components/SelectionList/TemporaryExpenseListItem.tsx index dcfeb7c05542..011a6d73ac4f 100644 --- a/src/components/SelectionList/TemporaryExpenseListItem.tsx +++ b/src/components/SelectionList/TemporaryExpenseListItem.tsx @@ -1,13 +1,15 @@ import React from 'react'; import {View} from 'react-native'; import Text from '@components/Text'; +import useThemeStyles from '@hooks/useThemeStyles'; import type {SearchTransaction} from '@src/types/onyx/SearchResults'; // NOTE: This is a completely temporary mock item so that something can be displayed in SearchWidget // This should be removed and implement properly in: https://github.com/Expensify/App/issues/39877 function ExpenseListItem({item}: {item: SearchTransaction}) { + const styles = useThemeStyles(); return ( - + Item: {item.transactionID} ); diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts index 3d9ebeea5d50..ccced12a0bc4 100644 --- a/src/libs/SearchUtils.ts +++ b/src/libs/SearchUtils.ts @@ -1,5 +1,6 @@ import ExpenseListItem from '@components/SelectionList/TemporaryExpenseListItem'; import type * as OnyxTypes from '@src/types/onyx'; +import type {SearchTransaction} from '@src/types/onyx/SearchResults'; const searchTypeToItemMap = { transaction: { @@ -7,7 +8,7 @@ const searchTypeToItemMap = { }, }; -const getTransactionsSections = (data: OnyxTypes.SearchResults['data']) => +const getTransactionsSections = (data: OnyxTypes.SearchResults['data']): SearchTransaction[] => Object.entries(data) .filter(([key]) => key.startsWith('transactions_')) .map(([, value]) => value); diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index de0114f49552..0165c5dccb34 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -2,6 +2,7 @@ import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; import * as UserUtils from '@libs/UserUtils'; import ONYXKEYS from '@src/ONYXKEYS'; +import {READ_COMMANDS} from '@libs/API/types'; let isNetworkOffline = false; Onyx.connect({ @@ -17,7 +18,7 @@ function search(query: string) { } const hash = UserUtils.hashText(query, 2 ** 32); - API.read('Search', {query, hash}); + API.read(READ_COMMANDS.SEARCH, {query, hash}); } export { diff --git a/src/pages/Search/EmptySearchView.tsx b/src/pages/Search/EmptySearchView.tsx index e4e7d6f8eb1b..bfeb46f06298 100644 --- a/src/pages/Search/EmptySearchView.tsx +++ b/src/pages/Search/EmptySearchView.tsx @@ -1,22 +1,17 @@ import React from 'react'; -import {View} from 'react-native'; import * as Illustrations from '@components/Icon/Illustrations'; import WorkspaceEmptyStateSection from '@components/WorkspaceEmptyStateSection'; import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; function EmptySearchView() { - const styles = useThemeStyles(); const {translate} = useLocalize(); return ( - - - + ); } diff --git a/src/pages/Search/SearchFilters.tsx b/src/pages/Search/SearchFilters.tsx index 46dfd05416ec..0ce2f958043c 100644 --- a/src/pages/Search/SearchFilters.tsx +++ b/src/pages/Search/SearchFilters.tsx @@ -1,7 +1,6 @@ import React from 'react'; import {View} from 'react-native'; import MenuItem from '@components/MenuItem'; -import useActiveRoute from '@hooks/useActiveRoute'; import useLocalize from '@hooks/useLocalize'; import useSingleExecution from '@hooks/useSingleExecution'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -14,17 +13,20 @@ import ROUTES from '@src/ROUTES'; import type IconAsset from '@src/types/utils/IconAsset'; import SearchFiltersNarrow from './SearchFiltersNarrow'; +type SearchFiltersProps = { + query: string; +}; + type SearchMenuFilterItem = { title: string; icon: IconAsset; route: Route; }; -function SearchFilters() { +function SearchFilters({query}: SearchFiltersProps) { const styles = useThemeStyles(); - const {singleExecution} = useSingleExecution(); - const activeRoute = useActiveRoute(); const {isSmallScreenWidth} = useWindowDimensions(); + const {singleExecution} = useSingleExecution(); const {translate} = useLocalize(); const filterItems: SearchMenuFilterItem[] = [ @@ -35,15 +37,11 @@ function SearchFilters() { }, ]; - const currentQuery = activeRoute?.params && 'query' in activeRoute.params ? activeRoute?.params?.query : ''; - if (isSmallScreenWidth) { - const activeItemLabel = String(currentQuery); - return ( ); } @@ -51,7 +49,7 @@ function SearchFilters() { return ( {filterItems.map((item) => { - const isActive = item.title.toLowerCase() === currentQuery; + const isActive = item.title.toLowerCase() === query; const onPress = singleExecution(() => Navigation.navigate(item.route)); return ( diff --git a/src/pages/Search/SearchFiltersNarrow.tsx b/src/pages/Search/SearchFiltersNarrow.tsx index 935c911fafff..01e750a9a2ce 100644 --- a/src/pages/Search/SearchFiltersNarrow.tsx +++ b/src/pages/Search/SearchFiltersNarrow.tsx @@ -1,5 +1,5 @@ import React, {useRef, useState} from 'react'; -import {Animated, StyleSheet, View} from 'react-native'; +import {Animated, View} from 'react-native'; import Icon from '@components/Icon'; import PopoverMenu from '@components/PopoverMenu'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; @@ -42,17 +42,16 @@ function SearchFiltersNarrow({filterItems, activeItemLabel}: SearchFiltersNarrow })); return ( - <> + {({hovered}) => ( - + - + ); } diff --git a/src/pages/Search/SearchPageBottomTab.tsx b/src/pages/Search/SearchPageBottomTab.tsx index 4615b805d570..169485f44001 100644 --- a/src/pages/Search/SearchPageBottomTab.tsx +++ b/src/pages/Search/SearchPageBottomTab.tsx @@ -1,16 +1,22 @@ import React from 'react'; import ScreenWrapper from '@components/ScreenWrapper'; +import Search from '@components/Search'; +import useActiveRoute from '@hooks/useActiveRoute'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import TopBar from '@navigation/AppNavigator/createCustomBottomTabNavigator/TopBar'; import SearchFilters from './SearchFilters'; -// import EmptySearchView from './EmptySearchView'; - function SearchPageBottomTab() { const {translate} = useLocalize(); + const {isSmallScreenWidth} = useWindowDimensions(); + const activeRoute = useActiveRoute(); const styles = useThemeStyles(); + const currentQuery = activeRoute?.params && 'query' in activeRoute.params ? activeRoute?.params?.query : ''; + const query = String(currentQuery); + return ( - - {/* */} - {/* Search results list goes here */} + + {isSmallScreenWidth && } ); } From 6665e29bd15a917e12d4ac549fc229d4fd2e3747 Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Fri, 26 Apr 2024 11:33:48 +0200 Subject: [PATCH 4/4] Add fixes to Search widget after review --- src/ONYXKEYS.ts | 4 +- src/components/Search.tsx | 37 ++----------------- ...m.tsx => TemporaryTransactionListItem.tsx} | 4 +- src/languages/en.ts | 6 +-- src/libs/API/parameters/Search.ts | 6 +++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 3 +- src/libs/SearchUtils.ts | 30 ++++++++++----- src/libs/actions/Search.ts | 6 +-- src/types/onyx/index.ts | 4 +- 10 files changed, 45 insertions(+), 56 deletions(-) rename src/components/SelectionList/{TemporaryExpenseListItem.tsx => TemporaryTransactionListItem.tsx} (85%) create mode 100644 src/libs/API/parameters/Search.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 6f8e1c21284d..238ee2cec0b2 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -358,7 +358,7 @@ const ONYXKEYS = { DEPRECATED_POLICY_MEMBER_LIST: 'policyMemberList_', // Search Page related - SEARCH: 'search_' + SNAPSHOT: 'snapshot_', }, /** List of Form ids */ @@ -563,7 +563,7 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.NEXT_STEP]: OnyxTypes.ReportNextStep; [ONYXKEYS.COLLECTION.POLICY_JOIN_MEMBER]: OnyxTypes.PolicyJoinMember; [ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS]: OnyxTypes.PolicyConnectionSyncProgress; - [ONYXKEYS.COLLECTION.SEARCH]: OnyxTypes.SearchResults; + [ONYXKEYS.COLLECTION.SNAPSHOT]: OnyxTypes.SearchResults; }; type OnyxValuesMapping = { diff --git a/src/components/Search.tsx b/src/components/Search.tsx index 611c1498e264..0fb0db1924a2 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -9,45 +9,14 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; import TableListItemSkeleton from './Skeletons/TableListItemSkeleton'; -// For testing purposes run this code in browser console to insert fake data: -// query is the param from URL, by default it will be "all" -// Onyx.set(`search_${query}`, { -// search: { -// offset: 0, -// type: 'transaction', -// hasMoreResults: false, -// }, -// data: { -// transactions_1234: { -// receipt: {source: 'http...'}, -// hasEReceipt: false, -// created: '2024-04-11 00:00:00', -// amount: 12500, -// type: 'cash', -// reportID: '1', -// transactionThreadReportID: '2', -// transactionID: '1234', -// }, -// transactions_5555: { -// receipt: {source: 'http...'}, -// hasEReceipt: false, -// created: '2024-04-11 00:00:00', -// amount: 12500, -// type: 'cash', // not present in live data (data outside of snapshot_) -// reportID: '1', -// transactionThreadReportID: '2', -// transactionID: '5555', -// }, -// }, -// }) - type SearchProps = { query: string; }; function Search({query}: SearchProps) { const {isOffline} = useNetwork(); - const [searchResults, searchResultsMeta] = useOnyx(`${ONYXKEYS.COLLECTION.SEARCH}${query}`); + const hash = SearchUtils.getQueryHash(query); + const [searchResults, searchResultsMeta] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`); useEffect(() => { SearchActions.search(query); @@ -67,7 +36,7 @@ function Search({query}: SearchProps) { const ListItem = SearchUtils.getListItem(); // This will be updated with the proper List component in another PR - return SearchUtils.getTransactionsSections(searchResults.data).map((item) => ( + return SearchUtils.getSections(searchResults.data).map((item) => ( @@ -15,4 +15,4 @@ function ExpenseListItem({item}: {item: SearchTransaction}) { ); } -export default ExpenseListItem; +export default TransactionListItem; diff --git a/src/languages/en.ts b/src/languages/en.ts index 4bf45a608b48..e4ff69fc6859 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2453,11 +2453,11 @@ export default { search: { resultsAreLimited: 'Search results are limited.', searchResults: { - emptyResults:{ + emptyResults: { title: 'Nothing to show', subtitle: 'Try creating something using the green + button.', - } - } + }, + }, }, genericErrorPage: { title: 'Uh-oh, something went wrong!', diff --git a/src/libs/API/parameters/Search.ts b/src/libs/API/parameters/Search.ts new file mode 100644 index 000000000000..44e8c9f2d0fb --- /dev/null +++ b/src/libs/API/parameters/Search.ts @@ -0,0 +1,6 @@ +type SearchParams = { + query: string; + hash: number; +}; + +export default SearchParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 2d7948076548..71f6a5d5990a 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -215,3 +215,4 @@ export type {default as ShareTrackedExpenseParams} from './ShareTrackedExpensePa export type {default as CategorizeTrackedExpenseParams} from './CategorizeTrackedExpenseParams'; export type {default as LeavePolicyParams} from './LeavePolicyParams'; export type {default as OpenPolicyAccountingPageParams} from './OpenPolicyAccountingPageParams'; +export type {default as SearchParams} from './Search'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 5a4000ec6cbc..8bced2295475 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -465,7 +465,7 @@ const READ_COMMANDS = { OPEN_POLICY_DISTANCE_RATES_PAGE: 'OpenPolicyDistanceRatesPage', OPEN_POLICY_MORE_FEATURES_PAGE: 'OpenPolicyMoreFeaturesPage', OPEN_POLICY_ACCOUNTING_PAGE: 'OpenPolicyAccountingPage', - SEARCH: 'Search' + SEARCH: 'Search', } as const; type ReadCommand = ValueOf; @@ -509,6 +509,7 @@ type ReadCommandParameters = { [READ_COMMANDS.OPEN_POLICY_DISTANCE_RATES_PAGE]: Parameters.OpenPolicyDistanceRatesPageParams; [READ_COMMANDS.OPEN_POLICY_MORE_FEATURES_PAGE]: Parameters.OpenPolicyMoreFeaturesPageParams; [READ_COMMANDS.OPEN_POLICY_ACCOUNTING_PAGE]: Parameters.OpenPolicyAccountingPageParams; + [READ_COMMANDS.SEARCH]: Parameters.SearchParams; }; const SIDE_EFFECT_REQUEST_COMMANDS = { diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts index ccced12a0bc4..77185ccbac02 100644 --- a/src/libs/SearchUtils.ts +++ b/src/libs/SearchUtils.ts @@ -1,24 +1,36 @@ -import ExpenseListItem from '@components/SelectionList/TemporaryExpenseListItem'; +import TransactionListItem from '@components/SelectionList/TemporaryTransactionListItem'; +import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; import type {SearchTransaction} from '@src/types/onyx/SearchResults'; +import * as UserUtils from './UserUtils'; + +function getTransactionsSections(data: OnyxTypes.SearchResults['data']): SearchTransaction[] { + return Object.entries(data) + .filter(([key]) => key.startsWith(ONYXKEYS.COLLECTION.TRANSACTION)) + .map(([, value]) => value); +} const searchTypeToItemMap = { transaction: { - listItem: ExpenseListItem, + listItem: TransactionListItem, + getSections: getTransactionsSections, }, }; -const getTransactionsSections = (data: OnyxTypes.SearchResults['data']): SearchTransaction[] => - Object.entries(data) - .filter(([key]) => key.startsWith('transactions_')) - .map(([, value]) => value); - /** * TODO: in future make this function generic and return specific item component based on type * For now only 1 search item type exists in the app so this function is simplified */ -function getListItem(): typeof ExpenseListItem { +function getListItem(): typeof TransactionListItem { return searchTypeToItemMap.transaction.listItem; } -export {getTransactionsSections, getListItem}; +function getSections(data: OnyxTypes.SearchResults['data']): SearchTransaction[] { + return searchTypeToItemMap.transaction.getSections(data); +} + +function getQueryHash(query: string): number { + return UserUtils.hashText(query, 2 ** 32); +} + +export {getQueryHash, getListItem, getSections}; diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index 0165c5dccb34..4bb78d7c161d 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -1,8 +1,8 @@ import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; -import * as UserUtils from '@libs/UserUtils'; -import ONYXKEYS from '@src/ONYXKEYS'; import {READ_COMMANDS} from '@libs/API/types'; +import * as SearchUtils from '@libs/SearchUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; let isNetworkOffline = false; Onyx.connect({ @@ -17,7 +17,7 @@ function search(query: string) { return; } - const hash = UserUtils.hashText(query, 2 ** 32); + const hash = SearchUtils.getQueryHash(query); API.read(READ_COMMANDS.SEARCH, {query, hash}); } diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 2b4cf4c87584..1695daebace8 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -63,6 +63,7 @@ import type ReportUserIsTyping from './ReportUserIsTyping'; import type Request from './Request'; import type Response from './Response'; import type ScreenShareRequest from './ScreenShareRequest'; +import type SearchResults from './SearchResults'; import type SecurityGroup from './SecurityGroup'; import type SelectedTabRequest from './SelectedTabRequest'; import type Session from './Session'; @@ -80,7 +81,6 @@ import type WalletStatement from './WalletStatement'; import type WalletTerms from './WalletTerms'; import type WalletTransfer from './WalletTransfer'; import type WorkspaceRateAndUnit from './WorkspaceRateAndUnit'; -import type SearchResults from './SearchResults' export type { Account, @@ -178,5 +178,5 @@ export type { Log, PolicyJoinMember, CapturedLogs, - SearchResults + SearchResults, };