diff --git a/src/__test__/components/plots/styling/MarkerGeneSelection.test.jsx b/src/__test__/components/plots/styling/MarkerGeneSelection.test.jsx
index 8e993b19fa..d8804afa70 100644
--- a/src/__test__/components/plots/styling/MarkerGeneSelection.test.jsx
+++ b/src/__test__/components/plots/styling/MarkerGeneSelection.test.jsx
@@ -12,16 +12,17 @@ import { plotTypes } from 'utils/constants';
const mockOnUpdate = jest.fn();
const mockOnReset = jest.fn();
-const mockOnGeneEnter = jest.fn();
+
+const plotType = plotTypes.DOT_PLOT;
const defaultProps = {
onUpdate: mockOnUpdate,
onReset: mockOnReset,
- onGeneEnter: mockOnGeneEnter,
+ plotUuid: 'dotPlotMain',
+ experimentId: 'experimentId',
+ searchBarUuid: 'searchBarUuid',
};
-const plotType = plotTypes.DOT_PLOT;
-
const markerGeneSelectionFactory = createTestComponentFactory(MarkerGeneSelection, defaultProps);
describe('MarkerGeneSelection', () => {
@@ -29,32 +30,6 @@ describe('MarkerGeneSelection', () => {
jest.clearAllMocks();
});
- it('Should show the custom gene input by default', async () => {
- const mockConfig = initialPlotConfigStates[plotType];
-
- await act(async () => {
- render(
- markerGeneSelectionFactory({ config: mockConfig }),
- );
- });
-
- // Expect screen to show the custom gene selection input by default
- expect(screen.getByText(/Type in a gene name/i)).toBeInTheDocument();
-
- // Typing genes and then pressing enter
- const geneInput = screen.getByRole('combobox');
-
- // This is not wrapped in act() because changes to the value causes a re-render
- // which causes mockOnGeneEnter to lose its memory of having been called
- userEvent.type(geneInput, 'ABC{enter}');
-
- // Expect geneEnter to be called
- expect(mockOnGeneEnter).toHaveBeenCalledTimes(1);
- const inputValue = mockOnGeneEnter.mock.calls[0][0];
-
- expect(inputValue).toEqual(['ABC']);
- });
-
it('Should show the number of marker genes input', async () => {
const mockConfig = { ...initialPlotConfigStates[plotType], useMarkerGenes: true };
diff --git a/src/__test__/pages/experiments/[experimentId]/plots-and-tables/dot-plot/__snapshots__/index.test.jsx.snap b/src/__test__/pages/experiments/[experimentId]/plots-and-tables/dot-plot/__snapshots__/index.test.jsx.snap
index 0ebd603d6c..9aec9580b6 100644
--- a/src/__test__/pages/experiments/[experimentId]/plots-and-tables/dot-plot/__snapshots__/index.test.jsx.snap
+++ b/src/__test__/pages/experiments/[experimentId]/plots-and-tables/dot-plot/__snapshots__/index.test.jsx.snap
@@ -37,13 +37,6 @@ Array [
},
Object {},
],
- Array [
- Object {
- "data": Array [],
- "filename": "Test_Experiment-dot_plot-louvain-All",
- },
- Object {},
- ],
Array [
Object {
"data": Array [
diff --git a/src/__test__/pages/experiments/[experimentId]/plots-and-tables/dot-plot/index.test.jsx b/src/__test__/pages/experiments/[experimentId]/plots-and-tables/dot-plot/index.test.jsx
index 7cdf17b36b..e910997918 100644
--- a/src/__test__/pages/experiments/[experimentId]/plots-and-tables/dot-plot/index.test.jsx
+++ b/src/__test__/pages/experiments/[experimentId]/plots-and-tables/dot-plot/index.test.jsx
@@ -3,6 +3,8 @@ import _ from 'lodash';
import { act } from 'react-dom/test-utils';
import { render, screen, fireEvent } from '@testing-library/react';
+import { mount } from 'enzyme';
+import { within } from '@testing-library/dom';
import '@testing-library/jest-dom';
import { Provider } from 'react-redux';
@@ -33,6 +35,9 @@ import userEvent from '@testing-library/user-event';
import { plotNames } from 'utils/constants';
import ExportAsCSV from 'components/plots/ExportAsCSV';
+import waitForComponentToPaint from '__test__/test-utils/waitForComponentToPaint';
+import { arrayMoveImmutable } from 'utils/array-move';
+
jest.mock('components/plots/ExportAsCSV', () => jest.fn(() => (<>>)));
jest.mock('components/header/UserButton', () => () => <>>);
jest.mock('react-resize-detector', () => (props) => {
@@ -86,6 +91,23 @@ const mockAPIResponse = _.merge(
const defaultProps = { experimentId };
const dotPlotPageFactory = createTestComponentFactory(DotPlotPage, defaultProps);
+// Helper function to get genes held within the tree
+const getTreeGenes = (container) => {
+ const treeNodeList = container.querySelectorAll('span[class*=ant-tree-title]');
+ return Array.from(treeNodeList).map((node) => node.textContent);
+};
+
+// Helper function to get current order of displayed genes in enzyme tests
+const getCurrentGeneOrder = (component) => {
+ const treeNodes = component.find('div.ant-tree-treenode');
+ const newOrder = [];
+ treeNodes.forEach((node) => {
+ newOrder.push(node.text());
+ });
+ newOrder.splice(0, 1);
+ return newOrder;
+};
+
const renderDotPlot = async (store) => {
await act(async () => {
render(
@@ -96,6 +118,14 @@ const renderDotPlot = async (store) => {
});
};
+const renderDotPlotForEnzyme = async (store) => (
+ mount(
+
+ {dotPlotPageFactory()}
+ ,
+ )
+);
+
enableFetchMocks();
let storeState = null;
@@ -226,7 +256,7 @@ describe('Dot plot page', () => {
// 2nd call to load dot plot
.mockImplementationOnce(() => null)
.mockImplementationOnce((Etag) => mockWorkerResponses[Etag]())
- // 3nd call to load dot plot
+ // 3rd call to load dot plot
.mockImplementationOnce(() => null)
.mockImplementationOnce((Etag) => mockWorkerResponses[Etag]());
@@ -271,4 +301,249 @@ describe('Dot plot page', () => {
expect(screen.getByText(/A comparison can not be run to determine the top marker genes/i)).toBeInTheDocument();
expect(screen.getByText(/Select another option from the 'Select data' menu/i)).toBeInTheDocument();
});
+
+ it('removing a gene keeps the order', async () => {
+ await renderDotPlot(storeState);
+
+ const geneTree = screen.getByRole('tree');
+
+ // first three genes of the data should be loaded by default
+ const loadedGenes = paginatedGeneExpressionData.rows.map((row) => (row.gene_names)).slice(0, 3);
+
+ // The genes in Data 5 should be in the tree
+ loadedGenes.forEach((geneName) => {
+ expect(within(geneTree).getByText(geneName)).toBeInTheDocument();
+ });
+
+ // Remove a gene using the X button
+ const genesListBeforeRemoval = getTreeGenes(geneTree);
+
+ const geneToRemove = within(geneTree).getByText(genesListBeforeRemoval[1]);
+
+ const geneRemoveButton = geneToRemove.nextSibling.firstChild;
+
+ userEvent.click(geneRemoveButton);
+
+ const genesListAfterRemoval = getTreeGenes(geneTree);
+
+ // remove element from list manually to compare
+ genesListBeforeRemoval.splice(1, 1);
+
+ // The gene should be deleted from the list
+ expect(_.isEqual(genesListAfterRemoval, genesListBeforeRemoval)).toEqual(true);
+ });
+
+ it('searches for genes and adds a valid gene', async () => {
+ await renderDotPlot(storeState);
+
+ const geneTree = screen.getByRole('tree');
+ const initialOrder = getTreeGenes(geneTree);
+
+ // check placeholder text is loaded
+ expect(screen.getByText('Search for genes...')).toBeInTheDocument();
+
+ const searchBox = screen.getByRole('combobox');
+
+ // search for genes using lowercase
+ userEvent.type(searchBox, 'ap');
+
+ // antd creates multiple elements for options
+ // find option element by title, clicking on element with role='option' does nothing
+ const option = screen.getByTitle('Apoe');
+
+ await act(async () => {
+ // the element has pointer-events set to 'none', skip check
+ // based on https://stackoverflow.com/questions/61080116
+ userEvent.click(option, undefined, { skipPointerEventsCheck: true });
+ });
+
+ // check the search text is cleared after selecting a valid option
+ expect(searchBox.value).toBe('');
+
+ // check the selected gene was added
+ expect(within(geneTree).getByText('Apoe')).toBeInTheDocument();
+
+ // check the genes were not re-ordered when adding
+ initialOrder.push('Apoe');
+ expect(_.isEqual(initialOrder, getTreeGenes(geneTree))).toEqual(true);
+ });
+
+ it('adds an already loaded gene and clears the input', async () => {
+ await renderDotPlot(storeState);
+
+ const searchBox = screen.getByRole('combobox');
+
+ userEvent.type(searchBox, 'ly');
+
+ const option = screen.getByTitle('Lyz2');
+
+ // expecting option to be disabled throws error, click the option instead and check reaction
+ await act(async () => {
+ userEvent.click(option, undefined, { skipPointerEventsCheck: true });
+ });
+
+ // search box shouldn't clear when selecting an already loaded gene
+ expect(searchBox.value).toBe('ly');
+
+ // clear button is automatically generated by antd and cannot be easily accessed
+ const clearButton = searchBox.closest('div[class*=ant-select-auto-complete]').lastChild;
+
+ userEvent.click(clearButton);
+
+ expect(searchBox.value).toBe('');
+ });
+
+ it('resets the data', async () => {
+ await renderDotPlot(storeState);
+
+ seekFromS3
+ .mockReset()
+ // 1st call to list genes
+ .mockImplementationOnce(() => null)
+ .mockImplementationOnce((Etag) => mockWorkerResponses[Etag]())
+ // 2nd call to load dot plot
+ .mockImplementationOnce(() => null)
+ .mockImplementationOnce((Etag) => mockWorkerResponses[Etag]())
+ // 3rd call to load dot plot
+ .mockImplementationOnce(() => null)
+ .mockImplementationOnce((Etag) => mockWorkerResponses[Etag]());
+
+ // add a gene to prepare for reset
+ const searchBox = screen.getByRole('combobox');
+
+ userEvent.type(searchBox, 'ap');
+
+ const option = screen.getByTitle('Apoe');
+
+ await act(async () => {
+ userEvent.click(option, undefined, { skipPointerEventsCheck: true });
+ });
+
+ const resetButton = screen.getAllByText('Reset')[1];
+
+ await act(async () => {
+ userEvent.click(resetButton);
+ });
+
+ // expect the gene only within the options of the search box, antd creates 2 elements
+ expect(screen.getAllByText('Apoe').length).toBe(2);
+ });
+});
+
+// drag and drop is impossible in RTL, use enzyme
+describe('Drag and drop enzyme tests', () => {
+ let component;
+ let tree;
+ let loadedGenes;
+
+ beforeEach(async () => {
+ jest.clearAllMocks();
+
+ seekFromS3
+ .mockReset()
+ // 1st call to list genes
+ .mockImplementationOnce(() => null)
+ .mockImplementationOnce((Etag) => mockWorkerResponses[Etag]())
+ // 2nd call to paginated gene expression
+ .mockImplementationOnce(() => null)
+ .mockImplementationOnce((Etag) => mockWorkerResponses[Etag]());
+
+ fetchMock.resetMocks();
+ fetchMock.mockIf(/.*/, mockAPI(mockAPIResponse));
+
+ storeState = makeStore();
+
+ await storeState.dispatch(loadBackendStatus(experimentId));
+
+ storeState.dispatch({
+ type: EXPERIMENT_SETTINGS_INFO_UPDATE,
+ payload: {
+ experimentId: fake.EXPERIMENT_ID,
+ experimentName: fake.EXPERIMENT_NAME,
+ },
+ });
+
+ component = await renderDotPlotForEnzyme(storeState);
+
+ await waitForComponentToPaint(component);
+
+ component.update();
+
+ // antd renders 5 elements, use the first one
+ tree = component.find({ 'data-testid': 'HierachicalTreeGenes' }).at(0);
+ loadedGenes = paginatedGeneExpressionData.rows.map((row) => (row.gene_names)).slice(0, 3).reverse();
+ });
+
+ it('changes nothing on drop in place', async () => {
+ // default genes are in the tree
+ loadedGenes.forEach((geneName) => {
+ expect(tree.containsMatchingElement(geneName));
+ });
+
+ // dropping in place does nothing
+ const info = {
+ dragNode: { key: 1, pos: '0-1' },
+ dropPosition: 1,
+ dropToGap: true,
+ };
+
+ tree.getElement().props.onDrop(info);
+
+ await act(async () => {
+ component.update();
+ });
+
+ const newOrder = getCurrentGeneOrder(component);
+
+ expect(_.isEqual(newOrder, loadedGenes)).toEqual(true);
+ });
+
+ it('changes nothing when not dropped in gap', async () => {
+ // default genes are in the tree
+ loadedGenes.forEach((geneName) => {
+ expect(tree.containsMatchingElement(geneName));
+ });
+
+ // not dropping to gap does nothing
+ const info = {
+ dragNode: { key: 0, pos: '0-0' },
+ dropPosition: 2,
+ dropToGap: false,
+ };
+
+ tree.getElement().props.onDrop(info);
+
+ await act(async () => {
+ component.update();
+ });
+
+ const newOrder = getCurrentGeneOrder(component);
+
+ expect(_.isEqual(newOrder, loadedGenes)).toEqual(true);
+ });
+
+ it('re-orders genes correctly', async () => {
+ // default genes are in the tree
+ loadedGenes.forEach((geneName) => {
+ expect(tree.containsMatchingElement(geneName));
+ });
+ // dropping to gap re-orders genes
+ const info = {
+ dragNode: { key: 0, pos: '0-0' },
+ dropPosition: 2,
+ dropToGap: true,
+ };
+
+ tree.getElement().props.onDrop(info);
+
+ await act(async () => {
+ component.update();
+ });
+
+ const newOrder = getCurrentGeneOrder(component);
+
+ const expectedOrder = arrayMoveImmutable(loadedGenes, 0, 1);
+
+ expect(_.isEqual(newOrder, expectedOrder)).toEqual(true);
+ });
});
diff --git a/src/__test__/pages/experiments/[experimentId]/plots-and-tables/marker-heatmap/index.test.jsx b/src/__test__/pages/experiments/[experimentId]/plots-and-tables/marker-heatmap/index.test.jsx
index 9a3db3fdb7..6bb350442b 100644
--- a/src/__test__/pages/experiments/[experimentId]/plots-and-tables/marker-heatmap/index.test.jsx
+++ b/src/__test__/pages/experiments/[experimentId]/plots-and-tables/marker-heatmap/index.test.jsx
@@ -85,12 +85,6 @@ const defaultProps = { experimentId };
const heatmapPageFactory = createTestComponentFactory(MarkerHeatmap, defaultProps);
-// Helper function to get displayed genes from the gene input
-const getDisplayedGenes = (container) => {
- const genesNodeList = container.querySelectorAll('span[class*=selection-item-content]');
- return Array.from(genesNodeList).map((gene) => gene.textContent);
-};
-
// Helper function to get genes held within the tree
const getTreeGenes = (container) => {
const treeNodeList = container.querySelectorAll('span[class*=ant-tree-title]');
@@ -242,19 +236,19 @@ describe('Marker heatmap plot', () => {
await renderHeatmapPage(storeState);
// Add in a new gene
- // This is done because we can not insert text into the genes list input
const genesToLoad = [...markerGenesData5.order, 'FAKEGENE'];
await act(async () => {
await storeState.dispatch(loadGeneExpression(experimentId, genesToLoad, plotUuid));
});
- expect(screen.getByText('FAKEGENE')).toBeInTheDocument();
+ // Get genes displayed in the tree
+ const geneTree = screen.getByRole('tree');
- // The returned value is a HTML NodeList
- const genesContainer = screen.getByText('FAKEGENE').closest('div[class*=selector]');
+ const displayedGenesList = getTreeGenes(geneTree);
- const displayedGenesList = getDisplayedGenes(genesContainer);
+ // check the added gene is in the tree
+ expect(within(geneTree).getByText('FAKEGENE')).toBeInTheDocument();
// Check that the genes is ordered correctly.
// This means that FAKEGENE should not be the last in the genes list
@@ -305,64 +299,8 @@ describe('Marker heatmap plot', () => {
});
it('removing a gene keeps the sorted order without re-sorting', async () => {
- seekFromS3
- .mockReset()
- // load genes list
- .mockImplementationOnce(() => null)
- .mockImplementationOnce((Etag) => mockWorkerResponses[Etag])
- // 1st load
- .mockImplementationOnce(() => null)
- .mockImplementationOnce((ETag) => mockWorkerResponses[ETag])
- // 2nd load
- .mockImplementationOnce(() => null)
- .mockImplementationOnce((ETag) => mockWorkerResponses[ETag]);
-
await renderHeatmapPage(storeState);
- // Setting up so that there is an inserted gene in the list
- const genesToLoad = [...markerGenesData5.order, 'FAKEGENE'];
-
- await act(async () => {
- // This is done because we can not insert text into the genes list input
- await storeState.dispatch(loadGeneExpression(experimentId, genesToLoad, plotUuid));
- });
-
- expect(screen.getByText('FAKEGENE')).toBeInTheDocument();
-
- // The returned value is a HTML NodeList
- const genesContainer = screen.getByText('FAKEGENE').closest('div[class*=selector]');
- const genesListBeforeRemoval = getDisplayedGenes(genesContainer);
-
- // Removing the 5th gene from the list
- // genesListBeforeRemoval is modified - splice removes the item from the list
- const geneToRemove = genesListBeforeRemoval.splice(5, 1);
- const geneRemoveButton = screen.getByText(geneToRemove).nextSibling;
-
- userEvent.click(geneRemoveButton);
-
- // Get newly displayed genes after the removal
- const genesListAfterRemoval = getDisplayedGenes(genesContainer);
-
- // The list of displayed genes should be in the same order as the displayed genes
- expect(_.isEqual(genesListAfterRemoval, genesListBeforeRemoval)).toEqual(true);
- });
-
- it('loads the tabs under gene selection', async () => {
- await renderHeatmapPage(storeState);
-
- expect(screen.getByText(/Add\/Remove genes/i)).toBeInTheDocument();
- expect(screen.getByText(/Search for and re-order genes/i)).toBeInTheDocument();
- });
-
- it('switches tabs and removes genes within the tree', async () => {
- await renderHeatmapPage(storeState);
-
- await act(async () => {
- userEvent.click(screen.getByText('Search for and re-order genes'));
- });
- // note: clicking another tab doesn't remove previous tab from screen
- // screen.getByText will find multiples of the same gene -> use within(geneTree)
-
const geneTree = screen.getByRole('tree');
// The genes in Data 5 should be in the tree
@@ -391,10 +329,6 @@ describe('Marker heatmap plot', () => {
it('searches for genes and adds a valid gene', async () => {
await renderHeatmapPage(storeState);
- await act(async () => {
- userEvent.click(screen.getByText('Search for and re-order genes'));
- });
-
// check placeholder text is loaded
expect(screen.getByText('Search for genes...')).toBeInTheDocument();
@@ -431,17 +365,13 @@ describe('Marker heatmap plot', () => {
it('adds an already loaded gene and clears the input', async () => {
await renderHeatmapPage(storeState);
- await act(async () => {
- userEvent.click(screen.getByText('Search for and re-order genes'));
- });
-
const searchBox = screen.getByRole('combobox');
userEvent.type(searchBox, 'tmem');
- // this finds option for selection box in 1st tab and search box, use second element
- const option = screen.getAllByTitle('Tmem176a')[1];
+ const option = screen.getByTitle('Tmem176a');
+ // expecting option to be disabled throws error, click the option instead and check reaction
await act(async () => {
userEvent.click(option, undefined, { skipPointerEventsCheck: true });
});
@@ -458,7 +388,7 @@ describe('Marker heatmap plot', () => {
});
});
-// drag and drop is impossible in jest, use enzyme
+// drag and drop is impossible in RTL, use enzyme
describe('Drag and drop enzyme tests', () => {
let component;
let tree;
@@ -489,12 +419,6 @@ describe('Drag and drop enzyme tests', () => {
await waitForComponentToPaint(component);
- // activate re-order genes tab
- await act(async () => {
- const reorderTab = component.find('div.ant-tabs-tab-btn');
- reorderTab.at(1).simulate('click');
- });
-
component.update();
// antd renders 5 elements, use the first one
@@ -510,7 +434,6 @@ describe('Drag and drop enzyme tests', () => {
// dropping in place does nothing
const info = {
dragNode: { key: 1, pos: '0-1' },
- node: { key: 1, pos: '0-1' },
dropPosition: 1,
dropToGap: true,
};
@@ -535,7 +458,6 @@ describe('Drag and drop enzyme tests', () => {
// not dropping to gap does nothing
const info = {
dragNode: { key: 1, pos: '0-1' },
- node: { key: 3, pos: '0-3' },
dropPosition: 4,
dropToGap: false,
};
@@ -559,7 +481,6 @@ describe('Drag and drop enzyme tests', () => {
// dropping to gap re-orders genes
const info = {
dragNode: { key: 1, pos: '0-1' },
- node: { key: 3, pos: '0-3' },
dropPosition: 4,
dropToGap: true,
};
diff --git a/src/components/data-exploration/hierarchical-tree/hierarchicalTree.css b/src/components/data-exploration/hierarchical-tree/hierarchicalTree.css
index 1f71759f77..9996c2c70d 100644
--- a/src/components/data-exploration/hierarchical-tree/hierarchicalTree.css
+++ b/src/components/data-exploration/hierarchical-tree/hierarchicalTree.css
@@ -1,3 +1,7 @@
+.ant-tree {
+ background-color: #f5f8fa;
+}
+
.ant-tree-list-holder-inner {
background-color: #f5f8fa;
}
\ No newline at end of file
diff --git a/src/components/plots/GeneReorderTool.jsx b/src/components/plots/GeneReorderTool.jsx
index c95023b65d..873b539da2 100644
--- a/src/components/plots/GeneReorderTool.jsx
+++ b/src/components/plots/GeneReorderTool.jsx
@@ -2,30 +2,24 @@ import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import PropTypes from 'prop-types';
-import { arrayMoveImmutable } from 'utils/array-move';
import { updatePlotConfig } from 'redux/actions/componentConfig';
-import { loadGeneExpression } from 'redux/actions/genes';
+
+import { arrayMoveImmutable } from 'utils/array-move';
import HierarchicalTreeGenes from 'components/plots/hierarchical-tree-genes/HierarchicalTreeGenes';
import { Space, Button } from 'antd';
import { CloseOutlined } from '@ant-design/icons';
const GeneReorderTool = (props) => {
- const { plotUuid } = (props);
+ const { plotUuid, onDelete } = props;
const dispatch = useDispatch();
-
+
const config = useSelector((state) => state.componentConfig[plotUuid]?.config);
- const experimentId = useSelector((state) => state.componentConfig[plotUuid]?.experimentId);
-
- const loadedMarkerGenes = useSelector(
- (state) => state.genes.expression.views[plotUuid]?.data,
- );
-
// Tree from antd requires format [{key: , title: }], made from gene names from loadedMarkerGenes and config
const composeGeneTree = (treeGenes) => {
- if (!treeGenes) {
+ if (!treeGenes.length) {
return [];
}
@@ -36,7 +30,7 @@ const GeneReorderTool = (props) => {
return data;
};
- const [geneTreeData, setGeneTreeData] = useState(composeGeneTree(loadedMarkerGenes));
+ const [geneTreeData, setGeneTreeData] = useState([]);
useEffect(() => {
setGeneTreeData(composeGeneTree(config?.selectedGenes));
@@ -56,7 +50,7 @@ const GeneReorderTool = (props) => {
const genes = geneTreeData.map((treeNode) => treeNode.title);
genes.splice(geneKey, 1);
- dispatch(loadGeneExpression(experimentId, genes, plotUuid));
+ onDelete(genes);
};
const renderTitles = (data) => {
@@ -101,6 +95,7 @@ GeneReorderTool.defaultProps = {};
GeneReorderTool.propTypes = {
plotUuid: PropTypes.string.isRequired,
+ onDelete: PropTypes.func.isRequired,
};
export default GeneReorderTool;
diff --git a/src/components/plots/GeneSearchBar.jsx b/src/components/plots/GeneSearchBar.jsx
index 337788d388..69ba3b265a 100644
--- a/src/components/plots/GeneSearchBar.jsx
+++ b/src/components/plots/GeneSearchBar.jsx
@@ -1,8 +1,6 @@
import React, { useState } from 'react';
-import _ from 'lodash';
import { AutoComplete } from 'antd';
-import { useSelector, useDispatch } from 'react-redux';
-import { loadGeneExpression } from 'redux/actions/genes';
+import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
const filterGenes = (searchText, geneList, loadedGenes) => {
@@ -17,9 +15,9 @@ const filterGenes = (searchText, geneList, loadedGenes) => {
};
const GeneSearchBar = (props) => {
- const { plotUuid, experimentId, searchBarUuid } = props;
-
- const dispatch = useDispatch();
+ const {
+ plotUuid, searchBarUuid, onSelect,
+ } = props;
const geneList = useSelector((state) => state.genes.properties.views[searchBarUuid]?.data);
@@ -30,14 +28,10 @@ const GeneSearchBar = (props) => {
// pass reactive component as value (search text) to allow auto clear on select
const [value, setValue] = useState('');
- const onSelect = (newGene) => {
- if (!geneList.includes(newGene) || config?.selectedGenes.includes(newGene)) {
- return;
- }
+ const onOptionSelect = (newGene) => {
+ const genes = [...config?.selectedGenes, newGene];
- const genes = _.clone(config?.selectedGenes);
- genes.push(newGene);
- dispatch(loadGeneExpression(experimentId, genes, plotUuid));
+ onSelect(genes);
setValue('');
};
@@ -52,7 +46,7 @@ const GeneSearchBar = (props) => {
value={value}
options={options}
style={{ width: '100%' }}
- onSelect={onSelect}
+ onSelect={onOptionSelect}
onSearch={onSearch}
placeholder='Search for genes...'
/>
@@ -61,8 +55,8 @@ const GeneSearchBar = (props) => {
GeneSearchBar.propTypes = {
plotUuid: PropTypes.string.isRequired,
- experimentId: PropTypes.string.isRequired,
searchBarUuid: PropTypes.string.isRequired,
+ onSelect: PropTypes.func.isRequired,
};
export default GeneSearchBar;
diff --git a/src/components/plots/ScrollOnDrag.jsx b/src/components/plots/ScrollOnDrag.jsx
new file mode 100644
index 0000000000..d328afa45c
--- /dev/null
+++ b/src/components/plots/ScrollOnDrag.jsx
@@ -0,0 +1,43 @@
+// find the Y position of an object in the document
+// based on https://www.quirksmode.org/js/findpos.html
+const findTop = (elem) => {
+ let posTop = 0;
+
+ if (!elem.offsetParent) {
+ return;
+ }
+ // recursion to add .offsetTop of an object and all parents of the object
+ // until obj.offsetParent is undefined, needed to compare with event.clientY for drag event
+ do {
+ posTop += elem.offsetTop;
+ } while (elem = elem.offsetParent);
+
+ return posTop;
+};
+
+const ScrollOnDrag = (treeScrollable) => {
+ const treeTop = findTop(treeScrollable);
+ // scrollable collapsable tablist is wrapped in a div with overflow
+ const tablist = document.getElementsByClassName('ant-collapse')[0].parentNode;
+ let interval;
+
+ const handleScrollOnDrag = (event) => {
+ const treeHeight = treeScrollable.clientHeight;
+ const relY = event.clientY - treeTop + tablist.scrollTop;
+
+ clearInterval(interval);
+
+ // drag event ends with relY = -treeTop, currently hardcoded to ignore
+ if (relY < 0 && relY !== -treeTop + tablist.scrollTop) {
+ interval = setInterval(() => { treeScrollable.scrollTop -= 5; }, 20);
+ }
+
+ if (relY > treeHeight) {
+ interval = setInterval(() => { treeScrollable.scrollTop += 5; }, 20);
+ }
+ };
+
+ document.addEventListener('drag', handleScrollOnDrag);
+};
+
+export default ScrollOnDrag;
diff --git a/src/components/plots/hierarchical-tree-genes/HierarchicalTreeGenes.css b/src/components/plots/hierarchical-tree-genes/HierarchicalTreeGenes.css
index c2a17c31c3..3e598ed07b 100644
--- a/src/components/plots/hierarchical-tree-genes/HierarchicalTreeGenes.css
+++ b/src/components/plots/hierarchical-tree-genes/HierarchicalTreeGenes.css
@@ -1,3 +1,3 @@
-.ant-tree-list-holder-inner {
+.ant-tree {
background-color: #f5f8fa;
- }
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/src/components/plots/hierarchical-tree-genes/HierarchicalTreeGenes.jsx b/src/components/plots/hierarchical-tree-genes/HierarchicalTreeGenes.jsx
index 13d9d31434..cff6d6543a 100644
--- a/src/components/plots/hierarchical-tree-genes/HierarchicalTreeGenes.jsx
+++ b/src/components/plots/hierarchical-tree-genes/HierarchicalTreeGenes.jsx
@@ -12,7 +12,7 @@ const HierarchicalTreeGenes = (props) => {
const onDrop = (info) => {
const {
- dragNode, node, dropPosition, dropToGap,
+ dragNode, dropPosition, dropToGap,
} = info;
// if dropped in place, ignore
@@ -31,12 +31,17 @@ const HierarchicalTreeGenes = (props) => {
if (!treeData) return ;
return (
-
+ // wrapping in div needed to not unload dragged element when scrolling
+ // add padding to the tree to make first drop position visible
+
+
+
);
};
diff --git a/src/components/plots/styling/MarkerGeneSelection.jsx b/src/components/plots/styling/MarkerGeneSelection.jsx
index 7622997125..6cd0e9f634 100644
--- a/src/components/plots/styling/MarkerGeneSelection.jsx
+++ b/src/components/plots/styling/MarkerGeneSelection.jsx
@@ -4,13 +4,15 @@ import {
Space,
Radio,
InputNumber,
- Select,
Button,
} from 'antd';
+import GeneReorderTool from 'components/plots/GeneReorderTool';
+import GeneSearchBar from 'components/plots/GeneSearchBar';
+
const MarkerGeneSelection = (props) => {
const {
- config, onUpdate, onReset, onGeneEnter,
+ config, plotUuid, searchBarUuid, onUpdate, onReset, onGenesChange,
} = props;
const [numGenes, setNumGenes] = useState(config.nMarkerGenes);
@@ -41,15 +43,16 @@ const MarkerGeneSelection = (props) => {
return (
- Type in a gene name and hit space or enter to add it to the heatmap.
-