Skip to content

Commit 4621b49

Browse files
authored
Merge pull request #84756 from callstack-internal/feauture/bulk-edit-expenses-3
Reapply "Reapply "Feat: bulk edit multiple""
2 parents 7079714 + 8645978 commit 4621b49

50 files changed

Lines changed: 3951 additions & 125 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/CONST/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -848,6 +848,7 @@ const CONST = {
848848
PERSONAL_CARD_IMPORT: 'personalCardImport',
849849
SUGGESTED_FOLLOWUPS: 'suggestedFollowups',
850850
FREEZE_CARD: 'freezeCard',
851+
BULK_EDIT: 'bulkEdit',
851852
NEW_MANUAL_EXPENSE_FLOW: 'newManualExpenseFlow',
852853
},
853854
BUTTON_STATES: {
@@ -3245,6 +3246,8 @@ const CONST = {
32453246
QUANTITY_MAX_LENGTH: 12,
32463247
// This is the transactionID used when going through the create expense flow so that it mimics a real transaction (like the edit flow)
32473248
OPTIMISTIC_TRANSACTION_ID: '1',
3249+
// This is the transactionID used when bulk editing multiple expenses
3250+
OPTIMISTIC_BULK_EDIT_TRANSACTION_ID: 'optimisticBulkEditTransactionID',
32483251
// This is the transactionID used when going through the distance split expense flow so that it mimics a draft transaction
32493252
OPTIMISTIC_DISTANCE_SPLIT_TRANSACTION_ID: '2',
32503253
// Note: These payment types are used when building IOU reportAction message values in the server and should
@@ -4557,6 +4560,7 @@ const CONST = {
45574560
TAX_RATE: 'taxRate',
45584561
TAX_AMOUNT: 'taxAmount',
45594562
REIMBURSABLE: 'reimbursable',
4563+
BILLABLE: 'billable',
45604564
REPORT: 'report',
45614565
},
45624566
FOOTER: {
@@ -7358,6 +7362,7 @@ const CONST = {
73587362
TAG: 'tag',
73597363
},
73607364
BULK_ACTION_TYPES: {
7365+
EDIT: 'edit',
73617366
EXPORT: 'export',
73627367
APPROVE: 'approve',
73637368
PAY: 'pay',

src/ONYXKEYS.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1047,6 +1047,12 @@ const ONYXKEYS = {
10471047
WORKSPACE_PER_DIEM_FORM_DRAFT: 'workspacePerDiemFormDraft',
10481048
ENABLE_GLOBAL_REIMBURSEMENTS: 'enableGlobalReimbursementsForm',
10491049
ENABLE_GLOBAL_REIMBURSEMENTS_DRAFT: 'enableGlobalReimbursementsFormDraft',
1050+
SEARCH_EDIT_MULTIPLE_DESCRIPTION_FORM: 'searchEditMultipleDescriptionForm',
1051+
SEARCH_EDIT_MULTIPLE_DESCRIPTION_FORM_DRAFT: 'searchEditMultipleDescriptionFormDraft',
1052+
SEARCH_EDIT_MULTIPLE_MERCHANT_FORM: 'searchEditMultipleMerchantForm',
1053+
SEARCH_EDIT_MULTIPLE_MERCHANT_FORM_DRAFT: 'searchEditMultipleMerchantFormDraft',
1054+
SEARCH_EDIT_MULTIPLE_DATE_FORM: 'searchEditMultipleDateForm',
1055+
SEARCH_EDIT_MULTIPLE_DATE_FORM_DRAFT: 'searchEditMultipleDateFormDraft',
10501056
CREATE_DOMAIN_FORM: 'createDomainForm',
10511057
CREATE_DOMAIN_FORM_DRAFT: 'createDomainFormDraft',
10521058
SPLIT_EXPENSE_EDIT_DATES: 'splitExpenseEditDates',
@@ -1189,6 +1195,9 @@ type OnyxFormValuesMapping = {
11891195
[ONYXKEYS.FORMS.INTERNATIONAL_BANK_ACCOUNT_FORM]: FormTypes.InternationalBankAccountForm;
11901196
[ONYXKEYS.FORMS.WORKSPACE_PER_DIEM_FORM]: FormTypes.WorkspacePerDiemForm;
11911197
[ONYXKEYS.FORMS.ENABLE_GLOBAL_REIMBURSEMENTS]: FormTypes.EnableGlobalReimbursementsForm;
1198+
[ONYXKEYS.FORMS.SEARCH_EDIT_MULTIPLE_DESCRIPTION_FORM]: FormTypes.SearchEditMultipleDescriptionForm;
1199+
[ONYXKEYS.FORMS.SEARCH_EDIT_MULTIPLE_MERCHANT_FORM]: FormTypes.SearchEditMultipleMerchantForm;
1200+
[ONYXKEYS.FORMS.SEARCH_EDIT_MULTIPLE_DATE_FORM]: FormTypes.SearchEditMultipleDateForm;
11921201
[ONYXKEYS.FORMS.CREATE_DOMAIN_FORM]: FormTypes.CreateDomainForm;
11931202
[ONYXKEYS.FORMS.SPLIT_EXPENSE_EDIT_DATES]: FormTypes.SplitExpenseEditDateForm;
11941203
[ONYXKEYS.FORMS.EXPENSE_RULE_FORM]: FormTypes.ExpenseRuleForm;

src/ROUTES.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,19 @@ const ROUTES = {
247247
},
248248
},
249249
SEARCH_REJECT_REASON_RHP: 'search/reject',
250+
SEARCH_EDIT_MULTIPLE_TRANSACTIONS_RHP: 'search/edit-multiple-transactions',
251+
SEARCH_EDIT_MULTIPLE_AMOUNT_RHP: 'search/edit-multiple/amount',
252+
SEARCH_EDIT_MULTIPLE_DESCRIPTION_RHP: 'search/edit-multiple/description',
253+
SEARCH_EDIT_MULTIPLE_MERCHANT_RHP: 'search/edit-multiple/merchant',
254+
SEARCH_EDIT_MULTIPLE_DATE_RHP: 'search/edit-multiple/date',
255+
SEARCH_EDIT_MULTIPLE_CATEGORY_RHP: 'search/edit-multiple/category',
256+
SEARCH_EDIT_MULTIPLE_TAG_RHP: {
257+
route: 'search/edit-multiple/tag/:tagListIndex',
258+
getRoute: (tagListIndex = 0) => `search/edit-multiple/tag/${tagListIndex}` as const,
259+
},
260+
SEARCH_EDIT_MULTIPLE_BILLABLE_RHP: 'search/edit-multiple/billable',
261+
SEARCH_EDIT_MULTIPLE_REIMBURSABLE_RHP: 'search/edit-multiple/reimbursable',
262+
SEARCH_EDIT_MULTIPLE_TAX_RHP: 'search/edit-multiple/tax',
250263
MOVE_TRANSACTIONS_SEARCH_RHP: {
251264
route: 'search/move-transactions/search/:backTo?',
252265
getRoute: (backTo?: string) => {

src/SCREENS.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,16 @@ const SCREENS = {
9898
TRANSACTION_HOLD_REASON_RHP: 'Search_Transaction_Hold_Reason_RHP',
9999
TRANSACTION_HOLD_REASON_SEARCH: 'Search_Transaction_Hold_Reason_Search',
100100
SEARCH_REJECT_REASON_RHP: 'Search_Reject_Reason_RHP',
101+
EDIT_MULTIPLE_TRANSACTIONS_RHP: 'Search_Edit_Multiple_Transactions_RHP',
102+
EDIT_MULTIPLE_AMOUNT_RHP: 'Search_Edit_Multiple_Amount_RHP',
103+
EDIT_MULTIPLE_DESCRIPTION_RHP: 'Search_Edit_Multiple_Description_RHP',
104+
EDIT_MULTIPLE_MERCHANT_RHP: 'Search_Edit_Multiple_Merchant_RHP',
105+
EDIT_MULTIPLE_DATE_RHP: 'Search_Edit_Multiple_Date_RHP',
106+
EDIT_MULTIPLE_CATEGORY_RHP: 'Search_Edit_Multiple_Category_RHP',
107+
EDIT_MULTIPLE_TAG_RHP: 'Search_Edit_Multiple_Tag_RHP',
108+
EDIT_MULTIPLE_BILLABLE_RHP: 'Search_Edit_Multiple_Billable_RHP',
109+
EDIT_MULTIPLE_REIMBURSABLE_RHP: 'Search_Edit_Multiple_Reimbursable_RHP',
110+
EDIT_MULTIPLE_TAX_RHP: 'Search_Edit_Multiple_Tax_RHP',
101111
TRANSACTIONS_CHANGE_REPORT_SEARCH_RHP: 'Search_Transactions_Change_Report_Search',
102112
},
103113
SETTINGS: {

src/components/ReportActionItem/MoneyRequestView.tsx

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ import {
7777
isTrackExpenseReportNew,
7878
shouldEnableNegative,
7979
} from '@libs/ReportUtils';
80-
import {hasEnabledTags} from '@libs/TagsOptionsListUtils';
80+
import {hasEnabledTags, shouldShowDependentTagList} from '@libs/TagsOptionsListUtils';
8181
import {
8282
getBillable,
8383
getCurrency,
@@ -87,7 +87,6 @@ import {
8787
getOriginalAmountForDisplay,
8888
getOriginalTransactionWithSplitInfo,
8989
getReimbursable,
90-
getTagArrayFromName,
9190
getTagForDisplay,
9291
getTaxName,
9392
hasMissingSmartscanFields,
@@ -739,30 +738,7 @@ function MoneyRequestView({
739738
const tagForDisplay = getTagForDisplay(updatedTransaction ?? transaction, index);
740739
let shouldShow = false;
741740
if (hasDependentTags) {
742-
if (index === 0) {
743-
shouldShow = true;
744-
} else {
745-
const prevTagValue = getTagForDisplay(transaction, index - 1);
746-
if (!prevTagValue) {
747-
shouldShow = false;
748-
} else {
749-
const parentTag = getTagArrayFromName(transactionTag ?? '')
750-
.slice(0, index)
751-
.join(':');
752-
753-
const availableTags = Object.values(tags).filter((policyTag) => {
754-
const filterRegex = policyTag.rules?.parentTagsFilter;
755-
if (!filterRegex) {
756-
return true;
757-
}
758-
759-
const regex = new RegExp(filterRegex);
760-
return regex.test(parentTag ?? '');
761-
});
762-
763-
shouldShow = availableTags.some((tag) => tag.enabled);
764-
}
765-
}
741+
shouldShow = shouldShowDependentTagList(index, transactionTag, tags);
766742
} else {
767743
shouldShow = !!tagForDisplay || (canEdit && hasEnabledOptions(tags));
768744
}

src/components/TaxPicker.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,25 @@ type TaxPickerProps = {
4040
* If enabled, the content will have a bottom padding equal to account for the safe bottom area inset.
4141
*/
4242
addBottomSafeAreaPadding?: boolean;
43+
44+
/**
45+
* If enabled, allows deselecting the currently selected tax rate by tapping it again.
46+
* When disabled (default), tapping the selected tax rate will dismiss the picker without calling onSubmit.
47+
*/
48+
allowDeselect?: boolean;
4349
};
4450

45-
function TaxPicker({selectedTaxRate = '', policyID, transactionID, onSubmit, action, iouType, onDismiss = Navigation.goBack, addBottomSafeAreaPadding}: TaxPickerProps) {
51+
function TaxPicker({
52+
selectedTaxRate = '',
53+
policyID,
54+
transactionID,
55+
onSubmit,
56+
action,
57+
iouType,
58+
onDismiss = Navigation.goBack,
59+
addBottomSafeAreaPadding,
60+
allowDeselect = false,
61+
}: TaxPickerProps) {
4662
const {translate, localeCompare} = useLocalize();
4763
const [searchValue, setSearchValue] = useState('');
4864
const [splitDraftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`);
@@ -119,7 +135,8 @@ function TaxPicker({selectedTaxRate = '', policyID, transactionID, onSubmit, act
119135
const currentTaxRateValue = taxCode ? taxRates?.taxes?.[taxCode]?.value : undefined;
120136
const hasMatchingTaxValue = taxValue === undefined || currentTaxRateValue === taxValue;
121137

122-
if (isSameTaxCode && hasMatchingTaxValue) {
138+
// If deselection is not allowed and the same option is selected, just dismiss
139+
if (!allowDeselect && isSameTaxCode && hasMatchingTaxValue) {
123140
onDismiss();
124141
return;
125142
}

src/hooks/useSearchBulkActions.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import Navigation from '@libs/Navigation/Navigation';
3737
import {getConnectedIntegration} from '@libs/PolicyUtils';
3838
import {getSecondaryExportReportActions, isMergeActionForSelectedTransactions} from '@libs/ReportSecondaryActionUtils';
3939
import {
40+
canEditMultipleTransactions,
4041
getIntegrationIcon,
4142
getReportOrDraftReport,
4243
isBusinessInvoiceRoom,
@@ -50,7 +51,7 @@ import {navigateToSearchRHP, shouldShowDeleteOption} from '@libs/SearchUIUtils';
5051
import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils';
5152
import {hasTransactionBeenRejected, isDeletedTransaction} from '@libs/TransactionUtils';
5253
import variables from '@styles/variables';
53-
import {canIOUBePaid, dismissRejectUseExplanation} from '@userActions/IOU';
54+
import {canIOUBePaid, dismissRejectUseExplanation, initBulkEditDraftTransaction} from '@userActions/IOU';
5455
import CONST from '@src/CONST';
5556
import ONYXKEYS from '@src/ONYXKEYS';
5657
import ROUTES from '@src/ROUTES';
@@ -64,6 +65,7 @@ import {useMemoizedLazyExpensifyIcons} from './useLazyAsset';
6465
import useLocalize from './useLocalize';
6566
import useNetwork from './useNetwork';
6667
import useOnyx from './useOnyx';
68+
import usePermissions from './usePermissions';
6769
import usePersonalPolicy from './usePersonalPolicy';
6870
import useSelfDMReport from './useSelfDMReport';
6971
import useTheme from './useTheme';
@@ -99,6 +101,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
99101
const {accountID} = currentUserPersonalDetails;
100102
const allTransactions = useAllTransactions();
101103
const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT);
104+
const [allReportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS);
102105
const [allReportNameValuePairs] = useOnyx(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS);
103106
const selfDMReport = useSelfDMReport();
104107
const [lastPaymentMethods] = useOnyx(ONYXKEYS.NVP_LAST_PAYMENT_METHOD);
@@ -111,6 +114,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
111114
const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS);
112115
const personalPolicy = usePersonalPolicy();
113116
const [userBillingGraceEndPeriods] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END);
117+
const {isBetaEnabled} = usePermissions();
114118
const [ownerBillingGraceEndPeriod] = useOnyx(ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END);
115119
const undeleteTransactions = useUndeleteTransactions();
116120

@@ -672,7 +676,9 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
672676
);
673677

674678
const onBulkPaySelectedRef = useRef(onBulkPaySelected);
675-
onBulkPaySelectedRef.current = onBulkPaySelected;
679+
useEffect(() => {
680+
onBulkPaySelectedRef.current = onBulkPaySelected;
681+
});
676682
const stableOnBulkPaySelected = useCallback((paymentMethod?: PaymentMethodType, additionalData?: BulkPaySelectionData) => {
677683
onBulkPaySelectedRef.current?.(paymentMethod, additionalData);
678684
}, []);
@@ -887,6 +893,30 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
887893
return [exportButtonOption];
888894
}
889895

896+
const isExpenseReportSearch = typeExpenseReport || searchResults?.search.type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT;
897+
const selectedTransactionsList = Object.values(selectedTransactions)
898+
.map((transaction) => transaction.transaction)
899+
.filter((transaction): transaction is Transaction => !!transaction);
900+
const canEditMultiple =
901+
canEditMultipleTransactions(selectedTransactionsList, allReportActions, allReports, policies, isExpenseReportSearch, searchResults?.data) && isBetaEnabled(CONST.BETAS.BULK_EDIT);
902+
903+
if (canEditMultiple) {
904+
options.push({
905+
icon: expensifyIcons.Pencil,
906+
text: translate('search.bulkActions.editMultiple'),
907+
value: CONST.SEARCH.BULK_ACTION_TYPES.EDIT,
908+
shouldCloseModalOnSelect: true,
909+
onSelected: () => {
910+
const selectedTransactionIDs = Object.keys(selectedTransactions).filter((transactionID) => {
911+
const selectedTransaction = selectedTransactions[transactionID];
912+
return !!selectedTransaction?.transaction?.transactionID || !!allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
913+
});
914+
initBulkEditDraftTransaction(selectedTransactionIDs);
915+
Navigation.navigate(ROUTES.SEARCH_EDIT_MULTIPLE_TRANSACTIONS_RHP);
916+
},
917+
});
918+
}
919+
890920
const areSelectedTransactionsIncludedInReports = selectedTransactionsKeys.every((id) =>
891921
selectedTransactions[id].reportID ? selectedReportIDs.includes(selectedTransactions[id].reportID) : true,
892922
);
@@ -1191,6 +1221,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
11911221
expensifyIcons.ArrowCollapse,
11921222
expensifyIcons.DocumentMerge,
11931223
expensifyIcons.ArrowSplit,
1224+
expensifyIcons.Pencil,
11941225
expensifyIcons.Trashcan,
11951226
expensifyIcons.RotateLeft,
11961227
expensifyIcons.Exclamation,
@@ -1206,6 +1237,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
12061237
selectedTransactionReportIDs,
12071238
selectedPolicyIDs,
12081239
policies,
1240+
allReportActions,
12091241
integrationsExportTemplates,
12101242
csvExportLayouts,
12111243
allReports,
@@ -1243,6 +1275,9 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
12431275
userBillingGraceEndPeriods,
12441276
ownerBillingGraceEndPeriod,
12451277
currentSearchKey,
1278+
allTransactions,
1279+
isBetaEnabled,
1280+
shouldShowBusinessBankAccountOptions,
12461281
]);
12471282

12481283
const handleOfflineModalClose = useCallback(() => {
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import {useSearchStateContext} from '@components/Search/SearchContext';
2+
import {getSearchBulkEditPolicyID} from '@libs/SearchUIUtils';
3+
import {withSnapshotReports, withSnapshotTransactions} from '@pages/Search/SearchEditMultiple/SearchEditMultipleUtils';
4+
import CONST from '@src/CONST';
5+
import ONYXKEYS from '@src/ONYXKEYS';
6+
import useOnyx from './useOnyx';
7+
8+
/**
9+
* Resolves the bulk-edit policyID from the selected transactions, using
10+
* snapshot-merged collections so that expenses only present in the search
11+
* snapshot (e.g. after a hard refresh) are still resolved correctly.
12+
*/
13+
function useSearchBulkEditPolicyID(): string | undefined {
14+
const {currentSearchResults} = useSearchStateContext();
15+
const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID);
16+
const [draftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_BULK_EDIT_TRANSACTION_ID}`);
17+
const [allTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION);
18+
const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT);
19+
20+
const snapshotData = currentSearchResults?.data;
21+
const mergedTransactions = withSnapshotTransactions(allTransactions, snapshotData);
22+
const mergedReports = withSnapshotReports(allReports, snapshotData);
23+
24+
const selectedTransactionIDs = draftTransaction?.selectedTransactionIDs ?? [];
25+
26+
return getSearchBulkEditPolicyID(selectedTransactionIDs, activePolicyID, mergedTransactions, mergedReports);
27+
}
28+
29+
export default useSearchBulkEditPolicyID;

0 commit comments

Comments
 (0)