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
23 changes: 22 additions & 1 deletion src/pages/workspace/WorkspaceInvitePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import {appendCountryCode} from '@libs/LoginUtils';
import Navigation from '@libs/Navigation/Navigation';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
import {getHeaderMessage} from '@libs/OptionsListUtils';
import {getHeaderMessage, getParticipantsOption} from '@libs/OptionsListUtils';
import {addSMSDomainIfPhoneNumber, parsePhoneNumber} from '@libs/PhoneNumber';
import {getIneligibleInvitees, getMemberAccountIDsForWorkspace, goBackFromInvalidPolicy} from '@libs/PolicyUtils';
import type {OptionData} from '@libs/ReportUtils';
Expand Down Expand Up @@ -50,6 +50,8 @@
const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false);
const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {initWithStoredValues: false, canBeMissing: true});
const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false});
const [invitedEmailsToAccountIDsDraft] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT}${route.params.policyID}`, {canBeMissing: true});
const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: false});
const openWorkspaceInvitePage = () => {
const policyMemberEmailsToAccountIDs = getMemberAccountIDsForWorkspace(policy?.employeeList);
policyOpenWorkspaceInvitePage(route.params.policyID, Object.keys(policyMemberEmailsToAccountIDs));
Expand All @@ -74,6 +76,24 @@
);
}, [policy?.employeeList]);

const initiallySelectedOptions = useMemo(() => {
if (!invitedEmailsToAccountIDsDraft || !personalDetails) {
return [];
}

// Convert InvitedEmailsToAccountIDs to OptionData[]
// The draft stores login -> accountID mappings
// Use getParticipantsOption to enrich with full user details
return Object.entries(invitedEmailsToAccountIDsDraft).map(([login, accountID]) => {
const participant = {
login,
accountID,
selected: true,
};
return getParticipantsOption(participant, personalDetails) as OptionData;
});
}, [invitedEmailsToAccountIDsDraft, personalDetails]);

const {searchTerm, setSearchTerm, availableOptions, selectedOptions, selectedOptionsForDisplay, toggleSelection, areOptionsInitialized, onListEndReached, searchOptions} =
useSearchSelector({
selectionMode: CONST.SEARCH_SELECTOR.SELECTION_MODE_MULTI,
Expand All @@ -82,6 +102,7 @@
excludeLogins: excludedUsers,
includeRecentReports: false,
shouldInitialize: didScreenTransitionEnd,
initialSelected: initiallySelectedOptions,
});

const sections: Sections[] = useMemo(() => {
Expand Down Expand Up @@ -116,7 +137,7 @@
}

return sectionsArr;
}, [areOptionsInitialized, selectedOptionsForDisplay, availableOptions.personalDetails.length, availableOptions.userToInvite, translate]);

Check warning on line 140 in src/pages/workspace/WorkspaceInvitePage.tsx

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

React Hook useMemo has a missing dependency: 'availableOptions.personalDetails'. Either include it or remove the dependency array

const handleToggleSelection = useCallback(
(option: OptionData) => {
Expand Down
224 changes: 224 additions & 0 deletions tests/actions/PolicyMemberTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -841,4 +841,228 @@ describe('actions/PolicyMember', () => {
expect(importedSpreadsheet?.importFinalModal.promptKeyParams).toStrictEqual({added: 2, updated: 2});
});
});

describe('setWorkspaceInviteMembersDraft', () => {
it('should save member selections to draft storage', async () => {
// Given a policy ID and member selections
const policyID = '1';
const user1Email = 'user1@example.com';
const user1AccountID = 1234;
const user2Email = 'user2@example.com';
const user2AccountID = 1235;
const invitedEmailsToAccountIDs = {
[user1Email]: user1AccountID,
[user2Email]: user2AccountID,
};

// When setWorkspaceInviteMembersDraft is called
Member.setWorkspaceInviteMembersDraft(policyID, invitedEmailsToAccountIDs);
await waitForBatchedUpdates();

// Then the draft should be saved to the correct Onyx key
const draft = await new Promise<typeof invitedEmailsToAccountIDs | null | undefined>((resolve) => {
const connection = Onyx.connect({
key: `${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT}${policyID}`,
waitForCollectionCallback: false,
callback: (value) => {
Onyx.disconnect(connection);
resolve(value as typeof invitedEmailsToAccountIDs | null | undefined);
},
});
});

expect(draft).toBeDefined();
expect(draft?.[user1Email]).toBe(user1AccountID);
expect(draft?.[user2Email]).toBe(user2AccountID);
});

it('should update existing draft with new selections', async () => {
// Given an existing draft
const policyID = '1';
const user1Email = 'user1@example.com';
const user1AccountID = 1234;
const initialDraft = {
[user1Email]: user1AccountID,
};

await Onyx.set(`${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT}${policyID}`, initialDraft);
await waitForBatchedUpdates();

// When new selections are saved
const user2Email = 'user2@example.com';
const user2AccountID = 1235;
const user3Email = 'user3@example.com';
const user3AccountID = 1236;
const newSelections = {
[user2Email]: user2AccountID,
[user3Email]: user3AccountID,
};

Member.setWorkspaceInviteMembersDraft(policyID, newSelections);
await waitForBatchedUpdates();

// Then the draft should be updated (not merged)
const draft = await new Promise<Record<string, number> | null | undefined>((resolve) => {
const connection = Onyx.connect({
key: `${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT}${policyID}`,
waitForCollectionCallback: false,
callback: (value) => {
Onyx.disconnect(connection);
resolve(value as Record<string, number> | null | undefined);
},
});
});

expect(draft).toBeDefined();
expect(draft?.[user2Email]).toBe(user2AccountID);
expect(draft?.[user3Email]).toBe(user3AccountID);
// Old user1 should be replaced (not merged)
expect(draft?.[user1Email]).toBeUndefined();
});

it('should handle empty selections', async () => {
// Given an existing draft
const policyID = '1';
const user1Email = 'user1@example.com';
const user1AccountID = 1234;
const initialDraft = {
[user1Email]: user1AccountID,
};

await Onyx.set(`${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT}${policyID}`, initialDraft);
await waitForBatchedUpdates();

// When empty selections are saved
const emptySelections = {};

Member.setWorkspaceInviteMembersDraft(policyID, emptySelections);
await waitForBatchedUpdates();

// Then the draft should be set to empty object
const draft = await new Promise<Record<string, number> | null | undefined>((resolve) => {
const connection = Onyx.connect({
key: `${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT}${policyID}`,
waitForCollectionCallback: false,
callback: (value) => {
Onyx.disconnect(connection);
resolve(value as Record<string, number> | null | undefined);
},
});
});

expect(draft).toBeDefined();
expect(Object.keys(draft ?? {}).length).toBe(0);
});

it('should save draft for multiple different workspaces independently', async () => {
// Given two different workspace IDs
const policyID1 = '1';
const policyID2 = '2';
const user1Email = 'user1@example.com';
const user1AccountID = 1234;
const user2Email = 'user2@example.com';
const user2AccountID = 1235;

const draft1 = {[user1Email]: user1AccountID};
const draft2 = {[user2Email]: user2AccountID};

// When drafts are saved for both workspaces
Member.setWorkspaceInviteMembersDraft(policyID1, draft1);
Member.setWorkspaceInviteMembersDraft(policyID2, draft2);
await waitForBatchedUpdates();

// Then each workspace should have its own independent draft
const savedDraft1 = await new Promise<Record<string, number> | null | undefined>((resolve) => {
const connection = Onyx.connect({
key: `${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT}${policyID1}`,
waitForCollectionCallback: false,
callback: (value) => {
Onyx.disconnect(connection);
resolve(value as Record<string, number> | null | undefined);
},
});
});

const savedDraft2 = await new Promise<Record<string, number> | null | undefined>((resolve) => {
const connection = Onyx.connect({
key: `${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT}${policyID2}`,
waitForCollectionCallback: false,
callback: (value) => {
Onyx.disconnect(connection);
resolve(value as Record<string, number> | null | undefined);
},
});
});

expect(savedDraft1?.[user1Email]).toBe(user1AccountID);
expect(savedDraft1?.[user2Email]).toBeUndefined();

expect(savedDraft2?.[user2Email]).toBe(user2AccountID);
expect(savedDraft2?.[user1Email]).toBeUndefined();
});

it('should handle large number of selected members', async () => {
// Given a large selection of members
const policyID = '1';
const largeSelection: Record<string, number> = {};

// Create 100 members
for (let i = 1; i <= 100; i++) {
largeSelection[`user${i}@example.com`] = 1000 + i;
}

// When the large selection is saved
Member.setWorkspaceInviteMembersDraft(policyID, largeSelection);
await waitForBatchedUpdates();

// Then all members should be saved correctly
const draft = await new Promise<Record<string, number> | null | undefined>((resolve) => {
const connection = Onyx.connect({
key: `${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT}${policyID}`,
waitForCollectionCallback: false,
callback: (value) => {
Onyx.disconnect(connection);
resolve(value as Record<string, number> | null | undefined);
},
});
});

expect(draft).toBeDefined();
expect(Object.keys(draft ?? {}).length).toBe(100);
expect(draft?.['user1@example.com']).toBe(1001);
expect(draft?.['user50@example.com']).toBe(1050);
expect(draft?.['user100@example.com']).toBe(1100);
});

it('should preserve accountID as number type in draft', async () => {
// Given member selections with number accountIDs
const policyID = '1';
const userEmail = 'user@example.com';
const userAccountID = 1234;
const invitedEmailsToAccountIDs = {
[userEmail]: userAccountID,
};

// When the draft is saved
Member.setWorkspaceInviteMembersDraft(policyID, invitedEmailsToAccountIDs);
await waitForBatchedUpdates();

// Then the accountID should remain as a number (not string)
const draft = await new Promise<Record<string, number> | null | undefined>((resolve) => {
const connection = Onyx.connect({
key: `${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT}${policyID}`,
waitForCollectionCallback: false,
callback: (value) => {
Onyx.disconnect(connection);
resolve(value as Record<string, number> | null | undefined);
},
});
});

expect(draft).toBeDefined();
expect(typeof draft?.[userEmail]).toBe('number');
expect(draft?.[userEmail]).toBe(1234);
expect(draft?.[userEmail]).not.toBe('1234');
});
});
});
Loading