-
Notifications
You must be signed in to change notification settings - Fork 3.9k
[Performance] Fix global create scan misidentifying Search context via fallback chain #93718
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
Changes from all commits
f5d3620
8bcd72e
bbe24fc
00dbfda
e6999a2
9c0f2a1
154f899
d714b58
d426b52
e85fa35
24ff2be
223695f
d7e0f34
9491ef3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| import {addPendingNewTransactionIDs} from '@libs/actions/IOU/PendingNewTransactions'; | ||
| import getIsNarrowLayout from '@libs/getIsNarrowLayout'; | ||
| import Log from '@libs/Log'; | ||
| import {getPreservedNavigatorState} from '@libs/Navigation/AppNavigator/createSplitNavigator/usePreserveNavigatorState'; | ||
| import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; | ||
| import {buildCannedSearchQuery, getCurrentSearchQueryJSON} from '@libs/SearchQueryUtils'; | ||
| import {setPendingSubmitFollowUpAction} from '@libs/telemetry/submitFollowUpAction'; | ||
|
|
@@ -22,6 +23,14 @@ type NavigateAfterExpenseCreateParams = { | |
| shouldNavigate?: boolean; | ||
| }; | ||
|
|
||
| function getNavigateAfterCreateSearchNavigatorState() { | ||
| const rootState = navigationRef.getRootState(); | ||
| const tabNavigatorRoute = rootState?.routes?.findLast((route) => route.name === NAVIGATORS.TAB_NAVIGATOR); | ||
| const tabState = tabNavigatorRoute?.state ?? (tabNavigatorRoute?.key ? getPreservedNavigatorState(tabNavigatorRoute.key) : undefined); | ||
| const searchNavigatorRoute = tabState?.routes?.findLast((route) => route.name === NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR); | ||
| return searchNavigatorRoute?.state ?? (searchNavigatorRoute?.key ? getPreservedNavigatorState(searchNavigatorRoute.key) : undefined); | ||
| } | ||
|
|
||
|
Comment on lines
+26
to
+33
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Strength: Extraction of getNavigateAfterCreateSearchNavigatorState into a named function improves readability and testability. Issue: This new helper does not use the improved This function skips the new fallback chain entirely. If the Bug potential: If the |
||
| /** | ||
| * Helper to navigate after an expense is created in order to standardize the post‑creation experience | ||
| * when creating an expense from the global create button. | ||
|
|
@@ -61,15 +70,14 @@ function navigateAfterExpenseCreate({ | |
|
|
||
| // When already on Search ROOT with the same type (expense vs invoice), we navigate to the same screen (no-op or refresh); record as dismiss_modal_only. | ||
| // When on another Search sub-tab (e.g. Chats), or on Search with a different type (e.g. on Invoice, submitting expense), record as navigate_to_search. | ||
| const rootState = navigationRef.getRootState(); | ||
| const searchNavigatorRoute = rootState?.routes?.findLast((route) => route.name === NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR); | ||
| const lastSearchRoute = searchNavigatorRoute?.state?.routes?.at(-1); | ||
| const alreadyOnSearchRoot = isSearchTopmostFullScreenRoute() && lastSearchRoute?.name === SCREENS.SEARCH.ROOT; | ||
| const searchNavigatorState = getNavigateAfterCreateSearchNavigatorState(); | ||
| const lastSearchRoute = searchNavigatorState?.routes?.at(-1); | ||
| const isSearchTopmost = isSearchTopmostFullScreenRoute(); | ||
| const alreadyOnSearchRoot = isSearchTopmost && lastSearchRoute?.name === SCREENS.SEARCH.ROOT; | ||
| const currentSearchQueryJSON = alreadyOnSearchRoot ? getCurrentSearchQueryJSON() : undefined; | ||
| const isSameSearchType = currentSearchQueryJSON?.type === type; | ||
| setPendingSubmitFollowUpAction( | ||
| alreadyOnSearchRoot && isSameSearchType ? CONST.TELEMETRY.SUBMIT_FOLLOW_UP_ACTION.DISMISS_MODAL_ONLY : CONST.TELEMETRY.SUBMIT_FOLLOW_UP_ACTION.NAVIGATE_TO_SEARCH, | ||
| ); | ||
| const followUpAction = alreadyOnSearchRoot && isSameSearchType ? CONST.TELEMETRY.SUBMIT_FOLLOW_UP_ACTION.DISMISS_MODAL_ONLY : CONST.TELEMETRY.SUBMIT_FOLLOW_UP_ACTION.NAVIGATE_TO_SEARCH; | ||
| setPendingSubmitFollowUpAction(followUpAction); | ||
|
|
||
| const queryString = buildCannedSearchQuery({type}); | ||
| const navigateToSearch = () => { | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Even if these methods are simple, lets create unit tests for them to make sure we gate from some unexpected changes to them
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added tests for |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -46,6 +46,7 @@ import {validateAmount} from './MoneyRequestUtils'; | |
| import {getPreservedNavigatorState} from './Navigation/AppNavigator/createSplitNavigator/usePreserveNavigatorState'; | ||
| import navigationRef from './Navigation/navigationRef'; | ||
| import type {SearchFullscreenNavigatorParamList} from './Navigation/types'; | ||
| import {isRecord} from './ObjectUtils'; | ||
| import {getDisplayNameOrDefault, getPersonalDetailByEmail} from './PersonalDetailsUtils'; | ||
| import {getCleanedTagName} from './PolicyUtils'; | ||
| import {getReportName} from './ReportNameUtils'; | ||
|
|
@@ -56,6 +57,17 @@ import {hashText} from './UserUtils'; | |
| import {isValidDate} from './ValidationUtils'; | ||
|
|
||
| type FilterKeys = keyof typeof CONST.SEARCH.SYNTAX_FILTER_KEYS; | ||
| type SearchRootParams = SearchFullscreenNavigatorParamList[typeof SCREENS.SEARCH.ROOT]; | ||
| type NavigationRouteLike = { | ||
| /** Unique React Navigation route identifier. */ | ||
| key?: string; | ||
| /** Screen name as registered in the navigator. */ | ||
| name?: string; | ||
| /** Screen-specific params passed to the route. */ | ||
| params?: Record<string, unknown>; | ||
| /** Nested navigator state, if this route is itself a navigator. */ | ||
| state?: unknown; | ||
| }; | ||
|
Comment on lines
+61
to
+70
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Issue: Impact: Type safety is weakened. Recommendation: type NavigationRouteLike = {
key?: string;
name?: string;
params?: Record<string, unknown>;
state?: unknown;
};Then function getRouteKey(route: NavigationRouteLike | unknown): string | undefined {
return isRecord(route) && typeof route.key === 'string' ? route.key : undefined;
}(Kept the runtime check since route is unknown at call site.)
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I updated the type and since we always pass the route which is
Comment on lines
+61
to
+70
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. docs
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. added! |
||
|
|
||
| // This map contains chars that match each operator | ||
| const operatorToCharMap = { | ||
|
|
@@ -1916,29 +1928,103 @@ function getQueryWithUpdatedValues(query: string, shouldSkipAmountConversion = f | |
| return buildSearchQueryString(standardizedQuery); | ||
| } | ||
|
|
||
| function isSearchRootParams(params: unknown): params is SearchRootParams { | ||
| return ( | ||
| !!params && | ||
| typeof params === 'object' && | ||
| 'q' in params && | ||
| typeof params.q === 'string' && | ||
| (!('rawQuery' in params) || params.rawQuery === undefined || typeof params.rawQuery === 'string') | ||
| ); | ||
| } | ||
|
|
||
| function isUnknownArray(value: unknown): value is unknown[] { | ||
| return Array.isArray(value); | ||
| } | ||
|
Comment on lines
+1941
to
+1943
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Issue: This adds function call overhead for minimal value. TypeScript's
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. doesn't work, |
||
|
|
||
| function getParamsState(params: unknown): unknown { | ||
| return isRecord(params) ? params.state : undefined; | ||
| } | ||
|
|
||
| function getRoutes(state: unknown): unknown[] | undefined { | ||
| if (!isRecord(state) || !isUnknownArray(state.routes)) { | ||
| return undefined; | ||
| } | ||
| return state.routes; | ||
| } | ||
|
|
||
| function getLastRouteByName(state: unknown, routeName: string): NavigationRouteLike | undefined { | ||
| const routes = getRoutes(state); | ||
| const route = routes?.findLast((candidate) => isRecord(candidate) && candidate.name === routeName); | ||
| return isRecord(route) ? route : undefined; | ||
| } | ||
|
|
||
| function getSearchRootParamsFromNestedNavigatorParams(params: unknown): SearchRootParams | undefined { | ||
| if (!params || typeof params !== 'object') { | ||
| return undefined; | ||
| } | ||
|
|
||
| const screen = 'screen' in params ? params.screen : undefined; | ||
| const nestedParams = 'params' in params ? params.params : undefined; | ||
| if (screen === SCREENS.SEARCH.ROOT) { | ||
| return isSearchRootParams(nestedParams) ? nestedParams : undefined; | ||
| } | ||
|
|
||
| if (screen === NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR) { | ||
| return getSearchRootParamsFromNestedNavigatorParams(nestedParams); | ||
| } | ||
|
|
||
| return undefined; | ||
| } | ||
|
|
||
| function getSearchRootParamsFromSearchNavigatorState(state: unknown): SearchRootParams | undefined { | ||
| const searchRootRoute = getLastRouteByName(state, SCREENS.SEARCH.ROOT); | ||
| const searchRootParams = searchRootRoute?.params; | ||
| return isSearchRootParams(searchRootParams) ? searchRootParams : undefined; | ||
| } | ||
|
|
||
| function getSearchRootParamsFromTabState(state: unknown): SearchRootParams | undefined { | ||
| const searchNavigatorRoute = getLastRouteByName(state, NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR); | ||
| return getSearchRootParamsFromNestedNavigatorParams(searchNavigatorRoute?.params) ?? getSearchRootParamsFromSearchNavigatorState(searchNavigatorRoute?.state); | ||
| } | ||
|
|
||
| function getSearchQueryJSONFromRouteParams(params: unknown) { | ||
| if (!isSearchRootParams(params)) { | ||
| return undefined; | ||
| } | ||
|
|
||
| return buildSearchQueryJSON(params.q, params.rawQuery); | ||
| } | ||
|
|
||
| function getCurrentSearchQueryJSON() { | ||
| const rootState = navigationRef.getRootState(); | ||
| const lastTabNavigator = rootState?.routes?.findLast((route) => route.name === NAVIGATORS.TAB_NAVIGATOR); | ||
| const lastSearchNavigator = lastTabNavigator?.state?.routes?.findLast((route) => route.name === NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR); | ||
| const tabStateFromParams = getParamsState(lastTabNavigator?.params); | ||
| const tabState = lastTabNavigator?.state ?? (lastTabNavigator?.key ? getPreservedNavigatorState(lastTabNavigator.key) : undefined) ?? tabStateFromParams; | ||
| const lastSearchNavigator = getLastRouteByName(tabState, NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR); | ||
| let lastSearchNavigatorState = lastSearchNavigator?.state; | ||
| if (!lastSearchNavigatorState) { | ||
| lastSearchNavigatorState = lastSearchNavigator?.key ? getPreservedNavigatorState(lastSearchNavigator?.key) : undefined; | ||
| lastSearchNavigatorState = lastSearchNavigator?.key ? getPreservedNavigatorState(lastSearchNavigator.key) : undefined; | ||
| } | ||
|
|
||
| const nestedSearchRootParams = | ||
| getSearchRootParamsFromNestedNavigatorParams(lastSearchNavigator?.params) ?? | ||
| getSearchRootParamsFromNestedNavigatorParams(lastTabNavigator?.params) ?? | ||
| getSearchRootParamsFromTabState(tabStateFromParams); | ||
|
|
||
| // When the SearchFullscreenNavigator has never been mounted (e.g. lazy tab not yet visited), | ||
| // neither .state nor the preserved state map will have an entry. Fall back to the default | ||
| // query that the navigator would use as its initialParams. | ||
| // neither .state nor the preserved state map will have an entry. Use nested route params when | ||
| // React Navigation provided them, otherwise fall back to the default initialParams query. | ||
| if (!lastSearchNavigatorState) { | ||
| const nestedQueryJSON = getSearchQueryJSONFromRouteParams(nestedSearchRootParams); | ||
| if (nestedQueryJSON) { | ||
| return nestedQueryJSON; | ||
| } | ||
| return buildSearchQueryJSON(buildSearchQueryString()); | ||
| } | ||
|
|
||
| const lastSearchRoute = lastSearchNavigatorState.routes.findLast((route) => route.name === SCREENS.SEARCH.ROOT); | ||
| if (!lastSearchRoute?.params) { | ||
| return; | ||
| } | ||
|
|
||
| const {q: searchParams, rawQuery} = lastSearchRoute.params as SearchFullscreenNavigatorParamList[typeof SCREENS.SEARCH.ROOT]; | ||
| const queryJSON = buildSearchQueryJSON(searchParams, rawQuery); | ||
| const lastSearchRoute = getLastRouteByName(lastSearchNavigatorState, SCREENS.SEARCH.ROOT); | ||
| const queryJSON = getSearchQueryJSONFromRouteParams(lastSearchRoute?.params); | ||
| if (!queryJSON) { | ||
| return; | ||
| } | ||
|
|
@@ -2240,6 +2326,10 @@ export { | |
| getDateModifierTitle, | ||
| applyContainsOperatorToTextFields, | ||
| serializeQueryJSONForBackend, | ||
| getLastRouteByName, | ||
| getParamsState, | ||
| getRoutes, | ||
| isSearchRootParams, | ||
| }; | ||
|
|
||
| export type {BuildUserReadableQueryStringParams}; | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.