Repository: vgteam/sequenceTubeMap
Pull request #343 (2023-10-02 00:26:18)

Issues/338 upload track files
#338 

- Remove the current custom upload page
- Allow files to be uploaded through the custom track system


Patch:
diff --git a/src/components/FileUploadFormRow.js b/src/components/FileUploadFormRow.js
deleted file mode 100644
index 778ef3b..0000000
--- a/src/components/FileUploadFormRow.js
+++ /dev/null
@@ -1,195 +0,0 @@
-import React, { Component } from "react";
-import PropTypes from "prop-types";
-import { Label, Input } from "reactstrap";
-
-const MAX_UPLOAD_SIZE = 5242880;
-
-class FileUploadFormRow extends Component {
-  constructor(props) {
-    super(props);
-    this.graphFileInput = React.createRef();
-    this.gbwtFileInput = React.createRef();
-    this.gamFileInput = React.createRef();
-    this.gamFile2Input = React.createRef();
-  }
-
-  onGraphFileChange = () => {
-    const file = this.graphFileInput.current.files[0];
-    if (file === undefined) {
-      this.props.handleFileUpload("graphFile", "none");
-    } else {
-      if (file.size > MAX_UPLOAD_SIZE) {
-        this.graphFileInput.current.value = "";
-        this.props.showFileSizeAlert();
-        return;
-      }
-      this.props.setUploadInProgress(true);
-      const formData = new FormData();
-      formData.append("graphFile", file);
-      const xhr = new XMLHttpRequest();
-      xhr.responseType = "json";
-      xhr.onreadystatechange = () => {
-        if (xhr.readyState === 4 && xhr.status === 200) {
-          // Every thing ok, file uploaded
-          this.props.setUploadInProgress(false);
-          this.props.handleFileUpload("graphFile", xhr.response.path);
-          this.props.getPathNames(xhr.response.path, "upload");
-        }
-      };
-      xhr.open("POST", `${this.props.apiUrl}/graphFileSubmission`, true);
-      xhr.send(formData);
-    }
-  };
-
-  onGbwtFileChange = () => {
-    const file = this.gbwtFileInput.current.files[0];
-    if (file === undefined) {
-      this.props.handleFileUpload("gbwtFile", "none");
-    } else {
-      if (file.size > MAX_UPLOAD_SIZE) {
-        this.gbwtFileInput.current.value = "";
-        this.props.showFileSizeAlert();
-        return;
-      }
-      this.props.setUploadInProgress(true);
-      const formData = new FormData();
-      formData.append("gbwtFile", file);
-      const xhr = new XMLHttpRequest();
-      xhr.responseType = "json";
-      xhr.onreadystatechange = () => {
-        if (xhr.readyState === 4 && xhr.status === 200) {
-          // Every thing ok, file uploaded
-          this.props.setUploadInProgress(false);
-          this.props.handleFileUpload("gbwtFile", xhr.response.path);
-        }
-      };
-      xhr.open("POST", `${this.props.apiUrl}/gbwtFileSubmission`, true);
-      xhr.send(formData);
-    }
-  };
-
-  onGamFileChange = () => {
-    const file = this.gamFileInput.current.files[0];
-    if (file === undefined) {
-      this.props.handleFileUpload("gamFile", "none");
-    } else {
-      if (file.size > MAX_UPLOAD_SIZE) {
-        this.gamFileInput.current.value = "";
-        this.props.showFileSizeAlert();
-        return;
-      }
-      this.props.setUploadInProgress(true);
-      const formData = new FormData();
-      formData.append("gamFile", file);
-      const xhr = new XMLHttpRequest();
-      xhr.responseType = "json";
-      xhr.onreadystatechange = () => {
-        if (xhr.readyState === 4 && xhr.status === 200) {
-          // Every thing ok, file uploaded
-          this.props.setUploadInProgress(false);
-          this.props.handleFileUpload("gamFile", xhr.response.path);
-        }
-      };
-      xhr.open("POST", `${this.props.apiUrl}/gamFileSubmission`, true);
-      xhr.send(formData);
-    }
-  };
-
-  onGamFile2Change = () => {
-    const file = this.gamFile2Input.current.files[0];
-    if (file === undefined) {
-      this.props.handleFileUpload("gamFile2", "none");
-    } else {
-      if (file.size > MAX_UPLOAD_SIZE) {
-        this.gamFile2Input.current.value = "";
-        this.props.showFileSizeAlert();
-        return;
-      }
-      this.props.setUploadInProgress(true);
-      const formData = new FormData();
-      formData.append("gamFile2", file);
-      const xhr = new XMLHttpRequest();
-      xhr.responseType = "json";
-      xhr.onreadystatechange = () => {
-        if (xhr.readyState === 4 && xhr.status === 200) {
-          // Every thing ok, file uploaded
-          this.props.setUploadInProgress(false);
-          this.props.handleFileUpload("gamFile2", xhr.response.path);
-        }
-      };
-      xhr.open("POST", `${this.props.apiUrl}/gamFile2Submission`, true);
-      xhr.send(formData);
-    }
-  };
-
-  render() {
-    return (
-      <React.Fragment>
-        <Label className="customData tight-label mb-2 mr-sm-2 mb-sm-0 ml-2">
-          graph file:
-        </Label>
-        <Input
-          type="file"
-          className="customDataUpload form-control-file"
-          id="graphFileUpload"
-          accept=".xg,.vg,.hg,.gbz,.pg"
-          innerRef={this.graphFileInput}
-          onChange={this.onGraphFileChange}
-        />
-        <Label
-          for="gbwtFileSelect"
-          className="customData tight-label mb-2 mr-sm-2 mb-sm-0 ml-2"
-        >
-          gbwt file:
-        </Label>
-        <Input
-          type="file"
-          className="customDataUpload form-control-file"
-          id="gbwtFileUpload"
-          accept=".gbwt,.gbz"
-          innerRef={this.gbwtFileInput}
-          onChange={this.onGbwtFileChange}
-        />
-        <Label
-          for="gamFileSelect"
-          className="customData tight-label mb-2 mr-sm-2 mb-sm-0 ml-2"
-        >
-          gam index:
-        </Label>
-        <Input
-          type="file"
-          className="customDataUpload form-control-file"
-          id="gamFileUpload"
-          accept=".gam"
-          innerRef={this.gamFileInput}
-          onChange={this.onGamFileChange}
-        />
-        <Label
-          for="gamFile2Select"
-          className="customData tight-label mb-2 mr-sm-2 mb-sm-0 ml-2"
-        >
-          gam index 2:
-        </Label>
-        <Input
-          type="file"
-          className="customDataUpload form-control-file"
-          id="gamFile2Upload"
-          accept=".gam"
-          innerRef={this.gamFile2Input}
-          onChange={this.onGamFile2Change}
-        />
-      </React.Fragment>
-    );
-  }
-}
-
-FileUploadFormRow.propTypes = {
-  apiUrl: PropTypes.string.isRequired,
-  getPathNames: PropTypes.func.isRequired,
-  handleFileUpload: PropTypes.func.isRequired,
-  handleInputChange: PropTypes.func.isRequired,
-  setUploadInProgress: PropTypes.func.isRequired,
-  showFileSizeAlert: PropTypes.func.isRequired,
-};
-
-export default FileUploadFormRow;
diff --git a/src/components/HeaderForm.js b/src/components/HeaderForm.js
index aad55b7..7d2b805 100644
--- a/src/components/HeaderForm.js
+++ b/src/components/HeaderForm.js
@@ -6,7 +6,6 @@ import { fetchAndParse } from "../fetchAndParse";
 // import defaultConfig from '../config.default.json';
 import config from "../config.json";
 import DataPositionFormRow from "./DataPositionFormRow";
