diff --git a/src/libs/WorkflowUtils.ts b/src/libs/WorkflowUtils.ts index 5e769f730a1f..72f0b7b4582a 100644 --- a/src/libs/WorkflowUtils.ts +++ b/src/libs/WorkflowUtils.ts @@ -262,10 +262,11 @@ function convertApprovalWorkflowToPolicyEmployees({ continue; } + const previousPendingAction = previousEmployeeList[approver.email]?.pendingAction; updatedEmployeeList[approver.email] = { email: approver.email, forwardsTo, - pendingAction, + pendingAction: previousPendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE ? previousPendingAction : pendingAction, pendingFields: { forwardsTo: pendingAction, }, @@ -281,10 +282,11 @@ function convertApprovalWorkflowToPolicyEmployees({ continue; } + const previousPendingAction = previousEmployeeList[email]?.pendingAction; updatedEmployeeList[email] = { ...(updatedEmployeeList[email] ? updatedEmployeeList[email] : {email}), submitsTo, - pendingAction, + pendingAction: previousPendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE ? previousPendingAction : pendingAction, pendingFields: { submitsTo: pendingAction, }, @@ -295,10 +297,11 @@ function convertApprovalWorkflowToPolicyEmployees({ // which will set the submitsTo field to the default approver email on backend. if (membersToRemove) { for (const {email} of membersToRemove) { + const previousPendingAction = previousEmployeeList[email]?.pendingAction; updatedEmployeeList[email] = { ...(updatedEmployeeList[email] ? updatedEmployeeList[email] : {email}), submitsTo: defaultApprover, - pendingAction, + pendingAction: previousPendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE ? previousPendingAction : pendingAction, }; } } @@ -307,10 +310,11 @@ function convertApprovalWorkflowToPolicyEmployees({ // which will reset the forwardsTo on the backend. if (approversToRemove) { for (const {email} of approversToRemove) { + const previousPendingAction = previousEmployeeList[email]?.pendingAction; updatedEmployeeList[email] = { ...(updatedEmployeeList[email] ? updatedEmployeeList[email] : {email}), forwardsTo: '', - pendingAction, + pendingAction: previousPendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE ? previousPendingAction : pendingAction, }; } } diff --git a/src/libs/actions/Policy/Member.ts b/src/libs/actions/Policy/Member.ts index bd5d5a807f1d..356d7d949765 100644 --- a/src/libs/actions/Policy/Member.ts +++ b/src/libs/actions/Policy/Member.ts @@ -24,17 +24,31 @@ import * as PhoneNumber from '@libs/PhoneNumber'; import {getDefaultApprover, isUserPolicyAdmin} from '@libs/PolicyUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; +import {updateWorkflowDataOnApproverRemoval} from '@libs/WorkflowUtils'; import * as FormActions from '@userActions/FormActions'; +import {getRemoveApprovalWorkflowOnyxData, getUpdateApprovalWorkflowOnyxData} from '@userActions/Workflow'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {ImportedSpreadsheetMemberData, InvitedEmailsToAccountIDs, Policy, PolicyEmployee, PolicyOwnershipChangeChecks, Report, ReportAction, ReportActions} from '@src/types/onyx'; +import type { + ImportedSpreadsheetMemberData, + InvitedEmailsToAccountIDs, + PersonalDetails, + PersonalDetailsList, + Policy, + PolicyEmployee, + PolicyOwnershipChangeChecks, + Report, + ReportAction, + ReportActions, +} from '@src/types/onyx'; +import type ApprovalWorkflow from '@src/types/onyx/ApprovalWorkflow'; import type {PendingAction} from '@src/types/onyx/OnyxCommon'; import type {JoinWorkspaceResolution} from '@src/types/onyx/OriginalMessage'; import type {ApprovalRule} from '@src/types/onyx/Policy'; import type {NotificationPreference, Participant} from '@src/types/onyx/Report'; import type {OnyxData} from '@src/types/onyx/Request'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import {createPolicyExpenseChats} from './Policy'; +import {createPolicyExpenseChats, getSetPolicyPreventSelfApprovalOnyxData} from './Policy'; type OnyxDataReturnType = { optimisticData: OnyxUpdate[]; @@ -395,7 +409,13 @@ function resetAccountingPreferredExporter(policyID: string, loginList: string[]) * Remove the passed members from the policy employeeList * Please see https://github.com/Expensify/App/blob/main/README.md#Security for more details */ -function removeMembers(policyID: string, selectedMemberEmails: string[], policyMemberEmailsToAccountIDs: Record) { +function removeMembers( + policyID: string, + selectedMemberEmails: string[], + policyMemberEmailsToAccountIDs: Record, + approvalWorkflows: ApprovalWorkflow[], + allPersonalDetails: OnyxEntry, +) { if (selectedMemberEmails.length === 0) { return; } @@ -407,6 +427,44 @@ function removeMembers(policyID: string, selectedMemberEmails: string[], policyM // eslint-disable-next-line @typescript-eslint/no-deprecated const policy = getPolicy(policyID); + const optimisticData: OnyxUpdate[] = []; + const successData: OnyxUpdate[] = []; + const failureData: OnyxUpdate[] = []; + + // Update approval workflows after member removal + // Check if any of the account IDs are approvers + const hasApprovers = selectedMemberEmails.some((selectedMemberEmail) => isApprover(policy, selectedMemberEmail)); + const ownerDetails = allPersonalDetails?.[policy?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID] ?? ({} as PersonalDetails); + + if (hasApprovers) { + const ownerEmail = ownerDetails.login; + accountIDs.forEach((accountID) => { + const removedApprover = allPersonalDetails?.[accountID]; + if (!removedApprover?.login || !ownerEmail) { + return; + } + const updatedWorkflows = updateWorkflowDataOnApproverRemoval({ + approvalWorkflows, + removedApprover, + ownerDetails, + }); + updatedWorkflows.forEach((workflow) => { + if (workflow?.removeApprovalWorkflow) { + const {removeApprovalWorkflow, ...updatedWorkflow} = workflow; + const onyxDataForRemoveApprovalWorkflow = getRemoveApprovalWorkflowOnyxData(updatedWorkflow, policy); + optimisticData.push(...(onyxDataForRemoveApprovalWorkflow.optimisticData ?? [])); + successData.push(...(onyxDataForRemoveApprovalWorkflow.successData ?? [])); + failureData.push(...(onyxDataForRemoveApprovalWorkflow.failureData ?? [])); + } else { + const onyxDataForUpdateApprovalWorkflow = getUpdateApprovalWorkflowOnyxData(workflow, [], [], policy); + optimisticData.push(...(onyxDataForUpdateApprovalWorkflow.optimisticData ?? [])); + successData.push(...(onyxDataForUpdateApprovalWorkflow.successData ?? [])); + failureData.push(...(onyxDataForUpdateApprovalWorkflow.failureData ?? [])); + } + }); + }); + } + const workspaceChats = ReportUtils.getWorkspaceChats(policyID, accountIDs); const optimisticClosedReportActions = workspaceChats.map(() => ReportUtils.buildOptimisticClosedReportAction(sessionEmail, policy?.name ?? '', CONST.REPORT.ARCHIVE_REASON.REMOVED_FROM_POLICY), @@ -477,38 +535,32 @@ function removeMembers(policyID: string, selectedMemberEmails: string[], policyM const approvalRules: ApprovalRule[] = policy?.rules?.approvalRules ?? []; const optimisticApprovalRules = approvalRules.filter((rule) => !selectedMemberEmails.includes(rule?.approver ?? '')); - const optimisticData: OnyxUpdate[] = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: policyKey, - value: { - employeeList: optimisticMembersState, - approver: selectedMemberEmails.includes(policy?.approver ?? '') ? policy?.owner : policy?.approver, - rules: { - ...(policy?.rules ?? {}), - approvalRules: optimisticApprovalRules, - }, + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: policyKey, + value: { + employeeList: optimisticMembersState, + approver: selectedMemberEmails.includes(policy?.approver ?? '') ? policy?.owner : policy?.approver, + rules: { + ...(policy?.rules ?? {}), + approvalRules: optimisticApprovalRules, }, }, - ]; + }); optimisticData.push(...announceRoomMembers.optimisticData, ...adminRoomMembers.optimisticData, ...preferredExporterOnyxData.optimisticData); - const successData: OnyxUpdate[] = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: policyKey, - value: {employeeList: successMembersState}, - }, - ]; + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: policyKey, + value: {employeeList: successMembersState}, + }); successData.push(...announceRoomMembers.successData, ...adminRoomMembers.successData, ...preferredExporterOnyxData.successData); - const failureData: OnyxUpdate[] = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: policyKey, - value: {employeeList: failureMembersState, approver: policy?.approver, rules: policy?.rules}, - }, - ]; + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: policyKey, + value: {employeeList: failureMembersState, approver: policy?.approver, rules: policy?.rules}, + }); failureData.push(...announceRoomMembers.failureData, ...adminRoomMembers.failureData, ...preferredExporterOnyxData.failureData); const pendingChatMembers = ReportUtils.getPendingChatMembers(accountIDs, [], CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); @@ -647,6 +699,16 @@ function removeMembers(policyID: string, selectedMemberEmails: string[], policyM policyID, }; + // Update "Prevent Self Approvals" after member removal + const previousEmployeesCount = Object.values(policy?.employeeList ?? {}).filter((employee) => employee.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE).length; + const remainingEmployeeCount = previousEmployeesCount - accountIDs.length; + if (remainingEmployeeCount === 1 && policy?.preventSelfApproval) { + const onyxDataForSetPolicyPreventSelfApproval = getSetPolicyPreventSelfApprovalOnyxData(policyID, false); + optimisticData.push(...(onyxDataForSetPolicyPreventSelfApproval.optimisticData ?? [])); + successData.push(...(onyxDataForSetPolicyPreventSelfApproval.successData ?? [])); + failureData.push(...(onyxDataForSetPolicyPreventSelfApproval.failureData ?? [])); + } + API.write(WRITE_COMMANDS.DELETE_MEMBERS_FROM_WORKSPACE, params, {optimisticData, successData, failureData}); } diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 1327f3526110..b8d4857ab25d 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -5677,18 +5677,13 @@ function setPolicyPreventMemberCreatedTitle(policyID: string, enforced: boolean) }); } -/** - * Call the API to enable or disable self approvals for the reports - * @param policyID - id of the policy to apply the naming pattern to - * @param preventSelfApproval - flag whether to prevent workspace members from approving their own expense reports - */ -function setPolicyPreventSelfApproval(policyID: string, preventSelfApproval: boolean) { +function getSetPolicyPreventSelfApprovalOnyxData(policyID: string, preventSelfApproval: boolean): OnyxData { // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 // eslint-disable-next-line @typescript-eslint/no-deprecated const policy = getPolicy(policyID); if (preventSelfApproval === policy?.preventSelfApproval) { - return; + return {}; } const optimisticData: OnyxUpdate[] = [ @@ -5733,6 +5728,25 @@ function setPolicyPreventSelfApproval(policyID: string, preventSelfApproval: boo }, ]; + return {optimisticData, failureData, successData}; +} + +/** + * Call the API to enable or disable self approvals for the reports + * @param policyID - id of the policy to apply the naming pattern to + * @param preventSelfApproval - flag whether to prevent workspace members from approving their own expense reports + */ +function setPolicyPreventSelfApproval(policyID: string, preventSelfApproval: boolean) { + // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 + // eslint-disable-next-line @typescript-eslint/no-deprecated + const policy = getPolicy(policyID); + + if (preventSelfApproval === policy?.preventSelfApproval) { + return; + } + + const {optimisticData, failureData, successData} = getSetPolicyPreventSelfApprovalOnyxData(policyID, preventSelfApproval); + const parameters: SetPolicyPreventSelfApprovalParams = { preventSelfApproval, policyID, @@ -6532,4 +6546,5 @@ export { clearPolicyTitleFieldError, inviteWorkspaceEmployeesToUber, setWorkspaceConfirmationCurrency, + getSetPolicyPreventSelfApprovalOnyxData, }; diff --git a/src/libs/actions/Workflow.ts b/src/libs/actions/Workflow.ts index 52f3cc015ec8..77d4008b3709 100644 --- a/src/libs/actions/Workflow.ts +++ b/src/libs/actions/Workflow.ts @@ -12,6 +12,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {ApprovalWorkflowOnyx, PersonalDetailsList, Policy} from '@src/types/onyx'; import type {Approver, Member} from '@src/types/onyx/ApprovalWorkflow'; import type ApprovalWorkflow from '@src/types/onyx/ApprovalWorkflow'; +import type {OnyxData} from '@src/types/onyx/Request'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; type SetApprovalWorkflowApproverParams = { @@ -82,9 +83,9 @@ function createApprovalWorkflow(approvalWorkflow: ApprovalWorkflow, policy: Onyx API.write(WRITE_COMMANDS.CREATE_WORKSPACE_APPROVAL, parameters, {optimisticData, failureData, successData}); } -function updateApprovalWorkflow(approvalWorkflow: ApprovalWorkflow, membersToRemove: Member[], approversToRemove: Approver[], policy: OnyxEntry) { +function getUpdateApprovalWorkflowOnyxData(approvalWorkflow: ApprovalWorkflow, membersToRemove: Member[], approversToRemove: Approver[], policy: OnyxEntry): OnyxData { if (!policy) { - return; + return {}; } const previousDefaultApprover = getDefaultApprover(policy); @@ -101,7 +102,7 @@ function updateApprovalWorkflow(approvalWorkflow: ApprovalWorkflow, membersToRem // If there are no changes to the employees list, we can exit early if (isEmptyObject(updatedEmployees) && !newDefaultApprover) { - return; + return {}; } const optimisticData: OnyxUpdate[] = [ @@ -142,6 +143,33 @@ function updateApprovalWorkflow(approvalWorkflow: ApprovalWorkflow, membersToRem }, ]; + return {optimisticData, failureData, successData}; +} + +function updateApprovalWorkflow(approvalWorkflow: ApprovalWorkflow, membersToRemove: Member[], approversToRemove: Approver[], policy: OnyxEntry) { + if (!policy) { + return; + } + + const previousDefaultApprover = getDefaultApprover(policy); + const newDefaultApprover = approvalWorkflow.isDefault ? approvalWorkflow.approvers.at(0)?.email : undefined; + const previousEmployeeList = Object.fromEntries(Object.entries(policy.employeeList ?? {}).map(([key, value]) => [key, {...value, pendingAction: null}])); + const updatedEmployees = convertApprovalWorkflowToPolicyEmployees({ + previousEmployeeList, + approvalWorkflow, + type: CONST.APPROVAL_WORKFLOW.TYPE.UPDATE, + membersToRemove, + approversToRemove, + defaultApprover: newDefaultApprover ?? previousDefaultApprover ?? '', + }); + + // If there are no changes to the employees list, we can exit early + if (isEmptyObject(updatedEmployees) && !newDefaultApprover) { + return; + } + + const {optimisticData, failureData, successData} = getUpdateApprovalWorkflowOnyxData(approvalWorkflow, membersToRemove, approversToRemove, policy); + const parameters: UpdateWorkspaceApprovalParams = { policyID: policy.id, employees: JSON.stringify(Object.values(updatedEmployees)), @@ -150,12 +178,12 @@ function updateApprovalWorkflow(approvalWorkflow: ApprovalWorkflow, membersToRem API.write(WRITE_COMMANDS.UPDATE_WORKSPACE_APPROVAL, parameters, {optimisticData, failureData, successData}); } -function removeApprovalWorkflow(approvalWorkflow: ApprovalWorkflow, policy: OnyxEntry) { +function getRemoveApprovalWorkflowOnyxData(approvalWorkflow: ApprovalWorkflow, policy: OnyxEntry): OnyxData { if (!policy) { - return; + return {}; } - const previousEmployeeList = Object.fromEntries(Object.entries(policy.employeeList ?? {}).map(([key, value]) => [key, {...value, pendingAction: null}])); + const previousEmployeeList = policy.employeeList ?? {}; const updatedEmployees = convertApprovalWorkflowToPolicyEmployees({previousEmployeeList, approvalWorkflow, type: CONST.APPROVAL_WORKFLOW.TYPE.REMOVE}); const updatedEmployeeList = {...previousEmployeeList, ...updatedEmployees}; @@ -200,6 +228,19 @@ function removeApprovalWorkflow(approvalWorkflow: ApprovalWorkflow, policy: Onyx }, ]; + return {optimisticData, failureData, successData}; +} + +function removeApprovalWorkflow(approvalWorkflow: ApprovalWorkflow, policy: OnyxEntry) { + if (!policy) { + return; + } + + const previousEmployeeList = Object.fromEntries(Object.entries(policy.employeeList ?? {}).map(([key, value]) => [key, {...value, pendingAction: null}])); + const updatedEmployees = convertApprovalWorkflowToPolicyEmployees({previousEmployeeList, approvalWorkflow, type: CONST.APPROVAL_WORKFLOW.TYPE.REMOVE}); + + const {optimisticData, failureData, successData} = getRemoveApprovalWorkflowOnyxData(approvalWorkflow, policy); + const parameters: RemoveWorkspaceApprovalParams = {policyID: policy.id, employees: JSON.stringify(Object.values(updatedEmployees))}; API.write(WRITE_COMMANDS.REMOVE_WORKSPACE_APPROVAL, parameters, {optimisticData, failureData, successData}); } @@ -322,4 +363,6 @@ export { clearApprovalWorkflowApprovers, clearApprovalWorkflow, validateApprovalWorkflow, + getRemoveApprovalWorkflowOnyxData, + getUpdateApprovalWorkflowOnyxData, }; diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index 85cd8a993f02..261dc7e488b4 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -43,7 +43,6 @@ import { removeMembers, updateWorkspaceMembersRole, } from '@libs/actions/Policy/Member'; -import {removeApprovalWorkflow as removeApprovalWorkflowAction, updateApprovalWorkflow} from '@libs/actions/Workflow'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import {getLatestErrorMessageField} from '@libs/ErrorUtils'; import Log from '@libs/Log'; @@ -55,14 +54,14 @@ import {getDisplayNameOrDefault, getPersonalDetailsByIDs} from '@libs/PersonalDe import {getMemberAccountIDsForWorkspace, isControlPolicy, isDeletedPolicyEmployee, isExpensifyTeam, isPaidGroupPolicy, isPolicyAdmin as isPolicyAdminUtils} from '@libs/PolicyUtils'; import {getDisplayNameForParticipant} from '@libs/ReportUtils'; import tokenizedSearch from '@libs/tokenizedSearch'; -import {convertPolicyEmployeesToApprovalWorkflows, updateWorkflowDataOnApproverRemoval} from '@libs/WorkflowUtils'; +import {convertPolicyEmployeesToApprovalWorkflows} from '@libs/WorkflowUtils'; import {close} from '@userActions/Modal'; import {dismissAddedWithPrimaryLoginMessages} from '@userActions/Policy/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, PolicyEmployee, PolicyEmployeeList} from '@src/types/onyx'; +import type {PolicyEmployee, PolicyEmployeeList} from '@src/types/onyx'; import type {PendingAction} from '@src/types/onyx/OnyxCommon'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import MemberRightIcon from './MemberRightIcon'; @@ -133,7 +132,6 @@ function WorkspaceMembersPage({personalDetails, route, policy}: WorkspaceMembers const policyID = route.params.policyID; const illustrations = useMemoizedLazyIllustrations(['ReceiptWrangler'] as const); - const ownerDetails = personalDetails?.[policy?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID] ?? ({} as PersonalDetails); const {approvalWorkflows} = useMemo( () => convertPolicyEmployeesToApprovalWorkflows({ @@ -205,38 +203,11 @@ function WorkspaceMembersPage({personalDetails, route, policy}: WorkspaceMembers * Please see https://github.com/Expensify/App/blob/main/README.md#Security for more details */ const removeUsers = () => { - // Check if any of the members are approvers - const hasApprovers = selectedEmployees.some((email) => isApprover(policy, email)); - - if (hasApprovers) { - const ownerEmail = ownerDetails.login; - for (const login of selectedEmployees) { - const accountID = policyMemberEmailsToAccountIDs[login]; - const removedApprover = personalDetails?.[accountID]; - if (!removedApprover?.login || !ownerEmail) { - continue; - } - const updatedWorkflows = updateWorkflowDataOnApproverRemoval({ - approvalWorkflows, - removedApprover, - ownerDetails, - }); - for (const workflow of updatedWorkflows) { - if (workflow?.removeApprovalWorkflow) { - const {removeApprovalWorkflow, ...updatedWorkflow} = workflow; - removeApprovalWorkflowAction(updatedWorkflow, policy); - } else { - updateApprovalWorkflow(workflow, [], [], policy); - } - } - } - } - setRemoveMembersConfirmModalVisible(false); // eslint-disable-next-line @typescript-eslint/no-deprecated InteractionManager.runAfterInteractions(() => { setSelectedEmployees([]); - removeMembers(policyID, selectedEmployees, policyMemberEmailsToAccountIDs); + removeMembers(policyID, selectedEmployees, policyMemberEmailsToAccountIDs, approvalWorkflows, personalDetails); }); }; diff --git a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx index c0d04fe4b483..c055c90758aa 100644 --- a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx +++ b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx @@ -27,8 +27,6 @@ import usePrevious from '@hooks/usePrevious'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeIllustrations from '@hooks/useThemeIllustrations'; import useThemeStyles from '@hooks/useThemeStyles'; -import {setPolicyPreventSelfApproval} from '@libs/actions/Policy/Policy'; -import {removeApprovalWorkflow as removeApprovalWorkflowAction, updateApprovalWorkflow} from '@libs/actions/Workflow'; import {getAllCardsForWorkspace, getCardFeedIcon, getCompanyFeeds, getPlaidInstitutionIconUrl, isExpensifyCardFullySetUp, lastFourNumbersFromCardName, maskCardNumber} from '@libs/CardUtils'; import {convertToDisplayString} from '@libs/CurrencyUtils'; import navigateAfterInteraction from '@libs/Navigation/navigateAfterInteraction'; @@ -36,7 +34,7 @@ import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavig import {getDisplayNameOrDefault, getPhoneNumber} from '@libs/PersonalDetailsUtils'; import {isControlPolicy} from '@libs/PolicyUtils'; import shouldRenderTransferOwnerButton from '@libs/shouldRenderTransferOwnerButton'; -import {convertPolicyEmployeesToApprovalWorkflows, updateWorkflowDataOnApproverRemoval} from '@libs/WorkflowUtils'; +import {convertPolicyEmployeesToApprovalWorkflows} from '@libs/WorkflowUtils'; import Navigation from '@navigation/Navigation'; import type {SettingsNavigatorParamList} from '@navigation/types'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; @@ -228,46 +226,9 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM // Function to remove a member and close the modal const removeMemberAndCloseModal = useCallback(() => { - removeMembers(policyID, [memberLogin], {[memberLogin]: accountID}); - const previousEmployeesCount = Object.keys(policy?.employeeList ?? {}).length; - const remainingEmployeeCount = previousEmployeesCount - 1; - if (remainingEmployeeCount === 1 && policy?.preventSelfApproval) { - // We can't let the "Prevent Self Approvals" enabled if there's only one workspace user - setPolicyPreventSelfApproval(policyID, false); - } + removeMembers(policyID, [memberLogin], {[memberLogin]: accountID}, approvalWorkflows, personalDetails); setIsRemoveMemberConfirmModalVisible(false); - }, [accountID, memberLogin, policy?.employeeList, policy?.preventSelfApproval, policyID]); - - const removeUser = useCallback(() => { - const ownerEmail = ownerDetails?.login; - const removedApprover = personalDetails?.[accountID]; - - // If the user is not an approver, proceed with member removal - if (!isApproverUserAction(policy, memberLogin) || !removedApprover?.login || !ownerEmail) { - removeMemberAndCloseModal(); - return; - } - - // Update approval workflows after approver removal - const updatedWorkflows = updateWorkflowDataOnApproverRemoval({ - approvalWorkflows, - removedApprover, - ownerDetails, - }); - - for (const workflow of updatedWorkflows) { - if (workflow?.removeApprovalWorkflow) { - const {removeApprovalWorkflow, ...updatedWorkflow} = workflow; - - removeApprovalWorkflowAction(updatedWorkflow, policy); - } else { - updateApprovalWorkflow(workflow, [], [], policy); - } - } - - // Remove the member and close the modal - removeMemberAndCloseModal(); - }, [accountID, approvalWorkflows, ownerDetails, personalDetails, policy, removeMemberAndCloseModal, memberLogin]); + }, [accountID, memberLogin, approvalWorkflows, policyID, personalDetails]); const navigateToProfile = useCallback(() => { Navigation.navigate(ROUTES.PROFILE.getRoute(accountID, Navigation.getActiveRoute())); @@ -395,7 +356,7 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM danger title={translate('workspace.people.removeMemberTitle')} isVisible={isRemoveMemberConfirmModalVisible} - onConfirm={removeUser} + onConfirm={removeMemberAndCloseModal} onCancel={() => setIsRemoveMemberConfirmModalVisible(false)} prompt={confirmModalPrompt} confirmText={translate('common.remove')} diff --git a/src/pages/workspace/rules/ExpenseReportRulesSection.tsx b/src/pages/workspace/rules/ExpenseReportRulesSection.tsx index bbd6949ad662..2a32e95ad4ea 100644 --- a/src/pages/workspace/rules/ExpenseReportRulesSection.tsx +++ b/src/pages/workspace/rules/ExpenseReportRulesSection.tsx @@ -25,6 +25,8 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) { const {environmentURL} = useEnvironment(); const workflowApprovalsUnavailable = getWorkflowApprovalsUnavailable(policy); const autoPayApprovedReportsUnavailable = !policy?.areWorkflowsEnabled || policy?.reimbursementChoice !== CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES || !hasVBBA(policyID); + const membersCount = Object.values(policy?.employeeList ?? {}).filter((employee) => employee.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE).length; + const shouldLockPreventSelfApprovals = workflowApprovalsUnavailable || membersCount === 1; const renderFallbackSubtitle = ({featureName, variant = 'unlock'}: {featureName: string; variant?: 'unlock' | 'enable'}) => { const moreFeaturesLink = `${environmentURL}/${ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID)}`; @@ -37,14 +39,14 @@ function ExpenseReportRulesSection({policyID}: ExpenseReportRulesSectionProps) { const optionItems = [ { title: translate('workspace.rules.expenseReportRules.preventSelfApprovalsTitle'), - subtitle: workflowApprovalsUnavailable + subtitle: shouldLockPreventSelfApprovals ? renderFallbackSubtitle({featureName: translate('common.approvals').toLowerCase()}) : translate('workspace.rules.expenseReportRules.preventSelfApprovalsSubtitle'), - shouldParseSubtitle: workflowApprovalsUnavailable, + shouldParseSubtitle: shouldLockPreventSelfApprovals, switchAccessibilityLabel: translate('workspace.rules.expenseReportRules.preventSelfApprovalsTitle'), isActive: policy?.preventSelfApproval && !workflowApprovalsUnavailable, - disabled: workflowApprovalsUnavailable, - showLockIcon: workflowApprovalsUnavailable, + disabled: shouldLockPreventSelfApprovals, + showLockIcon: shouldLockPreventSelfApprovals, pendingAction: policy?.pendingFields?.preventSelfApproval, onToggle: (isEnabled: boolean) => setPolicyPreventSelfApproval(policyID, isEnabled), }, diff --git a/tests/actions/PolicyMemberTest.ts b/tests/actions/PolicyMemberTest.ts index 7386f82c7bc9..c745287d97ac 100644 --- a/tests/actions/PolicyMemberTest.ts +++ b/tests/actions/PolicyMemberTest.ts @@ -7,7 +7,7 @@ import * as Member from '@src/libs/actions/Policy/Member'; import * as Policy from '@src/libs/actions/Policy/Policy'; import * as ReportActionsUtils from '@src/libs/ReportActionsUtils'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {ImportedSpreadsheet, Policy as PolicyType, Report, ReportAction, ReportMetadata} from '@src/types/onyx'; +import type {ImportedSpreadsheet, PersonalDetailsList, Policy as PolicyType, Report, ReportAction, ReportMetadata} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import createPersonalDetails from '../utils/collections/personalDetails'; import createRandomPolicy from '../utils/collections/policies'; @@ -427,11 +427,12 @@ describe('actions/PolicyMember', () => { const userAccountID = 1236; const userEmail = 'user@example.com'; - await Onyx.set(`${ONYXKEYS.PERSONAL_DETAILS_LIST}`, { + const allPersonalDetails = { [adminAccountID]: {login: adminEmail}, [auditorAccountID]: {login: auditorEmail}, [userAccountID]: {login: userEmail}, - }); + } as unknown as PersonalDetailsList; + await Onyx.set(`${ONYXKEYS.PERSONAL_DETAILS_LIST}`, allPersonalDetails); await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { ...createRandomPolicy(Number(policyID)), approver: defaultApprover, @@ -460,7 +461,7 @@ describe('actions/PolicyMember', () => { [auditorEmail]: auditorAccountID, [userEmail]: userAccountID, }; - Member.removeMembers(policyID, [adminEmail, auditorEmail, userEmail], memberEmailsToAccountIDs); + Member.removeMembers(policyID, [adminEmail, auditorEmail, userEmail], memberEmailsToAccountIDs, [], allPersonalDetails); await waitForBatchedUpdates(); @@ -517,9 +518,13 @@ describe('actions/PolicyMember', () => { [expenseAction.reportActionID]: expenseAction, }); + const allPersonalDetails = { + [userAccountID]: {login: userEmail}, + } as unknown as PersonalDetailsList; + // When removing a member from the workspace mockFetch?.pause?.(); - Member.removeMembers(policyID, [userEmail], {[userEmail]: userAccountID}); + Member.removeMembers(policyID, [userEmail], {[userEmail]: userAccountID}, [], allPersonalDetails); await waitForBatchedUpdates(); @@ -567,7 +572,10 @@ describe('actions/PolicyMember', () => { // When removing a member and the request fails mockFetch?.fail?.(); - Member.removeMembers(policyID, [userEmail], {[userEmail]: userAccountID}); + const allPersonalDetails = { + [userAccountID]: {login: userEmail}, + } as unknown as PersonalDetailsList; + Member.removeMembers(policyID, [userEmail], {[userEmail]: userAccountID}, [], allPersonalDetails); await waitForBatchedUpdates();