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
1 change: 1 addition & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1190,6 +1190,7 @@ const translations: TranslationDeepObject<typeof en> = {
`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',
Expand Down
1 change: 1 addition & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Comment thread
flodnv marked this conversation as resolved.
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.'),
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1150,6 +1150,7 @@ const translations: TranslationDeepObject<typeof en> = {
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',
Expand Down
1 change: 1 addition & 0 deletions src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1194,6 +1194,7 @@ const translations: TranslationDeepObject<typeof en> = {
`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',
Expand Down
1 change: 1 addition & 0 deletions src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1190,6 +1190,7 @@ const translations: TranslationDeepObject<typeof en> = {
`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',
Expand Down
1 change: 1 addition & 0 deletions src/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1173,6 +1173,7 @@ const translations: TranslationDeepObject<typeof en> = {
`このアップロードで追加される${count}人の新しいワークスペースメンバーについて、以下の内容を確認してください。既存のメンバーには、ロールの更新や招待メッセージは送信されません。`,
}),
importCompanyCardTransactionsPendingMessage: '新しいカードや取引が表示されるまでに少し時間がかかる場合があります。しばらくお待ちください。',
importMembersRolePermissionWarning: '一部のメンバー権限を割り当てる権限がありません。影響のある新しいメンバーは、メンバーとして招待されました。',
},
receipt: {
upload: '領収書をアップロード',
Expand Down
1 change: 1 addition & 0 deletions src/languages/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1189,6 +1189,7 @@ const translations: TranslationDeepObject<typeof en> = {
`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',
Expand Down
1 change: 1 addition & 0 deletions src/languages/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1185,6 +1185,7 @@ const translations: TranslationDeepObject<typeof en> = {
`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',
Expand Down
1 change: 1 addition & 0 deletions src/languages/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1189,6 +1189,7 @@ const translations: TranslationDeepObject<typeof en> = {
`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',
Expand Down
1 change: 1 addition & 0 deletions src/languages/zh-hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1141,6 +1141,7 @@ const translations: TranslationDeepObject<typeof en> = {
other: (count: number) => `请确认以下有关将通过本次上传添加的 ${count} 位新工作区成员的详细信息。现有成员将不会收到任何角色更新或邀请消息。`,
}),
importCompanyCardTransactionsPendingMessage: '新卡片和交易可能需要一些时间才会显示,请耐心等待。',
importMembersRolePermissionWarning: '您没有权限分配某些成员角色。所有受影响的新成员已按普通成员身份被邀请。',
},
receipt: {
upload: '上传收据',
Expand Down
1 change: 1 addition & 0 deletions src/libs/actions/ImportSpreadsheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ function closeImportPage(): Promise<void> {
columns: null,
importFinalModalID: null,
importFinalModal: null,
shouldShowMemberRolePermissionWarning: null,
// Clear the import settings so the next import starts fresh
importTransactionSettings: null,
});
Expand Down
11 changes: 6 additions & 5 deletions src/libs/actions/Policy/Member.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,14 +162,15 @@ 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',
promptKeyParams: {
added: addedMembersLength,
updated: updatedMembersLength,
},
...(shouldShowMemberRolePermissionWarning && {pendingMessageKey: 'spreadsheet.importMembersRolePermissionWarning'}),
};
}

Expand Down Expand Up @@ -1017,7 +1018,7 @@ type PolicyMember = {
overLimitForwardsTo?: string;
};

async function importPolicyMembers(policy: OnyxEntry<Policy>, members: PolicyMember[]): Promise<ImportFinalModal> {
async function importPolicyMembers(policy: OnyxEntry<Policy>, members: PolicyMember[], shouldShowMemberRolePermissionWarning = false): Promise<ImportFinalModal> {
if (!policy?.id) {
Log.warn('importPolicyMembers called without a valid policy');
return getImportFailedFinalModal();
Expand Down Expand Up @@ -1046,7 +1047,7 @@ async function importPolicyMembers(policy: OnyxEntry<Policy>, 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);

Expand Down Expand Up @@ -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<typeof CONST.POLICY.ROLE>) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
15 changes: 11 additions & 4 deletions src/pages/workspace/members/ImportedMembersPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ function ImportedMembersPage({route}: ImportedMembersPageProps) {
}

let isRoleMissing = false;
let shouldShowMemberRolePermissionWarning = false;

const columns = Object.values(spreadsheet?.columns ?? {});

Expand Down Expand Up @@ -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) {
Comment thread
flodnv marked this conversation as resolved.
role = CONST.POLICY.ROLE.USER;
shouldShowMemberRolePermissionWarning = true;
}
}
if (canAssignElevatedRoles && membersRolesColumn !== -1 && !role) {
isRoleMissing = true;
Expand Down Expand Up @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions src/types/onyx/ImportedSpreadsheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
9 changes: 9 additions & 0 deletions tests/actions/PolicyMemberTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
58 changes: 54 additions & 4 deletions tests/ui/ImportedMembersPageTest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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<number, string> = {};
for (const [index, columnName] of mappedColumns.entries()) {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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'));
});
});
Loading