Skip to content

Commit 31fe5b8

Browse files
authored
Merge pull request #59999 from software-mansion-labs/jnowakow/simplified-actions-on-header-v2
Simplified Actions on Report Header and Preview V2
2 parents 930d1c0 + 44e1cad commit 31fe5b8

32 files changed

+3592
-849
lines changed

src/CONST.ts

+18
Original file line numberDiff line numberDiff line change
@@ -1189,6 +1189,24 @@ const CONST = {
11891189
REVIEW_DUPLICATES: 'reviewDuplicates',
11901190
MARK_AS_CASH: 'markAsCash',
11911191
},
1192+
TRANSACTION_PRIMARY_ACTIONS: {
1193+
REMOVE_HOLD: 'removeHold',
1194+
REVIEW_DUPLICATES: 'reviewDuplicates',
1195+
MARK_AS_CASH: 'markAsCash',
1196+
},
1197+
REPORT_PREVIEW_ACTIONS: {
1198+
VIEW: 'view',
1199+
REVIEW: 'review',
1200+
SUBMIT: 'submit',
1201+
APPROVE: 'approve',
1202+
PAY: 'pay',
1203+
EXPORT_TO_ACCOUNTING: 'exportToAccounting',
1204+
},
1205+
TRANSACTION_SECONDARY_ACTIONS: {
1206+
HOLD: 'hold',
1207+
VIEW_DETAILS: 'viewDetails',
1208+
DELETE: 'delete',
1209+
},
11921210
ACTIONS: {
11931211
LIMIT: 50,
11941212
// OldDot Actions render getMessage from Web-Expensify/lib/Report/Action PHP files via getMessageOfOldDotReportAction in ReportActionsUtils.ts

src/components/AvatarWithDisplayName.tsx

+8-11
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ import ROUTES from '@src/ROUTES';
3030
import type {Policy, Report} from '@src/types/onyx';
3131
import type {Icon} from '@src/types/onyx/OnyxCommon';
3232
import {getButtonRole} from './Button/utils';
33-
import CaretWrapper from './CaretWrapper';
3433
import DisplayNames from './DisplayNames';
3534
import {FallbackAvatar} from './Icon/Expensicons';
3635
import MultipleAvatars from './MultipleAvatars';
@@ -155,16 +154,14 @@ function AvatarWithDisplayName({policy, report, isAnonymous = false, size = CONS
155154
</View>
156155
</PressableWithoutFeedback>
157156
<View style={[styles.flex1, styles.flexColumn]}>
158-
<CaretWrapper>
159-
<DisplayNames
160-
fullTitle={title}
161-
displayNamesWithTooltips={displayNamesWithTooltips}
162-
tooltipEnabled
163-
numberOfLines={1}
164-
textStyles={[isAnonymous ? styles.headerAnonymousFooter : styles.headerText, styles.pre]}
165-
shouldUseFullTitle={isMoneyRequestOrReport || isAnonymous}
166-
/>
167-
</CaretWrapper>
157+
<DisplayNames
158+
fullTitle={title}
159+
displayNamesWithTooltips={displayNamesWithTooltips}
160+
tooltipEnabled
161+
numberOfLines={1}
162+
textStyles={[isAnonymous ? styles.headerAnonymousFooter : styles.headerText, styles.pre]}
163+
shouldUseFullTitle={isMoneyRequestOrReport || isAnonymous}
164+
/>
168165
{Object.keys(parentNavigationSubtitleData).length > 0 && (
169166
<ParentNavigationSubtitle
170167
parentNavigationSubtitleData={parentNavigationSubtitleData}

src/components/MoneyReportHeader.tsx

+397-227
Large diffs are not rendered by default.

src/components/MoneyReportHeaderOld.tsx

+730
Large diffs are not rendered by default.

src/components/MoneyRequestHeader.tsx

+149-47
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
import {useRoute} from '@react-navigation/native';
22
import type {ReactNode} from 'react';
3-
import React, {useCallback, useEffect} from 'react';
3+
import React, {useCallback, useEffect, useMemo, useState} from 'react';
44
import {View} from 'react-native';
55
import type {OnyxEntry} from 'react-native-onyx';
66
import {useOnyx} from 'react-native-onyx';
7+
import type {ValueOf} from 'type-fest';
78
import useLocalize from '@hooks/useLocalize';
89
import useResponsiveLayout from '@hooks/useResponsiveLayout';
910
import useTheme from '@hooks/useTheme';
1011
import useThemeStyles from '@hooks/useThemeStyles';
1112
import useTransactionViolations from '@hooks/useTransactionViolations';
13+
import {deleteMoneyRequest, getNavigationUrlOnMoneyRequestDelete} from '@libs/actions/IOU';
1214
import Navigation from '@libs/Navigation/Navigation';
1315
import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils';
16+
import {getTransactionThreadPrimaryAction} from '@libs/ReportPrimaryActionUtils';
17+
import {getSecondaryTransactionThreadActions} from '@libs/ReportSecondaryActionUtils';
18+
import {changeMoneyRequestHoldStatus, navigateBackOnDeleteTransaction, navigateToDetailsPage} from '@libs/ReportUtils';
1419
import {
15-
checkIfShouldShowMarkAsCashButton,
1620
hasPendingRTERViolation as hasPendingRTERViolationTransactionUtils,
1721
hasReceipt,
1822
isDuplicate as isDuplicateTransactionUtils,
@@ -33,6 +37,10 @@ import type IconAsset from '@src/types/utils/IconAsset';
3337
import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
3438
import BrokenConnectionDescription from './BrokenConnectionDescription';
3539
import Button from './Button';
40+
import ButtonWithDropdownMenu from './ButtonWithDropdownMenu';
41+
import type {DropdownOption} from './ButtonWithDropdownMenu/types';
42+
import ConfirmModal from './ConfirmModal';
43+
import DecisionModal from './DecisionModal';
3644
import HeaderWithBackButton from './HeaderWithBackButton';
3745
import Icon from './Icon';
3846
import * as Expensicons from './Icon/Expensicons';
@@ -68,6 +76,8 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre
6876
);
6977
const transactionViolations = useTransactionViolations(transaction?.transactionID);
7078

79+
const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false);
80+
const [downloadErrorModalVisible, setDownloadErrorModalVisible] = useState(false);
7181
const [dismissedHoldUseExplanation, dismissedHoldUseExplanationResult] = useOnyx(ONYXKEYS.NVP_DISMISSED_HOLD_USE_EXPLANATION, {initialValue: true});
7282
const [isLoadingReportData] = useOnyx(ONYXKEYS.IS_LOADING_REPORT_DATA);
7383
const isLoadingHoldUseExplained = isLoadingOnyxValue(dismissedHoldUseExplanationResult);
@@ -85,7 +95,6 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre
8595
const hasPendingRTERViolation = hasPendingRTERViolationTransactionUtils(transactionViolations);
8696

8797
const shouldShowBrokenConnectionViolation = shouldShowBrokenConnectionViolationTransactionUtils(parentReport, policy, transactionViolations);
88-
const shouldShowMarkAsCashButton = checkIfShouldShowMarkAsCashButton(hasPendingRTERViolation, shouldShowBrokenConnectionViolation, parentReport, policy);
8998

9099
const markAsCash = useCallback(() => {
91100
markAsCashAction(transaction?.transactionID, reportID);
@@ -144,12 +153,89 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre
144153
Navigation.navigate(ROUTES.PROCESS_MONEY_REQUEST_HOLD.getRoute(Navigation.getReportRHPActiveRoute()));
145154
}, [dismissedHoldUseExplanation, isLoadingHoldUseExplained, isOnHold]);
146155

156+
const primaryAction = useMemo(() => {
157+
if (!report || !parentReport || !transaction) {
158+
return '';
159+
}
160+
return getTransactionThreadPrimaryAction(report, parentReport, transaction, transactionViolations, policy);
161+
}, [parentReport, policy, report, transaction, transactionViolations]);
162+
163+
const primaryActionImplementation = {
164+
[CONST.REPORT.TRANSACTION_PRIMARY_ACTIONS.REMOVE_HOLD]: (
165+
<Button
166+
success
167+
text={translate('iou.unhold')}
168+
onPress={() => {
169+
changeMoneyRequestHoldStatus(parentReportAction);
170+
}}
171+
/>
172+
),
173+
[CONST.REPORT.TRANSACTION_PRIMARY_ACTIONS.REVIEW_DUPLICATES]: (
174+
<Button
175+
success
176+
text={translate('iou.reviewDuplicates')}
177+
onPress={() => {
178+
if (!reportID) {
179+
return;
180+
}
181+
Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_PAGE.getRoute(reportID, Navigation.getReportRHPActiveRoute()));
182+
}}
183+
/>
184+
),
185+
[CONST.REPORT.TRANSACTION_PRIMARY_ACTIONS.MARK_AS_CASH]: (
186+
<Button
187+
success
188+
text={translate('iou.markAsCash')}
189+
onPress={markAsCash}
190+
/>
191+
),
192+
};
193+
194+
const secondaryActions = useMemo(() => {
195+
if (!parentReport || !transaction) {
196+
return [];
197+
}
198+
return getSecondaryTransactionThreadActions(parentReport, transaction);
199+
}, [parentReport, transaction]);
200+
201+
const secondaryActionsImplementation: Record<ValueOf<typeof CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS>, DropdownOption<ValueOf<typeof CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS>>> = {
202+
[CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.HOLD]: {
203+
text: translate('iou.hold'),
204+
icon: Expensicons.Stopwatch,
205+
value: CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.HOLD,
206+
onSelected: () => {
207+
if (!parentReportAction) {
208+
throw new Error('Parent action does not exist');
209+
}
210+
211+
changeMoneyRequestHoldStatus(parentReportAction);
212+
},
213+
},
214+
[CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.VIEW_DETAILS]: {
215+
value: CONST.REPORT.SECONDARY_ACTIONS.VIEW_DETAILS,
216+
text: translate('iou.viewDetails'),
217+
icon: Expensicons.Info,
218+
onSelected: () => {
219+
navigateToDetailsPage(report, Navigation.getReportRHPActiveRoute());
220+
},
221+
},
222+
[CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.DELETE]: {
223+
text: translate('common.delete'),
224+
icon: Expensicons.Trashcan,
225+
value: CONST.REPORT.SECONDARY_ACTIONS.DELETE,
226+
onSelected: () => {
227+
setIsDeleteModalVisible(true);
228+
},
229+
},
230+
};
231+
232+
const applicableSecondaryActions = secondaryActions.map((action) => secondaryActionsImplementation[action]);
233+
147234
return (
148235
<View style={[styles.pl0, styles.borderBottom]}>
149236
<HeaderWithBackButton
150237
shouldShowBorderBottom={false}
151238
shouldShowReportAvatarWithDisplay
152-
shouldEnableDetailPageNavigation
153239
shouldShowPinButton={false}
154240
report={
155241
reportID
@@ -164,53 +250,39 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre
164250
shouldShowBackButton={shouldUseNarrowLayout}
165251
shouldDisplaySearchRouter={shouldDisplaySearchRouter}
166252
onBackButtonPress={onBackButtonPress}
253+
shouldEnableDetailPageNavigation
167254
>
168-
{shouldShowMarkAsCashButton && !shouldUseNarrowLayout && (
169-
<Button
170-
success
171-
text={translate('iou.markAsCash')}
172-
style={[styles.p0]}
173-
onPress={markAsCash}
174-
/>
175-
)}
176-
{isDuplicate && !shouldUseNarrowLayout && (
177-
<Button
178-
success
179-
text={translate('iou.reviewDuplicates')}
180-
style={[styles.p0, styles.ml2]}
181-
onPress={() => {
182-
if (!reportID) {
183-
return;
184-
}
185-
Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_PAGE.getRoute(reportID, Navigation.getReportRHPActiveRoute()));
186-
}}
187-
/>
255+
{!shouldUseNarrowLayout && (
256+
<View style={[styles.flexRow, styles.gap2]}>
257+
{!!primaryAction && primaryActionImplementation[primaryAction]}
258+
{!!applicableSecondaryActions.length && (
259+
<ButtonWithDropdownMenu
260+
success={false}
261+
onPress={() => {}}
262+
shouldAlwaysShowDropdownMenu
263+
customText={translate('common.more')}
264+
options={applicableSecondaryActions}
265+
isSplitButton={false}
266+
/>
267+
)}
268+
</View>
188269
)}
189270
{shouldDisplayTransactionNavigation && <MoneyRequestReportTransactionsNavigation currentReportID={reportID} />}
190271
</HeaderWithBackButton>
191-
{shouldShowMarkAsCashButton && shouldUseNarrowLayout && (
192-
<View style={[styles.ph5, styles.pb3]}>
193-
<Button
194-
success
195-
text={translate('iou.markAsCash')}
196-
style={[styles.w100, styles.pr0]}
197-
onPress={markAsCash}
198-
/>
199-
</View>
200-
)}
201-
{isDuplicate && shouldUseNarrowLayout && (
202-
<View style={[styles.ph5, styles.pb3]}>
203-
<Button
204-
success
205-
text={translate('iou.reviewDuplicates')}
206-
style={[styles.w100, styles.pr0]}
207-
onPress={() => {
208-
if (!reportID) {
209-
return;
210-
}
211-
Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_PAGE.getRoute(reportID, Navigation.getReportRHPActiveRoute()));
212-
}}
213-
/>
272+
{shouldUseNarrowLayout && (
273+
<View style={[styles.flexRow, styles.gap2, styles.pb3, styles.ph5, styles.w100, styles.alignItemsCenter, styles.justifyContentCenter]}>
274+
{!!primaryAction && <View style={[styles.flexGrow4]}>{primaryActionImplementation[primaryAction]}</View>}
275+
{!!applicableSecondaryActions.length && (
276+
<ButtonWithDropdownMenu
277+
success={false}
278+
onPress={() => {}}
279+
shouldAlwaysShowDropdownMenu
280+
customText={translate('common.more')}
281+
options={applicableSecondaryActions}
282+
isSplitButton={false}
283+
wrapperStyle={[!primaryAction && styles.flexGrow4]}
284+
/>
285+
)}
214286
</View>
215287
)}
216288
{!!statusBarProps && (
@@ -222,6 +294,36 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre
222294
</View>
223295
)}
224296
<LoadingBar shouldShow={(isLoadingReportData && shouldUseNarrowLayout) ?? false} />
297+
<DecisionModal
298+
title={translate('common.downloadFailedTitle')}
299+
prompt={translate('common.downloadFailedDescription')}
300+
isSmallScreenWidth={isSmallScreenWidth}
301+
onSecondOptionSubmit={() => setDownloadErrorModalVisible(false)}
302+
secondOptionText={translate('common.buttonConfirm')}
303+
isVisible={downloadErrorModalVisible}
304+
onClose={() => setDownloadErrorModalVisible(false)}
305+
/>
306+
<ConfirmModal
307+
title={translate('iou.deleteExpense', {count: 1})}
308+
isVisible={isDeleteModalVisible}
309+
onConfirm={() => {
310+
setIsDeleteModalVisible(false);
311+
if (!parentReportAction || !transaction) {
312+
throw new Error('Data missing');
313+
}
314+
315+
deleteMoneyRequest(transaction?.transactionID, parentReportAction);
316+
317+
const goBackRoute = getNavigationUrlOnMoneyRequestDelete(transaction.transactionID, parentReportAction, true);
318+
navigateBackOnDeleteTransaction(goBackRoute);
319+
}}
320+
onCancel={() => setIsDeleteModalVisible(false)}
321+
prompt={translate('iou.deleteConfirmation', {count: 1})}
322+
confirmText={translate('common.delete')}
323+
cancelText={translate('common.cancel')}
324+
danger
325+
shouldEnableNewFocusManagement
326+
/>
225327
</View>
226328
);
227329
}

0 commit comments

Comments
 (0)