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
4 changes: 3 additions & 1 deletion src/components/Search/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
import useSearchShouldCalculateTotals from '@hooks/useSearchShouldCalculateTotals';
import useThemeStyles from '@hooks/useThemeStyles';
import {turnOffMobileSelectionMode, turnOnMobileSelectionMode} from '@libs/actions/MobileSelectionMode';
import {openSearch, setOptimisticDataForTransactionThreadPreview} from '@libs/actions/Search';
import type {TransactionPreviewData} from '@libs/actions/Search';
import {openSearch, setOptimisticDataForTransactionThreadPreview} from '@libs/actions/Search';
import Timing from '@libs/actions/Timing';
import {canUseTouchScreen} from '@libs/DeviceCapabilities';
import Log from '@libs/Log';
Expand Down Expand Up @@ -115,6 +115,7 @@
isFromOneTransactionReport: item.isFromOneTransactionReport,
convertedCurrency: item.convertedCurrency,
currency: item.currency,
ownerAccountID: item.report?.ownerAccountID ?? item.accountID,
},
];
}
Expand Down Expand Up @@ -197,6 +198,7 @@
convertedCurrency: item.convertedCurrency,
currency: item.currency,
isFromOneTransactionReport: item.isFromOneTransactionReport,
ownerAccountID: item.report?.ownerAccountID ?? item.accountID,
},
};
}
Expand Down Expand Up @@ -399,7 +401,7 @@
}, [lastSearchType, setShouldShowFiltersBarLoading, shouldShowLoadingState, type]);

