Repository: vgteam/sequenceTubeMap
Pull request #232 (2023-03-07 23:25:56)

Track file picker
File dropdown selection component alongside tests and demo

- would like to get redux to work 
- need to change file submission endpoints to get file upload working


Patch:
diff --git a/src/components/TrackFilePicker.demo.js b/src/components/TrackFilePicker.demo.js
new file mode 100644
index 0000000..d8e13ca
--- /dev/null
+++ b/src/components/TrackFilePicker.demo.js
@@ -0,0 +1,31 @@
+import Demo, {props as P} from 'react-demo'
+// See https://github.com/rpominov/react-demo for how to make a demo
+
+import React from "react";
+
+import TrackFilePicker from "./TrackFilePicker";
+
+export default (<Demo
+  props={{
+    tracks: P.json([{"files": [{"name": "fileA1.vg", "type": "graph"},
+                              {"name": "fileA2.gbwt", "type": "haplotype"}]},
+                    {"files": [{"name": "fileB1.gbwt", "type": "haplotype"},
+                              {"name": "fileB2.gam", "type": "read"}]},
+                    {"files": [{"name": "fileC1.xg", "type": "graph"}]}]),
+
+    value: P.json({"name": "fileA1.vg", "type": "graph"}),
+    fileType: P.choices(["graph", "haplotype", "read"]),
+    pickerType: P.choices(["dropdown", "upload"]),
+  }}
+>
+  {
+    (props, update) => {
+      return <TrackFilePicker {...props} handleInputChange={(file) => {
+        // takes file name as value, updates the fileSelect prop in TrackFilePicker
+        update({value: file});
+      }}/>
+    }
+  }
+</Demo>)
+
+
diff --git a/src/components/TrackFilePicker.js b/src/components/TrackFilePicker.js
new file mode 100644
index 0000000..8346cdb
--- /dev/null
+++ b/src/components/TrackFilePicker.js
@@ -0,0 +1,90 @@
+import PropTypes from "prop-types";
+import Select from "react-select";
+
+
+
+//handleInputChange takes in a string trackType
+export const TrackFilePicker = ({
+  tracks, // array of available tracks
+  fileType, // e.g read, gam, graph
+  value, // input file
+  handleInputChange,
+  pickerType, // either "dropdown or upload" to determine which component we render
+  className 
+}) => {
+
+
+    function onChange(option) {
+      // update parent state
+      value = option.value;
+      handleInputChange(option.value);
+    }
+
+
+    const fileOptions = []
+    // find all file options matching the specified file type
+    for (const track of tracks) {
+        for (const file of track["files"]) {
+          if (file["type"] === fileType) {
+            fileOptions.push(file);
+          }
+        }
+    }
+
+    // takes in an array of options and maps them into a format <Select> takes
+    const dropDownOptions = fileOptions.map((option) => ({
+      label: option["name"],
+      value: option,
+    }));
+    
+
+    if (pickerType === "dropdown"){
+      return(
+      // wrap Select container in div to easily query in tests
+        <div data-testid="file-select-component">
+            <Select 
+              options={dropDownOptions}
+              value={{label: value["name"], 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["label"];
+              }}
+              onChange={onChange}
+              autoComplete="on"
+              className={className}
+            />  
+          </div>
+      );
+    } else if (pickerType === "upload") {
+      // TODO: render <input> component and create upload onChange function 
+      return (
+        <div data-testid="file-select-component">
+          <input/>  
+        </div>
+
+      );
+    } else {
+      throw new Error("Invalid picker type");
+    }
+}
+
+TrackFilePicker.propTypes = {
+    tracks: PropTypes.array.isRequired,
+    fileType: PropTypes.string.isRequired,
+    value: PropTypes.object,
+    handleInputChange: PropTypes.func.isRequired,
+    pickerType: PropTypes.string,
+    className: PropTypes.string,
+}
+  
+TrackFilePicker.defaultProps = {
+  value: {"name": "Select a file", "type": undefined},
+  pickerType: "dropdown",
+  className: undefined
+}
+
+export default TrackFilePicker;
+


