Skip to content

Commit d27018c

Browse files
authored
Merge pull request #57607 from software-mansion-labs/kicu/add-search-navigator
[Better Expense Report View] Add new navigator to allow for new SearchMoneyRequestReport screen
2 parents a7794af + ee451eb commit d27018c

30 files changed

+441
-172
lines changed

src/NAVIGATORS.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@ export default {
1414
REPORTS_SPLIT_NAVIGATOR: 'ReportsSplitNavigator',
1515
SETTINGS_SPLIT_NAVIGATOR: 'SettingsSplitNavigator',
1616
WORKSPACE_SPLIT_NAVIGATOR: 'WorkspaceSplitNavigator',
17+
SEARCH_FULLSCREEN_NAVIGATOR: 'SearchFullscreenNavigator',
1718
} as const;

src/ROUTES.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,13 @@ const ROUTES = {
7878
return getUrlWithBackToParam(baseRoute, backTo);
7979
},
8080
},
81+
SEARCH_MONEY_REQUEST_REPORT: {
82+
route: 'search/report/:reportID',
83+
getRoute: ({reportID, backTo}: {reportID: string; backTo?: string}) => {
84+
const baseRoute = `search/view/${reportID}` as const;
85+
return getUrlWithBackToParam(baseRoute, backTo);
86+
},
87+
},
8188
TRANSACTION_HOLD_REASON_RHP: 'search/hold',
8289

8390
// This is a utility route used to go to the user's concierge chat, or the sign-in page if the user's not authenticated

