-
Notifications
You must be signed in to change notification settings - Fork 4k
Show Bypass approver option to the workspace admin when they are the report's current approver #98717
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Show Bypass approver option to the workspace admin when they are the report's current approver #98717
Changes from 2 commits
4b87753
1c3176f
6d282f5
928c86e
f1e636d
49457a6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; | ||
| import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; | ||
| import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; | ||
| import HeaderWithBackButton from '@components/HeaderWithBackButton'; | ||
| import RenderHTML from '@components/RenderHTML'; | ||
|
|
@@ -9,17 +10,20 @@ import SingleSelectListItem from '@components/SelectionList/ListItem/SingleSelec | |
| import type {ListItem} from '@components/SelectionList/types'; | ||
| import Text from '@components/Text'; | ||
|
|
||
| import {useCurrencyListActions} from '@hooks/useCurrencyList'; | ||
| import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; | ||
| import useDelegateAccountID from '@hooks/useDelegateAccountID'; | ||
| import useEnvironment from '@hooks/useEnvironment'; | ||
| import useLocalize from '@hooks/useLocalize'; | ||
| import useNetwork from '@hooks/useNetwork'; | ||
| import useOnyx from '@hooks/useOnyx'; | ||
| import usePermissions from '@hooks/usePermissions'; | ||
| import useThemeStyles from '@hooks/useThemeStyles'; | ||
|
|
||
| import {assignReportToMe} from '@libs/actions/IOU/ReportWorkflow'; | ||
| import {approveMoneyRequest, assignReportToMe} from '@libs/actions/IOU/ReportWorkflow'; | ||
| import {openBulkChangeApproverPage} from '@libs/actions/Search'; | ||
| import Navigation from '@libs/Navigation/Navigation'; | ||
| import {getLoginByAccountID} from '@libs/PersonalDetailsUtils'; | ||
| import {isControlPolicy, isPolicyAdmin} from '@libs/PolicyUtils'; | ||
| import {hasViolations as hasViolationsReportUtils, isAllowedToApproveExpenseReport} from '@libs/ReportUtils'; | ||
|
|
||
|
|
@@ -33,6 +37,7 @@ import type {Policy, Report} from '@src/types/onyx'; | |
|
|
||
| import type {OnyxCollection} from 'react-native-onyx'; | ||
|
|
||
| import {delegateEmailSelector} from '@selectors/Account'; | ||
| import {isTrackIntentUserSelector} from '@selectors/Onboarding'; | ||
| import React, {useEffect, useLayoutEffect, useRef, useState} from 'react'; | ||
| import {View} from 'react-native'; | ||
|
|
@@ -87,6 +92,16 @@ function SearchChangeApproverPage() { | |
| const [hasLoadedApp] = useOnyx(ONYXKEYS.HAS_LOADED_APP); | ||
| const [isLoadingBulkChangeApproverPage = true] = useOnyx(ONYXKEYS.IS_LOADING_BULK_CHANGE_APPROVER_PAGE); | ||
| const {isOffline} = useNetwork(); | ||
| const {getCurrencyDecimals} = useCurrencyListActions(); | ||
| const {isDelegateAccessRestricted} = useDelegateNoAccessState(); | ||
| const {showDelegateNoAccessModal} = useDelegateNoAccessActions(); | ||
| const [betas] = useOnyx(ONYXKEYS.BETAS); | ||
| const [userBillingGracePeriodEnds] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END); | ||
| const [amountOwed] = useOnyx(ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED); | ||
| const [ownerBillingGracePeriodEnd] = useOnyx(ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END); | ||
| const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); | ||
|
hungvu193 marked this conversation as resolved.
|
||
| const [delegateEmail] = useOnyx(ONYXKEYS.ACCOUNT, {selector: delegateEmailSelector}); | ||
| const delegateAccountID = useDelegateAccountID(); | ||
|
|
||
| const getOnyxReports = (allReports: OnyxCollection<Report>) => { | ||
| const reports = Object.create(null) as Record<string, Report>; | ||
|
|
@@ -164,17 +179,46 @@ function SearchChangeApproverPage() { | |
| return; | ||
| } | ||
|
|
||
| if (isDelegateAccessRestricted) { | ||
| showDelegateNoAccessModal(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We check for delegate access here, but we don't do the same in DynamicReportChangeApproverPage
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just done reading through the AI reviews, this is the same as #98717 (comment). Is it really out of scope? This PR adds the delegate access check before reassigning and approving the report. Before this PR: bypass approver only reassigns the approver; no delegate access check. This means a delegate can reassign the approver, bulk or individual.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm thinking about removing this check because we don't have this before 🤔 Wdyt
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we remove the delegate check (so it works like before), we are still left with I think it would be weird if we also show the restricted access modal when doing bulk approval before
But this means we need to customize
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah, I think that works, early return is better since we shouldn't show delegate modal inside the bulk actions
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
@bernhardoj How did you do this? AFter approving the Screen.Recording.2026-08-22.at.22.07.45.mov
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, it's when you are a delegate without access, so it only reassigns without approving.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh... So I guess we don't show
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You mean delegate without access? I'm actually kinda confuse with this bypass approver feature. If we are already the current approver (manager) (which is the case here), why do we need a bypass approver? I think this "bypass multiple times" can happen too in a case when the user is not the current manager. When the user is not the current manager, choosing bypass approver will only reassign the final approver to the current user, but will not approve the report. So, since the report is not approved yet, the user can choose bypass approver once again to finally approve it. If we disable bypass approver for delegate without access, then a delegate without access that is also not the current manager won't be able to reassign the approver to themselve, even though it's allowed.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I need to double check but, we only show this option when the user is allowed to approve the report. Once report is approved, this option should be hide. |
||
| return; | ||
| } | ||
|
|
||
| for (const selectedReport of selectedReports) { | ||
| const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${selectedReport.policyID}`]; | ||
| const report = selectedReport.reportID ? onyxReports?.[selectedReport.reportID] : undefined; | ||
| if (!policy || !report) { | ||
| continue; | ||
| } | ||
|
|
||
| const hasViolations = hasViolationsReportUtils(report.reportID, transactionViolations, currentUserDetails.accountID, currentUserDetails.email ?? ''); | ||
| assignReportToMe(report, currentUserDetails.accountID, currentUserDetails.email ?? '', policy, hasViolations, isASAPSubmitBetaEnabled, isTrackIntentUser, formatPhoneNumber); | ||
|
hungvu193 marked this conversation as resolved.
|
||
|
|
||
| // Taking control only makes the current user the final approver. When they already are the manager, the | ||
| // report stays waiting on them, so approve it as well to actually bypass the remaining approvers. | ||
| if (report.managerID !== currentUserDetails.accountID) { | ||
| const hasViolations = hasViolationsReportUtils(report.reportID, transactionViolations, currentUserDetails.accountID, currentUserDetails.email ?? ''); | ||
| assignReportToMe(report, currentUserDetails.accountID, currentUserDetails.email ?? '', policy, hasViolations, isASAPSubmitBetaEnabled, isTrackIntentUser, formatPhoneNumber); | ||
| continue; | ||
| } | ||
|
|
||
| approveMoneyRequest({ | ||
|
hungvu193 marked this conversation as resolved.
|
||
| getCurrencyDecimals, | ||
| expenseReport: report, | ||
| expenseReportPolicy: policy, | ||
| currentUserAccountIDParam: currentUserDetails.accountID, | ||
| currentUserEmailParam: currentUserDetails.email ?? '', | ||
| hasViolations, | ||
| isASAPSubmitBetaEnabled, | ||
| betas, | ||
| userBillingGracePeriodEnds, | ||
| amountOwed, | ||
| ownerBillingGracePeriodEnd, | ||
| ownerLogin: getLoginByAccountID(report.ownerAccountID, personalDetails), | ||
| delegateEmail, | ||
| delegateAccountID, | ||
| full: true, | ||
| shouldPlaySuccessSound: false, | ||
| isTrackIntentUser, | ||
| }); | ||
| } | ||
|
|
||
| // Note: This clears both reports and transactions | ||
|
|
@@ -202,19 +246,7 @@ function SearchChangeApproverPage() { | |
| return isPolicyAdmin(policy) && isAllowedToApproveExpenseReport(report, currentUserDetails.accountID, policy); | ||
| }); | ||
|
|
||
| const shouldShowBypassApproversOption = | ||
| hasPermission && | ||
| selectedReports.some((selectedReport) => { | ||
| const report = selectedReport.reportID ? onyxReports?.[selectedReport.reportID] : undefined; | ||
|
|
||
| if (!report) { | ||
| return false; | ||
| } | ||
|
|
||
| return report.managerID !== currentUserDetails.accountID; | ||
| }); | ||
|
|
||
| if (shouldShowBypassApproversOption) { | ||
| if (hasPermission) { | ||
| data.push({ | ||
| text: translate('iou.changeApprover.actions.bypassApprovers'), | ||
| keyForList: APPROVER_TYPE.BYPASS_APPROVER, | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.