Test patch:
diff --git a/src/components/TrackFilePicker.test.js b/src/components/TrackFilePicker.test.js
new file mode 100755
index 0000000..5e80653
--- /dev/null
+++ b/src/components/TrackFilePicker.test.js
@@ -0,0 +1,119 @@
+import React from 'react';
+import { render, fireEvent, waitFor, cleanup }  from '@testing-library/react';
+import {TrackFilePicker} from './TrackFilePicker';
+
+
+
+describe('TrackFilePicker', () => {
+    const testTracks = [{"files": [{"name": "fileA1.vg", "type": "graph"},
+                                   {"name": "fileA2.gbwt", "type": "haplotype"}]},
+                        {"files": [{"name": "fileB1.gbwt", "type": "haplotype"},
+                                   {"name": "fileB2.gam", "type": "read"}]},
+                        {"files": [{"name": "fileC1.xg", "type": "graph"}]}];
+
+    it('should render without errors', async () => {
+        const fakeOnChange = jest.fn();
+        const { getByText } = render(
+            <TrackFilePicker 
+            tracks={testTracks}
+            fileType={"graph"}
+            pickerType={"dropdown"}
+            handleInputChange={fakeOnChange}/>
+        );
+
+        // maybe need to search by "Select a file"
+        const placeholder = await getByText("Select a file");
+        expect(placeholder).toBeTruthy();
+    });
+    
+    it('should allow value to be controlled', async () => {
+        const fakeOnChange = jest.fn();
+        const { getByText, queryByText, rerender } = render(
+            <TrackFilePicker 
+            tracks={testTracks}
+            fileType={"graph"}
+            pickerType={"dropdown"}
+            value={{"name": "fileA1.vg", "type": "graph"}}
+            handleInputChange={fakeOnChange}/>
+        );
+
+        let displayed = getByText("fileA1.vg");
+        expect(displayed).toBeTruthy();
+        let notDisplayed = queryByText("fileC1.xg");
+        expect(notDisplayed).toBeFalsy();
+        
+        rerender(
+            <TrackFilePicker 
+            tracks={testTracks}
+            fileType={"graph"}
+            pickerType={"dropdown"}
+            value={{"name": "fileC1.xg", "type": "graph"}}
+            handleInputChange={fakeOnChange}/>
+        );
+        
+        displayed = getByText("fileC1.xg");
+        expect(displayed).toBeTruthy();
+        notDisplayed = queryByText("fileA1.vg");
+        expect(notDisplayed).toBeFalsy();
+    });
+
+    it('should call onChange when an option is selected', async () => {
+        const fakeOnChange = jest.fn();
+        const { getByText, queryByTestId } = render(
+            <TrackFilePicker 
+            tracks={testTracks}
+            fileType={"haplotype"}
+            pickerType={"dropdown"}
+            handleInputChange={fakeOnChange}/>
+        );
+
+        // find div containing the component
+        const fileSelectComponent = queryByTestId('file-select-component');
+
+        expect(fileSelectComponent).toBeDefined();
+        expect(fileSelectComponent).not.toBeNull();
+
+        // expand the select box
+        fireEvent.keyDown(fileSelectComponent.firstChild, {key: "ArrowDown"});
+        await waitFor(() => getByText("fileB1.gbwt"));
+        fireEvent.click(getByText("fileB1.gbwt"));
+
+        expect(fakeOnChange).toHaveBeenCalledTimes(1);
+        expect(fakeOnChange).toHaveBeenCalledWith({"name": "fileB1.gbwt", "type": "haplotype"});
+
+        // make sure we can repeat the process
+        fireEvent.keyDown(fileSelectComponent.firstChild, {key: "ArrowDown"});
+        await waitFor(() => getByText("fileA2.gbwt"));
+        fireEvent.click(getByText("fileA2.gbwt"));
+
+        expect(fakeOnChange).toHaveBeenCalledTimes(2);
+        expect(fakeOnChange).toHaveBeenCalledWith({"name": "fileA2.gbwt", "type": "haplotype"});
+
+    });
+
+    it('should call onChange when queried by input value' ,async() => {
+        const fakeOnChange = jest.fn();
+        const { getByText, queryByTestId, container  } = render(
+            <TrackFilePicker 
+            tracks={testTracks}
+            fileType={"graph"}
+            pickerType={"dropdown"}
+            handleInputChange={fakeOnChange}/>
+        );
+
+        const fileSelectComponent = queryByTestId('file-select-component');
+        
+        fireEvent.change(container.querySelector('input'), {
+            target: { value: 'fileC1.xg' },
+        });
+        
+        fireEvent.keyDown(fileSelectComponent.firstChild, { key: 'ArrowDown' });  
+
+        await waitFor(() => getByText("fileC1.xg"));
+        fireEvent.click(getByText("fileC1.xg"));
+
+        expect(fakeOnChange).toHaveBeenCalledTimes(1);
+        expect(fakeOnChange).toHaveBeenCalledWith({"name": "fileC1.xg", "type": "graph"});
+    })
+    
+});