diff --git a/src/languages/de.ts b/src/languages/de.ts index 95a23c719dfd..31be730be51a 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -1190,6 +1190,7 @@ const translations: TranslationDeepObject = { `Bitte bestätige die folgenden Details für die ${count} neuen Workspace-Mitglieder, die im Rahmen dieses Uploads hinzugefügt werden. Bestehende Mitglieder erhalten keine Rollenaktualisierungen oder Einladungsnachrichten.`, }), importCompanyCardTransactionsPendingMessage: 'Neue Karten und Transaktionen können etwas Zeit benötigen, bis sie erscheinen. Bitte haben Sie etwas Geduld.', + importMembersRolePermissionWarning: 'Sie haben keine Berechtigung, einige Mitgliederrollen zuzuweisen. Alle betroffenen neuen Mitglieder wurden als Mitglieder eingeladen.', }, receipt: { upload: 'Beleg hochladen', diff --git a/src/languages/en.ts b/src/languages/en.ts index 057d87f4e356..cd361c832ca9 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1233,6 +1233,7 @@ const translations = { return added > 1 ? `${added} members have been added.` : '1 member has been added.'; }, + importMembersRolePermissionWarning: "You don't have permission to assign some member roles. Any affected new members were invited as members.", importTagsSuccessfulDescription: ({tags}: {tags: number}) => (tags > 1 ? `${tags} tags have been added.` : '1 tag has been added.'), importMultiLevelTagsSuccessfulDescription: 'Multi-level tags have been added.', importPerDiemRatesSuccessfulDescription: ({rates}: {rates: number}) => (rates > 1 ? `${rates} per diem rates have been added.` : '1 per diem rate has been added.'), diff --git a/src/languages/es.ts b/src/languages/es.ts index 9750a85de185..922807aecad2 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1150,6 +1150,7 @@ const translations: TranslationDeepObject = { other: (count: number) => `Por favor confirma los detalles a continuación para los ${count} nuevos miembros del espacio de trabajo que se agregarán como parte de esta carga. Los miembros existentes no recibirán actualizaciones de rol ni mensajes de invitación.`, }), + importMembersRolePermissionWarning: 'No tienes permiso para asignar algunos roles de miembro. Los nuevos miembros afectados se han invitado como miembros.', }, receipt: { upload: 'Subir recibo', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 1b75c445f283..ac4b4d76fa42 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -1194,6 +1194,7 @@ const translations: TranslationDeepObject = { `Veuillez confirmer les détails ci-dessous pour les ${count} nouveaux membres de l’espace de travail qui seront ajoutés dans le cadre de ce téléversement. Les membres existants ne recevront aucune mise à jour de rôle ni message d’invitation.`, }), importCompanyCardTransactionsPendingMessage: 'L’apparition de nouvelles cartes et transactions peut prendre un certain temps, veuillez patienter.', + importMembersRolePermissionWarning: 'Vous n’avez pas l’autorisation d’assigner certains rôles de membre. Tous les nouveaux membres concernés ont été invités en tant que membres.', }, receipt: { upload: 'Télécharger le reçu', diff --git a/src/languages/it.ts b/src/languages/it.ts index dfcdf2ee5181..1c17db0c9a23 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -1190,6 +1190,7 @@ const translations: TranslationDeepObject = { `Conferma i dettagli riportati di seguito per i ${count} nuovi membri dello spazio di lavoro che verranno aggiunti come parte di questo caricamento. I membri già esistenti non riceveranno aggiornamenti di ruolo o messaggi di invito.`, }), importCompanyCardTransactionsPendingMessage: 'Le nuove carte e transazioni potrebbero impiegare un po’ di tempo per apparire, attendi per favore.', + importMembersRolePermissionWarning: "Non hai l'autorizzazione per assegnare alcuni ruoli ai membri. I nuovi membri interessati sono stati invitati come membri.", }, receipt: { upload: 'Carica ricevuta', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 0ffc19ec41b5..6fcd9964b645 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -1173,6 +1173,7 @@ const translations: TranslationDeepObject = { `このアップロードで追加される${count}人の新しいワークスペースメンバーについて、以下の内容を確認してください。既存のメンバーには、ロールの更新や招待メッセージは送信されません。`, }), importCompanyCardTransactionsPendingMessage: '新しいカードや取引が表示されるまでに少し時間がかかる場合があります。しばらくお待ちください。', + importMembersRolePermissionWarning: '一部のメンバー権限を割り当てる権限がありません。影響のある新しいメンバーは、メンバーとして招待されました。', }, receipt: { upload: '領収書をアップロード', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index d34f8495295a..6263f5f46acd 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -1189,6 +1189,7 @@ const translations: TranslationDeepObject = { `Bevestig hieronder de gegevens voor de ${count} nieuwe werkruimteleden die als onderdeel van deze upload worden toegevoegd. Bestaande leden ontvangen geen rolupdates of uitnodigingsberichten.`, }), importCompanyCardTransactionsPendingMessage: 'Nieuwe kaarten en transacties kunnen even duren voordat ze verschijnen, even geduld.', + importMembersRolePermissionWarning: 'Je hebt geen toestemming om sommige ledensrollen toe te wijzen. Alle betrokken nieuwe leden zijn uitgenodigd als lid.', }, receipt: { upload: 'Bon uploaden', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 5171d9d5e5d5..5246f7f2531a 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -1185,6 +1185,7 @@ const translations: TranslationDeepObject = { `Potwierdź poniższe szczegóły dotyczące ${count} nowych członków przestrzeni roboczej, którzy zostaną dodani w ramach tego przesyłania. Istniejący członkowie nie otrzymają żadnych aktualizacji ról ani wiadomości z zaproszeniem.`, }), importCompanyCardTransactionsPendingMessage: 'Nowe karty i transakcje mogą potrzebować trochę czasu, aby się pojawić, prosimy o cierpliwość.', + importMembersRolePermissionWarning: 'Nie masz uprawnień do przypisywania niektórych ról członków. Wszyscy nowi członkowie, których to dotyczy, zostali zaproszeni jako członkowie.', }, receipt: { upload: 'Prześlij paragon', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index d4c1672a2cb5..8553af81524f 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -1189,6 +1189,7 @@ const translations: TranslationDeepObject = { `Confirme os detalhes abaixo para os ${count} novos membros do workspace que serão adicionados como parte deste upload. Membros existentes não receberão nenhuma atualização de função nem mensagens de convite.`, }), importCompanyCardTransactionsPendingMessage: 'Novos cartões e transações podem levar algum tempo para aparecer, aguarde um momento.', + importMembersRolePermissionWarning: 'Você não tem permissão para atribuir alguns cargos de membro. Quaisquer novos membros afetados foram convidados como membros.', }, receipt: { upload: 'Carregar recibo', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 5547a6a118a9..23a67d6046ff 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -1141,6 +1141,7 @@ const translations: TranslationDeepObject = { other: (count: number) => `请确认以下有关将通过本次上传添加的 ${count} 位新工作区成员的详细信息。现有成员将不会收到任何角色更新或邀请消息。`, }), importCompanyCardTransactionsPendingMessage: '新卡片和交易可能需要一些时间才会显示,请耐心等待。', + importMembersRolePermissionWarning: '您没有权限分配某些成员角色。所有受影响的新成员已按普通成员身份被邀请。', }, receipt: { upload: '上传收据', diff --git a/src/libs/actions/ImportSpreadsheet.ts b/src/libs/actions/ImportSpreadsheet.ts index 5dc2fd8881ba..fd4f23394da9 100644 --- a/src/libs/actions/ImportSpreadsheet.ts +++ b/src/libs/actions/ImportSpreadsheet.ts @@ -75,6 +75,7 @@ function closeImportPage(): Promise { columns: null, importFinalModalID: null, importFinalModal: null, + shouldShowMemberRolePermissionWarning: null, // Clear the import settings so the next import starts fresh importTransactionSettings: null, }); diff --git a/src/libs/actions/Policy/Member.ts b/src/libs/actions/Policy/Member.ts index b5f121ac12da..fe91570f643c 100644 --- a/src/libs/actions/Policy/Member.ts +++ b/src/libs/actions/Policy/Member.ts @@ -162,7 +162,7 @@ function buildRoomMembersOnyxData( /** * Updates the import spreadsheet data according to the result of the import */ -function getImportMembersFinalModal(addedMembersLength: number, updatedMembersLength: number): ImportFinalModal { +function getImportMembersFinalModal(addedMembersLength: number, updatedMembersLength: number, shouldShowMemberRolePermissionWarning = false): ImportFinalModal { return { titleKey: 'spreadsheet.importSuccessfulTitle', promptKey: 'spreadsheet.importMembersSuccessfulDescription', @@ -170,6 +170,7 @@ function getImportMembersFinalModal(addedMembersLength: number, updatedMembersLe added: addedMembersLength, updated: updatedMembersLength, }, + ...(shouldShowMemberRolePermissionWarning && {pendingMessageKey: 'spreadsheet.importMembersRolePermissionWarning'}), }; } @@ -1017,7 +1018,7 @@ type PolicyMember = { overLimitForwardsTo?: string; }; -async function importPolicyMembers(policy: OnyxEntry, members: PolicyMember[]): Promise { +async function importPolicyMembers(policy: OnyxEntry, members: PolicyMember[], shouldShowMemberRolePermissionWarning = false): Promise { if (!policy?.id) { Log.warn('importPolicyMembers called without a valid policy'); return getImportFailedFinalModal(); @@ -1046,7 +1047,7 @@ async function importPolicyMembers(policy: OnyxEntry, members: PolicyMem }, {added: 0, updated: 0}, ); - const importFinalModal = getImportMembersFinalModal(added, updated); + const importFinalModal = getImportMembersFinalModal(added, updated, shouldShowMemberRolePermissionWarning); const shouldUpdateApprovalMode = members.some((member) => !!member.submitsTo || !!member.forwardsTo || !!member.overLimitForwardsTo || !!member.approvalLimit) && isControlPolicy(policy); @@ -1431,8 +1432,8 @@ function clearInviteDraft(policyID: string) { FormActions.clearDraftValues(ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM); } -function setImportedSpreadsheetMemberData(memberData: ImportedSpreadsheetMemberData[]) { - Onyx.set(ONYXKEYS.IMPORTED_SPREADSHEET_MEMBER_DATA, memberData); +function setImportedSpreadsheetMemberData(memberData: ImportedSpreadsheetMemberData[], shouldShowMemberRolePermissionWarning = false) { + return Promise.all([Onyx.set(ONYXKEYS.IMPORTED_SPREADSHEET_MEMBER_DATA, memberData), Onyx.merge(ONYXKEYS.IMPORTED_SPREADSHEET, {shouldShowMemberRolePermissionWarning})]); } function setImportedSpreadsheetMemberRole(role: ValueOf) { diff --git a/src/pages/workspace/members/ImportedMembersConfirmationPage.tsx b/src/pages/workspace/members/ImportedMembersConfirmationPage.tsx index b8b39396dc3d..794b11e5e820 100644 --- a/src/pages/workspace/members/ImportedMembersConfirmationPage.tsx +++ b/src/pages/workspace/members/ImportedMembersConfirmationPage.tsx @@ -109,7 +109,7 @@ function ImportedMembersConfirmationPage({route}: ImportedMembersConfirmationPag } setIsImporting(true); const membersWithRole = (importedSpreadsheetMemberData ?? []).map((member) => ({...member, role: member.role || role})); - const importFinalModal = await importPolicyMembers(policy, membersWithRole); + const importFinalModal = await importPolicyMembers(policy, membersWithRole, spreadsheet?.shouldShowMemberRolePermissionWarning); const didShowImportFinalModal = await showImportSpreadsheetConfirmModal(importFinalModal, {shouldHandleNavigationBack: false}); if (!didShowImportFinalModal) { setIsImporting(false); diff --git a/src/pages/workspace/members/ImportedMembersPage.tsx b/src/pages/workspace/members/ImportedMembersPage.tsx index e2ed6e0ee181..246715d3a3be 100644 --- a/src/pages/workspace/members/ImportedMembersPage.tsx +++ b/src/pages/workspace/members/ImportedMembersPage.tsx @@ -101,6 +101,7 @@ function ImportedMembersPage({route}: ImportedMembersPageProps) { } let isRoleMissing = false; + let shouldShowMemberRolePermissionWarning = false; const columns = Object.values(spreadsheet?.columns ?? {}); @@ -158,8 +159,14 @@ function ImportedMembersPage({route}: ImportedMembersPageProps) { let role = isPolicyMember ? (policy?.employeeList?.[email]?.role ?? '') : ''; const importedRole = membersRoles?.[containsHeader ? index + 1 : index]; const canManageCurrentRole = !isPolicyMember || canMemberManageMemberWithRole(policy, currentUserLogin, role); - if (canAssignElevatedRoles && membersRolesColumn !== -1 && importedRole && canManageCurrentRole && canMemberAssignRole(policy, currentUserLogin, importedRole)) { - role = importedRole; + const isImportedRoleValid = Object.values(CONST.POLICY.ROLE).some((policyRole) => policyRole === importedRole); + if (canAssignElevatedRoles && membersRolesColumn !== -1 && importedRole && canManageCurrentRole) { + if (canMemberAssignRole(policy, currentUserLogin, importedRole)) { + role = importedRole; + } else if (!isPolicyMember && isImportedRoleValid) { + role = CONST.POLICY.ROLE.USER; + shouldShowMemberRolePermissionWarning = true; + } } if (canAssignElevatedRoles && membersRolesColumn !== -1 && !role) { isRoleMissing = true; @@ -246,11 +253,11 @@ function ImportedMembersPage({route}: ImportedMembersPageProps) { } if (isRoleMissing) { - setImportedSpreadsheetMemberData(allMembers); + await setImportedSpreadsheetMemberData(allMembers, shouldShowMemberRolePermissionWarning); Navigation.navigate(ROUTES.WORKSPACE_MEMBERS_IMPORTED_CONFIRMATION.getRoute(policyID)); } else { setIsImporting(true); - const importFinalModal = await importPolicyMembers(policy, allMembers); + const importFinalModal = await importPolicyMembers(policy, allMembers, shouldShowMemberRolePermissionWarning); const didShowImportFinalModal = await showImportSpreadsheetConfirmModal(importFinalModal, {onModalHide: navigateBackToMembers}); if (!didShowImportFinalModal) { setIsImporting(false); diff --git a/src/types/onyx/ImportedSpreadsheet.ts b/src/types/onyx/ImportedSpreadsheet.ts index f289e29aec18..a15d052739fc 100644 --- a/src/types/onyx/ImportedSpreadsheet.ts +++ b/src/types/onyx/ImportedSpreadsheet.ts @@ -80,6 +80,9 @@ type ImportedSpreadsheet = { /** Modal to show after a queued import request finishes */ importFinalModal?: ImportFinalModalUnion | null; + + /** Whether the final member import modal should explain that restricted roles were replaced with the member role */ + shouldShowMemberRolePermissionWarning?: boolean; }; export default ImportedSpreadsheet; diff --git a/tests/actions/PolicyMemberTest.ts b/tests/actions/PolicyMemberTest.ts index b68a0198a646..a802ec2b5213 100644 --- a/tests/actions/PolicyMemberTest.ts +++ b/tests/actions/PolicyMemberTest.ts @@ -1099,6 +1099,15 @@ describe('actions/PolicyMember', () => { // Then it should show the singular member added success message expect(importFinalModal.promptKey).toBe('spreadsheet.importMembersSuccessfulDescription'); expect(importFinalModal.promptKeyParams).toStrictEqual({added: 1, updated: 0}); + expect(importFinalModal.pendingMessageKey).toBeUndefined(); + }); + + it('should include a role permission warning when restricted roles are replaced', async () => { + const policy = createRandomPolicy(1); + + const importFinalModal = await Member.importPolicyMembers(policy, [{email: 'user@gmail.com', role: CONST.POLICY.ROLE.USER}], true); + + expect(importFinalModal.pendingMessageKey).toBe('spreadsheet.importMembersRolePermissionWarning'); }); it('should show a "multiple members added message" when multiple new members are added', async () => { diff --git a/tests/ui/ImportedMembersPageTest.tsx b/tests/ui/ImportedMembersPageTest.tsx index fd9217edab21..972ff0c99b5b 100644 --- a/tests/ui/ImportedMembersPageTest.tsx +++ b/tests/ui/ImportedMembersPageTest.tsx @@ -4,6 +4,7 @@ import ComposeProviders from '@components/ComposeProviders'; import {LocaleContextProvider} from '@components/LocaleContextProvider'; import OnyxListItemProvider from '@components/OnyxListItemProvider'; +import * as Member from '@libs/actions/Policy/Member'; import Navigation from '@libs/Navigation/Navigation'; import ImportedMembersPage from '@pages/workspace/members/ImportedMembersPage'; @@ -21,6 +22,8 @@ import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct' const POLICY_ID = 'imported-members-test-policy'; const ADMIN_EMAIL = 'admin@example.com'; const ADMIN_ACCOUNT_ID = 1; +const PEOPLE_ADMIN_EMAIL = 'people-admin@example.com'; +const PEOPLE_ADMIN_ACCOUNT_ID = 2; // The confirm button on the import page renders `common.import` const IMPORT_BUTTON_TEXT = 'Import'; @@ -76,6 +79,19 @@ function buildSubmitPolicy(): Policy { } as Policy; } +function buildControlPolicyForPeopleAdmin(): Policy { + return { + ...buildSubmitPolicy(), + name: 'Test Control Workspace', + type: CONST.POLICY.TYPE.CORPORATE, + role: CONST.POLICY.ROLE.PEOPLE_ADMIN, + employeeList: { + [ADMIN_EMAIL]: {email: ADMIN_EMAIL, role: CONST.POLICY.ROLE.ADMIN}, + [PEOPLE_ADMIN_EMAIL]: {email: PEOPLE_ADMIN_EMAIL, role: CONST.POLICY.ROLE.PEOPLE_ADMIN}, + }, + } as Policy; +} + function buildSpreadsheet(mappedColumns: string[], data: string[][]): ImportedSpreadsheet { const columns: Record = {}; for (const [index, columnName] of mappedColumns.entries()) { @@ -108,12 +124,12 @@ function renderImportedMembersPage() { ); } -async function seedOnyx(spreadsheet: ImportedSpreadsheet) { +async function seedOnyx(spreadsheet: ImportedSpreadsheet, policy = buildSubmitPolicy(), login = ADMIN_EMAIL, accountID = ADMIN_ACCOUNT_ID) { await act(async () => { await Onyx.clear(); - await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, buildSubmitPolicy()); - await Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, {[ADMIN_ACCOUNT_ID]: buildPersonalDetails(ADMIN_EMAIL, ADMIN_ACCOUNT_ID, 'admin')}); - await Onyx.merge(ONYXKEYS.SESSION, {email: ADMIN_EMAIL, accountID: ADMIN_ACCOUNT_ID}); + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, policy); + await Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, {[accountID]: buildPersonalDetails(login, accountID, 'admin')}); + await Onyx.merge(ONYXKEYS.SESSION, {email: login, accountID}); await Onyx.set(ONYXKEYS.NETWORK, {shouldForceOffline: false}); await Onyx.set(ONYXKEYS.IMPORTED_SPREADSHEET, spreadsheet); await waitForBatchedUpdatesWithAct(); @@ -178,4 +194,38 @@ describe('ImportedMembersPage', () => { expect(Navigation.navigate).toHaveBeenCalledWith(expect.stringContaining(`upgrade/${CONST.UPGRADE_FEATURE_INTRO_MAPPING.controlPolicyRoles.alias}`)); expect(Navigation.navigate).toHaveBeenCalledWith(expect.stringContaining(`upgradePlanType=${CONST.POLICY.TYPE.CORPORATE}`)); }); + + it('imports restricted roles as members and includes a permission warning', async () => { + await seedOnyx( + buildSpreadsheet( + [CONST.CSV_IMPORT_COLUMNS.EMAIL, CONST.CSV_IMPORT_COLUMNS.ROLE], + [ + ['Email', 'new-member@example.com'], + ['Role', CONST.POLICY.ROLE.ADMIN], + ], + ), + buildControlPolicyForPeopleAdmin(), + PEOPLE_ADMIN_EMAIL, + PEOPLE_ADMIN_ACCOUNT_ID, + ); + const importPolicyMembersSpy = jest.spyOn(Member, 'importPolicyMembers').mockResolvedValue({ + titleKey: 'spreadsheet.importSuccessfulTitle', + promptKey: 'spreadsheet.importMembersSuccessfulDescription', + promptKeyParams: {added: 1, updated: 0}, + pendingMessageKey: 'spreadsheet.importMembersRolePermissionWarning', + }); + + renderImportedMembersPage(); + await waitForBatchedUpdatesWithAct(); + + fireEvent.press(screen.getByText(IMPORT_BUTTON_TEXT)); + await waitForBatchedUpdatesWithAct(); + + expect(importPolicyMembersSpy).toHaveBeenCalledWith( + expect.objectContaining({id: POLICY_ID}), + [expect.objectContaining({email: 'new-member@example.com', role: CONST.POLICY.ROLE.USER})], + true, + ); + expect(Navigation.navigate).not.toHaveBeenCalledWith(expect.stringContaining('/imported/confirmation')); + }); });