diff --git a/src/__test__/components/data-exploration/cell-sets-tool/CellSetsTool.test.jsx b/src/__test__/components/data-exploration/cell-sets-tool/CellSetsTool.test.jsx index 1af4480b28..858da2cc16 100644 --- a/src/__test__/components/data-exploration/cell-sets-tool/CellSetsTool.test.jsx +++ b/src/__test__/components/data-exploration/cell-sets-tool/CellSetsTool.test.jsx @@ -72,7 +72,6 @@ cellSetsWithAnnotatedCellClass.cellSets.push( ); const louvainClusters = cellSetsData.cellSets.find(({ key }) => key === 'louvain').children; -const sampleList = cellSetsData.cellSets.find(({ key }) => key === 'sample').children; const experimentId = '1234'; diff --git a/src/__test__/components/data-exploration/generic-gene-table/ComponentActions.test.jsx b/src/__test__/components/data-exploration/generic-gene-table/ComponentActions.test.jsx index cc7ec90b37..6151878f2a 100644 --- a/src/__test__/components/data-exploration/generic-gene-table/ComponentActions.test.jsx +++ b/src/__test__/components/data-exploration/generic-gene-table/ComponentActions.test.jsx @@ -77,8 +77,8 @@ describe('ComponentActions', () => { const dropdown = component.find(Dropdown); expect(dropdown.length).toEqual(1); - expect(dropdown.props().overlay.type.name).toEqual('Menu'); - expect(dropdown.props().overlay.props.children.length).toEqual(3); + expect(dropdown.props().menu.type.name).toEqual('Menu'); + expect(dropdown.props().menu.props.children.length).toEqual(3); }); it('Renders correctly when there are no selected genes', () => { @@ -114,7 +114,7 @@ describe('ComponentActions', () => { , ); - const menuButtons = component.find(Dropdown).props().overlay; + const menuButtons = component.find(Dropdown).props().menu; menuButtons.props.children[0].props.onClick(); // Wait for side-effect to propagate (properties loading and loaded). @@ -154,7 +154,7 @@ describe('ComponentActions', () => { , ); - const menuButtons = component.find(Dropdown).props().overlay; + const menuButtons = component.find(Dropdown).props().menu; menuButtons.props.children[1].props.onClick(); // Wait for side-effect to propagate (genes loaded). @@ -182,7 +182,7 @@ describe('ComponentActions', () => { , ); - const menuButtons = component.find(Dropdown).props().overlay; + const menuButtons = component.find(Dropdown).props().menu; menuButtons.props.children[2].props.onClick(); // Wait for side-effect to propagate (properties loading and loaded). diff --git a/src/__test__/components/data-exploration/generic-gene-table/LaunchPathwayAnalysisModal.test.jsx b/src/__test__/components/data-exploration/generic-gene-table/LaunchPathwayAnalysisModal.test.jsx index eff89990dc..ef9c80fe6c 100644 --- a/src/__test__/components/data-exploration/generic-gene-table/LaunchPathwayAnalysisModal.test.jsx +++ b/src/__test__/components/data-exploration/generic-gene-table/LaunchPathwayAnalysisModal.test.jsx @@ -12,7 +12,7 @@ import fetchMock, { enableFetchMocks } from 'jest-fetch-mock'; import handleError from 'utils/http/handleError'; import downloadFromUrl from 'utils/downloadFromUrl'; -import writeToFile from 'utils/writeToFileURL'; +import writeToFile from 'utils/upload/writeToFileURL'; import launchPathwayService from 'utils/pathwayAnalysis/launchPathwayService'; import getDiffExprGenes from 'utils/extraActionCreators/differentialExpression/getDiffExprGenes'; @@ -27,7 +27,7 @@ jest.mock('utils/extraActionCreators/differentialExpression/getBackgroundExpress jest.mock('utils/http/handleError'); jest.mock('utils/downloadFromUrl'); -jest.mock('utils/writeToFileURL'); +jest.mock('utils/upload/writeToFileURL'); const onCancel = jest.fn(); const onOpenAdvancedFilters = jest.fn(); diff --git a/src/__test__/components/data-management/DownloadDataButton.test.jsx b/src/__test__/components/data-management/DownloadDataButton.test.jsx index 4e2b6b151d..e82f0f6966 100644 --- a/src/__test__/components/data-management/DownloadDataButton.test.jsx +++ b/src/__test__/components/data-management/DownloadDataButton.test.jsx @@ -28,14 +28,14 @@ import fake from '__test__/test-utils/constants'; import { loadBackendStatus } from 'redux/actions/backendStatus'; import { loadProcessingSettings } from 'redux/actions/experimentSettings'; import downloadFromUrl from 'utils/downloadFromUrl'; -import writeToFileURL from 'utils/writeToFileURL'; +import writeToFileURL from 'utils/upload/writeToFileURL'; jest.mock('file-saver'); jest.mock('utils/work/fetchWork'); jest.mock('utils/pushNotificationMessage'); jest.mock('utils/downloadFromUrl'); -jest.mock('utils/writeToFileURL'); +jest.mock('utils/upload/writeToFileURL'); const experimentId = `${fake.EXPERIMENT_ID}-0`; diff --git a/src/__test__/components/data-management/ProjectCard.test.jsx b/src/__test__/components/data-management/ProjectCard.test.jsx index 8129925d89..130788cd26 100644 --- a/src/__test__/components/data-management/ProjectCard.test.jsx +++ b/src/__test__/components/data-management/ProjectCard.test.jsx @@ -12,7 +12,7 @@ import { Provider } from 'react-redux'; import thunk from 'redux-thunk'; import initialState, { experimentTemplate } from 'redux/reducers/experiments/initialState'; -import ProjectCard from 'components/data-management/ProjectCard'; +import ProjectCard from 'components/data-management/project/ProjectCard'; const experimentId = 'experimentId1'; const experimentName = 'Test Experiment'; diff --git a/src/__test__/components/data-management/ProjectDeleteModal.test.jsx b/src/__test__/components/data-management/ProjectDeleteModal.test.jsx index a65ed0e3a8..7804937c04 100644 --- a/src/__test__/components/data-management/ProjectDeleteModal.test.jsx +++ b/src/__test__/components/data-management/ProjectDeleteModal.test.jsx @@ -7,7 +7,7 @@ import { } from '@testing-library/react'; import configureMockStore from 'redux-mock-store'; import initialExperimentState from 'redux/reducers/experiments/initialState'; -import ProjectDeleteModal from 'components/data-management/ProjectDeleteModal'; +import ProjectDeleteModal from 'components/data-management/project/ProjectDeleteModal'; const mockStore = configureMockStore([thunk]); const experimentName = 'super cool experiment'; diff --git a/src/__test__/components/data-management/ProjectDetails.test.jsx b/src/__test__/components/data-management/ProjectDetails.test.jsx index b910b66fcf..6855d58f9c 100644 --- a/src/__test__/components/data-management/ProjectDetails.test.jsx +++ b/src/__test__/components/data-management/ProjectDetails.test.jsx @@ -28,7 +28,7 @@ import { initialExperimentBackendStatus } from 'redux/reducers/backendStatus/ini import PipelineStatus from 'utils/pipelineStatusValues'; import { sampleTech } from 'utils/constants'; import UploadStatus from 'utils/upload/UploadStatus'; -import ProjectDetails from 'components/data-management/ProjectDetails'; +import ProjectDetails from 'components/data-management/project/ProjectDetails'; import '__test__/test-utils/setupTests'; import createTestComponentFactory from '__test__/test-utils/testComponentFactory'; diff --git a/src/__test__/components/data-management/ProjectMenu.test.jsx b/src/__test__/components/data-management/ProjectMenu.test.jsx index 033ea7357e..eb798c5808 100644 --- a/src/__test__/components/data-management/ProjectMenu.test.jsx +++ b/src/__test__/components/data-management/ProjectMenu.test.jsx @@ -18,7 +18,7 @@ import { experiments } from '__test__/test-utils/mockData'; import createTestComponentFactory from '__test__/test-utils/testComponentFactory'; import mockAPI, { generateDefaultMockAPIResponses } from '__test__/test-utils/mockAPI'; -import ProjectMenu from 'components/data-management/ProjectMenu'; +import ProjectMenu from 'components/data-management/project/ProjectMenu'; import { loadExperiments, setActiveExperiment } from 'redux/actions/experiments'; const mockNavigateTo = jest.fn(); diff --git a/src/__test__/components/data-management/ProjectSearchBox.test.jsx b/src/__test__/components/data-management/ProjectSearchBox.test.jsx index 906165ad72..b13941716f 100644 --- a/src/__test__/components/data-management/ProjectSearchBox.test.jsx +++ b/src/__test__/components/data-management/ProjectSearchBox.test.jsx @@ -1,7 +1,7 @@ import React from 'react'; import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import ProjectSearchBox from 'components/data-management/ProjectSearchBox'; +import ProjectSearchBox from 'components/data-management/project/ProjectSearchBox'; const onChangeSpy = jest.fn(); diff --git a/src/__test__/components/data-management/ProjectsList.test.jsx b/src/__test__/components/data-management/ProjectsList.test.jsx index 3979ce904b..ea50b44ffc 100644 --- a/src/__test__/components/data-management/ProjectsList.test.jsx +++ b/src/__test__/components/data-management/ProjectsList.test.jsx @@ -5,8 +5,8 @@ import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import '@testing-library/jest-dom'; import initialState, { experimentTemplate } from 'redux/reducers/experiments/initialState'; -import ProjectsList from 'components/data-management/ProjectsList'; -import ProjectCard from 'components/data-management/ProjectCard'; +import ProjectsList from 'components/data-management/project/ProjectsList'; +import ProjectCard from 'components/data-management/project/ProjectCard'; import '__test__/test-utils/setupTests'; const mockStore = configureMockStore([thunk]); diff --git a/src/__test__/components/data-management/ProjectsListContainer.test.jsx b/src/__test__/components/data-management/ProjectsListContainer.test.jsx index cf8f667c1b..8ad49732a1 100644 --- a/src/__test__/components/data-management/ProjectsListContainer.test.jsx +++ b/src/__test__/components/data-management/ProjectsListContainer.test.jsx @@ -3,7 +3,7 @@ import '__test__/test-utils/setupTests'; import { render, screen } from '@testing-library/react'; -import ProjectsListContainer from 'components/data-management/ProjectsListContainer'; +import ProjectsListContainer from 'components/data-management/project/ProjectsListContainer'; import { Provider } from 'react-redux'; import React from 'react'; import { act } from 'react-dom/test-utils'; diff --git a/src/__test__/components/data-processing/PlotLayout.test.jsx b/src/__test__/components/data-processing/PlotLayout.test.jsx new file mode 100644 index 0000000000..22f7ca6e6f --- /dev/null +++ b/src/__test__/components/data-processing/PlotLayout.test.jsx @@ -0,0 +1,81 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { Provider } from 'react-redux'; +import thunk from 'redux-thunk'; +import configureStore from 'redux-mock-store'; +import PlotLayout from 'components/data-processing/PlotLayout'; +import '__test__/test-utils/setupTests'; + +// Mock data and store setup +const experimentId = 'exp1'; +const sampleId = 'sample1'; +const sampleIds = ['sample1', 'sample2']; +const filterName = 'testFilter'; +const filterTableUuid = 'filterTableUuid1'; +const plots = { + plot1: { + plotUuid: 'plotUuid1', + plot: jest.fn(), + plotType: 'type1', + }, + plot2: { + plotUuid: 'plotUuid2', + plot: jest.fn(), + plotType: 'type2', + }, +}; +const mockStore = configureStore([thunk]); +const initialState = { + componentConfig: { + plotUuid1: { config: {}, plotData: {} }, + plotUuid2: { config: {}, plotData: {} }, + filterTableUuid1: { plotData: {} }, + }, + experimentSettings: { + processing: { + [filterName]: { + [sampleId]: { filterSettings: {} }, + }, + }, + }, +}; + +const renderComponent = (store) => render( + +
} + stepHadErrors={false} + allowedPlotActions={{}} + /> + , +); + +describe('PlotLayout', () => { + let store; + + beforeEach(() => { + store = mockStore(initialState); + }); + + it('renders without crashing', () => { + renderComponent(store); + expect(screen.getByText('Filtering Settings')).toBeInTheDocument(); + }); + + it('changes selected plot on mini plot click', () => { + renderComponent(store); + const miniPlotButton = screen.getAllByRole('button')[0]; + userEvent.click(miniPlotButton); + expect(plots.plot1.plot).toHaveBeenCalled(); + }); +}); diff --git a/src/__test__/components/data-processing/StatusIndicator.test.jsx b/src/__test__/components/data-processing/StatusIndicator.test.jsx index 7f9b787ca4..7e459ddcb4 100644 --- a/src/__test__/components/data-processing/StatusIndicator.test.jsx +++ b/src/__test__/components/data-processing/StatusIndicator.test.jsx @@ -4,7 +4,6 @@ import { } from '@testing-library/react'; import StatusIndicator from 'components/data-processing/StatusIndicator'; import { Provider } from 'react-redux'; -import { act } from 'react-dom/test-utils'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import fake from '__test__/test-utils/constants'; diff --git a/src/__test__/components/plots/MultiViewGrid.test.jsx b/src/__test__/components/plots/MultiViewGrid.test.jsx index dae175b0b0..19593c6998 100644 --- a/src/__test__/components/plots/MultiViewGrid.test.jsx +++ b/src/__test__/components/plots/MultiViewGrid.test.jsx @@ -16,7 +16,7 @@ import { generateMultiViewGridPlotUuid } from 'utils/generateCustomPlotUuid'; import { makeStore } from 'redux/store'; import { updatePlotConfig } from 'redux/actions/componentConfig'; import loadConditionalComponentConfig from 'redux/actions/componentConfig/loadConditionalComponentConfig'; -import { arrayMoveImmutable } from 'utils/array-move'; +import { arrayMoveImmutable } from 'utils/arrayUtils'; import fake from '__test__/test-utils/constants'; import mockAPI, { 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 1cd5fdef31..2eaa0e485a 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 @@ -38,7 +38,7 @@ import { plotNames } from 'utils/constants'; import ExportAsCSV from 'components/plots/ExportAsCSV'; import waitForComponentToPaint from '__test__/test-utils/waitForComponentToPaint'; -import { arrayMoveImmutable } from 'utils/array-move'; +import { arrayMoveImmutable } from 'utils/arrayUtils'; jest.mock('components/plots/ExportAsCSV', () => jest.fn(() => (<>))); jest.mock('components/header/UserButton', () => () => <>); diff --git a/src/__test__/pages/experiments/[experimentId]/plots-and-tables/marker-heatmap/markerDragDrop.test.jsx b/src/__test__/pages/experiments/[experimentId]/plots-and-tables/marker-heatmap/markerDragDrop.test.jsx index 264fccc836..712901ad1d 100644 --- a/src/__test__/pages/experiments/[experimentId]/plots-and-tables/marker-heatmap/markerDragDrop.test.jsx +++ b/src/__test__/pages/experiments/[experimentId]/plots-and-tables/marker-heatmap/markerDragDrop.test.jsx @@ -23,7 +23,7 @@ import mockAPI, { } from '__test__/test-utils/mockAPI'; import createTestComponentFactory from '__test__/test-utils/testComponentFactory'; import waitForComponentToPaint from '__test__/test-utils/waitForComponentToPaint'; -import { arrayMoveImmutable } from 'utils/array-move'; +import { arrayMoveImmutable } from 'utils/arrayUtils'; jest.mock('components/header/UserButton', () => () => <>); jest.mock('react-resize-detector', () => (props) => { diff --git a/src/__test__/pages/experiments/[experimentId]/plots-and-tables/normalized-matrix/index.test.jsx b/src/__test__/pages/experiments/[experimentId]/plots-and-tables/normalized-matrix/index.test.jsx index 978ba2d9fe..b0b2a493ab 100644 --- a/src/__test__/pages/experiments/[experimentId]/plots-and-tables/normalized-matrix/index.test.jsx +++ b/src/__test__/pages/experiments/[experimentId]/plots-and-tables/normalized-matrix/index.test.jsx @@ -17,11 +17,11 @@ import { makeStore } from 'redux/store'; import mockAPI, { generateDefaultMockAPIResponses, promiseResponse, statusResponse } from '__test__/test-utils/mockAPI'; import cellSetsData from '__test__/data/cell_sets_with_clm.json'; import fetchWork from 'utils/work/fetchWork'; -import writeToFileURL from 'utils/writeToFileURL'; +import writeToFileURL from 'utils/upload/writeToFileURL'; import downloadFromUrl from 'utils/downloadFromUrl'; jest.mock('utils/work/fetchWork'); -jest.mock('utils/writeToFileURL'); +jest.mock('utils/upload/writeToFileURL'); jest.mock('utils/downloadFromUrl'); jest.mock('utils/pushNotificationMessage'); diff --git a/src/__test__/utils/writeToFileURL.test.js b/src/__test__/utils/writeToFileURL.test.js index c7978e321a..c125dff87a 100644 --- a/src/__test__/utils/writeToFileURL.test.js +++ b/src/__test__/utils/writeToFileURL.test.js @@ -1,4 +1,4 @@ -import writeToFileURL from 'utils/writeToFileURL'; +import writeToFileURL from 'utils/upload/writeToFileURL'; const mockCreateObjectURL = jest.fn(() => 'mockURL'); diff --git a/src/components/ColorPicker.jsx b/src/components/ColorPicker.jsx index 10592113c6..8144e675dd 100644 --- a/src/components/ColorPicker.jsx +++ b/src/components/ColorPicker.jsx @@ -34,8 +34,8 @@ const ColorPicker = (props) => { content={pickerComponent()} placement='bottom' trigger='click' - visible={visible} - onVisibleChange={(newVisible) => setVisible(newVisible)} + open={visible} + onOpenChange={(newVisible) => setVisible(newVisible)} destroyTooltipOnHide zIndex={zIndex} > diff --git a/src/components/ContentWrapper.jsx b/src/components/ContentWrapper.jsx index 26cc97b51c..e6423d6ad0 100644 --- a/src/components/ContentWrapper.jsx +++ b/src/components/ContentWrapper.jsx @@ -400,25 +400,53 @@ const ContentWrapper = (props) => { .includes(currentModule) && disableIfNoExperiment; const seuratCompleteDisable = disabledIfSeuratComplete && seuratComplete; - return ( - navigateTo( - module, - { experimentId: currentExperimentId }, - )} - > - {name} - - ); + return { + key: module, + icon, + label: name, + disabled: notProcessedExperimentDisable || pipelineStatusDisable + || seuratCompleteDisable || nonExperimentModule, + onClick: () => navigateTo( + module, + { experimentId: currentExperimentId }, + ), + }; }; if (!user) return <>; + const mainMenuItems = menuLinks + .filter((item) => !item.disableIfNoExperiment) + .map(menuItemRender); + + const groupMenuItems = menuLinks + .filter((item) => item.disableIfNoExperiment) + .map(menuItemRender); + + const groupItem = { + type: 'group', + label: !collapsed && ( + + + + {experimentName || 'No analysis'} + + {experimentName && Current analysis} + + + ), + children: groupMenuItems, + }; + + const menuItems = [...mainMenuItems, groupItem]; + return ( <> @@ -444,43 +472,10 @@ const ContentWrapper = (props) => { module === currentModule) - .map(({ module }) => module) - } + selectedKeys={menuLinks.filter(({ module }) => module === currentModule).map(({ module }) => module)} mode='inline' - > - {menuLinks.filter((item) => !item.disableIfNoExperiment).map(menuItemRender)} - - - - - {experimentName || 'No analysis'} - - {experimentName && ( - - Current analysis - - )} - - - - )} - > - {menuLinks.filter((item) => item.disableIfNoExperiment).map(menuItemRender)} - - - + items={menuItems} + />
{ ) : <>} {cellInfo.cellSets?.length > 0 ? cellInfo.cellSets.map((cellSetName) => ( -
+
{cellSetName}
)) : <>} diff --git a/src/components/data-exploration/cell-sets-tool/SubsetCellSetsModal.jsx b/src/components/data-exploration/cell-sets-tool/SubsetCellSetsModal.jsx index 38cadf6ac3..749d37bd1b 100644 --- a/src/components/data-exploration/cell-sets-tool/SubsetCellSetsModal.jsx +++ b/src/components/data-exploration/cell-sets-tool/SubsetCellSetsModal.jsx @@ -15,7 +15,7 @@ const SubsetCellSetsModal = (props) => { data-testid='subsetCellSetsModal' title='Subset cell sets' okText='Create' - visible + open onOk={() => { onOk(subsetExperimentName.current); }} cancelText='Cancel' onCancel={onCancel} diff --git a/src/components/data-exploration/differential-expression-tool/LaunchPathwayAnalysisModal.jsx b/src/components/data-exploration/differential-expression-tool/LaunchPathwayAnalysisModal.jsx index 7b703f7bb1..fe3de26b89 100644 --- a/src/components/data-exploration/differential-expression-tool/LaunchPathwayAnalysisModal.jsx +++ b/src/components/data-exploration/differential-expression-tool/LaunchPathwayAnalysisModal.jsx @@ -7,7 +7,7 @@ import PropTypes from 'prop-types'; import launchPathwayService from 'utils/pathwayAnalysis/launchPathwayService'; import getDiffExprGenes from 'utils/extraActionCreators/differentialExpression/getDiffExprGenes'; -import writeToFileURL from 'utils/writeToFileURL'; +import writeToFileURL from 'utils/upload/writeToFileURL'; import downloadFromUrl from 'utils/downloadFromUrl'; import getBackgroundExpressedGenes from 'utils/extraActionCreators/differentialExpression/getBackgroundExpressedGenes'; @@ -81,7 +81,7 @@ const LaunchPathwayAnalysisModal = (props) => { return ( <> { } return ( - + diff --git a/src/components/data-exploration/generic-gene-table/ExpressionCellSetModal.jsx b/src/components/data-exploration/generic-gene-table/ExpressionCellSetModal.jsx index 75bf202341..08de4d88c0 100644 --- a/src/components/data-exploration/generic-gene-table/ExpressionCellSetModal.jsx +++ b/src/components/data-exploration/generic-gene-table/ExpressionCellSetModal.jsx @@ -52,7 +52,7 @@ const ExpressionCellSetModal = (props) => { return ( { const menuItems = [ { - label: 'Metadata tracks', key: 'metadataTracks', children: [ + label: 'Metadata tracks', + key: 'metadataTracks', + children: [ { label: (), - key: "metadataTracksChild", - } - ] + key: 'metadataTracksChild', + }, + ], }, { - label: 'Group by', key: 'groupBy', children: [ + label: 'Group by', + key: 'groupBy', + children: [ { label: (), - key: "groupByChild", - } - ] + key: 'groupByChild', + }, + ], }, ]; diff --git a/src/components/data-management/DownloadDataButton.jsx b/src/components/data-management/DownloadDataButton.jsx index 4356ee03a4..d8ab5c4c93 100644 --- a/src/components/data-management/DownloadDataButton.jsx +++ b/src/components/data-management/DownloadDataButton.jsx @@ -101,7 +101,7 @@ const DownloadDataButton = () => { {downloadingProcessedSeurat && } - ) + ), }, { key: 'download-processing-settings', @@ -122,9 +122,9 @@ const DownloadDataButton = () => { Data Processing settings (.txt) ) - ) - } - ] + ), + }, + ]; return ( { items: menuItems, onClick: (e) => { if (e.key !== 'download-processed-seurat') setDropdownExpanded(false); - } + }, }} placement='bottomRight' disabled={ diff --git a/src/components/data-management/FileUploadModal.jsx b/src/components/data-management/FileUploadModal.jsx index c3a7515967..1469d0ee79 100644 --- a/src/components/data-management/FileUploadModal.jsx +++ b/src/components/data-management/FileUploadModal.jsx @@ -172,7 +172,7 @@ const FileUploadModal = (props) => { return ( { { return ( { return ( , 'Share with collaborators']} onCancel={onCancel} okButtonText='Done' diff --git a/src/components/data-management/UploadDetailsModal.jsx b/src/components/data-management/UploadDetailsModal.jsx index 9dc1c1df8f..5e7d4ba4fa 100644 --- a/src/components/data-management/UploadDetailsModal.jsx +++ b/src/components/data-management/UploadDetailsModal.jsx @@ -12,7 +12,7 @@ dayjs.extend(utc); const UploadDetailsModal = (props) => { const { - visible, onCancel, file, extraFields, onDownload, onRetry, onDelete, + onCancel, file, extraFields, onDownload, onRetry, onDelete, } = props; const { diff --git a/src/components/data-management/metadata/MetadataUploadModal.jsx b/src/components/data-management/metadata/MetadataUploadModal.jsx index 67f9a20cf2..4320bbe413 100644 --- a/src/components/data-management/metadata/MetadataUploadModal.jsx +++ b/src/components/data-management/metadata/MetadataUploadModal.jsx @@ -100,7 +100,7 @@ const MetadataUploadModal = (props) => { return ( { - - ))} - - - - - - - {filterTableData - ? - : } - - - - - - - - - - - -
- - - - - - + ); }; CellSizeDistribution.propTypes = { experimentId: PropTypes.string.isRequired, sampleId: PropTypes.string.isRequired, - sampleIds: PropTypes.array.isRequired, + sampleIds: PropTypes.arrayOf(PropTypes.string).isRequired, onConfigChange: PropTypes.func.isRequired, stepDisabled: PropTypes.bool, stepHadErrors: PropTypes.bool.isRequired, diff --git a/src/components/data-processing/ChangesNotAppliedModal.jsx b/src/components/data-processing/ChangesNotAppliedModal.jsx index 83bf29a536..5facd11b00 100644 --- a/src/components/data-processing/ChangesNotAppliedModal.jsx +++ b/src/components/data-processing/ChangesNotAppliedModal.jsx @@ -78,7 +78,7 @@ const ChangesNotAppliedModal = (props) => { onCloseModal()} - visible={!QCDisabledModalVisible} + open={!QCDisabledModalVisible} footer={( - - ))} - - - - - - {filterTableData - ? - : } - - - - - - - - - - -
- - - - - - + ); }; diff --git a/src/components/data-processing/DoubletScores/CalculationConfig.jsx b/src/components/data-processing/DoubletScores/CalculationConfig.jsx index f17b1ffbae..94c407a4aa 100644 --- a/src/components/data-processing/DoubletScores/CalculationConfig.jsx +++ b/src/components/data-processing/DoubletScores/CalculationConfig.jsx @@ -33,11 +33,15 @@ const DoubletScoresConfig = (props) => { ); }; - +DoubletScoresConfig.defaultProps = { + updateSettings: () => {}, + config: {}, + disabled: false, +}; DoubletScoresConfig.propTypes = { - updateSettings: PropTypes.func.isRequired, - config: PropTypes.object.isRequired, - disabled: PropTypes.bool.isRequired, + updateSettings: PropTypes.func, + config: PropTypes.object, + disabled: PropTypes.bool, }; export default DoubletScoresConfig; diff --git a/src/components/data-processing/DoubletScores/DoubletScores.jsx b/src/components/data-processing/DoubletScores/DoubletScores.jsx index 6a8d5a9088..5d0367e1a9 100644 --- a/src/components/data-processing/DoubletScores/DoubletScores.jsx +++ b/src/components/data-processing/DoubletScores/DoubletScores.jsx @@ -1,79 +1,32 @@ -import React, { useCallback, useEffect, useState } from 'react'; -import { useSelector, useDispatch } from 'react-redux'; -import _ from 'lodash'; +import React from 'react'; import PropTypes from 'prop-types'; -import { - Collapse, Row, Col, Space, Skeleton, Divider, -} from 'antd'; import { generateDataProcessingPlotUuid } from 'utils/generateCustomPlotUuid'; -import { - updatePlotConfig, - loadPlotConfig, - savePlotConfig, -} from 'redux/actions/componentConfig'; -import DoubletScoreHistogram from '../../plots/DoubletScoreHistogram'; - -import PlotStyling from '../../plots/styling/PlotStyling'; -import CalculationConfigContainer from '../CalculationConfigContainer'; +import PlotLayout from 'components/data-processing/PlotLayout'; +import generateSpec from 'utils/plotSpecs/generateDoubletScoreHistogram'; +import BasicFilterPlot +from 'components/plots/BasicFilterPlot'; import CalculationConfig from './CalculationConfig'; -import FilterResultTable from '../FilterResultTable'; - -const { Panel } = Collapse; - -const allowedPlotActions = { - export: true, - compiled: false, - source: false, - editor: false, -}; - -const filterName = 'doubletScores'; -const plotType = 'doubletScoreHistogram'; - -const DoubletScores = (props) => { - const { - experimentId, sampleId, sampleIds, onConfigChange, stepDisabled, stepHadErrors, - } = props; +const DoubletScores = ({ + experimentId, sampleId, sampleIds, onConfigChange, stepDisabled, stepHadErrors, +}) => { + const filterName = 'doubletScores'; + const plotType = 'doubletScoreHistogram'; const plotUuid = generateDataProcessingPlotUuid(sampleId, filterName, 0); const filterTableUuid = generateDataProcessingPlotUuid(sampleId, filterName, 1); - const filterTableData = useSelector((state) => state.componentConfig[filterTableUuid]?.plotData); - - const dispatch = useDispatch(); - - const [renderConfig, setRenderConfig] = useState(null); - - const debounceSave = useCallback( - _.debounce((uuid) => dispatch(savePlotConfig(experimentId, uuid)), 2000), [], - ); - - const config = useSelector((state) => state.componentConfig[plotUuid]?.config); - const expConfig = useSelector( - (state) => state.experimentSettings.processing[filterName][sampleId].filterSettings, - ); - const plotData = useSelector((state) => state.componentConfig[plotUuid]?.plotData); - useEffect(() => { - const newConfig = _.clone(config); - _.merge(newConfig, expConfig); - - setRenderConfig(newConfig); - }, [config, expConfig]); - - useEffect(() => { - if (!filterTableData) dispatch(loadPlotConfig(experimentId, filterTableUuid, 'filterTable')); - }, []); - - useEffect(() => { - if (!config) { - dispatch(loadPlotConfig(experimentId, plotUuid, plotType)); - } - }, []); - - const updatePlotWithChanges = (obj) => { - dispatch(updatePlotConfig(plotUuid, obj)); - debounceSave(plotUuid); + const plots = { + doubletScoreHistogram: { + plotUuid, + plot: (config, plotData, actions) => ( + + ), + plotType, + }, }; const plotStylingControlsConfig = [ @@ -95,73 +48,21 @@ const DoubletScores = (props) => { }, ]; - const renderPlot = () => { - // Spinner for main window - if (!config || !plotData || stepHadErrors) { - return ( -
- -
- ); - } - - if (renderConfig && plotData) { - return ( - - ); - } - }; - + const renderCalculationConfig = () => ; return ( - <> - - - - - {renderPlot()} - - - - - - {filterTableData - ? - : } - - - - - - - - - - - - -
- - - - - - + ); }; diff --git a/src/components/data-processing/GenesVsUMIs/CalculationConfig.jsx b/src/components/data-processing/GenesVsUMIs/CalculationConfig.jsx index 3879765641..09a41e3483 100644 --- a/src/components/data-processing/GenesVsUMIs/CalculationConfig.jsx +++ b/src/components/data-processing/GenesVsUMIs/CalculationConfig.jsx @@ -141,13 +141,20 @@ const GenesVsUMIsConfig = (props) => { ); }; +GenesVsUMIsConfig.defaultProps = { + updateSettings: () => {}, + config: {}, + disabled: false, + rerunRequired: false, + onQCRunClick: () => {}, +}; GenesVsUMIsConfig.propTypes = { - updateSettings: PropTypes.func.isRequired, - config: PropTypes.object.isRequired, - disabled: PropTypes.bool.isRequired, - rerunRequired: PropTypes.bool.isRequired, - onQCRunClick: PropTypes.func.isRequired, + updateSettings: PropTypes.func, + config: PropTypes.object, + disabled: PropTypes.bool, + rerunRequired: PropTypes.bool, + onQCRunClick: PropTypes.func, }; export default GenesVsUMIsConfig; diff --git a/src/components/data-processing/GenesVsUMIs/GenesVsUMIs.jsx b/src/components/data-processing/GenesVsUMIs/GenesVsUMIs.jsx index c5f9b752ff..806977b320 100644 --- a/src/components/data-processing/GenesVsUMIs/GenesVsUMIs.jsx +++ b/src/components/data-processing/GenesVsUMIs/GenesVsUMIs.jsx @@ -1,74 +1,52 @@ -import React, { useEffect, useCallback } from 'react'; -import { useSelector, useDispatch } from 'react-redux'; -import _ from 'lodash'; +import React from 'react'; import PropTypes from 'prop-types'; -import { - Collapse, Row, Col, Space, Skeleton, Divider, -} from 'antd'; import { generateDataProcessingPlotUuid } from 'utils/generateCustomPlotUuid'; -import { - updatePlotConfig, - loadPlotConfig, - savePlotConfig, -} from 'redux/actions/componentConfig'; +import { useSelector } from 'react-redux'; -import FeaturesVsUMIsScatterplot from '../../plots/FeaturesVsUMIsScatterplot'; - -import PlotStyling from '../../plots/styling/PlotStyling'; -import CalculationConfigContainer from '../CalculationConfigContainer'; +import PlotLayout from 'components/data-processing/PlotLayout'; +import BasicFilterPlot from 'components/plots/BasicFilterPlot'; +import generateSpec from 'utils/plotSpecs/generateFeaturesVsUMIsScatterplot'; +import transformOldFeaturesVsUMIsPlotData from 'components/plots/helpers/transformOldFeaturesVsUMIsPlotData'; import CalculationConfig from './CalculationConfig'; -import FilterResultTable from '../FilterResultTable'; - -const { Panel } = Collapse; - -const allowedPlotActions = { - export: true, - compiled: false, - source: true, - editor: false, -}; - -const filterName = 'numGenesVsNumUmis'; -const plotType = 'featuresVsUMIsScatterplot'; - -const GenesVsUMIs = (props) => { - const { - experimentId, sampleId, sampleIds, onConfigChange, stepDisabled, stepHadErrors, onQCRunClick, - } = props; +const GenesVsUMIs = ({ + experimentId, sampleId, sampleIds, onConfigChange, stepDisabled, stepHadErrors, onQCRunClick, +}) => { + const filterName = 'numGenesVsNumUmis'; + const plotType = 'featuresVsUMIsScatterplot'; const plotUuid = generateDataProcessingPlotUuid(sampleId, filterName, 0); const filterTableUuid = generateDataProcessingPlotUuid(sampleId, filterName, 1); - const filterTableData = useSelector((state) => state.componentConfig[filterTableUuid]?.plotData); - - const dispatch = useDispatch(); - - const debounceSave = useCallback( - _.debounce((uuid) => dispatch(savePlotConfig(experimentId, uuid)), 2000), [], - ); - - const config = useSelector( - (state) => state.componentConfig[plotUuid]?.config, - ); const expConfig = useSelector( (state) => state.experimentSettings.processing[filterName][sampleId].filterSettings, ); + const allowedPlotActions = { + export: true, + compiled: false, + source: true, + editor: false, + }; const plotData = useSelector( (state) => state.componentConfig[plotUuid]?.plotData, ); - useEffect(() => { - if (!config) { - dispatch(loadPlotConfig(experimentId, plotUuid, plotType)); - } - }, [config]); - - useEffect(() => { - if (!filterTableData) dispatch(loadPlotConfig(experimentId, filterTableUuid, 'filterTable')); - }, []); - - const updatePlotWithChanges = (obj) => { - dispatch(updatePlotConfig(plotUuid, obj)); - debounceSave(plotUuid); + const plots = { + featuresVsUMIsScatterplot: { + plotUuid, + plot: (config, data, actions) => { + // we can remove this if we migrate old plotData to the new schema + const needTransformPlotData = Array.isArray(data) && data.length; + const newPlotData = needTransformPlotData + ? transformOldFeaturesVsUMIsPlotData(data) + : data; + return ( + + ); + }, + plotType, + }, }; const plotStylingControlsConfig = [ @@ -90,76 +68,29 @@ const GenesVsUMIs = (props) => { }, ]; - const renderPlot = () => { - // Spinner for main window - if (!config || !plotData || stepHadErrors) { - return ( -
- -
- ); - } + const renderCalculationConfig = () => ( + - if (config && plotData) { - return ( - - ); - } - }; + ); return ( - <> - - - - - {renderPlot()} - - - - - - {filterTableData - ? - : } - - - - - - - - - - - - -
- - - - - - + ); }; diff --git a/src/components/data-processing/MitochondrialContent/CalculationConfig.jsx b/src/components/data-processing/MitochondrialContent/CalculationConfig.jsx index e506c39373..b7107b4dab 100644 --- a/src/components/data-processing/MitochondrialContent/CalculationConfig.jsx +++ b/src/components/data-processing/MitochondrialContent/CalculationConfig.jsx @@ -50,12 +50,16 @@ const CalculationConfig = (props) => { ); }; - +CalculationConfig.defaultProps = { + updateSettings: () => {}, + config: {}, + disabled: false, +}; CalculationConfig.propTypes = { - updateSettings: PropTypes.func.isRequired, - config: PropTypes.object.isRequired, + updateSettings: PropTypes.func, + config: PropTypes.object, plotType: PropTypes.string.isRequired, - disabled: PropTypes.bool.isRequired, + disabled: PropTypes.bool, }; export default CalculationConfig; diff --git a/src/components/data-processing/MitochondrialContent/MitochondrialContent.jsx b/src/components/data-processing/MitochondrialContent/MitochondrialContent.jsx index 20ee5ca02c..57c919f820 100644 --- a/src/components/data-processing/MitochondrialContent/MitochondrialContent.jsx +++ b/src/components/data-processing/MitochondrialContent/MitochondrialContent.jsx @@ -1,58 +1,21 @@ -import React, { useState, useEffect, useCallback } from 'react'; -import { useSelector, useDispatch } from 'react-redux'; -import _ from 'lodash'; +import React from 'react'; import PropTypes from 'prop-types'; -import { - Collapse, Row, Col, Space, Skeleton, Divider, -} from 'antd'; import { generateDataProcessingPlotUuid } from 'utils/generateCustomPlotUuid'; -import { - updatePlotConfig, - loadPlotConfig, - savePlotConfig, -} from 'redux/actions/componentConfig'; -import MitochondrialFractionHistogram from '../../plots/MitochondrialFractionHistogram'; -import MitochondrialFractionScatterplot from '../../plots/MitochondrialFractionScatterplot'; - -import PlotStyling from '../../plots/styling/PlotStyling'; -import MiniPlot from '../../plots/MiniPlot'; -import CalculationConfigContainer from '../CalculationConfigContainer'; +import PlotLayout from 'components/data-processing/PlotLayout'; +import BasicFilterPlot from 'components/plots/BasicFilterPlot'; +import generateFractionHistogramSpec from 'utils/plotSpecs/generateMitochondrialFractionHistogram'; +import generateFractionScatterplotSpec from 'utils/plotSpecs/generateMitochondrialFractionScatterplot'; import CalculationConfig from './CalculationConfig'; -import FilterResultTable from '../FilterResultTable'; - -const { Panel } = Collapse; const filterName = 'mitochondrialContent'; -const allowedPlotActions = { - export: true, - compiled: false, - source: false, - editor: false, -}; - const MitochondrialContent = (props) => { const { experimentId, sampleId, sampleIds, onConfigChange, stepDisabled, stepHadErrors, } = props; - - const dispatch = useDispatch(); - - const [selectedPlot, setSelectedPlot] = useState('histogram'); - const [plot, setPlot] = useState(null); const filterTableUuid = generateDataProcessingPlotUuid(sampleId, filterName, 2); - const filterTableData = useSelector((state) => state.componentConfig[filterTableUuid]?.plotData); - - const debounceSave = useCallback( - _.debounce((plotUuid) => dispatch(savePlotConfig(experimentId, plotUuid)), 2000), [], - ); - - const updatePlotWithChanges = (obj) => { - dispatch(updatePlotConfig(plots[selectedPlot].plotUuid, obj)); - debounceSave(plots[selectedPlot].plotUuid); - }; const plots = { histogram: { @@ -60,11 +23,10 @@ const MitochondrialContent = (props) => { plotUuid: generateDataProcessingPlotUuid(sampleId, filterName, 0), plotType: 'mitochondrialFractionHistogram', plot: (config, plotData, actions) => ( - ), }, @@ -73,52 +35,15 @@ const MitochondrialContent = (props) => { plotUuid: generateDataProcessingPlotUuid(sampleId, filterName, 1), plotType: 'mitochondrialFractionLogHistogram', plot: (config, plotData, actions) => ( - ), }, }; - const selectedConfig = useSelector( - (state) => state.componentConfig[plots[selectedPlot].plotUuid]?.config, - ); - - const expConfig = useSelector( - (state) => state.experimentSettings.processing[filterName][sampleId].filterSettings, - ); - - const selectedPlotData = useSelector( - (state) => state.componentConfig[plots[selectedPlot].plotUuid]?.plotData, - ); - - useEffect(() => { - Object.values(plots).forEach((obj) => { - if (!selectedConfig) { - dispatch(loadPlotConfig(experimentId, obj.plotUuid, obj.plotType)); - } - }); - }, []); - - useEffect(() => { - if (!filterTableData) dispatch(loadPlotConfig(experimentId, filterTableUuid, 'filterTable')); - }, []); - - useEffect(() => { - if (selectedConfig && selectedPlotData && expConfig) { - let newConfig = _.clone(selectedConfig); - - const expConfigSettings = expConfig.methodSettings[expConfig.method]; - - newConfig = _.merge(newConfig, expConfigSettings); - - setPlot(plots[selectedPlot].plot(newConfig, selectedPlotData, allowedPlotActions)); - } - }, [expConfig, selectedConfig, selectedPlotData]); - const plotStylingControlsConfig = [ { panelTitle: 'Legend', @@ -142,90 +67,22 @@ const MitochondrialContent = (props) => { }, ]; - const renderPlot = () => { - // Spinner for main window - if (!selectedConfig || !selectedPlotData || stepHadErrors) { - return ( -
- -
- ); - } - if (plot) { - return plot; - } - }; + const renderCalculationConfig = () => ; return ( - <> - - - - - {renderPlot()} - - - - - {Object.entries(plots).map(([key, plotObj]) => ( - - - ))} - - - - - - {filterTableData - ? - : } - - - - - - - - - - -
- - - - - - + ); }; diff --git a/src/components/data-processing/PlotLayout.jsx b/src/components/data-processing/PlotLayout.jsx new file mode 100644 index 0000000000..d69f9cb8b8 --- /dev/null +++ b/src/components/data-processing/PlotLayout.jsx @@ -0,0 +1,209 @@ +import React, { useEffect, useState, useCallback } from 'react'; +import { + Row, Col, Space, Skeleton, Divider, Collapse, +} from 'antd'; +import { useSelector, useDispatch } from 'react-redux'; +import _ from 'lodash'; +import MiniPlot from 'components/plots/MiniPlot'; +import FilterResultTable from 'components/data-processing/FilterResultTable'; +import CalculationConfigContainer from 'components/data-processing/CalculationConfigContainer'; +import PlotStyling from 'components/plots/styling/PlotStyling'; +import PropTypes from 'prop-types'; +import { + updatePlotConfig, + loadPlotConfig, + savePlotConfig, +} from 'redux/actions/componentConfig'; + +const { Panel } = Collapse; + +/* +This component is used to render the main plot and the mini plots for the +classifier, cell size distribution and mitochondrial content filters. +*/ +const PlotLayout = ({ + experimentId, + plots, + filterName, + filterTableUuid, + sampleId, + sampleIds, + onConfigChange, + stepDisabled, + plotStylingControlsConfig, + renderCalculationConfig, + stepHadErrors, + allowedPlotActions, +}) => { + const dispatch = useDispatch(); + const [plot, setPlot] = useState(null); + const [selectedPlot, setSelectedPlot] = useState(Object.keys(plots)[0]); + + const selectedPlotConfig = useSelector( + (state) => state.componentConfig[plots[selectedPlot].plotUuid]?.config, + ); + const filterTableData = useSelector((state) => state.componentConfig[filterTableUuid]?.plotData); + + const filterSettings = useSelector( + (state) => state.experimentSettings.processing[filterName][sampleId].filterSettings, + ); + + const selectedPlotData = useSelector( + (state) => state.componentConfig[plots[selectedPlot].plotUuid]?.plotData, + ); + + const selectedConfig = useSelector( + (state) => state.componentConfig[plots[selectedPlot].plotUuid]?.config, + ); + const debounceSave = useCallback( + _.debounce((plotUuid) => dispatch(savePlotConfig(experimentId, plotUuid)), 2000), [], + ); + + const updatePlotWithChanges = (obj) => { + dispatch(updatePlotConfig(plots[selectedPlot].plotUuid, obj)); + debounceSave(plots[selectedPlot].plotUuid); + }; + + useEffect(() => { + Object.values(plots).forEach((obj) => { + if (!selectedConfig) { + dispatch(loadPlotConfig(experimentId, obj.plotUuid, obj.plotType)); + } + }); + }, []); + + useEffect(() => { + if (!filterTableData) dispatch(loadPlotConfig(experimentId, filterTableUuid, 'filterTable')); + }, []); + + useEffect(() => { + if (selectedConfig && selectedPlotData && filterSettings) { + const newConfig = _.clone(selectedConfig); + // some filters have settings stored under filterSettings.methodSettings[method] + // like the mitochondrial content one + // so we need to check if the current filter is one of them + const expConfigSettings = filterSettings.method ? filterSettings.methodSettings[filterSettings.method] : filterSettings; + _.merge(newConfig, expConfigSettings); + setPlot(plots[selectedPlot].plot(newConfig, selectedPlotData, allowedPlotActions)); + } + }, [filterSettings, selectedConfig, selectedPlotData]); + const renderPlot = () => { + // Spinner for main window + if (!selectedPlotConfig || !selectedPlotData || stepHadErrors) { + return ( +
+ +
+ ); + } + + if (plot) { + return plot; + } + }; + const renderMiniPlots = () => { + if (Object.keys(plots).length > 1) { + return ( + + {Object.entries(plots).map(([key, plotObj]) => ( + + ))} + + ); + } + return null; + }; + return ( + <> + + + + + {renderPlot()} + + + {renderMiniPlots()} + + + + + {filterTableData + ? + : } + + + + + + + {renderCalculationConfig()} + + + +
+ + + + + + + ); +}; +PlotLayout.propTypes = { + experimentId: PropTypes.string.isRequired, + plots: PropTypes.object.isRequired, + filterName: PropTypes.string.isRequired, + sampleId: PropTypes.string.isRequired, + sampleIds: PropTypes.arrayOf(PropTypes.string).isRequired, + onConfigChange: PropTypes.func.isRequired, + stepDisabled: PropTypes.bool, + plotStylingControlsConfig: PropTypes.arrayOf(PropTypes.shape({ + panelTitle: PropTypes.string, + controls: PropTypes.arrayOf(PropTypes.string), + })).isRequired, + renderCalculationConfig: PropTypes.func.isRequired, + stepHadErrors: PropTypes.bool.isRequired, + filterTableUuid: PropTypes.string.isRequired, + allowedPlotActions: PropTypes.object, +}; + +PlotLayout.defaultProps = { + stepDisabled: false, + allowedPlotActions: { + export: true, + compiled: false, + source: false, + editor: true, + }, +}; +export default PlotLayout; diff --git a/src/components/data-processing/StatusIndicator.jsx b/src/components/data-processing/StatusIndicator.jsx index 512cf42618..81443fd40e 100644 --- a/src/components/data-processing/StatusIndicator.jsx +++ b/src/components/data-processing/StatusIndicator.jsx @@ -140,7 +140,7 @@ const StatusIndicator = (props) => { {statusIndicators[status]?.description} - );; + ); }; const renderIndicator = () => { @@ -172,9 +172,10 @@ const StatusIndicator = (props) => { ), key: 'status-indicator', - } - ] - }}> + }, + ], + }} + >