Skip to content
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
7 changes: 7 additions & 0 deletions src/components/Search/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,13 @@ function Search({
openSearch();
}, []);

useEffect(() => {
if (!prevIsOffline || isOffline) {
return;
}
openSearch();
}, [isOffline, prevIsOffline]);

const {newSearchResultKeys, handleSelectionListScroll, newTransactions} = useSearchHighlightAndScroll({
searchResults,
transactions,
Expand Down
16 changes: 13 additions & 3 deletions src/hooks/useSearchTypeMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React, {useCallback, useEffect, useMemo, useState} from 'react';
import type {OnyxCollection} from 'react-native-onyx';
import {usePersonalDetails} from '@components/OnyxListItemProvider';
import type {PopoverMenuItem} from '@components/PopoverMenu';
import {useSearchContext} from '@components/Search/SearchContext';
import type {SearchQueryJSON} from '@components/Search/types';
import ThreeDotsMenu from '@components/ThreeDotsMenu';
import {clearAllFilters} from '@libs/actions/Search';
Expand All @@ -24,6 +25,7 @@ import useLocalize from './useLocalize';
import useOnyx from './useOnyx';
import useSearchTypeMenuSections from './useSearchTypeMenuSections';
import useSingleExecution from './useSingleExecution';
import useSuggestedSearchDefaultNavigation from './useSuggestedSearchDefaultNavigation';
import useTheme from './useTheme';
import useThemeStyles from './useThemeStyles';
import useWindowDimensions from './useWindowDimensions';
Expand All @@ -36,7 +38,8 @@ export default function useSearchTypeMenu(queryJSON: SearchQueryJSON) {
const {singleExecution} = useSingleExecution();
const {windowHeight} = useWindowDimensions();
const {translate} = useLocalize();
const {typeMenuSections} = useSearchTypeMenuSections();
const {typeMenuSections, shouldShowSuggestedSearchSkeleton} = useSearchTypeMenuSections();
const {clearSelectedTransactions} = useSearchContext();
const {showDeleteModal, DeleteConfirmModal} = useDeleteSavedSearch();
const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true});
const personalDetails = usePersonalDetails();
Expand All @@ -52,6 +55,14 @@ export default function useSearchTypeMenu(queryJSON: SearchQueryJSON) {

const allCards = useMemo(() => mergeCardListWithWorkspaceFeeds(workspaceCardFeeds ?? CONST.EMPTY_OBJECT, userCardList), [userCardList, workspaceCardFeeds]);
const [allFeeds] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER, {canBeMissing: true});
const flattenedMenuItems = useMemo(() => typeMenuSections.flatMap((section) => section.menuItems), [typeMenuSections]);

useSuggestedSearchDefaultNavigation({
shouldShowSkeleton: shouldShowSuggestedSearchSkeleton,
flattenedMenuItems,
similarSearchHash,
clearSelectedTransactions,
});

// this is a performance fix, rendering popover menu takes a lot of time and we don't need this component initially, that's why we postpone rendering it until everything else is rendered
const [delayPopoverMenuFirstRender, setDelayPopoverMenuFirstRender] = useState(true);
Expand Down Expand Up @@ -132,9 +143,8 @@ export default function useSearchTypeMenu(queryJSON: SearchQueryJSON) {
return -1;
}

const flattenedMenuItems = typeMenuSections.map((section) => section.menuItems).flat();
return flattenedMenuItems.findIndex((item) => item.similarSearchHash === similarSearchHash);
}, [similarSearchHash, isSavedSearchActive, typeMenuSections]);
}, [similarSearchHash, isSavedSearchActive, flattenedMenuItems]);

