Skip to content

Commit

Permalink
[MDS-5720] Order file upload parts ascending, fix errors when uploadi…
Browse files Browse the repository at this point in the history
…ng multiple files (#2881)

* Fixed errors when uploading files

* MDS-5720 Fixed Project summary file replacement functionality

* MDS-5720 Removed comments

* MDS-5720 Cleanup

* Fixed broken tests
  • Loading branch information
simensma-fresh authored Jan 12, 2024
1 parent 13661d1 commit 2eb0b6c
Show file tree
Hide file tree
Showing 7 changed files with 274 additions and 95 deletions.
136 changes: 106 additions & 30 deletions services/common/src/components/forms/RenderFileUpload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ import withFeatureFlag from "@mds/common/providers/featureFlags/withFeatureFlag"
import { createRequestHeader } from "@mds/common/redux/utils/RequestHeaders";
import { FLUSH_SOUND, WATER_SOUND } from "@mds/common/constants/assets";
import { getSystemFlag } from "@mds/common/redux/selectors/authenticationSelectors";
import { MultipartDocumentUpload } from "@mds/common/utils/fileUploadHelper.interface";
import {
MultipartDocumentUpload,
UploadResult,
} from "@mds/common/utils/fileUploadHelper.interface";
import { HttpRequest, HttpResponse } from "tus-js-client";
import { BaseInputProps } from "./BaseInput";

Expand All @@ -33,7 +36,7 @@ type AfterSuccessActionType = [
interface FileUploadProps extends BaseInputProps {
uploadUrl: string;
acceptedFileTypesMap?: { [key: string]: string };
onFileLoad?: (fileName?: string, documentGuid?: string) => void;
onFileLoad?: (fileName?: string, documentGuid?: string, versionGuid?: string) => void;
chunkSize?: number;
onAbort?: () => void;
onUploadResponse?: (data: MultipartDocumentUpload) => void;
Expand Down Expand Up @@ -94,25 +97,40 @@ export const FileUpload = (props: FileUploadProps) => {
const system = useSelector(getSystemFlag);

const [showWhirlpool, setShowWhirlpool] = useState(false);
const [uploadResults, setUploadResults] = useState([]);
const [uploadData, setUploadData] = useState(null);
const [uploadProcess, setUploadProcess] = useState({
fieldName: null,
file: props.file || null,
metadata: null,
load: null,
error: null,
progress: null,
abort: null,
});

// Used to store intermittent results of upload parts to enable
// retries of parts that fail.
const [uploadResults, setUploadResults] = useState<{ [fileId: string]: UploadResult[] }>({});

// Used to store upload information about each upload and part
// including pre-signed urls to enable retries of parts that fail,
// and replace file functionality
const [uploadData, setUploadData] = useState<{ [fileId: string]: MultipartDocumentUpload }>({});

// Stores metadata and process function for each file, so we can manually
// trigger it. Currently, this is being used for the replace file functionality
// which dynamically changes the URL of the upload if you confirm the replacement
const [uploadProcess, setUploadProcess] = useState<{
[fileId: string]: {
fieldName: string;
file: File;
metadata: any;
load: (documentGuid: string) => void;
error: (file: File, err: any) => void;
progress: (file: File, progress: number) => void;
abort: () => void;
};
}>({});

let waterSound;
let flushSound;
let filepond;

const handleError = (file, err) => {
try {
const response = JSON.parse(err.originalRequest.getUnderlyingObject().response);
const response = err.originalRequest
? JSON.parse(err.originalRequest.getUnderlyingObject().response)
: err || {};

if (
!(
Expand All @@ -125,6 +143,7 @@ export const FileUpload = (props: FileUploadProps) => {
duration: 10,
});
}

if (props.onError) {
props.onError(file && file.name ? file.name : "", err);
}
Expand All @@ -136,7 +155,7 @@ export const FileUpload = (props: FileUploadProps) => {
}
};

const handleSuccess = (documentGuid, file, load, abort) => {
const handleSuccess = (documentGuid, file, load, abort, versionGuid?) => {
let intervalId; // eslint-disable-line prefer-const

const pollUploadStatus = async () => {
Expand All @@ -145,7 +164,7 @@ export const FileUpload = (props: FileUploadProps) => {
clearInterval(intervalId);
if (response.data.status === "Success") {
load(documentGuid);
props.onFileLoad(file.name, documentGuid);
props.onFileLoad(file.name, documentGuid, versionGuid);

if (props?.afterSuccess?.action) {
try {
Expand Down Expand Up @@ -191,34 +210,56 @@ export const FileUpload = (props: FileUploadProps) => {
intervalId = setInterval(pollUploadStatus, 1000);
};

function _s3MultipartUpload(uploadUrl, file, metadata, load, error, progress, abort) {
const setUploadResultsFor = (fileId, results) => {
setUploadResults({
...uploadResults,
[fileId]: results,
});
};

const setUploadProcessFor = (fileId, process) => {
setUploadProcess({
...uploadProcess,
[fileId]: process,
});
};
const setUploadDataFor = (fileId, data) => {
setUploadData({
...uploadData,
[fileId]: data,
});
};

function _s3MultipartUpload(fileId, uploadUrl, file, metadata, load, error, progress, abort) {
return new FileUploadHelper(file, {
uploadUrl: ENVIRONMENT.apiUrl + uploadUrl,
uploadResults: uploadResults,
uploadData: uploadData,
// Pass along results and upload configuration if exists from
// previous upload attempts for this file. Occurs if retrying a failed upload.
uploadResults: uploadResults[fileId],
uploadData: uploadData[fileId],
metadata: {
filename: file.name,
filetype: file.type || APPLICATION_OCTET_STREAM,
},
onError: (err, uploadResults) => {
setUploadResults(uploadResults);
setUploadResultsFor(fileId, uploadResults);
handleError(file, err);
error(err);
},
onInit: (uploadData) => {
setUploadData(uploadData);
setUploadDataFor(fileId, uploadData);
},
onProgress: (bytesUploaded, bytesTotal) => {
progress(true, bytesUploaded, bytesTotal);
},
onSuccess: (documentGuid) => {
handleSuccess(documentGuid, file, load, abort);
onSuccess: (documentGuid, versionGuid) => {
handleSuccess(documentGuid, file, load, abort, versionGuid);
},
onUploadResponse: props.onUploadResponse,
});
}

function _tusdUpload(uploadUrl, file, metadata, load, error, progress, abort) {
function _tusdUpload(fileId, uploadUrl, file, metadata, load, error, progress, abort) {
const upload = new tus.Upload(file, {
endpoint: ENVIRONMENT.apiUrl + uploadUrl,
retryDelays: [100, 1000, 3000],
Expand Down Expand Up @@ -269,10 +310,20 @@ export const FileUpload = (props: FileUploadProps) => {
}

const server = {
process: (fieldName, file, metadata, load, error, progress, abort) => {
process: (
fieldName,
file,
metadata,
load,
error,
progress,
abort,
transfer = null,
options = null
) => {
let upload;

setUploadProcess({
setUploadProcessFor(metadata.filepondid, {
fieldName,
file,
metadata,
Expand All @@ -281,15 +332,34 @@ export const FileUpload = (props: FileUploadProps) => {
progress,
abort,
});
setUploadData(null);
setUploadResults([]);

setUploadDataFor(metadata.filepondid, null);
setUploadResultsFor(metadata.filepondid, []);

const uploadUrl = props.shouldReplaceFile ? props.replaceFileUploadUrl : props.uploadUrl;

if (props.isFeatureEnabled("s3_multipart_upload")) {
upload = _s3MultipartUpload(uploadUrl, file, metadata, load, error, progress, abort);
upload = _s3MultipartUpload(
metadata.filepondid,
uploadUrl,
file,
metadata,
load,
error,
progress,
abort
);
} else {
upload = _tusdUpload(uploadUrl, file, metadata, load, error, progress, abort);
upload = _tusdUpload(
metadata.filepondid,
uploadUrl,
file,
metadata,
load,
error,
progress,
abort
);
}

upload.start();
Expand Down Expand Up @@ -335,6 +405,11 @@ export const FileUpload = (props: FileUploadProps) => {
};
}, []);

const handleFileAdd = (err, file) => {
// Add ID to file metadata so we can reference it later
file.setMetadata("filepondid", file.id);
};

const fileValidateTypeLabelExpectedTypesMap = invert(props.acceptedFileTypesMap);
const acceptedFileTypes = uniq(Object.values(props.acceptedFileTypesMap));

Expand Down Expand Up @@ -392,6 +467,7 @@ export const FileUpload = (props: FileUploadProps) => {
// maxFiles={props.maxFiles || undefined}
allowFileTypeValidation={acceptedFileTypes.length > 0}
acceptedFileTypes={acceptedFileTypes}
onaddfile={handleFileAdd}
onprocessfiles={props.onProcessFiles}
onprocessfileabort={props.onAbort}
// oninit={props.onInit}
Expand Down
37 changes: 22 additions & 15 deletions services/common/src/redux/customAxios.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import axios from "axios";
import { notification, Button } from "antd";
import * as String from "@mds/common/constants/strings";
import React from 'react';
import React from "react";
import * as API from "@mds/common/constants/API";
import { ENVIRONMENT } from "@mds/common";
import { createRequestHeader } from "./utils/RequestHeaders";
Expand All @@ -20,18 +20,19 @@ const formatErrorMessage = (errorMessage) => {
return errorMessage.replace("(psycopg2.", "(DatabaseError.");
};

const notifymAdmin = (error) => {
let CustomAxios;

const notifymAdmin = (error) => {
const business_message = error?.response?.data?.message;
const detailed_error = error?.response?.data?.detailed_error;

const payload = {
"business_error": business_message,
"detailed_error": detailed_error
business_error: business_message,
detailed_error: detailed_error,
};

// @ts-ignore
CustomAxios().post(ENVIRONMENT.apiUrl + API.REPORT_ERROR, payload, createRequestHeader())
CustomAxios()
.post(ENVIRONMENT.apiUrl + API.REPORT_ERROR, payload, createRequestHeader())
.then((response) => {
notification.success({
message: "Error details sent to Admin. Thank you.",
Expand All @@ -41,11 +42,10 @@ const notifymAdmin = (error) => {
})
.catch((err) => {
throw new Error(err);
})
});
};

// @ts-ignore
const CustomAxios = ({ errorToastMessage, suppressErrorNotification = false } = {}) => {
CustomAxios = ({ errorToastMessage = null, suppressErrorNotification = false } = {}) => {
const instance = axios.create();

instance.interceptors.response.use(
Expand All @@ -63,21 +63,29 @@ const CustomAxios = ({ errorToastMessage, suppressErrorNotification = false } =
(errorToastMessage === "default" || errorToastMessage === undefined) &&
!suppressErrorNotification
) {
console.error('Detailed Error: ', error?.response?.data?.detailed_error)
const notificationKey = 'errorNotification';
console.error("Detailed Error: ", error?.response?.data?.detailed_error);
const notificationKey = "errorNotification";

if (isFeatureEnabled(Feature.REPORT_ERROR)) {
notification.error({
key: notificationKey,
message: formatErrorMessage(error?.response?.data?.message ?? String.ERROR),
description: <p style={{ color: 'grey' }}>If you think this is a system error please help us to improve by informing the system Admin</p>,
description: (
<p style={{ color: "grey" }}>
If you think this is a system error please help us to improve by informing the
system Admin
</p>
),
duration: 10,
btn: (
<Button type="primary" size="small"
<Button
type="primary"
size="small"
onClick={() => {
notifymAdmin(error);
notification.close(notificationKey);
}}>
}}
>
Tell Admin
</Button>
),
Expand All @@ -89,7 +97,6 @@ const CustomAxios = ({ errorToastMessage, suppressErrorNotification = false } =
duration: 10,
});
}

} else if (errorToastMessage && !suppressErrorNotification) {
notification.error({
message: errorToastMessage,
Expand Down
2 changes: 1 addition & 1 deletion services/common/src/utils/fileUploadHelper.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export interface FileUploadHelperProps {
uploadData?: MultipartDocumentUpload;
onError: (err, uploadResults: UploadResult[]) => void;
onProgress: (bytesUploaded: number, bytesTotal: number) => void;
onSuccess: (documentManagerGuid: string) => void;
onSuccess: (documentManagerGuid: string, documentManagerVersionGuid?: string) => void;
onInit?: (uploadData: MultipartDocumentUpload) => void;
onUploadResponse?: (data: MultipartDocumentUpload) => void;
retryDelayMs?: number;
Expand Down
Loading

0 comments on commit 2eb0b6c

Please sign in to comment.