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",