Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7515,6 +7515,7 @@ const CONST = {
DUPLICATE: 'duplicate',
UNDELETE: 'undelete',
},
BULK_DUPLICATE_LIMIT: 50,
TRANSACTION_TYPE: {
CASH: 'cash',
CARD: 'card',
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useBulkDuplicateAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ function useBulkDuplicateAction({selectedTransactionsKeys, allTransactions, allR
if (onAfterDuplicate) {
onAfterDuplicate();
} else {
clearSelectedTransactions(undefined, true);
clearSelectedTransactions();
}
};

Expand Down
28 changes: 21 additions & 7 deletions src/hooks/useSearchBulkActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ import useConfirmModal from './useConfirmModal';
import {useCurrencyListActions} from './useCurrencyList';
import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails';
import useDefaultExpensePolicy from './useDefaultExpensePolicy';
import useEnvironment from './useEnvironment';
import {useMemoizedLazyExpensifyIcons} from './useLazyAsset';
import useLocalize from './useLocalize';
import useNetwork from './useNetwork';
Expand Down Expand Up @@ -175,11 +174,17 @@ function shouldShowBulkDuplicateOption({

const report = reportID ? ((searchData?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] as Report | undefined) ?? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]) : undefined;

if (isPerDiemRequest(transaction) && report?.policyID && defaultExpensePolicyID !== report.policyID) {
return false;
if (isPerDiemRequest(transaction)) {
const policyID = report?.policyID;
if (!policyID || defaultExpensePolicyID !== policyID) {
return false;
}
}

if (isDistanceRequest(transaction) && reportID) {
if (reportID === CONST.REPORT.UNREPORTED_REPORT_ID && activePolicyExpenseChat) {
return false;
}
const chatReportID = report?.chatReportID ?? report?.parentReportID;
const chatReport = chatReportID
? ((searchData?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`] as Report | undefined) ?? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`])
Expand All @@ -200,7 +205,6 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
const styles = useThemeStyles();
const theme = useTheme();
const {isOffline} = useNetwork();
const {isProduction} = useEnvironment();
const {isDelegateAccessRestricted} = useDelegateNoAccessState();
const {showDelegateNoAccessModal} = useDelegateNoAccessActions();
const {selectedTransactions, selectedReports, areAllMatchingItemsSelected, currentSearchResults, currentSearchKey} = useSearchStateContext();
Expand Down Expand Up @@ -832,7 +836,6 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {

const isDuplicateOptionVisible = useMemo(
() =>
!isProduction &&
shouldShowBulkDuplicateOption({
selectedTransactionsKeys,
selectedTransactions,
Expand All @@ -846,7 +849,6 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
searchData: currentSearchResults?.data,
}),
[
isProduction,
selectedTransactionsKeys,
selectedTransactions,
allTransactions,
Expand Down Expand Up @@ -1346,12 +1348,24 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
}

if (isDuplicateOptionVisible) {
const exceedsBulkDuplicateLimit = selectedTransactionsKeys.length > CONST.SEARCH.BULK_DUPLICATE_LIMIT;
options.push({
text: translate('search.bulkActions.duplicateExpense', {count: selectedTransactionsKeys.length}),
icon: expensifyIcons.ExpenseCopy,
value: CONST.SEARCH.BULK_ACTION_TYPES.DUPLICATE,
shouldCloseModalOnSelect: true,
onSelected: invokeDuplicateHandler,
onSelected: () => {
if (exceedsBulkDuplicateLimit) {
showConfirmModal({
title: translate('common.duplicateExpense'),
prompt: translate('iou.bulkDuplicateLimit'),
confirmText: translate('common.buttonConfirm'),
shouldShowCancelButton: false,
});
return;
}
invokeDuplicateHandler();
},
});
}

Expand Down
20 changes: 15 additions & 5 deletions src/hooks/useSelectedTransactionsActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ import ROUTES from '@src/ROUTES';
import type {Route} from '@src/ROUTES';
import type {Policy, Report, ReportAction, Session, Transaction} from '@src/types/onyx';
import useAllTransactions from './useAllTransactions';
import useConfirmModal from './useConfirmModal';
import {useCurrencyListActions} from './useCurrencyList';
import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails';
import useDefaultExpensePolicy from './useDefaultExpensePolicy';
import useDeleteTransactions from './useDeleteTransactions';
import useDuplicateTransactionsAndViolations from './useDuplicateTransactionsAndViolations';
import useEnvironment from './useEnvironment';
import {useMemoizedLazyExpensifyIcons} from './useLazyAsset';
import useLocalize from './useLocalize';
import useNetworkWithOfflineStatus from './useNetworkWithOfflineStatus';
Expand Down Expand Up @@ -112,7 +112,6 @@ function useSelectedTransactionsActions({
const {deleteTransactions} = useDeleteTransactions({report, reportActions, policy});
const {login, accountID: currentUserAccountID} = useCurrentUserPersonalDetails();
const defaultExpensePolicy = useDefaultExpensePolicy();
const {isProduction} = useEnvironment();

const selectedTransactionsList = selectedTransactionIDs.reduce((acc, transactionID) => {
const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
Expand Down Expand Up @@ -153,6 +152,7 @@ function useSelectedTransactionsActions({
const hasTransactionsFromMultipleOwners = hasUnknownOwner ? knownOwnerIDs.size > 0 || selectedTransactionIDs.length > 1 : knownOwnerIDs.size > 1;

const {translate, localeCompare} = useLocalize();
const {showConfirmModal} = useConfirmModal();
const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false);

const selectedTransactionsForDuplicate = useMemo(() => {
Expand All @@ -168,7 +168,6 @@ function useSelectedTransactionsActions({

const isDuplicateOptionVisible = useMemo(
() =>
!isProduction &&
shouldShowBulkDuplicateOption({
selectedTransactionsKeys: selectedTransactionIDs,
selectedTransactions: selectedTransactionsForDuplicate,
Expand All @@ -182,7 +181,6 @@ function useSelectedTransactionsActions({
searchData: undefined,
}),
[
isProduction,
selectedTransactionIDs,
selectedTransactionsForDuplicate,
allTransactions,
Expand Down Expand Up @@ -475,13 +473,25 @@ function useSelectedTransactionsActions({
}

if (isDuplicateOptionVisible) {
const exceedsBulkDuplicateLimit = selectedTransactionIDs.length > CONST.SEARCH.BULK_DUPLICATE_LIMIT;
// eslint-disable-next-line react-hooks/refs -- invokeDuplicateHandler reads a ref, but only at event-handler time (onSelected), never during render
options.push({
text: translate('search.bulkActions.duplicateExpense', {count: selectedTransactionIDs.length}),
icon: expensifyIcons.ExpenseCopy,
value: DUPLICATE,
shouldCloseModalOnSelect: true,
onSelected: invokeDuplicateHandler,
onSelected: () => {
if (exceedsBulkDuplicateLimit) {
showConfirmModal({
title: translate('common.duplicateExpense'),
prompt: translate('iou.bulkDuplicateLimit'),
confirmText: translate('common.buttonConfirm'),
shouldShowCancelButton: false,
});
return;
}
invokeDuplicateHandler();
},
});
}

Expand Down
1 change: 1 addition & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1671,6 +1671,7 @@ const translations: TranslationDeepObject<typeof en> = {
prompt: 'Aktivieren Sie die Steuerverfolgung im Workspace, um die Ausgabendetails zu bearbeiten oder die Steuer aus dieser Ausgabe zu löschen.',
confirmText: 'Steuer löschen',
},
bulkDuplicateLimit: `Sie können bis zu ${CONST.SEARCH.BULK_DUPLICATE_LIMIT} Ausgaben gleichzeitig duplizieren. Bitte wählen Sie weniger Ausgaben aus und versuchen Sie es erneut.`,
deleted: 'Gelöscht',
},
transactionMerge: {
Expand Down
1 change: 1 addition & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1712,6 +1712,7 @@ const translations = {
},
duplicateNonDefaultWorkspacePerDiemError: "You can't duplicate per diem expenses across workspaces because the rates may differ between workspaces.",
cannotDuplicateDistanceExpense: "You can't duplicate distance expenses across workspaces because the rates may differ between workspaces.",
bulkDuplicateLimit: `You can duplicate up to ${CONST.SEARCH.BULK_DUPLICATE_LIMIT} expenses at a time. Please select fewer expenses and try again.`,
taxDisabledAlert: {
title: 'Tax disabled',
prompt: 'Enable tax tracking on the workspace to edit the expense details or delete the tax from this expense.',
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1598,6 +1598,7 @@ const translations: TranslationDeepObject<typeof en> = {
},
duplicateNonDefaultWorkspacePerDiemError: 'No puedes duplicar gastos de viáticos entre espacios de trabajo porque las tarifas pueden variar entre ellos.',
cannotDuplicateDistanceExpense: 'No puedes duplicar gastos de distancia entre espacios de trabajo porque las tasas pueden diferir entre espacios de trabajo.',
bulkDuplicateLimit: `Solo puedes duplicar hasta ${CONST.SEARCH.BULK_DUPLICATE_LIMIT} gastos a la vez. Por favor, selecciona menos gastos e inténtalo de nuevo.`,
taxDisabledAlert: {
title: 'Impuesto deshabilitado',
prompt: 'Habilita el seguimiento de impuestos en el espacio de trabajo para editar los detalles del gasto o eliminar el impuesto de este gasto.',
Expand Down
1 change: 1 addition & 0 deletions src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1675,6 +1675,7 @@ const translations: TranslationDeepObject<typeof en> = {
prompt: 'Activez le suivi des taxes dans l’espace de travail pour modifier les détails de la dépense ou supprimer la taxe de cette dépense.',
confirmText: 'Supprimer la taxe',
},
bulkDuplicateLimit: `Vous pouvez dupliquer jusqu’à ${CONST.SEARCH.BULK_DUPLICATE_LIMIT} dépenses à la fois. Veuillez sélectionner moins de dépenses et réessayer.`,
deleted: 'Supprimé',
},
transactionMerge: {
Expand Down
1 change: 1 addition & 0 deletions src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1668,6 +1668,7 @@ const translations: TranslationDeepObject<typeof en> = {
prompt: 'Abilita il monitoraggio delle imposte nello spazio di lavoro per modificare i dettagli della spesa o eliminare l’imposta da questa spesa.',
confirmText: 'Elimina imposta',
},
bulkDuplicateLimit: `Puoi duplicare fino a ${CONST.SEARCH.BULK_DUPLICATE_LIMIT} spese alla volta. Seleziona meno spese e riprova.`,
deleted: 'Eliminato',
},
transactionMerge: {
Expand Down
1 change: 1 addition & 0 deletions src/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1648,6 +1648,7 @@ const translations: TranslationDeepObject<typeof en> = {
prompt: '経費の詳細を編集したり、この経費から税金を削除したりするには、ワークスペースで税金の追跡を有効にしてください。',
confirmText: '税を削除',
},
bulkDuplicateLimit: `一度に複製できる経費は最大で ${CONST.SEARCH.BULK_DUPLICATE_LIMIT} 件です。経費の数を減らして、もう一度お試しください。`,
deleted: '削除済み',
},
transactionMerge: {
Expand Down
1 change: 1 addition & 0 deletions src/languages/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1664,6 +1664,7 @@ const translations: TranslationDeepObject<typeof en> = {
prompt: 'Schakel belastingregistratie in voor de workspace om de onkostendetails te bewerken of de belasting uit deze onkostendeclaratie te verwijderen.',
confirmText: 'Belasting verwijderen',
},
bulkDuplicateLimit: `Je kunt maximaal ${CONST.SEARCH.BULK_DUPLICATE_LIMIT} uitgaven tegelijk dupliceren. Selecteer minder uitgaven en probeer het opnieuw.`,
deleted: 'Verwijderd',
},
transactionMerge: {
Expand Down
1 change: 1 addition & 0 deletions src/languages/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1664,6 +1664,7 @@ const translations: TranslationDeepObject<typeof en> = {
prompt: 'Włącz śledzenie podatku w przestrzeni roboczej, aby edytować szczegóły wydatku lub usunąć podatek z tego wydatku.',
confirmText: 'Usuń podatek',
},
bulkDuplicateLimit: `Możesz jednocześnie zduplikować maksymalnie ${CONST.SEARCH.BULK_DUPLICATE_LIMIT} wydatków. Wybierz mniej wydatków i spróbuj ponownie.`,
deleted: 'Usunięto',
},
transactionMerge: {
Expand Down
1 change: 1 addition & 0 deletions src/languages/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1661,6 +1661,7 @@ const translations: TranslationDeepObject<typeof en> = {
prompt: 'Ative o acompanhamento de impostos no espaço de trabalho para editar os detalhes da despesa ou excluir o imposto desta despesa.',
confirmText: 'Excluir imposto',
},
bulkDuplicateLimit: `Você pode duplicar até ${CONST.SEARCH.BULK_DUPLICATE_LIMIT} despesas por vez. Selecione menos despesas e tente novamente.`,
deleted: 'Excluído',
},
transactionMerge: {
Expand Down
1 change: 1 addition & 0 deletions src/languages/zh-hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1614,6 +1614,7 @@ const translations: TranslationDeepObject<typeof en> = {
failedToApproveViaDEW: (reason: string) => `批准失败。${reason}`,
cannotDuplicateDistanceExpense: '你无法在不同工作区之间复制里程报销,因为各个工作区的费率可能不同。',
taxDisabledAlert: {title: '税费已禁用', prompt: '请在工作区中启用税费跟踪,以便编辑此报销的详细信息或从该报销中删除税费。', confirmText: '删除税费'},
bulkDuplicateLimit: `您一次最多可以复制 ${CONST.SEARCH.BULK_DUPLICATE_LIMIT} 笔报销。请减少选择的报销数量后重试。`,
deleted: '已删除',
},
transactionMerge: {
Expand Down
20 changes: 18 additions & 2 deletions src/libs/actions/IOU/Duplicate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,8 @@ type DuplicateExpenseTransactionParams = {
targetPolicyTags: OnyxEntry<OnyxTypes.PolicyTagLists>;
shouldPlaySound?: boolean;
shouldDeferAutoSubmit?: boolean;
existingIOUReport?: OnyxEntry<OnyxTypes.Report>;
optimisticReportPreviewActionID?: string;
};

function duplicateExpenseTransaction({
Expand All @@ -690,6 +692,8 @@ function duplicateExpenseTransaction({
targetPolicyTags,
shouldPlaySound = true,
shouldDeferAutoSubmit = false,
existingIOUReport,
optimisticReportPreviewActionID: externalReportPreviewActionID,
}: DuplicateExpenseTransactionParams) {
if (!transaction) {
return;
Expand All @@ -704,10 +708,11 @@ function duplicateExpenseTransaction({

const params: RequestMoneyInformation = {
report: targetReport,
existingIOUReport,
optimisticChatReportID,
optimisticCreatedReportActionID: NumberUtils.rand64(),
optimisticIOUReportID,
optimisticReportPreviewActionID: NumberUtils.rand64(),
optimisticReportPreviewActionID: externalReportPreviewActionID ?? NumberUtils.rand64(),
participantParams: {
payeeAccountID: userAccountID,
payeeEmail: currentUserEmail,
Expand Down Expand Up @@ -991,12 +996,17 @@ function bulkDuplicateExpenses({

const optimisticChatReportID = generateReportID();
const optimisticIOUReportID = generateReportID();
const sharedReportPreviewActionID = NumberUtils.rand64();

// After the first iteration creates a new optimistic IOU report, subsequent
// iterations must know its ID so getMoneyRequestInformation can find and
// MERGE into it instead of SET-overwriting it. We carry a local copy of
// targetReport whose iouReportID is patched after the first pass.
// We also pass the optimistic IOU report object directly via existingIOUReport
// to avoid a stale-state race: Onyx subscriber callbacks are deferred, so the
// module-level allReports in IOU/index.ts is not yet updated when iteration 2 runs.
let currentTargetReport = targetReport;
let optimisticIOUReport: OnyxEntry<OnyxTypes.Report>;

for (let i = 0; i < transactionsToDuplicate.length; i++) {
const item = transactionsToDuplicate.at(i);
Expand All @@ -1007,7 +1017,7 @@ function bulkDuplicateExpenses({
const existingTransactionID = getExistingTransactionID(item.linkedTrackedExpenseReportAction);
const existingTransactionDraft = existingTransactionID ? transactionDrafts?.[existingTransactionID] : undefined;

duplicateExpenseTransaction({
const result = duplicateExpenseTransaction({
transaction: item,
optimisticChatReportID,
optimisticIOUReportID,
Expand All @@ -1029,8 +1039,14 @@ function bulkDuplicateExpenses({
targetPolicyTags,
shouldPlaySound: false,
shouldDeferAutoSubmit: !isLastExpense,
existingIOUReport: optimisticIOUReport,
optimisticReportPreviewActionID: sharedReportPreviewActionID,
});

if (result?.iouReport) {
optimisticIOUReport = result.iouReport;
}

if (currentTargetReport && !currentTargetReport.iouReportID) {
currentTargetReport = {...currentTargetReport, iouReportID: optimisticIOUReportID};
}
Expand Down
Loading
Loading