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

Hide mobile Search nav button + status tabs on scrollDown, but reveal on scrollUp #48258

Open
wants to merge 41 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
0cdffe1
add animation logic
SzymczakJ Aug 29, 2024
ce6f1cc
Merge branch 'main' into @szymczak/search-bar-hiding
SzymczakJ Aug 29, 2024
6ea15d8
prettify code
SzymczakJ Aug 30, 2024
ec1402b
adjust padding
SzymczakJ Sep 2, 2024
c2cb62e
Merge branch 'main' into @szymczak/search-bar-hiding
SzymczakJ Sep 2, 2024
e93ceca
make SearchPageHeader use SearchContext instead of accepting data param
SzymczakJ Sep 2, 2024
1e5ff51
move modals from Search/index to SearchPageHeader
SzymczakJ Sep 3, 2024
cd01f44
fix paddings
SzymczakJ Sep 3, 2024
ce7d213
fix EmptyState and Loading screen paddings
SzymczakJ Sep 3, 2024
709e93d
fix loading state bug
SzymczakJ Sep 3, 2024
317cc9e
fix PR comments
SzymczakJ Sep 3, 2024
8fd252b
Merge branch 'main' into @szymczak/search-bar-hiding
SzymczakJ Sep 4, 2024
8794f8c
fix typo in comment
SzymczakJ Sep 4, 2024
261df38
Merge branch 'main' into @szymczak/search-bar-hiding
SzymczakJ Sep 4, 2024
c59ff2f
Merge branch 'main' into @szymczak/search-bar-hiding
SzymczakJ Sep 6, 2024
c0866fa
fix PR comments
SzymczakJ Sep 6, 2024
ce2a6d3
remove getReportsFromSelectedTransactions from SearchUtils
SzymczakJ Sep 6, 2024
db5812c
fix
SzymczakJ Sep 6, 2024
b0bf4c0
fix lint
SzymczakJ Sep 6, 2024
217ebf3
Merge branch 'main' into @szymczak/search-bar-hiding
SzymczakJ Sep 9, 2024
552e596
merge main
SzymczakJ Sep 9, 2024
1218079
modify scroll speed
SzymczakJ Sep 9, 2024
c85e65f
fix animation
SzymczakJ Sep 10, 2024
a7cc0e4
Merge branch 'main' into @szymczak/search-bar-hiding
SzymczakJ Sep 11, 2024
28f7efe
clean up code
SzymczakJ Sep 11, 2024
b5ce7cd
fix pr comments
SzymczakJ Sep 11, 2024
060ea0d
fix pr comments
SzymczakJ Sep 11, 2024
888db7d
fix animation
SzymczakJ Sep 16, 2024
e222001
Merge branch 'main' into @szymczak/search-bar-hiding
SzymczakJ Sep 16, 2024
bb7fb4d
fix linter
SzymczakJ Sep 16, 2024
a51f7db
fix empty state not showing
SzymczakJ Sep 17, 2024
8319d34
clean up code
SzymczakJ Sep 18, 2024
9b7e621
Merge branch 'main' into @szymczak/search-bar-hiding
SzymczakJ Sep 18, 2024
487945d
clean up code
SzymczakJ Sep 18, 2024
7146a1d
fix styles
SzymczakJ Sep 18, 2024
91e44d4
fix PR comments
SzymczakJ Sep 20, 2024
0a672cf
Merge branch 'main' into @szymczak/search-bar-hiding
SzymczakJ Sep 20, 2024
786c2cb
fix PR comments
SzymczakJ Sep 20, 2024
3d556e4
fix modal not showing bug
SzymczakJ Sep 20, 2024
009d3d3
fix PR comments
SzymczakJ Sep 23, 2024
3fd8995
fix margin
SzymczakJ Sep 23, 2024
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
34 changes: 31 additions & 3 deletions src/components/Search/SearchContext.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,40 @@
import React, {useCallback, useContext, useMemo, useState} from 'react';
import type {ReportActionListItemType, ReportListItemType, TransactionListItemType} from '@components/SelectionList/types';
import {isTransactionListItemType} from '@libs/SearchUtils';
SzymczakJ marked this conversation as resolved.
Show resolved Hide resolved
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) =>
!isTransactionListItemType(item) &&
SzymczakJ marked this conversation as resolved.
Show resolved Hide resolved
!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 +44,14 @@ function SearchContextProvider({children}: ChildrenProps) {
}));
}, []);

