From d845a3a65c8371d7115ae5a6b49f4518ce591ea1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1na=20Kohanov=C3=A1?= Date: Wed, 9 Dec 2020 12:17:49 +0100 Subject: [PATCH 1/7] #469 Make ideas/actions pretty and recognisable --- js/components/tracking/EditableText.js | 65 +++++++++++++ js/components/tracking/timelineView.js | 119 ++++++++++++++++++++++++ js/components/tracking/trackingModal.js | 29 +----- js/reducers/tracking/constants.js | 8 ++ js/reducers/tracking/trackingActions.js | 40 +++++++- 5 files changed, 236 insertions(+), 25 deletions(-) create mode 100644 js/components/tracking/EditableText.js create mode 100644 js/components/tracking/timelineView.js diff --git a/js/components/tracking/EditableText.js b/js/components/tracking/EditableText.js new file mode 100644 index 000000000..cfdf446bf --- /dev/null +++ b/js/components/tracking/EditableText.js @@ -0,0 +1,65 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { makeStyles, TextField, IconButton, Tooltip, Grid } from '@material-ui/core'; +import { Edit } from '@material-ui/icons'; + +const useStyles = makeStyles(theme => ({ + search: { + width: '100%' + }, + fontSizeSmall: { + fontSize: '0.82rem' + } +})); + +const EditableInput = props => { + const inputRef = useRef(null); + const [inputVisible, setInputVisible] = useState(false); + const [text, setText] = useState(props.text); + const classes = useStyles(); + + function onClickOutSide(e) { + if (inputRef.current && !inputRef.current.contains(e.target)) { + setInputVisible(false); + } + } + + useEffect(() => { + // Handle outside clicks on mounted state + if (inputVisible) { + document.addEventListener('mousedown', onClickOutSide); + } + + // This is a necessary step to "dismount" unnecessary events when we destroy the component + return () => { + document.removeEventListener('mousedown', onClickOutSide); + }; + }); + + return ( + + {inputVisible ? ( + { + setText(e.target.value); + }} + /> + ) : ( + + { setInputVisible(true)}>{text}} + { + setInputVisible(true)}> + + + + + } + + )} + + ); +}; + +export default EditableInput; diff --git a/js/components/tracking/timelineView.js b/js/components/tracking/timelineView.js new file mode 100644 index 000000000..7163bc64d --- /dev/null +++ b/js/components/tracking/timelineView.js @@ -0,0 +1,119 @@ +import React, { useState, useRef, memo } from 'react'; +import { makeStyles, IconButton, Tooltip, Grid } from '@material-ui/core'; +import { Check, Clear, Warning, Favorite, Star } from '@material-ui/icons'; +import { actionAnnotation } from '../../reducers/tracking/constants'; +import { TimelineEvent } from 'react-event-timeline'; +import EditableText from './editableText'; +import palette from '../../theme/palette'; + +const useStyles = makeStyles(theme => ({ + headerGrid: { + height: '25px' + }, + grid: { + height: 'inherit' + }, + iconButton: { + padding: '6px' + }, + timelineEvent: { + borderBottom: '1px dashed ' + palette.divider, + paddingBottom: '10px' + } +})); + +const TimelineView = memo(({ data, index }) => { + const ref = useRef(null); + const [isHovering, setIsHovering] = useState(false); + + const classes = useStyles(); + + const getActionIcon = data => { + if (data && data.annotation) { + switch (data.annotation) { + case actionAnnotation.CHECK: + return ; + case actionAnnotation.CLEAR: + return ; + case actionAnnotation.WARNING: + return ; + case actionAnnotation.FAVORITE: + return ; + case actionAnnotation.STAR: + return ; + default: + return ; + } + } else { + return ; + } + }; + + const annotationActions = [ + {}}> + + + + , + {}}> + + + + , + {}}> + + + + , + {}}> + + + + , + {}}> + + + + + ]; + + return ( +
setIsHovering(true)} onMouseLeave={() => setIsHovering(false)}> + + + { + + + + } + {isHovering && ( + + {annotationActions && + annotationActions.map((action, index) => ( + + {action} + + ))} + + )} + +
+ } + createdAt={ + + {new Date(data.timestamp).toLocaleString()} + + } + icon={getActionIcon(data)} + iconColor={palette.primary.main} + className={classes.timelineEvent} + > + + ); +}); + +TimelineView.displayName = 'TimelineView'; +export default TimelineView; diff --git a/js/components/tracking/trackingModal.js b/js/components/tracking/trackingModal.js index d47761e84..4d204d8b5 100644 --- a/js/components/tracking/trackingModal.js +++ b/js/components/tracking/trackingModal.js @@ -2,10 +2,11 @@ import React, { memo, useCallback, useEffect } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import Modal from '../common/Modal'; import { Grid, makeStyles, IconButton, Tooltip } from '@material-ui/core'; -import { Timeline, TimelineEvent } from 'react-event-timeline'; -import { Check, Clear, Close } from '@material-ui/icons'; +import { Timeline } from 'react-event-timeline'; +import { Close } from '@material-ui/icons'; import palette from '../../theme/palette'; import { Panel } from '../common'; +import TimelineView from './timelineView'; import { setProjectTrackingActions } from '../../reducers/tracking/dispatchActions'; const useStyles = makeStyles(theme => ({ @@ -16,10 +17,7 @@ const useStyles = makeStyles(theme => ({ customContentModal: { height: '100%' }, - timelineEvent: { - borderBottom: '1px dashed ' + palette.divider, - paddingBottom: '10px' - }, + divContainer: { height: '100%', width: '100%', @@ -78,24 +76,7 @@ export const TrackingModal = memo(({ openModal, onModalClose }) => { {orderedActionList && orderedActionList.map((data, index) => { if (data && data != null) { - return ( - - ) : ( - - ) - } - iconColor={palette.primary.main} - className={classes.timelineEvent} - > - ); + return ; } })} diff --git a/js/reducers/tracking/constants.js b/js/reducers/tracking/constants.js index b1576102b..e39db6ac0 100644 --- a/js/reducers/tracking/constants.js +++ b/js/reducers/tracking/constants.js @@ -86,3 +86,11 @@ export const actionObjectType = { CROSS_REFERENCE: 'CROSS_REFERENCE', REPRESENTATION: 'REPRESENTATION' }; + +export const actionAnnotation = { + CHECK: 'CHECK', + CLEAR: 'CLEAR', + WARNING: 'WARNING', + FAVORITE: 'FAVORITE', + STAR: 'STAR' +}; diff --git a/js/reducers/tracking/trackingActions.js b/js/reducers/tracking/trackingActions.js index 963671514..88eede735 100644 --- a/js/reducers/tracking/trackingActions.js +++ b/js/reducers/tracking/trackingActions.js @@ -1,4 +1,4 @@ -import { actionType, actionObjectType, actionDescription } from './constants'; +import { actionType, actionObjectType, actionDescription, actionAnnotation } from './constants'; import { constants as apiConstants } from '../api/constants'; import { CONSTANTS as nglConstants } from '../ngl/constants'; import { constants as selectionConstants } from '../selection/constants'; @@ -17,6 +17,7 @@ export const findTrackAction = (action, state) => { let targetName = getTargetName(action.target_on, state); trackAction = { type: actionType.TARGET_LOADED, + annotation: actionAnnotation.CHECK, timestamp: Date.now(), username: username, object_type: actionObjectType.TARGET, @@ -33,6 +34,7 @@ export const findTrackAction = (action, state) => { let molGroupName = getMolGroupName(action.mol_group_on, state); trackAction = { type: actionType.SITE_TURNED_ON, + annotation: actionAnnotation.CHECK, timestamp: Date.now(), username: username, object_type: actionObjectType.SITE, @@ -48,6 +50,7 @@ export const findTrackAction = (action, state) => { let molGroupName = getMolGroupName(objectId, state); trackAction = { type: actionType.SITE_TURNED_OFF, + annotation: actionAnnotation.CHECK, timestamp: Date.now(), username: username, object_type: actionObjectType.SITE, @@ -63,6 +66,7 @@ export const findTrackAction = (action, state) => { trackAction = { type: actionType.ALL_HIDE, + annotation: actionAnnotation.CLEAR, timestamp: Date.now(), username: username, object_type: objectType, @@ -77,6 +81,7 @@ export const findTrackAction = (action, state) => { trackAction = { type: actionType.ALL_TURNED_ON, + annotation: actionAnnotation.CHECK, timestamp: Date.now(), username: username, object_type: objectType, @@ -99,6 +104,7 @@ export const findTrackAction = (action, state) => { trackAction = { type: actionType.ALL_TURNED_OFF, + annotation: actionAnnotation.CLEAR, timestamp: Date.now(), username: username, object_type: objectType, @@ -122,6 +128,7 @@ export const findTrackAction = (action, state) => { trackAction = { type: actionType.ALL_TURNED_ON_BY_TYPE, + annotation: actionAnnotation.CHECK, timestamp: Date.now(), username: username, object_type: objectType, @@ -138,6 +145,7 @@ export const findTrackAction = (action, state) => { trackAction = { type: actionType.ALL_TURNED_OFF_BY_TYPE, + annotation: actionAnnotation.CLEAR, timestamp: Date.now(), username: username, object_type: objectType, @@ -153,6 +161,7 @@ export const findTrackAction = (action, state) => { trackAction = { type: actionType.LIGAND_TURNED_ON, + annotation: actionAnnotation.CHECK, timestamp: Date.now(), username: username, object_type: objectType, @@ -171,6 +180,7 @@ export const findTrackAction = (action, state) => { trackAction = { type: actionType.LIGAND_TURNED_OFF, + annotation: actionAnnotation.CLEAR, timestamp: Date.now(), username: username, object_type: objectType, @@ -189,6 +199,7 @@ export const findTrackAction = (action, state) => { trackAction = { type: actionType.SIDECHAINS_TURNED_ON, + annotation: actionAnnotation.CHECK, timestamp: Date.now(), username: username, object_type: objectType, @@ -207,6 +218,7 @@ export const findTrackAction = (action, state) => { trackAction = { type: actionType.SIDECHAINS_TURNED_OFF, + annotation: actionAnnotation.CLEAR, timestamp: Date.now(), username: username, object_type: objectType, @@ -225,6 +237,7 @@ export const findTrackAction = (action, state) => { trackAction = { type: actionType.INTERACTIONS_TURNED_ON, + annotation: actionAnnotation.CHECK, timestamp: Date.now(), username: username, object_type: objectType, @@ -243,6 +256,7 @@ export const findTrackAction = (action, state) => { trackAction = { type: actionType.INTERACTIONS_TURNED_OFF, + annotation: actionAnnotation.CLEAR, timestamp: Date.now(), username: username, object_type: objectType, @@ -261,6 +275,7 @@ export const findTrackAction = (action, state) => { trackAction = { type: actionType.SURFACE_TURNED_ON, + annotation: actionAnnotation.CHECK, timestamp: Date.now(), username: username, object_type: objectType, @@ -279,6 +294,7 @@ export const findTrackAction = (action, state) => { trackAction = { type: actionType.SURFACE_TURNED_OFF, + annotation: actionAnnotation.CLEAR, timestamp: Date.now(), username: username, object_type: objectType, @@ -297,6 +313,7 @@ export const findTrackAction = (action, state) => { trackAction = { type: actionType.VECTORS_TURNED_ON, + annotation: actionAnnotation.CHECK, timestamp: Date.now(), username: username, object_type: objectType, @@ -315,6 +332,7 @@ export const findTrackAction = (action, state) => { trackAction = { type: actionType.VECTORS_TURNED_OFF, + annotation: actionAnnotation.CLEAR, timestamp: Date.now(), username: username, object_type: objectType, @@ -333,6 +351,7 @@ export const findTrackAction = (action, state) => { trackAction = { type: actionType.MOLECULE_ADDED_TO_SHOPPING_CART, + annotation: actionAnnotation.CHECK, timestamp: Date.now(), username: username, object_type: actionObjectType.MOLECULE, @@ -349,6 +368,7 @@ export const findTrackAction = (action, state) => { trackAction = { type: actionType.MOLECULE_REMOVED_FROM_SHOPPING_CART, + annotation: actionAnnotation.CLEAR, timestamp: Date.now(), username: username, object_type: objectType, @@ -365,6 +385,7 @@ export const findTrackAction = (action, state) => { trackAction = { type: actionType.VECTOR_SELECTED, + annotation: actionAnnotation.CHECK, timestamp: Date.now(), username: username, object_type: objectType, @@ -380,6 +401,7 @@ export const findTrackAction = (action, state) => { trackAction = { type: actionType.COMPOUND_SELECTED, + annotation: actionAnnotation.CHECK, timestamp: Date.now(), username: username, object_type: objectType, @@ -396,6 +418,7 @@ export const findTrackAction = (action, state) => { trackAction = { type: actionType.COMPOUND_DESELECTED, + annotation: actionAnnotation.CLEAR, timestamp: Date.now(), username: username, object_type: objectType, @@ -413,6 +436,7 @@ export const findTrackAction = (action, state) => { trackAction = { type: actionType.ALL_TURNED_ON, + annotation: actionAnnotation.CHECK, timestamp: Date.now(), username: username, object_type: objectType, @@ -434,6 +458,7 @@ export const findTrackAction = (action, state) => { trackAction = { type: actionType.ALL_TURNED_OFF, + annotation: actionAnnotation.CLEAR, timestamp: Date.now(), username: username, object_type: objectType, @@ -457,6 +482,7 @@ export const findTrackAction = (action, state) => { trackAction = { type: actionType.ALL_TURNED_ON_BY_TYPE, + annotation: actionAnnotation.CHECK, timestamp: Date.now(), username: username, object_type: objectType, @@ -475,6 +501,7 @@ export const findTrackAction = (action, state) => { trackAction = { type: actionType.ALL_TURNED_OFF_BY_TYPE, + annotation: actionAnnotation.CLEAR, timestamp: Date.now(), username: username, object_type: objectType, @@ -491,6 +518,7 @@ export const findTrackAction = (action, state) => { trackAction = { type: actionType.LIGAND_TURNED_ON, + annotation: actionAnnotation.CHECK, timestamp: Date.now(), username: username, object_type: objectType, @@ -508,6 +536,7 @@ export const findTrackAction = (action, state) => { trackAction = { type: actionType.LIGAND_TURNED_OFF, + annotation: actionAnnotation.CLEAR, timestamp: Date.now(), username: username, object_type: objectType, @@ -525,6 +554,7 @@ export const findTrackAction = (action, state) => { trackAction = { type: actionType.SIDECHAINS_TURNED_ON, + annotation: actionAnnotation.CHECK, timestamp: Date.now(), username: username, object_type: objectType, @@ -542,6 +572,7 @@ export const findTrackAction = (action, state) => { trackAction = { type: actionType.SIDECHAINS_TURNED_OFF, + annotation: actionAnnotation.CLEAR, timestamp: Date.now(), username: username, object_type: objectType, @@ -559,6 +590,7 @@ export const findTrackAction = (action, state) => { trackAction = { type: actionType.INTERACTIONS_TURNED_ON, + annotation: actionAnnotation.CHECK, timestamp: Date.now(), username: username, object_type: objectType, @@ -576,6 +608,7 @@ export const findTrackAction = (action, state) => { trackAction = { type: actionType.INTERACTIONS_TURNED_OFF, + annotation: actionAnnotation.CLEAR, timestamp: Date.now(), username: username, object_type: objectType, @@ -593,6 +626,7 @@ export const findTrackAction = (action, state) => { trackAction = { type: actionType.SURFACE_TURNED_ON, + annotation: actionAnnotation.CHECK, timestamp: Date.now(), username: username, object_type: objectType, @@ -610,6 +644,7 @@ export const findTrackAction = (action, state) => { trackAction = { type: actionType.SURFACE_TURNED_OFF, + annotation: actionAnnotation.CLEAR, timestamp: Date.now(), username: username, object_type: objectType, @@ -624,6 +659,7 @@ export const findTrackAction = (action, state) => { trackAction = { type: actionType.REPRESENTATION_CHANGED, + annotation: actionAnnotation.CHECK, timestamp: Date.now(), username: username, object_type: actionObjectType.REPRESENTATION, @@ -640,6 +676,7 @@ export const findTrackAction = (action, state) => { trackAction = { type: actionType.REPRESENTATION_ADDED, + annotation: actionAnnotation.CHECK, timestamp: Date.now(), username: username, object_type: actionObjectType.REPRESENTATION, @@ -654,6 +691,7 @@ export const findTrackAction = (action, state) => { trackAction = { type: actionType.REPRESENTATION_REMOVED, + annotation: actionAnnotation.CLEAR, timestamp: Date.now(), username: username, object_type: objectType, From 0f9d1cc676d339a70e1503d5dd239363c7b3a4dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1na=20Kohanov=C3=A1?= Date: Wed, 9 Dec 2020 13:31:00 +0100 Subject: [PATCH 2/7] #469 Make ideas/actions pretty and recognisable --- js/components/tracking/EditableText.js | 2 +- js/components/tracking/timelineView.js | 64 ++++++++++++++++++------- js/components/tracking/trackingModal.js | 2 +- 3 files changed, 49 insertions(+), 19 deletions(-) diff --git a/js/components/tracking/EditableText.js b/js/components/tracking/EditableText.js index cfdf446bf..0f0880305 100644 --- a/js/components/tracking/EditableText.js +++ b/js/components/tracking/EditableText.js @@ -47,7 +47,7 @@ const EditableInput = props => { }} /> ) : ( - + { setInputVisible(true)}>{text}} { setInputVisible(true)}> diff --git a/js/components/tracking/timelineView.js b/js/components/tracking/timelineView.js index 7163bc64d..e1f788b24 100644 --- a/js/components/tracking/timelineView.js +++ b/js/components/tracking/timelineView.js @@ -25,12 +25,13 @@ const useStyles = makeStyles(theme => ({ const TimelineView = memo(({ data, index }) => { const ref = useRef(null); const [isHovering, setIsHovering] = useState(false); + const [updatedIcon, setUpdatedIcon] = useState(null); const classes = useStyles(); - const getActionIcon = data => { - if (data && data.annotation) { - switch (data.annotation) { + const getActionIcon = annotation => { + if (annotation) { + switch (annotation) { case actionAnnotation.CHECK: return ; case actionAnnotation.CLEAR: @@ -50,27 +51,51 @@ const TimelineView = memo(({ data, index }) => { }; const annotationActions = [ - {}}> + setUpdatedIcon(actionAnnotation.CHECK)}> , - {}}> + { + setUpdatedIcon(actionAnnotation.CLEAR); + }} + > , - {}}> + { + setUpdatedIcon(actionAnnotation.WARNING); + }} + > , - {}}> + { + setUpdatedIcon(actionAnnotation.FAVORITE); + }} + > , - {}}> + { + setUpdatedIcon(actionAnnotation.STAR); + }} + > @@ -85,12 +110,20 @@ const TimelineView = memo(({ data, index }) => {
{ - - + + } {isHovering && ( - + {annotationActions && annotationActions.map((action, index) => ( @@ -102,12 +135,9 @@ const TimelineView = memo(({ data, index }) => {
} - createdAt={ - - {new Date(data.timestamp).toLocaleString()} - - } - icon={getActionIcon(data)} + createdAt={new Date(data.timestamp).toLocaleString()} + icon={updatedIcon && updatedIcon != null ? getActionIcon(updatedIcon) : getActionIcon(data.annotation)} + //icon={getActionIcon(data.annotation)} iconColor={palette.primary.main} className={classes.timelineEvent} > diff --git a/js/components/tracking/trackingModal.js b/js/components/tracking/trackingModal.js index 4d204d8b5..02d56bf1c 100644 --- a/js/components/tracking/trackingModal.js +++ b/js/components/tracking/trackingModal.js @@ -76,7 +76,7 @@ export const TrackingModal = memo(({ openModal, onModalClose }) => { {orderedActionList && orderedActionList.map((data, index) => { if (data && data != null) { - return ; + return ; } })} From 48e5071ea443e24944b029f37717e6edeb243551 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1na=20Kohanov=C3=A1?= Date: Wed, 9 Dec 2020 16:11:49 +0100 Subject: [PATCH 3/7] #469 Make ideas/actions pretty and recognisable --- js/components/tracking/EditableText.js | 13 ++++++----- js/components/tracking/timelineView.js | 30 ++++++++++++++++++++++--- js/components/tracking/trackingModal.js | 1 - js/reducers/tracking/dispatchActions.js | 27 ++++++++++++++++++++++ 4 files changed, 62 insertions(+), 9 deletions(-) diff --git a/js/components/tracking/EditableText.js b/js/components/tracking/EditableText.js index 0f0880305..91549d9de 100644 --- a/js/components/tracking/EditableText.js +++ b/js/components/tracking/EditableText.js @@ -11,17 +11,20 @@ const useStyles = makeStyles(theme => ({ } })); -const EditableInput = props => { +const EditableInput = ({ dataText, index, updateText }) => { const inputRef = useRef(null); const [inputVisible, setInputVisible] = useState(false); - const [text, setText] = useState(props.text); + const [text, setText] = useState(dataText); const classes = useStyles(); - function onClickOutSide(e) { + const onClickOutSide = e => { if (inputRef.current && !inputRef.current.contains(e.target)) { setInputVisible(false); + if (updateText && text !== dataText) { + updateText(text); + } } - } + }; useEffect(() => { // Handle outside clicks on mounted state @@ -47,7 +50,7 @@ const EditableInput = props => { }} /> ) : ( - + { setInputVisible(true)}>{text}} { setInputVisible(true)}> diff --git a/js/components/tracking/timelineView.js b/js/components/tracking/timelineView.js index e1f788b24..f2cbe44c4 100644 --- a/js/components/tracking/timelineView.js +++ b/js/components/tracking/timelineView.js @@ -1,10 +1,12 @@ import React, { useState, useRef, memo } from 'react'; +import { useDispatch } from 'react-redux'; import { makeStyles, IconButton, Tooltip, Grid } from '@material-ui/core'; import { Check, Clear, Warning, Favorite, Star } from '@material-ui/icons'; import { actionAnnotation } from '../../reducers/tracking/constants'; import { TimelineEvent } from 'react-event-timeline'; import EditableText from './editableText'; import palette from '../../theme/palette'; +import { updateTrackingActions } from '../../reducers/tracking/dispatchActions'; const useStyles = makeStyles(theme => ({ headerGrid: { @@ -23,6 +25,8 @@ const useStyles = makeStyles(theme => ({ })); const TimelineView = memo(({ data, index }) => { + const dispatch = useDispatch(); + const ref = useRef(null); const [isHovering, setIsHovering] = useState(false); const [updatedIcon, setUpdatedIcon] = useState(null); @@ -51,7 +55,14 @@ const TimelineView = memo(({ data, index }) => { }; const annotationActions = [ - setUpdatedIcon(actionAnnotation.CHECK)}> + { + setUpdatedIcon(actionAnnotation.CHECK); + updateDataAnnotation(actionAnnotation.CHECK); + }} + > @@ -61,6 +72,7 @@ const TimelineView = memo(({ data, index }) => { color={'primary'} onClick={() => { setUpdatedIcon(actionAnnotation.CLEAR); + updateDataAnnotation(actionAnnotation.CLEAR); }} > @@ -72,6 +84,7 @@ const TimelineView = memo(({ data, index }) => { color={'primary'} onClick={() => { setUpdatedIcon(actionAnnotation.WARNING); + updateDataAnnotation(actionAnnotation.WARNING); }} > @@ -83,6 +96,7 @@ const TimelineView = memo(({ data, index }) => { color={'primary'} onClick={() => { setUpdatedIcon(actionAnnotation.FAVORITE); + updateDataAnnotation(actionAnnotation.FAVORITE); }} > @@ -94,6 +108,7 @@ const TimelineView = memo(({ data, index }) => { color={'primary'} onClick={() => { setUpdatedIcon(actionAnnotation.STAR); + updateDataAnnotation(actionAnnotation.STAR); }} > @@ -102,6 +117,16 @@ const TimelineView = memo(({ data, index }) => { ]; + const updateDataText = text => { + data.text = text; + dispatch(updateTrackingActions(data)); + }; + + const updateDataAnnotation = annotation => { + data.annotation = annotation; + dispatch(updateTrackingActions(data)); + }; + return (
setIsHovering(true)} onMouseLeave={() => setIsHovering(false)}> { { - + } {isHovering && ( @@ -137,7 +162,6 @@ const TimelineView = memo(({ data, index }) => { } createdAt={new Date(data.timestamp).toLocaleString()} icon={updatedIcon && updatedIcon != null ? getActionIcon(updatedIcon) : getActionIcon(data.annotation)} - //icon={getActionIcon(data.annotation)} iconColor={palette.primary.main} className={classes.timelineEvent} > diff --git a/js/components/tracking/trackingModal.js b/js/components/tracking/trackingModal.js index 02d56bf1c..09cb10118 100644 --- a/js/components/tracking/trackingModal.js +++ b/js/components/tracking/trackingModal.js @@ -4,7 +4,6 @@ import Modal from '../common/Modal'; import { Grid, makeStyles, IconButton, Tooltip } from '@material-ui/core'; import { Timeline } from 'react-event-timeline'; import { Close } from '@material-ui/icons'; -import palette from '../../theme/palette'; import { Panel } from '../common'; import TimelineView from './timelineView'; import { setProjectTrackingActions } from '../../reducers/tracking/dispatchActions'; diff --git a/js/reducers/tracking/dispatchActions.js b/js/reducers/tracking/dispatchActions.js index 7cc6fa65c..784864620 100644 --- a/js/reducers/tracking/dispatchActions.js +++ b/js/reducers/tracking/dispatchActions.js @@ -1528,3 +1528,30 @@ export const sendInitTrackingActionByProjectId = target_on => (dispatch, getStat dispatch(saveTrackingActions(actions, snapshotID)); } }; + +export const updateTrackingActions = action => (dispatch, getState) => { + const state = getState(); + const project = state.projectReducers.currentProject; + const projectActions = state.trackingReducers.project_actions_list; + const projectID = project && project.projectID; + const actionID = action && action.Id; + + if (projectID && actionID) { + const dataToSend = { + last_update_date: moment().format(), + actions: JSON.stringify(projectActions) + }; + return api({ + url: `${base_url}/api/session-actions/${projectID}/${actionID}`, + method: METHOD.PUT, + data: JSON.stringify(dataToSend) + }) + .then(() => {}) + .catch(error => { + throw new Error(error); + }) + .finally(() => {}); + } else { + return Promise.resolve(); + } +}; From c21b22898888ae53bb17f90e0ff9c0ad5dc7d538 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1na=20Kohanov=C3=A1?= Date: Thu, 10 Dec 2020 10:10:15 +0100 Subject: [PATCH 4/7] #469 Make ideas/actions pretty and recognisable --- js/reducers/tracking/dispatchActions.js | 42 +++++++++++++++---------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/js/reducers/tracking/dispatchActions.js b/js/reducers/tracking/dispatchActions.js index 784864620..ae58b17ef 100644 --- a/js/reducers/tracking/dispatchActions.js +++ b/js/reducers/tracking/dispatchActions.js @@ -1455,7 +1455,8 @@ const getTrackingActions = projectID => (dispatch, getState) => { let listToSet = []; results.forEach(r => { let resultActions = JSON.parse(r.actions); - listToSet.push(...resultActions); + let actions = resultActions.map(obj => ({ ...obj, actionId: r.id })); + listToSet.push(...actions); }); let projectActions = [...listToSet, ...sendActions]; @@ -1534,23 +1535,32 @@ export const updateTrackingActions = action => (dispatch, getState) => { const project = state.projectReducers.currentProject; const projectActions = state.trackingReducers.project_actions_list; const projectID = project && project.projectID; - const actionID = action && action.Id; + let actionID = action && action.actionId; - if (projectID && actionID) { - const dataToSend = { - last_update_date: moment().format(), - actions: JSON.stringify(projectActions) - }; - return api({ - url: `${base_url}/api/session-actions/${projectID}/${actionID}`, - method: METHOD.PUT, - data: JSON.stringify(dataToSend) - }) - .then(() => {}) - .catch(error => { - throw new Error(error); + if (projectID && actionID && projectActions) { + let actions = projectActions.filter(a => a.actionId === actionID); + + if (actions && actions.length > 0) { + const dataToSend = { + session_action_id: actionID, + session_project: projectID, + author: project.authorID, + last_update_date: moment().format(), + actions: JSON.stringify(actions) + }; + return api({ + url: `${base_url}/api/session-actions/${actionID}`, + method: METHOD.PUT, + data: JSON.stringify(dataToSend) }) - .finally(() => {}); + .then(() => {}) + .catch(error => { + throw new Error(error); + }) + .finally(() => {}); + } else { + return Promise.resolve(); + } } else { return Promise.resolve(); } From 4772600abc97861fff2f8b681b679ab997f81936 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1na=20Kohanov=C3=A1?= Date: Mon, 14 Dec 2020 15:00:39 +0100 Subject: [PATCH 5/7] #469 Make ideas/actions pretty and recognisable --- .../snapshot/redux/dispatchActions.js | 13 +- js/components/tracking/timelineView.js | 137 ++++++--- js/components/userFeedback/browserApi.js | 9 +- js/reducers/tracking/actions.js | 5 + js/reducers/tracking/constants.js | 4 +- js/reducers/tracking/dispatchActions.js | 281 ++++++++++-------- js/reducers/tracking/trackingReducers.js | 8 +- package.json | 2 + 8 files changed, 291 insertions(+), 168 deletions(-) diff --git a/js/components/snapshot/redux/dispatchActions.js b/js/components/snapshot/redux/dispatchActions.js index e0fe26056..aa6360f65 100644 --- a/js/components/snapshot/redux/dispatchActions.js +++ b/js/components/snapshot/redux/dispatchActions.js @@ -27,6 +27,7 @@ import { selectFirstMolGroup } from '../../preview/moleculeGroups/redux/dispatch import { reloadDatasetsReducer } from '../../datasets/redux/actions'; import { saveCurrentActionsList } from '../../../reducers/tracking/dispatchActions'; import { sendTrackingActionsByProjectId, manageSendTrackingActions } from '../../../reducers/tracking/dispatchActions'; +import { captureScreenOfSnapshot } from '../../userFeedback/browserApi'; export const getListOfSnapshots = () => (dispatch, getState) => { const userID = DJANGO_CONTEXT['pk'] || null; @@ -215,7 +216,10 @@ export const createNewSnapshot = ({ title, description, type, author, parent, se }).then(res => { // redirect to project with newest created snapshot /:projectID/:snapshotID if (res.data.id && session_project) { - Promise.resolve(dispatch(saveCurrentActionsList(res.data.id, session_project, nglViewList))).then(() => { + let snapshot = { id: res.data.id, title: title }; + let project = { projectID: session_project, authorID: author }; + + Promise.resolve(dispatch(saveCurrentActionsList(snapshot, project, nglViewList))).then(() => { if (disableRedirect === false) { // Really bad usage or redirection. Hint for everybody in this line ignore it, but in other parts of code // use react-router ! @@ -251,6 +255,7 @@ export const activateSnapshotDialog = (loggedInUserID = undefined, finallyShareS const projectID = state.projectReducers.currentProject.projectID; const currentSnapshotAuthor = state.projectReducers.currentSnapshot.author; + dispatch(captureScreenOfSnapshot()); dispatch(manageSendTrackingActions()); dispatch(setDisableRedirect(finallyShareSnapshot)); @@ -321,7 +326,10 @@ export const createNewSnapshotWithoutStateModification = ({ disableRedirect: true }) ); - dispatch(saveCurrentActionsList(res.data.id, session_project, nglViewList)); + + let snapshot = { id: res.data.id, title: title }; + let project = { projectID: session_project, authorID: author }; + dispatch(saveCurrentActionsList(snapshot, project, nglViewList)); } }); }); @@ -332,6 +340,7 @@ export const saveAndShareSnapshot = nglViewList => (dispatch, getState) => { const targetId = state.apiReducers.target_on; const loggedInUserID = DJANGO_CONTEXT['pk']; + dispatch(captureScreenOfSnapshot()); dispatch(setDisableRedirect(true)); if (targetId) { diff --git a/js/components/tracking/timelineView.js b/js/components/tracking/timelineView.js index f2cbe44c4..533bdcef4 100644 --- a/js/components/tracking/timelineView.js +++ b/js/components/tracking/timelineView.js @@ -1,38 +1,52 @@ import React, { useState, useRef, memo } from 'react'; -import { useDispatch } from 'react-redux'; -import { makeStyles, IconButton, Tooltip, Grid } from '@material-ui/core'; +import { useDispatch, useSelector } from 'react-redux'; +import { makeStyles, IconButton, Tooltip, Grid, Box, Chip } from '@material-ui/core'; import { Check, Clear, Warning, Favorite, Star } from '@material-ui/icons'; import { actionAnnotation } from '../../reducers/tracking/constants'; import { TimelineEvent } from 'react-event-timeline'; import EditableText from './editableText'; import palette from '../../theme/palette'; import { updateTrackingActions } from '../../reducers/tracking/dispatchActions'; +import { actionType } from '../../reducers/tracking/constants'; +import Gallery from 'react-grid-gallery'; const useStyles = makeStyles(theme => ({ headerGrid: { - height: '25px' + height: '15px' }, grid: { height: 'inherit' }, iconButton: { - padding: '6px' + padding: '6px', + paddingTop: '0px' }, timelineEvent: { borderBottom: '1px dashed ' + palette.divider, paddingBottom: '10px' + }, + title: { + position: 'relative', + paddingLeft: '45px', + textAlign: 'left', + fontWeight: 'bold' + }, + titleMargin: { + marginRight: '5px' } })); const TimelineView = memo(({ data, index }) => { const dispatch = useDispatch(); + const classes = useStyles(); + + const currentSnapshotID = useSelector(state => state.projectReducers.currentSnapshot.id); + let isSelected = currentSnapshotID === data.object_id; const ref = useRef(null); const [isHovering, setIsHovering] = useState(false); const [updatedIcon, setUpdatedIcon] = useState(null); - const classes = useStyles(); - const getActionIcon = annotation => { if (annotation) { switch (annotation) { @@ -127,44 +141,85 @@ const TimelineView = memo(({ data, index }) => { dispatch(updateTrackingActions(data)); }; + const IMAGES = [ + { + src: data.image, + thumbnail: data.image, + thumbnailWidth: 0, + thumbnailHeight: 0, + caption: data.object_name + } + ]; + return (
setIsHovering(true)} onMouseLeave={() => setIsHovering(false)}> - - - { - - - - } - {isHovering && ( - - {annotationActions && - annotationActions.map((action, index) => ( - - {action} - - ))} + {data.type && data.type === actionType.SNAPSHOT ? ( + <> + + {isSelected && ( + + + + )} + { + + {`${data.text}`} + + } + { + + + + } + + + ) : ( + <> + + + { + + + + } + {isHovering && ( + + {annotationActions && + annotationActions.map((action, index) => ( + + {action} + + ))} + + )} - )} - -
- } - createdAt={new Date(data.timestamp).toLocaleString()} - icon={updatedIcon && updatedIcon != null ? getActionIcon(updatedIcon) : getActionIcon(data.annotation)} - iconColor={palette.primary.main} - className={classes.timelineEvent} - > +
+ } + createdAt={new Date(data.timestamp).toLocaleString()} + icon={updatedIcon && updatedIcon != null ? getActionIcon(updatedIcon) : getActionIcon(data.annotation)} + iconColor={palette.primary.main} + className={classes.timelineEvent} + > + + )} ); }); diff --git a/js/components/userFeedback/browserApi.js b/js/components/userFeedback/browserApi.js index 3a40b14ea..f14b006eb 100644 --- a/js/components/userFeedback/browserApi.js +++ b/js/components/userFeedback/browserApi.js @@ -1,5 +1,6 @@ import { setImageSource, setIsOpenForm } from './redux/actions'; - +import { setTrackingImageSource } from '../../reducers/tracking/actions'; +import html2canvas from 'html2canvas'; /* Getting image from screen capture or */ // https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser @@ -81,3 +82,9 @@ export const captureScreen = () => async dispatch => { dispatch(setImageSource(image)); dispatch(setIsOpenForm(true)); }; + +export const captureScreenOfSnapshot = () => async dispatch => { + html2canvas(document.body).then(canvas => { + dispatch(setTrackingImageSource(canvas.toDataURL())); + }); +}; diff --git a/js/reducers/tracking/actions.js b/js/reducers/tracking/actions.js index d3efec7a2..1fe45740d 100644 --- a/js/reducers/tracking/actions.js +++ b/js/reducers/tracking/actions.js @@ -104,3 +104,8 @@ export const resetTrackingState = function() { type: constants.RESET_TRACKING_STATE }; }; + +export const setTrackingImageSource = imageSource => ({ + type: constants.SET_TRACKING_IMAGE_SOURCE, + payload: imageSource +}); diff --git a/js/reducers/tracking/constants.js b/js/reducers/tracking/constants.js index e39db6ac0..4c5f7880a 100644 --- a/js/reducers/tracking/constants.js +++ b/js/reducers/tracking/constants.js @@ -15,7 +15,8 @@ export const constants = { SET_PROJECT_ACTIONS_LIST: prefix + 'SET_PROJECT_ACTIONS_LIST', SET_IS_ACTIONS_SAVING: prefix + 'SET_IS_ACTIONS_SAVING', SET_IS_ACTIONS_RESTORING: prefix + 'SET_IS_ACTIONS_RESTORING', - RESET_TRACKING_STATE: prefix + 'RESET_TRACKING_STATE' + RESET_TRACKING_STATE: prefix + 'RESET_TRACKING_STATE', + SET_TRACKING_IMAGE_SOURCE: prefix + 'SET_TRACKING_IMAGE_SOURCE' }; export const actionType = { @@ -44,6 +45,7 @@ export const actionType = { NGL_STATE: 'NGL_STATE', UNDO: 'UNDO', REDO: 'REDO', + SNAPSHOT: 'SNAPSHOT', ALL_HIDE: 'ALL_HIDE', ALL_TURNED_ON: 'ALL_TURNED_ON', ALL_TURNED_OFF: 'ALL_TURNED_OFF', diff --git a/js/reducers/tracking/dispatchActions.js b/js/reducers/tracking/dispatchActions.js index ae58b17ef..966e60a80 100644 --- a/js/reducers/tracking/dispatchActions.js +++ b/js/reducers/tracking/dispatchActions.js @@ -88,147 +88,184 @@ import { setDeselectedAllByType as setDeselectedAllByTypeOfDataset } from '../../components/datasets/redux/actions'; -export const saveCurrentActionsList = (snapshotID, projectID, nglViewList) => async (dispatch, getState) => { +export const saveCurrentActionsList = (snapshot, project, nglViewList) => async (dispatch, getState) => { + let projectID = project && project.projectID; let actionList = await dispatch(getTrackingActions(projectID)); - dispatch(saveActionsList(snapshotID, actionList, nglViewList)); + await dispatch(saveActionsList(project, snapshot, actionList, nglViewList)); }; -export const saveActionsList = (snapshotID, actionList, nglViewList) => (dispatch, getState) => { +const saveActionsList = (project, snapshot, actionList, nglViewList) => async (dispatch, getState) => { const state = getState(); - const currentTargetOn = state.apiReducers.target_on; - const currentSites = state.selectionReducers.mol_group_selection; - const currentLigands = state.selectionReducers.fragmentDisplayList; - const currentProteins = state.selectionReducers.proteinList; - const currentComplexes = state.selectionReducers.complexList; - const currentSurfaces = state.selectionReducers.surfaceList; - const currentVectors = state.selectionReducers.vectorOnList; - const currentBuyList = state.selectionReducers.to_buy_list; - const currentVector = state.selectionReducers.currentVector; - const currentSelectionAll = state.selectionReducers.moleculeAllSelection; - - const currentDatasetLigands = state.datasetsReducers.ligandLists; - const currentDatasetProteins = state.datasetsReducers.proteinLists; - const currentDatasetComplexes = state.datasetsReducers.complexLists; - const currentDatasetSurfaces = state.datasetsReducers.surfaceLists; - const currentDatasetSelectionAll = state.datasetsReducers.moleculeAllSelection; - - const currentDatasetBuyList = state.datasetsReducers.compoundsToBuyDatasetMap; - const currentobjectsInView = state.nglReducers.objectsInView; - - const currentTargets = (currentTargetOn && [currentTargetOn]) || []; - const currentVectorSmiles = (currentVector && [currentVector]) || []; - - let orderedActionList = actionList.reverse((a, b) => a.timestamp - b.timestamp); - - let currentActions = []; - - getCurrentActionList(orderedActionList, actionType.TARGET_LOADED, getCollection(currentTargets), currentActions); - getCurrentActionList(orderedActionList, actionType.SITE_TURNED_ON, getCollection(currentSites), currentActions); - getCurrentActionList(orderedActionList, actionType.LIGAND_TURNED_ON, getCollection(currentLigands), currentActions); - - getCurrentActionList(orderedActionList, actionType.ALL_TURNED_ON, getCollection(currentSelectionAll), currentActions); - getCurrentActionList( - orderedActionList, - actionType.ALL_TURNED_ON, - getCollectionOfDataset(currentDatasetSelectionAll), - currentActions - ); + const snapshotID = snapshot && snapshot.id; + if (snapshotID) { + const currentTargetOn = state.apiReducers.target_on; + const currentSites = state.selectionReducers.mol_group_selection; + const currentLigands = state.selectionReducers.fragmentDisplayList; + const currentProteins = state.selectionReducers.proteinList; + const currentComplexes = state.selectionReducers.complexList; + const currentSurfaces = state.selectionReducers.surfaceList; + const currentVectors = state.selectionReducers.vectorOnList; + const currentBuyList = state.selectionReducers.to_buy_list; + const currentVector = state.selectionReducers.currentVector; + const currentSelectionAll = state.selectionReducers.moleculeAllSelection; + + const currentDatasetLigands = state.datasetsReducers.ligandLists; + const currentDatasetProteins = state.datasetsReducers.proteinLists; + const currentDatasetComplexes = state.datasetsReducers.complexLists; + const currentDatasetSurfaces = state.datasetsReducers.surfaceLists; + const currentDatasetSelectionAll = state.datasetsReducers.moleculeAllSelection; + + const currentDatasetBuyList = state.datasetsReducers.compoundsToBuyDatasetMap; + const currentobjectsInView = state.nglReducers.objectsInView; + + const currentTargets = (currentTargetOn && [currentTargetOn]) || []; + const currentVectorSmiles = (currentVector && [currentVector]) || []; + + let orderedActionList = actionList.reverse((a, b) => a.timestamp - b.timestamp); + + let currentActions = []; + + getCurrentActionList(orderedActionList, actionType.TARGET_LOADED, getCollection(currentTargets), currentActions); + getCurrentActionList(orderedActionList, actionType.SITE_TURNED_ON, getCollection(currentSites), currentActions); + getCurrentActionList(orderedActionList, actionType.LIGAND_TURNED_ON, getCollection(currentLigands), currentActions); + + getCurrentActionList( + orderedActionList, + actionType.ALL_TURNED_ON, + getCollection(currentSelectionAll), + currentActions + ); + getCurrentActionList( + orderedActionList, + actionType.ALL_TURNED_ON, + getCollectionOfDataset(currentDatasetSelectionAll), + currentActions + ); - getCurrentActionList( - orderedActionList, - actionType.SIDECHAINS_TURNED_ON, - getCollection(currentProteins), - currentActions - ); + getCurrentActionList( + orderedActionList, + actionType.SIDECHAINS_TURNED_ON, + getCollection(currentProteins), + currentActions + ); - getCurrentActionList( - orderedActionList, - actionType.INTERACTIONS_TURNED_ON, - getCollection(currentComplexes), - currentActions - ); - getCurrentActionList(orderedActionList, actionType.SURFACE_TURNED_ON, getCollection(currentSurfaces), currentActions); - getCurrentActionList(orderedActionList, actionType.VECTORS_TURNED_ON, getCollection(currentVectors), currentActions); - getCurrentActionList( - orderedActionList, - actionType.VECTOR_SELECTED, - getCollection(currentVectorSmiles), - currentActions - ); + getCurrentActionList( + orderedActionList, + actionType.INTERACTIONS_TURNED_ON, + getCollection(currentComplexes), + currentActions + ); + getCurrentActionList( + orderedActionList, + actionType.SURFACE_TURNED_ON, + getCollection(currentSurfaces), + currentActions + ); + getCurrentActionList( + orderedActionList, + actionType.VECTORS_TURNED_ON, + getCollection(currentVectors), + currentActions + ); + getCurrentActionList( + orderedActionList, + actionType.VECTOR_SELECTED, + getCollection(currentVectorSmiles), + currentActions + ); - getCurrentActionList( - orderedActionList, - actionType.MOLECULE_ADDED_TO_SHOPPING_CART, - getCollectionOfShoppingCart(currentBuyList), - currentActions - ); + getCurrentActionList( + orderedActionList, + actionType.MOLECULE_ADDED_TO_SHOPPING_CART, + getCollectionOfShoppingCart(currentBuyList), + currentActions + ); - getCurrentActionList( - orderedActionList, - actionType.LIGAND_TURNED_ON, - getCollectionOfDataset(currentDatasetLigands), - currentActions - ); + getCurrentActionList( + orderedActionList, + actionType.LIGAND_TURNED_ON, + getCollectionOfDataset(currentDatasetLigands), + currentActions + ); - getCurrentActionList( - orderedActionList, - actionType.SIDECHAINS_TURNED_ON, - getCollectionOfDataset(currentDatasetProteins), - currentActions - ); + getCurrentActionList( + orderedActionList, + actionType.SIDECHAINS_TURNED_ON, + getCollectionOfDataset(currentDatasetProteins), + currentActions + ); - getCurrentActionList( - orderedActionList, - actionType.INTERACTIONS_TURNED_ON, - getCollectionOfDataset(currentDatasetComplexes), - currentActions - ); + getCurrentActionList( + orderedActionList, + actionType.INTERACTIONS_TURNED_ON, + getCollectionOfDataset(currentDatasetComplexes), + currentActions + ); - getCurrentActionList( - orderedActionList, - actionType.SURFACE_TURNED_ON, - getCollectionOfDataset(currentDatasetSurfaces), - currentActions - ); + getCurrentActionList( + orderedActionList, + actionType.SURFACE_TURNED_ON, + getCollectionOfDataset(currentDatasetSurfaces), + currentActions + ); - getCurrentActionList( - orderedActionList, - actionType.COMPOUND_SELECTED, - getCollectionOfDataset(currentDatasetBuyList), - currentActions - ); + getCurrentActionList( + orderedActionList, + actionType.COMPOUND_SELECTED, + getCollectionOfDataset(currentDatasetBuyList), + currentActions + ); - getCurrentActionList( - orderedActionList, - actionType.REPRESENTATION_ADDED, - getCollectionOfDatasetOfRepresentation(currentobjectsInView), - currentActions - ); + getCurrentActionList( + orderedActionList, + actionType.REPRESENTATION_ADDED, + getCollectionOfDatasetOfRepresentation(currentobjectsInView), + currentActions + ); - getCurrentActionList( - orderedActionList, - actionType.REPRESENTATION_CHANGED, - getCollectionOfDatasetOfRepresentation(currentobjectsInView), - currentActions - ); + getCurrentActionList( + orderedActionList, + actionType.REPRESENTATION_CHANGED, + getCollectionOfDatasetOfRepresentation(currentobjectsInView), + currentActions + ); - if (nglViewList) { - let nglStateList = nglViewList.map(nglView => { - return { id: nglView.id, orientation: nglView.stage.viewerControls.getOrientation() }; - }); + if (nglViewList) { + let nglStateList = nglViewList.map(nglView => { + return { id: nglView.id, orientation: nglView.stage.viewerControls.getOrientation() }; + }); - let trackAction = { - type: actionType.NGL_STATE, - timestamp: Date.now(), - nglStateList: nglStateList - }; + let trackAction = { + type: actionType.NGL_STATE, + timestamp: Date.now(), + nglStateList: nglStateList + }; - currentActions.push(Object.assign({ ...trackAction })); + currentActions.push(Object.assign({ ...trackAction })); + } + + await dispatch(saveSnapshotAction(snapshot, project)); + dispatch(setCurrentActionsList(currentActions)); + dispatch(saveTrackingActions(currentActions, snapshotID)); } +}; - dispatch(setCurrentActionsList(currentActions)); - dispatch(saveTrackingActions(currentActions, snapshotID)); +const saveSnapshotAction = (snapshot, project) => async (dispatch, getState) => { + const state = getState(); + const trackingImageSource = state.trackingReducers.trackingImageSource; + + let sendActions = []; + let snapshotAction = { + type: actionType.SNAPSHOT, + timestamp: Date.now(), + object_name: snapshot.title, + object_id: snapshot.id, + text: `Snapshot: ${snapshot.id} - ${snapshot.title}`, + image: trackingImageSource + }; + sendActions.push(snapshotAction); + await dispatch(sendTrackingActions(sendActions, project)); }; export const saveTrackingActions = (currentActions, snapshotID) => (dispatch, getState) => { @@ -1396,7 +1433,7 @@ export const checkSendTrackingActions = (save = false) => (dispatch, getState) = } }; -const sendTrackingActions = (sendActions, project, clear = true) => (dispatch, getState) => { +const sendTrackingActions = (sendActions, project, clear = true) => async (dispatch, getState) => { if (project) { const projectID = project && project.projectID; diff --git a/js/reducers/tracking/trackingReducers.js b/js/reducers/tracking/trackingReducers.js index bf8f5c569..2eb50d148 100644 --- a/js/reducers/tracking/trackingReducers.js +++ b/js/reducers/tracking/trackingReducers.js @@ -14,7 +14,8 @@ export const INITIAL_STATE = { send_actions_list: [], project_actions_list: [], isActionRestoring: false, - isActionRestored: false + isActionRestored: false, + trackingImageSource: '' }; export function trackingReducers(state = INITIAL_STATE, action = {}) { @@ -90,6 +91,11 @@ export function trackingReducers(state = INITIAL_STATE, action = {}) { isActionRestored: action.isActionRestored }); + case constants.SET_TRACKING_IMAGE_SOURCE: + return Object.assign({}, state, { + trackingImageSource: action.payload + }); + case constants.RESET_TRACKING_STATE: return INITIAL_STATE; diff --git a/package.json b/package.json index b1aac7e7e..2ae362757 100755 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "formik": "^2.1.4", "formik-material-ui": "^2.0.0-beta.1", "formik-material-ui-pickers": "^0.0.8", + "html2canvas": "^1.0.0-rc.7", "js-base64": "^2.5.2", "jszip": "^3.2.2", "lodash": "^4.17.15", @@ -61,6 +62,7 @@ "react-color": "^2.17.3", "react-dom": "^16.12.0", "react-event-timeline": "^1.6.3", + "react-grid-gallery": "^0.5.5", "react-hot-loader": "^4.12.18", "react-infinite-scroller": "^1.2.4", "react-redux": "^7.1.3", From 1d1da571f27f5c87f528604742641ee439c4081a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1na=20Kohanov=C3=A1?= Date: Wed, 16 Dec 2020 07:30:41 +0100 Subject: [PATCH 6/7] #469 Make ideas/actions pretty and recognisable --- .../projects/projectDetailDrawer/index.js | 110 +++++++++++++----- .../projects/redux/dispatchActions.js | 7 +- .../snapshot/redux/dispatchActions.js | 64 +++++----- js/components/tracking/timelineView.js | 4 +- js/reducers/tracking/actions.js | 7 ++ js/reducers/tracking/constants.js | 3 +- js/reducers/tracking/dispatchActions.js | 50 +++++--- js/reducers/tracking/trackingReducers.js | 6 + 8 files changed, 166 insertions(+), 85 deletions(-) diff --git a/js/components/projects/projectDetailDrawer/index.js b/js/components/projects/projectDetailDrawer/index.js index 7408b9bbd..6a07bb64d 100644 --- a/js/components/projects/projectDetailDrawer/index.js +++ b/js/components/projects/projectDetailDrawer/index.js @@ -1,15 +1,14 @@ -import React, { memo, useContext } from 'react'; -import { IconButton, makeStyles, Drawer, Typography, Grid } from '@material-ui/core'; +import React, { memo } from 'react'; +import { IconButton, makeStyles, Drawer, Typography, Grid, Box } from '@material-ui/core'; import { Share, Close } from '@material-ui/icons'; import { Gitgraph, templateExtend, TemplateName } from '@gitgraph/react'; import { base_url, URLS } from '../../routes/constants'; import moment from 'moment'; import Modal from '../../common/Modal'; import { useDispatch, useSelector } from 'react-redux'; -import { useHistory, useRouteMatch } from 'react-router-dom'; import palette from '../../../theme/palette'; import { setIsOpenModalBeforeExit, setSelectedSnapshotToSwitch, setSharedSnapshot } from '../../snapshot/redux/actions'; -import { NglContext } from '../../nglView/nglProvider'; +import Gallery from 'react-grid-gallery'; const useStyles = makeStyles(theme => ({ drawer: { @@ -72,46 +71,67 @@ const options = { export const ProjectDetailDrawer = memo(({ showHistory, setShowHistory }) => { const [open, setOpen] = React.useState(false); const classes = useStyles(); - let history = useHistory(); - let match = useRouteMatch(); - const { nglViewList } = useContext(NglContext); const dispatch = useDispatch(); - const projectID = match && match.params && match.params.projectId; const currentProjectID = useSelector(state => state.projectReducers.currentProject.projectID); const currentSnapshotID = useSelector(state => state.projectReducers.currentSnapshot.id); const currentSnapshotList = useSelector(state => state.projectReducers.currentSnapshotList); const currentSnapshotTree = useSelector(state => state.projectReducers.currentSnapshotTree); const isLoadingTree = useSelector(state => state.projectReducers.isLoadingTree); + const currentSnapshotImageList = useSelector(state => state.trackingReducers.snapshotActionImageList); const handleClickOnCommit = commit => { dispatch(setSelectedSnapshotToSwitch(commit.hash)); dispatch(setIsOpenModalBeforeExit(true)); }; - const commitFunction = ({ title, description, photo, author, email, hash, isSelected, created }) => ({ + const commitFunction = ({ title, description, photo, author, email, hash, isSelected, created, images }) => ({ hash: `${hash}`, subject: `${title}`, body: ( <> - {/* setOpen(true)} />*/} - {/**/} - {/* */} - {/**/} - {/*
*/} - - {`${moment(created).format('LLL')}, ${email}: `} - {description} - - { - dispatch( - setSharedSnapshot({ title, description, url: `${base_url}${URLS.projects}${currentProjectID}/${hash}` }) - ); - }} - > - - + + { + + {/* setOpen(true)} />*/} + {/**/} + {/* */} + {/**/} + {/*
*/} + + {`${moment(created).format('LLL')}, ${email}: `} + {description} + +
+ } + { + { + dispatch( + setSharedSnapshot({ + title, + description, + url: `${base_url}${URLS.projects}${currentProjectID}/${hash}` + }) + ); + }} + > + + + } + { + + + + } +
), onMessageClick: handleClickOnCommit, @@ -128,6 +148,20 @@ export const ProjectDetailDrawer = memo(({ showHistory, setShowHistory }) => { name: node.title }); + let currentSnapshotImage = currentSnapshotImageList.find(i => i.id === node.id); + const nodeImages = + currentSnapshotImage != null + ? [ + { + src: currentSnapshotImage.image, + thumbnail: currentSnapshotImage.image, + thumbnailWidth: 0, + thumbnailHeight: 0, + caption: currentSnapshotImage.title + } + ] + : []; + newBranch.commit( commitFunction({ title: node.title || '', @@ -136,7 +170,8 @@ export const ProjectDetailDrawer = memo(({ showHistory, setShowHistory }) => { email: (node.author && node.author.email) || '', hash: node.id, isSelected: currentSnapshotID === node.id, - created: node.created + created: node.created, + images: nodeImages }) ); @@ -150,6 +185,20 @@ export const ProjectDetailDrawer = memo(({ showHistory, setShowHistory }) => { setShowHistory(false); }; + let image = currentSnapshotTree != null ? currentSnapshotImageList.find(i => i.id === currentSnapshotTree.id) : null; + const images = + image != null + ? [ + { + src: image.image, + thumbnail: image.image, + thumbnailWidth: 0, + thumbnailHeight: 0, + caption: image.title + } + ] + : []; + return ( <> @@ -187,7 +236,8 @@ export const ProjectDetailDrawer = memo(({ showHistory, setShowHistory }) => { email: (currentSnapshotTree.author && currentSnapshotTree.author.email) || '', hash: currentSnapshotTree.id, isSelected: currentSnapshotID === currentSnapshotTree.id, - created: currentSnapshotTree.created + created: currentSnapshotTree.created, + images }) ); diff --git a/js/components/projects/redux/dispatchActions.js b/js/components/projects/redux/dispatchActions.js index bb1eb5133..34ed6b19d 100644 --- a/js/components/projects/redux/dispatchActions.js +++ b/js/components/projects/redux/dispatchActions.js @@ -393,10 +393,9 @@ export const createProjectFromScratch = ({ title, description, target, author, t }); }; -export const createProjectWithoutStateModification = data => dispatch => { - return api({ url: `${base_url}/api/session-projects/`, method: METHOD.POST, data }).then(response => { - return response.data.id; - }); +export const createProjectWithoutStateModification = data => async () => { + const response = await api({ url: `${base_url}/api/session-projects/`, method: METHOD.POST, data }); + return response.data.id; }; export const createInitSnapshotToProjectWitActions = (session_project, author, parent, target) => ( diff --git a/js/components/snapshot/redux/dispatchActions.js b/js/components/snapshot/redux/dispatchActions.js index aa6360f65..cc1641922 100644 --- a/js/components/snapshot/redux/dispatchActions.js +++ b/js/components/snapshot/redux/dispatchActions.js @@ -335,15 +335,15 @@ export const createNewSnapshotWithoutStateModification = ({ }); }; -export const saveAndShareSnapshot = nglViewList => (dispatch, getState) => { +export const saveAndShareSnapshot = nglViewList => async (dispatch, getState) => { const state = getState(); const targetId = state.apiReducers.target_on; const loggedInUserID = DJANGO_CONTEXT['pk']; - dispatch(captureScreenOfSnapshot()); dispatch(setDisableRedirect(true)); if (targetId) { + dispatch(captureScreenOfSnapshot()); dispatch(setIsLoadingSnapshotDialog(true)); const data = { title: ProjectCreationType.READ_ONLY, @@ -353,36 +353,34 @@ export const saveAndShareSnapshot = nglViewList => (dispatch, getState) => { tags: '[]' }; - dispatch(createProjectWithoutStateModification(data)) - .then(projectID => { - const username = DJANGO_CONTEXT['username']; - const title = moment().format('-- YYYY-MM-DD -- HH:mm:ss'); - const description = - loggedInUserID === undefined ? 'Snapshot generated by anonymous user' : `snapshot generated by ${username}`; - const type = SnapshotType.MANUAL; - const author = loggedInUserID || null; - const parent = null; - const session_project = projectID; - - dispatch(sendTrackingActionsByProjectId(projectID, author)); - - return dispatch( - createNewSnapshotWithoutStateModification({ - title, - description, - type, - author, - parent, - session_project, - nglViewList - }) - ); - }) - .catch(error => { - throw new Error(error); - }) - .finally(() => { - dispatch(setIsLoadingSnapshotDialog(false)); - }); + try { + let projectID = await dispatch(createProjectWithoutStateModification(data)); + const username = DJANGO_CONTEXT['username']; + const title = moment().format('-- YYYY-MM-DD -- HH:mm:ss'); + const description = + loggedInUserID === undefined ? 'Snapshot generated by anonymous user' : `snapshot generated by ${username}`; + const type = SnapshotType.MANUAL; + const author = loggedInUserID || null; + const parent = null; + const session_project = projectID; + + await dispatch(sendTrackingActionsByProjectId(projectID, author)); + + await dispatch( + createNewSnapshotWithoutStateModification({ + title, + description, + type, + author, + parent, + session_project, + nglViewList + }) + ); + + dispatch(setIsLoadingSnapshotDialog(false)); + } catch (error) { + throw new Error(error); + } } }; diff --git a/js/components/tracking/timelineView.js b/js/components/tracking/timelineView.js index 533bdcef4..18036eed2 100644 --- a/js/components/tracking/timelineView.js +++ b/js/components/tracking/timelineView.js @@ -141,7 +141,7 @@ const TimelineView = memo(({ data, index }) => { dispatch(updateTrackingActions(data)); }; - const IMAGES = [ + const images = [ { src: data.image, thumbnail: data.image, @@ -169,7 +169,7 @@ const TimelineView = memo(({ data, index }) => { { async (d currentActions.push(Object.assign({ ...trackAction })); } - await dispatch(saveSnapshotAction(snapshot, project)); + await dispatch(saveSnapshotAction(snapshot, project, currentActions)); + await dispatch(saveTrackingActions(currentActions, snapshotID)); dispatch(setCurrentActionsList(currentActions)); - dispatch(saveTrackingActions(currentActions, snapshotID)); } }; -const saveSnapshotAction = (snapshot, project) => async (dispatch, getState) => { +const saveSnapshotAction = (snapshot, project, currentActions) => async (dispatch, getState) => { const state = getState(); const trackingImageSource = state.trackingReducers.trackingImageSource; @@ -265,10 +271,11 @@ const saveSnapshotAction = (snapshot, project) => async (dispatch, getState) => image: trackingImageSource }; sendActions.push(snapshotAction); + currentActions.push(snapshotAction); await dispatch(sendTrackingActions(sendActions, project)); }; -export const saveTrackingActions = (currentActions, snapshotID) => (dispatch, getState) => { +export const saveTrackingActions = (currentActions, snapshotID) => async (dispatch, getState) => { const state = getState(); const project = state.projectReducers.currentProject; const projectID = project && project.projectID; @@ -470,6 +477,7 @@ export const restoreAfterTargetActions = (stages, projectId) => async (dispatch, await dispatch(loadAllDatasets(orderedActionList, targetId, majorView.stage)); await dispatch(restoreRepresentationActions(orderedActionList, stages)); await dispatch(restoreProject(projectId)); + dispatch(restoreSnapshotImageActions(projectId)); dispatch(restoreNglStateAction(orderedActionList, stages)); dispatch(setIsActionsRestoring(false, true)); } @@ -643,6 +651,18 @@ const restoreRepresentationActions = (moleculesAction, stages) => (dispatch, get } }; +const restoreSnapshotImageActions = projectID => async (dispatch, getState) => { + let actionList = await dispatch(getTrackingActions(projectID)); + + let snapshotActions = actionList.filter(action => action.type === actionType.SNAPSHOT); + if (snapshotActions) { + let actions = snapshotActions.map(s => { + return { id: s.object_id, image: s.image, title: s.object_name }; + }); + dispatch(setSnapshotImageActionList(actions)); + } +}; + const restoreProject = projectId => (dispatch, getState) => { if (projectId !== undefined) { return api({ url: `${base_url}/api/session-projects/${projectId}/` }).then(response => { @@ -1513,17 +1533,18 @@ const getTrackingActions = projectID => (dispatch, getState) => { } }; -const checkActionsProject = projectID => (dispatch, getState) => { +const checkActionsProject = projectID => async (dispatch, getState) => { const state = getState(); const currentProject = state.projectReducers.currentProject; const currentProjectID = currentProject && currentProject.projectID; - Promise.resolve(dispatch(getTrackingActions(projectID))).then(() => { - dispatch(copyActionsToProject(currentProject, true, currentProjectID && currentProjectID != null ? true : false)); - }); + await dispatch(getTrackingActions(projectID)); + await dispatch( + copyActionsToProject(currentProject, true, currentProjectID && currentProjectID != null ? true : false) + ); }; -const copyActionsToProject = (toProject, setActionList = true, clearSendList = true) => (dispatch, getState) => { +const copyActionsToProject = (toProject, setActionList = true, clearSendList = true) => async (dispatch, getState) => { const state = getState(); const actionList = state.trackingReducers.project_actions_list; @@ -1537,20 +1558,19 @@ const copyActionsToProject = (toProject, setActionList = true, clearSendList = t if (setActionList === true) { dispatch(setActionsList(newActionsList)); } - dispatch(sendTrackingActions(newActionsList, toProject, clearSendList)); + await dispatch(sendTrackingActions(newActionsList, toProject, clearSendList)); } }; -export const sendTrackingActionsByProjectId = (projectID, authorID) => (dispatch, getState) => { +export const sendTrackingActionsByProjectId = (projectID, authorID) => async (dispatch, getState) => { const state = getState(); const currentProject = state.projectReducers.currentProject; const currentProjectID = currentProject && currentProject.projectID; const project = { projectID, authorID }; - Promise.resolve(dispatch(getTrackingActions(currentProjectID))).then(() => { - dispatch(copyActionsToProject(project, false, currentProjectID && currentProjectID != null ? true : false)); - }); + await dispatch(getTrackingActions(currentProjectID)); + await dispatch(copyActionsToProject(project, false, currentProjectID && currentProjectID != null ? true : false)); }; export const sendInitTrackingActionByProjectId = target_on => (dispatch, getState) => { diff --git a/js/reducers/tracking/trackingReducers.js b/js/reducers/tracking/trackingReducers.js index 2eb50d148..1724a8e91 100644 --- a/js/reducers/tracking/trackingReducers.js +++ b/js/reducers/tracking/trackingReducers.js @@ -13,6 +13,7 @@ export const INITIAL_STATE = { isActionSaving: false, send_actions_list: [], project_actions_list: [], + snapshotActionImageList: [], isActionRestoring: false, isActionRestored: false, trackingImageSource: '' @@ -80,6 +81,11 @@ export function trackingReducers(state = INITIAL_STATE, action = {}) { project_actions_list: action.project_actions_list }); + case constants.SET_SNAPSOT_IMAGE_ACTIONS_LIST: + return Object.assign({}, state, { + snapshotActionImageList: action.snapshotActionImageList + }); + case constants.SET_IS_ACTIONS_SAVING: return Object.assign({}, state, { isActionSaving: action.isActionSaving From 02190a1eff3dc64b695e2ea64264cc7abaed0e62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1na=20Kohanov=C3=A1?= Date: Wed, 16 Dec 2020 13:07:30 +0100 Subject: [PATCH 7/7] #469 Make ideas/actions pretty and recognisable --- js/reducers/tracking/dispatchActions.js | 95 ++++++++++++++++++++++++- 1 file changed, 92 insertions(+), 3 deletions(-) diff --git a/js/reducers/tracking/dispatchActions.js b/js/reducers/tracking/dispatchActions.js index ef0aeec58..d2b30bcb6 100644 --- a/js/reducers/tracking/dispatchActions.js +++ b/js/reducers/tracking/dispatchActions.js @@ -97,6 +97,8 @@ import { export const saveCurrentActionsList = (snapshot, project, nglViewList) => async (dispatch, getState) => { let projectID = project && project.projectID; let actionList = await dispatch(getTrackingActions(projectID)); + + dispatch(setSnapshotToActions(actionList, snapshot, projectID)); await dispatch(saveActionsList(project, snapshot, actionList, nglViewList)); }; @@ -267,6 +269,7 @@ const saveSnapshotAction = (snapshot, project, currentActions) => async (dispatc timestamp: Date.now(), object_name: snapshot.title, object_id: snapshot.id, + snapshotId: snapshot.id, text: `Snapshot: ${snapshot.id} - ${snapshot.title}`, image: trackingImageSource }; @@ -275,6 +278,14 @@ const saveSnapshotAction = (snapshot, project, currentActions) => async (dispatc await dispatch(sendTrackingActions(sendActions, project)); }; +const setSnapshotToActions = (actionList, snapshot, projectID) => (dispatch, getState) => { + if (actionList && snapshot) { + let actionsWithoutSnapshot = actionList.filter(a => a.snapshotId === null || a.snapshotId === undefined); + let updatedActions = actionsWithoutSnapshot.map(obj => ({ ...obj, snapshotId: snapshot.id })); + dispatch(setAndUpdateTrackingActions(updatedActions, projectID)); + } +}; + export const saveTrackingActions = (currentActions, snapshotID) => async (dispatch, getState) => { const state = getState(); const project = state.projectReducers.currentProject; @@ -1494,11 +1505,11 @@ export const setProjectTrackingActions = () => (dispatch, getState) => { const state = getState(); const currentProject = state.projectReducers.currentProject; const projectID = currentProject && currentProject.projectID; - - dispatch(getTrackingActions(projectID)); + dispatch(setProjectActionList([])); + dispatch(getTrackingActions(projectID, true)); }; -const getTrackingActions = projectID => (dispatch, getState) => { +const getTrackingActions = (projectID, withTreeSeparation) => (dispatch, getState) => { const state = getState(); const sendActions = state.trackingReducers.send_actions_list; @@ -1516,6 +1527,10 @@ const getTrackingActions = projectID => (dispatch, getState) => { listToSet.push(...actions); }); + if (withTreeSeparation === true) { + listToSet = dispatch(separateTrackkingActionBySnapshotTree(listToSet)); + } + let projectActions = [...listToSet, ...sendActions]; dispatch(setProjectActionList(projectActions)); return Promise.resolve(projectActions); @@ -1533,6 +1548,40 @@ const getTrackingActions = projectID => (dispatch, getState) => { } }; +const separateTrackkingActionBySnapshotTree = actionList => (dispatch, getState) => { + const state = getState(); + const snapshotID = state.projectReducers.currentSnapshot && state.projectReducers.currentSnapshot.id; + const currentSnapshotTree = state.projectReducers.currentSnapshotTree; + const currentSnapshotList = state.projectReducers.currentSnapshotList; + + if (snapshotID && currentSnapshotTree != null) { + let treeActionList = []; + let snapshotIdList = []; + snapshotIdList.push(currentSnapshotTree.id); + + if (currentSnapshotList != null) { + for (const id in currentSnapshotList) { + let snapshot = currentSnapshotList[id]; + let snapshotChildren = snapshot.children; + + if ( + (snapshotChildren && snapshotChildren !== null && snapshotChildren.includes(snapshotID)) || + snapshot.id === snapshotID + ) { + snapshotIdList.push(snapshot.id); + } + } + } + + treeActionList = actionList.filter( + a => snapshotIdList.includes(a.snapshotId) || a.snapshotId === null || a.snapshotId === undefined + ); + return treeActionList; + } else { + return actionList; + } +}; + const checkActionsProject = projectID => async (dispatch, getState) => { const state = getState(); const currentProject = state.projectReducers.currentProject; @@ -1622,3 +1671,43 @@ export const updateTrackingActions = action => (dispatch, getState) => { return Promise.resolve(); } }; + +function groupArrayOfObjects(list, key) { + return list.reduce(function(rv, x) { + (rv[x[key]] = rv[x[key]] || []).push(x); + return rv; + }, {}); +} + +export const setAndUpdateTrackingActions = (actionList, projectID) => (dispatch, getState) => { + if (projectID) { + const groupBy = groupArrayOfObjects(actionList, 'actionId'); + + for (const group in groupBy) { + let actionID = group; + let actions = groupBy[group]; + if (actionID && actions && actions.length > 0) { + const dataToSend = { + session_action_id: actionID, + session_project: projectID, + last_update_date: moment().format(), + actions: JSON.stringify(actions) + }; + return api({ + url: `${base_url}/api/session-actions/${actionID}`, + method: METHOD.PUT, + data: JSON.stringify(dataToSend) + }) + .then(() => {}) + .catch(error => { + throw new Error(error); + }) + .finally(() => {}); + } else { + return Promise.resolve(); + } + } + } else { + return Promise.resolve(); + } +};