diff --git a/.github/workflows/build-staging.yaml b/.github/workflows/build-staging.yaml index 0ba30e560..2d816ef55 100644 --- a/.github/workflows/build-staging.yaml +++ b/.github/workflows/build-staging.yaml @@ -84,12 +84,6 @@ jobs: echo set-output name=BE_NAMESPACE::${BE_NAMESPACE} echo ::set-output name=BE_NAMESPACE::${BE_NAMESPACE} - # FE_BRANCH - FE_BRANCH="${{ env.FE_BRANCH }}" - if [ -n "${{ secrets.FE_BRANCH }}" ]; then FE_BRANCH="${{ secrets.FE_BRANCH }}"; fi - echo set-output name=FE_BRANCH::${FE_BRANCH} - echo ::set-output name=FE_BRANCH::${FE_BRANCH} - # FE_NAMESPACE FE_NAMESPACE="${{ env.FE_NAMESPACE }}" if [ -n "${{ secrets.FE_NAMESPACE }}" ]; then FE_NAMESPACE="${{ secrets.FE_NAMESPACE }}"; fi @@ -114,6 +108,14 @@ jobs: echo set-output name=STACK_NAMESPACE::${STACK_NAMESPACE} echo ::set-output name=STACK_NAMESPACE::${STACK_NAMESPACE} + # What branch/tag are we using? + # FE_BRANCH, or tag if used or a secret if no tag? + TAG="${{ env.FE_BRANCH }}" + if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then TAG="${{ env.GITHUB_REF_SLUG }}"; + elif [ -n "${{ secrets.FE_BRANCH }}" ]; then TAG="${{ secrets.FE_BRANCH }}"; fi + echo set-output name=tag::${TAG} + echo ::set-output name=tag::${TAG} + # Do we trigger downstream, i.e. is TRIGGER_DOWNSTREAM 'yes'? echo set-output name=trigger::${{ env.TRIGGER_DOWNSTREAM == 'yes' }} echo ::set-output name=trigger::${{ env.TRIGGER_DOWNSTREAM == 'yes' }} @@ -143,7 +145,7 @@ jobs: be_namespace=${{ steps.vars.outputs.BE_NAMESPACE }} be_image_tag=${{ steps.vars.outputs.BE_IMAGE_TAG }} fe_namespace=${{ steps.vars.outputs.FE_NAMESPACE }} - fe_branch=${{ steps.vars.outputs.FE_BRANCH }} + fe_branch=${{ steps.vars.outputs.tag }} stack_namespace=${{ steps.vars.outputs.STACK_NAMESPACE }} ci-user: ${{ secrets.STACK_USER }} ci-user-token: ${{ secrets.STACK_USER_TOKEN }} diff --git a/js/components/datasets/datasetMoleculeList.js b/js/components/datasets/datasetMoleculeList.js index 529aa3418..e3931ba7a 100644 --- a/js/components/datasets/datasetMoleculeList.js +++ b/js/components/datasets/datasetMoleculeList.js @@ -13,7 +13,8 @@ import { ButtonGroup, TextField, Checkbox, - InputAdornment + InputAdornment, + setRef } from '@material-ui/core'; import React, { useState, useEffect, memo, useRef, useContext, useCallback, useMemo } from 'react'; import { useDispatch, useSelector, shallowEqual } from 'react-redux'; @@ -210,13 +211,24 @@ const useStyles = makeStyles(theme => ({ color: theme.palette.primary.main, fontStyle: 'italic' }, + arrowsHighlight: { + borderColor: theme.palette.primary.main, + border: 'solid 2px', + backgroundColor: theme.palette.primary.main + }, arrow: { - width: 12, - height: 15 + width: 20, + height: 25, + color: 'white', + stroke: 'white', + strokeWidth: 2 + }, + iconButton: { + padding: 0 }, invisArrow: { - width: 12, - height: 15, + width: 20, + height: 25, visibility: 'hidden' }, arrows: { @@ -241,11 +253,19 @@ const useStyles = makeStyles(theme => ({ backgroundColor: compoundsColors.apricot.color }, textField: { - // marginLeft: theme.spacing(1), + marginLeft: theme.spacing(0.5), // marginRight: theme.spacing(1), - width: 70, + width: 90, + borderRadius: 10, + '& .MuiFormLabel-root': { paddingLeft: theme.spacing(1) + }, + '& .MuiInput-underline:before': { + borderBottom: '0px solid' + }, + '& .MuiInput-underline:after': { + borderBottom: '0px solid' } }, selectedInput: { @@ -260,7 +280,8 @@ const useStyles = makeStyles(theme => ({ }, editClassNameIconSelected: { padding: '0px', - color: theme.palette.primary.main + // color: theme.palette.primary.main + color: 'red' } })); @@ -328,7 +349,7 @@ const DatasetMoleculeList = ({ title, datasetID, url }) => { const compoundsToBuyList = useSelector(state => state.datasetsReducers.compoundsToBuyDatasetMap[datasetID]); - const { addMoleculeViewRef, setScrollToMoleculeId } = useScrollToSelected( + const { addMoleculeViewRef, setScrollToMoleculeId, getNode } = useScrollToSelected( datasetID, moleculesPerPage, setCurrentPage @@ -365,6 +386,14 @@ const DatasetMoleculeList = ({ title, datasetID, url }) => { [compoundsColors.apricot.key]: apricotInput }; + const inputRefs = { + [compoundsColors.blue.key]: useRef(), + [compoundsColors.red.key]: useRef(), + [compoundsColors.green.key]: useRef(), + [compoundsColors.purple.key]: useRef(), + [compoundsColors.apricot.key]: useRef() + }; + const compoundColors = useSelector(state => state.datasetsReducers.compoundColorByDataset[datasetID]) ?? {}; const isSelectedTypeOn = typeList => { @@ -683,6 +712,7 @@ const DatasetMoleculeList = ({ title, datasetID, url }) => { if (firstItem && nextItem) { const moleculeTitleNext = nextItem && nextItem.name; + const node = getNode(nextItem.id); setScrollToMoleculeId(nextItem.id); let dataValue = { @@ -694,6 +724,10 @@ const DatasetMoleculeList = ({ title, datasetID, url }) => { }; dispatch(setCrossReferenceCompoundName(moleculeTitleNext)); + + if (node) { + setSelectedMoleculeRef(node); + } dispatch( moveDatasetMoleculeUpDown(stage, datasetID, firstItem, datasetID, nextItem, dataValue, ARROW_TYPE.DOWN) ); @@ -713,7 +747,7 @@ const DatasetMoleculeList = ({ title, datasetID, url }) => { if (firstItem && prevItem) { const moleculeTitlePrev = prevItem && prevItem.name; - + const node = getNode(prevItem.id); setScrollToMoleculeId(prevItem.id); let dataValue = { @@ -725,6 +759,9 @@ const DatasetMoleculeList = ({ title, datasetID, url }) => { }; dispatch(setCrossReferenceCompoundName(moleculeTitlePrev)); + if (node) { + setSelectedMoleculeRef(node); + } dispatch(moveDatasetMoleculeUpDown(stage, datasetID, firstItem, datasetID, prevItem, dataValue, ARROW_TYPE.UP)); } } @@ -820,34 +857,41 @@ const DatasetMoleculeList = ({ title, datasetID, url }) => { {Object.keys(compoundsColors).map(item => ( <> - - dispatch(onChangeCompoundClassCheckbox(e))} - checked={currentCompoundClass === item} - > - + endAdornment: ( + dispatch(onStartEditColorClassName(e))} + onClick={e => { + dispatch(onStartEditColorClassName(e)); + inputRefs[item].current.focus(); + inputRefs[item].current.select(); + }} > + ), + startAdornment: ( + + dispatch(onChangeCompoundClassCheckbox(e))} + checked={currentCompoundClass === item} + > + ) }} + inputRef={inputRefs[item]} autoComplete="off" id={`${item}`} key={`CLASS_${item}`} @@ -857,7 +901,6 @@ const DatasetMoleculeList = ({ title, datasetID, url }) => { classes[item], currentCompoundClass === item && classes.selectedInput )} - label={compoundsColors[item].text} onChange={e => dispatch(onChangeCompoundClassValue(e))} onKeyDown={e => dispatch(onKeyDownCompoundClass(e))} // onClick={e => dispatch(onClickCompoundClass(e))} @@ -958,23 +1001,35 @@ const DatasetMoleculeList = ({ title, datasetID, url }) => { - + - + @@ -1062,6 +1117,7 @@ const DatasetMoleculeList = ({ title, datasetID, url }) => { disableP={locked && groupDatasetsNglControlButtonsDisabledState.protein} disableC={locked && groupDatasetsNglControlButtonsDisabledState.complex} dragDropEnabled + getNode={getNode} /> ); })} diff --git a/js/components/datasets/datasetMoleculeView/datasetMoleculeView.js b/js/components/datasets/datasetMoleculeView/datasetMoleculeView.js index 94eb0bbeb..9fd292851 100644 --- a/js/components/datasets/datasetMoleculeView/datasetMoleculeView.js +++ b/js/components/datasets/datasetMoleculeView/datasetMoleculeView.js @@ -5,7 +5,13 @@ import React, { memo, useEffect, useState, useRef, useContext, forwardRef } from 'react'; import { useDispatch, useSelector, shallowEqual } from 'react-redux'; import { Grid, Button, makeStyles, Tooltip, IconButton } from '@material-ui/core'; -import { ClearOutlined, CheckOutlined, Assignment, AssignmentTurnedIn } from '@material-ui/icons'; +import { + ClearOutlined, + CheckOutlined, + Assignment, + AssignmentTurnedIn, + KeyboardReturnOutlined +} from '@material-ui/icons'; import SVGInline from 'react-svg-inline'; import classNames from 'classnames'; import { VIEWS, ARROW_TYPE } from '../../../constants/constants'; @@ -28,7 +34,12 @@ import { getFirstUnlockedCompoundBefore, isDatasetCompoundIterrable, isDatasetCompoundLocked, - getAllVisibleButNotLockedCompounds + getAllVisibleButNotLockedCompounds, + getAllVisibleButNotLockedSelectedCompounds, + isCompoundLocked, + getFirstUnlockedSelectedCompoundAfter, + moveSelectedDatasetMoleculeUpDown, + getFirstUnlockedSelectedCompoundBefore } from '../redux/dispatchActions'; import { isAnyInspirationTurnedOn, getFilteredDatasetMoleculeList } from '../redux/selectors'; @@ -357,7 +368,8 @@ const DatasetMoleculeView = memo( disableP, disableC, inSelectedCompoundsList = false, - colorButtonsEnabled = true + colorButtonsEnabled = true, + getNode }, outsideRef ) => { @@ -382,6 +394,8 @@ const DatasetMoleculeView = memo( const currentCompoundClass = useSelector(state => state.previewReducers.compounds.currentCompoundClass); + const selectedCompounds = useSelector(state => state.datasetsReducers.selectedCompounds); + const disableMoleculeNglControlButtons = useSelector(state => state.datasetsReducers.disableDatasetsNglControlButtons[datasetID]?.[currentID]) || {}; @@ -398,6 +412,9 @@ const DatasetMoleculeView = memo( const isSurfaceOn = S; const askLockCompoundsQuestion = useSelector(state => state.datasetsReducers.askLockCompoundsQuestion); + const askLockSelectedCompoundsQuestion = useSelector( + state => state.datasetsReducers.askLockSelectedCompoundsQuestion + ); const isLockVisibleCompoundsDialogOpenLocal = useSelector( state => state.datasetsReducers.isLockVisibleCompoundsDialogOpenLocal @@ -414,7 +431,8 @@ const DatasetMoleculeView = memo( const hasAllValuesOn = isLigandOn && isProteinOn && isComplexOn; const hasSomeValuesOn = !hasAllValuesOn && (isLigandOn || isProteinOn || isComplexOn || isSurfaceOn); - let areArrowsVisible = (isLigandOn || isProteinOn || isComplexOn || isSurfaceOn) && !isLocked; + let areArrowsVisible = + (isLigandOn || isProteinOn || isComplexOn || isSurfaceOn) && !isLocked && !isCompoundFromVectorSelector(data); if (arrowsHidden) { areArrowsVisible = false; @@ -643,20 +661,29 @@ const DatasetMoleculeView = memo( } }; - const handleClickOnDownArrow = async event => { - const unlockedVisibleCompounds = dispatch(getAllVisibleButNotLockedCompounds(datasetID, currentID)); - if (unlockedVisibleCompounds?.length > 0 && askLockCompoundsQuestion) { + const moveDownSelectedCompound = anchorEl => { + //check if there are unlocked but visible compounds in the selected compounds list + const unlockedVisibleCompounds = dispatch(getAllVisibleButNotLockedSelectedCompounds(datasetID, currentID)); + if (unlockedVisibleCompounds?.length > 0 && askLockSelectedCompoundsQuestion) { dispatch(setCmpForLocalLockVisibleCompoundsDialog(data)); - setLockCompoundsDialogAnchorE1(event.currentTarget); + setLockCompoundsDialogAnchorE1(anchorEl); dispatch(setIsOpenLockVisibleCompoundsDialogLocal(true)); } else { - const refNext = ref.current.nextSibling; + let refNext = ref.current.nextSibling; scrollToElement(refNext); let nextItem = (nextItemData.hasOwnProperty('molecule') && nextItemData.molecule) || nextItemData; - const nextDatasetID = (nextItemData.hasOwnProperty('datasetID') && nextItemData.datasetID) || datasetID; - if (dispatch(isDatasetCompoundLocked(nextDatasetID, nextItem.id))) { - nextItem = dispatch(getFirstUnlockedCompoundAfter(nextDatasetID, nextItem.id)); + let nextDatasetID = (nextItemData.hasOwnProperty('datasetID') && nextItemData.datasetID) || datasetID; + if (dispatch(isCompoundLocked(nextDatasetID, nextItem)) || isCompoundFromVectorSelector(nextItem)) { + const unlockedCmp = dispatch(getFirstUnlockedSelectedCompoundAfter(nextDatasetID, nextItem.id)); + if (!unlockedCmp) { + return; + } + nextItem = unlockedCmp.molecule; + nextDatasetID = unlockedCmp.datasetID; + } + if (getNode) { + refNext = getNode(nextItem.id); } const moleculeTitleNext = nextItem && nextItem.name; @@ -668,29 +695,82 @@ const DatasetMoleculeView = memo( } dispatch( - moveDatasetMoleculeUpDown(stage, datasetID, data, nextDatasetID, nextItem, dataValue, ARROW_TYPE.DOWN) + moveSelectedDatasetMoleculeUpDown( + stage, + datasetID, + data, + nextDatasetID, + nextItem, + dataValue, + ARROW_TYPE.DOWN + ) ); } }; - const handleClickOnUpArrow = async event => { - const unlockedVisibleCompounds = dispatch(getAllVisibleButNotLockedCompounds(datasetID, currentID)); - if (unlockedVisibleCompounds?.length > 0 && askLockCompoundsQuestion) { + const handleClickOnDownArrow = async event => { + if (inSelectedCompoundsList) { + moveDownSelectedCompound(event.currentTarget); + } else { + const unlockedVisibleCompounds = dispatch(getAllVisibleButNotLockedCompounds(datasetID, currentID)); + if (unlockedVisibleCompounds?.length > 0 && askLockCompoundsQuestion) { + dispatch(setCmpForLocalLockVisibleCompoundsDialog(data)); + setLockCompoundsDialogAnchorE1(event.currentTarget); + dispatch(setIsOpenLockVisibleCompoundsDialogLocal(true)); + } else { + let refNext = ref.current.nextSibling; + scrollToElement(refNext); + + let nextItem = (nextItemData.hasOwnProperty('molecule') && nextItemData.molecule) || nextItemData; + const nextDatasetID = (nextItemData.hasOwnProperty('datasetID') && nextItemData.datasetID) || datasetID; + if (dispatch(isDatasetCompoundLocked(nextDatasetID, nextItem.id))) { + nextItem = dispatch(getFirstUnlockedCompoundAfter(nextDatasetID, nextItem.id)); + } + if (getNode) { + refNext = getNode(nextItem.id); + } + const moleculeTitleNext = nextItem && nextItem.name; + + let dataValue = { colourToggle, isLigandOn, isProteinOn, isComplexOn, isSurfaceOn }; + + dispatch(setCrossReferenceCompoundName(moleculeTitleNext)); + if (setRef && ref.current) { + setRef(refNext); + } + + dispatch( + moveDatasetMoleculeUpDown(stage, datasetID, data, nextDatasetID, nextItem, dataValue, ARROW_TYPE.DOWN) + ); + } + } + }; + + const moveUpSelectedCompound = anchorEl => { + const unlockedVisibleCompounds = dispatch(getAllVisibleButNotLockedSelectedCompounds(datasetID, currentID)); + if (unlockedVisibleCompounds?.length > 0 && askLockSelectedCompoundsQuestion) { dispatch(setCmpForLocalLockVisibleCompoundsDialog(data)); - setLockCompoundsDialogAnchorE1(event.currentTarget); + setLockCompoundsDialogAnchorE1(anchorEl); dispatch(setIsOpenLockVisibleCompoundsDialogLocal(true)); } else { - const refPrevious = ref.current.previousSibling; + let refPrevious = ref.current.previousSibling; scrollToElement(refPrevious); let previousItem = (previousItemData.hasOwnProperty('molecule') && previousItemData.molecule) || previousItemData; - const previousDatasetID = + let previousDatasetID = (previousItemData.hasOwnProperty('datasetID') && previousItemData.datasetID) || datasetID; - if (dispatch(isDatasetCompoundLocked(previousDatasetID, previousItem.id))) { - previousItem = dispatch(getFirstUnlockedCompoundBefore(previousDatasetID, previousItem.id)); + if (dispatch(isCompoundLocked(previousDatasetID, previousItem))) { + const unlockedCmp = dispatch(getFirstUnlockedSelectedCompoundBefore(previousDatasetID, previousItem.id)); + if (!unlockedCmp) { + return; + } + previousItem = unlockedCmp.molecule; + previousDatasetID = unlockedCmp.datasetID; + } + if (getNode) { + refPrevious = getNode(previousItem.id); } - const moleculeTitlePrev = previousItem && previousItem.name; + const moleculeTitlePrev = previousItem && previousDatasetID.name; let dataValue = { colourToggle, isLigandOn, isProteinOn, isComplexOn, isSurfaceOn }; @@ -700,11 +780,66 @@ const DatasetMoleculeView = memo( } dispatch( - moveDatasetMoleculeUpDown(stage, datasetID, data, previousDatasetID, previousItem, dataValue, ARROW_TYPE.UP) + moveSelectedDatasetMoleculeUpDown( + stage, + datasetID, + data, + previousDatasetID, + previousItem, + dataValue, + ARROW_TYPE.UP + ) ); } }; + const handleClickOnUpArrow = async event => { + if (inSelectedCompoundsList) { + moveUpSelectedCompound(event.currentTarget); + } else { + const unlockedVisibleCompounds = dispatch(getAllVisibleButNotLockedCompounds(datasetID, currentID)); + if (unlockedVisibleCompounds?.length > 0 && askLockCompoundsQuestion) { + dispatch(setCmpForLocalLockVisibleCompoundsDialog(data)); + setLockCompoundsDialogAnchorE1(event.currentTarget); + dispatch(setIsOpenLockVisibleCompoundsDialogLocal(true)); + } else { + let refPrevious = ref.current.previousSibling; + scrollToElement(refPrevious); + + let previousItem = + (previousItemData.hasOwnProperty('molecule') && previousItemData.molecule) || previousItemData; + const previousDatasetID = + (previousItemData.hasOwnProperty('datasetID') && previousItemData.datasetID) || datasetID; + if (dispatch(isDatasetCompoundLocked(previousDatasetID, previousItem.id))) { + previousItem = dispatch(getFirstUnlockedCompoundBefore(previousDatasetID, previousItem.id)); + } + if (getNode) { + refPrevious = getNode(previousItem.id); + } + const moleculeTitlePrev = previousItem && previousItem.name; + + let dataValue = { colourToggle, isLigandOn, isProteinOn, isComplexOn, isSurfaceOn }; + + dispatch(setCrossReferenceCompoundName(moleculeTitlePrev)); + if (setRef && ref.current) { + setRef(refPrevious); + } + + dispatch( + moveDatasetMoleculeUpDown( + stage, + datasetID, + data, + previousDatasetID, + previousItem, + dataValue, + ARROW_TYPE.UP + ) + ); + } + } + }; + const moleculeTitle = data && data.name; const datasetTitle = datasets?.find(item => `${item.id}` === `${datasetID}`)?.title; @@ -744,26 +879,30 @@ const DatasetMoleculeView = memo( anchorEl={lockCompoundsDialogAnchorE1} datasetId={datasetID} currentCmp={data} + isSelectedCompounds={inSelectedCompoundsList} /> )} {/*Site number*/} - { - const result = e.target.checked; - if (result) { - dispatch(appendCompoundToSelectedCompoundsByDataset(datasetID, currentID, moleculeTitle)); - } else { - dispatch(removeCompoundFromSelectedCompoundsByDataset(datasetID, currentID, moleculeTitle)); - dispatch(deselectVectorCompound(data)); - } - }} - /> + {!isCompoundFromVectorSelector(data) && ( + { + const result = e.target.checked; + if (result) { + dispatch(appendCompoundToSelectedCompoundsByDataset(datasetID, currentID, moleculeTitle)); + } else { + dispatch(removeCompoundFromSelectedCompoundsByDataset(datasetID, currentID, moleculeTitle)); + dispatch(deselectVectorCompound(data)); + } + }} + /> + )} {index + 1}. diff --git a/js/components/datasets/lockVisibleCompoundsDialog.js b/js/components/datasets/lockVisibleCompoundsDialog.js index 5cf01b898..af986d62e 100644 --- a/js/components/datasets/lockVisibleCompoundsDialog.js +++ b/js/components/datasets/lockVisibleCompoundsDialog.js @@ -5,10 +5,16 @@ import { Close } from '@material-ui/icons'; import { useDispatch } from 'react-redux'; import { setAskLockCompoundsQuestion, + setAskLockSelectedCompoundsQuestion, setIsOpenLockVisibleCompoundsDialogGlobal, setIsOpenLockVisibleCompoundsDialogLocal } from './redux/actions'; -import { getAllVisibleButNotLockedCompounds, lockCompounds } from './redux/dispatchActions'; +import { + getAllVisibleButNotLockedCompounds, + getAllVisibleButNotLockedSelectedCompounds, + lockCompounds, + lockSelectedCompounds +} from './redux/dispatchActions'; const useStyles = makeStyles(theme => ({ paper: { @@ -27,76 +33,99 @@ const useStyles = makeStyles(theme => ({ } })); -export const LockVisibleCompoundsDialog = forwardRef(({ open = false, anchorEl, datasetId, currentCmp }, ref) => { - const id = open ? 'simple-popover-lock-visible-compounds' : undefined; - const classes = useStyles(); - const dispatch = useDispatch(); +export const LockVisibleCompoundsDialog = forwardRef( + ({ open = false, anchorEl, datasetId, currentCmp, isSelectedCompounds = false }, ref) => { + const id = open ? 'simple-popover-lock-visible-compounds' : undefined; + const classes = useStyles(); + const dispatch = useDispatch(); - const handleYesClick = () => { - if (currentCmp) { - let cmpsToLock = dispatch(getAllVisibleButNotLockedCompounds(datasetId)); - if (cmpsToLock && cmpsToLock.length > 0) { - dispatch(lockCompounds(datasetId, cmpsToLock, currentCmp.id)); - } - } else { - let cmpsToLock = dispatch(getAllVisibleButNotLockedCompounds(datasetId)); - //we need to skip first element if we came here from global arrows - const firstCmpId = cmpsToLock[0]; - if (cmpsToLock && cmpsToLock.length > 0) { - dispatch(lockCompounds(datasetId, cmpsToLock, firstCmpId)); + const handleYesClick = () => { + if (!isSelectedCompounds) { + if (currentCmp) { + let cmpsToLock = dispatch(getAllVisibleButNotLockedCompounds(datasetId)); + if (cmpsToLock && cmpsToLock.length > 0) { + dispatch(lockCompounds(datasetId, cmpsToLock, currentCmp.id)); + } + } else { + let cmpsToLock = dispatch(getAllVisibleButNotLockedCompounds(datasetId)); + //we need to skip first element if we came here from global arrows + if (cmpsToLock && cmpsToLock.length > 0) { + const firstCmpId = cmpsToLock[0]; + dispatch(lockCompounds(datasetId, cmpsToLock, firstCmpId)); + } + } + } else { + if (currentCmp) { + let cmpsToLock = dispatch(getAllVisibleButNotLockedSelectedCompounds()); + if (cmpsToLock && cmpsToLock.length > 0) { + dispatch(lockSelectedCompounds(cmpsToLock, currentCmp.id)); + } + } else { + let cmpsToLock = dispatch(getAllVisibleButNotLockedSelectedCompounds()); + if (cmpsToLock && cmpsToLock.length > 0) { + const firstCmpId = cmpsToLock[0]; + dispatch(lockSelectedCompounds(cmpsToLock, firstCmpId)); + } + } } - } - dispatch(setIsOpenLockVisibleCompoundsDialogLocal(false)); - dispatch(setIsOpenLockVisibleCompoundsDialogGlobal(false)); - }; + dispatch(setIsOpenLockVisibleCompoundsDialogLocal(false)); + dispatch(setIsOpenLockVisibleCompoundsDialogGlobal(false)); + }; - const handleNoClick = () => { - dispatch(setAskLockCompoundsQuestion(false)); - dispatch(setIsOpenLockVisibleCompoundsDialogLocal(false)); - dispatch(setIsOpenLockVisibleCompoundsDialogGlobal(false)); - }; + const handleNoClick = () => { + if (!isSelectedCompounds) { + dispatch(setAskLockCompoundsQuestion(false)); + } else { + dispatch(setAskLockSelectedCompoundsQuestion(false)); + } + dispatch(setIsOpenLockVisibleCompoundsDialogLocal(false)); + dispatch(setIsOpenLockVisibleCompoundsDialogGlobal(false)); + }; - return ( - - - { - dispatch(setIsOpenLockVisibleCompoundsDialogLocal(false)); - dispatch(setIsOpenLockVisibleCompoundsDialogGlobal(false)); - }} - > - - - - ]} - > - - - Do you want to lock visible compounds? Otherwise they will disapear from the view. - - + return ( + + + { + dispatch(setIsOpenLockVisibleCompoundsDialogLocal(false)); + dispatch(setIsOpenLockVisibleCompoundsDialogGlobal(false)); + }} + > + + + + ]} + > + - + + Do you want to lock visible compounds? Otherwise they will disapear from the view. + - - + + + + + + + - - - - ); -}); + + + ); + } +); diff --git a/js/components/datasets/redux/actions.js b/js/components/datasets/redux/actions.js index fdf803108..08329a9f8 100644 --- a/js/components/datasets/redux/actions.js +++ b/js/components/datasets/redux/actions.js @@ -537,3 +537,13 @@ export const setEditedColorGroup = colorGroup => ({ type: constants.SET_EDITED_COLOR_GROUP, colorGroup: colorGroup }); + +export const setSelectedCompoundsList = compoundsList => ({ + type: constants.SET_SELECTED_COMPOUNDS_LIST, + compoundsList: compoundsList +}); + +export const setAskLockSelectedCompoundsQuestion = askLockCompoundsQuestion => ({ + type: constants.SET_ASK_LOCK_SELECTED_COMPOUNDS_QUESTION, + askLockCompoundsQuestion: askLockCompoundsQuestion +}); diff --git a/js/components/datasets/redux/constants.js b/js/components/datasets/redux/constants.js index f3cefcc80..40e44c20d 100644 --- a/js/components/datasets/redux/constants.js +++ b/js/components/datasets/redux/constants.js @@ -104,7 +104,10 @@ export const constants = { SET_IS_OPEN_LOCK_VISIBLE_COMPOUNDS_DIALOG_LOCAL: prefix + 'SET_IS_OPEN_LOCK_VISIBLE_COMPOUNDS_DIALOG_LOCAL', SET_CMP_FOR_LOCAL_LOCK_VISIBLE_COMPOUNDS_DIALOG: prefix + 'SET_CMP_FOR_LOCAL_LOCK_VISIBLE_COMPOUNDS_DIALOG', SET_ASK_LOCK_COMPOUNDS_QUESTION: prefix + 'SET_ASK_LOCK_COMPOUNDS_QUESTION', - SET_EDITED_COLOR_GROUP: prefix + 'SET_EDITED_COLOR_GROUP' + SET_EDITED_COLOR_GROUP: prefix + 'SET_EDITED_COLOR_GROUP', + + SET_SELECTED_COMPOUNDS_LIST: prefix + 'SET_SELECTED_COMPOUNDS_LIST', + SET_ASK_LOCK_SELECTED_COMPOUNDS_QUESTION: prefix + 'SET_ASK_LOCK_SELECTED_COMPOUNDS_QUESTION' }; export const COUNT_OF_VISIBLE_SCORES = 7; diff --git a/js/components/datasets/redux/dispatchActions.js b/js/components/datasets/redux/dispatchActions.js index 0536ad581..18b209d84 100644 --- a/js/components/datasets/redux/dispatchActions.js +++ b/js/components/datasets/redux/dispatchActions.js @@ -73,6 +73,8 @@ import { getRepresentationsByType } from '../../nglView/generatingObjects'; import { selectAllMoleculeList } from '../../preview/molecule/redux/selectors'; import { getCompoundById } from '../../../reducers/tracking/dispatchActionsSwitchSnapshot'; import { getRandomColor } from '../../preview/molecule/utils/color'; +import { BreakfastDiningOutlined } from '@mui/icons-material'; +import { isCompoundFromVectorSelector } from '../../preview/compounds/redux/dispatchActions'; export const initializeDatasetFilter = datasetID => (dispatch, getState) => { const state = getState(); @@ -544,7 +546,7 @@ export const autoHideDatasetDialogsOnScroll = ({ inspirationDialogRef, crossRefe currentBoundingClientRectInspiration.y !== 0 ) { if ( - currentBoundingClientRectInspiration.top < scrollBarBoundingClientRect.top || + Math.round(currentBoundingClientRectInspiration.top) < Math.round(scrollBarBoundingClientRect.top) || Math.abs(scrollBarBoundingClientRect.bottom - currentBoundingClientRectInspiration.top) < 42 ) { dispatch(setIsOpenInspirationDialog(false)); @@ -558,7 +560,7 @@ export const autoHideDatasetDialogsOnScroll = ({ inspirationDialogRef, crossRefe currentBoundingClientRectCrossReference.y !== 0 ) { if ( - currentBoundingClientRectCrossReference.top < scrollBarBoundingClientRect.top || + Math.round(currentBoundingClientRectCrossReference.top) < Math.round(scrollBarBoundingClientRect.top) || Math.abs(scrollBarBoundingClientRect.bottom - currentBoundingClientRectCrossReference.top) < 42 ) { dispatch(resetCrossReferenceDialog()); @@ -732,11 +734,26 @@ const moveSelectedDatasetMoleculeInspirationsSettings = (data, newItemData, stag ); }; +export const lockSelectedCompounds = (selectedCompounds, skipCmpId = 0) => (dispatch, getState) => { + let filteredCompounds = [...selectedCompounds]; + if (skipCmpId) { + filteredCompounds = selectedCompounds.filter(item => item.molecule.id !== skipCmpId); + } + + filteredCompounds?.forEach(item => { + const datasetID = item.datasetID; + const molecule = item.molecule; + const moleculeID = molecule.id; + const moleculeName = molecule.name; + dispatch(appendCompoundToSelectedCompoundsByDataset(datasetID, moleculeID, moleculeName)); + }); +}; + export const lockCompounds = (datasetID, compoundIds, skipCmpId = 0) => (dispatch, getState) => { const state = getState(); const compounds = state.datasetsReducers.moleculeLists[datasetID]; - let filteredCompounds = []; + let filteredCompounds = [...compoundIds]; if (skipCmpId) { filteredCompounds = compoundIds.filter(item => item !== skipCmpId); } @@ -761,6 +778,56 @@ const mergeCompoundIdsList = (compoundIdsList, subList) => { return result; }; +export const isCompoundLocked = (datasetID, compound) => (dispatch, getState) => { + const state = getState(); + const lockedCompounds = state.datasetsReducers.selectedCompoundsByDataset[datasetID] || []; + return lockedCompounds.includes(compound.id); +}; + +export const getAllVisibleButNotLockedSelectedCompounds = (skipDatasetID = '', skipCmpId = 0) => ( + dispatch, + getState +) => { + let result = []; + + const state = getState(); + const selectedCompounds = state.datasetsReducers.selectedCompounds || []; + + selectedCompounds.forEach(item => { + const datasetID = item.datasetID; + const molecule = item.molecule; + if (datasetID !== skipDatasetID || skipCmpId !== molecule.id) { + const isLocked = dispatch(isCompoundLocked(datasetID, molecule)); + if (!isLocked) { + let isVisible = dispatch(isCompoundVisible(datasetID, molecule.id)); + // isVisible |= state.datasetsReducers.ligandLists[datasetID].includes(molecule.id); + // isVisible |= state.datasetsReducers.proteinLists[datasetID].includes(molecule.id); + // isVisible |= state.datasetsReducers.complexLists[datasetID].includes(molecule.id); + // isVisible |= state.datasetsReducers.surfaceLists[datasetID].includes(molecule.id); + + if (isVisible) { + result.push(item); + } + } + } + }); + + return result; +}; + +export const isCompoundVisible = (datasetID, compoundId) => (dispatch, getState) => { + let isVisible = false; + + const state = getState(); + + isVisible |= state.datasetsReducers.ligandLists[datasetID]?.includes(compoundId); + isVisible |= state.datasetsReducers.proteinLists[datasetID]?.includes(compoundId); + isVisible |= state.datasetsReducers.complexLists[datasetID]?.includes(compoundId); + isVisible |= state.datasetsReducers.surfaceLists[datasetID]?.includes(compoundId); + + return isVisible; +}; + export const getAllVisibleButNotLockedCompounds = (datasetID, skipCmpId = 0) => (dispatch, getState) => { let result = []; @@ -821,6 +888,52 @@ export const getFirstUnlockedCompoundAfter = (datasetID, compoundID) => (dispatc return firstUnlockedCompound; }; +export const getFirstUnlockedSelectedCompoundAfter = (datasetID, compoundID) => (dispatch, getState) => { + const state = getState(); + + const compounds = state.datasetsReducers.selectedCompounds; + const currentItemIndex = compounds.findIndex(item => item.datasetID === datasetID && item.molecule.id === compoundID); + + let firstUnlockedCompound = null; + for (let i = currentItemIndex + 1; i < compounds.length; i++) { + const compound = compounds[i]; + if ( + !dispatch(isCompoundLocked(compound.datasetID, compound.molecule)) && + !isCompoundFromVectorSelector(compound.molecule) + ) { + firstUnlockedCompound = compound; + break; + } else { + continue; + } + } + + return firstUnlockedCompound; +}; + +export const getFirstUnlockedSelectedCompoundBefore = (datasetID, compoundID) => (dispatch, getState) => { + const state = getState(); + + const compounds = state.datasetsReducers.selectedCompounds; + const currentItemIndex = compounds.findIndex(item => item.datasetID === datasetID && item.molecule.id === compoundID); + + let firstUnlockedCompound = null; + for (let i = currentItemIndex - 1; i >= 0; i--) { + const compound = compounds[i]; + if ( + !dispatch(isCompoundLocked(compound.datasetID, compound.molecule)) && + !isCompoundFromVectorSelector(compound.molecule) + ) { + firstUnlockedCompound = compound; + break; + } else { + continue; + } + } + + return firstUnlockedCompound; +}; + export const getFirstUnlockedCompoundBefore = (datasetID, compoundID) => (dispatch, getState) => { const state = getState(); @@ -836,6 +949,53 @@ export const getFirstUnlockedCompoundBefore = (datasetID, compoundID) => (dispat return firstUnlockedCompound; }; +export const moveSelectedDatasetMoleculeUpDown = ( + stage, + datasetID, + item, + newItemDatasetID, + newItem, + data, + direction +) => async (dispatch, getState) => { + const state = getState(); + const allInspirations = state.datasetsReducers.allInspirations; + const objectsInView = state.nglReducers.objectsInView; + const selectedCompounds = state.datasetsReducers.selectedCompounds; + let lockedCompounds = {}; + selectedCompounds.forEach(item => { + if (dispatch(isCompoundLocked(item.datasetID, item.molecule))) { + if (lockedCompounds.hasOwnProperty(item.datasetID)) { + lockedCompounds[item.datasetID].push(item.molecule.id); + } else { + lockedCompounds[item.datasetID] = [item.molecule.id]; + } + } + }); + + //also skip next item + if (lockedCompounds.hasOwnProperty(newItemDatasetID)) { + lockedCompounds[newItemDatasetID].push(newItem.id); + } else { + lockedCompounds[newItemDatasetID] = [newItem.id]; + } + const dataValue = { ...data, objectsInView }; + + // ???? + // dispatch(setArrowUpDown(datasetID, item, newItem, direction, dataValue)); + + const inspirations = getInspirationsForMol(allInspirations, datasetID, newItem.id); + dispatch(setInspirationMoleculeDataList(inspirations)); + dispatch(clearCompoundView(newItem, datasetID, stage, true)); + await Promise.all([ + dispatch(moveSelectedMoleculeSettings(stage, item, newItem, newItemDatasetID, datasetID, dataValue, true)), + dispatch(moveSelectedDatasetMoleculeInspirationsSettings(item, newItem, stage, true)) + ]); + + dispatch(removeSelectedDatasetMolecules(stage, true, { ...lockedCompounds })); + dispatch(removeSelectedTypesOfDatasetInspirations([newItem], stage, true, datasetID)); +}; + /** * Performance optimization for datasetMoleculeView. Gets objectsInView and passes it to further dispatch requests. * It wouldnt do anything else in moleculeView. Also this chains the above 3 methods which were before passed to @@ -849,7 +1009,7 @@ export const moveDatasetMoleculeUpDown = (stage, datasetID, item, newItemDataset const state = getState(); const allInspirations = state.datasetsReducers.allInspirations; const objectsInView = state.nglReducers.objectsInView; - const lockedCompounds = state.datasetsReducers.selectedCompoundsByDataset[datasetID] ?? []; + let lockedCompounds = state.datasetsReducers.selectedCompoundsByDataset[datasetID] ?? []; const dataValue = { ...data, objectsInView }; diff --git a/js/components/datasets/redux/reducer.js b/js/components/datasets/redux/reducer.js index 290561220..3733ead65 100644 --- a/js/components/datasets/redux/reducer.js +++ b/js/components/datasets/redux/reducer.js @@ -50,6 +50,8 @@ export const INITIAL_STATE = { selectedCompoundsByDataset: {}, // map of $datasetID and its list of moleculeID compoundColorByDataset: {}, // map of $datasetID and its list of moleculeID + selectedCompounds: [], // list of selected compounds + selectedColorsInFilter: { [compoundsColors.blue.key]: compoundsColors.blue.key, [compoundsColors.red.key]: compoundsColors.red.key, @@ -76,7 +78,8 @@ export const INITIAL_STATE = { isLockVisibleCompoundsDialogOpenLocal: false, cmpForLocalLockVisibleCompoundsDialog: null, askLockCompoundsQuestion: true, - editedColorGroup: null + editedColorGroup: null, + askLockSelectedCompoundsQuestion: true }; /** @@ -645,6 +648,14 @@ export const datasetsReducers = (state = INITIAL_STATE, action = {}) => { return { ...state, askLockCompoundsQuestion: action.askLockCompoundsQuestion }; } + case constants.SET_ASK_LOCK_SELECTED_COMPOUNDS_QUESTION: { + return { ...state, askLockSelectedCompoundsQuestion: action.askLockCompoundsQuestion }; + } + + case constants.SET_SELECTED_COMPOUNDS_LIST: { + return { ...state, selectedCompounds: action.compoundsList }; + } + case constants.RESET_DATASETS_STATE_ON_SNAPSHOT_CHANGE: { const { datasets, diff --git a/js/components/datasets/selectedCompoundsList.js b/js/components/datasets/selectedCompoundsList.js index 15d10e3a6..7a529e923 100644 --- a/js/components/datasets/selectedCompoundsList.js +++ b/js/components/datasets/selectedCompoundsList.js @@ -1,4 +1,4 @@ -import React, { memo, useContext, useEffect, useRef, useState } from 'react'; +import React, { memo, useContext, useEffect, useMemo, useRef, useState } from 'react'; import { Panel } from '../common/Surfaces/Panel'; import { CircularProgress, @@ -11,7 +11,7 @@ import { InputAdornment, IconButton } from '@material-ui/core'; -import { CloudDownload, Edit } from '@material-ui/icons'; +import { ArrowDownward, ArrowUpward, CloudDownload, Edit } from '@material-ui/icons'; import { useDispatch, useSelector } from 'react-redux'; import { getMoleculesObjectIDListOfCompoundsToBuy, @@ -23,9 +23,22 @@ import { import InfiniteScroll from 'react-infinite-scroller'; import DatasetMoleculeView from './datasetMoleculeView'; import { InspirationDialog } from './inspirationDialog'; -import { setIsOpenInspirationDialog } from './redux/actions'; +import { + setCrossReferenceCompoundName, + setIsOpenInspirationDialog, + setIsOpenLockVisibleCompoundsDialogGlobal, + setSelectedCompoundsList +} from './redux/actions'; import { CrossReferenceDialog } from './crossReferenceDialog'; -import { autoHideDatasetDialogsOnScroll, resetCrossReferenceDialog } from './redux/dispatchActions'; +import { + autoHideDatasetDialogsOnScroll, + getAllVisibleButNotLockedSelectedCompounds, + isCompoundLocked, + isCompoundVisible, + moveDatasetMoleculeUpDown, + moveSelectedDatasetMoleculeUpDown, + resetCrossReferenceDialog +} from './redux/dispatchActions'; import { NglContext } from '../nglView/nglProvider'; import FileSaver from 'file-saver'; import JSZip from 'jszip'; @@ -48,6 +61,14 @@ import { HTML5Backend } from 'react-dnd-html5-backend'; import { compoundsColors } from '../preview/compounds/redux/constants'; import classNames from 'classnames'; import { fabClasses } from '@mui/material'; +import { is } from 'date-fns/locale'; +import { LockVisibleCompoundsDialog } from './lockVisibleCompoundsDialog'; +import { BreakfastDiningOutlined } from '@mui/icons-material'; +import { getRandomColor } from '../preview/molecule/utils/color'; +import { ARROW_TYPE, VIEWS } from '../../constants/constants'; +import { useScrollToCompound } from './useScrollToCompound'; +import useDisableNglControlButtons from '../preview/molecule/useDisableNglControlButtons'; +import useDisableDatasetNglControlButtons from './useDisableDatasetNglControlButtons'; const useStyles = makeStyles(theme => ({ container: { @@ -84,11 +105,19 @@ const useStyles = makeStyles(theme => ({ backgroundColor: compoundsColors.apricot.color }, textField: { - // marginLeft: theme.spacing(1), + marginLeft: theme.spacing(0.5), // marginRight: theme.spacing(1), - width: 70, + width: 90, + borderRadius: 10, + '& .MuiFormLabel-root': { paddingLeft: theme.spacing(1) + }, + '& .MuiInput-underline:before': { + borderBottom: '0px solid' + }, + '& .MuiInput-underline:after': { + borderBottom: '0px solid' } }, selectedInput: { @@ -103,7 +132,34 @@ const useStyles = makeStyles(theme => ({ }, editClassNameIconSelected: { padding: '0px', - color: theme.palette.primary.main + color: 'red' + }, + arrowsHighlight: { + borderColor: theme.palette.primary.main, + border: 'solid 2px', + backgroundColor: theme.palette.primary.main + }, + arrow: { + width: 20, + height: 25, + color: 'white', + stroke: 'white', + strokeWidth: 2 + }, + iconButton: { + padding: 0, + marginTop: '2px' + }, + invisArrow: { + width: 20, + height: 25, + visibility: 'hidden' + }, + arrows: { + height: '100%', + border: 'solid 1px', + borderColor: theme.palette.background.divider, + borderStyle: 'solid solid solid solid' } })); @@ -151,8 +207,26 @@ export const SelectedCompoundList = memo(() => { selectedMolecules = [...selectedMolecules, ...molsOfDataset.filter(mol => datasetCmpsToBuy?.includes(mol.id))]; }); + const { addMoleculeViewRef, setScrollToMoleculeId, getNode } = useScrollToCompound(); + const currentCompoundClass = useSelector(state => state.previewReducers.compounds.currentCompoundClass); + // const groupDatasetsNglControlButtonsDisabledState = useDisableDatasetNglControlButtons( + // lockedMolecules.map(cid => ({ datasetID, molecule: getCompoundForId(cid) })) + // ); + + const askLockSelectedCompoundsQuestion = useSelector( + state => state.datasetsReducers.askLockSelectedCompoundsQuestion + ); + + const lockVisibleCompoundsDialogRef = useRef(); + const isLockVisibleCompoundsDialogOpenGlobal = useSelector( + state => state.datasetsReducers.isLockVisibleCompoundsDialogOpenGlobal + ); + + const { getNglView } = useContext(NglContext); + const stage = getNglView(VIEWS.MAJOR_VIEW) && getNglView(VIEWS.MAJOR_VIEW).stage; + const blueInput = useSelector(state => state.previewReducers.compounds[compoundsColors.blue.key]); const redInput = useSelector(state => state.previewReducers.compounds[compoundsColors.red.key]); const greenInput = useSelector(state => state.previewReducers.compounds[compoundsColors.green.key]); @@ -167,10 +241,54 @@ export const SelectedCompoundList = memo(() => { [compoundsColors.apricot.key]: apricotInput }; + const inputRefs = { + [compoundsColors.blue.key]: useRef(), + [compoundsColors.red.key]: useRef(), + [compoundsColors.green.key]: useRef(), + [compoundsColors.purple.key]: useRef(), + [compoundsColors.apricot.key]: useRef() + }; + const compoundColors = useSelector(state => state.datasetsReducers.compoundColorByDataset); const colorFilterSettings = useSelector(state => state.datasetsReducers.selectedColorsInFilter); + const [lockCompoundsDialogAnchorE1, setLockCompoundsDialogAnchorE1] = useState(null); + + //we need to add also lists to dependancy array because isCompoundVisible depends on them + useEffect(() => { + dispatch(setSelectedCompoundsList(moleculesObjectIDListOfCompoundsToBuy)); + }, [moleculesObjectIDListOfCompoundsToBuy, dispatch]); + + let areArrowsVisible = useMemo(() => { + let result = false; + if (moleculesObjectIDListOfCompoundsToBuy) { + for (let i = 0; i < moleculesObjectIDListOfCompoundsToBuy.length; i++) { + const cmp = moleculesObjectIDListOfCompoundsToBuy[i]; + const dataset = cmp.datasetID; + const molecule = cmp.molecule; + const isVisible = dispatch(isCompoundVisible(dataset, molecule.id)); + if (isVisible) { + result = true; + break; + } + } + } + return result; + }, [dispatch, moleculesObjectIDListOfCompoundsToBuy, ligandList, proteinList, complexList, surfaceList]); + // if (!moleculesObjectIDListOfCompoundsToBuy) { + // for (let i = 0; i < moleculesObjectIDListOfCompoundsToBuy.length; i++) { + // const cmp = moleculesObjectIDListOfCompoundsToBuy[i]; + // const dataset = cmp.datasetID; + // const molecule = cmp.molecule; + // const isVisible = dispatch(isCompoundVisible(dataset, molecule.id)); + // if (isVisible) { + // areArrowsVisible = true; + // break; + // } + // } + // } + useEffect(() => { return () => { dispatch(setIsOpenInspirationDialog(false)); @@ -444,6 +562,142 @@ export const SelectedCompoundList = memo(() => { FileSaver.saveAs(zipBlob, 'selectedCompounds.zip'); }; + const getFirstItemForIterationStart = () => { + let result = null; + for (let i = 0; i < moleculesObjectIDListOfCompoundsToBuy.length; i++) { + const cmp = moleculesObjectIDListOfCompoundsToBuy[i]; + if (!dispatch(isCompoundLocked(cmp.datasetID, cmp.molecule)) && !isCompoundFromVectorSelector(cmp.molecule)) { + const isVisible = dispatch(isCompoundVisible(cmp.datasetID, cmp.molecule.id)); + if (isVisible) { + result = cmp; + break; + } + } + } + + return result; + }; + + const getNextItemForIteration = currentItem => { + let result = null; + + const currentItemIndex = moleculesObjectIDListOfCompoundsToBuy.findIndex( + cmp => cmp.molecule.id === currentItem.molecule.id && cmp.datasetID === currentItem.datasetID + ); + for (let i = currentItemIndex + 1; i < moleculesObjectIDListOfCompoundsToBuy.length; i++) { + const cmp = moleculesObjectIDListOfCompoundsToBuy[i]; + if (!dispatch(isCompoundLocked(cmp.datasetID, cmp.molecule)) && !isCompoundFromVectorSelector(cmp.molecule)) { + result = cmp; + break; + } + } + + return result; + }; + + const getPrevItemForIteration = currentItem => { + let result = null; + + const currentItemIndex = moleculesObjectIDListOfCompoundsToBuy.findIndex( + cmp => cmp.molecule.id === currentItem.molecule.id && cmp.datasetID === currentItem.datasetID + ); + for (let i = currentItemIndex - 1; i >= 0; i--) { + const cmp = moleculesObjectIDListOfCompoundsToBuy[i]; + if (!dispatch(isCompoundLocked(cmp.datasetID, cmp.molecule)) && !isCompoundFromVectorSelector(cmp.molecule)) { + result = cmp; + break; + } + } + + return result; + }; + + const handleClickOnDownArrow = async event => { + const unlockedVisibleCompounds = dispatch(getAllVisibleButNotLockedSelectedCompounds()); + //one unlocked compound is what we want because it designate where the iteration will start + if (unlockedVisibleCompounds?.length > 1 && askLockSelectedCompoundsQuestion) { + setLockCompoundsDialogAnchorE1(event.currentTarget); + dispatch(setIsOpenLockVisibleCompoundsDialogGlobal(true)); + } else { + const firstItem = getFirstItemForIterationStart(); + const nextItem = getNextItemForIteration(firstItem); + + if (firstItem && nextItem) { + const moleculeTitleNext = nextItem && nextItem.molecule?.name; + const node = getNode(nextItem.molecule?.id); + setScrollToMoleculeId(nextItem.molecule?.id); + + let dataValue = { + colourToggle: getRandomColor(firstItem.molecule), + isLigandOn: ligandList.includes(firstItem.molecule?.id), + isProteinOn: proteinList.includes(firstItem.molecule?.id), + isComplexOn: complexList.includes(firstItem.molecule?.id), + isSurfaceOn: surfaceList.includes(firstItem.molecule?.id) + }; + + dispatch(setCrossReferenceCompoundName(moleculeTitleNext)); + + if (node) { + setSelectedMoleculeRef(node); + } + dispatch( + moveSelectedDatasetMoleculeUpDown( + stage, + firstItem.datasetID, + firstItem.molecule, + nextItem.datasetID, + nextItem.molecule, + dataValue, + ARROW_TYPE.DOWN + ) + ); + } + } + }; + + const handleClickOnUpArrow = async event => { + const unlockedVisibleCompounds = dispatch(getAllVisibleButNotLockedSelectedCompounds()); + //one unlocked compound is what we want because it designate where the iteration will start + if (unlockedVisibleCompounds?.length > 1 && askLockSelectedCompoundsQuestion) { + setLockCompoundsDialogAnchorE1(event.currentTarget); + dispatch(setIsOpenLockVisibleCompoundsDialogGlobal(true)); + } else { + const firstItem = getFirstItemForIterationStart(); + const prevItem = getPrevItemForIteration(firstItem); + + if (firstItem && prevItem) { + const moleculeTitleNext = prevItem && prevItem.molecule?.name; + const node = getNode(prevItem.molecule?.id); + setScrollToMoleculeId(prevItem.molecule?.id); + + let dataValue = { + colourToggle: getRandomColor(firstItem.molecule), + isLigandOn: ligandList.includes(firstItem.molecule?.id), + isProteinOn: proteinList.includes(firstItem.molecule?.id), + isComplexOn: complexList.includes(firstItem.molecule?.id), + isSurfaceOn: surfaceList.includes(firstItem.molecule?.id) + }; + + dispatch(setCrossReferenceCompoundName(moleculeTitleNext)); + + if (node) { + setSelectedMoleculeRef(node); + } + dispatch( + moveSelectedDatasetMoleculeUpDown( + stage, + firstItem.datasetID, + firstItem.molecule, + prevItem.datasetID, + prevItem.molecule, + dataValue, + ARROW_TYPE.UP + ) + ); + } + } + }; + return ( { ref={inspirationDialogRef} /> )} + {askLockSelectedCompoundsQuestion && isLockVisibleCompoundsDialogOpenGlobal && ( + + )} {isOpenCrossReferenceDialog && ( )} - + {/* Selection */} {Object.keys(compoundsColors).map(item => ( <> - - dispatch(onClickFilterClassCheckBox(e))} - checked={colorFilterSettings.hasOwnProperty(item)} - > - + endAdornment: ( + dispatch(onStartEditColorClassName(e))} + onClick={e => { + dispatch(onStartEditColorClassName(e)); + inputRefs[item].current.focus(); + inputRefs[item].current.select(); + }} > + ), + startAdornment: ( + + dispatch(onClickFilterClassCheckBox(e))} + checked={colorFilterSettings.hasOwnProperty(item)} + > + ) }} autoComplete="off" + inputRef={inputRefs[item]} id={`${item}`} key={`CLASS_${item}`} variant="standard" @@ -519,7 +788,6 @@ export const SelectedCompoundList = memo(() => { classes[item], colorFilterSettings.hasOwnProperty(item) && classes.selectedInput )} - label={compoundsColors[item].text} onChange={e => dispatch(onChangeCompoundClassValue(e))} onKeyDown={e => dispatch(onKeyDownCompoundClass(e))} // onKeyDown={e => dispatch(onKeyDownFilterClass(e))} @@ -530,6 +798,44 @@ export const SelectedCompoundList = memo(() => { ))} + + + + + + + + + + + + + + {currentMolecules.length > 0 && ( @@ -593,6 +899,8 @@ export const SelectedCompoundList = memo(() => { } } } + + const isLocked = dispatch(isCompoundLocked(data.datasetID, data.molecule)); // } return ( isVisible && ( @@ -603,7 +911,8 @@ export const SelectedCompoundList = memo(() => { imageWidth={imgWidth} data={data.molecule} datasetID={data.datasetID} - setRef={setSelectedMoleculeRef} + // setRef={setSelectedMoleculeRef} + ref={addMoleculeViewRef} showCrossReferenceModal previousItemData={index > 0 && array[index - 1]} nextItemData={index < array?.length && array[index + 1]} @@ -612,12 +921,15 @@ export const SelectedCompoundList = memo(() => { C={complexList.includes(data.molecule.id)} S={surfaceList.includes(data.molecule.id)} V={false} - arrowsHidden + arrowsHidden={false} dragDropEnabled shoppingCartColors={shoppingCartColors} isAddedToShoppingCart={isAddedToShoppingCart} inSelectedCompoundsList colorButtonsEnabled={areColorButtonsEnabled} + isLocked={isLocked} + setRef={setSelectedMoleculeRef} + getNode={getNode} /> ) ); diff --git a/js/components/datasets/useScrollToCompound.js b/js/components/datasets/useScrollToCompound.js new file mode 100644 index 000000000..da333b329 --- /dev/null +++ b/js/components/datasets/useScrollToCompound.js @@ -0,0 +1,58 @@ +import { useCallback, useEffect, useState } from 'react'; + +export const useScrollToCompound = () => { + const [moleculeViewRefs, setMoleculeViewRefs] = useState({}); + const [scrollToMoleculeId, setScrollToMoleculeId] = useState(null); + + useEffect(() => { + if (scrollToMoleculeId !== null) { + const node = moleculeViewRefs[scrollToMoleculeId]; + if (node) { + setScrollToMoleculeId(null); + if (!elementIsVisibleInViewport(node)) { + setTimeout(() => { + node.scrollIntoView(); + }); + } + } + } + }, [moleculeViewRefs, scrollToMoleculeId]); + + const elementIsVisibleInViewport = (el, partiallyVisible = false) => { + const { top, left, bottom, right } = el.getBoundingClientRect(); + const { innerHeight, innerWidth } = window; + return partiallyVisible + ? ((top > 0 && top < innerHeight) || (bottom > 0 && bottom < innerHeight)) && + ((left > 0 && left < innerWidth) || (right > 0 && right < innerWidth)) + : top >= 0 && left >= 0 && bottom <= innerHeight && right <= innerWidth; + }; + + // Used to attach the ref of DOM nodes. + // const addMoleculeViewRef = useCallback((moleculeId, node) => { + // if (moleculeId && node) { + // setMoleculeViewRefs(prevRefs => ({ + // ...prevRefs, + // [moleculeId]: node + // })); + // } + // }, []); + + const addMoleculeViewRef = useCallback((moleculeId, node) => { + setMoleculeViewRefs(prevRefs => { + if (prevRefs.hasOwnProperty(moleculeId)) return prevRefs; + return { + ...prevRefs, + [moleculeId]: node + }; + }); + }, []); + + const getNode = useCallback( + molId => { + return moleculeViewRefs[molId]; + }, + [moleculeViewRefs] + ); + + return { addMoleculeViewRef, setScrollToMoleculeId, getNode }; +}; diff --git a/js/components/datasets/useScrollToSelected.js b/js/components/datasets/useScrollToSelected.js index e5c1db38d..e1c796277 100644 --- a/js/components/datasets/useScrollToSelected.js +++ b/js/components/datasets/useScrollToSelected.js @@ -97,12 +97,29 @@ export const useScrollToSelected = (datasetID, moleculesPerPage, setCurrentPage) }; // Used to attach the ref of DOM nodes. + // const addMoleculeViewRef = useCallback((moleculeId, node) => { + // setMoleculeViewRefs(prevRefs => ({ + // ...prevRefs, + // [moleculeId]: node + // })); + // }, []); + const addMoleculeViewRef = useCallback((moleculeId, node) => { - setMoleculeViewRefs(prevRefs => ({ - ...prevRefs, - [moleculeId]: node - })); + setMoleculeViewRefs(prevRefs => { + if (prevRefs.hasOwnProperty(moleculeId)) return prevRefs; + return { + ...prevRefs, + [moleculeId]: node + }; + }); }, []); - return { addMoleculeViewRef, setScrollToMoleculeId }; + const getNode = useCallback( + molId => { + return moleculeViewRefs[molId]; + }, + [moleculeViewRefs] + ); + + return { addMoleculeViewRef, setScrollToMoleculeId, getNode }; }; diff --git a/js/components/preview/Preview.js b/js/components/preview/Preview.js index a6d6d1f8e..27083a829 100644 --- a/js/components/preview/Preview.js +++ b/js/components/preview/Preview.js @@ -57,7 +57,7 @@ const useStyles = makeStyles(theme => ({ gap: theme.spacing(), flexWrap: 'wrap', height: '100%', - overflow: 'auto' + overflow: 'hidden' }, controls: { width: '100%' diff --git a/js/components/preview/ResizableLayout.js b/js/components/preview/ResizableLayout.js index 5be28829d..06a2e35f9 100644 --- a/js/components/preview/ResizableLayout.js +++ b/js/components/preview/ResizableLayout.js @@ -1,7 +1,7 @@ import { makeStyles } from '@material-ui/core'; import { clamp } from 'lodash'; import React, { useCallback, useEffect, useState } from 'react'; -import { useSelector } from 'react-redux'; +import { useSelector, useDispatch } from 'react-redux'; import { OutPortal } from 'react-reverse-portal'; import HitNavigator from './molecule/hitNavigator'; import { ProjectHistoryPanel } from './projectHistoryPanel'; @@ -10,6 +10,7 @@ import { RHS } from './rhs'; import TagDetails from './tags/details/tagDetails'; import TagSelector from './tags/tagSelector'; import { ViewerControls } from './viewerControls'; +import { setResizableLayout } from '../../reducers/selection/actions'; const useStyles = makeStyles(theme => ({ root: { @@ -32,13 +33,15 @@ const useStyles = makeStyles(theme => ({ })); const sideWidth = 500; -let panelHeight = 200; +let panelHeight = 0; let totalTagDetailHeight = 200; const resizerSize = 20; let screenHeight = 0; +let tagDetails = 100; export const ResizableLayout = ({ gridRef, hideProjects, showHistory, onShowHistoryChange, nglPortal }) => { const classes = useStyles(); + const dispatch = useDispatch(); const sidesOpen = useSelector(state => state.previewReducers.viewerControls.sidesOpen); @@ -48,8 +51,28 @@ export const ResizableLayout = ({ gridRef, hideProjects, showHistory, onShowHist const preTagList = useSelector(state => state.apiReducers.tagList); const [tagDetailsHeight, setTagDetailsHeight] = useState(); const [hitNavigatorHeight, setHitNavigatorHeight] = useState(panelHeight * 2); + const tagDetailView = useSelector(state => state.selectionReducers.tagDetailView); - totalTagDetailHeight = (preTagList.length*15) + 40; + const tags = useSelector(state => state.apiReducers.tagList); + const tagsLength = tags.length; + + if (tagDetailView) { + totalTagDetailHeight = (preTagList.length / 2) * 15 + 30; + } else { + if (preTagList.length < 10) { + totalTagDetailHeight = preTagList.length * 15 + 45; + } else { + totalTagDetailHeight = preTagList.length * 15 + 55; + } + } + + if (tagsLength > 7 && tagsLength < 11) { + tagDetails = 110 + } + if (tagsLength > 11 && tagsLength < 15) { + tagDetails = 130 + } + useEffect(() => { if (sidesOpen.LHS) { @@ -129,6 +152,7 @@ export const ResizableLayout = ({ gridRef, hideProjects, showHistory, onShowHist const onTagDetailsResize = useCallback( (_, y) => { + dispatch(setResizableLayout(true)) setTagDetailsHeight(() => { const gridRect = gridRef.current?.elementRef.current.firstChild.getBoundingClientRect(); @@ -172,15 +196,20 @@ export const ResizableLayout = ({ gridRef, hideProjects, showHistory, onShowHist - { - /* hide section Hit List Filter(LHS) - task #576 + {/* hide section Hit List Filter(LHS) - task #576
- */ - } -
+ */} +
diff --git a/js/components/preview/compounds/compoundList.js b/js/components/preview/compounds/compoundList.js index f28e29df6..7177b990d 100644 --- a/js/components/preview/compounds/compoundList.js +++ b/js/components/preview/compounds/compoundList.js @@ -36,11 +36,19 @@ import classNames from 'classnames'; const useStyles = makeStyles(theme => ({ textField: { - // marginLeft: theme.spacing(1), + marginLeft: theme.spacing(0.5), // marginRight: theme.spacing(1), - width: 70, + width: 90, + borderRadius: 10, + '& .MuiFormLabel-root': { paddingLeft: theme.spacing(1) + }, + '& .MuiInput-underline:before': { + borderBottom: '0px solid' + }, + '& .MuiInput-underline:after': { + borderBottom: '0px solid' } }, selectedInput: { @@ -86,7 +94,7 @@ const useStyles = makeStyles(theme => ({ }, editClassNameIconSelected: { padding: '0px', - color: theme.palette.primary.main + color: 'red' } })); @@ -113,6 +121,14 @@ export const CompoundList = memo(() => { [compoundsColors.apricot.key]: apricotInput }; + const inputRefs = { + [compoundsColors.blue.key]: useRef(), + [compoundsColors.red.key]: useRef(), + [compoundsColors.green.key]: useRef(), + [compoundsColors.purple.key]: useRef(), + [compoundsColors.apricot.key]: useRef() + }; + const currentCompoundClass = useSelector(state => state.previewReducers.compounds.currentCompoundClass); const canLoadMoreCompounds = useSelector(state => getCanLoadMoreCompounds(state)); const currentVector = useSelector(state => state.selectionReducers.currentVector); @@ -131,35 +147,42 @@ export const CompoundList = memo(() => { {Object.keys(compoundsColors).map(item => ( <> - - dispatch(onChangeCompoundClassCheckbox(e))} - checked={currentCompoundClass === item} - > - + endAdornment: ( + dispatch(onStartEditColorClassName(e))} + onClick={e => { + dispatch(onStartEditColorClassName(e)); + inputRefs[item].current.focus(); + inputRefs[item].current.select(); + }} > + ), + startAdornment: ( + + dispatch(onChangeCompoundClassCheckbox(e))} + checked={currentCompoundClass === item} + > + ) }} autoComplete="off" + inputRef={inputRefs[item]} id={`${item}`} key={`CLASS_${item}`} variant="standard" @@ -168,7 +191,6 @@ export const CompoundList = memo(() => { classes[item], currentCompoundClass === item && classes.selectedInput )} - label={compoundsColors[item].text} onChange={e => dispatch(onChangeCompoundClassValue(e))} onKeyDown={e => dispatch(onKeyDownCompoundClass(e))} onClick={e => dispatch(onClickCompoundClass(e))} diff --git a/js/components/preview/compounds/redux/dispatchActions.js b/js/components/preview/compounds/redux/dispatchActions.js index ce02a4f22..461d5f408 100644 --- a/js/components/preview/compounds/redux/dispatchActions.js +++ b/js/components/preview/compounds/redux/dispatchActions.js @@ -299,7 +299,7 @@ export const prepareFakeFilterData = () => (dispatch, getState) => { }; export const isCompoundFromVectorSelector = data => { - if (data['index'] !== undefined) { + if (data && data['index'] !== undefined) { return true; } else { return false; diff --git a/js/components/preview/compounds/redux/reducer.js b/js/components/preview/compounds/redux/reducer.js index d37121b06..4a3490939 100644 --- a/js/components/preview/compounds/redux/reducer.js +++ b/js/components/preview/compounds/redux/reducer.js @@ -8,15 +8,16 @@ const defaultSelectedCmpdsClass = { [compoundsColors.apricot.key]: [] }; -const defaultCompoundsClasses = { - [compoundsColors.blue.key]: undefined, - [compoundsColors.red.key]: undefined, - [compoundsColors.green.key]: undefined, - [compoundsColors.purple.key]: undefined, - [compoundsColors.apricot.key]: undefined +const defaultCmpdsValues = { + [compoundsColors.blue.key]: compoundsColors.blue.key, + [compoundsColors.red.key]: compoundsColors.red.key, + [compoundsColors.green.key]: compoundsColors.green.key, + [compoundsColors.purple.key]: compoundsColors.purple.key, + [compoundsColors.apricot.key]: compoundsColors.apricot.key }; export const INITIAL_STATE = { + ...defaultCmpdsValues, currentPage: -1, compoundsPerPage: 20, /* currentCompounds: [{ @@ -27,7 +28,7 @@ export const INITIAL_STATE = { }] */ currentCompounds: [], currentCompoundClass: compoundsColors.blue.key, - ...defaultCompoundsClasses, + // ...defaultCompoundsClasses, selectedCompoundsClass: defaultSelectedCmpdsClass, highlightedCompoundId: null, showedCompoundList: [], @@ -50,7 +51,7 @@ export const RESET_STATE = { }] */ currentCompounds: [], currentCompoundClass: compoundsColors.blue.key, - ...defaultCompoundsClasses, + ...defaultCmpdsValues, selectedCompoundsClass: defaultSelectedCmpdsClass, highlightedCompoundId: null, showedCompoundList: [], @@ -92,7 +93,7 @@ export const compounds = (state = INITIAL_STATE, action = {}) => { }); case constants.RESET_COMPOUND_CLASSES: - return Object.assign({}, state, { ...defaultCompoundsClasses }); + return Object.assign({}, state, { ...defaultCmpdsValues }); case constants.SET_HIGHLIGHTED_COMPOUND_ID: return Object.assign({}, state, { highlightedCompoundId: action.payload }); @@ -155,9 +156,9 @@ export const compounds = (state = INITIAL_STATE, action = {}) => { return Object.assign({}, state, { selectedCompoundsClass: defaultSelectedCmpdsClass }); - - case constants.SET_SELECTED_COMPOUNDS: - return {...state, allSelectedCompounds: action.payload}; + + case constants.SET_SELECTED_COMPOUNDS: + return { ...state, allSelectedCompounds: action.payload }; case constants.RELOAD_REDUCER: return Object.assign({}, state, { ...action.payload }); diff --git a/js/components/preview/molecule/moleculeList.js b/js/components/preview/molecule/moleculeList.js index 467717b00..7d2934acb 100644 --- a/js/components/preview/molecule/moleculeList.js +++ b/js/components/preview/molecule/moleculeList.js @@ -72,7 +72,7 @@ import { extractTargetFromURLParam } from '../utils'; const useStyles = makeStyles(theme => ({ container: { minHeight: '100px', - height: '99%', + height: '100%', width: 'inherit', color: theme.palette.black }, @@ -93,7 +93,7 @@ const useStyles = makeStyles(theme => ({ }, gridItemList: { overflow: 'auto', - height: `calc(100% - ${theme.spacing(6)}px - ${theme.spacing(2)}px)` + height: `calc(99% - ${theme.spacing(6)}px - ${theme.spacing(2)}px)` }, centered: { display: 'flex', diff --git a/js/components/preview/molecule/moleculeView/moleculeView.js b/js/components/preview/molecule/moleculeView/moleculeView.js index f809b74a4..57e79e980 100644 --- a/js/components/preview/molecule/moleculeView/moleculeView.js +++ b/js/components/preview/molecule/moleculeView/moleculeView.js @@ -48,9 +48,10 @@ 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'; import MoleculeSelectCheckbox from './moleculeSelectCheckbox'; import useClipboard from 'react-use-clipboard'; +import Popover from '@mui/material/Popover'; +import Typography from '@mui/material/Typography'; const useStyles = makeStyles(theme => ({ container: { @@ -224,9 +225,33 @@ const useStyles = makeStyles(theme => ({ backgroundColor: theme.palette.white }, imageActions: { + position: 'absolute', + top: 0, + left: 0 + }, + imageTagActions: { position: 'absolute', top: 0, right: 0 + }, + tagPopover: { + height: '15px', + width: '55px', + padding: '0px', + fontSize: '10px', + backgroundColor: '#e0e0e0', + borderRadius: '7px', + textAlign: 'center', + opacity: '0.40' + }, + popover: { + paddingLeft: '5px', + fontSize: '10px', + borderRadius: '7px', + border: '0px black solid', + paddingRight: '5px', + minWidth: '55px', + textAlign: 'center' } })); @@ -324,9 +349,12 @@ const MoleculeView = memo( const [densityModalOpen, setDensityModalOpen] = useState(false); const [moleculeTooltipOpen, setMoleculeTooltipOpen] = useState(false); - const [tagEditorTooltipOpen, setTagEditorTooltipOpen] = useState(false); + const [tagPopoverOpen, setTagPopoverOpen] = useState(null); + const moleculeImgRef = useRef(null); + const open = tagPopoverOpen; + let proteinData = data?.proteinData; const getDataForTagsTooltip = () => { @@ -334,15 +362,115 @@ const MoleculeView = memo( return assignedTags; }; - const generateTooltip = () => { + const handlePopoverOpen = event => { + setTagPopoverOpen(event.currentTarget); + }; + + const handlePopoverClose = () => { + setTagPopoverOpen(null); + }; + + const generateTagPopover = () => { const data = getDataForTagsTooltip(); - return ( - - {data.map((t, idx) => ( - /*{t}*/ - - ))} - + + const modifiedObjects = data.map(obj => { + if (obj.tag.length > 8) { + return { ...obj, tag: obj.tag.slice(0, 8) + '...' }; + } + return obj; + }); + + const firstThreeTags = modifiedObjects.slice(0, 3); + + return modifiedObjects.length > 0 ? ( +
+ + { + <> + {firstThreeTags.length > 0 ? ( +
+ {firstThreeTags[0].tag} +
+ ) : ( + '' + )} + {firstThreeTags.length > 1 ? ( +
+ {firstThreeTags[1].tag} +
+ ) : ( + '' + )} + {firstThreeTags.length > 2 ? ( +
+ {firstThreeTags[2].tag} +
+ ) : ( + '' + )} + + } +
+ + {data.map(t => ( +
+ {t.tag} +
+ ))} +
+
+ ) : ( +
); }; @@ -863,18 +991,7 @@ const MoleculeView = memo( {svg_image}
{(moleculeTooltipOpen || isTagEditorInvokedByMolecule) && ( - { - setTagEditorTooltipOpen(true); - }} - onClose={() => { - setTagEditorTooltipOpen(false); - }} - placement={'left'} - > + )}
+
{generateTagPopover()}
({ display: 'flex', flexDirection: 'column', overflow: 'auto', - height: '90%', + height: '100%', width: '100%', marginTop: -theme.spacing(), justifyContent: 'space-between', @@ -63,7 +65,7 @@ const useStyles = makeStyles(theme => ({ gap: 1 }, columnLabel: { - display: 'flex', + display: 'flex' }, dateLabel: { gridColumn: '6' @@ -75,10 +77,14 @@ const useStyles = makeStyles(theme => ({ columnTitle: { fontSize: theme.typography.pxToRem(13) }, + columnTitleGrid: { + fontSize: theme.typography.pxToRem(13), + position: "center" + }, tagModeSwitch: { - width: 132, // Should be adjusted if a label for the switch changes + width: 32, // Should be adjusted if a label for the switch changes // justify: 'flex-end', - marginRight: '110px', + marginRight: '90px', marginLeft: '1px' }, headerContainer: { @@ -108,7 +114,7 @@ const useStyles = makeStyles(theme => ({ }, '&:disabled': { borderRadius: 0, - borderColor: '#FFFFFF', + borderColor: '#FFFFFF' } }, contColButtonSelected: { @@ -126,7 +132,7 @@ const useStyles = makeStyles(theme => ({ backgroundColor: theme.palette.primary.main // color: theme.palette.black } - }, + } })); /** @@ -143,6 +149,9 @@ const TagDetails = memo(() => { const tagMode = useSelector(state => state.selectionReducers.tagFilteringMode); const displayAllMolecules = useSelector(state => state.selectionReducers.displayAllMolecules); const displayUntaggedMolecules = useSelector(state => state.selectionReducers.displayUntaggedMolecules); + const tagDetailView = useSelector(state => state.selectionReducers.tagDetailView); + const resizableLayout = useSelector(state => state.selectionReducers.resizableLayout); + const [tagList, setTagList] = useState([]); const [selectAll, setSelectAll] = useState(true); @@ -260,6 +269,10 @@ const TagDetails = memo(() => { dispatch(setTagFilteringMode(!tagMode)); }; + const viewModeSwitched = () => { + dispatch(setTagDetailView(!tagDetailView)); + }; + const TagModeSwitch = withStyles({ // '& .MuiFormControlLabel-root': { // marginLeft: '0px', @@ -311,43 +324,58 @@ const TagDetails = memo(() => { dispatch ])} headerActions={[ - - - - + + + - - - - + > + + } + label={tagMode ? 'Intersection' : 'Union'} + /> + + + + } + label={tagDetailView ? 'Grid' : 'List'} + /> + + + + + - ]} - > -
- - - - - - - - -
+
+
+
-
- {/* tag name */} -
- - Tag name - - handleHeaderSort('name')}> - - {[1, 2].includes(sortSwitch - offsetName) ? ( - sortSwitch % offsetName < 2 ? ( - + {tagDetailView ? ( + <> +
+ {/* START grid view */} + {/* tag name */} +
+ + Tag name + + handleHeaderSort('name')}> + + {[1, 2].includes(sortSwitch - offsetName) ? ( + sortSwitch % offsetName < 2 ? ( + + ) : ( + + ) ) : ( - - ) - ) : ( - - )} - - -
+ + )} + + +
+
+ + + {filteredTagList && + filteredTagList.map((tag, idx) => { + return ( + + + + ); + })} + + + ) : ( +
+ {/* tag name */} +
+ + Tag name + + handleHeaderSort('name')}> + + {[1, 2].includes(sortSwitch - offsetName) ? ( + sortSwitch % offsetName < 2 ? ( + + ) : ( + + ) + ) : ( + + )} + + +
- {/* category */} -
- - Category - - handleHeaderSort('category')}> - - {[1, 2].includes(sortSwitch - offsetCategory) ? ( - sortSwitch % offsetCategory < 2 ? ( - + {/* category */} +
+ + Category + + handleHeaderSort('category')}> + + {[1, 2].includes(sortSwitch - offsetCategory) ? ( + sortSwitch % offsetCategory < 2 ? ( + + ) : ( + + ) ) : ( - - ) - ) : ( - - )} - - -
+ + )} +
+
+
- {/* creator */} -
- - Creator - - handleHeaderSort('creator')}> - - {[1, 2].includes(sortSwitch - offsetCreator) ? ( - sortSwitch % offsetCreator < 2 ? ( - + {/* creator */} +
+ + Creator + + handleHeaderSort('creator')}> + + {[1, 2].includes(sortSwitch - offsetCreator) ? ( + sortSwitch % offsetCreator < 2 ? ( + + ) : ( + + ) ) : ( - - ) - ) : ( - - )} - - -
+ + )} +
+
+
- {/* date */} -
- - Date - - handleHeaderSort('date')}> - - {[1, 2].includes(sortSwitch - offsetDate) ? ( - sortSwitch % offsetDate < 2 ? ( - + {/* date */} +
+ + Date + + handleHeaderSort('date')}> + + {[1, 2].includes(sortSwitch - offsetDate) ? ( + sortSwitch % offsetDate < 2 ? ( + + ) : ( + + ) ) : ( - - ) - ) : ( - - )} - - -
-
+ + )} + + +
+
- {filteredTagList && - filteredTagList.map((tag, idx) => { - return ( - - ); - })} + {filteredTagList && + filteredTagList.map((tag, idx) => { + return ( + + ); + })} +
+ )} +
+
-
); diff --git a/js/components/preview/tags/details/tagGridRows.js b/js/components/preview/tags/details/tagGridRows.js new file mode 100644 index 000000000..a152df8d0 --- /dev/null +++ b/js/components/preview/tags/details/tagGridRows.js @@ -0,0 +1,38 @@ +import React, { memo, useState, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import TagView from '../tagView'; +import { removeSelectedTag, addSelectedTag } from '../redux/dispatchActions'; + +/** + * TagGridRows represents a Grid view for tags + */ +const TagGridRows = memo(({ tag }) => { + const dispatch = useDispatch(); + const selectedTagList = useSelector(state => state.selectionReducers.selectedTagList); + + const handleTagClick = (selected, tag) => { + if (selected) { + dispatch(removeSelectedTag(tag)); + } else { + dispatch(addSelectedTag(tag)); + } + }; + + return ( + <> + {/* TagView Chip */} + i.id === tag.id)} + handleClick={handleTagClick} + // disabled={!DJANGO_CONTEXT.pk} + disabled={false} + isEdit={true} + isTagEditor={true} + > + + ); +}); + +export default TagGridRows; diff --git a/js/components/preview/tags/modal/tagEditor.js b/js/components/preview/tags/modal/tagEditor.js index f4a411ab9..ed949bead 100644 --- a/js/components/preview/tags/modal/tagEditor.js +++ b/js/components/preview/tags/modal/tagEditor.js @@ -1,11 +1,11 @@ import React, { forwardRef, memo, useState } from 'react'; -import { Grid, Popper, IconButton, Tooltip, makeStyles } from '@material-ui/core'; +import { Grid, Popper, IconButton, Tooltip, makeStyles, FormControlLabel, Switch } from '@material-ui/core'; import { Panel } from '../../../common'; import { Close } from '@material-ui/icons'; import { useDispatch, useSelector } from 'react-redux'; import { updateMoleculeInMolLists, updateMoleculeTag } from '../../../../reducers/api/actions'; import { getMoleculeForId } from '../redux/dispatchActions'; -import { setMoleculeForTagEdit, setIsTagGlobalEdit } from '../../../../reducers/selection/actions'; +import { setMoleculeForTagEdit, setIsTagGlobalEdit, setAssignTagView } from '../../../../reducers/selection/actions'; import { updateExistingTag } from '../api/tagsApi'; import { DJANGO_CONTEXT } from '../../../../utils/djangoContext'; import { @@ -16,16 +16,20 @@ import { } from '../utils/tagUtils'; import TagCategory from '../tagCategory'; import { TaggingInProgressModal } from './taggingInProgressModal'; +import { withStyles } from '@material-ui/core/styles'; +import { blue } from '@material-ui/core/colors'; const useStyles = makeStyles(theme => ({ paper: { - height: 343, - overflowY: 'hidden' + maxHeight: 343, + height: 'auto', + overflowY: 'auto' }, content: { overflowY: 'auto', overflowX: 'hidden', - height: 300 + height: 'auto', + maxHeight: 300 }, contColButton: { minWidth: 'fit-content', @@ -95,6 +99,11 @@ const useStyles = makeStyles(theme => ({ '& .MuiInput-underline:after': { borderBottomColor: 'inherit' } + }, + tagModeSwitch: { + width: 32, // Should be adjusted if a label for the switch changes + marginRight: '100px', + marginLeft: '1px' } })); @@ -116,9 +125,10 @@ export const TagEditor = memo( moleculesToEditIds.push(molId); } const moleculesToEdit = moleculesToEditIds.map(id => dispatch(getMoleculeForId(id))); - moleculeTags = moleculeTags.sort(compareTagsAsc); + const assignTagView = useSelector(state => state.selectionReducers.assignTagView); + const handleCloseModal = () => { if (open) { dispatch(setOpenDialog(false)); @@ -201,6 +211,24 @@ export const TagEditor = memo( setTaggingInProgress(false); }; + const viewModeSwitched = () => { + dispatch(setAssignTagView(!assignTagView)); + }; + + const TagModeSwitch = withStyles({ + switchBase: { + color: blue[300], + '&$checked': { + color: blue[500] + }, + '&$checked + $track': { + backgroundColor: blue[500] + } + }, + checked: {}, + track: {} + })(Switch); + return ( + + } + label={assignTagView ? 'Grid' : 'List'} + /> + , ({ category: { - display: 'flex' + display: '-webkit-box' } })); @@ -18,6 +19,8 @@ const TagCategory = memo(({ tagClickCallback, disabled = false }) => { let tagList = useSelector(state => state.apiReducers.tagList); tagList = tagList.sort(compareTagsAsc); + const assignTagView = useSelector(state => state.selectionReducers.assignTagView); + const siteCategory = categoryList.find(c => c.category === CATEGORY_TYPE.SITE); const seriesCategory = categoryList.find(c => c.category === CATEGORY_TYPE.SERIES); const forumCategory = categoryList.find(c => c.category === CATEGORY_TYPE.FORUM); @@ -32,19 +35,31 @@ const TagCategory = memo(({ tagClickCallback, disabled = false }) => { return ( <> - - - - - - - - - - - - - + {assignTagView === false || assignTagView === undefined ? ( + <> + + + + + + + + + + + + + + ) : ( + <> + + + + + + + + )} ); }); diff --git a/js/components/preview/tags/tagCategoryGridView.js b/js/components/preview/tags/tagCategoryGridView.js new file mode 100644 index 000000000..2cf188bc1 --- /dev/null +++ b/js/components/preview/tags/tagCategoryGridView.js @@ -0,0 +1,117 @@ +import React, { memo } from 'react'; +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, getMoleculeForId } from './redux/dispatchActions'; +import { getAllTagsForMol } from './utils/tagUtils'; + +const useStyles = makeStyles(theme => ({ + divContainer: { + flexDirection: 'column', + display: 'flex', + height: '100%', + width: '100%', + textAlign: 'left', + gap: 1 + }, + categoryItem: { + paddingRight: theme.spacing(1), + textAlign: 'left' + }, + headerItemTitle: { + fontSize: theme.typography.pxToRem(13) + } +})); + +/** + * 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 TagCategoryGridView = memo(({ name, tags, specialTags, clickCallback, disabled = false }) => { + const classes = useStyles(); + const selectedTagList = useSelector(state => state.selectionReducers.selectedTagList); + const dispatch = useDispatch(); + + const tagList = useSelector(state => state.apiReducers.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 (clickCallback !== undefined) { + clickCallback(selected, tag); + } else { + if (selected) { + dispatch(removeSelectedTag(tag)); + } else { + dispatch(addSelectedTag(tag)); + } + } + }; + + const isTagSelected = tag => { + let result = false; + let partiallySelected = 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; + } else { + partiallySelected = true; + } + } + return { isSelected: result, isPartiallySelected: partiallySelected }; + }; + + return ( + <> + + {name && ( + + + {name} + + + )} + + {(tags || specialTags) && ( + + {tags && + tags.map((tag, idx) => { + let selected = selectedTagList.some(i => i.id === tag.id); + let tagSelected = isTagSelected(tag); + return ( + + + + ); + })} + + )} + + + ); +}); + +export default TagCategoryGridView; diff --git a/js/components/preview/tags/tagCategoryView.js b/js/components/preview/tags/tagCategoryListView.js similarity index 95% rename from js/components/preview/tags/tagCategoryView.js rename to js/components/preview/tags/tagCategoryListView.js index e0e43dc55..c030926d1 100644 --- a/js/components/preview/tags/tagCategoryView.js +++ b/js/components/preview/tags/tagCategoryListView.js @@ -19,9 +19,6 @@ const useStyles = makeStyles(theme => ({ paddingRight: theme.spacing(1), textAlign: 'left' }, - headerItem: { - paddingLeft: theme.spacing(2) - }, headerItemTitle: { fontSize: theme.typography.pxToRem(13) } @@ -32,7 +29,7 @@ const useStyles = makeStyles(theme => ({ * -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, disabled = false }) => { +const TagCategoryListView = memo(({ name, tags, specialTags, clickCallback, disabled = false }) => { const classes = useStyles(); const selectedTagList = useSelector(state => state.selectionReducers.selectedTagList); const dispatch = useDispatch(); @@ -115,4 +112,4 @@ const TagCategoryView = memo(({ name, tags, specialTags, clickCallback, disabled ); }); -export default TagCategoryView; +export default TagCategoryListView; diff --git a/js/components/preview/tags/tagView.js b/js/components/preview/tags/tagView.js index 8acddb214..f1dc3a323 100644 --- a/js/components/preview/tags/tagView.js +++ b/js/components/preview/tags/tagView.js @@ -1,7 +1,7 @@ import React, { memo, useState } from 'react'; import { Grid, makeStyles, Chip, Tooltip, Avatar } from '@material-ui/core'; import { Edit, Check } from '@material-ui/icons'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { getFontColorByBackgroundColor } from '../../../utils/colors'; import { TagEditModal } from './modal/tagEditModal'; import classNames from 'classnames'; @@ -22,7 +22,7 @@ const useStyles = makeStyles(theme => ({ flexDirection: 'row', justifyContent: 'flex-start', borderRadius: '7px', - lineHeight: '1.1', + //lineHeight: '0', textAlign: 'center', '& .MuiChip-labelSmall': { overflowWrap: 'anywhere', @@ -32,7 +32,9 @@ const useStyles = makeStyles(theme => ({ paddingRight: '2px', fontWeight: '400', fontStyle: 'normal', - letterSpacing: '0.144px' + letterSpacing: '0.144px', + width: 'inherit', + textAlign: 'left' }, '& .MuiChip-deleteIcon': { display: 'none', @@ -58,9 +60,9 @@ const useStyles = makeStyles(theme => ({ margin: '0 !important', padding: '0 !important', '& .MuiChip-labelSmall': { - textAlign: 'left !important' + //textAlign: 'left !important' }, - width: '200px' + width: '160px' }, chipSelected: { '& .MuiChip-iconSmall': { @@ -94,35 +96,41 @@ const TagView = memo( isTagEditor = false, partiallySelected = false }) => { - const tagData = tag; + const originalTagData = tag; const classes = useStyles(); const dispatch = useDispatch(); const [tagEditModalOpen, setTagEditModalOpen] = useState(false); - // console.log(`Tag: ${tagData.tag}`); - // console.log(`tagColor: ${tagColor}`); + const tagDetailView = useSelector(state => state.selectionReducers.tagDetailView); + + let tagData = []; + if (originalTagData.tag.length > 26) { + tagData = { ...originalTagData, tag: originalTagData.tag.slice(0, 26) + '...' }; + } else { + tagData = originalTagData; + } + const bgColor = tagData.colour || '#e0e0e0'; - // console.log(`bgColor: ${bgColor}`); const color = getFontColorByBackgroundColor(bgColor); - // console.log(`font color: ${color}`); + const style = isTagEditor - ? { backgroundColor: bgColor, color: color } + ? { + backgroundColor: bgColor, + color: color, + width: tagDetailView === true ? '160px' : '200px' + } : selected - ? { backgroundColor: bgColor, color: color } - : { backgroundColor: 'white', color: 'black', borderColor: bgColor }; - // console.log(`style: ${style}`); - - // console.log('-------------------------------'); - const isTagDisabled = false; - - const determineDisabled = () => { - // let result = false; - // if (isEdit && disabled) { - // result = true; - // } - // return result; - return disabled; - }; + ? { + backgroundColor: bgColor, + color: color, + width: '160px' + } + : { + backgroundColor: 'white', + color: 'black', + borderColor: bgColor, + width: '160px' + }; const handleDelete = () => { if (editable) { @@ -155,7 +163,7 @@ const TagView = memo( className: `${classes.chip} ${selected && !isSpecialTag ? classes.chipSelected : null} ${ classes.tagDetailsChip }`, - label: tagData.tag, + label: tagDetailView === true ? tagData.tag : originalTagData.tag, clickable: true, color: bgColor, style: style, @@ -163,13 +171,7 @@ const TagView = memo( handleClick && handleClick(selected, tag, allTags); }, deleteIcon: getDeleteIcon(), - onDelete: getDeleteAction(), - disabled: determineDisabled(), - icon: ( - - - - ) + onDelete: getDeleteAction() }; } else { return { @@ -177,43 +179,23 @@ const TagView = memo( className: `${classes.chip} ${selected && !isSpecialTag ? classes.chipSelected : null} ${ classes.tagDetailsChip }`, - label: tagData.tag, + label: tagDetailView === true ? tagData.tag : originalTagData.tag, clickable: true, color: bgColor, - style: { backgroundColor: 'white', border: '1px solid rgba(0, 0, 0, 0.23)', borderColor: bgColor }, + style: { + backgroundColor: 'white', + border: '1px solid rgba(0, 0, 0, 0.23)', + borderColor: bgColor, + width: tagDetailView === true ? '160px' : '200px' + }, onClick: () => { handleClick && handleClick(selected, tag, allTags); }, deleteIcon: getDeleteIcon(), - onDelete: getDeleteAction(), - disabled: determineDisabled() + onDelete: getDeleteAction() }; } - - return { - size: 'small', - className: `${classes.chip} ${selected && !isSpecialTag ? classes.chipSelected : null} ${ - classes.tagDetailsChip - }`, - label: tagData.tag, - clickable: true, - color: bgColor, - borderColor: bgColor, - style: style, - onClick: () => { - handleClick && handleClick(selected, tag, allTags); - }, - deleteIcon: getDeleteIcon(), - onDelete: getDeleteAction(), - disabled: determineDisabled(), - icon: ( - - - - ) - }; } - // If in Hit List Filter if (selected) { return { @@ -228,8 +210,7 @@ const TagView = memo( handleClick && handleClick(selected, tag, allTags); }, deleteIcon: getDeleteIcon(), - onDelete: getDeleteAction(), - disabled: determineDisabled() + onDelete: getDeleteAction() }; } @@ -246,15 +227,14 @@ const TagView = memo( handleClick && handleClick(selected, tag, allTags); }, deleteIcon: getDeleteIcon(), - onDelete: getDeleteAction(), - disabled: determineDisabled() + onDelete: getDeleteAction() }; }; return ( <> - + diff --git a/js/reducers/selection/actions.js b/js/reducers/selection/actions.js index 977588e0d..bb8ab4901 100644 --- a/js/reducers/selection/actions.js +++ b/js/reducers/selection/actions.js @@ -486,4 +486,23 @@ export const setNextXMolecules = nextXMolecules => { type: constants.SET_NEXT_X_MOLECULES, nextXMolecules: nextXMolecules }; -}; \ No newline at end of file +}; + +export const setTagDetailView = tagDetailView => { + return { + type: constants.SET_TAG_DETAIL_VIEW, + payload: tagDetailView + }; +}; + +export const setResizableLayout = projects => ({ + type: constants.SET_RESIZABLE_LAYOUT, + payload: projects +}); + +export const setAssignTagView = assignTagView => { + return { + type: constants.SET_ASSIGN_TAGS_VIEW, + payload: assignTagView + }; +}; diff --git a/js/reducers/selection/constants.js b/js/reducers/selection/constants.js index 4d3be5f14..4a7b6a6d7 100644 --- a/js/reducers/selection/constants.js +++ b/js/reducers/selection/constants.js @@ -76,7 +76,13 @@ export const constants = { SET_DISPLAY_UNTAGGED_MOLECULES: prefix + 'SET_DISPLAY_UNTAGGED_MOLECULES', SET_SELECT_ALL_MOLECULES: prefix + 'SET_SELECT_ALL_MOLECULES', SET_UNSELECT_ALL_MOLECULES: prefix + 'SET_UNSELECT_ALL_MOLECULES', - SET_NEXT_X_MOLECULES: prefix + 'SET_NEXT_X_MOLECULES' + SET_NEXT_X_MOLECULES: prefix + 'SET_NEXT_X_MOLECULES', + + SET_ASSIGN_TAGS_VIEW: prefix + 'SET_ASSIGN_TAGS_VIEW', + + SET_TAG_DETAIL_VIEW: prefix + 'SET_TAG_DETAIL_VIEW', + + SET_RESIZABLE_LAYOUT: prefix + 'SET_RESIZABLE_LAYOUT' }; export const PREDEFINED_FILTERS = { diff --git a/js/reducers/selection/selectionReducers.js b/js/reducers/selection/selectionReducers.js index 297b01ef7..6af29f86c 100644 --- a/js/reducers/selection/selectionReducers.js +++ b/js/reducers/selection/selectionReducers.js @@ -437,6 +437,15 @@ export function selectionReducers(state = INITIAL_STATE, action = {}) { case constants.SET_NEXT_X_MOLECULES: return { ...state, nextXMolecules: action.nextXMolecules }; + case constants.SET_ASSIGN_TAGS_VIEW: + return { ...state, assignTagView: action.payload }; + + case constants.SET_TAG_DETAIL_VIEW: + return { ...state, tagDetailView: action.payload }; + + case constants.SET_RESIZABLE_LAYOUT: + return Object.assign({}, state, { resizableLayout: action.payload }); + // Cases like: @@redux/INIT default: return state;