Skip to content

Commit

Permalink
Merge pull request #455 from m2ms/stagingcandidate
Browse files Browse the repository at this point in the history
  • Loading branch information
mwinokan authored Oct 14, 2024
2 parents c244d20 + 4d8ea0c commit 63fa397
Show file tree
Hide file tree
Showing 3 changed files with 169 additions and 17 deletions.
123 changes: 118 additions & 5 deletions js/components/preview/molecule/observationCmpList.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import {
Divider,
Typography,
IconButton,
ButtonGroup
ButtonGroup,
Select,
MenuItem,
Checkbox
} from '@material-ui/core';
import React, { useState, useEffect, useCallback, memo, useRef, useContext, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
Expand Down Expand Up @@ -321,6 +324,89 @@ export const ObservationCmpList = memo(({ hideProjects }) => {

const [predefinedFilter, setPredefinedFilter] = useState(filter !== undefined ? filter.predefined : DEFAULT_FILTER);

const [ascending, setAscending] = useState(true);
const handleAscendingChecked = (event) => setAscending(event.target.checked);
const SORT_OPTIONS = [
'POSE_NAME',
'COMPOUND_CODE',
'CANONSITE_NUMBER',
'CONFORMERSITE_NUMBER',
'OBSERVATION_COUNT'
];
const sortOptions = {
POSE_NAME: {
title: 'Pose name',
handler: (a, b, asc) => compareByPoseName(a, b, asc)
},
COMPOUND_CODE: {
title: 'Compound code',
handler: (a, b, asc) => compareByCompoundCode(a, b, asc)
},
CANONSITE_NUMBER: {
title: 'CanonSite number',
handler: (a, b, asc) => compareByCanonSiteNumber(a, b, asc)
},
CONFORMERSITE_NUMBER: {
title: 'ConformerSite number',
handler: (a, b, asc) => compareByConformerSiteNumber(a, b, asc)
},
OBSERVATION_COUNT: {
title: 'Observation count',
handler: (a, b, asc) => compareByObservationCount(a, b, asc)
}
};
const [sortOption, setSortOption] = useState(SORT_OPTIONS[0]);

const compareByPoseName = (a, b, asc) => {
const aName = a.code;
const bName = b.code;
return asc ? aName.localeCompare(bName, undefined, { numeric: true, sensitivity: 'base' })
: bName.localeCompare(aName, undefined, { numeric: true, sensitivity: 'base' });
};
const compareByCompoundCode = (a, b, asc) => {
const aName = a.main_site_observation_cmpd_code;
const bName = b.main_site_observation_cmpd_code;
return asc ? aName.localeCompare(bName, undefined, { numeric: true, sensitivity: 'base' })
: bName.localeCompare(aName, undefined, { numeric: true, sensitivity: 'base' });
};
const compareByCanonSiteNumber = (a, b, asc) => {
const aName = getCanonSiteTagPrefix(a);
const bName = getCanonSiteTagPrefix(b);
return asc ? aName.localeCompare(bName, undefined, { numeric: true, sensitivity: 'base' })
: bName.localeCompare(aName, undefined, { numeric: true, sensitivity: 'base' });
};
const compareByConformerSiteNumber = (a, b, asc) => {
const aName = getConformerSiteTagPrefix(a);
const bName = getConformerSiteTagPrefix(b);
return asc ? aName.localeCompare(bName, undefined, { numeric: true, sensitivity: 'base' })
: bName.localeCompare(aName, undefined, { numeric: true, sensitivity: 'base' });
};
const compareByObservationCount = (a, b, asc) => {
const aCount = a.site_observations.length;
const bCount = b.site_observations.length;
return asc ? aCount - bCount : bCount - aCount;
};

/**
* Get CanonSites tag for sorting
*/
const getCanonSiteTagPrefix = useCallback(pose => {
const mainObservation = pose.associatedObs.find(observation => observation.id === pose.main_site_observation);
const canonSitesTag = categories.find(tagCategory => tagCategory.category === 'CanonSites');
const canonSite = tags.find(tag => tag.category === canonSitesTag.id && tag.site_observations.includes(mainObservation.id));
return canonSite !== undefined ? canonSite.tag_prefix : '';
}, [categories, tags]);

/**
* Get ConformerSites tag for sorting
*/
const getConformerSiteTagPrefix = useCallback(pose => {
const mainObservation = pose.associatedObs.find(observation => observation.id === pose.main_site_observation);
const conformerSitesTag = categories.find(tagCategory => tagCategory.category === 'ConformerSites');
const conformerSite = tags.find(tag => tag.category === conformerSitesTag.id && tag.site_observations.includes(mainObservation.id));
return conformerSite !== undefined ? conformerSite.tag_prefix : '';
}, [categories, tags]);

const isActiveFilter = !!(filter || {}).active;

const { getNglView } = useContext(NglContext);
Expand Down Expand Up @@ -718,8 +804,9 @@ export const ObservationCmpList = memo(({ hideProjects }) => {
compounds.push(compound);
}
});
compounds.sort((a, b) => sortOptions[sortOption].handler(a, b, ascending));
return compounds;
}, [joinedMoleculeLists, lhsCompoundsList]);
}, [joinedMoleculeLists, lhsCompoundsList, sortOptions, sortOption, ascending]);