src/SCREENS.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const SCREENS = {
3737
},
3838
SEARCH: {
3939
ROOT: 'Search_Root',
40+
MONEY_REQUEST_REPORT: 'Search_Money_Request_Report',
4041
REPORT_RHP: 'Search_Report_RHP',
4142
ADVANCED_FILTERS_RHP: 'Search_Advanced_Filters_RHP',
4243
ADVANCED_FILTERS_DATE_RHP: 'Search_Advanced_Filters_Date_RHP',

src/components/Navigation/BottomTabBar/index.tsx

Lines changed: 2 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import * as Expensicons from '@components/Icon/Expensicons';
77
import DebugTabView from '@components/Navigation/DebugTabView';
88
import {PressableWithFeedback} from '@components/Pressable';
99
import {useProductTrainingContext} from '@components/ProductTrainingContext';
10-
import type {SearchQueryString} from '@components/Search/types';
1110
import Text from '@components/Text';
1211
import EducationalTooltip from '@components/Tooltip/EducationalTooltip';
1312
import useActiveWorkspace from '@hooks/useActiveWorkspace';
@@ -19,15 +18,14 @@ import useThemeStyles from '@hooks/useThemeStyles';
1918
import clearSelectedText from '@libs/clearSelectedText/clearSelectedText';
2019
import getPlatform from '@libs/getPlatform';
2120
import interceptAnonymousUser from '@libs/interceptAnonymousUser';
22-
import {getPolicy} from '@libs/PolicyUtils';
23-
import {buildCannedSearchQuery, buildSearchQueryJSON, buildSearchQueryString} from '@libs/SearchQueryUtils';
21+
import {buildCannedSearchQuery} from '@libs/SearchQueryUtils';
2422
import type {BrickRoad} from '@libs/WorkspacesSettingsUtils';
2523
import {getChatTabBrickRoad} from '@libs/WorkspacesSettingsUtils';
2624
import {getPreservedSplitNavigatorState} from '@navigation/AppNavigator/createSplitNavigator/usePreserveSplitNavigatorState';
2725
import {isFullScreenName} from '@navigation/helpers/isNavigatorName';
2826
import Navigation from '@navigation/Navigation';
2927
import navigationRef from '@navigation/navigationRef';
30-
import type {AuthScreensParamList, RootNavigatorParamList, State, WorkspaceSplitNavigatorParamList} from '@navigation/types';
28+
import type {WorkspaceSplitNavigatorParamList} from '@navigation/types';
3129
import BottomTabAvatar from '@pages/home/sidebar/BottomTabAvatar';
3230
import BottomTabBarFloatingActionButton from '@pages/home/sidebar/BottomTabBarFloatingActionButton';
3331
import variables from '@styles/variables';
@@ -43,34 +41,6 @@ type BottomTabBarProps = {
4341
isTooltipAllowed?: boolean;
4442
};
4543

46-
/**
47-
* Returns SearchQueryString that has policyID correctly set.
48-
*
49-
* When we're coming back to Search Screen we might have pre-existing policyID inside SearchQuery.
50-
* There are 2 cases when we might want to remove this `policyID`:
51-
* - if Policy was removed in another screen
52-
* - if WorkspaceSwitcher was used to globally unset a policyID
53-
* Otherwise policyID will be inserted into query
54-
*/
55-
function handleQueryWithPolicyID(query: SearchQueryString, activePolicyID?: string): SearchQueryString {
56-
const queryJSON = buildSearchQueryJSON(query);
57-
if (!queryJSON) {
58-
return query;
59-
}
60-
61-
const policyID = activePolicyID ?? queryJSON.policyID;
62-
const policy = getPolicy(policyID);
63-
64-
// In case policy is missing or there is no policy currently selected via WorkspaceSwitcher we remove it
65-
if (!activePolicyID || !policy) {
66-
delete queryJSON.policyID;
67-
} else {
68-
queryJSON.policyID = policyID;
69-
}
70-
71-
return buildSearchQueryString(queryJSON);
72-
}
73-
7444
function BottomTabBar({selectedTab, isTooltipAllowed = false}: BottomTabBarProps) {
7545
const theme = useTheme();
7646
const styles = useThemeStyles();
@@ -108,22 +78,6 @@ function BottomTabBar({selectedTab, isTooltipAllowed = false}: BottomTabBarProps
10878
}
10979
clearSelectedText();
11080
interceptAnonymousUser(() => {
111-
const rootState = navigationRef.getRootState() as State<RootNavigatorParamList>;
112-
const lastSearchRoute = rootState.routes.findLast((route) => route.name === SCREENS.SEARCH.ROOT);
113-
114-
if (lastSearchRoute) {
115-
const {q, ...rest} = lastSearchRoute.params as AuthScreensParamList[typeof SCREENS.SEARCH.ROOT];
116-
const cleanedQuery = handleQueryWithPolicyID(q, activeWorkspaceID);
117-
118-
Navigation.navigate(
119-
ROUTES.SEARCH_ROOT.getRoute({
120-
query: cleanedQuery,
121-
...rest,
122-
}),
123-
);
124-
return;
125-
}
126-
12781
const defaultCannedQuery = buildCannedSearchQuery();
12882
// when navigating to search we might have an activePolicyID set from workspace switcher
12983
const query = activeWorkspaceID ? `${defaultCannedQuery} ${CONST.SEARCH.SYNTAX_ROOT_KEYS.POLICY_ID}:${activeWorkspaceID}` : defaultCannedQuery;
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {SIDEBAR_TO_SPLIT} from '@libs/Navigation/linkingConfig/RELATIONS';
2+
import NAVIGATORS from '@src/NAVIGATORS';
23
import SCREENS from '@src/SCREENS';
34

4-
const SCREENS_WITH_BOTTOM_TAB_BAR = [...Object.keys(SIDEBAR_TO_SPLIT), SCREENS.SEARCH.ROOT, SCREENS.SETTINGS.WORKSPACES];
5+
const SCREENS_WITH_BOTTOM_TAB_BAR = [...Object.keys(SIDEBAR_TO_SPLIT), NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR, SCREENS.SETTINGS.WORKSPACES];
56

67
export default SCREENS_WITH_BOTTOM_TAB_BAR;

src/components/Search/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import {
3535
} from '@libs/SearchUIUtils';
3636
import {isOnHold} from '@libs/TransactionUtils';
3737
import Navigation from '@navigation/Navigation';
38-
import type {AuthScreensParamList} from '@navigation/types';
38+
import type {SearchFullscreenNavigatorParamList} from '@navigation/types';
3939
import EmptySearchView from '@pages/Search/EmptySearchView';
4040
import variables from '@styles/variables';
4141
import CONST from '@src/CONST';
@@ -133,7 +133,7 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo
133133
// We need to use isSmallScreenWidth instead of shouldUseNarrowLayout for enabling the selection mode on small screens only
134134
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
135135
const {isSmallScreenWidth, isLargeScreenWidth} = useResponsiveLayout();
136-
const navigation = useNavigation<PlatformStackNavigationProp<AuthScreensParamList>>();
136+
const navigation = useNavigation<PlatformStackNavigationProp<SearchFullscreenNavigatorParamList>>();
137137
const isFocused = useIsFocused();
138138
const [lastNonEmptySearchResults, setLastNonEmptySearchResults] = useState<SearchResults | undefined>(undefined);
139139
const {

src/libs/Navigation/AppNavigator/AuthScreens.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ import NetworkConnection from '@libs/NetworkConnection';
3434
import onyxSubscribe from '@libs/onyxSubscribe';
3535
import Pusher from '@libs/Pusher';
3636
import PusherConnectionManager from '@libs/PusherConnectionManager';
37-
import * as SearchQueryUtils from '@libs/SearchQueryUtils';
3837
import * as SessionUtils from '@libs/SessionUtils';
3938
import ConnectionCompletePage from '@pages/ConnectionCompletePage';
4039
import NotFoundPage from '@pages/ErrorPage/NotFoundPage';
@@ -97,7 +96,7 @@ const loadWorkspaceJoinUser = () => require<ReactComponentModule>('@pages/worksp
9796
const loadReportSplitNavigator = () => require<ReactComponentModule>('./Navigators/ReportsSplitNavigator').default;
9897
const loadSettingsSplitNavigator = () => require<ReactComponentModule>('./Navigators/SettingsSplitNavigator').default;
9998
const loadWorkspaceSplitNavigator = () => require<ReactComponentModule>('./Navigators/WorkspaceSplitNavigator').default;
100-
const loadSearchPage = () => require<ReactComponentModule>('@pages/Search/SearchPage').default;
99+
const loadSearchNavigator = () => require<ReactComponentModule>('./Navigators/SearchFullscreenNavigator').default;
101100

102101
function initializePusher() {
103102
return Pusher.init({
@@ -455,7 +454,7 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie
455454
return (
456455
<ComposeProviders components={[OptionsListContextProvider, ActiveWorkspaceContextProvider, ReportIDsContextProvider, SearchContextProvider]}>
457456
<RootStack.Navigator
458-
persistentScreens={[NAVIGATORS.REPORTS_SPLIT_NAVIGATOR, SCREENS.SEARCH.ROOT]}
457+
persistentScreens={[NAVIGATORS.REPORTS_SPLIT_NAVIGATOR, NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR]}
459458
// @ts-expect-error SidePane is a custom screen option that was added in a patch (when we migrate to react-navigation v7 we can use screenLayout instead)
460459
screenOptions={{sidePane: SidePaneWithOverlay}}
461460
>
@@ -471,10 +470,9 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie
471470
getComponent={loadSettingsSplitNavigator}
472471
/>
473472
<RootStack.Screen
474-
name={SCREENS.SEARCH.ROOT}
473+
name={NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR}
475474
options={rootNavigatorScreenOptions.fullScreen}
476-
getComponent={loadSearchPage}
477-
initialParams={{q: SearchQueryUtils.buildSearchQueryString()}}
475+
getComponent={loadSearchNavigator}
478476
/>
479477
<RootStack.Screen
480478
name={NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import React from 'react';
2+
import type {SearchFullscreenNavigatorParamList} from '@libs/Navigation/types';
3+
import * as SearchQueryUtils from '@libs/SearchQueryUtils';
4+
import createSearchFullscreenNavigator from '@navigation/AppNavigator/createSearchFullscreenNavigator';
5+
import FreezeWrapper from '@navigation/AppNavigator/FreezeWrapper';
6+
import useRootNavigatorScreenOptions from '@navigation/AppNavigator/useRootNavigatorScreenOptions';
7+
import SCREENS from '@src/SCREENS';
8+
import type ReactComponentModule from '@src/types/utils/ReactComponentModule';
9+
10+
const loadSearchPage = () => require<ReactComponentModule>('@pages/Search/SearchPage').default;
11+
const loadSearchMoneyReportPage = () => require<ReactComponentModule>('@pages/Search/SearchMoneyRequestReportPage').default;
12+
13+
const Stack = createSearchFullscreenNavigator<SearchFullscreenNavigatorParamList>();
14+
15+
function SearchFullscreenNavigator() {
16+
const rootNavigatorScreenOptions = useRootNavigatorScreenOptions();
17+
18+
return (
19+
<FreezeWrapper>
20+
<Stack.Navigator
21+
screenOptions={rootNavigatorScreenOptions.fullScreen}
22+
defaultCentralScreen={SCREENS.SEARCH.ROOT}
23+
>
24+
<Stack.Screen
25+
name={SCREENS.SEARCH.ROOT}
26+
getComponent={loadSearchPage}
27+
initialParams={{q: SearchQueryUtils.buildSearchQueryString()}}
28+
/>
29+
<Stack.Screen
30+
name={SCREENS.SEARCH.MONEY_REQUEST_REPORT}
31+
getComponent={loadSearchMoneyReportPage}
32+
/>
33+
</Stack.Navigator>
34+
</FreezeWrapper>
35+
);
36+
}
37+
38+
SearchFullscreenNavigator.displayName = 'SearchFullscreenNavigator';
39+
40+
export default SearchFullscreenNavigator;

src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts

Lines changed: 52 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,11 @@ function handleOpenWorkspaceSplitAction(
7777
}
7878

7979
/**
80-
* Handles the SWITCH_POLICY_ID action.
81-
* Information about the currently selected policy can be found in the last ReportsSplitNavigator or Search_Root.
82-
* As the policy can only be changed from Search or Inbox Tab, after changing the policy a new ReportsSplitNavigator or Search_Root with the changed policy has to be pushed to the navigation state.
80+
* Handles the SWITCH_POLICY_ID action for `SearchFullscreenNavigator`.
81+
* Information about the currently selected policy can be found in the last Search_Root.
82+
* After user changes the policy while on Search, a new Search_Root with the changed policy inside query param has to be pushed to the navigation state.
8383
*/
84-
function handleSwitchPolicyID(
84+
function handleSwitchPolicyIDFromSearchAction(
8585
state: StackNavigationState<ParamListBase>,
8686
action: SwitchPolicyIdActionType,
8787
configOptions: RouterConfigOptions,
@@ -110,6 +110,23 @@ function handleSwitchPolicyID(
110110
setActiveWorkspaceID(action.payload.policyID);
111111
return stackRouter.getStateForAction(state, newAction, configOptions);
112112
}
113+
// We don't have other navigators that should handle switch policy action.
114+
return null;
115+
}
116+
117+
/**
118+
* Handles the SWITCH_POLICY_ID action for `ReportsSplitNavigator`.
119+
* Information about the currently selected policy can be found in the last ReportsSplitNavigator.
120+
* After user changes the policy while on Inbox, a new ReportsSplitNavigator with the changed policy has to be pushed to the navigation state.
121+
*/
122+
function handleSwitchPolicyIDAction(
123+
state: StackNavigationState<ParamListBase>,
124+
action: SwitchPolicyIdActionType,
125+
configOptions: RouterConfigOptions,
126+
stackRouter: Router<StackNavigationState<ParamListBase>, CommonActions.Action | StackActionType>,
127+
setActiveWorkspaceID: (workspaceID: string | undefined) => void,
128+
) {
129+
const lastRoute = state.routes.at(-1);
113130
if (lastRoute?.name === NAVIGATORS.REPORTS_SPLIT_NAVIGATOR) {
114131
const newAction = StackActions.push(NAVIGATORS.REPORTS_SPLIT_NAVIGATOR, {policyID: action.payload.policyID});
115132

@@ -182,37 +199,42 @@ function handlePushSearchPageAction(
182199
stackRouter: Router<StackNavigationState<ParamListBase>, CommonActions.Action | StackActionType>,
183200
setActiveWorkspaceID: (workspaceID: string | undefined) => void,
184201
) {
185-
const currentParams = action.payload.params as RootNavigatorParamList[typeof SCREENS.SEARCH.ROOT];
186-
const queryJSON = SearchQueryUtils.buildSearchQueryJSON(currentParams.q);
187-
188-
if (!queryJSON) {
189-
return null;
190-
}
202+
let updatedAction = action;
203+
const currentParams = action.payload.params as RootNavigatorParamList[typeof NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR];
204+
if (currentParams?.screen === SCREENS.SEARCH.ROOT) {
205+
const searchParams = currentParams?.params;
206+
const queryJSON = SearchQueryUtils.buildSearchQueryJSON(searchParams.q);
207+
if (!queryJSON) {
208+
return null;
209+
}
191210

192-
if (!queryJSON.policyID) {
193-
const policyID = getPolicyIDFromState(state as State<RootNavigatorParamList>);
211+
if (!queryJSON.policyID) {
212+
const policyID = getPolicyIDFromState(state as State<RootNavigatorParamList>);
194213

195-
if (policyID) {
196-
queryJSON.policyID = policyID;
214+
if (policyID) {
215+
queryJSON.policyID = policyID;
216+
} else {
217+
delete queryJSON.policyID;
218+
}
197219
} else {
198-
delete queryJSON.policyID;
220+
setActiveWorkspaceID(queryJSON.policyID);
199221
}
200-
} else {
201-
setActiveWorkspaceID(queryJSON.policyID);
202-
}
203222

204-
const modifiedAction = {
205-
...action,
206-
payload: {
207-
...action.payload,
208-
params: {
209-
...action.payload.params,
210-
q: SearchQueryUtils.buildSearchQueryString(queryJSON),
223+
updatedAction = {
224+
...action,
225+
payload: {
226+
...action.payload,
227+
params: {
228+
...action.payload.params,
229+
params: {
230+
q: SearchQueryUtils.buildSearchQueryString(queryJSON),
231+
},
232+
},
211233
},
212-
},
213-
};
234+
};
235+
}
214236

215-
return stackRouter.getStateForAction(state, modifiedAction, configOptions);
237+
return stackRouter.getStateForAction(state, updatedAction, configOptions);
216238
}
217239

218240
/**
@@ -253,7 +275,8 @@ export {
253275
handleDismissModalAction,
254276
handlePushReportSplitAction,
255277
handlePushSearchPageAction,
256-
handleSwitchPolicyID,
278+
handleSwitchPolicyIDAction,
279+
handleSwitchPolicyIDFromSearchAction,
257280
handleNavigatingToModalFromModal,
258281
workspaceSplitsWithoutEnteringAnimation,
259282
reportsSplitsWithEnteringAnimation,

src/libs/Navigation/AppNavigator/createRootStackNavigator/RootStackRouter.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,14 @@ import isSideModalNavigator from '@libs/Navigation/helpers/isSideModalNavigator'
88
import * as Welcome from '@userActions/Welcome';
99
import CONST from '@src/CONST';
1010
import NAVIGATORS from '@src/NAVIGATORS';
11-
import SCREENS from '@src/SCREENS';
12-
import * as GetStateForActionHandlers from './GetStateForActionHandlers';
11+
import {
12+
handleDismissModalAction,
13+
handleNavigatingToModalFromModal,
14+
handleOpenWorkspaceSplitAction,
15+
handlePushReportSplitAction,
16+
handlePushSearchPageAction,
17+
handleSwitchPolicyIDAction,
18+
} from './GetStateForActionHandlers';
1319
import syncBrowserHistory from './syncBrowserHistory';
1420
import type {DismissModalActionType, OpenWorkspaceSplitActionType, PushActionType, RootStackNavigatorAction, RootStackNavigatorRouterOptions, SwitchPolicyIdActionType} from './types';
1521

@@ -65,24 +71,24 @@ function RootStackRouter(options: RootStackNavigatorRouterOptions) {
6571
...stackRouter,
6672
getStateForAction(state: StackNavigationState<ParamListBase>, action: RootStackNavigatorAction, configOptions: RouterConfigOptions) {
6773
if (isOpenWorkspaceSplitAction(action)) {
68-
return GetStateForActionHandlers.handleOpenWorkspaceSplitAction(state, action, configOptions, stackRouter);
74+
return handleOpenWorkspaceSplitAction(state, action, configOptions, stackRouter);
6975
}
7076

7177
if (isSwitchPolicyIdAction(action)) {
72-
return GetStateForActionHandlers.handleSwitchPolicyID(state, action, configOptions, stackRouter, setActiveWorkspaceID);
78+
return handleSwitchPolicyIDAction(state, action, configOptions, stackRouter, setActiveWorkspaceID);
7379
}
7480

7581
if (isDismissModalAction(action)) {
76-
return GetStateForActionHandlers.handleDismissModalAction(state, configOptions, stackRouter);
82+
return handleDismissModalAction(state, configOptions, stackRouter);
7783
}
7884

7985
if (isPushAction(action)) {
8086
if (action.payload.name === NAVIGATORS.REPORTS_SPLIT_NAVIGATOR) {
81-
return GetStateForActionHandlers.handlePushReportSplitAction(state, action, configOptions, stackRouter, setActiveWorkspaceID);
87+
return handlePushReportSplitAction(state, action, configOptions, stackRouter, setActiveWorkspaceID);
8288
}
8389

84-
if (action.payload.name === SCREENS.SEARCH.ROOT) {
85-
return GetStateForActionHandlers.handlePushSearchPageAction(state, action, configOptions, stackRouter, setActiveWorkspaceID);
90+
if (action.payload.name === NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR) {
91+
return handlePushSearchPageAction(state, action, configOptions, stackRouter, setActiveWorkspaceID);
8692
}
8793
}
8894

@@ -93,7 +99,7 @@ function RootStackRouter(options: RootStackNavigatorRouterOptions) {
9399
}
94100

95101
if (isNavigatingToModalFromModal(state, action)) {
96-
return GetStateForActionHandlers.handleNavigatingToModalFromModal(state, action, configOptions, stackRouter);
102+
return handleNavigatingToModalFromModal(state, action, configOptions, stackRouter);
97103
}
98104

99105
return stackRouter.getStateForAction(state, action, configOptions);

0 commit comments

Comments
 (0)