const setSelectedTransactions = useCallback((selectedTransactions: SelectedTransactions) => {
const setSelectedTransactions = useCallback((selectedTransactions: SelectedTransactions, data: TransactionListItemType[] | ReportListItemType[] | ReportActionListItemType[]) => {
// When selecting transaction we also have to manage reports to which these transactions belong to. We do this for sake of properly exporting to CSV.
SzymczakJ marked this conversation as resolved.
Show resolved Hide resolved
const selectedReports = getReportsFromSelectedTransactions(data, selectedTransactions);

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

Expand All @@ -40,19 +63,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
141 changes: 83 additions & 58 deletions src/components/Search/SearchPageHeader.tsx
Copy link
Contributor

Choose a reason for hiding this comment

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

Bug on small screens: Selected multiple expenses and clicked "delete," but the delete confirmation modal did not appear.

Simulator.Screen.Recording.-.iPhone.15.Pro.Max.-.2024-09-19.at.23.02.42.mp4

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 @@ -93,10 +94,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 @@ -120,37 +117,26 @@ 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 [deleteExpensesConfirmModalVisible, setDeleteExpensesConfirmModalVisible] = useState(false);
const [offlineModalVisible, setOfflineModalVisible] = useState(false);
const [downloadErrorModalVisible, setDownloadErrorModalVisible] = useState(false);

SzymczakJ marked this conversation as resolved.
Show resolved Hide resolved
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);
Expand All @@ -159,6 +145,16 @@ function SearchPageHeader({queryJSON, hash, onSelectDeleteOption, setOfflineModa

const subtitleStyles = isCannedQuery ? styles.textHeadlineH2 : {};

const handleDeleteExpenses = () => {
if (selectedTransactionsKeys.length === 0) {
return;
}

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

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

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

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

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

onSelectDeleteOption?.(selectedTransactionsKeys);
setDeleteExpensesConfirmModalVisible(true);
},
});
}
Expand Down Expand Up @@ -269,14 +265,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 @@ -301,31 +294,63 @@ function SearchPageHeader({queryJSON, hash, onSelectDeleteOption, setOfflineModa
};

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', {selectedNumber: selectedTransactionsKeys.length})}
options={headerButtonsOptions}
isSplitButton={false}
shouldUseStyleUtilityForAnchorPosition
/>
) : (
<Button
text={translate('search.filtersHeader')}
icon={Expensicons.Filters}
onPress={onPress}
/>
)}
</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', {selectedNumber: selectedTransactionsKeys.length})}
options={headerButtonsOptions}
isSplitButton={false}
shouldUseStyleUtilityForAnchorPosition
/>
) : (
<Button
text={translate('search.filtersHeader')}
icon={Expensicons.Filters}
onPress={onPress}
/>
)}
</HeaderWrapper>
<ConfirmModal
isVisible={deleteExpensesConfirmModalVisible}
onConfirm={handleDeleteExpenses}
onCancel={() => {
setDeleteExpensesConfirmModalVisible(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={() => setOfflineModalVisible(false)}
secondOptionText={translate('common.buttonConfirm')}
isVisible={offlineModalVisible}
onClose={() => setOfflineModalVisible(false)}
/>
<DecisionModal
title={translate('common.downloadFailedTitle')}
prompt={translate('common.downloadFailedDescription')}
isSmallScreenWidth={isSmallScreenWidth}
onSecondOptionSubmit={() => setDownloadErrorModalVisible(false)}
secondOptionText={translate('common.buttonConfirm')}
isVisible={downloadErrorModalVisible}
onClose={() => setDownloadErrorModalVisible(false)}
/>
</>
);
}

Expand Down
15 changes: 11 additions & 4 deletions src/components/Search/SearchStatusBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {ScrollView as RNScrollView} from 'react-native';
import Button from '@components/Button';
import * as Expensicons from '@components/Icon/Expensicons';
import ScrollView from '@components/ScrollView';
import SearchStatusSkeleton from '@components/Skeletons/SearchStatusSkeleton';
import useLocalize from '@hooks/useLocalize';
import useSingleExecution from '@hooks/useSingleExecution';
import useStyleUtils from '@hooks/useStyleUtils';
Expand All @@ -15,12 +16,13 @@ import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import type {SearchDataTypes} from '@src/types/onyx/SearchResults';
import type IconAsset from '@src/types/utils/IconAsset';
import {useSearchContext} from './SearchContext';
import type {ChatSearchStatus, ExpenseSearchStatus, InvoiceSearchStatus, SearchQueryString, SearchStatus, TripSearchStatus} from './types';

type SearchStatusBarProps = {
type: SearchDataTypes;
status: SearchStatus;
resetOffset: () => void;
onStatusChange?: () => void;
};

const expenseOptions: Array<{key: ExpenseSearchStatus; icon: IconAsset; text: TranslationPaths; query: SearchQueryString}> = [
Expand Down Expand Up @@ -145,7 +147,7 @@ function getOptions(type: SearchDataTypes) {
}
}

function SearchStatusBar({type, status, resetOffset}: SearchStatusBarProps) {
function SearchStatusBar({type, status, onStatusChange}: SearchStatusBarProps) {
const {singleExecution} = useSingleExecution();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
Expand All @@ -154,17 +156,22 @@ function SearchStatusBar({type, status, resetOffset}: SearchStatusBarProps) {
const options = getOptions(type);
const scrollRef = useRef<RNScrollView>(null);
const isScrolledRef = useRef(false);
const {shouldShowStatusBarLoading} = useSearchContext();

if (shouldShowStatusBarLoading) {
return <SearchStatusSkeleton shouldAnimate />;
}

return (
<ScrollView
style={[styles.flexRow, styles.mb2, styles.overflowScroll, styles.flexGrow0]}
ref={scrollRef}
style={[styles.flexRow, styles.mb5, styles.overflowScroll, styles.flexGrow0]}
horizontal
showsHorizontalScrollIndicator={false}
>
{options.map((item, index) => {
const onPress = singleExecution(() => {
resetOffset();
onStatusChange?.();
Navigation.setParams({q: item.query});
});
const isActive = status === item.key;
Expand Down
Loading
Loading