diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml old mode 100644 new mode 100755 diff --git a/docker-compose.localhost.yml b/docker-compose.localhost.yml index 49389d12c..2f75016ed 100644 --- a/docker-compose.localhost.yml +++ b/docker-compose.localhost.yml @@ -2,9 +2,9 @@ version: '3' services: mysql: - image: mysql:5.7.23 + image: mysql:5.7 volumes: - - ../data/mysql_data:/var/lib/mysql + - ../data/mysql_data:/var/lib/mysql environment: MYSQL_ROOT_PASSWORD: password MYSQL_DATABASE: django_db @@ -16,7 +16,7 @@ services: container_name: neo4j image: neo4j:3.5 ports: - # Comment these two out in produciton +# Comment these two out in production - "7474:7474" - "7687:7687" ulimits: @@ -36,7 +36,7 @@ services: volumes: - ../data/logs:/code/logs/ - ../data/media:/code/media/ - - ../fragalysis-frontend:/code/frontend + - ../fragalysis-frontend:/code/frontend/ - ../fragalysis-backend:/code/ ports: - "8080:80" @@ -45,15 +45,26 @@ services: - graph loader: container_name: loader - image: loader:latest + image: xchem/fragalysis-stack:latest + command: /bin/bash /code/docker-entrypoint.sh volumes: - - ../data/input:/fragalysis - - ../data/media:/code/media + - ../data/input:/fragalysis/ + - ../data/media:/code/media/ + - ../fragalysis-frontend:/code/frontend/ + - ../fragalysis-backend:/code/ + - ../dls-fragalysis-stack-openshift/images/loader/docker-entrypoint.sh:/code/docker-entrypoint.sh + - ../fragalysis-loader/run_loader.sh:/code/run_loader.sh + - ../fragalysis-loader/loader.py:/code/loader.py + - ../fragalysis-loader/test_entry.sh:/code/test_entry.sh + - ../fragalysis-loader/wait-for-it.sh:/code/wait-for-it.sh + - ../fragalysis-loader/database_check.py:/code/database_check.py + - ../fragalysis-loader/tests:/code/tests + - ../fragalysis-loader/loader:/code/loader environment: - MYSQL_ROOT_PASSWORD: password - MYSQL_DATABASE: django_db - MYSQL_PASSWORD: django_password - MYSQL_USER: django - DATA_ORIGIN: EXAMPLE + MYSQL_ROOT_PASSWORD: password + MYSQL_DATABASE: django_db + MYSQL_PASSWORD: django_password + MYSQL_USER: django + DATA_ORIGIN: EXAMPLE depends_on: - mysql diff --git a/js/components/header/index.js b/js/components/header/index.js index 669943711..6493dd828 100644 --- a/js/components/header/index.js +++ b/js/components/header/index.js @@ -23,10 +23,10 @@ import { Input, Person, Home, - Storage, SupervisorAccount, Menu as MenuIcon, - Work + Work, + Description } from '@material-ui/icons'; import { HeaderContext } from './headerContext'; import { Button } from '../common'; @@ -65,7 +65,7 @@ const useStyles = makeStyles(theme => ({ }, loadingPaper: { backgroundColor: theme.palette.background.default, - zIndex: 1, + zIndex: 1301, width: '100%', position: 'absolute', opacity: 0, @@ -296,13 +296,15 @@ export default memo( + - history.push(URLS.sessions)}> + history.push(URLS.projects)}> - + - + + history.push(URLS.management)}> diff --git a/js/components/helpers/useEnableUserInteracion.js b/js/components/helpers/useEnableUserInteracion.js index e7dd5afc4..afead7509 100644 --- a/js/components/helpers/useEnableUserInteracion.js +++ b/js/components/helpers/useEnableUserInteracion.js @@ -1,5 +1,6 @@ import { useState, useEffect } from 'react'; import { useSelector } from 'react-redux'; +import { VIEWS } from '../../constants/constants'; export const useDisableUserInteraction = () => { const [disableInteraction, setDisableInteraction] = useState(false); @@ -9,13 +10,19 @@ export const useDisableUserInteraction = () => { const countOfRemainingMoleculeGroups = useSelector(state => state.nglReducers.countOfRemainingMoleculeGroups); const proteinsHasLoaded = useSelector(state => state.nglReducers.proteinsHasLoaded); const countOfPendingNglObjects = useSelector(state => state.nglReducers.countOfPendingNglObjects); + const isLoadingTree = useSelector(state => state.projectReducers.isLoadingTree); + const isLoadingCurrentSnapshot = useSelector(state => state.projectReducers.isLoadingCurrentSnapshot); useEffect(() => { if ( + isLoadingTree === false && + isLoadingCurrentSnapshot === false && countOfPendingVectorLoadRequests === 0 && - countOfPendingNglObjects === 0 && + countOfPendingNglObjects[VIEWS.SUMMARY_VIEW] === 0 && + countOfPendingNglObjects[VIEWS.MAJOR_VIEW] === 0 && ((countOfRemainingMoleculeGroups === 0 && proteinsHasLoaded === true) || - (countOfRemainingMoleculeGroups === null && proteinsHasLoaded === null)) + (countOfRemainingMoleculeGroups === null && proteinsHasLoaded === null) || + (countOfRemainingMoleculeGroups === null && proteinsHasLoaded === true)) ) { if (disableInteraction === true) { setDisableInteraction(false); @@ -30,6 +37,8 @@ export const useDisableUserInteraction = () => { countOfPendingVectorLoadRequests, countOfRemainingMoleculeGroups, disableInteraction, + isLoadingCurrentSnapshot, + isLoadingTree, proteinsHasLoaded ]); diff --git a/js/components/landing/Landing.js b/js/components/landing/Landing.js index 18430f8c3..6f4b69f53 100644 --- a/js/components/landing/Landing.js +++ b/js/components/landing/Landing.js @@ -1,64 +1,65 @@ /** * Created by ricgillams on 21/06/2018. */ -import { Grid } from '@material-ui/core'; -import React, { memo, useEffect } from 'react'; +import { Grid, Link } from '@material-ui/core'; +import React, { memo, useContext, useEffect, useState } from 'react'; import TargetList from '../target/targetList'; -import SessionList from '../session/sessionList'; +//import SessionList from '../session/sessionList'; import { connect } from 'react-redux'; import * as apiActions from '../../reducers/api/actions'; import * as selectionActions from '../../reducers/selection/actions'; import { DJANGO_CONTEXT } from '../../utils/djangoContext'; +import { Projects } from '../projects'; +import { HeaderContext } from '../header/headerContext'; import { resetCurrentCompoundsSettings } from '../preview/compounds/redux/actions'; +import { resetProjectsReducer } from '../projects/redux/actions'; -const Landing = memo(({ resetSelectionState, resetTargetState, resetCurrentCompoundsSettings }) => { - let text_div; +const Landing = memo( + ({ resetSelectionState, resetTargetState, resetCurrentCompoundsSettings, resetProjectsReducer }) => { + const { setSnackBarTitle } = useContext(HeaderContext); + const [loginText, setLoginText] = useState("You're logged in as " + DJANGO_CONTEXT['username']); - if (DJANGO_CONTEXT['authenticated'] === true) { - var entry_text = "You're logged in as " + DJANGO_CONTEXT['username']; - text_div =

{entry_text}

