Skip to content

Commit

Permalink
Merge pull request Expensify#50137 from abzokhattab/Add-the-download-…
Browse files Browse the repository at this point in the history
…option-to-the-report

Add the Download option to the report details screen
  • Loading branch information
rlinoz authored Oct 17, 2024
2 parents f95b99b + ac24511 commit f8c7d43
Show file tree
Hide file tree
Showing 6 changed files with 155 additions and 68 deletions.
1 change: 1 addition & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2907,6 +2907,7 @@ const CONST = {
SETTINGS: 'settings',
LEAVE_ROOM: 'leaveRoom',
PRIVATE_NOTES: 'privateNotes',
DOWNLOAD: 'download',
EXPORT: 'export',
DELETE: 'delete',
MARK_AS_INCOMPLETE: 'markAsIncomplete',
Expand Down
6 changes: 6 additions & 0 deletions src/libs/API/parameters/ExportReportCSVParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
type ExportReportCSVParams = {
transactionIDList: string[];
reportID: string;
};

export default ExportReportCSVParams;
1 change: 1 addition & 0 deletions src/libs/API/parameters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ export type {default as UpdateSageIntacctGenericTypeParams} from './UpdateSageIn
export type {default as UpdateNetSuiteCustomersJobsParams} from './UpdateNetSuiteCustomersJobsParams';
export type {default as CopyExistingPolicyConnectionParams} from './CopyExistingPolicyConnectionParams';
export type {default as ExportSearchItemsToCSVParams} from './ExportSearchItemsToCSVParams';
export type {default as ExportReportCSVParams} from './ExportReportCSVParams';
export type {default as UpdateExpensifyCardLimitParams} from './UpdateExpensifyCardLimitParams';
export type {CreateWorkspaceApprovalParams, UpdateWorkspaceApprovalParams, RemoveWorkspaceApprovalParams} from './WorkspaceApprovalParams';
export type {default as StartIssueNewCardFlowParams} from './StartIssueNewCardFlowParams';
Expand Down
2 changes: 2 additions & 0 deletions src/libs/API/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ const WRITE_COMMANDS = {
EXPORT_CATEGORIES_CSV: 'ExportCategoriesCSV',
EXPORT_MEMBERS_CSV: 'ExportMembersCSV',
EXPORT_TAGS_CSV: 'ExportTagsCSV',
EXPORT_REPORT_TO_CSV: 'ExportReportToCSV',
RENAME_WORKSPACE_CATEGORY: 'RenameWorkspaceCategory',
CREATE_POLICY_TAG: 'CreatePolicyTag',
RENAME_POLICY_TAG: 'RenamePolicyTag',
Expand Down Expand Up @@ -824,6 +825,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.UPDATE_SAGE_INTACCT_SYNC_TAX_CONFIGURATION]: Parameters.UpdateSageIntacctGenericTypeParams<'enabled', boolean>;
[WRITE_COMMANDS.UPDATE_SAGE_INTACCT_USER_DIMENSION]: Parameters.UpdateSageIntacctGenericTypeParams<'dimensions', string>;
[WRITE_COMMANDS.EXPORT_SEARCH_ITEMS_TO_CSV]: Parameters.ExportSearchItemsToCSVParams;
[WRITE_COMMANDS.EXPORT_REPORT_TO_CSV]: Parameters.ExportReportCSVParams;
[WRITE_COMMANDS.CREATE_WORKSPACE_APPROVAL]: Parameters.CreateWorkspaceApprovalParams;
[WRITE_COMMANDS.UPDATE_WORKSPACE_APPROVAL]: Parameters.UpdateWorkspaceApprovalParams;
[WRITE_COMMANDS.REMOVE_WORKSPACE_APPROVAL]: Parameters.RemoveWorkspaceApprovalParams;
Expand Down
159 changes: 91 additions & 68 deletions src/libs/actions/Report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,22 +48,26 @@ import type {
UpdateReportWriteCapabilityParams,
UpdateRoomDescriptionParams,
} from '@libs/API/parameters';
import type ExportReportCSVParams from '@libs/API/parameters/ExportReportCSVParams';
import type UpdateRoomVisibilityParams from '@libs/API/parameters/UpdateRoomVisibilityParams';
import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types';
import * as ApiUtils from '@libs/ApiUtils';
import * as CollectionUtils from '@libs/CollectionUtils';
import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types';
import DateUtils from '@libs/DateUtils';
import {prepareDraftComment} from '@libs/DraftCommentUtils';
import * as EmojiUtils from '@libs/EmojiUtils';
import * as Environment from '@libs/Environment/Environment';
import * as ErrorUtils from '@libs/ErrorUtils';
import fileDownload from '@libs/fileDownload';
import HttpUtils from '@libs/HttpUtils';
import isPublicScreenRoute from '@libs/isPublicScreenRoute';
import * as Localize from '@libs/Localize';
import Log from '@libs/Log';
import {registerPaginationConfig} from '@libs/Middleware/Pagination';
import Navigation, {navigationRef} from '@libs/Navigation/Navigation';
import {isOnboardingFlowName} from '@libs/NavigationUtils';
import enhanceParameters from '@libs/Network/enhanceParameters';
import type {NetworkStatus} from '@libs/NetworkConnection';
import LocalNotification from '@libs/Notification/LocalNotification';
import Parser from '@libs/Parser';
Expand Down Expand Up @@ -100,8 +104,8 @@ import type {
} from '@src/types/onyx';
import type {Decision} from '@src/types/onyx/OriginalMessage';
import type {ConnectionName} from '@src/types/onyx/Policy';
import type {NotificationPreference, Participants, Participant as ReportParticipant, RoomVisibility, WriteCapability} from '@src/types/onyx/Report';
import type Report from '@src/types/onyx/Report';
import type {NotificationPreference, Participants, Participant as ReportParticipant, RoomVisibility, WriteCapability} from '@src/types/onyx/Report';
import type {Message, ReportActions} from '@src/types/onyx/ReportAction';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import * as CachedPDFPaths from './CachedPDFPaths';
Expand Down Expand Up @@ -4121,92 +4125,111 @@ function markAsManuallyExported(reportID: string, connectionName: ConnectionName
API.write(WRITE_COMMANDS.MARK_AS_EXPORTED, params, {optimisticData, successData, failureData});
}

function exportReportToCSV({reportID, transactionIDList}: ExportReportCSVParams, onDownloadFailed: () => void) {
const finalParameters = enhanceParameters(WRITE_COMMANDS.EXPORT_REPORT_TO_CSV, {
reportID,
transactionIDList,
});

const formData = new FormData();
Object.entries(finalParameters).forEach(([key, value]) => {
if (Array.isArray(value)) {
formData.append(key, value.join(','));
} else {
formData.append(key, String(value));
}
});

fileDownload(ApiUtils.getCommandURL({command: WRITE_COMMANDS.EXPORT_REPORT_TO_CSV}), 'Expensify.csv', '', false, formData, CONST.NETWORK.METHOD.POST, onDownloadFailed);
}

export type {Video};

export {
searchInServer,
addComment,
addAttachment,
updateDescription,
updateWriteCapability,
updateNotificationPreference,
subscribeToReportTypingEvents,
subscribeToReportLeavingEvents,
unsubscribeFromReportChannel,
unsubscribeFromLeavingRoomReportChannel,
saveReportDraftComment,
broadcastUserIsTyping,
addComment,
addPolicyReport,
broadcastUserIsLeavingRoom,
togglePinnedState,
editReportComment,
handleUserDeletedLinksInHtml,
broadcastUserIsTyping,
clearAddRoomMemberError,
clearAvatarErrors,
clearGroupChat,
clearIOUError,
clearNewRoomFormError,
clearPolicyRoomNameErrors,
clearPrivateNotesError,
clearReportFieldKeyErrors,
completeOnboarding,
deleteReport,
deleteReportActionDraft,
saveReportActionDraft,
deleteReportComment,
navigateToConciergeChat,
addPolicyReport,
deleteReport,
navigateToConciergeChatAndDeleteReport,
setIsComposerFullSize,
deleteReportField,
dismissTrackExpenseActionableWhisper,
editReportComment,
expandURLPreview,
exportReportToCSV,
exportToIntegration,
flagComment,
getCurrentUserAccountID,
getDraftPrivateNote,
getMostRecentReportID,
getNewerActions,
getOlderActions,
getReportPrivateNote,
handleReportChanged,
handleUserDeletedLinksInHtml,
hasErrorInPrivateNotes,
inviteToGroupChat,
inviteToRoom,
joinRoom,
leaveGroupChat,
leaveRoom,
markAsManuallyExported,
markCommentAsUnread,
readNewestAction,
openReport,
openReportFromDeepLink,
navigateToAndOpenChildReport,
navigateToAndOpenReport,
navigateToAndOpenReportWithAccountIDs,
navigateToAndOpenChildReport,
toggleSubscribeToChildReport,
updatePolicyRoomName,
clearPolicyRoomNameErrors,
clearIOUError,
subscribeToNewActionEvent,
navigateToConciergeChat,
navigateToConciergeChatAndDeleteReport,
notifyNewAction,
showReportActionNotification,
toggleEmojiReaction,
shouldShowReportActionNotification,
getMostRecentReportID,
joinRoom,
leaveRoom,
inviteToRoom,
inviteToGroupChat,
removeFromRoom,
getCurrentUserAccountID,
setLastOpenedPublicRoom,
flagComment,
openLastOpenedPublicRoom,
updatePrivateNotes,
getReportPrivateNote,
clearPrivateNotesError,
hasErrorInPrivateNotes,
getOlderActions,
getNewerActions,
openReport,
openReportFromDeepLink,
openRoomMembersPage,
savePrivateNotesDraft,
getDraftPrivateNote,
updateLastVisitTime,
clearNewRoomFormError,
updateReportField,
updateReportName,
deleteReportField,
clearReportFieldKeyErrors,
readNewestAction,
removeFromGroupChat,
removeFromRoom,
resolveActionableMentionWhisper,
resolveActionableReportMentionWhisper,
updateRoomVisibility,
dismissTrackExpenseActionableWhisper,
savePrivateNotesDraft,
saveReportActionDraft,
saveReportDraftComment,
searchInServer,
setGroupDraft,
clearGroupChat,
setIsComposerFullSize,
setLastOpenedPublicRoom,
shouldShowReportActionNotification,
showReportActionNotification,
startNewChat,
completeOnboarding,
updateGroupChatName,
subscribeToNewActionEvent,
subscribeToReportLeavingEvents,
subscribeToReportTypingEvents,
toggleEmojiReaction,
togglePinnedState,
toggleSubscribeToChildReport,
unsubscribeFromLeavingRoomReportChannel,
unsubscribeFromReportChannel,
updateDescription,
updateGroupChatAvatar,
leaveGroupChat,
removeFromGroupChat,
updateGroupChatMemberRoles,
updateGroupChatName,
updateLastVisitTime,
updateLoadingInitialReportAction,
clearAddRoomMemberError,
clearAvatarErrors,
exportToIntegration,
markAsManuallyExported,
handleReportChanged,
updateNotificationPreference,
updatePolicyRoomName,
updatePrivateNotes,
updateReportField,
updateReportName,
updateRoomVisibility,
updateWriteCapability,
};
54 changes: 54 additions & 0 deletions src/pages/ReportDetailsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {useOnyx} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import AvatarWithImagePicker from '@components/AvatarWithImagePicker';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
import DecisionModal from '@components/DecisionModal';
import DelegateNoAccessModal from '@components/DelegateNoAccessModal';
import DisplayNames from '@components/DisplayNames';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
Expand All @@ -29,14 +30,17 @@ import useDelegateUserDetails from '@hooks/useDelegateUserDetails';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import usePaginatedReportActions from '@hooks/usePaginatedReportActions';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import * as ReportActions from '@libs/actions/Report';
import Navigation from '@libs/Navigation/Navigation';
import type {ReportDetailsNavigatorParamList} from '@libs/Navigation/types';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as PolicyUtils from '@libs/PolicyUtils';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as ReportUtils from '@libs/ReportUtils';
import StringUtils from '@libs/StringUtils';
import {getAllReportTransactions} from '@libs/TransactionUtils';
import * as IOU from '@userActions/IOU';
import * as Report from '@userActions/Report';
import * as Session from '@userActions/Session';
Expand Down Expand Up @@ -90,6 +94,7 @@ function ReportDetailsPage({policies, report, route}: ReportDetailsPageProps) {
const {reportActions} = usePaginatedReportActions(report.reportID || '-1');
/* eslint-enable @typescript-eslint/prefer-nullish-coalescing */
const {currentSearchHash} = useSearchContext();
const {isSmallScreenWidth} = useResponsiveLayout();

const transactionThreadReportID = useMemo(
() => ReportActionsUtils.getOneTransactionThreadReportID(report.reportID, reportActions ?? [], isOffline),
Expand All @@ -100,11 +105,14 @@ function ReportDetailsPage({policies, report, route}: ReportDetailsPageProps) {
const [isDebugModeEnabled] = useOnyx(ONYXKEYS.USER, {selector: (user) => !!user?.isDebugModeEnabled});
const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST);
const [session] = useOnyx(ONYXKEYS.SESSION);
const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION);

const [isLastMemberLeavingGroupModalVisible, setIsLastMemberLeavingGroupModalVisible] = useState(false);
const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false);
const [isUnapproveModalVisible, setIsUnapproveModalVisible] = useState(false);
const [isConfirmModalVisible, setIsConfirmModalVisible] = useState(false);
const [offlineModalVisible, setOfflineModalVisible] = useState(false);
const [downloadErrorModalVisible, setDownloadErrorModalVisible] = useState(false);
const policy = useMemo(() => policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID ?? '-1'}`], [policies, report?.policyID]);
const isPolicyAdmin = useMemo(() => PolicyUtils.isPolicyAdmin(policy), [policy]);
const isPolicyEmployee = useMemo(() => PolicyUtils.isPolicyEmployee(report?.policyID ?? '-1', policies), [report?.policyID, policies]);
Expand Down Expand Up @@ -143,6 +151,13 @@ function ReportDetailsPage({policies, report, route}: ReportDetailsPageProps) {
}, [report, personalDetails, shouldOpenRoomMembersPage]);
const connectedIntegration = PolicyUtils.getConnectedIntegration(policy);

const transactionIDList = useMemo(() => {
if (!isMoneyRequestReport) {
return [];
}
return getAllReportTransactions(report.reportID, transactions).map((transaction) => transaction.transactionID);
}, [isMoneyRequestReport, report.reportID, transactions]);

// Get the active chat members by filtering out the pending members with delete action
const activeChatMembers = participants.flatMap((accountID) => {
const pendingMember = report?.pendingChatMembers?.findLast((member) => member.accountID === accountID.toString());
Expand Down Expand Up @@ -409,6 +424,25 @@ function ReportDetailsPage({policies, report, route}: ReportDetailsPageProps) {
});
}

if (isMoneyRequestReport) {
items.push({
key: CONST.REPORT_DETAILS_MENU_ITEM.DOWNLOAD,
translationKey: 'common.download',
icon: Expensicons.Download,
isAnonymousAction: false,
action: () => {
if (isOffline) {
setOfflineModalVisible(true);
return;
}

ReportActions.exportReportToCSV({reportID: report.reportID, transactionIDList}, () => {
setDownloadErrorModalVisible(true);
});
},
});
}

if (policy && connectedIntegration && isPolicyAdmin && !isSingleTransactionView && isExpenseReport) {
items.push({
key: CONST.REPORT_DETAILS_MENU_ITEM.EXPORT,
Expand Down Expand Up @@ -471,6 +505,8 @@ function ReportDetailsPage({policies, report, route}: ReportDetailsPageProps) {
shouldOpenRoomMembersPage,
shouldShowCancelPaymentButton,
session,
isOffline,
transactionIDList,
leaveChat,
canUnapproveRequest,
isDebugModeEnabled,
Expand Down Expand Up @@ -878,6 +914,24 @@ function ReportDetailsPage({policies, report, route}: ReportDetailsPageProps) {
onCancel={() => setIsUnapproveModalVisible(false)}
prompt={unapproveWarningText}
/>
<DecisionModal
title={translate('common.youAppearToBeOffline')}
prompt={translate('common.offlinePrompt')}
isSmallScreenWidth={isSmallScreenWidth}
onSecondOptionSubmit={() => setOfflineModalVisible(false)}
secondOptionText={translate('common.buttonConfirm')}
isVisible={offlineModalVisible}
onClose={() => setOfflineModalVisible(false)}
/>
<DecisionModal
title={translate('common.downloadFailedTitle')}
prompt={translate('common.downloadFailedDescription')}
isSmallScreenWidth={isSmallScreenWidth}
onSecondOptionSubmit={() => setDownloadErrorModalVisible(false)}
secondOptionText={translate('common.buttonConfirm')}
isVisible={downloadErrorModalVisible}
onClose={() => setDownloadErrorModalVisible(false)}
/>
</FullPageNotFoundView>
</ScreenWrapper>
);
Expand Down

0 comments on commit f8c7d43

Please sign in to comment.