Unified header menus with components
This commit is contained in:
@@ -5,6 +5,8 @@ import { createAction } from 'redux-actions';
|
|||||||
|
|
||||||
export const resetDocument = createAction('RESET_DOCUMENT');
|
export const resetDocument = createAction('RESET_DOCUMENT');
|
||||||
|
|
||||||
|
// GET
|
||||||
|
|
||||||
export const FETCH_DOCUMENT_PENDING = 'FETCH_DOCUMENT_PENDING';
|
export const FETCH_DOCUMENT_PENDING = 'FETCH_DOCUMENT_PENDING';
|
||||||
export const FETCH_DOCUMENT_SUCCESS = 'FETCH_DOCUMENT_SUCCESS';
|
export const FETCH_DOCUMENT_SUCCESS = 'FETCH_DOCUMENT_SUCCESS';
|
||||||
export const FETCH_DOCUMENT_FAILURE = 'FETCH_DOCUMENT_FAILURE';
|
export const FETCH_DOCUMENT_FAILURE = 'FETCH_DOCUMENT_FAILURE';
|
||||||
@@ -29,6 +31,8 @@ export function fetchDocumentAsync(documentId) {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// POST/UPDATE
|
||||||
|
|
||||||
export const SAVE_DOCUMENT_PENDING = 'SAVE_DOCUMENT_PENDING';
|
export const SAVE_DOCUMENT_PENDING = 'SAVE_DOCUMENT_PENDING';
|
||||||
export const SAVE_DOCUMENT_SUCCESS = 'SAVE_DOCUMENT_SUCCESS';
|
export const SAVE_DOCUMENT_SUCCESS = 'SAVE_DOCUMENT_SUCCESS';
|
||||||
export const SAVE_DOCUMENT_FAILURE = 'SAVE_DOCUMENT_FAILURE';
|
export const SAVE_DOCUMENT_FAILURE = 'SAVE_DOCUMENT_FAILURE';
|
||||||
@@ -65,3 +69,23 @@ export function saveDocumentAsync(atlasId, documentId, title, text) {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// documents.delete
|
||||||
|
|
||||||
|
export const deleteDocumentPending = createAction('DELETE_DOCUMENT_PENDING');
|
||||||
|
export const deleteDocumentSuccess = createAction('DELETE_DOCUMENT_SUCCESS');
|
||||||
|
export const deleteDocumentFailure = createAction('DELETE_DOCUMENT_FAILURE');
|
||||||
|
|
||||||
|
export const deleteDocument = (documentId, returnPath) => {
|
||||||
|
return (dispatch) => {
|
||||||
|
dispatch(deleteDocumentPending());
|
||||||
|
|
||||||
|
client.post('/documents.delete', { id: documentId })
|
||||||
|
.then(data => {
|
||||||
|
dispatch(deleteDocumentSuccess(documentId));
|
||||||
|
dispatch(replace(returnPath));
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
dispatch(deleteDocumentFailure(err));
|
||||||
|
})
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
66
src/components/DropdownMenu/DropdownMenu.js
Normal file
66
src/components/DropdownMenu/DropdownMenu.js
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styles from './DropdownMenu.scss';
|
||||||
|
|
||||||
|
const MenuItem = (props) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={ styles.menuItem }
|
||||||
|
onClick={ props.onClick}
|
||||||
|
>{ props.children }</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
MenuItem.propTypes = {
|
||||||
|
onClick: React.PropTypes.func,
|
||||||
|
children: React.PropTypes.node.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
class DropdownMenu extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
label: React.PropTypes.string.isRequired,
|
||||||
|
children: React.PropTypes.node.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
state = {
|
||||||
|
menuVisible: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseEnter = () => {
|
||||||
|
this.setState({ menuVisible: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseLeave = () => {
|
||||||
|
this.setState({ menuVisible: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
onClick = () => {
|
||||||
|
this.setState({ menuVisible: !this.state.menuVisible });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={ styles.menuContainer }
|
||||||
|
onMouseEnter={ this.onMouseEnter }
|
||||||
|
onMouseLeave={ this.onMouseLeave }
|
||||||
|
>
|
||||||
|
<div className={ styles.label } onClick={ this.onClick }>
|
||||||
|
{ this.props.label }
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{ this.state.menuVisible ? (
|
||||||
|
<div className={ styles.menu }>
|
||||||
|
{ this.props.children }
|
||||||
|
</div>
|
||||||
|
) : null }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DropdownMenu;
|
||||||
|
export {
|
||||||
|
MenuItem,
|
||||||
|
}
|
||||||
40
src/components/DropdownMenu/DropdownMenu.scss
Normal file
40
src/components/DropdownMenu/DropdownMenu.scss
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
@import '../../utils/constants.scss';
|
||||||
|
|
||||||
|
.label {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
min-height: 43px;
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
color: $linkColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menuContainer {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
position: absolute;
|
||||||
|
top: 42px;
|
||||||
|
right: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
min-width: 150px;
|
||||||
|
padding: 5px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menuItem {
|
||||||
|
margin: 0;
|
||||||
|
padding: 5px 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $textColor;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/components/DropdownMenu/index.js
Normal file
5
src/components/DropdownMenu/index.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import DropdownMenu, { MenuItem } from './DropdownMenu';
|
||||||
|
export default DropdownMenu;
|
||||||
|
export {
|
||||||
|
MenuItem,
|
||||||
|
};
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import Link from 'react-router/lib/Link';
|
import Link from 'react-router/lib/Link';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { logoutUser } from 'actions/UserActions';
|
||||||
|
|
||||||
import HeaderMenu from './components/HeaderMenu';
|
import DropdownMenu, { MenuItem } from 'components/DropdownMenu';
|
||||||
import Flex from 'components/Flex';
|
import Flex from 'components/Flex';
|
||||||
import LoadingIndicator from 'components/LoadingIndicator';
|
import LoadingIndicator from 'components/LoadingIndicator';
|
||||||
|
import { Avatar } from 'rebass';
|
||||||
|
|
||||||
import styles from './Layout.scss';
|
import styles from './Layout.scss';
|
||||||
import classNames from 'classnames/bind';
|
import classNames from 'classnames/bind';
|
||||||
@@ -18,6 +21,10 @@ class Layout extends React.Component {
|
|||||||
loading: React.PropTypes.bool,
|
loading: React.PropTypes.bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onLogout = () => {
|
||||||
|
this.props.logoutUser();
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className={ styles.container }>
|
<div className={ styles.container }>
|
||||||
@@ -35,11 +42,19 @@ class Layout extends React.Component {
|
|||||||
<Flex align="center" className={ styles.actions }>
|
<Flex align="center" className={ styles.actions }>
|
||||||
{ this.props.actions }
|
{ this.props.actions }
|
||||||
</Flex>
|
</Flex>
|
||||||
<HeaderMenu>
|
|
||||||
<img src={ this.props.avatarUrl } />
|
<DropdownMenu label={
|
||||||
</HeaderMenu>
|
<Avatar
|
||||||
|
circle
|
||||||
|
size={24}
|
||||||
|
src={ this.props.avatarUrl }
|
||||||
|
/>
|
||||||
|
}>
|
||||||
|
<MenuItem onClick={ this.onLogout }>Logout</MenuItem>
|
||||||
|
</DropdownMenu>
|
||||||
</Flex>
|
</Flex>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={ styles.content }>
|
<div className={ styles.content }>
|
||||||
{ this.props.children }
|
{ this.props.children }
|
||||||
</div>
|
</div>
|
||||||
@@ -55,6 +70,13 @@ const mapStateToProps = (state) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => {
|
||||||
|
return bindActionCreators({
|
||||||
|
logoutUser,
|
||||||
|
}, dispatch)
|
||||||
|
}
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
|
mapDispatchToProps,
|
||||||
)(Layout);
|
)(Layout);
|
||||||
@@ -43,7 +43,3 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions a {
|
|
||||||
text-decoration: none;
|
|
||||||
margin-right: 15px;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { logoutUser } from 'actions/UserActions';
|
|
||||||
|
|
||||||
import styles from './HeaderMenu.scss';
|
|
||||||
|
|
||||||
class HeaderMenu extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
children: React.PropTypes.node.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
state = {
|
|
||||||
menuVisible: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
onMouseEnter = () => {
|
|
||||||
this.setState({ menuVisible: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
onMouseLeave = () => {
|
|
||||||
this.setState({ menuVisible: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
onClick = () => {
|
|
||||||
this.setState({ menuVisible: !this.state.menuVisible });
|
|
||||||
}
|
|
||||||
|
|
||||||
logout = (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
this.props.logout();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={ styles.menu }
|
|
||||||
onMouseEnter={ this.onMouseEnter }
|
|
||||||
onMouseLeave={ this.onMouseLeave }
|
|
||||||
>
|
|
||||||
<div className={ styles.content } onClick={ this.onClick }>
|
|
||||||
{ this.props.children }
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{ this.state.menuVisible ? (
|
|
||||||
<div className={ styles.menu }>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a href='/' onClick={ this.logout }>Logout</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
) : null }
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => {
|
|
||||||
return {
|
|
||||||
logout: () => {
|
|
||||||
dispatch(logoutUser())
|
|
||||||
},
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export default connect(null, mapDispatchToProps)(HeaderMenu);
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
@import '../../../../utils/constants.scss';
|
|
||||||
|
|
||||||
.content {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
min-height: 43px;
|
|
||||||
min-width: 43px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
img {
|
|
||||||
height: 24px;
|
|
||||||
width: 24px;
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu {
|
|
||||||
position: absolute;
|
|
||||||
top: 42px;
|
|
||||||
right: 0;
|
|
||||||
z-index: 1000;
|
|
||||||
border: 1px solid #eee;
|
|
||||||
|
|
||||||
ul {
|
|
||||||
margin: 0;
|
|
||||||
padding: 5px 0;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
list-style-type: none;
|
|
||||||
|
|
||||||
a {
|
|
||||||
display: flex;
|
|
||||||
margin: 0;
|
|
||||||
padding: 5px 10px;
|
|
||||||
min-width: 150px;
|
|
||||||
|
|
||||||
color: $textColor;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
import HeaderMenu from './HeaderMenu';
|
|
||||||
export default HeaderMenu;
|
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import Layout from './Layout';
|
import Layout from './Layout';
|
||||||
import Title from './components/Title';
|
import Title from './components/Title';
|
||||||
|
import HeaderLink from './components/HeaderLink';
|
||||||
|
|
||||||
export default Layout;
|
export default Layout;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Title,
|
Title,
|
||||||
|
HeaderLink,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,12 +2,16 @@ import React from 'react';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import Link from 'react-router/lib/Link';
|
import Link from 'react-router/lib/Link';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { fetchDocumentAsync } from 'actions/DocumentActions';
|
import {
|
||||||
|
fetchDocumentAsync,
|
||||||
|
deleteDocument,
|
||||||
|
} from 'actions/DocumentActions';
|
||||||
|
|
||||||
import Layout from 'components/Layout';
|
import Layout, { HeaderLink } from 'components/Layout';
|
||||||
import AtlasPreviewLoading from 'components/AtlasPreviewLoading';
|
import AtlasPreviewLoading from 'components/AtlasPreviewLoading';
|
||||||
import CenteredContent from 'components/CenteredContent';
|
import CenteredContent from 'components/CenteredContent';
|
||||||
import Document from 'components/Document';
|
import Document from 'components/Document';
|
||||||
|
import DropdownMenu, { MenuItem } from 'components/DropdownMenu';
|
||||||
|
|
||||||
import styles from './DocumentScene.scss';
|
import styles from './DocumentScene.scss';
|
||||||
|
|
||||||
@@ -35,12 +39,30 @@ class DocumentScene extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onDelete = () => {
|
||||||
|
if (confirm("Are you sure you want to delete this document?")) {
|
||||||
|
this.props.deleteDocument(
|
||||||
|
this.props.document.id,
|
||||||
|
`/atlas/${ this.props.document.atlas.id }`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const document = this.props.document;
|
const document = this.props.document;
|
||||||
let title;
|
let title;
|
||||||
let actions;
|
let actions;
|
||||||
if (document) {
|
if (document) {
|
||||||
actions = <Link to={ `/documents/${document.id}/edit` }>Edit</Link>;
|
actions = (
|
||||||
|
<div className={ styles.actions }>
|
||||||
|
<HeaderLink>
|
||||||
|
<Link to={ `/documents/${document.id}/edit` }>Edit</Link>
|
||||||
|
</HeaderLink>
|
||||||
|
<DropdownMenu label="More">
|
||||||
|
<MenuItem onClick={ this.onDelete }>Delete</MenuItem>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
title = `${document.atlas.name} - ${document.title}`;
|
title = `${document.atlas.name} - ${document.title}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +94,7 @@ const mapStateToProps = (state) => {
|
|||||||
const mapDispatchToProps = (dispatch) => {
|
const mapDispatchToProps = (dispatch) => {
|
||||||
return bindActionCreators({
|
return bindActionCreators({
|
||||||
fetchDocumentAsync,
|
fetchDocumentAsync,
|
||||||
|
deleteDocument,
|
||||||
}, dispatch)
|
}, dispatch)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user