diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 3959f76a626f..dc82f542068f 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -312,9 +312,7 @@ const ONYXKEYS = { COLLECTION: { DOWNLOAD: 'download_', POLICY: 'policy_', - POLICY_MEMBERS: 'policyMembers_', POLICY_DRAFTS: 'policyDrafts_', - POLICY_MEMBERS_DRAFTS: 'policyMembersDrafts_', POLICY_JOIN_MEMBER: 'policyJoinMember_', POLICY_CATEGORIES: 'policyCategories_', POLICY_RECENTLY_USED_CATEGORIES: 'policyRecentlyUsedCategories_', @@ -526,10 +524,8 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.POLICY_DRAFTS]: OnyxTypes.Policy; [ONYXKEYS.COLLECTION.POLICY_CATEGORIES]: OnyxTypes.PolicyCategories; [ONYXKEYS.COLLECTION.POLICY_TAGS]: OnyxTypes.PolicyTagList; - [ONYXKEYS.COLLECTION.POLICY_MEMBERS]: OnyxTypes.PolicyMembers; - [ONYXKEYS.COLLECTION.POLICY_MEMBERS_DRAFTS]: OnyxTypes.PolicyMember; [ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES]: OnyxTypes.RecentlyUsedCategories; - [ONYXKEYS.COLLECTION.DEPRECATED_POLICY_MEMBER_LIST]: OnyxTypes.PolicyMembers; + [ONYXKEYS.COLLECTION.DEPRECATED_POLICY_MEMBER_LIST]: OnyxTypes.PolicyEmployeeList; [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT]: OnyxTypes.InvitedEmailsToAccountIDs; [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MESSAGE_DRAFT]: string; [ONYXKEYS.COLLECTION.REPORT]: OnyxTypes.Report; diff --git a/src/components/Indicator.tsx b/src/components/Indicator.tsx index e3d226a17999..8830681bc55f 100644 --- a/src/components/Indicator.tsx +++ b/src/components/Indicator.tsx @@ -8,14 +8,11 @@ import * as PolicyUtils from '@libs/PolicyUtils'; import * as UserUtils from '@libs/UserUtils'; import * as PaymentMethods from '@userActions/PaymentMethods'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {BankAccountList, FundList, LoginList, Policy, PolicyMembers, ReimbursementAccount, UserWallet, WalletTerms} from '@src/types/onyx'; +import type {BankAccountList, FundList, LoginList, Policy, ReimbursementAccount, UserWallet, WalletTerms} from '@src/types/onyx'; type CheckingMethod = () => boolean; type IndicatorOnyxProps = { - /** The employee list of all policies (coming from Onyx) */ - allPolicyMembers: OnyxCollection; - /** All the user's policies (from Onyx via withFullPolicy) */ policies: OnyxCollection; @@ -40,14 +37,13 @@ type IndicatorOnyxProps = { type IndicatorProps = IndicatorOnyxProps; -function Indicator({reimbursementAccount, allPolicyMembers, policies, bankAccountList, fundList, userWallet, walletTerms, loginList}: IndicatorOnyxProps) { +function Indicator({reimbursementAccount, policies, bankAccountList, fundList, userWallet, walletTerms, loginList}: IndicatorOnyxProps) { const theme = useTheme(); const styles = useThemeStyles(); // If a policy was just deleted from Onyx, then Onyx will pass a null value to the props, and // those should be cleaned out before doing any error checking const cleanPolicies = Object.fromEntries(Object.entries(policies ?? {}).filter(([, policy]) => policy?.id)); - const cleanAllPolicyMembers = Object.fromEntries(Object.entries(allPolicyMembers ?? {}).filter(([, policyMembers]) => !!policyMembers)); // All of the error & info-checking methods are put into an array. This is so that using _.some() will return // early as soon as the first error / info condition is returned. This makes the checks very efficient since @@ -57,7 +53,7 @@ function Indicator({reimbursementAccount, allPolicyMembers, policies, bankAccoun () => PaymentMethods.hasPaymentMethodError(bankAccountList, fundList), () => Object.values(cleanPolicies).some(PolicyUtils.hasPolicyError), () => Object.values(cleanPolicies).some(PolicyUtils.hasCustomUnitsError), - () => Object.values(cleanAllPolicyMembers).some(PolicyUtils.hasPolicyMemberError), + () => Object.values(cleanPolicies).some(PolicyUtils.hasEmployeeListError), () => Object.keys(reimbursementAccount?.errors ?? {}).length > 0, () => !!loginList && UserUtils.hasLoginListError(loginList), @@ -77,9 +73,6 @@ function Indicator({reimbursementAccount, allPolicyMembers, policies, bankAccoun Indicator.displayName = 'Indicator'; export default withOnyx({ - allPolicyMembers: { - key: ONYXKEYS.COLLECTION.POLICY_MEMBERS, - }, policies: { key: ONYXKEYS.COLLECTION.POLICY, }, diff --git a/src/libs/Navigation/AppNavigator/ReportScreenIDSetter.ts b/src/libs/Navigation/AppNavigator/ReportScreenIDSetter.ts index 529f0f3d31a7..5407a451682a 100644 --- a/src/libs/Navigation/AppNavigator/ReportScreenIDSetter.ts +++ b/src/libs/Navigation/AppNavigator/ReportScreenIDSetter.ts @@ -3,10 +3,10 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; import usePermissions from '@hooks/usePermissions'; -import {getPolicyMembersByIdWithoutCurrentUser} from '@libs/PolicyUtils'; +import {getPolicyEmployeeListByIdWithoutCurrentUser} from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Policy, PolicyMembers, Report, ReportMetadata} from '@src/types/onyx'; +import type {PersonalDetailsList, Policy, Report, ReportMetadata} from '@src/types/onyx'; import type {ReportScreenWrapperProps} from './ReportScreenWrapper'; type ReportScreenIDSetterComponentProps = { @@ -16,8 +16,8 @@ type ReportScreenIDSetterComponentProps = { /** The policies which the user has access to */ policies: OnyxCollection; - /** Members of all the workspaces the user is member of */ - policyMembers: OnyxCollection; + /** The personal details of the person who is logged in */ + personalDetails: OnyxEntry; /** Whether user is a new user */ isFirstTimeNewExpensifyUser: OnyxEntry; @@ -58,7 +58,7 @@ const getLastAccessedReportID = ( }; // This wrapper is reponsible for opening the last accessed report if there is no reportID specified in the route params -function ReportScreenIDSetter({route, reports, policies, policyMembers = {}, navigation, isFirstTimeNewExpensifyUser = false, reportMetadata, accountID}: ReportScreenIDSetterProps) { +function ReportScreenIDSetter({route, reports, policies, navigation, isFirstTimeNewExpensifyUser = false, reportMetadata, accountID, personalDetails}: ReportScreenIDSetterProps) { const {canUseDefaultRooms} = usePermissions(); const {activeWorkspaceID} = useActiveWorkspace(); @@ -73,7 +73,7 @@ function ReportScreenIDSetter({route, reports, policies, policyMembers = {}, nav return; } - const policyMemberAccountIDs = getPolicyMembersByIdWithoutCurrentUser(policyMembers, activeWorkspaceID, accountID); + const policyMemberAccountIDs = getPolicyEmployeeListByIdWithoutCurrentUser(policies, activeWorkspaceID, accountID); // If there is no reportID in route, try to find last accessed and use it for setParams const reportID = getLastAccessedReportID( @@ -92,7 +92,7 @@ function ReportScreenIDSetter({route, reports, policies, policyMembers = {}, nav if (reportID) { navigation.setParams({reportID: String(reportID)}); } - }, [route, navigation, reports, canUseDefaultRooms, policies, isFirstTimeNewExpensifyUser, reportMetadata, activeWorkspaceID, policyMembers, accountID]); + }, [route, navigation, reports, canUseDefaultRooms, policies, isFirstTimeNewExpensifyUser, reportMetadata, activeWorkspaceID, personalDetails, accountID]); // The ReportScreen without the reportID set will display a skeleton // until the reportID is loaded and set in the route param @@ -110,10 +110,6 @@ export default withOnyx session?.accountID, }, + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, })(ReportScreenIDSetter); diff --git a/src/libs/Navigation/dismissModalWithReport.ts b/src/libs/Navigation/dismissModalWithReport.ts index 2622cc2b9855..c0405c2c9da0 100644 --- a/src/libs/Navigation/dismissModalWithReport.ts +++ b/src/libs/Navigation/dismissModalWithReport.ts @@ -3,7 +3,7 @@ import type {NavigationContainerRef} from '@react-navigation/native'; import {StackActions} from '@react-navigation/native'; import {findLastIndex} from 'lodash'; import Log from '@libs/Log'; -import getPolicyMemberAccountIDs from '@libs/PolicyMembersUtils'; +import getPolicyEmployeeAccountIDs from '@libs/PolicyEmployeeListUtils'; import {doesReportBelongToWorkspace} from '@libs/ReportUtils'; import NAVIGATORS from '@src/NAVIGATORS'; import ROUTES from '@src/ROUTES'; @@ -47,7 +47,7 @@ function dismissModalWithReport(targetReport: Report | EmptyObject, navigationRe if (targetReport.reportID !== getTopmostReportId(state)) { const reportState = getStateFromPath(ROUTES.REPORT_WITH_ID.getRoute(targetReport.reportID)); const policyID = getPolicyIDFromState(state as State); - const policyMemberAccountIDs = getPolicyMemberAccountIDs(policyID); + const policyMemberAccountIDs = getPolicyEmployeeAccountIDs(policyID); const shouldOpenAllWorkspace = isEmptyObject(targetReport) ? true : !doesReportBelongToWorkspace(targetReport, policyMemberAccountIDs, policyID); if (shouldOpenAllWorkspace) { diff --git a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts index 36051fa35c56..4b17adf86841 100644 --- a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts +++ b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts @@ -3,7 +3,7 @@ import * as OnyxUpdates from '@libs/actions/OnyxUpdates'; import * as ActiveClientManager from '@libs/ActiveClientManager'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; -import getPolicyMemberAccountIDs from '@libs/PolicyMembersUtils'; +import getPolicyEmployeeAccountIDs from '@libs/PolicyEmployeeListUtils'; import {extractPolicyIDFromPath} from '@libs/PolicyUtils'; import {doesReportBelongToWorkspace, getReport} from '@libs/ReportUtils'; import Visibility from '@libs/Visibility'; @@ -65,9 +65,9 @@ export default function subscribeToReportCommentPushNotifications() { const policyID = lastVisitedPath && extractPolicyIDFromPath(lastVisitedPath); const report = getReport(reportID.toString()); - const policyMembersAccountIDs = policyID ? getPolicyMemberAccountIDs(policyID) : []; + const policyEmployeeAccountIDs = policyID ? getPolicyEmployeeAccountIDs(policyID) : []; - const reportBelongsToWorkspace = policyID && !isEmptyObject(report) && doesReportBelongToWorkspace(report, policyMembersAccountIDs, policyID); + const reportBelongsToWorkspace = policyID && !isEmptyObject(report) && doesReportBelongToWorkspace(report, policyEmployeeAccountIDs, policyID); Log.info('[PushNotification] onSelected() - called', false, {reportID, reportActionID}); Navigation.isNavigationReady() diff --git a/src/libs/PolicyEmployeeListUtils.ts b/src/libs/PolicyEmployeeListUtils.ts new file mode 100644 index 000000000000..305ffda87e28 --- /dev/null +++ b/src/libs/PolicyEmployeeListUtils.ts @@ -0,0 +1,25 @@ +import type {OnyxCollection} from 'react-native-onyx'; +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Policy} from '@src/types/onyx'; +import {getCurrentUserAccountID} from './actions/Report'; +import {getPolicyEmployeeListByIdWithoutCurrentUser} from './PolicyUtils'; + +let allPolicies: OnyxCollection = {}; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.POLICY, + waitForCollectionCallback: true, + callback: (value) => (allPolicies = value), +}); + +function getPolicyEmployeeAccountIDs(policyID?: string) { + if (!policyID) { + return []; + } + + const currentUserAccountID = getCurrentUserAccountID(); + + return getPolicyEmployeeListByIdWithoutCurrentUser(allPolicies, policyID, currentUserAccountID); +} + +export default getPolicyEmployeeAccountIDs; diff --git a/src/libs/PolicyMembersUtils.ts b/src/libs/PolicyMembersUtils.ts deleted file mode 100644 index 4376de150f17..000000000000 --- a/src/libs/PolicyMembersUtils.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type {OnyxCollection} from 'react-native-onyx'; -import Onyx from 'react-native-onyx'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type {PolicyMembers} from '@src/types/onyx'; -import {getCurrentUserAccountID} from './actions/Report'; -import {getPolicyMembersByIdWithoutCurrentUser} from './PolicyUtils'; - -let policyMembers: OnyxCollection; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.POLICY_MEMBERS, - waitForCollectionCallback: true, - callback: (value) => (policyMembers = value), -}); - -function getPolicyMemberAccountIDs(policyID?: string) { - if (!policyID) { - return []; - } - - const currentUserAccountID = getCurrentUserAccountID(); - - return getPolicyMembersByIdWithoutCurrentUser(policyMembers, policyID, currentUserAccountID); -} - -export default getPolicyMemberAccountIDs; diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index b162a7c6df93..b4cf4b164a19 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -4,13 +4,14 @@ import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {PersonalDetailsList, Policy, PolicyCategories, PolicyMembers, PolicyTagList, PolicyTags, TaxRate} from '@src/types/onyx'; +import type {Policy, PolicyCategories, PolicyEmployeeList, PolicyTagList, PolicyTags, TaxRate} from '@src/types/onyx'; import type {PolicyFeatureName, Rate} from '@src/types/onyx/Policy'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import getPolicyIDFromState from './Navigation/getPolicyIDFromState'; import Navigation, {navigationRef} from './Navigation/Navigation'; import type {RootStackParamList, State} from './Navigation/types'; +import {getPersonalDetailByEmail} from './PersonalDetailsUtils'; type MemberEmailsToAccountIDs = Record; @@ -25,11 +26,10 @@ function getActivePolicies(policies: OnyxCollection): Policy[] | undefin } /** - * Checks if we have any errors stored within the POLICY_MEMBERS. Determines whether we should show a red brick road error or not. - * Data structure: {accountID: {role:'user', errors: []}, accountID2: {role:'admin', errors: [{1231312313: 'Unable to do X'}]}, ...} + * Checks if we have any errors stored within the policy?.employeeList. Determines whether we should show a red brick road error or not. */ -function hasPolicyMemberError(policyMembers: OnyxEntry): boolean { - return Object.values(policyMembers ?? {}).some((member) => Object.keys(member?.errors ?? {}).length > 0); +function hasEmployeeListError(policy: OnyxEntry): boolean { + return Object.values(policy?.employeeList ?? {}).some((employee) => Object.keys(employee?.errors ?? {}).length > 0); } /** @@ -90,9 +90,8 @@ function getUnitRateValue(toLocaleDigit: (arg: string) => string, customUnitRate /** * Get the brick road indicator status for a policy. The policy has an error status if there is a policy member error, a custom unit error or a field error. */ -function getPolicyBrickRoadIndicatorStatus(policy: OnyxEntry, policyMembersCollection: OnyxCollection): ValueOf | undefined { - const policyMembers = policyMembersCollection?.[`${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policy?.id}`] ?? {}; - if (hasPolicyMemberError(policyMembers) || hasCustomUnitsError(policy) || hasPolicyErrorFields(policy)) { +function getPolicyBrickRoadIndicatorStatus(policy: OnyxEntry): ValueOf | undefined { + if (hasEmployeeListError(policy) || hasCustomUnitsError(policy) || hasPolicyErrorFields(policy)) { return CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; } return undefined; @@ -128,7 +127,7 @@ const isPolicyAdmin = (policy: OnyxEntry | EmptyObject): boolean => poli */ const isFreeGroupPolicy = (policy: OnyxEntry | EmptyObject): boolean => policy?.type === CONST.POLICY.TYPE.FREE; -const isPolicyMember = (policyID: string, policies: OnyxCollection): boolean => Object.values(policies ?? {}).some((policy) => policy?.id === policyID); +const isPolicyEmployee = (policyID: string, policies: OnyxCollection): boolean => Object.values(policies ?? {}).some((policy) => policy?.id === policyID); /** * Checks if the current user is an owner (creator) of the policy. @@ -140,18 +139,19 @@ const isPolicyOwner = (policy: OnyxEntry, currentUserAccountID: number): * * We only return members without errors. Otherwise, the members with errors would immediately be removed before the user has a chance to read the error. */ -function getMemberAccountIDsForWorkspace(policyMembers: OnyxEntry, personalDetails: OnyxEntry): MemberEmailsToAccountIDs { +function getMemberAccountIDsForWorkspace(employeeList: PolicyEmployeeList | undefined): MemberEmailsToAccountIDs { + const members = employeeList ?? {}; const memberEmailsToAccountIDs: MemberEmailsToAccountIDs = {}; - Object.keys(policyMembers ?? {}).forEach((accountID) => { - const member = policyMembers?.[accountID]; + Object.keys(members).forEach((email) => { + const member = members?.[email]; if (Object.keys(member?.errors ?? {})?.length > 0) { return; } - const personalDetail = personalDetails?.[accountID]; + const personalDetail = getPersonalDetailByEmail(email); if (!personalDetail?.login) { return; } - memberEmailsToAccountIDs[personalDetail.login] = Number(accountID); + memberEmailsToAccountIDs[email] = Number(personalDetail.accountID); }); return memberEmailsToAccountIDs; } @@ -159,19 +159,19 @@ function getMemberAccountIDsForWorkspace(policyMembers: OnyxEntry /** * Get login list that we should not show in the workspace invite options */ -function getIneligibleInvitees(policyMembers: OnyxEntry, personalDetails: OnyxEntry): string[] { +function getIneligibleInvitees(employeeList?: PolicyEmployeeList): string[] { + const policyEmployeeList = employeeList ?? {}; const memberEmailsToExclude: string[] = [...CONST.EXPENSIFY_EMAILS]; - Object.keys(policyMembers ?? {}).forEach((accountID) => { - const policyMember = policyMembers?.[accountID]; + Object.keys(policyEmployeeList).forEach((email) => { + const policyEmployee = policyEmployeeList?.[email]; // Policy members that are pending delete or have errors are not valid and we should show them in the invite options (don't exclude them). - if (policyMember?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policyMember?.errors ?? {}).length > 0) { + if (policyEmployee?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policyEmployee?.errors ?? {}).length > 0) { return; } - const memberEmail = personalDetails?.[accountID]?.login; - if (!memberEmail) { + if (!email) { return; } - memberEmailsToExclude.push(memberEmail); + memberEmailsToExclude.push(email); }); return memberEmailsToExclude; @@ -279,12 +279,12 @@ function getPathWithoutPolicyID(path: string) { return path.replace(CONST.REGEX.PATH_WITHOUT_POLICY_ID, '/'); } -function getPolicyMembersByIdWithoutCurrentUser(policyMembers: OnyxCollection, currentPolicyID?: string, currentUserAccountID?: number) { - return policyMembers - ? Object.keys(policyMembers[`${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${currentPolicyID}`] ?? {}) - .map((policyMemberAccountID) => Number(policyMemberAccountID)) - .filter((policyMemberAccountID) => policyMemberAccountID !== currentUserAccountID) - : []; +function getPolicyEmployeeListByIdWithoutCurrentUser(policies: OnyxCollection>, currentPolicyID?: string, currentUserAccountID?: number) { + const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${currentPolicyID}`] ?? null; + const policyMemberEmailsToAccountIDs = getMemberAccountIDsForWorkspace(policy?.employeeList); + return Object.values(policyMemberEmailsToAccountIDs) + .map((policyMemberAccountID) => Number(policyMemberAccountID)) + .filter((policyMemberAccountID) => policyMemberAccountID !== currentUserAccountID); } function goBackFromInvalidPolicy() { @@ -321,7 +321,7 @@ function getPolicyIDFromNavigationState() { export { getActivePolicies, hasAccountingConnections, - hasPolicyMemberError, + hasEmployeeListError, hasPolicyError, hasPolicyErrorFields, hasCustomUnitsError, @@ -345,12 +345,12 @@ export { getCleanedTagName, getCountOfEnabledTagsOfList, isPendingDeletePolicy, - isPolicyMember, + isPolicyEmployee, isPolicyOwner, isPaidGroupPolicy, extractPolicyIDFromPath, getPathWithoutPolicyID, - getPolicyMembersByIdWithoutCurrentUser, + getPolicyEmployeeListByIdWithoutCurrentUser, goBackFromInvalidPolicy, isPolicyFeatureEnabled, hasTaxRateError, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index efae0b5261d3..f31b4a780c5a 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5089,7 +5089,7 @@ function getMoneyRequestOptions(report: OnyxEntry, policy: OnyxEntry, isPolicyMember: boolean): boolean { +function canLeaveRoom(report: OnyxEntry, isPolicyEmployee: boolean): boolean { if (!report?.visibility) { if ( report?.chatType === CONST.REPORT.CHAT_TYPE.POLICY_ADMINS || @@ -5102,7 +5102,7 @@ function canLeaveRoom(report: OnyxEntry, isPolicyMember: boolean): boole // DM chats don't have a chatType return false; } - } else if (isPublicAnnounceRoom(report) && isPolicyMember) { + } else if (isPublicAnnounceRoom(report) && isPolicyEmployee) { return false; } return true; diff --git a/src/libs/WorkspacesSettingsUtils.ts b/src/libs/WorkspacesSettingsUtils.ts index 995bcba06a5c..db41bb18aeef 100644 --- a/src/libs/WorkspacesSettingsUtils.ts +++ b/src/libs/WorkspacesSettingsUtils.ts @@ -4,13 +4,13 @@ import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Policy, PolicyMembers, ReimbursementAccount, Report, ReportActions} from '@src/types/onyx'; +import type {Policy, ReimbursementAccount, Report, ReportActions} from '@src/types/onyx'; import type {Unit} from '@src/types/onyx/Policy'; import * as CollectionUtils from './CollectionUtils'; import * as CurrencyUtils from './CurrencyUtils'; import type {Phrase, PhraseParameters} from './Localize'; import * as OptionsListUtils from './OptionsListUtils'; -import {hasCustomUnitsError, hasPolicyError, hasPolicyMemberError, hasTaxRateError} from './PolicyUtils'; +import {hasCustomUnitsError, hasEmployeeListError, hasPolicyError, hasTaxRateError} from './PolicyUtils'; import * as ReportUtils from './ReportUtils'; type CheckingMethod = () => boolean; @@ -33,16 +33,6 @@ Onyx.connect({ callback: (value) => (allPolicies = value), }); -let allPolicyMembers: OnyxCollection; - -Onyx.connect({ - key: ONYXKEYS.COLLECTION.POLICY_MEMBERS, - waitForCollectionCallback: true, - callback: (val) => { - allPolicyMembers = val; - }, -}); - let reimbursementAccount: OnyxEntry; Onyx.connect({ @@ -88,17 +78,16 @@ const getBrickRoadForPolicy = (report: Report): BrickRoad => { return shouldShowGreenDotIndicator ? CONST.BRICK_ROAD_INDICATOR_STATUS.INFO : undefined; }; -function hasGlobalWorkspaceSettingsRBR(policies: OnyxCollection, policyMembers: OnyxCollection) { +function hasGlobalWorkspaceSettingsRBR(policies: OnyxCollection) { // When attempting to open a policy with an invalid policyID, the policy collection is updated to include policy objects with error information. // Only policies displayed on the policy list page should be verified. Otherwise, the user will encounter an RBR unrelated to any policies on the list. const cleanPolicies = Object.fromEntries(Object.entries(policies ?? {}).filter(([, policy]) => policy?.id)); - const cleanAllPolicyMembers = Object.fromEntries(Object.entries(policyMembers ?? {}).filter(([, policyMemberValues]) => !!policyMemberValues)); const errorCheckingMethods: CheckingMethod[] = [ () => Object.values(cleanPolicies).some(hasPolicyError), () => Object.values(cleanPolicies).some(hasCustomUnitsError), () => Object.values(cleanPolicies).some(hasTaxRateError), - () => Object.values(cleanAllPolicyMembers).some(hasPolicyMemberError), + () => Object.values(cleanPolicies).some(hasEmployeeListError), () => Object.keys(reimbursementAccount?.errors ?? {}).length > 0, ]; @@ -106,7 +95,7 @@ function hasGlobalWorkspaceSettingsRBR(policies: OnyxCollection, policyM } function hasWorkspaceSettingsRBR(policy: Policy) { - const policyMemberError = allPolicyMembers ? hasPolicyMemberError(allPolicyMembers[`${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policy.id}`]) : false; + const policyMemberError = hasEmployeeListError(policy); const taxRateError = hasTaxRateError(policy); return Object.keys(reimbursementAccount?.errors ?? {}).length > 0 || hasPolicyError(policy) || hasCustomUnitsError(policy) || policyMemberError || taxRateError; @@ -143,7 +132,7 @@ function getChatTabBrickRoad(policyID?: string): BrickRoad | undefined { function checkIfWorkspaceSettingsTabHasRBR(policyID?: string) { if (!policyID) { - return hasGlobalWorkspaceSettingsRBR(allPolicies, allPolicyMembers); + return hasGlobalWorkspaceSettingsRBR(allPolicies); } const policy = allPolicies ? allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`] : null; diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index 74965bcaca13..fffc992746d9 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -79,7 +79,7 @@ import type { Policy, PolicyCategories, PolicyCategory, - PolicyMember, + PolicyEmployee, PolicyOwnershipChangeChecks, PolicyTag, PolicyTagList, @@ -182,15 +182,6 @@ Onyx.connect({ callback: (value) => (allReports = value), }); -let allPolicyMembers: OnyxCollection; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.POLICY_MEMBERS, - waitForCollectionCallback: true, - callback: (val) => { - allPolicyMembers = val; - }, -}); - let lastAccessedWorkspacePolicyID: OnyxEntry = null; Onyx.connect({ key: ONYXKEYS.LAST_ACCESSED_WORKSPACE_POLICY_ID, @@ -841,28 +832,29 @@ function removeMembers(accountIDs: number[], policyID: string) { return; } - const membersListKey = `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}` as const; + const policyKey = `${ONYXKEYS.COLLECTION.POLICY}${policyID}` as const; const policy = getPolicy(policyID); const workspaceChats = ReportUtils.getWorkspaceChats(policyID, accountIDs); + const emailList = accountIDs.map((accountID) => allPersonalDetails?.[accountID]?.login).filter((login) => !!login) as string[]; const optimisticClosedReportActions = workspaceChats.map(() => ReportUtils.buildOptimisticClosedReportAction(sessionEmail, policy.name, CONST.REPORT.ARCHIVE_REASON.REMOVED_FROM_POLICY)); const announceRoomMembers = removeOptimisticAnnounceRoomMembers(policy.id, policy.name, accountIDs); - const optimisticMembersState: OnyxCollection = {}; - const successMembersState: OnyxCollection = {}; - const failureMembersState: OnyxCollection = {}; - accountIDs.forEach((accountID) => { - optimisticMembersState[accountID] = {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}; - successMembersState[accountID] = null; - failureMembersState[accountID] = {errors: ErrorUtils.getMicroSecondOnyxError('workspace.people.error.genericRemove')}; + const optimisticMembersState: OnyxCollection = {}; + const successMembersState: OnyxCollection = {}; + const failureMembersState: OnyxCollection = {}; + emailList.forEach((email) => { + optimisticMembersState[email] = {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}; + successMembersState[email] = null; + failureMembersState[email] = {errors: ErrorUtils.getMicroSecondOnyxError('workspace.people.error.genericRemove')}; }); const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: membersListKey, - value: optimisticMembersState, + key: policyKey, + value: {employeeList: optimisticMembersState}, }, ...announceRoomMembers.onyxOptimisticData, ]; @@ -870,8 +862,8 @@ function removeMembers(accountIDs: number[], policyID: string) { const successData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: membersListKey, - value: successMembersState, + key: policyKey, + value: {employeeList: successMembersState}, }, ...announceRoomMembers.onyxSuccessData, ]; @@ -879,8 +871,8 @@ function removeMembers(accountIDs: number[], policyID: string) { const failureData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: membersListKey, - value: failureMembersState, + key: policyKey, + value: {employeeList: failureMembersState}, }, ...announceRoomMembers.onyxFailureData, ]; @@ -926,9 +918,8 @@ function removeMembers(accountIDs: number[], policyID: string) { // If we delete all these logins then we should clear the informative messages since they are no longer relevant. if (!isEmptyObject(policy?.primaryLoginsInvited ?? {})) { // Take the current policy members and remove them optimistically - const policyMemberAccountIDs = Object.keys(allPolicyMembers?.[`${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}`] ?? {}).map((accountID) => Number(accountID)); - const remainingMemberAccountIDs = policyMemberAccountIDs.filter((accountID) => !accountIDs.includes(accountID)); - const remainingLogins: string[] = PersonalDetailsUtils.getLoginsByAccountIDs(remainingMemberAccountIDs); + const employeeListEmails = Object.keys(allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]?.employeeList ?? {}); + const remainingLogins = employeeListEmails.filter((email) => !emailList.includes(email)); const invitedPrimaryToSecondaryLogins: Record = {}; if (policy.primaryLoginsInvited) { @@ -939,7 +930,7 @@ function removeMembers(accountIDs: number[], policyID: string) { if (!remainingLogins.some((remainingLogin) => !!invitedPrimaryToSecondaryLogins[remainingLogin])) { optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: membersListKey, + key: policyKey, value: { primaryLoginsInvited: null, }, @@ -969,7 +960,7 @@ function removeMembers(accountIDs: number[], policyID: string) { }); const params: DeleteMembersFromWorkspaceParams = { - emailList: accountIDs.map((accountID) => allPersonalDetails?.[accountID]?.login).join(','), + emailList: emailList.join(','), policyID, }; @@ -977,52 +968,47 @@ function removeMembers(accountIDs: number[], policyID: string) { } function leaveWorkspace(policyID: string) { - const membersListKey = `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}` as const; const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; const workspaceChats = ReportUtils.getAllWorkspaceReports(policyID); const optimisticData: OnyxUpdate[] = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: membersListKey, - value: { - [sessionAccountID]: { - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, - }, - }, - }, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + employeeList: { + [sessionEmail]: { + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + }, + }, }, }, ]; + const successData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: membersListKey, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { - [sessionAccountID]: null, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + employeeList: { + [sessionEmail]: null, + }, }, }, ]; const failureData: OnyxUpdate[] = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: membersListKey, - value: { - [sessionAccountID]: { - errors: ErrorUtils.getMicroSecondOnyxError('workspace.people.error.genericRemove'), - }, - }, - }, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { pendingAction: policy?.pendingAction, + employeeList: { + [sessionEmail]: { + errors: ErrorUtils.getMicroSecondOnyxError('workspace.people.error.genericRemove'), + }, + }, }, }, ]; @@ -1071,7 +1057,7 @@ function leaveWorkspace(policyID: string) { } function updateWorkspaceMembersRole(policyID: string, accountIDs: number[], newRole: typeof CONST.POLICY.ROLE.ADMIN | typeof CONST.POLICY.ROLE.USER) { - const previousPolicyMembers = {...allPolicyMembers}; + const previousEmployeeList = {...allPolicies?.[policyID]?.employeeList}; const memberRoles: WorkspaceMembersRoleData[] = accountIDs.reduce((result: WorkspaceMembersRoleData[], accountID: number) => { if (!allPersonalDetails?.[accountID]?.login) { return result; @@ -1089,13 +1075,15 @@ function updateWorkspaceMembersRole(policyID: string, accountIDs: number[], newR const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}`, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { - ...memberRoles.reduce((member: Record, current) => { - // eslint-disable-next-line no-param-reassign - member[current.accountID] = {role: current?.role, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}; - return member; - }, {}), + employeeList: { + ...memberRoles.reduce((member: Record, current) => { + // eslint-disable-next-line no-param-reassign + member[current.email] = {role: current?.role, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}; + return member; + }, {}), + }, errors: null, }, }, @@ -1104,13 +1092,15 @@ function updateWorkspaceMembersRole(policyID: string, accountIDs: number[], newR const successData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}`, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { - ...memberRoles.reduce((member: Record, current) => { - // eslint-disable-next-line no-param-reassign - member[current.accountID] = {role: current?.role, pendingAction: null}; - return member; - }, {}), + employeeList: { + ...memberRoles.reduce((member: Record, current) => { + // eslint-disable-next-line no-param-reassign + member[current.email] = {role: current?.role, pendingAction: null}; + return member; + }, {}), + }, errors: null, }, }, @@ -1119,9 +1109,9 @@ function updateWorkspaceMembersRole(policyID: string, accountIDs: number[], newR const failureData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}`, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { - ...(previousPolicyMembers[`${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}`] as Record), + employeeList: previousEmployeeList, errors: ErrorUtils.getMicroSecondOnyxError('workspace.editor.genericFailureMessage'), }, }, @@ -1391,7 +1381,7 @@ function createPolicyExpenseChats(policyID: string, invitedEmailsToAccountIDs: I * Please see https://github.com/Expensify/App/blob/main/README.md#Security for more details */ function addMembersToWorkspace(invitedEmailsToAccountIDs: InvitedEmailsToAccountIDs, welcomeNote: string, policyID: string) { - const membersListKey = `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}` as const; + const policyKey = `${ONYXKEYS.COLLECTION.POLICY}${policyID}` as const; const logins = Object.keys(invitedEmailsToAccountIDs).map((memberLogin) => PhoneNumber.addSMSDomainIfPhoneNumber(memberLogin)); const accountIDs = Object.values(invitedEmailsToAccountIDs); @@ -1403,14 +1393,13 @@ function addMembersToWorkspace(invitedEmailsToAccountIDs: InvitedEmailsToAccount // create onyx data for policy expense chats for each new member const membersChats = createPolicyExpenseChats(policyID, invitedEmailsToAccountIDs); - const optimisticMembersState: OnyxCollection = {}; - const failureMembersState: OnyxCollection = {}; - accountIDs.forEach((accountID) => { - optimisticMembersState[accountID] = { - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - role: CONST.POLICY.ROLE.USER, - }; - failureMembersState[accountID] = { + const optimisticMembersState: OnyxCollection = {}; + const successMembersState: OnyxCollection = {}; + const failureMembersState: OnyxCollection = {}; + Object.keys(invitedEmailsToAccountIDs).forEach((email) => { + optimisticMembersState[email] = {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, role: CONST.POLICY.ROLE.USER}; + successMembersState[email] = {pendingAction: null}; + failureMembersState[email] = { errors: ErrorUtils.getMicroSecondOnyxError('workspace.people.error.genericAdd'), }; }); @@ -1418,10 +1407,12 @@ function addMembersToWorkspace(invitedEmailsToAccountIDs: InvitedEmailsToAccount const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: membersListKey, + key: policyKey, // Convert to object with each key containing {pendingAction: ‘add’} - value: optimisticMembersState, + value: { + employeeList: optimisticMembersState, + }, }, ...newPersonalDetailsOnyxData.optimisticData, ...membersChats.onyxOptimisticData, @@ -1431,19 +1422,10 @@ function addMembersToWorkspace(invitedEmailsToAccountIDs: InvitedEmailsToAccount const successData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: membersListKey, - - // Convert to object with each key clearing pendingAction, when it is an existing account. - // Remove the object, when it is a newly created account. - value: accountIDs.reduce((accountIDsWithClearedPendingAction, accountID) => { - const value = { - ...allPolicyMembers?.[accountID], - pendingAction: null, - errors: null, - }; - - return {...accountIDsWithClearedPendingAction, [accountID]: value}; - }, {}), + key: policyKey, + value: { + employeeList: successMembersState, + }, }, ...newPersonalDetailsOnyxData.finallyData, ...membersChats.onyxSuccessData, @@ -1453,7 +1435,7 @@ function addMembersToWorkspace(invitedEmailsToAccountIDs: InvitedEmailsToAccount const failureData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: membersListKey, + key: policyKey, // Convert to object with each key containing the error. We don’t // need to remove the members since that is handled by onClose of OfflineWithFeedback. @@ -1919,10 +1901,13 @@ function updateWorkspaceCustomUnitAndRate(policyID: string, currentCustomUnit: C * Removes an error after trying to delete a member */ function clearDeleteMemberError(policyID: string, accountID: number) { - Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}`, { - [accountID]: { - pendingAction: null, - errors: null, + const email = allPersonalDetails?.[accountID]?.login ?? ''; + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + employeeList: { + [email]: { + pendingAction: null, + errors: null, + }, }, }); } @@ -1931,8 +1916,11 @@ function clearDeleteMemberError(policyID: string, accountID: number) { * Removes an error after trying to add a member */ function clearAddMemberError(policyID: string, accountID: number) { - Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}`, { - [accountID]: null, + const email = allPersonalDetails?.[accountID]?.login ?? ''; + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + employeeList: { + [email]: null, + }, }); Onyx.merge(`${ONYXKEYS.PERSONAL_DETAILS_LIST}`, { [accountID]: null, @@ -2066,22 +2054,18 @@ function createDraftInitialWorkspace(policyOwnerEmail = '', policyName = '', pol customUnits, makeMeAdmin, autoReporting: true, + employeeList: { + [sessionEmail]: { + role: CONST.POLICY.ROLE.ADMIN, + errors: {}, + }, + }, approvalMode: CONST.POLICY.APPROVAL_MODE.OPTIONAL, harvesting: { enabled: true, }, }, }, - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.POLICY_MEMBERS_DRAFTS}${policyID}`, - value: { - [sessionAccountID]: { - role: CONST.POLICY.ROLE.ADMIN, - errors: {}, - }, - }, - }, ]; Onyx.update(optimisticData); @@ -2114,6 +2098,7 @@ function createWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policyName expenseReportActionData, expenseCreatedReportActionID, } = ReportUtils.buildOptimisticWorkspaceChats(policyID, workspaceName); + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.SET, @@ -2140,15 +2125,11 @@ function createWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policyName areWorkflowsEnabled: false, areReportFieldsEnabled: false, areConnectionsEnabled: false, - }, - }, - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}`, - value: { - [sessionAccountID]: { - role: CONST.POLICY.ROLE.ADMIN, - errors: {}, + employeeList: { + [sessionEmail]: { + role: CONST.POLICY.ROLE.ADMIN, + errors: {}, + }, }, }, }, @@ -2202,11 +2183,6 @@ function createWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policyName key: `${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${policyID}`, value: null, }, - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.POLICY_MEMBERS_DRAFTS}${policyID}`, - value: null, - }, ]; const successData: OnyxUpdate[] = [ @@ -2277,9 +2253,9 @@ function createWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policyName const failureData: OnyxUpdate[] = [ { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}`, - value: null, + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: {employeeList: null}, }, { onyxMethod: Onyx.METHOD.SET, @@ -2633,6 +2609,16 @@ function createWorkspaceFromIOUPayment(iouReport: Report | EmptyObject): string areWorkflowsEnabled: false, areReportFieldsEnabled: false, areConnectionsEnabled: false, + employeeList: { + [sessionEmail]: { + role: CONST.POLICY.ROLE.ADMIN, + errors: {}, + }, + [employeeEmail]: { + role: CONST.POLICY.ROLE.USER, + errors: {}, + }, + }, }; const optimisticData: OnyxUpdate[] = [ @@ -2641,20 +2627,6 @@ function createWorkspaceFromIOUPayment(iouReport: Report | EmptyObject): string key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: newWorkspace, }, - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}`, - value: { - [sessionAccountID]: { - role: CONST.POLICY.ROLE.ADMIN, - errors: {}, - }, - [employeeAccountID]: { - role: CONST.POLICY.ROLE.USER, - errors: {}, - }, - }, - }, { onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT}${announceChatReportID}`, @@ -2710,13 +2682,6 @@ function createWorkspaceFromIOUPayment(iouReport: Report | EmptyObject): string pendingAction: null, }, }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.POLICY_MEMBERS_DRAFTS}${policyID}`, - value: { - pendingAction: null, - }, - }, ...employeeWorkspaceChat.onyxOptimisticData, ]; @@ -2787,13 +2752,6 @@ function createWorkspaceFromIOUPayment(iouReport: Report | EmptyObject): string ]; const failureData: OnyxUpdate[] = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}`, - value: { - pendingAction: null, - }, - }, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${announceChatReportID}`, diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 2478d3815039..f599208ff915 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -59,7 +59,7 @@ import Navigation from '@libs/Navigation/Navigation'; import LocalNotification from '@libs/Notification/LocalNotification'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as PhoneNumber from '@libs/PhoneNumber'; -import getPolicyMemberAccountIDs from '@libs/PolicyMembersUtils'; +import getPolicyEmployeeAccountIDs from '@libs/PolicyEmployeeListUtils'; import {extractPolicyIDFromPath} from '@libs/PolicyUtils'; import processReportIDDeeplink from '@libs/processReportIDDeeplink'; import * as Pusher from '@libs/Pusher/pusher'; @@ -2186,8 +2186,8 @@ function showReportActionNotification(reportID: string, reportAction: ReportActi const onClick = () => Modal.close(() => { const policyID = lastVisitedPath && extractPolicyIDFromPath(lastVisitedPath); - const policyMembersAccountIDs = policyID ? getPolicyMemberAccountIDs(policyID) : []; - const reportBelongsToWorkspace = policyID ? doesReportBelongToWorkspace(report, policyMembersAccountIDs, policyID) : false; + const policyEmployeeAccountIDs = policyID ? getPolicyEmployeeAccountIDs(policyID) : []; + const reportBelongsToWorkspace = policyID ? doesReportBelongToWorkspace(report, policyEmployeeAccountIDs, policyID) : false; if (!reportBelongsToWorkspace) { Navigation.navigateWithSwitchPolicyID({route: ROUTES.HOME}); } diff --git a/src/libs/actions/TeachersUnite.ts b/src/libs/actions/TeachersUnite.ts index 1f8b32724bd4..36fd9d340aeb 100644 --- a/src/libs/actions/TeachersUnite.ts +++ b/src/libs/actions/TeachersUnite.ts @@ -96,17 +96,13 @@ function addSchoolPrincipal(firstName: string, partnerUserID: string, lastName: role: CONST.POLICY.ROLE.USER, owner: sessionEmail, outputCurrency: allPersonalDetails?.[sessionAccountID]?.localCurrencyCode ?? CONST.CURRENCY.USD, - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - }, - }, - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}`, - value: { - [sessionAccountID]: { - role: CONST.POLICY.ROLE.USER, - errors: {}, + employeeList: { + [sessionEmail]: { + role: CONST.POLICY.ROLE.USER, + errors: {}, + }, }, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, }, }, { @@ -155,9 +151,11 @@ function addSchoolPrincipal(firstName: string, partnerUserID: string, lastName: const failureData: OnyxUpdate[] = [ { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}`, - value: null, + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + [sessionEmail]: null, + }, }, { onyxMethod: Onyx.METHOD.SET, diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index 7f1dadab8c0e..0646f37f9b26 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -67,7 +67,7 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD const route = useRoute(); const policy = useMemo(() => policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID ?? ''}`], [policies, report?.policyID]); const isPolicyAdmin = useMemo(() => PolicyUtils.isPolicyAdmin(policy ?? null), [policy]); - const isPolicyMember = useMemo(() => PolicyUtils.isPolicyMember(report?.policyID ?? '', policies), [report?.policyID, policies]); + const isPolicyEmployee = useMemo(() => PolicyUtils.isPolicyEmployee(report?.policyID ?? '', policies), [report?.policyID, policies]); const shouldUseFullTitle = useMemo(() => ReportUtils.shouldUseFullTitleToDisplay(report), [report]); const isChatRoom = useMemo(() => ReportUtils.isChatRoom(report), [report]); const isUserCreatedPolicyRoom = useMemo(() => ReportUtils.isUserCreatedPolicyRoom(report), [report]); @@ -137,9 +137,9 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD // - The report is a user created room and the room and the current user is a workspace member i.e. non-workspace members should not see this option. if ( (isGroupChat || - (isDefaultRoom && isChatThread && isPolicyMember) || + (isDefaultRoom && isChatThread && isPolicyEmployee) || (!isUserCreatedPolicyRoom && participants.length) || - (isUserCreatedPolicyRoom && (isPolicyMember || (isChatThread && !ReportUtils.isPublicRoom(report))))) && + (isUserCreatedPolicyRoom && (isPolicyEmployee || (isChatThread && !ReportUtils.isPublicRoom(report))))) && !ReportUtils.isConciergeChatReport(report) ) { items.push({ @@ -157,8 +157,8 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD }, }); } else if ( - (isUserCreatedPolicyRoom && (!participants.length || !isPolicyMember)) || - ((isDefaultRoom || ReportUtils.isPolicyExpenseChat(report)) && isChatThread && !isPolicyMember) + (isUserCreatedPolicyRoom && (!participants.length || !isPolicyEmployee)) || + ((isDefaultRoom || ReportUtils.isPolicyExpenseChat(report)) && isChatThread && !isPolicyEmployee) ) { items.push({ key: CONST.REPORT_DETAILS_MENU_ITEM.INVITE, @@ -201,7 +201,7 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD isMoneyRequestReport, report, isGroupDMChat, - isPolicyMember, + isPolicyEmployee, isUserCreatedPolicyRoom, session, isSelfDM, diff --git a/src/pages/RoomInvitePage.tsx b/src/pages/RoomInvitePage.tsx index b540dfb5f4e9..79198e81fc43 100644 --- a/src/pages/RoomInvitePage.tsx +++ b/src/pages/RoomInvitePage.tsx @@ -156,13 +156,13 @@ function RoomInvitePage({ // Non policy members should not be able to view the participants of a room const reportID = report?.reportID; - const isPolicyMember = useMemo(() => (report?.policyID ? PolicyUtils.isPolicyMember(report.policyID, policies as Record) : false), [report?.policyID, policies]); + const isPolicyEmployee = useMemo(() => (report?.policyID ? PolicyUtils.isPolicyEmployee(report.policyID, policies as Record) : false), [report?.policyID, policies]); const backRoute = useMemo(() => { if (role === CONST.IOU.SHARE.ROLE.ACCOUNTANT) { return ROUTES.REPORT_WITH_ID.getRoute(reportID); } - return reportID && (isPolicyMember ? ROUTES.ROOM_MEMBERS.getRoute(reportID) : ROUTES.REPORT_WITH_ID_DETAILS.getRoute(reportID)); - }, [isPolicyMember, reportID, role]); + return reportID && (isPolicyEmployee ? ROUTES.ROOM_MEMBERS.getRoute(reportID) : ROUTES.REPORT_WITH_ID_DETAILS.getRoute(reportID)); + }, [isPolicyEmployee, reportID, role]); const reportName = useMemo(() => ReportUtils.getReportName(report), [report]); const inviteUsers = useCallback(() => { if (!validate()) { diff --git a/src/pages/RoomMembersPage.tsx b/src/pages/RoomMembersPage.tsx index d025a3bde265..009eaaf15a3c 100644 --- a/src/pages/RoomMembersPage.tsx +++ b/src/pages/RoomMembersPage.tsx @@ -224,11 +224,11 @@ function RoomMembersPage({report, session, policies}: RoomMembersPageProps) { return result; }; - const isPolicyMember = useMemo(() => { + const isPolicyEmployee = useMemo(() => { if (!report?.policyID || policies === null) { return false; } - return PolicyUtils.isPolicyMember(report.policyID, policies); + return PolicyUtils.isPolicyEmployee(report.policyID, policies); }, [report?.policyID, policies]); const data = getMemberOptions(); const headerMessage = searchValue.trim() && !data.length ? translate('roomMembersPage.memberNotFound') : ''; @@ -240,7 +240,7 @@ function RoomMembersPage({report, session, policies}: RoomMembersPageProps) { > { diff --git a/src/pages/home/HeaderView.tsx b/src/pages/home/HeaderView.tsx index 1a88719c2649..56828bce7847 100644 --- a/src/pages/home/HeaderView.tsx +++ b/src/pages/home/HeaderView.tsx @@ -94,11 +94,11 @@ function HeaderView({report, personalDetails, parentReport, parentReportAction, const isCanceledTaskReport = ReportUtils.isCanceledTaskReport(report, parentReportAction); const isWhisperAction = ReportActionsUtils.isWhisperAction(parentReportAction); const isUserCreatedPolicyRoom = ReportUtils.isUserCreatedPolicyRoom(report); - const isPolicyMember = useMemo(() => !isEmptyObject(policy), [policy]); - const canLeaveRoom = ReportUtils.canLeaveRoom(report, isPolicyMember); - const canLeavePolicyExpenseChat = ReportUtils.canLeavePolicyExpenseChat(report, policy); + const isPolicyEmployee = useMemo(() => !isEmptyObject(policy), [policy]); + const canLeaveRoom = ReportUtils.canLeaveRoom(report, isPolicyEmployee); const reportDescription = ReportUtils.getReportDescriptionText(report); const policyName = ReportUtils.getPolicyName(report, true); + const canLeavePolicyExpenseChat = ReportUtils.canLeavePolicyExpenseChat(report, policy); const policyDescription = ReportUtils.getPolicyDescriptionText(policy); const isPersonalExpenseChat = isPolicyExpenseChat && ReportUtils.isCurrentUserSubmitter(report.reportID); const shouldShowSubtitle = () => { @@ -153,7 +153,7 @@ function HeaderView({report, personalDetails, parentReport, parentReportAction, onSelected: join, }); } else if (canLeave) { - const isWorkspaceMemberLeavingWorkspaceRoom = !isChatThread && (report.visibility === CONST.REPORT.VISIBILITY.RESTRICTED || isPolicyExpenseChat) && isPolicyMember; + const isWorkspaceMemberLeavingWorkspaceRoom = !isChatThread && (report.visibility === CONST.REPORT.VISIBILITY.RESTRICTED || isPolicyExpenseChat) && isPolicyEmployee; threeDotMenuItems.push({ icon: Expensicons.ChatBubbles, text: translate('common.leave'), diff --git a/src/pages/home/sidebar/AllSettingsScreen.tsx b/src/pages/home/sidebar/AllSettingsScreen.tsx index 7151cc84e735..8056d9bcc413 100644 --- a/src/pages/home/sidebar/AllSettingsScreen.tsx +++ b/src/pages/home/sidebar/AllSettingsScreen.tsx @@ -18,16 +18,15 @@ import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {Policy, PolicyMembers} from '@src/types/onyx'; +import type {Policy} from '@src/types/onyx'; type AllSettingsScreenOnyxProps = { policies: OnyxCollection; - policyMembers: OnyxCollection; }; type AllSettingsScreenProps = AllSettingsScreenOnyxProps; -function AllSettingsScreen({policies, policyMembers}: AllSettingsScreenProps) { +function AllSettingsScreen({policies}: AllSettingsScreenProps) { const styles = useThemeStyles(); const waitForNavigate = useWaitForNavigation(); const {translate} = useLocalize(); @@ -48,7 +47,7 @@ function AllSettingsScreen({policies, policyMembers}: AllSettingsScreenProps) { })(); }, focused: !isSmallScreenWidth, - brickRoadIndicator: hasGlobalWorkspaceSettingsRBR(policies, policyMembers) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + brickRoadIndicator: hasGlobalWorkspaceSettingsRBR(policies) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, }, ...(shouldShowSubscriptionsMenu ? [ @@ -90,7 +89,7 @@ function AllSettingsScreen({policies, policyMembers}: AllSettingsScreenProps) { hoverAndPressStyle: styles.hoveredComponentBG, brickRoadIndicator: item.brickRoadIndicator, })); - }, [isSmallScreenWidth, styles.hoveredComponentBG, styles.sectionMenuItem, translate, waitForNavigate, policies, policyMembers]); + }, [isSmallScreenWidth, styles.hoveredComponentBG, styles.sectionMenuItem, translate, waitForNavigate, policies]); return ( ({ policies: { key: ONYXKEYS.COLLECTION.POLICY, }, - policyMembers: { - key: ONYXKEYS.COLLECTION.POLICY_MEMBERS, - }, })(AllSettingsScreen); diff --git a/src/pages/home/sidebar/SidebarLinksData.tsx b/src/pages/home/sidebar/SidebarLinksData.tsx index e56962a331a2..1000ceff1a76 100644 --- a/src/pages/home/sidebar/SidebarLinksData.tsx +++ b/src/pages/home/sidebar/SidebarLinksData.tsx @@ -15,7 +15,7 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; -import {getPolicyMembersByIdWithoutCurrentUser} from '@libs/PolicyUtils'; +import {getPolicyEmployeeListByIdWithoutCurrentUser} from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import SidebarUtils from '@libs/SidebarUtils'; import * as Policy from '@userActions/Policy'; @@ -26,7 +26,7 @@ import type {Message} from '@src/types/onyx/ReportAction'; import SidebarLinks from './SidebarLinks'; type ChatReportSelector = OnyxTypes.Report & {isUnreadWithMention: boolean}; -type PolicySelector = Pick; +type PolicySelector = Pick; type ReportActionsSelector = Array>; type SidebarLinksDataOnyxProps = { @@ -51,9 +51,6 @@ type SidebarLinksDataOnyxProps = { /** All of the transaction violations */ transactionViolations: OnyxCollection; - /** All policy members */ - policyMembers: OnyxCollection; - /** Drafts of reports */ reportsDrafts: OnyxCollection; }; @@ -77,7 +74,6 @@ function SidebarLinksData({ onLinkClick, policies, priorityMode = CONST.PRIORITY_MODE.DEFAULT, - policyMembers, transactionViolations, reportsDrafts, }: SidebarLinksDataProps) { @@ -88,7 +84,8 @@ function SidebarLinksData({ const {activeWorkspaceID} = useActiveWorkspace(); const {translate} = useLocalize(); const prevPriorityMode = usePrevious(priorityMode); - const policyMemberAccountIDs = getPolicyMembersByIdWithoutCurrentUser(policyMembers, activeWorkspaceID, accountID); + + const policyMemberAccountIDs = getPolicyEmployeeListByIdWithoutCurrentUser(policies, activeWorkspaceID, accountID); // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => Policy.openWorkspace(activeWorkspaceID ?? '', policyMemberAccountIDs), [activeWorkspaceID]); @@ -246,6 +243,7 @@ const policySelector = (policy: OnyxEntry): PolicySelector => type: policy.type, name: policy.name, avatar: policy.avatar, + employeeList: policy.employeeList, }) as PolicySelector; const SidebarLinkDataWithCurrentReportID = withCurrentReportID( @@ -265,7 +263,6 @@ const SidebarLinkDataWithCurrentReportID = withCurrentReportID( lodashIsEqual(prevProps.policies, nextProps.policies) && lodashIsEqual(prevProps.insets, nextProps.insets) && prevProps.onLinkClick === nextProps.onLinkClick && - lodashIsEqual(prevProps.policyMembers, nextProps.policyMembers) && lodashIsEqual(prevProps.transactionViolations, nextProps.transactionViolations) && prevProps.currentReportID === nextProps.currentReportID && lodashIsEqual(prevProps.reportsDrafts, nextProps.reportsDrafts), @@ -299,9 +296,6 @@ export default withOnyx; - - /** Members of all the workspaces the user is member of */ - policyMembers: OnyxCollection; }; type InitialSettingsPageProps = InitialSettingsPageOnyxProps & WithCurrentUserPersonalDetailsProps; @@ -98,7 +95,7 @@ type MenuData = { type Menu = {sectionStyle: StyleProp; sectionTranslationKey: TranslationPaths; items: MenuData[]}; -function InitialSettingsPage({session, userWallet, bankAccountList, fundList, walletTerms, loginList, currentUserPersonalDetails, policies, policyMembers}: InitialSettingsPageProps) { +function InitialSettingsPage({session, userWallet, bankAccountList, fundList, walletTerms, loginList, currentUserPersonalDetails, policies}: InitialSettingsPageProps) { const network = useNetwork(); const theme = useTheme(); const styles = useThemeStyles(); @@ -189,7 +186,7 @@ function InitialSettingsPage({session, userWallet, bankAccountList, fundList, wa translationKey: 'common.workspaces', icon: Expensicons.Building, routeName: ROUTES.SETTINGS_WORKSPACES, - brickRoadIndicator: hasGlobalWorkspaceSettingsRBR(policies, policyMembers) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + brickRoadIndicator: hasGlobalWorkspaceSettingsRBR(policies) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, }, { translationKey: 'allSettingsScreen.cardsAndDomains', @@ -221,7 +218,7 @@ function InitialSettingsPage({session, userWallet, bankAccountList, fundList, wa sectionTranslationKey: 'common.workspaces', items, }; - }, [policies, policyMembers, styles.workspaceSettingsSectionContainer]); + }, [policies, styles.workspaceSettingsSectionContainer]); /** * Retuns a list of menu items data for general section @@ -531,8 +528,5 @@ export default withCurrentUserPersonalDetails( policies: { key: ONYXKEYS.COLLECTION.POLICY, }, - policyMembers: { - key: ONYXKEYS.COLLECTION.POLICY_MEMBERS, - }, })(InitialSettingsPage), ); diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index a6a131f5372c..c066319cd31e 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -61,7 +61,7 @@ function dismissError(policyID: string) { Policy.removeWorkspace(policyID); } -function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, reimbursementAccount, policyCategories}: WorkspaceInitialPageProps) { +function WorkspaceInitialPage({policyDraft, policy: policyProp, reimbursementAccount, policyCategories}: WorkspaceInitialPageProps) { const styles = useThemeStyles(); const policy = policyDraft?.id ? policyDraft : policyProp; const [isCurrencyModalOpen, setIsCurrencyModalOpen] = useState(false); @@ -101,7 +101,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r ReimbursementAccount.navigateToBankAccountRoute(policyID); }, [policyID, policyName]); - const hasMembersError = PolicyUtils.hasPolicyMemberError(policyMembers); + const hasMembersError = PolicyUtils.hasEmployeeListError(policy); const hasPolicyCategoryError = PolicyUtils.hasPolicyCategoriesError(policyCategories); const hasGeneralSettingsError = !isEmptyObject(policy?.errorFields?.generalSettings ?? {}) || !isEmptyObject(policy?.errorFields?.avatar ?? {}); const shouldShowProtectedItems = PolicyUtils.isPolicyAdmin(policy); diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index 4a85e01d973a..0310104590e5 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -30,7 +30,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import type {Beta, InvitedEmailsToAccountIDs, PersonalDetailsList} from '@src/types/onyx'; +import type {Beta, InvitedEmailsToAccountIDs} from '@src/types/onyx'; import type {Errors} from '@src/types/onyx/OnyxCommon'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import SearchInputManager from './SearchInputManager'; @@ -40,9 +40,6 @@ import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscree type MembersSection = SectionListData>; type WorkspaceInvitePageOnyxProps = { - /** All of the personal details for everyone */ - personalDetails: OnyxEntry; - /** Beta features list */ betas: OnyxEntry; @@ -55,15 +52,7 @@ type WorkspaceInvitePageProps = WithPolicyAndFullscreenLoadingProps & WorkspaceInvitePageOnyxProps & StackScreenProps; -function WorkspaceInvitePage({ - route, - policyMembers, - personalDetails: personalDetailsProp, - betas, - invitedEmailsToAccountIDsDraft, - policy, - isLoadingReportData = true, -}: WorkspaceInvitePageProps) { +function WorkspaceInvitePage({route, betas, invitedEmailsToAccountIDsDraft, policy, isLoadingReportData = true}: WorkspaceInvitePageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const [searchTerm, setSearchTerm] = useState(''); @@ -72,7 +61,7 @@ function WorkspaceInvitePage({ const [usersToInvite, setUsersToInvite] = useState([]); const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); const openWorkspaceInvitePage = () => { - const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(policyMembers, personalDetailsProp); + const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(policy?.employeeList); Policy.openWorkspaceInvitePage(route.params.policyID, Object.keys(policyMemberEmailsToAccountIDs)); }; const {options, areOptionsInitialized} = useOptionsList({ @@ -94,7 +83,7 @@ function WorkspaceInvitePage({ useNetwork({onReconnect: openWorkspaceInvitePage}); - const excludedUsers = useMemo(() => PolicyUtils.getIneligibleInvitees(policyMembers, personalDetailsProp), [policyMembers, personalDetailsProp]); + const excludedUsers = useMemo(() => PolicyUtils.getIneligibleInvitees(policy?.employeeList), [policy?.employeeList]); useEffect(() => { const newUsersToInviteDict: Record = {}; @@ -102,7 +91,7 @@ function WorkspaceInvitePage({ const newSelectedOptionsDict: Record = {}; const inviteOptions = OptionsListUtils.getMemberInviteOptions(options.personalDetails, betas ?? [], searchTerm, excludedUsers, true); - // Update selectedOptions with the latest personalDetails and policyMembers information + // Update selectedOptions with the latest personalDetails and policyEmployeeList information const detailsMap: Record = {}; inviteOptions.personalDetails.forEach((detail) => { if (!detail.login) { @@ -152,7 +141,7 @@ function WorkspaceInvitePage({ setSelectedOptions(Object.values(newSelectedOptionsDict)); // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to recalculate when selectedOptions change - }, [options.personalDetails, policyMembers, betas, searchTerm, excludedUsers]); + }, [options.personalDetails, policy?.employeeList, betas, searchTerm, excludedUsers]); const sections: MembersSection[] = useMemo(() => { const sectionsArr: MembersSection[] = []; @@ -336,9 +325,6 @@ WorkspaceInvitePage.displayName = 'WorkspaceInvitePage'; export default withNavigationTransitionEnd( withPolicyAndFullscreenLoading( withOnyx({ - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, betas: { key: ONYXKEYS.BETAS, }, diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index a66a8dd74cce..9c5631eff0aa 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -39,7 +39,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import type {InvitedEmailsToAccountIDs, PersonalDetailsList, PolicyMember, PolicyMembers, Session} from '@src/types/onyx'; +import type {InvitedEmailsToAccountIDs, PersonalDetailsList, PolicyEmployee, PolicyEmployeeList, Session} from '@src/types/onyx'; import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; @@ -47,21 +47,16 @@ import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; import WorkspacePageWithSections from './WorkspacePageWithSections'; type WorkspaceMembersPageOnyxProps = { - /** Personal details of all users */ - personalDetails: OnyxEntry; - /** Session info for the currently logged in user. */ session: OnyxEntry; /** An object containing the accountID for every invited user email */ invitedEmailsToAccountIDsDraft: OnyxEntry; }; - type WorkspaceMembersPageProps = WithPolicyAndFullscreenLoadingProps & WithCurrentUserPersonalDetailsProps & WorkspaceMembersPageOnyxProps & StackScreenProps; - /** * Inverts an object, equivalent of _.invert */ @@ -73,7 +68,8 @@ function invertObject(object: Record): Record { type MemberOption = Omit & {accountID: number}; -function WorkspaceMembersPage({policyMembers, personalDetails, invitedEmailsToAccountIDsDraft, route, policy, session, currentUserPersonalDetails}: WorkspaceMembersPageProps) { +function WorkspaceMembersPage({personalDetails, invitedEmailsToAccountIDsDraft, route, policy, session, currentUserPersonalDetails}: WorkspaceMembersPageProps) { + const policyMemberEmailsToAccountIDs = useMemo(() => PolicyUtils.getMemberAccountIDsForWorkspace(policy?.employeeList), [policy?.employeeList]); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const [selectedEmployees, setSelectedEmployees] = useState([]); @@ -81,44 +77,42 @@ function WorkspaceMembersPage({policyMembers, personalDetails, invitedEmailsToAc const [errors, setErrors] = useState({}); const {isOffline} = useNetwork(); const prevIsOffline = usePrevious(isOffline); - const accountIDs = useMemo(() => Object.keys(policyMembers ?? {}).map((accountID) => Number(accountID)), [policyMembers]); + const accountIDs = useMemo(() => Object.values(policyMemberEmailsToAccountIDs ?? {}).map((accountID) => Number(accountID)), [policyMemberEmailsToAccountIDs]); const prevAccountIDs = usePrevious(accountIDs); const textInputRef = useRef(null); - const isOfflineAndNoMemberDataAvailable = isEmptyObject(policyMembers) && isOffline; + const isOfflineAndNoMemberDataAvailable = isEmptyObject(policy?.employeeList) && isOffline; const prevPersonalDetails = usePrevious(personalDetails); const {translate, formatPhoneNumber, preferredLocale} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); const dropdownButtonRef = useRef(null); const isPolicyAdmin = PolicyUtils.isPolicyAdmin(policy); const isLoading = useMemo( - () => !isOfflineAndNoMemberDataAvailable && (!OptionsListUtils.isPersonalDetailsReady(personalDetails) || isEmptyObject(policyMembers)), - [isOfflineAndNoMemberDataAvailable, personalDetails, policyMembers], + () => !isOfflineAndNoMemberDataAvailable && (!OptionsListUtils.isPersonalDetailsReady(personalDetails) || isEmptyObject(policy?.employeeList)), + [isOfflineAndNoMemberDataAvailable, personalDetails, policy?.employeeList], ); const selectionListRef = useRef(null); const isFocused = useIsFocused(); - const policyID = route.params.policyID; - /** - * Get filtered personalDetails list with current policyMembers + * Get filtered personalDetails list with current employeeList */ - const filterPersonalDetails = (members: OnyxEntry, details: OnyxEntry): PersonalDetailsList => + const filterPersonalDetails = (members: OnyxEntry, details: OnyxEntry): PersonalDetailsList => Object.keys(members ?? {}).reduce((result, key) => { - if (details?.[key]) { + const memberAccountIdKey = policyMemberEmailsToAccountIDs[key] ?? ''; + if (details?.[memberAccountIdKey]) { return { ...result, - [key]: details[key], + [memberAccountIdKey]: details[memberAccountIdKey], }; } return result; }, {}); - /** * Get members for the current workspace */ const getWorkspaceMembers = useCallback(() => { - Policy.openWorkspaceMembersPage(route.params.policyID, Object.keys(PolicyUtils.getMemberAccountIDsForWorkspace(policyMembers, personalDetails))); - }, [route.params.policyID, policyMembers, personalDetails]); + Policy.openWorkspaceMembersPage(route.params.policyID, Object.keys(PolicyUtils.getMemberAccountIDsForWorkspace(policy?.employeeList))); + }, [route.params.policyID, policy?.employeeList]); /** * Check if the current selection includes members that cannot be removed @@ -135,7 +129,7 @@ function WorkspaceMembersPage({policyMembers, personalDetails, invitedEmailsToAc // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedEmployees, policy?.owner, session?.accountID]); - // useFocusEffect would make getWorkspaceMembers get called twice on fresh login because policyMember is a dependency of getWorkspaceMembers. + // useFocus would make getWorkspaceMembers get called twice on fresh login because policyEmployee is a dependency of getWorkspaceMembers. useEffect(() => { if (!isFocused) { setSelectedEmployees([]); @@ -155,20 +149,23 @@ function WorkspaceMembersPage({policyMembers, personalDetails, invitedEmailsToAc } setSelectedEmployees((prevSelected) => { // Filter all personal details in order to use the elements needed for the current workspace - const currentPersonalDetails = filterPersonalDetails(policyMembers, personalDetails); + const currentPersonalDetails = filterPersonalDetails(policy?.employeeList ?? {}, personalDetails); // We need to filter the previous selected employees by the new personal details, since unknown/new user id's change when transitioning from offline to online const prevSelectedElements = prevSelected.map((id) => { const prevItem = prevPersonalDetails?.id; const res = Object.values(currentPersonalDetails).find((item) => prevItem?.login === item?.login); return res?.accountID ?? id; }); + + const currentSelectedElements = Object.entries(PolicyUtils.getMemberAccountIDsForWorkspace(policy?.employeeList)) + .filter((employee) => policy?.employeeList?.[employee[0]]?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) + .map((employee) => employee[1]); + // This is an equivalent of the lodash intersection function. The reduce method below is used to filter the items that exist in both arrays. - return [prevSelectedElements, Object.values(PolicyUtils.getMemberAccountIDsForWorkspace(policyMembers, personalDetails))].reduce((prev, members) => - prev.filter((item) => members.includes(item)), - ); + return [prevSelectedElements, currentSelectedElements].reduce((prev, members) => prev.filter((item) => members.includes(item))); }); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [policyMembers]); + }, [policy?.employeeList, policyMemberEmailsToAccountIDs]); useEffect(() => { const isReconnecting = prevIsOffline && !isOffline; @@ -277,7 +274,6 @@ function WorkspaceMembersPage({policyMembers, personalDetails, invitedEmailsToAc Navigation.navigate(ROUTES.PROFILE.getRoute(item.accountID)); return; } - Policy.clearWorkspaceOwnerChangeFlow(policyID); Navigation.navigate(ROUTES.WORKSPACE_MEMBER_DETAILS.getRoute(route.params.policyID, item.accountID)); }, @@ -301,21 +297,20 @@ function WorkspaceMembersPage({policyMembers, personalDetails, invitedEmailsToAc /** * Check if the policy member is deleted from the workspace */ - const isDeletedPolicyMember = useCallback( - (policyMember: PolicyMember): boolean => !isOffline && policyMember.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && isEmptyObject(policyMember.errors), + const isDeletedPolicyEmployee = useCallback( + (policyEmployee: PolicyEmployee): boolean => !isOffline && policyEmployee.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && isEmptyObject(policyEmployee.errors), [isOffline], ); + const policyOwner = policy?.owner; const currentUserLogin = currentUserPersonalDetails.login; - const invitedPrimaryToSecondaryLogins = invertObject(policy?.primaryLoginsInvited ?? {}); - const getUsers = useCallback((): MemberOption[] => { let result: MemberOption[] = []; - Object.entries(policyMembers ?? {}).forEach(([accountIDKey, policyMember]) => { - const accountID = Number(accountIDKey); - if (isDeletedPolicyMember(policyMember)) { + Object.entries(policy?.employeeList ?? {}).forEach(([email, policyEmployee]) => { + const accountID = Number(policyMemberEmailsToAccountIDs[email] ?? ''); + if (isDeletedPolicyEmployee(policyEmployee)) { return; } @@ -338,8 +333,7 @@ function WorkspaceMembersPage({policyMembers, personalDetails, invitedEmailsToAc const isSelected = selectedEmployees.includes(accountID); const isOwner = policy?.owner === details.login; - const isAdmin = policyMember.role === CONST.POLICY.ROLE.ADMIN; - + const isAdmin = policyEmployee.role === CONST.POLICY.ROLE.ADMIN; let roleBadge = null; if (isOwner || isAdmin) { roleBadge = ( @@ -352,11 +346,11 @@ function WorkspaceMembersPage({policyMembers, personalDetails, invitedEmailsToAc } result.push({ - keyForList: accountIDKey, + keyForList: String(accountID), accountID, isSelected, isDisabledCheckbox: !(isPolicyAdmin && accountID !== policy?.ownerAccountID && accountID !== session?.accountID), - isDisabled: isPolicyAdmin && (policyMember.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || !isEmptyObject(policyMember.errors)), + isDisabled: isPolicyAdmin && (policyEmployee.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || !isEmptyObject(policyEmployee.errors)), text: formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(details)), alternateText: formatPhoneNumber(details?.login ?? ''), rightElement: roleBadge, @@ -368,28 +362,26 @@ function WorkspaceMembersPage({policyMembers, personalDetails, invitedEmailsToAc id: accountID, }, ], - errors: policyMember.errors, - pendingAction: policyMember.pendingAction, - + errors: policyEmployee.errors, + pendingAction: policyEmployee.pendingAction, // Note which secondary login was used to invite this primary login invitedSecondaryLogin: details?.login ? invitedPrimaryToSecondaryLogins[details.login] ?? '' : '', }); }); - result = result.sort((a, b) => (a.text ?? '').toLowerCase().localeCompare((b.text ?? '').toLowerCase())); - return result; }, [ StyleUtils, currentUserLogin, formatPhoneNumber, invitedPrimaryToSecondaryLogins, - isDeletedPolicyMember, + isDeletedPolicyEmployee, isPolicyAdmin, personalDetails, policy?.owner, policy?.ownerAccountID, - policyMembers, + policy?.employeeList, + policyMemberEmailsToAccountIDs, policyOwner, selectedEmployees, session?.accountID, @@ -419,7 +411,7 @@ function WorkspaceMembersPage({policyMembers, personalDetails, invitedEmailsToAc return translate('workspace.common.mustBeOnlineToViewMembers'); } - return !isLoading && isEmptyObject(policyMembers) ? translate('workspace.common.memberNotFound') : ''; + return !isLoading && isEmptyObject(policy?.employeeList) ? translate('workspace.common.memberNotFound') : ''; }; const getHeaderContent = () => ( @@ -448,11 +440,9 @@ function WorkspaceMembersPage({policyMembers, personalDetails, invitedEmailsToAc ); - if (isPolicyAdmin) { return header; } - return {header}; }; @@ -461,7 +451,7 @@ function WorkspaceMembersPage({policyMembers, personalDetails, invitedEmailsToAc return; } - const accountIDsToUpdate = selectedEmployees.filter((id) => policyMembers?.[id].role !== role); + const accountIDsToUpdate = selectedEmployees.filter((id) => policy?.employeeList?.[policyMemberEmailsToAccountIDs[id]]?.role !== role); Policy.updateWorkspaceMembersRole(route.params.policyID, accountIDsToUpdate, role); setSelectedEmployees([]); @@ -478,7 +468,7 @@ function WorkspaceMembersPage({policyMembers, personalDetails, invitedEmailsToAc ]; if (PolicyUtils.isPaidGroupPolicy(policy)) { - if (selectedEmployees.find((employee) => policyMembers?.[employee]?.role === CONST.POLICY.ROLE.ADMIN)) { + if (selectedEmployees.find((employeeEmail) => policy?.employeeList?.[policyMemberEmailsToAccountIDs[employeeEmail]]?.role === CONST.POLICY.ROLE.ADMIN)) { options.push({ text: translate('workspace.people.makeMember'), value: CONST.POLICY.MEMBERS_BULK_ACTION_TYPES.MAKE_MEMBER, @@ -487,7 +477,7 @@ function WorkspaceMembersPage({policyMembers, personalDetails, invitedEmailsToAc }); } - if (selectedEmployees.find((employee) => policyMembers?.[employee]?.role === CONST.POLICY.ROLE.USER)) { + if (selectedEmployees.find((employeeEmail) => policy?.employeeList?.[policyMemberEmailsToAccountIDs[employeeEmail]]?.role === CONST.POLICY.ROLE.USER)) { options.push({ text: translate('workspace.people.makeAdmin'), value: CONST.POLICY.MEMBERS_BULK_ACTION_TYPES.MAKE_ADMIN, @@ -496,7 +486,6 @@ function WorkspaceMembersPage({policyMembers, personalDetails, invitedEmailsToAc }); } } - return options; }; @@ -504,7 +493,6 @@ function WorkspaceMembersPage({policyMembers, personalDetails, invitedEmailsToAc if (!isPolicyAdmin) { return null; } - return ( {selectedEmployees.length > 0 ? ( @@ -599,9 +587,6 @@ WorkspaceMembersPage.displayName = 'WorkspaceMembersPage'; export default withCurrentUserPersonalDetails( withPolicyAndFullscreenLoading( withOnyx({ - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, invitedEmailsToAccountIDsDraft: { key: ({route}) => `${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT}${route.params.policyID.toString()}`, }, diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx index 3f4dc7297b15..8e77da494ab3 100755 --- a/src/pages/workspace/WorkspacesListPage.tsx +++ b/src/pages/workspace/WorkspacesListPage.tsx @@ -37,7 +37,7 @@ import * as Session from '@userActions/Session'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {PolicyMembers, Policy as PolicyType, ReimbursementAccount, Report, Session as SessionType} from '@src/types/onyx'; +import type {Policy as PolicyType, ReimbursementAccount, Report, Session as SessionType} from '@src/types/onyx'; import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; @@ -75,9 +75,6 @@ type WorkspaceListPageOnyxProps = { /** Bank account attached to free plan */ reimbursementAccount: OnyxEntry; - /** A collection of objects for all policies which key policy member objects by accountIDs */ - allPolicyMembers: OnyxCollection; - /** All reports shared with the user (coming from Onyx) */ reports: OnyxCollection; @@ -118,7 +115,7 @@ function dismissWorkspaceError(policyID: string, pendingAction: OnyxCommon.Pendi throw new Error('Not implemented'); } -function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, reports, session}: WorkspaceListPageProps) { +function WorkspacesListPage({policies, reimbursementAccount, reports, session}: WorkspaceListPageProps) { const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -336,7 +333,7 @@ function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, r title: policy.name, icon: policy.avatar ? policy.avatar : ReportUtils.getDefaultWorkspaceAvatar(policy.name), action: () => Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(policy.id)), - brickRoadIndicator: reimbursementAccountBrickRoadIndicator ?? PolicyUtils.getPolicyBrickRoadIndicatorStatus(policy, allPolicyMembers), + brickRoadIndicator: reimbursementAccountBrickRoadIndicator ?? PolicyUtils.getPolicyBrickRoadIndicatorStatus(policy), pendingAction: policy.pendingAction, errors: policy.errors, dismissError: () => { @@ -358,7 +355,7 @@ function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, r }; }) .sort((a, b) => localeCompare(a.title, b.title)); - }, [reimbursementAccount?.errors, policies, isOffline, theme.textLight, allPolicyMembers, policyRooms]); + }, [reimbursementAccount?.errors, policies, isOffline, theme.textLight, policyRooms]); if (isEmptyObject(workspaces)) { return ( @@ -449,9 +446,6 @@ export default withPolicyAndFullscreenLoading( policies: { key: ONYXKEYS.COLLECTION.POLICY, }, - allPolicyMembers: { - key: ONYXKEYS.COLLECTION.POLICY_MEMBERS, - }, // @ts-expect-error: ONYXKEYS.REIMBURSEMENT_ACCOUNT is conflicting with ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM reimbursementAccount: { key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, diff --git a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx index 57a416b7d130..32b43a230619 100644 --- a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx +++ b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx @@ -2,7 +2,6 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import Avatar from '@components/Avatar'; import Button from '@components/Button'; @@ -28,7 +27,6 @@ import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPol import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type {PersonalDetails, PersonalDetailsList} from '@src/types/onyx'; @@ -44,7 +42,7 @@ type WorkspaceMemberDetailsPageProps = Omit; -function WorkspaceMemberDetailsPage({personalDetails, policyMembers, policy, route}: WorkspaceMemberDetailsPageProps) { +function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceMemberDetailsPageProps) { const styles = useThemeStyles(); const {isOffline} = useNetwork(); const {translate} = useLocalize(); @@ -57,14 +55,15 @@ function WorkspaceMemberDetailsPage({personalDetails, policyMembers, policy, rou const accountID = Number(route.params.accountID); const policyID = route.params.policyID; - const member = policyMembers?.[accountID]; + const memberLogin = personalDetails?.[accountID]?.login ?? ''; + const member = policy?.employeeList?.[memberLogin]; const details = personalDetails?.[accountID] ?? ({} as PersonalDetails); const avatar = details.avatar ?? UserUtils.getDefaultAvatar(); const fallbackIcon = details.fallbackIcon ?? ''; const displayName = details.displayName ?? ''; const isSelectedMemberOwner = policy?.owner === details.login; const isSelectedMemberCurrentUser = accountID === currentUserPersonalDetails?.accountID; - const isCurrentUserAdmin = policyMembers?.[currentUserPersonalDetails?.accountID]?.role === CONST.POLICY.ROLE.ADMIN; + const isCurrentUserAdmin = policy?.employeeList?.[personalDetails?.[currentUserPersonalDetails?.accountID]?.login ?? '']?.role === CONST.POLICY.ROLE.ADMIN; const isCurrentUserOwner = policy?.owner === currentUserPersonalDetails?.login; const roleItems: ListItemType[] = useMemo( @@ -78,7 +77,7 @@ function WorkspaceMemberDetailsPage({personalDetails, policyMembers, policy, rou { value: CONST.POLICY.ROLE.USER, text: translate('common.member'), - isSelected: member?.role === CONST.POLICY.ROLE.USER, + isSelected: member?.role !== CONST.POLICY.ROLE.ADMIN, keyForList: CONST.POLICY.ROLE.USER, }, ], @@ -218,10 +217,4 @@ function WorkspaceMemberDetailsPage({personalDetails, policyMembers, policy, rou WorkspaceMemberDetailsPage.displayName = 'WorkspaceMemberDetailsPage'; -export default withPolicyAndFullscreenLoading( - withOnyx({ - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, - })(WorkspaceMemberDetailsPage), -); +export default withPolicyAndFullscreenLoading(WorkspaceMemberDetailsPage); diff --git a/src/pages/workspace/members/WorkspaceMemberDetailsRoleSelectionModal.tsx b/src/pages/workspace/members/WorkspaceMemberDetailsRoleSelectionModal.tsx index 9613a697013e..f1d4334f7069 100644 --- a/src/pages/workspace/members/WorkspaceMemberDetailsRoleSelectionModal.tsx +++ b/src/pages/workspace/members/WorkspaceMemberDetailsRoleSelectionModal.tsx @@ -43,7 +43,10 @@ function WorkspaceMemberDetailsRoleSelectionModal({isVisible, items, onRoleChang hideModalContentWhileAnimating useNativeDriver > - + ; - policyMembers: OnyxEntry; policyDraft: OnyxEntry; - policyMembersDraft: OnyxEntry; }; type WithPolicyProps = WithPolicyOnyxProps & { @@ -135,9 +129,7 @@ type WithPolicyProps = WithPolicyOnyxProps & { const policyDefaultProps: WithPolicyOnyxProps = { policy: {} as OnyxTypes.Policy, - policyMembers: {}, policyDraft: {} as OnyxTypes.Policy, - policyMembersDraft: {}, }; /* @@ -168,15 +160,9 @@ export default function (WrappedComponent: policy: { key: (props) => `${ONYXKEYS.COLLECTION.POLICY}${getPolicyIDFromRoute(props.route)}`, }, - policyMembers: { - key: (props) => `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${getPolicyIDFromRoute(props.route)}`, - }, policyDraft: { key: (props) => `${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${getPolicyIDFromRoute(props.route)}`, }, - policyMembersDraft: { - key: (props) => `${ONYXKEYS.COLLECTION.POLICY_MEMBERS_DRAFTS}${getPolicyIDFromRoute(props.route)}`, - }, })(forwardRef(WithPolicy)); } diff --git a/src/pages/workspace/withPolicyAndFullscreenLoading.tsx b/src/pages/workspace/withPolicyAndFullscreenLoading.tsx index 93e10cc1760c..161320441843 100644 --- a/src/pages/workspace/withPolicyAndFullscreenLoading.tsx +++ b/src/pages/workspace/withPolicyAndFullscreenLoading.tsx @@ -6,12 +6,16 @@ import {withOnyx} from 'react-native-onyx'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import compose from '@libs/compose'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {PersonalDetailsList} from '@src/types/onyx'; import type {WithPolicyOnyxProps, WithPolicyProps} from './withPolicy'; import withPolicy, {policyDefaultProps} from './withPolicy'; type WithPolicyAndFullscreenLoadingOnyxProps = { /** Indicated whether the report data is loading */ isLoadingReportData: OnyxEntry; + + /** Personal details of all users */ + personalDetails: OnyxEntry; }; type WithPolicyAndFullscreenLoadingProps = WithPolicyProps & WithPolicyAndFullscreenLoadingOnyxProps; @@ -24,14 +28,7 @@ export default function withPolicyAndFullscreenLoading>, ): ComponentWithPolicyAndFullscreenLoading { function WithPolicyAndFullscreenLoading( - { - isLoadingReportData = true, - policy = policyDefaultProps.policy, - policyDraft = policyDefaultProps.policyDraft, - policyMembers = policyDefaultProps.policyMembers, - policyMembersDraft = policyDefaultProps.policyMembersDraft, - ...rest - }: TProps, + {isLoadingReportData = true, policy = policyDefaultProps.policy, policyDraft = policyDefaultProps.policyDraft, ...rest}: TProps, ref: ForwardedRef, ) { if (isLoadingReportData && isEmpty(policy) && isEmpty(policyDraft)) { @@ -45,8 +42,6 @@ export default function withPolicyAndFullscreenLoading ); @@ -59,6 +54,9 @@ export default function withPolicyAndFullscreenLoading & {accountID: number}; type MembersSection = SectionListData>; -function WorkspaceWorkflowsApproverPage({policy, policyMembers, personalDetails, isLoadingReportData = true, route}: WorkspaceWorkflowsApproverPageProps) { +function WorkspaceWorkflowsApproverPage({policy, personalDetails, isLoadingReportData = true, route}: WorkspaceWorkflowsApproverPageProps) { const {translate} = useLocalize(); const policyName = policy?.name ?? ''; const [searchTerm, setSearchTerm] = useState(''); @@ -52,21 +49,24 @@ function WorkspaceWorkflowsApproverPage({policy, policyMembers, personalDetails, const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const isDeletedPolicyMember = useCallback( - (policyMember: PolicyMember) => !isOffline && policyMember.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && isEmptyObject(policyMember.errors), + const isDeletedPolicyEmployee = useCallback( + (policyEmployee: PolicyEmployee) => !isOffline && policyEmployee.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && isEmptyObject(policyEmployee.errors), [isOffline], ); - const [formattedPolicyMembers, formattedApprover] = useMemo(() => { + const [formattedPolicyEmployeeList, formattedApprover] = useMemo(() => { const policyMemberDetails: MemberOption[] = []; const approverDetails: MemberOption[] = []; - Object.entries(policyMembers ?? {}).forEach(([accountIDKey, policyMember]) => { - const accountID = Number(accountIDKey); - if (isDeletedPolicyMember(policyMember)) { + const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(policy?.employeeList); + + Object.entries(policy?.employeeList ?? {}).forEach(([email, policyEmployee]) => { + if (isDeletedPolicyEmployee(policyEmployee)) { return; } + const accountID = Number(policyMemberEmailsToAccountIDs[email] ?? ''); + const details = personalDetails?.[accountID]; if (!details) { Log.hmmm(`[WorkspaceMembersPage] no personal details found for policy member with accountID: ${accountID}`); @@ -74,7 +74,7 @@ function WorkspaceWorkflowsApproverPage({policy, policyMembers, personalDetails, } const isOwner = policy?.owner === details.login; - const isAdmin = policyMember.role === CONST.POLICY.ROLE.ADMIN; + const isAdmin = policyEmployee.role === CONST.POLICY.ROLE.ADMIN; let roleBadge = null; if (isOwner || isAdmin) { @@ -88,10 +88,10 @@ function WorkspaceWorkflowsApproverPage({policy, policyMembers, personalDetails, } const formattedMember = { - keyForList: accountIDKey, + keyForList: String(accountID), accountID, isSelected: policy?.approver === details.login, - isDisabled: policyMember.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || !isEmptyObject(policyMember.errors), + isDisabled: policyEmployee.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || !isEmptyObject(policyEmployee.errors), text: formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(details)), alternateText: formatPhoneNumber(details?.login ?? ''), rightElement: roleBadge, @@ -103,8 +103,8 @@ function WorkspaceWorkflowsApproverPage({policy, policyMembers, personalDetails, id: accountID, }, ], - errors: policyMember.errors, - pendingAction: policyMember.pendingAction, + errors: policyEmployee.errors, + pendingAction: policyEmployee.pendingAction, }; if (policy?.approver === details.login) { @@ -114,13 +114,13 @@ function WorkspaceWorkflowsApproverPage({policy, policyMembers, personalDetails, } }); return [policyMemberDetails, approverDetails]; - }, [personalDetails, policyMembers, translate, policy?.approver, StyleUtils, isDeletedPolicyMember, policy?.owner, styles]); + }, [personalDetails, translate, policy?.approver, StyleUtils, isDeletedPolicyEmployee, policy?.owner, styles, policy?.employeeList]); const sections: MembersSection[] = useMemo(() => { const sectionsArray: MembersSection[] = []; if (searchTerm !== '') { - const filteredOptions = [...formattedApprover, ...formattedPolicyMembers].filter((option) => { + const filteredOptions = [...formattedApprover, ...formattedPolicyEmployeeList].filter((option) => { const searchValue = OptionsListUtils.getSearchValueForPhoneOrEmail(searchTerm); return !!option.text?.toLowerCase().includes(searchValue) || !!option.login?.toLowerCase().includes(searchValue); }); @@ -141,12 +141,12 @@ function WorkspaceWorkflowsApproverPage({policy, policyMembers, personalDetails, sectionsArray.push({ title: translate('common.all'), - data: formattedPolicyMembers, + data: formattedPolicyEmployeeList, shouldShow: true, }); return sectionsArray; - }, [formattedPolicyMembers, formattedApprover, searchTerm, translate]); + }, [formattedPolicyEmployeeList, formattedApprover, searchTerm, translate]); const headerMessage = useMemo( () => (searchTerm && !sections[0].data.length ? translate('common.noResultsFound') : ''), @@ -202,11 +202,4 @@ function WorkspaceWorkflowsApproverPage({policy, policyMembers, personalDetails, WorkspaceWorkflowsApproverPage.displayName = 'WorkspaceWorkflowsApproverPage'; -export default compose( - withOnyx({ - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, - }), - withPolicyAndFullscreenLoading, -)(WorkspaceWorkflowsApproverPage); +export default withPolicyAndFullscreenLoading(WorkspaceWorkflowsApproverPage); diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx index f2d8cda48ef0..da51b2c3e8e3 100644 --- a/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx @@ -2,7 +2,6 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React, {useCallback, useMemo, useState} from 'react'; import type {SectionListData} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; import Badge from '@components/Badge'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -14,7 +13,6 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import {formatPhoneNumber} from '@libs/LocalePhoneNumber'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; @@ -29,9 +27,8 @@ import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullsc import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; -import type {PersonalDetailsList, PolicyMember} from '@src/types/onyx'; +import type {PersonalDetailsList, PolicyEmployee} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; type WorkspaceWorkflowsPayerPageOnyxProps = { @@ -45,7 +42,7 @@ type WorkspaceWorkflowsPayerPageProps = WorkspaceWorkflowsPayerPageOnyxProps & type MemberOption = Omit & {accountID: number}; type MembersSection = SectionListData>; -function WorkspaceWorkflowsPayerPage({route, policy, policyMembers, personalDetails, isLoadingReportData = true}: WorkspaceWorkflowsPayerPageProps) { +function WorkspaceWorkflowsPayerPage({route, policy, personalDetails, isLoadingReportData = true}: WorkspaceWorkflowsPayerPageProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -54,8 +51,8 @@ function WorkspaceWorkflowsPayerPage({route, policy, policyMembers, personalDeta const [searchTerm, setSearchTerm] = useState(''); - const isDeletedPolicyMember = useCallback( - (policyMember: PolicyMember) => !isOffline && policyMember.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && isEmptyObject(policyMember.errors), + const isDeletedPolicyEmployee = useCallback( + (policyEmployee: PolicyEmployee) => !isOffline && policyEmployee.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && isEmptyObject(policyEmployee.errors), [isOffline], ); @@ -63,8 +60,10 @@ function WorkspaceWorkflowsPayerPage({route, policy, policyMembers, personalDeta const policyAdminDetails: MemberOption[] = []; const authorizedPayerDetails: MemberOption[] = []; - Object.entries(policyMembers ?? {}).forEach(([accountIDKey, policyMember]) => { - const accountID = Number(accountIDKey); + const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(policy?.employeeList); + + Object.entries(policy?.employeeList ?? {}).forEach(([email, policyEmployee]) => { + const accountID = policyMemberEmailsToAccountIDs?.[email] ?? ''; const details = personalDetails?.[accountID]; if (!details) { Log.hmmm(`[WorkspaceMembersPage] no personal details found for policy member with accountID: ${accountID}`); @@ -72,8 +71,8 @@ function WorkspaceWorkflowsPayerPage({route, policy, policyMembers, personalDeta } const isOwner = policy?.owner === details?.login; - const isAdmin = policyMember.role === CONST.POLICY.ROLE.ADMIN; - const shouldSkipMember = isDeletedPolicyMember(policyMember) || PolicyUtils.isExpensifyTeam(details?.login) || (!isOwner && !isAdmin); + const isAdmin = policyEmployee.role === CONST.POLICY.ROLE.ADMIN; + const shouldSkipMember = isDeletedPolicyEmployee(policyEmployee) || PolicyUtils.isExpensifyTeam(details?.login) || (!isOwner && !isAdmin); if (shouldSkipMember) { return; @@ -90,10 +89,10 @@ function WorkspaceWorkflowsPayerPage({route, policy, policyMembers, personalDeta const isAuthorizedPayer = policy?.achAccount?.reimburser === details?.login; const formattedMember = { - keyForList: accountIDKey, + keyForList: String(accountID), accountID, isSelected: isAuthorizedPayer, - isDisabled: policyMember.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || !isEmptyObject(policyMember.errors), + isDisabled: policyEmployee.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || !isEmptyObject(policyEmployee.errors), text: formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(details)), alternateText: formatPhoneNumber(details?.login ?? ''), rightElement: roleBadge, @@ -105,8 +104,8 @@ function WorkspaceWorkflowsPayerPage({route, policy, policyMembers, personalDeta id: accountID, }, ], - errors: policyMember.errors, - pendingAction: policyMember.pendingAction ?? isAuthorizedPayer ? policy?.pendingFields?.reimburser : null, + errors: policyEmployee.errors, + pendingAction: policyEmployee.pendingAction ?? isAuthorizedPayer ? policy?.pendingFields?.reimburser : null, }; if (isAuthorizedPayer) { @@ -116,7 +115,7 @@ function WorkspaceWorkflowsPayerPage({route, policy, policyMembers, personalDeta } }); return [policyAdminDetails, authorizedPayerDetails]; - }, [personalDetails, policyMembers, translate, policy?.achAccount?.reimburser, isDeletedPolicyMember, policy?.owner, styles, StyleUtils, policy?.pendingFields?.reimburser]); + }, [personalDetails, policy?.employeeList, translate, policy?.achAccount?.reimburser, isDeletedPolicyEmployee, policy?.owner, styles, StyleUtils, policy?.pendingFields?.reimburser]); const sections: MembersSection[] = useMemo(() => { const sectionsArray: MembersSection[] = []; @@ -213,11 +212,4 @@ function WorkspaceWorkflowsPayerPage({route, policy, policyMembers, personalDeta WorkspaceWorkflowsPayerPage.displayName = 'WorkspaceWorkflowsPayerPage'; -export default compose( - withOnyx({ - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, - }), - withPolicyAndFullscreenLoading, -)(WorkspaceWorkflowsPayerPage); +export default withPolicyAndFullscreenLoading(WorkspaceWorkflowsPayerPage); diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 3d85a6a2a526..f3cd26c73d1a 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -333,7 +333,7 @@ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback< submitsTo?: number; /** The employee list of the policy */ - employeeList?: OnyxTypes.PolicyMembers | []; + employeeList?: OnyxTypes.PolicyEmployeeList; /** The reimbursement choice for policy */ reimbursementChoice?: ValueOf; diff --git a/src/types/onyx/PolicyMember.ts b/src/types/onyx/PolicyEmployee.ts similarity index 73% rename from src/types/onyx/PolicyMember.ts rename to src/types/onyx/PolicyEmployee.ts index 366a7ef7d530..4a5f374de44a 100644 --- a/src/types/onyx/PolicyMember.ts +++ b/src/types/onyx/PolicyEmployee.ts @@ -1,6 +1,6 @@ import type * as OnyxCommon from './OnyxCommon'; -type PolicyMember = OnyxCommon.OnyxValueWithOfflineFeedback<{ +type PolicyEmployee = OnyxCommon.OnyxValueWithOfflineFeedback<{ /** Role of the user in the policy */ role?: string; @@ -20,7 +20,7 @@ type PolicyMember = OnyxCommon.OnyxValueWithOfflineFeedback<{ errors?: OnyxCommon.Errors; }>; -type PolicyMembers = Record; +type PolicyEmployeeList = Record; -export default PolicyMember; -export type {PolicyMembers}; +export default PolicyEmployee; +export type {PolicyEmployeeList}; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index f6d96a2ee85e..ea0870a7b8c6 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -37,9 +37,9 @@ import type PlaidData from './PlaidData'; import type Policy from './Policy'; import type {PolicyConnectionSyncProgress, PolicyReportField, TaxRate, TaxRates, TaxRatesWithDefault} from './Policy'; import type {PolicyCategories, PolicyCategory} from './PolicyCategory'; +import type {PolicyEmployeeList} from './PolicyEmployee'; +import type PolicyEmployee from './PolicyEmployee'; import type PolicyJoinMember from './PolicyJoinMember'; -import type {PolicyMembers} from './PolicyMember'; -import type PolicyMember from './PolicyMember'; import type PolicyOwnershipChangeChecks from './PolicyOwnershipChangeChecks'; import type {PolicyTag, PolicyTagList, PolicyTags} from './PolicyTag'; import type PreferredTheme from './PreferredTheme'; @@ -117,9 +117,9 @@ export type { Policy, PolicyCategories, PolicyCategory, + PolicyEmployee, + PolicyEmployeeList, PolicyConnectionSyncProgress, - PolicyMember, - PolicyMembers, PolicyOwnershipChangeChecks, PolicyTag, PolicyTags, diff --git a/tests/actions/PolicyTest.ts b/tests/actions/PolicyTest.ts index 18a6337a9b93..44b69bcb86fe 100644 --- a/tests/actions/PolicyTest.ts +++ b/tests/actions/PolicyTest.ts @@ -4,7 +4,7 @@ import CONST from '@src/CONST'; import OnyxUpdateManager from '@src/libs/actions/OnyxUpdateManager'; import * as Policy from '@src/libs/actions/Policy'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PolicyMembers, Policy as PolicyType, Report, ReportAction, ReportActions} from '@src/types/onyx'; +import type {Policy as PolicyType, Report, ReportAction, ReportActions} from '@src/types/onyx'; import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; @@ -59,19 +59,7 @@ describe('actions/Policy', () => { expect(policy?.owner).toBe(ESH_EMAIL); expect(policy?.isPolicyExpenseChatEnabled).toBe(true); expect(policy?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); - - const policyMembers: OnyxEntry = await new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}`, - callback: (members) => { - Onyx.disconnect(connectionID); - resolve(members); - }, - }); - }); - - // check if the user was added as an admin to the policy - expect(policyMembers?.[ESH_ACCOUNT_ID]?.role).toBe(CONST.POLICY.ROLE.ADMIN); + expect(policy?.employeeList).toEqual({[ESH_EMAIL]: {errors: {}, role: CONST.POLICY.ROLE.ADMIN}}); let allReports: OnyxCollection = await new Promise((resolve) => { const connectionID = Onyx.connect({ diff --git a/tests/perf-test/PolicyUtils.perf-test.ts b/tests/perf-test/PolicyUtils.perf-test.ts index 98403b310f25..0fc0a71bef03 100644 --- a/tests/perf-test/PolicyUtils.perf-test.ts +++ b/tests/perf-test/PolicyUtils.perf-test.ts @@ -1,33 +1,29 @@ import {measureFunction} from 'reassure'; import {getMemberAccountIDsForWorkspace} from '@libs/PolicyUtils'; -import type {PersonalDetails, PolicyMember} from '@src/types/onyx'; import createCollection from '../utils/collections/createCollection'; -import createPersonalDetails from '../utils/collections/personalDetails'; -import createRandomPolicyMember from '../utils/collections/policyMembers'; +import createRandomPolicyEmployeeList from '../utils/collections/policyEmployeeList'; describe('PolicyUtils', () => { describe('getMemberAccountIDsForWorkspace', () => { test('500 policy members with personal details', async () => { - const policyMembers = createCollection( + const policyEmployeeList = createCollection( (_, index) => index, - () => createRandomPolicyMember(), + () => createRandomPolicyEmployeeList(), ); - const personalDetails = createCollection((_, index) => index, createPersonalDetails); - await measureFunction(() => getMemberAccountIDsForWorkspace(policyMembers, personalDetails)); + await measureFunction(() => getMemberAccountIDsForWorkspace(policyEmployeeList)); }); test('500 policy members with errors and personal details', async () => { - const policyMembers = createCollection( + const policyEmployeeList = createCollection( (_, index) => index, () => ({ - ...createRandomPolicyMember(), + ...createRandomPolicyEmployeeList(), errors: {error: 'Error message'}, }), ); - const personalDetails = createCollection((_, index) => index, createPersonalDetails); - await measureFunction(() => getMemberAccountIDsForWorkspace(policyMembers, personalDetails)); + await measureFunction(() => getMemberAccountIDsForWorkspace(policyEmployeeList)); }); }); }); diff --git a/tests/utils/LHNTestUtils.tsx b/tests/utils/LHNTestUtils.tsx index 0630fddb81b7..248b0f946edc 100644 --- a/tests/utils/LHNTestUtils.tsx +++ b/tests/utils/LHNTestUtils.tsx @@ -248,7 +248,7 @@ function getFakePolicy(id = '1', name = 'Workspace-Test-001'): Policy { owner: 'myuser@gmail.com', outputCurrency: 'BRL', avatar: '', - employeeList: [], + employeeList: {}, isPolicyExpenseChatEnabled: true, lastModified: '1697323926777105', autoReporting: true, diff --git a/tests/utils/collections/policyEmployeeList.ts b/tests/utils/collections/policyEmployeeList.ts new file mode 100644 index 000000000000..9e04c71748b7 --- /dev/null +++ b/tests/utils/collections/policyEmployeeList.ts @@ -0,0 +1,9 @@ +import {randWord} from '@ngneat/falso'; +import type {PolicyEmployee} from '@src/types/onyx'; + +export default function createRandomPolicyEmployeeList(): PolicyEmployee { + return { + role: randWord(), + errors: {}, + }; +} diff --git a/tests/utils/collections/policyMembers.ts b/tests/utils/collections/policyMembers.ts deleted file mode 100644 index 076c8ddb2d3d..000000000000 --- a/tests/utils/collections/policyMembers.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {randWord} from '@ngneat/falso'; -import type {PolicyMember} from '@src/types/onyx'; - -export default function createRandomPolicyMember(): PolicyMember { - return { - role: randWord(), - errors: {}, - }; -}