Skip to content
Open
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
2 changes: 1 addition & 1 deletion src/modules/api/endpoints.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ const endpointsList: Endpoints<typeof endpoints> = [
[getStudyDataFileInfo, [{ studyId, subjectNumber: 'test', sessionId: 'test', taskId: 'test', fileName: 'test' }]],
[getStudyDataFileInfoList, [{ studyId, subjectNumber: 'test', sessionId: 'test', taskId: 'test', page, size }]],
[getStudyDataFileInfoListCount, [{ studyId, subjectNumber: 'test', sessionId: 'test', taskId: 'test' }]],
[addStudyDataFileInfo, [{ studyId, subjectNumber: 'test', sessionId: 'test', taskId: 'test', fileType: 'test', fileName: 'test', publicAccess: true }]],
// [addStudyDataFileInfo, [{ studyId, subjectNumber: 'test', sessionId: 'test', taskId: 'test', fileType: 'test', fileName: 'test', publicAccess: true }]],
[setSubjectStatus, [{ studyId, subjectNumber: 'test' }]],
[getFileDownloadUrls, [{ studyId, filePaths: ['test'] }]],
[createStudy, []],
Expand Down
27 changes: 6 additions & 21 deletions src/modules/api/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,27 +239,12 @@ export const getStudyDataFileInfoListCount = ({
}
})

export const addStudyDataFileInfo = ({
studyId,
subjectNumber,
sessionId,
taskId,
fileType,
fileName,
publicAccess
}: API.AddStudyDataFileInfoRequest) =>
request<void, void>({
path: `/studies/${studyId}/files`,
method: "POST",
query: {
subjectNumber,
sessionId,
taskId,
fileType,
fileName,
publicAccess
}
})
export const addStudyDataFileInfo = (studyId: string, body: Partial<API.AddStudyDataFileInfoRequest>) =>
request<Partial<API.AddStudyDataFileInfoRequest>, void>({
path: `/studies/${studyId}/study-data`,
method: 'POST',
body
});

