Skip to content

Move "Download as PDF" to "More" button #62022

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
May 15, 2025
Merged
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
3 changes: 2 additions & 1 deletion src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1201,7 +1201,8 @@ const CONST = {
EXPORT_TO_ACCOUNTING: 'exportToAccounting',
MARK_AS_EXPORTED: 'markAsExported',
HOLD: 'hold',
DOWNLOAD: 'download',
DOWNLOAD_CSV: 'downloadCSV',
DOWNLOAD_PDF: 'downloadPDF',
CHANGE_WORKSPACE: 'changeWorkspace',
VIEW_DETAILS: 'viewDetails',
DELETE: 'delete',
Expand Down
86 changes: 81 additions & 5 deletions src/components/MoneyReportHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {useRoute} from '@react-navigation/native';
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {View} from 'react-native';
import {ActivityIndicator, View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {useOnyx} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
Expand All @@ -15,7 +15,7 @@ import useSelectedTransactionsActions from '@hooks/useSelectedTransactionsAction
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode';
import {exportReportToCSV, exportToIntegration, markAsManuallyExported, openUnreportedExpense} from '@libs/actions/Report';
import {downloadReportPDF, exportReportToCSV, exportReportToPDF, exportToIntegration, markAsManuallyExported, openUnreportedExpense} from '@libs/actions/Report';
import {convertToDisplayString} from '@libs/CurrencyUtils';
import {getThreadReportIDsForTransactions} from '@libs/MoneyRequestReportUtils';
import Navigation from '@libs/Navigation/Navigation';
Expand Down Expand Up @@ -90,11 +90,13 @@ import type {DropdownOption} from './ButtonWithDropdownMenu/types';
import ConfirmModal from './ConfirmModal';
import DecisionModal from './DecisionModal';
import DelegateNoAccessModal from './DelegateNoAccessModal';
import Header from './Header';
import HeaderWithBackButton from './HeaderWithBackButton';
import Icon from './Icon';
import * as Expensicons from './Icon/Expensicons';
import type {PaymentMethod} from './KYCWall/types';
import LoadingBar from './LoadingBar';
import Modal from './Modal';
import MoneyReportHeaderStatusBar from './MoneyReportHeaderStatusBar';
import type {MoneyRequestHeaderStatusBarProps} from './MoneyRequestHeaderStatusBar';
import MoneyRequestHeaderStatusBar from './MoneyRequestHeaderStatusBar';
Expand Down Expand Up @@ -136,6 +138,9 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const [nextStep] = useOnyx(`${ONYXKEYS.COLLECTION.NEXT_STEP}${moneyRequestReport?.reportID}`, {canBeMissing: true});
const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`, {canBeMissing: true});
const [reportPDFFilename] = useOnyx(`${ONYXKEYS.COLLECTION.NVP_EXPENSIFY_REPORT_PDF_FILENAME}${moneyRequestReport?.reportID}`, {canBeMissing: true}) ?? null;
const [download] = useOnyx(`${ONYXKEYS.COLLECTION.DOWNLOAD}${reportPDFFilename}`, {canBeMissing: true});
const isDownloadingPDF = download?.isDownloading ?? false;
const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false});
const requestParentReportAction = useMemo(() => {
if (!reportActions || !transactionThreadReport?.parentReportActionID) {
Expand All @@ -162,6 +167,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false);
const [isUnapproveModalVisible, setIsUnapproveModalVisible] = useState(false);
const [isReopenWarningModalVisible, setIsReopenWarningModalVisible] = useState(false);
const [isPDFModalVisible, setIsPDFModalVisible] = useState(false);

const [exportModalStatus, setExportModalStatus] = useState<ExportType | null>(null);

Expand Down Expand Up @@ -189,6 +195,17 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
() => Object.fromEntries(Object.entries(allViolations ?? {}).filter(([key]) => transactionIDs.includes(key.replace(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, '')))),
[allViolations, transactionIDs],
);

const messagePDF = useMemo(() => {
if (!reportPDFFilename) {
return translate('reportDetailsPage.waitForPDF');
}
if (reportPDFFilename === CONST.REPORT_DETAILS_MENU_ITEM.ERROR) {
return translate('reportDetailsPage.errorPDF');
}
return translate('reportDetailsPage.generatedPDF');
}, [reportPDFFilename, translate]);

// Check if there is pending rter violation in all transactionViolations with given transactionIDs.
const hasAllPendingRTERViolations = allHavePendingRTERViolation(transactionIDs, violations);
// Check if user should see broken connection violation warning.
Expand Down Expand Up @@ -535,6 +552,11 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea

const {canUseRetractNewDot, canUseTableReportView} = usePermissions();

const beginPDFExport = (reportID: string) => {
setIsPDFModalVisible(true);
exportReportToPDF({reportID});
};

const secondaryActions = useMemo(() => {
if (!moneyRequestReport) {
return [];
Expand All @@ -551,9 +573,9 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
navigateToDetailsPage(moneyRequestReport, Navigation.getReportRHPActiveRoute());
},
},
[CONST.REPORT.SECONDARY_ACTIONS.DOWNLOAD]: {
value: CONST.REPORT.SECONDARY_ACTIONS.DOWNLOAD,
text: translate('common.download'),
[CONST.REPORT.SECONDARY_ACTIONS.DOWNLOAD_CSV]: {
value: CONST.REPORT.SECONDARY_ACTIONS.DOWNLOAD_CSV,
text: translate('common.downloadAsCSV'),
icon: Expensicons.Download,
onSelected: () => {
if (!moneyRequestReport) {
Expand All @@ -564,6 +586,17 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
});
},
},
[CONST.REPORT.SECONDARY_ACTIONS.DOWNLOAD_PDF]: {
value: CONST.REPORT.SECONDARY_ACTIONS.DOWNLOAD_PDF,
text: translate('common.downloadAsPDF'),
icon: Expensicons.Document,
onSelected: () => {
if (!moneyRequestReport) {
return;
}
beginPDFExport(moneyRequestReport.reportID);
},
},
[CONST.REPORT.SECONDARY_ACTIONS.SUBMIT]: {
value: CONST.REPORT.SECONDARY_ACTIONS.SUBMIT,
text: translate('common.submit'),
Expand Down Expand Up @@ -964,6 +997,49 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
isVisible={isDownloadErrorModalVisible}
onClose={() => setIsDownloadErrorModalVisible(false)}
/>
<Modal
onClose={() => setIsPDFModalVisible(false)}
isVisible={isPDFModalVisible}
type={isSmallScreenWidth ? CONST.MODAL.MODAL_TYPE.BOTTOM_DOCKED : CONST.MODAL.MODAL_TYPE.CONFIRM}
innerContainerStyle={styles.pv0}
shouldUseNewModal
>
<View style={[styles.m5]}>
<View>
<View style={[styles.flexRow, styles.mb4]}>
<Header
title={translate('reportDetailsPage.generatingPDF')}
containerStyles={[styles.alignItemsCenter]}
/>
</View>
<View>
<Text>{messagePDF}</Text>
{!reportPDFFilename && (
<ActivityIndicator
size={CONST.ACTIVITY_INDICATOR_SIZE.LARGE}
color={theme.textSupporting}
style={styles.mt3}
/>
)}
</View>
</View>
{!!reportPDFFilename && reportPDFFilename !== 'error' && (
<Button
isLoading={isDownloadingPDF}
style={[styles.mt3, styles.noSelect]}
onPress={() => downloadReportPDF(reportPDFFilename ?? '', moneyRequestReport?.reportName ?? '')}
text={translate('common.download')}
/>
)}
{(!reportPDFFilename || reportPDFFilename === 'error') && (
<Button
style={[styles.mt3, styles.noSelect]}
onPress={() => setIsPDFModalVisible(false)}
text={translate('common.close')}
/>
)}
</View>
</Modal>
</View>
);
}
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/useSelectedTransactionsActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,8 @@ function useSelectedTransactionsActions({
}

options.push({
value: CONST.REPORT.SECONDARY_ACTIONS.DOWNLOAD,
text: translate('common.download'),
value: CONST.REPORT.SECONDARY_ACTIONS.DOWNLOAD_CSV,
text: translate('common.downloadAsCSV'),
icon: Expensicons.Download,
onSelected: () => {
if (!report) {
Expand Down
4 changes: 3 additions & 1 deletion src/libs/ReportSecondaryActionUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,9 @@ function getSecondaryReportActions(
options.push(CONST.REPORT.SECONDARY_ACTIONS.HOLD);
}

options.push(CONST.REPORT.SECONDARY_ACTIONS.DOWNLOAD);
options.push(CONST.REPORT.SECONDARY_ACTIONS.DOWNLOAD_CSV);

options.push(CONST.REPORT.SECONDARY_ACTIONS.DOWNLOAD_PDF);

if (isChangeWorkspaceAction(report, policy)) {
options.push(CONST.REPORT.SECONDARY_ACTIONS.CHANGE_WORKSPACE);
Expand Down
86 changes: 1 addition & 85 deletions src/pages/ReportDetailsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
import {Str} from 'expensify-common';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {ActivityIndicator, View} from 'react-native';
import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {useOnyx} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import AvatarWithImagePicker from '@components/AvatarWithImagePicker';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
import Button from '@components/Button';
import ConfirmModal from '@components/ConfirmModal';
import DecisionModal from '@components/DecisionModal';
import DelegateNoAccessModal from '@components/DelegateNoAccessModal';
import DisplayNames from '@components/DisplayNames';
import FixedFooter from '@components/FixedFooter';
import Header from '@components/Header';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import MentionReportContext from '@components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/MentionReportContext';
import * as Expensicons from '@components/Icon/Expensicons';
import MenuItem from '@components/MenuItem';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import Modal from '@components/Modal';
import {useMoneyRequestReportContext} from '@components/MoneyRequestReportView/MoneyRequestReportContext';
import MultipleAvatars from '@components/MultipleAvatars';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
Expand All @@ -38,7 +35,6 @@ import useNetwork from '@hooks/useNetwork';
import usePaginatedReportActions from '@hooks/usePaginatedReportActions';
import usePermissions from '@hooks/usePermissions';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import getBase62ReportID from '@libs/getBase62ReportID';
import Navigation from '@libs/Navigation/Navigation';
Expand Down Expand Up @@ -117,9 +113,7 @@ import {
import {
clearAvatarErrors,
clearPolicyRoomNameErrors,
downloadReportPDF,
exportReportToCSV,
exportReportToPDF,
getReportPrivateNote,
hasErrorInPrivateNotes,
leaveGroupChat,
Expand Down Expand Up @@ -167,16 +161,11 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta
const {translate} = useLocalize();
const {isOffline} = useNetwork();
const {canUseTableReportView} = usePermissions();
const theme = useTheme();
const styles = useThemeStyles();
const backTo = route.params.backTo;

const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`, {canBeMissing: true});

const [reportPDFFilename] = useOnyx(`${ONYXKEYS.COLLECTION.NVP_EXPENSIFY_REPORT_PDF_FILENAME}${report?.reportID}`, {canBeMissing: true}) ?? null;
const [download] = useOnyx(`${ONYXKEYS.COLLECTION.DOWNLOAD}${reportPDFFilename}`, {canBeMissing: true});
const isDownloadingPDF = download?.isDownloading ?? false;

const [parentReportAction] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`, {
selector: (actions) => (report?.parentReportActionID ? actions?.[report.parentReportActionID] : undefined),
canBeMissing: true,
Expand Down Expand Up @@ -211,7 +200,6 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta
const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false);
const [isUnapproveModalVisible, setIsUnapproveModalVisible] = useState(false);
const [isConfirmModalVisible, setIsConfirmModalVisible] = useState(false);
const [isPDFModalVisible, setIsPDFModalVisible] = useState(false);
const [offlineModalVisible, setOfflineModalVisible] = useState(false);
const [downloadErrorModalVisible, setDownloadErrorModalVisible] = useState(false);
const policy = useMemo(() => policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`], [policies, report?.policyID]);
Expand Down Expand Up @@ -253,16 +241,6 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta
return '';
}, [report]);

