diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/actions/profile.js | 14 | ||||
| -rw-r--r-- | src/actions/users.js | 18 | ||||
| -rw-r--r-- | src/api/sagas/index.js | 4 | ||||
| -rw-r--r-- | src/api/sagas/profile.js | 12 | ||||
| -rw-r--r-- | src/api/sagas/users.js | 2 | ||||
| -rw-r--r-- | src/auth/index.js | 2 | ||||
| -rw-r--r-- | src/components/modals/ConfirmationModal.js | 35 | ||||
| -rw-r--r-- | src/components/modals/Modal.js | 14 | ||||
| -rw-r--r-- | src/containers/profile/DeleteProfileModal.js | 36 | ||||
| -rw-r--r-- | src/containers/simulations/NewSimulationModal.js | 2 | ||||
| -rw-r--r-- | src/pages/Profile.js | 37 | ||||
| -rw-r--r-- | src/pages/Profile.sass | 2 | ||||
| -rw-r--r-- | src/reducers/index.js | 5 | ||||
| -rw-r--r-- | src/reducers/modals.js | 30 | ||||
| -rw-r--r-- | src/reducers/simulations.js | 13 | ||||
| -rw-r--r-- | src/routes/index.js | 12 |
16 files changed, 211 insertions, 27 deletions
diff --git a/src/actions/profile.js b/src/actions/profile.js new file mode 100644 index 00000000..421e7602 --- /dev/null +++ b/src/actions/profile.js @@ -0,0 +1,14 @@ +export const OPEN_DELETE_PROFILE_MODAL = "OPEN_DELETE_PROFILE_MODAL"; +export const CLOSE_DELETE_PROFILE_MODAL = "CLOSE_DELETE_PROFILE_MODAL"; + +export function openDeleteProfileModal() { + return { + type: OPEN_DELETE_PROFILE_MODAL + }; +} + +export function closeDeleteProfileModal() { + return { + type: CLOSE_DELETE_PROFILE_MODAL + }; +} diff --git a/src/actions/users.js b/src/actions/users.js index 093adddd..04a5b95a 100644 --- a/src/actions/users.js +++ b/src/actions/users.js @@ -1,5 +1,7 @@ export const FETCH_AUTHORIZATIONS_OF_CURRENT_USER = "FETCH_AUTHORIZATIONS_OF_CURRENT_USER"; export const FETCH_AUTHORIZATIONS_OF_CURRENT_USER_SUCCEEDED = "FETCH_AUTHORIZATIONS_OF_CURRENT_USER_SUCCEEDED"; +export const DELETE_CURRENT_USER = "DELETE_CURRENT_USER"; +export const DELETE_CURRENT_USER_SUCCEEDED = "DELETE_CURRENT_USER_SUCCEEDED"; export function fetchAuthorizationsOfCurrentUser() { return (dispatch, getState) => { @@ -17,3 +19,19 @@ export function fetchAuthorizationsOfCurrentUserSucceeded(authorizationsOfCurren authorizationsOfCurrentUser }; } + +export function deleteCurrentUser() { + return (dispatch, getState) => { + const {auth} = getState(); + dispatch({ + type: DELETE_CURRENT_USER, + userId: auth.userId + }); + }; +} + +export function deleteCurrentUserSucceeded() { + return { + type: DELETE_CURRENT_USER_SUCCEEDED + }; +} diff --git a/src/api/sagas/index.js b/src/api/sagas/index.js index 426b344a..7fe57453 100644 --- a/src/api/sagas/index.js +++ b/src/api/sagas/index.js @@ -1,7 +1,8 @@ import {takeEvery} from "redux-saga/effects"; import {LOG_IN} from "../../actions/auth"; import {ADD_SIMULATION, DELETE_SIMULATION} from "../../actions/simulations"; -import {FETCH_AUTHORIZATIONS_OF_CURRENT_USER} from "../../actions/users"; +import {DELETE_CURRENT_USER, FETCH_AUTHORIZATIONS_OF_CURRENT_USER} from "../../actions/users"; +import {onDeleteCurrentUser} from "./profile"; import {onSimulationAdd, onSimulationDelete} from "./simulations"; import {onFetchAuthorizationsOfCurrentUser, onFetchLoggedInUser} from "./users"; @@ -10,4 +11,5 @@ export default function* rootSaga() { yield takeEvery(FETCH_AUTHORIZATIONS_OF_CURRENT_USER, onFetchAuthorizationsOfCurrentUser); yield takeEvery(ADD_SIMULATION, onSimulationAdd); yield takeEvery(DELETE_SIMULATION, onSimulationDelete); + yield takeEvery(DELETE_CURRENT_USER, onDeleteCurrentUser); } diff --git a/src/api/sagas/profile.js b/src/api/sagas/profile.js new file mode 100644 index 00000000..3c4e1825 --- /dev/null +++ b/src/api/sagas/profile.js @@ -0,0 +1,12 @@ +import {call, put} from "redux-saga/effects"; +import {deleteCurrentUserSucceeded} from "../../actions/users"; +import {deleteUser} from "../routes/users"; + +export function* onDeleteCurrentUser(action) { + try { + yield call(deleteUser, action.userId); + yield put(deleteCurrentUserSucceeded()); + } catch (error) { + console.log(error); + } +} diff --git a/src/api/sagas/users.js b/src/api/sagas/users.js index d3ef32a6..c1daab30 100644 --- a/src/api/sagas/users.js +++ b/src/api/sagas/users.js @@ -2,6 +2,7 @@ import {call, put} from "redux-saga/effects"; import {logInSucceeded} from "../../actions/auth"; import {addToAuthorizationStore} from "../../actions/objects"; import {fetchAuthorizationsOfCurrentUserSucceeded} from "../../actions/users"; +import {saveAuthLocalStorage} from "../../auth/index"; import {performTokenSignIn} from "../routes/auth"; import {addUser, getAuthorizationsByUser} from "../routes/users"; import {fetchAndStoreSimulation, fetchAndStoreUser} from "./objects"; @@ -12,6 +13,7 @@ export function* onFetchLoggedInUser(action) { let userId = tokenResponse.userId; if (tokenResponse.isNewUser) { + saveAuthLocalStorage({authToken: action.payload.authToken}); const newUser = yield call(addUser, action.payload); userId = newUser.id; } diff --git a/src/auth/index.js b/src/auth/index.js index 8950c529..fb2a7038 100644 --- a/src/auth/index.js +++ b/src/auth/index.js @@ -1,4 +1,5 @@ import {LOG_IN_SUCCEEDED, LOG_OUT} from "../actions/auth"; +import {DELETE_CURRENT_USER_SUCCEEDED} from "../actions/users"; const getAuthObject = () => { const authItem = localStorage.getItem("auth"); @@ -43,6 +44,7 @@ export const authRedirectMiddleware = store => next => action => { window.location.href = "/simulations"; break; case LOG_OUT: + case DELETE_CURRENT_USER_SUCCEEDED: clearAuthLocalStorage(); window.location.href = "/"; break; diff --git a/src/components/modals/ConfirmationModal.js b/src/components/modals/ConfirmationModal.js new file mode 100644 index 00000000..4543cfd4 --- /dev/null +++ b/src/components/modals/ConfirmationModal.js @@ -0,0 +1,35 @@ +import PropTypes from "prop-types"; +import React from "react"; +import Modal from "./Modal"; + +class ConfirmationModal extends React.Component { + static propTypes = { + title: PropTypes.string.isRequired, + message: PropTypes.string.isRequired, + show: PropTypes.bool.isRequired, + callback: PropTypes.func.isRequired, + }; + + onConfirm() { + this.props.callback(true); + } + + onCancel() { + this.props.callback(false); + } + + render() { + return ( + <Modal title={this.props.title} + show={this.props.show} + onSubmit={this.onConfirm.bind(this)} + onCancel={this.onCancel.bind(this)} + submitButtonType="danger" + submitButtonText="Confirm"> + {this.props.message} + </Modal> + ); + } +} + +export default ConfirmationModal; diff --git a/src/components/modals/Modal.js b/src/components/modals/Modal.js index 0b3301af..06273a46 100644 --- a/src/components/modals/Modal.js +++ b/src/components/modals/Modal.js @@ -1,3 +1,4 @@ +import classNames from "classnames"; import PropTypes from "prop-types"; import React from "react"; @@ -7,6 +8,12 @@ class Modal extends React.Component { show: PropTypes.bool.isRequired, onSubmit: PropTypes.func.isRequired, onCancel: PropTypes.func.isRequired, + submitButtonType: PropTypes.string, + submitButtonText: PropTypes.string, + }; + static defaultProps = { + submitButtonType: "primary", + submitButtonText: "Save", }; static idCounter = 0; @@ -83,7 +90,7 @@ class Modal extends React.Component { <h5 className="modal-title">{this.props.title}</h5> <button type="button" className="close" onClick={this.onCancel.bind(this)} aria-label="Close"> - <span aria-hidden="true">×</span> + <span>×</span> </button> </div> <div className="modal-body"> @@ -93,8 +100,9 @@ class Modal extends React.Component { <button type="button" className="btn btn-secondary" onClick={this.onCancel.bind(this)}> Close </button> - <button type="button" className="btn btn-primary" onClick={this.onSubmit.bind(this)}> - Save + <button type="button" className={classNames("btn", "btn-" + this.props.submitButtonType)} + onClick={this.onSubmit.bind(this)}> + {this.props.submitButtonText} </button> </div> </div> diff --git a/src/containers/profile/DeleteProfileModal.js b/src/containers/profile/DeleteProfileModal.js new file mode 100644 index 00000000..b59db055 --- /dev/null +++ b/src/containers/profile/DeleteProfileModal.js @@ -0,0 +1,36 @@ +import React from "react"; +import {connect} from "react-redux"; +import {closeDeleteProfileModal} from "../../actions/profile"; +import {deleteCurrentUser} from "../../actions/users"; +import ConfirmationModal from "../../components/modals/ConfirmationModal"; + +const NewSimulationModalComponent = ({visible, callback}) => ( + <ConfirmationModal title="Delete my account" + message="Are you sure you want do delete your OpenDC account?" + show={visible} + callback={callback}/> +); + +const mapStateToProps = state => { + return { + visible: state.modals.deleteProfileModalVisible + }; +}; + +const mapDispatchToProps = dispatch => { + return { + callback: (isConfirmed) => { + if (isConfirmed) { + dispatch(deleteCurrentUser()); + } + dispatch(closeDeleteProfileModal()); + } + }; +}; + +const NewSimulationModal = connect( + mapStateToProps, + mapDispatchToProps +)(NewSimulationModalComponent); + +export default NewSimulationModal; diff --git a/src/containers/simulations/NewSimulationModal.js b/src/containers/simulations/NewSimulationModal.js index a4a3d2a8..cb858fed 100644 --- a/src/containers/simulations/NewSimulationModal.js +++ b/src/containers/simulations/NewSimulationModal.js @@ -11,7 +11,7 @@ const NewSimulationModalComponent = ({visible, callback}) => ( const mapStateToProps = state => { return { - visible: state.newSimulationModalVisible + visible: state.modals.newSimulationModalVisible }; }; diff --git a/src/pages/Profile.js b/src/pages/Profile.js new file mode 100644 index 00000000..3c3b0899 --- /dev/null +++ b/src/pages/Profile.js @@ -0,0 +1,37 @@ +import React from 'react'; +import {connect} from "react-redux"; +import {openDeleteProfileModal} from "../actions/profile"; +import Navbar from "../components/navigation/Navbar"; +import Login from "../containers/auth/Login"; +import DeleteProfileModal from "../containers/profile/DeleteProfileModal"; +import "./Profile.css"; + +const ProfileContainer = ({onDelete}) => ( + <div className="full-height"> + <Navbar/> + <div className="container profile-page-container full-height"> + <h2>Profile Settings</h2> + <button className="btn btn-danger" onClick={onDelete}>Delete my account on OpenDC</button> + <p> + This does not delete your Google account, it simply disconnects it from the OpenDC app and deletes any + simulation info that is associated with you (simulations you own, and any authorizations you may + have on other projects). + </p> + </div> + <DeleteProfileModal/> + <Login visible={false}/> + </div> +); + +const mapDispatchToProps = dispatch => { + return { + onDelete: () => dispatch(openDeleteProfileModal()), + }; +}; + +const Profile = connect( + undefined, + mapDispatchToProps +)(ProfileContainer); + +export default Profile; diff --git a/src/pages/Profile.sass b/src/pages/Profile.sass new file mode 100644 index 00000000..9bb0a7e4 --- /dev/null +++ b/src/pages/Profile.sass @@ -0,0 +1,2 @@ +.profile-page-container + padding-top: 2rem diff --git a/src/reducers/index.js b/src/reducers/index.js index bfafaedd..40a51a04 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -1,13 +1,14 @@ import {combineReducers} from "redux"; import {auth} from "./auth"; +import {modals} from "./modals"; import {objects} from "./objects"; -import {authorizationsOfCurrentUser, authVisibilityFilter, newSimulationModalVisible} from "./simulations"; +import {authorizationsOfCurrentUser, authVisibilityFilter} from "./simulations"; const rootReducer = combineReducers({ auth, objects, + modals, authorizationsOfCurrentUser, - newSimulationModalVisible, authVisibilityFilter, }); diff --git a/src/reducers/modals.js b/src/reducers/modals.js new file mode 100644 index 00000000..e74b66b9 --- /dev/null +++ b/src/reducers/modals.js @@ -0,0 +1,30 @@ +import {combineReducers} from "redux"; +import {CLOSE_DELETE_PROFILE_MODAL, OPEN_DELETE_PROFILE_MODAL} from "../actions/profile"; +import {CLOSE_NEW_SIMULATION_MODAL, OPEN_NEW_SIMULATION_MODAL} from "../actions/simulations"; + +function newSimulationModalVisible(state = false, action) { + switch (action.type) { + case OPEN_NEW_SIMULATION_MODAL: + return true; + case CLOSE_NEW_SIMULATION_MODAL: + return false; + default: + return state; + } +} + +function deleteProfileModalVisible(state = false, action) { + switch (action.type) { + case OPEN_DELETE_PROFILE_MODAL: + return true; + case CLOSE_DELETE_PROFILE_MODAL: + return false; + default: + return state; + } +} + +export const modals = combineReducers({ + newSimulationModalVisible, + deleteProfileModalVisible, +}); diff --git a/src/reducers/simulations.js b/src/reducers/simulations.js index 9d830877..7d0b9d66 100644 --- a/src/reducers/simulations.js +++ b/src/reducers/simulations.js @@ -1,8 +1,6 @@ import { ADD_SIMULATION_SUCCEEDED, - CLOSE_NEW_SIMULATION_MODAL, DELETE_SIMULATION_SUCCEEDED, - OPEN_NEW_SIMULATION_MODAL, SET_AUTH_VISIBILITY_FILTER } from "../actions/simulations"; import {FETCH_AUTHORIZATIONS_OF_CURRENT_USER_SUCCEEDED} from "../actions/users"; @@ -23,17 +21,6 @@ export function authorizationsOfCurrentUser(state = [], action) { } } -export function newSimulationModalVisible(state = false, action) { - switch (action.type) { - case OPEN_NEW_SIMULATION_MODAL: - return true; - case CLOSE_NEW_SIMULATION_MODAL: - return false; - default: - return state; - } -} - export function authVisibilityFilter(state = "SHOW_ALL", action) { switch (action.type) { case SET_AUTH_VISIBILITY_FILTER: diff --git a/src/routes/index.js b/src/routes/index.js index 6257017e..af1b70b5 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -3,19 +3,17 @@ import {BrowserRouter, Redirect, Route, Switch} from "react-router-dom"; import {userIsLoggedIn} from "../auth/index"; import Home from "../pages/Home"; import NotFound from "../pages/NotFound"; +import Profile from "../pages/Profile"; import Simulations from "../pages/Simulations"; +const ProtectedComponent = (component) => () => userIsLoggedIn() ? component : <Redirect to="/"/>; + const Routes = () => ( <BrowserRouter> <Switch> <Route exact path="/" component={Home}/> - <Route exact path="/simulations" render={() => ( - userIsLoggedIn() ? ( - <Simulations/> - ) : ( - <Redirect to="/"/> - ) - )}/> + <Route exact path="/simulations" render={ProtectedComponent(<Simulations/>)}/> + <Route exact path="/profile" render={ProtectedComponent(<Profile/>)}/> <Route path="/*" component={NotFound}/> </Switch> </BrowserRouter> |
