diff --git a/.vscode/launch.json b/.vscode/launch.json index 5d359075a..773c93a62 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,19 +5,19 @@ "version": "0.2.0", "configurations": [ { - "type": "chrome", - "request": "launch", - "name": "Launch Chrome against localhost", - "url": "http://localhost:8080", - "webRoot": "${workspaceFolder}" - }, - { - "name": "Attach to Chrome against localhost", + "name": "Attach to Chrome", "port": 9222, "request": "attach", "type": "pwa-chrome", "urlFilter": "http://localhost:8080/*", "webRoot": "${workspaceFolder}" + }, + { + "type": "chrome", + "request": "launch", + "name": "Launch Chrome against localhost", + "url": "http://localhost:8080", + "webRoot": "${workspaceFolder}" } ] } diff --git a/js/components/common/Modal/index.js b/js/components/common/Modal/index.js index 004fa7d7d..237b00452 100644 --- a/js/components/common/Modal/index.js +++ b/js/components/common/Modal/index.js @@ -24,7 +24,18 @@ const useStyles = makeStyles(theme => ({ })); export const Modal = memo( - ({ children, open, loading, onClose, noPadding, resizable, onResize, otherClasses, ...rest }) => { + ({ + children, + open, + loading, + onClose, + noPadding, + resizable, + onResize, + otherClasses, + otherContentClasses, + ...rest + }) => { const classes = useStyles(); const content = loading ? : children; @@ -48,7 +59,13 @@ export const Modal = memo( { [otherClasses]: !!otherClasses } )} > -
{content}
+
+ {content} +
); diff --git a/js/components/common/Surfaces/Panel/index.js b/js/components/common/Surfaces/Panel/index.js index 2c819e854..1d204e8b3 100644 --- a/js/components/common/Surfaces/Panel/index.js +++ b/js/components/common/Surfaces/Panel/index.js @@ -13,7 +13,8 @@ import ExpandLess from '@material-ui/icons/ExpandLess'; const useStyles = makeStyles(theme => ({ root: { - backgroundColor: theme.palette.background.paper + backgroundColor: theme.palette.background.paper, + height: '100%' }, body: { padding: theme.spacing(1) @@ -113,7 +114,7 @@ export const Panel = memo( {title && ( 2 ? 4 : 6) : 12} + xs={hasExpansion || headerActions ? (headerActions && headerActions.length > 1 ? 4 : 6) : 12} className={classes.headerTitle} > {withTooltip ? ( @@ -135,7 +136,7 @@ export const Panel = memo( container direction="row" justify="flex-end" - xs={title ? (headerActions && headerActions.length > 2 ? 8 : 6) : 12} + xs={title ? (headerActions && headerActions.length > 1 ? 8 : 6) : 12} > {headerActions && headerActions.map((action, index) => ( diff --git a/js/components/datasets/crossReferenceDialog.js b/js/components/datasets/crossReferenceDialog.js index d832ebb5c..78361819d 100644 --- a/js/components/datasets/crossReferenceDialog.js +++ b/js/components/datasets/crossReferenceDialog.js @@ -60,7 +60,8 @@ const useStyles = makeStyles(theme => ({ }, content: { overflowY: 'auto', - height: 214 + height: 214, + width: 'fit-content' }, search: { margin: theme.spacing(1), @@ -317,21 +318,27 @@ export const CrossReferenceDialog = memo(
{moleculeList.length > 0 && - moleculeList.map((data, index, array) => ( - 0 && array[index - 1]} - nextItemData={index < array?.length && array[index + 1]} - removeOfAllSelectedTypes={removeOfAllSelectedTypes} - /> - ))} + moleculeList.map((data, index, array) => { + let molecule = Object.assign({ isCrossReference: true }, data.molecule); + let previousData = index > 0 && Object.assign({ isCrossReference: true }, array[index - 1]); + let nextData = index < array?.length && Object.assign({ isCrossReference: true }, array[index + 1]); + + return ( + + ); + })} {!(moleculeList.length > 0) && ( diff --git a/js/components/datasets/datasetMoleculeView.js b/js/components/datasets/datasetMoleculeView.js index 74365ec00..3f3588eac 100644 --- a/js/components/datasets/datasetMoleculeView.js +++ b/js/components/datasets/datasetMoleculeView.js @@ -37,7 +37,6 @@ import { ArrowDownward, ArrowUpward, MyLocation } from '@material-ui/icons'; import { isNumber, isString } from 'lodash'; import { SvgTooltip } from '../common'; - const useStyles = makeStyles(theme => ({ container: { padding: theme.spacing(1) / 4, @@ -162,6 +161,10 @@ const useStyles = makeStyles(theme => ({ inheritWidth: { width: 'inherit' }, + widthOverflow: { + maxWidth: '180px', + overflow: 'hidden' + }, rank: { fontStyle: 'italic', fontSize: 7 @@ -541,9 +544,9 @@ export const DatasetMoleculeView = memo( onChange={e => { const result = e.target.checked; if (result) { - dispatch(appendMoleculeToCompoundsOfDatasetToBuy(datasetID, currentID)); + dispatch(appendMoleculeToCompoundsOfDatasetToBuy(datasetID, currentID, moleculeTitle)); } else { - dispatch(removeMoleculeFromCompoundsOfDatasetToBuy(datasetID, currentID)); + dispatch(removeMoleculeFromCompoundsOfDatasetToBuy(datasetID, currentID, moleculeTitle)); } }} /> @@ -554,7 +557,13 @@ export const DatasetMoleculeView = memo( {/* Title label */} - +
diff --git a/js/components/datasets/inspirationDialog.js b/js/components/datasets/inspirationDialog.js index 78e61c1c4..007782ab3 100644 --- a/js/components/datasets/inspirationDialog.js +++ b/js/components/datasets/inspirationDialog.js @@ -218,29 +218,38 @@ export const InspirationDialog = memo( surface: removeSurface }; + const selectMoleculeSite = moleculeGroupSite => {}; + const removeOfAllSelectedTypes = () => { proteinList?.forEach(moleculeID => { - const foundedMolecule = moleculeList?.find(mol => mol.id === moleculeID); + let foundedMolecule = moleculeList?.find(mol => mol.id === moleculeID); + foundedMolecule = foundedMolecule && Object.assign({ isInspiration: true }, foundedMolecule); + dispatch(removeHitProtein(stage, foundedMolecule, colourList[foundedMolecule.id % colourList.length])); }); complexList?.forEach(moleculeID => { - const foundedMolecule = moleculeList?.find(mol => mol.id === moleculeID); + let foundedMolecule = moleculeList?.find(mol => mol.id === moleculeID); + foundedMolecule = foundedMolecule && Object.assign({ isInspiration: true }, foundedMolecule); dispatch(removeComplex(stage, foundedMolecule, colourList[foundedMolecule.id % colourList.length])); }); ligandList?.forEach(moleculeID => { - const foundedMolecule = moleculeList?.find(mol => mol.id === moleculeID); + let foundedMolecule = moleculeList?.find(mol => mol.id === moleculeID); + foundedMolecule = foundedMolecule && Object.assign({ isInspiration: true }, foundedMolecule); dispatch(removeLigand(stage, foundedMolecule, colourList[foundedMolecule.id % colourList.length])); }); surfaceList?.forEach(moleculeID => { - const foundedMolecule = moleculeList?.find(mol => mol.id === moleculeID); + let foundedMolecule = moleculeList?.find(mol => mol.id === moleculeID); + foundedMolecule = foundedMolecule && Object.assign({ isInspiration: true }, foundedMolecule); dispatch(removeSurface(stage, foundedMolecule, colourList[foundedMolecule.id % colourList.length])); }); densityList?.forEach(moleculeID => { - const foundedMolecule = moleculeList?.find(mol => mol.id === moleculeID); + let foundedMolecule = moleculeList?.find(mol => mol.id === moleculeID); + foundedMolecule = foundedMolecule && Object.assign({ isInspiration: true }, foundedMolecule); dispatch(removeDensity(stage, foundedMolecule, colourList[foundedMolecule.id % colourList.length])); }); vectorOnList?.forEach(moleculeID => { - const foundedMolecule = moleculeList?.find(mol => mol.id === moleculeID); + let foundedMolecule = moleculeList?.find(mol => mol.id === moleculeID); + foundedMolecule = foundedMolecule && Object.assign({ isInspiration: true }, foundedMolecule); dispatch(removeVector(stage, foundedMolecule, colourList[foundedMolecule.id % colourList.length])); }); }; @@ -386,19 +395,26 @@ export const InspirationDialog = memo(
{moleculeList.length > 0 && - moleculeList.map((molecule, index, array) => ( - 0 && array[index - 1]} - nextItemData={index < array?.length && array[index + 1]} - removeOfAllSelectedTypes={removeOfAllSelectedTypes} - /> - ))} + moleculeList.map((molecule, index, array) => { + let data = Object.assign({ isInspiration: true }, molecule); + let previousData = index > 0 && Object.assign({ isInspiration: true }, array[index - 1]); + let nextData = index < array?.length && Object.assign({ isInspiration: true }, array[index + 1]); + + return ( + + ); + })} {!(moleculeList.length > 0) && ( diff --git a/js/components/datasets/redux/actions.js b/js/components/datasets/redux/actions.js index c8044aacb..7159713be 100644 --- a/js/components/datasets/redux/actions.js +++ b/js/components/datasets/redux/actions.js @@ -278,14 +278,14 @@ export const setFilterWithInspirations = isChecked => ({ payload: isChecked }); -export const appendMoleculeToCompoundsOfDatasetToBuy = (datasetID, moleculeID) => ({ +export const appendMoleculeToCompoundsOfDatasetToBuy = (datasetID, moleculeID, moleculeTitle) => ({ type: constants.APPEND_MOLECULE_TO_COMPOUNDS_TO_BUY_OF_DATASET, - payload: { datasetID, moleculeID } + payload: { datasetID, moleculeID, moleculeTitle } }); -export const removeMoleculeFromCompoundsOfDatasetToBuy = (datasetID, moleculeID) => ({ +export const removeMoleculeFromCompoundsOfDatasetToBuy = (datasetID, moleculeID, moleculeTitle) => ({ type: constants.REMOVE_MOLECULE_FROM_COMPOUNDS_TO_BUY_OF_DATASET, - payload: { datasetID, moleculeID } + payload: { datasetID, moleculeID, moleculeTitle } }); export const reloadDatasetsReducer = savedDatasetsReducers => { diff --git a/js/components/datasets/redux/dispatchActions.js b/js/components/datasets/redux/dispatchActions.js index 166efe7e8..8a5ae26e6 100644 --- a/js/components/datasets/redux/dispatchActions.js +++ b/js/components/datasets/redux/dispatchActions.js @@ -28,7 +28,7 @@ import { } from './actions'; import { base_url } from '../../routes/constants'; import { - generateMoleculeId, + generateMoleculeCompoundId, generateHitProteinObject, generateComplexObject, generateSurfaceObject, @@ -64,7 +64,7 @@ export const addDatasetHitProtein = (stage, data, colourToggle, datasetID) => di const currentOrientation = stage.viewerControls.getOrientation(); dispatch(setOrientation(VIEWS.MAJOR_VIEW, currentOrientation)); }); - dispatch(appendProteinList(datasetID, generateMoleculeId(data))); + dispatch(appendProteinList(datasetID, generateMoleculeCompoundId(data))); }; export const removeDatasetHitProtein = (stage, data, colourToggle, datasetID) => dispatch => { @@ -77,7 +77,7 @@ export const removeDatasetHitProtein = (stage, data, colourToggle, datasetID) => stage ) ); - dispatch(removeFromProteinList(datasetID, generateMoleculeId(data))); + dispatch(removeFromProteinList(datasetID, generateMoleculeCompoundId(data))); }; export const addDatasetComplex = (stage, data, colourToggle, datasetID) => dispatch => { @@ -94,7 +94,7 @@ export const addDatasetComplex = (stage, data, colourToggle, datasetID) => dispa const currentOrientation = stage.viewerControls.getOrientation(); dispatch(setOrientation(VIEWS.MAJOR_VIEW, currentOrientation)); }); - dispatch(appendComplexList(datasetID, generateMoleculeId(data))); + dispatch(appendComplexList(datasetID, generateMoleculeCompoundId(data))); }; export const removeDatasetComplex = (stage, data, colourToggle, datasetID) => dispatch => { @@ -104,7 +104,7 @@ export const removeDatasetComplex = (stage, data, colourToggle, datasetID) => di stage ) ); - dispatch(removeFromComplexList(datasetID, generateMoleculeId(data))); + dispatch(removeFromComplexList(datasetID, generateMoleculeCompoundId(data))); }; export const addDatasetSurface = (stage, data, colourToggle, datasetID) => dispatch => { @@ -121,7 +121,7 @@ export const addDatasetSurface = (stage, data, colourToggle, datasetID) => dispa const currentOrientation = stage.viewerControls.getOrientation(); dispatch(setOrientation(VIEWS.MAJOR_VIEW, currentOrientation)); }); - dispatch(appendSurfaceList(datasetID, generateMoleculeId(data))); + dispatch(appendSurfaceList(datasetID, generateMoleculeCompoundId(data))); }; export const removeDatasetSurface = (stage, data, colourToggle, datasetID) => dispatch => { @@ -131,7 +131,7 @@ export const removeDatasetSurface = (stage, data, colourToggle, datasetID) => di stage ) ); - dispatch(removeFromSurfaceList(datasetID, generateMoleculeId(data))); + dispatch(removeFromSurfaceList(datasetID, generateMoleculeCompoundId(data))); }; export const addDatasetLigand = (stage, data, colourToggle, datasetID) => dispatch => { @@ -151,7 +151,7 @@ export const addDatasetLigand = (stage, data, colourToggle, datasetID) => dispat // keep current orientation of NGL View stage.viewerControls.orient(currentOrientation); }); - dispatch(appendLigandList(datasetID, generateMoleculeId(data))); + dispatch(appendLigandList(datasetID, generateMoleculeCompoundId(data))); }; export const removeDatasetLigand = (stage, data, colourToggle, datasetID) => dispatch => { @@ -161,7 +161,7 @@ export const removeDatasetLigand = (stage, data, colourToggle, datasetID) => dis stage ) ); - dispatch(removeFromLigandList(datasetID, generateMoleculeId(data))); + dispatch(removeFromLigandList(datasetID, generateMoleculeCompoundId(data))); }; export const loadDataSets = targetId => dispatch => diff --git a/js/components/datasets/redux/selectors.js b/js/components/datasets/redux/selectors.js index 0b33faa5a..3ceaf3a7e 100644 --- a/js/components/datasets/redux/selectors.js +++ b/js/components/datasets/redux/selectors.js @@ -254,20 +254,24 @@ export const getFilteredDatasetMoleculeList = createSelector( for (let prioAttr of sortedAttributes) { const order = filterProperties[prioAttr].order; - const scoreValueOfA = Object.keys(a.numerical_scores).find(key => key === prioAttr) && a.numerical_scores[prioAttr]; - scoreValueOfA = scoreValueOfA || (Object.keys(a.text_scores).find(key => key === prioAttr) && a.text_scores[prioAttr]); - const scoreValueOfB = Object.keys(b.numerical_scores).find(key => key === prioAttr) && b.numerical_scores[prioAttr]; - scoreValueOfB = scoreValueOfB || (Object.keys(b.text_scores).find(key => key === prioAttr) && b.text_scores[prioAttr]); - - if (scoreValueOfA === "Y") { + const scoreValueOfA = + Object.keys(a.numerical_scores).find(key => key === prioAttr) && a.numerical_scores[prioAttr]; + scoreValueOfA = + scoreValueOfA || (Object.keys(a.text_scores).find(key => key === prioAttr) && a.text_scores[prioAttr]); + const scoreValueOfB = + Object.keys(b.numerical_scores).find(key => key === prioAttr) && b.numerical_scores[prioAttr]; + scoreValueOfB = + scoreValueOfB || (Object.keys(b.text_scores).find(key => key === prioAttr) && b.text_scores[prioAttr]); + + if (scoreValueOfA === 'Y') { scoreValueOfA = 1; - } else if (scoreValueOfA === "N") { + } else if (scoreValueOfA === 'N') { scoreValueOfA = 0; } - if (scoreValueOfB === "Y") { + if (scoreValueOfB === 'Y') { scoreValueOfB = 1; - } else if (scoreValueOfB === "N") { + } else if (scoreValueOfB === 'N') { scoreValueOfB = 0; } @@ -314,7 +318,10 @@ export const getCrossReferenceCompoundListByCompoundName = createSelector( Object.keys(moleculesDatasetMap).forEach(datasetID => { const currentList = moleculesDatasetMap[datasetID]; if (currentList && Array.isArray(currentList)) { - results.push({ molecule: currentList.find(item => item.name === compoundName), datasetID }); + let molecule = currentList.find(item => item.name === compoundName); + if (molecule) { + results.push({ molecule, datasetID }); + } } }); return results; diff --git a/js/components/datasets/selectedCompoundsList.js b/js/components/datasets/selectedCompoundsList.js index ee6979dad..bcc764b97 100644 --- a/js/components/datasets/selectedCompoundsList.js +++ b/js/components/datasets/selectedCompoundsList.js @@ -1,6 +1,7 @@ import React, { memo, useContext, useEffect, useRef, useState } from 'react'; import { Panel } from '../common/Surfaces/Panel'; -import { CircularProgress, Grid, makeStyles, Typography } from '@material-ui/core'; +import { CircularProgress, Grid, makeStyles, Typography, Button } from '@material-ui/core'; +import { CloudDownload } from '@material-ui/icons'; import { useDispatch, useSelector } from 'react-redux'; import { getMoleculesObjectIDListOfCompoundsToBuy } from './redux/selectors'; import InfiniteScroll from 'react-infinite-scroller'; @@ -19,6 +20,8 @@ import { import MoleculeView from '../preview/molecule/moleculeView'; import { NglContext } from '../nglView/nglProvider'; import { VIEWS } from '../../constants/constants'; +import FileSaver from 'file-saver'; +import JSZip from 'jszip'; const useStyles = makeStyles(theme => ({ container: { @@ -35,6 +38,9 @@ const useStyles = makeStyles(theme => ({ }, notFound: { paddingTop: theme.spacing(2) + }, + sdfButton: { + marginRight: theme.spacing(1) } })); @@ -46,8 +52,7 @@ export const SelectedCompoundList = memo(({ height }) => { const moleculesPerPage = 5; const dispatch = useDispatch(); const [currentPage, setCurrentPage] = useState(0); - const moleculesObjectIDListOfCompoundsToBuy = useSelector(getMoleculesObjectIDListOfCompoundsToBuy); - const isOpenInspirationDialog = useSelector(state => state.datasetsReducers.isOpenInspirationDialog); + const moleculesObjectIDListOfCompoundsToBuy = useSelector(getMoleculesObjectIDListOfCompoundsToBuy); const isOpenInspirationDialog = useSelector(state => state.datasetsReducers.isOpenInspirationDialog); const isOpenCrossReferenceDialog = useSelector(state => state.datasetsReducers.isOpenCrossReferenceDialog); const [selectedMoleculeRef, setSelectedMoleculeRef] = useState(null); const inspirationDialogRef = useRef(); @@ -132,8 +137,56 @@ export const SelectedCompoundList = memo(({ height }) => { }; }, [dispatch]); + const downloadAsCsv = () => { + let data = 'smiles,dataset'; + moleculesObjectIDListOfCompoundsToBuy.forEach(compound => { + data += `\n${compound.molecule.smiles},${compound.datasetID}`; + }); + const dataBlob = new Blob([data], {type: 'text/csv;charset=utf-8'}); + + FileSaver.saveAs(dataBlob, 'selectedCompounds.csv'); + }; + + const downloadAsSdf = async () => { + const zip = new JSZip(); + const folders = {}; + + moleculesObjectIDListOfCompoundsToBuy.forEach(compound => { + const datasetID = compound.datasetID; + let folder = folders[datasetID]; + if (!folder) { + folder = zip.folder(datasetID); + folders[datasetID] = folder; + } + + const { name, sdf_info } = compound.molecule; + folder.file(`${name}.sdf`, sdf_info); + }); + + const zipBlob = await zip.generateAsync({ type: 'blob' }); + FileSaver.saveAs(zipBlob, 'selectedCompounds.zip'); + }; + return ( - + } + > + Download CSV + , + + ]}> {isOpenInspirationDialog && ( { currentMolecule.V = true; break; default: - currentMolecule = { name: part, L: true, P: false, C: false, S: false, V: false }; - molecules.push(currentMolecule); + if (part.toLowerCase() === URL_TOKENS.exact) { + currentMolecule.exact = true; + } else { + currentMolecule = { name: part, L: true, P: false, C: false, S: false, V: false, exact: false }; + molecules.push(currentMolecule); + } break; } } else { - currentMolecule = { name: part, L: true, P: false, C: false, S: false, V: false }; + currentMolecule = { name: part, L: true, P: false, C: false, S: false, V: false, exact: false }; molecules.push(currentMolecule); } } else { diff --git a/js/components/header/index.js b/js/components/header/index.js index 1b4af97b5..30ea8e58b 100644 --- a/js/components/header/index.js +++ b/js/components/header/index.js @@ -26,7 +26,8 @@ import { SupervisorAccount, Menu as MenuIcon, Work, - Description + Description, + Timeline } from '@material-ui/icons'; import { HeaderContext } from './headerContext'; import { Button } from '../common'; @@ -39,6 +40,7 @@ import { useHistory } from 'react-router-dom'; import { IssueReport } from '../userFeedback/issueReport'; import { IdeaReport } from '../userFeedback/ideaReport'; import { FundersModal } from '../funders/fundersModal'; +import { TrackingModal } from '../tracking/trackingModal'; // eslint-disable-next-line import/extensions import { version } from '../../../package.json'; @@ -94,6 +96,7 @@ export default memo( const [openMenu, setOpenMenu] = useState(false); const [openFunders, setOpenFunders] = useState(false); + const [openTrackingModal, setOpenTrackingModal] = useState(false); const openXchem = () => { // window.location.href = 'https://www.diamond.ac.uk/Instruments/Mx/Fragment-Screening.html'; @@ -211,6 +214,16 @@ export default memo( + + + @@ -275,6 +288,7 @@ export default memo( )} setOpenFunders(false)} /> + setOpenTrackingModal(false)} /> { }; export const generateMoleculeId = data => ({ - id: data.id + id: data.id, + name: data.protein_code, + isInspiration: data.isInspiration +}); + +export const generateMoleculeCompoundId = data => ({ + id: data.id, + name: data.name, + isCrossReference: data.isCrossReference }); export const getVectorWithColorByCountOfCompounds = (item, currentVectorCompounds) => { diff --git a/js/components/preview/molecule/moleculeView.js b/js/components/preview/molecule/moleculeView.js index 891a1818c..e6e3391aa 100644 --- a/js/components/preview/molecule/moleculeView.js +++ b/js/components/preview/molecule/moleculeView.js @@ -32,7 +32,6 @@ import { moleculeProperty } from './helperConstants'; import { centerOnLigandByMoleculeID } from '../../../reducers/ngl/dispatchActions'; import { SvgTooltip } from '../../common'; - const useStyles = makeStyles(theme => ({ container: { padding: theme.spacing(1) / 4, @@ -346,7 +345,9 @@ const MoleculeView = memo( isLigandOn || isProteinOn || isComplexOn || isSurfaceOn || isVectorOn ? selected_style : not_selected_style; const addNewLigand = () => { - selectMoleculeSite(data.site); + if (selectMoleculeSite) { + selectMoleculeSite(data.site); + } dispatch(addLigand(stage, data, colourToggle)); }; @@ -377,7 +378,9 @@ const MoleculeView = memo( }; const addNewProtein = () => { - selectMoleculeSite(data.site); + if (selectMoleculeSite) { + selectMoleculeSite(data.site); + } dispatch(addHitProtein(stage, data, colourToggle)); }; @@ -403,7 +406,9 @@ const MoleculeView = memo( }; const addNewComplex = () => { - selectMoleculeSite(data.site); + if (selectMoleculeSite) { + selectMoleculeSite(data.site); + } dispatch(addComplex(stage, data, colourToggle)); }; @@ -428,7 +433,9 @@ const MoleculeView = memo( }; const addNewSurface = () => { - selectMoleculeSite(data.site); + if (selectMoleculeSite) { + selectMoleculeSite(data.site); + } dispatch(addSurface(stage, data, colourToggle)); }; @@ -445,7 +452,9 @@ const MoleculeView = memo( }; const addNewDensity = () => { - selectMoleculeSite(data.site); + if (selectMoleculeSite) { + selectMoleculeSite(data.site); + } dispatch(addDensity(stage, data, colourToggle)); }; @@ -462,7 +471,9 @@ const MoleculeView = memo( }; const addNewVector = () => { - selectMoleculeSite(data.site); + if (selectMoleculeSite) { + selectMoleculeSite(data.site); + } dispatch(addVector(stage, data)).catch(error => { throw new Error(error); }); @@ -537,7 +548,7 @@ const MoleculeView = memo( moveSelectedMolSettings(previousItemData); }; - let moleculeTitle = data?.protein_code.replace(`${target_on_name}-`, ''); + let moleculeTitle = data?.protein_code.replace(new RegExp(`${target_on_name}-`, 'i'), ''); return ( <> diff --git a/js/components/preview/molecule/redux/dispatchActions.js b/js/components/preview/molecule/redux/dispatchActions.js index 9ace772fd..078eeb080 100644 --- a/js/components/preview/molecule/redux/dispatchActions.js +++ b/js/components/preview/molecule/redux/dispatchActions.js @@ -42,8 +42,8 @@ import { setCompoundImage } from '../../summary/redux/actions'; import { noCompoundImage } from '../../summary/redux/reducer'; import { getMoleculeOfCurrentVector } from '../../../../reducers/selection/selectors'; import { resetCurrentCompoundsSettings } from '../../compounds/redux/actions'; -import {selectMoleculeGroup} from '../../moleculeGroups/redux/dispatchActions'; -import {setDirectAccess, setDirectAccessProcessed} from '../../../../reducers/api/actions'; +import { selectMoleculeGroup } from '../../moleculeGroups/redux/dispatchActions'; +import { setDirectAccess, setDirectAccessProcessed } from '../../../../reducers/api/actions'; /** * Convert the JSON into a list of arrow objects @@ -390,7 +390,6 @@ export const applyDirectSelection = (stage, stageSummaryView) => (dispatch, getS const state = getState(); const directDisplay = state.apiReducers.direct_access; - const mol_group_selection = state.selectionReducers.mol_group_selection; const fragmentDisplayList = state.selectionReducers.fragmentDisplayList; const proteinList = state.selectionReducers.proteinList; const complexList = state.selectionReducers.complexList; @@ -404,14 +403,19 @@ export const applyDirectSelection = (stage, stageSummaryView) => (dispatch, getS //const molGroupMap = getMolGroupNameToId(); directDisplay.molecules.forEach(m => { let keys = Object.keys(allMols); + let directProteinNameModded = m.name.toLowerCase(); + let directProteinCodeModded = `${directDisplay.target.toLowerCase()}-${directProteinNameModded}`; for (let groupIndex = 0; groupIndex < keys.length; groupIndex++) { let groupId = keys[groupIndex]; let molList = allMols[groupId]; let molCount = molList.length; for (let molIndex = 0; molIndex < molCount; molIndex++) { let mol = molList[molIndex]; - if (mol.protein_code.includes(m.name) || mol.protein_code.includes(m.name.toLowerCase())) { + let proteinCodeModded = mol.protein_code.toLowerCase(); + if (m.exact ? proteinCodeModded === directProteinCodeModded : proteinCodeModded.includes(directProteinNameModded)) { let molGroupId = groupId; + // Has to be declared here because otherwise we read stale value + const mol_group_selection = getState().selectionReducers.mol_group_selection; if (!mol_group_selection.includes(parseInt(molGroupId))) { let molGroup = mol_group_list.find(g => g.id === parseInt(molGroupId)); dispatch(selectMoleculeGroup(molGroup, stageSummaryView)); @@ -438,5 +442,4 @@ export const applyDirectSelection = (stage, stageSummaryView) => (dispatch, getS // dispatch(setDirectAccess({})); dispatch(setDirectAccessProcessed(true)); } - }; diff --git a/js/components/preview/viewerControls/displayControls/index.js b/js/components/preview/viewerControls/displayControls/index.js index e33f11d8b..ef9653586 100644 --- a/js/components/preview/viewerControls/displayControls/index.js +++ b/js/components/preview/viewerControls/displayControls/index.js @@ -107,7 +107,7 @@ export default memo(({ open, onClose }) => { // remove from nglReducer and selectionReducer dispatch(deleteObject(targetObject, nglView.stage, true)); } else { - dispatch(removeComponentRepresentation(parentKey, representation.uuid)); + dispatch(removeComponentRepresentation(parentKey, representation)); } } }; @@ -118,7 +118,7 @@ export default memo(({ open, onClose }) => { const targetObject = objectsInView[parentKey]; const nglView = getNglView(objectsInView[parentKey].display_div); const comp = nglView.stage.getComponentsByName(parentKey).first; - comp.eachRepresentation(representation => dispatch(removeComponentRepresentation(parentKey, representation.uuid))); + comp.eachRepresentation(representation => dispatch(removeComponentRepresentation(parentKey, representation))); // remove from nglReducer and selectionReducer dispatch(deleteObject(targetObject, nglView.stage, true)); diff --git a/js/components/tracking/trackingModal.js b/js/components/tracking/trackingModal.js new file mode 100644 index 000000000..24d78e6fb --- /dev/null +++ b/js/components/tracking/trackingModal.js @@ -0,0 +1,83 @@ +import React, { memo } from 'react'; +import { useSelector } from 'react-redux'; +import Modal from '../common/Modal'; +import { Grid, makeStyles } from '@material-ui/core'; +import { Timeline, TimelineEvent } from 'react-event-timeline'; +import { Check, Clear } from '@material-ui/icons'; +import palette from '../../theme/palette'; +import { Panel } from '../common'; + +const useStyles = makeStyles(theme => ({ + customModal: { + width: '70%', + height: '90%' + }, + customContentModal: { + height: '100%' + }, + timelineEvent: { + borderBottom: '1px dashed ' + palette.divider, + paddingBottom: '10px' + }, + divContainer: { + height: '100%', + width: '100%', + paddingTop: theme.spacing(1) / 2 + }, + divScrollable: { + height: '100%', + width: '100%', + overflow: 'auto' + }, + containerExpanded: { height: '100%' } +})); + +export const TrackingModal = memo(({ openModal, onModalClose }) => { + const classes = useStyles(); + const actionList = useSelector(state => state.trackingReducers.truck_actions_list); + const orderedActionList = actionList.sort((a, b) => a.timestamp - b.timestamp); + + if (openModal === undefined) { + console.log('undefined openModal'); + onModalClose(); + } + + return ( + onModalClose()} + > + + +
+
+ + {orderedActionList && + orderedActionList.map((data, index) => ( + + ) : ( + + ) + } + iconColor={palette.primary.main} + className={classes.timelineEvent} + > + ))} + +
+
+
+
+
+ ); +}); diff --git a/js/index.js b/js/index.js index a9250104e..fb8ddd49e 100644 --- a/js/index.js +++ b/js/index.js @@ -11,7 +11,9 @@ import { applyMiddleware, createStore } from 'redux'; import { rootReducer } from './reducers/rootReducer'; import { saveStore } from './components/helpers/globalStore'; import thunkMiddleware from 'redux-thunk'; +import trackingMiddleware from './reducers/tracking/trackingMiddleware'; import { composeWithDevTools } from 'redux-devtools-extension'; + require('react-hot-loader/patch'); if (process.env.NODE_ENV === 'production') { @@ -37,7 +39,8 @@ if (process.env.NODE_ENV === 'production') { const middlewareEnhancer = applyMiddleware( //loggerMiddleware, - thunkMiddleware + thunkMiddleware, + trackingMiddleware ); const enhancers = [middlewareEnhancer]; const composedEnhancers = composeWithDevTools(...enhancers); diff --git a/js/reducers/api/selectors.js b/js/reducers/api/selectors.js index 409289146..3bca464a3 100644 --- a/js/reducers/api/selectors.js +++ b/js/reducers/api/selectors.js @@ -1 +1,2 @@ export const getMoleculeList = state => state.apiReducers.molecule_list; +export const getAllMoleculeList = state => state.apiReducers.all_mol_lists; diff --git a/js/reducers/ngl/actions.js b/js/reducers/ngl/actions.js index 3ab83d5d5..000f43a5c 100644 --- a/js/reducers/ngl/actions.js +++ b/js/reducers/ngl/actions.js @@ -23,9 +23,9 @@ export const addComponentRepresentation = (objectInViewID, newRepresentation) => objectInViewID }); -export const removeComponentRepresentation = (objectInViewID, representationID) => ({ +export const removeComponentRepresentation = (objectInViewID, representation) => ({ type: CONSTANTS.REMOVE_COMPONENT_REPRESENTATION, - representationID, + representation, objectInViewID }); diff --git a/js/reducers/ngl/nglReducers.js b/js/reducers/ngl/nglReducers.js index 009f1981d..a187a370b 100644 --- a/js/reducers/ngl/nglReducers.js +++ b/js/reducers/ngl/nglReducers.js @@ -80,12 +80,12 @@ export default function nglReducers(state = INITIAL_STATE, action = {}) { }); case CONSTANTS.REMOVE_COMPONENT_REPRESENTATION: + const representationID = action.representation && action.representation.uuid; const newObjInViewWithRemovedRepresentation = JSON.parse(JSON.stringify(state.objectsInView)); if (newObjInViewWithRemovedRepresentation[action.objectInViewID].representations) { for (let i = 0; i < newObjInViewWithRemovedRepresentation[action.objectInViewID].representations.length; i++) { if ( - newObjInViewWithRemovedRepresentation[action.objectInViewID].representations[i].uuid === - action.representationID + newObjInViewWithRemovedRepresentation[action.objectInViewID].representations[i].uuid === representationID ) { newObjInViewWithRemovedRepresentation[action.objectInViewID].representations.splice(i, 1); break; diff --git a/js/reducers/rootReducer.js b/js/reducers/rootReducer.js index 92c06c584..17933c666 100644 --- a/js/reducers/rootReducer.js +++ b/js/reducers/rootReducer.js @@ -11,6 +11,7 @@ import { previewReducers } from '../components/preview/redux'; import { projectReducers } from '../components/projects/redux/reducer'; import { issueReducers } from '../components/userFeedback/redux/reducer'; import { datasetsReducers } from '../components/datasets/redux/reducer'; +import trackingReducers from './tracking/trackingReducers'; const rootReducer = combineReducers({ apiReducers, @@ -21,7 +22,8 @@ const rootReducer = combineReducers({ previewReducers, projectReducers, issueReducers, - datasetsReducers + datasetsReducers, + trackingReducers }); export { rootReducer }; diff --git a/js/reducers/selection/selectors.js b/js/reducers/selection/selectors.js index f54f9b288..8aa98b9d5 100644 --- a/js/reducers/selection/selectors.js +++ b/js/reducers/selection/selectors.js @@ -1,5 +1,5 @@ import { createSelector } from 'reselect'; -import { getMoleculeList } from '../api/selectors'; +import { getAllMoleculeList } from '../api/selectors'; const getCurrentCompoundClass = state => state.previewReducers.compounds.currentCompoundClass; const getVectorList = state => state.selectionReducers.vector_list; @@ -10,14 +10,16 @@ const getCompoundsOfVectors = state => state.selectionReducers.compoundsOfVector export const getMoleculeOfCurrentVector = createSelector( getCurrentVector, getVectorList, - getMoleculeList, + getAllMoleculeList, (selectedVectorSmile, vectorList, moleculeList) => { if (selectedVectorSmile !== null && vectorList && moleculeList) { const foundedVector = vectorList.find(vector => vector.name.includes(selectedVectorSmile)); if (foundedVector) { - for (const molecule in moleculeList) { - if (moleculeList.hasOwnProperty(molecule)) { - if (molecule.id === foundedVector.moleculeId) { + for (const moleculeProperty in moleculeList) { + if (moleculeList.hasOwnProperty(moleculeProperty)) { + let molecules = moleculeList[moleculeProperty]; + let molecule = molecules.find(m => m.id === foundedVector.moleculeId); + if (molecule) { return molecule; } } diff --git a/js/reducers/tracking/actions.js b/js/reducers/tracking/actions.js new file mode 100644 index 000000000..decfb1e3a --- /dev/null +++ b/js/reducers/tracking/actions.js @@ -0,0 +1,22 @@ +import { constants } from './constants'; + +export const setActionsList = function(truck_actions_list) { + return { + type: constants.SET_ACTIONS_LIST, + truck_actions_list: truck_actions_list + }; +}; + +export const appendToActionList = function(truck_action) { + return { + type: constants.APPEND_ACTIONS_LIST, + truck_action: truck_action + }; +}; + +export const setCurrentActionsList = function(current_actions_list) { + return { + type: constants.SET_CURRENT_ACTIONS_LIST, + current_actions_list: current_actions_list + }; +}; diff --git a/js/reducers/tracking/constants.js b/js/reducers/tracking/constants.js new file mode 100644 index 000000000..18b77abef --- /dev/null +++ b/js/reducers/tracking/constants.js @@ -0,0 +1,62 @@ +const prefix = 'REDUCERS_TRACKING_'; + +export const constants = { + SET_ACTIONS_LIST: prefix + 'SET_ACTIONS_LIST', + APPEND_ACTIONS_LIST: prefix + 'APPEND_ACTIONS_LIST', + SET_CURRENT_ACTIONS_LIST: prefix + 'SET_CURRENT_ACTIONS_LIST' +}; + +export const actionType = { + TARGET_LOADED: 'TARGET_LOADED', + SITE_TURNED_ON: 'SITE_TURNED_ON', + SITE_TURNED_OFF: 'SITE_TURNED_OFF', + LIGAND_TURNED_ON: 'LIGAND_TURNED_ON', + LIGAND_TURNED_OFF: 'LIGAND_TURNED_OFF', + SIDECHAINS_TURNED_ON: 'SIDECHAINS_TURNED_ON', + SIDECHAINS_TURNED_OFF: 'SIDECHAINS_TURNED_OFF', + INTERACTIONS_TURNED_ON: 'INTERACTIONS_TURNED_ON', + INTERACTIONS_TURNED_OFF: 'INTERACTIONS_TURNED_OFF', + SURFACE_TURNED_ON: 'SURFACE_TURNED_ON', + SURFACE_TURNED_OFF: 'SURFACE_TURNED_OFF', + VECTORS_TURNED_ON: 'VECTORS_TURNED_ON', + VECTORS_TURNED_OFF: 'VECTORS_TURNED_OFF', + VECTOR_SELECTED: 'VECTOR_SELECTED', + VECTOR_DESELECTED: 'VECTOR_DESELECTED', + MOLECULE_ADDED_TO_SHOPPING_CART: 'MOLECULE_ADDED_TO_SHOPPING_CART', + MOLECULE_REMOVED_FROM_SHOPPING_CART: 'MOLECULE_REMOVED_FROM_SHOPPING_CART', + COMPOUND_SELECTED: 'COMPOUND_SELECTED', + COMPOUND_DESELECTED: 'COMPOUND_DESELECTED', + REPRESENTATION_CHANGED: 'REPRESENTATION_CHANGED', + REPRESENTATION_ADDED: 'REPRESENTATION_ADDED', + REPRESENTATION_REMOVED: 'REPRESENTATION_REMOVED' +}; + +export const actionDescription = { + LOADED: 'was loaded', + TURNED_ON: 'was turned on', + TURNED_OFF: 'was turned off', + SELECTED: 'was selected', + DESELECTED: 'was deselected', + ADDED: 'was added', + REMOVED: 'was removed', + CHANGED: 'was changed', + TO_SHOPPING_CART: 'to shopping cart', + FROM_SHOPPING_CART: 'from shopping cart', + LIGAND: 'Ligand', + SIDECHAINS: 'Sidechain', + INTERACTIONS: 'Interaction', + VECTOR: 'Vector', + SURFACE: 'Surface', + SITE: 'Site', + TARGET: 'Target' +}; + +export const actionObjectType = { + TARGET: 'TARGET', + SITE: 'SITE', + MOLECULE: 'MOLECULE', + COMPOUND: 'COMPOUND', + INSPIRATION: 'INSPIRATION', + CROSS_REFERENCE: 'CROSS_REFERENCE', + REPRESENTATION: 'REPRESENTATION' +}; diff --git a/js/reducers/tracking/dispatchActions.js b/js/reducers/tracking/dispatchActions.js new file mode 100644 index 000000000..ae62942f7 --- /dev/null +++ b/js/reducers/tracking/dispatchActions.js @@ -0,0 +1,185 @@ +import { setCurrentActionsList } from './actions'; +import { actionType } from './constants'; +import { VIEWS } from '../../../js/constants/constants'; + +export const selectCurrentActionsList = () => (dispatch, getState) => { + const state = getState(); + + const actionList = state.trackingReducers.truck_actions_list; + const currentTargetOn = state.apiReducers.target_on; + const currentSites = state.selectionReducers.mol_group_selection; + const currentLigands = state.selectionReducers.fragmentDisplayList; + const currentProteins = state.selectionReducers.proteinList; + const currentComplexes = state.selectionReducers.complexLists; + const currentSurfaces = state.selectionReducers.surfaceList; + const currentVectors = state.selectionReducers.vectorOnList; + const currentBuyList = state.selectionReducers.to_buy_list; + const currentVector = state.selectionReducers.currentVector; + + const currentDatasetLigands = state.datasetsReducers.ligandLists; + const currentDatasetProteins = state.datasetsReducers.proteinList; + const currentDatasetComplexes = state.datasetsReducers.complexLists; + const currentDatasetSurfaces = state.datasetsReducers.surfaceLists; + + const currentDatasetBuyList = state.datasetsReducers.compoundsToBuyDatasetMap; + const currentobjectsInView = state.nglReducers.objectsInView; + + const orderedActionList = actionList.reverse((a, b) => a.timestamp - b.timestamp); + const currentTargets = (currentTargetOn && [currentTargetOn]) || []; + const currentVectorSmiles = (currentVector && [currentVector]) || []; + + let currentActions = []; + + getCurrentActionList(orderedActionList, actionType.TARGET_LOADED, getCollection(currentTargets), currentActions); + getCurrentActionList(orderedActionList, actionType.SITE_TURNED_ON, getCollection(currentSites), currentActions); + getCurrentActionList(orderedActionList, actionType.LIGAND_TURNED_ON, getCollection(currentLigands), currentActions); + getCurrentActionList( + orderedActionList, + actionType.SIDECHAINS_TURNED_ON, + getCollection(currentProteins), + currentActions + ); + getCurrentActionList( + orderedActionList, + actionType.INTERACTIONS_TURNED_ON, + getCollection(currentComplexes), + currentActions + ); + getCurrentActionList(orderedActionList, actionType.SURFACE_TURNED_ON, getCollection(currentSurfaces), currentActions); + getCurrentActionList(orderedActionList, actionType.VECTORS_TURNED_ON, getCollection(currentVectors), currentActions); + getCurrentActionList( + orderedActionList, + actionType.VECTOR_SELECTED, + getCollection(currentVectorSmiles), + currentActions + ); + + getCurrentActionList( + orderedActionList, + actionType.MOLECULE_ADDED_TO_SHOPPING_CART, + getCollectionOfShoppingCart(currentBuyList), + currentActions + ); + + getCurrentActionList( + orderedActionList, + actionType.LIGAND_TURNED_ON, + getCollectionOfDataset(currentDatasetLigands), + currentActions + ); + getCurrentActionList( + orderedActionList, + actionType.SIDECHAINS_TURNED_ON, + getCollectionOfDataset(currentDatasetProteins), + currentActions + ); + getCurrentActionList( + orderedActionList, + actionType.INTERACTIONS_TURNED_ON, + getCollectionOfDataset(currentDatasetComplexes), + + currentActions + ); + getCurrentActionList( + orderedActionList, + actionType.SURFACE_TURNED_ON, + getCollectionOfDataset(currentDatasetSurfaces), + currentActions + ); + + getCurrentActionList( + orderedActionList, + actionType.COMPOUND_SELECTED, + getCollectionOfDataset(currentDatasetBuyList), + currentActions + ); + + getCurrentActionList( + orderedActionList, + actionType.REPRESENTATION_CHANGED, + getCollectionOfDatasetOfRepresentation(currentobjectsInView), + currentActions + ); + + dispatch(setCurrentActionsList(currentActions)); +}; + +const getCurrentActionList = (orderedActionList, type, collection, currentActions) => { + let actionList = + type !== actionType.REPRESENTATION_CHANGED + ? orderedActionList.filter(action => action.type === type) + : orderedActionList.filter( + action => + action.type === actionType.REPRESENTATION_ADDED || + action.type === actionType.REPRESENTATION_REMOVED || + action.type === actionType.REPRESENTATION_CHANGED + ); + if (collection) { + collection.forEach(data => { + let action = actionList.find(action => action.object_id === data.id && action.dataset_id === data.datasetId); + + if (action) { + currentActions.push(Object.assign(mapCurrentAction(action))); + } + }); + } +}; + +const mapCurrentAction = action => { + return Object.assign({ + timestamp: action.timestamp, + object_name: action.object_name, + object_type: action.object_type, + action_type: action.type + }); +}; + +const getCollection = dataList => { + let list = []; + if (dataList) { + var result = dataList.map(value => ({ id: value })); + list.push(...result); + } + return list; +}; + +const getCollectionOfDataset = dataList => { + let list = []; + if (dataList) { + for (const datasetId in dataList) { + let values = dataList[datasetId]; + if (values) { + var result = values.map(value => ({ id: value, datasetId: datasetId })); + list.push(...result); + } + } + } + return list; +}; + +const getCollectionOfShoppingCart = dataList => { + let list = []; + if (dataList) { + dataList.forEach(data => { + let value = data.vector; + if (value) { + list.push({ id: value }); + } + }); + } + return list; +}; + +const getCollectionOfDatasetOfRepresentation = dataList => { + let list = []; + for (const view in dataList) { + let objectView = dataList[view]; + if (objectView && objectView !== null && objectView.display_div === VIEWS.MAJOR_VIEW) { + let value = dataList[view].name; + if (value) { + list.push({ id: value }); + } + } + } + return list; +}; diff --git a/js/reducers/tracking/trackingActions.js b/js/reducers/tracking/trackingActions.js new file mode 100644 index 000000000..edb1a3313 --- /dev/null +++ b/js/reducers/tracking/trackingActions.js @@ -0,0 +1,495 @@ +import { actionType, actionObjectType, actionDescription } from './constants'; +import { constants as apiConstants } from '../api/constants'; +import { CONSTANTS as nglConstants } from '../ngl/constants'; +import { constants as selectionConstants } from '../selection/constants'; +import { constants as customDatasetConstants } from '../../components/datasets/redux/constants'; +import { DJANGO_CONTEXT } from '../../utils/djangoContext'; + +export const findTruckAction = (action, state) => { + const username = DJANGO_CONTEXT['username']; + let truckAction = null; + if (action.type.includes(apiConstants.SET_TARGET_ON)) { + if (action.target_on) { + let targetName = getTargetName(action.target_on, state); + truckAction = { + type: actionType.TARGET_LOADED, + timestamp: Date.now(), + username: username, + object_type: actionObjectType.TARGET, + object_name: targetName, + object_id: action.target_on, + text: `${actionDescription.TARGET} ${targetName} ${actionDescription.LOADED}` + }; + } + } else if (action.type.includes(apiConstants.SET_MOL_GROUP_ON)) { + if (action.mol_group_on) { + let molGroupSelection = state.selectionReducers.mol_group_selection; + let currentMolGroup = molGroupSelection && molGroupSelection.find(o => o === action.mol_group_on); + if (!currentMolGroup) { + let molGroupName = getMolGroupName(action.mol_group_on, state); + truckAction = { + type: actionType.SITE_TURNED_ON, + timestamp: Date.now(), + username: username, + object_type: actionObjectType.SITE, + object_name: molGroupName, + object_id: action.mol_group_on, + text: `${actionDescription.SITE} ${molGroupName} ${actionDescription.TURNED_ON}` + }; + } + } + } else if (action.type.includes(selectionConstants.SET_OBJECT_SELECTION)) { + let objectId = action.payload && action.payload[0]; + if (objectId) { + let molGroupName = getMolGroupName(objectId, state); + truckAction = { + type: actionType.SITE_TURNED_OFF, + timestamp: Date.now(), + username: username, + object_type: actionObjectType.SITE, + object_name: molGroupName, + object_id: objectId, + text: `${actionDescription.SITE} ${molGroupName} ${actionDescription.TURNED_OFF}` + }; + } + } else if (action.type.includes(selectionConstants.APPEND_FRAGMENT_DISPLAY_LIST)) { + if (action.item) { + let objectType = action.item.isInspiration === true ? actionObjectType.INSPIRATION : actionObjectType.MOLECULE; + let objectName = action.item.name || getMoleculeName(action.item.id, state); + + truckAction = { + type: actionType.LIGAND_TURNED_ON, + timestamp: Date.now(), + username: username, + object_type: objectType, + object_name: objectName, + object_id: action.item.id, + text: `${actionDescription.LIGAND} ${actionDescription.TURNED_ON} ${objectType} ${objectName}` + }; + } + } else if (action.type.includes(selectionConstants.REMOVE_FROM_FRAGMENT_DISPLAY_LIST)) { + if (action.item) { + let objectType = action.item.isInspiration === true ? actionObjectType.INSPIRATION : actionObjectType.MOLECULE; + let objectName = action.item.name || getMoleculeName(action.item.id, state); + + truckAction = { + type: actionType.LIGAND_TURNED_OFF, + timestamp: Date.now(), + username: username, + object_type: objectType, + object_name: objectName, + object_id: action.item.id, + text: `${actionDescription.LIGAND} ${actionDescription.TURNED_OFF} ${objectType} ${objectName}` + }; + } + } else if (action.type.includes(selectionConstants.APPEND_PROTEIN_LIST)) { + if (action.item) { + let objectType = action.item.isInspiration === true ? actionObjectType.INSPIRATION : actionObjectType.MOLECULE; + let objectName = action.item.name || getMoleculeName(action.item.id, state); + + truckAction = { + type: actionType.SIDECHAINS_TURNED_ON, + timestamp: Date.now(), + username: username, + object_type: objectType, + object_name: objectName, + object_id: action.item.id, + text: `${actionDescription.SIDECHAINS} ${actionDescription.TURNED_ON} ${objectType} ${objectName}` + }; + } + } else if (action.type.includes(selectionConstants.REMOVE_FROM_PROTEIN_LIST)) { + if (action.item) { + let objectType = action.item.isInspiration === true ? actionObjectType.INSPIRATION : actionObjectType.MOLECULE; + let objectName = action.item.name || getMoleculeName(action.item.id, state); + + truckAction = { + type: actionType.SIDECHAINS_TURNED_OFF, + timestamp: Date.now(), + username: username, + object_type: objectType, + object_name: objectName, + object_id: action.item.id, + text: `${actionDescription.SIDECHAINS} ${actionDescription.TURNED_OFF} ${objectType} ${objectName}` + }; + } + } else if (action.type.includes(selectionConstants.APPEND_COMPLEX_LIST)) { + if (action.item) { + let objectType = action.item.isInspiration === true ? actionObjectType.INSPIRATION : actionObjectType.MOLECULE; + let objectName = action.item.name || getMoleculeName(action.item.id, state); + + truckAction = { + type: actionType.INTERACTIONS_TURNED_ON, + timestamp: Date.now(), + username: username, + object_type: objectType, + object_name: objectName, + object_id: action.item.id, + text: `${actionDescription.INTERACTIONS} ${actionDescription.TURNED_ON} ${objectType} ${objectName}` + }; + } + } else if (action.type.includes(selectionConstants.REMOVE_FROM_COMPLEX_LIST)) { + if (action.item) { + let objectType = action.item.isInspiration === true ? actionObjectType.INSPIRATION : actionObjectType.MOLECULE; + let objectName = action.item.name || getMoleculeName(action.item.id, state); + + truckAction = { + type: actionType.INTERACTIONS_TURNED_OFF, + timestamp: Date.now(), + username: username, + object_type: objectType, + object_name: objectName, + object_id: action.item.id, + text: `${actionDescription.INTERACTIONS} ${actionDescription.TURNED_OFF} ${objectType} ${objectName}` + }; + } + } else if (action.type.includes(selectionConstants.APPEND_SURFACE_LIST)) { + if (action.item) { + let objectType = action.item.isInspiration === true ? actionObjectType.INSPIRATION : actionObjectType.MOLECULE; + let objectName = action.item.name || getMoleculeName(action.item.id, state); + + truckAction = { + type: actionType.SURFACE_TURNED_ON, + timestamp: Date.now(), + username: username, + object_type: objectType, + object_name: objectName, + object_id: action.item.id, + text: `${actionDescription.SURFACE} ${actionDescription.TURNED_ON} ${objectType} ${objectName}` + }; + } + } else if (action.type.includes(selectionConstants.REMOVE_FROM_SURFACE_LIST)) { + if (action.item) { + let objectType = action.item.isInspiration === true ? actionObjectType.INSPIRATION : actionObjectType.MOLECULE; + let objectName = action.item.name || getMoleculeName(action.item.id, state); + + truckAction = { + type: actionType.SURFACE_TURNED_OFF, + timestamp: Date.now(), + username: username, + object_type: objectType, + object_name: objectName, + object_id: action.item.id, + text: `${actionDescription.SURFACE} ${actionDescription.TURNED_OFF} ${objectType} ${objectName}` + }; + } + } else if (action.type.includes(selectionConstants.APPEND_VECTOR_ON_LIST)) { + if (action.item) { + let objectType = action.item.isInspiration === true ? actionObjectType.INSPIRATION : actionObjectType.MOLECULE; + let objectName = action.item.name || getMoleculeName(action.item.id, state); + + truckAction = { + type: actionType.VECTORS_TURNED_ON, + timestamp: Date.now(), + username: username, + object_type: objectType, + object_name: objectName, + object_id: action.item.id, + text: `${actionDescription.VECTOR} ${actionDescription.TURNED_ON} ${objectType} ${objectName}` + }; + } + } else if (action.type.includes(selectionConstants.REMOVE_FROM_VECTOR_ON_LIST)) { + if (action.item) { + let objectType = action.item.isInspiration === true ? actionObjectType.INSPIRATION : actionObjectType.MOLECULE; + let objectName = action.item.name || getMoleculeName(action.item.id, state); + + truckAction = { + type: actionType.VECTORS_TURNED_OFF, + timestamp: Date.now(), + username: username, + object_type: objectType, + object_name: objectName, + object_id: action.item.id, + text: `${actionDescription.VECTOR} ${actionDescription.TURNED_OFF} ${objectType} ${objectName}` + }; + } + } else if (action.type.includes(selectionConstants.APPEND_TO_BUY_LIST)) { + if (action.item) { + let objectType = actionObjectType.MOLECULE; + let objectName = action.vector; + + truckAction = { + type: actionType.MOLECULE_ADDED_TO_SHOPPING_CART, + timestamp: Date.now(), + username: username, + object_type: actionObjectType.MOLECULE, + object_name: objectName, + object_id: objectName, + text: `${objectType} ${objectName} ${actionDescription.ADDED} ${actionDescription.TO_SHOPPING_CART}` + }; + } + } else if (action.type.includes(selectionConstants.REMOVE_FROM_TO_BUY_LIST)) { + if (action.item) { + let objectType = actionObjectType.MOLECULE; + let objectName = action.vector; + + truckAction = { + type: actionType.MOLECULE_REMOVED_FROM_SHOPPING_CART, + timestamp: Date.now(), + username: username, + object_type: objectType, + object_name: objectName, + object_id: objectName, + text: `${objectType} ${objectName} ${actionDescription.REMOVED} ${actionDescription.FROM_SHOPPING_CART}` + }; + } + } else if (action.type.includes(selectionConstants.SET_CURRENT_VECTOR)) { + if (action.payload) { + let objectType = actionObjectType.MOLECULE; + let objectName = action.payload; + + truckAction = { + type: actionType.VECTOR_SELECTED, + timestamp: Date.now(), + username: username, + object_type: objectType, + object_name: objectName, + object_id: action.payload, + text: `${actionDescription.VECTOR} ${objectName} ${actionDescription.SELECTED}` + }; + } + } else if (action.type.includes(customDatasetConstants.APPEND_MOLECULE_TO_COMPOUNDS_TO_BUY_OF_DATASET)) { + if (action.payload) { + let objectType = actionObjectType.COMPOUND; + let objectName = action.payload.moleculeTitle; + + truckAction = { + type: actionType.COMPOUND_SELECTED, + timestamp: Date.now(), + username: username, + object_type: objectType, + object_name: objectName, + object_id: action.payload.moleculeID, + dataset_id: action.payload.datasetID, + text: `${objectType} ${objectName} ${actionDescription.SELECTED} of dataset: ${action.payload.datasetID}` + }; + } + } else if (action.type.includes(customDatasetConstants.REMOVE_MOLECULE_FROM_COMPOUNDS_TO_BUY_OF_DATASET)) { + if (action.payload) { + let objectType = actionObjectType.COMPOUND; + let objectName = action.payload.moleculeTitle; + + truckAction = { + type: actionType.COMPOUND_DESELECTED, + timestamp: Date.now(), + username: username, + object_type: objectType, + object_name: objectName, + object_id: action.payload.moleculeID, + dataset_id: action.payload.datasetID, + text: `${objectType} ${objectName} ${actionDescription.DESELECTED} of dataset: ${action.payload.datasetID}` + }; + } + } else if (action.type.includes(customDatasetConstants.APPEND_LIGAND_LIST)) { + if (action.payload && action.payload.item) { + let objectType = + action.payload.item.isCrossReference === true ? actionObjectType.CROSS_REFERENCE : actionObjectType.COMPOUND; + let objectName = action.payload.item.name; + + truckAction = { + type: actionType.LIGAND_TURNED_ON, + timestamp: Date.now(), + username: username, + object_type: objectType, + object_name: objectName, + object_id: action.payload.item.id, + dataset_id: action.payload.datasetID, + text: `${actionDescription.LIGAND} ${actionDescription.TURNED_ON} ${objectType} ${objectName} of dataset: ${action.payload.datasetID}` + }; + } + } else if (action.type.includes(customDatasetConstants.REMOVE_FROM_LIGAND_LIST)) { + if (action.payload && action.payload.item) { + let objectType = + action.payload.item.isCrossReference === true ? actionObjectType.CROSS_REFERENCE : actionObjectType.COMPOUND; + let objectName = action.payload.item.name; + + truckAction = { + type: actionType.LIGAND_TURNED_OFF, + timestamp: Date.now(), + username: username, + object_type: objectType, + object_name: objectName, + object_id: action.payload.item.id, + dataset_id: action.payload.datasetID, + text: `${actionDescription.LIGAND} ${actionDescription.TURNED_OFF} ${objectType} ${objectName} of dataset: ${action.payload.datasetID}` + }; + } + } else if (action.type.includes(customDatasetConstants.APPEND_PROTEIN_LIST)) { + if (action.payload && action.payload.item) { + let objectType = + action.payload.item.isCrossReference === true ? actionObjectType.CROSS_REFERENCE : actionObjectType.COMPOUND; + let objectName = action.payload.item.name; + + truckAction = { + type: actionType.SIDECHAINS_TURNED_ON, + timestamp: Date.now(), + username: username, + object_type: objectType, + object_name: objectName, + object_id: action.payload.item.id, + dataset_id: action.payload.datasetID, + text: `${actionDescription.SIDECHAINS} ${actionDescription.TURNED_ON} ${objectType} ${objectName} of dataset: ${action.payload.datasetID}` + }; + } + } else if (action.type.includes(customDatasetConstants.REMOVE_FROM_PROTEIN_LIST)) { + if (action.payload && action.payload.item) { + let objectType = + action.payload.item.isCrossReference === true ? actionObjectType.CROSS_REFERENCE : actionObjectType.COMPOUND; + let objectName = action.payload.item.name; + + truckAction = { + type: actionType.SIDECHAINS_TURNED_OFF, + timestamp: Date.now(), + username: username, + object_type: objectType, + object_name: objectName, + object_id: action.payload.item.id, + dataset_id: action.payload.datasetID, + text: `${actionDescription.SIDECHAINS} ${actionDescription.TURNED_OFF} ${objectType} ${objectName} of dataset: ${action.payload.datasetID}` + }; + } + } else if (action.type.includes(customDatasetConstants.APPEND_COMPLEX_LIST)) { + if (action.payload && action.payload.item) { + let objectType = + action.payload.item.isCrossReference === true ? actionObjectType.CROSS_REFERENCE : actionObjectType.COMPOUND; + let objectName = action.payload.item.name; + + truckAction = { + type: actionType.INTERACTIONS_TURNED_ON, + timestamp: Date.now(), + username: username, + object_type: objectType, + object_name: objectName, + object_id: action.payload.item.id, + dataset_id: action.payload.datasetID, + text: `${actionDescription.INTERACTIONS} ${actionDescription.TURNED_ON} ${objectType} ${objectName} of dataset: ${action.payload.datasetID}` + }; + } + } else if (action.type.includes(customDatasetConstants.REMOVE_FROM_COMPLEX_LIST)) { + if (action.payload && action.payload.item) { + let objectType = + action.payload.item.isCrossReference === true ? actionObjectType.CROSS_REFERENCE : actionObjectType.COMPOUND; + let objectName = action.payload.item.name; + + truckAction = { + type: actionType.INTERACTIONS_TURNED_OFF, + timestamp: Date.now(), + username: username, + object_type: objectType, + object_name: objectName, + object_id: action.payload.item.id, + dataset_id: action.payload.datasetID, + text: `${actionDescription.INTERACTIONS} ${actionDescription.TURNED_OFF} ${objectType} ${objectName} of dataset: ${action.payload.datasetID}` + }; + } + } else if (action.type.includes(customDatasetConstants.APPEND_SURFACE_LIST)) { + if (action.payload && action.payload.item) { + let objectType = + action.payload.item.isCrossReference === true ? actionObjectType.CROSS_REFERENCE : actionObjectType.COMPOUND; + let objectName = action.payload.item.name; + + truckAction = { + type: actionType.SURFACE_TURNED_ON, + timestamp: Date.now(), + username: username, + object_type: objectType, + object_name: objectName, + object_id: action.payload.item.id, + dataset_id: action.payload.datasetID, + text: `${actionDescription.SURFACE} ${actionDescription.TURNED_ON} ${objectType} ${objectName} of dataset: ${action.payload.datasetID}` + }; + } + } else if (action.type.includes(customDatasetConstants.REMOVE_FROM_SURFACE_LIST)) { + if (action.payload && action.payload.item) { + let objectType = + action.payload.item.isCrossReference === true ? actionObjectType.CROSS_REFERENCE : actionObjectType.COMPOUND; + let objectName = action.payload.item.name; + + truckAction = { + type: actionType.SURFACE_TURNED_OFF, + timestamp: Date.now(), + username: username, + object_type: objectType, + object_name: objectName, + object_id: action.payload.item.id, + dataset_id: action.payload.datasetID, + text: `${actionDescription.SURFACE} ${actionDescription.TURNED_OFF} ${objectType} ${objectName} of dataset: ${action.payload.datasetID}` + }; + } + } else if (action.type.includes(nglConstants.UPDATE_COMPONENT_REPRESENTATION)) { + let objectType = actionObjectType.REPRESENTATION; + + truckAction = { + type: actionType.REPRESENTATION_CHANGED, + timestamp: Date.now(), + username: username, + object_type: actionObjectType.REPRESENTATION, + object_name: action.objectInViewID, + object_id: action.objectInViewID, + representation_id: action.representationID, + new_representation: action.newRepresentation, + text: `${objectType} parameter of ${action.objectInViewID} ${actionDescription.CHANGED}` + }; + } else if (action.type.includes(nglConstants.ADD_COMPONENT_REPRESENTATION)) { + let objectType = actionObjectType.REPRESENTATION; + let representationName = action.newRepresentation && action.newRepresentation.type; + + truckAction = { + type: actionType.REPRESENTATION_ADDED, + timestamp: Date.now(), + username: username, + object_type: actionObjectType.REPRESENTATION, + object_name: representationName, + object_id: action.objectInViewID, + new_representation: action.newRepresentation, + text: `${objectType} ${representationName} of ${action.objectInViewID} ${actionDescription.ADDED}` + }; + } else if (action.type.includes(nglConstants.REMOVE_COMPONENT_REPRESENTATION)) { + let objectType = actionObjectType.REPRESENTATION; + let representationName = action.representation && action.representation.type; + + truckAction = { + type: actionType.REPRESENTATION_REMOVED, + timestamp: Date.now(), + username: username, + object_type: objectType, + object_name: representationName, + object_id: action.objectInViewID, + representation_id: action.representationID, + text: `${objectType} ${representationName} of ${action.objectInViewID} ${actionDescription.REMOVED}` + }; + } + + return truckAction; +}; + +const getMolGroupName = (molGroupId, state) => { + let molGroupList = state.apiReducers.mol_group_list; + let molGroup = molGroupList.find(group => group.id === molGroupId); + let molGroupName = (molGroup && molGroup.description) || ''; + return molGroupName; +}; + +const getTargetName = (targetId, state) => { + let targetList = state.apiReducers.target_id_list; + let target = targetList.find(target => target.id === targetId); + let targetName = (target && target.title) || ''; + return targetName; +}; + +const getMoleculeName = (moleculeId, state) => { + let moleculeList = state.apiReducers.all_mol_lists; + let moleculeName = ''; + + if (moleculeList) { + for (const group in moleculeList) { + let molecules = moleculeList[group]; + + let molecule = molecules.find(molecule => molecule.id === moleculeId); + if (molecule && molecule != null) { + moleculeName = molecule.protein_code; + break; + } + } + } + return moleculeName; +}; diff --git a/js/reducers/tracking/trackingMiddleware.js b/js/reducers/tracking/trackingMiddleware.js new file mode 100644 index 000000000..f284a210b --- /dev/null +++ b/js/reducers/tracking/trackingMiddleware.js @@ -0,0 +1,19 @@ +import { appendToActionList } from './actions'; +import { constants } from './constants'; +import { findTruckAction } from './trackingActions'; + +const trackingMiddleware = ({ dispatch, getState }) => next => action => { + //console.log(`Redux Log:`, action); + + const state = getState(); + if (!action.type.includes(constants.APPEND_ACTIONS_LIST)) { + let truckAction = findTruckAction(action, state); + if (truckAction && truckAction != null) { + dispatch(appendToActionList(truckAction)); + } + } + + next(action); +}; + +export default trackingMiddleware; diff --git a/js/reducers/tracking/trackingReducers.js b/js/reducers/tracking/trackingReducers.js new file mode 100644 index 000000000..a9e9e793b --- /dev/null +++ b/js/reducers/tracking/trackingReducers.js @@ -0,0 +1,28 @@ +import { constants } from './constants'; + +export const INITIAL_STATE = { + truck_actions_list: [], + current_actions_list: [] +}; + +export default function trackingReducers(state = INITIAL_STATE, action = {}) { + switch (action.type) { + case constants.SET_ACTIONS_LIST: + return Object.assign({}, state, { + truck_actions_list: action.truck_actions_list + }); + + case constants.APPEND_ACTIONS_LIST: + return Object.assign({}, state, { + truck_actions_list: [...new Set([...state.truck_actions_list, action.truck_action])] + }); + + case constants.SET_CURRENT_ACTIONS_LIST: + return Object.assign({}, state, { + current_actions_list: action.current_actions_list + }); + + default: + return state; + } +} diff --git a/package.json b/package.json index f44f91aaf..f45476057 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fragalysis-frontend", - "version": "0.9.11", + "version": "0.9.29", "description": "Frontend for fragalysis", "main": "webpack.config.js", "scripts": { @@ -60,6 +60,7 @@ "react-canvas-draw": "^1.1.0", "react-color": "^2.17.3", "react-dom": "^16.12.0", + "react-event-timeline": "^1.6.3", "react-hot-loader": "^4.12.18", "react-infinite-scroller": "^1.2.4", "react-redux": "^7.1.3",