From c922e7b6dbde462d64699d22ed11cd6007b9bc05 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 19 Jun 2024 22:17:52 +0700 Subject: [PATCH 1/5] feat: Display warning prompt when removing an approver from a control policy --- src/languages/en.ts | 3 ++ src/languages/es.ts | 3 ++ src/languages/types.ts | 6 +++ src/libs/actions/Policy/Member.ts | 44 +++++++++++++++++++ src/pages/workspace/WorkspaceMembersPage.tsx | 14 +++++- .../members/WorkspaceMemberDetailsPage.tsx | 10 ++++- src/types/onyx/PolicyEmployee.ts | 3 ++ 7 files changed, 81 insertions(+), 2 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 8d64d0a2cda66..4881cbf860dd0 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -49,6 +49,7 @@ import type { PaySomeoneParams, ReimbursementRateParams, RemovedTheRequestParams, + RemoveMembersWarningPrompt, RenamedRoomActionParams, ReportArchiveReasonsClosedParams, ReportArchiveReasonsMergedParams, @@ -2340,6 +2341,8 @@ export default { people: { genericFailureMessage: 'An error occurred removing a user from the workspace, please try again.', removeMembersPrompt: 'Are you sure you want to remove these members?', + removeMembersWarningPrompt: ({memberName, ownerName}: RemoveMembersWarningPrompt) => + `${memberName} is an approver in this workspace. When you unshare this workspace with them, we’ll replace them in the approval workflow with the workspace owner, ${ownerName}`, removeMembersTitle: 'Remove members', removeMemberButtonTitle: 'Remove from workspace', removeMemberGroupButtonTitle: 'Remove from group', diff --git a/src/languages/es.ts b/src/languages/es.ts index 0745c5df75824..83bbc90dfc1df 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -49,6 +49,7 @@ import type { PaySomeoneParams, ReimbursementRateParams, RemovedTheRequestParams, + RemoveMembersWarningPrompt, RenamedRoomActionParams, ReportArchiveReasonsClosedParams, ReportArchiveReasonsMergedParams, @@ -2376,6 +2377,8 @@ export default { people: { genericFailureMessage: 'Se ha producido un error al intentar eliminar a un usuario del espacio de trabajo. Por favor, inténtalo más tarde.', removeMembersPrompt: '¿Estás seguro de que deseas eliminar a estos miembros?', + removeMembersWarningPrompt: ({memberName, ownerName}: RemoveMembersWarningPrompt) => + `${memberName} es un aprobador en este espacio de trabajo. Cuando dejes de compartir este espacio de trabajo con ellos, los sustituiremos en el flujo de trabajo de aprobación por el propietario del espacio de trabajo, ${ownerName}`, removeMembersTitle: 'Eliminar miembros', removeMemberButtonTitle: 'Quitar del espacio de trabajo', removeMemberGroupButtonTitle: 'Quitar del grupo', diff --git a/src/languages/types.ts b/src/languages/types.ts index de9b1d2dadebc..c38fb4aadae57 100644 --- a/src/languages/types.ts +++ b/src/languages/types.ts @@ -298,6 +298,11 @@ type DistanceRateOperationsParams = {count: number}; type ReimbursementRateParams = {unit: Unit}; +type RemoveMembersWarningPrompt = { + memberName: string; + ownerName: string; +}; + export type { AddressLineParams, AdminCanceledRequestParams, @@ -402,4 +407,5 @@ export type { WelcomeNoteParams, WelcomeToRoomParams, ZipCodeExampleFormatParams, + RemoveMembersWarningPrompt, }; diff --git a/src/libs/actions/Policy/Member.ts b/src/libs/actions/Policy/Member.ts index e1c48e4d0522d..b1db41d3d9a6e 100644 --- a/src/libs/actions/Policy/Member.ts +++ b/src/libs/actions/Policy/Member.ts @@ -101,6 +101,13 @@ Onyx.connect({ }, }); +function isApproval(policy: OnyxEntry, employeeAccountID: number) { + const employeeLogin = allPersonalDetails?.[employeeAccountID]?.login; + return Object.values(policy?.employeeList ?? {}).some( + (employee) => employee?.submitsTo === employeeLogin || employee?.forwardsTo === employeeLogin || employee?.overLimitForwardsTo === employeeLogin, + ); +} + /** * Returns the policy of the report */ @@ -241,6 +248,42 @@ function removeMembers(accountIDs: number[], policyID: string) { failureMembersState[email] = {errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.people.error.genericRemove')}; }); + Object.keys(policy?.employeeList ?? {}).forEach((employeeEmail) => { + const employee = policy?.employeeList?.[employeeEmail]; + optimisticMembersState[employeeEmail] = optimisticMembersState[employeeEmail] ?? {}; + failureMembersState[employeeEmail] = failureMembersState[employeeEmail] ?? {}; + if (employee?.submitsTo && emailList.includes(employee?.submitsTo)) { + optimisticMembersState[employeeEmail] = { + ...optimisticMembersState[employeeEmail], + submitsTo: policy.owner, + }; + failureMembersState[employeeEmail] = { + ...failureMembersState[employeeEmail], + submitsTo: employee?.submitsTo, + }; + } + if (employee?.forwardsTo && emailList.includes(employee?.forwardsTo)) { + optimisticMembersState[employeeEmail] = { + ...optimisticMembersState[employeeEmail], + forwardsTo: policy.owner, + }; + failureMembersState[employeeEmail] = { + ...failureMembersState[employeeEmail], + forwardsTo: employee?.forwardsTo, + }; + } + if (employee?.overLimitForwardsTo && emailList.includes(employee?.overLimitForwardsTo)) { + optimisticMembersState[employeeEmail] = { + ...optimisticMembersState[employeeEmail], + overLimitForwardsTo: policy.owner, + }; + failureMembersState[employeeEmail] = { + ...failureMembersState[employeeEmail], + overLimitForwardsTo: employee?.overLimitForwardsTo, + }; + } + }); + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -804,6 +847,7 @@ export { inviteMemberToWorkspace, acceptJoinRequest, declineJoinRequest, + isApproval, }; export type {NewCustomUnit}; diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index 6ed851c70f4e7..e1cbae73d10a7 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -36,6 +36,7 @@ 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 {getDisplayNameForParticipant} from '@libs/ReportUtils'; import * as Member from '@userActions/Policy/Member'; import * as Policy from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; @@ -95,6 +96,17 @@ function WorkspaceMembersPage({personalDetails, invitedEmailsToAccountIDsDraft, const selectionListRef = useRef(null); const isFocused = useIsFocused(); const policyID = route.params.policyID; + + const confirmModalPrompt = useMemo(() => { + const approvalAccountID = selectedEmployees.find((selectedEmployee) => Member.isApproval(policy, selectedEmployee)); + if (!approvalAccountID) { + return translate('workspace.people.removeMembersPrompt'); + } + return translate('workspace.people.removeMembersWarningPrompt', { + memberName: getDisplayNameForParticipant(approvalAccountID), + ownerName: getDisplayNameForParticipant(policy?.ownerAccountID), + }); + }, [selectedEmployees, policy, translate]); /** * Get filtered personalDetails list with current employeeList */ @@ -549,7 +561,7 @@ function WorkspaceMembersPage({personalDetails, invitedEmailsToAccountIDsDraft, isVisible={removeMembersConfirmModalVisible} onConfirm={removeUsers} onCancel={() => setRemoveMembersConfirmModalVisible(false)} - prompt={translate('workspace.people.removeMembersPrompt')} + prompt={confirmModalPrompt} confirmText={translate('common.remove')} cancelText={translate('common.cancel')} onModalHide={() => { diff --git a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx index 1675c78b2efb3..491d72a42b7d9 100644 --- a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx +++ b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx @@ -66,6 +66,14 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM const isCurrentUserAdmin = policy?.employeeList?.[personalDetails?.[currentUserPersonalDetails?.accountID]?.login ?? '']?.role === CONST.POLICY.ROLE.ADMIN; const isCurrentUserOwner = policy?.owner === currentUserPersonalDetails?.login; + const confirmModalPrompt = useMemo(() => { + const isApproval = Member.isApproval(policy, accountID); + if (!isApproval) { + translate('workspace.people.removeMemberPrompt', {memberName: displayName}); + } + return translate('workspace.people.removeMembersWarningPrompt', {memberName: displayName, ownerName: policy?.owner ?? ''}); + }, [accountID, policy, displayName, translate]); + const roleItems: ListItemType[] = useMemo( () => [ { @@ -188,7 +196,7 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM isVisible={isRemoveMemberConfirmModalVisible} onConfirm={removeUser} onCancel={() => setIsRemoveMemberConfirmModalVisible(false)} - prompt={translate('workspace.people.removeMemberPrompt', {memberName: displayName})} + prompt={confirmModalPrompt} confirmText={translate('common.remove')} cancelText={translate('common.cancel')} /> diff --git a/src/types/onyx/PolicyEmployee.ts b/src/types/onyx/PolicyEmployee.ts index 741e1e01ec057..d0c3d4be6b8ae 100644 --- a/src/types/onyx/PolicyEmployee.ts +++ b/src/types/onyx/PolicyEmployee.ts @@ -14,6 +14,9 @@ type PolicyEmployee = OnyxCommon.OnyxValueWithOfflineFeedback<{ /** Email of the user this user submits all reports to */ submitsTo?: string; + /** Email of the user this user forwards all reports when the amount of the report is over limited */ + overLimitForwardsTo?: string; + /** * Errors from api calls on the specific user * {: 'error message', : 'error message 2'} From 0cdd9414c5b6b975acccb375126b17163b4a87a0 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Thu, 20 Jun 2024 12:01:39 +0700 Subject: [PATCH 2/5] update spanish translation --- src/languages/es.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 091f017644a7d..4bfd8db5f914c 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2377,7 +2377,7 @@ export default { genericFailureMessage: 'Se ha producido un error al intentar eliminar a un usuario del espacio de trabajo. Por favor, inténtalo más tarde.', removeMembersPrompt: '¿Estás seguro de que deseas eliminar a estos miembros?', removeMembersWarningPrompt: ({memberName, ownerName}: RemoveMembersWarningPrompt) => - `${memberName} es un aprobador en este espacio de trabajo. Cuando dejes de compartir este espacio de trabajo con ellos, los sustituiremos en el flujo de trabajo de aprobación por el propietario del espacio de trabajo, ${ownerName}`, + `${memberName} es un aprobador en este espacio de trabajo. Cuando lo elimine de este espacio de trabajo, los sustituiremos en el flujo de trabajo de aprobación por el propietario del espacio de trabajo, ${ownerName}`, removeMembersTitle: 'Eliminar miembros', removeMemberButtonTitle: 'Quitar del espacio de trabajo', removeMemberGroupButtonTitle: 'Quitar del grupo', From c28f25493e76672ae2d3de4fbc5ea2d9414903d6 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Thu, 27 Jun 2024 16:34:13 +0700 Subject: [PATCH 3/5] rename function --- src/libs/actions/Policy/Member.ts | 5 +++-- src/pages/workspace/WorkspaceMembersPage.tsx | 6 +++--- src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx | 4 ++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/libs/actions/Policy/Member.ts b/src/libs/actions/Policy/Member.ts index c4ff5c39913ef..1fb0265f4bfa8 100644 --- a/src/libs/actions/Policy/Member.ts +++ b/src/libs/actions/Policy/Member.ts @@ -101,7 +101,8 @@ Onyx.connect({ }, }); -function isApproval(policy: OnyxEntry, employeeAccountID: number) { +/** Check if the passed employee is an approver in the policy's employeeList */ +function isApprover(policy: OnyxEntry, employeeAccountID: number) { const employeeLogin = allPersonalDetails?.[employeeAccountID]?.login; return Object.values(policy?.employeeList ?? {}).some( (employee) => employee?.submitsTo === employeeLogin || employee?.forwardsTo === employeeLogin || employee?.overLimitForwardsTo === employeeLogin, @@ -844,7 +845,7 @@ export { inviteMemberToWorkspace, acceptJoinRequest, declineJoinRequest, - isApproval, + isApprover, }; export type {NewCustomUnit}; diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index 892fc29faa2b4..8f1bb6ee12bad 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -98,12 +98,12 @@ function WorkspaceMembersPage({personalDetails, invitedEmailsToAccountIDsDraft, const policyID = route.params.policyID; const confirmModalPrompt = useMemo(() => { - const approvalAccountID = selectedEmployees.find((selectedEmployee) => Member.isApproval(policy, selectedEmployee)); - if (!approvalAccountID) { + const approverAccountID = selectedEmployees.find((selectedEmployee) => Member.isApprover(policy, selectedEmployee)); + if (!approverAccountID) { return translate('workspace.people.removeMembersPrompt'); } return translate('workspace.people.removeMembersWarningPrompt', { - memberName: getDisplayNameForParticipant(approvalAccountID), + memberName: getDisplayNameForParticipant(approverAccountID), ownerName: getDisplayNameForParticipant(policy?.ownerAccountID), }); }, [selectedEmployees, policy, translate]); diff --git a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx index 491d72a42b7d9..c23266f578913 100644 --- a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx +++ b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx @@ -67,8 +67,8 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM const isCurrentUserOwner = policy?.owner === currentUserPersonalDetails?.login; const confirmModalPrompt = useMemo(() => { - const isApproval = Member.isApproval(policy, accountID); - if (!isApproval) { + const isApprover = Member.isApprover(policy, accountID); + if (!isApprover) { translate('workspace.people.removeMemberPrompt', {memberName: displayName}); } return translate('workspace.people.removeMembersWarningPrompt', {memberName: displayName, ownerName: policy?.owner ?? ''}); From b6da7ed433ddc4b80ebc358157a490479a1d5159 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Thu, 27 Jun 2024 16:39:58 +0700 Subject: [PATCH 4/5] fix ts check --- src/libs/actions/Policy/Member.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/Policy/Member.ts b/src/libs/actions/Policy/Member.ts index 1fb0265f4bfa8..f8472bd430984 100644 --- a/src/libs/actions/Policy/Member.ts +++ b/src/libs/actions/Policy/Member.ts @@ -258,7 +258,7 @@ function removeMembers(accountIDs: number[], policyID: string) { if (employee?.submitsTo && emailList.includes(employee?.submitsTo)) { optimisticMembersState[employeeEmail] = { ...optimisticMembersState[employeeEmail], - submitsTo: policy.owner, + submitsTo: policy?.owner, }; failureMembersState[employeeEmail] = { ...failureMembersState[employeeEmail], @@ -268,7 +268,7 @@ function removeMembers(accountIDs: number[], policyID: string) { if (employee?.forwardsTo && emailList.includes(employee?.forwardsTo)) { optimisticMembersState[employeeEmail] = { ...optimisticMembersState[employeeEmail], - forwardsTo: policy.owner, + forwardsTo: policy?.owner, }; failureMembersState[employeeEmail] = { ...failureMembersState[employeeEmail], @@ -278,7 +278,7 @@ function removeMembers(accountIDs: number[], policyID: string) { if (employee?.overLimitForwardsTo && emailList.includes(employee?.overLimitForwardsTo)) { optimisticMembersState[employeeEmail] = { ...optimisticMembersState[employeeEmail], - overLimitForwardsTo: policy.owner, + overLimitForwardsTo: policy?.owner, }; failureMembersState[employeeEmail] = { ...failureMembersState[employeeEmail], From 661d14458bc7a2bc05592f2c56a8e061d4a5fac9 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Thu, 27 Jun 2024 18:33:42 +0700 Subject: [PATCH 5/5] update comment --- src/types/onyx/PolicyEmployee.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/types/onyx/PolicyEmployee.ts b/src/types/onyx/PolicyEmployee.ts index d0c3d4be6b8ae..91b7f16538218 100644 --- a/src/types/onyx/PolicyEmployee.ts +++ b/src/types/onyx/PolicyEmployee.ts @@ -8,13 +8,16 @@ type PolicyEmployee = OnyxCommon.OnyxValueWithOfflineFeedback<{ /** Email of the user */ email?: string; - /** Email of the user this user forwards all approved reports to */ + /** Determines if this employee should approve a report. If report total > approvalLimit, next approver will be 'overLimitForwardsTo', otherwise 'forwardsTo' */ + approvalLimit?: number; + + /** Email of the user this user forwards all approved reports to (when report total under 'approvalLimit' or when 'overLimitForwardsTo' is not set) */ forwardsTo?: string; /** Email of the user this user submits all reports to */ submitsTo?: string; - /** Email of the user this user forwards all reports when the amount of the report is over limited */ + /** Email of the user this user forwards all reports to when the report total is over the 'approvalLimit' */ overLimitForwardsTo?: string; /**