Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
40a61a4
Create services and models folder before adding in entry components
cbolles Dec 11, 2023
8ebed9f
Add placeholder or upload components
cbolles Dec 11, 2023
1bc26fd
Add in upload session pipe
cbolles Dec 11, 2023
0056de7
Begin adding in logic for completing a session
cbolles Dec 11, 2023
063fc25
Add dataset selection to upload logic
cbolles Dec 12, 2023
b1b4574
Add the ability to go back on dataset selection
cbolles Dec 12, 2023
b6ce536
Further build out CSV upload sectiong
cbolles Dec 13, 2023
2ed0c0c
Begin working on adding in CSV upload support to backend
cbolles Dec 13, 2023
3439dcb
Working CSV upload
cbolles Dec 13, 2023
bae3d86
Begin work on the CSV validation
cbolles Dec 13, 2023
684a4d7
CSV downloading on backend
cbolles Dec 13, 2023
e51827f
Reading of CSV file
cbolles Dec 14, 2023
f272e8f
Working CSV validation
cbolles Dec 14, 2023
0abd342
Clean session deletion
cbolles Dec 14, 2023
3a9a3d6
Implement upload step checks
cbolles Dec 15, 2023
b0b97b8
Support for video upload
cbolles Dec 15, 2023
d2e2300
Have entries uploaded to a dedicated prefix
cbolles Dec 15, 2023
878f83f
Add status bar to upload
cbolles Dec 15, 2023
18ca3f5
Completion of upload process
cbolles Dec 18, 2023
a762cfd
Add support for images, ensure names are unique in bucket
cbolles Dec 18, 2023
5bcddbb
Add status enum to upload result
cbolles Dec 18, 2023
4cd496b
Handle having model reaching to completion
cbolles Dec 18, 2023
e0f9ff1
Clear model on close
cbolles Dec 18, 2023
cf68e4f
Add content type to entry
cbolles Dec 18, 2023
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
777 changes: 750 additions & 27 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@mui/x-date-pickers": "^6.9.0",
"ajv": "^8.12.0",
"ajv-errors": "^3.0.0",
"axios": "^1.6.2",
"esbuild": "^0.19.0",
"graphql": "^16.8.0",
"injection-js": "^2.4.0",
Expand Down
23 changes: 13 additions & 10 deletions packages/client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { ApolloClient, ApolloProvider, InMemoryCache, concat, createHttpLink } f
import { setContext } from '@apollo/client/link/context';
import { StudyProvider } from './context/Study.context';
import { ConfirmationProvider } from './context/Confirmation.context';
import { DatasetProvider } from './context/Dataset.context';

