Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Search v1] Create Search widget #40903

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
SNAPSHOT: 'snapshot_',
},

/** List of Form ids */
Expand Down Expand Up @@ -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.SNAPSHOT]: OnyxTypes.SearchResults;
};

type OnyxValuesMapping = {
Expand Down
93 changes: 36 additions & 57 deletions src/components/Search.tsx
Original file line number Diff line number Diff line change
@@ -1,68 +1,47 @@
import React from 'react';
import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native';
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 Text from './Text';
import Tooltip from './Tooltip';
import React, {useEffect} from 'react';
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 isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
import TableListItemSkeleton from './Skeletons/TableListItemSkeleton';

type SearchProps = {
// Callback fired when component is pressed
onPress: (event?: GestureResponderEvent | KeyboardEvent) => void;
query: string;
};

// Text explaining what the user can search for
placeholder?: string;
function Search({query}: SearchProps) {
const {isOffline} = useNetwork();
const hash = SearchUtils.getQueryHash(query);
const [searchResults, searchResultsMeta] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`);

// Text showing up in a tooltip when component is hovered
tooltip?: string;
useEffect(() => {
SearchActions.search(query);
}, [query]);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I found it to be a bit weird that refreshing the page doesn't trigger a call to search, but that's something we could iron out later in a follow up if needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the search() gets called, but its the implementation of isNetworkOffline thats breaking it.
I did some logging and in the beginning the NETWORK sets isOffline to true for a split second, and that is when the useEffect triggers. Then a bit later isOffline is correctly set to false but by then effect has already run and no search call was made.

Not sure how to fix this, need some guidance from you. Is that the intended behavior of ONYXKEYS.NETWORK?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that's a blocker for now. Let's move on and address this as a follow up.

// Styles to apply on the outer element
style?: StyleProp<ViewStyle>;
const isLoading = !isOffline && isLoadingOnyxValue(searchResultsMeta);
const shouldShowEmptyState = isEmptyObject(searchResults);

/** Styles to apply to the outermost element */
containerStyle?: StyleProp<ViewStyle>;
};
if (isLoading) {
return <TableListItemSkeleton shouldAnimate />;
}

if (shouldShowEmptyState) {
return <EmptySearchView />;
}

function Search({onPress, placeholder, tooltip, style, containerStyle}: SearchProps) {
const styles = useThemeStyles();
const theme = useTheme();
const {translate} = useLocalize();
const ListItem = SearchUtils.getListItem();

return (
<View style={containerStyle}>
<Tooltip text={tooltip ?? translate('common.search')}>
<PressableWithFeedback
accessibilityLabel={tooltip ?? translate('common.search')}
role={CONST.ROLE.BUTTON}
onPress={onPress}
style={styles.searchPressable}
>
{({hovered}) => (
<View style={[styles.searchContainer, hovered && styles.searchContainerHovered, style]}>
<Icon
src={Expensicons.MagnifyingGlass}
width={variables.iconSizeSmall}
height={variables.iconSizeSmall}
fill={theme.icon}
/>
<Text
style={styles.searchInputStyle}
numberOfLines={1}
>
{placeholder ?? translate('common.searchWithThreeDots')}
</Text>
</View>
)}
</PressableWithFeedback>
</Tooltip>
</View>
);
// This will be updated with the proper List component in another PR
return SearchUtils.getSections(searchResults.data).map((item) => (
<ListItem
key={item.transactionID}
item={item}
/>
));
}

Search.displayName = 'Search';
Expand Down
18 changes: 18 additions & 0 deletions src/components/SelectionList/TemporaryTransactionListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
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 TransactionListItem({item}: {item: SearchTransaction}) {
const styles = useThemeStyles();
return (
<View style={[styles.pt8]}>
<Text>Item: {item.transactionID}</Text>
</View>
);
}

export default TransactionListItem;
6 changes: 6 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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!',
Expand Down
6 changes: 6 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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!',
Expand Down
6 changes: 6 additions & 0 deletions src/libs/API/parameters/Search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
type SearchParams = {
query: string;
hash: number;
};

export default SearchParams;
1 change: 1 addition & 0 deletions src/libs/API/parameters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
2 changes: 2 additions & 0 deletions src/libs/API/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof READ_COMMANDS>;
luacmartins marked this conversation as resolved.
Show resolved Hide resolved
Expand Down Expand Up @@ -508,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 = {
Expand Down
36 changes: 36 additions & 0 deletions src/libs/SearchUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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: TransactionListItem,
getSections: getTransactionsSections,
},
};

/**
* 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
*/
luacmartins marked this conversation as resolved.
Show resolved Hide resolved
function getListItem(): typeof TransactionListItem {
return searchTypeToItemMap.transaction.listItem;
}

luacmartins marked this conversation as resolved.
Show resolved Hide resolved
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};
27 changes: 27 additions & 0 deletions src/libs/actions/Search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Onyx from 'react-native-onyx';
import * as API from '@libs/API';
import {READ_COMMANDS} from '@libs/API/types';
import * as SearchUtils from '@libs/SearchUtils';
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 = SearchUtils.getQueryHash(query);
API.read(READ_COMMANDS.SEARCH, {query, hash});
}

export {
// eslint-disable-next-line import/prefer-default-export
search,
};
14 changes: 12 additions & 2 deletions src/pages/Search/EmptySearchView.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import React from 'react';
import TableListItemSkeleton from '@components/Skeletons/TableListItemSkeleton';
import * as Illustrations from '@components/Icon/Illustrations';
import WorkspaceEmptyStateSection from '@components/WorkspaceEmptyStateSection';
import useLocalize from '@hooks/useLocalize';

function EmptySearchView() {
return <TableListItemSkeleton shouldAnimate />;
const {translate} = useLocalize();

return (
<WorkspaceEmptyStateSection
icon={Illustrations.EmptyStateExpenses}
title={translate('search.searchResults.emptyResults.title')}
subtitle={translate('search.searchResults.emptyResults.subtitle')}
/>
);
}

EmptySearchView.displayName = 'EmptySearchView';
Expand Down
18 changes: 8 additions & 10 deletions src/pages/Search/SearchFilters.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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[] = [
Expand All @@ -35,23 +37,19 @@ function SearchFilters() {
},
];

const currentQuery = activeRoute?.params && 'query' in activeRoute.params ? activeRoute?.params?.query : '';

if (isSmallScreenWidth) {
const activeItemLabel = String(currentQuery);

return (
<SearchFiltersNarrow
filterItems={filterItems}
activeItemLabel={activeItemLabel}
activeItemLabel={String(query)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
activeItemLabel={String(query)}
activeItemLabel={query}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not block on this. We can address this in a follow up @Kicu @WojtekBoman

/>
);
}

return (
<View style={[styles.pb4, styles.mh3, styles.mt3]}>
{filterItems.map((item) => {
const isActive = item.title.toLowerCase() === currentQuery;
const isActive = item.title.toLowerCase() === query;
const onPress = singleExecution(() => Navigation.navigate(item.route));

return (
Expand Down
11 changes: 5 additions & 6 deletions src/pages/Search/SearchFiltersNarrow.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -42,17 +42,16 @@ function SearchFiltersNarrow({filterItems, activeItemLabel}: SearchFiltersNarrow
}));

return (
<>
<View style={[styles.pb4]}>
<PressableWithFeedback
accessible
accessibilityLabel={popoverMenuItems[activeItemIndex]?.text ?? ''}
style={[styles.tabSelectorButton]}
wrapperStyle={[styles.flex1]}
ref={buttonRef}
style={[styles.tabSelectorButton]}
onPress={openMenu}
>
{({hovered}) => (
<Animated.View style={[styles.tabSelectorButton, StyleSheet.absoluteFill, styles.tabBackground(hovered, true, theme.border), styles.mh3]}>
<Animated.View style={[styles.tabSelectorButton, styles.tabBackground(hovered, true, theme.border), styles.w100, styles.mh3]}>
<View style={[styles.flexRow]}>
<Icon
src={popoverMenuItems[activeItemIndex]?.icon ?? Expensicons.All}
Expand All @@ -75,7 +74,7 @@ function SearchFiltersNarrow({filterItems, activeItemLabel}: SearchFiltersNarrow
onItemSelected={closeMenu}
anchorRef={buttonRef}
/>
</>
</View>
);
}

Expand Down
6 changes: 2 additions & 4 deletions src/pages/Search/SearchPage.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +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';

type SearchPageProps = StackScreenProps<CentralPaneNavigatorParamList, typeof SCREENS.SEARCH.CENTRAL_PANE>;
Expand All @@ -14,8 +13,7 @@ function SearchPage({route}: SearchPageProps) {

return (
<ScreenWrapper testID={SearchPage.displayName}>
<SearchResults query={route.params.query} />
{/* <EmptySearchView /> */}
<Search query={route.params.query} />
</ScreenWrapper>
);
}
Expand Down
Loading
Loading