Skip to content

Commit e9cdd22

Browse files
authored
Merge pull request #62270 from software-mansion-labs/jnowakow/add-bulk-move-transactions-on-search
Add move option to search selected menu
2 parents b35e3cd + 5c37f43 commit e9cdd22

File tree

13 files changed

+126
-17
lines changed

13 files changed

+126
-17
lines changed

src/CONST.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6499,6 +6499,7 @@ const CONST = {
64996499
HOLD: 'hold',
65006500
UNHOLD: 'unhold',
65016501
DELETE: 'delete',
6502+
CHANGE_REPORT: 'changeReport',
65026503
},
65036504
TRANSACTION_TYPE: {
65046505
CASH: 'cash',

src/ROUTES.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ const ROUTES = {
124124
},
125125
},
126126
TRANSACTION_HOLD_REASON_RHP: 'search/hold',
127+
MOVE_TRANSACTIONS_SEARCH_RHP: 'search/move-transactions',
127128

128129
// This is a utility route used to go to the user's concierge chat, or the sign-in page if the user's not authenticated
129130
CONCIERGE: 'concierge',

src/SCREENS.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ const SCREENS = {
7070
SAVED_SEARCH_RENAME_RHP: 'Search_Saved_Search_Rename_RHP',
7171
ADVANCED_FILTERS_IN_RHP: 'Search_Advanced_Filters_In_RHP',
7272
TRANSACTION_HOLD_REASON_RHP: 'Search_Transaction_Hold_Reason_RHP',
73+
TRANSACTIONS_CHANGE_REPORT_SEARCH_RHP: 'Search_Transactions_Change_Report_RHP',
7374
},
7475
SETTINGS: {
7576
ROOT: 'Settings_Root',

src/components/Search/index.tsx

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ import {canUseTouchScreen} from '@libs/DeviceCapabilities';
2222
import Log from '@libs/Log';
2323
import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute';
2424
import type {PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types';
25-
import {generateReportID} from '@libs/ReportUtils';
25+
import {getIOUActionForTransactionID} from '@libs/ReportActionsUtils';
26+
import {canEditFieldOfMoneyRequest, generateReportID} from '@libs/ReportUtils';
2627
import {buildSearchQueryString} from '@libs/SearchQueryUtils';
2728
import {
2829
getListItem,
@@ -44,6 +45,7 @@ import EmptySearchView from '@pages/Search/EmptySearchView';
4445
import CONST from '@src/CONST';
4546
import ONYXKEYS from '@src/ONYXKEYS';
4647
import ROUTES from '@src/ROUTES';
48+
import type {ReportAction} from '@src/types/onyx';
4749
import type SearchResults from '@src/types/onyx/SearchResults';
4850
import {useSearchContext} from './SearchContext';
4951
import SearchList from './SearchList';
@@ -59,7 +61,7 @@ type SearchProps = {
5961
handleSearch: (value: SearchParams) => void;
6062
};
6163

62-
function mapTransactionItemToSelectedEntry(item: TransactionListItemType): [string, SelectedTransactionInfo] {
64+
function mapTransactionItemToSelectedEntry(item: TransactionListItemType, reportActions: ReportAction[]): [string, SelectedTransactionInfo] {
6365
return [
6466
item.keyForList,
6567
{
@@ -68,6 +70,7 @@ function mapTransactionItemToSelectedEntry(item: TransactionListItemType): [stri
6870
canHold: item.canHold,
6971
isHeld: isOnHold(item),
7072
canUnhold: item.canUnhold,
73+
canChangeReport: canEditFieldOfMoneyRequest(getIOUActionForTransactionID(reportActions, item.transactionID), CONST.EDIT_REQUEST_FIELD.REPORT),
7174
action: item.action,
7275
reportID: item.reportID,
7376
policyID: item.policyID,
@@ -105,7 +108,7 @@ function mapToItemWithSelectionInfo(item: SearchListItem, selectedTransactions:
105108
};
106109
}
107110

108-
function prepareTransactionsList(item: TransactionListItemType, selectedTransactions: SelectedTransactions) {
111+
function prepareTransactionsList(item: TransactionListItemType, selectedTransactions: SelectedTransactions, reportActions: ReportAction[]) {
109112
if (selectedTransactions[item.keyForList]?.isSelected) {
110113
const {[item.keyForList]: omittedTransaction, ...transactions} = selectedTransactions;
111114

@@ -120,6 +123,7 @@ function prepareTransactionsList(item: TransactionListItemType, selectedTransact
120123
canHold: item.canHold,
121124
isHeld: isOnHold(item),
122125
canUnhold: item.canUnhold,
126+
canChangeReport: canEditFieldOfMoneyRequest(getIOUActionForTransactionID(reportActions, item.transactionID), CONST.EDIT_REQUEST_FIELD.REPORT),
123127
action: item.action,
124128
reportID: item.reportID,
125129
policyID: item.policyID,
@@ -158,6 +162,13 @@ function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onS
158162
const previousTransactions = usePrevious(transactions);
159163
const [reportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS, {canBeMissing: true});
160164
const previousReportActions = usePrevious(reportActions);
165+
const reportActionsArray = useMemo(
166+
() =>
167+
Object.values(reportActions ?? {})
168+
.filter((reportAction) => !!reportAction)
169+
.flatMap((fillteredReportActions) => Object.values(fillteredReportActions ?? {})),
170+
[reportActions],
171+
);
161172
const {translate} = useLocalize();
162173
const searchListRef = useRef<SelectionListHandle | null>(null);
163174

@@ -256,6 +267,7 @@ function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onS
256267
canHold: transaction.canHold,
257268
isHeld: isOnHold(transaction),
258269
canUnhold: transaction.canUnhold,
270+
canChangeReport: canEditFieldOfMoneyRequest(getIOUActionForTransactionID(reportActionsArray, transaction.transactionID), CONST.EDIT_REQUEST_FIELD.REPORT),
259271
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
260272
isSelected: isExportMode || selectedTransactions[transaction.transactionID].isSelected,
261273
canDelete: transaction.canDelete,
@@ -278,6 +290,7 @@ function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onS
278290
canHold: transaction.canHold,
279291
isHeld: isOnHold(transaction),
280292
canUnhold: transaction.canUnhold,
293+
canChangeReport: canEditFieldOfMoneyRequest(getIOUActionForTransactionID(reportActionsArray, transaction.transactionID), CONST.EDIT_REQUEST_FIELD.REPORT),
281294
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
282295
isSelected: isExportMode || selectedTransactions[transaction.transactionID].isSelected,
283296
canDelete: transaction.canDelete,
@@ -344,7 +357,7 @@ function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onS
344357
return;
345358
}
346359

347-
setSelectedTransactions(prepareTransactionsList(item, selectedTransactions), data);
360+
setSelectedTransactions(prepareTransactionsList(item, selectedTransactions, reportActionsArray), data);
348361
return;
349362
}
350363

@@ -362,12 +375,12 @@ function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onS
362375
setSelectedTransactions(
363376
{
364377
...selectedTransactions,
365-
...Object.fromEntries(item.transactions.map(mapTransactionItemToSelectedEntry)),
378+
...Object.fromEntries(item.transactions.map((transactionItem) => mapTransactionItemToSelectedEntry(transactionItem, reportActionsArray))),
366379
},
367380
data,
368381
);
369382
},
370-
[data, selectedTransactions, setSelectedTransactions],
383+
[data, reportActionsArray, selectedTransactions, setSelectedTransactions],
371384
);
372385

373386
const openReport = useCallback(
@@ -523,12 +536,20 @@ function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onS
523536
}
524537

525538
if (areItemsOfReportType) {
526-
setSelectedTransactions(Object.fromEntries((data as ReportListItemType[]).flatMap((item) => item.transactions.map(mapTransactionItemToSelectedEntry))), data);
539+
setSelectedTransactions(
540+
Object.fromEntries(
541+
(data as ReportListItemType[]).flatMap((item) => item.transactions.map((transactionItem) => mapTransactionItemToSelectedEntry(transactionItem, reportActionsArray))),
542+
),
543+
data,
544+
);
527545

528546
return;
529547
}
530548

531-
setSelectedTransactions(Object.fromEntries((data as TransactionListItemType[]).map(mapTransactionItemToSelectedEntry)), data);
549+
setSelectedTransactions(
550+
Object.fromEntries((data as TransactionListItemType[]).map((transactionItem) => mapTransactionItemToSelectedEntry(transactionItem, reportActionsArray))),
551+
data,
552+
);
532553
};
533554

534555
const onSortPress = (column: SearchColumnType, order: SortOrder) => {

src/components/Search/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ type SelectedTransactionInfo = {
1414
/** If the transaction can be put on hold */
1515
canHold: boolean;
1616

17+
/** If the transaction can be moved to other report */
18+
canChangeReport: boolean;
19+
1720
/** Whether the transaction is currently held */
1821
isHeld: boolean;
1922

src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,7 @@ const SearchReportModalStackNavigator = createModalStackNavigator<SearchReportPa
700700
[SCREENS.SEARCH.REPORT_RHP]: () => require<ReactComponentModule>('../../../../pages/home/ReportScreen').default,
701701
[SCREENS.SEARCH.MONEY_REQUEST_REPORT_HOLD_TRANSACTIONS]: () => require<ReactComponentModule>('../../../../pages/Search/SearchMoneyRequestReportHoldReasonPage').default,
702702
[SCREENS.SEARCH.TRANSACTION_HOLD_REASON_RHP]: () => require<ReactComponentModule>('../../../../pages/Search/SearchHoldReasonPage').default,
703+
[SCREENS.SEARCH.TRANSACTIONS_CHANGE_REPORT_SEARCH_RHP]: () => require<ReactComponentModule>('../../../../pages/Search/SearchTransactionsChangeReport').default,
703704
},
704705
() => ({
705706
animation: Animations.NONE,

src/libs/Navigation/linkingConfig/RELATIONS/SEARCH_TO_RHP.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const SEARCH_TO_RHP: Partial<Record<keyof SearchFullscreenNavigatorParamList, st
66
[SCREENS.SEARCH.ROOT]: [
77
SCREENS.SEARCH.REPORT_RHP,
88
SCREENS.SEARCH.TRANSACTION_HOLD_REASON_RHP,
9+
SCREENS.SEARCH.TRANSACTIONS_CHANGE_REPORT_SEARCH_RHP,
910
SCREENS.SEARCH.ADVANCED_FILTERS_RHP,
1011
SCREENS.SEARCH.ADVANCED_FILTERS_CURRENCY_RHP,
1112
SCREENS.SEARCH.ADVANCED_FILTERS_DATE_RHP,

src/libs/Navigation/linkingConfig/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1458,6 +1458,7 @@ const config: LinkingOptions<RootNavigatorParamList>['config'] = {
14581458
[SCREENS.SEARCH.REPORT_RHP]: ROUTES.SEARCH_REPORT.route,
14591459
[SCREENS.SEARCH.MONEY_REQUEST_REPORT_HOLD_TRANSACTIONS]: ROUTES.SEARCH_MONEY_REQUEST_REPORT_HOLD_TRANSACTIONS.route,
14601460
[SCREENS.SEARCH.TRANSACTION_HOLD_REASON_RHP]: ROUTES.TRANSACTION_HOLD_REASON_RHP,
1461+
[SCREENS.SEARCH.TRANSACTIONS_CHANGE_REPORT_SEARCH_RHP]: ROUTES.MOVE_TRANSACTIONS_SEARCH_RHP,
14611462
},
14621463
},
14631464
[SCREENS.RIGHT_MODAL.SEARCH_ADVANCED_FILTERS]: {

src/pages/Search/SearchPage.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,17 @@ function SearchPage({route}: SearchPageProps) {
324324
});
325325
}
326326

327+
const canAllTransactionsBeMoved = selectedTransactionsKeys.every((id) => selectedTransactions[id].canChangeReport);
328+
329+
if (canAllTransactionsBeMoved) {
330+
options.push({
331+
text: translate('iou.moveExpenses', {count: selectedTransactionsKeys.length}),
332+
icon: Expensicons.DocumentMerge,
333+
value: CONST.SEARCH.BULK_ACTION_TYPES.CHANGE_REPORT,
334+
onSelected: () => Navigation.navigate(ROUTES.MOVE_TRANSACTIONS_SEARCH_RHP),
335+
});
336+
}
337+
327338
const shouldShowDeleteOption = !isOffline && selectedTransactionsKeys.every((id) => selectedTransactions[id].canDelete);
328339

329340
if (shouldShowDeleteOption) {
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import React, {useMemo} from 'react';
2+
import {useOnyx} from 'react-native-onyx';
3+
import {useSearchContext} from '@components/Search/SearchContext';
4+
import type {ListItem} from '@components/SelectionList/types';
5+
import {changeTransactionsReport} from '@libs/actions/Transaction';
6+
import Navigation from '@libs/Navigation/Navigation';
7+
import IOURequestEditReportCommon from '@pages/iou/request/step/IOURequestEditReportCommon';
8+
import ONYXKEYS from '@src/ONYXKEYS';
9+
import type {Report} from '@src/types/onyx';
10+
11+
type ReportListItem = ListItem & {
12+
/** reportID of the report */
13+
value: string;
14+
};
15+
16+
function SearchTransactionsChangeReport() {
17+
const {selectedTransactions, clearSelectedTransactions} = useSearchContext();
18+
const selectedTransactionsKeys = useMemo(() => Object.keys(selectedTransactions), [selectedTransactions]);
19+
20+
const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: false});
21+
const transactionsReports = useMemo(() => {
22+
const reports = Object.values(selectedTransactions).reduce((acc, transaction) => {
23+
const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transaction.reportID}`];
24+
if (report) {
25+
acc.add(report);
26+
}
27+
return acc;
28+
}, new Set<Report>());
29+
return [...reports];
30+
}, [allReports, selectedTransactions]);
31+
32+
const selectReport = (item: ReportListItem) => {
33+
if (selectedTransactionsKeys.length === 0) {
34+
return;
35+
}
36+
37+
changeTransactionsReport(selectedTransactionsKeys, item.value);
38+
clearSelectedTransactions();
39+
40+
Navigation.goBack();
41+
};
42+
43+
return (
44+
<IOURequestEditReportCommon
45+
backTo={undefined}
46+
transactionsReports={transactionsReports}
47+
selectReport={selectReport}
48+
/>
49+
);
50+
}
51+
52+
SearchTransactionsChangeReport.displayName = 'SearchTransactionsChangeReport';
53+
54+
export default SearchTransactionsChangeReport;

src/pages/iou/request/step/IOURequestEditReport.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ function IOURequestEditReport({route}: IOURequestEditReportProps) {
3838
return (
3939
<IOURequestEditReportCommon
4040
backTo={backTo}
41-
transactionReport={transactionReport}
41+
transactionsReports={transactionReport ? [transactionReport] : []}
4242
selectReport={selectReport}
4343
/>
4444
);

src/pages/iou/request/step/IOURequestEditReportCommon.tsx

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,23 +38,37 @@ const reportSelector = (report: OnyxEntry<Report>): OnyxEntry<Report> =>
3838

3939
type Props = {
4040
backTo: Route | undefined;
41-
transactionReport: OnyxEntry<Report>;
41+
transactionsReports: Report[];
4242
selectReport: (item: ReportListItem) => void;
4343
};
4444

45-
function IOURequestEditReportCommon({backTo, transactionReport, selectReport}: Props) {
45+
function IOURequestEditReportCommon({backTo, transactionsReports, selectReport}: Props) {
4646
const {translate} = useLocalize();
4747
const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: (reports) => mapOnyxCollectionItems(reports, reportSelector), canBeMissing: true});
48+
const [allPoliciesID] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: (policies) => mapOnyxCollectionItems(policies, (policy) => policy?.id), canBeMissing: false});
49+
4850
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
4951
const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState('');
5052

51-
const expenseReports = getOutstandingReportsForUser(transactionReport?.policyID, transactionReport?.ownerAccountID ?? currentUserPersonalDetails.accountID, allReports ?? {});
53+
const expenseReports = useMemo(
54+
() =>
55+
Object.values(allPoliciesID ?? {}).flatMap((policyID) => {
56+
if (!policyID) {
57+
return [];
58+
}
59+
const reports = getOutstandingReportsForUser(policyID, currentUserPersonalDetails.accountID, allReports ?? {});
60+
return reports;
61+
}),
62+
[allPoliciesID, allReports, currentUserPersonalDetails.accountID],
63+
);
64+
5265
const reportOptions: ReportListItem[] = useMemo(() => {
5366
if (!allReports) {
5467
return [];
5568
}
5669

57-
const isTransactionReportCorrect = expenseReports.some((report) => report?.reportID === transactionReport?.reportID);
70+
const onlyReport = transactionsReports.length === 1 ? transactionsReports.at(0) : undefined;
71+
5872
return expenseReports
5973
.sort((a, b) => a?.reportName?.localeCompare(b?.reportName?.toLowerCase() ?? '') ?? 0)
6074
.filter((report) => !debouncedSearchValue || report?.reportName?.toLowerCase().includes(debouncedSearchValue.toLowerCase()))
@@ -63,9 +77,9 @@ function IOURequestEditReportCommon({backTo, transactionReport, selectReport}: P
6377
text: report.reportName,
6478
value: report.reportID,
6579
keyForList: report.reportID,
66-
isSelected: isTransactionReportCorrect ? report.reportID === transactionReport?.reportID : expenseReports.at(0)?.reportID === report.reportID,
80+
isSelected: onlyReport && report.reportID === onlyReport?.reportID,
6781
}));
68-
}, [allReports, debouncedSearchValue, expenseReports, transactionReport?.reportID]);
82+
}, [allReports, debouncedSearchValue, expenseReports, transactionsReports]);
6983

7084
const navigateBack = () => {
7185
Navigation.goBack(backTo);
@@ -90,7 +104,7 @@ function IOURequestEditReportCommon({backTo, transactionReport, selectReport}: P
90104
textInputLabel={expenseReports.length >= CONST.STANDARD_LIST_ITEM_LIMIT ? translate('common.search') : undefined}
91105
shouldSingleExecuteRowSelect
92106
headerMessage={headerMessage}
93-
initiallyFocusedOptionKey={transactionReport?.reportID}
107+
initiallyFocusedOptionKey={transactionsReports.length === 1 ? transactionsReports.at(0)?.reportID : undefined}
94108
ListItem={UserListItem}
95109
/>
96110
</StepScreenWrapper>

src/pages/iou/request/step/IOURequestStepReport.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) {
4242
return (
4343
<IOURequestEditReportCommon
4444
backTo={backTo}
45-
transactionReport={transactionReport}
45+
transactionsReports={transactionReport ? [transactionReport] : []}
4646
selectReport={selectReport}
4747
/>
4848
);

0 commit comments

Comments
 (0)