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

Use autocomplete on Search Results Header #52568

Merged
137 changes: 11 additions & 126 deletions src/components/Search/SearchPageHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,120 +1,38 @@
import React, {useEffect, useMemo, useState} from 'react';
import React, {useMemo, useState} from 'react';
import {InteractionManager, View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import Button from '@components/Button';
import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu';
import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types';
import ConfirmModal from '@components/ConfirmModal';
import DecisionModal from '@components/DecisionModal';
import Header from '@components/Header';
import type HeaderWithBackButtonProps from '@components/HeaderWithBackButton/types';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import * as Illustrations from '@components/Icon/Illustrations';
import {usePersonalDetails} from '@components/OnyxProvider';
import Text from '@components/Text';
import useActiveWorkspace from '@hooks/useActiveWorkspace';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import * as SearchActions from '@libs/actions/Search';
import Log from '@libs/Log';
import Navigation from '@libs/Navigation/Navigation';
import {getAllTaxRates} from '@libs/PolicyUtils';
import * as SearchQueryUtils from '@libs/SearchQueryUtils';
import SearchSelectedNarrow from '@pages/Search/SearchSelectedNarrow';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {SearchDataTypes} from '@src/types/onyx/SearchResults';
import type DeepValueOf from '@src/types/utils/DeepValueOf';
import type IconAsset from '@src/types/utils/IconAsset';
import {useSearchContext} from './SearchContext';
import SearchButton from './SearchRouter/SearchButton';
import SearchRouterInput from './SearchRouter/SearchRouterInput';
import SearchPageHeaderInput from './SearchPageHeaderInput';
import type {SearchQueryJSON} from './types';

type HeaderWrapperProps = Pick<HeaderWithBackButtonProps, 'icon' | 'children'> & {
text: string;
value: string;
isCannedQuery: boolean;
onSubmit: () => void;
setValue: (input: string) => void;
};

function HeaderWrapper({icon, children, text, value, isCannedQuery, onSubmit, setValue}: HeaderWrapperProps) {
const styles = useThemeStyles();
// If the icon is present, the header bar should be taller and use different font.
const isCentralPaneSettings = !!icon;

return (
<View
dataSet={{dragArea: false}}
style={[styles.headerBar, isCentralPaneSettings && styles.headerBarDesktopHeight]}
>
{isCannedQuery ? (
<View style={[styles.dFlex, styles.flexRow, styles.alignItemsCenter, styles.flexGrow1, styles.justifyContentBetween, styles.overflowHidden]}>
{!!icon && (
<Icon
src={icon}
width={variables.iconHeader}
height={variables.iconHeader}
additionalStyles={[styles.mr2]}
/>
)}
<Header subtitle={<Text style={[styles.textLarge, styles.textHeadlineH2]}>{text}</Text>} />
<View style={[styles.reportOptions, styles.flexRow, styles.pr5, styles.alignItemsCenter, styles.gap2]}>{children}</View>
</View>
) : (
<View style={styles.pr5}>
<SearchRouterInput
value={value}
onSubmit={onSubmit}
updateSearch={setValue}
autoFocus={false}
isFullWidth
wrapperStyle={[styles.searchRouterInputResults, styles.br2]}
wrapperFocusedStyle={styles.searchRouterInputResultsFocused}
rightComponent={children}
routerListRef={undefined}
/>
</View>
)}
</View>
);
}

type SearchPageHeaderProps = {
queryJSON: SearchQueryJSON;
hash: number;
};
type SearchPageHeaderProps = {queryJSON: SearchQueryJSON};

type SearchHeaderOptionValue = DeepValueOf<typeof CONST.SEARCH.BULK_ACTION_TYPES> | undefined;

type HeaderContent = {
icon: IconAsset;
titleText: TranslationPaths;
};

function getHeaderContent(type: SearchDataTypes): HeaderContent {
switch (type) {
case CONST.SEARCH.DATA_TYPES.INVOICE:
return {icon: Illustrations.EnvelopeReceipt, titleText: 'workspace.common.invoices'};
case CONST.SEARCH.DATA_TYPES.TRIP:
return {icon: Illustrations.Luggage, titleText: 'travel.trips'};
case CONST.SEARCH.DATA_TYPES.CHAT:
return {icon: Illustrations.CommentBubblesBlue, titleText: 'common.chats'};
case CONST.SEARCH.DATA_TYPES.EXPENSE:
default:
return {icon: Illustrations.MoneyReceipts, titleText: 'common.expenses'};
}
}

function SearchPageHeader({queryJSON, hash}: SearchPageHeaderProps) {
function SearchPageHeader({queryJSON}: SearchPageHeaderProps) {
const {translate} = useLocalize();
const theme = useTheme();
const styles = useThemeStyles();
Expand All @@ -136,19 +54,10 @@ function SearchPageHeader({queryJSON, hash}: SearchPageHeaderProps) {
const [isOfflineModalVisible, setIsOfflineModalVisible] = useState(false);
const [isDownloadErrorModalVisible, setIsDownloadErrorModalVisible] = useState(false);

const {status, type} = queryJSON;
const isCannedQuery = SearchQueryUtils.isCannedSearchQuery(queryJSON);
const headerText = isCannedQuery ? translate(getHeaderContent(type).titleText) : SearchQueryUtils.buildUserReadableQueryString(queryJSON, personalDetails, cardList, reports, taxRates);
const [inputValue, setInputValue] = useState(headerText);

useEffect(() => {
setInputValue(headerText);
}, [headerText]);
const {status, hash} = queryJSON;

const selectedTransactionsKeys = Object.keys(selectedTransactions ?? {});

const headerIcon = getHeaderContent(type).icon;

const handleDeleteExpenses = () => {
if (selectedTransactionsKeys.length === 0) {
return;
Expand Down Expand Up @@ -182,7 +91,7 @@ function SearchPageHeader({queryJSON, hash}: SearchPageHeaderProps) {
return;
}

const reportIDList = selectedReports?.filter((report) => !!report) ?? [];
const reportIDList = selectedReports.filter((report): report is string => !!report) ?? [];
SearchActions.exportSearchItemsToCSV(
{query: status, jsonQuery: JSON.stringify(queryJSON), reportIDList, transactionIDList: selectedTransactionsKeys, policyIDs: [activeWorkspaceID ?? '']},
() => {
Expand Down Expand Up @@ -327,41 +236,18 @@ function SearchPageHeader({queryJSON, hash}: SearchPageHeaderProps) {
return null;
}

const onPress = () => {
const onFiltersButtonPress = () => {
const filterFormValues = SearchQueryUtils.buildFilterFormValuesFromQuery(queryJSON, policyCategories, policyTagsLists, currencyList, personalDetails, cardList, reports, taxRates);
SearchActions.updateAdvancedFilters(filterFormValues);

Navigation.navigate(ROUTES.SEARCH_ADVANCED_FILTERS);
};

const onSubmit = () => {
if (!inputValue) {
return;
}
const inputQueryJSON = SearchQueryUtils.buildSearchQueryJSON(inputValue);
if (inputQueryJSON) {
// Todo traverse the tree to update all the display values into id values; this is only temporary until autocomplete code from SearchRouter is implement here
// After https://github.com/Expensify/App/pull/51633 is merged, autocomplete functionality will be included into this component, and `getFindIDFromDisplayValue` can be removed
const computeNodeValueFn = SearchQueryUtils.getFindIDFromDisplayValue(cardList, taxRates);
const standardizedQuery = SearchQueryUtils.traverseAndUpdatedQuery(inputQueryJSON, computeNodeValueFn);
const query = SearchQueryUtils.buildSearchQueryString(standardizedQuery);
SearchActions.clearAllFilters();
Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query}));
} else {
Log.alert(`${CONST.ERROR.ENSURE_BUGBOT} user query failed to parse`, inputValue, false);
}
};
const isCannedQuery = SearchQueryUtils.isCannedSearchQuery(queryJSON);

return (
<>
<HeaderWrapper
icon={headerIcon}
text={headerText}
value={inputValue}
isCannedQuery={isCannedQuery}
onSubmit={onSubmit}
setValue={setInputValue}
>
<SearchPageHeaderInput queryJSON={queryJSON}>
{headerButtonsOptions.length > 0 ? (
<ButtonWithDropdownMenu
onPress={() => null}
Expand All @@ -377,11 +263,10 @@ function SearchPageHeader({queryJSON, hash}: SearchPageHeaderProps) {
innerStyles={!isCannedQuery && [styles.searchRouterInputResults, styles.borderNone]}
text={translate('search.filtersHeader')}
icon={Expensicons.Filters}
onPress={onPress}
onPress={onFiltersButtonPress}
/>
)}
{isCannedQuery && <SearchButton />}
</HeaderWrapper>
</SearchPageHeaderInput>
<ConfirmModal
isVisible={isDeleteExpensesConfirmModalVisible}
onConfirm={handleDeleteExpenses}
Expand Down
Loading
Loading