Skip to content
Draft
6 changes: 5 additions & 1 deletion website/src/components/Edit/EditPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ const InnerEditPage: FC<EditPageProps> = ({
EditableSequences.fromInitialData(dataToEdit, submissionDataTypes.maxSequencesPerEntry),
);
const [fileMapping, setFileMapping] = useState<FilesBySubmissionId | undefined>(undefined);

const isCreatingRevision = dataToEdit.status === approvedForReleaseStatus;
const extraFilesEnabled = submissionDataTypes.files?.enabled ?? false;

Expand Down Expand Up @@ -167,6 +166,11 @@ const InnerEditPage: FC<EditPageProps> = ({
inputMode='form'
groupId={dataToEdit.groupId}
fileCategories={submissionDataTypes.files?.categories ?? []}
defaultFileMapping={
dataToEdit.submittedData.files
? { [dataToEdit.submissionId]: dataToEdit.submittedData.files }
: undefined
}
setFileMapping={setFileMapping}
onError={(msg) => toast.error(msg, { position: 'top-center', autoClose: false })}
/>
Expand Down
3 changes: 3 additions & 0 deletions website/src/components/Submission/DataUploadForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ export const ExtraFilesUpload = ({
inputMode,
groupId,
fileCategories,
defaultFileMapping,
setFileMapping,
onError,
}: {
Expand All @@ -292,6 +293,7 @@ export const ExtraFilesUpload = ({
inputMode: InputMode;
groupId: number;
fileCategories: FileCategory[];
defaultFileMapping?: FilesBySubmissionId;
setFileMapping: Dispatch<SetStateAction<FilesBySubmissionId | undefined>>;
onError: (message: string) => void;
}) => {
Expand All @@ -315,6 +317,7 @@ export const ExtraFilesUpload = ({
clientConfig={clientConfig}
groupId={groupId}
onError={onError}
defaultFileMapping={defaultFileMapping}
setFileMapping={setFileMapping}
/>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,4 +180,58 @@ describe('FolderUploadComponent', () => {
expect(screen.queryByText('.DS_Store')).not.toBeInTheDocument();
});
});

describe('previous uploads', () => {
// Note: previous uploads are keyed by the real submission id (not the 'dummySubmissionId'
// used for freshly selected form files), so these tests also guard the discard key lookup.
const submissionId = 'SUBMISSION_ID_123';
const formPropsWithPreviousUploads = {
...defaultProps,
inputMode: 'form' as const,
defaultFileMapping: {
[submissionId]: {
extraFiles: [
{ fileId: 'file-1', name: 'previous-a.txt' },
{ fileId: 'file-2', name: 'previous-b.txt' },
],
},
},
};

it('renders previously uploaded files with a "previous upload" label', () => {
render(<FolderUploadComponent {...formPropsWithPreviousUploads} />);

expect(screen.getByText('previous-a.txt')).toBeInTheDocument();
expect(screen.getByText('previous-b.txt')).toBeInTheDocument();
expect(screen.getAllByText('(previous upload)')).toHaveLength(2);
});

it('discards an individual previous upload while keeping the others', async () => {
render(<FolderUploadComponent {...formPropsWithPreviousUploads} />);

await userEvent.click(screen.getByTestId('discard_extraFiles_previous-a.txt'));

await waitFor(() => expect(screen.queryByText('previous-a.txt')).not.toBeInTheDocument());
expect(screen.getByText('previous-b.txt')).toBeInTheDocument();
});

it('reverts to the upload prompt after discarding the last previous upload', async () => {
const singleFileProps = {
...formPropsWithPreviousUploads,
defaultFileMapping: {
[submissionId]: {
extraFiles: [{ fileId: 'file-1', name: 'previous-a.txt' }],
},
},
};
render(<FolderUploadComponent {...singleFileProps} />);

await userEvent.click(screen.getByTestId('discard_extraFiles_previous-a.txt'));

await waitFor(() =>
expect(screen.getByText(`Upload folder: ${defaultProps.fileCategory.displayName}`)).toBeInTheDocument(),
);
expect(screen.queryByText('previous-a.txt')).not.toBeInTheDocument();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ type AwaitingUrlState = {
>;
};

type SingleFileUpload = Pending | Uploaded | Error;
type SingleFileUpload = Pending | Uploaded | PreviousUpload | Error;

type UploadInProgressState = {
type: 'uploadInProgress';
Expand All @@ -40,12 +40,12 @@ type UploadInProgressState = {

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

type FileUploadState = AwaitingUrlState | UploadInProgressState | UploadCompleted;

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

type Pending = {
type: 'pending';
Expand All @@ -68,6 +68,12 @@ type Uploaded = {
size: number;
};

type PreviousUpload = {
type: 'previousUpload';
fileId: string;
name: string;
};

type Error = {
type: 'error';
name: string;
Expand All @@ -81,6 +87,7 @@ type FolderUploadComponentProps = {
accessToken: string;
clientConfig: ClientConfig;
groupId: number;
defaultFileMapping?: FilesBySubmissionId;
setFileMapping: Dispatch<SetStateAction<FilesBySubmissionId | undefined>>;
onError: (message: string) => void;
};
Expand All @@ -91,11 +98,25 @@ export const FolderUploadComponent: FC<FolderUploadComponentProps> = ({
accessToken,
clientConfig,
groupId,
defaultFileMapping,
setFileMapping,
onError,
}) => {
const isClient = useClientFlag();
const [fileUploadState, setFileUploadState] = useState<FileUploadState | undefined>(undefined);
const [fileUploadState, setFileUploadState] = useState<FileUploadState | undefined>(() => {
if (defaultFileMapping === undefined) return undefined;
const previousUploadFiles: Record<SubmissionId, PreviousUpload[]> = {};

Object.entries(defaultFileMapping).forEach(([submissionId, categories]) => {
const fileCategoryFiles = categories[fileCategory.name] ?? [];
previousUploadFiles[submissionId] = fileCategoryFiles.map((file) => ({
type: 'previousUpload',
fileId: file.fileId,
name: file.name,
}));
});
return { type: 'uploadCompleted', files: previousUploadFiles };
});
const [isDragging, setIsDragging] = useState(false);

const backendClient = new BackendClient(clientConfig.backendUrl);
Expand Down Expand Up @@ -179,9 +200,16 @@ export const FolderUploadComponent: FC<FolderUploadComponentProps> = ({
}
} else {
return produce(currentMapping ?? {}, (draft) => {
draft.dummySubmissionId = {
[fileCategory.name]: [],
};
const submissionIds = Object.keys(draft);
if (submissionIds.length === 0) {
draft.dummySubmissionId = {
[fileCategory.name]: [],
};
} else {
submissionIds.forEach((submissionId) => {
draft[submissionId][fileCategory.name] = [];
});
}
});
}
});
Expand Down Expand Up @@ -302,6 +330,19 @@ export const FolderUploadComponent: FC<FolderUploadComponentProps> = ({
}
};

const handleDiscardFile = (submissionId: string, file: SingleFileUpload) => {
setFileUploadState((state) => {
if (state?.type === 'uploadCompleted') {
const remaining = state.files[submissionId].filter((f) => f.name !== file.name);
if (remaining.length === 0) return undefined;
return produce(state, (draft) => {
draft.files[submissionId] = remaining;
});
}
return state;
});
};

return fileUploadState === undefined || fileUploadState.type === 'awaitingUrls' ? (
<div
className={`flex flex-col items-center justify-center flex-1 py-2 px-4 border rounded-lg ${fileUploadState !== undefined ? 'border-hidden' : isDragging ? 'border-dashed border-yellow-400 bg-yellow-50' : 'border-dashed border-gray-900/25'}`}
Expand Down Expand Up @@ -373,7 +414,18 @@ export const FolderUploadComponent: FC<FolderUploadComponentProps> = ({
<h3 className='text-sm font-medium'>Files</h3>
{inputMode === 'form'
? Object.values(fileUploadState.files)[0].map((file) => (
<FileListItem key={file.name} file={file} />
<div key={file.name} className='flex items-center mb-2'>
<div className='flex-1 min-w-0'>
<FileListItem file={file} />
</div>
<Button
onClick={() => handleDiscardFile(Object.keys(fileUploadState.files)[0], file)}
data-testid={`discard_${fileCategory.name}_${file.name}`}
className='text-xs whitespace-nowrap text-gray-700 py-1.5 px-4 border border-gray-300 rounded-md hover:bg-gray-50 ml-2'
>
Discard file
</Button>
</div>
))
: Object.entries(fileUploadState.files).flatMap(([submissionId, files]) => [
<h4 key={submissionId} className='text-xs font-medium py-2'>
Expand All @@ -390,7 +442,7 @@ export const FolderUploadComponent: FC<FolderUploadComponentProps> = ({
data-testid={`discard_${fileCategory.name}`}
className='text-xs break-words text-gray-700 py-1.5 px-4 border border-gray-300 rounded-md hover:bg-gray-50'
>
Discard files
Discard all files
</Button>
</div>
);
Expand All @@ -410,7 +462,11 @@ const FileListItem: FC<FileListeItemProps> = ({ file }) => {
<LucideFile className='h-4 w-4 text-gray-500 ml-1 mr-1' />
<div className='flex-1 min-w-0 flex items-center'>
<span className='text-xs text-gray-700 truncate max-w-[140px]'>{file.name}</span>
<span className='text-xs text-gray-400 ml-2 whitespace-nowrap'>({formatFileSize(file.size)})</span>
{file.type === 'previousUpload' ? (
<span className='text-xs text-gray-400 ml-2 whitespace-nowrap'>(previous upload)</span>
) : (
<span className='text-xs text-gray-400 ml-2 whitespace-nowrap'>({formatFileSize(file.size)})</span>
)}
{showProgress && <span className='text-xs text-blue-500 ml-2'>{percentage}%</span>}
</div>
{/* Status icon */}
Expand All @@ -434,6 +490,7 @@ const getStatusIcon = (status: UploadStatus) => {
switch (status) {
case 'pending':
return <LucideLoader className='animate-spin h-3 w-3 text-blue-500' />;
case 'previousUpload':
case 'uploaded':
return <span className='text-green-500 text-xs'>✓</span>;
case 'error':
Expand Down
Loading