const messagePDF = useMemo(() => {
if (!reportPDFFilename) {
return translate('reportDetailsPage.waitForPDF');
}
if (reportPDFFilename === CONST.REPORT_DETAILS_MENU_ITEM.ERROR) {
return translate('reportDetailsPage.errorPDF');
}
return translate('reportDetailsPage.generatedPDF');
}, [reportPDFFilename, translate]);

const isSystemChat = useMemo(() => isSystemChatUtil(report), [report]);
const isGroupChat = useMemo(() => isGroupChatUtil(report), [report]);
const isRootGroupChat = useMemo(() => isRootGroupChatUtil(report), [report]);
Expand Down Expand Up @@ -415,11 +393,6 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta
setIsConfirmModalVisible(false);
}, [moneyRequestReport, chatReport]);

const beginPDFExport = useCallback(() => {
setIsPDFModalVisible(true);
exportReportToPDF({reportID: report.reportID});
}, [report]);

const menuItems: ReportDetailsPageMenuItem[] = useMemo(() => {
const items: ReportDetailsPageMenuItem[] = [];

Expand Down Expand Up @@ -577,19 +550,6 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta
});
},
});
items.push({
key: CONST.REPORT_DETAILS_MENU_ITEM.DOWNLOAD_PDF,
translationKey: 'common.downloadAsPDF',
icon: Expensicons.Document,
isAnonymousAction: false,
action: () => {
if (isOffline) {
setOfflineModalVisible(true);
} else {
beginPDFExport();
}
},
});
}