; - } else { - text_div = ( -

- To view own targets login here: - - FedID Login - -

- ); - } + useEffect(() => { + if (DJANGO_CONTEXT['authenticated'] !== true) { + setLoginText( + <> + {'To view own targets login here: '} + + FedID Login + + + ); + } + }, []); - useEffect(() => { - resetTargetState(); - resetSelectionState(); - resetCurrentCompoundsSettings(true); - }, [resetTargetState, resetSelectionState, resetCurrentCompoundsSettings]); + useEffect(() => { + resetTargetState(); + resetSelectionState(); + setSnackBarTitle(loginText); + resetCurrentCompoundsSettings(true); + resetProjectsReducer(); + }, [ + resetTargetState, + resetSelectionState, + setSnackBarTitle, + loginText, + resetCurrentCompoundsSettings, + resetProjectsReducer + ]); - return ( - - - -

Welcome to Fragalysis

- {text_div} + return ( + + + - -

- - Target status overview - {' '} - (only accessible within Diamond) -

+ + {/**/} +
- - - - - - -
- ); -}); + ); + } +); function mapStateToProps(state) { return {}; @@ -66,7 +67,8 @@ function mapStateToProps(state) { const mapDispatchToProps = { resetSelectionState: selectionActions.resetSelectionState, resetTargetState: apiActions.resetTargetState, - resetCurrentCompoundsSettings + resetCurrentCompoundsSettings, + resetProjectsReducer }; export default connect(mapStateToProps, mapDispatchToProps)(Landing); diff --git a/js/components/preview/Preview.js b/js/components/preview/Preview.js index 66314f1ac..3a2430d10 100644 --- a/js/components/preview/Preview.js +++ b/js/components/preview/Preview.js @@ -2,7 +2,7 @@ * Created by abradley on 14/04/2018. */ -import React, { memo, useEffect, useRef, useState } from 'react'; +import React, { memo, useContext, useEffect, useRef, useState } from 'react'; import { Grid, makeStyles, useTheme } from '@material-ui/core'; import NGLView from '../nglView/nglView'; import MoleculeList from './molecule/moleculeList'; @@ -12,13 +12,17 @@ import { CompoundList } from './compounds/compoundList'; import { ViewerControls } from './viewerControls'; import { ComputeSize } from '../../utils/computeSize'; import { withUpdatingTarget } from '../target/withUpdatingTarget'; -import ModalStateSave from '../session/modalStateSave'; import { VIEWS } from '../../constants/constants'; import { withLoadingProtein } from './withLoadingProtein'; -import { withSessionManagement } from '../session/withSessionManagement'; +import { withSnapshotManagement } from '../snapshot/withSnapshotManagement'; import { useDispatch } from 'react-redux'; -import { removeAllNglComponents } from '../../reducers/ngl/actions'; -import { resetCurrentCompoundsSettings } from './compounds/redux/actions'; +import { ProjectHistory } from './projectHistory'; +import { ProjectDetailDrawer } from '../projects/projectDetailDrawer'; +import { NewSnapshotModal } from '../snapshot/modals/newSnapshotModal'; +import { HeaderContext } from '../header/headerContext'; +import { unmountPreviewComponent } from './redux/dispatchActions'; +import { NglContext } from '../nglView/nglProvider'; +import { SaveSnapshotBeforeExit } from '../snapshot/modals/saveSnapshotBeforeExit'; //import HotspotList from '../hotspot/hotspotList'; const hitNavigatorWidth = 504; @@ -59,10 +63,12 @@ const useStyles = makeStyles(theme => ({ } })); -const Preview = memo(({ isStateLoaded, headerHeight }) => { +const Preview = memo(({ isStateLoaded, hideProjects }) => { const classes = useStyles(); const theme = useTheme(); + const { headerHeight } = useContext(HeaderContext); + const { nglViewList } = useContext(NglContext); const nglViewerControlsRef = useRef(null); const dispatch = useDispatch(); @@ -79,15 +85,19 @@ const Preview = memo(({ isStateLoaded, headerHeight }) => { const [summaryViewHeight, setSummaryViewHeight] = useState(0); - const compoundHeight = `calc(100vh - ${headerHeight}px - ${theme.spacing(2)}px - ${summaryViewHeight}px - 64px)`; + const [projectHistoryHeight, setProjectHistoryHeight] = useState(0); + + const compoundHeight = `calc(100vh - ${headerHeight}px - ${theme.spacing( + 2 + )}px - ${summaryViewHeight}px - ${projectHistoryHeight}px - 72px)`; + const [showHistory, setShowHistory] = useState(false); useEffect(() => { // Unmount Preview - reset NGL state return () => { - dispatch(removeAllNglComponents()); - dispatch(resetCurrentCompoundsSettings(true)); + dispatch(unmountPreviewComponent(nglViewList)); }; - }, [dispatch]); + }, [dispatch, nglViewList]); return ( <> @@ -95,7 +105,11 @@ const Preview = memo(({ isStateLoaded, headerHeight }) => { {/* Hit cluster selector */} - + {/* Hit navigator */} @@ -103,6 +117,7 @@ const Preview = memo(({ isStateLoaded, headerHeight }) => { height={moleculeListHeight} setFilterItemsHeight={setFilterItemsHeight} filterItemsHeight={filterItemsHeight} + hideProjects={hideProjects} /> @@ -129,14 +144,24 @@ const Preview = memo(({ isStateLoaded, headerHeight }) => { + {!hideProjects && ( + + setShowHistory(!showHistory)} + /> + + )}
{/* */}
- + + + {!hideProjects && } ); }); -export default withSessionManagement(withUpdatingTarget(withLoadingProtein(Preview))); +export default withSnapshotManagement(withUpdatingTarget(withLoadingProtein(Preview))); diff --git a/js/components/preview/molecule/moleculeList.js b/js/components/preview/molecule/moleculeList.js index 431db8add..24e69ff48 100644 --- a/js/components/preview/molecule/moleculeList.js +++ b/js/components/preview/molecule/moleculeList.js @@ -199,7 +199,8 @@ const MoleculeList = memo( filterSettings, sortDialogOpen, setSortDialogOpen, - firstLoad + firstLoad, + hideProjects }) => { const classes = useStyles(); const dispatch = useDispatch(); @@ -262,7 +263,14 @@ const MoleculeList = memo( console.log('initializing filter'); setPredefinedFilter(dispatch(initializeFilter()).predefined); // initialize molecules on first target load - if (stage && cached_mol_lists && cached_mol_lists[mol_group_on] && firstLoadRef && firstLoadRef.current) { + if ( + stage && + cached_mol_lists && + cached_mol_lists[mol_group_on] && + firstLoadRef && + firstLoadRef.current && + hideProjects + ) { console.log('initializing molecules'); firstLoadRef.current = false; dispatch(setFirstLoad(false)); diff --git a/js/components/preview/molecule/moleculeView.js b/js/components/preview/molecule/moleculeView.js index 5eb8642c7..0079b078b 100644 --- a/js/components/preview/molecule/moleculeView.js +++ b/js/components/preview/molecule/moleculeView.js @@ -28,9 +28,6 @@ import { } from './redux/dispatchActions'; import { base_url } from '../../routes/constants'; import { moleculeProperty } from './helperConstants'; -import { api } from '../../../utils/api'; -import { generateObjectList } from '../../session/helpers'; -import { getTotalCountOfCompounds } from './molecules_helpers'; const useStyles = makeStyles(theme => ({ container: { diff --git a/js/components/preview/moleculeGroups/redux/dispatchActions.js b/js/components/preview/moleculeGroups/redux/dispatchActions.js index e6c9fe59c..4af96ca29 100644 --- a/js/components/preview/moleculeGroups/redux/dispatchActions.js +++ b/js/components/preview/moleculeGroups/redux/dispatchActions.js @@ -4,7 +4,7 @@ import { decrementCountOfRemainingMoleculeGroupsWithSavingDefaultState, deleteObject, loadObject, - reloadNglViewFromScene + reloadNglViewFromSnapshot } from '../../../../reducers/ngl/dispatchActions'; import { getJoinedMoleculeList } from '../../molecule/redux/selectors'; import { @@ -27,7 +27,6 @@ import { removeMoleculeOrientation, setCountOfRemainingMoleculeGroups } from '.. import { setMolGroupList, setMolGroupOn } from '../../../../reducers/api/actions'; import { getUrl, loadFromServer } from '../../../../utils/genericList'; import { OBJECT_TYPE } from '../../../nglView/constants'; -import { SCENES } from '../../../../reducers/ngl/constants'; import { setSortDialogOpen } from '../../molecule/redux/actions'; import { resetCurrentCompoundsSettings } from '../../compounds/redux/actions'; @@ -94,12 +93,12 @@ export const clearAfterDeselectingMoleculeGroup = ({ molGroupId, currentMolGroup }); }; -export const saveMoleculeGroupsToNglView = (molGroupList, stage) => dispatch => { +export const saveMoleculeGroupsToNglView = (molGroupList, stage, projectId) => dispatch => { if (molGroupList) { dispatch(setCountOfRemainingMoleculeGroups(molGroupList.length)); molGroupList.map(data => dispatch(loadObject(Object.assign({ display_div: VIEWS.SUMMARY_VIEW }, generateSphere(data)), stage)).then(() => - dispatch(decrementCountOfRemainingMoleculeGroupsWithSavingDefaultState()) + dispatch(decrementCountOfRemainingMoleculeGroupsWithSavingDefaultState(projectId)) ) ); } @@ -139,10 +138,15 @@ export const selectFirstMolGroup = ({ summaryView }) => (dispatch, getState) => } }; -export const loadMoleculeGroups = ({ summaryView, setOldUrl, oldUrl, onCancel, isStateLoaded }) => ( - dispatch, - getState -) => { +export const loadMoleculeGroups = ({ + summaryView, + setOldUrl, + oldUrl, + onCancel, + isStateLoaded, + projectId, + hideProjects +}) => (dispatch, getState) => { const state = getState(); const group_type = state.apiReducers.group_type; const target_on = state.apiReducers.target_on; @@ -153,11 +157,13 @@ export const loadMoleculeGroups = ({ summaryView, setOldUrl, oldUrl, onCancel, i url: getUrl({ list_type, target_on, group_type }), setOldUrl: url => setOldUrl(url), old_url: oldUrl, - afterPush: data_list => dispatch(saveMoleculeGroupsToNglView(data_list, summaryView)), + afterPush: data_list => dispatch(saveMoleculeGroupsToNglView(data_list, summaryView, projectId)), list_type, setObjectList: async mol_group_list => { await dispatch(setMolGroupList(mol_group_list)); - dispatch(selectFirstMolGroup({ summaryView })); + if (hideProjects) { + dispatch(selectFirstMolGroup({ summaryView })); + } }, cancel: onCancel }); @@ -168,17 +174,14 @@ export const loadMoleculeGroups = ({ summaryView, setOldUrl, oldUrl, onCancel, i return Promise.resolve(); }; -export const clearMoleculeGroupSelection = ({ getNglView }) => dispatch => { +export const clearMoleculeGroupSelection = ({ getNglView }) => (dispatch, getState) => { // Reset NGL VIEWS to default state const majorViewStage = getNglView(VIEWS.MAJOR_VIEW) && getNglView(VIEWS.MAJOR_VIEW).stage; const summaryViewStage = getNglView(VIEWS.SUMMARY_VIEW) && getNglView(VIEWS.SUMMARY_VIEW).stage; + const snapshot = getState().projectReducers.currentSnapshot.data.nglReducers; - dispatch(reloadNglViewFromScene(majorViewStage, VIEWS.MAJOR_VIEW, SCENES.defaultScene)).catch(error => { - throw new Error(error); - }); - dispatch(reloadNglViewFromScene(summaryViewStage, VIEWS.SUMMARY_VIEW, SCENES.defaultScene)).catch(error => { - throw new Error(error); - }); + dispatch(reloadNglViewFromSnapshot(majorViewStage, VIEWS.MAJOR_VIEW, snapshot)); + dispatch(reloadNglViewFromSnapshot(summaryViewStage, VIEWS.SUMMARY_VIEW, snapshot)); // Reset selection reducer // remove sites selection diff --git a/js/components/preview/moleculeGroups/withLoadingMolGroupList.js b/js/components/preview/moleculeGroups/withLoadingMolGroupList.js index 30fd134cc..6da1c0c02 100644 --- a/js/components/preview/moleculeGroups/withLoadingMolGroupList.js +++ b/js/components/preview/moleculeGroups/withLoadingMolGroupList.js @@ -6,15 +6,19 @@ import { useDispatch } from 'react-redux'; import { VIEWS } from '../../../constants/constants'; import { NglContext } from '../../nglView/nglProvider'; import { loadMoleculeGroups } from './redux/dispatchActions'; +import { useRouteMatch } from 'react-router-dom'; // is responsible for loading molecules list export const withLoadingMolGroupList = WrappedComponent => { - return memo(({ isStateLoaded, ...rest }) => { + return memo(({ isStateLoaded, hideProjects, ...rest }) => { + const [state, setState] = useState(); const [wasLoaded, setWasLoaded] = useState(false); const { getNglView } = useContext(NglContext); const [oldUrl, setOldUrl] = useState(''); const onCancel = useCallback(() => {}, []); + let match = useRouteMatch(); + const projectId = match && match.params && match.params.projectId; const dispatch = useDispatch(); @@ -28,10 +32,14 @@ export const withLoadingMolGroupList = WrappedComponent => { setOldUrl, oldUrl: oldUrl.current, onCancel, - isStateLoaded + isStateLoaded, + projectId, + hideProjects }) ).catch(error => { - throw new Error(error); + setState(() => { + throw error; + }); }); setWasLoaded(true); } @@ -39,7 +47,7 @@ export const withLoadingMolGroupList = WrappedComponent => { return () => { onCancel(); }; - }, [isStateLoaded, onCancel, dispatch, oldUrl, getNglView, wasLoaded]); + }, [isStateLoaded, hideProjects, onCancel, dispatch, oldUrl, getNglView, projectId, wasLoaded]); return ; }); diff --git a/js/components/preview/projectHistory/index.js b/js/components/preview/projectHistory/index.js new file mode 100644 index 000000000..199458e87 --- /dev/null +++ b/js/components/preview/projectHistory/index.js @@ -0,0 +1,234 @@ +import React, { memo, useContext, useEffect, useRef } from 'react'; +import { Panel } from '../../common/Surfaces/Panel'; +import { + Grid, + IconButton, + Typography, + Table, + TableBody, + TableRow, + TableCell, + TableHead, + makeStyles +} from '@material-ui/core'; +import { templateExtend, TemplateName, Orientation, Gitgraph } from '@gitgraph/react'; +import { MergeType, Share } from '@material-ui/icons'; +import { Button } from '../../common/Inputs/Button'; +import { useDispatch, useSelector } from 'react-redux'; +import { useHistory, useRouteMatch } from 'react-router-dom'; +import { base_url, URLS } from '../../routes/constants'; +import { loadSnapshotTree } from '../../projects/redux/dispatchActions'; +import palette from '../../../theme/palette'; +import { ModalShareSnapshot } from '../../snapshot/modals/modalShareSnapshot'; +import { setIsOpenModalBeforeExit, setSelectedSnapshotToSwitch, setSharedSnapshot } from '../../snapshot/redux/actions'; +import { resetReducersBetweenSnapshots, switchBetweenSnapshots } from '../redux/dispatchActions'; +import { NglContext } from '../../nglView/nglProvider'; + +export const heightOfProjectHistory = '164px'; + +const useStyles = makeStyles(theme => ({ + containerExpanded: { + width: '100%', + height: heightOfProjectHistory, + overflow: 'auto' + }, + containerCollapsed: { + height: 0 + }, + nglViewItem: { + paddingLeft: theme.spacing(1) / 2 + }, + checklistItem: { + height: '100%' + } +})); + +const template = templateExtend(TemplateName.Metro, { + branch: { + lineWidth: 3, + spacing: 12, + label: { + font: 'normal 8pt Arial', + pointerWidth: 100, + display: false + } + }, + commit: { + message: { + displayHash: false, + font: 'normal 10pt Arial', + displayAuthor: false + }, + spacing: 24, + dot: { + size: 8 + } + }, + + tag: { + font: 'normal 8pt Arial', + color: palette.primary.contrastText, + bgColor: palette.primary.main + } +}); + +const options = { + template, + orientation: Orientation.Horizontal +}; + +export const ProjectHistory = memo(({ setHeight, showFullHistory }) => { + const classes = useStyles(); + const ref = useRef(null); + let history = useHistory(); + const { nglViewList } = useContext(NglContext); + const dispatch = useDispatch(); + let match = useRouteMatch(); + const projectID = match && match.params && match.params.projectId; + const snapshotId = match && match.params && match.params.snapshotId; + + const currentProjectID = useSelector(state => state.projectReducers.currentProject.projectID); + const currentSnapshotID = useSelector(state => state.projectReducers.currentSnapshot.id); + const currentSnapshotTitle = useSelector(state => state.projectReducers.currentSnapshot.title); + const currentSnapshotDescription = useSelector(state => state.projectReducers.currentSnapshot.description); + const currentSnapshotList = useSelector(state => state.projectReducers.currentSnapshotList); + const currentSnapshotTree = useSelector(state => state.projectReducers.currentSnapshotTree); + const isLoadingTree = useSelector(state => state.projectReducers.isLoadingTree); + + const handleClickOnCommit = commit => { + dispatch(setSelectedSnapshotToSwitch(commit.hash)); + dispatch(setIsOpenModalBeforeExit(true)); + }; + + const commitFunction = ({ title, hash, isSelected = false }) => ({ + hash: `${hash}`, + subject: `${title}`, + onMessageClick: handleClickOnCommit, + onClick: handleClickOnCommit, + style: + (isSelected === true && { dot: { size: 10, color: 'red', strokeColor: 'blue', strokeWidth: 2 } }) || undefined + }); + + const renderTreeNode = (childID, gitgraph, parentBranch) => { + const node = currentSnapshotList[childID]; + if (node !== undefined) { + const newBranch = gitgraph.branch({ + name: node.title, + from: parentBranch + }); + + newBranch.commit( + commitFunction({ + title: node.title || '', + hash: node.id, + isSelected: currentSnapshotID === node.id + }) + ); + + node.children.forEach(childID => { + renderTreeNode(childID, gitgraph, newBranch); + }); + } + }; + + useEffect(() => { + if (currentSnapshotID !== null) { + dispatch(loadSnapshotTree(projectID)).catch(error => { + throw new Error(error); + }); + } + }, [currentSnapshotID, dispatch, projectID, snapshotId]); + + return ( + <> + } + onClick={() => { + dispatch( + setSharedSnapshot({ + title: currentSnapshotTitle, + description: currentSnapshotDescription, + url: `${base_url}${URLS.projects}${currentProjectID}/${currentSnapshotID}` + }) + ); + }} + > + Share + , + + ]} + hasExpansion + defaultExpanded + onExpandChange={expand => { + if (ref.current && setHeight instanceof Function) { + setHeight(ref.current.offsetHeight); + } + }} + > +
+ {isLoadingTree === false && + currentSnapshotTree !== null && + currentSnapshotTree.children !== null && + currentSnapshotTree.title !== null && + currentSnapshotTree.id !== null && + currentSnapshotID !== null && + currentSnapshotList !== null && ( + + {gitgraph => { + const initBranch = gitgraph.branch(currentSnapshotTree.title); + + initBranch.commit( + commitFunction({ + title: currentSnapshotTree.title || '', + hash: currentSnapshotTree.id, + isSelected: currentSnapshotID === currentSnapshotTree.id + }) + ); + + currentSnapshotTree.children.forEach(childID => { + renderTreeNode(childID, gitgraph, initBranch); + }); + }} + + )} +
+ {/**/} + {/* */} + {/* */} + {/* */} + {/* Title*/} + {/* Author*/} + {/* Created*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* {snapshotDetail.name}*/} + {/* */} + {/* */} + {/* {snapshotDetail.author && snapshotDetail.author.username},*/} + {/* {snapshotDetail.author && snapshotDetail.author.email}*/} + {/* */} + {/* */} + {/* {snapshotDetail.created && moment(snapshotDetail.created).format('LLL')}*/} + {/* */} + {/* */} + {/* */} + {/*
*/} + {/*
*/} +
+ + + ); +}); diff --git a/js/components/preview/redux/dispatchActions.js b/js/components/preview/redux/dispatchActions.js index eca1dcb39..ac0001336 100644 --- a/js/components/preview/redux/dispatchActions.js +++ b/js/components/preview/redux/dispatchActions.js @@ -1,8 +1,13 @@ import { generateProteinObject } from '../../nglView/generatingObjects'; import { SUFFIX, VIEWS } from '../../../constants/constants'; -import { loadObject, setProteinsHasLoaded, setOrientation } from '../../../reducers/ngl/dispatchActions'; +import { loadObject, setOrientation } from '../../../reducers/ngl/dispatchActions'; import { reloadSummaryReducer } from '../summary/redux/actions'; -import { reloadCompoundsReducer } from '../compounds/redux/actions'; +import { reloadCompoundsReducer, resetCurrentCompoundsSettings } from '../compounds/redux/actions'; +import { removeAllNglComponents, setProteinLoadingState } from '../../../reducers/ngl/actions'; +import { createInitialSnapshot, reloadSession } from '../../snapshot/redux/dispatchActions'; +import { resetLoadedSnapshots, resetProjectsReducer } from '../../projects/redux/actions'; +import { resetSelectionState } from '../../../reducers/selection/actions'; +import { URLS } from '../../routes/constants'; // import { reloadMoleculeReducer } from '../molecule/redux/actions'; const loadProtein = nglView => (dispatch, getState) => { @@ -29,15 +34,29 @@ const loadProtein = nglView => (dispatch, getState) => { return Promise.reject('Cannot load Protein to NGL View ID ', nglView.id); }; -export const shouldLoadProtein = (nglViewList, isStateLoaded) => (dispatch, getState) => { +export const shouldLoadProtein = ({ + nglViewList, + isStateLoaded, + routeProjectID, + routeSnapshotID, + currentSnapshotID, + isLoadingCurrentSnapshot +}) => (dispatch, getState) => { const state = getState(); const targetIdList = state.apiReducers.target_id_list; const targetOnName = state.apiReducers.target_on_name; - - if (targetIdList && targetIdList.length > 0 && nglViewList && nglViewList.length > 0) { + const currentSnapshotData = state.projectReducers.currentSnapshot.data; + // const isLoadingCurrentSnapshot = state.projectReducers.isLoadingCurrentSnapshot; + if ( + targetIdList && + targetIdList.length > 0 && + nglViewList && + nglViewList.length > 0 && + isLoadingCurrentSnapshot === false + ) { // 1. Generate new protein or skip this action and everything will be loaded from session - if (!isStateLoaded) { - dispatch(setProteinsHasLoaded(false)); + if (!isStateLoaded && currentSnapshotID === null && !routeSnapshotID) { + dispatch(setProteinLoadingState(false)); Promise.all( nglViewList.map(nglView => dispatch(loadProtein(nglView)).finally(() => { @@ -45,11 +64,27 @@ export const shouldLoadProtein = (nglViewList, isStateLoaded) => (dispatch, getS }) ) ) - .then(() => dispatch(setProteinsHasLoaded(true))) - .catch(() => dispatch(setProteinsHasLoaded(false))); - } else { - dispatch(setProteinsHasLoaded(true, true)); + .then(() => { + dispatch(setProteinLoadingState(true)); + if (getState().nglReducers.countOfRemainingMoleculeGroups === 0) { + dispatch(createInitialSnapshot(routeProjectID)); + } + }) + .catch(error => { + dispatch(setProteinLoadingState(false)); + throw new Error(error); + }); + } + + // decide to load existing snapshot + else if ( + currentSnapshotID !== null && + (!routeSnapshotID || routeSnapshotID === currentSnapshotID.toString()) && + currentSnapshotData !== null + ) { + dispatch(reloadSession(currentSnapshotData, nglViewList)); } + if (targetOnName !== undefined) { document.title = targetOnName + ': Fragalysis'; } @@ -59,5 +94,37 @@ export const shouldLoadProtein = (nglViewList, isStateLoaded) => (dispatch, getS export const reloadPreviewReducer = newState => dispatch => { dispatch(reloadSummaryReducer(newState.summary)); dispatch(reloadCompoundsReducer(newState.compounds)); - // dispatch(reloadMoleculeReducer(newState.molecule)); +}; + +export const unmountPreviewComponent = (stages = []) => dispatch => { + stages.forEach(stage => { + if (stage.stage !== undefined || stage.stage !== null) { + dispatch(removeAllNglComponents(stage.stage)); + } + }); + + dispatch(resetCurrentCompoundsSettings(true)); + dispatch(resetProjectsReducer()); + + dispatch(resetSelectionState()); +}; + +export const resetReducersBetweenSnapshots = (stages = []) => dispatch => { + stages.forEach(stage => { + if (stage.stage !== undefined || stage.stage !== null) { + dispatch(removeAllNglComponents(stage.stage)); + } + }); + + dispatch(resetLoadedSnapshots()); + dispatch(resetSelectionState()); +}; + +export const switchBetweenSnapshots = ({ nglViewList, projectID, snapshotID, history }) => (dispatch, getState) => { + if (projectID && snapshotID) { + dispatch(resetReducersBetweenSnapshots(nglViewList)); + history.push(`${URLS.projects}${projectID}/${snapshotID}`); + } else { + throw new Error('ProjectID or SnapshotID is missing!'); + } }; diff --git a/js/components/preview/withLoadingProtein.js b/js/components/preview/withLoadingProtein.js index 76b26b884..0f7300e48 100644 --- a/js/components/preview/withLoadingProtein.js +++ b/js/components/preview/withLoadingProtein.js @@ -2,27 +2,43 @@ * Created by abradley on 13/03/2018. */ import React, { memo, useContext, useEffect } from 'react'; -import { connect } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { NglContext } from '../nglView/nglProvider'; import { shouldLoadProtein } from './redux/dispatchActions'; +import { useRouteMatch } from 'react-router-dom'; // is responsible for loading molecules list export const withLoadingProtein = WrappedComponent => { - const ProteinLoader = memo(({ isStateLoaded, shouldLoadProtein, ...rest }) => { + return memo(({ isStateLoaded, ...rest }) => { const { nglViewList } = useContext(NglContext); + let match = useRouteMatch(); + const dispatch = useDispatch(); + const routeProjectID = match && match.params && match.params.projectId; + const routeSnapshotID = match && match.params && match.params.snapshotId; + const currentSnapshotID = useSelector(state => state.projectReducers.currentSnapshot.id); + const isLoadingCurrentSnapshot = useSelector(state => state.projectReducers.isLoadingCurrentSnapshot); useEffect(() => { - shouldLoadProtein(nglViewList, isStateLoaded); - }, [isStateLoaded, nglViewList, shouldLoadProtein]); + dispatch( + shouldLoadProtein({ + nglViewList, + isStateLoaded, + routeProjectID, + routeSnapshotID, + currentSnapshotID, + isLoadingCurrentSnapshot + }) + ); + }, [ + dispatch, + isStateLoaded, + nglViewList, + routeProjectID, + routeSnapshotID, + currentSnapshotID, + isLoadingCurrentSnapshot + ]); return ; }); - - function mapStateToProps(state) { - return {}; - } - const mapDispatchToProps = { - shouldLoadProtein - }; - return connect(mapStateToProps, mapDispatchToProps)(ProteinLoader); }; diff --git a/js/components/projects/addProjectDetail/index.js b/js/components/projects/addProjectDetail/index.js new file mode 100644 index 000000000..4bb53180a --- /dev/null +++ b/js/components/projects/addProjectDetail/index.js @@ -0,0 +1,156 @@ +import React, { memo, useState } from 'react'; +import { Grid, makeStyles, Typography } from '@material-ui/core'; +import { DJANGO_CONTEXT } from '../../../utils/djangoContext'; +import { Form, Formik } from 'formik'; +import { TextField } from 'formik-material-ui'; +import { InputFieldAvatar } from '../projectModal/inputFieldAvatar'; +import { Description, Label, Title } 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'; + +const useStyles = makeStyles(theme => ({ + body: { + width: '100%', + marginTop: theme.spacing(1), + marginBottom: theme.spacing(1) + }, + input: { + width: 400 + }, + margin: { + margin: theme.spacing(1) + }, + formControl: { + margin: theme.spacing(1), + width: 400 + } +})); + +export const AddProjectDetail = memo(({ handleCloseModal }) => { + const classes = useStyles(); + const [state, setState] = useState(); + + const dispatch = useDispatch(); + const targetId = useSelector(state => state.apiReducers.target_on); + const isProjectModalLoading = useSelector(state => state.projectReducers.isProjectModalLoading); + + const [tags, setTags] = React.useState([]); + + return ( + <> + Project Details + { + const errors = {}; + if (!values.title) { + errors.title = 'Required!'; + } + if (!values.description) { + errors.description = 'Required!'; + } + return errors; + }} + onSubmit={values => { + const data = { + title: values.title, + description: values.description, + target: targetId, + author: DJANGO_CONTEXT['pk'] || null, + tags: JSON.stringify(tags) + }; + dispatch(createProjectFromSnapshotDialog(data)).catch(error => { + setState(() => { + throw error; + }); + }); + }} + > + {({ submitForm }) => ( +
+ + + } + field={ + + } + /> + + + } + field={ + + } + /> + + + } + field={ + option} + onChange={(e, data) => { + setTags(data); + }} + disabled={isProjectModalLoading} + renderInput={params => ( + { + if (e.key === 'Enter') { + setTags([...tags, e.target.value]); + } + }} + /> + )} + /> + } + /> + + + + + + + + + + +
+ )} +
+ + ); +}); diff --git a/js/components/projects/index.js b/js/components/projects/index.js new file mode 100644 index 000000000..01e0e51dc --- /dev/null +++ b/js/components/projects/index.js @@ -0,0 +1,192 @@ +import React, { memo, useEffect } from 'react'; +import { Panel } from '../common/Surfaces/Panel'; +import { + Table, + makeStyles, + TableBody, + TableHead, + TableCell, + TableRow, + TablePagination, + TableFooter, + IconButton, + InputAdornment, + TextField, + Chip, + Tooltip, + Zoom +} from '@material-ui/core'; +import { Delete, Add, Search } from '@material-ui/icons'; +import { Link } from 'react-router-dom'; +import { debounce } from 'lodash'; +import { URLS } from '../routes/constants'; +import moment from 'moment'; +import { setProjectModalOpen } from './redux/actions'; +import { useDispatch, useSelector } from 'react-redux'; +import { ProjectModal } from './projectModal'; +import { loadListOfProjects, removeProject, searchInProjects } from './redux/dispatchActions'; +import { DJANGO_CONTEXT } from '../../utils/djangoContext'; + +const useStyles = makeStyles(theme => ({ + table: { + minWidth: 650 + }, + search: { + margin: theme.spacing(1) + }, + chip: { + margin: theme.spacing(1) / 2 + } +})); + +export const Projects = memo(({}) => { + const classes = useStyles(); + const [page, setPage] = React.useState(0); + const [rowsPerPage, setRowsPerPage] = React.useState(10); + const dispatch = useDispatch(); + + const listOfProjects = useSelector(state => state.projectReducers.listOfProjects).map(project => { + return { + id: project.id, + name: project.title, + tags: JSON.parse(project.tags), + target: project.target.title, + createdAt: project.init_date, + author: (project.author && project.author.email) || '-', + description: project.description + }; + }); + + useEffect(() => { + dispatch(loadListOfProjects()).catch(error => { + throw new Error(error); + }); + }, [dispatch]); + + const handleChangePage = (event, newPage) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = event => { + setRowsPerPage(parseInt(event.target.value, 10)); + setPage(0); + }; + + let debouncedFn; + + const handleSearch = event => { + /* signal to React not to nullify the event object */ + event.persist(); + if (!debouncedFn) { + debouncedFn = debounce(() => { + dispatch(searchInProjects(event.target.value)).catch(error => { + throw new Error(error); + }); + }, 500); + } + debouncedFn(); + }; + + return ( + <> + + + + ) + }} + onChange={handleSearch} + />, + dispatch(setProjectModalOpen(true))} + disabled={DJANGO_CONTEXT['username'] === 'NOT_LOGGED_IN'} + > + + + ]} + > + + + + Name + Target + Tags + Author + Created at + Actions + + + + {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')} + + + dispatch(removeProject(project.id)).catch(error => { + throw new Error(error); + }) + } + > + + + + + + ))} + + + + + + +
+
+ + + ); +}); diff --git a/js/components/projects/projectDetailDrawer/index.js b/js/components/projects/projectDetailDrawer/index.js new file mode 100644 index 000000000..7408b9bbd --- /dev/null +++ b/js/components/projects/projectDetailDrawer/index.js @@ -0,0 +1,208 @@ +import React, { memo, useContext } from 'react'; +import { IconButton, makeStyles, Drawer, Typography, Grid } from '@material-ui/core'; +import { Share, Close } from '@material-ui/icons'; +import { Gitgraph, templateExtend, TemplateName } from '@gitgraph/react'; +import { base_url, URLS } from '../../routes/constants'; +import moment from 'moment'; +import Modal from '../../common/Modal'; +import { useDispatch, useSelector } from 'react-redux'; +import { useHistory, useRouteMatch } from 'react-router-dom'; +import palette from '../../../theme/palette'; +import { setIsOpenModalBeforeExit, setSelectedSnapshotToSwitch, setSharedSnapshot } from '../../snapshot/redux/actions'; +import { NglContext } from '../../nglView/nglProvider'; + +const useStyles = makeStyles(theme => ({ + drawer: { + height: 400, + overflow: 'auto' + }, + thumbnail: { + float: 'left', + paddingLeft: 12, + paddingRight: 12, + paddingTop: 12, + width: 66 + }, + historyHeader: { + backgroundColor: theme.palette.primary.main, + color: theme.palette.primary.contrastText, + position: 'fixed', + width: '100%' + }, + historyBody: { + marginTop: 49 + }, + headerTitle: { + padding: theme.spacing(1) + } +})); + +const myTemplate = templateExtend(TemplateName.Metro, { + branch: { + lineWidth: 3, + spacing: 25, + label: { + font: 'normal 8pt Arial', + pointerWidth: 100, + display: false + } + }, + commit: { + message: { + displayHash: false, + font: 'normal 10pt Arial', + displayAuthor: false + }, + spacing: 15, + dot: { + size: 8 + } + }, + tag: { + font: 'normal 8pt Arial', + color: palette.primary.contrastText, + bgColor: palette.primary.main + } +}); + +const options = { + template: myTemplate +}; + +export const ProjectDetailDrawer = memo(({ showHistory, setShowHistory }) => { + const [open, setOpen] = React.useState(false); + const classes = useStyles(); + let history = useHistory(); + let match = useRouteMatch(); + const { nglViewList } = useContext(NglContext); + const dispatch = useDispatch(); + const projectID = match && match.params && match.params.projectId; + const currentProjectID = useSelector(state => state.projectReducers.currentProject.projectID); + const currentSnapshotID = useSelector(state => state.projectReducers.currentSnapshot.id); + const currentSnapshotList = useSelector(state => state.projectReducers.currentSnapshotList); + const currentSnapshotTree = useSelector(state => state.projectReducers.currentSnapshotTree); + const isLoadingTree = useSelector(state => state.projectReducers.isLoadingTree); + + const handleClickOnCommit = commit => { + dispatch(setSelectedSnapshotToSwitch(commit.hash)); + dispatch(setIsOpenModalBeforeExit(true)); + }; + + const commitFunction = ({ title, description, photo, author, email, hash, isSelected, created }) => ({ + hash: `${hash}`, + subject: `${title}`, + body: ( + <> + {/* setOpen(true)} />*/} + {/**/} + {/* */} + {/**/} + {/*
*/} + + {`${moment(created).format('LLL')}, ${email}: `} + {description} + + { + dispatch( + setSharedSnapshot({ title, description, url: `${base_url}${URLS.projects}${currentProjectID}/${hash}` }) + ); + }} + > + + + + ), + onMessageClick: handleClickOnCommit, + onClick: handleClickOnCommit, + style: isSelected ? { dot: { size: 10, color: 'red', strokeColor: 'blue', strokeWidth: 2 } } : undefined, + tag: (isSelected === true && 'selected snapshot') || undefined + }); + + const renderTreeNode = (childID, gitgraph, parentBranch) => { + const node = currentSnapshotList[childID]; + if (node !== undefined) { + const newBranch = gitgraph.branch({ + from: parentBranch, + name: node.title + }); + + newBranch.commit( + commitFunction({ + title: node.title || '', + description: node.description || '', + author: (node.author && node.author.username) || '', + email: (node.author && node.author.email) || '', + hash: node.id, + isSelected: currentSnapshotID === node.id, + created: node.created + }) + ); + + node.children.forEach(childID => { + renderTreeNode(childID, gitgraph, newBranch); + }); + } + }; + + const handleCloseHistory = () => { + setShowHistory(false); + }; + + return ( + <> + +
+
+ + + + Project History + + + + + + + + +
+
+ {isLoadingTree === false && + currentSnapshotTree !== null && + currentSnapshotTree.children !== null && + currentSnapshotTree.title !== null && + currentSnapshotTree.id !== null && + currentSnapshotID !== null && + currentSnapshotList !== null && ( + + {gitgraph => { + const initBranch = gitgraph.branch(currentSnapshotTree.title); + initBranch.commit( + commitFunction({ + title: currentSnapshotTree.title || '', + description: currentSnapshotTree.description || '', + author: (currentSnapshotTree.author && currentSnapshotTree.author.username) || '', + email: (currentSnapshotTree.author && currentSnapshotTree.author.email) || '', + hash: currentSnapshotTree.id, + isSelected: currentSnapshotID === currentSnapshotTree.id, + created: currentSnapshotTree.created + }) + ); + + currentSnapshotTree.children.forEach(childID => { + renderTreeNode(childID, gitgraph, initBranch); + }); + }} + + )} +
+
+
+ setOpen(false)}> + + + + ); +}); diff --git a/js/components/projects/projectDetailSessionList/customTreeItem.js b/js/components/projects/projectDetailSessionList/customTreeItem.js new file mode 100644 index 000000000..80f2f5650 --- /dev/null +++ b/js/components/projects/projectDetailSessionList/customTreeItem.js @@ -0,0 +1,36 @@ +import React, { memo } from 'react'; +import { Grid, IconButton, Typography } from '@material-ui/core'; +import moment from 'moment'; +import { CheckCircle, Delete, Share } from '@material-ui/icons'; +import { TreeItem } from '@material-ui/lab'; + +export const CustomTreeItem = memo(({ children, nodeId, title, description }) => { + return ( + + + {title} + + Created: {moment().format('LLL')} Description: {description} + + + + + + + + + + + + + +
+ } + > + {children} + + ); +}); diff --git a/js/components/projects/projectDetailSessionList/index.js b/js/components/projects/projectDetailSessionList/index.js new file mode 100644 index 000000000..21a3c699e --- /dev/null +++ b/js/components/projects/projectDetailSessionList/index.js @@ -0,0 +1,195 @@ +import React, { memo } from 'react'; +import { Panel } from '../../common/Surfaces/Panel'; +import { InputAdornment, makeStyles, TextField, IconButton } from '@material-ui/core'; +import { Delete, Search, Share } from '@material-ui/icons'; +import moment from 'moment'; +import { Gitgraph, templateExtend, TemplateName } from '@gitgraph/react'; +import Modal from '../../common/Modal'; +import { URLS } from '../../routes/constants'; + +const useStyles = makeStyles(theme => ({ + root: { + flexGrow: 1 + }, + table: { + minWidth: 650 + }, + search: { + margin: theme.spacing(1) + }, + thumbnail: { + float: 'left', + paddingLeft: theme.spacing(1), + paddingRight: theme.spacing(1), + paddingTop: theme.spacing(2), + paddingBottom: theme.spacing(2), + width: 66 + }, + paper: { + position: 'absolute', + width: 400, + backgroundColor: theme.palette.background.paper, + border: '2px solid #000', + boxShadow: theme.shadows[5], + padding: theme.spacing(2, 4, 3) + } +})); + +export const ProjectDetailSessionList = memo(({ history }) => { + const classes = useStyles(); + const [open, setOpen] = React.useState(false); + + var myTemplate = templateExtend(TemplateName.Metro, { + branch: { + lineWidth: 3, + spacing: 25, + label: { + font: 'normal 8pt Arial', + pointerWidth: 100 + // display: false + } + }, + commit: { + message: { + displayHash: false, + font: 'normal 11pt Arial' + // displayAuthor: false + }, + spacing: 10, + dot: { + size: 8 + } + }, + tag: { + font: 'normal 8pt Arial' + } + }); + + const options = { + template: myTemplate + }; + + const handleClickOnCommit = commit => { + console.log('redirecting to session'); + history.push(`${URLS.target}NUDT5A`); + }; + + const commitFunction = ({ title, description, photo, author, email }) => ({ + author: ` <${email}>`, + subject: `${moment().format('LLL')}: ${title}`, + body: ( + <> + setOpen(true)} /> + {description} + + + + + + + + ), + onMessageClick: handleClickOnCommit, + onClick: handleClickOnCommit + }); + + return ( + + + + ) + }} + /> + ]} + > + + {gitgraph => { + const master = gitgraph.branch('Basic molecules'); + master.commit( + commitFunction({ + title: 'Add basic molecules', + description: 'Add all molecules are in base form', + author: 'Tibor Postek', + email: 'tibor.postek@m2ms.sk' + }) + ); + const majorSnapshot = gitgraph.branch('Major molecule functionality'); + majorSnapshot.commit( + commitFunction({ + title: 'Major snapshop', + description: 'Create all major interactions are implemented', + author: 'Pavol Brunclik', + email: 'pavol.brunclik@m2ms.sk' + }) + ); + const cancerMolecules = gitgraph.branch('Cancer branch molecules'); + cancerMolecules.commit( + commitFunction({ + title: 'Add cancer molecule models', + description: 'Base model and 3 options are available', + author: 'Pavol Brunclik', + email: 'pavol.brunclik@m2ms.sk' + }) + ); + cancerMolecules.commit( + commitFunction({ + title: 'Add cancer molecule', + description: 'This molecule is rare and expensive', + author: 'Tibor Postek', + email: 'tibor.postek@m2ms.sk' + }) + ); + cancerMolecules.commit( + commitFunction({ + title: 'Add medical experiment', + description: 'Testing molecules for fighting with cancer', + author: 'Tibor Postek', + email: 'tibor.postek@m2ms.sk' + }) + ); + cancerMolecules.commit( + commitFunction({ + title: 'Add biological experiment', + description: 'Testing molecules on animals, that are disabled by with cancer', + author: 'Tibor Postek', + email: 'tibor.postek@m2ms.sk' + }) + ); + + majorSnapshot.commit( + commitFunction({ + title: 'Create automatic snapshop', + description: 'Automatic generated snapshot', + author: 'Pavol Brunclik', + email: 'pavol.brunclik@m2ms.sk' + }) + ); + + master.commit( + commitFunction({ + title: 'Add methods', + description: 'Add method of molecules molecule verification', + author: 'Tibor Postek', + email: 'tibor.postek@m2ms.sk' + }) + ); + }} + + setOpen(false)}> + + + + ); +}); diff --git a/js/components/projects/projectModal/index.js b/js/components/projects/projectModal/index.js new file mode 100644 index 000000000..3c6a28560 --- /dev/null +++ b/js/components/projects/projectModal/index.js @@ -0,0 +1,339 @@ +import React, { memo, useEffect, useState } from 'react'; +import Modal from '../../common/Modal'; +import { useDispatch, useSelector } from 'react-redux'; +import { groupBy } from 'lodash'; +import { setProjectModalOpen } from '../redux/actions'; +import { + makeStyles, + Radio, + Grid, + Typography, + MenuItem, + InputLabel, + FormControl, + FormHelperText, + FormControlLabel, + ListItemText +} from '@material-ui/core'; +import { Title, Description, Label, Link } from '@material-ui/icons'; +import { Autocomplete } from '@material-ui/lab'; +import { useHistory } from 'react-router-dom'; +import { DJANGO_CONTEXT } from '../../../utils/djangoContext'; +import { InputFieldAvatar } from './inputFieldAvatar'; +import { ProjectCreationType, SnapshotProjectType } from '../redux/constants'; +import { Formik, Form } from 'formik'; +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'; + +const useStyles = makeStyles(theme => ({ + body: { + width: '100%', + marginTop: theme.spacing(1), + marginBottom: theme.spacing(1) + }, + input: { + width: 400 + }, + margin: { + margin: theme.spacing(1) + }, + formControl: { + margin: theme.spacing(1), + width: 400 + } +})); + +export const ProjectModal = memo(({}) => { + const classes = useStyles(); + const [state, setState] = useState(); + let history = useHistory(); + + const dispatch = useDispatch(); + const isProjectModalOpen = useSelector(state => state.projectReducers.isProjectModalOpen); + const isProjectModalLoading = useSelector(state => state.projectReducers.isProjectModalLoading); + const listOfProjects = useSelector(state => state.projectReducers.listOfProjects); + const isLoadingListOfSnapshots = useSelector(state => state.snapshotReducers.isLoadingListOfSnapshots); + const grouppedListOfSnapshots = groupBy( + useSelector(state => state.snapshotReducers.listOfSnapshots), + 'session_project.id' + ); + const targetList = useSelector(state => state.apiReducers.target_id_list); + + const handleCloseModal = () => { + if (isProjectModalLoading === false) { + dispatch(setProjectModalOpen(false)); + } + }; + + const [tags, setTags] = React.useState([]); + + useEffect(() => { + dispatch(getListOfSnapshots()); + }, [dispatch]); + + const getProjectTitle = projectID => { + if (projectID === 'undefined') { + return SnapshotProjectType.NOT_ASSIGNED; + } + const project = listOfProjects.find(item => `${item.id}` === projectID); + return project && `${project.title} - ${project.description}`; + }; + + return ( + + Create project + { + const errors = {}; + if (!values.title) { + errors.title = 'Required!'; + } + if (!values.description) { + errors.description = 'Required!'; + } + if (values.type === ProjectCreationType.NEW && values.targetId === '') { + errors.targetId = 'Required!'; + } + if (values.type === ProjectCreationType.FROM_SNAPSHOT && values.parentSnapshotId === '') { + errors.parentSnapshotId = 'Required!'; + } + if (values.type === '') { + errors.type = 'Type of Project is required!'; + } + return errors; + }} + onSubmit={values => { + const data = { + title: values.title, + description: values.description, + target: values.targetId, + author: DJANGO_CONTEXT['pk'], + tags: JSON.stringify(tags) + }; + + // Create from snapshot + if (values.type === ProjectCreationType.FROM_SNAPSHOT) { + dispatch( + createProjectFromSnapshot({ + ...data, + history, + parentSnapshotId: values.parentSnapshotId + }) + ) + .catch(error => { + setState(() => { + throw error; + }); + }) + .finally(() => { + handleCloseModal(); + }); + } + + // Create from scratch + if (values.type === ProjectCreationType.NEW) { + dispatch( + createProjectFromScratch({ + ...data, + history + }) + ) + .catch(error => { + setState(() => { + throw error; + }); + }) + .finally(() => { + handleCloseModal(); + }); + } + }} + > + {({ submitForm, errors, values }) => ( +
+ + + + + } + label="New Project" + disabled={isProjectModalLoading} + /> + } + label="From Snapshot" + disabled={isProjectModalLoading} + /> + + {errors.type && {errors.type}} + + + + } + field={ + + } + /> + + + } + field={ + + } + /> + + {values && values.type === ProjectCreationType.NEW && ( + + } + field={ + + + Target + + + {errors.targetId} + + } + /> + + )} + {values && values.type === ProjectCreationType.FROM_SNAPSHOT && ( + + } + field={ + + + From Snapshot + + + {errors.parentSnapshotId} + + } + /> + + )} + + } + field={ + option} + onChange={(e, data) => { + setTags(data); + }} + disabled={isProjectModalLoading} + renderInput={params => ( + { + if (e.key === 'Enter') { + setTags([...tags, e.target.value]); + } + }} + /> + )} + /> + } + /> + + + + + + + + + + +
+ )} +
+
+ ); +}); diff --git a/js/components/projects/projectModal/inputFieldAvatar.js b/js/components/projects/projectModal/inputFieldAvatar.js new file mode 100644 index 000000000..474a587aa --- /dev/null +++ b/js/components/projects/projectModal/inputFieldAvatar.js @@ -0,0 +1,24 @@ +import React, { memo } from 'react'; +import { Grid, makeStyles } from '@material-ui/core'; + +const useStyles = makeStyles(theme => ({ + input: { + width: 400 + }, + margin: { + margin: theme.spacing(1) + } +})); + +export const InputFieldAvatar = memo(({ icon, field }) => { + const classes = useStyles(); + + return ( +
+ + {icon} + {field} + +
+ ); +}); diff --git a/js/components/projects/projectPreview/index.js b/js/components/projects/projectPreview/index.js new file mode 100644 index 000000000..ca25e818b --- /dev/null +++ b/js/components/projects/projectPreview/index.js @@ -0,0 +1,62 @@ +import React, { memo, useContext, useEffect, useRef, useState } from 'react'; +import Preview from '../../preview/Preview'; +import { useDispatch, useSelector } from 'react-redux'; +import { useRouteMatch } from 'react-router-dom'; +import { loadCurrentSnapshotByID, loadSnapshotByProjectID } from '../redux/dispatchActions'; +import { HeaderContext } from '../../header/headerContext'; + +export const ProjectPreview = memo(({}) => { + const { setSnackBarTitle } = useContext(HeaderContext); + const [canShow, setCanShow] = useState(undefined); + const isSnapshotLoaded = useRef(undefined); + let match = useRouteMatch(); + const dispatch = useDispatch(); + const projectId = match && match.params && match.params.projectId; + const snapshotId = match && match.params && match.params.snapshotId; + const currentSnapshotID = useSelector(state => state.projectReducers.currentSnapshot.id); + + useEffect(() => { + if (!snapshotId && currentSnapshotID === null) { + dispatch(loadSnapshotByProjectID(projectId)) + .then(response => { + if (response !== false) { + isSnapshotLoaded.current = response; + setCanShow(true); + } + }) + .catch(error => { + setCanShow(true); + throw new Error(error); + }); + } else { + dispatch(loadCurrentSnapshotByID(snapshotId || currentSnapshotID)) + .then(response => { + if (response !== false) { + if (response) { + if (response.session_project && `${response.session_project.id}` === projectId) { + isSnapshotLoaded.current = response.id; + setCanShow(true); + } else { + setCanShow(false); + } + } else { + isSnapshotLoaded.current = response; + setCanShow(false); + } + } + }) + .catch(error => { + setCanShow(false); + throw new Error(error); + }); + } + }, [currentSnapshotID, dispatch, projectId, snapshotId]); + + if (canShow === false) { + setSnackBarTitle('Not valid snapshot!'); + } + + return canShow === true && isSnapshotLoaded.current !== undefined ? ( + + ) : null; +}); diff --git a/js/components/projects/redux/actions.js b/js/components/projects/redux/actions.js new file mode 100644 index 000000000..93e2bd61d --- /dev/null +++ b/js/components/projects/redux/actions.js @@ -0,0 +1,62 @@ +import { constants } from './constants'; + +export const setCurrentProject = ({ projectID, authorID, title, description, targetID, tags, type }) => ({ + type: constants.SET_CURRENT_PROJECT, + payload: { projectID, authorID, title, description, targetID, tags, type } +}); +export const setCurrentProjectProperty = (key, value) => ({ + type: constants.SET_CURRENT_PROJECT_PROPERTY, + payload: { key, value } +}); + +export const resetProjectState = () => ({ + type: constants.RESET_CURRENT_PROJECT_STATE +}); + +export const setProjectModalOpen = isOpen => ({ + type: constants.SET_PROJECT_MODAL_OPEN, + payload: isOpen +}); + +export const setProjectModalIsLoading = isLoading => ({ + type: constants.SET_PROJECT_MODAL_IS_LOADING, + payload: isLoading +}); + +export const setCurrentSnapshot = currentSnapshot => ({ + type: constants.SET_SNAPSHOT, + payload: { currentSnapshot } +}); + +export const resetCurrentSnapshot = () => ({ + type: constants.RESET_SNAPSHOT +}); + +export const setIsLoadingCurrentSnapshot = isLoading => ({ + type: constants.SET_IS_LOADING_CURRENT_SNAPSHOT, + payload: isLoading +}); + +export const resetProjectsReducer = () => ({ type: constants.RESET_PROJECTS_REDUCER }); + +export const resetLoadedSnapshots = () => ({ type: constants.RESET_LOADED_SNAPSHOTS }); + +export const setListOfProjects = projects => ({ + type: constants.SET_LIST_OF_PROJECTS, + payload: projects +}); + +export const setIsLoadingTree = isLoading => ({ + type: constants.SET_IS_LOADING_TREE, + payload: isLoading +}); + +export const setCurrentSnapshotTree = tree => ({ + type: constants.SET_CURRENT_SNAPSHOT_TREE, + payload: tree +}); + +export const setCurrentSnapshotList = list => ({ + type: constants.SET_CURRENT_SNAPSHOT_LIST, + payload: list +}); diff --git a/js/components/projects/redux/constants.js b/js/components/projects/redux/constants.js new file mode 100644 index 000000000..770fad20a --- /dev/null +++ b/js/components/projects/redux/constants.js @@ -0,0 +1,35 @@ +const prefix = 'PROJECTS_'; + +export const constants = { + SET_CURRENT_PROJECT: prefix + 'SET_CURRENT_PROJECT', + SET_CURRENT_PROJECT_PROPERTY: prefix + 'SET_CURRENT_PROJECT_PROPERTY', + RESET_CURRENT_PROJECT_STATE: prefix + 'RESET_CURRENT_PROJECT_STATE', + SET_PROJECT_MODAL_OPEN: prefix + 'SET_PROJECT_MODAL_OPEN', + SET_PROJECT_MODAL_IS_LOADING: prefix + 'SET_PROJECT_MODAL_IS_LOADING', + SET_SNAPSHOT: prefix + 'SET_SNAPSHOT', + RESET_SNAPSHOT: prefix + 'RESET_SNAPSHOT', + SET_IS_LOADING_CURRENT_SNAPSHOT: prefix + 'SET_IS_LOADING_CURRENT_SNAPSHOT', + + RESET_PROJECTS_REDUCER: prefix + 'RESET_PROJECTS_REDUCER', + SET_LIST_OF_PROJECTS: prefix + 'SET_LIST_OF_PROJECTS', + SET_IS_LOADING_TREE: prefix + 'SET_IS_LOADING_TREE', + SET_CURRENT_SNAPSHOT_TREE: prefix + 'SET_CURRENT_SNAPSHOT_TREE', + SET_CURRENT_SNAPSHOT_LIST: prefix + 'SET_CURRENT_SNAPSHOT_LIST', + + RESET_LOADED_SNAPSHOTS: prefix + 'RESET_LOADED_SNAPSHOTS' +}; + +export const ProjectCreationType = { + NEW: 'NEW', + FROM_SNAPSHOT: 'FROM_SNAPSHOT' +}; + +export const SnapshotType = { + INIT: 'INIT', // Initial snapshot generated by system + AUTO: 'AUTO', //Automatic generated by system + MANUAL: 'MANUAL' //Manual generated by user action +}; + +export const SnapshotProjectType = { + NOT_ASSIGNED: 'Not assigned to project' +}; diff --git a/js/components/projects/redux/dispatchActions.js b/js/components/projects/redux/dispatchActions.js new file mode 100644 index 000000000..caeb6f2a1 --- /dev/null +++ b/js/components/projects/redux/dispatchActions.js @@ -0,0 +1,364 @@ +import { + setListOfProjects, + setCurrentSnapshot, + resetCurrentSnapshot, + setCurrentProjectProperty, + setProjectModalIsLoading, + setCurrentSnapshotTree, + setCurrentSnapshotList, + setIsLoadingTree, + setIsLoadingCurrentSnapshot, + setCurrentProject +} from './actions'; +import { api, METHOD } from '../../../utils/api'; +import { base_url, URLS } from '../../routes/constants'; +import { setDialogCurrentStep } from '../../snapshot/redux/actions'; +import { createInitSnapshotFromCopy, getListOfSnapshots } from '../../snapshot/redux/dispatchActions'; +import { SnapshotType } from './constants'; + +export const assignSnapshotToProject = ({ projectID, snapshotID, ...rest }) => (dispatch, getState) => { + dispatch(resetCurrentSnapshot()); + return api({ + url: `${base_url}/api/snapshots/${snapshotID}/`, + data: { session_project: projectID, ...rest }, + method: METHOD.PATCH + }) + .then(response => + dispatch( + setCurrentSnapshot({ + id: response.data.id, + type: response.data.type, + title: response.data.title, + author: response.data.author, + description: response.data.description, + created: response.data.created, + children: response.data.children, + parent: response.data.parent, + data: JSON.parse(response.data.data) + }) + ) + ) + + .catch(error => { + throw new Error(error); + }) + .finally(() => { + dispatch(getListOfSnapshots()); + }); +}; + +export const loadListOfProjects = () => (dispatch, getState) => { + return api({ url: `${base_url}/api/session-projects/` }).then(response => + dispatch(setListOfProjects((response && response.data && response.data.results) || [])) + ); +}; + +export const searchInProjects = title => (dispatch, getState) => { + return api({ url: `${base_url}/api/session-projects/?title=${title}` }).then(response => + dispatch(setListOfProjects((response && response.data && response.data.results) || [])) + ); +}; + +export const removeSnapshotByID = snapshotID => dispatch => { + return api({ url: `${base_url}/api/snapshots/${snapshotID}` }).then(response => { + if (response.data && response.data.id !== undefined) { + if (response.data.children && response.data.children.length > 0) { + return dispatch(removeChildren(response.data.children)); + } else { + return api({ url: `${base_url}/api/snapshots/${snapshotID}/`, method: METHOD.DELETE }); + } + } + }); +}; + +const removeChildren = (children = []) => dispatch => { + if (children && children.length > 0) { + return Promise.all(children.map(childID => dispatch(removeSnapshotByID(childID)))); + } +}; + +export const removeSnapshotTree = projectID => dispatch => { + return api({ url: `${base_url}/api/snapshots/?session_project=${projectID}&type=INIT` }).then(response => { + if (response.data.count === 0) { + return Promise.resolve('Not found INITIAL snapshot'); + } else if (response.data.count === 1) { + const tree = parseSnapshotAttributes(response.data.results[0]); + if (tree.children && tree.children.length === 0) { + return dispatch(removeChildren([tree.id])); + } + return dispatch(removeChildren(tree.children)); + } + }); +}; + +export const removeProject = projectID => dispatch => { + dispatch(setIsLoadingTree(true)); + return dispatch(removeSnapshotTree(projectID)) + .then(() => dispatch(removeSnapshotTree(projectID))) + .then(() => + api({ url: `${base_url}/api/session-projects/${projectID}/`, method: METHOD.DELETE }).then(() => + dispatch(loadListOfProjects()) + ) + ) + .finally(() => { + dispatch(setIsLoadingTree(false)); + dispatch(getListOfSnapshots()); + }); +}; + +export const loadSnapshotByProjectID = projectID => (dispatch, getState) => { + const state = getState(); + const isLoadingCurrentSnapshot = state.projectReducers.isLoadingCurrentSnapshot; + if (isLoadingCurrentSnapshot === false) { + dispatch(setIsLoadingCurrentSnapshot(true)); + return api({ url: `${base_url}/api/snapshots/?session_project=${projectID}&type=INIT` }) + .then(response => { + if (response.data.results.length === 0) { + dispatch(resetCurrentSnapshot()); + return Promise.resolve(null); + } else if (response.data.results[0] !== undefined) { + dispatch( + setCurrentSnapshot({ + id: response.data.results[0].id, + type: response.data.results[0].type, + title: response.data.results[0].title, + author: response.data.results[0].author, + description: response.data.results[0].description, + created: response.data.results[0].created, + children: response.data.results[0].children, + parent: response.data.results[0].parent, + data: JSON.parse(response.data.results[0].data) + }) + ); + return Promise.resolve(response.data.results[0].id); + } + }) + .catch(error => { + dispatch(resetCurrentSnapshot()); + }) + .finally(() => { + dispatch(setIsLoadingCurrentSnapshot(false)); + }); + } + return Promise.resolve(false); +}; + +export const loadCurrentSnapshotByID = snapshotID => (dispatch, getState) => { + const state = getState(); + const isLoadingCurrentSnapshot = state.projectReducers.isLoadingCurrentSnapshot; + if (isLoadingCurrentSnapshot === false) { + dispatch(setIsLoadingCurrentSnapshot(true)); + return api({ url: `${base_url}/api/snapshots/${snapshotID}` }) + .then(response => { + if (response.data.id === undefined) { + dispatch(resetCurrentSnapshot()); + return Promise.resolve(null); + } else { + dispatch( + setCurrentSnapshot({ + id: response.data.id, + type: response.data.type, + title: response.data.title, + author: response.data.author, + description: response.data.description, + created: response.data.created, + children: response.data.children, + parent: response.data.parent, + data: JSON.parse(response.data.data) + }) + ); + return Promise.resolve(response.data); + } + }) + .catch(error => { + dispatch(resetCurrentSnapshot()); + }) + .finally(() => { + dispatch(setIsLoadingCurrentSnapshot(false)); + }); + } + return Promise.resolve(false); +}; + +const parseSnapshotAttributes = data => ({ + id: data.id, + type: data.type, + title: data.title, + author: data.author, + description: data.description, + created: data.created, + children: data.children +}); + +export const getSnapshotAttributesByID = snapshotID => (dispatch, getState) => { + return api({ url: `${base_url}/api/snapshots/${snapshotID}` }).then(async response => { + if (response.data && response.data.id !== undefined) { + let currentSnapshotList = JSON.parse(JSON.stringify(getState().projectReducers.currentSnapshotList)); + if (currentSnapshotList === null) { + currentSnapshotList = {}; + } + currentSnapshotList[snapshotID] = parseSnapshotAttributes(response.data); + dispatch(setCurrentSnapshotList(currentSnapshotList)); + + if (response.data.children && response.data.children.length > 0) { + return dispatch(populateChildren(response.data.children)); + } else { + return Promise.resolve(); + } + } + }); +}; + +const populateChildren = (children = []) => (dispatch, getState) => { + if (children && children.length > 0) { + return Promise.all(children.map(childID => dispatch(getSnapshotAttributesByID(childID)))); + } +}; + +export const loadSnapshotTree = projectID => (dispatch, getState) => { + dispatch(setIsLoadingTree(true)); + dispatch(setCurrentSnapshotTree(null)); + return api({ url: `${base_url}/api/snapshots/?session_project=${projectID}&type=INIT` }) + .then(response => { + if (response.data.count === 0) { + return Promise.reject('Not found INITIAL snapshot'); + } else if (response.data.count === 1) { + const tree = parseSnapshotAttributes(response.data.results[0]); + dispatch(setCurrentSnapshotTree(tree)); + if (tree.children && tree.children.length === 0) { + return dispatch(populateChildren([tree.id])); + } + return dispatch(populateChildren(tree.children)); + } + }) + .finally(() => { + dispatch(setIsLoadingTree(false)); + }); +}; + +export const createProjectFromSnapshotDialog = data => dispatch => { + dispatch(setProjectModalIsLoading(true)); + return api({ url: `${base_url}/api/session-projects/`, method: METHOD.POST, data }) + .then(response => { + const projectID = response.data.id; + dispatch(setCurrentProjectProperty('projectID', projectID)); + }) + .finally(() => { + dispatch(setDialogCurrentStep(1)); + }); +}; + +export const createProject = ({ title, description, target, author, tags }) => dispatch => { + dispatch(setProjectModalIsLoading(true)); + return api({ + url: `${base_url}/api/session-projects/`, + method: METHOD.POST, + data: { title, description, target, author, tags } + }).then(response => { + const projectID = response.data.id; + const title = response.data.title; + const authorID = response.data.author; + const description = response.data.description; + const targetID = response.data.target; + const tags = response.data.tags; + + return dispatch(setCurrentProject({ projectID, authorID, title, description, targetID, tags })); + }); +}; + +const copySnapshot = (selectedSnapshot, projectID, history) => dispatch => { + return dispatch( + createInitSnapshotFromCopy({ + title: selectedSnapshot.title, + author: (selectedSnapshot && selectedSnapshot.author && selectedSnapshot.author.id) || null, + description: selectedSnapshot.description, + data: JSON.parse(selectedSnapshot.data), + created: selectedSnapshot.created, + parent: null, + children: selectedSnapshot.children, + session_project: projectID + }) + ) + .then(() => { + history.push(`${URLS.projects}${projectID}`); + }) + .finally(() => { + dispatch(setProjectModalIsLoading(false)); + }); +}; + +export const createProjectFromSnapshot = ({ title, description, author, tags, history, parentSnapshotId }) => ( + dispatch, + getState +) => { + const listOfSnapshots = getState().snapshotReducers.listOfSnapshots; + const selectedSnapshot = listOfSnapshots.find(item => item.id === parentSnapshotId); + const snapshotData = JSON.parse(selectedSnapshot && selectedSnapshot.data); + + dispatch(setProjectModalIsLoading(true)); + return dispatch( + createProject({ + title, + description, + target: (snapshotData && snapshotData.apiReducers && snapshotData.apiReducers.target_on) || null, + author, + tags + }) + ).then(() => { + const { projectID } = getState().projectReducers.currentProject; + + // in case when snapshot has assigned project => make copy + if (selectedSnapshot && selectedSnapshot.session_project !== null) { + return dispatch(copySnapshot(selectedSnapshot, projectID, history)); + } + // in case when snapshot has not assigned project => mark snapshot as INIT and assign to project + else if (selectedSnapshot && selectedSnapshot.session_project === null) { + // in case when snapshot has no parent, use given snapshot, but mark it as INIT + if (!selectedSnapshot.parent) { + return dispatch( + assignSnapshotToProject({ + projectID, + snapshotID: selectedSnapshot.id, + type: SnapshotType.INIT + }) + ) + .then(() => { + history.push(`${URLS.projects}${projectID}`); + }) + .finally(() => { + dispatch(setProjectModalIsLoading(false)); + }); + } // in case when snapshot has parent => create new snapshot with INIT type and copy all data from previous snapshot + else { + return dispatch(copySnapshot(selectedSnapshot, projectID, history)); + } + } + }); +}; + +export const createProjectFromScratch = ({ title, description, target, author, tags, history }) => ( + dispatch, + getState +) => { + dispatch(setProjectModalIsLoading(true)); + return api({ + url: `${base_url}/api/session-projects/`, + method: METHOD.POST, + data: { title, description, target, author, tags } + }) + .then(response => { + const projectID = response.data.id; + const title = response.data.title; + const authorID = response.data.author; + const description = response.data.description; + const targetID = response.data.target; + const tags = response.data.tags; + + dispatch(setCurrentProject({ projectID, authorID, title, description, targetID, tags })); + // create project_target relationShip on BE + history.push(`${URLS.projects}${projectID}`); + }) + .finally(() => { + dispatch(setProjectModalIsLoading(false)); + }); +}; diff --git a/js/components/projects/redux/reducer.js b/js/components/projects/redux/reducer.js new file mode 100644 index 000000000..43d5b2a2a --- /dev/null +++ b/js/components/projects/redux/reducer.js @@ -0,0 +1,92 @@ +import { constants } from './constants'; +const initCurrentSnapshot = { + id: null, + type: null, + title: null, + author: null, + description: null, + children: [], // if it has got children, it is created branch, + parent: null, + created: null, + data: null +}; + +export const INITIAL_STATE = { + currentProject: { + projectID: null, + authorID: null, + title: null, + description: null, + targetID: null, + tags: [], + type: null + }, + isLoadingCurrentSnapshot: false, + currentSnapshot: initCurrentSnapshot, + isProjectModalOpen: false, + isProjectModalLoading: false, + listOfProjects: [], + isLoadingTree: false, + currentSnapshotTree: null, + currentSnapshotList: null +}; + +export const projectReducers = (state = INITIAL_STATE, action = {}) => { + switch (action.type) { + case constants.SET_CURRENT_PROJECT: + return Object.assign({}, state, { + currentProject: action.payload + }); + + case constants.SET_CURRENT_PROJECT_PROPERTY: + const currProject = JSON.parse(JSON.stringify(state.currentProject)); + currProject[action.payload.key] = action.payload.value; + + return Object.assign({}, state, { currentProject: currProject }); + + case constants.RESET_CURRENT_PROJECT_STATE: + const currProj = JSON.parse(JSON.stringify(INITIAL_STATE.currentProject)); + return Object.assign({}, state, { currentProject: currProj }); + + case constants.SET_PROJECT_MODAL_OPEN: + return Object.assign({}, state, { isProjectModalOpen: action.payload }); + + case constants.SET_PROJECT_MODAL_IS_LOADING: + return Object.assign({}, state, { isProjectModalLoading: action.payload }); + + case constants.SET_SNAPSHOT: + return Object.assign({}, state, { currentSnapshot: action.payload.currentSnapshot }); + + case constants.RESET_SNAPSHOT: + return Object.assign({}, state, { currentSnapshot: initCurrentSnapshot }); + + case constants.SET_IS_LOADING_CURRENT_SNAPSHOT: + return Object.assign({}, state, { isLoadingCurrentSnapshot: action.payload }); + + case constants.SET_LIST_OF_PROJECTS: + return Object.assign({}, state, { listOfProjects: action.payload }); + + case constants.SET_CURRENT_SNAPSHOT_TREE: + return Object.assign({}, state, { currentSnapshotTree: action.payload }); + + case constants.SET_IS_LOADING_TREE: + return Object.assign({}, state, { isLoadingTree: action.payload }); + + case constants.SET_CURRENT_SNAPSHOT_LIST: + return Object.assign({}, state, { currentSnapshotList: action.payload }); + + case constants.RESET_PROJECTS_REDUCER: + return Object.assign({}, INITIAL_STATE); + + case constants.RESET_LOADED_SNAPSHOTS: + const currentState = JSON.parse(JSON.stringify(state)); + currentState.currentSnapshot = initCurrentSnapshot; + currentState.currentSnapshotTree = null; + currentState.currentSnapshotList = null; + + return Object.assign({}, currentState); + + default: + return state; + } +}; diff --git a/js/components/routes/Routes.js b/js/components/routes/Routes.js index a82230491..c5be1a3bc 100644 --- a/js/components/routes/Routes.js +++ b/js/components/routes/Routes.js @@ -6,13 +6,15 @@ import { Management } from '../management/management'; import Tindspect from '../tindspect/Tindspect'; import Landing from '../landing/Landing'; import Preview from '../preview/Preview'; +import { ProjectPreview } from '../projects/projectPreview'; import Funders from '../funders/fundersHolder'; import { withLoadingTargetList } from '../target/withLoadingTargetIdList'; import { BrowserCheck } from '../errorHandling/browserCheck'; import { URLS } from './constants'; import { HeaderContext } from '../header/headerContext'; import { Close } from '@material-ui/icons'; -import SessionList from '../session/sessionList'; +import { Projects } from '../projects'; +import { ProjectDetailSessionList } from '../projects/projectDetailSessionList'; const useStyles = makeStyles(theme => ({ content: { @@ -40,22 +42,17 @@ const Routes = memo(() => {
+ + + } /> + } /> } - /> - - } - /> - } + path={`${URLS.target}:target`} + render={routeProps => } /> @@ -79,7 +76,7 @@ const Routes = memo(() => { } - > + /> ); }); diff --git a/js/components/routes/constants.js b/js/components/routes/constants.js index 92ab7e960..17a876053 100644 --- a/js/components/routes/constants.js +++ b/js/components/routes/constants.js @@ -1,13 +1,17 @@ export const URLS = { - sessions: '/viewer/react/sessions', - landing: '/viewer/react/landing', + sessions: '/viewer/react/sessions/', + landing: '/viewer/react/landing/', fragglebox: '/viewer/react/fragglebox/', snapshot: '/viewer/react/snapshot/', - prodLanding: 'https://fragalysis.diamond.ac.uk/viewer/react/landing', - login: '/accounts/login', - logout: '/accounts/logout', - management: '/viewer/react/management', - funders: '/viewer/react/funders' + prodLanding: 'https://fragalysis.diamond.ac.uk/viewer/react/landing/', + login: '/accounts/login/', + logout: '/accounts/logout/', + management: '/viewer/react/management/', + funders: '/viewer/react/funders/', + target: '/viewer/react/preview/target/', + + // Projects feature + projects: '/viewer/react/projects/' }; export const base_url = window.location.protocol + '//' + window.location.host; diff --git a/js/components/session/modalStateSave.js b/js/components/session/modalStateSave.js deleted file mode 100644 index 17972c68c..000000000 --- a/js/components/session/modalStateSave.js +++ /dev/null @@ -1,232 +0,0 @@ -/** - * Created by ricgillams on 14/06/2018. - */ - -import React, { memo, useState, useEffect, useContext } from 'react'; -import { connect } from 'react-redux'; -import Modal from '../common/Modal'; -import { Grid, makeStyles } from '@material-ui/core'; -import * as apiActions from '../../reducers/api/actions'; -import { TextField } from '../common/Inputs/TextField'; -import { Button } from '../common/Inputs/Button'; -import { savingStateConst } from './constants'; -import { updateClipboard } from './helpers'; -import { api } from '../../utils/api'; -import { HeaderContext } from '../header/headerContext'; - -const useStyles = makeStyles(theme => ({ - row: { - width: 'inherit' - }, - textField: { - width: 'inherit', - margin: theme.spacing(1) - } -})); - -const ModalStateSave = memo( - ({ saveType, savingState, latestSession, latestSnapshot, sessionId, setSavingState, setSessionTitle }) => { - const [fraggleBoxLoc, setFraggleBoxLoc] = useState(); - const [snapshotLoc, setSnapshotLoc] = useState(); - const [title, setTitle] = useState(''); - const classes = useStyles(); - const { setSnackBarTitle } = useContext(HeaderContext); - - let urlToCopy = ''; - const port = window.location.port ? `:${window.location.port}` : ''; - let sessionRename = false; - let linkTitle = ''; - - const getCookie = name => { - if (!document.cookie) { - return null; - } - const xsrfCookies = document.cookie - .split(';') - .map(c => c.trim()) - .filter(c => c.startsWith(name + '=')); - if (xsrfCookies.length === 0) { - return null; - } - return decodeURIComponent(xsrfCookies[0].split('=')[1]); - }; - - const openFraggleLink = () => { - var url = ''; - if (savingState === savingStateConst.savingSnapshot) { - url = - window.location.protocol + - '//' + - window.location.hostname + - port + - '/viewer/react/snapshot/' + - latestSnapshot; - window.open(url); - } else if ( - savingState === savingStateConst.savingSession || - savingState === savingStateConst.overwritingSession - ) { - url = - window.location.protocol + - '//' + - window.location.hostname + - port + - '/viewer/react/fragglebox/' + - latestSession; - window.open(url); - } - }; - - const getTitle = () => { - api({ - url: '/api/viewscene/?uuid=' + latestSession, - method: 'get', - headers: { - accept: 'application/json', - 'content-type': 'application/json' - } - }) - .then(response => { - var downloadedTitle = - response.data && response.data.results.length > 0 ? response.data.results[JSON.stringify(0)].title : ''; - setSessionTitle(downloadedTitle); - return downloadedTitle; - }) - .then(t => setTitle(t)) - .catch(error => { - throw new Error(error); - }); - }; - - const handleSessionNaming = e => { - if (e.keyCode === 13) { - var titleTemp = e.target.value; - setSessionTitle(titleTemp); - const csrfToken = getCookie('csrftoken'); - var formattedState = { - uuid: latestSession, - title: titleTemp - }; - api({ - url: '/api/viewscene/' + JSON.parse(sessionId), - method: 'PATCH', - headers: { - 'X-CSRFToken': csrfToken, - accept: 'application/json', - 'content-type': 'application/json' - }, - data: JSON.stringify(formattedState) - }).catch(error => { - throw new Error(error); - }); - } - }; - - const closeModal = () => { - setFraggleBoxLoc(undefined); - setSnapshotLoc(undefined); - setTitle(''); - setSavingState(savingStateConst.UNSET); - }; - - useEffect(() => { - if (latestSession !== undefined || latestSnapshot !== undefined) { - setFraggleBoxLoc(latestSession); - setSnapshotLoc(latestSnapshot); - } - }, [latestSession, latestSnapshot]); - - let isLoading = true; - - if (snapshotLoc !== undefined || fraggleBoxLoc !== undefined) { - if (savingState === savingStateConst.savingSnapshot) { - urlToCopy = - window.location.protocol + - '//' + - window.location.hostname + - port + - '/viewer/react/snapshot/' + - latestSnapshot; - linkTitle = 'A permanent, fixed snapshot of the current state has been saved: '; - if (latestSnapshot !== undefined) { - isLoading = false; - } - } else if (savingState === savingStateConst.savingSession) { - if (title === '') { - getTitle(); - } else { - isLoading = false; - } - sessionRename = true; - urlToCopy = - window.location.protocol + - '//' + - window.location.hostname + - port + - '/viewer/react/fragglebox/' + - latestSession; - linkTitle = 'A new session has been generated: '; - } else if (savingState === savingStateConst.overwritingSession) { - if (saveType === '') { - isLoading = false; - // snackbar title is overwritten tho by withSessionManagement update - setSnackBarTitle('Your session was successfully saved.'); - closeModal(); - } - } - } - - return ( - - - {sessionRename === true && ( - - setTitle(e.target.value)} - onKeyDown={handleSessionNaming} - className={classes.textField} - helperText="To overwrite session name, enter new title above and press enter." - /> - - )} - {linkTitle} - - {urlToCopy} - - - - - - - - - - - - - - - - - ); - } -); - -function mapStateToProps(state) { - return { - saveType: state.sessionReducers.saveType, - savingState: state.apiReducers.savingState, - latestSession: state.apiReducers.latestSession, - latestSnapshot: state.apiReducers.latestSnapshot, - sessionId: state.apiReducers.sessionId - }; -} - -const mapDispatchToProps = { - setSavingState: apiActions.setSavingState, - setSessionTitle: apiActions.setSessionTitle -}; - -export default connect(mapStateToProps, mapDispatchToProps)(ModalStateSave); diff --git a/js/components/session/redux/actions.js b/js/components/session/redux/actions.js deleted file mode 100644 index 0a1a825b0..000000000 --- a/js/components/session/redux/actions.js +++ /dev/null @@ -1,9 +0,0 @@ -import { constants } from './constants'; - -export const setSaveType = saveType => ({ type: constants.SET_SAVE_TYPE, payload: saveType }); - -export const setNextUUID = uuid => ({ type: constants.SET_NEXT_UUID, payload: uuid }); - -export const setNewSessionFlag = flag => ({ type: constants.SET_NEW_SESSION_FLAG, payload: flag }); - -export const setLoadedSession = loadedSession => ({ type: constants.SET_LOADED_SESSION, payload: loadedSession }); diff --git a/js/components/session/redux/constants.js b/js/components/session/redux/constants.js deleted file mode 100644 index 095742c91..000000000 --- a/js/components/session/redux/constants.js +++ /dev/null @@ -1,8 +0,0 @@ -const prefix = 'SESSION_'; - -export const constants = { - SET_SAVE_TYPE: prefix + 'SET_SAVE_TYPE', - SET_NEXT_UUID: prefix + 'SET_NEXT_UUID', - SET_NEW_SESSION_FLAG: prefix + 'SET_NEW_SESSION_FLAG', - SET_LOADED_SESSION: prefix + 'SET_LOADED_SESSION' -}; diff --git a/js/components/session/redux/dispatchActions.js b/js/components/session/redux/dispatchActions.js deleted file mode 100644 index 21411ac9f..000000000 --- a/js/components/session/redux/dispatchActions.js +++ /dev/null @@ -1,221 +0,0 @@ -import { SCENES } from '../../../reducers/ngl/constants'; -import { - reloadApiState, - setUuid, - setLatestSession, - setLatestSnapshot, - setSavingState, - setSessionId, - setSessionTitle, - setTargetUnrecognised -} from '../../../reducers/api/actions'; -import { reloadSelectionReducer } from '../../../reducers/selection/actions'; -import { reloadNglViewFromScene } from '../../../reducers/ngl/dispatchActions'; -import { api, getCsrfToken, METHOD } from '../../../utils/api'; -import { canCheckTarget } from '../helpers'; -import { saveCurrentStateAsSessionScene } from '../../../reducers/ngl/actions'; -import { savingStateConst, savingTypeConst } from '../constants'; -import { setLoadedSession, setNewSessionFlag, setNextUUID, setSaveType } from './actions'; -import { getStore } from '../../helpers/globalStore'; -import { DJANGO_CONTEXT } from '../../../utils/djangoContext'; -import { reloadPreviewReducer } from '../../preview/redux/dispatchActions'; - -export const reloadSession = (myJson, nglViewList) => dispatch => { - let jsonOfView = JSON.parse(JSON.parse(JSON.parse(myJson.scene)).state); - dispatch(reloadApiState(jsonOfView.apiReducers)); - dispatch(setSessionId(myJson.id)); - dispatch(setSessionTitle(myJson.title)); - - if (nglViewList.length > 0) { - dispatch(reloadSelectionReducer(jsonOfView.selectionReducers)); - nglViewList.forEach(nglView => { - dispatch(reloadNglViewFromScene(nglView.stage, nglView.id, SCENES.sessionScene, jsonOfView)); - }); - - if (jsonOfView.selectionReducers.vectorOnList.length !== 0) { - dispatch(reloadPreviewReducer(jsonOfView.previewReducers)); - } - } -}; - -export const setTargetAndReloadSession = ({ pathname, nglViewList, loadedSession, targetIdList }) => dispatch => { - if (loadedSession) { - let jsonOfView = JSON.parse(JSON.parse(JSON.parse(loadedSession.scene)).state); - let target = jsonOfView.apiReducers.target_on_name; - let targetUnrecognised = true; - targetIdList.forEach(item => { - if (target === item.title) { - targetUnrecognised = false; - } - }); - - if (canCheckTarget(pathname) === false) { - dispatch(setTargetUnrecognised(targetUnrecognised)); - } - if (targetUnrecognised === false && targetIdList.length > 0 && canCheckTarget(pathname) === true) { - dispatch(reloadSession(loadedSession, nglViewList)); - } - } -}; - -export const postToServer = sessionState => dispatch => { - dispatch(saveCurrentStateAsSessionScene()); - dispatch(setSavingState(sessionState)); -}; - -export const newSession = () => dispatch => { - dispatch(postToServer(savingStateConst.savingSession)); - dispatch(setSaveType(savingTypeConst.sessionNew)); -}; -export const saveSession = () => dispatch => { - dispatch(postToServer(savingStateConst.overwritingSession)); - dispatch(setSaveType(savingTypeConst.sessionSave)); -}; - -export const newSnapshot = () => dispatch => { - dispatch(postToServer(savingStateConst.savingSnapshot)); - dispatch(setSaveType(savingTypeConst.snapshotNew)); -}; - -export const getSessionDetails = () => (dispatch, getState) => { - const uuid = getState().apiReducers.uuid; - - return api({ method: METHOD.GET, url: '/api/viewscene/?uuid=' + uuid }).then(response => - response.data && response.data.results.length > 0 - ? setSessionTitle(response.data.results[JSON.stringify(0)].title) - : setSessionTitle('') - ); -}; - -export const updateCurrentTarget = myJson => (dispatch, getState) => { - const state = getState(); - const saveType = state.sessionReducers.saveType; - - if (saveType === savingTypeConst.sessionNew && myJson) { - dispatch(setUuid(myJson.uuid)); - dispatch(setSessionId(myJson.id)); - dispatch(setSessionTitle(myJson.title)); - dispatch(setSaveType('')); - dispatch(setNextUUID('')); - return dispatch(getSessionDetails()); - } else if (saveType === savingTypeConst.sessionSave) { - dispatch(setSaveType('')); - return dispatch(getSessionDetails()); - } else if (saveType === savingTypeConst.snapshotNew && myJson) { - dispatch(setLatestSnapshot(myJson.uuid)); - dispatch(setSaveType('')); - return Promise.resolve(); - } - return Promise.reject('Cannot update current target'); -}; - -export const generateNextUuid = () => (dispatch, getState) => { - const { nextUuid } = getState().sessionReducers; - - if (nextUuid === '') { - const uuidv4 = require('uuid/v4'); - dispatch(setNextUUID(uuidv4())); - dispatch(setNewSessionFlag(1)); - } -}; - -export const reloadScene = ({ saveType, newSessionFlag, nextUuid, uuid, sessionId }) => (dispatch, getState) => { - dispatch(generateNextUuid()); - const targetId = getState().apiReducers.target_on; - - if (saveType.length <= 0 && uuid !== 'UNSET') { - return api({ method: METHOD.GET, url: '/api/viewscene/?uuid=' + uuid }).then(response => - dispatch(setLoadedSession(response.data.results[0])) - ); - } - - let store = JSON.stringify(getStore().getState()); - const timeOptions = { - year: 'numeric', - month: 'numeric', - day: 'numeric', - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - hour12: false - }; - let TITLE = 'Created on ' + new Intl.DateTimeFormat('en-GB', timeOptions).format(Date.now()); - let userId = DJANGO_CONTEXT['pk']; - let stateObject = JSON.parse(store); - let newPresentObject = Object.assign(stateObject.apiReducers, { - latestSession: nextUuid - }); - - const fullState = { - state: JSON.stringify({ - apiReducers: newPresentObject, - nglReducers: stateObject.nglReducers, - selectionReducers: stateObject.selectionReducers, - previewReducers: stateObject.previewReducers - }) - }; - - if (saveType === savingTypeConst.sessionNew && newSessionFlag === 1) { - dispatch(setNewSessionFlag(0)); - const formattedState = { - uuid: nextUuid, - title: TITLE, - user_id: userId, - scene: JSON.stringify(JSON.stringify(fullState)), - target_id: targetId - }; - return api({ - url: '/api/viewscene/', - method: METHOD.POST, - headers: { - 'X-CSRFToken': getCsrfToken(), - accept: 'application/json', - 'content-type': 'application/json' - }, - data: JSON.stringify(formattedState) - }).then(response => { - dispatch(updateCurrentTarget(response.data)); - dispatch(setLatestSession(nextUuid)); - }); - } else if (saveType === savingTypeConst.sessionSave) { - const formattedState = { - scene: JSON.stringify(JSON.stringify(fullState)) - }; - return api({ - url: '/api/viewscene/' + JSON.parse(sessionId), - method: METHOD.PATCH, - headers: { - 'X-CSRFToken': getCsrfToken(), - accept: 'application/json', - 'content-type': 'application/json' - }, - data: JSON.stringify(formattedState) - }).then(response => { - dispatch(updateCurrentTarget(response.data)); - // latest session should be set also because of proper saving cycle - dispatch(setLatestSession(uuid)); - }); - } else if (saveType === savingTypeConst.snapshotNew) { - const uuidv4 = require('uuid/v4'); - const formattedState = { - uuid: uuidv4(), - title: 'shared snapshot', - user_id: userId, - scene: JSON.stringify(JSON.stringify(fullState)), - target_id: targetId - }; - return api({ - url: '/api/viewscene/', - method: METHOD.POST, - headers: { - 'X-CSRFToken': getCsrfToken(), - accept: 'application/json', - 'content-type': 'application/json' - }, - data: JSON.stringify(formattedState) - }).then(response => { - dispatch(updateCurrentTarget(response.data)); - }); - } - return Promise.resolve('No scene data to reload'); -}; diff --git a/js/components/session/redux/reducer.js b/js/components/session/redux/reducer.js deleted file mode 100644 index 816e5689b..000000000 --- a/js/components/session/redux/reducer.js +++ /dev/null @@ -1,35 +0,0 @@ -import { constants } from './constants'; - -export const INITIAL_STATE = { - saveType: '', - nextUuid: '', - newSessionFlag: 0, - loadedSession: undefined -}; - -export const sessionReducers = (state = INITIAL_STATE, action = {}) => { - switch (action.type) { - case constants.SET_SAVE_TYPE: - return Object.assign({}, state, { - saveType: action.payload - }); - - case constants.SET_NEXT_UUID: - return Object.assign({}, state, { - nextUuid: action.payload - }); - - case constants.SET_NEW_SESSION_FLAG: - return Object.assign({}, state, { - newSessionFlag: action.payload - }); - - case constants.SET_LOADED_SESSION: - return Object.assign({}, state, { - loadedSession: action.payload - }); - - default: - return state; - } -}; diff --git a/js/components/session/withSessionManagement.js b/js/components/session/withSessionManagement.js deleted file mode 100644 index 5ce68de28..000000000 --- a/js/components/session/withSessionManagement.js +++ /dev/null @@ -1,136 +0,0 @@ -import React, { memo, useContext, useEffect, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { Button } from '@material-ui/core'; -import { Save, SaveOutlined, Share } from '@material-ui/icons'; -import DownloadPdb from './downloadPdb'; -import { savingStateConst } from './constants'; - -import { NglContext } from '../nglView/nglProvider'; -import { HeaderContext } from '../header/headerContext'; -import { setTargetAndReloadSession, reloadScene, newSession, saveSession, newSnapshot } from './redux/dispatchActions'; -import { snackbarColors } from '../header/constants'; - -/** - * Created by ricgillams on 13/06/2018. - */ - -export const withSessionManagement = WrappedComponent => { - return memo(({ ...rest }) => { - const [state, setState] = useState(); - - const { pathname } = window.location; - const { nglViewList } = useContext(NglContext); - const { setHeaderNavbarTitle, setHeaderButtons, setSnackBarTitle, setSnackBarColor } = useContext(HeaderContext); - const dispatch = useDispatch(); - const savingState = useSelector(state => state.apiReducers.savingState); - const sessionTitle = useSelector(state => state.apiReducers.sessionTitle); - const uuid = useSelector(state => state.apiReducers.uuid); - const sessionId = useSelector(state => state.apiReducers.sessionId); - const saveType = useSelector(state => state.sessionReducers.saveType); - const newSessionFlag = useSelector(state => state.sessionReducers.newSessionFlag); - const nextUuid = useSelector(state => state.sessionReducers.nextUuid); - const loadedSession = useSelector(state => state.sessionReducers.loadedSession); - const targetIdList = useSelector(state => state.apiReducers.target_id_list); - const targetName = useSelector(state => state.apiReducers.target_on_name); - - const disableButtons = - (savingState && - (savingState.startsWith(savingStateConst.saving) || savingState.startsWith(savingStateConst.overwriting))) || - false; - - useEffect(() => { - dispatch(setTargetAndReloadSession({ pathname, nglViewList, loadedSession, targetIdList })); - }, [dispatch, loadedSession, nglViewList, pathname, targetIdList]); - - useEffect(() => { - dispatch(reloadScene({ saveType, newSessionFlag, nextUuid, uuid, sessionId })).catch(error => { - setState(() => { - throw error; - }); - }); - }, [dispatch, newSessionFlag, nextUuid, saveType, sessionId, setState, uuid]); - - // Function for set Header buttons, target title and snackBar information about session - useEffect(() => { - if (targetName !== undefined) { - setHeaderNavbarTitle(targetName); - } - if (sessionTitle === undefined || sessionTitle === 'undefined') { - setHeaderButtons([ - , - , - - ]); - setSnackBarTitle('Currently no active session.'); - setSnackBarColor(snackbarColors.default); - } else { - setHeaderButtons([ - , - , - , - - ]); - setSnackBarTitle(`Session: ${sessionTitle}`); - setSnackBarColor(snackbarColors.default); - } - - return () => { - setHeaderButtons(null); - setSnackBarTitle(null); - setHeaderNavbarTitle(''); - }; - }, [ - disableButtons, - dispatch, - sessionTitle, - setHeaderNavbarTitle, - setHeaderButtons, - setSnackBarTitle, - targetIdList, - targetName, - setSnackBarColor - ]); - - return ; - }); -}; diff --git a/js/components/session/constants.js b/js/components/snapshot/constants.js similarity index 100% rename from js/components/session/constants.js rename to js/components/snapshot/constants.js diff --git a/js/components/session/downloadPdb.js b/js/components/snapshot/downloadPdb.js similarity index 100% rename from js/components/session/downloadPdb.js rename to js/components/snapshot/downloadPdb.js diff --git a/js/components/session/helpers.js b/js/components/snapshot/helpers.js similarity index 100% rename from js/components/session/helpers.js rename to js/components/snapshot/helpers.js diff --git a/js/components/snapshot/modals/modalShareSnapshot.js b/js/components/snapshot/modals/modalShareSnapshot.js new file mode 100644 index 000000000..27c18fcee --- /dev/null +++ b/js/components/snapshot/modals/modalShareSnapshot.js @@ -0,0 +1,46 @@ +/** + * Created by ricgillams on 14/06/2018. + */ + +import React, { memo } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import Modal from '../../common/Modal'; +import { DialogTitle, DialogContent, DialogContentText, DialogActions } from '@material-ui/core'; +import { Button } from '../../common/Inputs/Button'; +import { updateClipboard } from '../helpers'; +import { setSharedSnapshot } from '../redux/actions'; +import { initSharedSnapshot } from '../redux/reducer'; + +export const ModalShareSnapshot = memo(({}) => { + const dispatch = useDispatch(); + const sharedSnapshot = useSelector(state => state.snapshotReducers.sharedSnapshot); + + const openInNewTab = () => { + window.open(sharedSnapshot.url); + }; + + const closeModal = () => { + dispatch(setSharedSnapshot(initSharedSnapshot)); + }; + + return ( + + {sharedSnapshot.title} + + {sharedSnapshot.description} + {sharedSnapshot.url} + + + + + + + + ); +}); diff --git a/js/components/snapshot/modals/newSnapshotForm.js b/js/components/snapshot/modals/newSnapshotForm.js new file mode 100644 index 000000000..512624ecf --- /dev/null +++ b/js/components/snapshot/modals/newSnapshotForm.js @@ -0,0 +1,124 @@ +import React, { memo, useState } from 'react'; +import { Grid, makeStyles, Typography } from '@material-ui/core'; +import { useDispatch, useSelector } from 'react-redux'; +import { DJANGO_CONTEXT } from '../../../utils/djangoContext'; +import { Form, Formik } from 'formik'; +import { InputFieldAvatar } from '../../projects/projectModal/inputFieldAvatar'; +import { Description, Title } 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 { useHistory } from 'react-router-dom'; + +const useStyles = makeStyles(theme => ({ + body: { + width: '100%', + marginTop: theme.spacing(1), + marginBottom: theme.spacing(1) + }, + input: { + width: 400 + }, + margin: { + margin: theme.spacing(1) + }, + formControl: { + margin: theme.spacing(1), + width: 400 + } +})); + +export const NewSnapshotForm = memo(({ handleCloseModal }) => { + const classes = useStyles(); + const [state, setState] = useState(); + const dispatch = useDispatch(); + + const currentSnapshot = useSelector(state => state.projectReducers.currentSnapshot); + const currentProject = useSelector(state => state.projectReducers.currentProject); + const isLoadingSnapshotDialog = useSelector(state => state.snapshotReducers.isLoadingSnapshotDialog); + + return ( + <> + Snapshot details + { + const errors = {}; + if (!values.title) { + errors.title = 'Required!'; + } + if (!values.description) { + errors.description = 'Required!'; + } + return errors; + }} + onSubmit={values => { + const title = values.title; + const description = values.description; + const type = SnapshotType.MANUAL; + const author = DJANGO_CONTEXT['pk'] || null; + const parent = currentSnapshot.id; + const session_project = currentProject.projectID; + + dispatch(createNewSnapshot({ title, description, type, author, parent, session_project })).catch(error => { + setState(() => { + throw error; + }); + }); + }} + > + {({ submitForm }) => ( +
+ + + } + field={ + + } + /> + + + } + field={ + + } + /> + + + + + + + + + + +
+ )} +
+ + ); +}); diff --git a/js/components/snapshot/modals/newSnapshotModal.js b/js/components/snapshot/modals/newSnapshotModal.js new file mode 100644 index 000000000..1dc74a5ba --- /dev/null +++ b/js/components/snapshot/modals/newSnapshotModal.js @@ -0,0 +1,24 @@ +import React, { memo } from 'react'; +import Modal from '../../common/Modal'; +import { useDispatch, useSelector } from 'react-redux'; +import { setOpenSnapshotSavingDialog } from '../redux/actions'; +import { NewSnapshotForm } from './newSnapshotForm'; +import { AddProjectDetail } from '../../projects/addProjectDetail'; + +export const NewSnapshotModal = memo(({}) => { + const dispatch = useDispatch(); + const openSavingDialog = useSelector(state => state.snapshotReducers.openSavingDialog); + const dialogCurrentStep = useSelector(state => state.snapshotReducers.dialogCurrentStep); + const projectID = useSelector(state => state.projectReducers.currentProject.projectID); + + const handleCloseModal = () => { + dispatch(setOpenSnapshotSavingDialog(false)); + }; + + return ( + + {!projectID && dialogCurrentStep === 0 && } + {projectID && } + + ); +}); diff --git a/js/components/snapshot/modals/saveSnapshotBeforeExit.js b/js/components/snapshot/modals/saveSnapshotBeforeExit.js new file mode 100644 index 000000000..a0c250225 --- /dev/null +++ b/js/components/snapshot/modals/saveSnapshotBeforeExit.js @@ -0,0 +1,52 @@ +import React, { memo, useContext } from 'react'; +import Modal from '../../common/Modal'; +import { DialogActions, DialogContent, DialogContentText, DialogTitle } from '@material-ui/core'; +import { Button } from '../../common/Inputs/Button'; +import { switchBetweenSnapshots } from '../../preview/redux/dispatchActions'; +import { useDispatch, useSelector } from 'react-redux'; +import { NglContext } from '../../nglView/nglProvider'; +import { useHistory, useRouteMatch } from 'react-router-dom'; +import { setIsOpenModalBeforeExit, setOpenSnapshotSavingDialog, setSelectedSnapshotToSwitch } from '../redux/actions'; + +export const SaveSnapshotBeforeExit = memo(() => { + const { nglViewList } = useContext(NglContext); + let history = useHistory(); + let match = useRouteMatch(); + const projectID = match && match.params && match.params.projectId; + const isOpen = useSelector(state => state.snapshotReducers.isOpenModalSaveSnapshotBeforeExit); + const snapshotID = useSelector(state => state.snapshotReducers.selectedSnapshotToSwitch); + + const dispatch = useDispatch(); + + const handleCloseModal = () => { + dispatch(setIsOpenModalBeforeExit(false)); + }; + + const handleOnNo = () => { + dispatch(switchBetweenSnapshots({ nglViewList, projectID, snapshotID, history })); + dispatch(setSelectedSnapshotToSwitch(null)); + handleCloseModal(); + }; + + const handleOnYes = () => { + dispatch(setOpenSnapshotSavingDialog(true)); + handleCloseModal(); + }; + + return ( + + Do you want to save all changes? + + Please consider saving your changes because you can lose them. + + + + + + + ); +}); diff --git a/js/components/snapshot/redux/actions.js b/js/components/snapshot/redux/actions.js new file mode 100644 index 000000000..db684d1ea --- /dev/null +++ b/js/components/snapshot/redux/actions.js @@ -0,0 +1,42 @@ +import { constants } from './constants'; +import { initSharedSnapshot } from './reducer'; + +export const setOpenSnapshotSavingDialog = (isOpen = false) => ({ + type: constants.SET_OPEN_SAVING_DIALOG, + payload: isOpen +}); + +export const setDialogCurrentStep = (currentStep = 0) => ({ + type: constants.SET_DIALOG_CURRENT_STEP, + payload: currentStep +}); + +export const setIsLoadingSnapshotDialog = isLoading => ({ + type: constants.SET_IS_LOADING_SNAPSHOT_DIALOG, + payload: isLoading +}); + +export const setListOfSnapshots = list => ({ + type: constants.SET_LIST_OF_SNAPSHOTS, + payload: list +}); + +export const setIsLoadingListOfSnapshots = isLoading => ({ + type: constants.SET_IS_LOADING_LIST_OF_SNAPSHOTS, + payload: isLoading +}); + +export const setSharedSnapshot = (sharedSnapshot = initSharedSnapshot) => ({ + type: constants.SET_SHARED_SNAPSHOT, + payload: sharedSnapshot +}); + +export const setIsOpenModalBeforeExit = (isOpen = false) => ({ + type: constants.SET_IS_OPEN_MODAL_BEFORE_EXIT, + payload: isOpen +}); + +export const setSelectedSnapshotToSwitch = (snapshot = null) => ({ + type: constants.SET_SELECTED_SNAPSHOT_TO_SWITCH, + payload: snapshot +}); diff --git a/js/components/snapshot/redux/constants.js b/js/components/snapshot/redux/constants.js new file mode 100644 index 000000000..51063a508 --- /dev/null +++ b/js/components/snapshot/redux/constants.js @@ -0,0 +1,20 @@ +const prefix = 'SESSION_'; + +export const constants = { + SET_SAVE_TYPE: prefix + 'SET_SAVE_TYPE', + SET_NEXT_UUID: prefix + 'SET_NEXT_UUID', + SET_NEW_SESSION_FLAG: prefix + 'SET_NEW_SESSION_FLAG', + SET_LOADED_SESSION: prefix + 'SET_LOADED_SESSION', + + SET_OPEN_SAVING_DIALOG: prefix + 'SET_OPEN_SAVING_DIALOG', + SET_DIALOG_CURRENT_STEP: prefix + 'SET_DIALOG_CURRENT_STEP', + SET_IS_LOADING_SNAPSHOT_DIALOG: prefix + 'SET_IS_LOADING_SNAPSHOT_DIALOG', + + SET_LIST_OF_SNAPSHOTS: prefix + 'SET_LIST_OF_SNAPSHOTS', + SET_IS_LOADING_LIST_OF_SNAPSHOTS: prefix + 'SET_IS_LOADING_LIST_OF_SNAPSHOTS', + + SET_SHARED_SNAPSHOT: prefix + 'SET_SHARED_SNAPSHOT', + + SET_IS_OPEN_MODAL_BEFORE_EXIT: prefix + 'SET_IS_OPEN_MODAL_BEFORE_EXIT', + SET_SELECTED_SNAPSHOT_TO_SWITCH: prefix + 'SET_SELECTED_SNAPSHOT_TO_SWITCH' +}; diff --git a/js/components/snapshot/redux/dispatchActions.js b/js/components/snapshot/redux/dispatchActions.js new file mode 100644 index 000000000..1ee92a032 --- /dev/null +++ b/js/components/snapshot/redux/dispatchActions.js @@ -0,0 +1,280 @@ +import { reloadApiState, setSessionTitle } from '../../../reducers/api/actions'; +import { reloadSelectionReducer } from '../../../reducers/selection/actions'; +import { api, METHOD } from '../../../utils/api'; +import { + setDialogCurrentStep, + setIsLoadingListOfSnapshots, + setIsLoadingSnapshotDialog, + setListOfSnapshots, + setOpenSnapshotSavingDialog +} from './actions'; +import { DJANGO_CONTEXT } from '../../../utils/djangoContext'; +import { assignSnapshotToProject, loadSnapshotTree } from '../../projects/redux/dispatchActions'; +import { reloadPreviewReducer } from '../../preview/redux/dispatchActions'; +import { SnapshotType } from '../../projects/redux/constants'; +import moment from 'moment'; +import { setProteinLoadingState } from '../../../reducers/ngl/actions'; +import { reloadNglViewFromSnapshot } from '../../../reducers/ngl/dispatchActions'; +import { base_url, URLS } from '../../routes/constants'; +import { resetCurrentSnapshot, setCurrentSnapshot } from '../../projects/redux/actions'; + +export const reloadSession = (snapshotData, nglViewList) => (dispatch, getState) => { + const state = getState(); + const snapshotTitle = state.projectReducers.currentSnapshot.title; + + dispatch(reloadApiState(snapshotData.apiReducers)); + // dispatch(setSessionId(myJson.id)); + dispatch(setSessionTitle(snapshotTitle)); + + if (nglViewList.length > 0) { + dispatch(reloadSelectionReducer(snapshotData.selectionReducers)); + + nglViewList.forEach(nglView => { + dispatch(reloadNglViewFromSnapshot(nglView.stage, nglView.id, snapshotData.nglReducers)); + }); + + if (snapshotData.selectionReducers.vectorOnList.length !== 0) { + dispatch(reloadPreviewReducer(snapshotData.previewReducers)); + } + } + + dispatch(setProteinLoadingState(true)); +}; + +export const saveCurrentSnapshot = ({ + type, + title, + author, + description, + data, + created, + parent, + children, + session_project = null +}) => (dispatch, getState) => { + dispatch(resetCurrentSnapshot()); + return api({ + url: `${base_url}/api/snapshots/`, + data: { type, title, author, description, data: JSON.stringify(data), created, parent, children, session_project }, + method: METHOD.POST + }) + .then(response => + dispatch( + setCurrentSnapshot({ + id: response.data.id, + type, + title, + author, + description, + created, + parent, + children: response.data.children, + data + }) + ) + ) + + .catch(error => { + throw new Error(error); + }) + .finally(() => { + dispatch(getListOfSnapshots()); + }); +}; + +export const createInitialSnapshot = projectID => async (dispatch, getState) => { + const { apiReducers, nglReducers, selectionReducers, previewReducers } = getState(); + const data = { apiReducers, nglReducers, selectionReducers, previewReducers }; + const type = SnapshotType.INIT; + const title = 'Initial Snapshot'; + const author = DJANGO_CONTEXT.pk || null; + const description = 'Auto generated initial snapshot'; + const parent = null; + const children = []; + const created = moment(); + + // store initial snapshot to BE + if (projectID) { + await dispatch(saveCurrentSnapshot({ type, title, author, description, data, created, parent, children })); + + await dispatch(assignSnapshotToProject({ projectID, snapshotID: getState().projectReducers.currentSnapshot.id })); + dispatch(loadSnapshotTree(projectID)).catch(error => { + throw new Error(error); + }); + } + // store initial snapshot only to redux state + else { + dispatch( + setCurrentSnapshot({ + id: null, + type, + title, + author, + description, + created, + parent, + children, + data + }) + ); + } +}; + +export const createInitSnapshotFromCopy = ({ + title, + author, + description, + data, + created, + parent, + children, + session_project +}) => (dispatch, getState) => { + if (session_project) { + return dispatch( + saveCurrentSnapshot({ + type: SnapshotType.INIT, + title, + author, + description, + data, + created, + parent, + children, + session_project + }) + ); + } + return Promise.reject('ProjectID is missing'); +}; + +export const createNewSnapshot = ({ title, description, type, author, parent, session_project }) => ( + dispatch, + getState +) => { + const state = getState(); + const { apiReducers, nglReducers, selectionReducers, previewReducers } = state; + const data = { apiReducers, nglReducers, selectionReducers, previewReducers }; + const selectedSnapshotToSwitch = state.snapshotReducers.selectedSnapshotToSwitch; + + if (!session_project) { + return Promise.reject('Project ID is missing!'); + } + + let newType = type; + + return Promise.all([ + dispatch(setIsLoadingSnapshotDialog(true)), + api({ url: `${base_url}/api/snapshots/?session_project=${session_project}&type=INIT` }).then(response => { + if (response.data.count === 0) { + newType = SnapshotType.INIT; + } + return api({ + url: `${base_url}/api/snapshots/`, + data: { + title, + description, + type: newType, + author, + parent, + session_project, + data: JSON.stringify(data), + children: [] + }, + method: METHOD.POST + }).then(response => { + Promise.all([ + dispatch(setDialogCurrentStep(0)), + dispatch(setIsLoadingSnapshotDialog(false)), + dispatch(setOpenSnapshotSavingDialog(false)), + dispatch( + setCurrentSnapshot({ + id: response.data.id, + type: response.data.type, + title: response.data.title, + author: response.data.author, + description: response.data.description, + created: response.data.created, + children: response.data.children, + parent: response.data.parent, + data: JSON.parse(response.data.data) + }) + ), + dispatch(getListOfSnapshots()) + ]); + // redirect to project with newest created snapshot /:projectID/:snapshotID + if (response.data.id && session_project) { + // Really bad usage or redirection. Hint for everybody in this line ignore it, but in other parts of code + // use react-router ! + window.location.replace( + `${URLS.projects}${session_project}/${ + selectedSnapshotToSwitch === null ? response.data.id : selectedSnapshotToSwitch + }` + ); + } + }); + }) + ]); +}; + +export const createSnapshotFromOld = (snapshot, history) => (dispatch, getState) => { + if (!snapshot) { + return Promise.reject('Snapshot is missing!'); + } + const { title, description, data, author, session_project, children } = snapshot; + if (!session_project) { + return Promise.reject('Project ID is missing!'); + } + + return Promise.all([ + dispatch(setIsLoadingSnapshotDialog(true)), + api({ + url: `${base_url}/api/snapshots/`, + data: { + title, + description, + type: SnapshotType.INIT, + author, + parent: null, + session_project, + data: JSON.stringify(data), + children + }, + method: METHOD.POST + }).then(response => { + Promise.all([ + dispatch( + setCurrentSnapshot({ + id: response.data.id, + type: response.data.type, + title: response.data.title, + author: response.data.author, + description: response.data.description, + created: response.data.created, + children: response.data.children, + parent: response.data.parent, + data: JSON.parse(response.data.data) + }) + ), + dispatch(getListOfSnapshots()) + ]); + // redirect to project with newest created snapshot /:projectID/:snapshotID + if (response.data.id && session_project) { + history.push(`${URLS.projects}${session_project}/${response.data.id}`); + } + }) + ]); +}; + +export const getListOfSnapshots = () => (dispatch, getState) => { + dispatch(setIsLoadingListOfSnapshots(true)); + return api({ url: `${base_url}/api/snapshots/?session_project!=null` }) + .then(response => { + if (response && response.data && response.data.results) { + dispatch(setListOfSnapshots(response.data.results)); + } + }) + .finally(() => { + dispatch(setIsLoadingListOfSnapshots(false)); + }); +}; diff --git a/js/components/snapshot/redux/reducer.js b/js/components/snapshot/redux/reducer.js new file mode 100644 index 000000000..86ef0618f --- /dev/null +++ b/js/components/snapshot/redux/reducer.js @@ -0,0 +1,88 @@ +import { constants } from './constants'; + +export const initSharedSnapshot = { + url: null, + title: null, + description: null +}; + +export const INITIAL_STATE = { + saveType: '', + nextUuid: '', + newSessionFlag: 0, + loadedSession: undefined, + openSavingDialog: false, + dialogCurrentStep: 0, + isLoadingSnapshotDialog: false, + listOfSnapshots: [], + isLoadingListOfSnapshots: false, + sharedSnapshotURL: null, + sharedSnapshot: initSharedSnapshot, + isOpenModalSaveSnapshotBeforeExit: false, + selectedSnapshotToSwitch: null +}; + +export const snapshotReducers = (state = INITIAL_STATE, action = {}) => { + switch (action.type) { + case constants.SET_SAVE_TYPE: + return Object.assign({}, state, { + saveType: action.payload + }); + + case constants.SET_NEXT_UUID: + return Object.assign({}, state, { + nextUuid: action.payload + }); + + case constants.SET_NEW_SESSION_FLAG: + return Object.assign({}, state, { + newSessionFlag: action.payload + }); + + case constants.SET_LOADED_SESSION: + return Object.assign({}, state, { + loadedSession: action.payload + }); + case constants.SET_OPEN_SAVING_DIALOG: + return Object.assign({}, state, { + openSavingDialog: action.payload + }); + + case constants.SET_DIALOG_CURRENT_STEP: + return Object.assign({}, state, { + dialogCurrentStep: action.payload + }); + + case constants.SET_IS_LOADING_SNAPSHOT_DIALOG: + return Object.assign({}, state, { + isLoadingSnapshotDialog: action.payload + }); + + case constants.SET_LIST_OF_SNAPSHOTS: + return Object.assign({}, state, { + listOfSnapshots: action.payload + }); + + case constants.SET_IS_LOADING_LIST_OF_SNAPSHOTS: + return Object.assign({}, state, { + isLoadingListOfSnapshots: action.payload + }); + case constants.SET_SHARED_SNAPSHOT: + return Object.assign({}, state, { + sharedSnapshot: action.payload + }); + + case constants.SET_IS_OPEN_MODAL_BEFORE_EXIT: + return Object.assign({}, state, { + isOpenModalSaveSnapshotBeforeExit: action.payload + }); + + case constants.SET_SELECTED_SNAPSHOT_TO_SWITCH: + return Object.assign({}, state, { + selectedSnapshotToSwitch: action.payload + }); + + default: + return state; + } +}; diff --git a/js/components/session/sessionList.js b/js/components/snapshot/sessionList.js similarity index 100% rename from js/components/session/sessionList.js rename to js/components/snapshot/sessionList.js diff --git a/js/components/snapshot/withSnapshotManagement.js b/js/components/snapshot/withSnapshotManagement.js new file mode 100644 index 000000000..3834ce6a5 --- /dev/null +++ b/js/components/snapshot/withSnapshotManagement.js @@ -0,0 +1,74 @@ +import React, { memo, useContext, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { Button } from '@material-ui/core'; +import { Save } from '@material-ui/icons'; +import DownloadPdb from './downloadPdb'; +import { savingStateConst } from './constants'; +import { HeaderContext } from '../header/headerContext'; +import { setOpenSnapshotSavingDialog } from './redux/actions'; +import { useRouteMatch } from 'react-router-dom'; + +/** + * Created by ricgillams on 13/06/2018. + */ + +export const withSnapshotManagement = WrappedComponent => { + return memo(({ ...rest }) => { + let match = useRouteMatch(); + const { setHeaderNavbarTitle, setHeaderButtons, setSnackBarTitle, setSnackBarColor } = useContext(HeaderContext); + const dispatch = useDispatch(); + const savingState = useSelector(state => state.apiReducers.savingState); + const sessionTitle = useSelector(state => state.apiReducers.sessionTitle); + + const targetIdList = useSelector(state => state.apiReducers.target_id_list); + const targetName = useSelector(state => state.apiReducers.target_on_name); + const projectId = match && match.params && match.params.projectId; + + const disableButtons = + (savingState && + (savingState.startsWith(savingStateConst.saving) || savingState.startsWith(savingStateConst.overwriting))) || + !projectId || + !targetName || + false; + + // Function for set Header buttons, target title and snackBar information about session + useEffect(() => { + if (targetName !== undefined) { + setHeaderNavbarTitle(targetName); + } + setHeaderButtons([ + , + + ]); + // setSnackBarTitle('Currently no active session.'); + // setSnackBarTitle(`Session: ${sessionTitle}`); + + return () => { + setHeaderButtons(null); + setSnackBarTitle(null); + setHeaderNavbarTitle(''); + }; + }, [ + disableButtons, + dispatch, + sessionTitle, + setHeaderNavbarTitle, + setHeaderButtons, + setSnackBarTitle, + targetIdList, + targetName, + setSnackBarColor, + projectId + ]); + + return ; + }); +}; diff --git a/js/components/target/redux/dispatchActions.js b/js/components/target/redux/dispatchActions.js index 0d2b86d9e..a34e99140 100644 --- a/js/components/target/redux/dispatchActions.js +++ b/js/components/target/redux/dispatchActions.js @@ -10,6 +10,8 @@ import { import { setOldUrl } from './actions'; import { api } from '../../../utils/api'; import { resetSelectionState } from '../../../reducers/selection/actions'; +import { base_url } from '../../routes/constants'; +import { setCurrentProject } from '../../projects/redux/actions'; export const loadTargetList = onCancel => (dispatch, getState) => { const oldUrl = getState().targetReducers.oldUrl; @@ -24,31 +26,55 @@ export const loadTargetList = onCancel => (dispatch, getState) => { }); }; -export const updateTarget = (notCheckTarget, target, setIsLoading, targetIdList) => dispatch => { - if (!notCheckTarget) { - // Get from the REST API - let targetUnrecognisedFlag = true; - if (target !== undefined) { - if (targetIdList && targetIdList.length > 0) { - targetIdList.forEach(targetId => { - if (target === targetId.title) { - targetUnrecognisedFlag = false; - } - }); - } - dispatch(setTargetUnrecognised(targetUnrecognisedFlag)); +export const updateTarget = ({ target, setIsLoading, targetIdList, projectId }) => dispatch => { + // Get from the REST API + let targetUnrecognisedFlag = true; + if (target !== undefined) { + if (targetIdList && targetIdList.length > 0) { + targetIdList.forEach(targetId => { + if (target === targetId.title) { + targetUnrecognisedFlag = false; + } + }); } - - if (targetUnrecognisedFlag === false) { - setIsLoading(true); - return api({ - url: `${window.location.protocol}//${window.location.host}/api/targets/?title=${target}` - }) + dispatch(setTargetUnrecognised(targetUnrecognisedFlag)); + } else if (projectId !== undefined) { + targetUnrecognisedFlag = false; + dispatch(setTargetUnrecognised(targetUnrecognisedFlag)); + } + // for Targets + if (targetUnrecognisedFlag === false) { + setIsLoading(true); + let url = undefined; + if (target) { + url = `${base_url}/api/targets/?title=${target}`; + return api({ url }) .then(response => { return dispatch(setTargetOn(response.data['results'][0].id)); }) .finally(() => setIsLoading(false)); } + // for Projects + else if (projectId !== undefined) { + setIsLoading(true); + return api({ url: `${base_url}/api/session-projects/${projectId}/` }) + .then(response => { + return Promise.all([ + dispatch(setTargetOn(response.data.target.id)), + dispatch( + setCurrentProject({ + projectID: response.data.id, + authorID: response.data.author.id, + title: response.data.title, + description: response.data.description, + targetID: response.data.target.id, + tags: JSON.parse(response.data.tags) + }) + ) + ]); + }) + .finally(() => setIsLoading(false)); + } } return Promise.resolve(); }; diff --git a/js/components/target/targetList.js b/js/components/target/targetList.js index b9745eff7..918a6f535 100644 --- a/js/components/target/targetList.js +++ b/js/components/target/targetList.js @@ -7,10 +7,11 @@ import { connect } from 'react-redux'; import { ListItemText, ListItemSecondaryAction } from '@material-ui/core'; import { List, ListItem, Panel } from '../common'; import { Link } from 'react-router-dom'; +import { URLS } from '../routes/constants'; const TargetList = memo(({ target_id_list }) => { const render_method = data => { - const preview = '/viewer/react/preview/target/' + data.title; + 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']; diff --git a/js/components/target/withUpdatingTarget.js b/js/components/target/withUpdatingTarget.js index 297acfb06..312303ba1 100644 --- a/js/components/target/withUpdatingTarget.js +++ b/js/components/target/withUpdatingTarget.js @@ -1,27 +1,22 @@ -import React, { memo, useContext, useEffect } from 'react'; +import React, { memo, useContext, useEffect, useState } from 'react'; import { connect } from 'react-redux'; import { HeaderContext } from '../header/headerContext'; import HandleUnrecognisedTarget from './handleUnrecognisedTarget'; import { updateTarget, setTargetUUIDs, resetTargetAndSelection } from './redux/dispatchActions'; +import { useRouteMatch } from 'react-router-dom'; export const withUpdatingTarget = WrappedContainer => { const UpdateTarget = memo( - ({ - match, - target_on, - resetSelection, - notCheckTarget, - updateTarget, - setTargetUUIDs, - resetTargetAndSelection, - targetIdList, - ...rest - }) => { - const target = match.params.target; - const uuid = match.params.uuid; - const snapshotUuid = match.params.snapshotUuid; + ({ target_on, resetSelection, updateTarget, setTargetUUIDs, resetTargetAndSelection, targetIdList, ...rest }) => { + let match = useRouteMatch(); + + const target = match && match.params && match.params.target; + const uuid = match && match.params && match.params.uuid; + const snapshotUuid = match && match.params && match.params.snapshotUuid; + const projectId = match && match.params && match.params.projectId; const { isLoading, setIsLoading } = useContext(HeaderContext); + const [state, setState] = useState(); useEffect(() => { resetTargetAndSelection(resetSelection); @@ -32,10 +27,12 @@ export const withUpdatingTarget = WrappedContainer => { }, [setTargetUUIDs, snapshotUuid, uuid]); useEffect(() => { - updateTarget(notCheckTarget, target, setIsLoading, targetIdList).catch(error => { - throw new Error(error); + updateTarget({ target, setIsLoading, targetIdList, projectId }).catch(error => { + setState(() => { + throw error; + }); }); - }, [notCheckTarget, setIsLoading, target, updateTarget, targetIdList]); + }, [setIsLoading, target, updateTarget, targetIdList, projectId]); if (isLoading === true) { return null; diff --git a/js/img/helpScreenshot.png b/js/img/helpScreenshot.png new file mode 100644 index 000000000..dfc121399 Binary files /dev/null and b/js/img/helpScreenshot.png differ diff --git a/js/reducers/api/apiReducers.js b/js/reducers/api/apiReducers.js index 2e87c2286..7c89b2aa3 100644 --- a/js/reducers/api/apiReducers.js +++ b/js/reducers/api/apiReducers.js @@ -2,7 +2,7 @@ * Created by abradley on 06/03/2018. */ import { constants } from './constants'; -import { savingStateConst } from '../../components/session/constants'; +import { savingStateConst } from '../../components/snapshot/constants'; export const INITIAL_STATE = { project_id: undefined, @@ -30,7 +30,7 @@ export const INITIAL_STATE = { latestSession: undefined, latestSnapshot: undefined, targetUnrecognised: undefined, - uuid: savingStateConst.UNSET, + uuid: savingStateConst.UNSET, // used only for reloading from scene sessionId: undefined, sessionIdList: [], sessionTitle: undefined, diff --git a/js/reducers/ngl/actions.js b/js/reducers/ngl/actions.js index 239e3980f..211040d27 100644 --- a/js/reducers/ngl/actions.js +++ b/js/reducers/ngl/actions.js @@ -44,15 +44,9 @@ export const setNglOrientation = (orientation, div_id) => ({ type: CONSTANTS.SET export const setProteinLoadingState = hasLoaded => ({ type: CONSTANTS.SET_PROTEINS_HAS_LOADED, payload: hasLoaded }); -export const saveCurrentStateAsDefaultScene = () => ({ type: CONSTANTS.SAVE_NGL_STATE_AS_DEFAULT_SCENE }); - -export const saveCurrentStateAsSessionScene = () => ({ type: CONSTANTS.SAVE_NGL_STATE_AS_SESSION_SCENE }); - -export const resetStateToDefaultScene = () => ({ type: CONSTANTS.RESET_NGL_VIEW_TO_DEFAULT_SCENE }); - -export const resetStateToSessionScene = sessionData => ({ - type: CONSTANTS.RESET_NGL_VIEW_TO_SESSION_SCENE, - payload: sessionData +export const setNglStateFromCurrentSnapshot = snapshot => ({ + type: CONSTANTS.SET_NGL_STATE_FROM_CURRENT_SNAPSHOT, + payload: snapshot }); export const removeAllNglComponents = (stage = undefined) => ({ type: CONSTANTS.REMOVE_ALL_NGL_COMPONENTS, stage }); @@ -67,12 +61,14 @@ export const decrementCountOfRemainingMoleculeGroups = decrementedCount => ({ payload: decrementedCount }); -export const incrementCountOfPendingNglObjects = () => ({ - type: CONSTANTS.INCREMENT_COUNT_OF_PENDING_NGL_OBJECTS +export const incrementCountOfPendingNglObjects = NglViewId => ({ + type: CONSTANTS.INCREMENT_COUNT_OF_PENDING_NGL_OBJECTS, + payload: NglViewId }); -export const decrementCountOfPendingNglObjects = () => ({ - type: CONSTANTS.DECREMENT_COUNT_OF_PENDING_NGL_OBJECTS +export const decrementCountOfPendingNglObjects = NglViewId => ({ + type: CONSTANTS.DECREMENT_COUNT_OF_PENDING_NGL_OBJECTS, + payload: NglViewId }); export const appendMoleculeOrientation = (moleculeGroupID, orientation) => ({ diff --git a/js/reducers/ngl/actions.test.js b/js/reducers/ngl/actions.test.js index 3c487265e..d862ef084 100644 --- a/js/reducers/ngl/actions.test.js +++ b/js/reducers/ngl/actions.test.js @@ -321,7 +321,7 @@ describe("testing ngl reducer's actions", () => { nglOrientations: { a: [24, 566] } }; - let result = nglReducers(initialState, actions.resetStateToSessionScene(sessionScene)); + let result = nglReducers(initialState, actions.setNglStateFromCurrentSnapshot(sessionScene)); expect(result.objectsInView).toStrictEqual(sessionScene.objectsInView); expect(result.nglOrientations).toStrictEqual(sessionScene.nglOrientations); }); diff --git a/js/reducers/ngl/constants.js b/js/reducers/ngl/constants.js index db0fef464..78ca7400b 100644 --- a/js/reducers/ngl/constants.js +++ b/js/reducers/ngl/constants.js @@ -10,10 +10,7 @@ export const CONSTANTS = { SET_NGL_VIEW_PARAMS: prefix + 'SET_NGL_VIEW_PARAMS', SET_ORIENTATION: prefix + 'SET_ORIENTATION', - RESET_NGL_VIEW_TO_DEFAULT_SCENE: prefix + 'RESET_NGL_VIEW_TO_DEFAULT_SCENE', - RESET_NGL_VIEW_TO_SESSION_SCENE: prefix + 'RESET_NGL_VIEW_TO_SESSION_SCENE', - SAVE_NGL_STATE_AS_DEFAULT_SCENE: prefix + 'SAVE_NGL_STATE_AS_DEFAULT_SCENE', - SAVE_NGL_STATE_AS_SESSION_SCENE: prefix + 'SAVE_NGL_STATE_AS_SESSION_SCENE', + SET_NGL_STATE_FROM_CURRENT_SNAPSHOT: prefix + 'SET_NGL_STATE_FROM_CURRENT_SNAPSHOT', REMOVE_ALL_NGL_COMPONENTS: prefix + 'REMOVE_ALL_NGL_COMPONENTS', // Helper variables for marking that protein and molecule groups are successful loaded diff --git a/js/reducers/ngl/dispatchActions.js b/js/reducers/ngl/dispatchActions.js index a778c3ba2..0b042340c 100644 --- a/js/reducers/ngl/dispatchActions.js +++ b/js/reducers/ngl/dispatchActions.js @@ -4,16 +4,12 @@ import { deleteNglObject, incrementCountOfPendingNglObjects, loadNglObject, - resetStateToDefaultScene, - resetStateToSessionScene, - saveCurrentStateAsDefaultScene, + setNglStateFromCurrentSnapshot, setMoleculeOrientations, setNglOrientation, - setNglViewParams, - setProteinLoadingState + setNglViewParams } from './actions'; import { isEmpty, isEqual } from 'lodash'; -import { SCENES } from './constants'; import { createRepresentationsArray } from '../../components/nglView/generatingObjects'; import { SELECTION_TYPE } from '../../components/nglView/constants'; import { @@ -25,10 +21,11 @@ import { removeFromDensityList } from '../selection/actions'; import { nglObjectDictionary } from '../../components/nglView/renderingObjects'; +import { createInitialSnapshot } from '../../components/snapshot/redux/dispatchActions'; -export const loadObject = (target, stage, previousRepresentations, orientationMatrix) => dispatch => { +export const loadObject = (target, stage, previousRepresentations, orientationMatrix) => (dispatch, getState) => { if (stage) { - dispatch(incrementCountOfPendingNglObjects()); + dispatch(incrementCountOfPendingNglObjects(target.display_div)); return nglObjectDictionary[target.OBJECT_TYPE]( stage, target, @@ -40,7 +37,7 @@ export const loadObject = (target, stage, previousRepresentations, orientationMa .catch(error => { console.error(error); }) - .finally(() => dispatch(decrementCountOfPendingNglObjects())); + .finally(() => dispatch(decrementCountOfPendingNglObjects(target.display_div))); } return Promise.reject('Instance of NGL View is missing'); }; @@ -75,31 +72,16 @@ export const deleteObject = (target, stage, deleteFromSelections) => dispatch => dispatch(deleteNglObject(target)); }; -export const decrementCountOfRemainingMoleculeGroupsWithSavingDefaultState = () => (dispatch, getState) => { +export const decrementCountOfRemainingMoleculeGroupsWithSavingDefaultState = projectId => (dispatch, getState) => { const state = getState(); const decrementedCount = state.nglReducers.countOfRemainingMoleculeGroups - 1; + // decide to create INIT snapshot if (decrementedCount === 0 && state.nglReducers.proteinsHasLoaded === true) { - dispatch(saveCurrentStateAsDefaultScene()); + dispatch(createInitialSnapshot(projectId)); } dispatch(decrementCountOfRemainingMoleculeGroups(decrementedCount)); }; -// Helper actions for marking that protein and molecule groups are successful loaded -export const setProteinsHasLoaded = (hasLoaded = false, withoutSavingToDefaultState = false) => ( - dispatch, - getState -) => { - const state = getState(); - if ( - state.nglReducers.countOfRemainingMoleculeGroups === 0 && - hasLoaded === true && - withoutSavingToDefaultState === false - ) { - dispatch(saveCurrentStateAsDefaultScene()); - } - dispatch(setProteinLoadingState(hasLoaded)); -}; - export const setOrientation = (div_id, orientation) => (dispatch, getState) => { const nglOrientations = getState().nglReducers.nglOrientations; @@ -117,31 +99,22 @@ export const setOrientation = (div_id, orientation) => (dispatch, getState) => { * * @param stage - instance of NGL view * @param display_div - id of NGL View div - * @param scene - type of scene (default or session) - * @param sessionData - new session data loaded from API - * @returns {function(...[*]=)} + * @param snapshot - snapshot data of NGL View */ -export const reloadNglViewFromScene = (stage, display_div, scene, sessionData) => (dispatch, getState) => { - const currentScene = (sessionData && sessionData.nglReducers[scene]) || getState().nglReducers[scene]; - switch (scene) { - case SCENES.defaultScene: - dispatch(resetStateToDefaultScene()); - break; - case SCENES.sessionScene: - dispatch(resetStateToSessionScene(sessionData)); - break; - } +export const reloadNglViewFromSnapshot = (stage, display_div, snapshot) => (dispatch, getState) => { + dispatch(setNglStateFromCurrentSnapshot(snapshot)); + // Remove all components in NGL View stage.removeAllComponents(); // Reconstruction of state in NGL View from currentScene data // objectsInView - return Promise.all( - Object.keys(currentScene.objectsInView || {}).map(objInView => { - if (currentScene.objectsInView[objInView].display_div === display_div) { - let representations = currentScene.objectsInView[objInView].representations; + Promise.all( + Object.keys(snapshot.objectsInView || {}).map(objInView => { + if (snapshot.objectsInView[objInView].display_div === display_div) { + let representations = snapshot.objectsInView[objInView].representations; return dispatch( - loadObject(currentScene.objectsInView[objInView], stage, createRepresentationsArray(representations)) + loadObject(snapshot.objectsInView[objInView], stage, createRepresentationsArray(representations)) ); } else { return Promise.resolve(); @@ -149,19 +122,19 @@ export const reloadNglViewFromScene = (stage, display_div, scene, sessionData) = }) ).finally(() => { // loop over nglViewParams - Object.keys(currentScene.viewParams).forEach(param => { - dispatch(setNglViewParams(param, currentScene.viewParams[param], stage)); + Object.keys(snapshot.viewParams).forEach(param => { + dispatch(setNglViewParams(param, snapshot.viewParams[param], stage)); }); // nglOrientations - const newOrientation = currentScene.nglOrientations[display_div]; + const newOrientation = snapshot.nglOrientations[display_div]; if (newOrientation) { stage.viewerControls.orient(newOrientation.elements); } // set molecule orientations - if (currentScene.moleculeOrientations) { - dispatch(setMoleculeOrientations(currentScene.moleculeOrientations)); + if (snapshot.moleculeOrientations) { + dispatch(setMoleculeOrientations(snapshot.moleculeOrientations)); } }); }; diff --git a/js/reducers/ngl/dispatchActions.test.js b/js/reducers/ngl/dispatchActions.test.js index 4ec777a27..98b4ae396 100644 --- a/js/reducers/ngl/dispatchActions.test.js +++ b/js/reducers/ngl/dispatchActions.test.js @@ -4,7 +4,7 @@ import { decrementCountOfRemainingMoleculeGroupsWithSavingDefaultState, deleteObject, loadObject, - reloadNglViewFromScene, + reloadNglViewFromSnapshot, setOrientation, setProteinsHasLoaded } from './dispatchActions'; @@ -17,7 +17,7 @@ import { incrementCountOfPendingNglObjects, loadNglObject, resetStateToDefaultScene, - resetStateToSessionScene, + setNglStateFromCurrentSnapshot, saveCurrentStateAsDefaultScene, setNglOrientation, setProteinLoadingState @@ -289,10 +289,11 @@ describe("testing ngl reducer's async actions", () => { } } }; - store.dispatch(reloadNglViewFromScene(stage, display_div, scene, sessionData)); - expect(await getAction(store, resetStateToSessionScene)).toStrictEqual({ - type: getActionType(resetStateToSessionScene), + store.dispatch(reloadNglViewFromSnapshot(stage, display_div, scene, sessionData)); + + expect(await getAction(store, setNglStateFromCurrentSnapshot)).toStrictEqual({ + type: getActionType(setNglStateFromCurrentSnapshot), payload: sessionData }); }); @@ -318,7 +319,7 @@ describe("testing ngl reducer's async actions", () => { }; const display_div = 'MajorView'; - store.dispatch(reloadNglViewFromScene(stage, display_div, scene)); + store.dispatch(reloadNglViewFromSnapshot(stage, display_div, scene)); expect(await getAction(store, resetStateToDefaultScene)).not.toBeNull(); }); diff --git a/js/reducers/ngl/nglReducers.js b/js/reducers/ngl/nglReducers.js index c2d1a914c..38aed6478 100644 --- a/js/reducers/ngl/nglReducers.js +++ b/js/reducers/ngl/nglReducers.js @@ -1,5 +1,7 @@ import { BACKGROUND_COLOR, NGL_PARAMS } from '../../components/nglView/constants'; -import { CONSTANTS, SCENES } from './constants'; +import { CONSTANTS } from './constants'; +import NglView from '../../components/nglView/nglView'; +import { VIEWS } from '../../constants/constants'; export const INITIAL_STATE = { // NGL Scene properties @@ -29,12 +31,14 @@ export const INITIAL_STATE = { [NGL_PARAMS.fogNear]: 50, [NGL_PARAMS.fogFar]: 62 }, - [SCENES.defaultScene]: {}, - [SCENES.sessionScene]: {}, + // Helper variables for marking that protein and molecule groups are successful loaded countOfRemainingMoleculeGroups: null, proteinsHasLoaded: null, - countOfPendingNglObjects: 0, + countOfPendingNglObjects: { + [VIEWS.MAJOR_VIEW]: 0, + [VIEWS.SUMMARY_VIEW]: 0 + }, moleculeOrientations: {} }; @@ -118,60 +122,12 @@ export default function nglReducers(state = INITIAL_STATE, action = {}) { viewParams: newViewParams }); - case CONSTANTS.RESET_NGL_VIEW_TO_DEFAULT_SCENE: - const newStateWithoutScene = JSON.parse(JSON.stringify(state.defaultScene)); - return Object.assign({}, state, newStateWithoutScene); - - case CONSTANTS.RESET_NGL_VIEW_TO_SESSION_SCENE: - // return Object.assign({}, state, action.payload); - // in payload are apiReducers, nglReducers and selectionsReducers - // they are probabably not needed for ngl and they flood nglReducers recursively in time - return Object.assign({}, state); - - case CONSTANTS.SAVE_NGL_STATE_AS_DEFAULT_SCENE: - // load state from default scene and replace current state by these data - const stateWithoutScene = JSON.parse(JSON.stringify(state)); - delete stateWithoutScene[SCENES.defaultScene]; - delete stateWithoutScene[SCENES.sessionScene]; - delete stateWithoutScene['countOfRemainingMoleculeGroups']; - delete stateWithoutScene['proteinsHasLoaded']; - delete stateWithoutScene['countOfPendingNglObjects']; - - Object.keys(stateWithoutScene.objectsInView).forEach(objInView => { - stateWithoutScene.objectsInView[objInView].representations = stateWithoutScene.objectsInView[ - objInView - ].representations.map(item => { - delete item['lastKnownID']; - delete item['uuid']; - return item; - }); - }); - - return Object.assign({}, state, { - [SCENES.defaultScene]: stateWithoutScene - }); - - case CONSTANTS.SAVE_NGL_STATE_AS_SESSION_SCENE: - // load state from default scene and replace current state by these data - const stateWithoutSessionScene = JSON.parse(JSON.stringify(state)); - delete stateWithoutSessionScene[SCENES.sessionScene]; - delete stateWithoutSessionScene['countOfRemainingMoleculeGroups']; - delete stateWithoutSessionScene['proteinsHasLoaded']; - delete stateWithoutSessionScene['countOfPendingNglObjects']; - - Object.keys(stateWithoutSessionScene.objectsInView).forEach(objInView => { - stateWithoutSessionScene.objectsInView[objInView].representations = stateWithoutSessionScene.objectsInView[ - objInView - ].representations.map(item => { - delete item['lastKnownID']; - delete item['uuid']; - return item; - }); - }); - - return Object.assign({}, state, { - [SCENES.sessionScene]: stateWithoutSessionScene - }); + case CONSTANTS.SET_NGL_STATE_FROM_CURRENT_SNAPSHOT: + const snapshot = action.payload; + delete snapshot.countOfPendingNglObjects; + delete snapshot.countOfRemainingMoleculeGroups; + delete snapshot.proteinsHasLoaded; + return Object.assign({}, state, snapshot); case CONSTANTS.REMOVE_ALL_NGL_COMPONENTS: if (action.stage) { @@ -191,10 +147,14 @@ export default function nglReducers(state = INITIAL_STATE, action = {}) { return Object.assign({}, state, { countOfRemainingMoleculeGroups: action.payload }); case CONSTANTS.DECREMENT_COUNT_OF_PENDING_NGL_OBJECTS: - return Object.assign({}, state, { countOfPendingNglObjects: state.countOfPendingNglObjects - 1 }); + const newCounts = JSON.parse(JSON.stringify(state.countOfPendingNglObjects)); + newCounts[action.payload] = state.countOfPendingNglObjects[action.payload] - 1; + return Object.assign({}, state, { countOfPendingNglObjects: newCounts }); case CONSTANTS.INCREMENT_COUNT_OF_PENDING_NGL_OBJECTS: - return Object.assign({}, state, { countOfPendingNglObjects: state.countOfPendingNglObjects + 1 }); + const newPendingCounts = JSON.parse(JSON.stringify(state.countOfPendingNglObjects)); + newPendingCounts[action.payload] = state.countOfPendingNglObjects[action.payload] + 1; + return Object.assign({}, state, { countOfPendingNglObjects: newPendingCounts }); case CONSTANTS.SET_MOLECULE_ORIENTATIONS: return Object.assign({}, state, { moleculeOrientations: action.payload }); diff --git a/js/reducers/rootReducer.js b/js/reducers/rootReducer.js index 5587f2f3d..7f9526fe8 100644 --- a/js/reducers/rootReducer.js +++ b/js/reducers/rootReducer.js @@ -6,8 +6,9 @@ import apiReducers from './api/apiReducers'; import nglReducers from './ngl/nglReducers'; import selectionReducers from './selection/selectionReducers'; import { targetReducers } from '../components/target/redux/reducer'; -import { sessionReducers } from '../components/session/redux/reducer'; +import { snapshotReducers } from '../components/snapshot/redux/reducer'; import { previewReducers } from '../components/preview/redux'; +import { projectReducers } from '../components/projects/redux/reducer'; import { issueReducers } from '../components/userFeedback/redux/reducer'; const rootReducer = combineReducers({ @@ -15,8 +16,9 @@ const rootReducer = combineReducers({ nglReducers, selectionReducers, targetReducers, - sessionReducers, + snapshotReducers, previewReducers, + projectReducers, issueReducers }); diff --git a/js/utils/api.js b/js/utils/api.js index d853fb118..9088449f3 100644 --- a/js/utils/api.js +++ b/js/utils/api.js @@ -23,7 +23,15 @@ export const api = ({ url, method, headers, data, cancel }) => axios({ url, method: method !== undefined ? method : METHOD.GET, - headers, + headers: + headers !== undefined + ? headers + : { + accept: 'application/json', + 'content-type': 'application/json', + 'X-Requested-With': 'XMLHttpRequest', + 'X-CSRFToken': getCsrfToken() + }, data, cancelToken: new CancelToken(function executor(c) { // An executor function receives a cancel function as a parameter diff --git a/package.json b/package.json old mode 100644 new mode 100755 index 5d0f8efb0..1e286dbb0 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@babel/plugin-proposal-decorators": "^7.0.0", "@babel/polyfill": "^7.0.0", "@date-io/date-fns": "1.x", + "@gitgraph/react": "^1.5.3", "@hot-loader/react-dom": "^16.11.0", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.5.1", @@ -53,6 +54,7 @@ "jszip": "^3.2.2", "lodash": "^4.17.15", "logrocket": "^1.0.3", + "moment": "^2.24.0", "ngl": "2.0.0-dev.37", "react": "^16.11.0", "react-canvas-draw": "^1.1.0", @@ -105,6 +107,7 @@ "eslint-plugin-prettier": "^3.1.2", "eslint-plugin-react": "^7.17.0", "eslint-plugin-react-hooks": "^2.3.0", + "file-loader": "^5.0.2", "jest": "^24.9.0", "jest-canvas-mock": "^2.2.0", "lint-staged": "^9.5.0", diff --git a/yarn.lock b/yarn.lock index 2ae36882e..2207f9c8f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -764,7 +764,14 @@ "@babel/plugin-transform-react-jsx-self" "^7.7.4" "@babel/plugin-transform-react-jsx-source" "^7.7.4" -"@babel/runtime@^7.1.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3": +"@babel/runtime@^7.1.2", "@babel/runtime@^7.4.0": + version "7.8.7" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.8.7.tgz#8fefce9802db54881ba59f90bb28719b4996324d" + integrity sha512-+AATMUFppJDw6aiR5NVPHqIQBlV/Pj8wY/EZH+lmvRdUo9xBaz/rF3alAwFJQavvKfeOlPE7oaaDHVbcySbCsg== + dependencies: + regenerator-runtime "^0.13.4" + +"@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3": version "7.7.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.2.tgz#111a78002a5c25fc8e3361bedc9529c696b85a6a" integrity sha512-JONRbXbTXc9WQE2mAZd1p0Z3DZ/6vaQIkgYMSTP3KjRCyd7rCZCcfhCyX+YjwcKxcZ82UrxbRD358bpExNgrjw== @@ -869,6 +876,18 @@ resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.7.4.tgz#f14932887422c9056b15a8d222a9074a7dfa2831" integrity sha512-fxfMSBMX3tlIbKUdtGKxqB1fyrH6gVrX39Gsv3y8lRYKUqlgDt3UMqQyGnR1bQMa2B8aGnhLZokZgg8vT0Le+A== +"@gitgraph/core@1.4.4": + version "1.4.4" + resolved "https://registry.yarnpkg.com/@gitgraph/core/-/core-1.4.4.tgz#5c76e3e33c82e2f7dd651f3bda142c3ba8ae3158" + integrity sha512-RLnObfv7XdHpc6k+CKQbSPC+KUJ5VKpDW1ff8RIf2oAIYGWDqdhsSg5PIbh68IbU++kkTrl8vb1zBn9KqOSwpg== + +"@gitgraph/react@^1.5.3": + version "1.5.3" + resolved "https://registry.yarnpkg.com/@gitgraph/react/-/react-1.5.3.tgz#f4a688cfbe96b43ec3109ef72223c41f26d0f367" + integrity sha512-0FYwkFvTXjfzH6JivLF3vNyGgjYYPkIucMJ1j3ghlPFfg1ux0lkHVcba5CTj1dplzj7vc/RKl8KM7vYXdtk3HA== + dependencies: + "@gitgraph/core" "1.4.4" + "@hot-loader/react-dom@^16.11.0": version "16.11.0" resolved "https://registry.yarnpkg.com/@hot-loader/react-dom/-/react-dom-16.11.0.tgz#c0b483923b289db5431516f56ee2a69448ebf9bd" @@ -3255,6 +3274,11 @@ emojis-list@^2.0.0: resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" integrity sha1-TapNnbAPmBmIDHn6RXrlsJof04k= +emojis-list@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" + integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== + encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" @@ -3958,6 +3982,14 @@ file-entry-cache@^5.0.1: dependencies: flat-cache "^2.0.1" +file-loader@^5.0.2: + version "5.1.0" + resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-5.1.0.tgz#cb56c070efc0e40666424309bd0d9e45ac6f2bb8" + integrity sha512-u/VkLGskw3Ue59nyOwUwXI/6nuBCo7KBkniB/l7ICwr/7cPNGsL1WCXUp3GB0qgOOKU1TiP49bv4DZF/LJqprg== + dependencies: + loader-utils "^1.4.0" + schema-utils "^2.5.0" + file-saver@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.2.tgz#06d6e728a9ea2df2cce2f8d9e84dfcdc338ec17a" @@ -4539,20 +4571,20 @@ hmac-drbg@^1.0.0: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.2.1, hoist-non-react-statics@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz#b09178f0122184fb95acf525daaecb4d8f45958b" - integrity sha512-0XsbTXxgiaCDYDIWFcwkmerZPSwywfUqYmwT4jzewKTQSWoE6FCMoUVOeBJWK3E/CrWbxRG3m5GzY4lnIwGRBA== - dependencies: - react-is "^16.7.0" - -hoist-non-react-statics@^3.3.2: +hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== dependencies: react-is "^16.7.0" +hoist-non-react-statics@^3.2.1, hoist-non-react-statics@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz#b09178f0122184fb95acf525daaecb4d8f45958b" + integrity sha512-0XsbTXxgiaCDYDIWFcwkmerZPSwywfUqYmwT4jzewKTQSWoE6FCMoUVOeBJWK3E/CrWbxRG3m5GzY4lnIwGRBA== + dependencies: + react-is "^16.7.0" + homedir-polyfill@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" @@ -5929,6 +5961,15 @@ loader-utils@1.2.3, loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2. emojis-list "^2.0.0" json5 "^1.0.1" +loader-utils@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613" + integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^1.0.1" + locate-path@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" @@ -6350,6 +6391,11 @@ mkdirp@^0.5.0, mkdirp@^0.5.1: dependencies: minimist "0.0.8" +moment@^2.24.0: + version "2.24.0" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" + integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== + moo@^0.4.3: version "0.4.3" resolved "https://registry.yarnpkg.com/moo/-/moo-0.4.3.tgz#3f847a26f31cf625a956a87f2b10fbc013bfd10e" @@ -7576,7 +7622,12 @@ react-is@^16.12.0, react-is@^16.8.0, react-is@^16.8.4, react-is@^16.8.6: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c" integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q== -react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.9.0: +react-is@^16.6.0: + version "16.13.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.0.tgz#0f37c3613c34fe6b37cd7f763a0d6293ab15c527" + integrity sha512-GFMtL0vHkiBv9HluwNZTggSn/sCyEt9n02aM0dSAjGGyqyNlAyftYm4phPxdvCigG15JreC5biwxCgTAJZ7yAA== + +react-is@^16.7.0, react-is@^16.8.1, react-is@^16.9.0: version "16.11.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.11.0.tgz#b85dfecd48ad1ce469ff558a882ca8e8313928fa" integrity sha512-gbBVYR2p8mnriqAwWx9LbuUrShnAuSCNnuPGyc7GJrMVQtPDAh8iLpv7FRuMPFb56KkaVZIYSz1PrjI9q0QPCw== @@ -7814,6 +7865,11 @@ regenerator-runtime@^0.13.2: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz#7cf6a77d8f5c6f60eb73c5fc1955b2ceb01e6bf5" integrity sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw== +regenerator-runtime@^0.13.4: + version "0.13.5" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697" + integrity sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA== + regenerator-transform@^0.14.0: version "0.14.1" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.1.tgz#3b2fce4e1ab7732c08f665dfdb314749c7ddd2fb" @@ -8864,9 +8920,9 @@ timers-browserify@^2.0.4: setimmediate "^1.0.4" tiny-invariant@^1.0.2: - version "1.0.6" - resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.0.6.tgz#b3f9b38835e36a41c843a3b0907a5a7b3755de73" - integrity sha512-FOyLWWVjG+aC0UqG76V53yAWdXfH8bO6FNmyZOuUrzDzK8DI3/JRY25UD7+g49JWM1LXwymsKERB+DzI0dTEQA== + version "1.1.0" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875" + integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw== tiny-warning@^1.0.0, tiny-warning@^1.0.2: version "1.0.3"