useEffect(() => {
if (isObservationDialogOpen && observationsForLHSCmp?.length > 0) {
Expand Down Expand Up @@ -1113,7 +1200,7 @@ export const ObservationCmpList = memo(({ hideProjects }) => {

{
<Tooltip title={selectAllHitsPressed ? 'Unselect all hits' : 'Select all hits'}>
<Grid item style={{ marginLeft: '20px' }}>
<Grid item style={{ marginLeft: '5px' }}>
<Button
variant="outlined"
className={classNames(classes.contColButton, {
Expand All @@ -1133,7 +1220,7 @@ export const ObservationCmpList = memo(({ hideProjects }) => {
}
{selectedDisplayHits === true ? (
<Tooltip title={'Unselect displayed hits'}>
<Grid item style={{ marginLeft: '20px' }}>
<Grid item style={{ marginLeft: '5px' }}>
<Button
variant="outlined"
className={classNames(classes.contColButton, {
Expand All @@ -1152,7 +1239,7 @@ export const ObservationCmpList = memo(({ hideProjects }) => {
</Tooltip>
) : (
<Tooltip title={'Select displayed hits'}>
<Grid item style={{ marginLeft: '20px' }}>
<Grid item style={{ marginLeft: '5px' }}>
<Button
variant="outlined"
className={classNames(classes.contColButton, {
Expand All @@ -1174,6 +1261,32 @@ export const ObservationCmpList = memo(({ hideProjects }) => {
<Typography variant="caption" className={classes.noOfSelectedHits}>{`Selected: ${allSelectedMolecules ? allSelectedMolecules.length : 0
}`}</Typography>
</Grid>
<Grid style={{ marginTop: '4px' }}>
<Typography variant="caption" className={classes.noOfSelectedHits}>Sort by</Typography>
</Grid>
<Grid style={{ marginTop: '4px', marginLeft: '4px' }}>
<Tooltip title={sortOption ? sortOptions[sortOption].title : "Sort by"}>
<Select
value={sortOption}
onChange={(event) => setSortOption(event.target.value)}
// fullWidth
size="small"
style={{ fontSize: 10, width: 75 }}
>
{SORT_OPTIONS.map((option, index) => (
<MenuItem key={`${index}-${option}`} value={option} style={{ fontSize: 12, padding: '3px 7px' }}>
{sortOptions[option].title}
</MenuItem>
))}
</Select>
</Tooltip>
</Grid>
<Tooltip title={ascending ? "Ascending" : "Descending"}>
<Grid style={{ marginTop: '4px' }}>
<Checkbox checked={ascending} onChange={handleAscendingChecked} size="small" style={{ padding: 3 }} />
<Typography variant="caption" className={classes.noOfSelectedHits}>ASC</Typography>
</Grid>
</Tooltip>
</Grid>
<Grid container spacing={1} direction="column" justifyContent="flex-start" className={classes.container}>
<Grid item>
Expand Down
47 changes: 36 additions & 11 deletions js/components/preview/molecule/observationsDialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,9 +176,12 @@ const useStyles = makeStyles(theme => ({
// fontWeight: 'bold',
paddingLeft: theme.spacing(1) / 4,
paddingRight: theme.spacing(1) / 4,
'&:hover': {
'&:hover:not([disabled])': {
cursor: 'pointer',
backgroundColor: theme.palette.primary.light
},
'&:is([disabled])': {
color: theme.palette.dividerDark
}
},
dropdownContentSide: {
Expand Down Expand Up @@ -662,6 +665,16 @@ export const ObservationsDialog = memo(
}
};

/**
* Get observation list for un/tagging
*
* @returns {Object[]} observations
*/
const observationsForTagOperations = useMemo(() => {
// observationsDataList - all observations, moleculeList - filtered, allSelectedMolecules - selected
return allSelectedMolecules.length > 0 ? allSelectedMolecules : moleculeList;
}, [allSelectedMolecules, moleculeList]);

const tagObservations = async (tag, tagToExclude) => {
try {
// setTaggingInProgress(true);
Expand All @@ -677,7 +690,7 @@ export const ObservationsDialog = memo(
}

let molTagObjects = [];
observationsDataList.forEach(m => {
observationsForTagOperations.forEach(m => {
if (!m.tags_set.some(id => id === tag.id)) {
let newMol = { ...m };
newMol.tags_set.push(tag.id);
Expand Down Expand Up @@ -745,7 +758,7 @@ export const ObservationsDialog = memo(
}

let molTagObjects = [];
observationsDataList.forEach(m => {
observationsForTagOperations.forEach(m => {
let newMol = { ...m };
newMol.tags_set = newMol.tags_set.filter(id => id !== tag.id);

Expand Down Expand Up @@ -839,6 +852,17 @@ export const ObservationsDialog = memo(
toastInfo(`Tag for observations was changed from "${mainObservationTag.upload_name}" to "${tag.upload_name}". They could disappear based on your tag selection`, { autoHideDuration: 5000 });
};

/**
* Decide if XCA tag change should not be allowed
*
* @param {string} category category of tag
* @returns {boolean}
*/
const disableXCATagChange = useCallback(category => {
// #1522 CanonSite tags should not be allowed to change if there are selected only some observations
return category === 'CanonSites' && allSelectedMolecules.length > 0 && (allSelectedMolecules.length !== moleculeList.length);
}, [allSelectedMolecules, moleculeList]);

return (
<Popper id={id} open={open} anchorEl={anchorEl} placement="left-start" ref={ref}>
<Panel
Expand Down Expand Up @@ -1135,15 +1159,16 @@ export const ObservationsDialog = memo(
</Button>
<Grid container direction="row" className={classes.dropdownContent}>
{XCA_TAG_CATEGORIES.map(category =>
<Grid key={category} item className={classNames(classes.dropdown, classes.dropdownItem)}>
<Grid key={category} item className={classNames(classes.dropdown, classes.dropdownItem)} disabled={disableXCATagChange(category)}>
Change {PLURAL_TO_SINGULAR[category]}
<Grid container direction="row" className={classNames(classes.dropdownContent, classes.dropdownContentSide)}>
{getTagsForCategory(category)?.map(tag => (
<Grid key={tag.id} item className={classes.dropdownItem} onClick={() => handleXCAtagChange(tag)}>
{tag.upload_name}
</Grid>
))}
</Grid>
{!disableXCATagChange(category) &&
<Grid container direction="row" className={classNames(classes.dropdownContent, classes.dropdownContentSide)}>
{getTagsForCategory(category)?.map(tag => (
<Grid key={tag.id} item className={classes.dropdownItem} onClick={() => handleXCAtagChange(tag)}>
{tag.upload_name}
</Grid>
))}
</Grid>}
</Grid>
)}
</Grid>
Expand Down
16 changes: 15 additions & 1 deletion js/reducers/selection/selectionReducers.js
Original file line number Diff line number Diff line change
Expand Up @@ -455,7 +455,21 @@ export function selectionReducers(state = INITIAL_STATE, action = {}) {

case constants.REMOVE_FROM_SELECTED_TAG_LIST:
let diminishedSelectedTagList = new Set(state.selectedTagList);
diminishedSelectedTagList.delete(action.item);
// TODO sometimes tag object changes for its updated version
// handle it here or replace it in set in every place of change?
if (diminishedSelectedTagList.has(action.item)) {
diminishedSelectedTagList.delete(action.item);
} else {
let tagToDelete = null;
diminishedSelectedTagList.forEach(tag => {
if (tag.id === action.item.id) {
tagToDelete = tag;
}
});
if (tagToDelete) {
diminishedSelectedTagList.delete(tagToDelete);
}
}
return Object.assign({}, state, { selectedTagList: [...diminishedSelectedTagList] });

case constants.SET_IS_TAG_GLOBAL_EDIT:
Expand Down

0 comments on commit 63fa397

Please sign in to comment.