Skip to content

Commit

Permalink
Merge pull request #48258 from software-mansion-labs/@szymczak/search…
Browse files Browse the repository at this point in the history
…-bar-hiding

Hide mobile Search nav button + status tabs on scrollDown, but reveal on scrollUp
  • Loading branch information
luacmartins authored Sep 30, 2024
2 parents 0e3b82c + 029f277 commit decf515
Show file tree
Hide file tree
Showing 19 changed files with 582 additions and 406 deletions.
33 changes: 30 additions & 3 deletions src/components/Search/SearchContext.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,39 @@
import React, {useCallback, useContext, useMemo, useState} from 'react';
import type {ReportActionListItemType, ReportListItemType, TransactionListItemType} from '@components/SelectionList/types';
import * as SearchUtils from '@libs/SearchUtils';
import type ChildrenProps from '@src/types/utils/ChildrenProps';
import type {SearchContext, SelectedTransactions} from './types';

const defaultSearchContext = {
currentSearchHash: -1,
selectedTransactions: {},
selectedReports: [],
setCurrentSearchHash: () => {},
setSelectedTransactions: () => {},
clearSelectedTransactions: () => {},
shouldShowStatusBarLoading: false,
setShouldShowStatusBarLoading: () => {},
};

const Context = React.createContext<SearchContext>(defaultSearchContext);

function getReportsFromSelectedTransactions(data: TransactionListItemType[] | ReportListItemType[] | ReportActionListItemType[], selectedTransactions: SelectedTransactions) {
return (data ?? [])
.filter(
(item) =>
!SearchUtils.isTransactionListItemType(item) &&
!SearchUtils.isReportActionListItemType(item) &&
item.reportID &&
item?.transactions?.every((transaction: {keyForList: string | number}) => selectedTransactions[transaction.keyForList]?.isSelected),
)
.map((item) => item.reportID);
}

