-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Expand file tree
/
Copy pathMoneyRequestReportTransactionItem.tsx
More file actions
325 lines (276 loc) · 13.9 KB
/
Copy pathMoneyRequestReportTransactionItem.tsx
File metadata and controls
325 lines (276 loc) · 13.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
import {getButtonRole} from '@components/Button/utils';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import {PressableWithFeedback} from '@components/Pressable';
import type {SearchColumnType, TableColumnSize} from '@components/Search/types';
import TransactionItemRow from '@components/TransactionItemRow';
import {useEditingCellState} from '@components/TransactionItemRow/EditableCell';
import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle';
import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useResponsiveLayoutOnWideRHP from '@hooks/useResponsiveLayoutOnWideRHP';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useTransactionInlineEdit from '@hooks/useTransactionInlineEdit';
import ControlSelection from '@libs/ControlSelection';
import canUseTouchScreen from '@libs/DeviceCapabilities/canUseTouchScreen';
import {hasFlexColumn} from '@libs/SearchUIUtils';
import {getTransactionPendingAction, isTransactionPendingDelete} from '@libs/TransactionUtils';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import type {CardList, Policy, PolicyCategories, PolicyTagLists, Report, TransactionViolations} from '@src/types/onyx';
import type {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import React, {useEffect, useRef, useState} from 'react';
import type {TransactionWithOptionalHighlight} from './MoneyRequestReportTransactionList';
type MoneyRequestReportTransactionItemProps = {
/** The transaction that is being displayed */
transaction: TransactionWithOptionalHighlight;
/** Pre-filtered violations for this transaction. Computed once at the parent so each row doesn't subscribe to Onyx individually. */
violations: TransactionViolations;
/** Report to which the transaction belongs */
report: Report;
/** Policy to which the transaction belongs */
policy: OnyxEntry<Policy>;
/** Categories for the policy to which the transaction belongs */
policyCategories?: PolicyCategories;
/** Tag lists for the policy to which the transaction belongs */
policyTagLists?: PolicyTagLists;
/** Whether the mobile selection mode is enabled */
isSelectionModeEnabled: boolean;
/** Callback function triggered upon pressing a transaction checkbox. */
toggleTransaction: (transactionID: string) => void;
/** Callback function triggered upon pressing a transaction. */
handleOnPress: (transactionID: string) => void;
/** Callback function triggered upon long pressing a transaction. */
handleLongPress: (transactionID: string) => void;
/** Whether the transaction is selected */
isSelected: boolean;
/** The size of the date column */
dateColumnSize: TableColumnSize;
/** The size of the amount column */
amountColumnSize: TableColumnSize;
/** The size of the tax amount column */
taxAmountColumnSize: TableColumnSize;
/** Columns to show */
columns: SearchColumnType[];
/** Callback function that scrolls to this transaction in case it is newly added */
scrollToNewTransaction?: (offset: number) => void;
/** Callback function that navigates to the transaction thread */
onArrowRightPress?: (transactionID: string) => void;
/** Whether this transaction should be highlighted as newly added */
shouldBeHighlighted: boolean;
/** List of cards for the user */
nonPersonalAndWorkspaceCards: CardList;
/** Whether this is the last item in the list */
isLastItem?: boolean;
/** Whether the list is horizontally scrollable */
shouldScrollHorizontally?: boolean;
/** Precomputed transaction-thread report ID for this transaction. Lets the RBR row early-return for clean rows
* instead of mounting the heavy RBR inner; the parent computes it once so rows don't scan report actions individually. */
transactionThreadReportID?: string;
};
type MoneyRequestReportTransactionItemBodyProps = MoneyRequestReportTransactionItemProps & {
/** Inline-edit values from `useTransactionInlineEdit`. Undefined on narrow layouts where the hook is skipped. */
inlineEdit?: InlineEditValues;
/** Highlight animation style, computed by the parent so its state survives the narrow↔wide swap on resize. */
animatedHighlightStyle: ReturnType<typeof useAnimatedHighlightStyle>;
};
function MoneyRequestReportTransactionItemBody({
transaction,
violations,
report,
policy,
policyCategories,
policyTagLists,
isSelectionModeEnabled,
toggleTransaction,
isSelected,
handleOnPress,
handleLongPress,
columns,
dateColumnSize,
amountColumnSize,
taxAmountColumnSize,
scrollToNewTransaction,
onArrowRightPress,
shouldBeHighlighted,
nonPersonalAndWorkspaceCards,
isLastItem = false,
shouldScrollHorizontally = false,
transactionThreadReportID,
inlineEdit,
animatedHighlightStyle,
}: MoneyRequestReportTransactionItemBodyProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const {isEditingCell, wasRecentlyEditingCell} = useEditingCellState();
const [shouldDisableHoverStyle, setShouldDisableHoverStyle] = useState(false);
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
const {isSmallScreenWidth, isMediumScreenWidth} = useResponsiveLayout();
const {shouldUseNarrowLayout} = useResponsiveLayoutOnWideRHP();
const isPendingDelete = isTransactionPendingDelete(transaction);
const pendingAction = getTransactionPendingAction(transaction);
// On narrow layouts `inlineEdit` is undefined (the parent skips the hook). The fallback ref
// keeps the press handler shape identical without ever being mutated on narrow.
const fallbackEditingOnMouseDownRef = useRef(false);
const wasEditingOnMouseDownRef = inlineEdit?.wasEditingOnMouseDownRef ?? fallbackEditingOnMouseDownRef;
const viewRef = useRef<View>(null);
// This useEffect scrolls to this transaction when it is newly added to the report
useEffect(() => {
if (!shouldBeHighlighted || !scrollToNewTransaction) {
return;
}
viewRef?.current?.measure((x, y, width, height, pageX, pageY) => {
scrollToNewTransaction?.(pageY);
});
}, [scrollToNewTransaction, shouldBeHighlighted]);
useEffect(() => {
if (!wasRecentlyEditingCell) {
return;
}
queueMicrotask(() => setShouldDisableHoverStyle(true));
}, [wasRecentlyEditingCell]);
const handleMouseDown = (e?: React.MouseEvent) => {
wasEditingOnMouseDownRef.current = isEditingCell;
if (!isEditingCell) {
e?.preventDefault();
}
};
const handleHoverIn = () => setShouldDisableHoverStyle(false);
return (
<OfflineWithFeedback
pendingAction={pendingAction}
errors={transaction.errors}
errorRowStyles={[styles.ph3, styles.pb2]}
style={!shouldUseNarrowLayout && isLastItem && [styles.tableBottomRadius, styles.overflowHidden]}
>
<PressableWithFeedback
key={transaction.transactionID}
onPress={() => {
// Prevent row press from firing while a cell is being inline-edited (e.g. pressing Space would otherwise open the expense)
// See https://github.com/Expensify/App/issues/88646 for more details
if (isEditingCell) {
return;
}
// If a cell was being edited when the user tapped the row, suppress navigation
// so the second tap doesn't immediately open the transaction detail.
if (wasEditingOnMouseDownRef.current) {
wasEditingOnMouseDownRef.current = false;
return;
}
handleOnPress(transaction.transactionID);
}}
accessibilityLabel={translate('iou.viewDetails')}
sentryLabel={CONST.SENTRY_LABEL.REPORT.MONEY_REQUEST_REPORT_TRANSACTION_ITEM}
role={getButtonRole(true)}
isNested
id={transaction.transactionID}
style={[styles.transactionListItemStyle, !shouldUseNarrowLayout ? StyleUtils.getSearchTableRowPressableStyle(isLastItem, isSelected) : styles.noBorderRadius]}
hoverStyle={[!isPendingDelete && !shouldDisableHoverStyle && styles.hoveredComponentBG, isSelected && styles.activeComponentBG]}
dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}}
onMouseDown={handleMouseDown}
onHoverIn={handleHoverIn}
onPressIn={() => {
wasEditingOnMouseDownRef.current = wasEditingOnMouseDownRef.current || isEditingCell;
if (canUseTouchScreen()) {
ControlSelection.block();
}
}}
onPressOut={() => ControlSelection.unblock()}
onLongPress={() => {
handleLongPress(transaction.transactionID);
}}
disabled={isTransactionPendingDelete(transaction)}
ref={viewRef}
wrapperStyle={[animatedHighlightStyle, styles.userSelectNone, shouldUseNarrowLayout && !isLastItem && StyleUtils.getSelectedBorderBottomStyle(isSelected)]}
>
{({hovered}) => (
<TransactionItemRow
transactionItem={transaction}
violations={violations}
report={report}
policy={policy}
policyCategories={policyCategories}
policyTagLists={policyTagLists}
transactionThreadReportID={transactionThreadReportID}
isSelected={isSelected}
dateColumnSize={dateColumnSize}
amountColumnSize={amountColumnSize}
taxAmountColumnSize={taxAmountColumnSize}
shouldShowTooltip
shouldUseNarrowLayout={shouldUseNarrowLayout || (isMediumScreenWidth && !shouldScrollHorizontally)}
shouldShowCheckbox={!!isSelectionModeEnabled || !isSmallScreenWidth}
onCheckboxPress={toggleTransaction}
columns={columns}
isDisabled={isPendingDelete}
style={!shouldUseNarrowLayout ? [styles.p3, styles.pv2, styles.noBorderRadius] : [styles.p4, styles.noBorderRadius]}
onButtonPress={() => {
handleOnPress(transaction.transactionID);
}}
onArrowRightPress={() => onArrowRightPress?.(transaction.transactionID)}
isHover={hovered}
nonPersonalAndWorkspaceCards={nonPersonalAndWorkspaceCards}
shouldRemoveTotalColumnFlex={hasFlexColumn(columns)}
canEditDate={inlineEdit?.canEditDate}
canEditMerchant={inlineEdit?.canEditMerchant}
canEditDescription={inlineEdit?.canEditDescription}
canEditCategory={inlineEdit?.canEditCategory}
canEditAmount={inlineEdit?.canEditAmount}
canEditTag={inlineEdit?.canEditTag}
onEditDate={inlineEdit?.onEditDate}
onEditMerchant={inlineEdit?.onEditMerchant}
onEditDescription={inlineEdit?.onEditDescription}
onEditCategory={inlineEdit?.onEditCategory}
onEditAmount={inlineEdit?.onEditAmount}
onEditTag={inlineEdit?.onEditTag}
/>
)}
</PressableWithFeedback>
</OfflineWithFeedback>
);
}
type InlineEditValues = ReturnType<typeof useTransactionInlineEdit>;
function MoneyRequestReportTransactionItemWithInlineEdit(props: Omit<MoneyRequestReportTransactionItemBodyProps, 'inlineEdit'>) {
const inlineEdit = useTransactionInlineEdit({
transactionID: props.transaction.transactionID,
});
return (
<MoneyRequestReportTransactionItemBody
{...props}
inlineEdit={inlineEdit}
/>
);
}
function MoneyRequestReportTransactionItem(props: MoneyRequestReportTransactionItemProps) {
const {isMediumScreenWidth} = useResponsiveLayout();
const {shouldUseNarrowLayout} = useResponsiveLayoutOnWideRHP();
const theme = useTheme();
// Mirrors the layout check inside TransactionItemRow so the narrow body never pays for useTransactionInlineEdit.
const isNarrowLayout = shouldUseNarrowLayout || (isMediumScreenWidth && !props.shouldScrollHorizontally);
// Hoisted out of the body so the highlight animation timeline survives the narrow↔wide
// component-type swap caused by browser resize.
const animatedHighlightStyle = useAnimatedHighlightStyle({
borderRadius: shouldUseNarrowLayout ? variables.componentBorderRadius : 0,
shouldHighlight: props.shouldBeHighlighted,
highlightColor: theme.messageHighlightBG,
backgroundColor: theme.highlightBG,
shouldApplyOtherStyles: !shouldUseNarrowLayout,
});
if (isNarrowLayout) {
return (
<MoneyRequestReportTransactionItemBody
{...props}
animatedHighlightStyle={animatedHighlightStyle}
/>
);
}
return (
<MoneyRequestReportTransactionItemWithInlineEdit
{...props}
animatedHighlightStyle={animatedHighlightStyle}
/>
);
}
export default MoneyRequestReportTransactionItem;