-
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
base: main
Are you sure you want to change the base?
Changes from all commits
f5d3620
8bcd72e
bbe24fc
00dbfda
e6999a2
9c0f2a1
154f899
d714b58
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 |
|---|---|---|
|
|
@@ -22,6 +22,12 @@ type NavigateAfterExpenseCreateParams = { | |
| shouldNavigate?: boolean; | ||
| }; | ||
|
|
||
| function getNavigateAfterCreateSearchNavigatorState() { | ||
| const rootState = navigationRef.getRootState(); | ||
| const searchNavigatorRoute = rootState?.routes?.findLast((route) => route.name === NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR); | ||
| return searchNavigatorRoute?.state; | ||
| } | ||
|
|
||
|
Comment on lines
+25
to
+30
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 +67,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 = () => { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -56,6 +56,13 @@ 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 = { | ||
| key?: unknown; | ||
| name?: unknown; | ||
| params?: unknown; | ||
| state?: unknown; | ||
| }; | ||
|
Comment on lines
+60
to
+65
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.) |
||
|
|
||
| // This map contains chars that match each operator | ||
| const operatorToCharMap = { | ||
|
|
@@ -1916,29 +1923,120 @@ 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 isRecord(value: unknown): value is Record<string, unknown> { | ||
| return !!value && typeof value === 'object' && !Array.isArray(value); | ||
| } | ||
|
Comment on lines
+1936
to
+1938
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: // SearchQueryUtils.ts (line 1936)
function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === 'object' && !Array.isArray(value);
}
// ImportOnyxStateUtils.ts (line 13)
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && !Array.isArray(value) && value !== null;
}Impact: Code duplication. Two identical type guards in the same code base. Recommendation: Extract |
||
|
|
||
| function isUnknownArray(value: unknown): value is unknown[] { | ||
| return Array.isArray(value); | ||
| } | ||
|
Comment on lines
+1940
to
+1942
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 |
||
|
|
||
| function getRouteKey(route: unknown): string | undefined { | ||
| return isRecord(route) && typeof route.key === 'string' ? route.key : undefined; | ||
| } | ||
|
|
||
| function getRouteParams(route: unknown): unknown { | ||
| return isRecord(route) ? route.params : undefined; | ||
| } | ||
|
|
||
| function getRouteState(route: unknown): unknown { | ||
| return isRecord(route) ? route.state : undefined; | ||
| } | ||
|
|
||
| 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 = getRouteParams(searchRootRoute); | ||
| return isSearchRootParams(searchRootParams) ? searchRootParams : undefined; | ||
| } | ||
|
|
||
| function getSearchRootParamsFromTabState(state: unknown): SearchRootParams | undefined { | ||
| const searchNavigatorRoute = getLastRouteByName(state, NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR); | ||
| return getSearchRootParamsFromNestedNavigatorParams(getRouteParams(searchNavigatorRoute)) ?? getSearchRootParamsFromSearchNavigatorState(getRouteState(searchNavigatorRoute)); | ||
| } | ||
|
|
||
| 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); | ||
| let lastSearchNavigatorState = lastSearchNavigator?.state; | ||
| 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 = getRouteState(lastSearchNavigator); | ||
| if (!lastSearchNavigatorState) { | ||
| lastSearchNavigatorState = lastSearchNavigator?.key ? getPreservedNavigatorState(lastSearchNavigator?.key) : undefined; | ||
| const lastSearchNavigatorKey = getRouteKey(lastSearchNavigator); | ||
| lastSearchNavigatorState = lastSearchNavigatorKey ? getPreservedNavigatorState(lastSearchNavigatorKey) : undefined; | ||
| } | ||
|
|
||
| const nestedSearchRootParams = | ||
| getSearchRootParamsFromNestedNavigatorParams(getRouteParams(lastSearchNavigator)) ?? | ||
| 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(getRouteParams(lastSearchRoute)); | ||
| if (!queryJSON) { | ||
| return; | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.