const drawerWidth = 256;
const Main = styled('main', { shouldForwardProp: (prop) => prop !== 'open' })<{
Expand Down Expand Up @@ -89,17 +90,19 @@ const AppInternal: FC = () => {
const mainView: ReactNode = (
<ProjectProvider>
<StudyProvider>
<Box>
<NavBar drawerOpen={drawerOpen} setDrawerOpen={setDrawerOpen} />
</Box>
<Main open={drawerOpen}>
<Box sx={{ display: 'flex' }}>
<SideBar open={drawerOpen} drawerWidth={drawerWidth} />
<Box sx={{ flexGrow: 1, width: '90%' }}>
<MyRoutes />
</Box>
<DatasetProvider>
<Box>
<NavBar drawerOpen={drawerOpen} setDrawerOpen={setDrawerOpen} />
</Box>
</Main>
<Main open={drawerOpen}>
<Box sx={{ display: 'flex' }}>
<SideBar open={drawerOpen} drawerWidth={drawerWidth} />
<Box sx={{ flexGrow: 1, width: '90%' }}>
<MyRoutes />
</Box>
</Box>
</Main>
</DatasetProvider>
</StudyProvider>
</ProjectProvider>
);
Expand Down
196 changes: 100 additions & 96 deletions packages/client/src/components/UploadEntries.component.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
import Button from '@mui/material/Button';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import DownloadIcon from '@mui/icons-material/Download';
import { useState } from 'react';
import { Box, FormControl, IconButton, MenuItem, Paper, Select, Step, StepContent, StepLabel, Stepper, Typography } from '@mui/material';
import { useState, useEffect } from 'react';
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Box,
Step,
StepContent,
StepLabel,
Stepper,
Typography,
} from '@mui/material';
import { Dataset, UploadSession } from '../graphql/graphql';
import { CSVUpload } from './upload/CSVUpload.component';
import { StatusMessage } from '../models/StatusMessage';
import { DatasetSelect } from './upload/DatasetSelect.component';
import { EntryUpload } from './upload/EntryUpload.component';

interface ShowProps {
show: boolean;
Expand All @@ -14,135 +25,128 @@ interface ShowProps {

export const UploadEntries: React.FC<ShowProps> = (props: ShowProps) => {
const [activeStep, setActiveStep] = useState(0);
const [name, setName] = useState('');
const [nameValid, setNameValid] = useState(false);
const [entriesValid, setEntriesValid] = useState(false);
const [videosValid, setVideosValid] = useState(false);
const [selectedDataset, setSelectedDataset] = useState<Dataset | null>(null);
const [currentStepLimit, setCurrentStepLimit] = useState(0);
const [uploadSession, setUploadSession] = useState<UploadSession | null>(null);
const [validationMessage, setValidationMessage] = useState<StatusMessage | null>(null);
const [csvValid, setCsvValid] = useState<boolean>(false);
const [entryUploadComplete, setEntryUploadComplete] = useState<boolean>(false);

const handleNameChange = (event: any) => {
setName(event.target.value);
setNameValid(true);
};

//need to add logic for checking csv format
//currently the funcion validates as soon as user
//clicks the button for both CSV and Videos upload
const handleEntriesUpload = () => {
setEntriesValid(true);
};
useEffect(() => {
let activeStep = 0;
let currentStepLimit = 0;

const handleVideosUpload = () => {
setVideosValid(true);
};
if (selectedDataset) {
activeStep++;
currentStepLimit++;
}

const handleNext = () => {
if (activeStep == 0 && nameValid) {
console.log('first spot');
setActiveStep((prevActiveStep) => prevActiveStep + 1);
} else if (activeStep == 1 && entriesValid) {
console.log('second spot');
setActiveStep((prevActiveStep) => prevActiveStep + 1);
} else if (activeStep == 2 && videosValid) {
console.log('third spot');
setActiveStep((prevActiveStep) => prevActiveStep + 1);
if (selectedDataset && csvValid) {
activeStep++;
currentStepLimit++;
}
};

const handleBack = () => {
setActiveStep((prevActiveStep) => prevActiveStep - 1);
};
if (selectedDataset && csvValid && entryUploadComplete) {
currentStepLimit++;
}

const handleReset = () => {
setActiveStep(0);
};
setActiveStep(activeStep);
setCurrentStepLimit(currentStepLimit);
}, [selectedDataset, csvValid, entryUploadComplete]);

const steps = [
{
label: 'Select Dataset to Upload To',
description: `Select Existing Dataset.`,
element: (
<FormControl variant="standard" sx={{ m: 1, minWidth: 120 }}>
<Select sx={{ width: 200 }} labelId="demo-simple-select-standard-label" id="demo-simple-select-standard" value={name} onChange={handleNameChange} label="name">
<MenuItem value={10}>
<Typography variant="body2">dataset name 1</Typography>
</MenuItem>
<MenuItem value={20}>
<Typography variant="body2">collection of data</Typography>
</MenuItem>
<MenuItem value={30}>
<Typography variant="body2">verbs and conjugations</Typography>
</MenuItem>
</Select>
</FormControl>
)
element: <DatasetSelect setSelectedDataset={setSelectedDataset} selectedDataset={selectedDataset} />
},
{
label: 'Upload Information on Entries',
description: '',
element: (
<Box sx={{ display: 'flex', flexDirection: 'row' }}>
<Button onClick={handleEntriesUpload}>Upload CSV</Button>
<IconButton sx={{ color: 'darkgreen', marginLeft: '20px' }}>
<DownloadIcon />
</IconButton>
</Box>
)
element: <CSVUpload
dataset={selectedDataset}
uploadSession={uploadSession}
setUploadSession={setUploadSession}
setValidationMessage={setValidationMessage}
setCsvValid={setCsvValid}
/>
},
{
label: 'Upload Entry Videos',
description: '',
element: (
<Button onClick={handleVideosUpload} variant="outlined" sx={{ margin: '10px' }}>
Upload Videos (ZIP)
</Button>
)
element: <EntryUpload
uploadSession={uploadSession}
setValidationMessage={setValidationMessage}
setEntryUploadComplete={setEntryUploadComplete} />
}
];

const onClose = () => {
props.toggleModal();
setActiveStep(0);
setCurrentStepLimit(0);
setSelectedDataset(null);
setUploadSession(null);
setValidationMessage(null);
setCsvValid(false);
setEntryUploadComplete(false);
}

const nextOrComplete = () => {
if (activeStep === steps.length - 1) {
onClose();
} else {
if (selectedDataset) {
setActiveStep((prevActiveStep) => prevActiveStep + 1);
}
setActiveStep(activeStep + 1);
}
};


return (
<div>
<Dialog open={props.show} onClose={props.toggleModal}>
<Dialog open={props.show} onClose={onClose}>
<DialogTitle sx={{ fontWeight: 'bold', marginTop: '10px' }}>New Entry Upload</DialogTitle>
<DialogContent>
<Box sx={{ minWidth: 400 }}>
<Stepper sx={{ minWidth: 400 }} activeStep={activeStep} orientation="vertical">
{steps.map((step, index) => (
{steps.map((step) => (
<Step key={step.label}>
<StepLabel optional={index === 2 ? <Typography variant="caption">Last step</Typography> : null}>{step.label}</StepLabel>
<StepLabel>{step.label}</StepLabel>
<StepContent>
<Typography variant="body2">{step.description}</Typography>
{step.element ? step.element : null}
<Box sx={{ mb: 2 }}>
<div>
<Button variant="contained" onClick={handleNext} sx={{ mt: 1, mr: 1 }}>
{index === steps.length - 1 ? 'Finish' : 'Continue'}
</Button>
<Button disabled={index === 0} onClick={handleBack} sx={{ mt: 1, mr: 1 }}>
Back
</Button>
</div>
</Box>
{step.element}
</StepContent>
</Step>
))}
</Stepper>
{activeStep === steps.length && (
<Paper square elevation={0} sx={{ p: 3 }}>
<Typography>All steps completed - you&apos;re finished</Typography>
<Button onClick={handleReset} sx={{ mt: 1, mr: 1 }}>
Reset
</Button>
</Paper>
)}
<ValidationMessageDisplay validationMessage={validationMessage} />
</Box>
</DialogContent>
<DialogActions sx={{ marginBottom: '15px', marginRight: '15px' }}>
<Button onClick={props.toggleModal}>Cancel</Button>
<Button variant="contained" onClick={props.toggleModal}>
Upload
{activeStep != 0 && <Button onClick={() => setActiveStep(activeStep - 1)}>Back</Button>}
<Button onClick={onClose}>Cancel</Button>
<Button
onClick={nextOrComplete}
disabled={activeStep >= currentStepLimit}>
{activeStep == steps.length - 1 ? 'Complete' : 'Next'}
</Button>
</DialogActions>
</Dialog>
</div>
);
};

interface ValidationMessageDisplayProps {
validationMessage: StatusMessage | null;
}

// TODO: Have the display have various states for each severity
const ValidationMessageDisplay: React.FC<ValidationMessageDisplayProps> = ({ validationMessage }) => {
return (
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
{validationMessage && <Typography variant="body2">{validationMessage.message}</Typography>}
</Box>
);
};
96 changes: 96 additions & 0 deletions packages/client/src/components/upload/CSVUpload.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Dataset, UploadSession, UploadStatus } from '../../graphql/graphql';
import { Dispatch, SetStateAction, ChangeEvent } from 'react';
import { StatusMessage } from '../../models/StatusMessage';
import { CreateUploadSessionDocument, GetCsvUploadUrlDocument, ValidateCsvDocument } from '../../graphql/upload-session/upload-session';
import { useApolloClient } from '@apollo/client';
import axios from 'axios';
import { Box, Button } from '@mui/material';
import UploadIcon from '@mui/icons-material/Upload';


export interface CSVUploadProps {
dataset: Dataset | null;
uploadSession: UploadSession | null;
setUploadSession: Dispatch<SetStateAction<UploadSession | null>>;
setValidationMessage: Dispatch<SetStateAction<StatusMessage | null>>;
setCsvValid: Dispatch<SetStateAction<boolean>>;
}

export const CSVUpload: React.FC<CSVUploadProps> = ({ dataset, setUploadSession, setValidationMessage, setCsvValid }) => {
const apolloClient = useApolloClient();

// Implemented with using the apollo client directly instead of the useMutation hook
// to reduce the need for multiple use effects to handle each step change
const handleCSVChange = async (event: ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) {
return;
}

// First create an upload session
const sessionCreation = await apolloClient.mutate({
mutation: CreateUploadSessionDocument,
variables: { dataset: dataset?._id }
});

if (!sessionCreation.data?.createUploadSession) {
console.error('Failed to create upload session');
return;
}

const uploadSession = sessionCreation.data.createUploadSession;
setUploadSession(uploadSession);

// Next get the upload url
const uploadUrlQuery = await apolloClient.query({
query: GetCsvUploadUrlDocument,
variables: { session: uploadSession._id }
});


if (!uploadUrlQuery.data?.getCSVUploadURL) {
console.error('Failed to get upload url');
return;
}

const uploadUrl = uploadUrlQuery.data.getCSVUploadURL;

// Upload the CSV to the url
const upload = await axios.put(uploadUrl, file, {
headers: {
'Content-Type': 'text/csv'
}
});

if (upload.status != 200) {
console.error('Failed to upload CSV');
return;
}

// Trigger the CSV validation
const validation = await apolloClient.query({
query: ValidateCsvDocument,
variables: { session: uploadSession._id }
});

// Share any validation results
const result = validation.data!.validateCSV;
if (result.status == UploadStatus.Success ) {
setValidationMessage({ severity: 'success', message: 'CSV validated successfully' });
setCsvValid(true);
} else {
setValidationMessage({ severity: 'error', message: result.message });
setCsvValid(false);
}
};


return (
<Box sx={{ display: 'flex', flexDirection: 'row' }}>
<Button component="label" variant="contained" color="primary" startIcon={<UploadIcon />} sx={{ m: 1 }}>
Upload CSV
<input type="file" hidden onChange={handleCSVChange} accept='.csv' />
</Button>
</Box>
);
};
Loading