useEffect(() => {
const focusedRoute = findFocusedRoute(navigationRef.getRootState());

Check warning on line 404 in src/components/Search/index.tsx

View workflow job for this annotation

GitHub Actions / ESLint check

React Hook useMemo has a missing dependency: 'allReportNameValuePairs'. Either include it or remove the dependency array
const isMigratedModalDisplayed = focusedRoute?.name === NAVIGATORS.MIGRATED_USER_MODAL_NAVIGATOR || focusedRoute?.name === SCREENS.MIGRATED_USER_WELCOME_MODAL.ROOT;

const comingBackOnlineWithNoResults = prevIsOffline && !isOffline && searchResults === undefined;
Expand Down
3 changes: 3 additions & 0 deletions src/components/Search/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ type SelectedTransactionInfo = {

/** Whether it is the only expense of the parent expense report */
isFromOneTransactionReport?: boolean;

/** Account ID of the report owner */
ownerAccountID?: number;
};

/** Model of selected transactions */
Expand Down
66 changes: 55 additions & 11 deletions src/hooks/useSelectedTransactionsActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
canEditFieldOfMoneyRequest,
canHoldUnholdReportAction,
canUserPerformWriteAction as canUserPerformWriteActionReportUtils,
getReportOrDraftReport,
isArchivedReport,
isInvoiceReport,
isMoneyRequestReport as isMoneyRequestReportUtils,
Expand Down Expand Up @@ -58,7 +59,7 @@ function useSelectedTransactionsActions({
beginExportWithTemplate: (templateName: string, templateType: string, transactionIDList: string[], policyID?: string) => void;
}) {
const {isOffline} = useNetworkWithOfflineStatus();
const {selectedTransactionIDs, clearSelectedTransactions} = useSearchContext();
const {selectedTransactionIDs, clearSelectedTransactions, selectedTransactions: selectedTransactionsMeta} = useSearchContext();
const [allTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {canBeMissing: false});
const [outstandingReportsByPolicyID] = useOnyx(ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID, {canBeMissing: true});
const [lastVisitedPath] = useOnyx(ONYXKEYS.LAST_VISITED_PATH, {canBeMissing: true});
Expand All @@ -69,7 +70,7 @@ function useSelectedTransactionsActions({

const {duplicateTransactions, duplicateTransactionViolations} = useDuplicateTransactionsAndViolations(selectedTransactionIDs);
const isReportArchived = useReportIsArchived(report?.reportID);
const selectedTransactions = useMemo(
const selectedTransactionsList = useMemo(
() =>
selectedTransactionIDs.reduce((acc, transactionID) => {
const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
Expand All @@ -80,6 +81,48 @@ function useSelectedTransactionsActions({
}, [] as Transaction[]),
[allTransactions, selectedTransactionIDs],
);
const hasTransactionsFromMultipleOwners = useMemo(() => {
const knownOwnerIDs = new Set<number>();
let hasUnknownOwner = false;

for (const selectedTransactionInfo of Object.values(selectedTransactionsMeta ?? {})) {
const ownerAccountID = selectedTransactionInfo?.ownerAccountID;
if (typeof ownerAccountID === 'number') {
knownOwnerIDs.add(ownerAccountID);
if (knownOwnerIDs.size > 1) {
return true;
}
} else {
hasUnknownOwner = true;
}
}

for (const selectedTransaction of selectedTransactionsList) {
const reportID = selectedTransaction?.reportID;
if (!reportID || reportID === CONST.REPORT.UNREPORTED_REPORT_ID) {
hasUnknownOwner = true;
continue;
}

const parentReport = getReportOrDraftReport(reportID);
const ownerAccountID = parentReport?.ownerAccountID;

if (typeof ownerAccountID === 'number') {
knownOwnerIDs.add(ownerAccountID);
if (knownOwnerIDs.size > 1) {
return true;
}
} else {
hasUnknownOwner = true;
}
}

if (hasUnknownOwner) {
return knownOwnerIDs.size > 0 || selectedTransactionIDs.length > 1;
}

return false;
}, [selectedTransactionsList, selectedTransactionsMeta, selectedTransactionIDs]);

const {translate} = useLocalize();
const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false);
Expand Down Expand Up @@ -171,10 +214,10 @@ function useSelectedTransactionsActions({
const options = [];
const isMoneyRequestReport = isMoneyRequestReportUtils(report);
const isReportReimbursed = report?.stateNum === CONST.REPORT.STATE_NUM.APPROVED && report?.statusNum === CONST.REPORT.STATUS_NUM.REIMBURSED;
let canHoldTransactions = selectedTransactions.length > 0 && isMoneyRequestReport && !isReportReimbursed;
let canUnholdTransactions = selectedTransactions.length > 0 && isMoneyRequestReport;
let canHoldTransactions = selectedTransactionsList.length > 0 && isMoneyRequestReport && !isReportReimbursed;
let canUnholdTransactions = selectedTransactionsList.length > 0 && isMoneyRequestReport;

selectedTransactions.forEach((selectedTransaction) => {
selectedTransactionsList.forEach((selectedTransaction) => {
if (!canHoldTransactions && !canUnholdTransactions) {
return;
}
Expand Down Expand Up @@ -271,7 +314,7 @@ function useSelectedTransactionsActions({
subMenuItems: getExportOptions(),
});

const canSelectedExpensesBeMoved = selectedTransactions.every((transaction) => {
const canSelectedExpensesBeMoved = selectedTransactionsList.every((transaction) => {
if (!transaction) {
return false;
}
Expand All @@ -282,7 +325,7 @@ function useSelectedTransactionsActions({
});

const canUserPerformWriteAction = canUserPerformWriteActionReportUtils(report, isReportArchived);
if (canSelectedExpensesBeMoved && canUserPerformWriteAction) {
if (canSelectedExpensesBeMoved && canUserPerformWriteAction && !hasTransactionsFromMultipleOwners) {
options.push({
text: translate('iou.moveExpenses', {count: selectedTransactionIDs.length}),
icon: Expensicons.DocumentMerge,
Expand All @@ -296,14 +339,14 @@ function useSelectedTransactionsActions({
}

// In phase 1, we only show merge action if report is eligible for merge and only one transaction is selected
const canMergeTransaction = selectedTransactions.length === 1 && report && isMergeAction(report, selectedTransactions, policy);
const canMergeTransaction = selectedTransactionsList.length === 1 && report && isMergeAction(report, selectedTransactionsList, policy);
if (canMergeTransaction) {
options.push({
text: translate('common.merge'),
icon: Expensicons.ArrowCollapse,
value: MERGE,
onSelected: () => {
const targetTransaction = selectedTransactions.at(0);
const targetTransaction = selectedTransactionsList.at(0);

if (!report || !targetTransaction) {
return;
Expand All @@ -315,7 +358,7 @@ function useSelectedTransactionsActions({
});
}

const canAllSelectedTransactionsBeRemoved = Object.values(selectedTransactions).every((transaction) => {
const canAllSelectedTransactionsBeRemoved = selectedTransactionsList.every((transaction) => {
const canRemoveTransaction = canDeleteCardTransactionByLiabilityType(transaction);
const action = getIOUActionForTransactionID(reportActions, transaction.transactionID);
const isActionDeleted = isDeletedAction(action);
Expand All @@ -338,7 +381,7 @@ function useSelectedTransactionsActions({
}, [
selectedTransactionIDs,
report,
selectedTransactions,
selectedTransactionsList,
translate,
isReportArchived,
policy,
Expand All @@ -356,6 +399,7 @@ function useSelectedTransactionsActions({
lastVisitedPath,
session?.accountID,
showDeleteModal,
hasTransactionsFromMultipleOwners,
]);

return {
Expand Down
23 changes: 21 additions & 2 deletions src/pages/Search/SearchPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ import Navigation from '@libs/Navigation/Navigation';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
import type {SearchFullscreenNavigatorParamList} from '@libs/Navigation/types';
import {getActiveAdminWorkspaces, hasVBBA, isPaidGroupPolicy} from '@libs/PolicyUtils';
import {generateReportID, getPolicyExpenseChat, isExpenseReport as isExpenseReportUtil, isIOUReport as isIOUReportUtil} from '@libs/ReportUtils';
import {generateReportID, getPolicyExpenseChat, getReportOrDraftReport, isExpenseReport as isExpenseReportUtil, isIOUReport as isIOUReportUtil} from '@libs/ReportUtils';
import {buildCannedSearchQuery, buildSearchQueryJSON} from '@libs/SearchQueryUtils';
import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils';
import type {ReceiptFile} from '@pages/iou/request/step/IOURequestStepScan/types';
Expand Down Expand Up @@ -452,9 +452,28 @@ function SearchPage({route}: SearchPageProps) {
});
}

const ownerAccountIDs = new Set<number>();
let hasUnknownOwner = false;
for (const id of selectedTransactionsKeys) {
const transactionEntry = selectedTransactions[id];
if (!transactionEntry) {
continue;
}
const ownerAccountID = transactionEntry.ownerAccountID ?? getReportOrDraftReport(transactionEntry.reportID)?.ownerAccountID;
if (typeof ownerAccountID === 'number') {
ownerAccountIDs.add(ownerAccountID);
if (ownerAccountIDs.size > 1) {
break;
}
} else {
hasUnknownOwner = true;
}
}
const hasMultipleOwners = ownerAccountIDs.size > 1 || (hasUnknownOwner && (ownerAccountIDs.size > 0 || selectedTransactionsKeys.length > 1));

const canAllTransactionsBeMoved = selectedTransactionsKeys.every((id) => selectedTransactions[id].canChangeReport);

if (canAllTransactionsBeMoved) {
if (canAllTransactionsBeMoved && !hasMultipleOwners) {
options.push({
text: translate('iou.moveExpenses', {count: selectedTransactionsKeys.length}),
icon: Expensicons.DocumentMerge,
Expand Down
28 changes: 27 additions & 1 deletion src/pages/Search/SearchTransactionsChangeReport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {createNewReport} from '@libs/actions/Report';
import {changeTransactionsReport} from '@libs/actions/Transaction';
import Navigation from '@libs/Navigation/Navigation';
import Permissions from '@libs/Permissions';
import {hasViolations as hasViolationsReportUtils} from '@libs/ReportUtils';
import {getReportOrDraftReport, hasViolations as hasViolationsReportUtils} from '@libs/ReportUtils';
import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils';
import IOURequestEditReportCommon from '@pages/iou/request/step/IOURequestEditReportCommon';
import CONST from '@src/CONST';
Expand Down Expand Up @@ -42,6 +42,30 @@ function SearchTransactionsChangeReport() {
Object.values(selectedTransactions).every((transaction) => transaction.reportID === firstTransactionReportID) && firstTransactionReportID !== CONST.REPORT.UNREPORTED_REPORT_ID
? firstTransactionReportID
: undefined;
const areAllTransactionsUnreported =
selectedTransactionsKeys.length > 0 && selectedTransactionsKeys.every((transactionKey) => selectedTransactions[transactionKey]?.reportID === CONST.REPORT.UNREPORTED_REPORT_ID);
const targetOwnerAccountID = useMemo(() => {
if (selectedTransactionsKeys.length === 0) {
return undefined;
}

// Prefer owner metadata attached to each selection (handles unreported expenses)
const ownerFromSelection = selectedTransactionsKeys.map((transactionKey) => selectedTransactions[transactionKey]?.ownerAccountID).find((ownerID) => typeof ownerID === 'number');
if (ownerFromSelection !== undefined) {
return ownerFromSelection;
}

const reportIDWithOwner = selectedTransactionsKeys
.map((transactionKey) => selectedTransactions[transactionKey]?.reportID)
.find((reportID) => reportID && reportID !== CONST.REPORT.UNREPORTED_REPORT_ID);

if (!reportIDWithOwner) {
return undefined;
}

const report = getReportOrDraftReport(reportIDWithOwner);
return report?.ownerAccountID;
}, [selectedTransactions, selectedTransactionsKeys]);

const createReport = () => {
if (shouldSelectPolicy) {
Expand Down Expand Up @@ -109,6 +133,8 @@ function SearchTransactionsChangeReport() {
removeFromReport={removeFromReport}
createReport={createReport}
isEditing
isUnreported={areAllTransactionsUnreported}
targetOwnerAccountID={targetOwnerAccountID}
/>
);
}
Expand Down
27 changes: 20 additions & 7 deletions src/pages/iou/request/step/IOURequestEditReportCommon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type Props = {
transactionIDs?: string[];
selectedReportID?: string;
selectedPolicyID?: string;
targetOwnerAccountID?: number;
selectReport: (item: TransactionGroupListItem) => void;
removeFromReport?: () => void;
isEditing?: boolean;
Expand All @@ -53,6 +54,7 @@ function IOURequestEditReportCommon({
selectReport,
selectedReportID,
selectedPolicyID,
targetOwnerAccountID,
removeFromReport,
isEditing = false,
isUnreported,
Expand All @@ -66,7 +68,17 @@ function IOURequestEditReportCommon({
const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true});
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
const [selectedReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${selectedReportID}`, {canBeMissing: true});
const reportOwnerAccountID = useMemo(() => selectedReport?.ownerAccountID ?? currentUserPersonalDetails.accountID, [selectedReport, currentUserPersonalDetails.accountID]);
const resolvedReportOwnerAccountID = useMemo(() => {
if (targetOwnerAccountID !== undefined) {
return targetOwnerAccountID;
}

if (selectedReport?.ownerAccountID !== undefined) {
return selectedReport.ownerAccountID;
}

return currentUserPersonalDetails.accountID;
}, [targetOwnerAccountID, selectedReport, currentUserPersonalDetails.accountID]);
const reportPolicy = usePolicy(selectedReport?.policyID);
const {policyForMovingExpenses} = usePolicyForMovingExpenses();

Expand All @@ -75,7 +87,8 @@ function IOURequestEditReportCommon({
const [allPoliciesID] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: policiesSelector, canBeMissing: false});

const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState('');
const isOwner = selectedReport ? selectedReport.ownerAccountID === currentUserPersonalDetails.accountID : false;
const isSelectedReportUnreported = useMemo(() => !!(isUnreported ?? selectedReportID === CONST.REPORT.UNREPORTED_REPORT_ID), [isUnreported, selectedReportID]);
const isOwner = selectedReport ? selectedReport.ownerAccountID === currentUserPersonalDetails.accountID : isSelectedReportUnreported;
const isReportIOU = selectedReport ? isIOUReport(selectedReport) : false;

const reportTransactions = useReportTransactions(selectedReportID);
Expand All @@ -89,7 +102,7 @@ function IOURequestEditReportCommon({
.some((transaction) => transaction?.comment?.liabilityType === CONST.TRANSACTION.LIABILITY_TYPE.RESTRICT);
}, [transactionIDs, selectedReport, reportTransactions]);

const shouldShowRemoveFromReport = isEditing && isOwner && !isReportIOU && !isUnreported && !isCardTransaction;
const shouldShowRemoveFromReport = isEditing && isOwner && !isReportIOU && !isSelectedReportUnreported && !isCardTransaction;

const expenseReports = useMemo(() => {
// Early return if no reports are available to prevent useless loop
Expand All @@ -106,7 +119,7 @@ function IOURequestEditReportCommon({
}
const reports = getOutstandingReportsForUser(
policyID,
reportOwnerAccountID,
resolvedReportOwnerAccountID,
outstandingReportsByPolicyID?.[policyID ?? CONST.DEFAULT_NUMBER_ID] ?? {},
reportNameValuePairs,
isEditing,
Expand All @@ -117,12 +130,12 @@ function IOURequestEditReportCommon({
}
return getOutstandingReportsForUser(
selectedPolicyID,
reportOwnerAccountID,
resolvedReportOwnerAccountID,
outstandingReportsByPolicyID?.[selectedPolicyID ?? CONST.DEFAULT_NUMBER_ID] ?? {},
reportNameValuePairs,
isEditing,
);
}, [outstandingReportsByPolicyID, reportOwnerAccountID, allPoliciesID, reportNameValuePairs, selectedReport, selectedPolicyID, isEditing]);
}, [outstandingReportsByPolicyID, resolvedReportOwnerAccountID, allPoliciesID, reportNameValuePairs, selectedReport, selectedPolicyID, isEditing]);

const reportOptions: TransactionGroupListItem[] = useMemo(() => {
if (!outstandingReportsByPolicyID || isEmptyObject(outstandingReportsByPolicyID)) {
Expand Down Expand Up @@ -194,7 +207,7 @@ function IOURequestEditReportCommon({
icon={Expensicons.Document}
/>
);
}, [createReport, isEditing, isOwner, translate, policyForMovingExpenses?.name]);
}, [createReport, isEditing, isOwner, translate, policyForMovingExpenses?.name, isSelectedReportUnreported]);

// eslint-disable-next-line rulesdir/no-negated-variables
const shouldShowNotFoundPage = useMemo(() => {
Expand Down
Loading