From b79a1f7298ad76169b5d018ee0c7189788ec0f01 Mon Sep 17 00:00:00 2001 From: Huu Le <20178761+huult@users.noreply.github.com> Date: Sat, 14 Dec 2024 01:24:41 +0700 Subject: [PATCH 01/13] update approver remove --- src/libs/WorkflowUtils.ts | 81 ++++++++++++++++++- .../members/WorkspaceMemberDetailsPage.tsx | 34 +++++++- 2 files changed, 111 insertions(+), 4 deletions(-) diff --git a/src/libs/WorkflowUtils.ts b/src/libs/WorkflowUtils.ts index 1b13d270b6891..563836fbeda83 100644 --- a/src/libs/WorkflowUtils.ts +++ b/src/libs/WorkflowUtils.ts @@ -4,6 +4,7 @@ import CONST from '@src/CONST'; import type {ApprovalWorkflowOnyx, Approver, Member} from '@src/types/onyx/ApprovalWorkflow'; import type ApprovalWorkflow from '@src/types/onyx/ApprovalWorkflow'; import type {PersonalDetailsList} from '@src/types/onyx/PersonalDetails'; +import type PersonalDetails from '@src/types/onyx/PersonalDetails'; import type {PolicyEmployeeList} from '@src/types/onyx/PolicyEmployee'; const INITIAL_APPROVAL_WORKFLOW: ApprovalWorkflowOnyx = { @@ -200,6 +201,21 @@ type ConvertApprovalWorkflowToPolicyEmployeesParams = { type: ValueOf; }; +type UpdateWorkflowDataOnApproverRemovalParams = { + /** + * An array of approval workflows that need to be updated. + */ + approvalWorkflows: ApprovalWorkflow[]; + /** + * The email of the approver being removed + */ + removedApprover: PersonalDetails; + /** + * The email of the workspace owner + */ + ownerDetails: PersonalDetails; +}; + /** * This function converts an approval workflow into a list of policy employees. * An optimized list is created that contains only the updated employees to maintain minimal data changes. @@ -281,5 +297,68 @@ function convertApprovalWorkflowToPolicyEmployees({ return updatedEmployeeList; } +function updateWorkflowDataOnApproverRemoval({approvalWorkflows, removedApprover, ownerDetails}: UpdateWorkflowDataOnApproverRemovalParams): ApprovalWorkflow[] { + const defaultWorkflow = approvalWorkflows.find((workflow) => workflow.isDefault); + const removedApproverEmail = removedApprover.login; + const ownerEmail = ownerDetails.login; + const ownerAvatar = ownerDetails.avatar ?? ''; + const ownerDisplayName = ownerDetails.displayName ?? ''; + + return approvalWorkflows.flatMap((workflow) => { + const [currentApprover] = workflow.approvers; + const isSingleApprover = workflow.approvers.length === 1; + const isApproverToRemove = currentApprover?.email === removedApproverEmail; + const defaultHasOwner = defaultWorkflow?.approvers.some((approver) => approver.email === ownerEmail); + + if (workflow.isDefault) { + // Handle default workflow + if (isSingleApprover && isApproverToRemove && currentApprover?.email !== ownerEmail) { + return { + ...workflow, + approvers: [ + { + ...currentApprover, + avatar: ownerAvatar, + displayName: ownerDisplayName, + email: ownerEmail ?? '', + }, + ], + }; + } + return workflow; + } + + if (isSingleApprover) { + // Remove workflows with a single approver when owner is the approver + if (currentApprover?.email === ownerEmail) { + return []; + } + + // Handle case where the approver is to be removed + if (isApproverToRemove) { + // Remove workflow if the default workflow includes the owner or approver is to be replaced + if (defaultHasOwner) { + return []; + } + + // Replace the approver with owner details + return { + ...workflow, + approvers: [ + { + ...currentApprover, + avatar: ownerAvatar, + displayName: ownerDisplayName, + email: ownerEmail ?? '', + }, + ], + }; + } + } + + // Return the unchanged workflow in other cases + return workflow; + }); +} -export {calculateApprovers, convertPolicyEmployeesToApprovalWorkflows, convertApprovalWorkflowToPolicyEmployees, INITIAL_APPROVAL_WORKFLOW}; +export {calculateApprovers, convertPolicyEmployeesToApprovalWorkflows, convertApprovalWorkflowToPolicyEmployees, INITIAL_APPROVAL_WORKFLOW, updateWorkflowDataOnApproverRemoval}; diff --git a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx index 85a5d2372ee98..fe1b0cb813d2b 100644 --- a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx +++ b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx @@ -26,6 +26,7 @@ import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavig import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import shouldRenderTransferOwnerButton from '@libs/shouldRenderTransferOwnerButton'; +import {convertPolicyEmployeesToApprovalWorkflows, updateWorkflowDataOnApproverRemoval} from '@libs/WorkflowUtils'; import Navigation from '@navigation/Navigation'; import type {SettingsNavigatorParamList} from '@navigation/types'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; @@ -85,6 +86,18 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM const policyOwnerDisplayName = formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(ownerDetails)) ?? policy?.owner ?? ''; const hasMultipleFeeds = Object.values(CardUtils.getCompanyFeeds(cardFeeds)).filter((feed) => !feed.pending).length > 0; const paymentAccountID = cardSettings?.paymentBankAccountID ?? 0; + const policyApproverEmail = policy?.approver; + const {approvalWorkflows} = useMemo( + () => + convertPolicyEmployeesToApprovalWorkflows({ + employees: policy?.employeeList ?? {}, + defaultApprover: policyApproverEmail ?? policy?.owner ?? '', + personalDetails: personalDetails ?? {}, + }), + [personalDetails, policy?.employeeList, policy?.owner, policyApproverEmail], + ); + + console.log('****** approvalWorkflows ******', approvalWorkflows); useEffect(() => { CompanyCards.openPolicyCompanyCardsPage(policyID, workspaceAccountID); @@ -158,9 +171,24 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM }; const removeUser = useCallback(() => { - Member.removeMembers([accountID], policyID); - setIsRemoveMemberConfirmModalVisible(false); - }, [accountID, policyID]); + const ownerEmail = ownerDetails.login; + const isUserApprover = Member.isApprover(policy, accountID); + if (!ownerEmail || !isUserApprover) { + return; + } + const removedApprover = personalDetails?.[accountID]; + + if (!removedApprover?.login) { + return; + } + const updatedWorkflow = updateWorkflowDataOnApproverRemoval({approvalWorkflows, removedApprover, ownerDetails}); + + console.log('****** updatedWorkflow ******', updatedWorkflow); + + // Workflow.updateApprovalWorkflow(policyID, updatedWorkflow, [], []); + // Member.removeMembers([accountID], policyID); + // setIsRemoveMemberConfirmModalVisible(false); + }, [accountID, approvalWorkflows, ownerDetails, personalDetails, policy]); const navigateToProfile = useCallback(() => { Navigation.navigate(ROUTES.PROFILE.getRoute(accountID, Navigation.getActiveRoute())); From a554fb0d2cddc68247ab6ba09dbbddea1629b497 Mon Sep 17 00:00:00 2001 From: Huu Le <20178761+huult@users.noreply.github.com> Date: Sat, 14 Dec 2024 02:20:10 +0700 Subject: [PATCH 02/13] update utils func --- src/libs/WorkflowUtils.ts | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/libs/WorkflowUtils.ts b/src/libs/WorkflowUtils.ts index 563836fbeda83..ad15305a83673 100644 --- a/src/libs/WorkflowUtils.ts +++ b/src/libs/WorkflowUtils.ts @@ -307,6 +307,7 @@ function updateWorkflowDataOnApproverRemoval({approvalWorkflows, removedApprover return approvalWorkflows.flatMap((workflow) => { const [currentApprover] = workflow.approvers; const isSingleApprover = workflow.approvers.length === 1; + const isMultipleApprovers = workflow.approvers.length > 1; const isApproverToRemove = currentApprover?.email === removedApproverEmail; const defaultHasOwner = defaultWorkflow?.approvers.some((approver) => approver.email === ownerEmail); @@ -356,6 +357,42 @@ function updateWorkflowDataOnApproverRemoval({approvalWorkflows, removedApprover } } + if (isMultipleApprovers && defaultHasOwner && workflow.approvers.some((item) => item.email === removedApproverEmail)) { + const removedApproverIndex = workflow.approvers.findIndex((item) => item.email === removedApproverEmail); + + // If the removed approver is the first in the list, return an empty array + if (removedApproverIndex === 0) { + return []; + } + + const updateApprovers = workflow.approvers.slice(0, removedApproverIndex); + const updateApproversHasOwner = updateApprovers.some((approver) => approver.email === ownerEmail); + + // If the owner is already in the approvers list, return the workflow with the updated approvers + if (updateApproversHasOwner) { + return { + ...workflow, + approvers: updateApprovers, + }; + } + + // Update forwardsTo if necessary and prepare the new approver object + const updatedApprovers = updateApprovers.flatMap((item) => (item.forwardsTo === removedApproverEmail ? {...item, forwardsTo: ownerEmail} : item)); + + const newApprover = { + email: ownerEmail ?? '', + forwardsTo: '', + avatar: ownerDetails?.avatar ?? '', + displayName: ownerDetails?.displayName ?? '', + isCircularReference: workflow.approvers.at(removedApproverIndex)?.isCircularReference, + }; + + return { + ...workflow, + approvers: [...updatedApprovers, newApprover], + }; + } + // Return the unchanged workflow in other cases return workflow; }); From c9ec9725da9c290d4ad26a900bbe30c204490881 Mon Sep 17 00:00:00 2001 From: Huu Le <20178761+huult@users.noreply.github.com> Date: Mon, 16 Dec 2024 12:17:22 +0700 Subject: [PATCH 03/13] Update workflow for removing a member --- .../members/WorkspaceMemberDetailsPage.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx index fe1b0cb813d2b..b36e21ea35033 100644 --- a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx +++ b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx @@ -37,6 +37,7 @@ import variables from '@styles/variables'; import * as Card from '@userActions/Card'; import * as CompanyCards from '@userActions/CompanyCards'; import * as Member from '@userActions/Policy/Member'; +import * as Workflow from '@userActions/Workflow'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -181,14 +182,15 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM if (!removedApprover?.login) { return; } - const updatedWorkflow = updateWorkflowDataOnApproverRemoval({approvalWorkflows, removedApprover, ownerDetails}); + const updatedWorkflows = updateWorkflowDataOnApproverRemoval({approvalWorkflows, removedApprover, ownerDetails}); - console.log('****** updatedWorkflow ******', updatedWorkflow); + updatedWorkflows.forEach((workflow) => { + Workflow.updateApprovalWorkflow(policyID, workflow, [], []); + }); - // Workflow.updateApprovalWorkflow(policyID, updatedWorkflow, [], []); - // Member.removeMembers([accountID], policyID); - // setIsRemoveMemberConfirmModalVisible(false); - }, [accountID, approvalWorkflows, ownerDetails, personalDetails, policy]); + Member.removeMembers([accountID], policyID); + setIsRemoveMemberConfirmModalVisible(false); + }, [accountID, approvalWorkflows, ownerDetails, personalDetails, policy, policyID]); const navigateToProfile = useCallback(() => { Navigation.navigate(ROUTES.PROFILE.getRoute(accountID, Navigation.getActiveRoute())); From 22d979e736b84449fe2a9eee33bf42d79cc52a4e Mon Sep 17 00:00:00 2001 From: Huu Le <20178761+huult@users.noreply.github.com> Date: Mon, 16 Dec 2024 12:22:37 +0700 Subject: [PATCH 04/13] Remove member if not an approver --- .../members/WorkspaceMemberDetailsPage.tsx | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx index b36e21ea35033..79ae73754b1e0 100644 --- a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx +++ b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx @@ -98,8 +98,6 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM [personalDetails, policy?.employeeList, policy?.owner, policyApproverEmail], ); - console.log('****** approvalWorkflows ******', approvalWorkflows); - useEffect(() => { CompanyCards.openPolicyCompanyCardsPage(policyID, workspaceAccountID); }, [policyID, workspaceAccountID]); @@ -173,25 +171,33 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM const removeUser = useCallback(() => { const ownerEmail = ownerDetails.login; - const isUserApprover = Member.isApprover(policy, accountID); - if (!ownerEmail || !isUserApprover) { + const removedApprover = personalDetails?.[accountID]; + + // If the user is not an approver, simply remove the member and close the modal + if (!Member.isApprover(policy, accountID)) { + Member.removeMembers([accountID], policyID); + setIsRemoveMemberConfirmModalVisible(false); return; } - const removedApprover = personalDetails?.[accountID]; - if (!removedApprover?.login) { + // Ensure both the approver's login and the owner's email exist + if (!removedApprover?.login || !ownerEmail) { return; } - const updatedWorkflows = updateWorkflowDataOnApproverRemoval({approvalWorkflows, removedApprover, ownerDetails}); - updatedWorkflows.forEach((workflow) => { - Workflow.updateApprovalWorkflow(policyID, workflow, [], []); + // Update workflows after removing the approver + const updatedWorkflows = updateWorkflowDataOnApproverRemoval({ + approvalWorkflows, + removedApprover, + ownerDetails, }); + updatedWorkflows.forEach((workflow) => Workflow.updateApprovalWorkflow(policyID, workflow, [], [])); + + // Remove the member and close the modal Member.removeMembers([accountID], policyID); setIsRemoveMemberConfirmModalVisible(false); }, [accountID, approvalWorkflows, ownerDetails, personalDetails, policy, policyID]); - const navigateToProfile = useCallback(() => { Navigation.navigate(ROUTES.PROFILE.getRoute(accountID, Navigation.getActiveRoute())); }, [accountID]); From 27862f193fd2121d938919e22abc06c1e35385ce Mon Sep 17 00:00:00 2001 From: Huu Le <20178761+huult@users.noreply.github.com> Date: Mon, 16 Dec 2024 13:09:53 +0700 Subject: [PATCH 05/13] update logic to remove workflow --- src/libs/WorkflowUtils.ts | 10 +++- src/pages/workspace/WorkspaceMembersPage.tsx | 59 +++++++++++++++++-- .../members/WorkspaceMemberDetailsPage.tsx | 36 ++++++----- 3 files changed, 83 insertions(+), 22 deletions(-) diff --git a/src/libs/WorkflowUtils.ts b/src/libs/WorkflowUtils.ts index ad15305a83673..61df48626d064 100644 --- a/src/libs/WorkflowUtils.ts +++ b/src/libs/WorkflowUtils.ts @@ -332,14 +332,20 @@ function updateWorkflowDataOnApproverRemoval({approvalWorkflows, removedApprover if (isSingleApprover) { // Remove workflows with a single approver when owner is the approver if (currentApprover?.email === ownerEmail) { - return []; + return { + ...workflow, + removeWorkflow: true, + }; } // Handle case where the approver is to be removed if (isApproverToRemove) { // Remove workflow if the default workflow includes the owner or approver is to be replaced if (defaultHasOwner) { - return []; + return { + ...workflow, + removeWorkflow: true, + }; } // Replace the approver with owner details diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index 00fb559263b72..919d26c0c3b79 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -41,14 +41,16 @@ import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import {getDisplayNameForParticipant} from '@libs/ReportUtils'; +import {convertPolicyEmployeesToApprovalWorkflows, updateWorkflowDataOnApproverRemoval} from '@libs/WorkflowUtils'; import * as Modal from '@userActions/Modal'; import * as Member from '@userActions/Policy/Member'; import * as Policy from '@userActions/Policy/Policy'; +import * as Workflow from '@userActions/Workflow'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import type {PersonalDetailsList, PolicyEmployeeList} from '@src/types/onyx'; +import type {PersonalDetails, PersonalDetailsList, PolicyEmployeeList} 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'; @@ -86,6 +88,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson const isOfflineAndNoMemberDataAvailable = isEmptyObject(policy?.employeeList) && isOffline; const prevPersonalDetails = usePrevious(personalDetails); const {translate, formatPhoneNumber, preferredLocale} = useLocalize(); + const ownerDetails = personalDetails?.[policy?.ownerAccountID ?? -1] ?? ({} as PersonalDetails); // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to apply the correct modal type for the decision modal // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth @@ -104,6 +107,17 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson const isFocused = useIsFocused(); const policyID = route.params.policyID; + const policyApproverEmail = policy?.approver; + const {approvalWorkflows} = useMemo( + () => + convertPolicyEmployeesToApprovalWorkflows({ + employees: policy?.employeeList ?? {}, + defaultApprover: policyApproverEmail ?? policy?.owner ?? '', + personalDetails: personalDetails ?? {}, + }), + [personalDetails, policy?.employeeList, policy?.owner, policyApproverEmail], + ); + const canSelectMultiple = isPolicyAdmin && (shouldUseNarrowLayout ? selectionMode?.isEnabled : true); const confirmModalPrompt = useMemo(() => { @@ -222,11 +236,46 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson // Remove the admin from the list const accountIDsToRemove = session?.accountID ? selectedEmployees.filter((id) => id !== session.accountID) : selectedEmployees; - setSelectedEmployees([]); - setRemoveMembersConfirmModalVisible(false); - InteractionManager.runAfterInteractions(() => { - Member.removeMembers(accountIDsToRemove, route.params.policyID); + + // Check if any of the account IDs are approvers + const hasApprovers = accountIDsToRemove.some((accountID) => Member.isApprover(policy, accountID)); + + if (!hasApprovers) { + setSelectedEmployees([]); + setRemoveMembersConfirmModalVisible(false); + + InteractionManager.runAfterInteractions(() => { + Member.removeMembers(accountIDsToRemove, route.params.policyID); + }); + + return; + } + + const ownerEmail = ownerDetails.login; + + accountIDsToRemove.forEach((accountID) => { + const removedApprover = personalDetails?.[accountID]; + + if (!removedApprover?.login || !ownerEmail) { + return; + } + + const updatedWorkflows = updateWorkflowDataOnApproverRemoval({ + approvalWorkflows, + removedApprover, + ownerDetails, + }); + console.log('****** updatedWorkflows ******', updatedWorkflows); + + updatedWorkflows.forEach((workflow) => Workflow.updateApprovalWorkflow(policyID, workflow, [], [])); }); + + // setSelectedEmployees([]); + // setRemoveMembersConfirmModalVisible(false); + + // InteractionManager.runAfterInteractions(() => { + // Member.removeMembers(accountIDsToRemove, route.params.policyID); + // }); }; /** diff --git a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx index 79ae73754b1e0..f6bd451d09cf1 100644 --- a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx +++ b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx @@ -169,35 +169,41 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM setIsRemoveMemberConfirmModalVisible(true); }; + // Function to remove a member and close the modal + const removeMemberAndCloseModal = useCallback(() => { + Member.removeMembers([accountID], policyID); + setIsRemoveMemberConfirmModalVisible(false); + }, [accountID, policyID]); + const removeUser = useCallback(() => { - const ownerEmail = ownerDetails.login; + const ownerEmail = ownerDetails?.login; const removedApprover = personalDetails?.[accountID]; - // If the user is not an approver, simply remove the member and close the modal - if (!Member.isApprover(policy, accountID)) { - Member.removeMembers([accountID], policyID); - setIsRemoveMemberConfirmModalVisible(false); - return; - } - - // Ensure both the approver's login and the owner's email exist - if (!removedApprover?.login || !ownerEmail) { + // If the user is not an approver, proceed with member removal + if (!Member.isApprover(policy, accountID) || !removedApprover?.login || !ownerEmail) { + removeMemberAndCloseModal(); return; } - // Update workflows after removing the approver + // Update approval workflows after approver removal const updatedWorkflows = updateWorkflowDataOnApproverRemoval({ approvalWorkflows, removedApprover, ownerDetails, }); - updatedWorkflows.forEach((workflow) => Workflow.updateApprovalWorkflow(policyID, workflow, [], [])); + updatedWorkflows.forEach((workflow) => { + if (workflow?.removeWorkflow) { + Workflow.removeApprovalWorkflow(policyID, workflow); + } else { + Workflow.updateApprovalWorkflow(policyID, workflow, [], []); + } + }); // Remove the member and close the modal - Member.removeMembers([accountID], policyID); - setIsRemoveMemberConfirmModalVisible(false); - }, [accountID, approvalWorkflows, ownerDetails, personalDetails, policy, policyID]); + removeMemberAndCloseModal(); + }, [accountID, approvalWorkflows, ownerDetails, personalDetails, policy, policyID, removeMemberAndCloseModal]); + const navigateToProfile = useCallback(() => { Navigation.navigate(ROUTES.PROFILE.getRoute(accountID, Navigation.getActiveRoute())); }, [accountID]); From 44d7ea15efc0b5fd84a68a18389cf220082a087f Mon Sep 17 00:00:00 2001 From: Huu Le <20178761+huult@users.noreply.github.com> Date: Mon, 16 Dec 2024 13:30:15 +0700 Subject: [PATCH 06/13] Update case remove approval workflow --- src/libs/WorkflowUtils.ts | 12 ++++++++--- src/pages/workspace/WorkspaceMembersPage.tsx | 21 ++++++++++++------- .../members/WorkspaceMemberDetailsPage.tsx | 6 ++++-- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/libs/WorkflowUtils.ts b/src/libs/WorkflowUtils.ts index 61df48626d064..84fc25b320671 100644 --- a/src/libs/WorkflowUtils.ts +++ b/src/libs/WorkflowUtils.ts @@ -216,6 +216,12 @@ type UpdateWorkflowDataOnApproverRemovalParams = { ownerDetails: PersonalDetails; }; +type UpdateWorkflowDataOnApproverRemovalResult = Array< + ApprovalWorkflow & { + removeApprovalWorkflow?: boolean; + } +>; + /** * This function converts an approval workflow into a list of policy employees. * An optimized list is created that contains only the updated employees to maintain minimal data changes. @@ -297,7 +303,7 @@ function convertApprovalWorkflowToPolicyEmployees({ return updatedEmployeeList; } -function updateWorkflowDataOnApproverRemoval({approvalWorkflows, removedApprover, ownerDetails}: UpdateWorkflowDataOnApproverRemovalParams): ApprovalWorkflow[] { +function updateWorkflowDataOnApproverRemoval({approvalWorkflows, removedApprover, ownerDetails}: UpdateWorkflowDataOnApproverRemovalParams): UpdateWorkflowDataOnApproverRemovalResult { const defaultWorkflow = approvalWorkflows.find((workflow) => workflow.isDefault); const removedApproverEmail = removedApprover.login; const ownerEmail = ownerDetails.login; @@ -334,7 +340,7 @@ function updateWorkflowDataOnApproverRemoval({approvalWorkflows, removedApprover if (currentApprover?.email === ownerEmail) { return { ...workflow, - removeWorkflow: true, + removeApprovalWorkflow: true, }; } @@ -344,7 +350,7 @@ function updateWorkflowDataOnApproverRemoval({approvalWorkflows, removedApprover if (defaultHasOwner) { return { ...workflow, - removeWorkflow: true, + removeApprovalWorkflow: true, }; } diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index 919d26c0c3b79..f0264abdf5728 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -265,17 +265,24 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson removedApprover, ownerDetails, }); - console.log('****** updatedWorkflows ******', updatedWorkflows); - updatedWorkflows.forEach((workflow) => Workflow.updateApprovalWorkflow(policyID, workflow, [], [])); + updatedWorkflows.forEach((workflow) => { + if (workflow?.removeApprovalWorkflow) { + const {removeApprovalWorkflow, ...updatedWorkflow} = workflow; + + Workflow.removeApprovalWorkflow(policyID, updatedWorkflow); + } else { + Workflow.updateApprovalWorkflow(policyID, workflow, [], []); + } + }); }); - // setSelectedEmployees([]); - // setRemoveMembersConfirmModalVisible(false); + setSelectedEmployees([]); + setRemoveMembersConfirmModalVisible(false); - // InteractionManager.runAfterInteractions(() => { - // Member.removeMembers(accountIDsToRemove, route.params.policyID); - // }); + InteractionManager.runAfterInteractions(() => { + Member.removeMembers(accountIDsToRemove, route.params.policyID); + }); }; /** diff --git a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx index f6bd451d09cf1..8adcaae3abf15 100644 --- a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx +++ b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx @@ -193,8 +193,10 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM }); updatedWorkflows.forEach((workflow) => { - if (workflow?.removeWorkflow) { - Workflow.removeApprovalWorkflow(policyID, workflow); + if (workflow?.removeApprovalWorkflow) { + const {removeApprovalWorkflow, ...updatedWorkflow} = workflow; + + Workflow.removeApprovalWorkflow(policyID, updatedWorkflow); } else { Workflow.updateApprovalWorkflow(policyID, workflow, [], []); } From 1ab0c7332b173cdacb2aa4139ad6cf82274b6fa6 Mon Sep 17 00:00:00 2001 From: Huu Le <20178761+huult@users.noreply.github.com> Date: Mon, 16 Dec 2024 14:01:50 +0700 Subject: [PATCH 07/13] add useMemo into owner details --- src/libs/WorkflowUtils.ts | 5 ++++- src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/libs/WorkflowUtils.ts b/src/libs/WorkflowUtils.ts index 84fc25b320671..e921aeda12134 100644 --- a/src/libs/WorkflowUtils.ts +++ b/src/libs/WorkflowUtils.ts @@ -374,7 +374,10 @@ function updateWorkflowDataOnApproverRemoval({approvalWorkflows, removedApprover // If the removed approver is the first in the list, return an empty array if (removedApproverIndex === 0) { - return []; + return { + ...workflow, + removeApprovalWorkflow: true, + }; } const updateApprovers = workflow.approvers.slice(0, removedApproverIndex); diff --git a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx index 8adcaae3abf15..b8c7d7693e88c 100644 --- a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx +++ b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx @@ -83,7 +83,10 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM const isSelectedMemberCurrentUser = accountID === currentUserPersonalDetails?.accountID; const isCurrentUserAdmin = policy?.employeeList?.[personalDetails?.[currentUserPersonalDetails?.accountID]?.login ?? '']?.role === CONST.POLICY.ROLE.ADMIN; const isCurrentUserOwner = policy?.owner === currentUserPersonalDetails?.login; - const ownerDetails = personalDetails?.[policy?.ownerAccountID ?? -1] ?? ({} as PersonalDetails); + const ownerDetails = useMemo(() => { + const ownerID = policy?.ownerAccountID ?? -1; + return personalDetails?.[ownerID] ?? ({} as PersonalDetails); + }, [personalDetails, policy?.ownerAccountID]); const policyOwnerDisplayName = formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(ownerDetails)) ?? policy?.owner ?? ''; const hasMultipleFeeds = Object.values(CardUtils.getCompanyFeeds(cardFeeds)).filter((feed) => !feed.pending).length > 0; const paymentAccountID = cardSettings?.paymentBankAccountID ?? 0; From 806869b1907c34a4ac892e04d6c5a3eb29f36006 Mon Sep 17 00:00:00 2001 From: Huu Le <20178761+huult@users.noreply.github.com> Date: Mon, 16 Dec 2024 16:39:20 +0700 Subject: [PATCH 08/13] add unit test for updateWorkflowDataOnApproverRemoval --- src/libs/WorkflowUtils.ts | 2 +- tests/unit/WorkflowUtilsTest.ts | 168 ++++++++++++++++++++++++++++++++ 2 files changed, 169 insertions(+), 1 deletion(-) diff --git a/src/libs/WorkflowUtils.ts b/src/libs/WorkflowUtils.ts index e921aeda12134..a553fca14dc3c 100644 --- a/src/libs/WorkflowUtils.ts +++ b/src/libs/WorkflowUtils.ts @@ -396,7 +396,7 @@ function updateWorkflowDataOnApproverRemoval({approvalWorkflows, removedApprover const newApprover = { email: ownerEmail ?? '', - forwardsTo: '', + forwardsTo: undefined, avatar: ownerDetails?.avatar ?? '', displayName: ownerDetails?.displayName ?? '', isCircularReference: workflow.approvers.at(removedApproverIndex)?.isCircularReference, diff --git a/tests/unit/WorkflowUtilsTest.ts b/tests/unit/WorkflowUtilsTest.ts index effc35f2609d0..4fe8d1384aa1e 100644 --- a/tests/unit/WorkflowUtilsTest.ts +++ b/tests/unit/WorkflowUtilsTest.ts @@ -440,4 +440,172 @@ describe('WorkflowUtils', () => { }); }); }); + + describe('updateWorkflowDataOnApproverRemoval', () => { + it('Should remove Workflow 2 if its approvers are removed and it has no approvers, with Workspace (default) having the approver as the Workspace Owner.', () => { + const approvalWorkflow1: ApprovalWorkflow = { + members: [buildMember(1), buildMember(2)], + approvers: [buildApprover(1)], + isDefault: true, + }; + const approvalWorkflow2: ApprovalWorkflow = { + members: [buildMember(1), buildMember(2)], + approvers: [buildApprover(2)], + isDefault: false, + }; + + const ownerDetails = personalDetails[1]; + const removedApprover = personalDetails[2]; + + if (!removedApprover || !ownerDetails) { + return; + } + + const updateWorkflowDataOnApproverRemoval = WorkflowUtils.updateWorkflowDataOnApproverRemoval({ + approvalWorkflows: [approvalWorkflow1, approvalWorkflow2], + removedApprover, + ownerDetails, + }); + + expect(updateWorkflowDataOnApproverRemoval).toEqual([approvalWorkflow1, {...approvalWorkflow2, removeApprovalWorkflow: true}]); + }); + it('Should replace the approvers in Workflow 2 with the Workspace Owner if it has no approvers and the approver in Workspace (default) is different from the Workspace Owner', () => { + const approvalWorkflow1: ApprovalWorkflow = { + members: [buildMember(1), buildMember(2)], + approvers: [buildApprover(3)], + isDefault: true, + }; + const approvalWorkflow2: ApprovalWorkflow = { + members: [buildMember(1), buildMember(2)], + approvers: [buildApprover(2)], + isDefault: false, + }; + + const ownerDetails = personalDetails[1]; + const removedApprover = personalDetails[2]; + + if (!removedApprover || !ownerDetails) { + return; + } + + const updateWorkflowDataOnApproverRemoval = WorkflowUtils.updateWorkflowDataOnApproverRemoval({ + approvalWorkflows: [approvalWorkflow1, approvalWorkflow2], + removedApprover, + ownerDetails, + }); + + expect(updateWorkflowDataOnApproverRemoval).toEqual([approvalWorkflow1, {...approvalWorkflow2, approvers: [buildApprover(1)]}]); + }); + it('Should remove Workflow 2 if its approver is the Workspace Owner and the default Workspace approver is removed.', () => { + const approvalWorkflow1: ApprovalWorkflow = { + members: [buildMember(1), buildMember(2)], + approvers: [buildApprover(3)], + isDefault: true, + }; + const approvalWorkflow2: ApprovalWorkflow = { + members: [buildMember(1), buildMember(2)], + approvers: [buildApprover(1)], + isDefault: false, + }; + + const ownerDetails = personalDetails[1]; + const removedApprover = personalDetails[3]; + + if (!removedApprover || !ownerDetails) { + return; + } + + const updateWorkflowDataOnApproverRemoval = WorkflowUtils.updateWorkflowDataOnApproverRemoval({ + approvalWorkflows: [approvalWorkflow1, approvalWorkflow2], + removedApprover, + ownerDetails, + }); + + expect(updateWorkflowDataOnApproverRemoval).toEqual([ + {...approvalWorkflow1, approvers: [buildApprover(1)]}, + {...approvalWorkflow2, removeApprovalWorkflow: true}, + ]); + }); + it('Should replace the latest approver of Workflow 2 with the Workspace Owner if the latest approver of Workflow 2 is removed', () => { + const approvalWorkflow1: ApprovalWorkflow = { + members: [buildMember(1), buildMember(2)], + approvers: [buildApprover(1)], + isDefault: true, + }; + const approvalWorkflow2: ApprovalWorkflow = { + members: [buildMember(1), buildMember(2)], + approvers: [buildApprover(2), buildApprover(3), buildApprover(4)], + isDefault: false, + }; + + const ownerDetails = personalDetails[1]; + const removedApprover = personalDetails[4]; + + if (!removedApprover || !ownerDetails) { + return; + } + + const updateWorkflowDataOnApproverRemoval = WorkflowUtils.updateWorkflowDataOnApproverRemoval({ + approvalWorkflows: [approvalWorkflow1, approvalWorkflow2], + removedApprover, + ownerDetails, + }); + + expect(updateWorkflowDataOnApproverRemoval).toEqual([approvalWorkflow1, {...approvalWorkflow2, approvers: [buildApprover(2), buildApprover(3), buildApprover(1)]}]); + }); + it('Should remove the approvers that have submitsTo set to the removed approver, update the removed approver to the Workspace Owner, and ensure there was a previous approver before this one', () => { + const approvalWorkflow1: ApprovalWorkflow = { + members: [buildMember(1), buildMember(2)], + approvers: [buildApprover(1)], + isDefault: true, + }; + const approvalWorkflow2: ApprovalWorkflow = { + members: [buildMember(1), buildMember(2)], + approvers: [buildApprover(2), buildApprover(3), buildApprover(4)], + isDefault: false, + }; + + const ownerDetails = personalDetails[1]; + const removedApprover = personalDetails[3]; + + if (!removedApprover || !ownerDetails) { + return; + } + + const updateWorkflowDataOnApproverRemoval = WorkflowUtils.updateWorkflowDataOnApproverRemoval({ + approvalWorkflows: [approvalWorkflow1, approvalWorkflow2], + removedApprover, + ownerDetails, + }); + + expect(updateWorkflowDataOnApproverRemoval).toEqual([approvalWorkflow1, {...approvalWorkflow2, approvers: [buildApprover(2), buildApprover(1)]}]); + }); + it('Should remove Workflow 2 if it has no approvers and the default Workspace approver is the approve', () => { + const approvalWorkflow1: ApprovalWorkflow = { + members: [buildMember(1), buildMember(2)], + approvers: [buildApprover(1)], + isDefault: true, + }; + const approvalWorkflow2: ApprovalWorkflow = { + members: [buildMember(1), buildMember(2)], + approvers: [buildApprover(2), buildApprover(3), buildApprover(4)], + isDefault: false, + }; + + const ownerDetails = personalDetails[1]; + const removedApprover = personalDetails[2]; + + if (!removedApprover || !ownerDetails) { + return; + } + + const updateWorkflowDataOnApproverRemoval = WorkflowUtils.updateWorkflowDataOnApproverRemoval({ + approvalWorkflows: [approvalWorkflow1, approvalWorkflow2], + removedApprover, + ownerDetails, + }); + + expect(updateWorkflowDataOnApproverRemoval).toEqual([approvalWorkflow1, {...approvalWorkflow2, removeApprovalWorkflow: true}]); + }); + }); }); From c444d483f21a8f4ff6026b1a747efe9b145504f2 Mon Sep 17 00:00:00 2001 From: Huu Le <20178761+huult@users.noreply.github.com> Date: Mon, 16 Dec 2024 16:42:01 +0700 Subject: [PATCH 09/13] remove check defaultHasOwner --- src/libs/WorkflowUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/WorkflowUtils.ts b/src/libs/WorkflowUtils.ts index a553fca14dc3c..2715977dbc0f2 100644 --- a/src/libs/WorkflowUtils.ts +++ b/src/libs/WorkflowUtils.ts @@ -369,7 +369,7 @@ function updateWorkflowDataOnApproverRemoval({approvalWorkflows, removedApprover } } - if (isMultipleApprovers && defaultHasOwner && workflow.approvers.some((item) => item.email === removedApproverEmail)) { + if (isMultipleApprovers && workflow.approvers.some((item) => item.email === removedApproverEmail)) { const removedApproverIndex = workflow.approvers.findIndex((item) => item.email === removedApproverEmail); // If the removed approver is the first in the list, return an empty array From 08d6f46e2afc3ee629b048ad59c3d3fb950c2675 Mon Sep 17 00:00:00 2001 From: Huu Le <20178761+huult@users.noreply.github.com> Date: Fri, 27 Dec 2024 22:24:24 +0700 Subject: [PATCH 10/13] fix eslint --- src/libs/WorkflowUtils.ts | 2 +- src/pages/workspace/WorkspaceMembersPage.tsx | 2 +- src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/libs/WorkflowUtils.ts b/src/libs/WorkflowUtils.ts index 2715977dbc0f2..22cf7e2ed65a6 100644 --- a/src/libs/WorkflowUtils.ts +++ b/src/libs/WorkflowUtils.ts @@ -158,7 +158,7 @@ function convertPolicyEmployeesToApprovalWorkflows({employees, defaultApprover, return 1; } - return (a.approvers.at(0)?.displayName ?? '-1').localeCompare(b.approvers.at(0)?.displayName ?? '-1'); + return (a.approvers.at(0)?.displayName ?? CONST.DEFAULT_NUMBER_ID).toString().localeCompare((b.approvers.at(0)?.displayName ?? CONST.DEFAULT_NUMBER_ID).toString()); }); // Add a default workflow if one doesn't exist (no employees submit to the default approver) diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index 2bb67e23f8c45..b2821671a17c8 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -88,7 +88,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson const isOfflineAndNoMemberDataAvailable = isEmptyObject(policy?.employeeList) && isOffline; const prevPersonalDetails = usePrevious(personalDetails); const {translate, formatPhoneNumber, preferredLocale} = useLocalize(); - const ownerDetails = personalDetails?.[policy?.ownerAccountID ?? -1] ?? ({} as PersonalDetails); + const ownerDetails = personalDetails?.[policy?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID] ?? ({} as PersonalDetails); // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to apply the correct modal type for the decision modal // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth diff --git a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx index 156652fd8ad33..01206f68e4bd1 100644 --- a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx +++ b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx @@ -83,7 +83,9 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM const isSelectedMemberCurrentUser = accountID === currentUserPersonalDetails?.accountID; const isCurrentUserAdmin = policy?.employeeList?.[personalDetails?.[currentUserPersonalDetails?.accountID]?.login ?? '']?.role === CONST.POLICY.ROLE.ADMIN; const isCurrentUserOwner = policy?.owner === currentUserPersonalDetails?.login; - const ownerDetails = personalDetails?.[policy?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID] ?? ({} as PersonalDetails); + const ownerDetails = useMemo(() => { + return personalDetails?.[policy?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID] ?? ({} as PersonalDetails); + }, [personalDetails, policy?.ownerAccountID]); const policyOwnerDisplayName = formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(ownerDetails)) ?? policy?.owner ?? ''; const hasMultipleFeeds = Object.values(CardUtils.getCompanyFeeds(cardFeeds)).filter((feed) => !feed.pending).length > 0; const paymentAccountID = cardSettings?.paymentBankAccountID ?? CONST.DEFAULT_NUMBER_ID; From 62648c0cbb78ee35737ae652e6a3e6d0bdfc4b7a Mon Sep 17 00:00:00 2001 From: Huu Le <20178761+huult@users.noreply.github.com> Date: Thu, 16 Jan 2025 09:55:40 +0700 Subject: [PATCH 11/13] fix lint --- src/pages/workspace/WorkspaceMembersPage.tsx | 90 ++++++++++--------- .../members/WorkspaceMemberDetailsPage.tsx | 54 ++++++----- 2 files changed, 76 insertions(+), 68 deletions(-) diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index 3da900e0ef9f9..1bd720c0ccc49 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -30,21 +30,31 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; -import * as DeviceCapabilities from '@libs/DeviceCapabilities'; -import * as LocalePhoneNumber from '@libs/LocalePhoneNumber'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; +import {formatPhoneNumber as formatPhone} from '@libs/LocalePhoneNumber'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {FullScreenNavigatorParamList} from '@libs/Navigation/types'; -import * as OptionsListUtils from '@libs/OptionsListUtils'; -import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; -import * as PolicyUtils from '@libs/PolicyUtils'; +import {isPersonalDetailsReady, sortAlphabetically} from '@libs/OptionsListUtils'; +import {getDisplayNameOrDefault, getPersonalDetailsByIDs} from '@libs/PersonalDetailsUtils'; +import {isPolicyAdmin as checkIfPolicyAdmin, getMemberAccountIDsForWorkspace, isDeletedPolicyEmployee, isExpensifyTeam, isPaidGroupPolicy} from '@libs/PolicyUtils'; import {getDisplayNameForParticipant} from '@libs/ReportUtils'; import {convertPolicyEmployeesToApprovalWorkflows, updateWorkflowDataOnApproverRemoval} from '@libs/WorkflowUtils'; -import * as Modal from '@userActions/Modal'; -import * as Member from '@userActions/Policy/Member'; -import * as Policy from '@userActions/Policy/Policy'; -import * as Workflow from '@userActions/Workflow'; +import {close} from '@userActions/Modal'; +import { + clearAddMemberError, + clearDeleteMemberError, + clearInviteDraft, + clearWorkspaceOwnerChangeFlow, + downloadMembersCSV, + isApprover, + openWorkspaceMembersPage, + removeMembers, + updateWorkspaceMembersRole, +} from '@userActions/Policy/Member'; +import {dismissAddedWithPrimaryLoginMessages} from '@userActions/Policy/Policy'; +import {removeApprovalWorkflow as removeApprovalWorkflowAction, updateApprovalWorkflow} from '@userActions/Workflow'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -71,7 +81,7 @@ function invertObject(object: Record): Record { type MemberOption = Omit & {accountID: number}; function WorkspaceMembersPage({personalDetails, route, policy, currentUserPersonalDetails}: WorkspaceMembersPageProps) { - const policyMemberEmailsToAccountIDs = useMemo(() => PolicyUtils.getMemberAccountIDsForWorkspace(policy?.employeeList, true), [policy?.employeeList]); + const policyMemberEmailsToAccountIDs = useMemo(() => getMemberAccountIDsForWorkspace(policy?.employeeList, true), [policy?.employeeList]); const styles = useThemeStyles(); const [selectedEmployees, setSelectedEmployees] = useState([]); const [removeMembersConfirmModalVisible, setRemoveMembersConfirmModalVisible] = useState(false); @@ -92,9 +102,9 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to apply the correct modal type for the decision modal // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); - const isPolicyAdmin = PolicyUtils.isPolicyAdmin(policy); + const isPolicyAdmin = checkIfPolicyAdmin(policy); const isLoading = useMemo( - () => !isOfflineAndNoMemberDataAvailable && (!OptionsListUtils.isPersonalDetailsReady(personalDetails) || isEmptyObject(policy?.employeeList)), + () => !isOfflineAndNoMemberDataAvailable && (!isPersonalDetailsReady(personalDetails) || isEmptyObject(policy?.employeeList)), [isOfflineAndNoMemberDataAvailable, personalDetails, policy?.employeeList], ); @@ -120,11 +130,11 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson const canSelectMultiple = isPolicyAdmin && (shouldUseNarrowLayout ? selectionMode?.isEnabled : true); const confirmModalPrompt = useMemo(() => { - const approverAccountID = selectedEmployees.find((selectedEmployee) => Member.isApprover(policy, selectedEmployee)); + const approverAccountID = selectedEmployees.find((selectedEmployee) => isApprover(policy, selectedEmployee)); if (!approverAccountID) { return translate('workspace.people.removeMembersPrompt', { count: selectedEmployees.length, - memberName: LocalePhoneNumber.formatPhoneNumber(PersonalDetailsUtils.getPersonalDetailsByIDs(selectedEmployees, currentUserAccountID).at(0)?.displayName ?? ''), + memberName: formatPhone(getPersonalDetailsByIDs(selectedEmployees, currentUserAccountID).at(0)?.displayName ?? ''), }); } return translate('workspace.people.removeMembersWarningPrompt', { @@ -147,7 +157,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson * Get members for the current workspace */ const getWorkspaceMembers = useCallback(() => { - Member.openWorkspaceMembersPage(route.params.policyID, Object.keys(PolicyUtils.getMemberAccountIDsForWorkspace(policy?.employeeList))); + openWorkspaceMembersPage(route.params.policyID, Object.keys(getMemberAccountIDsForWorkspace(policy?.employeeList))); }, [route.params.policyID, policy?.employeeList]); /** @@ -193,7 +203,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson return res?.accountID ?? id; }); - const currentSelectedElements = Object.entries(PolicyUtils.getMemberAccountIDsForWorkspace(policy?.employeeList)) + const currentSelectedElements = Object.entries(getMemberAccountIDsForWorkspace(policy?.employeeList)) .filter((employee) => policy?.employeeList?.[employee[0]]?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) .map((employee) => employee[1]); @@ -215,7 +225,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson * Open the modal to invite a user */ const inviteUser = useCallback(() => { - Member.clearInviteDraft(route.params.policyID); + clearInviteDraft(route.params.policyID); Navigation.navigate(ROUTES.WORKSPACE_INVITE.getRoute(route.params.policyID, Navigation.getActiveRouteWithoutParams())); }, [route.params.policyID]); @@ -232,14 +242,14 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson const accountIDsToRemove = session?.accountID ? selectedEmployees.filter((id) => id !== session.accountID) : selectedEmployees; // Check if any of the account IDs are approvers - const hasApprovers = accountIDsToRemove.some((accountID) => Member.isApprover(policy, accountID)); + const hasApprovers = accountIDsToRemove.some((accountID) => isApprover(policy, accountID)); if (!hasApprovers) { setSelectedEmployees([]); setRemoveMembersConfirmModalVisible(false); InteractionManager.runAfterInteractions(() => { - Member.removeMembers(accountIDsToRemove, route.params.policyID); + removeMembers(accountIDsToRemove, route.params.policyID); }); return; @@ -264,9 +274,9 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson if (workflow?.removeApprovalWorkflow) { const {removeApprovalWorkflow, ...updatedWorkflow} = workflow; - Workflow.removeApprovalWorkflow(policyID, updatedWorkflow); + removeApprovalWorkflowAction(policyID, updatedWorkflow); } else { - Workflow.updateApprovalWorkflow(policyID, workflow, [], []); + updateApprovalWorkflow(policyID, workflow, [], []); } }); }); @@ -275,7 +285,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson setRemoveMembersConfirmModalVisible(false); InteractionManager.runAfterInteractions(() => { - Member.removeMembers(accountIDsToRemove, route.params.policyID); + removeMembers(accountIDsToRemove, route.params.policyID); }); }; @@ -354,11 +364,11 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson /** Opens the member details page */ const openMemberDetails = useCallback( (item: MemberOption) => { - if (!isPolicyAdmin || !PolicyUtils.isPaidGroupPolicy(policy)) { + if (!isPolicyAdmin || !isPaidGroupPolicy(policy)) { Navigation.navigate(ROUTES.PROFILE.getRoute(item.accountID)); return; } - Member.clearWorkspaceOwnerChangeFlow(policyID); + clearWorkspaceOwnerChangeFlow(policyID); Navigation.navigate(ROUTES.WORKSPACE_MEMBER_DETAILS.getRoute(route.params.policyID, item.accountID)); }, [isPolicyAdmin, policy, policyID, route.params.policyID], @@ -370,9 +380,9 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson const dismissError = useCallback( (item: MemberOption) => { if (item.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { - Member.clearDeleteMemberError(route.params.policyID, item.accountID); + clearDeleteMemberError(route.params.policyID, item.accountID); } else { - Member.clearAddMemberError(route.params.policyID, item.accountID); + clearAddMemberError(route.params.policyID, item.accountID); } }, [route.params.policyID], @@ -386,7 +396,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson Object.entries(policy?.employeeList ?? {}).forEach(([email, policyEmployee]) => { const accountID = Number(policyMemberEmailsToAccountIDs[email] ?? ''); - if (PolicyUtils.isDeletedPolicyEmployee(policyEmployee, isOffline)) { + if (isDeletedPolicyEmployee(policyEmployee, isOffline)) { return; } @@ -400,8 +410,8 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson // If this policy is owned by Expensify then show all support (expensify.com or team.expensify.com) emails // We don't want to show guides as policy members unless the user is a guide. Some customers get confused when they // see random people added to their policy, but guides having access to the policies help set them up. - if (PolicyUtils.isExpensifyTeam(details?.login ?? details?.displayName)) { - if (policyOwner && currentUserLogin && !PolicyUtils.isExpensifyTeam(policyOwner) && !PolicyUtils.isExpensifyTeam(currentUserLogin)) { + if (isExpensifyTeam(details?.login ?? details?.displayName)) { + if (policyOwner && currentUserLogin && !isExpensifyTeam(policyOwner) && !isExpensifyTeam(currentUserLogin)) { return; } } @@ -427,7 +437,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson isDisabled: isPendingDeleteOrError, isInteractive: !details.isOptimisticPersonalDetail, cursorStyle: details.isOptimisticPersonalDetail ? styles.cursorDefault : {}, - text: formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(details)), + text: formatPhoneNumber(getDisplayNameOrDefault(details)), alternateText: formatPhoneNumber(details?.login ?? ''), rightElement: roleBadge, icons: [ @@ -444,7 +454,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson invitedSecondaryLogin: details?.login ? invitedPrimaryToSecondaryLogins[details.login] ?? '' : '', }); }); - result = OptionsListUtils.sortAlphabetically(result, 'text'); + result = sortAlphabetically(result, 'text'); return result; }, [ isOffline, @@ -476,7 +486,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson } const invitedEmails = Object.values(invitedEmailsToAccountIDsDraft).map(String); selectionListRef.current?.scrollAndHighlightItem?.(invitedEmails); - Member.clearInviteDraft(route.params.policyID); + clearInviteDraft(route.params.policyID); }, [invitedEmailsToAccountIDsDraft, isFocused, accountIDs, prevAccountIDs, route.params.policyID]); const getHeaderMessage = () => { @@ -496,7 +506,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson // eslint-disable-next-line @typescript-eslint/naming-convention messages={{0: translate('workspace.people.addedWithPrimary')}} containerStyles={[styles.pb5, styles.ph5]} - onClose={() => Policy.dismissAddedWithPrimaryLoginMessages(policyID)} + onClose={() => dismissAddedWithPrimaryLoginMessages(policyID)} /> )} @@ -530,7 +540,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson return policy?.employeeList?.[email]?.role !== role; }); - Member.updateWorkspaceMembersRole(route.params.policyID, accountIDsToUpdate, role); + updateWorkspaceMembersRole(route.params.policyID, accountIDsToUpdate, role); setSelectedEmployees([]); }; @@ -544,7 +554,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson }, ]; - if (!PolicyUtils.isPaidGroupPolicy(policy)) { + if (!isPaidGroupPolicy(policy)) { return options; } @@ -631,7 +641,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson text: translate('spreadsheet.importSpreadsheet'), onSelected: () => { if (isOffline) { - Modal.close(() => setIsOfflineModalVisible(true)); + close(() => setIsOfflineModalVisible(true)); return; } Navigation.navigate(ROUTES.WORKSPACE_MEMBERS_IMPORT.getRoute(policyID)); @@ -642,12 +652,12 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson text: translate('spreadsheet.downloadCSV'), onSelected: () => { if (isOffline) { - Modal.close(() => setIsOfflineModalVisible(true)); + close(() => setIsOfflineModalVisible(true)); return; } - Modal.close(() => { - Member.downloadMembersCSV(policyID, () => { + close(() => { + downloadMembersCSV(policyID, () => { setIsDownloadFailureModalVisible(true); }); }); @@ -740,7 +750,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson onSelectAll={() => toggleAllUsers(data)} onDismissError={dismissError} showLoadingPlaceholder={isLoading} - shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} + shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()} textInputRef={textInputRef} customListHeader={getCustomListHeader()} listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} diff --git a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx index b58b906edd37b..f1a3e6a26e327 100644 --- a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx +++ b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx @@ -20,11 +20,11 @@ import useNetwork from '@hooks/useNetwork'; import usePrevious from '@hooks/usePrevious'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as CardUtils from '@libs/CardUtils'; -import * as CurrencyUtils from '@libs/CurrencyUtils'; +import {getAllCardsForWorkspace, getCardFeedIcon, getCompanyFeeds, maskCardNumber} from '@libs/CardUtils'; +import {convertToDisplayString} from '@libs/CurrencyUtils'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; -import * as PolicyUtils from '@libs/PolicyUtils'; +import {getDisplayNameOrDefault} from '@libs/PersonalDetailsUtils'; +import {getWorkspaceAccountID} from '@libs/PolicyUtils'; import shouldRenderTransferOwnerButton from '@libs/shouldRenderTransferOwnerButton'; import {convertPolicyEmployeesToApprovalWorkflows, updateWorkflowDataOnApproverRemoval} from '@libs/WorkflowUtils'; import Navigation from '@navigation/Navigation'; @@ -34,10 +34,10 @@ import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; import variables from '@styles/variables'; -import * as Card from '@userActions/Card'; -import * as CompanyCards from '@userActions/CompanyCards'; -import * as Member from '@userActions/Policy/Member'; -import * as Workflow from '@userActions/Workflow'; +import {setIssueNewCardStepAndData} from '@userActions/Card'; +import {openPolicyCompanyCardsPage} from '@userActions/CompanyCards'; +import {isApprover as checkIfApprover, clearWorkspaceOwnerChangeFlow, removeMembers, requestWorkspaceOwnerChange, updateWorkspaceMembersRole} from '@userActions/Policy/Member'; +import {removeApprovalWorkflow as removeApprovalWorkflowAction, updateApprovalWorkflow} from '@userActions/Workflow'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -57,7 +57,7 @@ type WorkspaceMemberDetailsPageProps = Omit { return personalDetails?.[policy?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID] ?? ({} as PersonalDetails); }, [personalDetails, policy?.ownerAccountID]); - const policyOwnerDisplayName = formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(ownerDetails)) ?? policy?.owner ?? ''; - const hasMultipleFeeds = Object.values(CardUtils.getCompanyFeeds(cardFeeds)).filter((feed) => !feed.pending).length > 0; + const policyOwnerDisplayName = formatPhoneNumber(getDisplayNameOrDefault(ownerDetails)) ?? policy?.owner ?? ''; + const hasMultipleFeeds = Object.values(getCompanyFeeds(cardFeeds)).filter((feed) => !feed.pending).length > 0; const policyApproverEmail = policy?.approver; const {approvalWorkflows} = useMemo( () => @@ -96,11 +96,11 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM [personalDetails, policy?.employeeList, policy?.owner, policyApproverEmail], ); - const workspaceCards = CardUtils.getAllCardsForWorkspace(workspaceAccountID); + const workspaceCards = getAllCardsForWorkspace(workspaceAccountID); const hasWorkspaceCardsAssigned = !!workspaceCards && !!Object.values(workspaceCards).length; useEffect(() => { - CompanyCards.openPolicyCompanyCardsPage(policyID, workspaceAccountID); + openPolicyCompanyCardsPage(policyID, workspaceAccountID); }, [policyID, workspaceAccountID]); const memberCards = useMemo(() => { @@ -111,7 +111,7 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM }, [accountID, workspaceCards]); const confirmModalPrompt = useMemo(() => { - const isApprover = Member.isApprover(policy, accountID); + const isApprover = checkIfApprover(policy, accountID); if (!isApprover) { return translate('workspace.people.removeMemberPrompt', {memberName: displayName}); } @@ -161,7 +161,7 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM // Function to remove a member and close the modal const removeMemberAndCloseModal = useCallback(() => { - Member.removeMembers([accountID], policyID); + removeMembers([accountID], policyID); setIsRemoveMemberConfirmModalVisible(false); }, [accountID, policyID]); @@ -170,7 +170,7 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM const removedApprover = personalDetails?.[accountID]; // If the user is not an approver, proceed with member removal - if (!Member.isApprover(policy, accountID) || !removedApprover?.login || !ownerEmail) { + if (!checkIfApprover(policy, accountID) || !removedApprover?.login || !ownerEmail) { removeMemberAndCloseModal(); return; } @@ -186,9 +186,9 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM if (workflow?.removeApprovalWorkflow) { const {removeApprovalWorkflow, ...updatedWorkflow} = workflow; - Workflow.removeApprovalWorkflow(policyID, updatedWorkflow); + removeApprovalWorkflowAction(policyID, updatedWorkflow); } else { - Workflow.updateApprovalWorkflow(policyID, workflow, [], []); + updateApprovalWorkflow(policyID, workflow, [], []); } }); @@ -218,7 +218,7 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM } const activeRoute = Navigation.getActiveRoute(); - Card.setIssueNewCardStepAndData({ + setIssueNewCardStepAndData({ step: CONST.EXPENSIFY_CARD.STEP.CARD_TYPE, data: { assigneeEmail: memberLogin, @@ -235,14 +235,14 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM const changeRole = useCallback( ({value}: ListItemType) => { setIsRoleSelectionModalVisible(false); - Member.updateWorkspaceMembersRole(policyID, [accountID], value); + updateWorkspaceMembersRole(policyID, [accountID], value); }, [accountID, policyID], ); const startChangeOwnershipFlow = useCallback(() => { - Member.clearWorkspaceOwnerChangeFlow(policyID); - Member.requestWorkspaceOwnerChange(policyID); + clearWorkspaceOwnerChangeFlow(policyID); + requestWorkspaceOwnerChange(policyID); Navigation.navigate(ROUTES.WORKSPACE_OWNER_CHANGE_CHECK.getRoute(policyID, accountID, 'amountOwed' as ValueOf)); }, [accountID, policyID]); @@ -361,13 +361,11 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM > Date: Thu, 16 Jan 2025 10:13:13 +0700 Subject: [PATCH 12/13] add descriptions and avoid repetition --- src/libs/WorkflowUtils.ts | 5 ++ src/pages/workspace/WorkspaceMembersPage.tsx | 54 ++++++++------------ 2 files changed, 25 insertions(+), 34 deletions(-) diff --git a/src/libs/WorkflowUtils.ts b/src/libs/WorkflowUtils.ts index 22cf7e2ed65a6..bcd4212d561ce 100644 --- a/src/libs/WorkflowUtils.ts +++ b/src/libs/WorkflowUtils.ts @@ -218,6 +218,11 @@ type UpdateWorkflowDataOnApproverRemovalParams = { type UpdateWorkflowDataOnApproverRemovalResult = Array< ApprovalWorkflow & { + /** + * @property {boolean} [removeApprovalWorkflow] - A flag that determines if the approval workflow should be removed. + * - `true`: Indicates the approval workflow needs to be removed. + * - `false` or `undefined`: No removal is required; the workflow will be updated instead. + */ removeApprovalWorkflow?: boolean; } >; diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index 1bd720c0ccc49..c1fb0431014b1 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -244,42 +244,28 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson // Check if any of the account IDs are approvers const hasApprovers = accountIDsToRemove.some((accountID) => isApprover(policy, accountID)); - if (!hasApprovers) { - setSelectedEmployees([]); - setRemoveMembersConfirmModalVisible(false); - - InteractionManager.runAfterInteractions(() => { - removeMembers(accountIDsToRemove, route.params.policyID); - }); - - return; - } - - const ownerEmail = ownerDetails.login; - - accountIDsToRemove.forEach((accountID) => { - const removedApprover = personalDetails?.[accountID]; - - if (!removedApprover?.login || !ownerEmail) { - return; - } - - const updatedWorkflows = updateWorkflowDataOnApproverRemoval({ - approvalWorkflows, - removedApprover, - ownerDetails, - }); - - updatedWorkflows.forEach((workflow) => { - if (workflow?.removeApprovalWorkflow) { - const {removeApprovalWorkflow, ...updatedWorkflow} = workflow; - - removeApprovalWorkflowAction(policyID, updatedWorkflow); - } else { - updateApprovalWorkflow(policyID, workflow, [], []); + if (hasApprovers) { + const ownerEmail = ownerDetails.login; + accountIDsToRemove.forEach((accountID) => { + const removedApprover = personalDetails?.[accountID]; + if (!removedApprover?.login || !ownerEmail) { + return; } + const updatedWorkflows = updateWorkflowDataOnApproverRemoval({ + approvalWorkflows, + removedApprover, + ownerDetails, + }); + updatedWorkflows.forEach((workflow) => { + if (workflow?.removeApprovalWorkflow) { + const {removeApprovalWorkflow, ...updatedWorkflow} = workflow; + removeApprovalWorkflowAction(policyID, updatedWorkflow); + } else { + updateApprovalWorkflow(policyID, workflow, [], []); + } + }); }); - }); + } setSelectedEmployees([]); setRemoveMembersConfirmModalVisible(false); From b740dd6cefa6d56fb971dc8dc3acdf4e55b24b1c Mon Sep 17 00:00:00 2001 From: Huu Le <20178761+huult@users.noreply.github.com> Date: Tue, 11 Feb 2025 12:43:30 +0700 Subject: [PATCH 13/13] fix eslint --- src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx index 343795dd21e23..74114d4384e8c 100644 --- a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx +++ b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx @@ -80,7 +80,7 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM const isSelectedMemberCurrentUser = accountID === currentUserPersonalDetails?.accountID; const isCurrentUserAdmin = policy?.employeeList?.[personalDetails?.[currentUserPersonalDetails?.accountID]?.login ?? '']?.role === CONST.POLICY.ROLE.ADMIN; const isCurrentUserOwner = policy?.owner === currentUserPersonalDetails?.login; - const ownerDetails = personalDetails?.[policy?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID] ?? ({} as PersonalDetails); + const ownerDetails = useMemo(() => personalDetails?.[policy?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID] ?? ({} as PersonalDetails), [personalDetails, policy?.ownerAccountID]); const policyOwnerDisplayName = formatPhoneNumber(getDisplayNameOrDefault(ownerDetails)) ?? policy?.owner ?? ''; const hasMultipleFeeds = Object.values(getCompanyFeeds(cardFeeds)).filter((feed) => !feed.pending).length > 0;