const popoverMenuItems = useMemo(() => {
return typeMenuSections
Expand Down
12 changes: 11 additions & 1 deletion src/hooks/useSearchTypeMenuSections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,12 @@ const useSearchTypeMenuSections = () => {
openCreateReportConfirmation();
}, [pendingReportCreation, openCreateReportConfirmation]);

const isSuggestedSearchDataReady = useMemo(() => {
const policiesList = Object.values(allPolicies ?? {}).filter((policy): policy is NonNullable<typeof policy> => policy !== null && policy !== undefined);

return policiesList.some((policy) => policy.employeeList !== undefined && policy.exporter !== undefined);
}, [allPolicies]);

const typeMenuSections = useMemo(
() =>
createTypeMenuSections(
Expand Down Expand Up @@ -145,7 +151,11 @@ const useSearchTypeMenuSections = () => {
],
);

return {typeMenuSections, CreateReportConfirmationModal};
return {
typeMenuSections,
CreateReportConfirmationModal,
shouldShowSuggestedSearchSkeleton: !isSuggestedSearchDataReady && !isOffline,
};
};

export default useSearchTypeMenuSections;
43 changes: 43 additions & 0 deletions src/hooks/useSuggestedSearchDefaultNavigation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {useEffect, useRef} from 'react';
import {clearAllFilters} from '@libs/actions/Search';
import Navigation from '@libs/Navigation/Navigation';
import type {SearchTypeMenuItem} from '@libs/SearchUIUtils';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';

type UseSuggestedSearchDefaultNavigationParams = {
shouldShowSkeleton: boolean;
flattenedMenuItems: SearchTypeMenuItem[];
similarSearchHash?: number;
clearSelectedTransactions: () => void;
};

function useSuggestedSearchDefaultNavigation({shouldShowSkeleton, flattenedMenuItems, similarSearchHash, clearSelectedTransactions}: UseSuggestedSearchDefaultNavigationParams) {
const hasShownSkeleton = useRef(false);

useEffect(() => {
if (shouldShowSkeleton) {
hasShownSkeleton.current = true;
return;
}

if (!hasShownSkeleton.current) {
return;
}

hasShownSkeleton.current = false;

const defaultMenuItem =
flattenedMenuItems.find((item) => item.key === CONST.SEARCH.SEARCH_KEYS.APPROVE) ?? flattenedMenuItems.find((item) => item.key === CONST.SEARCH.SEARCH_KEYS.SUBMIT);

if (!defaultMenuItem || similarSearchHash === defaultMenuItem.similarSearchHash) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The PR introduced a bug where the app overrides user-selected search tabs (e.g., Expenses) by auto-navigating to a default tab (e.g., Approve) after skeleton loading. This happens because the condition doesn’t fully account for intentional user navigation. More details and fix proposal.

return;
}

clearAllFilters();
clearSelectedTransactions();
Navigation.navigate(ROUTES.SEARCH_ROOT.getRoute({query: defaultMenuItem.searchQuery}));
}, [shouldShowSkeleton, flattenedMenuItems, similarSearchHash, clearSelectedTransactions]);
}

export default useSuggestedSearchDefaultNavigation;
103 changes: 49 additions & 54 deletions src/pages/Search/SearchTypeMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import {useIsFocused, useRoute} from '@react-navigation/native';
import {accountIDSelector} from '@selectors/Session';
import React, {useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef} from 'react';
import React, {useCallback, useContext, useLayoutEffect, useMemo, useRef} from 'react';
import {View} from 'react-native';
// eslint-disable-next-line no-restricted-imports
import type {ScrollView as RNScrollView, ScrollViewProps} from 'react-native';
import Animated, {FadeIn} from 'react-native-reanimated';
import MenuItem from '@components/MenuItem';
import type {MenuItemWithLink} from '@components/MenuItemList';
import MenuItemList from '@components/MenuItemList';
Expand All @@ -21,6 +20,7 @@ import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import useSearchTypeMenuSections from '@hooks/useSearchTypeMenuSections';
import useSingleExecution from '@hooks/useSingleExecution';
import useSuggestedSearchDefaultNavigation from '@hooks/useSuggestedSearchDefaultNavigation';
import useThemeStyles from '@hooks/useThemeStyles';
import {clearAllFilters} from '@libs/actions/Search';
import {mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils';
Expand All @@ -35,6 +35,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {SaveSearchItem} from '@src/types/onyx/SaveSearch';
import SavedSearchItemThreeDotMenu from './SavedSearchItemThreeDotMenu';
import SuggestedSearchSkeleton from './SuggestedSearchSkeleton';

type SearchTypeMenuProps = {
queryJSON: SearchQueryJSON | undefined;
Expand All @@ -47,7 +48,7 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) {
const {singleExecution} = useSingleExecution();
const {translate} = useLocalize();
const [savedSearches] = useOnyx(ONYXKEYS.SAVED_SEARCHES, {canBeMissing: true});
const {typeMenuSections, CreateReportConfirmationModal} = useSearchTypeMenuSections();
const {typeMenuSections, CreateReportConfirmationModal, shouldShowSuggestedSearchSkeleton} = useSearchTypeMenuSections();
const isFocused = useIsFocused();
const {
shouldShowProductTrainingTooltip: shouldShowSavedSearchTooltip,
Expand All @@ -69,19 +70,15 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) {
const taxRates = getAllTaxRates(allPolicies);
const [currentUserAccountID = -1] = useOnyx(ONYXKEYS.SESSION, {selector: accountIDSelector, canBeMissing: false});
const {clearSelectedTransactions} = useSearchContext();
const initialSearchKeys = useRef<string[]>([]);

// The first time we render all of the sections the user can see, we need to mark these as 'rendered', such that we
// dont animate them in. We only animate in items that a user gains access to later on
useEffect(() => {
if (initialSearchKeys.current.length) {
return;
}
const flattenedMenuItems = useMemo(() => typeMenuSections.flatMap((section) => section.menuItems), [typeMenuSections]);

initialSearchKeys.current = typeMenuSections.flatMap((section) => {
return section.menuItems.map((item) => item.key);
});
}, [typeMenuSections]);
useSuggestedSearchDefaultNavigation({
shouldShowSkeleton: shouldShowSuggestedSearchSkeleton,
flattenedMenuItems,
similarSearchHash,
clearSelectedTransactions,
});

const getOverflowMenu = useCallback((itemName: string, itemHash: number, itemQuery: string) => getOverflowMenuUtil(itemName, itemHash, itemQuery, showDeleteModal), [showDeleteModal]);
const createSavedSearchMenuItem = useCallback(
Expand Down Expand Up @@ -206,9 +203,8 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) {
return -1;
}

const flattenedMenuItems = typeMenuSections.map((section) => section.menuItems).flat();
return flattenedMenuItems.findIndex((item) => item.similarSearchHash === similarSearchHash);
}, [similarSearchHash, isSavedSearchActive, typeMenuSections]);
}, [similarSearchHash, isSavedSearchActive, flattenedMenuItems]);

