diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 2cd9a60e1446..24ec39448a75 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -588,6 +588,9 @@ const ONYXKEYS = { /** Stores the information about the members imported from the spreadsheet */ IMPORTED_SPREADSHEET_MEMBER_DATA: 'importedSpreadsheetMemberData', + /** Stores the role selected for members being imported from a spreadsheet */ + IMPORTED_SPREADSHEET_MEMBER_ROLE: 'importedSpreadsheetMemberRole', + /** Stores the route to open after changing app permission from settings */ LAST_ROUTE: 'lastRoute', @@ -1571,6 +1574,7 @@ type OnyxValuesMapping = { [ONYXKEYS.APPROVAL_WORKFLOW]: OnyxTypes.ApprovalWorkflowOnyx; [ONYXKEYS.IMPORTED_SPREADSHEET]: OnyxTypes.ImportedSpreadsheet; [ONYXKEYS.IMPORTED_SPREADSHEET_MEMBER_DATA]: OnyxTypes.ImportedSpreadsheetMemberData[]; + [ONYXKEYS.IMPORTED_SPREADSHEET_MEMBER_ROLE]: ValueOf; [ONYXKEYS.LAST_ROUTE]: string; [ONYXKEYS.IS_USING_IMPORTED_STATE]: boolean; [ONYXKEYS.NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES]: Record; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 81a83c6d07f7..6ae47df62f43 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -104,6 +104,10 @@ const DYNAMIC_ROUTES = { path: 'expense-limit-type', entryScreens: [SCREENS.WORKSPACE.DYNAMIC_CATEGORY_FLAG_AMOUNTS_OVER], }, + IMPORTED_MEMBERS_ROLE: { + path: 'imported-members-role', + entryScreens: [SCREENS.WORKSPACE.MEMBERS_IMPORTED_CONFIRMATION], + }, REPORT_SETTINGS_NAME: { path: 'settings/name', entryScreens: [SCREENS.REPORT_DETAILS.ROOT], diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 0d941a895dfc..16f9aaf4beef 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -824,6 +824,7 @@ const SCREENS = { DYNAMIC_CATEGORY_DEFAULT_TAX_RATE: 'Dynamic_Category_Default_Tax_Rate', DYNAMIC_CATEGORY_FLAG_AMOUNTS_OVER: 'Dynamic_Category_Flag_Amounts_Over', DYNAMIC_EXPENSE_LIMIT_TYPE_SELECTOR: 'Dynamic_Expense_Limit_Type_Selector', + DYNAMIC_IMPORTED_MEMBERS_ROLE: 'Dynamic_Imported_Members_Role', DYNAMIC_CATEGORY_DESCRIPTION_HINT: 'Dynamic_Category_Description_Hint', DYNAMIC_CATEGORY_APPROVER: 'Dynamic_Category_Approver', DYNAMIC_CATEGORY_REQUIRE_RECEIPTS_OVER: 'Dynamic_Category_Require_Receipts_Over', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 11dbe5962ca5..7e96049388d2 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -610,6 +610,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/categories/DynamicCategoryDefaultTaxRatePage').default, [SCREENS.WORKSPACE.DYNAMIC_CATEGORY_FLAG_AMOUNTS_OVER]: () => require('../../../../pages/workspace/categories/DynamicCategoryFlagAmountsOverPage').default, [SCREENS.WORKSPACE.DYNAMIC_EXPENSE_LIMIT_TYPE_SELECTOR]: () => require('../../../../pages/workspace/categories/DynamicExpenseLimitTypeSelectorPage').default, + [SCREENS.WORKSPACE.DYNAMIC_IMPORTED_MEMBERS_ROLE]: () => require('../../../../pages/workspace/members/DynamicImportedMembersRoleSelectionPage').default, [SCREENS.WORKSPACE.DYNAMIC_CATEGORY_DESCRIPTION_HINT]: () => require('../../../../pages/workspace/categories/DynamicCategoryDescriptionHintPage').default, [SCREENS.WORKSPACE.DYNAMIC_CATEGORY_REQUIRE_RECEIPTS_OVER]: () => require('../../../../pages/workspace/categories/DynamicCategoryRequireReceiptsOverPage').default, [SCREENS.WORKSPACE.DYNAMIC_CATEGORY_REQUIRE_ITEMIZED_RECEIPTS_OVER]: () => diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts index dd4197b70fb9..e1d1714f4d09 100755 --- a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts @@ -24,6 +24,7 @@ const WORKSPACE_TO_RHP: Partial['config'] = { [SCREENS.WORKSPACE.DYNAMIC_CATEGORY_DEFAULT_TAX_RATE]: DYNAMIC_ROUTES.WORKSPACE_CATEGORY_DEFAULT_TAX_RATE.path, [SCREENS.WORKSPACE.DYNAMIC_CATEGORY_FLAG_AMOUNTS_OVER]: DYNAMIC_ROUTES.WORKSPACE_CATEGORY_FLAG_AMOUNTS_OVER.path, [SCREENS.WORKSPACE.DYNAMIC_EXPENSE_LIMIT_TYPE_SELECTOR]: DYNAMIC_ROUTES.EXPENSE_LIMIT_TYPE_SELECTOR.path, + [SCREENS.WORKSPACE.DYNAMIC_IMPORTED_MEMBERS_ROLE]: DYNAMIC_ROUTES.IMPORTED_MEMBERS_ROLE.path, [SCREENS.WORKSPACE.DYNAMIC_CATEGORY_DESCRIPTION_HINT]: DYNAMIC_ROUTES.WORKSPACE_CATEGORY_DESCRIPTION_HINT.path, [SCREENS.WORKSPACE.DYNAMIC_CATEGORY_APPROVER]: DYNAMIC_ROUTES.WORKSPACE_CATEGORY_APPROVER.path, [SCREENS.WORKSPACE.DYNAMIC_CATEGORY_REQUIRE_RECEIPTS_OVER]: DYNAMIC_ROUTES.WORKSPACE_CATEGORY_REQUIRE_RECEIPTS_OVER.path, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index b3e1b144a495..e36072ac31fc 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -386,6 +386,9 @@ type SettingsNavigatorParamList = { policyID: string; categoryName: string; }; + [SCREENS.WORKSPACE.DYNAMIC_IMPORTED_MEMBERS_ROLE]: { + policyID: string; + }; [SCREENS.WORKSPACE.DYNAMIC_CATEGORY_DESCRIPTION_HINT]: { policyID: string; categoryName: string; diff --git a/src/libs/actions/Policy/Member.ts b/src/libs/actions/Policy/Member.ts index a59721afb0ca..2d664a33a23c 100644 --- a/src/libs/actions/Policy/Member.ts +++ b/src/libs/actions/Policy/Member.ts @@ -1398,8 +1398,13 @@ function setImportedSpreadsheetMemberData(memberData: ImportedSpreadsheetMemberD Onyx.set(ONYXKEYS.IMPORTED_SPREADSHEET_MEMBER_DATA, memberData); } +function setImportedSpreadsheetMemberRole(role: ValueOf) { + Onyx.set(ONYXKEYS.IMPORTED_SPREADSHEET_MEMBER_ROLE, role); +} + function clearImportedSpreadsheetMemberData() { Onyx.set(ONYXKEYS.IMPORTED_SPREADSHEET_MEMBER_DATA, null); + Onyx.set(ONYXKEYS.IMPORTED_SPREADSHEET_MEMBER_ROLE, null); } export { @@ -1430,5 +1435,6 @@ export { setWorkspaceInviteApproverDraft, clearWorkspaceInviteApproverDraft, setImportedSpreadsheetMemberData, + setImportedSpreadsheetMemberRole, clearImportedSpreadsheetMemberData, }; diff --git a/src/pages/workspace/WorkspaceMemberRoleSelectionModal.tsx b/src/pages/workspace/WorkspaceMemberRoleSelectionModal.tsx deleted file mode 100644 index da2bad85375e..000000000000 --- a/src/pages/workspace/WorkspaceMemberRoleSelectionModal.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import React from 'react'; -import {View} from 'react-native'; -import type {ValueOf} from 'type-fest'; -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 type {ListItem} from '@components/SelectionList/types'; -import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; -import CONST from '@src/CONST'; - -type ListItemType = ListItem> & { - value: ValueOf; - text: string; - alternateText: string; - isSelected: boolean; -}; - -type WorkspaceMemberDetailsPageProps = { - /** Whether the modal is visible */ - isVisible: boolean; - - /** The list of items to render */ - items: ListItemType[]; - - /** Function to call when the user selects a role */ - onRoleChange: ({value}: ListItemType) => void; - - /** Function to call when the user closes the role selection modal */ - onClose: () => void; -}; - -function WorkspaceMemberDetailsRoleSelectionModal({isVisible, items, onRoleChange, onClose}: WorkspaceMemberDetailsPageProps) { - const {translate} = useLocalize(); - const styles = useThemeStyles(); - - return ( - onClose?.()} - onModalHide={onClose} - enableEdgeToEdgeBottomSafeAreaPadding - > - - - - item.isSelected)?.keyForList} - addBottomSafeAreaPadding - /> - - - - ); -} - -export type {ListItemType}; - -export default WorkspaceMemberDetailsRoleSelectionModal; diff --git a/src/pages/workspace/members/DynamicImportedMembersRoleSelectionPage.tsx b/src/pages/workspace/members/DynamicImportedMembersRoleSelectionPage.tsx new file mode 100644 index 000000000000..e7fb3915a8b8 --- /dev/null +++ b/src/pages/workspace/members/DynamicImportedMembersRoleSelectionPage.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import ScreenWrapper from '@components/ScreenWrapper'; +import WorkspaceMemberRoleList from '@components/WorkspaceMemberRoleList'; +import useDynamicBackPath from '@hooks/useDynamicBackPath'; +import useOnyx from '@hooks/useOnyx'; +import usePolicy from '@hooks/usePolicy'; +import {setImportedSpreadsheetMemberRole} from '@libs/actions/Policy/Member'; +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 CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import {DYNAMIC_ROUTES} from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; + +type DynamicImportedMembersRoleSelectionPageProps = PlatformStackScreenProps; + +function DynamicImportedMembersRoleSelectionPage({route}: DynamicImportedMembersRoleSelectionPageProps) { + const {policyID} = route.params; + const policy = usePolicy(policyID); + const [role = CONST.POLICY.ROLE.USER] = useOnyx(ONYXKEYS.IMPORTED_SPREADSHEET_MEMBER_ROLE); + const backPath = useDynamicBackPath(DYNAMIC_ROUTES.IMPORTED_MEMBERS_ROLE.path); + + return ( + + + { + setImportedSpreadsheetMemberRole(value); + Navigation.setNavigationActionToMicrotaskQueue(() => { + Navigation.goBack(backPath); + }); + }} + navigateBackTo={backPath} + /> + + + ); +} + +export default DynamicImportedMembersRoleSelectionPage; diff --git a/src/pages/workspace/members/ImportedMembersConfirmationPage.tsx b/src/pages/workspace/members/ImportedMembersConfirmationPage.tsx index 7bcd00b74b0c..aff9d2608eda 100644 --- a/src/pages/workspace/members/ImportedMembersConfirmationPage.tsx +++ b/src/pages/workspace/members/ImportedMembersConfirmationPage.tsx @@ -2,7 +2,6 @@ import {useIsFocused} from '@react-navigation/native'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import type {GestureResponderEvent} from 'react-native/Libraries/Types/CoreEventTypes'; -import type {ValueOf} from 'type-fest'; import Button from '@components/Button'; import FixedFooter from '@components/FixedFooter'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -22,17 +21,16 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {closeImportPage} from '@libs/actions/ImportSpreadsheet'; import {openExternalLink} from '@libs/actions/Link'; import {clearImportedSpreadsheetMemberData, importPolicyMembers} from '@libs/actions/Policy/Member'; +import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import {getAccountIDsByLogins} from '@libs/PersonalDetailsUtils'; -import {isControlPolicy, isPolicyMemberWithoutPendingDelete} from '@libs/PolicyUtils'; +import {isPolicyMemberWithoutPendingDelete} from '@libs/PolicyUtils'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; -import WorkspaceMemberDetailsRoleSelectionModal from '@pages/workspace/WorkspaceMemberRoleSelectionModal'; -import type {ListItemType} from '@pages/workspace/WorkspaceMemberRoleSelectionModal'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; +import ROUTES, {DYNAMIC_ROUTES} from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; type ImportedMembersConfirmationPageProps = PlatformStackScreenProps; @@ -41,8 +39,7 @@ function ImportedMembersConfirmationPage({route}: ImportedMembersConfirmationPag const styles = useThemeStyles(); const {translate} = useLocalize(); const [spreadsheet] = useOnyx(ONYXKEYS.IMPORTED_SPREADSHEET); - const [role, setRole] = useState>(CONST.POLICY.ROLE.USER); - const [isRoleSelectionModalVisible, setIsRoleSelectionModalVisible] = useState(false); + const [role = CONST.POLICY.ROLE.USER] = useOnyx(ONYXKEYS.IMPORTED_SPREADSHEET_MEMBER_ROLE); const policyID = route.params.policyID; const policy = usePolicy(policyID); @@ -108,42 +105,6 @@ function ImportedMembersConfirmationPage({route}: ImportedMembersConfirmationPag Navigation.goBack(ROUTES.WORKSPACE_MEMBERS.getRoute(policyID)); }; - const onRoleChange = (item: ListItemType) => { - setRole(item.value); - setIsRoleSelectionModalVisible(false); - }; - - const roleItems: ListItemType[] = useMemo(() => { - const items: ListItemType[] = [ - { - value: CONST.POLICY.ROLE.ADMIN, - text: translate('common.admin'), - alternateText: translate('workspace.common.adminAlternateText'), - isSelected: role === CONST.POLICY.ROLE.ADMIN, - keyForList: CONST.POLICY.ROLE.ADMIN, - }, - { - value: CONST.POLICY.ROLE.AUDITOR, - text: translate('common.auditor'), - alternateText: translate('workspace.common.auditorAlternateText'), - isSelected: role === CONST.POLICY.ROLE.AUDITOR, - keyForList: CONST.POLICY.ROLE.AUDITOR, - }, - { - value: CONST.POLICY.ROLE.USER, - text: translate('common.member'), - alternateText: translate('workspace.common.memberAlternateText'), - isSelected: role === CONST.POLICY.ROLE.USER, - keyForList: CONST.POLICY.ROLE.USER, - }, - ]; - - if (!isControlPolicy(policy)) { - return items.filter((item) => item.value !== CONST.POLICY.ROLE.AUDITOR); - } - return items; - }, [role, translate, policy]); - if (!spreadsheet || !importedSpreadsheetMemberData) { return ; } @@ -184,9 +145,7 @@ function ImportedMembersConfirmationPage({route}: ImportedMembersConfirmationPag title={translate(`workspace.common.roleName`, role)} description={translate('common.role')} shouldShowRightIcon - onPress={() => { - setIsRoleSelectionModalVisible(true); - }} + onPress={() => Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.IMPORTED_MEMBERS_ROLE.path))} /> @@ -220,12 +179,6 @@ function ImportedMembersConfirmationPage({route}: ImportedMembersConfirmationPag closeImportPageAndModal={closeImportPageAndModal} shouldHandleNavigationBack={false} /> - setIsRoleSelectionModalVisible(false)} - /> ); } diff --git a/tests/actions/PolicyMemberTest.ts b/tests/actions/PolicyMemberTest.ts index 9820cba3d157..8d5a2a788eac 100644 --- a/tests/actions/PolicyMemberTest.ts +++ b/tests/actions/PolicyMemberTest.ts @@ -1510,4 +1510,41 @@ describe('actions/PolicyMember', () => { expect(clearedApproverDraft).toBeFalsy(); }); }); + + describe('imported spreadsheet member role (migrated @react-navigation role picker, #90855)', () => { + it('setImportedSpreadsheetMemberRole stores the selected role in Onyx', async () => { + Member.setImportedSpreadsheetMemberRole(CONST.POLICY.ROLE.ADMIN); + await waitForBatchedUpdates(); + + const role = await getOnyxValue(ONYXKEYS.IMPORTED_SPREADSHEET_MEMBER_ROLE); + + // The role is now lifted into Onyx (previously it lived only in the parent's useState) + expect(role).toBe(CONST.POLICY.ROLE.ADMIN); + }); + + it('setImportedSpreadsheetMemberData stores the imported member data in Onyx', async () => { + const memberData = [{email: 'importtest@example.com', role: '', submitsTo: '', forwardsTo: ''}]; + Member.setImportedSpreadsheetMemberData(memberData); + await waitForBatchedUpdates(); + + const storedData = await getOnyxValue(ONYXKEYS.IMPORTED_SPREADSHEET_MEMBER_DATA); + + expect(storedData).toEqual(memberData); + }); + + it('clearImportedSpreadsheetMemberData clears BOTH the member data and the lifted role, so re-entering the import flow resets the role to its default', async () => { + // Simulate an in-progress import where a non-default role (Admin) was picked + Member.setImportedSpreadsheetMemberData([{email: 'importtest@example.com', role: '', submitsTo: '', forwardsTo: ''}]); + Member.setImportedSpreadsheetMemberRole(CONST.POLICY.ROLE.ADMIN); + await waitForBatchedUpdates(); + expect(await getOnyxValue(ONYXKEYS.IMPORTED_SPREADSHEET_MEMBER_ROLE)).toBe(CONST.POLICY.ROLE.ADMIN); + + // The unmount cleanup (ImportedMembersConfirmationPage) / re-entering the flow must clear both keys + Member.clearImportedSpreadsheetMemberData(); + await waitForBatchedUpdates(); + + expect(await getOnyxValue(ONYXKEYS.IMPORTED_SPREADSHEET_MEMBER_DATA)).toBeFalsy(); + expect(await getOnyxValue(ONYXKEYS.IMPORTED_SPREADSHEET_MEMBER_ROLE)).toBeFalsy(); + }); + }); });