Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ type TransactionItemRowNarrowProps = Pick<
| 'isInSingleTransactionReport'
| 'shouldShowRadioButton'
| 'onRadioButtonPress'
| 'shouldStopRadioButtonMouseDownPropagation'
| 'radioButtonContainerStyle'
| 'radioButtonWrapperStyle'
| 'shouldShowErrors'
| 'isDisabled'
| 'violations'
Expand All @@ -51,6 +54,9 @@ function TransactionItemRowNarrow({
isInSingleTransactionReport = false,
shouldShowRadioButton = false,
onRadioButtonPress = () => {},
shouldStopRadioButtonMouseDownPropagation = false,
radioButtonContainerStyle,
radioButtonWrapperStyle,
shouldShowErrors = true,
isDisabled = false,
violations,
Expand Down Expand Up @@ -149,12 +155,14 @@ function TransactionItemRowNarrow({
</View>
)}
{shouldShowRadioButton && (
<View style={[styles.ml3, styles.justifyContentCenter]}>
<View style={[styles.ml3, styles.justifyContentCenter, radioButtonContainerStyle]}>
<RadioButton
isChecked={isSelected}
disabled={isDisabled}
onPress={() => onRadioButtonPress?.(transactionItem.transactionID)}
accessibilityLabel={CONST.ROLE.RADIO}
shouldStopMouseDownPropagation={shouldStopRadioButtonMouseDownPropagation}
style={radioButtonWrapperStyle}
/>
</View>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ function TransactionItemRowWide({
isInSingleTransactionReport = false,
shouldShowRadioButton = false,
onRadioButtonPress = () => {},
shouldStopRadioButtonMouseDownPropagation = false,
radioButtonContainerStyle,
shouldShowErrors = true,
isDisabled = false,
violations,
Expand Down Expand Up @@ -592,12 +594,13 @@ function TransactionItemRowWide({
)}
{columns?.map(renderColumn)}
{shouldShowRadioButton && (
<View style={[styles.ml1, styles.justifyContentCenter]}>
<View style={[styles.ml1, styles.justifyContentCenter, radioButtonContainerStyle]}>
<RadioButton
isChecked={isSelected}
disabled={isDisabled}
onPress={() => onRadioButtonPress?.(transactionItem.transactionID)}
accessibilityLabel={CONST.ROLE.RADIO}
shouldStopMouseDownPropagation={shouldStopRadioButtonMouseDownPropagation}
/>
</View>
)}
Expand Down
8 changes: 8 additions & 0 deletions src/components/TransactionItemRow/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ function TransactionItemRow({
isInSingleTransactionReport = false,
shouldShowRadioButton = false,
onRadioButtonPress = () => {},
shouldStopRadioButtonMouseDownPropagation = false,
radioButtonContainerStyle,
radioButtonWrapperStyle,
shouldShowErrors = true,
shouldHighlightItemWhenSelected = true,
isDisabled = false,
Expand Down Expand Up @@ -145,6 +148,9 @@ function TransactionItemRow({
isInSingleTransactionReport,
shouldShowRadioButton,
onRadioButtonPress,
shouldStopRadioButtonMouseDownPropagation,
radioButtonContainerStyle,
radioButtonWrapperStyle,
shouldShowErrors,
isDisabled,
violations,
Expand Down Expand Up @@ -191,6 +197,8 @@ function TransactionItemRow({
isInSingleTransactionReport,
shouldShowRadioButton,
onRadioButtonPress,
shouldStopRadioButtonMouseDownPropagation,
radioButtonContainerStyle,
shouldShowErrors,
shouldHighlightItemWhenSelected,
isDisabled,
Expand Down
3 changes: 3 additions & 0 deletions src/components/TransactionItemRow/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ type TransactionItemRowProps = {
isInSingleTransactionReport?: boolean;
shouldShowRadioButton?: boolean;
onRadioButtonPress?: (transactionID: string) => void;
shouldStopRadioButtonMouseDownPropagation?: boolean;
radioButtonContainerStyle?: StyleProp<ViewStyle>;
radioButtonWrapperStyle?: StyleProp<ViewStyle>;
shouldShowErrors?: boolean;
shouldHighlightItemWhenSelected?: boolean;
isDisabled?: boolean;
Expand Down
1 change: 1 addition & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1523,6 +1523,7 @@ const translations: TranslationDeepObject<typeof en> = {
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',
Expand Down
1 change: 1 addition & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1577,6 +1577,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',
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1484,6 +1484,7 @@ const translations: TranslationDeepObject<typeof en> = {
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',
Expand Down
1 change: 1 addition & 0 deletions src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1528,6 +1528,7 @@ const translations: TranslationDeepObject<typeof en> = {
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é',
Expand Down
1 change: 1 addition & 0 deletions src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1522,6 +1522,7 @@ const translations: TranslationDeepObject<typeof en> = {
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',
Expand Down
1 change: 1 addition & 0 deletions src/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1503,6 +1503,7 @@ const translations: TranslationDeepObject<typeof en> = {
someDuplicatesArePaid: 'これらの重複の一部は、すでに承認または支払い済みです。',
reviewDuplicates: '重複を確認',
keepAll: 'すべて保持',
keepSelected: '選択したものを保持',
noDuplicatesTitle: '準備完了!',
noDuplicatesDescription: '確認が必要な重複取引はありません。',
confirmApprove: '承認金額を確認',
Expand Down
1 change: 1 addition & 0 deletions src/languages/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1518,6 +1518,7 @@ const translations: TranslationDeepObject<typeof en> = {
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',
Expand Down
1 change: 1 addition & 0 deletions src/languages/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1517,6 +1517,7 @@ const translations: TranslationDeepObject<typeof en> = {
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',
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 @@ -1516,6 +1516,7 @@ const translations: TranslationDeepObject<typeof en> = {
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',
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 @@ -1470,6 +1470,7 @@ const translations: TranslationDeepObject<typeof en> = {
someDuplicatesArePaid: '其中一些重复项已被批准或支付。',
reviewDuplicates: '审核重复项',
keepAll: '全部保留',
keepSelected: '保留所选项',
noDuplicatesTitle: '全部完成!',
noDuplicatesDescription: '这里没有需要审核的重复交易。',
confirmApprove: '确认批准金额',
Expand Down
124 changes: 75 additions & 49 deletions src/pages/TransactionDuplicate/DuplicateTransactionItem.tsx
Original file line number Diff line number Diff line change
@@ -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<Transaction>;
index: number;
isLastItem: boolean;
isSelected: boolean;
shouldShowSelection?: boolean;
onSelectTransaction: (transactionID: string) => void;
onPreviewPressed: (reportID: string) => void;
};

const linkedTransactionRouteErrorSelector = (transaction: OnyxEntry<Transaction>) => 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 (
<View style={styles.pb2}>
<ReportActionItemStateContext.Provider value={stateValue}>
<ReportActionItemActionsContext.Provider value={actionsValue}>
<ReportActionItem
action={action}
report={report}
parentReportAction={getReportAction(report?.parentReportID, report?.parentReportActionID)}
index={index}
displayAsGroup={false}
shouldDisplayNewMarker={false}
isFirstVisibleReportAction={false}
shouldDisplayContextMenu={false}
personalDetails={personalDetails}
draftMessage={matchingDraftMessage}
linkedTransactionRouteError={linkedTransactionRouteError}
/>
</ReportActionItemActionsContext.Provider>
</ReportActionItemStateContext.Provider>
</View>
<OfflineWithFeedback pendingAction={transaction.pendingAction}>
<PressableWithFeedback
sentryLabel={CONST.SENTRY_LABEL.SEARCH.TRANSACTION_LIST_ITEM}
onPress={handlePreviewPress}
accessibilityLabel={transaction.comment?.comment ?? ''}
Comment on lines +73 to +76
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Prevent radio taps from triggering preview navigation

The row-level PressableWithFeedback always calls handlePreviewPress, but the new radio selector lives inside this same pressable and there is no propagation guard, so clicking the radio can also fire the parent press handler. In web/mWeb this means choosing a duplicate immediately navigates into the transaction preview instead of just selecting it, which breaks the new “Keep selected” flow unless users avoid the radio input. Please stop the radio interaction from bubbling to the parent press action.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed. I stopped the duplicate-review radio interaction from bubbling to the row press, so selecting a radio no longer opens the preview.

role={getButtonRole(true)}
isNested
hoverStyle={styles.hoveredComponentBG}
style={!isLastItem && styles.borderBottom}
>
<TransactionItemRow
transactionItem={transaction as TransactionListItemType}
report={report}
policy={policy}
shouldUseNarrowLayout
isSelected={isSelected}
shouldShowTooltip={false}
dateColumnSize={CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL}
amountColumnSize={CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL}
taxAmountColumnSize={CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL}
shouldHighlightItemWhenSelected={false}
shouldShowErrors={false}
style={[styles.p4, shouldShowSelection ? styles.pr0 : styles.pr4]}
shouldShowRadioButton={shouldShowSelection}
shouldStopRadioButtonMouseDownPropagation
radioButtonContainerStyle={styles.ml0}
radioButtonWrapperStyle={[styles.justifyContentCenter, styles.pr3half, {paddingLeft: 10, height: variables.w44}]}
onRadioButtonPress={() => onSelectTransaction(transaction.transactionID)}
/>
</PressableWithFeedback>
</OfflineWithFeedback>
);
}

Expand Down
49 changes: 21 additions & 28 deletions src/pages/TransactionDuplicate/DuplicateTransactionsList.tsx
Original file line number Diff line number Diff line change
@@ -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<OnyxEntry<Transaction>>;
selectedTransactionID?: string;
shouldShowSelection?: boolean;
onSelectTransaction: (transactionID: string) => void;
onPreviewPressed: (reportID: string) => void;
};

const keyExtractor: FlatListProps<OnyxEntry<Transaction>>['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<OnyxEntry<Transaction>>) => (
<DuplicateTransactionItem
transaction={item}
index={index}
onPreviewPressed={onPreviewPressed}
/>
),
[onPreviewPressed],
);
const theme = useTheme();

return (
<FlatList
data={transactions}
renderItem={renderItem}
keyExtractor={keyExtractor}
maintainVisibleContentPosition={maintainVisibleContentPosition}
contentContainerStyle={styles.pt5}
/>
<View style={[styles.expenseWidgetRadius, styles.overflowHidden, {backgroundColor: theme.cardBG}]}>
{transactions.map((transaction, index) => (
<DuplicateTransactionItem
key={transaction?.transactionID ?? transaction?.created ?? 'duplicate-transaction'}
transaction={transaction}
isLastItem={index === transactions.length - 1}
isSelected={transaction?.transactionID === selectedTransactionID}
shouldShowSelection={shouldShowSelection}
onSelectTransaction={onSelectTransaction}
onPreviewPressed={onPreviewPressed}
/>
))}
</View>
);
}

Expand Down
Loading
Loading