Skip to content
36 changes: 25 additions & 11 deletions website/src/components/Edit/EditPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { getAccessionVersionString, parseAccessionVersionFromString } from '../.
import { displayConfirmationDialog } from '../ConfirmationDialog.tsx';
import { SequenceEntryHistoryMenu } from '../SequenceDetailsPage/SequenceEntryHistoryMenu.tsx';
import { ExtraFilesUpload } from '../Submission/DataUploadForm.tsx';
import { type FileCategoryStatus } from '../Submission/FileUpload/FolderUploadComponent.tsx';
import { Button } from '../common/Button';
import ErrorBox from '../common/ErrorBox';
import { Spinner } from '../common/Spinner';
Expand Down Expand Up @@ -68,7 +69,14 @@ const InnerEditPage: FC<EditPageProps> = ({
? { [dataToEdit.submissionId]: dataToEdit.submittedData.files }
: undefined,
);

const [fileCategoryStatus, setFileCategoryStatus] = useState<FileCategoryStatus>(() => {
const status: FileCategoryStatus = {};
(submissionDataTypes.files?.categories ?? []).forEach((category) => {
status[category.name] = undefined;
});
return status;
});
const isFileUploadsPending = Object.values(fileCategoryStatus).some((status) => status === 'uploadInProgress');
const isCreatingRevision = dataToEdit.status === approvedForReleaseStatus;

const { mutate: submitRevision, isPending: isRevisionPending } = useSubmitRevision(
Expand All @@ -87,6 +95,20 @@ const InnerEditPage: FC<EditPageProps> = ({
(message) => toast.error(message, { position: 'top-center', autoClose: false }),
);

const handleSubmit = () => {
if (isFileUploadsPending) {
toast.error('Please wait for files to finish uploading before submitting.', {
position: 'top-center',
autoClose: false,
});
return;
}
displayConfirmationDialog({
dialogText: 'Do you really want to submit?',
onConfirmation: submitEditedDataForAccessionVersion,
});
};

const submitEditedDataForAccessionVersion = () => {
const fileMappingForSubmission = extraFilesEnabled ? fileMapping : undefined;

Expand Down Expand Up @@ -210,22 +232,14 @@ const InnerEditPage: FC<EditPageProps> = ({
fileCategories={submissionDataTypes.files?.categories ?? []}
fileMapping={fileMapping}
setFileMapping={setFileMapping}
setFileCategoryStatus={setFileCategoryStatus}
formSubmissionId={dataToEdit.submissionId}
onError={(msg) => toast.error(msg, { position: 'top-center', autoClose: false })}
/>
</div>
)}
<div className={isCreatingRevision ? 'flex justify-end gap-x-6' : 'flex items-center gap-4 mt-4'}>
<Button
variant='primary'
onClick={() =>
displayConfirmationDialog({
dialogText: 'Do you really want to submit?',
onConfirmation: submitEditedDataForAccessionVersion,
})
}
disabled={isPending}
>
<Button variant='primary' onClick={handleSubmit} alsoDisabledIf={isPending}>
{isPending && <Spinner size='sm' className='mr-2' />}
{isCreatingRevision ? 'Upload and proceed to Approval' : 'Submit edits and proceed to Approval'}
</Button>
Expand Down
21 changes: 19 additions & 2 deletions website/src/components/Submission/DataUploadForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { type FormEvent, useState, type Dispatch, type SetStateAction } from 're

import { type FileFactory, FormOrUploadWrapper, type InputMode } from './FormOrUploadWrapper.tsx';
import { getClientLogger } from '../../clientLogger.ts';
import { FolderUploadComponent } from './FileUpload/FolderUploadComponent.tsx';
import { FolderUploadComponent, type FileCategoryStatus } from './FileUpload/FolderUploadComponent.tsx';
import DataUseTermsSelector from '../../components/DataUseTerms/DataUseTermsSelector';
import { SubmissionRouteUtils } from '../../routes/SubmissionRoute.ts';
import { backendApi } from '../../services/backendApi.ts';
Expand All @@ -17,7 +17,7 @@ import {
restrictedDataUseTermsOption,
type FilesBySubmissionId,
} from '../../types/backend.ts';
import type { FileCategory, InputField } from '../../types/config.ts';
import { type FileCategory, type InputField } from '../../types/config.ts';
import type { SubmissionDataTypes } from '../../types/config.ts';
import type { ClientConfig } from '../../types/runtimeConfig.ts';
import { dateTimeInMonths } from '../../utils/DateTimeInMonths.tsx';
Expand Down Expand Up @@ -67,6 +67,14 @@ const InnerDataUploadForm = ({
const { submit, revise, isPending } = useSubmitFiles(accessToken, organism, clientConfig, onSuccess, onError);
const [fileFactory, setFileFactory] = useState<FileFactory | undefined>(undefined);
const [fileMapping, setFileMapping] = useState<FilesBySubmissionId | undefined>(undefined);
const [fileCategoryStatus, setFileCategoryStatus] = useState<FileCategoryStatus>(() => {
const status: FileCategoryStatus = {};
(submissionDataTypes.files?.categories ?? []).forEach((category) => {
status[category.name] = undefined;
});
return status;
});
const isFileUploadsPending = Object.values(fileCategoryStatus).some((status) => status === 'uploadInProgress');
const [dataUseTermsType, setDataUseTermsType] = useState<DataUseTermsOption>(openDataUseTermsOption);
const [restrictedUntil, setRestrictedUntil] = useState<DateTime>(dateTimeInMonths(6));

Expand Down Expand Up @@ -103,6 +111,11 @@ const InnerDataUploadForm = ({
return;
}

if (isFileUploadsPending) {
onError('Please wait for files to finish uploading before proceeding to Approval.');
return;
}

let fileMappingWithSubmissionId = fileMapping;
// for single submission, use the submissionID that the user gave in the form
if (extraFilesEnabled && inputMode === 'form' && fileMapping !== undefined) {
Expand Down Expand Up @@ -179,6 +192,7 @@ const InnerDataUploadForm = ({
onError={onError}
fileMapping={fileMapping}
setFileMapping={setFileMapping}
setFileCategoryStatus={setFileCategoryStatus}
/>
<hr />
</>
Expand Down Expand Up @@ -281,6 +295,7 @@ export const ExtraFilesUpload = ({
fileCategories,
fileMapping,
setFileMapping,
setFileCategoryStatus,
formSubmissionId,
onError,
}: {
Expand All @@ -291,6 +306,7 @@ export const ExtraFilesUpload = ({
fileCategories: FileCategory[];
fileMapping: FilesBySubmissionId | undefined;
setFileMapping: Dispatch<SetStateAction<FilesBySubmissionId | undefined>>;
setFileCategoryStatus: Dispatch<SetStateAction<FileCategoryStatus>>;
formSubmissionId?: string;
onError: (message: string) => void;
}) => {
Expand All @@ -316,6 +332,7 @@ export const ExtraFilesUpload = ({
onError={onError}
fileMapping={fileMapping}
setFileMapping={setFileMapping}
setFileCategoryStatus={setFileCategoryStatus}
formSubmissionId={formSubmissionId}
/>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ vi.mock('../../../utils/multipartUpload', async () => {
});

const mockSetFileMapping = vi.fn();
const mockSetFileCategoryStatus = vi.fn();
const mockOnError = vi.fn();

const defaultProps = {
Expand All @@ -44,6 +45,7 @@ const defaultProps = {
groupId: 1,
fileMapping: undefined,
setFileMapping: mockSetFileMapping,
setFileCategoryStatus: mockSetFileCategoryStatus,
onError: mockOnError,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ type FileAndName = {
name: string;
};

type FileCategoryName = string;

export type FileCategoryStatus = Record<FileCategoryName, string | undefined>;

/**
* The state that the component is in, right after the user dropped the files.
* We're awaiting the presigned upload URLs from the backend, to start uploading.
Expand All @@ -33,29 +37,30 @@ type AwaitingUrlState = {
files: Record<SubmissionId, FileAndName[]>;
};

type SingleFileUpload = Pending | Uploaded | PreviousUpload | Error;

type UploadInProgressState = {
type: 'uploadInProgress';
files: Record<SubmissionId, SingleFileUpload[]>;
files: Record<SubmissionId, FileUpload[]>;
};

type UploadCompleted = {
type: 'uploadCompleted';
files: Record<SubmissionId, (Uploaded | PreviousUpload)[]>;
files: Record<SubmissionId, CompletedFileUpload[]>;
};

type FileUploadState = AwaitingUrlState | UploadInProgressState | UploadCompleted;

type UploadStatus = 'pending' | 'uploaded' | 'previousUpload' | 'error';

type FileUpload = Pending | Uploaded | PreviousUpload | UploadError;

type CompletedFileUpload = Uploaded | PreviousUpload | UploadError;

type Pending = {
type: 'pending';
file: File;
name: string;
size: number;
fileId: string;

urls: string[];
uploadedParts: number;
totalParts: number;
Expand All @@ -76,7 +81,7 @@ type PreviousUpload = {
name: string;
};

type Error = {
type UploadError = {
type: 'error';
name: string;
size: number;
Expand All @@ -91,12 +96,32 @@ type FolderUploadComponentProps = {
groupId: number;
fileMapping: FilesBySubmissionId | undefined;
setFileMapping: Dispatch<SetStateAction<FilesBySubmissionId | undefined>>;
setFileCategoryStatus: Dispatch<SetStateAction<FileCategoryStatus>>;
// Passed when the submissionId is known (e.g. editing/revising an entry) in form mode,
// where it is used instead of the dummySubmissionId placeholder.
formSubmissionId?: string;
onError: (message: string) => void;
};

const getInitialFileUploadState = (fileCategory: FileCategory, fileMapping: FilesBySubmissionId | undefined) => {
if (fileMapping === undefined) return undefined;

const previousUploadFiles: Record<SubmissionId, PreviousUpload[]> = {};
Object.entries(fileMapping).forEach(([submissionId, categories]) => {
const fileCategoryFiles = categories[fileCategory.name] ?? [];
previousUploadFiles[submissionId] = fileCategoryFiles.map((file) => ({
type: 'previousUpload',
fileId: file.fileId,
name: file.name,
}));
});

const hasPreviousFiles = Object.values(previousUploadFiles).some((files) => files.length > 0);
if (!hasPreviousFiles) return undefined;

return { type: 'uploadCompleted', files: previousUploadFiles } as FileUploadState;
};

export const FolderUploadComponent: FC<FolderUploadComponentProps> = ({
fileCategory,
inputMode,
Expand All @@ -105,28 +130,14 @@ export const FolderUploadComponent: FC<FolderUploadComponentProps> = ({
groupId,
fileMapping,
setFileMapping,
setFileCategoryStatus,
formSubmissionId,
onError,
}) => {
const isClient = useClientFlag();
const [fileUploadState, setFileUploadState] = useState<FileUploadState | undefined>(() => {
if (fileMapping === undefined) return undefined;

const previousUploadFiles: Record<SubmissionId, PreviousUpload[]> = {};
Object.entries(fileMapping).forEach(([submissionId, categories]) => {
const fileCategoryFiles = categories[fileCategory.name] ?? [];
previousUploadFiles[submissionId] = fileCategoryFiles.map((file) => ({
type: 'previousUpload',
fileId: file.fileId,
name: file.name,
}));
});

const hasPreviousFiles = Object.values(previousUploadFiles).some((files) => files.length > 0);
if (!hasPreviousFiles) return undefined;

return { type: 'uploadCompleted', files: previousUploadFiles };
});
const [fileUploadState, setFileUploadState] = useState<FileUploadState | undefined>(() =>
getInitialFileUploadState(fileCategory, fileMapping),
);
const [isDragging, setIsDragging] = useState(false);

const backendClient = new BackendClient(clientConfig.backendUrl);
Expand Down Expand Up @@ -182,7 +193,6 @@ export const FolderUploadComponent: FC<FolderUploadComponentProps> = ({
(err) => {
updateFileState(submissionId, pending.fileId, 'error', err.detail);
onError(err.detail);
throw new Error(`Upload of file ${pending.fileId} failed: ${err.detail}`);
},
);
}
Expand Down Expand Up @@ -253,6 +263,10 @@ export const FolderUploadComponent: FC<FolderUploadComponentProps> = ({
for (const [submissionId, files] of Object.entries(fileUploadState.files)) {
pendingFiles[submissionId] = await requestFileUploads(files);
}
setFileCategoryStatus((state) => ({
...state,
[fileCategory.name]: 'uploadInProgress',
}));
setFileUploadState({ type: 'uploadInProgress', files: pendingFiles });
void startUploading(pendingFiles);
})();
Expand All @@ -262,12 +276,16 @@ export const FolderUploadComponent: FC<FolderUploadComponentProps> = ({
if (
Object.values(fileUploadState.files)
.flatMap((x) => x)
.every(({ type }) => type === 'uploaded' || type === 'previousUpload')
.every(({ type }) => type === 'uploaded' || type === 'previousUpload' || type === 'error')
) {
setFileCategoryStatus((state) => ({
...state,
[fileCategory.name]: 'uploadCompleted',
}));
setFileUploadState({
type: 'uploadCompleted',
files: fileUploadState.files as Record<SubmissionId, (Uploaded | PreviousUpload)[]>,
});
files: fileUploadState.files,
} as UploadCompleted);
}
break;
}
Expand All @@ -280,7 +298,9 @@ export const FolderUploadComponent: FC<FolderUploadComponentProps> = ({
} else {
draft[submissionId] = {};
}
draft[submissionId][fileCategory.name] = files;
draft[submissionId][fileCategory.name] = files.filter(
(file) => file.type === 'previousUpload' || file.type === 'uploaded',
);
});
}),
);
Expand Down Expand Up @@ -327,7 +347,7 @@ export const FolderUploadComponent: FC<FolderUploadComponentProps> = ({
}
};

const handleDiscardFile = (submissionId: string, file: SingleFileUpload) => {
const handleDiscardFile = (submissionId: string, file: FileUpload) => {
setFileUploadState((state) => {
if (state?.type === 'uploadCompleted') {
const remainingFiles = state.files[submissionId].filter((f) => f.name !== file.name);
Expand Down Expand Up @@ -374,6 +394,10 @@ export const FolderUploadComponent: FC<FolderUploadComponentProps> = ({
const addAdditionalFiles = async () => {
const nonCollidingFiles = existingFiles.filter((file) => !uniqueSelectedNames.has(file.name));
const newPendingFiles = await requestFileUploads(filesArray.map((f) => ({ file: f, name: f.name })));
setFileCategoryStatus((state) => ({
...state,
[fileCategory.name]: 'uploadInProgress',
}));
setFileUploadState({
type: 'uploadInProgress',
files: { [submissionId]: [...nonCollidingFiles, ...newPendingFiles] },
Expand Down Expand Up @@ -470,7 +494,7 @@ export const FolderUploadComponent: FC<FolderUploadComponentProps> = ({
</div>
<Button
onClick={() => handleDiscardFile(formSubmissionId ?? DUMMY_SUBMISSION_ID, file)}
disabled={fileUploadState.type !== 'uploadCompleted'}
alsoDisabledIf={fileUploadState.type !== 'uploadCompleted'}
data-testid={`discard_${fileCategory.name}_${file.name}`}
variant='outline-neutral'
className='font-normal!'
Expand Down Expand Up @@ -506,7 +530,7 @@ export const FolderUploadComponent: FC<FolderUploadComponentProps> = ({
)}
<Button
onClick={() => document.getElementById(`${fileCategory.name}_add`)?.click()}
disabled={fileUploadState.type !== 'uploadCompleted'}
alsoDisabledIf={fileUploadState.type !== 'uploadCompleted'}
data-testid={`add_button_${fileCategory.name}`}
variant='outline-neutral'
className='font-normal!'
Expand Down Expand Up @@ -537,7 +561,7 @@ export const FolderUploadComponent: FC<FolderUploadComponentProps> = ({
};

type FileListeItemProps = {
file: SingleFileUpload;
file: FileUpload;
};

const FileListItem: FC<FileListeItemProps> = ({ file }) => {
Expand Down
Loading