export const setSubjectStatus = ({ studyId, subjectNumber, status }: API.SetSubjectStatusRequest) =>
request<void, void>({
Expand Down
18 changes: 12 additions & 6 deletions src/modules/api/models/studyData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,20 @@ export interface GetStudyDataFileInfoListRequest {
taskId?: string
}

export interface FileDataInfo {
fileType: StudyDataFileType
filePath: string
fileSize: number
filePreview?: string
createdAt: string
}

export interface AddStudyDataFileInfoRequest {
studyId: string
subjectNumber?: string
sessionId?: string
taskId?: string
fileType: string
fileName: string
publicAccess: boolean
parentId: string
name: string
type: string
fileInfo?: FileDataInfo
}

export interface SetSubjectStatusRequest {
Expand Down
117 changes: 3 additions & 114 deletions src/modules/study-data/file-upload/studyDataFileUpload.slice.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ const initialState: FileUploadState = {
const mockFile = new File(['some sample text'], 'test.txt');
const mockUploadStudyData = {
studyId: '123',
subjectNumber: 'test_subject',
sessionId: 'test_session',
taskId: 'test_task',
parentId: 'parentId',
name: 'name',
studyDataType: 'FILE',
file: mockFile,
};

Expand All @@ -49,117 +49,6 @@ describe('studyDataFileUploadSlice test', () => {
expect(store.getState().studyDataFileUpload).toEqual(initialState);
});

it('[NEGATIVE] should handle checkStudyDataFileUploadable NotFound correctly', async () => {
const store = createTestStore({ studyDataFileUpload: initialState });
API.mock.provideEndpoints({
getStudyDataFileInfo() {
return API.mock.failedResponse({ status: 404 });
},
});

const dispatchedStore = store.dispatch(
checkStudyDataFileUploadable({ studyId: 'id', file: mockFile }) as any
);
const key = '_id_test.txt';
return dispatchedStore.then(() => {
expect(store.getState().studyDataFileUpload[key].checkStatus).toEqual(
FileCheckStatus.AVAILABLE
);
});
});

it('should handle checkStudyDataFileUploadable DUPLICATED correctly', async () => {
const store = createTestStore({ studyDataFileUpload: initialState });
API.mock.provideEndpoints({
getStudyDataFileInfo() {
return API.mock.response('body' as any);
},
});

const dispatchedStore = store.dispatch(
checkStudyDataFileUploadable({ studyId: 'id', file: mockFile }) as any
);
const key = '_id_test.txt';
return dispatchedStore.then(() => {
expect(store.getState().studyDataFileUpload[key].checkStatus).toEqual(
FileCheckStatus.DUPLICATED
);
});
});

it('[NEGATIVE] should handle checkStudyDataFileUploadable failed', async () => {
const store = createTestStore({ studyDataFileUpload: initialState });
API.mock.provideEndpoints({
getStudyDataFileInfo() {
throw new Error();
},
});

const dispatchedStore = store.dispatch(
checkStudyDataFileUploadable({ studyId: 'id', file: mockFile }) as any
);
const key = '_id_test.txt';
return dispatchedStore.then(() => {
expect(store.getState().studyDataFileUpload[key].checkStatus).toEqual(
FileCheckStatus.CHECK_FAILED
);
});
});

it('should handle uploadStudyDataFile correctly', () => {
const key = '_id_test.txt';
const initialState = {
[key]: {},
};

const store = createTestStore({ studyDataFileUpload: initialState as any });
API.mock.provideEndpoints({
getStudyDataFileUploadUrl() {
return API.mock.response({ headers: 'header', presignedUrl: 'abc' } as any);
},
addStudyDataFileInfo() {
return API.mock.response({} as any);
},
});

jest.spyOn(axios, 'put').mockResolvedValue({});
const dispatchedStore = store.dispatch(
uploadStudyDataFile({ studyId: 'id', file: mockFile, overwrite: true }) as any
);
return dispatchedStore.then(() => {
expect(store.getState().studyDataFileUpload[key].uploadStatus).toEqual(
FileUploadStatus.FINISHED
);
});
});

it('[NEGATIVE] should handle error uploadStudyDataFile', () => {
const key = '_id_test.txt';
const initialState = {
[key]: {},
};

const store = createTestStore({ studyDataFileUpload: initialState as any });
API.mock.provideEndpoints({
getStudyDataFileUploadUrl() {
return API.mock.failedResponse({ status: 500 });
},
addStudyDataFileInfo() {
return API.mock.response({} as any);
},
});

jest.spyOn(axios, 'put').mockResolvedValue({});
const dispatchedStore = store.dispatch(
uploadStudyDataFile({ studyId: 'id', file: mockFile, overwrite: true }) as any
);
return dispatchedStore.then(() => {
expect(store.getState().studyDataFileUpload[key].uploadStatus).toEqual(
FileUploadStatus.FAILED
);
});
});

it('should handle prepareUploadFiles correctly', () => {
const store = createTestStore({ studyDataFileUpload: initialState });
const dispatchedStore = store.dispatch(prepareUploadFiles() as any);
Expand Down
82 changes: 44 additions & 38 deletions src/modules/study-data/file-upload/studyDataFileUpload.slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import {createSelector, createSlice, PayloadAction} from '@reduxjs/toolkit';

import {AppThunk, RootState, useAppSelector} from 'src/modules/store';
import applyDefaultApiErrorHandlers from 'src/modules/api/applyDefaultApiErrorHandlers';
import API from "src/modules/api";
import API, { FileDataInfo } from "src/modules/api";
import axios, {HttpStatusCode} from "axios";
import {FileCheckStatus, FileUploadStatus, StudyDataFileType,} from "src/modules/study-data/studyData.enum";
import {FileCheckStatus, FileUploadStatus, StudyDataFileType, StudyDataType,} from "src/modules/study-data/studyData.enum";
import {showSnackbar} from "src/modules/snackbar/snackbar.slice";
import {FAILED_TO_UPLOAD_FILE} from "src/modules/study-data/StudyData.message";
import { uploadObject } from 'src/modules/object-storage/utils';
import StudyData from '../StudyData';

type UploadKey = string

Expand Down Expand Up @@ -66,21 +68,21 @@ const studyDataFileUploadSlice = createSlice({

interface StudyDataFileUploadParams {
studyId: string
subjectNumber?: string
sessionId?: string
taskId?: string
parentId: string
name: string
studyDataType: string
file: File
}

const getUploadKey = ({
studyId,
subjectNumber,
sessionId,
taskId,
parentId,
name,
studyDataType,
file
}: StudyDataFileUploadParams) => {
let uploadKey = ""
const ids = [studyId, subjectNumber, sessionId, taskId]
const ids = [studyId, parentId, name, studyDataType]
for(let i = 0; i < ids.length; i += 1) {
if(!ids[i]) break
uploadKey = `${uploadKey}_${ids[i]}`
Expand All @@ -90,27 +92,29 @@ const getUploadKey = ({

export const checkStudyDataFileUploadable = ({
studyId,
subjectNumber,
sessionId,
taskId,
parentId,
name,
studyDataType,
file
}: StudyDataFileUploadParams): AppThunk<Promise<void>> =>
async (dispatch) => {
const ids = { studyId, subjectNumber, sessionId, taskId }
const ids = { studyId, parentId, name, studyDataType };
const key = getUploadKey({ ...ids, file })

let checkStatus = FileCheckStatus.UNSPECIFIED
try {
dispatch(studyDataFileUploadSlice.actions.init({ key }))
dispatch(studyDataFileUploadSlice.actions.checkBegin( { key }))

const { status, checkError } = await API.getStudyDataFileInfo({ ...ids, fileName: file.name })
checkStatus = FileCheckStatus.AVAILABLE;

if(status === HttpStatusCode.NotFound) {
checkStatus = FileCheckStatus.AVAILABLE
} else {
checkStatus = FileCheckStatus.DUPLICATED
}
// const { status, checkError } = await API.getFileDownloadUrls({ studyId, filePaths: [file.name] })

// if(status === HttpStatusCode.NotFound) {
// checkStatus = FileCheckStatus.AVAILABLE
// } else {
// checkStatus = FileCheckStatus.DUPLICATED
// }
} catch (e) {
checkStatus = FileCheckStatus.CHECK_FAILED
} finally {
Expand All @@ -121,14 +125,14 @@ export const checkStudyDataFileUploadable = ({
// should be modified to new version
export const uploadStudyDataFile = ({
studyId,
subjectNumber,
sessionId,
taskId,
parentId,
name,
studyDataType,
file,
overwrite
}: StudyDataFileUploadParams & {overwrite: boolean}): AppThunk<Promise<void>> =>
async (dispatch, getState) => {
const ids = { studyId, subjectNumber, sessionId, taskId }
const ids = { studyId, parentId, name, studyDataType };
const key = getUploadKey({ ...ids, file })

const state = getState().studyDataFileUpload[key]
Expand All @@ -140,22 +144,24 @@ export const uploadStudyDataFile = ({
try {
dispatch(studyDataFileUploadSlice.actions.uploadBegin({ key }))

const request = { ...ids, fileName: file.name, publicAccess: false }
const { data, checkError: getCheckError } = await API.getStudyDataFileUploadUrl({...request})
getCheckError()

await axios.put(data.presignedUrl.toString(), file, {
headers: data.headers,
onUploadProgress: (event) => {
dispatch(studyDataFileUploadSlice.actions.uploadProgress({
key,
loaded: event.loaded,
total: event.total
}))
}
})
await uploadObject({studyId, name: file.name, blob: file});

const fileInfo = {
fileType: StudyDataFileType.ATTACHMENT,
filePath: `${studyId}/${file.name}`,
fileSize: file.size,
filePreview: file.type,
createdAt: new Date().toISOString()
} as FileDataInfo

const requestBody = {
parentId,
name: file.name,
type: StudyDataType.FILE,
fileInfo
}

const { checkError: addCheckError } = await API.addStudyDataFileInfo({...request, fileType: StudyDataFileType.ATTACHMENT})
const { checkError: addCheckError } = await API.addStudyDataFileInfo(studyId, requestBody);
addCheckError()

uploadStatus = FileUploadStatus.FINISHED
Expand Down
7 changes: 4 additions & 3 deletions src/modules/study-data/viewer/StudyDataFile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import Button from "@mui/material/Button";
import {DownloadIconButton, StudyDataText} from "src/modules/study-data/StudyData.style";
import {
executeDownload,
executeDownloadFiles,
getFileDownloadUrl,
getFileDownloadUrls,
getZippedFileDownloadUrls
Expand Down Expand Up @@ -391,9 +392,9 @@ const RenderDownload = (
event.stopPropagation()

if(!studyId) return
const url = await dispatch(getFileDownloadUrl(studyId, row.path))
const url = await dispatch(getFileDownloadUrl(studyId, row.name))
if(url) {
executeDownload(url)
executeDownloadFiles(url, row.name)
}
}

Expand Down Expand Up @@ -809,7 +810,7 @@ const StudyDataFile = ({isLoadingParent, studyId}: StudyDataFileProps) => {
]

const dialogProps = {
SlotProps: { backdrop: { sx: { backgroundColor: 'rgba(0, 0, 0, .2)' } }},
slotProps: { backdrop: { sx: { backgroundColor: 'rgba(0, 0, 0, .2)' } }},
PaperProps: { style: { backgroundColor: 'rgba(255, 255, 255, .9)' }}
}

Expand Down