Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',

Expand Down Expand Up @@ -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<typeof CONST.POLICY.ROLE>;
[ONYXKEYS.LAST_ROUTE]: string;
[ONYXKEYS.IS_USING_IMPORTED_STATE]: boolean;
[ONYXKEYS.NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES]: Record<string, string>;
Expand Down
4 changes: 4 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
1 change: 1 addition & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,7 @@ const SettingsModalStackNavigator = createModalStackNavigator<SettingsNavigatorP
[SCREENS.WORKSPACE.DYNAMIC_CATEGORY_DEFAULT_TAX_RATE]: () => require<ReactComponentModule>('../../../../pages/workspace/categories/DynamicCategoryDefaultTaxRatePage').default,
[SCREENS.WORKSPACE.DYNAMIC_CATEGORY_FLAG_AMOUNTS_OVER]: () => require<ReactComponentModule>('../../../../pages/workspace/categories/DynamicCategoryFlagAmountsOverPage').default,
[SCREENS.WORKSPACE.DYNAMIC_EXPENSE_LIMIT_TYPE_SELECTOR]: () => require<ReactComponentModule>('../../../../pages/workspace/categories/DynamicExpenseLimitTypeSelectorPage').default,
[SCREENS.WORKSPACE.DYNAMIC_IMPORTED_MEMBERS_ROLE]: () => require<ReactComponentModule>('../../../../pages/workspace/members/DynamicImportedMembersRoleSelectionPage').default,
[SCREENS.WORKSPACE.DYNAMIC_CATEGORY_DESCRIPTION_HINT]: () => require<ReactComponentModule>('../../../../pages/workspace/categories/DynamicCategoryDescriptionHintPage').default,
[SCREENS.WORKSPACE.DYNAMIC_CATEGORY_REQUIRE_RECEIPTS_OVER]: () => require<ReactComponentModule>('../../../../pages/workspace/categories/DynamicCategoryRequireReceiptsOverPage').default,
[SCREENS.WORKSPACE.DYNAMIC_CATEGORY_REQUIRE_ITEMIZED_RECEIPTS_OVER]: () =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const WORKSPACE_TO_RHP: Partial<Record<keyof WorkspaceSplitNavigatorParamList, s
SCREENS.WORKSPACE.MEMBERS_IMPORT,
SCREENS.WORKSPACE.MEMBERS_IMPORTED,
SCREENS.WORKSPACE.MEMBERS_IMPORTED_CONFIRMATION,
SCREENS.WORKSPACE.DYNAMIC_IMPORTED_MEMBERS_ROLE,
],
[SCREENS.WORKSPACE.ROOMS]: [],
[SCREENS.WORKSPACE.WORKFLOWS]: [
Expand Down
1 change: 1 addition & 0 deletions src/libs/Navigation/linkingConfig/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1061,6 +1061,7 @@ const config: LinkingOptions<RootNavigatorParamList>['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,
Expand Down
3 changes: 3 additions & 0 deletions src/libs/Navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 6 additions & 0 deletions src/libs/actions/Policy/Member.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1398,8 +1398,13 @@ function setImportedSpreadsheetMemberData(memberData: ImportedSpreadsheetMemberD
Onyx.set(ONYXKEYS.IMPORTED_SPREADSHEET_MEMBER_DATA, memberData);
}

function setImportedSpreadsheetMemberRole(role: ValueOf<typeof CONST.POLICY.ROLE>) {
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 {
Expand Down Expand Up @@ -1430,5 +1435,6 @@ export {
setWorkspaceInviteApproverDraft,
clearWorkspaceInviteApproverDraft,
setImportedSpreadsheetMemberData,
setImportedSpreadsheetMemberRole,
clearImportedSpreadsheetMemberData,
};
73 changes: 0 additions & 73 deletions src/pages/workspace/WorkspaceMemberRoleSelectionModal.tsx

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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<SettingsNavigatorParamList, typeof SCREENS.WORKSPACE.DYNAMIC_IMPORTED_MEMBERS_ROLE>;

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 (
<AccessOrNotFoundWrapper
policyID={policyID}
accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]}
>
<ScreenWrapper
testID="DynamicImportedMembersRoleSelectionPage"
enableEdgeToEdgeBottomSafeAreaPadding
shouldEnableMaxHeight
>
<WorkspaceMemberRoleList
role={role}
policy={policy}
onSelectRole={({value}) => {
setImportedSpreadsheetMemberRole(value);
Navigation.setNavigationActionToMicrotaskQueue(() => {
Navigation.goBack(backPath);
});
}}
navigateBackTo={backPath}
/>
</ScreenWrapper>
</AccessOrNotFoundWrapper>
);
}

export default DynamicImportedMembersRoleSelectionPage;
57 changes: 5 additions & 52 deletions src/pages/workspace/members/ImportedMembersConfirmationPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<SettingsNavigatorParamList, typeof SCREENS.WORKSPACE.MEMBERS_IMPORTED>;
Expand All @@ -41,8 +39,7 @@ function ImportedMembersConfirmationPage({route}: ImportedMembersConfirmationPag
const styles = useThemeStyles();
const {translate} = useLocalize();
const [spreadsheet] = useOnyx(ONYXKEYS.IMPORTED_SPREADSHEET);
const [role, setRole] = useState<ValueOf<typeof CONST.POLICY.ROLE>>(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);
Expand Down Expand Up @@ -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 <NotFoundPage />;
}
Expand Down Expand Up @@ -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))}
/>
</View>
</View>
Expand Down Expand Up @@ -220,12 +179,6 @@ function ImportedMembersConfirmationPage({route}: ImportedMembersConfirmationPag
closeImportPageAndModal={closeImportPageAndModal}
shouldHandleNavigationBack={false}
/>
<WorkspaceMemberDetailsRoleSelectionModal
isVisible={isRoleSelectionModalVisible}
items={roleItems}
onRoleChange={onRoleChange}
onClose={() => setIsRoleSelectionModalVisible(false)}
/>
</ScreenWrapper>
);
}
Expand Down
37 changes: 37 additions & 0 deletions tests/actions/PolicyMemberTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
});
Loading