-import FileUploadFormRow from "./FileUploadFormRow";
 import ExampleSelectButtons from "./ExampleSelectButtons";
 import RegionInput from "./RegionInput";
 import TrackPicker from "./TrackPicker";
@@ -20,8 +19,7 @@ const DATA_SOURCES = config.DATA_SOURCES;
 const MAX_UPLOAD_SIZE_DESCRIPTION = "5 MB";
 const dataTypes = {
   BUILT_IN: "built-in",
-  FILE_UPLOAD: "file-upload",
-  MOUNTED_FILES: "mounted files",
+  CUSTOM_FILES: "mounted files",
   EXAMPLES: "examples",
 };
 const fileTypes = {
@@ -307,7 +305,7 @@ class HeaderForm extends Component {
       } else {
         json.bedFiles.unshift("none");
 
-        if (this.state.dataType === dataTypes.MOUNTED_FILES) {
+        if (this.state.dataType === dataTypes.CUSTOM_FILES) {
           this.setState((state) => {
             const bedSelect = json.bedFiles.includes(state.bedSelect)
               ? state.bedSelect
@@ -415,21 +413,14 @@ class HeaderForm extends Component {
   handleDataSourceChange = (event) => {
     const value = event.target.value;
 
-    if (value === dataTypes.FILE_UPLOAD) {
-      const newState = {
-        ...CLEAR_STATE,
-        dataType: dataTypes.FILE_UPLOAD,
-        error: this.state.error,
-      };
-      this.setState(newState, () => {});
-    } else if (value === dataTypes.MOUNTED_FILES) {
+    if (value === dataTypes.CUSTOM_FILES) {
       this.setState((state) => {
         return {
           ...CLEAR_STATE,
           bedFile: "none",
           // not sure why we would like to keep the previous selection when changing data sources. What I know is it creates a bug for the regions, where the tubemap tries to read the previous bedFile (e.g. defaulted to example 1), can't find it and raises an error
           // bedFile: state.bedSelect,
-          dataType: dataTypes.MOUNTED_FILES,
+          dataType: dataTypes.CUSTOM_FILES,
         };
       });
     } else if (value === dataTypes.EXAMPLES) {
@@ -601,21 +592,6 @@ class HeaderForm extends Component {
     this.budgeRegion(-0.5);
   };
 
-  handleFileUpload = (fileType, fileName) => {
-    /// Apply a file that was uploaded as a track.
-    if (fileType === "graphFile") {
-      this.setTrackFile(fileTypes.GRAPH, 0, fileName);
-    } else if (fileType === "gbwtFile") {
-      this.setTrackFile(fileTypes.HAPLOTYPE, 0, fileName);
-    } else if (fileType === "gamFile") {
-      this.setTrackFile(fileTypes.READ, 0, fileName);
-    } else if (fileType === "gamFile2") {
-      this.setTrackFile(fileTypes.READ, 1, fileName);
-    } else {
-      throw new Error("Unknown file type " + fileType + " uploaded: " + fileName);
-    }
-  };
-
   showFileSizeAlert = () => {
     this.setState({ fileSizeAlert: true });
   };
@@ -624,6 +600,39 @@ class HeaderForm extends Component {
     this.setState({ uploadInProgress: val });
   };
 
+  // Sends uploaded file to server and returns a path to the file
+  handleFileUpload = async (fileType, file) => {
+    return new Promise(function (resolve, reject) {
+      if (file.size > config.MAXUPLOADSIZE) {
+        this.showFileSizeAlert();
+        return;
+      }
+  
+      this.setUploadInProgress(true);
+  
+      const formData = new FormData();
+      formData.append("trackFile", file);
+      // Make sure server can identify a Read file
+      formData.append("fileType", fileType); 
+      const xhr = new XMLHttpRequest();
+      xhr.responseType = "json";
+      xhr.onreadystatechange = () => {
+        if (xhr.readyState === 4 && xhr.status === 200) {
+          // Every thing ok, file uploaded
+          this.setUploadInProgress(false);
+          if (fileType === "graph") {
+            this.getPathNames(xhr.response.path);
+          }
+
+          resolve(xhr.response.path);
+        }
+      };
+  
+      xhr.open("POST", `${this.props.apiUrl}/trackFileSubmission`, true);
+      xhr.send(formData);
+    }.bind(this));
+  };
+
   setUpWebsocket = () => {
     this.ws = new WebSocket(this.props.apiUrl.replace(/^http/, "ws"));
     this.ws.onmessage = (message) => {
@@ -667,16 +676,12 @@ class HeaderForm extends Component {
       <option value={dataTypes.EXAMPLES} key="syntheticExamples">
         synthetic data examples
       </option>,
-      <option value={dataTypes.FILE_UPLOAD} key="customFileUpload">
-        custom (file upload)
-      </option>,
-      <option value={dataTypes.MOUNTED_FILES} key={dataTypes.MOUNTED_FILES}>
-        custom (mounted files)
+      <option value={dataTypes.CUSTOM_FILES} key={dataTypes.CUSTOM_FILES}>
+        custom
       </option>
     );
 
-    const mountedFilesFlag = this.state.dataType === dataTypes.MOUNTED_FILES;
-    const uploadFilesFlag = this.state.dataType === dataTypes.FILE_UPLOAD;
+    const customFilesFlag = this.state.dataType === dataTypes.CUSTOM_FILES;
     const examplesFlag = this.state.dataType === dataTypes.EXAMPLES;
     const viewTargetHasChange = !viewTargetsEqual(this.getNextViewTarget(), this.props.getCurrentViewTarget());
 
@@ -729,7 +734,7 @@ class HeaderForm extends Component {
                 {dataSourceDropdownOptions}
               </select>
               &nbsp;
-              {mountedFilesFlag && (
+              {customFilesFlag && (
                 <React.Fragment>
                   <Label
                     for="bedSelectInput"
@@ -760,28 +765,18 @@ class HeaderForm extends Component {
                 />
               )}
 
-              {mountedFilesFlag && 
+              {customFilesFlag && 
                 <div style={{display: "flex"}}>
                   {DataPositionFormRowComponent}
                   <TrackPicker
                     tracks={this.state.tracks}
                     availableTracks={this.state.fileSelectOptions}
                     onChange={this.handleInputChange}
+                    handleFileUpload={this.handleFileUpload}
                   ></TrackPicker>
                 </div>
               }
 
-              {uploadFilesFlag && (
-                <FileUploadFormRow
-                  apiUrl={this.props.apiUrl}
-                  getPathNames={this.getPathNames}
-                  handleInputChange={this.handleInputChange}
-                  handleFileUpload={this.handleFileUpload}
-                  showFileSizeAlert={this.showFileSizeAlert}
-                  setUploadInProgress={this.setUploadInProgress}
-                />
-              )}
-
               <Row>
                 <Alert
                   color="danger"
@@ -802,7 +797,7 @@ class HeaderForm extends Component {
                     setColorSetting={this.props.setColorSetting}
                   />
                 ) : (
-                  !mountedFilesFlag && DataPositionFormRowComponent 
+                  !customFilesFlag && DataPositionFormRowComponent 
                 )}
               </Row>
             </Col>
diff --git a/src/components/PickerTypeDropdown.js b/src/components/PickerTypeDropdown.js
new file mode 100644
index 0000000..18051e3
--- /dev/null
+++ b/src/components/PickerTypeDropdown.js
@@ -0,0 +1,54 @@
+import PropTypes from "prop-types";
+import Select from "react-select";
+import React from "react";
+
+export const PickerTypeDropdown = ({
+    value,
+    handleInputChange,
+    pickerOptions,
+    testID,
+}) => {
+
+    function onChange(option) {
+        handleInputChange(option.value);
+    }
+
+    const dropDownOptions = pickerOptions.map((option) => ({
+        label: option,
+        value: option,
+    }));
+
+    return(
+        <div data-testid={testID}>
+            <Select 
+            options={dropDownOptions}
+            value={{label: value, value: value}}
+            // Identical-looking object literals will compare unequal, so we
+            // need to provide a way to turn them into strings so that
+            // `value` can be matched up with the corresponding item in
+            // `options`.
+            getOptionValue={(o) => {
+                return o["value"];
+            }}
+            onChange={onChange}
+            autoComplete="on"
+            />  
+        </div>
+    );
+}
+
+
+PickerTypeDropdown.propTypes = {
+    handleInputChange: PropTypes.func.isRequired,
+    pickerOptions: PropTypes.array.isRequired,
+    value: PropTypes.string.isRequired,
+    testID: PropTypes.string,
+}
+  
+PickerTypeDropdown.defaultProps = {
+    value: "mounted",
+    pickerOptions: ["upload, mounted"],
+    testID: "picker-type-component"
+}
+
+export default PickerTypeDropdown;
\ No newline at end of file
diff --git a/src/components/TrackFilePicker.demo.js b/src/components/TrackFilePicker.demo.js
index a13cfdc..aa0b496 100644
--- a/src/components/TrackFilePicker.demo.js
+++ b/src/components/TrackFilePicker.demo.js
@@ -15,7 +15,7 @@ export default (<Demo
 
     value: P.string("fileA1.vg"),
     fileType: P.choices(["graph", "haplotype", "read"]),
-    pickerType: P.choices(["dropdown", "upload"]),
+    pickerType: P.choices(["mounted", "upload"]),
   }}
 >
   {
@@ -23,7 +23,11 @@ export default (<Demo
       return <TrackFilePicker {...props} handleInputChange={(file) => {
         // takes file name as value, updates the fileSelect prop in TrackFilePicker
         update({value: file});
-      }}/>
+      }}
+      handleFileUpload={(fileType, file) => {
+        console.log("handling file upload");
+      }}
+    />
     }
   }
 </Demo>)
diff --git a/src/components/TrackFilePicker.js b/src/components/TrackFilePicker.js
index c1ae681..a0015e4 100644
--- a/src/components/TrackFilePicker.js
+++ b/src/components/TrackFilePicker.js
@@ -1,6 +1,8 @@
 import PropTypes from "prop-types";
 import Select from "react-select";
 import React from "react";
+import config from "./../config.json";
+import { Input } from "reactstrap";
 
 
 
@@ -22,9 +24,20 @@ export const TrackFilePicker = ({
   handleInputChange,
   pickerType, // either "dropdown or upload" to determine which component we render
   className,
-  testID
+  testID,
+  handleFileUpload,
 }) => {
 
+    let uploadFileInput = React.createRef();
+    let acceptedExtensions = config.fileTypeToExtensions[fileType];
+
+    async function uploadOnChange() {
+      const file = uploadFileInput.current.files[0];
+
+      const completePath = await handleFileUpload(fileType, file);
+      handleInputChange(completePath);
+    }
+
 
     function mountedOnChange(option) {
       // update parent state
@@ -52,7 +65,7 @@ export const TrackFilePicker = ({
     }));
     
 
-    if (pickerType === "dropdown"){
+    if (pickerType === "mounted"){
       return(
       // wrap Select container in div to easily query in tests
         <div data-testid={testID}>
@@ -73,10 +86,16 @@ export const TrackFilePicker = ({
           </div>
       );
     } else if (pickerType === "upload") {
-      // TODO: render <input> component and create upload onChange function 
       return (
-        <div data-testid="file-select-component">
-          <input/>  
+        <div style={{display: 'flex',  justifyContent:'center', alignItems:'center', marginTop: 5}}>
+          <Input
+          data-testid={testID}
+          type="file"
+          className="customDataUpload form-control-file"
+          accept={acceptedExtensions}
+          innerRef={uploadFileInput}
+          onChange={uploadOnChange}
+        />
         </div>
 
       );
@@ -92,13 +111,14 @@ TrackFilePicker.propTypes = {
     handleInputChange: PropTypes.func.isRequired,
     pickerType: PropTypes.string,
     className: PropTypes.string,
-    testID: PropTypes.string
+    testID: PropTypes.string,
+    handleFileUpload: PropTypes.func.isRequired,
 }
   
 TrackFilePicker.defaultProps = {
   value: "Select a file",
   fileType: "graph",
-  pickerType: "dropdown",
+  pickerType: "mounted",
   className: undefined,
   testID: "file-select-component"
 }
diff --git a/src/components/TrackList.js b/src/components/TrackList.js
index 4e89c28..6e71f37 100644
--- a/src/components/TrackList.js
+++ b/src/components/TrackList.js
@@ -14,6 +14,7 @@ export const TrackList = ({
   availableColors,
   onChange, // expects a new tracks object
   onDelete,
+  handleFileUpload,
 }) => {
 
   function trackItemOnChange(trackID, trackProps) {
@@ -39,7 +40,8 @@ export const TrackList = ({
               onChange={trackItemOnChange}
               onDelete={onDelete}
               trackID={parseInt(trackID)}
-              key={trackID} />
+              key={trackID}
+              handleFileUpload={handleFileUpload}/>
           );
       })
 
@@ -58,6 +60,7 @@ TrackList.propTypes = {
   availableColors: PropTypes.array,
   onChange: PropTypes.func.isRequired,
   onDelete: PropTypes.func.isRequired,
+  handleFileUpload: PropTypes.func.isRequired,
 }
   
 
diff --git a/src/components/TrackListItem.js b/src/components/TrackListItem.js
index ff03951..bf91f37 100644
--- a/src/components/TrackListItem.js
+++ b/src/components/TrackListItem.js
@@ -9,6 +9,7 @@ import {TrackTypeDropdown} from './TrackTypeDropdown';
 import {TrackDeleteButton} from './TrackDeleteButton';
 import {TrackSettingsButton} from './TrackSettingsButton';
 import React, { useEffect, useState} from 'react';
+import config from "./../config.json";
 
 
 export const TrackListItem = ({
@@ -24,11 +25,14 @@ export const TrackListItem = ({
     onChange, // expects a new trackProps object
     onDelete,
     trackID,
+    handleFileUpload,
   }) => {
     // propChanges only store new trackType, trackFile, and trackColorSettings changes
     // reset after onChange is called
     const [propChanges, setPropChanges] = useState({});
 
+    const [pickerType, setPickerType] = useState("mounted");
+
   
     const trackTypeOnChange = async(newTrackType) => {
       // wait until file change to call onChange
@@ -76,24 +80,35 @@ export const TrackListItem = ({
     const displayedFile = "trackFile" in propChanges ? propChanges["trackFile"] : trackProps["trackFile"];
     console.log(displayedFile);
     return (
-      <Container key={trackID} style={{ width: "800px", marginLeft: 0 }}>
+      <Container key={trackID} style={{ width: "900px", marginLeft: 0, marginRight: 15 }}>
         <Row className="g-0">
-          <Col className="tracklist-dropdown" lg="5">
+          <Col className="tracklist-dropdown">
             <TrackTypeDropdown value={propChanges["trackType"] || trackProps["trackType"]} 
                               onChange={trackTypeOnChange}
                               testID={"file-type-select-component".concat(trackID)}
+                              options={["graph", "haplotype", "read"]}
                               />
           </Col>
-          <Col className="tracklist-dropdown" lg="5">
+          <Col className="tracklist-dropdown">
+            <TrackTypeDropdown
+              value={pickerType}
+              onChange={setPickerType}
+              testID={"picker-type-select-component".concat(trackID)}
+              options={config.pickerTypeOptions}
+            />
+          </Col>
+
+          <Col className="tracklist-dropdown">
             <TrackFilePicker tracks={availableTracks} 
                             fileType={propChanges["trackType"] || trackProps["trackType"]} 
                             value={displayedFile}
-                            pickerType={"dropdown"} 
+                            pickerType={pickerType} 
                             handleInputChange={trackFileOnChange}
                             testID={"file-select-component".concat(trackID)}
+                            handleFileUpload={handleFileUpload}
                             />
           </Col>
-          <Col className="tracklist-button" lg="1">
+          <Col className="tracklist-button" md="1">
             <TrackSettingsButton fileType={propChanges["trackType"] || trackProps["trackType"]}
                                 trackColorSettings={propChanges["trackColorSettings"] || trackProps["trackColorSettings"]}
                                 setTrackColorSetting={trackSettingsOnChange}
@@ -118,6 +133,7 @@ export const TrackListItem = ({
     onChange: PropTypes.func.isRequired,
     onDelete: PropTypes.func.isRequired,
     trackID: PropTypes.number.isRequired,
+    handleFileUpload: PropTypes.func.isRequired,
   }
     
   
diff --git a/src/components/TrackPicker.js b/src/components/TrackPicker.js
index 8c1afa1..864fc1d 100644
--- a/src/components/TrackPicker.js
+++ b/src/components/TrackPicker.js
@@ -12,6 +12,7 @@ export const TrackPicker = ({
     availableTracks,
     availableColors,
     onChange,
+    handleFileUpload,
 }) => {
     // based off of https://react-popup.elazizi.com/controlled-popup/#using-open-prop
     const [open, setOpen] = useState(false);
@@ -30,6 +31,7 @@ export const TrackPicker = ({
               availableTracks={availableTracks}
               availableColors={availableColors}
               onChange={onChange}
+              handleFileUpload={handleFileUpload}
           />
         </PopupDialog>
       </div>
@@ -40,7 +42,8 @@ TrackPicker.propTypes = {
     tracks: PropTypes.object.isRequired,
     availableTracks: PropTypes.array.isRequired,
     availableColors: PropTypes.array,
-    onChange: PropTypes.func
+    onChange: PropTypes.func,
+    handleFileUpload: PropTypes.func,
 }
 
 TrackPicker.defaultProps = {
diff --git a/src/components/TrackPickerDisplay.js b/src/components/TrackPickerDisplay.js
index 86ff970..c5a8302 100644
--- a/src/components/TrackPickerDisplay.js
+++ b/src/components/TrackPickerDisplay.js
@@ -17,6 +17,7 @@ export const TrackPickerDisplay = ({
     availableTracks, // array of tracks(see types.ts)
     availableColors, // array of ColorPalettes
     onChange, // expects a new tracks object
+    handleFileUpload,
 }) => {
 
     const [trackListChanges, setTrackListChanges] = useState({});
@@ -98,6 +99,7 @@ export const TrackPickerDisplay = ({
                     availableColors={availableColors}
                     onChange={trackListOnChange}
                     onDelete={onDelete}
+                    handleFileUpload={handleFileUpload}
                 />
             </Row>
             <Row>
@@ -114,7 +116,8 @@ TrackPickerDisplay.propTypes = {
     tracks: PropTypes.object.isRequired,
     availableTracks: PropTypes.array.isRequired,
     availableColors: PropTypes.array,
-    onChange: PropTypes.func
+    onChange: PropTypes.func,
+    handleFileUpload: PropTypes.func.isRequired,
 }
 
 TrackPickerDisplay.defaultProps = {
diff --git a/src/components/TrackTypeDropdown.js b/src/components/TrackTypeDropdown.js
index 0e5292b..26bac36 100644
--- a/src/components/TrackTypeDropdown.js
+++ b/src/components/TrackTypeDropdown.js
@@ -11,6 +11,8 @@ import Select from "react-select";
  * 
  */
 
+
+// TODO: rename TrackTypeDropdown to be generalized dropdown component
 export function TrackTypeDropdown (props) {
   
     
@@ -21,7 +23,7 @@ export function TrackTypeDropdown (props) {
         <Select {...props} onChange={o => {
           props.onChange(o.value)}
         }
-          options={["graph", "haplotype", "read"].map(o => ({
+          options={props.options.map(o => ({
             label: o, value: o
           }))}
           value={{label: props.value, value: props.value}}
@@ -44,14 +46,16 @@ TrackTypeDropdown.propTypes = {
   className: PropTypes.string,
   value: PropTypes.string,
   onChange: PropTypes.func.isRequired,
-  testID: PropTypes.string
+  testID: PropTypes.string,
+  options: PropTypes.array,
 };
 
 TrackTypeDropdown.defaultProps = {
   id: undefined,
   className: undefined,
   value: "graph",
-  testID: "file-type-select-component"
+  testID: "file-type-select-component",
+  options: ["graph", "haplotype", "read"]
 };
 
 export default TrackTypeDropdown;
diff --git a/src/config.json b/src/config.json
index dcbd59b..b50ed42 100644
--- a/src/config.json
+++ b/src/config.json
@@ -73,6 +73,15 @@
       "auxPalette": "reds",
       "colorReadsByMappingQuality": false
     }
+  },
+
+  "fileTypeToExtensions": {
+    "graph": ".xg,.vg,.hg,.gbz,.pg",
+    "haplotype": ".gbwt,.gbz",
+    "read": ".gam"
+  },
+
+  "MAXUPLOADSIZE": 5242880,
+  "pickerTypeOptions": ["mounted", "upload"]
 
-  }
 }
diff --git a/src/server.mjs b/src/server.mjs
index cc4e091..9a8b934 100644
--- a/src/server.mjs
+++ b/src/server.mjs
@@ -168,23 +168,16 @@ api.use((req, res, next) => {
   next();
 });
 
-api.post("/graphFileSubmission", upload.single("graphFile"), (req, res) => {
-  console.log("/graphFileSubmission");
+// Store files uploaded from trackFilePicker via multer
+api.post("/trackFileSubmission", upload.single("trackFile"), (req, res) => {
+  console.log("/trackFileSubmission");
   console.log(req.file);
-  res.json({ path: path.relative(UPLOAD_DATA_PATH, req.file.path) });
-});
-
-api.post("/gbwtFileSubmission", upload.single("gbwtFile"), (req, res) => {
-  console.log("/gbwtFileSubmission");
-  console.log(req.file);
-  res.json({ path: path.relative(UPLOAD_DATA_PATH, req.file.path) });
-});
-
-api.post("/gamFileSubmission", upload.single("gamFile"), (req, res) => {
-  console.log("/gamFileSubmission");
-  console.log(req.file);
-  indexGamSorted(req, res);
-});
+  if (req.body.fileType === fileTypes["READ"]) {
+    indexGamSorted(req, res);
+  } else {
+    res.json({ path: path.relative(".", req.file.path) });
+  }
+})
 
 function indexGamSorted(req, res) {
   const prefix = req.file.path.substring(0, req.file.path.lastIndexOf("."));
@@ -208,7 +201,7 @@ function indexGamSorted(req, res) {
 
   vgIndexChild.on("close", () => {
     sortedGamFile.end();
-    res.json({ path: path.relative(UPLOAD_DATA_PATH, prefix + ".sorted.gam") });
+    res.json({ path: path.relative(".", prefix + ".sorted.gam") });
   });
 }
 


Test patch:
diff --git a/src/components/TrackFilePicker.test.js b/src/components/TrackFilePicker.test.js
index fc50686..104f262 100644
--- a/src/components/TrackFilePicker.test.js
+++ b/src/components/TrackFilePicker.test.js
@@ -1,5 +1,6 @@
 import React from 'react';
 import { render, fireEvent, waitFor }  from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
 import {TrackFilePicker} from './TrackFilePicker';
 
 
@@ -17,7 +18,7 @@ describe('TrackFilePicker', () => {
             <TrackFilePicker 
             tracks={testTracks}
             fileType={"graph"}
-            pickerType={"dropdown"}
+            pickerType={"mounted"}
             handleInputChange={fakeOnChange}/>
         );
 
@@ -32,7 +33,7 @@ describe('TrackFilePicker', () => {
             <TrackFilePicker 
             tracks={testTracks}
             fileType={"graph"}
-            pickerType={"dropdown"}
+            pickerType={"mounted"}
             value={"fileA1.vg"}
             handleInputChange={fakeOnChange}/>
         );
@@ -46,7 +47,7 @@ describe('TrackFilePicker', () => {
             <TrackFilePicker 
             tracks={testTracks}
             fileType={"graph"}
-            pickerType={"dropdown"}
+            pickerType={"mounted"}
             value={"fileC1.xg"}
             handleInputChange={fakeOnChange}/>
         );
@@ -63,7 +64,7 @@ describe('TrackFilePicker', () => {
             <TrackFilePicker 
             tracks={testTracks}
             fileType={"haplotype"}
-            pickerType={"dropdown"}
+            pickerType={"mounted"}
             handleInputChange={fakeOnChange}/>
         );
 
@@ -97,7 +98,7 @@ describe('TrackFilePicker', () => {
             <TrackFilePicker 
             tracks={testTracks}
             fileType={"graph"}
-            pickerType={"dropdown"}
+            pickerType={"mounted"}
             handleInputChange={fakeOnChange}/>
         );
 
@@ -115,5 +116,33 @@ describe('TrackFilePicker', () => {
         expect(fakeOnChange).toHaveBeenCalledTimes(1);
         expect(fakeOnChange).toHaveBeenCalledWith("fileC1.xg");
     })
+
+    it('should call call handleFileUpload when a file is inputted', async() => {
+        const fakeOnChange = jest.fn();
+        const fakeHandleFileUpload = jest.fn();
+
+        const { queryByTestId } = render(
+            <TrackFilePicker 
+            tracks={testTracks}
+            fileType={"graph"}
+            pickerType={"upload"}
+            handleInputChange={fakeOnChange}
+            handleFileUpload={fakeHandleFileUpload}/>
+        );
+
+        const fakeFile = new File(["example_data"], "example.vg", { type: "text/vg" });
+
+
+        const fileSelectComponent = queryByTestId("file-select-component");
+
+        await waitFor(() => {
+            userEvent.upload(fileSelectComponent, fakeFile);
+        });  
+
+        expect(fakeHandleFileUpload).toHaveBeenCalledTimes(1);
+        expect(fileSelectComponent.files.length).toBe(1);
+        expect(fileSelectComponent.files[0]).toStrictEqual(fakeFile);
+
+    })
     
 });
diff --git a/src/end-to-end.test.js b/src/end-to-end.test.js
index 74c77c6..d200108 100644
--- a/src/end-to-end.test.js
+++ b/src/end-to-end.test.js
@@ -4,6 +4,7 @@
 // not-really-standard implementations of JS modules in play. So we import the
 // server's start function and put it in an object pretendign to be a module.
 import { start } from "./server.mjs";
+import fs from "fs-extra";
 const server = { start };
 
 import React from "react";
@@ -345,18 +346,18 @@ it("produces correct link for view before & after go is pressed", async () => {
     "http://localhost?tracks[0][trackFile]=exampleData%2Fcactus.vg.xg&tracks[0][trackType]=graph&tracks[1][trackFile]=exampleData%2Fcactus-NA12879.sorted.gam&tracks[1][trackType]=read&bedFile=exampleData%2Fcactus.bed&name=cactus&region=ref%3A1-100&dataType=built-in";
   // Make sure link has changed after pressing go
   expect(fakeClipboard).toEqual(expectedLinkCactus);
-});
+}, 20000);
 
 it("can retrieve the list of mounted graph files", async () => {
   // Wait for everything to settle so we don't stop the server while it is thinking
   await waitForLoadEnd();
 
-  // Swap over to the mounted files mode
+  // Swap over to the custom files mode
   await act(async () => {
     let dropdown = document.getElementById("dataSourceSelect");
     await userEvent.selectOptions(
       screen.getByLabelText(/Data/i),
-      "custom (mounted files)"
+      "custom"
     );
   });
 
@@ -390,3 +391,87 @@ it("can retrieve the list of mounted graph files", async () => {
 
   expect(screen.queryByText("cactus.vg.xg")).toBeTruthy();
 });
+
+// uploads a cactus.vg file and renders the svg
+it("can accept uploaded files", async () => {
+  await waitForLoadEnd();
+
+  // Swap over to the custom files mode
+  await act(async () => {
+    let dropdown = document.getElementById("dataSourceSelect");
+    await userEvent.selectOptions(
+      screen.getByLabelText(/Data/i),
+      "custom"
+    );
+  });
+
+  // Find the select box's input
+  let trackSelectButton = screen.queryByTestId("TrackPickerButton");
+  expect(trackSelectButton).toBeTruthy();
+
+  // open track selection
+  await act(async () => {
+    userEvent.click(trackSelectButton);
+  });
+
+
+  // add a new track
+  await waitFor(() => {
+    fireEvent.click(screen.queryByTestId("track-add-button-component"));
+  });
+
+  await waitFor(() => {
+    // wait for picker type dropdown
+    fireEvent.keyDown(screen.queryByTestId('picker-type-select-component1').firstChild, {key: "ArrowDown"});
+  });
+
+  // select the upload option
+  await waitFor(() => screen.getByText("upload"));
+  fireEvent.click(screen.getByText("upload"));
+
+  await waitFor(() => screen.queryByTestId("file-select-component1"));
+
+  const fileUploader = screen.queryByTestId("file-select-component1");
+
+  const file = await fs.readFileSync("exampleData/cactus.vg");
+
+  await waitFor(() => {
+    userEvent.upload(fileUploader, file);
+  });
+  
+  // make sure the file is in the upload component
+  expect(fileUploader.files.length).toBe(1);
+  expect(fileUploader.files[0]).toStrictEqual(file);
+
+  // exit the track picker
+  fireEvent.click(screen.queryByTestId("TrackPickerCloseButton"));
+
+  // try to compute the svg
+  const autocomplete = screen.getByTestId("autocomplete");
+  const input = autocomplete.querySelector("input");
+
+  await userEvent.clear(input);
+
+  // Input region
+  // using fireEvent because userEvent has no change
+  fireEvent.focus(input);
+  fireEvent.change(input, { target: { value: "node:1+10" } });
+  expect(input.value).toBe("node:1+10");
+  fireEvent.keyDown(autocomplete, { key: "Enter" });
+
+  // Wait for rendered response
+  await waitFor(() => screen.getByTestId("autocomplete"));
+
+  // Click go
+  let go = document.getElementById("goButton");
+  await userEvent.click(go);
+
+  await waitForLoadEnd();
+
+  // See if correct svg rendered
+  let svg = document.getElementById("svg");
+  expect(svg).toBeTruthy();
+  expect(svg.getElementsByTagName("title").length).toEqual(1054);
+
+// increase timeout to allow fetching of url
+});
\ No newline at end of file