diff --git a/contributingGuides/NAVIGATION.md b/contributingGuides/NAVIGATION.md index 9cee8c3b9f0a9..2d1e35c4ab1e8 100644 --- a/contributingGuides/NAVIGATION.md +++ b/contributingGuides/NAVIGATION.md @@ -711,10 +711,10 @@ Do not use dynamic routes when: - `path`: The URL suffix (e.g. `'verify-account'`). - `entryScreens`: List of screen names that are allowed to have this suffix appended (access control; see [Entry Screens (Access Control)](#entry-screens-access-control)). -`createDynamicRoute(suffix)` — [`createDynamicRoute.ts`](src/libs/Navigation/helpers/createDynamicRoute.ts). Accepts a `DynamicRouteSuffix` (from `DYNAMIC_ROUTES`), appends it to the current active route and returns the full route. Use the following when navigating to a dynamic route: +`createDynamicRoute(suffix)` — [`createDynamicRoute.ts`](src/libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute.ts). Accepts a `DynamicRouteSuffix` (from `DYNAMIC_ROUTES`), appends it to the current active route and returns the full route. Use the following when navigating to a dynamic route: ```ts -import createDynamicRoute from '@libs/Navigation/helpers/createDynamicRoute'; +import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; import ROUTES, {DYNAMIC_ROUTES} from '@src/ROUTES'; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 23188706a6e21..7f5e5f9edb4e7 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -99,6 +99,10 @@ const DYNAMIC_ROUTES = { path: 'owner-selector', entryScreens: [], }, + REPORT_SETTINGS_NAME: { + path: 'settings/name', + entryScreens: [SCREENS.REPORT_DETAILS.ROOT, SCREENS.RIGHT_MODAL.REPORT_SETTINGS, SCREENS.REPORT, SCREENS.RIGHT_MODAL.SEARCH_REPORT, SCREENS.SEARCH.ROOT], + }, ADDRESS_COUNTRY: { path: 'country', entryScreens: [ @@ -792,12 +796,6 @@ const ROUTES = { // eslint-disable-next-line no-restricted-syntax -- Legacy route generation getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/settings` as const, backTo), }, - REPORT_SETTINGS_NAME: { - route: 'r/:reportID/settings/name', - - // eslint-disable-next-line no-restricted-syntax -- Legacy route generation - getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/settings/name` as const, backTo), - }, REPORT_SETTINGS_NOTIFICATION_PREFERENCES: { route: 'r/:reportID/settings/notification-preferences', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 6c2599a91dc50..df4c03214ff58 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -443,7 +443,7 @@ const SCREENS = { REPORT_SETTINGS: { ROOT: 'Report_Settings_Root', - NAME: 'Report_Settings_Name', + DYNAMIC_SETTINGS_NAME: 'Dynamic_Report_Settings_Name', NOTIFICATION_PREFERENCES: 'Report_Settings_Notification_Preferences', WRITE_CAPABILITY: 'Report_Settings_Write_Capability', VISIBILITY: 'Report_Settings_Visibility', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 30d5ba47887e8..4c285a4287803 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -277,7 +277,7 @@ const ReportChangeApproverModalStackNavigator = createModalStackNavigator({ [SCREENS.REPORT_SETTINGS.ROOT]: () => require('../../../../pages/settings/Report/ReportSettingsPage').default, - [SCREENS.REPORT_SETTINGS.NAME]: () => require('../../../../pages/settings/Report/NamePage').default, + [SCREENS.REPORT_SETTINGS.DYNAMIC_SETTINGS_NAME]: () => require('../../../../pages/settings/Report/DynamicNamePage').default, [SCREENS.REPORT_SETTINGS.NOTIFICATION_PREFERENCES]: () => require('../../../../pages/settings/Report/NotificationPreferencePage').default, [SCREENS.REPORT_SETTINGS.WRITE_CAPABILITY]: () => require('../../../../pages/settings/Report/WriteCapabilityPage').default, [SCREENS.REPORT_SETTINGS.VISIBILITY]: () => require('../../../../pages/settings/Report/VisibilityPage').default, diff --git a/src/libs/Navigation/helpers/dynamicRoutesUtils/getStateForDynamicRoute.ts b/src/libs/Navigation/helpers/dynamicRoutesUtils/getStateForDynamicRoute.ts index ce35dc567c20a..ed257c9f9fb69 100644 --- a/src/libs/Navigation/helpers/dynamicRoutesUtils/getStateForDynamicRoute.ts +++ b/src/libs/Navigation/helpers/dynamicRoutesUtils/getStateForDynamicRoute.ts @@ -6,7 +6,7 @@ import splitPathAndQuery from './splitPathAndQuery'; type LeafRoute = { name: string; path: string; - params?: Record; + params?: Record; }; type NestedRoute = { @@ -54,7 +54,7 @@ function getRouteNamesForDynamicRoute(dynamicRouteName: DynamicRouteSuffix): str return null; } -function getStateForDynamicRoute(path: string, dynamicRouteName: keyof typeof DYNAMIC_ROUTES) { +function getStateForDynamicRoute(path: string, dynamicRouteName: keyof typeof DYNAMIC_ROUTES, parentRouteParams?: Record) { const routeConfig = getRouteNamesForDynamicRoute(DYNAMIC_ROUTES[dynamicRouteName].path); const [, query] = splitPathAndQuery(path); const params = getParamsFromQuery(query); @@ -67,12 +67,15 @@ function getStateForDynamicRoute(path: string, dynamicRouteName: keyof typeof DY const buildNestedState = (routes: string[], currentIndex: number): RouteNode => { const currentRoute = routes.at(currentIndex); - // If this is the last route, create leaf node with path + // If this is the last route, create leaf node with path and inherited params if (currentIndex === routes.length - 1) { return { name: currentRoute ?? '', path, - params, + params: { + ...params, + ...(parentRouteParams ?? {}), + }, }; } diff --git a/src/libs/Navigation/helpers/getStateFromPath.ts b/src/libs/Navigation/helpers/getStateFromPath.ts index 92119fdbf2281..2c4d7a11dcce2 100644 --- a/src/libs/Navigation/helpers/getStateFromPath.ts +++ b/src/libs/Navigation/helpers/getStateFromPath.ts @@ -39,7 +39,7 @@ function getStateFromPath(path: Route): PartialState { if (focusedRoute?.name) { if (entryScreens.includes(focusedRoute.name as Screen)) { // Generate navigation state for the dynamic route - const dynamicRouteState = getStateForDynamicRoute(normalizedPath, dynamicRoute as DynamicRouteKey); + const dynamicRouteState = getStateForDynamicRoute(normalizedPath, dynamicRoute as DynamicRouteKey, focusedRoute?.params as Record | undefined); return dynamicRouteState; } diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index b533a716fbef8..ec42c02a58fd3 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1396,9 +1396,7 @@ const config: LinkingOptions['config'] = { [SCREENS.REPORT_SETTINGS.ROOT]: { path: ROUTES.REPORT_SETTINGS.route, }, - [SCREENS.REPORT_SETTINGS.NAME]: { - path: ROUTES.REPORT_SETTINGS_NAME.route, - }, + [SCREENS.REPORT_SETTINGS.DYNAMIC_SETTINGS_NAME]: DYNAMIC_ROUTES.REPORT_SETTINGS_NAME.path, [SCREENS.REPORT_SETTINGS.NOTIFICATION_PREFERENCES]: { path: ROUTES.REPORT_SETTINGS_NOTIFICATION_PREFERENCES.route, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 072269fe167f3..b1f9291ce4f2a 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1683,10 +1683,8 @@ type ReportSettingsNavigatorParamList = { // eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md backTo?: Routes; }; - [SCREENS.REPORT_SETTINGS.NAME]: { + [SCREENS.REPORT_SETTINGS.DYNAMIC_SETTINGS_NAME]: { reportID: string; - // eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md - backTo?: Routes; }; [SCREENS.REPORT_SETTINGS.NOTIFICATION_PREFERENCES]: { reportID: string; diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index da4bb7a036755..3b94341c71c1e 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -43,6 +43,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import getBase62ReportID from '@libs/getBase62ReportID'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; +import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {ReportDetailsNavigatorParamList, RightModalNavigatorParamList} from '@libs/Navigation/types'; @@ -125,7 +126,7 @@ import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; -import ROUTES from '@src/ROUTES'; +import ROUTES, {DYNAMIC_ROUTES} from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; @@ -818,7 +819,9 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail furtherDetails={chatRoomSubtitle && !isGroupChat ? additionalRoomDetails : ''} furtherDetailsNumberOfLines={isWorkspaceChat ? 0 : undefined} furtherDetailsStyle={isWorkspaceChat ? [styles.textAlignCenter, styles.breakWord] : undefined} - onPress={() => Navigation.navigate(ROUTES.REPORT_SETTINGS_NAME.getRoute(report.reportID, backTo))} + onPress={() => { + Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.REPORT_SETTINGS_NAME.path)); + }} numberOfLinesTitle={isThread ? 2 : 0} shouldBreakWord /> diff --git a/src/pages/inbox/report/withReportOrNotFound.tsx b/src/pages/inbox/report/withReportOrNotFound.tsx index 1a701c21fc2bc..1dcb01d41caf4 100644 --- a/src/pages/inbox/report/withReportOrNotFound.tsx +++ b/src/pages/inbox/report/withReportOrNotFound.tsx @@ -53,6 +53,7 @@ type ScreenProps = | PlatformStackScreenProps | PlatformStackScreenProps | PlatformStackScreenProps + | PlatformStackScreenProps | PlatformStackScreenProps | PlatformStackScreenProps | PlatformStackScreenProps; diff --git a/src/pages/settings/Report/NamePage.tsx b/src/pages/settings/Report/DynamicNamePage.tsx similarity index 58% rename from src/pages/settings/Report/NamePage.tsx rename to src/pages/settings/Report/DynamicNamePage.tsx index f64465a24782e..8395590444673 100644 --- a/src/pages/settings/Report/NamePage.tsx +++ b/src/pages/settings/Report/DynamicNamePage.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import useDynamicBackPath from '@hooks/useDynamicBackPath'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import {isGroupChat, isTripRoom} from '@libs/ReportUtils'; import type {ReportSettingsNavigatorParamList} from '@navigation/types'; @@ -6,12 +7,15 @@ import GroupChatNameEditPage from '@pages/GroupChatNameEditPage'; import withReportOrNotFound from '@pages/inbox/report/withReportOrNotFound'; import type {WithReportOrNotFoundProps} from '@pages/inbox/report/withReportOrNotFound'; import TripChatNameEditPage from '@pages/TripChatNameEditPage'; +import {DYNAMIC_ROUTES} from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import RoomNamePage from './RoomNamePage'; -type NamePageProps = WithReportOrNotFoundProps & PlatformStackScreenProps; +type DynamicNamePageProps = WithReportOrNotFoundProps & PlatformStackScreenProps; + +function DynamicNamePage({report}: DynamicNamePageProps) { + const backPath = useDynamicBackPath(DYNAMIC_ROUTES.REPORT_SETTINGS_NAME.path); -function NamePage({report}: NamePageProps) { if (isTripRoom(report)) { return ; } @@ -20,7 +24,12 @@ function NamePage({report}: NamePageProps) { return ; } - return ; + return ( + + ); } -export default withReportOrNotFound()(NamePage); +export default withReportOrNotFound()(DynamicNamePage); diff --git a/src/pages/settings/Report/RoomNamePage.tsx b/src/pages/settings/Report/RoomNamePage.tsx index 9cea524517111..b3da6b9fd460a 100644 --- a/src/pages/settings/Report/RoomNamePage.tsx +++ b/src/pages/settings/Report/RoomNamePage.tsx @@ -1,4 +1,4 @@ -import {useIsFocused, useRoute} from '@react-navigation/native'; +import {useIsFocused} from '@react-navigation/native'; import React, {useCallback, useRef} from 'react'; import {View} from 'react-native'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; @@ -15,35 +15,31 @@ import useReportIsArchived from '@hooks/useReportIsArchived'; import useThemeStyles from '@hooks/useThemeStyles'; import {addErrorMessage} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; -import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {ReportSettingsNavigatorParamList} from '@libs/Navigation/types'; import {shouldDisableRename} from '@libs/ReportUtils'; import {isExistingRoomName, isReservedRoomName, isValidRoomNameWithoutLimits} from '@libs/ValidationUtils'; import {updatePolicyRoomName as updatePolicyRoomNameReportAction} from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; -import type SCREENS from '@src/SCREENS'; +import type {Route} from '@src/ROUTES'; import INPUT_IDS from '@src/types/form/RoomNameForm'; import type {Report} from '@src/types/onyx'; type RoomNamePageProps = { report: Report; + navigateBackTo?: Route; }; -function RoomNamePage({report}: RoomNamePageProps) { - const route = useRoute>(); +function RoomNamePage({report, navigateBackTo}: RoomNamePageProps) { const styles = useThemeStyles(); const roomNameInputRef = useRef(null); const isFocused = useIsFocused(); const {translate} = useLocalize(); - const reportID = report?.reportID; const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT); const isReportArchived = useReportIsArchived(report?.reportID); const goBack = useCallback(() => { - Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.goBack(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(reportID, route.params.backTo))); - }, [reportID, route.params.backTo]); + Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.goBack(navigateBackTo)); + }, [navigateBackTo]); const validate = useCallback( (values: FormOnyxValues) => { diff --git a/tests/navigation/getStateForDynamicRouteTests.ts b/tests/navigation/getStateForDynamicRouteTests.ts index cf3dc7c588f0e..b1f337ed6c859 100644 --- a/tests/navigation/getStateForDynamicRouteTests.ts +++ b/tests/navigation/getStateForDynamicRouteTests.ts @@ -31,6 +31,7 @@ jest.mock('@src/ROUTES', () => ({ type LeafRoute = { name: string; path: string; + params?: Record; }; type NestedRoute = { @@ -111,4 +112,23 @@ describe('getStateForDynamicRoute', () => { expect(state?.index).toBe(0); expect(Array.isArray(state?.routes)).toBe(true); }); + + it('should inherit parent route params on the leaf node', () => { + const path = '/r/12345/settings/name'; + const parentParams = {reportID: '12345'}; + const result = getStateForDynamicRoute(path, KEY_TEST as unknown as keyof typeof DYNAMIC_ROUTES, parentParams); + + const rootRoute = result.routes.at(0) as NestedRoute | undefined; + const leafRoute = rootRoute?.state.routes.at(0) as LeafRoute | undefined; + expect(leafRoute?.params).toEqual(parentParams); + }); + + it('should not include params on the leaf node when parentRouteParams is undefined', () => { + const path = '/some/path/test-path'; + const result = getStateForDynamicRoute(path, KEY_TEST as unknown as keyof typeof DYNAMIC_ROUTES); + + const rootRoute = result.routes.at(0) as NestedRoute | undefined; + const leafRoute = rootRoute?.state.routes.at(0) as LeafRoute | undefined; + expect(leafRoute?.params).toBeUndefined(); + }); }); diff --git a/tests/navigation/getStateFromPathTests.ts b/tests/navigation/getStateFromPathTests.ts index ffa5cb2ace840..5808b6f164e4d 100644 --- a/tests/navigation/getStateFromPathTests.ts +++ b/tests/navigation/getStateFromPathTests.ts @@ -57,9 +57,10 @@ describe('getStateFromPath', () => { it('should generate dynamic state when authorized screen is focused', () => { const fullPath = '/settings/wallet/verify-account'; const baseRouteState = {routes: [{name: 'Wallet'}]}; + const focusedRouteParams = {walletID: '456'}; mockRNGetStateFromPath.mockReturnValue(baseRouteState); - mockFindFocusedRoute.mockReturnValue({name: 'Wallet'}); + mockFindFocusedRoute.mockReturnValue({name: 'Wallet', params: focusedRouteParams}); const expectedDynamicState = {routes: [{name: 'DynamicRoot'}]}; mockGetStateForDynamicRoute.mockReturnValue(expectedDynamicState); @@ -67,7 +68,7 @@ describe('getStateFromPath', () => { const result = getStateFromPath(fullPath as unknown as Route); expect(result).toBe(expectedDynamicState); - expect(mockGetStateForDynamicRoute).toHaveBeenCalledWith(fullPath, 'VERIFY_ACCOUNT'); + expect(mockGetStateForDynamicRoute).toHaveBeenCalledWith(fullPath, 'VERIFY_ACCOUNT', focusedRouteParams); }); it('should fallback to standard RN parsing if focused screen is NOT authorized for dynamic route', () => {