return (
<>
Expand All @@ -218,39 +214,38 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) {
ref={scrollViewRef}
showsVerticalScrollIndicator={false}
>
<View style={[styles.pb4, styles.mh3, styles.gap4]}>
{typeMenuSections.map((section, sectionIndex) => (
<View key={section.translationPath}>
<Text style={styles.sectionTitle}>{translate(section.translationPath)}</Text>

{section.translationPath === 'search.savedSearchesMenuItemTitle' ? (
<>
{renderSavedSearchesSection(savedSearchesMenuItems)}
{/* DeleteConfirmModal is a stable JSX element returned by the hook.
Returning the element directly keeps the component identity across re-renders so React
can play its exit animation instead of removing it instantly. */}
{DeleteConfirmModal}
</>
) : (
<>
{section.menuItems.map((item, itemIndex) => {
const previousItemCount = typeMenuSections.slice(0, sectionIndex).reduce((acc, sec) => acc + sec.menuItems.length, 0);
const flattenedIndex = previousItemCount + itemIndex;
const focused = activeItemIndex === flattenedIndex;
{shouldShowSuggestedSearchSkeleton ? (
<View style={[styles.pb4, styles.mh3, styles.gap4]}>
<SuggestedSearchSkeleton />
</View>
) : (
<View style={[styles.pb4, styles.mh3, styles.gap4]}>
{typeMenuSections.map((section, sectionIndex) => (
<View key={section.translationPath}>
<Text style={styles.sectionTitle}>{translate(section.translationPath)}</Text>

const onPress = singleExecution(() => {
clearAllFilters();
clearSelectedTransactions();
Navigation.navigate(ROUTES.SEARCH_ROOT.getRoute({query: item.searchQuery}));
});
{section.translationPath === 'search.savedSearchesMenuItemTitle' ? (
<>
{renderSavedSearchesSection(savedSearchesMenuItems)}
{/* DeleteConfirmModal is a stable JSX element returned by the hook.
Returning the element directly keeps the component identity across re-renders so React
can play its exit animation instead of removing it instantly. */}
{DeleteConfirmModal}
</>
) : (
<>
{section.menuItems.map((item, itemIndex) => {
const previousItemCount = typeMenuSections.slice(0, sectionIndex).reduce((acc, sec) => acc + sec.menuItems.length, 0);
const flattenedIndex = previousItemCount + itemIndex;
const focused = activeItemIndex === flattenedIndex;

const isInitialItem = !initialSearchKeys.current.length || initialSearchKeys.current.includes(item.key);
const onPress = singleExecution(() => {
clearAllFilters();
clearSelectedTransactions();
Navigation.navigate(ROUTES.SEARCH_ROOT.getRoute({query: item.searchQuery}));
});

return (
<Animated.View
key={item.translationPath}
entering={!isInitialItem ? FadeIn : undefined}
>
return (
<MenuItem
key={item.key}
disabled={false}
Expand All @@ -264,14 +259,14 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) {
onPress={onPress}
shouldIconUseAutoWidthStyle
/>
</Animated.View>
);
})}
</>
)}
</View>
))}
</View>
);
})}
</>
)}
</View>
))}
</View>
)}
</ScrollView>
</>
);
Expand Down
Loading
Loading