diff --git a/docker-compose.dev.vector.yml b/docker-compose.dev.vector.yml index 103cce1ec..e57a99de7 100644 --- a/docker-compose.dev.vector.yml +++ b/docker-compose.dev.vector.yml @@ -53,8 +53,8 @@ services: start_period: 10s web: container_name: web_dock - image: xchem/fragalysis-stack:latest - # image: alanbchristie/fragalysis-backend:1187.3 + # image: xchem/fragalysis-stack:latest + image: alanbchristie/fragalysis-backend:1187.3 # image: boriskovarm2ms/fragalysis-stack:experiment2 # image: kaliif/fragalysis-backend:latest command: /bin/bash /code/launch-stack.sh diff --git a/js/components/preview/molecule/observationCmpList.js b/js/components/preview/molecule/observationCmpList.js index 4cb59732c..2e7c82941 100644 --- a/js/components/preview/molecule/observationCmpList.js +++ b/js/components/preview/molecule/observationCmpList.js @@ -77,6 +77,7 @@ import { extractTargetFromURLParam } from '../utils'; import { LoadingContext } from '../../loading'; import { DJANGO_CONTEXT } from '../../../utils/djangoContext'; import ObservationCmpView from './observationCmpView'; +import { ObservationsDialog } from './observationsDialog'; const useStyles = makeStyles(theme => ({ container: { @@ -291,6 +292,8 @@ export const ObservationCmpList = memo(({ hideProjects }) => { const obsCmpsToEditIds = useSelector(state => state.selectionReducers.obsCmpsToEdit); const isGlobalEdit = useSelector(state => state.selectionReducers.isGlobalEdit); + const isObservationDialogOpen = useSelector(state => state.selectionReducers.isObservationDialogOpen); + const object_selection = useSelector(state => state.selectionReducers.mol_group_selection); const all_mol_lists = useSelector(state => state.apiReducers.all_mol_lists); @@ -394,7 +397,7 @@ export const ObservationCmpList = memo(({ hideProjects }) => { proteinList, molForTagEditId, isTagEditorOpen, - moleculesToEditIds + obsCmpsToEditIds ]); // eslint-disable-next-line react-hooks/exhaustive-deps joinedMoleculeLists = useMemo(() => addSelectedMoleculesFromUnselectedSites(joinedMoleculeLists, complexList), [ @@ -403,7 +406,7 @@ export const ObservationCmpList = memo(({ hideProjects }) => { complexList, molForTagEditId, isTagEditorOpen, - moleculesToEditIds + obsCmpsToEditIds ]); joinedMoleculeLists = useMemo( () => addSelectedMoleculesFromUnselectedSites(joinedMoleculeLists, fragmentDisplayList), @@ -414,7 +417,7 @@ export const ObservationCmpList = memo(({ hideProjects }) => { fragmentDisplayList, molForTagEditId, isTagEditorOpen, - moleculesToEditIds + obsCmpsToEditIds ] ); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -424,7 +427,7 @@ export const ObservationCmpList = memo(({ hideProjects }) => { surfaceList, molForTagEditId, isTagEditorOpen, - moleculesToEditIds + obsCmpsToEditIds ]); // eslint-disable-next-line react-hooks/exhaustive-deps joinedMoleculeLists = useMemo(() => addSelectedMoleculesFromUnselectedSites(joinedMoleculeLists, densityList), [ @@ -433,7 +436,7 @@ export const ObservationCmpList = memo(({ hideProjects }) => { densityList, molForTagEditId, isTagEditorOpen, - moleculesToEditIds + obsCmpsToEditIds ]); // eslint-disable-next-line react-hooks/exhaustive-deps joinedMoleculeLists = useMemo(() => addSelectedMoleculesFromUnselectedSites(joinedMoleculeLists, vectorOnList), [ @@ -442,7 +445,7 @@ export const ObservationCmpList = memo(({ hideProjects }) => { vectorOnList, molForTagEditId, isTagEditorOpen, - moleculesToEditIds + obsCmpsToEditIds ]); if (isActiveFilter) { @@ -470,8 +473,8 @@ export const ObservationCmpList = memo(({ hideProjects }) => { } } - if (moleculesToEditIds && moleculesToEditIds.length > 0 && isGlobalEdit) { - moleculesToEditIds.forEach(mid => { + if (obsCmpsToEditIds && obsCmpsToEditIds.length > 0 && isGlobalEdit) { + obsCmpsToEditIds.forEach(mid => { if (!joinedMoleculeLists.some(m => m.id === mid)) { const tagEditMol = dispatch(getMoleculeForId(mid)); if (tagEditMol) { @@ -569,6 +572,12 @@ export const ObservationCmpList = memo(({ hideProjects }) => { [allMoleculesList, moleculesToEditIds, molForTagEditId] ); + const allSelectedLHSCmps = useMemo( + () => + allMoleculesList.filter(molecule => obsCmpsToEditIds.includes(molecule.id) || molecule.id === molForTagEditId), + [allMoleculesList, obsCmpsToEditIds, molForTagEditId] + ); + let currentMolecules = joinedMoleculeLists.slice(0, listItemOffset); if ( fragmentDisplayList.length === 0 && @@ -976,6 +985,9 @@ export const ObservationCmpList = memo(({ hideProjects }) => { setIsOpenLPCAlert(false); }} /> + {isObservationDialogOpen && ( + + )} {isTagEditorOpen && ( { - {console.log('tagEditorRef', tagEditorRef)} {currentMolecules.length > 0 && ( <> @@ -1241,6 +1252,7 @@ export const ObservationCmpList = memo(({ hideProjects }) => { */} {filteredLHSCompoundsList.map((data, index, array) => { const molsForCmp = compoundMolecules[data.id]; + const selected = allSelectedLHSCmps.some(molecule => molecule.id === data.id); return ( { Q={containsAtLeastOne(qualityList, molsForCmp)} V={containsAtLeastOne(vectorOnList, molsForCmp)} I={containsAtLeastOne(informationList, molsForCmp)} - selected={false} + selected={selected} disableL={false} disableP={false} disableC={false} diff --git a/js/components/preview/molecule/observationCmpView/observationCmpView.js b/js/components/preview/molecule/observationCmpView/observationCmpView.js index a61a6a2b5..f09df7fe6 100644 --- a/js/components/preview/molecule/observationCmpView/observationCmpView.js +++ b/js/components/preview/molecule/observationCmpView/observationCmpView.js @@ -41,7 +41,11 @@ import { setMoleculeForTagEdit, setTagEditorOpen, appendToMolListToEdit, - removeFromMolListToEdit + removeFromMolListToEdit, + setOpenObservationsDialog, + setObservationsForLHSCmp, + appendToObsCmpListToEdit, + removeFromObsCmpListToEdit } from '../../../../reducers/selection/actions'; import { moleculeProperty } from '../helperConstants'; import { centerOnLigandByMoleculeID } from '../../../../reducers/ngl/dispatchActions'; @@ -57,6 +61,7 @@ import { Edit } from '@material-ui/icons'; import { DJANGO_CONTEXT } from '../../../../utils/djangoContext'; import { getFontColorByBackgroundColor } from '../../../../utils/colors'; import MoleculeSelectCheckbox from '../moleculeView/moleculeSelectCheckbox'; +import { isAnyObservationTurnedOnForCmp } from '../../../../reducers/selection/selectors'; const useStyles = makeStyles(theme => ({ container: { @@ -345,6 +350,8 @@ const ObservationCmpView = memo( const tagList = useSelector(state => state.apiReducers.tagList); const tagEditorOpen = useSelector(state => state.selectionReducers.tagEditorOpened); + const isObservationDialogOpen = useSelector(state => state.selectionReducers.isObservationDialogOpen); + const [tagEditModalOpenNew, setTagEditModalOpenNew] = useState(tagEditorOpen); const { getNglView } = useContext(NglContext); @@ -379,6 +386,10 @@ const ObservationCmpView = memo( const [moleculeTooltipOpen, setMoleculeTooltipOpen] = useState(false); const [tagPopoverOpen, setTagPopoverOpen] = useState(null); + const isAnyObservationOn = useSelector(state => + isAnyObservationTurnedOnForCmp(state, observations?.map(obs => obs.id) || []) + ); + const moleculeImgRef = useRef(null); const open = tagPopoverOpen ? true : false; @@ -875,6 +886,7 @@ const ObservationCmpView = memo( }; const [loadingVector, setLoadingVector] = useState(false); + const onVector = () => { setLoadingVector(true); if (isVectorOn === false) { @@ -958,9 +970,9 @@ const ObservationCmpView = memo( onChange={e => { const result = e.target.checked; if (result) { - dispatch(appendToMolListToEdit(currentID)); + dispatch(appendToObsCmpListToEdit(currentID)); } else { - dispatch(removeFromMolListToEdit(currentID)); + dispatch(removeFromObsCmpListToEdit(currentID)); } }} /> @@ -1171,6 +1183,32 @@ const ObservationCmpView = memo( + + + + + {/* diff --git a/js/components/preview/molecule/observationsDialog.js b/js/components/preview/molecule/observationsDialog.js new file mode 100644 index 000000000..df2cf19b3 --- /dev/null +++ b/js/components/preview/molecule/observationsDialog.js @@ -0,0 +1,464 @@ +import React, { forwardRef, memo, useCallback, useContext, useMemo, useRef, useState } from 'react'; +import { CircularProgress, Grid, Popper, IconButton, Typography, Tooltip } from '@material-ui/core'; +import { Close } from '@material-ui/icons'; +import { makeStyles } from '@material-ui/styles'; +import { useDispatch, useSelector } from 'react-redux'; +import classNames from 'classnames'; +import { NglContext } from '../../nglView/nglProvider'; +import { VIEWS } from '../../../constants/constants'; +import { changeButtonClassname } from '../../datasets/helpers'; +import { + addComplex, + addHitProtein, + addLigand, + addSurface, + removeComplex, + removeHitProtein, + removeLigand, + removeSelectedMolTypes, + removeSurface, + withDisabledMoleculesNglControlButtons +} from './redux/dispatchActions'; +import { colourList } from './utils/color'; +import { + setDeselectedAllByType, + setOpenObservationsDialog, + setSelectedAllByType +} from '../../../reducers/selection/actions'; +import useDisableNglControlButtons from './useDisableNglControlButtons'; +import { Button, Panel } from '../../common'; +import SearchField from '../../common/Components/SearchField'; +import GroupNglControlButtonsContext from './groupNglControlButtonsContext'; +import MoleculeView from './moleculeView'; + +const useStyles = makeStyles(theme => ({ + paper: { + width: 472, + height: 294, + overflowY: 'hidden' + }, + molHeader: { + marginLeft: 19, + width: 'calc(100% - 19px)' + }, + rightBorder: { + borderRight: '1px solid', + borderRightColor: theme.palette.background.divider, + fontWeight: 'bold', + paddingLeft: theme.spacing(1) / 2, + paddingRight: theme.spacing(1) / 2, + paddingTop: theme.spacing(1), + paddingBottom: theme.spacing(1), + fontSize: 8, + width: 25, + textAlign: 'center', + '&:last-child': { + borderRight: 'none', + width: 32 + } + }, + headerButton: { + paddingTop: 10 + }, + content: { + overflowY: 'auto', + height: 214 + }, + search: { + width: 140 + }, + notFound: { + paddingTop: theme.spacing(2) + }, + contButtonsMargin: { + marginTop: theme.spacing(1) / 2, + marginBottom: theme.spacing(1) / 2, + marginLeft: theme.spacing(2) + }, + contColButton: { + minWidth: 'fit-content', + paddingLeft: theme.spacing(1) / 4, + paddingRight: theme.spacing(1) / 4, + paddingBottom: 0, + paddingTop: 0, + fontWeight: 'bold', + fontSize: 9, + borderRadius: 0, + borderColor: theme.palette.primary.main, + backgroundColor: theme.palette.primary.light, + '&:hover': { + backgroundColor: theme.palette.primary.light + //color: theme.palette.primary.contrastText + }, + '&:disabled': { + borderRadius: 0, + borderColor: 'white' + } + }, + contColButtonSelected: { + backgroundColor: theme.palette.primary.main, + color: theme.palette.primary.contrastText, + '&:hover': { + backgroundColor: theme.palette.primary.main + //color: theme.palette.black + } + }, + contColButtonHalfSelected: { + backgroundColor: theme.palette.primary.semidark, + color: theme.palette.primary.contrastText, + '&:hover': { + backgroundColor: theme.palette.primary.semidark + //color: theme.palette.black + } + } +})); + +export const ObservationsDialog = memo( + forwardRef(({ open = false, anchorEl }, ref) => { + const id = open ? 'simple-popover-compound-inspirations' : undefined; + const imgHeight = 49; + const imgWidth = 150; + const classes = useStyles(); + const [searchString, setSearchString] = useState(null); + const selectedAll = useRef(false); + + const { getNglView } = useContext(NglContext); + const stage = getNglView(VIEWS.MAJOR_VIEW) && getNglView(VIEWS.MAJOR_VIEW).stage; + + const isLoadingInspirationListOfMolecules = useSelector( + state => state.datasetsReducers.isLoadingInspirationListOfMolecules + ); + const observationsDataList = useSelector(state => state.selectionReducers.observationsForLHSCmp); + + const ligandList = useSelector(state => state.selectionReducers.fragmentDisplayList); + const proteinList = useSelector(state => state.selectionReducers.proteinList); + const complexList = useSelector(state => state.selectionReducers.complexList); + const surfaceList = useSelector(state => state.selectionReducers.surfaceList); + const densityList = useSelector(state => state.selectionReducers.densityList); + const densityListCustom = useSelector(state => state.selectionReducers.densityListCustom); + const qualityList = useSelector(state => state.selectionReducers.qualityList); + const vectorOnList = useSelector(state => state.selectionReducers.vectorOnList); + const informationList = useSelector(state => state.selectionReducers.informationList); + const molForTagEditId = useSelector(state => state.selectionReducers.molForTagEdit); + const moleculesToEditIds = useSelector(state => state.selectionReducers.moleculesToEdit); + + const dispatch = useDispatch(); + + const [tagEditorAnchorEl, setTagEditorAnchorEl] = useState(null); + + const moleculeList = useMemo(() => { + if (searchString !== null) { + return observationsDataList.filter(molecule => + molecule.code.toLowerCase().includes(searchString.toLowerCase()) + ); + } + return observationsDataList; + }, [observationsDataList, searchString]); + + const allSelectedMolecules = observationsDataList.filter( + molecule => moleculesToEditIds.includes(molecule.id) || molecule.id === molForTagEditId + ); + + // TODO: refactor from this line (duplicity in datasetMoleculeList.js) + const isLigandOn = changeButtonClassname( + ligandList.filter(moleculeID => allSelectedMolecules.find(molecule => molecule.id === moleculeID) !== undefined), + allSelectedMolecules + ); + const isProteinOn = changeButtonClassname( + proteinList.filter(moleculeID => allSelectedMolecules.find(molecule => molecule.id === moleculeID) !== undefined), + allSelectedMolecules + ); + const isComplexOn = changeButtonClassname( + complexList.filter(moleculeID => allSelectedMolecules.find(molecule => molecule.id === moleculeID) !== undefined), + allSelectedMolecules + ); + + const addType = { + ligand: addLigand, + protein: addHitProtein, + complex: addComplex, + surface: addSurface + }; + + const removeType = { + ligand: removeLigand, + protein: removeHitProtein, + complex: removeComplex, + surface: removeSurface + }; + + const removeSelectedTypes = useCallback( + (skipMolecules = [], skipTracking = false) => { + const molecules = [...moleculeList].filter(molecule => !skipMolecules.some(mol => molecule.id === mol.id)); + dispatch(removeSelectedMolTypes(stage, molecules, skipTracking, true)); + }, + [dispatch, moleculeList, stage] + ); + + const removeSelectedType = (type, skipTracking = false) => { + if (type === 'ligand') { + allSelectedMolecules.forEach(molecule => { + dispatch(removeType[type](stage, molecule, skipTracking)); + }); + } else { + allSelectedMolecules.forEach(molecule => { + dispatch(removeType[type](stage, molecule, colourList[molecule.id % colourList.length], skipTracking)); + }); + } + + selectedAll.current = false; + }; + + const addNewType = (type, skipTracking = false) => { + dispatch( + withDisabledMoleculesNglControlButtons( + allSelectedMolecules.map(molecule => molecule.id), + type, + async () => { + const promises = []; + + if (type === 'ligand') { + allSelectedMolecules.forEach(molecule => { + promises.push( + dispatch( + addType[type]( + stage, + molecule, + colourList[molecule.id % colourList.length], + false, + true, + skipTracking + ) + ) + ); + }); + } else { + allSelectedMolecules.forEach(molecule => { + promises.push( + dispatch(addType[type](stage, molecule, colourList[molecule.id % colourList.length], skipTracking)) + ); + }); + } + + await Promise.all(promises); + } + ) + ); + }; + + const ucfirst = string => { + return string.charAt(0).toUpperCase() + string.slice(1); + }; + + const onButtonToggle = (type, calledFromSelectAll = false) => { + if (calledFromSelectAll === true && selectedAll.current === true) { + // REDO + if (eval('is' + ucfirst(type) + 'On') === false) { + addNewType(type); + } + } else if (calledFromSelectAll && selectedAll.current === false) { + removeSelectedType(type); + } else if (!calledFromSelectAll) { + if (eval('is' + ucfirst(type) + 'On') === false) { + let molecules = getSelectedMoleculesByType(type, true); + dispatch(setSelectedAllByType(type, molecules, true)); + addNewType(type, true); + } else { + let molecules = getSelectedMoleculesByType(type, false); + dispatch(setDeselectedAllByType(type, molecules, true)); + removeSelectedType(type, true); + } + } + }; + + const getSelectedMoleculesByType = (type, isAdd) => { + switch (type) { + case 'ligand': + return isAdd ? getMoleculesToSelect(ligandList) : getMoleculesToDeselect(ligandList); + case 'protein': + return isAdd ? getMoleculesToSelect(proteinList) : getMoleculesToDeselect(proteinList); + case 'complex': + return isAdd ? getMoleculesToSelect(complexList) : getMoleculesToDeselect(complexList); + default: + return null; + } + }; + + const getMoleculesToSelect = list => { + let molecules = allSelectedMolecules.filter(m => !list.includes(m.id)); + return molecules; + }; + + const getMoleculesToDeselect = list => { + let molecules = allSelectedMolecules.filter(m => list.includes(m.id)); + return molecules; + }; + + const groupNglControlButtonsDisabledState = useDisableNglControlButtons(allSelectedMolecules); + + // TODO refactor to this line + + return ( + + , + + dispatch(setOpenObservationsDialog(false))} + > + + + + ]} + > + {isLoadingInspirationListOfMolecules === false && moleculeList && ( + <> + + + {/* {Object.keys(moleculeProperty).map(key => ( + + {moleculeProperty[key]} + + ))} */} + {allSelectedMolecules.length > 0 && ( + + + + + + + + + + + + + + + {/* C stands for contacts now */} + + + + + + )} + + +
+ {moleculeList.length > 0 && + moleculeList.map((molecule, index, array) => { + let data = molecule; + let previousData = index > 0 && { ...array[index - 1] }; + let nextData = index < array?.length && { ...array[index + 1] }; + const selected = allSelectedMolecules.some(molecule => molecule.id === data.id); + + return ( + + + + ); + })} + {!(moleculeList.length > 0) && ( + + + No molecules found! + + + )} +
+ + )} + {isLoadingInspirationListOfMolecules === true && ( + + + + + + )} +
+
+ ); + }) +); diff --git a/js/reducers/selection/actions.js b/js/reducers/selection/actions.js index 7bd53f8c3..a13560ac0 100644 --- a/js/reducers/selection/actions.js +++ b/js/reducers/selection/actions.js @@ -405,6 +405,20 @@ export const setTagEditorOpen = isOpen => { }; }; +export const setOpenObservationsDialog = isOpen => { + return { + type: constants.SET_OPEN_OBSERVATIONS_DIALOG, + isOpen: isOpen + }; +}; + +export const setObservationsForLHSCmp = observations => { + return { + type: constants.SET_OBSERVATIONS_FOR_LHS_CMP, + observations: observations + }; +}; + export const setMoleculeForTagEdit = molId => { return { type: constants.SET_MOLECULE_FOR_TAG_EDIT, @@ -464,7 +478,7 @@ export const appendToObsCmpListToEdit = cmpId => { export const removeFromObsCmpListToEdit = cmpId => { return { type: constants.REMOVE_FROM_OBS_MOL_LIST_TO_EDIT, - molId: cmpId + cmpId: cmpId }; }; diff --git a/js/reducers/selection/constants.js b/js/reducers/selection/constants.js index 011716efa..6a62e382f 100644 --- a/js/reducers/selection/constants.js +++ b/js/reducers/selection/constants.js @@ -76,6 +76,10 @@ export const constants = { APPEND_TO_OBS_MOL_LIST_TO_EDIT: prefix + 'APPEND_TO_OBS_MOL_LIST_TO_EDIT', REMOVE_FROM_OBS_MOL_LIST_TO_EDIT: prefix + 'REMOVE_FROM_OBS_MOL_LIST_TO_EDIT', + SET_OPEN_OBSERVATIONS_DIALOG: prefix + 'SET_OPEN_OBSERVATIONS_DIALOG', + + SET_OBSERVATIONS_FOR_LHS_CMP: prefix + 'SET_OBSERVATIONS_FOR_LHS_CMP', + SET_TAG_TO_EDIT: prefix + 'SET_TAG_TO_EDIT', SET_DISPLAY_ALL_MOLECULES: prefix + 'SET_DISPLAY_ALL_MOLECULES', SET_DISPLAY_UNTAGGED_MOLECULES: prefix + 'SET_DISPLAY_UNTAGGED_MOLECULES', diff --git a/js/reducers/selection/selectionReducers.js b/js/reducers/selection/selectionReducers.js index 95a200038..6337ef944 100644 --- a/js/reducers/selection/selectionReducers.js +++ b/js/reducers/selection/selectionReducers.js @@ -46,7 +46,10 @@ export const INITIAL_STATE = { //display all molecules in hit navigator regardless of the tag selection displayAllMolecules: false, displayUntaggedMolecules: false, - nextXMolecules: 0 + nextXMolecules: 0, + + isObservationDialogOpen: false, + observationsForLHSCmp: [] }; export function selectionReducers(state = INITIAL_STATE, action = {}) { @@ -152,6 +155,12 @@ export function selectionReducers(state = INITIAL_STATE, action = {}) { diminishedComplexList.delete(action.item.id); return Object.assign({}, state, { complexList: [...diminishedComplexList] }); + case constants.SET_OPEN_OBSERVATIONS_DIALOG: + return { ...state, isObservationDialogOpen: action.isOpen }; + + case constants.SET_OBSERVATIONS_FOR_LHS_CMP: + return { ...state, observationsForLHSCmp: [...action.observations] }; + case constants.SET_SURFACE_LIST: let newSurfaceList = new Set(); action.surfaceList.forEach(f => { diff --git a/js/reducers/selection/selectors.js b/js/reducers/selection/selectors.js index cc2721bcf..696a54e68 100644 --- a/js/reducers/selection/selectors.js +++ b/js/reducers/selection/selectors.js @@ -7,6 +7,13 @@ const getCurrentVector = state => state.selectionReducers.currentVector; const getBondColorMapOfVectors = state => state.selectionReducers.bondColorMapOfVectors; const getCompoundsOfVectors = state => state.selectionReducers.compoundsOfVectors; +const fragmentDisplayList = state => state.selectionReducers.fragmentDisplayList; +const proteinList = state => state.selectionReducers.proteinList; +const complexList = state => state.selectionReducers.complexList; +const surfaceList = state => state.selectionReducers.surfaceList; +const densityList = state => state.selectionReducers.densityList; +const vectorOnList = state => state.selectionReducers.vectorOnList; + export const getMoleculeOfCurrentVector = createSelector( getCurrentVector, getVectorList, @@ -92,3 +99,29 @@ export const getAllCompoundsList = createSelector( return compoundsList; } ); + +export const isAnyObservationTurnedOnForCmp = createSelector( + (_, observations = []) => observations, + fragmentDisplayList, + proteinList, + complexList, + surfaceList, + densityList, + vectorOnList, + (observations, ligands, proteins, complexis, surfaces, densities, vectors) => { + const allLists = new Set(ligands); + proteins.forEach(p => allLists.add(p)); + complexis.forEach(p => allLists.add(p)); + surfaces.forEach(p => allLists.add(p)); + densities.forEach(p => allLists.add(p)); + vectors.forEach(p => allLists.add(p)); + let hasInspiration = false; + observations.forEach(moleculeID => { + if (allLists.has(moleculeID)) { + hasInspiration = true; + return hasInspiration; + } + }); + return hasInspiration; + } +);