if (policy && connectedIntegration && isPolicyAdmin && !isSingleTransactionView && isExpenseReport) {
Expand Down Expand Up @@ -700,7 +660,6 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta
canActionTask,
isOffline,
transactionIDList,
beginPDFExport,
unapproveExpenseReportOrShowModal,
isRootGroupChat,
leaveChat,
Expand Down Expand Up @@ -1174,49 +1133,6 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta
isVisible={downloadErrorModalVisible}
onClose={() => setDownloadErrorModalVisible(false)}
/>
<Modal
onClose={() => setIsPDFModalVisible(false)}
isVisible={isPDFModalVisible}
type={isSmallScreenWidth ? CONST.MODAL.MODAL_TYPE.BOTTOM_DOCKED : CONST.MODAL.MODAL_TYPE.CONFIRM}
innerContainerStyle={styles.pv0}
shouldUseNewModal
>
<View style={[styles.m5]}>
<View>
<View style={[styles.flexRow, styles.mb4]}>
<Header
title={translate('reportDetailsPage.generatingPDF')}
containerStyles={[styles.alignItemsCenter]}
/>
</View>
<View>
<Text>{messagePDF}</Text>
{!reportPDFFilename && (
<ActivityIndicator
size={CONST.ACTIVITY_INDICATOR_SIZE.LARGE}
color={theme.textSupporting}
style={styles.mt3}
/>
)}
</View>
</View>
{!!reportPDFFilename && reportPDFFilename !== 'error' && (
<Button
isLoading={isDownloadingPDF}
style={[styles.mt3, styles.noSelect]}
onPress={() => downloadReportPDF(reportPDFFilename ?? '', reportName)}
text={translate('common.download')}
/>
)}
{(!reportPDFFilename || reportPDFFilename === 'error') && (
<Button
style={[styles.mt3, styles.noSelect]}
onPress={() => setIsPDFModalVisible(false)}
text={translate('common.close')}
/>
)}
</View>
</Modal>
</FullPageNotFoundView>
</ScreenWrapper>
);
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/ReportSecondaryActionUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ describe('getSecondaryAction', () => {
const report = {} as unknown as Report;
const policy = {} as unknown as Policy;

const result = [CONST.REPORT.SECONDARY_ACTIONS.DOWNLOAD, CONST.REPORT.SECONDARY_ACTIONS.VIEW_DETAILS];
const result = [CONST.REPORT.SECONDARY_ACTIONS.DOWNLOAD_CSV, CONST.REPORT.SECONDARY_ACTIONS.DOWNLOAD_PDF, CONST.REPORT.SECONDARY_ACTIONS.VIEW_DETAILS];
expect(getSecondaryReportActions(report, [], {}, policy)).toEqual(result);
});

Expand Down