From be05bcb7f2e4ac327a43f18939da230c24525aa5 Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 21 May 2026 08:08:58 -0700 Subject: [PATCH 1/3] Migrate NetSuiteCustomListSelectorModal to a @react-navigation modal screen --- src/ROUTES.ts | 9 ++ src/SCREENS.ts | 1 + .../ModalStackNavigators/index.tsx | 2 + .../RELATIONS/WORKSPACE_TO_RHP.ts | 1 + src/libs/Navigation/linkingConfig/config.ts | 1 + src/libs/Navigation/types.ts | 3 + .../NetSuiteCustomListPicker.tsx | 54 ++----- ...tsx => NetSuiteCustomListSelectorPage.tsx} | 76 +++++----- .../subPages/ChooseCustomListStep.tsx | 1 - .../ui/NetSuiteCustomListSelectorPageTest.tsx | 135 ++++++++++++++++++ 10 files changed, 204 insertions(+), 79 deletions(-) rename src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/{NetSuiteCustomListSelectorModal.tsx => NetSuiteCustomListSelectorPage.tsx} (59%) create mode 100644 tests/ui/NetSuiteCustomListSelectorPageTest.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 685f14201a98..62a66505c70f 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -3623,6 +3623,15 @@ const ROUTES = { return `workspaces/${policyID}/accounting/netsuite/import/custom-list/new/${subPage}${action ? `/${action}` : ''}` as const; }, }, + POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_LIST_SELECTOR: { + route: 'workspaces/:policyID/accounting/netsuite/import/custom-list/list-selector', + getRoute: (policyID: string | undefined) => { + if (!policyID) { + Log.warn('Invalid policyID is used to build the POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_LIST_SELECTOR route'); + } + return `workspaces/${policyID}/accounting/netsuite/import/custom-list/list-selector` as const; + }, + }, POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_SEGMENT_ADD: { route: 'workspaces/:policyID/accounting/netsuite/import/custom-segment/new/:subPage?/:action?', getRoute: (policyID: string | undefined, subPage?: string, action?: 'edit') => { diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 9fe35bd517f7..214f54f75121 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -629,6 +629,7 @@ const SCREENS = { NETSUITE_IMPORT_CUSTOM_FIELD_VIEW: 'Policy_Accounting_NetSuite_Import_Custom_Field_View', NETSUITE_IMPORT_CUSTOM_FIELD_EDIT: 'Policy_Accounting_NetSuite_Import_Custom_Field_Edit', NETSUITE_IMPORT_CUSTOM_LIST_ADD: 'Policy_Accounting_NetSuite_Import_Custom_List_Add', + NETSUITE_IMPORT_CUSTOM_LIST_SELECTOR: 'Policy_Accounting_NetSuite_Import_Custom_List_Selector', NETSUITE_IMPORT_CUSTOM_SEGMENT_ADD: 'Policy_Accounting_NetSuite_Import_Custom_Segment_Add', NETSUITE_IMPORT_CUSTOMERS_OR_PROJECTS: 'Policy_Accounting_NetSuite_Import_CustomersOrProjects', NETSUITE_IMPORT_CUSTOMERS_OR_PROJECTS_SELECT: 'Policy_Accounting_NetSuite_Import_CustomersOrProjects_Select', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 3bc6d27d9bbe..107c95e2a73d 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -806,6 +806,8 @@ const SettingsModalStackNavigator = createModalStackNavigator('../../../../pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldEdit').default, [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT_CUSTOM_LIST_ADD]: () => require('../../../../pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteImportAddCustomListPage').default, + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT_CUSTOM_LIST_SELECTOR]: () => + require('../../../../pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomListSelectorPage').default, [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT_CUSTOM_SEGMENT_ADD]: () => require('../../../../pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteImportAddCustomSegmentPage').default, [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT_CUSTOMERS_OR_PROJECTS]: () => diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts index 1efa9b448f7b..aab1c746c1c9 100755 --- a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts @@ -119,6 +119,7 @@ const WORKSPACE_TO_RHP: Partial['config'] = { [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT_CUSTOM_FIELD_VIEW]: {path: ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_FIELD_VIEW.route}, [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT_CUSTOM_FIELD_EDIT]: {path: ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_FIELD_EDIT.route}, [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT_CUSTOM_LIST_ADD]: {path: ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_LIST_ADD.route}, + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT_CUSTOM_LIST_SELECTOR]: {path: ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_LIST_SELECTOR.route}, [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT_CUSTOM_SEGMENT_ADD]: {path: ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_SEGMENT_ADD.route}, [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT_CUSTOMERS_OR_PROJECTS]: {path: ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOMERS_OR_PROJECTS.route}, [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT_CUSTOMERS_OR_PROJECTS_SELECT]: {path: ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOMERS_OR_PROJECTS_SELECT.route}, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index d3fe8919e11c..fafa8bbbe1c6 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -959,6 +959,9 @@ type SettingsNavigatorParamList = { subPage?: string; action?: 'edit'; }; + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT_CUSTOM_LIST_SELECTOR]: { + policyID: string; + }; [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_IMPORT_CUSTOM_SEGMENT_ADD]: { policyID: string; subPage?: string; diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomListPicker.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomListPicker.tsx index 6d2887b2daba..81b21e00c9e6 100644 --- a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomListPicker.tsx +++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomListPicker.tsx @@ -1,11 +1,10 @@ -import React, {useState} from 'react'; +import React from 'react'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; -import type {CustomListSelectorType} from '@pages/workspace/accounting/netsuite/types'; import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; import type {Policy} from '@src/types/onyx'; -import NetSuiteCustomListSelectorModal from './NetSuiteCustomListSelectorModal'; type NetSuiteCustomListPickerProps = { /** Current value of the selected item */ @@ -14,52 +13,23 @@ type NetSuiteCustomListPickerProps = { /** Current connected policy */ policy?: Policy; - /** Callback when the list item is selected */ - onInputChange?: (value: string, key?: string) => void; - - /** Id of the internalID input to be updated on input change */ - internalIDInputID?: string; - /** Form Error description */ errorText?: string; }; -function NetSuiteCustomListPicker({value, policy, internalIDInputID, errorText, onInputChange = () => {}}: NetSuiteCustomListPickerProps) { +function NetSuiteCustomListPicker({value, policy, errorText}: NetSuiteCustomListPickerProps) { const {translate} = useLocalize(); - const [isPickerVisible, setIsPickerVisible] = useState(false); - - const hidePickerModal = () => { - setIsPickerVisible(false); - }; - - const updateInput = (item: CustomListSelectorType) => { - onInputChange?.(item.value); - if (internalIDInputID) { - onInputChange(item.id, internalIDInputID); - } - hidePickerModal(); - }; + const policyID = policy?.id; return ( - <> - setIsPickerVisible(true)} - brickRoadIndicator={errorText ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} - errorText={errorText} - /> - - + Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_LIST_SELECTOR.getRoute(policyID))} + brickRoadIndicator={errorText ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} + errorText={errorText} + /> ); } diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomListSelectorModal.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomListSelectorPage.tsx similarity index 59% rename from src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomListSelectorModal.tsx rename to src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomListSelectorPage.tsx index 014fd0f0136c..0e42f15365d8 100644 --- a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomListSelectorModal.tsx +++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomListSelectorPage.tsx @@ -1,41 +1,36 @@ import {Str} from 'expensify-common'; import React, {useMemo} from 'react'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import Modal from '@components/Modal'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import SingleSelectListItem from '@components/SelectionList/ListItem/SingleSelectListItem'; import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import usePolicy from '@hooks/usePolicy'; +import {setDraftValues} from '@libs/actions/FormActions'; +import Navigation from '@libs/Navigation/Navigation'; +import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import type {CustomListSelectorType} from '@pages/workspace/accounting/netsuite/types'; import CONST from '@src/CONST'; -import type {Policy} from '@src/types/onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; +import INPUT_IDS from '@src/types/form/NetSuiteCustomFieldForm'; -type NetSuiteCustomListSelectorModalProps = { - /** Whether the modal is visible */ - isVisible: boolean; +type NetSuiteCustomListSelectorPageProps = PlatformStackScreenProps; - /** Function to call when the user closes the business type selector modal */ - onClose: () => void; - - /** Label to display on field */ - label: string; - - /** Custom List value selected */ - currentCustomListValue: string; - - policy?: Policy; - - /** Function to call when the user selects a custom list */ - onCustomListSelected: (value: CustomListSelectorType) => void; - - /** Function to call when the user presses on the modal backdrop */ - onBackdropPress?: () => void; -}; - -function NetSuiteCustomListSelectorModal({isVisible, currentCustomListValue, onCustomListSelected, onClose, label, policy, onBackdropPress}: NetSuiteCustomListSelectorModalProps) { +function NetSuiteCustomListSelectorPage({ + route: { + params: {policyID}, + }, +}: NetSuiteCustomListSelectorPageProps) { const {translate} = useLocalize(); const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); + const policy = usePolicy(policyID); + const [formDraft] = useOnyx(ONYXKEYS.FORMS.NETSUITE_CUSTOM_LIST_ADD_FORM_DRAFT); + const currentCustomListValue = formDraft?.[INPUT_IDS.LIST_NAME] ?? ''; const rawCustomLists = policy?.connections?.netsuite?.options?.data?.customLists; @@ -69,29 +64,36 @@ function NetSuiteCustomListSelectorModal({isVisible, currentCustomListValue, onC [searchValue, showTextInput, translate, setSearchValue, debouncedSearchValue, options.length], ); + const label = translate('workspace.netsuite.import.importCustomFields.customLists.fields.listName'); + + const onSelectRow = (item: CustomListSelectorType) => { + setDraftValues(ONYXKEYS.FORMS.NETSUITE_CUSTOM_LIST_ADD_FORM, { + [INPUT_IDS.LIST_NAME]: item.value, + [INPUT_IDS.INTERNAL_ID]: item.id, + }); + Navigation.goBack(); + }; + return ( - Navigation.goBack()} /> - + ); } -export default NetSuiteCustomListSelectorModal; +NetSuiteCustomListSelectorPage.displayName = 'NetSuiteCustomListSelectorPage'; + +export default NetSuiteCustomListSelectorPage; diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/subPages/ChooseCustomListStep.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/subPages/ChooseCustomListStep.tsx index de58b07829a1..7adec4094e25 100644 --- a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/subPages/ChooseCustomListStep.tsx +++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/subPages/ChooseCustomListStep.tsx @@ -50,7 +50,6 @@ function ChooseCustomListStep({policy, onNext, isEditing, netSuiteCustomFieldFor InputComponent={NetSuiteCustomListPicker} inputID={INPUT_IDS.LIST_NAME} policy={policy} - internalIDInputID={INPUT_IDS.INTERNAL_ID} defaultValue={netSuiteCustomFieldFormValues[INPUT_IDS.LIST_NAME]} /> diff --git a/tests/ui/NetSuiteCustomListSelectorPageTest.tsx b/tests/ui/NetSuiteCustomListSelectorPageTest.tsx new file mode 100644 index 000000000000..d221409a2998 --- /dev/null +++ b/tests/ui/NetSuiteCustomListSelectorPageTest.tsx @@ -0,0 +1,135 @@ +import type * as ReactNavigation from '@react-navigation/native'; +import {act, render} from '@testing-library/react-native'; +import React from 'react'; +import SelectionList from '@components/SelectionList'; +import {setDraftValues} from '@libs/actions/FormActions'; +import Navigation from '@libs/Navigation/Navigation'; +import NetSuiteCustomListSelectorPage from '@pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomListSelectorPage'; +import type {CustomListSelectorType} from '@pages/workspace/accounting/netsuite/types'; +import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/NetSuiteCustomFieldForm'; + +const mockUseState = React.useState; + +const mockCustomLists = [ + {id: '123', name: 'Department', internalID: '123', scriptID: 'custcol_dept'}, + {id: '456', name: 'Project', internalID: '456', scriptID: 'custcol_proj'}, +]; + +const mockPolicy = { + id: 'P1', + connections: { + netsuite: { + options: { + data: { + customLists: mockCustomLists, + }, + }, + }, + }, +}; + +let mockFormDraft: Record | undefined; + +jest.mock('@react-navigation/native', () => { + const actualNavigation: typeof ReactNavigation = jest.requireActual('@react-navigation/native'); + return { + ...actualNavigation, + useFocusEffect: jest.fn(), + }; +}); + +jest.mock('@components/HeaderWithBackButton', () => jest.fn(() => null)); +jest.mock('@components/ScreenWrapper', () => jest.fn(({children}: {children: React.ReactNode}) => children)); +jest.mock('@components/SelectionList', () => jest.fn(() => null)); +jest.mock('@components/SelectionList/ListItem/SingleSelectListItem', () => jest.fn(() => null)); +jest.mock('@pages/workspace/AccessOrNotFoundWrapper', () => jest.fn(({children}: {children: React.ReactNode}) => children)); +jest.mock('@hooks/useDebouncedState', () => + jest.fn((initialValue: string) => { + const [value, setValue] = mockUseState(initialValue); + return [value, value, setValue]; + }), +); +jest.mock('@hooks/useLocalize', () => + jest.fn(() => ({ + translate: (key: string) => key, + })), +); +jest.mock('@hooks/usePolicy', () => jest.fn(() => mockPolicy)); +jest.mock('@hooks/useOnyx', () => jest.fn(() => [mockFormDraft, {status: 'loaded'}])); +jest.mock('@libs/actions/FormActions', () => ({ + setDraftValues: jest.fn(), +})); +jest.mock('@libs/Navigation/Navigation', () => ({ + goBack: jest.fn(), + navigate: jest.fn(), +})); + +describe('NetSuiteCustomListSelectorPage', () => { + const mockedSelectionList = jest.mocked(SelectionList); + const mockedSetDraftValues = jest.mocked(setDraftValues); + const mockedNavigationGoBack = jest.mocked(Navigation.goBack); + + beforeEach(() => { + mockedSelectionList.mockClear(); + mockedSetDraftValues.mockClear(); + mockedNavigationGoBack.mockClear(); + mockFormDraft = undefined; + }); + + it('builds option rows from the policy custom lists and marks the draft value as selected', () => { + mockFormDraft = {[INPUT_IDS.LIST_NAME]: 'Project'}; + + render( + , + ); + + const selectionListProps = mockedSelectionList.mock.lastCall?.[0]; + expect(selectionListProps?.data).toEqual([ + expect.objectContaining({value: 'Department', isSelected: false, keyForList: 'Department', id: '123'}), + expect.objectContaining({value: 'Project', isSelected: true, keyForList: 'Project', id: '456'}), + ]); + expect(selectionListProps?.initiallyFocusedItemKey).toBe('Project'); + }); + + it('writes both listName and internalID to the form draft on row select then navigates back', () => { + render( + , + ); + + const selectionListProps = mockedSelectionList.mock.lastCall?.[0]; + const selectedRow = selectionListProps?.data.find((item) => (item as CustomListSelectorType).value === 'Department') as CustomListSelectorType; + selectionListProps?.onSelectRow?.(selectedRow); + + expect(mockedSetDraftValues).toHaveBeenCalledWith(ONYXKEYS.FORMS.NETSUITE_CUSTOM_LIST_ADD_FORM, { + [INPUT_IDS.LIST_NAME]: 'Department', + [INPUT_IDS.INTERNAL_ID]: '123', + }); + expect(mockedNavigationGoBack).toHaveBeenCalledTimes(1); + }); + + it('renders an empty option set with a no-results header message when search filters everything out', () => { + render( + , + ); + + const initialProps = mockedSelectionList.mock.lastCall?.[0]; + + act(() => { + initialProps?.textInputOptions?.onChangeText?.('zzzzz'); + }); + + const filteredProps = mockedSelectionList.mock.lastCall?.[0]; + expect(filteredProps?.data).toEqual([]); + expect(filteredProps?.textInputOptions?.headerMessage).toBe('common.noResultsFound'); + }); +}); From 71f462e5527456ffba92f91f458df1c123079dde Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 21 May 2026 10:10:05 -0700 Subject: [PATCH 2/3] Drive the picker route from the URL policyID param The NetSuiteCustomListPicker built its target route from policy?.id, which is only populated after the policy Onyx record finishes hydrating. If the picker is tapped before that, Navigation.navigate is called with workspaces/undefined/... and lands on an invalid workspace context. Plumb policyIDParam (the route param read by the parent page) down through CustomFieldSubPageWithPolicy -> ChooseCustomListStep -> the picker so the target route uses the value that is already guaranteed at the parent screen mount. Bail out of onPress when policyID is missing so we never produce an "undefined" deep link even before the prop arrives. --- .../NetSuiteCustomListPicker.tsx | 15 ++++--- .../NetSuiteImportAddCustomListContent.tsx | 1 + .../NetSuiteImportAddCustomSegmentContent.tsx | 1 + .../subPages/ChooseCustomListStep.tsx | 4 +- .../workspace/accounting/netsuite/types.ts | 3 ++ tests/ui/NetSuiteCustomListPickerTest.tsx | 42 +++++++++++++++++++ 6 files changed, 58 insertions(+), 8 deletions(-) create mode 100644 tests/ui/NetSuiteCustomListPickerTest.tsx diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomListPicker.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomListPicker.tsx index 81b21e00c9e6..748cf7eb0181 100644 --- a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomListPicker.tsx +++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomListPicker.tsx @@ -4,29 +4,32 @@ import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; -import type {Policy} from '@src/types/onyx'; type NetSuiteCustomListPickerProps = { /** Current value of the selected item */ value?: string; - /** Current connected policy */ - policy?: Policy; + /** Policy ID from the parent route's URL params (preferred over policy?.id because it is set before the Onyx policy record hydrates) */ + policyID?: string; /** Form Error description */ errorText?: string; }; -function NetSuiteCustomListPicker({value, policy, errorText}: NetSuiteCustomListPickerProps) { +function NetSuiteCustomListPicker({value, policyID, errorText}: NetSuiteCustomListPickerProps) { const {translate} = useLocalize(); - const policyID = policy?.id; return ( Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_LIST_SELECTOR.getRoute(policyID))} + onPress={() => { + if (!policyID) { + return; + } + Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_LIST_SELECTOR.getRoute(policyID)); + }} brickRoadIndicator={errorText ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} errorText={errorText} /> diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteImportAddCustomListContent.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteImportAddCustomListContent.tsx index 1cf833446c7a..e002af31ce8e 100644 --- a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteImportAddCustomListContent.tsx +++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteImportAddCustomListContent.tsx @@ -140,6 +140,7 @@ function NetSuiteImportAddCustomListContent({policy, draftValues, policyIDParam} onNext={handleNextScreen} onMove={moveTo} policy={policy} + policyIDParam={policyIDParam} importCustomField={CONST.NETSUITE_CONFIG.IMPORT_CUSTOM_FIELDS.CUSTOM_LISTS} netSuiteCustomFieldFormValues={values} customLists={customLists} diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteImportAddCustomSegmentContent.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteImportAddCustomSegmentContent.tsx index d61b5a76c244..f985159982e4 100644 --- a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteImportAddCustomSegmentContent.tsx +++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteImportAddCustomSegmentContent.tsx @@ -145,6 +145,7 @@ function NetSuiteImportAddCustomSegmentContent({policy, policyIDParam, draftValu onNext={handleNextScreen} onMove={moveTo} policy={policy} + policyIDParam={policyIDParam} importCustomField={CONST.NETSUITE_CONFIG.IMPORT_CUSTOM_FIELDS.CUSTOM_SEGMENTS} customSegmentType={customSegmentType} setCustomSegmentType={setCustomSegmentType} diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/subPages/ChooseCustomListStep.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/subPages/ChooseCustomListStep.tsx index 7adec4094e25..a3603274afd8 100644 --- a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/subPages/ChooseCustomListStep.tsx +++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/subPages/ChooseCustomListStep.tsx @@ -14,7 +14,7 @@ import INPUT_IDS from '@src/types/form/NetSuiteCustomFieldForm'; const STEP_FIELDS = [INPUT_IDS.LIST_NAME, INPUT_IDS.INTERNAL_ID]; -function ChooseCustomListStep({policy, onNext, isEditing, netSuiteCustomFieldFormValues}: CustomFieldSubPageWithPolicy) { +function ChooseCustomListStep({policyIDParam, onNext, isEditing, netSuiteCustomFieldFormValues}: CustomFieldSubPageWithPolicy) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -49,7 +49,7 @@ function ChooseCustomListStep({policy, onNext, isEditing, netSuiteCustomFieldFor diff --git a/src/pages/workspace/accounting/netsuite/types.ts b/src/pages/workspace/accounting/netsuite/types.ts index 7825b9743f19..0e2954c0a82f 100644 --- a/src/pages/workspace/accounting/netsuite/types.ts +++ b/src/pages/workspace/accounting/netsuite/types.ts @@ -81,6 +81,9 @@ type CustomFieldSubPageWithPolicy = SubPageProps & { /** Current policy in the form steps */ policy: Policy | undefined; + /** Policy ID from the parent route's URL params (set before the policy Onyx record finishes hydrating) */ + policyIDParam: string | undefined; + /** Whether the page is a custom segment or custom list */ importCustomField: ValueOf; diff --git a/tests/ui/NetSuiteCustomListPickerTest.tsx b/tests/ui/NetSuiteCustomListPickerTest.tsx new file mode 100644 index 000000000000..f5d56ae85927 --- /dev/null +++ b/tests/ui/NetSuiteCustomListPickerTest.tsx @@ -0,0 +1,42 @@ +import {render} from '@testing-library/react-native'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import Navigation from '@libs/Navigation/Navigation'; +import NetSuiteCustomListPicker from '@pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomListPicker'; +import ROUTES from '@src/ROUTES'; + +jest.mock('@components/MenuItemWithTopDescription', () => jest.fn(() => null)); +jest.mock('@hooks/useLocalize', () => + jest.fn(() => ({ + translate: (key: string) => key, + })), +); +jest.mock('@libs/Navigation/Navigation', () => ({ + navigate: jest.fn(), +})); + +describe('NetSuiteCustomListPicker', () => { + const mockedMenuItem = jest.mocked(MenuItemWithTopDescription); + const mockedNavigate = jest.mocked(Navigation.navigate); + + beforeEach(() => { + mockedMenuItem.mockClear(); + mockedNavigate.mockClear(); + }); + + it('navigates to the selector route using the route policyID when the picker is pressed', () => { + render(); + + mockedMenuItem.mock.lastCall?.[0].onPress?.({} as never); + + expect(mockedNavigate).toHaveBeenCalledTimes(1); + expect(mockedNavigate).toHaveBeenCalledWith(ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_LIST_SELECTOR.getRoute('P1')); + }); + + it('does not navigate when policyID is undefined so an "undefined" deep link is never produced', () => { + render(); + + mockedMenuItem.mock.lastCall?.[0].onPress?.({} as never); + + expect(mockedNavigate).not.toHaveBeenCalled(); + }); +}); From 1016198168233296f0fdaf271b351a491db732d4 Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 21 May 2026 12:04:09 -0700 Subject: [PATCH 3/3] Simplify mock custom list fixture in NetSuiteCustomListSelectorPage test --- tests/ui/NetSuiteCustomListSelectorPageTest.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ui/NetSuiteCustomListSelectorPageTest.tsx b/tests/ui/NetSuiteCustomListSelectorPageTest.tsx index d221409a2998..0f08850ffd3d 100644 --- a/tests/ui/NetSuiteCustomListSelectorPageTest.tsx +++ b/tests/ui/NetSuiteCustomListSelectorPageTest.tsx @@ -12,8 +12,8 @@ import INPUT_IDS from '@src/types/form/NetSuiteCustomFieldForm'; const mockUseState = React.useState; const mockCustomLists = [ - {id: '123', name: 'Department', internalID: '123', scriptID: 'custcol_dept'}, - {id: '456', name: 'Project', internalID: '456', scriptID: 'custcol_proj'}, + {id: '123', name: 'Department'}, + {id: '456', name: 'Project'}, ]; const mockPolicy = {