function SearchContextProvider({children}: ChildrenProps) {
const [searchContextData, setSearchContextData] = useState<Pick<SearchContext, 'currentSearchHash' | 'selectedTransactions'>>({
const [searchContextData, setSearchContextData] = useState<Pick<SearchContext, 'currentSearchHash' | 'selectedTransactions' | 'selectedReports'>>({
currentSearchHash: defaultSearchContext.currentSearchHash,
selectedTransactions: defaultSearchContext.selectedTransactions,
selectedReports: defaultSearchContext.selectedReports,
});

const setCurrentSearchHash = useCallback((searchHash: number) => {
Expand All @@ -25,10 +43,14 @@ function SearchContextProvider({children}: ChildrenProps) {
}));
}, []);

const setSelectedTransactions = useCallback((selectedTransactions: SelectedTransactions) => {
const setSelectedTransactions = useCallback((selectedTransactions: SelectedTransactions, data: TransactionListItemType[] | ReportListItemType[] | ReportActionListItemType[]) => {
// When selecting transactions, we also need to manage the reports to which these transactions belong. This is done to ensure proper exporting to CSV.
const selectedReports = getReportsFromSelectedTransactions(data, selectedTransactions);

setSearchContextData((prevState) => ({
...prevState,
selectedTransactions,
selectedReports,
}));
}, []);

Expand All @@ -40,19 +62,24 @@ function SearchContextProvider({children}: ChildrenProps) {
setSearchContextData((prevState) => ({
...prevState,
selectedTransactions: {},
selectedReports: [],
}));
},
[searchContextData.currentSearchHash],
);

const [shouldShowStatusBarLoading, setShouldShowStatusBarLoading] = useState(false);

const searchContext = useMemo<SearchContext>(
() => ({
...searchContextData,
setCurrentSearchHash,
setSelectedTransactions,
clearSelectedTransactions,
shouldShowStatusBarLoading,
setShouldShowStatusBarLoading,
}),
[searchContextData, setCurrentSearchHash, setSelectedTransactions, clearSelectedTransactions],
[searchContextData, setCurrentSearchHash, setSelectedTransactions, clearSelectedTransactions, shouldShowStatusBarLoading],
);

return <Context.Provider value={searchContext}>{children}</Context.Provider>;
Expand Down
187 changes: 121 additions & 66 deletions src/components/Search/SearchPageHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import React, {useMemo} from 'react';
import React, {useMemo, useState} from 'react';
import type {StyleProp, TextStyle} from 'react-native';
import {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 type {ReportActionListItemType, ReportListItemType, TransactionListItemType} from '@components/SelectionList/types';
import Text from '@components/Text';
import useActiveWorkspace from '@hooks/useActiveWorkspace';
import useLocalize from '@hooks/useLocalize';
Expand All @@ -29,7 +30,7 @@ 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, SearchReport} from '@src/types/onyx/SearchResults';
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';
Expand Down Expand Up @@ -94,10 +95,6 @@ function HeaderWrapper({icon, title, subtitle, children, subtitleStyles = {}}: H
type SearchPageHeaderProps = {
queryJSON: SearchQueryJSON;
hash: number;
onSelectDeleteOption?: (itemsToDelete: string[]) => void;
setOfflineModalOpen?: () => void;
setDownloadErrorModalOpen?: () => void;
data?: TransactionListItemType[] | ReportListItemType[] | ReportActionListItemType[];
};

type SearchHeaderOptionValue = DeepValueOf<typeof CONST.SEARCH.BULK_ACTION_TYPES> | undefined;
Expand All @@ -121,44 +118,42 @@ function getHeaderContent(type: SearchDataTypes): HeaderContent {
}
}

function SearchPageHeader({queryJSON, hash, onSelectDeleteOption, setOfflineModalOpen, setDownloadErrorModalOpen, data}: SearchPageHeaderProps) {
function SearchPageHeader({queryJSON, hash}: SearchPageHeaderProps) {
const {translate} = useLocalize();
const theme = useTheme();
const styles = useThemeStyles();
const {isOffline} = useNetwork();
const {activeWorkspaceID} = useActiveWorkspace();
const {shouldUseNarrowLayout} = useResponsiveLayout();
const {selectedTransactions} = useSearchContext();
const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout();
const {selectedTransactions, clearSelectedTransactions, selectedReports} = useSearchContext();
const [selectionMode] = useOnyx(ONYXKEYS.MOBILE_SELECTION_MODE);
const personalDetails = usePersonalDetails();
const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT);
const taxRates = getAllTaxRates();
const [cardList = {}] = useOnyx(ONYXKEYS.CARD_LIST);
const [isDeleteExpensesConfirmModalVisible, setIsDeleteExpensesConfirmModalVisible] = useState(false);
const [isOfflineModalVisible, setIsOfflineModalVisible] = useState(false);
const [isDownloadErrorModalVisible, setIsDownloadErrorModalVisible] = useState(false);

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

const selectedReports: Array<SearchReport['reportID']> = useMemo(
() =>
(data ?? [])
.filter(
(item) =>
!SearchUtils.isTransactionListItemType(item) &&
!SearchUtils.isReportActionListItemType(item) &&
item.reportID &&
item.transactions.every((transaction: {keyForList: string | number}) => selectedTransactions[transaction.keyForList]?.isSelected),
)
.map((item) => item.reportID),
[data, selectedTransactions],
);
const {status, type} = queryJSON;

const isCannedQuery = SearchUtils.isCannedSearchQuery(queryJSON);

const headerSubtitle = isCannedQuery ? translate(getHeaderContent(type).titleText) : SearchUtils.getSearchHeaderTitle(queryJSON, personalDetails, cardList, reports, taxRates);
const headerTitle = isCannedQuery ? '' : translate('search.filtersHeader');
const headerIcon = isCannedQuery ? getHeaderContent(type).icon : Illustrations.Filters;

const subtitleStyles = isCannedQuery ? styles.textHeadlineH2 : {};
const handleDeleteExpenses = () => {
if (selectedTransactionsKeys.length === 0) {
return;
}

clearSelectedTransactions();
setIsDeleteExpensesConfirmModalVisible(false);
SearchActions.deleteMoneyRequestOnSearch(hash, selectedTransactionsKeys);
};

const headerButtonsOptions = useMemo(() => {
if (selectedTransactionsKeys.length === 0) {
Expand All @@ -174,15 +169,15 @@ function SearchPageHeader({queryJSON, hash, onSelectDeleteOption, setOfflineModa
shouldCloseModalOnSelect: true,
onSelected: () => {
if (isOffline) {
setOfflineModalOpen?.();
setIsOfflineModalVisible(true);
return;
}

const reportIDList = (selectedReports?.filter((report) => !!report) as string[]) ?? [];
SearchActions.exportSearchItemsToCSV(
{query: status, jsonQuery: JSON.stringify(queryJSON), reportIDList, transactionIDList: selectedTransactionsKeys, policyIDs: [activeWorkspaceID ?? '']},
() => {
setDownloadErrorModalOpen?.();
setIsDownloadErrorModalVisible(true);
},
);
},
Expand All @@ -198,7 +193,7 @@ function SearchPageHeader({queryJSON, hash, onSelectDeleteOption, setOfflineModa
shouldCloseModalOnSelect: true,
onSelected: () => {
if (isOffline) {
setOfflineModalOpen?.();
setIsOfflineModalVisible(true);
return;
}

Expand All @@ -217,7 +212,7 @@ function SearchPageHeader({queryJSON, hash, onSelectDeleteOption, setOfflineModa
shouldCloseModalOnSelect: true,
onSelected: () => {
if (isOffline) {
setOfflineModalOpen?.();
setIsOfflineModalVisible(true);
return;
}

Expand All @@ -236,11 +231,10 @@ function SearchPageHeader({queryJSON, hash, onSelectDeleteOption, setOfflineModa
shouldCloseModalOnSelect: true,
onSelected: () => {
if (isOffline) {
setOfflineModalOpen?.();
setIsOfflineModalVisible(true);
return;
}

onSelectDeleteOption?.(selectedTransactionsKeys);
setIsDeleteExpensesConfirmModalVisible(true);
},
});
}
Expand Down Expand Up @@ -270,14 +264,11 @@ function SearchPageHeader({queryJSON, hash, onSelectDeleteOption, setOfflineModa
selectedTransactionsKeys,
selectedTransactions,
translate,
onSelectDeleteOption,
hash,
theme.icon,
styles.colorMuted,
styles.fontWeightNormal,
isOffline,
setOfflineModalOpen,
setDownloadErrorModalOpen,
activeWorkspaceID,
selectedReports,
styles.textWrap,
Expand All @@ -286,10 +277,42 @@ function SearchPageHeader({queryJSON, hash, onSelectDeleteOption, setOfflineModa
if (shouldUseNarrowLayout) {
if (selectionMode?.isEnabled) {
return (
<SearchSelectedNarrow
options={headerButtonsOptions}
itemsLength={selectedTransactionsKeys.length}
/>
<View>
<SearchSelectedNarrow
options={headerButtonsOptions}
itemsLength={selectedTransactionsKeys.length}
/>
<ConfirmModal
isVisible={isDeleteExpensesConfirmModalVisible}
onConfirm={handleDeleteExpenses}
onCancel={() => {
setIsDeleteExpensesConfirmModalVisible(false);
}}
title={translate('iou.deleteExpense', {count: selectedTransactionsKeys.length})}
prompt={translate('iou.deleteConfirmation', {count: selectedTransactionsKeys.length})}
confirmText={translate('common.delete')}
cancelText={translate('common.cancel')}
danger
/>
<DecisionModal
title={translate('common.youAppearToBeOffline')}
prompt={translate('common.offlinePrompt')}
isSmallScreenWidth={isSmallScreenWidth}
onSecondOptionSubmit={() => setIsOfflineModalVisible(false)}
secondOptionText={translate('common.buttonConfirm')}
isVisible={isOfflineModalVisible}
onClose={() => setIsOfflineModalVisible(false)}
/>
<DecisionModal
title={translate('common.downloadFailedTitle')}
prompt={translate('common.downloadFailedDescription')}
isSmallScreenWidth={isSmallScreenWidth}
onSecondOptionSubmit={() => setIsDownloadErrorModalVisible(false)}
secondOptionText={translate('common.buttonConfirm')}
isVisible={isDownloadErrorModalVisible}
onClose={() => setIsDownloadErrorModalVisible(false)}
/>
</View>
);
}
return null;
Expand All @@ -304,34 +327,66 @@ function SearchPageHeader({queryJSON, hash, onSelectDeleteOption, setOfflineModa
const displaySearchRouter = SearchUtils.isCannedSearchQuery(queryJSON);

return (
<HeaderWrapper
title={headerTitle}
subtitle={headerSubtitle}
icon={headerIcon}
subtitleStyles={subtitleStyles}
>
<>
{headerButtonsOptions.length > 0 ? (
<ButtonWithDropdownMenu
onPress={() => null}
shouldAlwaysShowDropdownMenu
pressOnEnter
buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM}
customText={translate('workspace.common.selected', {count: selectedTransactionsKeys.length})}
options={headerButtonsOptions}
isSplitButton={false}
shouldUseStyleUtilityForAnchorPosition
/>
) : (
<Button
text={translate('search.filtersHeader')}
icon={Expensicons.Filters}
onPress={onPress}
/>
)}
{displaySearchRouter && <SearchButton />}
</>
</HeaderWrapper>
<>
<HeaderWrapper
title={headerTitle}
subtitle={headerSubtitle}
icon={headerIcon}
subtitleStyles={subtitleStyles}
>
<>
{headerButtonsOptions.length > 0 ? (
<ButtonWithDropdownMenu
onPress={() => null}
shouldAlwaysShowDropdownMenu
pressOnEnter
buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM}
customText={translate('workspace.common.selected', {count: selectedTransactionsKeys.length})}
options={headerButtonsOptions}
isSplitButton={false}
shouldUseStyleUtilityForAnchorPosition
/>
) : (
<Button
text={translate('search.filtersHeader')}
icon={Expensicons.Filters}
onPress={onPress}
/>
)}
{displaySearchRouter && <SearchButton />}
</>
</HeaderWrapper>
<ConfirmModal
isVisible={isDeleteExpensesConfirmModalVisible}
onConfirm={handleDeleteExpenses}
onCancel={() => {
setIsDeleteExpensesConfirmModalVisible(false);
}}
title={translate('iou.deleteExpense', {count: selectedTransactionsKeys.length})}
prompt={translate('iou.deleteConfirmation', {count: selectedTransactionsKeys.length})}
confirmText={translate('common.delete')}
cancelText={translate('common.cancel')}
danger
/>
<DecisionModal
title={translate('common.youAppearToBeOffline')}
prompt={translate('common.offlinePrompt')}
isSmallScreenWidth={isSmallScreenWidth}
onSecondOptionSubmit={() => setIsOfflineModalVisible(false)}
secondOptionText={translate('common.buttonConfirm')}
isVisible={isOfflineModalVisible}
onClose={() => setIsOfflineModalVisible(false)}
/>
<DecisionModal
title={translate('common.downloadFailedTitle')}
prompt={translate('common.downloadFailedDescription')}
isSmallScreenWidth={isSmallScreenWidth}
onSecondOptionSubmit={() => setIsDownloadErrorModalVisible(false)}
secondOptionText={translate('common.buttonConfirm')}
isVisible={isDownloadErrorModalVisible}
onClose={() => setIsDownloadErrorModalVisible(false)}
/>
</>
);
}

Expand Down
Loading

0 comments on commit decf515

Please sign in to comment.