diff --git a/js/components/common/Components/ColorPicker/index.js b/js/components/common/Components/ColorPicker/index.js index 79c51b516..903afd264 100644 --- a/js/components/common/Components/ColorPicker/index.js +++ b/js/components/common/Components/ColorPicker/index.js @@ -1,4 +1,4 @@ -import React, { memo, useState } from 'react'; +import React, { memo, useState, useEffect } from 'react'; import { makeStyles, Popper } from '@material-ui/core'; import { SketchPicker } from 'react-color'; @@ -36,6 +36,10 @@ export const ColorPicker = memo(({ selectedColor, setSelectedColor, anchorEl, di const [color, setColor] = useState(selectedColor); const [anchorE1, setAnchorE1] = useState(null); + useEffect(() => { + setColor(selectedColor); + }, [selectedColor]); + const handleClick = event => { if (!disabled) { setAnchorE1(event.currentTarget); diff --git a/js/components/common/Surfaces/Panel/index.js b/js/components/common/Surfaces/Panel/index.js index 1d204e8b3..2623bf347 100644 --- a/js/components/common/Surfaces/Panel/index.js +++ b/js/components/common/Surfaces/Panel/index.js @@ -112,11 +112,7 @@ export const Panel = memo( className={classes.headerGrid} > {title && ( - 1 ? 4 : 6) : 12} - className={classes.headerTitle} - > + {withTooltip ? ( @@ -131,24 +127,20 @@ export const Panel = memo( )} {(headerActions || hasExpansion) && ( - 1 ? 8 : 6) : 12} - > - {headerActions && - headerActions.map((action, index) => ( - - {action} - - ))} - {hasExpansion && ( - - {expanded ? : } - - )} + + + {headerActions && + headerActions.map((action, index) => ( + + {action} + + ))} + {hasExpansion && ( + + {expanded ? : } + + )} + )} diff --git a/js/components/datasets/datasetMoleculeList.js b/js/components/datasets/datasetMoleculeList.js index eb967edee..9d36eece9 100644 --- a/js/components/datasets/datasetMoleculeList.js +++ b/js/components/datasets/datasetMoleculeList.js @@ -37,7 +37,7 @@ import { removeDatasetSurface, autoHideDatasetDialogsOnScroll, moveMoleculeInspirationsSettings, - removeAllSelectedDatasetMolecules + removeSelectedDatasetMolecules } from './redux/dispatchActions'; import { setFilterDialogOpen, setSearchStringOfCompoundSet } from './redux/actions'; import { DatasetFilter } from './datasetFilter'; @@ -299,17 +299,19 @@ export const DatasetMoleculeList = memo( surface: removeDatasetSurface }; - const removeOfAllSelectedTypesOfInspirations = skipTracking => { - let molecules = [...getJoinedMoleculeList, ...inspirationMoleculeDataList]; + const removeSelectedTypesOfInspirations = (skipMolecules = [], skipTracking = false) => { + const molecules = [...getJoinedMoleculeList, ...inspirationMoleculeDataList].filter( + molecule => !skipMolecules.includes(molecule) + ); dispatch(hideAllSelectedMolecules(stage, [...molecules], false, skipTracking)); }; - const removeOfAllSelectedTypes = skipTracking => { - dispatch(removeAllSelectedDatasetMolecules(stage, skipTracking)); + const removeOSelectedTypes = (skipMolecules = {}, skipTracking = false) => { + dispatch(removeSelectedDatasetMolecules(stage, skipTracking, skipMolecules)); }; const moveSelectedMoleculeInspirationsSettings = (data, newItemData, skipTracking) => (dispatch, getState) => { - dispatch( + return dispatch( moveMoleculeInspirationsSettings( data, newItemData, @@ -666,8 +668,8 @@ export const DatasetMoleculeList = memo( showCrossReferenceModal previousItemData={index > 0 && array[index - 1]} nextItemData={index < array?.length && array[index + 1]} - removeOfAllSelectedTypes={removeOfAllSelectedTypes} - removeOfAllSelectedTypesOfInspirations={removeOfAllSelectedTypesOfInspirations} + removeSelectedTypes={removeOSelectedTypes} + removeSelectedTypesOfInspirations={removeSelectedTypesOfInspirations} moveSelectedMoleculeInspirationsSettings={moveSelectedMoleculeInspirationsSettings} L={ligandList.includes(data.id)} P={proteinList.includes(data.id)} diff --git a/js/components/datasets/datasetMoleculeView.js b/js/components/datasets/datasetMoleculeView.js index 578d6676b..4aa468bd7 100644 --- a/js/components/datasets/datasetMoleculeView.js +++ b/js/components/datasets/datasetMoleculeView.js @@ -248,8 +248,8 @@ export const DatasetMoleculeView = memo( index, previousItemData, nextItemData, - removeOfAllSelectedTypes, - removeOfAllSelectedTypesOfInspirations, + removeSelectedTypes, + removeSelectedTypesOfInspirations, moveSelectedMoleculeInspirationsSettings, L, P, @@ -486,7 +486,7 @@ export const DatasetMoleculeView = memo( }); }; - const handleClickOnDownArrow = () => { + const handleClickOnDownArrow = async () => { const refNext = ref.current.nextSibling; scrollToElement(refNext); @@ -497,20 +497,22 @@ export const DatasetMoleculeView = memo( let dataValue = { objectsInView, colourToggle, isLigandOn, isProteinOn, isComplexOn, isSurfaceOn }; dispatch(setArrowUpDown(datasetID, data, nextItem, ARROW_TYPE.DOWN, dataValue)); - removeOfAllSelectedTypes(true); - removeOfAllSelectedTypesOfInspirations(true); - const inspirations = getInspirationsForMol(allInspirations, datasetID, nextItem.id); dispatch(setInspirationMoleculeDataList(inspirations)); - dispatch(moveSelectedMoleculeSettings(stage, data, nextItem, nextDatasetID, datasetID, dataValue, true)); - dispatch(moveSelectedMoleculeInspirationsSettings(data, nextItem, true)); + await Promise.all([ + dispatch(moveSelectedMoleculeSettings(stage, data, nextItem, nextDatasetID, datasetID, dataValue, true)), + dispatch(moveSelectedMoleculeInspirationsSettings(data, nextItem, true)) + ]); dispatch(setCrossReferenceCompoundName(moleculeTitleNext)); if (setRef && ref.current) { setRef(refNext); } + + removeSelectedTypes({ [nextDatasetID]: [nextItem] }, true); + removeSelectedTypesOfInspirations([nextItem], true); }; - const handleClickOnUpArrow = () => { + const handleClickOnUpArrow = async () => { const refPrevious = ref.current.previousSibling; scrollToElement(refPrevious); @@ -523,18 +525,22 @@ export const DatasetMoleculeView = memo( let dataValue = { objectsInView, colourToggle, isLigandOn, isProteinOn, isComplexOn, isSurfaceOn }; dispatch(setArrowUpDown(datasetID, data, previousItem, ARROW_TYPE.UP, dataValue)); - removeOfAllSelectedTypes(true); - removeOfAllSelectedTypesOfInspirations(true); - const inspirations = getInspirationsForMol(allInspirations, datasetID, previousItem.id); dispatch(setInspirationMoleculeDataList(inspirations)); - dispatch(moveSelectedMoleculeSettings(stage, data, previousItem, previousDatasetID, datasetID, dataValue, true)); + await Promise.all([ + dispatch( + moveSelectedMoleculeSettings(stage, data, previousItem, previousDatasetID, datasetID, dataValue, true) + ), + dispatch(moveSelectedMoleculeInspirationsSettings(data, previousItem, true)) + ]); - dispatch(moveSelectedMoleculeInspirationsSettings(data, previousItem, true)); dispatch(setCrossReferenceCompoundName(moleculeTitlePrev)); if (setRef && ref.current) { setRef(refPrevious); } + + removeSelectedTypes({ [previousDatasetID]: [previousItem] }, true); + removeSelectedTypesOfInspirations([previousItem], true); }; const moleculeTitle = data && data.name; diff --git a/js/components/datasets/inspirationDialog.js b/js/components/datasets/inspirationDialog.js index d7d642341..16b6cbe9a 100644 --- a/js/components/datasets/inspirationDialog.js +++ b/js/components/datasets/inspirationDialog.js @@ -21,7 +21,7 @@ import { removeLigand, removeHitProtein, removeSurface, - removeAllSelectedMolTypes + removeSelectedMolTypes } from '../preview/molecule/redux/dispatchActions'; import MoleculeView from '../preview/molecule/moleculeView'; import { moleculeProperty } from '../preview/molecule/helperConstants'; @@ -211,7 +211,7 @@ export const InspirationDialog = memo( const selectMoleculeSite = moleculeGroupSite => {}; const removeOfAllSelectedTypes = (skipTracking = false) => { - dispatch(removeAllSelectedMolTypes(stage, moleculeList, skipTracking, true)); + dispatch(removeSelectedMolTypes(stage, moleculeList, skipTracking, true)); }; const removeSelectedType = (type, skipTracking = false) => { diff --git a/js/components/datasets/redux/dispatchActions.js b/js/components/datasets/redux/dispatchActions.js index 81297cacd..e4c22e948 100644 --- a/js/components/datasets/redux/dispatchActions.js +++ b/js/components/datasets/redux/dispatchActions.js @@ -75,7 +75,8 @@ export const addDatasetHitProtein = ( skipTracking = false, representations = undefined ) => dispatch => { - dispatch( + dispatch(appendProteinList(datasetID, generateMoleculeCompoundId(data), skipTracking)); + return dispatch( loadObject({ target: Object.assign( { display_div: VIEWS.MAJOR_VIEW }, @@ -89,7 +90,6 @@ export const addDatasetHitProtein = ( const currentOrientation = stage.viewerControls.getOrientation(); dispatch(setOrientation(VIEWS.MAJOR_VIEW, currentOrientation)); }); - dispatch(appendProteinList(datasetID, generateMoleculeCompoundId(data), skipTracking)); }; export const removeDatasetHitProtein = (stage, data, colourToggle, datasetID, skipTracking = false) => dispatch => { @@ -113,7 +113,8 @@ export const addDatasetComplex = ( skipTracking = false, representations = undefined ) => dispatch => { - dispatch( + dispatch(appendComplexList(datasetID, generateMoleculeCompoundId(data), skipTracking)); + return dispatch( loadObject({ target: Object.assign( { display_div: VIEWS.MAJOR_VIEW }, @@ -127,7 +128,6 @@ export const addDatasetComplex = ( const currentOrientation = stage.viewerControls.getOrientation(); dispatch(setOrientation(VIEWS.MAJOR_VIEW, currentOrientation)); }); - dispatch(appendComplexList(datasetID, generateMoleculeCompoundId(data), skipTracking)); }; export const removeDatasetComplex = (stage, data, colourToggle, datasetID, skipTracking = false) => dispatch => { @@ -141,7 +141,8 @@ export const removeDatasetComplex = (stage, data, colourToggle, datasetID, skipT }; export const addDatasetSurface = (stage, data, colourToggle, datasetID, representations = undefined) => dispatch => { - dispatch( + dispatch(appendSurfaceList(datasetID, generateMoleculeCompoundId(data))); + return dispatch( loadObject({ target: Object.assign( { display_div: VIEWS.MAJOR_VIEW }, @@ -155,7 +156,6 @@ export const addDatasetSurface = (stage, data, colourToggle, datasetID, represen const currentOrientation = stage.viewerControls.getOrientation(); dispatch(setOrientation(VIEWS.MAJOR_VIEW, currentOrientation)); }); - dispatch(appendSurfaceList(datasetID, generateMoleculeCompoundId(data))); }; export const removeDatasetSurface = (stage, data, colourToggle, datasetID) => dispatch => { @@ -176,8 +176,9 @@ export const addDatasetLigand = ( skipTracking = false, representations = undefined ) => dispatch => { + dispatch(appendLigandList(datasetID, generateMoleculeCompoundId(data), skipTracking)); const currentOrientation = stage.viewerControls.getOrientation(); - dispatch( + return dispatch( loadObject({ target: Object.assign({ display_div: VIEWS.MAJOR_VIEW }, generateMoleculeObject(data, colourToggle, datasetID)), stage, @@ -193,7 +194,6 @@ export const addDatasetLigand = ( // keep current orientation of NGL View stage.viewerControls.orient(currentOrientation); }); - dispatch(appendLigandList(datasetID, generateMoleculeCompoundId(data), skipTracking)); }; export const removeDatasetLigand = (stage, data, colourToggle, datasetID, skipTracking = false) => dispatch => { @@ -639,66 +639,77 @@ export const autoHideDatasetDialogsOnScroll = ({ inspirationDialogRef, crossRefe } }; -export const removeAllSelectedDatasetMolecules = (stage, skipTracking) => (dispatch, getState) => { +export const removeSelectedDatasetMolecules = (stage, skipTracking, skipMolecules = {}) => (dispatch, getState) => { const state = getState(); const datasets = state.datasetsReducers.datasets; const currentMolecules = state.datasetsReducers.moleculeLists; if (datasets) { datasets.forEach(dataset => { - let datasetID = dataset.id; + const datasetID = dataset.id; const ligandList = state.datasetsReducers.ligandLists[datasetID]; const proteinList = state.datasetsReducers.proteinLists[datasetID]; const complexList = state.datasetsReducers.complexLists[datasetID]; const surfaceList = state.datasetsReducers.surfaceLists[datasetID]; - let molecules = currentMolecules[datasetID]; + const molecules = currentMolecules[datasetID].filter(molecule => { + return !skipMolecules[datasetID]?.includes(molecule); + }); + ligandList?.forEach(moleculeID => { const foundedMolecule = molecules?.find(mol => mol.id === moleculeID); - dispatch( - removeDatasetLigand( - stage, - foundedMolecule, - colourList[foundedMolecule.id % colourList.length], - datasetID, - skipTracking - ) - ); + if (foundedMolecule) { + dispatch( + removeDatasetLigand( + stage, + foundedMolecule, + colourList[foundedMolecule.id % colourList.length], + datasetID, + skipTracking + ) + ); + } }); proteinList?.forEach(moleculeID => { const foundedMolecule = molecules?.find(mol => mol.id === moleculeID); - dispatch( - removeDatasetHitProtein( - stage, - foundedMolecule, - colourList[foundedMolecule.id % colourList.length], - datasetID, - skipTracking - ) - ); + if (foundedMolecule) { + dispatch( + removeDatasetHitProtein( + stage, + foundedMolecule, + colourList[foundedMolecule.id % colourList.length], + datasetID, + skipTracking + ) + ); + } }); complexList?.forEach(moleculeID => { const foundedMolecule = molecules?.find(mol => mol.id === moleculeID); - dispatch( - removeDatasetComplex( - stage, - foundedMolecule, - colourList[foundedMolecule.id % colourList.length], - datasetID, - skipTracking - ) - ); + if (foundedMolecule) { + dispatch( + removeDatasetComplex( + stage, + foundedMolecule, + colourList[foundedMolecule.id % colourList.length], + datasetID, + skipTracking + ) + ); + } }); surfaceList?.forEach(moleculeID => { const foundedMolecule = molecules?.find(mol => mol.id === moleculeID); - dispatch( - removeDatasetSurface( - stage, - foundedMolecule, - colourList[foundedMolecule.id % colourList.length], - datasetID, - skipTracking - ) - ); + if (foundedMolecule) { + dispatch( + removeDatasetSurface( + stage, + foundedMolecule, + colourList[foundedMolecule.id % colourList.length], + datasetID, + skipTracking + ) + ); + } }); }); } @@ -758,7 +769,7 @@ export const moveMoleculeInspirationsSettings = ( isAnyInspirationVectorOn || isAnyInspirationQualityOn ) { - dispatch( + return dispatch( moveInspirations( stage, objectsInView, @@ -792,79 +803,97 @@ const moveInspirations = ( ) => (dispatch, getState) => { const state = getState(); const molecules = state.datasetsReducers.inspirationMoleculeDataList; + const promises = []; if (molecules) { molecules.forEach(molecule => { if (molecule) { if (isAnyInspirationLigandOn) { let representations = getRepresentationsByType(objectsInView, molecule, OBJECT_TYPE.LIGAND); - dispatch( - addLigand( - stage, - molecule, - colourList[molecule.id % colourList.length], - false, - isAnyInspirationQualityOn, - skipTracking, - representations + promises.push( + dispatch( + addLigand( + stage, + molecule, + colourList[molecule.id % colourList.length], + false, + isAnyInspirationQualityOn, + skipTracking, + representations + ) ) ); } if (isAnyInspirationProteinOn) { let representations = getRepresentationsByType(objectsInView, molecule, OBJECT_TYPE.HIT_PROTEIN); - dispatch( - addHitProtein(stage, molecule, colourList[molecule.id % colourList.length], skipTracking, representations) + promises.push( + dispatch( + addHitProtein(stage, molecule, colourList[molecule.id % colourList.length], skipTracking, representations) + ) ); } if (isAnyInspirationComplexOn) { let representations = getRepresentationsByType(objectsInView, molecule, OBJECT_TYPE.COMPLEX); - dispatch( - addComplex(stage, molecule, colourList[molecule.id % colourList.length], skipTracking, representations) + promises.push( + dispatch( + addComplex(stage, molecule, colourList[molecule.id % colourList.length], skipTracking, representations) + ) ); } if (isAnyInspirationSurfaceOn) { let representations = getRepresentationsByType(objectsInView, molecule, OBJECT_TYPE.SURFACE); - dispatch( - addSurface(stage, molecule, colourList[molecule.id % colourList.length], skipTracking, representations) + promises.push( + dispatch( + addSurface(stage, molecule, colourList[molecule.id % colourList.length], skipTracking, representations) + ) ); } if (isAnyInspirationQualityOn) { let representations = getRepresentationsByType(objectsInView, molecule, OBJECT_TYPE.QUALITY); - dispatch( - addQuality(stage, molecule, colourList[molecule.id % colourList.length], skipTracking, representations) + promises.push( + dispatch( + addQuality(stage, molecule, colourList[molecule.id % colourList.length], skipTracking, representations) + ) ); } if (isAnyInspirationDensityOn) { let representations = getRepresentationsByType(objectsInView, molecule, OBJECT_TYPE.DENSITY); - dispatch( - addDensity( - stage, - molecule, - colourList[molecule.id % colourList.length], - false, - skipTracking, - representations + promises.push( + dispatch( + addDensity( + stage, + molecule, + colourList[molecule.id % colourList.length], + false, + skipTracking, + representations + ) ) ); } if (isAnyInspirationDensityOnCustom) { let representations = getRepresentationsByType(objectsInView, molecule, OBJECT_TYPE.DENSITY); - dispatch( - addDensityCustomView( - stage, - molecule, - colourList[molecule.id % colourList.length], - false, - skipTracking, - representations + promises.push( + dispatch( + addDensityCustomView( + stage, + molecule, + colourList[molecule.id % colourList.length], + false, + skipTracking, + representations + ) ) ); } if (isAnyInspirationVectorOn) { - dispatch(addVector(stage, molecule, colourList[molecule.id % colourList.length], skipTracking)); + promises.push( + dispatch(addVector(stage, molecule, colourList[molecule.id % colourList.length], skipTracking)) + ); } } }); } + return Promise.all(promises); }; export const moveSelectedInspirations = ( @@ -966,30 +995,42 @@ export const moveSelectedMoleculeSettings = ( data, skipTracking ) => (dispatch, getState) => { + const promises = []; if (newItem && data) { if (data.isLigandOn) { let representations = getRepresentationsByType(data.objectsInView, item, OBJECT_TYPE.LIGAND, datasetID); - dispatch(addDatasetLigand(stage, newItem, data.colourToggle, datasetIdOfMolecule, skipTracking, representations)); + promises.push( + dispatch( + addDatasetLigand(stage, newItem, data.colourToggle, datasetIdOfMolecule, skipTracking, representations) + ) + ); } if (data.isProteinOn) { let representations = getRepresentationsByType(data.objectsInView, item, OBJECT_TYPE.PROTEIN, datasetID); - dispatch( - addDatasetHitProtein(stage, newItem, data.colourToggle, datasetIdOfMolecule, skipTracking, representations) + promises.push( + dispatch( + addDatasetHitProtein(stage, newItem, data.colourToggle, datasetIdOfMolecule, skipTracking, representations) + ) ); } if (data.isComplexOn) { let representations = getRepresentationsByType(data.objectsInView, item, OBJECT_TYPE.COMPLEX, datasetID); - dispatch( - addDatasetComplex(stage, newItem, data.colourToggle, datasetIdOfMolecule, skipTracking, representations) + promises.push( + dispatch( + addDatasetComplex(stage, newItem, data.colourToggle, datasetIdOfMolecule, skipTracking, representations) + ) ); } if (data.isSurfaceOn) { let representations = getRepresentationsByType(data.objectsInView, item, OBJECT_TYPE.SURFACE, datasetID); - dispatch( - addDatasetSurface(stage, newItem, data.colourToggle, datasetIdOfMolecule, skipTracking, representations) + promises.push( + dispatch( + addDatasetSurface(stage, newItem, data.colourToggle, datasetIdOfMolecule, skipTracking, representations) + ) ); } } + return Promise.all(promises); }; export const getDatasetMoleculeID = (datasetID, moleculeID) => `datasetID-${datasetID}_moleculeID-${moleculeID}`; diff --git a/js/components/preview/Preview.js b/js/components/preview/Preview.js index 548a7ce26..9853f4b54 100644 --- a/js/components/preview/Preview.js +++ b/js/components/preview/Preview.js @@ -9,6 +9,7 @@ import HitNavigator from './molecule/hitNavigator'; import { CustomDatasetList } from '../datasets/customDatasetList'; import MolGroupSelector from './moleculeGroups/molGroupSelector'; import TagSelector from './tags/tagSelector'; +import TagDetails from './tags/details/tagDetails'; import { SummaryView } from './summary/summaryView'; import { CompoundList } from './compounds/compoundList'; import { ViewerControls } from './viewerControls'; @@ -173,13 +174,14 @@ const Preview = memo(({ isStateLoaded, hideProjects }) => { return allMolsMap; }, [all_mol_lists]); + const [tagDetailsHeight, setTagDetailsHeight] = useState(0); const [molGroupsHeight, setMolGroupsHeight] = useState(0); const [filterItemsHeight, setFilterItemsHeight] = useState(0); const [filterItemsHeightDataset, setFilterItemsHeightDataset] = useState(0); /* Hit navigator list height */ - const moleculeListHeight = `calc(100vh - ${headerHeight}px - ${theme.spacing(2)}px - ${molGroupsHeight}px - ${ - filterItemsHeight > 0 ? filterItemsHeight + theme.spacing(1) / 2 : 0 + const moleculeListHeight = `calc(100vh - ${headerHeight}px - ${theme.spacing(2)}px - ${tagDetailsHeight}px - ${molGroupsHeight}px + - ${filterItemsHeight > 0 ? filterItemsHeight + theme.spacing(1) / 2 : 0 }px - ${theme.spacing(8)}px)`; /* Custom dataset list height */ @@ -236,6 +238,10 @@ const Preview = memo(({ isStateLoaded, hideProjects }) => { <> + {/* Tag details pane */} + + + {/* Hit cluster selector */} diff --git a/js/components/preview/molecule/moleculeList.js b/js/components/preview/molecule/moleculeList.js index 243ddb134..689efc892 100644 --- a/js/components/preview/molecule/moleculeList.js +++ b/js/components/preview/molecule/moleculeList.js @@ -46,7 +46,7 @@ import { removeLigand, initializeMolecules, applyDirectSelection, - removeAllSelectedMolTypes, + removeSelectedMolTypes, addQuality, removeQuality } from './redux/dispatchActions'; @@ -61,7 +61,6 @@ import * as listType from '../../../constants/listTypes'; import { useRouteMatch } from 'react-router-dom'; import { setSortDialogOpen } from './redux/actions'; import { AlertModal } from '../../common/Modal/AlertModal'; -import { onSelectMoleculeGroup } from '../moleculeGroups/redux/dispatchActions'; import { setSelectedAllByType, setDeselectedAllByType, @@ -70,7 +69,7 @@ import { setIsTagGlobalEdit } from '../../../reducers/selection/actions'; import { TagEditor } from '../tags/modal/tagEditor'; -import { getMoleculeForId } from '../tags/redux/dispatchActions'; +import { getMoleculeForId, selectTag } from '../tags/redux/dispatchActions'; const useStyles = makeStyles(theme => ({ container: { @@ -267,7 +266,6 @@ export const MoleculeList = memo(({ height, setFilterItemsHeight, filterItemsHei const allInspirationMoleculeDataList = useSelector(state => state.datasetsReducers.allInspirationMoleculeDataList); - const mol_group_list = useSelector(state => state.apiReducers.mol_group_list); const all_mol_lists = useSelector(state => state.apiReducers.all_mol_lists); const directDisplay = useSelector(state => state.apiReducers.direct_access); const directAccessProcessed = useSelector(state => state.apiReducers.direct_access_processed); @@ -416,7 +414,7 @@ export const MoleculeList = memo(({ height, setFilterItemsHeight, filterItemsHei useEffect(() => { if ((proteinsHasLoaded === true || proteinsHasLoaded === null) && all_mol_lists.length > 0) { if (!directAccessProcessed && directDisplay && directDisplay.molecules && directDisplay.molecules.length > 0) { - dispatch(applyDirectSelection(majorViewStage, stageSummaryView)); + dispatch(applyDirectSelection(majorViewStage)); wereMoleculesInitialized.current = true; } if ( @@ -457,7 +455,6 @@ export const MoleculeList = memo(({ height, setFilterItemsHeight, filterItemsHei all_mol_lists, directDisplay, directAccessProcessed, - stageSummaryView, object_selection, tags, categories, @@ -582,20 +579,24 @@ export const MoleculeList = memo(({ height, setFilterItemsHeight, filterItemsHei selectedAll.current = false; }; - const removeOfAllSelectedTypes = (skipTracking = false) => { - let molecules = [...getJoinedMoleculeList, ...allInspirationMoleculeDataList]; - dispatch(removeAllSelectedMolTypes(majorViewStage, molecules, skipTracking, false)); + const removeSelectedTypes = (skipMolecules = [], skipTracking = false) => { + const molecules = [...getJoinedMoleculeList, ...allInspirationMoleculeDataList].filter( + molecule => !skipMolecules.includes(molecule) + ); + dispatch(removeSelectedMolTypes(majorViewStage, molecules, skipTracking, false)); }; - const selectMoleculeSite = moleculeGroupSite => { - const moleculeGroup = mol_group_list[moleculeGroupSite - 1]; - dispatch(onSelectMoleculeGroup({ moleculeGroup, stageSummaryView, majorViewStage, selectGroup: true })); + const selectMoleculeTags = moleculeTagsSet => { + const moleculeTags = tags.filter((tag) => moleculeTagsSet.includes(tag.id)); + moleculeTags.forEach((tag) => { + dispatch(selectTag(tag)); + }) }; const addNewType = (type, skipTracking = false) => { if (type === 'ligand') { joinedMoleculeLists.forEach(molecule => { - selectMoleculeSite(molecule.site); + selectMoleculeTags(molecule.tags_set); dispatch( addType[type]( majorViewStage, @@ -609,7 +610,7 @@ export const MoleculeList = memo(({ height, setFilterItemsHeight, filterItemsHei }); } else { joinedMoleculeLists.forEach(molecule => { - selectMoleculeSite(molecule.site); + selectMoleculeTags(molecule.tags_set); dispatch(addType[type](majorViewStage, molecule, colourList[molecule.id % colourList.length], skipTracking)); }); } @@ -718,7 +719,7 @@ export const MoleculeList = memo(({ height, setFilterItemsHeight, filterItemsHei color={'inherit'} disabled={!joinedMoleculeListsCopy.length || noTagsReceived} onClick={event => { - if (isTagEditorOpen === false && moleculesToEditIds && moleculesToEditIds.length > 0) { + if (isTagEditorOpen === false) { setTagEditorAnchorEl(event.currentTarget); dispatch(setIsTagGlobalEdit(true)); dispatch(setTagEditorOpen(true)); @@ -937,7 +938,7 @@ export const MoleculeList = memo(({ height, setFilterItemsHeight, filterItemsHei previousItemData={index > 0 && array[index - 1]} nextItemData={index < array?.length && array[index + 1]} setRef={setTagEditorAnchorEl} - removeOfAllSelectedTypes={removeOfAllSelectedTypes} + removeSelectedTypes={removeSelectedTypes} L={fragmentDisplayList.includes(data.id)} P={proteinList.includes(data.id)} C={complexList.includes(data.id)} @@ -947,7 +948,7 @@ export const MoleculeList = memo(({ height, setFilterItemsHeight, filterItemsHei Q={qualityList.includes(data.id)} V={vectorOnList.includes(data.id)} I={informationList.includes(data.id)} - selectMoleculeSite={selectMoleculeSite} + selectMoleculeSite={selectMoleculeTags} eventInfo={data?.proteinData?.event_info} sigmaaInfo={data?.proteinData?.sigmaa_info} diffInfo={data?.proteinData?.diff_info} diff --git a/js/components/preview/molecule/moleculeView.js b/js/components/preview/molecule/moleculeView.js index 5b3547b1e..66bb61ae7 100644 --- a/js/components/preview/molecule/moleculeView.js +++ b/js/components/preview/molecule/moleculeView.js @@ -4,7 +4,7 @@ import React, { memo, useEffect, useState, useRef, useContext, useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { Grid, Button, makeStyles, Typography, Tooltip, IconButton, Checkbox } from '@material-ui/core'; +import { Grid, Button, makeStyles, Typography, Tooltip, IconButton, Checkbox, Paper } from '@material-ui/core'; import { MyLocation, ArrowDownward, ArrowUpward, Warning, Label, Edit } from '@material-ui/icons'; import SVGInline from 'react-svg-inline'; import classNames from 'classnames'; @@ -50,6 +50,7 @@ import { MOL_TYPE } from './redux/constants'; import { DensityMapsModal } from './modals/densityMapsModal'; import { getRandomColor } from './utils/color'; import { getAllTagsForMol } from '../tags/utils/tagUtils'; +import TagView from '../tags/tagView'; const useStyles = makeStyles(theme => ({ container: { @@ -210,6 +211,9 @@ const useStyles = makeStyles(theme => ({ '&:hover': { color: theme.palette.primary.dark } + }, + tooltip: { + backgroundColor: theme.palette.white } })); @@ -228,7 +232,7 @@ const MoleculeView = memo( previousItemData, nextItemData, setRef, - removeOfAllSelectedTypes, + removeSelectedTypes, L, P, C, @@ -330,25 +334,25 @@ const MoleculeView = memo( const getDataForTagsTooltip = () => { const assignedTags = getAllTagsForMol(data, tagList); - const dataForTooltip = []; + return assignedTags; + /*const dataForTooltip = []; assignedTags && assignedTags.forEach(tag => { dataForTooltip.push(tag.tag); }); - return dataForTooltip; + return dataForTooltip;*/ }; const generateTooltip = () => { const data = getDataForTagsTooltip(); return ( - - {'Edit tags'} -
- {data.map(t => ( - {t} + + {data.map((t, idx) => ( + /*{t}*/ + ))} -
+ ); }; @@ -615,7 +619,7 @@ const MoleculeView = memo( }); }; - const handleClickOnDownArrow = () => { + const handleClickOnDownArrow = async () => { const refNext = ref.current.nextSibling; scrollToElement(refNext); @@ -631,12 +635,13 @@ const MoleculeView = memo( objectsInView: objectsInView, colourToggle: colourToggle }; - removeOfAllSelectedTypes(true); - dispatch(moveSelectedMolSettings(stage, data, nextItemData, dataValue, true)); + // Needs to be awaited since adding elements to NGL viewer is done asynchronously + await dispatch(moveSelectedMolSettings(stage, data, nextItemData, dataValue, true)); dispatch(setArrowUpDown(data, nextItemData, ARROW_TYPE.DOWN, dataValue)); + removeSelectedTypes([nextItemData], true); }; - const handleClickOnUpArrow = () => { + const handleClickOnUpArrow = async () => { const refPrevious = ref.current.previousSibling; scrollToElement(refPrevious); @@ -652,9 +657,10 @@ const MoleculeView = memo( objectsInView: objectsInView, colourToggle: colourToggle }; - removeOfAllSelectedTypes(true); - dispatch(moveSelectedMolSettings(stage, data, previousItemData, dataValue, true)); + // Needs to be awaited since adding elements to NGL viewer is done asynchronously + await dispatch(moveSelectedMolSettings(stage, data, previousItemData, dataValue, true)); dispatch(setArrowUpDown(data, previousItemData, ARROW_TYPE.UP, dataValue)); + removeSelectedTypes([previousItemData], true); }; let moleculeTitle = data?.protein_code.replace(new RegExp(`${target_on_name}-`, 'i'), ''); @@ -666,7 +672,7 @@ const MoleculeView = memo( - diff --git a/js/components/preview/molecule/redux/dispatchActions.js b/js/components/preview/molecule/redux/dispatchActions.js index 69a3e3698..99996b0ce 100644 --- a/js/components/preview/molecule/redux/dispatchActions.js +++ b/js/components/preview/molecule/redux/dispatchActions.js @@ -202,7 +202,8 @@ export const addHitProtein = ( skipTracking = false, representations = undefined ) => dispatch => { - dispatch( + dispatch(appendProteinList(generateMoleculeId(data), skipTracking)); + return dispatch( loadObject({ target: Object.assign({ display_div: VIEWS.MAJOR_VIEW }, generateHitProteinObject(data, colourToggle, base_url)), stage, @@ -213,7 +214,6 @@ export const addHitProtein = ( const currentOrientation = stage.viewerControls.getOrientation(); dispatch(setOrientation(VIEWS.MAJOR_VIEW, currentOrientation)); }); - dispatch(appendProteinList(generateMoleculeId(data), skipTracking)); }; export const removeHitProtein = (stage, data, colourToggle, skipTracking = false) => dispatch => { @@ -233,7 +233,8 @@ export const addComplex = ( skipTracking = false, representations = undefined ) => dispatch => { - dispatch( + dispatch(appendComplexList(generateMoleculeId(data), skipTracking)); + return dispatch( loadObject({ target: Object.assign({ display_div: VIEWS.MAJOR_VIEW }, generateComplexObject(data, colourToggle, base_url)), stage, @@ -244,7 +245,6 @@ export const addComplex = ( const currentOrientation = stage.viewerControls.getOrientation(); dispatch(setOrientation(VIEWS.MAJOR_VIEW, currentOrientation)); }); - dispatch(appendComplexList(generateMoleculeId(data), skipTracking)); }; export const removeComplex = (stage, data, colourToggle, skipTracking = false) => dispatch => { @@ -264,7 +264,8 @@ export const addSurface = ( skipTracking = false, representations = undefined ) => dispatch => { - dispatch( + dispatch(appendSurfaceList(generateMoleculeId(data), skipTracking)); + return dispatch( loadObject({ target: Object.assign({ display_div: VIEWS.MAJOR_VIEW }, generateSurfaceObject(data, colourToggle, base_url)), stage, @@ -275,7 +276,6 @@ export const addSurface = ( const currentOrientation = stage.viewerControls.getOrientation(); dispatch(setOrientation(VIEWS.MAJOR_VIEW, currentOrientation)); }); - dispatch(appendSurfaceList(generateMoleculeId(data), skipTracking)); }; export const removeSurface = (stage, data, colourToggle, skipTracking = false) => dispatch => { @@ -316,11 +316,11 @@ export const addDensity = ( ) => (dispatch, getState) => { if (data.proteinData) { return dispatch(setDensity(stage, data, colourToggle, isWireframeStyle, skipTracking, representations)); - } else { - dispatch(getDensityMapData(data)).then(() => { - return dispatch(setDensity(stage, data, colourToggle, isWireframeStyle, skipTracking, representations)); - }); } + + return dispatch(getDensityMapData(data)).then(() => { + return dispatch(setDensity(stage, data, colourToggle, isWireframeStyle, skipTracking, representations)); + }); }; const setDensity = (stage, data, colourToggle, isWireframeStyle, skipTracking = false, representations = undefined) => ( @@ -406,18 +406,18 @@ export const addDensityCustomView = ( // const contour_DENSITY_MAP_sigmaa = viewParams[NGL_PARAMS.contour_DENSITY_MAP_sigmaa]; // const contour_DENSITY_MAP_diff = viewParams[NGL_PARAMS.contour_DENSITY_MAP_diff]; if (data.proteinData) { - dispatch(setDensityCustom(stage, data, colourToggle, isWireframeStyle, skipTracking, representations)); + return dispatch(setDensityCustom(stage, data, colourToggle, isWireframeStyle, skipTracking, representations)); // dispatch(setNglViewParams(NGL_PARAMS.contour_DENSITY, invertedWireframe)); // dispatch(setNglViewParams(NGL_PARAMS.contour_DENSITY_MAP_sigmaa, invertedWireframe)); // dispatch(setNglViewParams(NGL_PARAMS.contour_DENSITY_MAP_diff, invertedWireframe)); - } else { - dispatch(getDensityMapData(data)).then(() => { - dispatch(setDensityCustom(stage, data, colourToggle, isWireframeStyle, skipTracking, representations)); - // dispatch(setNglViewParams(NGL_PARAMS.contour_DENSITY, invertedWireframe)); - // dispatch(setNglViewParams(NGL_PARAMS.contour_DENSITY_MAP_sigmaa, invertedWireframe)); - // dispatch(setNglViewParams(NGL_PARAMS.contour_DENSITY_MAP_diff, invertedWireframe)); - }); } + + return dispatch(getDensityMapData(data)).then(() => { + dispatch(setDensityCustom(stage, data, colourToggle, isWireframeStyle, skipTracking, representations)); + // dispatch(setNglViewParams(NGL_PARAMS.contour_DENSITY, invertedWireframe)); + // dispatch(setNglViewParams(NGL_PARAMS.contour_DENSITY_MAP_sigmaa, invertedWireframe)); + // dispatch(setNglViewParams(NGL_PARAMS.contour_DENSITY_MAP_diff, invertedWireframe)); + }); }; export const toggleDensityWireframe = (currentWireframeSetting, densityData) => dispatch => { @@ -554,8 +554,8 @@ export const addQuality = (stage, data, colourToggle, skipTracking = false, repr getState ) => { dispatch(deleteObject(Object.assign({ display_div: VIEWS.MAJOR_VIEW }, generateMoleculeObject(data)), stage)); - dispatch(addLigand(stage, data, colourToggle, false, true, true, representations)); dispatch(appendQualityList(generateMoleculeId(data), skipTracking)); + return dispatch(addLigand(stage, data, colourToggle, false, true, true, representations)); }; export const removeQuality = (stage, data, colourToggle, skipTracking = false) => dispatch => { @@ -749,28 +749,31 @@ export const hideAllSelectedMolecules = (stage, currentMolecules, isHideAll, ski }; export const moveSelectedMolSettings = (stage, item, newItem, data, skipTracking) => (dispatch, getState) => { + const promises = []; if (newItem && data) { if (data.isLigandOn) { const ligandRepresentations = getRepresentationsByType(data.objectsInView, item, OBJECT_TYPE.LIGAND); - dispatch( - addLigand(stage, newItem, data.colourToggle, false, data.isQualityOn, skipTracking, ligandRepresentations) + promises.push( + dispatch( + addLigand(stage, newItem, data.colourToggle, false, data.isQualityOn, skipTracking, ligandRepresentations) + ) ); } if (data.isProteinOn) { const proteinRepresentations = getRepresentationsByType(data.objectsInView, item, OBJECT_TYPE.HIT_PROTEIN); - dispatch(addHitProtein(stage, newItem, data.colourToggle, skipTracking, proteinRepresentations)); + promises.push(dispatch(addHitProtein(stage, newItem, data.colourToggle, skipTracking, proteinRepresentations))); } if (data.isComplexOn) { const complaxRepresentations = getRepresentationsByType(data.objectsInView, item, OBJECT_TYPE.COMPLEX); - dispatch(addComplex(stage, newItem, data.colourToggle, skipTracking, complaxRepresentations)); + promises.push(dispatch(addComplex(stage, newItem, data.colourToggle, skipTracking, complaxRepresentations))); } if (data.isSurfaceOn) { const surfaceRepresentations = getRepresentationsByType(data.objectsInView, item, OBJECT_TYPE.SURFACE); - dispatch(addSurface(stage, newItem, data.colourToggle, skipTracking, surfaceRepresentations)); + promises.push(dispatch(addSurface(stage, newItem, data.colourToggle, skipTracking, surfaceRepresentations))); } if (data.isQualityOn) { const qualityRepresentations = getRepresentationsByType(data.objectsInView, item, OBJECT_TYPE.QUALITY); - dispatch(addQuality(stage, newItem, data.colourToggle, skipTracking, qualityRepresentations)); + promises.push(dispatch(addQuality(stage, newItem, data.colourToggle, skipTracking, qualityRepresentations))); } if (data.isDensityOn) { // const densityRepresentations = getRepresentationsForDensities(data.objectsInView, item, OBJECT_TYPE.DENSITY); @@ -786,14 +789,13 @@ export const moveSelectedMolSettings = (stage, item, newItem, data, skipTracking // ); } if (data.isVectorOn) { - dispatch(addVector(stage, newItem, skipTracking)).catch(error => { - throw new Error(error); - }); + promises.push(dispatch(addVector(stage, newItem, skipTracking))); } } + return Promise.all(promises); }; -export const removeAllSelectedMolTypes = (majorViewStage, molecules, skipTracking, isInspiration) => ( +export const removeSelectedMolTypes = (majorViewStage, molecules, skipTracking, isInspiration) => ( dispatch, getState ) => { @@ -810,61 +812,92 @@ export const removeAllSelectedMolTypes = (majorViewStage, molecules, skipTrackin proteinList?.forEach(moleculeID => { let foundedMolecule = joinedMoleculeLists?.find(mol => mol.id === moleculeID); - foundedMolecule = foundedMolecule && Object.assign({ isInspiration: isInspiration }, foundedMolecule); - dispatch( - removeHitProtein( - majorViewStage, - foundedMolecule, - colourList[foundedMolecule.id % colourList.length], - skipTracking - ) - ); + if (foundedMolecule) { + foundedMolecule = Object.assign({ isInspiration: isInspiration }, foundedMolecule); + + dispatch( + removeHitProtein( + majorViewStage, + foundedMolecule, + colourList[foundedMolecule.id % colourList.length], + skipTracking + ) + ); + } }); complexList?.forEach(moleculeID => { let foundedMolecule = joinedMoleculeLists?.find(mol => mol.id === moleculeID); - foundedMolecule = foundedMolecule && Object.assign({ isInspiration: isInspiration }, foundedMolecule); - dispatch( - removeComplex(majorViewStage, foundedMolecule, colourList[foundedMolecule.id % colourList.length], skipTracking) - ); + + if (foundedMolecule) { + foundedMolecule = Object.assign({ isInspiration: isInspiration }, foundedMolecule); + + dispatch( + removeComplex(majorViewStage, foundedMolecule, colourList[foundedMolecule.id % colourList.length], skipTracking) + ); + } }); fragmentDisplayList?.forEach(moleculeID => { let foundedMolecule = joinedMoleculeLists?.find(mol => mol.id === moleculeID); - foundedMolecule = foundedMolecule && Object.assign({ isInspiration: isInspiration }, foundedMolecule); - dispatch(removeLigand(majorViewStage, foundedMolecule, skipTracking)); + + if (foundedMolecule) { + foundedMolecule = Object.assign({ isInspiration: isInspiration }, foundedMolecule); + + dispatch(removeLigand(majorViewStage, foundedMolecule, skipTracking)); + } }); surfaceList?.forEach(moleculeID => { let foundedMolecule = joinedMoleculeLists?.find(mol => mol.id === moleculeID); - foundedMolecule = foundedMolecule && Object.assign({ isInspiration: isInspiration }, foundedMolecule); - dispatch( - removeSurface(majorViewStage, foundedMolecule, colourList[foundedMolecule.id % colourList.length], skipTracking) - ); + + if (foundedMolecule) { + foundedMolecule = Object.assign({ isInspiration: isInspiration }, foundedMolecule); + + dispatch( + removeSurface(majorViewStage, foundedMolecule, colourList[foundedMolecule.id % colourList.length], skipTracking) + ); + } }); qualityList?.forEach(moleculeID => { let foundedMolecule = joinedMoleculeLists?.find(mol => mol.id === moleculeID); - foundedMolecule = foundedMolecule && Object.assign({ isInspiration: isInspiration }, foundedMolecule); - dispatch( - removeQuality(majorViewStage, foundedMolecule, colourList[foundedMolecule.id % colourList.length], skipTracking) - ); + + if (foundedMolecule) { + foundedMolecule = Object.assign({ isInspiration: isInspiration }, foundedMolecule); + + dispatch( + removeQuality(majorViewStage, foundedMolecule, colourList[foundedMolecule.id % colourList.length], skipTracking) + ); + } }); densityList?.forEach(moleculeID => { let foundedMolecule = joinedMoleculeLists?.find(mol => mol.id === moleculeID); - foundedMolecule = foundedMolecule && Object.assign({ isInspiration: isInspiration }, foundedMolecule); - dispatch( - removeDensity(majorViewStage, foundedMolecule, colourList[foundedMolecule.id % colourList.length], skipTracking) - ); + + if (foundedMolecule) { + foundedMolecule = Object.assign({ isInspiration: isInspiration }, foundedMolecule); + + dispatch( + removeDensity(majorViewStage, foundedMolecule, colourList[foundedMolecule.id % colourList.length], skipTracking) + ); + } }); densityListCustom?.forEach(moleculeID => { let foundedMolecule = joinedMoleculeLists?.find(mol => mol.id === moleculeID); - foundedMolecule = foundedMolecule && Object.assign({ isInspiration: isInspiration }, foundedMolecule); - dispatch( - removeDensity(majorViewStage, foundedMolecule, colourList[foundedMolecule.id % colourList.length], skipTracking) - ); + + if (foundedMolecule) { + foundedMolecule = Object.assign({ isInspiration: isInspiration }, foundedMolecule); + + dispatch( + removeDensity(majorViewStage, foundedMolecule, colourList[foundedMolecule.id % colourList.length], skipTracking) + ); + } }); vectorOnList?.forEach(moleculeID => { let foundedMolecule = joinedMoleculeLists?.find(mol => mol.id === moleculeID); - foundedMolecule = foundedMolecule && Object.assign({ isInspiration: isInspiration }, foundedMolecule); - dispatch(removeVector(majorViewStage, foundedMolecule, skipTracking)); + + if (foundedMolecule) { + foundedMolecule = Object.assign({ isInspiration: isInspiration }, foundedMolecule); + + dispatch(removeVector(majorViewStage, foundedMolecule, skipTracking)); + } }); }; @@ -884,7 +917,7 @@ export const removeAllSelectedMolTypes = (majorViewStage, molecules, skipTrackin // return Promise.resolve(resultMolGroupID); // }); -export const applyDirectSelection = (stage, stageSummaryView) => (dispatch, getState) => { +export const applyDirectSelection = stage => (dispatch, getState) => { const state = getState(); const directDisplay = state.apiReducers.direct_access; @@ -898,10 +931,9 @@ export const applyDirectSelection = (stage, stageSummaryView) => (dispatch, getS if (!directAccessProcessed && directDisplay && directDisplay.molecules && directDisplay.molecules.length > 0) { const allMols = state.apiReducers.all_mol_lists; directDisplay.molecules.forEach(m => { - let keys = Object.keys(allMols); let directProteinNameModded = m.name.toLowerCase(); let directProteinCodeModded = `${directDisplay.target.toLowerCase()}-${directProteinNameModded}`; - for (let molIndex = 0; molIndex < keys.length; molIndex++) { + for (let molIndex = 0; molIndex < allMols.length; molIndex++) { let molList = allMols; let mol = molList[molIndex]; let proteinCodeModded = mol.protein_code.toLowerCase(); diff --git a/js/components/preview/tags/api/tagsApi.js b/js/components/preview/tags/api/tagsApi.js index 868e11050..2945d91a1 100644 --- a/js/components/preview/tags/api/tagsApi.js +++ b/js/components/preview/tags/api/tagsApi.js @@ -75,3 +75,17 @@ export const getTagByName = tagName => { } }); }; + +export const deleteExistingTag = (tag, tagId) => { + const jsonString = JSON.stringify(tag); + let url = `${base_url}/api/molecule_tag/${tagId}/`; + return api({ + url: url, + method: METHOD.DELETE, + data: jsonString + }) + .then(resp => { + return resp.data; + }) + .catch(err => console.log(err)); +}; diff --git a/js/components/preview/tags/details/newTagDetailRow.js b/js/components/preview/tags/details/newTagDetailRow.js new file mode 100644 index 000000000..d56216f60 --- /dev/null +++ b/js/components/preview/tags/details/newTagDetailRow.js @@ -0,0 +1,233 @@ +import React, { memo, useState, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { CATEGORY_TYPE, CATEGORY_ID, CATEGORY_TYPE_BY_ID } from '../../../../constants/constants'; +import { ColorPicker } from '../../../common/Components/ColorPicker'; +import { + DEFAULT_TAG_COLOR, + augumentTagObjectWithId, + createMoleculeTagObject +} from '../utils/tagUtils'; +import { DJANGO_CONTEXT } from '../../../../utils/djangoContext'; +import { updateTagProp, removeSelectedTag } from '../redux/dispatchActions'; +import { + Grid, + TextField, + makeStyles, + Button, + Select, + MenuItem +} from '@material-ui/core'; +import { createNewTag, deleteExistingTag } from '../api/tagsApi'; +import { appendTagList, setTagToEdit, removeFromTagList } from '../../../../reducers/selection/actions'; +import { appendMoleculeTag, updateMoleculeInMolLists } from '../../../../reducers/api/actions'; + +const useStyles = makeStyles(theme => ({ + divContainer: { + flexDirection: 'row', + display: 'flex', + height: '100%', + width: '100%', + paddingTop: theme.spacing(1) / 2, + marginRight: '1px', + marginLeft: '1px' + }, + select: { + color: 'inherit', + fill: 'inherit', + '&:hover:not(.Mui-disabled):before': { + borderColor: 'inherit' + }, + '&:before': { + borderColor: 'inherit' + }, + '&:not(.Mui-disabled)': { + fill: theme.palette.white + } + } +})); + +/** + * NewTagDetailRow provides possibility to create and edit tags in TagDetails panel + */ +const NewTagDetailRow = memo(({ moleculesToEditIds, moleculesToEdit }) => { + const classes = useStyles(); + const dispatch = useDispatch(); + + const targetName = useSelector(state => state.apiReducers.target_on_name); + const targetId = useSelector(state => state.apiReducers.target_on); + const tagToEdit = useSelector(state => state.selectionReducers.tagToEdit); + const allMolList = useSelector(state => state.apiReducers.all_mol_lists); + + const [newTagCategory, setNewTagCategory] = useState(0); + const [newTagColor, setNewTagColor] = useState(DEFAULT_TAG_COLOR); + const [newTagName, setNewTagName] = useState(''); + const [newTagLink, setNewTagLink] = useState(''); + + useEffect(() => { + if (tagToEdit) { + setNewTagCategory(tagToEdit.category_id); + if (tagToEdit.colour) setNewTagColor(tagToEdit.colour); + setNewTagName(tagToEdit.tag); + setNewTagLink(tagToEdit.discourse_url); + } + }, [tagToEdit]); + + const resetTagToEditState = () => { + dispatch(setTagToEdit(null)); + setNewTagCategory(0); + setNewTagColor(DEFAULT_TAG_COLOR); + setNewTagName(''); + setNewTagLink(''); + }; + + const onCategoryForNewTagChange = event => { + setNewTagCategory(event.target.value); + // apply also default color for every categpry (TODO move values to constants?) + let defaultColor = DEFAULT_TAG_COLOR; + switch (CATEGORY_TYPE_BY_ID[event.target.value]) { + case CATEGORY_TYPE.SITE: + defaultColor = "#d7191c"; + break; + case CATEGORY_TYPE.SERIES: + defaultColor = "#fdae61"; + break; + case CATEGORY_TYPE.FORUM: + defaultColor = "#abd9e9"; + break; + case CATEGORY_TYPE.OTHER: + defaultColor = "#2c7bb6"; + break; + default: + break; + } + setNewTagColor(defaultColor); + }; + + const onNameForNewTagChange = event => { + setNewTagName(event.target.value); + }; + + const createTag = () => { + if (newTagName && newTagCategory) { + const newTag = { tag: newTagName, colour: newTagColor, category_id: newTagCategory, discourse_url: newTagLink }; + const tagObject = createMoleculeTagObject( + newTagName, + moleculesToEdit.length ? moleculesToEdit[0].proteinData.target_id : targetId, + newTagCategory, + DJANGO_CONTEXT.pk, + newTagColor, + newTagLink, + [...moleculesToEditIds] + ); + createNewTag(tagObject, targetName).then(molTag => { + let augMolTagObject = augumentTagObjectWithId(newTag, molTag.id); + dispatch(appendTagList(augMolTagObject)); + dispatch(appendMoleculeTag(molTag)); + }); + // reset tag/fields after creating new one + resetTagToEditState(); + } + }; + + const updateTag = () => { + if (tagToEdit && newTagCategory && newTagName) { + // update all props at once + dispatch(updateTagProp(Object.assign({}, tagToEdit, { + category_id: newTagCategory, + colour: newTagColor, + tag: newTagName, + discourse_url: newTagLink + }), newTagName, "tag")); + // reset tag/fields after updating selected one + resetTagToEditState(); + } + }; + + const deleteTag = () => { + dispatch(removeSelectedTag(tagToEdit)); + dispatch(removeFromTagList(tagToEdit)); + // remove from all molecules + const molsForTag = allMolList.filter(mol => { + const tags = mol.tags_set.filter(id => id === tagToEdit.id); + return tags && tags.length ? true : false; + }); + if (molsForTag && molsForTag.length) { + molsForTag.forEach(m => { + let newMol = { ...m }; + newMol.tags_set = newMol.tags_set.filter(id => id !== tagToEdit.id); + dispatch(updateMoleculeInMolLists(newMol)); + }); + } + deleteExistingTag(tagToEdit, tagToEdit.id); + // reset tag/fields after removing selected tag + resetTagToEditState(); + }; + + return ( + + + + + + { + setNewTagColor(value); + }} + disabled={!DJANGO_CONTEXT.pk} + /> + + + + + + {tagToEdit ? + + + + + + + + + : + + + + } + + + ); +}); + +export default NewTagDetailRow; diff --git a/js/components/preview/tags/details/tagDetailRow.js b/js/components/preview/tags/details/tagDetailRow.js new file mode 100644 index 000000000..61b8124e3 --- /dev/null +++ b/js/components/preview/tags/details/tagDetailRow.js @@ -0,0 +1,266 @@ +import React, { memo, useState, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { CATEGORY_TYPE_BY_ID } from '../../../../constants/constants'; +import TagView from '../tagView'; +import { + getDefaultTagDiscoursePostText +} from '../utils/tagUtils'; +import { DJANGO_CONTEXT } from '../../../../utils/djangoContext'; +import { + updateTagProp, + selectTag, + unselectTag, + removeSelectedTag, + addSelectedTag +} from '../redux/dispatchActions'; +import { + Grid, + Tooltip, + makeStyles, + Button, + Typography, + IconButton +} from '@material-ui/core'; +import { Edit } from '@material-ui/icons'; +import { isURL } from '../../../../utils/common'; +import classNames from 'classnames'; +import { createTagPost, isDiscourseAvailable } from '../../../../utils/discourse'; +import { setTagToEdit, appendToMolListToEdit, removeFromMolListToEdit } from '../../../../reducers/selection/actions'; + +const useStyles = makeStyles(theme => ({ + contColButton: { + minWidth: 'fit-content', + paddingLeft: theme.spacing(1) / 4, + paddingRight: theme.spacing(1) / 4, + paddingBottom: 0, + paddingTop: 0, + fontWeight: 'bold', + fontSize: 9, + borderRadius: 7, + borderColor: theme.palette.primary.main, + backgroundColor: theme.palette.primary.light, + '&:disabled': { + borderRadius: 0, + borderColor: 'white' + }, + '&:hover': { + backgroundColor: theme.palette.primary.light + } + }, + contColButtonSelected: { + backgroundColor: theme.palette.primary.main, + color: theme.palette.primary.contrastText, + '&:hover': { + backgroundColor: theme.palette.primary.main + } + }, + contColButtonHalfSelected: { + backgroundColor: theme.palette.primary.semidark, + color: theme.palette.primary.contrastText, + '&:hover': { + backgroundColor: theme.palette.primary.light, + color: theme.palette.black + } + }, + divContainer: { + flexDirection: 'row', + display: 'flex', + // height: '100%', + width: '100%' + /*paddingTop: theme.spacing(1) / 2, + marginRight: '1px', + marginLeft: '1px'*/ + }, + editButton: { + backgroundColor: theme.palette.success.light, + color: theme.palette.success.contrastText, + '&:hover': { + backgroundColor: theme.palette.success.dark, + color: theme.palette.success.contrastText + } + } +})); + +/** + * TagDetailRow represents a row of TagDetails panel summary + */ +const TagDetailRow = memo(({ tag, moleculesToEditIds, moleculesToEdit }) => { + const classes = useStyles(); + const dispatch = useDispatch(); + const [allMoleculesOfTag, setAllMoleculesOfTag] = useState([]); + + const targetName = useSelector(state => state.apiReducers.target_on_name); + const selectedTagList = useSelector(state => state.selectionReducers.selectedTagList); + const allMolList = useSelector(state => state.apiReducers.all_mol_lists); + + useEffect(() => { + if (allMolList.length) { + setAllMoleculesOfTag(allMolList.filter(mol => { + const tags = mol.tags_set.filter(id => id === tag.id); + return tags && tags.length ? true : false; + })); + } + }, [allMolList, tag]); + + const handleSelectHits = () => { + if (hasSelectedMolecule(tag)) { + // deselect all + allMoleculesOfTag.forEach(mol => { + if (moleculesToEditIds.includes(mol.id)) { + dispatch(removeFromMolListToEdit(mol.id)); + } + }); + dispatch(unselectTag(tag)); + } else { + // select all + allMoleculesOfTag.forEach(mol => { + if (!moleculesToEditIds.includes(mol.id)) { + dispatch(appendToMolListToEdit(mol.id)); + } + }); + dispatch(selectTag(tag)); + } + }; + + const hasSelectedMolecule = () => { + let result = false; + for (let i = 0; i < moleculesToEdit.length; i++) { + const mol = moleculesToEdit[i]; + if (mol.tags_set.some(id => id === tag.id)) { + result = true; + break; + } + } + return result; + } + + const handleTagClick = (selected, tag) => { + if (selected) { + dispatch(removeSelectedTag(tag)); + } else { + dispatch(addSelectedTag(tag)); + } + }; + + const handleEditTag = (tag) => { + dispatch(setTagToEdit(tag)); + }; + + return ( + + + i.id === tag.id)} + handleClick={handleTagClick} + disabled={!DJANGO_CONTEXT.pk} + isEdit={true} + isTagEditor={true} + > + + + + + {CATEGORY_TYPE_BY_ID[tag.category_id]} + + + + + + + + + + + + + + + + + + + + {tag.user_id} + + + + + {navigator.language ? + (new Date(tag.create_date)).toLocaleDateString(navigator.language) : + (new Date(tag.create_date)).toLocaleDateString() + } + + + + {/* + + + {theme.palette.error.light} + */} + handleEditTag(tag)} + disabled={!DJANGO_CONTEXT.pk} + aria-label="edit tag" + > + + + + + + + ); +}); + +export default TagDetailRow; diff --git a/js/components/preview/tags/details/tagDetails.js b/js/components/preview/tags/details/tagDetails.js new file mode 100644 index 000000000..a8939f078 --- /dev/null +++ b/js/components/preview/tags/details/tagDetails.js @@ -0,0 +1,343 @@ +import React, { memo, useRef, useEffect, useCallback, useState } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { + Grid, + Typography, + makeStyles, + IconButton, + Tooltip +} from '@material-ui/core'; +import { Panel } from '../../../common/Surfaces/Panel'; +import TagDetailRow from './tagDetailRow'; +import NewTagDetailRow from './newTagDetailRow'; +import { + compareTagsAsc, + compareTagsDesc, + compareTagsByCategoryAsc, + compareTagsByCategoryDesc, + compareTagsByCreatorAsc, + compareTagsByCreatorDesc, + compareTagsByDateAsc, + compareTagsByDateDesc +} from '../utils/tagUtils'; +import { Sort, ArrowDropDown, ArrowDropUp } from '@material-ui/icons'; +import { + getMoleculeForId +} from '../redux/dispatchActions'; + +export const heightOfBody = '164px'; +export const defaultHeaderPadding = 15; + +const useStyles = makeStyles(theme => ({ + containerExpanded: { + height: heightOfBody, + display: 'flex', + flexDirection: 'column', + resize: 'vertical', + overflow: 'hidden', + width: '100%' + }, + headerRow: { + maxHeight: "20%" + }, + headerTitleCreator: { + marginLeft: -6 + }, + headerTitleCategory: { + marginLeft: -10 + }, + headerButtonCategory: { + marginLeft: 5 + }, + headerButtonTitle: { + marginLeft: -5 + }, + newTagRow: { + maxHeight: "30%" + }, + tagListWrapper: { + overflowY: 'auto', + overflowX: 'hidden', + height: '100%' + } +})); + +/** + * TagDetails is a wrapper panel for tags summary, their editing and creating new ones + */ +const TagDetails = memo(({ handleHeightChange }) => { + const classes = useStyles(); + const ref = useRef(null); + const elementRef = useRef(null); + const dispatch = useDispatch(); + const [headerPadding, setheaderPadding] = useState(defaultHeaderPadding); + const [elementHeight, setElementHeight] = useState(0); + const [sortSwitch, setSortSwitch] = useState(0); + + const preTagList = useSelector(state => state.selectionReducers.tagList); + const [tagList, setTagList] = useState([]); + + useEffect(() => { + setTagList([...preTagList].sort(compareTagsAsc)); + return () => { setTagList([]) }; + }, [preTagList]); + + const moleculesToEditIds = useSelector(state => state.selectionReducers.moleculesToEdit); + const moleculesToEdit = moleculesToEditIds && moleculesToEditIds.length > 0 && !(moleculesToEditIds.length === 1 && moleculesToEditIds[0] === null) ? moleculesToEditIds.map(id => dispatch(getMoleculeForId(id))) : []; + + /*const moleculesToEditIds = useSelector(state => state.selectionReducers.moleculesToEdit); + const [moleculesToEdit, setMoleculesToEdit] = useState([]); + useEffect(() => { + if (moleculesToEditIds && moleculesToEditIds.length > 0 && !(moleculesToEditIds.length === 1 && moleculesToEditIds[0] === null)) { + setMoleculesToEdit(moleculesToEditIds.map(id => dispatch(getMoleculeForId(id)))); + } else { + setMoleculesToEdit([]); + } + return () => { setMoleculesToEdit([]) }; + }, [moleculesToEditIds, dispatch]);*/ + + useEffect(() => { + const element = elementRef.current; + if (element) { + element.addEventListener('resize', handleResize); + const observer = new MutationObserver(checkResize); + observer.observe(element, { attributes: true, attributeOldValue: true, attributeFilter: ['style'] }); + } + + return () => { + if (element) { + element.removeEventListener('resize', handleResize); + } + }; + }, [elementRef, handleResize, checkResize]); + + useEffect(() => { + handleScroll(elementRef.current?.childNodes[1], headerPadding); + }, [elementRef, handleScroll, headerPadding, elementHeight]); + + const offsetName = 10; + const offsetCategory = 20; + const offsetCreator = 30; + const offsetDate = 40; + const handleHeaderSort = useCallback((type) => { + switch (type) { + case "name": + if (sortSwitch === offsetName + 1) { + // change direction + setTagList([...tagList].sort(compareTagsAsc)); + setSortSwitch(sortSwitch + 1); + } else if (sortSwitch === offsetName + 2) { + // reset sort + setTagList([...tagList].sort(compareTagsAsc)); + setSortSwitch(0); + } else { + // start sorting + setTagList([...tagList].sort(compareTagsDesc)); + setSortSwitch(offsetName + 1); + } + break; + case "category": + if (sortSwitch === offsetCategory + 1) { + // change direction + setTagList([...tagList].sort(compareTagsByCategoryAsc)); + setSortSwitch(sortSwitch + 1); + } else if (sortSwitch === offsetCategory + 2) { + // reset sort + setTagList([...tagList].sort(compareTagsAsc)); + setSortSwitch(0); + } else { + // start sorting + setTagList([...tagList].sort(compareTagsByCategoryDesc)); + setSortSwitch(offsetCategory + 1); + } + break; + case "creator": + if (sortSwitch === offsetCreator + 1) { + // change direction + setTagList([...tagList].sort(compareTagsByCreatorAsc)); + setSortSwitch(sortSwitch + 1); + } else if (sortSwitch === offsetCreator + 2) { + // reset sort + setTagList([...tagList].sort(compareTagsAsc)); + setSortSwitch(0); + } else { + // start sorting + setTagList([...tagList].sort(compareTagsByCreatorDesc)); + setSortSwitch(offsetCreator + 1); + } + break; + case "date": + if (sortSwitch === offsetDate + 1) { + // change direction + setTagList([...tagList].sort(compareTagsByDateAsc)); + setSortSwitch(sortSwitch + 1); + } else if (sortSwitch === offsetDate + 2) { + // reset sort + setTagList([...tagList].sort(compareTagsAsc)); + setSortSwitch(0); + } else { + // start sorting + setTagList([...tagList].sort(compareTagsByDateDesc)); + setSortSwitch(offsetDate + 1); + } + break; + default: + // tagList = tagList.sort(compareTagsAsc); + break; + } + }, [sortSwitch, tagList]); + + const handleResize = useCallback( + event => { + //console.log('resize ' + ref.current.clientHeight); + handleHeightChange(ref.current.offsetHeight); + }, + [handleHeightChange] + ); + + const handleScroll = useCallback( + (el, h) => { + if (el) { + const hasVerticalScrollbar = el.scrollHeight > el.clientHeight; + if (!hasVerticalScrollbar) { + if (h !== 0) { + setheaderPadding(0); + } + } else { + if (h !== defaultHeaderPadding) { + setheaderPadding(defaultHeaderPadding); + } + } + } + }, + [setheaderPadding] + ); + + const checkResize = useCallback( + mutations => { + const el = mutations[0].target; + const w = el.clientWidth; + const h = el.clientHeight; + + if (elementHeight !== h) { + setElementHeight(h); + + const event = new CustomEvent('resize', { detail: { width: w, height: h } }); + el.dispatchEvent(event); + } + }, + [elementHeight] + ); + + return ( + { + if (ref.current && handleHeightChange instanceof Function) { + handleHeightChange(ref.current.offsetHeight); + } + }} + > + + + + + + Tag name + + + + handleHeaderSort("name")} + > + + {[1, 2].includes(sortSwitch - offsetName) ? sortSwitch % offsetName < 2 ? : : } + + + + + + + + Category + + + + handleHeaderSort("category")} + > + + {[1, 2].includes(sortSwitch - offsetCategory) ? sortSwitch % offsetCategory < 2 ? : : } + + + + + + + + + + + + + + + + Creator + + + + handleHeaderSort("creator")} + > + + {[1, 2].includes(sortSwitch - offsetCreator) ? sortSwitch % offsetCreator < 2 ? : : } + + + + + + + + Date + + + + handleHeaderSort("date")} + > + + {[1, 2].includes(sortSwitch - offsetDate) ? sortSwitch % offsetDate < 2 ? : : } + + + + + + + + + + + {/*
*/} + {tagList && tagList.map((tag, idx) => { + return (); + })} + {/*
*/} +
+ + + +
+
+ ); +}); + +export default TagDetails; diff --git a/js/components/preview/tags/modal/tagEditor.js b/js/components/preview/tags/modal/tagEditor.js index a37cdbc16..b5ac53524 100644 --- a/js/components/preview/tags/modal/tagEditor.js +++ b/js/components/preview/tags/modal/tagEditor.js @@ -1,48 +1,28 @@ -import React, { forwardRef, memo, useState } from 'react'; +import React, { forwardRef, memo } from 'react'; import { Grid, Popper, IconButton, - InputAdornment, - TextField, Tooltip, - makeStyles, - Button, - useTheme, - Select, - MenuItem + makeStyles } from '@material-ui/core'; import { Panel } from '../../../common'; -import { ColorPicker } from '../../../common/Components/ColorPicker'; -import { Close, Search } from '@material-ui/icons'; +import { Close } from '@material-ui/icons'; import { useDispatch, useSelector } from 'react-redux'; -import { debounce } from 'lodash'; -import classNames from 'classnames'; -import TagView from '../tagView'; -import { updateMoleculeInMolLists, updateMoleculeTag, appendMoleculeTag } from '../../../../reducers/api/actions'; +import { updateMoleculeInMolLists, updateMoleculeTag } from '../../../../reducers/api/actions'; import { - displayInListForTag, - hideInListForTag, - updateTagProp, - getMoleculeForId, - selectTag, - unselectTag + getMoleculeForId } from '../redux/dispatchActions'; -import { CATEGORY_TYPE, CATEGORY_ID } from '../../../../constants/constants'; -import { appendTagList, setMoleculeForTagEdit, setIsTagGlobalEdit } from '../../../../reducers/selection/actions'; -import { createNewTag, updateExistingTag } from '../api/tagsApi'; +import { setMoleculeForTagEdit, setIsTagGlobalEdit } from '../../../../reducers/selection/actions'; +import { updateExistingTag } from '../api/tagsApi'; import { DJANGO_CONTEXT } from '../../../../utils/djangoContext'; import { compareTagsAsc, - DEFAULT_TAG_COLOR, augumentTagObjectWithId, createMoleculeTagObject, - getMoleculeTagForTag, - getAllTagsForMol, - getDefaultTagDiscoursePostText + getMoleculeTagForTag } from '../utils/tagUtils'; -import { isURL } from '../../../../utils/common'; -import { createTagPost, isDiscourseAvailable } from '../../../../utils/discourse'; +import TagCategory from '../tagCategory'; const useStyles = makeStyles(theme => ({ paper: { @@ -130,17 +110,9 @@ export const TagEditor = memo( const id = open ? 'simple-popover-mols-tag-editor' : undefined; const classes = useStyles(); const dispatch = useDispatch(); - const [searchString, setSearchString] = useState(null); - const [newTagCategory, setNewTagCategory] = useState(1); - const [newTagColor, setNewTagColor] = useState(DEFAULT_TAG_COLOR); - const [newTagName, setNewTagName] = useState(null); - const [newTagLink, setNewTagLink] = useState(''); - let tagList = useSelector(state => state.selectionReducers.tagList); let moleculeTags = useSelector(state => state.apiReducers.moleculeTags); - const displayAllInList = useSelector(state => state.selectionReducers.listAllList); const isTagGlobalEdit = useSelector(state => state.selectionReducers.isGlobalEdit); const molId = useSelector(state => state.selectionReducers.molForTagEdit); - const targetName = useSelector(state => state.apiReducers.target_on_name); let moleculesToEditIds = useSelector(state => state.selectionReducers.moleculesToEdit); if (!isTagGlobalEdit) { moleculesToEditIds = []; @@ -148,18 +120,9 @@ export const TagEditor = memo( } const moleculesToEdit = moleculesToEditIds.map(id => dispatch(getMoleculeForId(id))); - tagList = tagList.sort(compareTagsAsc); moleculeTags = moleculeTags.sort(compareTagsAsc); - const resetNewTagFields = () => { - setNewTagCategory(1); - setNewTagColor(DEFAULT_TAG_COLOR); - setNewTagName(null); - setNewTagLink(''); - }; - const handleCloseModal = () => { - setSearchString(null); if (open) { dispatch(setOpenDialog(false)); dispatch(setMoleculeForTagEdit(null)); @@ -167,19 +130,6 @@ export const TagEditor = memo( } }; - const isTagSelected = tag => { - let result = false; - for (let i = 0; i < moleculesToEdit.length; i++) { - const m = moleculesToEdit[i]; - const tagsForMol = getAllTagsForMol(m, tagList); - if (tagsForMol && tagsForMol.some(t => t.id === tag.id)) { - result = true; - break; - } - } - return result; - }; - const handleTagClick = (selected, tag) => { let molTagObjects = []; if (selected) { @@ -235,255 +185,24 @@ export const TagEditor = memo( } }; - let filteredTags = tagList; - if (searchString !== null) { - filteredTags = tagList.filter( - tag => isTagSelected(tag) || tag.tag.toLowerCase().includes(searchString.toLowerCase()) - ); - } - - let debouncedFn; - - const handleSearch = event => { - /* signal to React not to nullify the event object */ - event.persist(); - if (!debouncedFn) { - debouncedFn = debounce(() => { - setSearchString(event.target.value !== '' ? event.target.value : null); - }, 350); - } - debouncedFn(); - }; - - const handleDisplayAllInList = tag => { - if (isTagDislayedInList(tag)) { - dispatch(hideInListForTag(tag)); - dispatch(unselectTag(tag)); - } else { - dispatch(displayInListForTag(tag)); - dispatch(selectTag(tag)); - } - }; - - const isTagDislayedInList = tag => { - return displayAllInList.includes(tag.id); - }; - - const onCategoryForNewTagChange = event => { - setNewTagCategory(event.target.value); - }; - - const onNameForNewTagChange = event => { - setNewTagName(event.target.value); - }; - - const createTag = () => { - if (newTagName) { - const newTag = { tag: newTagName, colour: newTagColor, category_id: newTagCategory, discourse_url: newTagLink }; - const tagObject = createMoleculeTagObject( - newTagName, - moleculesToEdit[0].proteinData.target_id, - newTagCategory, - DJANGO_CONTEXT.pk, - newTagColor, - newTagLink, - [...moleculesToEditIds] - ); - createNewTag(tagObject, targetName).then(molTag => { - let augMolTagObject = augumentTagObjectWithId(newTag, molTag.id); - dispatch(appendTagList(augMolTagObject)); - dispatch(appendMoleculeTag(molTag)); - }); - // resetNewTagFields(); - } - }; - - const onUpdateTag = (tag, value, prop) => { - if (value) { - dispatch(updateTagProp(tag, value, prop)); - } - }; - return ( - - - ) - }} - onChange={handleSearch} - disabled={false} - />, - + ]} > -
- - - - - - - { - setNewTagColor(value); - }} - disabled={!DJANGO_CONTEXT.pk} - /> - - - - - - - - - - - {filteredTags && - filteredTags.map((tag, idx) => { - return ( - - - - - - { - onUpdateTag(tag, value, 'colour'); - }} - /> - - - - - - - - - - - - - - - - - - - - ); - })} - -
+ + +
); diff --git a/js/components/preview/tags/tagCategory.js b/js/components/preview/tags/tagCategory.js index 15296e395..09f993013 100644 --- a/js/components/preview/tags/tagCategory.js +++ b/js/components/preview/tags/tagCategory.js @@ -15,7 +15,7 @@ const useStyles = makeStyles(theme => ({ } })); -const TagCategory = memo(({ headerPadding = 0 }) => { +const TagCategory = memo(({ headerPadding = 0, tagClickCallback }) => { const classes = useStyles(); const categoryList = useSelector(state => state.selectionReducers.categoryList); @@ -44,10 +44,10 @@ const TagCategory = memo(({ headerPadding = 0 }) => {
- - - - + + + + ); diff --git a/js/components/preview/tags/tagCategoryView.js b/js/components/preview/tags/tagCategoryView.js index 04eaea384..f9e7f78f3 100644 --- a/js/components/preview/tags/tagCategoryView.js +++ b/js/components/preview/tags/tagCategoryView.js @@ -3,7 +3,8 @@ import { Grid, makeStyles, Typography } from '@material-ui/core'; import { useDispatch } from 'react-redux'; import { useSelector } from 'react-redux'; import TagView from './tagView'; -import { removeSelectedTag, addSelectedTag } from './redux/dispatchActions'; +import { removeSelectedTag, addSelectedTag, getMoleculeForId } from './redux/dispatchActions'; +import { getAllTagsForMol } from './utils/tagUtils'; const useStyles = makeStyles(theme => ({ divContainer: { @@ -23,18 +24,51 @@ const useStyles = makeStyles(theme => ({ } })); -const TagCategoryView = memo(({ name, tags, specialTags }) => { +/** + * TagCategoryView has two ways of behavior depending if clickCallback is defined or not: + * -if is- behaves as Assign tag element and assignes tags to hits + * -if is NOT- behaves as Hit filter element and filters hits in Hit navigator + */ +const TagCategoryView = memo(({ name, tags, specialTags, clickCallback }) => { const classes = useStyles(); const selectedTagList = useSelector(state => state.selectionReducers.selectedTagList); const dispatch = useDispatch(); + const tagList = useSelector(state => state.selectionReducers.tagList); + const isTagGlobalEdit = useSelector(state => state.selectionReducers.isGlobalEdit); + const molId = useSelector(state => state.selectionReducers.molForTagEdit); + let moleculesToEditIds = useSelector(state => state.selectionReducers.moleculesToEdit); + if (!isTagGlobalEdit) { + moleculesToEditIds = []; + moleculesToEditIds.push(molId); + } + const moleculesToEdit = moleculesToEditIds && moleculesToEditIds.length > 0 && !(moleculesToEditIds.length === 1 && moleculesToEditIds[0] === null) ? moleculesToEditIds.map(id => dispatch(getMoleculeForId(id))) : []; + const handleTagClick = (selected, tag, allTags) => { - if (selected) { - dispatch(removeSelectedTag(tag)); + if (clickCallback !== undefined) { + clickCallback(selected, tag); } else { - dispatch(addSelectedTag(tag)); + if (selected) { + dispatch(removeSelectedTag(tag)); + } else { + dispatch(addSelectedTag(tag)); + } + } + }; + + const isTagSelected = tag => { + let result = false; + for (let i = 0; i < moleculesToEdit.length; i++) { + const m = moleculesToEdit[i]; + const tagsForMol = getAllTagsForMol(m, tagList); + if (tagsForMol && tagsForMol.some(t => t.id === tag.id)) { + result = true; + break; + } } + return result; }; + return ( <> @@ -52,7 +86,12 @@ const TagCategoryView = memo(({ name, tags, specialTags }) => { tags.map((tag, idx) => { let selected = selectedTagList.some(i => i.id === tag.id); return ( - + ); })} diff --git a/js/components/preview/tags/tagSelector.js b/js/components/preview/tags/tagSelector.js index cba3df0ae..521590f59 100644 --- a/js/components/preview/tags/tagSelector.js +++ b/js/components/preview/tags/tagSelector.js @@ -1,5 +1,5 @@ import React, { memo, useRef, useEffect, useCallback, useState } from 'react'; -import { Grid, makeStyles, Switch, FormControlLabel } from '@material-ui/core'; +import { Grid, makeStyles, Switch, FormControlLabel, Tooltip } from '@material-ui/core'; import { Delete, DoneAll } from '@material-ui/icons'; import { Panel } from '../../common/Surfaces/Panel'; import { Button } from '../../common/Inputs/Button'; @@ -30,6 +30,9 @@ const useStyles = makeStyles(theme => ({ }, selectorItem: { height: '100%' + }, + tagModeSwitch: { + width: 132 // Should be adjusted if a label for the switch changes } })); @@ -136,22 +139,31 @@ const TagSelector = memo(({ handleHeightChange }) => { hasHeader hasExpansion defaultExpanded - title="Tag selector" + title="Hit List Filter" headerActions={[ - } - label={tagMode ? 'Exclusive' : 'Inclusive'} - />, + + } + label={tagMode ? 'Intersection' : 'Union'} + /> + , ]} onExpandChange={expand => { diff --git a/js/components/preview/tags/tagView.js b/js/components/preview/tags/tagView.js index 6d1843d7a..350236e41 100644 --- a/js/components/preview/tags/tagView.js +++ b/js/components/preview/tags/tagView.js @@ -7,7 +7,7 @@ import { TagEditModal } from './modal/tagEditModal'; const useStyles = makeStyles(theme => ({ tagItem: { - paddingBottom: 6 + paddingBottom: 0 }, chip: { // maxWidth: '100%', diff --git a/js/components/preview/tags/utils/tagUtils.js b/js/components/preview/tags/utils/tagUtils.js index e2dfc82be..1007af134 100644 --- a/js/components/preview/tags/utils/tagUtils.js +++ b/js/components/preview/tags/utils/tagUtils.js @@ -1,4 +1,4 @@ -import { CATEGORY_ID, CATEGORY_TYPE } from '../../../../constants/constants'; +import { CATEGORY_ID, CATEGORY_TYPE, CATEGORY_TYPE_BY_ID } from '../../../../constants/constants'; export const DEFAULT_TAG_COLOR = '#E0E0E0'; @@ -39,6 +39,76 @@ export const compareTagsAsc = (a, b) => { return 0; }; +export const compareTagsDesc = (a, b) => { + if (a.tag > b.tag) { + return -1; + } + if (a.tag < b.tag) { + return 1; + } + return 0; +}; + +export const compareTagsByCategoryAsc = (a, b) => { + if (CATEGORY_TYPE_BY_ID[a.category_id] < CATEGORY_TYPE_BY_ID[b.category_id]) { + return -1; + } + if (CATEGORY_TYPE_BY_ID[a.category_id] > CATEGORY_TYPE_BY_ID[b.category_id]) { + return 1; + } + return 0; +}; + +export const compareTagsByCategoryDesc = (a, b) => { + if (CATEGORY_TYPE_BY_ID[a.category_id] > CATEGORY_TYPE_BY_ID[b.category_id]) { + return -1; + } + if (CATEGORY_TYPE_BY_ID[a.category_id] < CATEGORY_TYPE_BY_ID[b.category_id]) { + return 1; + } + return 0; +}; + +export const compareTagsByCreatorAsc = (a, b) => { + if (a.user_id < b.user_id) { + return -1; + } + if (a.user_id > b.user_id) { + return 1; + } + return 0; +}; + +export const compareTagsByCreatorDesc = (a, b) => { + if (a.user_id > b.user_id) { + return -1; + } + if (a.user_id < b.user_id) { + return 1; + } + return 0; +}; + +export const compareTagsByDateAsc = (a, b) => { + if (a.create_date < b.create_date) { + return -1; + } + if (a.create_date > b.create_date) { + return 1; + } + return 0; +}; + +export const compareTagsByDateDesc = (a, b) => { + if (a.create_date > b.create_date) { + return -1; + } + if (a.create_date < b.create_date) { + return 1; + } + return 0; +}; + export const getMoleculeTagForTag = (tagList, tagId) => { return tagList.find(t => t.id === tagId); }; diff --git a/js/constants/constants.js b/js/constants/constants.js index 4bc1411ab..f937c3a91 100644 --- a/js/constants/constants.js +++ b/js/constants/constants.js @@ -34,7 +34,7 @@ export const ARROW_TYPE = { export const CATEGORY_TYPE = { SITE: 'Sites', SERIES: 'Series', - FORUM: 'Forum', + FORUM: 'Discussion', OTHER: 'Other' }; @@ -45,6 +45,13 @@ export const CATEGORY_ID = { [CATEGORY_TYPE.OTHER]: 4 }; +export const CATEGORY_TYPE_BY_ID = { + 1: CATEGORY_TYPE.SITE, + 2: CATEGORY_TYPE.SERIES, + 3: CATEGORY_TYPE.FORUM, + 4: CATEGORY_TYPE.OTHER +}; + export const TAG_TYPE = { ALL: 'ALL', UNTAGGED: 'UNTAGGED' diff --git a/js/reducers/api/apiReducers.js b/js/reducers/api/apiReducers.js index 750d8e327..ae417adb0 100644 --- a/js/reducers/api/apiReducers.js +++ b/js/reducers/api/apiReducers.js @@ -12,7 +12,7 @@ export const INITIAL_STATE = { mol_group_list: [], molecule_list: [], cached_mol_lists: {}, - all_mol_lists: {}, + all_mol_lists: [], allMolecules: [], moleculeTags: [], duck_yank_data: {}, diff --git a/js/reducers/selection/actions.js b/js/reducers/selection/actions.js index d0546a833..4d479c1f1 100644 --- a/js/reducers/selection/actions.js +++ b/js/reducers/selection/actions.js @@ -471,6 +471,14 @@ export const appendTagList = function(item, skipTracking = false) { }; }; +export const removeFromTagList = function(item, skipTracking = false) { + return { + type: constants.REMOVE_FROM_TAG_LIST, + item: item, + skipTracking + }; +}; + export const setTagEditorOpen = isOpen => { return { type: constants.SET_TAG_EDITOR_OPEN, @@ -535,3 +543,10 @@ export const removeFromMolListToEdit = molId => { molId: molId }; }; + +export const setTagToEdit = tag => { + return { + type: constants.SET_TAG_TO_EDIT, + tagToEdit: tag + }; +}; diff --git a/js/reducers/selection/constants.js b/js/reducers/selection/constants.js index 4763d0dd0..91c296959 100644 --- a/js/reducers/selection/constants.js +++ b/js/reducers/selection/constants.js @@ -82,7 +82,9 @@ export const constants = { SET_IS_TAG_GLOBAL_EDIT: prefix + 'SET_IS_TAG_GLOBAL_EDIT', SET_MOL_LIST_TO_EDIT: prefix + 'SET_MOL_LIST_TO_EDIT', APPEND_TO_MOL_LIST_TO_EDIT: prefix + 'APPEND_TO_MOL_LIST_TO_EDIT', - REMOVE_FROM_MOL_LIST_TO_EDIT: prefix + 'REMOVE_FROM_MOL_LIST_TO_EDIT' + REMOVE_FROM_MOL_LIST_TO_EDIT: prefix + 'REMOVE_FROM_MOL_LIST_TO_EDIT', + + SET_TAG_TO_EDIT: prefix + 'SET_TAG_TO_EDIT' }; export const PREDEFINED_FILTERS = { diff --git a/js/reducers/selection/selectionReducers.js b/js/reducers/selection/selectionReducers.js index cc8e5e6ed..ac0bafd9f 100644 --- a/js/reducers/selection/selectionReducers.js +++ b/js/reducers/selection/selectionReducers.js @@ -45,7 +45,10 @@ export const INITIAL_STATE = { // } currentVector: null, // selected vector smile (ID) of compoundsOfVectors displayedMoleculesInHitNav: [], - moleculesToEdit: [] + moleculesToEdit: [], + + // tags + tagToEdit: null }; export function selectionReducers(state = INITIAL_STATE, action = {}) { @@ -485,6 +488,12 @@ export function selectionReducers(state = INITIAL_STATE, action = {}) { let reducedMolListToEdit = state.moleculesToEdit.filter(mid => mid !== action.molId); return { ...state, moleculesToEdit: [...reducedMolListToEdit] }; + case constants.SET_TAG_TO_EDIT: { + return Object.assign({}, state, { + tagToEdit: action.tagToEdit + }); + } + // Cases like: @@redux/INIT default: return state; diff --git a/js/reducers/tracking/dispatchActions.js b/js/reducers/tracking/dispatchActions.js index 0148d8243..1c49054a3 100644 --- a/js/reducers/tracking/dispatchActions.js +++ b/js/reducers/tracking/dispatchActions.js @@ -35,7 +35,7 @@ import { removeQuality, removeVector, moveSelectedMolSettings, - removeAllSelectedMolTypes, + removeSelectedMolTypes, hideAllSelectedMolecules, addDensity, addDensityCustomView, @@ -61,7 +61,7 @@ import { removeDatasetSurface, loadDataSets, loadDatasetCompoundsWithScores, - removeAllSelectedDatasetMolecules, + removeSelectedDatasetMolecules, moveSelectedMoleculeSettings, moveSelectedInspirations, moveMoleculeInspirationsSettings, @@ -2741,7 +2741,7 @@ const handleArrowNavigationActionOfMolecule = (action, isSelected, majorViewStag let isInspiration = newItem && newItem.isInspiration; let data = action.data; - dispatch(removeAllSelectedMolTypes(majorViewStage, molecules, true, isInspiration)); + dispatch(removeSelectedMolTypes(majorViewStage, molecules, true, isInspiration)); dispatch(moveSelectedMolSettings(majorViewStage, item, newItem, data, true)); dispatch(setArrowUpDown(item, newItem, action.arrowType, data)); } @@ -2768,7 +2768,7 @@ const handleArrowNavigationActionOfCompound = (action, isSelected, majorViewStag const qualityListMolecule = data.qualityList; dispatch(hideAllSelectedMolecules(majorViewStage, molecules, false, true)); - dispatch(removeAllSelectedDatasetMolecules(majorViewStage, true)); + dispatch(removeSelectedDatasetMolecules(majorViewStage, true)); const newDatasetID = (newItem.hasOwnProperty('datasetID') && newItem.datasetID) || datasetID; const moleculeTitlePrev = newItem && newItem.name; diff --git a/js/reducers/tracking/trackingActions.js b/js/reducers/tracking/trackingActions.js index 0a91bfdbb..d58bce6df 100644 --- a/js/reducers/tracking/trackingActions.js +++ b/js/reducers/tracking/trackingActions.js @@ -1623,21 +1623,9 @@ const getTargetName = (targetId, state) => { }; 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; + const moleculeList = state.apiReducers.all_mol_lists; + const molecule = moleculeList.find(molecule => molecule.id === moleculeId); + return molecule?.protein_code ?? ''; }; const getTypeDescriptionOfSelectedAllAction = type => { diff --git a/package.json b/package.json index 6b02440ab..fb005719e 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fragalysis-frontend", - "version": "0.10.41", + "version": "0.10.45", "description": "Frontend for fragalysis", "main": "webpack.config.js", "scripts": {