Skip to content
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

SPR-168 Download all files #80

Merged
merged 3 commits into from
Jul 11, 2023
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
35 changes: 33 additions & 2 deletions api/controllers/requests-api-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { getUserInfo } from '../keycloak/utils';
import Constants from '../constants/Constants';
import { Purchase } from '../interfaces/Purchase';
import { Approval } from '../interfaces/Approval';
import JSZip from 'jszip';
import { IFile } from '../interfaces/IFile';
import { sendChangeNotification, sendRequestSubmittedNotification } from '../helpers/useGCNotify';
import { getIDIRUser, IDIRUser } from '../helpers/useCssApi';

Expand Down Expand Up @@ -427,8 +429,37 @@ export const getFile = async (req: Request, res: Response) => {
return res.status(200).json({ file: desiredFile.file });
} else {
// No date? Return all files
// TODO: Mark all files as downloaded.
return res.status(200).json({ files: allFiles });
// If any files are undefined (missing purchase or approvals), remove them.
// Remove any deleted files
const trimmedFiles = allFiles.filter((fileObj) => fileObj && !fileObj.deleted);

// Zip the files together
const zip = new JSZip();
trimmedFiles.forEach((fileObj: IFile) => {
// Only need the base64 hash, not declaritive info
zip.file(fileObj.name, fileObj.file.split('base64,')[1], {
base64: true, // must specify files are base64
});
});
// Creates the zip folder (as base64), then sends with appropriate declarative info
zip.generateAsync({ type: 'base64' }).then((content: string) => {
res.status(200).json({ zip: `data:application/zip;base64,${content}` });
});

// Mark all files as downloaded.
await collection.updateOne(
{
_id: { $eq: new ObjectId(id) },
},
{
$set: {
'purchases.$[].fileObj.downloaded': true,
'approvals.$[].fileObj.downloaded': true,
},
},
);

return res;
dbarkowsky marked this conversation as resolved.
Show resolved Hide resolved
}
}
} catch (e) {
Expand Down
1 change: 1 addition & 0 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"dotenv": "16.3.1",
"express": "4.18.2",
"express-rate-limit": "6.7.0",
"jszip": "3.10.1",
"mongodb": "5.6.0",
"morgan": "1.10.0",
"node-cron": "3.0.2",
Expand Down
18 changes: 17 additions & 1 deletion app/src/components/custom/forms/RequestForm.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Dispatch, SetStateAction, useContext, useState, MouseEvent, CSSProperties } from 'react';
import { RequestStates, convertStateToStatus } from '../../../utils/convertState';
import { RequestStates, convertStateToStatus } from '../../../helpers/convertState';
import { useNavigate } from 'react-router-dom';
import { ReimbursementRequest } from '../../../interfaces/ReimbursementRequest';
import {
Expand Down Expand Up @@ -32,6 +32,7 @@ import { useAuthService } from '../../../keycloak';
import BackButton from '../../bcgov/BackButton';
import DeletePrompt from '../modals/DeletePrompt';
import { ErrorContext, errorStyles } from '../notifications/ErrorWrapper';
import { getAllFiles } from '../../../helpers/fileDownloadAll';

/**
* @interface
Expand Down Expand Up @@ -240,6 +241,21 @@ const RequestForm = (props: RequestFormProps) => {
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
>
<MenuItem
onClick={() => {
try {
getAllFiles(reimbursementRequest?._id || '', authState.accessToken);
} catch (e: any) {
setErrorState({
text: 'File could not be retrieved.',
open: true,
style: errorStyles.error,
});
}
}}
>
Download All Files
</MenuItem>
<MenuItem
onClick={() => {
navigate(`/user/${reimbursementRequest?.idir}`);
Expand Down
2 changes: 1 addition & 1 deletion app/src/components/custom/tables/ApprovalTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { bcgov } from '../../../constants/colours';
import CustomTableCell from './CustomTableCell';
import HeaderCell from './HeaderCell';
import { IFile } from '../../../interfaces/IFile';
import FileUpload from '../uploaders/FileUpload';
import FileUpload from '../fileHandlers/FileUpload';
import { Dispatch, SetStateAction, useRef } from 'react';
import dayjs, { Dayjs } from 'dayjs';
import { DatePicker } from '@mui/x-date-pickers';
Expand Down
2 changes: 1 addition & 1 deletion app/src/components/custom/tables/PurchaseTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Purchase } from '../../../interfaces/Purchase';
import { bcgov } from '../../../constants/colours';
import CustomTableCell from './CustomTableCell';
import HeaderCell from './HeaderCell';
import FileUpload from '../uploaders/FileUpload';
import FileUpload from '../fileHandlers/FileUpload';
import { Dispatch, SetStateAction } from 'react';
import { IFile } from '../../../interfaces/IFile';

Expand Down
4 changes: 2 additions & 2 deletions app/src/components/custom/tables/RequestsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
import HeaderCell from './HeaderCell';
import CustomTableCell from './CustomTableCell';
import { ReimbursementRequest } from '../../../interfaces/ReimbursementRequest';
import { RequestStates, convertStateToStatus } from '../../../utils/convertState';
import { RequestStates, convertStateToStatus } from '../../../helpers/convertState';
import { bcgov } from '../../../constants/colours';
import LinkButton from '../../bcgov/LinkButton';
import { buttonStyles } from '../../bcgov/ButtonStyles';
Expand All @@ -30,7 +30,7 @@ import PaginationControl, { PaginationControlObject } from './PaginationControl'
// Date Picker
import { DateRangePicker } from 'rsuite';
import 'rsuite/dist/rsuite.css';
import { dateRanges } from '../../../utils/dateRanges';
import { dateRanges } from '../../../helpers/dateRanges';

/**
* @interface
Expand Down
File renamed without changes.
File renamed without changes.
23 changes: 23 additions & 0 deletions app/src/helpers/fileDownloadAll.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Constants from '../constants/Constants';
import axios from 'axios';

/**
* @description Gets all files from a specific request record and downloads them as a .zip file.
* @param {string} id - The ID of the request record.
* @param {string} token - The authentication token from the user's auth state.
*/
export const getAllFiles = async (id: string, token: string) => {
const { BACKEND_URL } = Constants;
const axiosReqConfig = {
url: `${BACKEND_URL}/api/requests/${id}/files`,
method: `get`,
headers: {
Authorization: `Bearer ${token}`,
},
};
const file: string = await axios(axiosReqConfig).then((response) => response.data.zip);
const tempLink = document.createElement('a');
tempLink.href = file;
tempLink.download = `allFiles${id}.zip`;
tempLink.click();
};
2 changes: 1 addition & 1 deletion app/src/pages/IndividualRequest.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useState, useContext } from 'react';
import { RequestStates } from '../utils/convertState';
import { RequestStates } from '../helpers/convertState';
import { useNavigate, useParams } from 'react-router-dom';
import axios from 'axios';
import Constants from '../constants/Constants';
Expand Down
Loading