Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions src/components/Search/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import Log from '@libs/Log';
import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute';
import type {PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types';
import Performance from '@libs/Performance';
import {canEditFieldOfMoneyRequest, canHoldUnholdReportAction, isOneTransactionReport, selectFilteredReportActions} from '@libs/ReportUtils';
import {canAddOrDeleteTransactions, canEditFieldOfMoneyRequest, canHoldUnholdReportAction, isOneTransactionReport, selectFilteredReportActions} from '@libs/ReportUtils';
import {buildCannedSearchQuery, buildSearchQueryJSON, buildSearchQueryString} from '@libs/SearchQueryUtils';
import {
createAndOpenSearchTransactionThread,
Expand Down Expand Up @@ -97,7 +97,7 @@ function mapTransactionItemToSelectedEntry(item: TransactionListItemType, outsta
item.keyForList,
{
isSelected: true,
canDelete: item.canDelete,
canDelete: canAddOrDeleteTransactions(item.report, item.policy),
canHold: canHoldRequest,
isHeld: isOnHold(item),
canUnhold: canUnholdRequest,
Expand Down Expand Up @@ -179,7 +179,7 @@ function prepareTransactionsList(item: TransactionListItemType, selectedTransact
...selectedTransactions,
[item.keyForList]: {
isSelected: true,
canDelete: item.canDelete,
canDelete: canAddOrDeleteTransactions(item.report, item.policy),
canHold: canHoldRequest,
isHeld: isOnHold(item),
canUnhold: canUnholdRequest,
Expand Down Expand Up @@ -514,7 +514,8 @@ function Search({
),
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
isSelected: areAllMatchingItemsSelected || selectedTransactions[transactionItem.transactionID]?.isSelected || isExpenseReportType,
canDelete: transactionItem.canDelete,
canDelete: canAddOrDeleteTransactions(transactionItem.report, transactionItem.policy),

reportID: transactionItem.reportID,
policyID: transactionItem.report?.policyID,
amount: transactionItem.modifiedAmount ?? transactionItem.amount,
Expand Down Expand Up @@ -559,7 +560,8 @@ function Search({
),
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
isSelected: areAllMatchingItemsSelected || selectedTransactions[transactionItem.transactionID].isSelected,
canDelete: transactionItem.canDelete,
canDelete: canAddOrDeleteTransactions(transactionItem.report, transactionItem.policy),

reportID: transactionItem.reportID,
policyID: transactionItem.report?.policyID,
amount: transactionItem.modifiedAmount ?? transactionItem.amount,
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/useSelectedTransactionsActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import Navigation from '@libs/Navigation/Navigation';
import {getIOUActionForTransactionID, getReportAction, isDeletedAction} from '@libs/ReportActionsUtils';
import {isMergeAction} from '@libs/ReportSecondaryActionUtils';
import {
canAddOrDeleteTransactions,
canDeleteCardTransactionByLiabilityType,
canDeleteTransaction,
canEditFieldOfMoneyRequest,
canHoldUnholdReportAction,
canUserPerformWriteAction as canUserPerformWriteActionReportUtils,
Expand Down Expand Up @@ -313,7 +313,7 @@ function useSelectedTransactionsActions({
return canRemoveTransaction && isIOUActionOwner && !isActionDeleted;
});

const canRemoveReportTransaction = canDeleteTransaction(report, isReportArchived);
const canRemoveReportTransaction = canAddOrDeleteTransactions(report, policy, isReportArchived);

if (canRemoveReportTransaction && canAllSelectedTransactionsBeRemoved) {
options.push({
Expand Down
3 changes: 2 additions & 1 deletion src/libs/ReportSecondaryActionUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import {
hasOnlyNonReimbursableTransactions,
hasReportBeenReopened as hasReportBeenReopenedUtils,
hasReportBeenRetracted as hasReportBeenRetractedUtils,
isArchivedReport,
isArchivedReport, // eslint-disable-next-line @typescript-eslint/no-deprecated
isAwaitingFirstLevelApproval,
isClosedReport as isClosedReportUtils,
isCurrentUserSubmitter,
Expand Down Expand Up @@ -125,6 +125,7 @@ function isSplitAction(report: Report, reportTransactions: Transaction[], origin
}

// Hide split option for the submitter if the report is forwarded
// eslint-disable-next-line @typescript-eslint/no-deprecated
return (isSubmitter && isAwaitingFirstLevelApproval(report)) || isAdmin || isManager;
}

Expand Down
93 changes: 73 additions & 20 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ import {
getAccountIDsByLogins,
getDisplayNameOrDefault,
getEffectiveDisplayName,
getLoginByAccountID,
getLoginsByAccountIDs,
getPersonalDetailByEmail,
getPersonalDetailsByIDs,
Expand All @@ -146,6 +147,7 @@ import {
getCleanedTagName,
getConnectedIntegration,
getCorrectedAutoReportingFrequency,
getDefaultApprover,
getForwardsToAccount,
getManagerAccountEmail,
getManagerAccountID,
Expand All @@ -155,6 +157,7 @@ import {
getRuleApprovers,
getSubmitToAccountID,
hasDependentTags as hasDependentTagsPolicyUtils,
hasDynamicExternalWorkflow,
isExpensifyTeam,
isInstantSubmitEnabled,
isPaidGroupPolicy as isPaidGroupPolicyPolicyUtils,
Expand Down Expand Up @@ -1438,7 +1441,7 @@ function isIOUReport(reportOrID: OnyxInputOrEntry<Report> | string): boolean {
/**
* Checks if a report is an IOU report using report
*/
function isIOUReportUsingReport(report: OnyxEntry<Report>): report is Report {
function isIOUReportUsingReport(report: OnyxEntry<Report>): boolean {
return report?.type === CONST.REPORT.TYPE.IOU;
}

Expand Down Expand Up @@ -1963,6 +1966,9 @@ function requiresManualSubmission(report: OnyxEntry<Report>, policy: OnyxEntry<P
return isManualSubmitEnabled || (isOpenReport(report) && isInstantSubmitEnabled(policy) && isSubmitAndClose(policy));
}

/**
* @deprecated This function will be removed soon. Please use isAwaitingFirstLevelApprovalNew instead
*/
function isAwaitingFirstLevelApproval(report: OnyxEntry<Report>): boolean {
if (!report) {
return false;
Expand All @@ -1975,6 +1981,63 @@ function isAwaitingFirstLevelApproval(report: OnyxEntry<Report>): boolean {
return isProcessingReport(report) && submitsToAccountID === report.managerID;
}

function isAwaitingFirstLevelApprovalNew(report: OnyxEntry<Report>, reportActions: ReportAction[], policy: OnyxEntry<Policy>): boolean {
if (!report) {
return false;
}

if (!isProcessingReport(report)) {
return false;
}

if (isIOUReportUsingReport(report)) {
return true;
}

if (hasDynamicExternalWorkflow(policy)) {
return false;
}

if (policy?.approvalMode === CONST.POLICY.APPROVAL_MODE.BASIC) {
return true;
}

// If the report is part of a policy with Instant Submit, this data should be stored in the CREATED action
// as Instant Submit reports do not have a SUBMITTED action.
// For all other cases, use the most recent SUBMITTED action instead.
const usedReportAction = isInstantSubmitEnabled(policy)
? reportActions?.find((action) => action.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED)
: reportActions
?.filter((action) => action.actionName === CONST.REPORT.ACTIONS.TYPE.SUBMITTED)
?.sort((a, b) => {
if (!a.created || !b.created) {
return !a.created ? 1 : -1;
}
return a.created < b.created ? 1 : -1;
})
?.at(0);

const originalMessage = getOriginalMessage(usedReportAction);
if (!originalMessage) {
return false;
}
let submittedTo: number | undefined = 'submittedTo' in originalMessage ? originalMessage?.submittedTo : undefined;

if (!submittedTo) {
const submittedToLogin = 'to' in originalMessage ? (originalMessage?.to ?? '') : '';
submittedTo = getAccountIDsByLogins([submittedToLogin])?.at(0);
}

if (!submittedTo) {
const managerID = 'managerOnVacation' in originalMessage && originalMessage?.managerOnVacation ? originalMessage?.managerOnVacation : report.managerID;
const approverAccountID =
policy?.employeeList?.[getLoginByAccountID(report?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID) ?? '']?.submitsTo ?? getAccountIDsByLogins([getDefaultApprover(policy)])?.at(0);
return managerID === approverAccountID;
}

return report.managerID === submittedTo;
}

/**
* Updates optimistic transaction violations to OnyxData for the given policy and categories onyx update.
*
Expand Down Expand Up @@ -2684,21 +2747,17 @@ function getChildReportNotificationPreference(reportAction: OnyxInputOrEntry<Rep
return isActionCreator(reportAction) ? CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS : CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN;
}

function canAddOrDeleteTransactions(moneyRequestReport: OnyxEntry<Report>, isReportArchived = false): boolean {
function canAddOrDeleteTransactions(moneyRequestReport: OnyxEntry<Report>, policy?: OnyxEntry<Policy>, isReportArchived = false): boolean {
if (!isMoneyRequestReport(moneyRequestReport) || isReportArchived) {
return false;
}
// This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850
// eslint-disable-next-line @typescript-eslint/no-deprecated
const policy = getPolicy(moneyRequestReport?.policyID);

// Adding or deleting transactions is not allowed on a closed report
if (moneyRequestReport?.statusNum === CONST.REPORT.STATUS_NUM.CLOSED && !isOpenReport(moneyRequestReport)) {
return false;
}

if (isInstantSubmitEnabled(policy) && isProcessingReport(moneyRequestReport)) {
return isAwaitingFirstLevelApproval(moneyRequestReport);
return isAwaitingFirstLevelApprovalNew(moneyRequestReport, Object.values(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${moneyRequestReport?.reportID}`] ?? {}), policy);
}

if (isReportApproved({report: moneyRequestReport}) || isClosedReport(moneyRequestReport) || isSettled(moneyRequestReport?.reportID)) {
Expand Down Expand Up @@ -2727,17 +2786,7 @@ function canAddTransaction(moneyRequestReport: OnyxEntry<Report>, isReportArchiv
return false;
}

return canAddOrDeleteTransactions(moneyRequestReport, isReportArchived);
}

/**
* Checks whether the supplied report supports deleting more transactions from it.
* Return true if:
* - report is a non-settled IOU
* - report is a non-approved IOU
*/
function canDeleteTransaction(moneyRequestReport: OnyxEntry<Report>, isReportArchived = false): boolean {
return canAddOrDeleteTransactions(moneyRequestReport, isReportArchived);
return canAddOrDeleteTransactions(moneyRequestReport, policy, isReportArchived);
}

/**
Expand Down Expand Up @@ -2767,6 +2816,7 @@ function isMoneyRequestReportEligibleForMerge(reportID: string, isAdmin: boolean
}

if (isSubmitter) {
// eslint-disable-next-line @typescript-eslint/no-deprecated
return isOpenReport(report) || (isIOUReport(report) && isProcessingReport(report)) || isAwaitingFirstLevelApproval(report);
}

Expand Down Expand Up @@ -2953,7 +3003,7 @@ function canDeleteReportAction(

if (isActionOwner) {
if (!isEmptyObject(report) && (isMoneyRequestReport(report) || isInvoiceReport(report))) {
return canDeleteTransaction(report) && canCardTransactionBeDeleted;
return canAddOrDeleteTransactions(report, policy ?? undefined) && canCardTransactionBeDeleted;
}
if (isTrackExpenseAction(reportAction)) {
return canCardTransactionBeDeleted;
Expand Down Expand Up @@ -4659,6 +4709,7 @@ function canEditReportPolicy(report: OnyxEntry<Report>, reportPolicy: OnyxEntry<
}

if (isSubmitted) {
// eslint-disable-next-line @typescript-eslint/no-deprecated
return (isSubmitter && isAwaitingFirstLevelApproval(report)) || isManager || isAdmin;
}

Expand Down Expand Up @@ -12981,7 +13032,7 @@ export {
canAccessReport,
isReportNotFound,
canAddTransaction,
canDeleteTransaction,
canAddOrDeleteTransactions,
canBeAutoReimbursed,
canCreateRequest,
canCreateTaskInReport,
Expand Down Expand Up @@ -13197,7 +13248,9 @@ export {
isOpenReport,
requiresManualSubmission,
isReportIDApproved,
// eslint-disable-next-line @typescript-eslint/no-deprecated
isAwaitingFirstLevelApproval,
isAwaitingFirstLevelApprovalNew,
isPublicAnnounceRoom,
isPublicRoom,
isReportApproved,
Expand Down
1 change: 0 additions & 1 deletion src/libs/SearchUIUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2659,7 +2659,6 @@ function getTransactionFromTransactionListItem(item: TransactionListItemType): O
isTaxAmountColumnWide,
violations,
hash,
canDelete,
accountID,
policyID,
...transaction
Expand Down
4 changes: 2 additions & 2 deletions src/pages/ReportDetailsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ import Permissions from '@libs/Permissions';
import {isPolicyAdmin as isPolicyAdminUtil, isPolicyEmployee as isPolicyEmployeeUtil, shouldShowPolicy} from '@libs/PolicyUtils';
import {getOneTransactionThreadReportID, getOriginalMessage, getTrackExpenseActionableWhisper, isDeletedAction, isMoneyRequestAction, isTrackExpenseAction} from '@libs/ReportActionsUtils';
import {
canAddOrDeleteTransactions,
canDeleteCardTransactionByLiabilityType,
canDeleteTransaction,
canEditReportDescription as canEditReportDescriptionUtil,
canJoinChat,
canLeaveChat,
Expand Down Expand Up @@ -291,7 +291,7 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail
!isClosedReport(report) &&
isTaskModifiable &&
isTaskActionable;
const canDeleteRequest = isActionOwner && (canDeleteTransaction(moneyRequestReport, isMoneyRequestReportArchived) || isSelfDMTrackExpenseReport) && !isDeletedParentAction;
const canDeleteRequest = isActionOwner && (canAddOrDeleteTransactions(moneyRequestReport, policy, isMoneyRequestReportArchived) || isSelfDMTrackExpenseReport) && !isDeletedParentAction;
const iouTransactionID = isMoneyRequestAction(requestParentReportAction) ? getOriginalMessage(requestParentReportAction)?.IOUTransactionID : undefined;
const [iouTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${iouTransactionID}`, {canBeMissing: true});
const {duplicateTransactions, duplicateTransactionViolations} = useDuplicateTransactionsAndViolations(iouTransactionID ? [iouTransactionID] : []);
Expand Down
3 changes: 0 additions & 3 deletions src/types/onyx/SearchResults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,6 @@ type SearchTransaction = {
/** The transaction amount */
amount: number;

/** If the transaction can be deleted */
canDelete: boolean;

/** The edited transaction amount */
modifiedAmount: number;

Expand Down
1 change: 0 additions & 1 deletion tests/unit/MoneyRequestReportUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ const transactionItemBaseMock: TransactionListItemType = {
policy: policyBaseMock,
reportAction: reportActionBaseMock,
holdReportAction: undefined,
canDelete: true,
cardID: undefined,
cardName: undefined,
category: '',
Expand Down
12 changes: 6 additions & 6 deletions tests/unit/ReportUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ import {
buildOptimisticReportPreview,
buildParticipantsFromAccountIDs,
buildTransactionThread,
canAddOrDeleteTransactions,
canAddTransaction,
canCreateRequest,
canDeleteMoneyRequestReport,
canDeleteReportAction,
canDeleteTransaction,
canEditMoneyRequest,
canEditReportDescription,
canEditRoomVisibility,
Expand Down Expand Up @@ -5984,7 +5984,7 @@ describe('ReportUtils', () => {
});
});

describe('canDeleteTransaction', () => {
describe('canAddOrDeleteTransactions', () => {
it('should return true for a non-archived report', async () => {
// Given a non-archived expense report
const report: Report = {
Expand All @@ -5996,7 +5996,7 @@ describe('ReportUtils', () => {
// When it's checked if the transactions can be deleted
// Simulate how components determined if a report is archived by using this hook
const {result: isReportArchived} = renderHook(() => useReportIsArchived(report?.reportID));
const result = canDeleteTransaction(report, isReportArchived.current);
const result = canAddOrDeleteTransactions(report, policy, isReportArchived.current);

// Then the result is true
expect(result).toBe(true);
Expand All @@ -6013,7 +6013,7 @@ describe('ReportUtils', () => {

// When it's checked if the transactions can be deleted
const {result: isReportArchived} = renderHook(() => useReportIsArchived(report?.reportID));
const result = canDeleteTransaction(report, isReportArchived.current);
const result = canAddOrDeleteTransactions(report, policy, isReportArchived.current);

// Then the result is false
expect(result).toBe(false);
Expand Down Expand Up @@ -6062,7 +6062,7 @@ describe('ReportUtils', () => {

await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${openReport.reportID}`, openReport);

expect(canDeleteTransaction(openReport, false)).toBe(true);
expect(canAddOrDeleteTransactions(openReport, policy, false)).toBe(true);
});

it('should return false for closed report when workflow is disabled', async () => {
Expand All @@ -6075,7 +6075,7 @@ describe('ReportUtils', () => {

await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${closedReport.reportID}`, closedReport);

expect(canDeleteTransaction(closedReport, false)).toBe(false);
expect(canAddOrDeleteTransactions(closedReport, policy, false)).toBe(false);
});
});
});
Expand Down
Loading
Loading