diff --git a/src/components/TransactionItemRow/TransactionItemRowNarrow.tsx b/src/components/TransactionItemRow/TransactionItemRowNarrow.tsx index 58e4c35036ea..effebeb361a9 100644 --- a/src/components/TransactionItemRow/TransactionItemRowNarrow.tsx +++ b/src/components/TransactionItemRow/TransactionItemRowNarrow.tsx @@ -30,6 +30,9 @@ type TransactionItemRowNarrowProps = Pick< | 'isInSingleTransactionReport' | 'shouldShowRadioButton' | 'onRadioButtonPress' + | 'shouldStopRadioButtonMouseDownPropagation' + | 'radioButtonContainerStyle' + | 'radioButtonWrapperStyle' | 'shouldShowErrors' | 'isDisabled' | 'violations' @@ -51,6 +54,9 @@ function TransactionItemRowNarrow({ isInSingleTransactionReport = false, shouldShowRadioButton = false, onRadioButtonPress = () => {}, + shouldStopRadioButtonMouseDownPropagation = false, + radioButtonContainerStyle, + radioButtonWrapperStyle, shouldShowErrors = true, isDisabled = false, violations, @@ -149,12 +155,14 @@ function TransactionItemRowNarrow({ )} {shouldShowRadioButton && ( - + onRadioButtonPress?.(transactionItem.transactionID)} accessibilityLabel={CONST.ROLE.RADIO} + shouldStopMouseDownPropagation={shouldStopRadioButtonMouseDownPropagation} + style={radioButtonWrapperStyle} /> )} diff --git a/src/components/TransactionItemRow/TransactionItemRowWide.tsx b/src/components/TransactionItemRow/TransactionItemRowWide.tsx index 4d1052dccf27..7e71ad05b635 100644 --- a/src/components/TransactionItemRow/TransactionItemRowWide.tsx +++ b/src/components/TransactionItemRow/TransactionItemRowWide.tsx @@ -76,6 +76,8 @@ function TransactionItemRowWide({ isInSingleTransactionReport = false, shouldShowRadioButton = false, onRadioButtonPress = () => {}, + shouldStopRadioButtonMouseDownPropagation = false, + radioButtonContainerStyle, shouldShowErrors = true, isDisabled = false, violations, @@ -592,12 +594,13 @@ function TransactionItemRowWide({ )} {columns?.map(renderColumn)} {shouldShowRadioButton && ( - + onRadioButtonPress?.(transactionItem.transactionID)} accessibilityLabel={CONST.ROLE.RADIO} + shouldStopMouseDownPropagation={shouldStopRadioButtonMouseDownPropagation} /> )} diff --git a/src/components/TransactionItemRow/index.tsx b/src/components/TransactionItemRow/index.tsx index 98bdc4fc0b7b..4b964e77126e 100644 --- a/src/components/TransactionItemRow/index.tsx +++ b/src/components/TransactionItemRow/index.tsx @@ -66,6 +66,9 @@ function TransactionItemRow({ isInSingleTransactionReport = false, shouldShowRadioButton = false, onRadioButtonPress = () => {}, + shouldStopRadioButtonMouseDownPropagation = false, + radioButtonContainerStyle, + radioButtonWrapperStyle, shouldShowErrors = true, shouldHighlightItemWhenSelected = true, isDisabled = false, @@ -145,6 +148,9 @@ function TransactionItemRow({ isInSingleTransactionReport, shouldShowRadioButton, onRadioButtonPress, + shouldStopRadioButtonMouseDownPropagation, + radioButtonContainerStyle, + radioButtonWrapperStyle, shouldShowErrors, isDisabled, violations, @@ -191,6 +197,8 @@ function TransactionItemRow({ isInSingleTransactionReport, shouldShowRadioButton, onRadioButtonPress, + shouldStopRadioButtonMouseDownPropagation, + radioButtonContainerStyle, shouldShowErrors, shouldHighlightItemWhenSelected, isDisabled, diff --git a/src/components/TransactionItemRow/types.ts b/src/components/TransactionItemRow/types.ts index 77df4666b994..13f18c7a6c79 100644 --- a/src/components/TransactionItemRow/types.ts +++ b/src/components/TransactionItemRow/types.ts @@ -74,6 +74,9 @@ type TransactionItemRowProps = { isInSingleTransactionReport?: boolean; shouldShowRadioButton?: boolean; onRadioButtonPress?: (transactionID: string) => void; + shouldStopRadioButtonMouseDownPropagation?: boolean; + radioButtonContainerStyle?: StyleProp; + radioButtonWrapperStyle?: StyleProp; shouldShowErrors?: boolean; shouldHighlightItemWhenSelected?: boolean; isDisabled?: boolean; diff --git a/src/languages/de.ts b/src/languages/de.ts index e37036aab28c..f6f6894c3b1e 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -1528,6 +1528,7 @@ const translations: TranslationDeepObject = { someDuplicatesArePaid: 'Einige dieser Duplikate wurden bereits genehmigt oder bezahlt.', reviewDuplicates: 'Duplikate prüfen', keepAll: 'Alle behalten', + keepSelected: 'Auswahl behalten', noDuplicatesTitle: 'Alles erledigt!', noDuplicatesDescription: 'Es gibt hier keine doppelten Transaktionen zur Überprüfung.', confirmApprove: 'Genehmigungsbetrag bestätigen', diff --git a/src/languages/en.ts b/src/languages/en.ts index 49751c7f25e1..143329798481 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1583,6 +1583,7 @@ const translations = { someDuplicatesArePaid: 'Some of these duplicates have been approved or paid already.', reviewDuplicates: 'Review duplicates', keepAll: 'Keep all', + keepSelected: 'Keep selected', noDuplicatesTitle: 'All set!', noDuplicatesDescription: 'There are no duplicate transactions for review here.', confirmApprove: 'Confirm approval amount', diff --git a/src/languages/es.ts b/src/languages/es.ts index 5b418ad43894..1f7758d654d7 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1489,6 +1489,7 @@ const translations: TranslationDeepObject = { someDuplicatesArePaid: 'Algunos de estos duplicados ya han sido aprobados o pagados.', reviewDuplicates: 'Revisar duplicados', keepAll: 'Mantener todos', + keepSelected: 'Mantener seleccionado', noDuplicatesTitle: '¡Todo listo!', noDuplicatesDescription: 'No hay transacciones duplicadas para revisar aquí.', confirmApprove: 'Confirmar importe a aprobar', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 2683942a9d3e..e6e42125be37 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -1533,6 +1533,7 @@ const translations: TranslationDeepObject = { someDuplicatesArePaid: 'Certains de ces doublons ont déjà été approuvés ou payés.', reviewDuplicates: 'Examiner les doublons', keepAll: 'Tout garder', + keepSelected: 'Garder la sélection', noDuplicatesTitle: 'Tout est en ordre !', noDuplicatesDescription: "Il n'y a aucune transaction en double à vérifier ici.", confirmApprove: 'Confirmer le montant approuvé', diff --git a/src/languages/it.ts b/src/languages/it.ts index 7f0c25d4116d..f7c220dbc6a0 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -1527,6 +1527,7 @@ const translations: TranslationDeepObject = { someDuplicatesArePaid: 'Alcuni di questi duplicati sono già stati approvati o pagati.', reviewDuplicates: 'Controlla duplicati', keepAll: 'Mantieni tutto', + keepSelected: 'Mantieni selezionati', noDuplicatesTitle: 'Tutto a posto!', noDuplicatesDescription: 'Non ci sono transazioni duplicate da verificare qui.', confirmApprove: 'Conferma l’importo approvato', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 07d0d149d99f..0f5602822cd2 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -1508,6 +1508,7 @@ const translations: TranslationDeepObject = { someDuplicatesArePaid: 'これらの重複の一部は、すでに承認または支払い済みです。', reviewDuplicates: '重複を確認', keepAll: 'すべて保持', + keepSelected: '選択したものを保持', noDuplicatesTitle: '準備完了!', noDuplicatesDescription: '確認が必要な重複取引はありません。', confirmApprove: '承認金額を確認', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 05ec9060c099..858f1462d628 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -1523,6 +1523,7 @@ const translations: TranslationDeepObject = { someDuplicatesArePaid: 'Sommige van deze duplicaten zijn al goedgekeurd of betaald.', reviewDuplicates: 'Dubbele items controleren', keepAll: 'Alles behouden', + keepSelected: 'Selectie behouden', noDuplicatesTitle: 'Alles in orde!', noDuplicatesDescription: 'Er zijn hier geen dubbele transacties om te beoordelen.', confirmApprove: 'Bevestig goedkeuringsbedrag', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index a9cbefbdf59a..b417becd2943 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -1522,6 +1522,7 @@ const translations: TranslationDeepObject = { someDuplicatesArePaid: 'Niektóre z tych duplikatów zostały już zatwierdzone lub opłacone.', reviewDuplicates: 'Przejrzyj duplikaty', keepAll: 'Zachowaj wszystko', + keepSelected: 'Zachowaj wybrane', noDuplicatesTitle: 'Wszystko gotowe!', noDuplicatesDescription: 'Nie ma tutaj zduplikowanych transakcji do sprawdzenia.', confirmApprove: 'Potwierdź kwotę zatwierdzenia', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 4ffea7018791..a6118e0d491a 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -1521,6 +1521,7 @@ const translations: TranslationDeepObject = { someDuplicatesArePaid: 'Alguns desses duplicados já foram aprovados ou pagos.', reviewDuplicates: 'Revisar duplicados', keepAll: 'Manter tudo', + keepSelected: 'Manter selecionados', noDuplicatesTitle: 'Tudo pronto!', noDuplicatesDescription: 'Não há transações duplicadas para revisar aqui.', confirmApprove: 'Confirmar valor da aprovação', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 2d2500a42fc7..3fb1ac6d9af9 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -1475,6 +1475,7 @@ const translations: TranslationDeepObject = { someDuplicatesArePaid: '其中一些重复项已被批准或支付。', reviewDuplicates: '审核重复项', keepAll: '全部保留', + keepSelected: '保留所选项', noDuplicatesTitle: '全部完成!', noDuplicatesDescription: '这里没有需要审核的重复交易。', confirmApprove: '确认批准金额', diff --git a/src/pages/TransactionDuplicate/DuplicateTransactionItem.tsx b/src/pages/TransactionDuplicate/DuplicateTransactionItem.tsx index 474e0749d59d..b91c9dbdd51c 100644 --- a/src/pages/TransactionDuplicate/DuplicateTransactionItem.tsx +++ b/src/pages/TransactionDuplicate/DuplicateTransactionItem.tsx @@ -1,79 +1,105 @@ -import React, {useMemo} from 'react'; -import {View} from 'react-native'; +import React from 'react'; import type {OnyxEntry} from 'react-native-onyx'; -import {usePersonalDetails} from '@components/OnyxListItemProvider'; +import {getButtonRole} from '@components/Button/utils'; +import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import {PressableWithFeedback} from '@components/Pressable'; +import type {TransactionListItemType} from '@components/Search/SearchList/ListItem/types'; +import TransactionItemRow from '@components/TransactionItemRow'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; -import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; -import {getOriginalMessage, getReportAction, isMoneyRequestAction} from '@libs/ReportActionsUtils'; -import {getOriginalReportID} from '@libs/ReportUtils'; -import ReportActionItem from '@pages/inbox/report/ReportActionItem'; -import {ReportActionItemActionsContext, ReportActionItemStateContext} from '@pages/inbox/report/ReportActionItemContext'; +import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils'; +import variables from '@styles/variables'; +import {createTransactionThreadReport} from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Transaction} from '@src/types/onyx'; type DuplicateTransactionItemProps = { transaction: OnyxEntry; - index: number; + isLastItem: boolean; + isSelected: boolean; + shouldShowSelection?: boolean; + onSelectTransaction: (transactionID: string) => void; onPreviewPressed: (reportID: string) => void; }; -const linkedTransactionRouteErrorSelector = (transaction: OnyxEntry) => transaction?.errorFields?.route ?? null; - -function DuplicateTransactionItem({transaction, index, onPreviewPressed}: DuplicateTransactionItemProps) { +function DuplicateTransactionItem({transaction, isLastItem, isSelected, shouldShowSelection = true, onSelectTransaction, onPreviewPressed}: DuplicateTransactionItemProps) { const styles = useThemeStyles(); - const personalDetails = usePersonalDetails(); - + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transaction?.reportID}`); const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`); + const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`); + const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); + const [betas] = useOnyx(ONYXKEYS.BETAS); const action = Object.values(reportActions ?? {})?.find((reportAction) => { - const IOUTransactionID = isMoneyRequestAction(reportAction) ? getOriginalMessage(reportAction)?.IOUTransactionID : CONST.DEFAULT_NUMBER_ID; - return IOUTransactionID === transaction?.transactionID; + const iouTransactionID = isMoneyRequestAction(reportAction) ? getOriginalMessage(reportAction)?.IOUTransactionID : CONST.DEFAULT_NUMBER_ID; + return iouTransactionID === transaction?.transactionID; }); - const originalReportID = getOriginalReportID(report?.reportID, action, reportActions); + const handlePreviewPress = () => { + if (!action || !report) { + return; + } - const [draftMessage] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${originalReportID}`); + if (action.childReportID) { + onPreviewPressed(action.childReportID); + return; + } - const [linkedTransactionRouteError] = useOnyx( - `${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(isMoneyRequestAction(action) ? getOriginalMessage(action)?.IOUTransactionID : undefined)}`, - { - selector: linkedTransactionRouteErrorSelector, - }, - ); + const transactionThreadReport = createTransactionThreadReport({ + introSelected, + currentUserLogin: currentUserPersonalDetails.login ?? '', + currentUserAccountID: currentUserPersonalDetails.accountID, + betas, + iouReport: report, + iouReportAction: action, + transaction, + }); + if (!transactionThreadReport?.reportID) { + return; + } - const stateValue = useMemo(() => ({shouldOpenReportInRHP: true}), []); - const actionsValue = useMemo(() => ({onPreviewPressed}), [onPreviewPressed]); + onPreviewPressed(transactionThreadReport.reportID); + }; - if (!action || !report) { + if (!action || !report || !transaction) { return null; } - const reportDraftMessage = draftMessage?.[action.reportActionID]; - const matchingDraftMessage = reportDraftMessage?.message; - return ( - - - - - - - + + + onSelectTransaction(transaction.transactionID)} + /> + + ); } diff --git a/src/pages/TransactionDuplicate/DuplicateTransactionsList.tsx b/src/pages/TransactionDuplicate/DuplicateTransactionsList.tsx index b77dcebc6f94..3f3f3ad19483 100644 --- a/src/pages/TransactionDuplicate/DuplicateTransactionsList.tsx +++ b/src/pages/TransactionDuplicate/DuplicateTransactionsList.tsx @@ -1,44 +1,37 @@ -import React, {useCallback} from 'react'; -import type {FlatListProps, ListRenderItemInfo, ScrollViewProps} from 'react-native'; +import React from 'react'; +import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; -import FlatList from '@components/FlatList/FlatList'; +import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import type {Transaction} from '@src/types/onyx'; import DuplicateTransactionItem from './DuplicateTransactionItem'; type DuplicateTransactionsListProps = { transactions: Array>; + selectedTransactionID?: string; + shouldShowSelection?: boolean; + onSelectTransaction: (transactionID: string) => void; onPreviewPressed: (reportID: string) => void; }; -const keyExtractor: FlatListProps>['keyExtractor'] = (item, index) => `${item?.transactionID}+${index}`; - -const maintainVisibleContentPosition: ScrollViewProps['maintainVisibleContentPosition'] = { - minIndexForVisible: 1, -}; - -function DuplicateTransactionsList({transactions, onPreviewPressed}: DuplicateTransactionsListProps) { +function DuplicateTransactionsList({transactions, selectedTransactionID, shouldShowSelection = true, onSelectTransaction, onPreviewPressed}: DuplicateTransactionsListProps) { const styles = useThemeStyles(); - - const renderItem = useCallback( - ({item, index}: ListRenderItemInfo>) => ( - - ), - [onPreviewPressed], - ); + const theme = useTheme(); return ( - + + {transactions.map((transaction, index) => ( + + ))} + ); } diff --git a/src/pages/TransactionDuplicate/Review.tsx b/src/pages/TransactionDuplicate/Review.tsx index b00113b985f7..f01f7ffbabe4 100644 --- a/src/pages/TransactionDuplicate/Review.tsx +++ b/src/pages/TransactionDuplicate/Review.tsx @@ -1,13 +1,15 @@ import {useFocusEffect, useRoute} from '@react-navigation/native'; -import React, {useEffect, useRef} from 'react'; +import React, {useCallback, useEffect, useRef, useState} from 'react'; import {View} from 'react-native'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import Button from '@components/Button'; import ConfirmationPage from '@components/ConfirmationPage'; +import FixedFooter from '@components/FixedFooter'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView'; import ReportHeaderSkeletonView from '@components/ReportHeaderSkeletonView'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; @@ -26,6 +28,7 @@ import {getLinkedTransactionID, getReportAction} from '@libs/ReportActionsUtils' import {isReportIDApproved, isSettled} from '@libs/ReportUtils'; import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; import {doesDeleteNavigateBackUrlIncludeSpecificDuplicatesReview, getParentReportActionDeletionStatus, hasLoadedReportActions, isThreadReportDeleted} from '@libs/TransactionNavigationUtils'; +import {getReviewNavigationRoute} from '@libs/TransactionPreviewUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -71,6 +74,14 @@ function TransactionDuplicateReview() { } } transactions.sort((a, b) => new Date(a?.created ?? '').getTime() - new Date(b?.created ?? '').getTime()); + const [selectedTransactionID, setSelectedTransactionID] = useState(transactionID); + const defaultSelectedTransactionID = transactionID ?? transactions.at(0)?.transactionID; + const effectiveSelectedTransactionID = transactions.some((transaction) => transaction.transactionID === selectedTransactionID) ? selectedTransactionID : defaultSelectedTransactionID; + const selectedTransaction = transactions.find((transaction) => transaction.transactionID === effectiveSelectedTransactionID); + const [selectedTransactionReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${selectedTransaction?.reportID}`); + const [selectedTransactionPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${selectedTransactionReport?.policyID}`); + const [selectedTransactionPolicyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${selectedTransactionReport?.policyID}`); + const [selectedTransactionPolicyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${selectedTransactionReport?.policyID}`); const hasSettledOrApprovedTransaction = transactions.some((transaction) => isSettled(transaction?.reportID) || isReportIDApproved(transaction?.reportID)); const hasLoadedThreadReportActions = hasLoadedReportActions(reportLoadingState, isOffline); @@ -120,21 +131,25 @@ function TransactionDuplicateReview() { clearDeleteTransactionNavigateBackUrl(); }, [isDeleteNavigateBackToThisReview, wasTransactionDeleted]); - useFocusEffect(() => { - return () => { - if (!deleteTransactionNavigateBackUrl) { + useFocusEffect( + useCallback(() => { + return () => { + if (!deleteTransactionNavigateBackUrl) { + return; + } + clearDeleteTransactionNavigateBackUrl(); + }; + }, [deleteTransactionNavigateBackUrl]), + ); + + useFocusEffect( + useCallback(() => { + if (!originalTransactionIDsListRef.current) { return; } - clearDeleteTransactionNavigateBackUrl(); - }; - }); - - useFocusEffect(() => { - if (!originalTransactionIDsListRef.current) { - return; - } - setActiveTransactionIDs(originalTransactionIDsListRef.current); - }); + setActiveTransactionIDs(originalTransactionIDsListRef.current); + }, []), + ); const onPreviewPressed = (reportID: string) => { const siblingTransactionIDsList = transactions.map((transaction) => transaction.transactionID); @@ -160,6 +175,25 @@ function TransactionDuplicateReview() { Navigation.goBack(); }; + const keepSelected = () => { + if (!selectedTransaction || !selectedTransactionReport) { + return; + } + + Navigation.navigate( + getReviewNavigationRoute( + Navigation.getActiveRoute(), + route.params.threadReportID, + selectedTransaction, + transactions.filter((transaction) => transaction.transactionID !== selectedTransaction.transactionID), + selectedTransactionPolicy, + selectedTransactionPolicyCategories, + selectedTransactionPolicyTags ?? {}, + selectedTransactionReport, + ), + ); + }; + if (isLoadingPage) { return ( @@ -195,23 +229,48 @@ function TransactionDuplicateReview() { } return ( - + Navigation.goBack(route.params.backTo)} /> - -