-
Notifications
You must be signed in to change notification settings - Fork 3.7k
[Duplicate Expense] Allow bulk duplication. #84657
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
Changes from 17 commits
5c80f02
0a910f3
17b8a60
cff4f62
e2b5c55
2dae542
5760d55
9591889
ab91ed8
c0b25f5
2de7466
55447e2
0bb2736
3b6a21e
dca96c3
fe9b4e0
d2c9894
c2150c8
d2802aa
d6e1f5d
9036e5c
370c6e3
556354f
f59ca89
7f485d8
01cc84f
e3ec590
1f1e7b3
b96226c
ecd002e
7546137
e4a450b
6e10ed9
7cd5378
db0da62
efa3ce3
d003789
255d93d
aaed1b8
a124b9c
5bbe3b6
b84f459
6551b4d
77cb659
09c81ea
d3be3f3
0ce86c5
f40173f
3542312
0d7a532
2d3c64e
3ad6703
fe9eb48
0a2f1b0
af30acd
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,3 +1,5 @@ | ||
| import {hasSeenTourSelector} from '@selectors/Onboarding'; | ||
| import {validTransactionDraftsSelector} from '@selectors/TransactionDraft'; | ||
| import {useCallback, useEffect, useMemo, useRef, useState} from 'react'; | ||
| import {InteractionManager} from 'react-native'; | ||
| import type {OnyxCollection} from 'react-native-onyx'; | ||
|
|
@@ -10,6 +12,7 @@ import type {PopoverMenuItem} from '@components/PopoverMenu'; | |
| import {useSearchActionsContext, useSearchStateContext} from '@components/Search/SearchContext'; | ||
| import type {SearchHeaderOptionValue} from '@components/Search/SearchPageHeader/SearchPageHeader'; | ||
| import type {BulkPaySelectionData, PaymentData, SearchQueryJSON} from '@components/Search/types'; | ||
| import {bulkDuplicateExpenses} from '@libs/actions/IOU/Duplicate'; | ||
| import {setupMergeTransactionDataAndNavigate} from '@libs/actions/MergeTransaction'; | ||
| import {deleteAppReport, markAsManuallyExported, moveIOUReportToPolicy, moveIOUReportToPolicyAndInviteSubmitter} from '@libs/actions/Report'; | ||
| import { | ||
|
|
@@ -39,16 +42,20 @@ import {getConnectedIntegration, hasDynamicExternalWorkflow} from '@libs/PolicyU | |
| import {getSecondaryExportReportActions, isMergeActionForSelectedTransactions} from '@libs/ReportSecondaryActionUtils'; | ||
| import { | ||
| getIntegrationIcon, | ||
| getPolicyExpenseChat, | ||
| getReportOrDraftReport, | ||
| isArchivedReport, | ||
| isBusinessInvoiceRoom, | ||
| isCurrentUserSubmitter, | ||
| isDM, | ||
| isExpenseReport as isExpenseReportUtil, | ||
| isInvoiceReport, | ||
| isIOUReport as isIOUReportUtil, | ||
| isSelfDM, | ||
| } from '@libs/ReportUtils'; | ||
| import {navigateToSearchRHP, shouldShowDeleteOption} from '@libs/SearchUIUtils'; | ||
| import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; | ||
| import {hasTransactionBeenRejected} from '@libs/TransactionUtils'; | ||
| import {hasCustomUnitOutOfPolicyViolation, hasTransactionBeenRejected, isDistanceRequest, isManagedCardTransaction, isPerDiemRequest, isScanning} from '@libs/TransactionUtils'; | ||
| import variables from '@styles/variables'; | ||
| import {canIOUBePaid, dismissRejectUseExplanation} from '@userActions/IOU'; | ||
| import {openOldDotLink} from '@userActions/Link'; | ||
|
|
@@ -60,6 +67,7 @@ import useAllTransactions from './useAllTransactions'; | |
| import useBulkPayOptions from './useBulkPayOptions'; | ||
| import useConfirmModal from './useConfirmModal'; | ||
| import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails'; | ||
| import useDefaultExpensePolicy from './useDefaultExpensePolicy'; | ||
| import {useMemoizedLazyExpensifyIcons} from './useLazyAsset'; | ||
| import useLocalize from './useLocalize'; | ||
| import useNetwork from './useNetwork'; | ||
|
|
@@ -100,9 +108,24 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { | |
| const [csvExportLayouts] = useOnyx(ONYXKEYS.NVP_CSV_EXPORT_LAYOUTS); | ||
| const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION); | ||
| const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); | ||
| const [allReportNameValuePairs] = useOnyx(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS); | ||
| const personalPolicy = usePersonalPolicy(); | ||
| const [userBillingGraceEndPeriodCollection] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END); | ||
|
|
||
| const defaultExpensePolicy = useDefaultExpensePolicy(); | ||
| const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); | ||
| const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); | ||
| const [quickAction] = useOnyx(ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE); | ||
| const [policyRecentlyUsedCurrencies] = useOnyx(ONYXKEYS.RECENTLY_USED_CURRENCIES); | ||
| const [isSelfTourViewed = false] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector}); | ||
| const [transactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {selector: validTransactionDraftsSelector}); | ||
| const draftTransactionIDs = Object.keys(transactionDrafts ?? {}); | ||
| const [betas] = useOnyx(ONYXKEYS.BETAS); | ||
| const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); | ||
| const [recentWaypoints] = useOnyx(ONYXKEYS.NVP_RECENT_WAYPOINTS); | ||
| const [allPolicyCategories] = useOnyx(ONYXKEYS.COLLECTION.POLICY_CATEGORIES); | ||
| const [allPolicyTags] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS); | ||
|
|
||
| // Cache the last search results that had data, so the merge option remains available | ||
| // while results are temporarily unset (e.g. during sorting/loading). | ||
| const lastNonEmptySearchResultsRef = useRef<SearchResults | undefined>(undefined); | ||
|
|
@@ -119,6 +142,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { | |
| const {showConfirmModal} = useConfirmModal(); | ||
| const {isBetaEnabled} = usePermissions(); | ||
| const isDEWBetaEnabled = isBetaEnabled(CONST.BETAS.NEW_DOT_DEW); | ||
| const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT); | ||
| const [isHoldEducationalModalVisible, setIsHoldEducationalModalVisible] = useState(false); | ||
| const [rejectModalAction, setRejectModalAction] = useState<ValueOf< | ||
| typeof CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.HOLD | typeof CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.REJECT | ||
|
|
@@ -144,6 +168,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { | |
| 'Exclamation', | ||
| 'MoneyBag', | ||
| 'ArrowSplit', | ||
| 'ExpenseCopy', | ||
| 'QBOSquare', | ||
| 'XeroSquare', | ||
| 'NetSuiteSquare', | ||
|
|
@@ -709,6 +734,53 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { | |
| ); | ||
| }, [selectedTransactionReportIDs, currentUserPersonalDetails?.accountID, currentSearchResults?.data]); | ||
|
|
||
| const handleDuplicateSelectedTransactions = useCallback(() => { | ||
| const activePolicyExpenseChat = getPolicyExpenseChat(accountID, defaultExpensePolicy?.id); | ||
| const activePolicyCategories = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${defaultExpensePolicy?.id}`] ?? {}; | ||
| const targetPolicyTags = defaultExpensePolicy ? (allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${defaultExpensePolicy.id}`] ?? {}) : {}; | ||
|
|
||
| bulkDuplicateExpenses({ | ||
| transactionIDs: selectedTransactionsKeys, | ||
| allTransactions: allTransactions ?? {}, | ||
| targetPolicy: defaultExpensePolicy ?? undefined, | ||
| targetPolicyCategories: activePolicyCategories, | ||
| targetPolicyTags, | ||
| targetReport: activePolicyExpenseChat, | ||
| personalDetails, | ||
| isASAPSubmitBetaEnabled, | ||
| introSelected, | ||
| activePolicyID, | ||
| quickAction, | ||
| policyRecentlyUsedCurrencies: policyRecentlyUsedCurrencies ?? [], | ||
| isSelfTourViewed, | ||
| transactionDrafts, | ||
| draftTransactionIDs, | ||
| betas, | ||
| recentWaypoints, | ||
| }); | ||
|
|
||
| clearSelectedTransactions(undefined, true); | ||
| }, [ | ||
| selectedTransactionsKeys, | ||
| accountID, | ||
| defaultExpensePolicy, | ||
| allTransactions, | ||
| allPolicyCategories, | ||
| allPolicyTags, | ||
| personalDetails, | ||
| isASAPSubmitBetaEnabled, | ||
| introSelected, | ||
| activePolicyID, | ||
| quickAction, | ||
| policyRecentlyUsedCurrencies, | ||
| isSelfTourViewed, | ||
| transactionDrafts, | ||
| draftTransactionIDs, | ||
| betas, | ||
| recentWaypoints, | ||
| clearSelectedTransactions, | ||
| ]); | ||
|
|
||
| const headerButtonsOptions = useMemo(() => { | ||
| if (selectedTransactionsKeys.length === 0 || status == null || !hash) { | ||
| return CONST.EMPTY_ARRAY as unknown as Array<DropdownOption<SearchHeaderOptionValue>>; | ||
|
|
@@ -1127,6 +1199,59 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { | |
| }); | ||
| } | ||
|
|
||
| const activePolicyExpenseChat = getPolicyExpenseChat(currentUserPersonalDetails.accountID, defaultExpensePolicy?.id); | ||
| const shouldShowDuplicateOption = | ||
| !typeExpenseReport && | ||
| selectedTransactionsKeys.length > 0 && | ||
| selectedTransactionsKeys.every((id) => { | ||
| const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${id}`]; | ||
| if (!transaction || isManagedCardTransaction(transaction) || isScanning(transaction)) { | ||
| return false; | ||
| } | ||
| const dates = transaction?.comment?.customUnit?.attributes?.dates; | ||
| if (isPerDiemRequest(transaction) && (!dates?.start || !dates?.end)) { | ||
| return false; | ||
Krishna2323 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| const reportID = selectedTransactions[id]?.reportID; | ||
| if (reportID && !isCurrentUserSubmitter(getReportOrDraftReport(reportID))) { | ||
|
||
| return false; | ||
| } | ||
| const transactionViolations = allTransactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${id}`]; | ||
| if (hasCustomUnitOutOfPolicyViolation(transactionViolations)) { | ||
| return false; | ||
| } | ||
| if (isPerDiemRequest(transaction)) { | ||
| const report = reportID ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] : undefined; | ||
| if (report?.policyID && defaultExpensePolicy?.id !== report.policyID) { | ||
| return false; | ||
| } | ||
| } | ||
| if (isDistanceRequest(transaction) && reportID) { | ||
| const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; | ||
| const chatReportID = report?.chatReportID ?? report?.parentReportID; | ||
| const chatReport = chatReportID ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`] : undefined; | ||
| const reportNVP = allReportNameValuePairs?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`]; | ||
| const chatReportNVP = chatReportID ? allReportNameValuePairs?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${chatReportID}`] : undefined; | ||
| const isReportArchived = isArchivedReport(reportNVP) || isArchivedReport(chatReportNVP); | ||
| if (isReportArchived || (activePolicyExpenseChat && chatReport && (isDM(chatReport) || isSelfDM(chatReport)))) { | ||
| return false; | ||
| } | ||
| } | ||
| return true; | ||
| }); | ||
|
|
||
| if (shouldShowDuplicateOption) { | ||
| options.push({ | ||
| text: translate('search.bulkActions.duplicateExpense'), | ||
| icon: expensifyIcons.ExpenseCopy, | ||
| value: CONST.SEARCH.BULK_ACTION_TYPES.DUPLICATE, | ||
Krishna2323 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| shouldCloseModalOnSelect: true, | ||
| onSelected: () => { | ||
| handleDuplicateSelectedTransactions(); | ||
| }, | ||
| }); | ||
| } | ||
|
|
||
| if (shouldShowDeleteOption(selectedTransactions, currentSearchResults?.data, selectedReports, queryJSON?.type)) { | ||
| options.push({ | ||
| icon: expensifyIcons.Trashcan, | ||
|
|
@@ -1176,6 +1301,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { | |
| expensifyIcons.ArrowCollapse, | ||
| expensifyIcons.DocumentMerge, | ||
| expensifyIcons.ArrowSplit, | ||
| expensifyIcons.ExpenseCopy, | ||
| expensifyIcons.Trashcan, | ||
| expensifyIcons.Exclamation, | ||
| translate, | ||
|
|
@@ -1217,6 +1343,10 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { | |
| localeCompare, | ||
| firstTransaction, | ||
| firstTransactionPolicy, | ||
| allTransactions, | ||
| allReportNameValuePairs, | ||
| defaultExpensePolicy?.id, | ||
| handleDuplicateSelectedTransactions, | ||
| handleDeleteSelectedTransactions, | ||
| theme.icon, | ||
| styles.colorMuted, | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.