From 874021177850a4ed0a5ff389c2ca9f209a9de53a Mon Sep 17 00:00:00 2001 From: boriskovar-m2ms Date: Wed, 21 Apr 2021 14:38:15 +0200 Subject: [PATCH] Implementation of discourse (#239) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update package.json - updated version number to 0.9.69 * - prelim changes * #557 Bug? Snapshot doesn't restore precise RHS tab * #571 [L] toggle not restored from snapshot for things on Selected Compounds tab * - implemented first phase of discourse integration * Squashed commit of the following: commit 431c372cfd29dc1054cae3cd2f3083d369265816 Author: Boris Kovar Date: Wed Mar 24 15:56:39 2021 +0100 - implemented first phase of discourse integration commit 103da7e4a72380042fc9e7d97cd4171f9e9e5743 Author: Boris Kovar Date: Thu Mar 11 10:50:30 2021 +0100 - prelim changes * - updated version number to 0.10.1 * Squashed commit of the following: commit 5761defb26b6103cfb3b16a4630400bc4c786478 Author: ag-m2ms Date: Wed Feb 3 14:23:02 2021 +0100 Fixed layout and imports commit d9b589587cd46d97ba0369e8c8f428e2b970fbbe Merge: 717ad541 083cc6da Author: ag-m2ms Date: Wed Feb 3 13:55:03 2021 +0100 Merge branch 'allfunctionality' into #487 commit 717ad54119a1a00c2e1ac5bcebd3536917db5e2c Author: ag-m2ms Date: Wed Feb 3 13:54:33 2021 +0100 Removed unnecessary parts commit 6c23110eaf43122acd1c37f156c02e61c7fa7819 Author: ag-m2ms Date: Wed Feb 3 13:36:29 2021 +0100 Fixed undo commit 19f839c1c127c38d5f5f9b92956765e20322dc6d Author: ag-m2ms Date: Mon Feb 1 15:31:13 2021 +0100 Fixed button history tracking commit bf7b8a8b6a445ac2de305be0d3d07024c8c5f039 Author: ag-m2ms Date: Mon Feb 1 14:15:34 2021 +0100 Replaced throttle with debounce for nglView tracking commit 57b71e02848cf16db43a807b002437b768c765b6 Author: ag-m2ms Date: Mon Feb 1 12:58:06 2021 +0100 Clear history on project change commit 419c2685812a25aa5732291102b559b60909ac00 Author: ag-m2ms Date: Mon Feb 1 12:10:50 2021 +0100 Separate orientation actions commit 272c265c3dc2121f5ee9b515c522197145ed7b71 Merge: f8440879 bef6400b Author: ag-m2ms Date: Mon Feb 1 10:34:26 2021 +0100 Merge branch 'allfunctionality' into #487 commit f8440879ec6a11a453771540955f37f2611321b0 Author: ag-m2ms Date: Thu Jan 21 13:52:19 2021 +0100 Ctreated temporary buttons commit 9b8a2ac8b943d21350ee515cccf64b5180d94ff7 Author: ag-m2ms Date: Thu Jan 21 11:54:52 2021 +0100 Tidying up commit cb6c477658dd8e970784b39a2dd9e396555ca2e3 Author: ag-m2ms Date: Thu Jan 21 11:25:23 2021 +0100 Change ngl view on undo/redo commit edfb5715b81d17409f916bfb0e29831804821391 Author: ag-m2ms Date: Wed Jan 20 16:37:42 2021 +0100 Initial implementation * - implemented modal window for discourse errors * - updated version number to 0.10.2 * - slightly updated error description * - loading discourse links in synchronous manner to avoid net::ERR_INSUFFICIENT_RESOURCES * - updated version number to 0.10.3 * - implemented "lazy" handling of the Discourse * Squashed commit of the following: commit b9205ddf6d076bed7c4b3ee3b63d11acd3422fc6 Author: Boris Kovar Date: Mon Mar 29 15:13:32 2021 +0200 - implemented "lazy" handling of the Discourse * - updated version number to 0.10.4 * - hide link to project icon if no project is loaded * - updated version number to 0.10.5 * - updated error message * - used icon button instead of link in the target list because it behaved differently in test env from behavior in local env * - updated version number to 0.10.6 * - fixed issue when creating project from the preview and the first post for the first snapshot was not created * - updated version number to 0.10.7 * - visual improvements * - updated version number to 0.10.8 * - visual improvements * - updated version number to 0.10.9 * - fix (maybe) for that sometimes a discourse post was attempted before the project and/or snapshot were created * - updated version number to 0.10.10 * - fixed problem where snapshot post was attempted before project post was completed which caused that the snapshot post was never created * - updated version number to 0.10.11 * - fixed saving of the session actions * - updated version number to 0.10.12 * - checkpoint * - updated version number to 0.10.13 * #602 Snapshot tree and discourse posts * #602 Snapshot tree and discourse posts * - create discourse topic and post are disabled when creation process is ongoing - if discourse user doesn't exists the notice is displayed * - changed version number to 0.10.14 * - open discourse link in the new tab * - updated version number to 0.10.15 * - minor fixes * - updated version number to 0.10.16 * - fixed #609 * - updated version number to 0.10.17 Co-authored-by: Adriána Kohanová Co-authored-by: Adriána Kurillová --- .vscode/launch.json | 4 +- docker-compose.dev.vector.yml | 6 + .../datasets/selectedCompoundsList.js | 3 - js/components/discourse/RegisterNotice.js | 19 + js/components/header/discourseErrorModal.js | 46 + js/components/header/index.js | 78 +- js/components/nglView/nglView.js | 6 +- .../compounds/redux/dispatchActions.js | 29 +- .../preview/redux/dispatchActions.js | 3 + js/components/preview/viewerControls/index.js | 60 +- .../projects/addProjectDetail/index.js | 94 +- js/components/projects/index.js | 69 +- js/components/projects/projectModal/index.js | 92 +- js/components/projects/redux/actions.js | 10 + js/components/projects/redux/constants.js | 4 +- .../projects/redux/dispatchActions.js | 20 +- js/components/projects/redux/reducer.js | 10 +- .../snapshot/modals/newSnapshotForm.js | 61 +- .../snapshot/redux/dispatchActions.js | 94 +- js/components/target/redux/actions.js | 7 + js/components/target/redux/constatnts.js | 4 +- js/components/target/redux/reducer.js | 10 +- js/components/target/targetList.js | 46 +- js/index.js | 4 +- js/reducers/api/actions.js | 4 + js/reducers/api/apiReducers.js | 9 +- js/reducers/api/constants.js | 3 +- js/reducers/ngl/actions.js | 2 + js/reducers/ngl/constants.js | 1 + js/reducers/ngl/dispatchActions.js | 24 +- js/reducers/ngl/nglReducers.js | 16 +- js/reducers/nglTracking/actions.js | 150 ++ js/reducers/nglTracking/constants.js | 115 ++ js/reducers/nglTracking/dispatchActions.js | 1626 +++++++++++++++++ .../nglTracking/nglTrackingMiddleware.js | 21 + .../nglTracking/nglTrackingReducers.js | 131 ++ js/reducers/nglTracking/trackingActions.js | 60 + js/reducers/rootReducer.js | 5 +- js/reducers/tracking/dispatchActions.js | 25 +- js/reducers/tracking/trackingActions.js | 6 +- js/utils/discourse.js | 76 + package.json | 2 +- 42 files changed, 2903 insertions(+), 152 deletions(-) create mode 100644 js/components/discourse/RegisterNotice.js create mode 100644 js/components/header/discourseErrorModal.js create mode 100644 js/reducers/nglTracking/actions.js create mode 100644 js/reducers/nglTracking/constants.js create mode 100644 js/reducers/nglTracking/dispatchActions.js create mode 100644 js/reducers/nglTracking/nglTrackingMiddleware.js create mode 100644 js/reducers/nglTracking/nglTrackingReducers.js create mode 100644 js/reducers/nglTracking/trackingActions.js create mode 100644 js/utils/discourse.js diff --git a/.vscode/launch.json b/.vscode/launch.json index 773c93a62..1c021104d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,14 +9,14 @@ "port": 9222, "request": "attach", "type": "pwa-chrome", - "urlFilter": "http://localhost:8080/*", + "urlFilter": "http://127.0.0.1:8080/*", "webRoot": "${workspaceFolder}" }, { "type": "chrome", "request": "launch", "name": "Launch Chrome against localhost", - "url": "http://localhost:8080", + "url": "http://127.0.0.1:8080", "webRoot": "${workspaceFolder}" } ] diff --git a/docker-compose.dev.vector.yml b/docker-compose.dev.vector.yml index 987d18fd3..ee7db9f28 100644 --- a/docker-compose.dev.vector.yml +++ b/docker-compose.dev.vector.yml @@ -44,6 +44,12 @@ services: POSTGRESQL_HOST: database POSTGRESQL_PORT: 5432 NEO4J_AUTH: neo4j/test123 + OIDC_RP_CLIENT_SECRET: c6245428-04c7-466f-9c4f-58c340e981c2 + DISCOURSE_API_KEY: d6036de5e412123b77048c0b92d42fb108484ea1eef2918d67b236802fa8cd4f + FRAGALYSIS_BACKEND_SENTRY_DNS: https://27fa0675f555431aa02ca552e93d8cfb@o194333.ingest.sentry.io/1298290 + ISPYB_FLAG: 'False' + DISCOURSE_DEV_POST_SUFFIX: '-boris-local' + DISCOURSE_HOST: 'https://discourse.xchem-dev.diamond.ac.uk' depends_on: - database - graph diff --git a/js/components/datasets/selectedCompoundsList.js b/js/components/datasets/selectedCompoundsList.js index 1119beb5b..fac730448 100644 --- a/js/components/datasets/selectedCompoundsList.js +++ b/js/components/datasets/selectedCompoundsList.js @@ -241,7 +241,6 @@ export const SelectedCompoundList = memo(({ height }) => { const getCompoundIds = (mols) => { let result = {diffIds: {}, namesToIds: {}, idsInOrder: new Set()}; - let currentIdIndex = 0; mols.forEach(mol => { if (mol.molecule.hasOwnProperty('compound_ids')) { const ids = mol.molecule['compound_ids']; @@ -256,7 +255,6 @@ export const SelectedCompoundList = memo(({ height }) => { result.diffIds[vendorId].fieldsArray.push(idFieldName); result.namesToIds[idFieldName] = vendorId; result.idsInOrder.add(idFieldName); - currentIdIndex++; } else { if (perMolVendors.hasOwnProperty(vendorId)) { const perMolVendorCount = perMolVendors[vendorId]; @@ -269,7 +267,6 @@ export const SelectedCompoundList = memo(({ height }) => { result.diffIds[vendorId].fieldsArray.push(idFieldName); result.namesToIds[idFieldName] = vendorId; result.idsInOrder.add(idFieldName); - currentIdIndex++; } } else { perMolVendors[vendorId] = 1; diff --git a/js/components/discourse/RegisterNotice.js b/js/components/discourse/RegisterNotice.js new file mode 100644 index 000000000..695765548 --- /dev/null +++ b/js/components/discourse/RegisterNotice.js @@ -0,0 +1,19 @@ +import React from 'react'; +import { Typography, Link, Grid } from '@material-ui/core'; +import { getDiscourseURL } from '../../utils/discourse'; + +export const RegisterNotice = () => { + return ( + + + Your Discourse account doesn't exist. You can register + + + + {' '} + here + + + + ); +}; diff --git a/js/components/header/discourseErrorModal.js b/js/components/header/discourseErrorModal.js new file mode 100644 index 000000000..eb250eea8 --- /dev/null +++ b/js/components/header/discourseErrorModal.js @@ -0,0 +1,46 @@ +import React, { memo } from 'react'; +import Modal from '../common/Modal'; +import { Grid, Typography, Button } from '@material-ui/core'; +import { useDispatch } from 'react-redux'; +import { setOpenDiscourseErrorModal } from '../../reducers/api/actions'; + +export const DiscourseErrorModal = memo(({ openModal }) => { + const dispatch = useDispatch(); + + const closeModal = () => { + dispatch(setOpenDiscourseErrorModal(false)); + }; + + return ( + { + closeModal(); + }} + > + + + DISCOURSE ERROR! + + + + Most likely there already is topic with same name as is this project name or your discourse user name + doesn't match your fragalysis user name. Also please check if descriptions you provided are not just random + letters because they also tend to dismissed by the Discourse server. + + + + + {' '} + + + + ); +}); diff --git a/js/components/header/index.js b/js/components/header/index.js index 00903d4fa..b33608e29 100644 --- a/js/components/header/index.js +++ b/js/components/header/index.js @@ -16,7 +16,8 @@ import { Avatar, Box, ButtonGroup, - LinearProgress + LinearProgress, + Tooltip } from '@material-ui/core'; import { PowerSettingsNew, @@ -27,7 +28,9 @@ import { Menu as MenuIcon, Work, Description, - Timeline + Timeline, + QuestionAnswer, + Chat } from '@material-ui/icons'; import { HeaderContext } from './headerContext'; import { Button } from '../common'; @@ -38,11 +41,15 @@ import { DJANGO_CONTEXT } from '../../utils/djangoContext'; // import { useDisableUserInteraction } from '../helpers/useEnableUserInteracion'; import { useHistory } from 'react-router-dom'; import { IssueReport } from '../userFeedback/issueReport'; -import { IdeaReport } from '../userFeedback/ideaReport'; import { FundersModal } from '../funders/fundersModal'; import { TrackingModal } from '../tracking/trackingModal'; // eslint-disable-next-line import/extensions import { version } from '../../../package.json'; +import { isDiscourseAvailable } from '../../utils/discourse'; +import { useSelector, useDispatch } from 'react-redux'; +import { generateDiscourseTargetURL, getExistingPost } from '../../utils/discourse'; +import { DiscourseErrorModal } from './discourseErrorModal'; +import { setOpenDiscourseErrorModal } from '../../reducers/api/actions'; const useStyles = makeStyles(theme => ({ padding: { @@ -89,6 +96,7 @@ const useStyles = makeStyles(theme => ({ export default memo( forwardRef(({ headerHeight = 0, setHeaderHeight }, ref) => { + const dispatch = useDispatch(); let history = useHistory(); const classes = useStyles(); const { isLoading, headerNavbarTitle, setHeaderNavbarTitle, headerButtons } = useContext(HeaderContext); @@ -98,6 +106,15 @@ export default memo( const [openFunders, setOpenFunders] = useState(false); const [openTrackingModal, setOpenTrackingModal] = useState(false); + const currentProject = useSelector(state => state.projectReducers.currentProject); + const targetName = useSelector(state => state.apiReducers.target_on_name); + + const openDiscourseError = useSelector(state => state.apiReducers.open_discourse_error_modal); + + const discourseAvailable = isDiscourseAvailable(); + const targetDiscourseVisible = discourseAvailable && targetName; + const projectDiscourseVisible = discourseAvailable && currentProject && currentProject.title; + const openXchem = () => { // window.location.href = 'https://www.diamond.ac.uk/Instruments/Mx/Fragment-Screening.html'; window.open('https://www.diamond.ac.uk/Instruments/Mx/Fragment-Screening.html', '_blank'); @@ -123,6 +140,10 @@ export default memo( window.open('https://covid.postera.ai/covid', '_blank'); }; + const openDiscourseLink = url => { + window.open(url, '_blank'); + }; + let authListItem; let username = null; @@ -212,6 +233,56 @@ export default memo( {headerButtons && headerButtons.map(item => item)} + + {discourseAvailable && ( + + {targetDiscourseVisible && ( + + + + )} + {projectDiscourseVisible && ( + + + + )} + + )} + @@ -287,6 +358,7 @@ export default memo( setOpenFunders(false)} /> setOpenTrackingModal(false)} /> + { + debounce(() => { const newStage = getNglView(div_id); if (newStage) { const currentOrientation = newStage.stage.viewerControls.getOrientation(); @@ -147,7 +147,7 @@ function mapStateToProps(state) { } const mapDispatchToProps = { setMolGroupSelection: selectionActions.setMolGroupSelection, - setOrientation: nglDispatchActions.setOrientation, + setOrientation: nglDispatchActions.setOrientationByInteraction, removeAllNglComponents: nglActions.removeAllNglComponents, handleNglViewPick }; diff --git a/js/components/preview/compounds/redux/dispatchActions.js b/js/components/preview/compounds/redux/dispatchActions.js index f13a97103..baa9e6e6d 100644 --- a/js/components/preview/compounds/redux/dispatchActions.js +++ b/js/components/preview/compounds/redux/dispatchActions.js @@ -212,17 +212,21 @@ const clearCompounds = (items, majorViewStage) => (dispatch, getState) => { }; export const prepareFakeFilterData = () => (dispatch, getState) => { - dispatch(updateFilterShowedScoreProperties({ - datasetID: AUX_VECTOR_SELECTOR_DATASET_ID, - scoreList: [{id: 1, name: 'smiles', description: 'smiles', computed_set: AUX_VECTOR_SELECTOR_DATASET_ID}, - {id: 2, name: 'mol', description: 'mol', computed_set: AUX_VECTOR_SELECTOR_DATASET_ID}, - {id: 3, name: 'vector', description: 'vector', computed_set: AUX_VECTOR_SELECTOR_DATASET_ID}, - {id: 4, name: 'class', description: 'class', computed_set: AUX_VECTOR_SELECTOR_DATASET_ID}, - {id: 5, name: 'compoundClass', description: 'compoundClass', computed_set: AUX_VECTOR_SELECTOR_DATASET_ID}] - })); -} - -export const isCompoundFromVectorSelector = (data) => { + dispatch( + updateFilterShowedScoreProperties({ + datasetID: AUX_VECTOR_SELECTOR_DATASET_ID, + scoreList: [ + { id: 1, name: 'smiles', description: 'smiles', computed_set: AUX_VECTOR_SELECTOR_DATASET_ID }, + { id: 2, name: 'mol', description: 'mol', computed_set: AUX_VECTOR_SELECTOR_DATASET_ID }, + { id: 3, name: 'vector', description: 'vector', computed_set: AUX_VECTOR_SELECTOR_DATASET_ID }, + { id: 4, name: 'class', description: 'class', computed_set: AUX_VECTOR_SELECTOR_DATASET_ID }, + { id: 5, name: 'compoundClass', description: 'compoundClass', computed_set: AUX_VECTOR_SELECTOR_DATASET_ID } + ] + }) + ); +}; + +export const isCompoundFromVectorSelector = data => { if (data['index'] !== undefined) { return true; } else { @@ -330,7 +334,8 @@ export const handleBuyListAll = ({ isSelected, items, majorViewStage }) => (disp } }; -export const handleShowVectorCompound = ({ isSelected, data, index, majorViewStage }) => async (dispatch, getState) => { +export const handleShowVectorCompound = ({ isSelected, data, majorViewStage }) => async (dispatch, getState) => { + const index = data.index; await dispatch(showCompoundNglView({ majorViewStage, data, index })); if (isSelected === false) { dispatch(removeShowedCompoundFromList(index, data)); diff --git a/js/components/preview/redux/dispatchActions.js b/js/components/preview/redux/dispatchActions.js index fee6aafbc..c9d0aa082 100644 --- a/js/components/preview/redux/dispatchActions.js +++ b/js/components/preview/redux/dispatchActions.js @@ -12,6 +12,7 @@ import { resetDatasetsState } from '../../datasets/redux/actions'; import { restoreAfterTargetActions } from '../../../reducers/tracking/dispatchActions'; import { resetTrackingState } from '../../../reducers/tracking/actions'; import { setTargetOn } from '../../../reducers/api/actions'; +import { resetNglTrackingState } from '../../../reducers/nglTracking/dispatchActions'; const loadProtein = nglView => (dispatch, getState) => { const state = getState(); @@ -122,6 +123,7 @@ export const unmountPreviewComponent = (stages = []) => dispatch => { }); dispatch(resetTrackingState()); + dispatch(resetNglTrackingState()); dispatch(resetCurrentCompoundsSettings(true)); dispatch(resetProjectsReducer()); @@ -146,6 +148,7 @@ export const resetReducersBetweenSnapshots = (stages = []) => dispatch => { dispatch(resetSelectionState()); dispatch(resetDatasetsState()); dispatch(resetTrackingState()); + dispatch(resetNglTrackingState()); dispatch(setTargetOn(undefined)); }; diff --git a/js/components/preview/viewerControls/index.js b/js/components/preview/viewerControls/index.js index b01b3ef55..4813e57b8 100644 --- a/js/components/preview/viewerControls/index.js +++ b/js/components/preview/viewerControls/index.js @@ -20,7 +20,14 @@ import { getRedoActionText, restoreNglViewSettings } from '../../../../js/reducers/tracking/dispatchActions'; +import { + undoAction as nglUndoAction, + redoAction as nglRedoAction, + getUndoActionText as nglGetUndoActionText, + getRedoActionText as nglGetRedoActionText +} from '../../../../js/reducers/nglTracking/dispatchActions'; import { NglContext } from '../../nglView/nglProvider'; +import { nglTrackingRedo, nglTrackingUndo } from '../../../reducers/nglTracking/actions'; const drawers = { settings: 'settings', @@ -36,7 +43,7 @@ const useStyles = makeStyles(theme => ({ }, buttonMargin: { padding: theme.spacing(1), - marginLeft: theme.spacing(8) + marginLeft: theme.spacing(1) } })); @@ -49,8 +56,23 @@ export const ViewerControls = memo(({}) => { const [redoTooltip, setRedoTooltip] = useState('Redo'); const [canUndo, setCanUndo] = useState(true); const [canRedo, setCanRedo] = useState(false); + const [nglUndoTooltip, nglSetUndoTooltip] = useState('Undo'); + const [nglRedoTooltip, nglSetRedoTooltip] = useState('Redo'); const isActionTracking = useSelector(state => state.trackingReducers.isActionTracking); + const nglUndoLength = useSelector(state => state.undoableNglTrackingReducers.past).length; + const nglRedoLength = useSelector(state => state.undoableNglTrackingReducers.future).length; + const nglCanUndo = nglUndoLength > 0; + const nglCanRedo = nglRedoLength > 0; + + useEffect(() => { + nglSetUndoTooltip(dispatch(nglGetUndoActionText())); + }, [dispatch, nglUndoLength]); + + useEffect(() => { + nglSetRedoTooltip(dispatch(nglGetRedoActionText())); + }, [dispatch, nglRedoLength]); + const openDrawer = key => { //close all and open selected by key let newDrawerState = JSON.parse(JSON.stringify(initDrawers)); @@ -71,6 +93,11 @@ export const ViewerControls = memo(({}) => { setRedoTooltip(dispatch(getRedoActionText())); }; + const nglDoUndo = () => { + dispatch(nglTrackingUndo()); + dispatch(nglUndoAction(nglViewList)); + }; + const doRedo = () => { dispatch(UndoActionCreators.redo()); setCanRedo(dispatch(getCanRedo())); @@ -81,6 +108,11 @@ export const ViewerControls = memo(({}) => { setRedoTooltip(dispatch(getRedoActionText())); }; + const nglDoRedo = () => { + dispatch(nglTrackingRedo()); + dispatch(nglRedoAction(nglViewList)); + }; + const handleUserKeyPress = useCallback(e => { var evtobj = window.event ? window.event : e; if (evtobj.keyCode === 90 && evtobj.ctrlKey) { @@ -107,6 +139,19 @@ export const ViewerControls = memo(({}) => { + + + + + + diff --git a/js/components/projects/addProjectDetail/index.js b/js/components/projects/addProjectDetail/index.js index b48ed4682..ced1d0277 100644 --- a/js/components/projects/addProjectDetail/index.js +++ b/js/components/projects/addProjectDetail/index.js @@ -1,15 +1,17 @@ import React, { memo, useState } from 'react'; -import { Grid, makeStyles, Typography } from '@material-ui/core'; +import { Grid, makeStyles, Typography, FormControlLabel, Checkbox } from '@material-ui/core'; import { DJANGO_CONTEXT } from '../../../utils/djangoContext'; import { Form, Formik, Field } from 'formik'; import { TextField } from 'formik-material-ui'; import { InputFieldAvatar } from '../projectModal/inputFieldAvatar'; -import { Description, Label, Title } from '@material-ui/icons'; +import { Description, Label, Title, QuestionAnswer } from '@material-ui/icons'; import { Autocomplete } from '@material-ui/lab'; import { Button } from '../../common/Inputs/Button'; import { useDispatch, useSelector } from 'react-redux'; -import { createProjectFromSnapshotDialog } from '../redux/dispatchActions'; +import { createProjectFromSnapshotDialog, createProjectDiscoursePost } from '../redux/dispatchActions'; import { manageSendTrackingActions } from '../../../reducers/tracking/dispatchActions'; +import { isDiscourseAvailable, getExistingPost, isDiscourseUserAvailable } from '../../../utils/discourse'; +import { RegisterNotice } from '../../discourse/RegisterNotice'; const useStyles = makeStyles(theme => ({ body: { @@ -32,14 +34,38 @@ const useStyles = makeStyles(theme => ({ export const AddProjectDetail = memo(({ handleCloseModal }) => { const classes = useStyles(); const [state, setState] = useState(); + let [createDiscourse, setCreateDiscourse] = useState(true); const dispatch = useDispatch(); const targetId = useSelector(state => state.apiReducers.target_on); + const targetName = useSelector(state => state.apiReducers.target_on_name); const projectID = useSelector(state => state.projectReducers.currentProject.projectID); const isProjectModalLoading = useSelector(state => state.projectReducers.isProjectModalLoading); const [tags, setTags] = React.useState([]); + const discourseAvailable = isDiscourseAvailable(); + const dicourseUserAvailable = isDiscourseUserAvailable(); + + createDiscourse &= dicourseUserAvailable; + + const validateProjectName = async value => { + let error; + // console.log(`Project title validating and value is: ${value}`); + + if (!value) { + error = 'Required!'; + } else if (createDiscourse) { + const response = await getExistingPost(value); + // console.log(response); + if (response.data['Post url']) { + error = 'Already exists!'; + } + } + + return error; + }; + return ( <> Project Details @@ -51,11 +77,10 @@ export const AddProjectDetail = memo(({ handleCloseModal }) => { }} validate={values => { const errors = {}; - if (!values.title) { - errors.title = 'Required!'; - } if (!values.description) { errors.description = 'Required!'; + } else if (values.description.length < 20) { + errors.description = 'Description must be at least 20 characters long!'; } return errors; }} @@ -69,15 +94,26 @@ export const AddProjectDetail = memo(({ handleCloseModal }) => { }; const oldProjectID = projectID; - dispatch(createProjectFromSnapshotDialog(data)) - .then(() => { - dispatch(manageSendTrackingActions(oldProjectID, true)); - }) - .catch(error => { - setState(() => { - throw error; + if (createDiscourse) { + dispatch(createProjectDiscoursePost(values.title, targetName, values.description, tags)) + .then(() => dispatch(createProjectFromSnapshotDialog(data))) + .then(() => dispatch(manageSendTrackingActions(oldProjectID, true))) + .catch(error => { + setState(() => { + throw error; + }); }); - }); + } else { + dispatch(createProjectFromSnapshotDialog(data)) + .then(() => { + dispatch(manageSendTrackingActions(oldProjectID, true)); + }) + .catch(error => { + setState(() => { + throw error; + }); + }); + } }} > {({ submitForm, isSubmitting }) => ( @@ -94,6 +130,7 @@ export const AddProjectDetail = memo(({ handleCloseModal }) => { label="Title" required disabled={isProjectModalLoading || isSubmitting} + validate={validateProjectName} /> } /> @@ -146,15 +183,40 @@ export const AddProjectDetail = memo(({ handleCloseModal }) => { } /> + + } + field={ + setCreateDiscourse(!createDiscourse)} + disabled={ + !discourseAvailable || !dicourseUserAvailable || isProjectModalLoading || isSubmitting + } + name="createDisTopic" + /> + } + label="Create Discourse topic" + /> + } + /> + + {!dicourseUserAvailable && ( + + + + )} - - diff --git a/js/components/projects/index.js b/js/components/projects/index.js index 8b53db995..bd0a7f6df 100644 --- a/js/components/projects/index.js +++ b/js/components/projects/index.js @@ -16,7 +16,7 @@ import { Tooltip, Zoom } from '@material-ui/core'; -import { Delete, Add, Search } from '@material-ui/icons'; +import { Delete, Add, Search, QuestionAnswer } from '@material-ui/icons'; import { Link } from 'react-router-dom'; import { debounce } from 'lodash'; import { URLS } from '../routes/constants'; @@ -26,6 +26,8 @@ import { useDispatch, useSelector } from 'react-redux'; import { ProjectModal } from './projectModal'; import { loadListOfAllProjects, removeProject, searchInProjects } from './redux/dispatchActions'; import { DJANGO_CONTEXT } from '../../utils/djangoContext'; +import { isDiscourseAvailable, getExistingPost } from '../../utils/discourse'; +import { setOpenDiscourseErrorModal } from '../../reducers/api/actions'; const useStyles = makeStyles(theme => ({ table: { @@ -84,6 +86,8 @@ export const Projects = memo(({}) => { let debouncedFn; + const discourseAvailable = isDiscourseAvailable(); + const handleSearch = event => { /* signal to React not to nullify the event object */ event.persist(); @@ -142,29 +146,25 @@ export const Projects = memo(({}) => { {listOfProjects.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage).map(project => ( - - + + {project.name} - - {project.target} - - - {project.tags && - project.tags.map((tag, index) => ( - - ))} - - {project.author} - {moment(project.createdAt).format('LLL')} - + + + {project.target} + + + {project.tags && + project.tags.map((tag, index) => ( + + ))} + + {project.author} + {moment(project.createdAt).format('LLL')} + + @@ -175,9 +175,30 @@ export const Projects = memo(({}) => { > - - - + + {discourseAvailable && ( + + { + getExistingPost(project.name) + .then(response => { + if (response.data['Post url']) { + const link = response.data['Post url']; + window.open(link, '_blank'); + } + }) + .catch(err => { + console.log(err); + dispatch(setOpenDiscourseErrorModal(true)); + }); + }} + > + + + + )} + + ))} diff --git a/js/components/projects/projectModal/index.js b/js/components/projects/projectModal/index.js index 49ff281da..8fa24dbee 100644 --- a/js/components/projects/projectModal/index.js +++ b/js/components/projects/projectModal/index.js @@ -13,9 +13,10 @@ import { FormControl, FormHelperText, FormControlLabel, - ListItemText + ListItemText, + Checkbox } from '@material-ui/core'; -import { Title, Description, Label, Link } from '@material-ui/icons'; +import { Title, Description, Label, Link, QuestionAnswer } from '@material-ui/icons'; import { Autocomplete } from '@material-ui/lab'; import { useHistory } from 'react-router-dom'; import { DJANGO_CONTEXT } from '../../../utils/djangoContext'; @@ -26,7 +27,13 @@ import { TextField, Select, RadioGroup } from 'formik-material-ui'; import { Button } from '../../common/Inputs/Button'; import { getListOfSnapshots } from '../../snapshot/redux/dispatchActions'; import moment from 'moment'; -import { createProjectFromScratch, createProjectFromSnapshot } from '../redux/dispatchActions'; +import { + createProjectFromScratch, + createProjectFromSnapshot, + createProjectDiscoursePost +} from '../redux/dispatchActions'; +import { isDiscourseAvailable, getExistingPost, isDiscourseUserAvailable } from '../../../utils/discourse'; +import { RegisterNotice } from '../../discourse/RegisterNotice'; const useStyles = makeStyles(theme => ({ body: { @@ -49,8 +56,12 @@ const useStyles = makeStyles(theme => ({ export const ProjectModal = memo(({}) => { const classes = useStyles(); const [state, setState] = useState(); + let [createDiscourse, setCreateDiscourse] = useState(true); let history = useHistory(); + const dicourseUserAvailable = isDiscourseUserAvailable(); + createDiscourse &= dicourseUserAvailable; + const dispatch = useDispatch(); const isProjectModalOpen = useSelector(state => state.projectReducers.isProjectModalOpen); const isProjectModalLoading = useSelector(state => state.projectReducers.isProjectModalLoading); @@ -62,6 +73,29 @@ export const ProjectModal = memo(({}) => { ); const targetList = useSelector(state => state.apiReducers.target_id_list); + const findTargetNameForId = id => { + return targetList.find(target => target.id === id); + }; + + const discourseAvailable = isDiscourseAvailable(); + + const validateProjectName = async value => { + let error; + // console.log(`Project title validating and value is: ${value}`); + + if (!value) { + error = 'Required!'; + } else if (createDiscourse) { + const response = await getExistingPost(value); + // console.log(response); + if (response.data['Post url']) { + error = 'Already exists!'; + } + } + + return error; + }; + const handleCloseModal = () => { if (isProjectModalLoading === false) { dispatch(setProjectModalOpen(false)); @@ -101,6 +135,8 @@ export const ProjectModal = memo(({}) => { } if (!values.description) { errors.description = 'Required!'; + } else if (values.description.length < 20) { + errors.description = 'Description must be at least 20 characters long!'; } if (values.type === ProjectCreationType.NEW && values.targetId === '') { errors.targetId = 'Required!'; @@ -131,6 +167,14 @@ export const ProjectModal = memo(({}) => { parentSnapshotId: values.parentSnapshotId }) ) + .then(() => { + if (createDiscourse) { + const target = findTargetNameForId(values.targetId); + if (target) { + dispatch(createProjectDiscoursePost(values.title, target.title, values.description, tags)); + } + } + }) .catch(error => { setState(() => { throw error; @@ -149,6 +193,14 @@ export const ProjectModal = memo(({}) => { history }) ) + .then(() => { + if (createDiscourse) { + const target = findTargetNameForId(values.targetId); + if (target) { + dispatch(createProjectDiscoursePost(values.title, target.title, values.description, tags)); + } + } + }) .catch(error => { setState(() => { throw error; @@ -160,7 +212,7 @@ export const ProjectModal = memo(({}) => { } }} > - {({ submitForm, errors, values }) => ( + {({ submitForm, isSubmitting, errors, values }) => (
@@ -198,6 +250,7 @@ export const ProjectModal = memo(({}) => { label="Title" required disabled={isProjectModalLoading} + validate={validateProjectName} /> } /> @@ -323,15 +376,42 @@ export const ProjectModal = memo(({}) => { } /> + + } + field={ + { + setCreateDiscourse(!createDiscourse); + }} + name="createDisTopic" + disabled={ + !discourseAvailable || !dicourseUserAvailable || isProjectModalLoading || isSubmitting + } + /> + } + label="Create Discourse topic" + /> + } + /> + + {!dicourseUserAvailable && ( + + + + )} - - diff --git a/js/components/projects/redux/actions.js b/js/components/projects/redux/actions.js index 8b0c0e7aa..25a168999 100644 --- a/js/components/projects/redux/actions.js +++ b/js/components/projects/redux/actions.js @@ -79,3 +79,13 @@ export const setForceProjectCreated = isCreated => ({ type: constants.SET_FORCE_PROJECT_CREATED, payload: isCreated }); + +export const setProjectDiscourseLinks = links => ({ + type: constants.SET_PROJECT_DISCOURSE_LINKS, + payload: links +}); + +export const setCurrentProjectDiscourseLink = link => ({ + type: constants.SET_CURRENT_PROJECT_DISCOURSE_LINK, + payload: link +}); diff --git a/js/components/projects/redux/constants.js b/js/components/projects/redux/constants.js index 27ecc3e11..d51dc56f1 100644 --- a/js/components/projects/redux/constants.js +++ b/js/components/projects/redux/constants.js @@ -21,7 +21,9 @@ export const constants = { RESET_LOADED_SNAPSHOTS: prefix + 'RESET_LOADED_SNAPSHOTS', SET_FORCE_CREATE_PROJECT: prefix + 'SET_FORCE_CREATE_PROJECT', - SET_FORCE_PROJECT_CREATED: prefix + 'SET_FORCE_PROJECT_CREATED' + SET_FORCE_PROJECT_CREATED: prefix + 'SET_FORCE_PROJECT_CREATED', + SET_PROJECT_DISCOURSE_LINKS: prefix + 'SET_PROJECT_DISCOURSE_LINKS', + SET_CURRENT_PROJECT_DISCOURSE_LINK: prefix + 'SET_CURRENT_PROJECT_DISCOURSE_LINK' }; export const ProjectCreationType = { diff --git a/js/components/projects/redux/dispatchActions.js b/js/components/projects/redux/dispatchActions.js index 1556023be..7c049933a 100644 --- a/js/components/projects/redux/dispatchActions.js +++ b/js/components/projects/redux/dispatchActions.js @@ -11,7 +11,8 @@ import { setCurrentProject, setForceCreateProject, setForceProjectCreated, - setIsLoadingListOfProjects + setIsLoadingListOfProjects, + setCurrentProjectDiscourseLink } from './actions'; import { api, METHOD } from '../../../utils/api'; import { base_url, URLS } from '../../routes/constants'; @@ -21,8 +22,11 @@ import { SnapshotType } from './constants'; import { DJANGO_CONTEXT } from '../../../utils/djangoContext'; import { sendInitTrackingActionByProjectId } from '../../../reducers/tracking/dispatchActions'; import { resetTrackingState } from '../../../reducers/tracking/actions'; +import { createProjectPost } from '../../../utils/discourse'; +import { setOpenDiscourseErrorModal } from '../../../reducers/api/actions'; import moment from 'moment'; +import { resetNglTrackingState } from '../../../reducers/nglTracking/dispatchActions'; export const assignSnapshotToProject = ({ projectID, snapshotID, ...rest }) => (dispatch, getState) => { dispatch(resetCurrentSnapshot()); @@ -266,6 +270,7 @@ export const createProjectFromSnapshotDialog = data => dispatch => { .then(response => { const projectID = response.data.id; dispatch(setCurrentProjectProperty('projectID', projectID)); + dispatch(setCurrentProjectProperty('title', response.data.title)); }) .finally(() => { dispatch(setForceCreateProject(false)); @@ -274,6 +279,17 @@ export const createProjectFromSnapshotDialog = data => dispatch => { }); }; +export const createProjectDiscoursePost = (projectName, targetName, msg, tags) => (dispatch, getState) => { + return createProjectPost(projectName, targetName, msg, tags) + .then(response => { + dispatch(setCurrentProjectDiscourseLink(response.data['Post url'])); + }) + .catch(err => { + console.log(err); + dispatch(setOpenDiscourseErrorModal(true)); + }); +}; + export const createProject = ({ title, description, target, author, tags }) => dispatch => { dispatch(setProjectModalIsLoading(true)); return api({ @@ -323,6 +339,7 @@ export const createProjectFromSnapshot = ({ title, description, author, tags, hi dispatch(setProjectModalIsLoading(true)); dispatch(resetTrackingState()); + dispatch(resetNglTrackingState()); return dispatch( createProject({ title, @@ -369,6 +386,7 @@ export const createProjectFromScratch = ({ title, description, target, author, t ) => { dispatch(setProjectModalIsLoading(true)); dispatch(resetTrackingState()); + dispatch(resetNglTrackingState()); return api({ url: `${base_url}/api/session-projects/`, method: METHOD.POST, diff --git a/js/components/projects/redux/reducer.js b/js/components/projects/redux/reducer.js index 1740ce2e7..9696eeef3 100644 --- a/js/components/projects/redux/reducer.js +++ b/js/components/projects/redux/reducer.js @@ -21,6 +21,7 @@ export const INITIAL_STATE = { tags: [], type: null }, + currentProjectDiscourseLink: null, isLoadingCurrentSnapshot: false, currentSnapshot: initCurrentSnapshot, isProjectModalOpen: false, @@ -31,7 +32,8 @@ export const INITIAL_STATE = { currentSnapshotTree: null, currentSnapshotList: null, forceCreateProject: false, - isForceProjectCreated: false + isForceProjectCreated: false, + projectDiscourseLinks: null }; export const projectReducers = (state = INITIAL_STATE, action = {}) => { @@ -41,6 +43,9 @@ export const projectReducers = (state = INITIAL_STATE, action = {}) => { currentProject: action.payload }); + case constants.SET_PROJECT_DISCOURSE_LINKS: + return { ...state, projectDiscourseLinks: { ...action.payload } }; + case constants.SET_CURRENT_PROJECT_PROPERTY: const currProject = JSON.parse(JSON.stringify(state.currentProject)); currProject[action.payload.key] = action.payload.value; @@ -91,6 +96,9 @@ export const projectReducers = (state = INITIAL_STATE, action = {}) => { case constants.SET_FORCE_PROJECT_CREATED: return Object.assign({}, state, { isForceProjectCreated: action.payload }); + case constants.SET_CURRENT_PROJECT_DISCOURSE_LINK: + return Object.assign({}, state, { currentProjectDiscourseLink: action.payload }); + case constants.RESET_PROJECTS_REDUCER: return Object.assign({}, INITIAL_STATE); diff --git a/js/components/snapshot/modals/newSnapshotForm.js b/js/components/snapshot/modals/newSnapshotForm.js index b0c9c43ba..38119db20 100644 --- a/js/components/snapshot/modals/newSnapshotForm.js +++ b/js/components/snapshot/modals/newSnapshotForm.js @@ -4,12 +4,14 @@ import { useDispatch, useSelector } from 'react-redux'; import { DJANGO_CONTEXT } from '../../../utils/djangoContext'; import { Form, Formik, Field } from 'formik'; import { InputFieldAvatar } from '../../projects/projectModal/inputFieldAvatar'; -import { Description, Title } from '@material-ui/icons'; +import { Description, Title, QuestionAnswer, FindReplace } from '@material-ui/icons'; import { TextField } from 'formik-material-ui'; import { Button } from '../../common/Inputs/Button'; import { SnapshotType } from '../../projects/redux/constants'; import { createNewSnapshot } from '../redux/dispatchActions'; import { NglContext } from '../../nglView/nglProvider'; +import { isDiscourseAvailable, isDiscourseUserAvailable } from '../../../utils/discourse'; +import { RegisterNotice } from '../../discourse/RegisterNotice'; import moment from 'moment'; @@ -40,6 +42,11 @@ export const NewSnapshotForm = memo(({ handleCloseModal }) => { const dispatch = useDispatch(); const { nglViewList } = useContext(NglContext); const [overwriteSnapshot, setoverwriteSnapshot] = useState(false); + let [createDiscourse, setCreateDiscourse] = useState(true); + + const discourseAvailable = isDiscourseAvailable(); + const dicourseUserAvailable = isDiscourseUserAvailable(); + createDiscourse &= dicourseUserAvailable; const currentSnapshot = useSelector(state => state.projectReducers.currentSnapshot); const currentProject = useSelector(state => state.projectReducers.currentProject); @@ -95,7 +102,8 @@ export const NewSnapshotForm = memo(({ handleCloseModal }) => { parent, session_project, nglViewList, - overwriteSnapshot + overwriteSnapshot, + createDiscourse }) ).catch(error => { setState(() => { @@ -139,23 +147,48 @@ export const NewSnapshotForm = memo(({ handleCloseModal }) => { {currentSnapshotId && ( - { - toggleoverwriteSnapshot(); - }} + } + field={ + { + toggleoverwriteSnapshot(); + }} + disabled={isLoadingSnapshotDialog || isSubmitting} + name="overwrite" + /> + } + label="Overwrite current snapshot" /> } - label="Overwrite current snapshot" - labelPlacement="end" - className={classes.checkbox} - disabled={isLoadingSnapshotDialog || isSubmitting} /> )} + + } + field={ + setCreateDiscourse(!createDiscourse)} + disabled={!discourseAvailable || isSubmitting || !dicourseUserAvailable} + name="createDisTopic" + /> + } + label="Create Discourse post" + /> + } + /> + + {!dicourseUserAvailable && ( + + + + )} diff --git a/js/components/snapshot/redux/dispatchActions.js b/js/components/snapshot/redux/dispatchActions.js index ff94a798d..a810bef45 100644 --- a/js/components/snapshot/redux/dispatchActions.js +++ b/js/components/snapshot/redux/dispatchActions.js @@ -40,6 +40,7 @@ import { } from '../../../reducers/tracking/dispatchActions'; import { captureScreenOfSnapshot } from '../../userFeedback/browserApi'; import { setCurrentProject } from '../../projects/redux/actions'; +import { createProjectPost } from '../../../utils/discourse'; export const getListOfSnapshots = () => (dispatch, getState) => { const userID = DJANGO_CONTEXT['pk'] || null; @@ -199,7 +200,8 @@ export const createNewSnapshot = ({ parent, session_project, nglViewList, - overwriteSnapshot + overwriteSnapshot, + createDiscourse = false }) => (dispatch, getState) => { const state = getState(); const selectedSnapshotToSwitch = state.snapshotReducers.selectedSnapshotToSwitch; @@ -255,10 +257,14 @@ export const createNewSnapshot = ({ if (res.data.id && session_project) { let snapshot = { id: res.data.id, title: title }; let project = { projectID: session_project, authorID: author }; + console.log('created snapshot id: ' + res.data.id); Promise.resolve(dispatch(saveCurrentActionsList(snapshot, project, nglViewList))).then(() => { if (disableRedirect === false) { if (selectedSnapshotToSwitch != null) { + if (createDiscourse) { + dispatch(createSnapshotDiscoursePost(res.data.id)); + } window.location.replace(`${URLS.projects}${session_project}/${selectedSnapshotToSwitch}`); } else { // A hacky way of changing the URL without triggering react-router @@ -274,45 +280,55 @@ export const createNewSnapshot = ({ const response = await api({ url: `${base_url}/api/snapshots/?session_project=${session_project}` }); + const length = response.data.results.length; if (length === 0) { dispatch(resetCurrentSnapshot()); - } else if (response.data.results[length - 1] !== undefined) { - // If the tree fails to load, bail out first without modifying the store - dispatch(loadSnapshotTree(projectResponse.data.id)); - // Pick the latest snapshot which should be the last one - dispatch( - setCurrentSnapshot({ - id: response.data.results[length - 1].id, - type: response.data.results[length - 1].type, - title: response.data.results[length - 1].title, - author: response.data.results[length - 1].author, - description: response.data.results[length - 1].description, - created: response.data.results[length - 1].created, - children: response.data.results[length - 1].children, - parent: response.data.results[length - 1].parent, - data: '[]' - }) - ); - dispatch( - setCurrentProject({ - projectID: projectResponse.data.id, - authorID: (projectResponse.data.author && projectResponse.data.author.id) || null, - title: projectResponse.data.title, - description: projectResponse.data.description, - targetID: projectResponse.data.target.id, - tags: JSON.parse(projectResponse.data.tags) - }) - ); - dispatch(setOpenSnapshotSavingDialog(false)); - dispatch(setIsLoadingSnapshotDialog(false)); - dispatch(setSnapshotJustSaved(projectResponse.data.id)); - dispatch(setDialogCurrentStep()); + } else { + const createdSnapshot = + response.data.results && response.data.results.find(r => r.id === res.data.id); + console.log('new snapshot id: ' + JSON.stringify(createdSnapshot?.id)); + + if (createdSnapshot !== undefined && createdSnapshot !== null) { + // If the tree fails to load, bail out first without modifying the store + await dispatch(loadSnapshotTree(projectResponse.data.id)); + await dispatch( + setCurrentSnapshot({ + id: createdSnapshot.id, + type: createdSnapshot.type, + title: createdSnapshot.title, + author: createdSnapshot.author, + description: createdSnapshot.description, + created: createdSnapshot.created, + children: createdSnapshot.children, + parent: createdSnapshot.parent, + data: '[]' + }) + ); + await dispatch( + setCurrentProject({ + projectID: projectResponse.data.id, + authorID: (projectResponse.data.author && projectResponse.data.author.id) || null, + title: projectResponse.data.title, + description: projectResponse.data.description, + targetID: projectResponse.data.target.id, + tags: JSON.parse(projectResponse.data.tags) + }) + ); + if (createDiscourse) { + dispatch(createSnapshotDiscoursePost()); + } + dispatch(setOpenSnapshotSavingDialog(false)); + dispatch(setIsLoadingSnapshotDialog(false)); + dispatch(setSnapshotJustSaved(projectResponse.data.id)); + dispatch(setDialogCurrentStep()); + } } }) .catch(error => { dispatch(resetCurrentSnapshot()); dispatch(setIsLoadingSnapshotDialog(false)); + console.log(`Error while saving snapshot: ${error}`); }); } } else { @@ -334,6 +350,16 @@ export const createNewSnapshot = ({ } }; +export const createSnapshotDiscoursePost = (snapshotId = undefined) => (dispatch, getState) => { + const state = getState(); + const currentProject = state.projectReducers.currentProject; + const currentSnapshotId = snapshotId === undefined ? state.projectReducers.currentSnapshot.id : snapshotId; + const targetName = state.apiReducers.target_on_name; + const url = `${base_url}${URLS.projects}${currentProject.projectID}/${currentSnapshotId}`; + const msg = `${url}`; + return createProjectPost(currentProject.title, targetName, msg, []); +}; + export const activateSnapshotDialog = (loggedInUserID = undefined, finallyShareSnapshot = false) => ( dispatch, getState @@ -423,7 +449,7 @@ export const createNewSnapshotWithoutStateModification = ({ }); }; -export const saveAndShareSnapshot = ( nglViewList, showDialog = true ) => async (dispatch, getState) => { +export const saveAndShareSnapshot = (nglViewList, showDialog = true) => async (dispatch, getState) => { const state = getState(); const targetId = state.apiReducers.target_on; const loggedInUserID = DJANGO_CONTEXT['pk']; @@ -467,7 +493,7 @@ export const saveAndShareSnapshot = ( nglViewList, showDialog = true ) => async nglViewList }) ); - + if (showDialog) { dispatch(setIsLoadingSnapshotDialog(false)); } diff --git a/js/components/target/redux/actions.js b/js/components/target/redux/actions.js index d8302f51c..9881e4b36 100644 --- a/js/components/target/redux/actions.js +++ b/js/components/target/redux/actions.js @@ -3,3 +3,10 @@ import { constants } from './constatnts'; export const setOldUrl = url => ({ type: constants.SET_OLD_URL, payload: url }); export const setIsTargetLoading = isLoading => ({ type: constants.SET_TARGET_IS_LOADING, payload: isLoading }); + +export const setTargetDiscourseLinks = links => ({ + type: constants.SET_TARGET_DISCOURSE_LINKS, + payload: links +}); + +export const setCurrentTargetLink = link => ({ type: constants.SET_CURRENT_TARGET_LINK, payload: link }); diff --git a/js/components/target/redux/constatnts.js b/js/components/target/redux/constatnts.js index 1a69c8ec5..42ecb4eb3 100644 --- a/js/components/target/redux/constatnts.js +++ b/js/components/target/redux/constatnts.js @@ -2,5 +2,7 @@ const prefix = 'TARGET_'; export const constants = { SET_OLD_URL: prefix + 'SET_OLD_URL', - SET_TARGET_IS_LOADING: prefix + 'SET_TARGET_IS_LOADING' + SET_TARGET_IS_LOADING: prefix + 'SET_TARGET_IS_LOADING', + SET_TARGET_DISCOURSE_LINKS: prefix + 'SET_TARGET_DISCOURSE_LINKS', + SET_CURRENT_TARGET_LINK: prefix + 'SET_CURRENT_TARGET_LINK' }; diff --git a/js/components/target/redux/reducer.js b/js/components/target/redux/reducer.js index 5d6d856a2..448a44338 100644 --- a/js/components/target/redux/reducer.js +++ b/js/components/target/redux/reducer.js @@ -2,7 +2,9 @@ import { constants } from './constatnts'; export const INITIAL_STATE = { oldUrl: '', - isTargetLoading: false + isTargetLoading: false, + targetDiscourseLinks: null, + currentTargetLink: null }; export const targetReducers = (state = INITIAL_STATE, action = {}) => { @@ -17,6 +19,12 @@ export const targetReducers = (state = INITIAL_STATE, action = {}) => { isTargetLoading: action.payload }); + case constants.SET_TARGET_DISCOURSE_LINKS: + return { ...state, targetDiscourseLinks: { ...action.payload } }; + + case constants.SET_CURRENT_TARGET_LINK: + return { ...state, currentTargetLink: action.payload }; + default: return state; } diff --git a/js/components/target/targetList.js b/js/components/target/targetList.js index ca1eb49cb..534708d09 100644 --- a/js/components/target/targetList.js +++ b/js/components/target/targetList.js @@ -3,13 +3,17 @@ */ import React, { memo } from 'react'; -import { connect, useSelector } from 'react-redux'; -import { ListItemText, ListItemSecondaryAction } from '@material-ui/core'; +import { useSelector, useDispatch } from 'react-redux'; +import { ListItemText, ListItemSecondaryAction, Grid, IconButton, Tooltip } from '@material-ui/core'; import { List, ListItem, Panel } from '../common'; import { Link } from 'react-router-dom'; import { URLS } from '../routes/constants'; +import { isDiscourseAvailable, generateDiscourseTargetURL } from '../../utils/discourse'; +import { setOpenDiscourseErrorModal } from '../../reducers/api/actions'; +import { Chat } from '@material-ui/icons'; export const TargetList = memo(() => { + const dispatch = useDispatch(); const isTargetLoading = useSelector(state => state.targetReducers.isTargetLoading); const target_id_list = useSelector(state => state.apiReducers.target_id_list); @@ -17,19 +21,43 @@ export const TargetList = memo(() => { const preview = URLS.target + data.title; const sgcUrl = 'https://thesgc.org/sites/default/files/XChem/' + data.title + '/html/index.html'; const sgcUploaded = ['BRD1A', 'DCLRE1AA', 'FALZA', 'FAM83BA', 'HAO1A', 'NUDT4A', 'NUDT5A', 'NUDT7A', 'PARP14A']; + const discourseAvailable = isDiscourseAvailable(); + // const [discourseUrl, setDiscourseUrl] = useState(); return ( - {sgcUploaded.includes(data.title) && ( - - - Open SGC summary - - - )} + + + {sgcUploaded.includes(data.title) && ( + + Open SGC summary + + )} + {discourseAvailable && ( + + { + generateDiscourseTargetURL(data.title) + .then(response => { + const link = response.data['Post url']; + window.open(link, '_blank'); + }) + .catch(err => { + console.log(err); + dispatch(setOpenDiscourseErrorModal(true)); + }); + }} + > + + + + )} + + ); }; diff --git a/js/index.js b/js/index.js index 809d28816..da00629c0 100644 --- a/js/index.js +++ b/js/index.js @@ -11,6 +11,7 @@ import { rootReducer } from './reducers/rootReducer'; import { saveStore } from './components/helpers/globalStore'; import thunkMiddleware from 'redux-thunk'; import trackingMiddleware from './reducers/tracking/trackingMiddleware'; +import nglTrackingMiddleware from './reducers/nglTracking/nglTrackingMiddleware'; import { composeWithDevTools } from 'redux-devtools-extension'; require('react-hot-loader/patch'); @@ -18,7 +19,8 @@ require('react-hot-loader/patch'); const middlewareEnhancer = applyMiddleware( //loggerMiddleware, thunkMiddleware, - trackingMiddleware + trackingMiddleware, + nglTrackingMiddleware ); const enhancers = [middlewareEnhancer]; const composedEnhancers = composeWithDevTools(...enhancers); diff --git a/js/reducers/api/actions.js b/js/reducers/api/actions.js index e68f3d5f8..7892da68c 100644 --- a/js/reducers/api/actions.js +++ b/js/reducers/api/actions.js @@ -3,6 +3,10 @@ */ import { constants } from './constants'; +export const setOpenDiscourseErrorModal = open => { + return { type: constants.SET_OPEN_DISCOURSE_ERROR_MODAL, payload: open }; +}; + export const setTargetIdList = function(input_json) { return { type: constants.SET_TARGET_ID_LIST, diff --git a/js/reducers/api/apiReducers.js b/js/reducers/api/apiReducers.js index fc390916b..faade3a5b 100644 --- a/js/reducers/api/apiReducers.js +++ b/js/reducers/api/apiReducers.js @@ -38,7 +38,8 @@ export const INITIAL_STATE = { sessionTitle: undefined, user_id: undefined, direct_access: {}, - direct_access_processed: false + direct_access_processed: false, + open_discourse_error_modal: false }; export const RESET_TARGET_STATE = { @@ -70,12 +71,16 @@ export const RESET_TARGET_STATE = { sessionIdList: [], sessionTitle: undefined, user_id: undefined, - direct_access: {} + direct_access: {}, + open_discourse_error_modal: false // direct_access_processed: false }; export default function apiReducers(state = INITIAL_STATE, action = {}) { switch (action.type) { + case constants.SET_OPEN_DISCOURSE_ERROR_MODAL: + return Object.assign({}, state, { open_discourse_error_modal: action.payload }); + case constants.SET_TARGET_ID_LIST: return Object.assign({}, state, { target_id_list: action.target_id_list diff --git a/js/reducers/api/constants.js b/js/reducers/api/constants.js index d646b877a..0a62f76cc 100644 --- a/js/reducers/api/constants.js +++ b/js/reducers/api/constants.js @@ -30,5 +30,6 @@ export const constants = { RELOAD_API_STATE: prefix + 'RELOAD_API_STATE', RESET_TARGET_STATE: prefix + 'RESET_TARGET_STATE', SET_DIRECT_ACCESS: prefix + 'SET_DIRECT_ACCESS', - SET_DIRECT_ACCESS_PROCESSED: prefix + 'SET_DIRECT_ACCESS_PROCESSED' + SET_DIRECT_ACCESS_PROCESSED: prefix + 'SET_DIRECT_ACCESS_PROCESSED', + SET_OPEN_DISCOURSE_ERROR_MODAL: prefix + 'SET_OPEN_DISCOURSE_ERROR_MODAL' }; diff --git a/js/reducers/ngl/actions.js b/js/reducers/ngl/actions.js index 233dabb06..57adbf4d5 100644 --- a/js/reducers/ngl/actions.js +++ b/js/reducers/ngl/actions.js @@ -139,6 +139,8 @@ export const setNglFogFarAction = (newValue, oldValue) => { export const setNglOrientation = (orientation, div_id) => ({ type: CONSTANTS.SET_ORIENTATION, orientation, div_id }); +export const setNglOrientationByInteraction = (orientation, oldOrientation, div_id) => ({ type: CONSTANTS.SET_ORIENTATION_BY_INTERACTION, orientation, oldOrientation, div_id }); + export const setProteinLoadingState = hasLoaded => ({ type: CONSTANTS.SET_PROTEINS_HAS_LOADED, payload: hasLoaded }); export const setNglStateFromCurrentSnapshot = snapshot => ({ diff --git a/js/reducers/ngl/constants.js b/js/reducers/ngl/constants.js index f35064079..0269e44a5 100644 --- a/js/reducers/ngl/constants.js +++ b/js/reducers/ngl/constants.js @@ -13,6 +13,7 @@ export const CONSTANTS = { SET_NGL_VIEW_PARAMS: prefix + 'SET_NGL_VIEW_PARAMS', SET_ORIENTATION: prefix + 'SET_ORIENTATION', + SET_ORIENTATION_BY_INTERACTION: prefix + 'SET_ORIENTATION_BY_INTERACTION', SET_NGL_STATE_FROM_CURRENT_SNAPSHOT: prefix + 'SET_NGL_STATE_FROM_CURRENT_SNAPSHOT', REMOVE_ALL_NGL_COMPONENTS: prefix + 'REMOVE_ALL_NGL_COMPONENTS', diff --git a/js/reducers/ngl/dispatchActions.js b/js/reducers/ngl/dispatchActions.js index f6677442c..9da1aeb66 100644 --- a/js/reducers/ngl/dispatchActions.js +++ b/js/reducers/ngl/dispatchActions.js @@ -13,7 +13,8 @@ import { setNglClipFarAction, setNglClipDistAction, setNglFogNearAction, - setNglFogFarAction + setNglFogFarAction, + setNglOrientationByInteraction } from './actions'; import { isEmpty, isEqual } from 'lodash'; import { createRepresentationsArray } from '../../components/nglView/generatingObjects'; @@ -123,6 +124,19 @@ export const setOrientation = (div_id, orientation) => (dispatch, getState) => { } }; +export const setOrientationByInteraction = (div_id, orientation) => (dispatch, getState) => { + const nglOrientations = getState().nglReducers.nglOrientations; + + if ( + orientation && + ((nglOrientations && nglOrientations[div_id] && !isEqual(orientation.elements, nglOrientations[div_id].elements)) || + isEmpty(nglOrientations) || + (nglOrientations && nglOrientations[div_id] === undefined)) + ) { + dispatch(setNglOrientationByInteraction(orientation, nglOrientations[div_id], div_id)); + } +}; + export const centerOnLigandByMoleculeID = (stage, moleculeID) => (dispatch, getState) => { if (moleculeID && stage) { const state = getState(); @@ -210,4 +224,10 @@ export const setNglFogNear = (newValue, oldValue, major) => (dispatch, getState) export const setNglFogFar = (newValue, oldValue, major) => (dispatch, getState) => { dispatch(setNglViewParams(NGL_PARAMS.fogFar, newValue, major, VIEWS.MAJOR_VIEW)); dispatch(setNglFogFarAction(newValue, oldValue)); -}; \ No newline at end of file +}; + +export const restoreNglOrientation = (orientation, oldOrientation, div_id, stages) => (dispatch, getState) => { + const view = stages.find(view => view.id === div_id); + view.stage.viewerControls.orient(orientation); + dispatch(setNglOrientationByInteraction(orientation, oldOrientation, div_id)); +} \ No newline at end of file diff --git a/js/reducers/ngl/nglReducers.js b/js/reducers/ngl/nglReducers.js index b161112d2..efffb7f48 100644 --- a/js/reducers/ngl/nglReducers.js +++ b/js/reducers/ngl/nglReducers.js @@ -106,9 +106,19 @@ export default function nglReducers(state = INITIAL_STATE, action = {}) { }); case CONSTANTS.SET_ORIENTATION: - const div_id = action.div_id; - const orientation = action.orientation; - const toSetDiv = JSON.parse(JSON.stringify(state.nglOrientations)); + let div_id = action.div_id; + let orientation = action.orientation; + let toSetDiv = JSON.parse(JSON.stringify(state.nglOrientations)); + toSetDiv[div_id] = orientation; + + return Object.assign({}, state, { + nglOrientations: toSetDiv + }); + + case CONSTANTS.SET_ORIENTATION_BY_INTERACTION: + div_id = action.div_id; + orientation = action.orientation; + toSetDiv = JSON.parse(JSON.stringify(state.nglOrientations)); toSetDiv[div_id] = orientation; return Object.assign({}, state, { diff --git a/js/reducers/nglTracking/actions.js b/js/reducers/nglTracking/actions.js new file mode 100644 index 000000000..be6c9f05e --- /dev/null +++ b/js/reducers/nglTracking/actions.js @@ -0,0 +1,150 @@ +import { constants, undoConstants } from './constants'; + +export const setActionsList = function(track_actions_list) { + return { + type: constants.SET_ACTIONS_LIST, + track_actions_list: track_actions_list + }; +}; + +export const appendToActionList = function(track_action) { + return { + type: constants.APPEND_ACTIONS_LIST, + track_action: track_action + }; +}; + +export const appendToUndoRedoActionList = function(track_action) { + return { + type: constants.APPEND_UNDO_REDO_ACTIONS_LIST, + track_action: track_action + }; +}; + +export const setUndoRedoActionList = (undo_redo_actions_list) => { + return { + type: constants.SET_UNDO_REDO_ACTIONS_LIST, + undo_redo_actions_list: undo_redo_actions_list + }; +} + +export const setCurrentActionsList = function(current_actions_list) { + return { + type: constants.SET_CURRENT_ACTIONS_LIST, + current_actions_list: current_actions_list + }; +}; + +export const setIsTrackingMoleculesRestoring = function(isTrackingMoleculesRestoring) { + return { + type: constants.SET_IS_TRACKING_MOLECULES_RESTORING, + isTrackingMoleculesRestoring: isTrackingMoleculesRestoring + }; +}; + +export const setIsTrackingCompoundsRestoring = function(isTrackingCompoundsRestoring) { + return { + type: constants.SET_IS_TRACKING_COMPOUNDS_RESTORING, + isTrackingCompoundsRestoring: isTrackingCompoundsRestoring + }; +}; + +export const setIsUndoRedoAction = function(isUndoRedoAction) { + return { + type: constants.SET_IS_UNDO_REDO_ACTION, + isUndoRedoAction: isUndoRedoAction + }; +}; + +export const setIsActionsSending = function(isActionsSending) { + return { + type: constants.SET_IS_ACTIONS_SENDING, + isActionsSending: isActionsSending + }; +}; + +export const setIsActionsLoading = function(isActionsLoading) { + return { + type: constants.SET_IS_ACTIONS_LOADING, + isActionsLoading: isActionsLoading + }; +}; + +export const setSendActionsList = function(track_actions_list) { + return { + type: constants.SET_SEND_ACTIONS_LIST, + send_actions_list: track_actions_list + }; +}; + +export const appendToSendActionList = function(track_action) { + return { + type: constants.APPEND_SEND_ACTIONS_LIST, + track_action: track_action + }; +}; + +export const setProjectActionList = function(project_actions_list) { + return { + type: constants.SET_PROJECT_ACTIONS_LIST, + project_actions_list: project_actions_list + }; +}; + +export const setSnapshotImageActionList = function(snapshotActionImageList) { + return { + type: constants.SET_SNAPSOT_IMAGE_ACTIONS_LIST, + snapshotActionImageList: snapshotActionImageList + }; +}; + +export const setIsActionsSaving = function(isActionSaving) { + return { + type: constants.SET_IS_ACTIONS_SAVING, + isActionSaving: isActionSaving + }; +}; + +export const setIsActionsRestoring = function(isActionRestoring, isActionRestored) { + return { + type: constants.SET_IS_ACTIONS_RESTORING, + isActionRestoring: isActionRestoring, + isActionRestored: isActionRestored + }; +}; + +export const setIsActionTracking = function(isActionTracking) { + return { + type: constants.SET_IS_ACTION_TRACKING, + isActionTracking: isActionTracking + }; +}; + +export const resetTrackingState = function() { + return { + type: constants.RESET_TRACKING_STATE + }; +}; + +export const setTrackingImageSource = imageSource => ({ + type: constants.SET_TRACKING_IMAGE_SOURCE, + payload: imageSource +}); + +export const nglTrackingUndo = function() { + return { + type: undoConstants.NGL_TRACKING_UNDO + }; +} + +export const nglTrackingRedo = function() { + return { + type: undoConstants.NGL_TRACKING_REDO + }; +} + +export const nglClearHistory = function() { + return { + type: undoConstants.NGL_CLEAR_HISTORY + } +} \ No newline at end of file diff --git a/js/reducers/nglTracking/constants.js b/js/reducers/nglTracking/constants.js new file mode 100644 index 000000000..e1bd6318d --- /dev/null +++ b/js/reducers/nglTracking/constants.js @@ -0,0 +1,115 @@ +const prefix = 'REDUCERS_NGL_TRACKING_'; + +export const constants = { + SET_ACTIONS_LIST: prefix + 'SET_ACTIONS_LIST', + APPEND_ACTIONS_LIST: prefix + 'APPEND_ACTIONS_LIST', + SET_CURRENT_ACTIONS_LIST: prefix + 'SET_CURRENT_ACTIONS_LIST', + SET_IS_TRACKING_COMPOUNDS_RESTORING: prefix + 'SET_IS_TRACKING_COMPOUNDS_RESTORING', + SET_IS_TRACKING_MOLECULES_RESTORING: prefix + 'SET_IS_TRACKING_MOLECULES_RESTORING', + SET_IS_UNDO_REDO_ACTION: prefix + 'SET_IS_UNDO_REDO_ACTION', + SET_SEND_ACTIONS_LIST: prefix + 'SET_SEND_ACTIONS_LIST', + APPEND_SEND_ACTIONS_LIST: prefix + 'APPEND_SEND_ACTIONS_LIST', + SET_IS_ACTIONS_SENDING: prefix + 'SET_IS_ACTIONS_SENDING', + SET_IS_ACTIONS_LOADING: prefix + 'SET_IS_ACTIONS_LOADING', + 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', + SET_IS_ACTION_TRACKING: prefix + 'SET_IS_ACTION_TRACKING', + RESET_TRACKING_STATE: prefix + 'RESET_TRACKING_STATE', + SET_TRACKING_IMAGE_SOURCE: prefix + 'SET_TRACKING_IMAGE_SOURCE', + SET_SNAPSOT_IMAGE_ACTIONS_LIST: prefix + 'SET_SNAPSOT_IMAGE_ACTIONS_LIST', + APPEND_UNDO_REDO_ACTIONS_LIST: prefix + 'APPEND_UNDO_REDO_ACTIONS_LIST', + SET_UNDO_REDO_ACTIONS_LIST: prefix + 'SET_UNDO_REDO_ACTIONS_LIST' +}; + +export const actionType = { + TARGET_LOADED: 'TARGET_LOADED', + SITE_TURNED_ON: 'SITE_TURNED_ON', + SITE_TURNED_OFF: 'SITE_TURNED_OFF', + LIGAND_TURNED_ON: 'LIGAND_TURNED_ON', + LIGAND_TURNED_OFF: 'LIGAND_TURNED_OFF', + SIDECHAINS_TURNED_ON: 'SIDECHAINS_TURNED_ON', + SIDECHAINS_TURNED_OFF: 'SIDECHAINS_TURNED_OFF', + INTERACTIONS_TURNED_ON: 'INTERACTIONS_TURNED_ON', + INTERACTIONS_TURNED_OFF: 'INTERACTIONS_TURNED_OFF', + SURFACE_TURNED_ON: 'SURFACE_TURNED_ON', + SURFACE_TURNED_OFF: 'SURFACE_TURNED_OFF', + VECTORS_TURNED_ON: 'VECTORS_TURNED_ON', + VECTORS_TURNED_OFF: 'VECTORS_TURNED_OFF', + VECTOR_SELECTED: 'VECTOR_SELECTED', + VECTOR_DESELECTED: 'VECTOR_DESELECTED', + MOLECULE_ADDED_TO_SHOPPING_CART: 'MOLECULE_ADDED_TO_SHOPPING_CART', + MOLECULE_REMOVED_FROM_SHOPPING_CART: 'MOLECULE_REMOVED_FROM_SHOPPING_CART', + COMPOUND_SELECTED: 'COMPOUND_SELECTED', + COMPOUND_DESELECTED: 'COMPOUND_DESELECTED', + REPRESENTATION_UPDATED: 'REPRESENTATION_UPDATED', + REPRESENTATION_ADDED: 'REPRESENTATION_ADDED', + REPRESENTATION_REMOVED: 'REPRESENTATION_REMOVED', + REPRESENTATION_CHANGED: 'REPRESENTATION_CHANGED', + 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', + ALL_TURNED_ON_BY_TYPE: 'ALL_TURNED_ON_BY_TYPE', + ALL_TURNED_OFF_BY_TYPE: 'ALL_TURNED_OFF_BY_TYPE', + BACKGROUND_COLOR_CHANGED: 'BACKGROUND_COLOR_CHANGED', + CLIP_NEAR: 'CLIP_NEAR', + ORIENTATION: 'ORIENTATION' +}; + +export const actionDescription = { + LOADED: 'was loaded', + TURNED_ON: 'was turned on', + TURNED_OFF: 'was turned off', + SELECTED: 'was selected', + DESELECTED: 'was deselected', + HIDDEN: 'hidden', + CANCELED: 'canceled', + ADDED: 'was added', + REMOVED: 'was removed', + CHANGED: 'was changed', + UPDATED: 'was updated', + TO_SHOPPING_CART: 'to shopping cart', + FROM_SHOPPING_CART: 'from shopping cart', + LIGAND: 'Ligand', + SIDECHAIN: 'Sidechain', + INTERACTION: 'Interaction', + VECTOR: 'Vector', + SURFACE: 'Surface', + SITE: 'Site', + TARGET: 'Target', + ALL: 'All', + LIGANDS: 'Ligands', + SIDECHAINS: 'Sidechains', + INTERACTIONS: 'Interactions' +}; + +export const actionObjectType = { + TARGET: 'TARGET', + SITE: 'SITE', + MOLECULE: 'MOLECULE', + COMPOUND: 'COMPOUND', + INSPIRATION: 'INSPIRATION', + CROSS_REFERENCE: 'CROSS_REFERENCE', + REPRESENTATION: 'REPRESENTATION', + VIEWER_SETTINGS: 'VIEWER_SETTINGS' +}; + +export const actionAnnotation = { + CHECK: 'CHECK', + CLEAR: 'CLEAR', + WARNING: 'WARNING', + FAVORITE: 'FAVORITE', + STAR: 'STAR' +}; + +export const NUM_OF_SECONDS_TO_IGNORE_MERGE = 5; + +export const undoConstants = { + NGL_TRACKING_UNDO: prefix + 'UNDO', + NGL_TRACKING_REDO: prefix + 'REDO', + NGL_CLEAR_HISTORY: prefix + 'CLEAR_HISTORY' +} \ No newline at end of file diff --git a/js/reducers/nglTracking/dispatchActions.js b/js/reducers/nglTracking/dispatchActions.js new file mode 100644 index 000000000..75b4f97fb --- /dev/null +++ b/js/reducers/nglTracking/dispatchActions.js @@ -0,0 +1,1626 @@ +import { + setCurrentActionsList, + setIsTrackingMoleculesRestoring, + setIsTrackingCompoundsRestoring, + setIsUndoRedoAction, + nglClearHistory +} from './actions'; +import { createInitAction } from './trackingActions'; +import { actionType, actionObjectType, NUM_OF_SECONDS_TO_IGNORE_MERGE } from './constants'; +import { VIEWS } from '../../../js/constants/constants'; +import { setCurrentVector, appendToBuyList } from '../selection/actions'; +import { + resetReducersForRestoringActions, + loadProteinOfRestoringActions +} from '../../components/preview/redux/dispatchActions'; +import { setCurrentProject } from '../../components/projects/redux/actions'; +import { + selectMoleculeGroup, + loadMoleculeGroupsOfTarget +} from '../../components/preview/moleculeGroups/redux/dispatchActions'; +import { loadTargetList } from '../../components/target/redux/dispatchActions'; +import { resetTargetState, setTargetOn } from '../api/actions'; +import { + addComplex, + addLigand, + addHitProtein, + addSurface, + addVector, + removeComplex, + removeLigand, + removeHitProtein, + removeSurface, + removeVector +} from '../../components/preview/molecule/redux/dispatchActions'; +import { colourList } from '../../components/preview/molecule/moleculeView'; +import { + addDatasetComplex, + addDatasetLigand, + addDatasetHitProtein, + addDatasetSurface, + removeDatasetComplex, + removeDatasetLigand, + removeDatasetHitProtein, + removeDatasetSurface, + loadDataSets, + loadDatasetCompoundsWithScores +} from '../../components/datasets/redux/dispatchActions'; +import { + appendMoleculeToCompoundsOfDatasetToBuy, + setMoleculeListIsLoading +} from '../../components/datasets/redux/actions'; +import { setAllMolLists } from '../api/actions'; +import { getUrl, loadAllMolsFromMolGroup } from '../../../js/utils/genericList'; +import { addComponentRepresentation, updateComponentRepresentation } from '../../../js/reducers/ngl/actions'; +import * as listType from '../../constants/listTypes'; +import { assignRepresentationToComp } from '../../components/nglView/generatingObjects'; +import { setOrientation, restoreNglOrientation } from '../../../js/reducers/ngl/dispatchActions'; +import { + setSendActionsList, + setIsActionsSending, + setIsActionsLoading, + setActionsList, + setSnapshotImageActionList, + setUndoRedoActionList +} from './actions'; +import { api, METHOD } from '../../../js/utils/api'; +import { base_url } from '../../components/routes/constants'; +import { CONSTANTS } from '../../../js/constants/constants'; +import moment from 'moment'; +import { + appendToSendActionList, + setProjectActionList, + setIsActionsSaving, + setIsActionsRestoring, + resetTrackingState, + setIsActionTracking +} from './actions'; +import { setSelectedAll, setSelectedAllByType } from '../../../js/reducers/selection/actions'; +import { + setSelectedAll as setSelectedAllOfDataset, + setSelectedAllByType as setSelectedAllByTypeOfDataset +} from '../../components/datasets/redux/actions'; + +export const addCurrentActionsListToSnapshot = (snapshot, project, nglViewList) => async dispatch => { + let projectID = project && project.projectID; + let actionList = await dispatch(getTrackingActions(projectID)); + + await dispatch(setSnapshotToActions(actionList, snapshot, projectID, project, nglViewList, true)); +}; + +export const saveCurrentActionsList = (snapshot, project, nglViewList, all = false) => async dispatch => { + let projectID = project && project.projectID; + let actionList = await dispatch(getTrackingActions(projectID)); + + if (all === false) { + dispatch(setSnapshotToActions(actionList, snapshot, projectID, project, nglViewList, false)); + } else { + dispatch(setSnapshotToAllActions(actionList, snapshot, projectID)); + } + await dispatch(saveActionsList(project, snapshot, actionList, nglViewList)); +}; + +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 currentSelectionAll = state.selectionReducers.moleculeAllSelection; + + const currentDatasetLigands = state.datasetsReducers.ligandLists; + const currentDatasetProteins = state.datasetsReducers.proteinLists; + const currentDatasetComplexes = state.datasetsReducers.complexLists; + const currentDatasetSelectionAll = state.datasetsReducers.moleculeAllSelection; + + const currentTargets = (currentTargetOn && [currentTargetOn]) || []; + + 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); + + getCurrentActionListOfAllSelection( + orderedActionList, + actionType.ALL_TURNED_ON, + getCollection(currentSelectionAll), + currentActions, + getCollection(currentLigands), + getCollection(currentProteins), + getCollection(currentComplexes) + ); + + getCurrentActionListOfAllSelectionByType( + orderedActionList, + actionType.ALL_TURNED_ON_BY_TYPE, + 'ligand', + getCollection(currentLigands), + currentActions + ); + + getCurrentActionListOfAllSelectionByType( + orderedActionList, + actionType.ALL_TURNED_ON_BY_TYPE, + 'protein', + getCollection(currentProteins), + currentActions + ); + + getCurrentActionListOfAllSelectionByType( + orderedActionList, + actionType.ALL_TURNED_ON_BY_TYPE, + 'complex', + getCollection(currentComplexes), + currentActions + ); + + getCurrentActionListOfAllSelectionByTypeOfDataset( + orderedActionList, + actionType.ALL_TURNED_ON_BY_TYPE, + 'ligand', + getCollectionOfDataset(currentDatasetLigands), + currentActions + ); + + getCurrentActionListOfAllSelectionByTypeOfDataset( + orderedActionList, + actionType.ALL_TURNED_ON_BY_TYPE, + 'protein', + getCollectionOfDataset(currentDatasetProteins), + currentActions + ); + + getCurrentActionListOfAllSelectionByTypeOfDataset( + orderedActionList, + actionType.ALL_TURNED_ON_BY_TYPE, + 'complex', + getCollectionOfDataset(currentDatasetComplexes), + currentActions + ); + + getCurrentActionListOfAllSelection( + orderedActionList, + actionType.ALL_TURNED_ON, + getCollectionOfDataset(currentDatasetSelectionAll), + currentActions, + getCollectionOfDataset(currentDatasetLigands), + getCollectionOfDataset(currentDatasetProteins), + getCollectionOfDataset(currentDatasetComplexes) + ); + + getCurrentActionList( + orderedActionList, + actionType.SIDECHAINS_TURNED_ON, + getCollection(currentProteins), + 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.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.LIGAND_TURNED_ON, + getCollectionOfDataset(currentDatasetLigands), + currentActions + ); + + getCurrentActionList( + orderedActionList, + actionType.SIDECHAINS_TURNED_ON, + getCollectionOfDataset(currentDatasetProteins), + currentActions + ); + + getCurrentActionList( + orderedActionList, + actionType.INTERACTIONS_TURNED_ON, + getCollectionOfDataset(currentDatasetComplexes), + currentActions + ); + + getCurrentActionList( + orderedActionList, + actionType.SURFACE_TURNED_ON, + getCollectionOfDataset(currentDatasetSurfaces), + currentActions + ); + + getCurrentActionList( + orderedActionList, + actionType.COMPOUND_SELECTED, + getCollectionOfDataset(currentDatasetBuyList), + currentActions + ); + + getCurrentActionList( + orderedActionList, + actionType.REPRESENTATION_ADDED, + getCollectionOfDatasetOfRepresentation(currentobjectsInView), + currentActions + ); + + getCurrentActionList( + orderedActionList, + actionType.REPRESENTATION_UPDATED, + getCollectionOfDatasetOfRepresentation(currentobjectsInView), + currentActions + ); + + 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 + }; + + currentActions.push(Object.assign({ ...trackAction })); + } + + await dispatch(saveSnapshotAction(snapshot, project, currentActions)); + await dispatch(saveTrackingActions(currentActions, snapshotID)); + dispatch(setCurrentActionsList(currentActions)); + } +}; + +const saveSnapshotAction = (snapshot, project, currentActions) => async (dispatch, getState) => { + const state = getState(); + const trackingImageSource = state.nglTrackingReducers.trackingImageSource; + + let sendActions = []; + let snapshotAction = { + type: actionType.SNAPSHOT, + timestamp: Date.now(), + object_name: snapshot.title, + object_id: snapshot.id, + snapshotId: snapshot.id, + text: `Snapshot: ${snapshot.id} - ${snapshot.title}`, + image: trackingImageSource + }; + sendActions.push(snapshotAction); + currentActions.push(snapshotAction); + await dispatch(sendTrackingActions(sendActions, project)); +}; + +const setSnapshotToActions = ( + actionList, + snapshot, + projectID, + project, + nglViewList, + addToSnapshot +) => async dispatch => { + 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)); + + if (addToSnapshot === true) { + await dispatch(saveActionsList(project, snapshot, updatedActions, nglViewList)); + } + } +}; + +const setSnapshotToAllActions = (actionList, snapshot, projectID) => async dispatch => { + if (actionList && snapshot) { + let updatedActions = actionList.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; + const projectID = project && project.projectID; + + if (snapshotID) { + dispatch(setIsActionsSaving(true)); + + const dataToSend = { + session_project: projectID, + author: project.authorID, + snapshot: snapshotID, + last_update_date: moment().format(), + actions: JSON.stringify(currentActions) + }; + return api({ + url: `${base_url}/api/snapshot-actions/`, + method: METHOD.POST, + data: JSON.stringify(dataToSend) + }) + .then(() => { + dispatch(setCurrentActionsList([])); + }) + .catch(error => { + throw new Error(error); + }) + .finally(() => { + dispatch(setIsActionsSaving(false)); + }); + } else { + return Promise.resolve(); + } +}; + +const getCurrentActionList = (orderedActionList, type, collection, currentActions) => { + let actionList = orderedActionList.filter(action => action.type === type); + + if (collection) { + collection.forEach(data => { + let action = actionList.find(action => action.object_id === data.id && action.dataset_id === data.datasetId); + + if (action) { + currentActions.push(Object.assign({ ...action })); + } + }); + } +}; + +const getCurrentActionListOfAllSelection = ( + orderedActionList, + type, + collection, + currentActions, + ligandList, + proteinList, + complexList +) => { + let actionList = orderedActionList.filter(action => action.type === type); + + if (collection) { + collection.forEach(data => { + let action = actionList.find(action => action.object_id === data.id && action.dataset_id === data.datasetId); + + if (action) { + let ligandAction = ligandList.find( + data => data.id === action.object_id && action.dataset_id === data.datasetId + ); + let proteinAction = proteinList.find( + data => data.id === action.object_id && action.dataset_id === data.datasetId + ); + let complexAction = complexList.find( + data => data.id === action.object_id && action.dataset_id === data.datasetId + ); + + let isLigand = ligandAction && ligandAction != null ? true : false; + let isProtein = proteinAction && proteinAction != null ? true : false; + let isComplex = complexAction && complexAction != null ? true : false; + currentActions.push( + Object.assign({ ...action, isLigand: isLigand, isProtein: isProtein, isComplex: isComplex }) + ); + } + }); + } +}; + +const getCurrentActionListOfAllSelectionByType = (orderedActionList, type, controlType, collection, currentActions) => { + let action = orderedActionList.find( + action => + action.type === type && + action.control_type === controlType && + (action.object_type === actionObjectType.MOLECULE || action.object_type === actionObjectType.INSPIRATION) + ); + if (action && collection) { + let actionItems = action.items; + let items = []; + collection.forEach(data => { + let item = actionItems.find(action => action.id === data.id && action.dataset_id === data.datasetId); + if (item) { + items.push(item); + } + }); + + currentActions.push(Object.assign({ ...action, items: items })); + } +}; + +const getCurrentActionListOfAllSelectionByTypeOfDataset = ( + orderedActionList, + type, + controlType, + collection, + currentActions +) => { + let action = orderedActionList.find( + action => + action.type === type && + action.control_type === controlType && + (action.object_type === actionObjectType.COMPOUND || action.object_type === actionObjectType.CROSS_REFERENCE) + ); + if (action && collection) { + let actionItems = action.items; + let items = []; + collection.forEach(data => { + let item = actionItems.find(item => item.molecule.id === data.id && item.datasetID === data.datasetId); + if (item) { + items.push(item); + } + }); + + currentActions.push(Object.assign({ ...action, items: items })); + } +}; + +const getCollection = dataList => { + let list = []; + if (dataList) { + var result = dataList.map(value => ({ id: value })); + list.push(...result); + } + return list; +}; + +const getCollectionOfDataset = dataList => { + let list = []; + if (dataList) { + for (const datasetId in dataList) { + let values = dataList[datasetId]; + if (values) { + var result = values.map(value => ({ id: value, datasetId: datasetId })); + list.push(...result); + } + } + } + return list; +}; + +const getCollectionOfShoppingCart = dataList => { + let list = []; + if (dataList) { + dataList.forEach(data => { + let value = data.vector; + if (value) { + list.push({ id: value }); + } + }); + } + return list; +}; + +const getCollectionOfDatasetOfRepresentation = dataList => { + let list = []; + for (const view in dataList) { + let objectView = dataList[view]; + if (objectView && objectView !== null && objectView.display_div === VIEWS.MAJOR_VIEW) { + let value = dataList[view].name; + if (value) { + list.push({ id: value }); + } + } + } + return list; +}; + +export const resetRestoringState = () => dispatch => { + dispatch(setActionsList([])); + dispatch(setProjectActionList([])); + dispatch(setSendActionsList([])); + + dispatch(setTargetOn(undefined)); + dispatch(setIsActionsRestoring(false, false)); +}; + +export const restoreCurrentActionsList = snapshotID => async dispatch => { + dispatch(resetTrackingState()); + dispatch(setIsActionsRestoring(true, false)); + + await dispatch(restoreTrackingActions(snapshotID)); + dispatch(setIsTrackingMoleculesRestoring(true)); + dispatch(setIsTrackingCompoundsRestoring(true)); + dispatch(resetTargetState()); + dispatch(resetReducersForRestoringActions()); + dispatch(restoreStateBySavedActionList()); +}; + +const restoreTrackingActions = snapshotID => async dispatch => { + if (snapshotID) { + try { + const response = await api({ + url: `${base_url}/api/snapshot-actions/?snapshot=${snapshotID}` + }); + let results = response.data.results; + let listToSet = []; + results.forEach(r => { + let resultActions = JSON.parse(r.actions); + listToSet.push(...resultActions); + }); + + let snapshotActions = [...listToSet]; + dispatch(setCurrentActionsList(snapshotActions)); + } catch (error) { + throw new Error(error); + } + } else { + return Promise.resolve(); + } +}; + +const restoreStateBySavedActionList = () => (dispatch, getState) => { + const state = getState(); + + const currentActionList = state.nglTrackingReducers.current_actions_list; + const orderedActionList = currentActionList.sort((a, b) => a.timestamp - b.timestamp); + + let onCancel = () => {}; + dispatch(loadTargetList(onCancel)) + .then(() => dispatch(restoreTargetActions(orderedActionList))) + .catch(error => { + throw new Error(error); + }); + return () => { + onCancel(); + }; +}; + +const restoreTargetActions = orderedActionList => (dispatch, getState) => { + const state = getState(); + + let targetAction = orderedActionList.find(action => action.type === actionType.TARGET_LOADED); + if (targetAction) { + let target = getTarget(targetAction.object_name, state); + if (target) { + dispatch(setTargetOn(target.id)); + } + } +}; + +export const restoreAfterTargetActions = (stages, projectId) => async (dispatch, getState) => { + const state = getState(); + + const currentActionList = state.nglTrackingReducers.current_actions_list; + const orderedActionList = currentActionList.sort((a, b) => a.timestamp - b.timestamp); + const targetId = state.apiReducers.target_on; + + if (targetId && stages && stages.length > 0) { + const majorView = stages.find(view => view.id === VIEWS.MAJOR_VIEW); + const summaryView = stages.find(view => view.id === VIEWS.SUMMARY_VIEW); + + await dispatch(loadProteinOfRestoringActions({ nglViewList: stages })); + + await dispatch( + loadMoleculeGroupsOfTarget({ + summaryView: summaryView.stage, + isStateLoaded: false, + setOldUrl: () => {}, + target_on: targetId + }) + ) + .catch(error => { + throw error; + }) + .finally(() => {}); + + await dispatch(restoreSitesActions(orderedActionList, summaryView)); + await dispatch(loadData(orderedActionList, targetId, majorView)); + await dispatch(restoreActions(orderedActionList, majorView.stage)); + await dispatch(restoreRepresentationActions(orderedActionList, stages)); + await dispatch(restoreProject(projectId)); + dispatch(restoreSnapshotImageActions(projectId)); + dispatch(restoreNglStateAction(orderedActionList, stages)); + dispatch(setIsActionsRestoring(false, true)); + } +}; + +const restoreNglStateAction = (orderedActionList, stages) => dispatch => { + let actions = orderedActionList.filter(action => action.type === actionType.NGL_STATE); + let action = [...actions].pop(); + if (action && action.nglStateList) { + action.nglStateList.forEach(nglView => { + dispatch(setOrientation(nglView.id, nglView.orientation)); + let viewStage = stages.find(s => s.id === nglView.id); + if (viewStage) { + viewStage.stage.viewerControls.orient(nglView.orientation.elements); + } + }); + } +}; + +const restoreActions = (orderedActionList, stage) => dispatch => { + dispatch(restoreMoleculesActions(orderedActionList, stage)); +}; + +const loadData = (orderedActionList, targetId, majorView) => async dispatch => { + await dispatch(loadAllMolecules(orderedActionList, targetId, majorView.stage)); + await dispatch(loadAllDatasets(orderedActionList, targetId, majorView.stage)); +}; + +const loadAllDatasets = (orderedActionList, target_on, stage) => async dispatch => { + dispatch(setMoleculeListIsLoading(true)); + + await dispatch(loadDataSets(target_on)); + await dispatch(loadDatasetCompoundsWithScores()); + dispatch(setMoleculeListIsLoading(false)); + + dispatch(restoreCompoundsActions(orderedActionList, stage)); +}; + +const loadAllMolecules = (orderedActionList, target_on) => async (dispatch, getState) => { + const state = getState(); + const list_type = listType.MOLECULE; + + let molGroupList = state.apiReducers.mol_group_list; + + let promises = []; + molGroupList.forEach(molGroup => { + let id = molGroup.id; + let url = getUrl({ list_type, target_on, mol_group_on: id }); + promises.push( + loadAllMolsFromMolGroup({ + url, + mol_group: id + }) + ); + }); + try { + const results = await Promise.all(promises); + let listToSet = {}; + results.forEach(molResult => { + listToSet[molResult.mol_group] = molResult.molecules; + }); + dispatch(setAllMolLists(listToSet)); + } catch (error) { + throw new Error(error); + } +}; + +const restoreSitesActions = (orderedActionList, summaryView) => (dispatch, getState) => { + const state = getState(); + + let sitesAction = orderedActionList.filter(action => action.type === actionType.SITE_TURNED_ON); + if (sitesAction) { + sitesAction.forEach(action => { + let molGroup = getMolGroup(action.object_name, state); + if (molGroup) { + dispatch(selectMoleculeGroup(molGroup, summaryView.stage)); + } + }); + } +}; + +const restoreMoleculesActions = (orderedActionList, stage) => (dispatch, getState) => { + const state = getState(); + let moleculesAction = orderedActionList.filter( + action => action.object_type === actionObjectType.MOLECULE || action.object_type === actionObjectType.INSPIRATION + ); + + if (moleculesAction) { + dispatch(addNewType(moleculesAction, actionType.LIGAND_TURNED_ON, 'ligand', stage, state)); + dispatch(addNewType(moleculesAction, actionType.SIDECHAINS_TURNED_ON, 'protein', stage, state)); + dispatch(addNewType(moleculesAction, actionType.INTERACTIONS_TURNED_ON, 'complex', stage, state)); + dispatch(addNewType(moleculesAction, actionType.SURFACE_TURNED_ON, 'surface', stage, state)); + dispatch(addNewType(moleculesAction, actionType.VECTORS_TURNED_ON, 'vector', stage, state)); + } + + let vectorAction = orderedActionList.find(action => action.type === actionType.VECTOR_SELECTED); + if (vectorAction) { + dispatch(setCurrentVector(vectorAction.object_name)); + } + + dispatch(restoreCartActions(moleculesAction)); + dispatch(restoreAllSelectionActions(orderedActionList, stage, true)); + dispatch(restoreAllSelectionByTypeActions(orderedActionList, stage, true)); + dispatch(setIsTrackingMoleculesRestoring(false)); +}; + +const restoreCartActions = moleculesAction => dispatch => { + let shoppingCartActions = moleculesAction.filter( + action => action.type === actionType.MOLECULE_ADDED_TO_SHOPPING_CART + ); + if (shoppingCartActions) { + shoppingCartActions.forEach(action => { + let data = action.item; + if (data) { + dispatch(appendToBuyList(data)); + } + }); + } +}; + +const restoreAllSelectionActions = (moleculesAction, stage, isSelection) => (dispatch, getState) => { + let state = getState(); + + let actions = + isSelection === true + ? moleculesAction.filter( + action => + action.type === actionType.ALL_TURNED_ON && + (action.object_type === actionObjectType.INSPIRATION || action.object_type === actionObjectType.MOLECULE) + ) + : moleculesAction.filter( + action => + action.type === actionType.ALL_TURNED_ON && + (action.object_type === actionObjectType.CROSS_REFERENCE || + action.object_type === actionObjectType.COMPOUND) + ); + + if (actions) { + actions.forEach(action => { + if (action) { + if (isSelection) { + dispatch(setSelectedAll(action.item, action.isLigand, action.isProtein, action.isComplex)); + } else { + dispatch( + setSelectedAllOfDataset(action.dataset_id, action.item, action.isLigand, action.isProtein, action.isComplex) + ); + } + + if (action.isLigand) { + dispatch(handleMoleculeAction(action, 'ligand', true, stage, state, true)); + } + + if (action.isProtein) { + dispatch(handleMoleculeAction(action, 'protein', true, stage, state, true)); + } + + if (action.isComplex) { + dispatch(handleMoleculeAction(action, 'complex', true, stage, state, true)); + } + } + }); + } +}; + +const restoreAllSelectionByTypeActions = (moleculesAction, stage, isSelection) => dispatch => { + let actions = + isSelection === true + ? moleculesAction.filter( + action => + action.type === actionType.ALL_TURNED_ON_BY_TYPE && + (action.object_type === actionObjectType.INSPIRATION || action.object_type === actionObjectType.MOLECULE) + ) + : moleculesAction.filter( + action => + action.type === actionType.ALL_TURNED_ON_BY_TYPE && + (action.object_type === actionObjectType.CROSS_REFERENCE || + action.object_type === actionObjectType.COMPOUND) + ); + + if (actions) { + actions.forEach(action => { + if (action) { + let actionItems = action.items; + let type = action.control_type; + + if (isSelection) { + dispatch(setSelectedAllByType(type, actionItems, action.object_type === actionObjectType.INSPIRATION)); + + actionItems.forEach(data => { + if (data) { + if (type === 'ligand') { + dispatch(addType[type](stage, data, colourList[data.id % colourList.length], true, true)); + } else { + dispatch(addType[type](stage, data, colourList[data.id % colourList.length], true)); + } + } + }); + } else { + dispatch( + setSelectedAllByTypeOfDataset( + type, + action.dataset_id, + actionItems, + action.object_type === actionObjectType.CROSS_REFERENCE + ) + ); + + actionItems.forEach(data => { + if (data && data.molecule) { + dispatch( + addTypeCompound[type]( + stage, + data.molecule, + colourList[data.molecule.id % colourList.length], + data.datasetID, + true + ) + ); + } + }); + } + } + }); + } +}; + +const restoreRepresentationActions = (moleculesAction, stages) => dispatch => { + const nglView = stages.find(view => view.id === VIEWS.MAJOR_VIEW); + + let representationsActions = moleculesAction.filter(action => action.type === actionType.REPRESENTATION_ADDED); + if (representationsActions) { + representationsActions.forEach(action => { + dispatch(addRepresentation(action.object_id, action.representation, nglView)); + }); + } + + let representationsChangesActions = moleculesAction.filter( + action => action.type === actionType.REPRESENTATION_UPDATED + ); + if (representationsChangesActions) { + representationsChangesActions.forEach(action => { + dispatch(updateRepresentation(true, action.change, action.object_id, action.representation, nglView)); + }); + } +}; + +const restoreSnapshotImageActions = projectID => async dispatch => { + 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, timestamp: s.timestamp }; + }); + const key = 'object_id'; + const arrayUniqueByKey = [...new Map(actions.map(item => [item[key], item])).values()]; + dispatch(setSnapshotImageActionList(arrayUniqueByKey)); + } +}; + +const restoreProject = projectId => dispatch => { + if (projectId !== undefined) { + return api({ url: `${base_url}/api/session-projects/${projectId}/` }).then(response => { + let promises = []; + promises.push( + dispatch( + setCurrentProject({ + projectID: response.data.id, + authorID: (response.data.author && response.data.author.id) || null, + title: response.data.title, + description: response.data.description, + targetID: response.data.target.id, + tags: JSON.parse(response.data.tags) + }) + ) + ); + return Promise.all(promises); + }); + } +}; + +const restoreCompoundsActions = (orderedActionList, stage) => (dispatch, getState) => { + const state = getState(); + + let compoundsAction = orderedActionList.filter( + action => + action.object_type === actionObjectType.COMPOUND || action.object_type === actionObjectType.CROSS_REFERENCE + ); + + if (compoundsAction) { + dispatch(addNewTypeCompound(compoundsAction, actionType.LIGAND_TURNED_ON, 'ligand', stage, state)); + dispatch(addNewTypeCompound(compoundsAction, actionType.SIDECHAINS_TURNED_ON, 'protein', stage, state)); + dispatch(addNewTypeCompound(compoundsAction, actionType.INTERACTIONS_TURNED_ON, 'complex', stage, state)); + dispatch(addNewTypeCompound(compoundsAction, actionType.SURFACE_TURNED_ON, 'surface', stage, state)); + } + + let compoundsSelectedAction = compoundsAction.filter(action => action.type === actionType.COMPOUND_SELECTED); + + compoundsSelectedAction.forEach(action => { + let data = getCompound(action, state); + if (data) { + dispatch(appendMoleculeToCompoundsOfDatasetToBuy(action.dataset_id, data.id, data.name)); + } + }); + + dispatch(restoreAllSelectionActions(orderedActionList, stage, false)); + dispatch(restoreAllSelectionByTypeActions(orderedActionList, stage, false)); + dispatch(setIsTrackingCompoundsRestoring(false)); +}; + +const addType = { + ligand: addLigand, + protein: addHitProtein, + complex: addComplex, + surface: addSurface, + vector: addVector +}; + +const addTypeCompound = { + ligand: addDatasetLigand, + protein: addDatasetHitProtein, + complex: addDatasetComplex, + surface: addDatasetSurface +}; + +const addNewType = (moleculesAction, actionType, type, stage, state, skipTracking = false) => dispatch => { + let actions = moleculesAction.filter(action => action.type === actionType); + if (actions) { + actions.forEach(action => { + let data = getMolecule(action.object_name, state); + if (data) { + if (type === 'ligand') { + dispatch(addType[type](stage, data, colourList[data.id % colourList.length], true, skipTracking)); + } else { + dispatch(addType[type](stage, data, colourList[data.id % colourList.length], skipTracking)); + } + } + }); + } +}; + +const addNewTypeOfAction = (action, type, stage, state, skipTracking = false) => dispatch => { + if (action) { + let data = getMolecule(action.object_name, state); + if (data) { + if (type === 'ligand') { + dispatch(addType[type](stage, data, colourList[data.id % colourList.length], true, skipTracking)); + } else { + dispatch(addType[type](stage, data, colourList[data.id % colourList.length], skipTracking)); + } + } + } +}; + +const addNewTypeCompound = (moleculesAction, actionType, type, stage, state, skipTracking = false) => dispatch => { + let actions = moleculesAction.filter(action => action.type === actionType); + if (actions) { + actions.forEach(action => { + let data = getCompound(action, state); + if (data) { + dispatch( + addTypeCompound[type](stage, data, colourList[data.id % colourList.length], action.dataset_id, skipTracking) + ); + } + }); + } +}; + +const addNewTypeCompoundOfAction = (action, type, stage, state, skipTracking = false) => dispatch => { + if (action) { + let data = getCompound(action, state); + if (data) { + dispatch( + addTypeCompound[type](stage, data, colourList[data.id % colourList.length], action.dataset_id, skipTracking) + ); + } + } +}; + +const getTarget = (targetName, state) => { + let targetList = state.apiReducers.target_id_list; + let target = targetList.find(target => target.title === targetName); + return target; +}; + +const getMolGroup = (molGroupName, state) => { + let molGroupList = state.apiReducers.mol_group_list; + let molGroup = molGroupList.find(group => group.description === molGroupName); + return molGroup; +}; + +const getMolecule = (moleculeName, state) => { + let moleculeList = state.apiReducers.all_mol_lists; + let molecule = null; + + if (moleculeList) { + for (const group in moleculeList) { + let molecules = moleculeList[group]; + molecule = molecules.find(m => m.protein_code === moleculeName); + if (molecule && molecule != null) { + break; + } + } + } + return molecule; +}; + +const getCompound = (action, state) => { + let moleculeList = state.datasetsReducers.moleculeLists; + let molecule = null; + + let name = action.object_name; + let datasetID = action.dataset_id; + + if (moleculeList) { + let moleculeListOfDataset = moleculeList[datasetID]; + if (moleculeListOfDataset) { + molecule = moleculeListOfDataset.find(m => m.name === name); + } + } + return molecule; +}; + +export const undoAction = (stages = []) => dispatch => { + dispatch(setIsUndoRedoAction(true)); + let action = dispatch(getUndoAction()); + if (action) { + Promise.resolve(dispatch(handleUndoAction(action, stages))).then(() => { + dispatch(setIsUndoRedoAction(false)); + }); + } +}; + +const getUndoAction = () => (dispatch, getState) => { + const state = getState(); + const actionUndoList = state.undoableNglTrackingReducers.future; + + let action = { text: '' }; + let actions = actionUndoList && actionUndoList[0]; + if (actions) { + let actionsLenght = actions.undo_redo_actions_list.length; + actionsLenght = actionsLenght > 0 ? actionsLenght - 1 : actionsLenght; + action = actions.undo_redo_actions_list[actionsLenght]; + } + + return action; +}; + +const getRedoAction = () => (dispatch, getState) => { + const state = getState(); + const actions = state.undoableNglTrackingReducers.present; + + let action = { text: '' }; + if (actions) { + let actionsLenght = actions.undo_redo_actions_list.length; + actionsLenght = actionsLenght > 0 ? actionsLenght - 1 : actionsLenght; + action = actions.undo_redo_actions_list[actionsLenght]; + } + + return action; +}; + +const getNextUndoAction = () => (dispatch, getState) => { + const state = getState(); + const actionUndoList = state.undoableNglTrackingReducers.present; + + let action = { text: '' }; + let actions = actionUndoList && actionUndoList.undo_redo_actions_list; + if (actions) { + let actionsLenght = actions.length; + actionsLenght = actionsLenght > 0 ? actionsLenght - 1 : actionsLenght; + action = actions[actionsLenght]; + } + + return action; +}; + +const getNextRedoAction = () => (dispatch, getState) => { + const state = getState(); + const actionUndoList = state.undoableNglTrackingReducers.future; + + let action = { text: '' }; + let actionss = actionUndoList && actionUndoList[0]; + + let actions = actionss && actionss.undo_redo_actions_list; + if (actions) { + let actionsLenght = actions.length; + actionsLenght = actionsLenght > 0 ? actionsLenght - 1 : actionsLenght; + action = actions[actionsLenght]; + } + + return action; +}; + +export const redoAction = (stages = []) => dispatch => { + dispatch(setIsUndoRedoAction(true)); + let action = dispatch(getRedoAction()); + if (action) { + Promise.resolve(dispatch(dispatch(handleRedoAction(action, stages)))).then(() => { + dispatch(setIsUndoRedoAction(false)); + }); + } +}; + +const handleUndoAction = (action, stages) => (dispatch, getState) => { + if (action) { + const type = action.type; + switch (type) { + case actionType.ORIENTATION: + dispatch(restoreNglOrientation(action.oldSetting, action.newSetting, action.div_id, stages)); + break; + default: + break; + } + } +}; + +const handleRedoAction = (action, stages) => (dispatch, getState) => { + if (action) { + const type = action.type; + switch (type) { + case actionType.ORIENTATION: + dispatch(restoreNglOrientation(action.newSetting, action.oldSetting, action.div_id, stages)); + break; + default: + break; + } + } +}; + +const addRepresentation = (action, parentKey, representation, nglView, update, skipTracking = false) => dispatch => { + const oldRepresentation = representation; + const newRepresentationType = oldRepresentation.type; + const comp = nglView.stage.getComponentsByName(parentKey).first; + const newRepresentation = assignRepresentationToComp( + newRepresentationType, + oldRepresentation.params, + comp, + oldRepresentation.lastKnownID + ); + action.representation = newRepresentation; + if (update === true) { + action.newRepresentation = newRepresentation; + } else { + action.oldRepresentation = newRepresentation; + } + dispatch(addComponentRepresentation(parentKey, newRepresentation, skipTracking)); +}; + +const updateRepresentation = (isAdd, change, parentKey, representation, nglView) => dispatch => { + const comp = nglView.stage.getComponentsByName(parentKey).first; + const r = comp.reprList.find(rep => rep.uuid === representation.uuid || rep.uuid === representation.lastKnownID); + if (r && change) { + let key = change.key; + let value = isAdd ? change.value : change.oldValue; + + r.setParameters({ [key]: value }); + representation.params[key] = value; + + dispatch(updateComponentRepresentation(parentKey, representation.uuid, representation)); + } +}; + +const handleMoleculeAction = (action, type, isAdd, stage, state, skipTracking) => dispatch => { + if (action.object_type === actionObjectType.MOLECULE || action.object_type === actionObjectType.INSPIRATION) { + if (isAdd) { + dispatch(addNewTypeOfAction(action, type, stage, state, skipTracking)); + } else { + dispatch(removeNewType(action, type, stage, state, skipTracking)); + } + } else if ( + action.object_type === actionObjectType.COMPOUND || + action.object_type === actionObjectType.CROSS_REFERENCE + ) { + if (isAdd) { + dispatch(addNewTypeCompoundOfAction(action, type, stage, state, skipTracking)); + } else { + dispatch(removeNewTypeCompound(action, type, stage, state, skipTracking)); + } + } +}; + +const removeType = { + ligand: removeLigand, + protein: removeHitProtein, + complex: removeComplex, + surface: removeSurface, + vector: removeVector +}; + +const removeTypeCompound = { + ligand: removeDatasetLigand, + protein: removeDatasetHitProtein, + complex: removeDatasetComplex, + surface: removeDatasetSurface +}; + +const removeNewType = (action, type, stage, state, skipTracking) => dispatch => { + if (action) { + let data = getMolecule(action.object_name, state); + if (data) { + if (type === 'ligand') { + dispatch(removeType[type](stage, data, skipTracking)); + } else { + dispatch(removeType[type](stage, data, colourList[data.id % colourList.length], skipTracking)); + } + } + } +}; + +const removeNewTypeCompound = (action, type, stage, state, skipTracking) => dispatch => { + if (action) { + let data = getCompound(action, state); + if (data) { + dispatch( + removeTypeCompound[type](stage, data, colourList[data.id % colourList.length], action.dataset_id, skipTracking) + ); + } + } +}; + +export const getUndoActionText = () => dispatch => { + let action = dispatch(getNextUndoAction()); + return action?.text ?? ''; +}; + +export const getRedoActionText = () => dispatch => { + let action = dispatch(getNextRedoAction()); + return action?.text ?? ''; +}; + +export const appendAndSendTrackingActions = trackAction => (dispatch, getState) => { + const state = getState(); + const isUndoRedoAction = state.nglTrackingReducers.isUndoRedoAction; + dispatch(setIsActionTracking(true)); + + if (trackAction && trackAction !== null) { + const actionList = state.nglTrackingReducers.track_actions_list; + const sendActionList = state.nglTrackingReducers.send_actions_list; + const mergedActionList = mergeActions(trackAction, [...actionList]); + const mergedSendActionList = mergeActions(trackAction, [...sendActionList]); + dispatch(setActionsList(mergedActionList)); + dispatch(setSendActionsList(mergedSendActionList)); + + if (isUndoRedoAction === false) { + const undoRedoActionList = state.nglTrackingReducers.undo_redo_actions_list; + const mergedUndoRedoActionList = mergeActions(trackAction, [...undoRedoActionList]); + dispatch(setUndoRedoActionList(mergedUndoRedoActionList)); + } + } + dispatch(setIsActionTracking(false)); + dispatch(checkSendTrackingActions()); +}; + +export const mergeActions = (trackAction, list) => { + return [...list, trackAction]; +}; + +export const manageSendTrackingActions = (projectID, copy) => dispatch => { + if (copy) { + dispatch(checkActionsProject(projectID)); + } else { + dispatch(checkSendTrackingActions(true)); + } +}; + +export const checkSendTrackingActions = (save = false) => (dispatch, getState) => { + const state = getState(); + const currentProject = state.projectReducers.currentProject; + const sendActions = state.nglTrackingReducers.send_actions_list; + const length = sendActions.length; + + if (length >= CONSTANTS.COUNT_SEND_TRACK_ACTIONS || save) { + dispatch(sendTrackingActions(sendActions, currentProject)); + } +}; + +const sendTrackingActions = (sendActions, project, clear = true) => async dispatch => { + if (project) { + const projectID = project && project.projectID; + + if (projectID && sendActions && sendActions.length > 0) { + dispatch(setIsActionsSending(true)); + + const dataToSend = { + session_project: projectID, + author: project.authorID, + last_update_date: moment().format(), + actions: JSON.stringify(sendActions) + }; + return api({ + url: `${base_url}/api/session-actions/`, + method: METHOD.POST, + data: JSON.stringify(dataToSend) + }) + .then(() => { + if (clear === true) { + dispatch(setSendActionsList([])); + } + }) + .catch(error => { + throw new Error(error); + }) + .finally(() => { + dispatch(setIsActionsSending(false)); + }); + } else { + return Promise.resolve(); + } + } else { + return Promise.resolve(); + } +}; + +export const setProjectTrackingActions = () => (dispatch, getState) => { + const state = getState(); + const currentProject = state.projectReducers.currentProject; + const projectID = currentProject && currentProject.projectID; + dispatch(setProjectActionList([])); + dispatch(getTrackingActions(projectID, true)); +}; + +const getTrackingActions = (projectID, withTreeSeparation) => (dispatch, getState) => { + const state = getState(); + const sendActions = state.nglTrackingReducers.send_actions_list; + + if (projectID) { + dispatch(setIsActionsLoading(true)); + return api({ + url: `${base_url}/api/session-actions/?session_project=${projectID}` + }) + .then(response => { + let results = response.data.results; + let listToSet = []; + results.forEach(r => { + let resultActions = JSON.parse(r.actions); + let actions = resultActions.map(obj => ({ ...obj, actionId: r.id })); + listToSet.push(...actions); + }); + + if (withTreeSeparation === true) { + listToSet = dispatch(separateTrackkingActionBySnapshotTree(listToSet)); + + let actionsWithoutSnapshot = listToSet.filter(action => action.type !== actionType.SNAPSHOT); + let snapshotActions = listToSet.filter(action => action.type === actionType.SNAPSHOT); + if (snapshotActions) { + const key = 'object_id'; + const arrayUniqueByKey = [...new Map(snapshotActions.map(item => [item[key], item])).values()]; + actionsWithoutSnapshot.push(...arrayUniqueByKey); + listToSet = actionsWithoutSnapshot; + } + } + + let projectActions = [...listToSet, ...sendActions]; + dispatch(setProjectActionList(projectActions)); + return Promise.resolve(projectActions); + }) + .catch(error => { + throw new Error(error); + }) + .finally(() => { + dispatch(setIsActionsLoading(false)); + }); + } else { + let projectActions = [...sendActions]; + dispatch(setProjectActionList(projectActions)); + return Promise.resolve(projectActions); + } +}; + +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; + const currentProjectID = currentProject && currentProject.projectID; + + await dispatch(getTrackingActions(projectID)); + await dispatch( + copyActionsToProject(currentProject, true, currentProjectID && currentProjectID != null ? true : false) + ); +}; + +const copyActionsToProject = (toProject, setActionList = true, clearSendList = true) => async (dispatch, getState) => { + const state = getState(); + const actionList = state.nglTrackingReducers.project_actions_list; + + if (toProject) { + let newActionsList = []; + + actionList.forEach(r => { + newActionsList.push(Object.assign({ ...r })); + }); + + if (setActionList === true) { + dispatch(setActionsList(newActionsList)); + } + await dispatch(sendTrackingActions(newActionsList, toProject, clearSendList)); + } +}; + +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 }; + + await dispatch(getTrackingActions(currentProjectID)); + await dispatch(copyActionsToProject(project, false, currentProjectID && currentProjectID != null ? true : false)); +}; + +export const sendInitTrackingActionByProjectId = target_on => (dispatch, getState) => { + const state = getState(); + const snapshotID = state.projectReducers.currentSnapshot && state.projectReducers.currentSnapshot.id; + + let trackAction = dispatch(createInitAction(target_on)); + if (trackAction && trackAction != null) { + let actions = []; + actions.push(trackAction); + dispatch(appendToSendActionList(trackAction)); + dispatch(checkSendTrackingActions(true)); + dispatch(saveTrackingActions(actions, snapshotID)); + } +}; + +export const updateTrackingActions = action => (dispatch, getState) => { + const state = getState(); + const project = state.projectReducers.currentProject; + const projectActions = state.nglTrackingReducers.project_actions_list; + const projectID = project && project.projectID; + let actionID = action && action.actionId; + + 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) + }) + .then(() => {}) + .catch(error => { + throw new Error(error); + }) + .finally(() => {}); + } else { + return Promise.resolve(); + } + } else { + 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) => () => { + 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(); + } +}; + +export const resetNglTrackingState = () => (dispatch, getState) => { + dispatch(resetTrackingState()); + dispatch(nglClearHistory()); +}; diff --git a/js/reducers/nglTracking/nglTrackingMiddleware.js b/js/reducers/nglTracking/nglTrackingMiddleware.js new file mode 100644 index 000000000..af91db69f --- /dev/null +++ b/js/reducers/nglTracking/nglTrackingMiddleware.js @@ -0,0 +1,21 @@ +import { appendAndSendTrackingActions } from './dispatchActions'; +import { constants } from './constants'; +import { findTrackAction } from './trackingActions'; + +const nglTrackingMiddleware = ({ dispatch, getState }) => next => action => { + //console.log(`Redux Log:`, action); + + if (action) { + const state = getState(); + if (action && !action.type.includes(constants.APPEND_ACTIONS_LIST)) { + let trackAction = findTrackAction(action, state); + if (trackAction && trackAction != null) { + dispatch(appendAndSendTrackingActions(trackAction)); + } + } + + next(action); + } +}; + +export default nglTrackingMiddleware; diff --git a/js/reducers/nglTracking/nglTrackingReducers.js b/js/reducers/nglTracking/nglTrackingReducers.js new file mode 100644 index 000000000..d68509dc4 --- /dev/null +++ b/js/reducers/nglTracking/nglTrackingReducers.js @@ -0,0 +1,131 @@ +import { constants, undoConstants } from './constants'; +import { undoable } from '../../undoredo/reducer'; +import { includeAction } from '../../undoredo/helpers'; + +export const INITIAL_STATE = { + track_actions_list: [], + undo_redo_actions_list: [], + current_actions_list: [], + isTrackingMoleculesRestoring: false, + isTrackingCompoundsRestoring: false, + isUndoRedoAction: false, + isActionsSending: false, + isActionsLoading: false, + isActionSaving: false, + send_actions_list: [], + project_actions_list: [], + snapshotActionImageList: [], + isActionRestoring: false, + isActionRestored: false, + isActionTracking: false, + trackingImageSource: '' +}; + +export function nglTrackingReducers(state = INITIAL_STATE, action = {}) { + switch (action.type) { + case constants.SET_ACTIONS_LIST: + return Object.assign({}, state, { + track_actions_list: action.track_actions_list + }); + + case constants.APPEND_ACTIONS_LIST: + return Object.assign({}, state, { + track_actions_list: [...new Set([...state.track_actions_list, action.track_action])] + }); + + case constants.APPEND_UNDO_REDO_ACTIONS_LIST: + return Object.assign({}, state, { + undo_redo_actions_list: [...new Set([...state.undo_redo_actions_list, action.track_action])] + }); + + case constants.SET_UNDO_REDO_ACTIONS_LIST: + return { + ...state, undo_redo_actions_list: action.undo_redo_actions_list + }; + + case constants.SET_CURRENT_ACTIONS_LIST: + return Object.assign({}, state, { + current_actions_list: [...action.current_actions_list] + }); + + case constants.SET_IS_TRACKING_MOLECULES_RESTORING: + return Object.assign({}, state, { + isTrackingMoleculesRestoring: action.isTrackingMoleculesRestoring + }); + + case constants.SET_IS_TRACKING_COMPOUNDS_RESTORING: + return Object.assign({}, state, { + isTrackingCompoundsRestoring: action.isTrackingCompoundsRestoring + }); + + case constants.SET_IS_UNDO_REDO_ACTION: + return Object.assign({}, state, { + isUndoRedoAction: action.isUndoRedoAction + }); + + case constants.SET_IS_ACTIONS_SENDING: + return Object.assign({}, state, { + isActionsSending: action.isActionsSending + }); + + case constants.SET_IS_ACTIONS_LOADING: + return Object.assign({}, state, { + isActionsLoading: action.isActionsLoading + }); + + case constants.SET_SEND_ACTIONS_LIST: + return Object.assign({}, state, { + send_actions_list: [...action.send_actions_list] + }); + + case constants.APPEND_SEND_ACTIONS_LIST: + return Object.assign({}, state, { + send_actions_list: [...new Set([...state.send_actions_list, action.track_action])] + }); + + case constants.SET_PROJECT_ACTIONS_LIST: + return Object.assign({}, state, { + 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 + }); + + case constants.SET_IS_ACTIONS_RESTORING: + return Object.assign({}, state, { + isActionRestoring: action.isActionRestoring, + isActionRestored: action.isActionRestored + }); + case constants.SET_IS_ACTION_TRACKING: + return Object.assign({}, state, { + isActionTracking: action.isActionTracking + }); + + case constants.SET_TRACKING_IMAGE_SOURCE: + return Object.assign({}, state, { + trackingImageSource: action.payload + }); + + case constants.RESET_TRACKING_STATE: + return INITIAL_STATE; + + default: + return state; + } +} + +export const undoableNglTrackingReducers = undoable(nglTrackingReducers, { + limit: false, + filter: includeAction(constants.SET_UNDO_REDO_ACTIONS_LIST), + undoType: undoConstants.NGL_TRACKING_UNDO, + redoType: undoConstants.NGL_TRACKING_REDO, + clearHistoryType: undoConstants.NGL_CLEAR_HISTORY +}); + \ No newline at end of file diff --git a/js/reducers/nglTracking/trackingActions.js b/js/reducers/nglTracking/trackingActions.js new file mode 100644 index 000000000..5f9e24be9 --- /dev/null +++ b/js/reducers/nglTracking/trackingActions.js @@ -0,0 +1,60 @@ +import { actionType, actionObjectType, actionDescription, actionAnnotation } from './constants'; +import { CONSTANTS as nglConstants } from '../ngl/constants'; +import { DJANGO_CONTEXT } from '../../utils/djangoContext'; + +export const findTrackAction = (action, state) => { + const username = DJANGO_CONTEXT['username']; + const isActionRestoring = state.nglTrackingReducers.isActionRestoring; + + let trackAction = null; + if (isActionRestoring === false && action.skipTracking !== true) { + if (action.type.includes(nglConstants.SET_ORIENTATION_BY_INTERACTION)) { + const { oldOrientation: oldSetting, orientation: newSetting, div_id } = action; + + trackAction = { + type: actionType.ORIENTATION, + merge: true, + annotation: actionAnnotation.CHECK, + timestamp: Date.now(), + username: username, + object_type: 'NGL', + object_name: 'NGL', + oldSetting, + newSetting, + div_id, + getText: function() { + return 'NGL transformation changed'; + }, + text: 'NGL transformation changed' + }; + } + } + return trackAction; +}; + +const getTargetName = (targetId, state) => { + let targetList = state.apiReducers.target_id_list; + let target = targetList.find(target => target.id === targetId); + let targetName = (target && target.title) || ''; + return targetName; +}; + +export const createInitAction = target_on => (dispatch, getState) => { + const state = getState(); + const username = DJANGO_CONTEXT['username']; + + if (target_on) { + let targetName = getTargetName(target_on, state); + let trackAction = { + type: actionType.TARGET_LOADED, + timestamp: Date.now(), + username: username, + object_type: actionObjectType.TARGET, + object_name: targetName, + object_id: target_on, + text: `${actionDescription.TARGET} ${targetName} ${actionDescription.LOADED}` + }; + + return trackAction; + } +}; diff --git a/js/reducers/rootReducer.js b/js/reducers/rootReducer.js index a31dc2c45..f3c6a0c59 100644 --- a/js/reducers/rootReducer.js +++ b/js/reducers/rootReducer.js @@ -12,6 +12,7 @@ import { projectReducers } from '../components/projects/redux/reducer'; import { issueReducers } from '../components/userFeedback/redux/reducer'; import { datasetsReducers } from '../components/datasets/redux/reducer'; import { trackingReducers, undoableTrackingReducers } from './tracking/trackingReducers'; +import { nglTrackingReducers, undoableNglTrackingReducers } from './nglTracking/nglTrackingReducers'; const rootReducer = combineReducers({ apiReducers, @@ -24,7 +25,9 @@ const rootReducer = combineReducers({ issueReducers, datasetsReducers, trackingReducers, - undoableTrackingReducers + undoableTrackingReducers, + nglTrackingReducers, + undoableNglTrackingReducers }); export { rootReducer }; diff --git a/js/reducers/tracking/dispatchActions.js b/js/reducers/tracking/dispatchActions.js index 57c19f223..4b311e8ab 100644 --- a/js/reducers/tracking/dispatchActions.js +++ b/js/reducers/tracking/dispatchActions.js @@ -467,7 +467,7 @@ const getCurrentActionList = (orderedActionList, type, collection, currentAction if (collection) { collection.forEach(data => { - let action = actionList.find(action => action.object_id === data.id && action.dataset_id === data.datasetId); + let action = actionList.find(a => a.object_id === data.id && a.dataset_id === data.datasetId); if (action) { currentActions.push(Object.assign({ ...action })); @@ -972,8 +972,7 @@ const restoreCartActions = (orderedActionList, majorViewStage) => async (dispatc if (vectorCompoundActions) { vectorCompoundActions.forEach(action => { let data = action.item; - let compoundId = action.compoundId; - dispatch(handleShowVectorCompound({ isSelected: true, data, index: compoundId, majorViewStage: majorViewStage })); + dispatch(handleShowVectorCompound({ isSelected: true, data, majorViewStage: majorViewStage })); }); } }; @@ -1107,6 +1106,10 @@ const restoreRepresentationActions = (moleculesAction, stages) => (dispatch, get }; const restoreTabActions = moleculesAction => (dispatch, getState) => { + const state = getState(); + const customDatasets = state.datasetsReducers.datasets; + let firstCustomDatasetTitle = (customDatasets && customDatasets[0] && customDatasets[0].title) || ''; + let action = moleculesAction.find(action => action.type === actionType.TAB); if (action) { dispatch(setTabValue(action.oldObjectId, action.object_id, action.object_name, action.oldObjectName)); @@ -1122,6 +1125,15 @@ const restoreTabActions = moleculesAction => (dispatch, getState) => { indexAction.oldObjectName ) ); + } else { + if (action && action.object_id === 2 && action.object_name !== firstCustomDatasetTitle) { + let dataset = customDatasets.find(d => d.title === action.object_name); + var index = customDatasets.findIndex(d => d.title === action.object_name); + + if (dataset) { + dispatch(setSelectedDatasetIndex(index, index, dataset.title, dataset.title, true)); + } + } } let filterAction = moleculesAction.find(action => action.type === actionType.DATASET_FILTER); @@ -1936,8 +1948,7 @@ const handleVectorAction = (action, isSelected) => (dispatch, getState) => { const handleVectorCompoundAction = (action, isSelected, majorViewStage) => (dispatch, getState) => { if (action) { let data = action.item; - let compoundId = action.compoundId; - dispatch(handleShowVectorCompound({ isSelected, data, index: compoundId, majorViewStage: majorViewStage })); + dispatch(handleShowVectorCompound({ isSelected, data, majorViewStage: majorViewStage })); } }; @@ -2673,7 +2684,7 @@ export const updateTrackingActions = action => (dispatch, getState) => { actions: JSON.stringify(actions) }; return api({ - url: `${base_url}/api/session-actions/${actionID}`, + url: `${base_url}/api/session-actions/${actionID}/`, method: METHOD.PUT, data: JSON.stringify(dataToSend) }) @@ -2712,7 +2723,7 @@ export const setAndUpdateTrackingActions = (actionList, projectID) => (dispatch, actions: JSON.stringify(actions) }; return api({ - url: `${base_url}/api/session-actions/${actionID}`, + url: `${base_url}/api/session-actions/${actionID}/`, method: METHOD.PUT, data: JSON.stringify(dataToSend) }) diff --git a/js/reducers/tracking/trackingActions.js b/js/reducers/tracking/trackingActions.js index 59bd91bdc..6b60cd131 100644 --- a/js/reducers/tracking/trackingActions.js +++ b/js/reducers/tracking/trackingActions.js @@ -449,6 +449,7 @@ export const findTrackAction = (action, state) => { } else if (action.type === previewCompoundConstants.APPEND_SHOWED_COMPOUND_LIST) { let objectType = actionObjectType.COMPOUND; let objectName = action.item && action.item.vector; + let objectId = action.item && action.item.smiles; trackAction = { type: actionType.VECTOR_COUMPOUND_ADDED, @@ -457,7 +458,7 @@ export const findTrackAction = (action, state) => { username: username, object_type: objectType, object_name: objectName, - object_id: action.payload, + object_id: objectId, item: action.item, compoundId: action.payload, text: `${actionDescription.COMPOUND} ${objectName} ${actionDescription.ADDED}` @@ -465,6 +466,7 @@ export const findTrackAction = (action, state) => { } else if (action.type === previewCompoundConstants.REMOVE_SHOWED_COMPOUND_LIST) { let objectType = actionObjectType.COMPOUND; let objectName = action.item && action.item.vector; + let objectId = action.item && action.item.smiles; trackAction = { type: actionType.VECTOR_COUMPOUND_REMOVED, @@ -473,7 +475,7 @@ export const findTrackAction = (action, state) => { username: username, object_type: objectType, object_name: objectName, - object_id: action.payload, + object_id: objectId, item: action.item, compoundId: action.payload, text: `${actionDescription.COMPOUND} ${objectName} ${actionDescription.REMOVED}` diff --git a/js/utils/discourse.js b/js/utils/discourse.js new file mode 100644 index 000000000..a421336ad --- /dev/null +++ b/js/utils/discourse.js @@ -0,0 +1,76 @@ +import { DJANGO_CONTEXT } from './djangoContext'; +import { api, METHOD } from './api'; +import { base_url } from '../components/routes/constants'; + +const DEFAULT_PARENT_CATEGORY_NAME = 'Fragalysis targets'; +const DEFAULT_CATEGORY_COLOUR = '0088CC'; +const DEFAULT_TEXT_COLOUR = 'FFFFFF'; + +const getDiscourseRequestObject = (params = {}) => { + const request = { + category_name: '', + parent_category_name: DEFAULT_PARENT_CATEGORY_NAME, + category_colour: DEFAULT_CATEGORY_COLOUR, + category_text_colour: DEFAULT_TEXT_COLOUR, + post_title: '', + post_content: '', + post_tags: '{}' + }; + + return { ...request, ...params }; +}; + +export const isDiscourseAvailable = () => { + return DJANGO_CONTEXT && DJANGO_CONTEXT['authenticated'] && DJANGO_CONTEXT['discourse_available']; +}; + +export const isDiscourseUserAvailable = () => { + return DJANGO_CONTEXT && DJANGO_CONTEXT['user_present_on_discourse']; +}; + +export const getDiscourseURL = () => { + return DJANGO_CONTEXT && DJANGO_CONTEXT['discourse_host']; +}; + +export const generateDiscourseTargetURL = targetName => { + let jsonData = getDiscourseRequestObject({ category_name: targetName }); + return api({ + url: `${base_url}/api/discourse_post/`, + method: METHOD.POST, + data: jsonData + }); +}; + +export const createProjectPost = (projectName, targetName, msg, tags) => { + let jsonData = getDiscourseRequestObject({ + category_name: targetName, + post_title: projectName, + post_content: msg, + post_tags: JSON.stringify(tags) + }); + console.log(JSON.stringify(jsonData)); + return api({ + url: `${base_url}/api/discourse_post/`, + method: METHOD.POST, + data: jsonData + }); +}; + +export const getExistingPost = projectName => { + let jsonData = getDiscourseRequestObject({ + post_title: projectName + }); + console.log(JSON.stringify(jsonData)); + return api({ + url: `${base_url}/api/discourse_post/`, + method: METHOD.POST, + data: jsonData + }); +}; + +export const getProjectPosts = projectName => { + return api({ + url: `${base_url}/api/discourse_post/?post_title=${encodeURIComponent(projectName)}`, + method: METHOD.GET + }); +}; diff --git a/package.json b/package.json index 9da9152ec..905dc7488 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fragalysis-frontend", - "version": "0.9.69", + "version": "0.10.17", "description": "Frontend for fragalysis", "main": "webpack.config.js", "scripts": {