Combined documents and collections in the sidepanel

This commit is contained in:
Jori Lallo
2017-09-17 23:45:40 -07:00
parent 53c0c9180b
commit 0a5f5712ef
13 changed files with 254 additions and 223 deletions

View File

@ -1,80 +1,98 @@
// @flow
import React from 'react';
import invariant from 'invariant';
import { observable } from 'mobx';
import { observer } from 'mobx-react';
import keydown from 'react-keydown';
import styled from 'styled-components';
import Portal from 'react-portal';
import Flex from 'components/Flex';
import { color } from 'styles/constants';
import { fadeAndScaleIn } from 'styles/animations';
type DropdownMenuProps = {
label: React.Element<any>,
onShow?: Function,
onClose?: Function,
children?: React.Element<any>,
style?: Object,
};
@observer class DropdownMenu extends React.Component {
props: DropdownMenuProps;
actionRef: Object;
@observable open: boolean = false;
@observable top: number;
@observable left: number;
@observable right: number;
handleClick = () => {
this.open = !this.open;
handleClick = (ev: SyntheticEvent) => {
ev.preventDefault();
ev.stopPropagation();
invariant(document.body, 'why you not here');
const bodyRect = document.body.getBoundingClientRect();
// $FlowIssue it's there
const targetRect = ev.currentTarget.getBoundingClientRect();
this.open = true;
this.top = targetRect.bottom - bodyRect.top;
this.right = bodyRect.width - targetRect.left - targetRect.width;
if (this.props.onShow) this.props.onShow();
};
@keydown('esc')
handleEscape() {
this.open = false;
}
handleClickOutside = (ev: SyntheticEvent) => {
ev.stopPropagation();
handleClose = (ev: SyntheticEvent) => {
this.open = false;
if (this.props.onClose) this.props.onClose();
};
render() {
const openAction = (
<Label
onClick={this.handleClick}
innerRef={ref => (this.actionRef = ref)}
>
{this.props.label}
</Label>
);
return (
<MenuContainer onClick={this.handleClick}>
{this.open && <Backdrop onClick={this.handleClickOutside} />}
<Label>
{this.props.label}
</Label>
{this.open &&
<Menu style={this.props.style}>
<div>
{openAction}
<Portal
closeOnEsc
closeOnOutsideClick
isOpened={this.open}
onClose={this.handleClose}
>
<Menu
style={this.props.style}
left={this.left}
top={this.top}
right={this.right}
>
{this.props.children}
</Menu>}
</MenuContainer>
</Menu>
</Portal>
</div>
);
}
}
const Backdrop = styled.div`
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 999;
`;
const Label = styled(Flex).attrs({
justify: 'center',
align: 'center',
})`
width: 22px;
height: 22px;
z-index: 1000;
cursor: pointer;
`;
const MenuContainer = styled.div`
position: relative;
`;
const Menu = styled.div`
animation: ${fadeAndScaleIn} 250ms ease;
transform-origin: 75% 0;
position: absolute;
right: 0;
right: ${({ right }) => right}px;
top: ${({ top }) => top}px;
z-index: 1000;
border: ${color.slateLight};
background: ${color.white};

View File

@ -14,11 +14,9 @@ import { LoadingIndicatorBar } from 'components/LoadingIndicator';
import Scrollable from 'components/Scrollable';
import Icon from 'components/Icon';
import Toasts from 'components/Toasts';
import CollectionMenu from 'menus/CollectionMenu';
import AccountMenu from 'menus/AccountMenu';
import SidebarCollection from './components/SidebarCollection';
import SidebarCollectionList from './components/SidebarCollectionList';
import SidebarCollections from './components/SidebarCollections';
import SidebarLink from './components/SidebarLink';
import HeaderBlock from './components/HeaderBlock';
import Modals from './components/Modals';
@ -84,7 +82,7 @@ type Props = {
};
render() {
const { auth, documents, collections, ui } = this.props;
const { auth, documents, ui } = this.props;
const { user, team } = auth;
return (
@ -130,20 +128,11 @@ type Props = {
</SidebarLink>
</LinkSection>
<LinkSection>
{collections.active
? <CollectionAction>
<CollectionMenu collection={collections.active} />
</CollectionAction>
: <CollectionAction onClick={this.handleCreateCollection}>
<Icon type="PlusCircle" />
</CollectionAction>}
{collections.active
? <SidebarCollection
document={documents.active}
collection={collections.active}
history={this.props.history}
/>
: <SidebarCollectionList history={this.props.history} />}
<SidebarCollections
history={this.props.history}
activeDocument={documents.active}
onCreateCollection={this.handleCreateCollection}
/>
</LinkSection>
</Scrollable>
</Flex>
@ -160,18 +149,6 @@ type Props = {
}
}
const CollectionAction = styled.a`
position: absolute;
top: -4px;
right: ${layout.hpadding};
color: ${color.slate};
svg { opacity: .75; }
&:hover {
svg { opacity: 1; }
}
`;
const Container = styled(Flex)`
position: relative;
width: 100%;

View File

@ -1,84 +0,0 @@
// @flow
import React from 'react';
import { observer } from 'mobx-react';
import Flex from 'components/Flex';
import styled from 'styled-components';
import { color, layout } from 'styles/constants';
import SidebarLink from '../SidebarLink';
import DropToImport from 'components/DropToImport';
import Collection from 'models/Collection';
import Document from 'models/Document';
import type { NavigationNode } from 'types';
type Props = {
collection: ?Collection,
document: ?Document,
history: Object,
};
const activeStyle = {
color: color.black,
background: color.slateDark,
};
@observer class SidebarCollection extends React.Component {
props: Props;
renderDocuments(documentList: Array<NavigationNode>, depth: number = 0) {
const { document, history } = this.props;
const canDropToImport = depth === 0;
if (document) {
return documentList.map(doc => (
<Flex column key={doc.id}>
{canDropToImport &&
<DropToImport
history={history}
documentId={doc.id}
activeStyle={activeStyle}
>
<SidebarLink to={doc.url}>{doc.title}</SidebarLink>
</DropToImport>}
{!canDropToImport &&
<SidebarLink to={doc.url}>{doc.title}</SidebarLink>}
{(document.pathToDocument.map(entry => entry.id).includes(doc.id) ||
document.id === doc.id) &&
<Children column>
{doc.children && this.renderDocuments(doc.children, depth + 1)}
</Children>}
</Flex>
));
}
}
render() {
const { collection } = this.props;
if (collection) {
return (
<Flex column>
<Header>{collection.name}</Header>
{this.renderDocuments(collection.documents)}
</Flex>
);
}
return null;
}
}
const Header = styled(Flex)`
font-size: 11px;
font-weight: 500;
text-transform: uppercase;
color: ${color.slate};
letter-spacing: 0.04em;
padding: 0 ${layout.hpadding};
`;
const Children = styled(Flex)`
margin-left: 20px;
`;
export default SidebarCollection;

View File

@ -1,3 +0,0 @@
// @flow
import SidebarCollection from './SidebarCollection';
export default SidebarCollection;

View File

@ -1,52 +0,0 @@
// @flow
import React from 'react';
import { observer, inject } from 'mobx-react';
import Flex from 'components/Flex';
import styled from 'styled-components';
import { color, layout, fontWeight } from 'styles/constants';
import SidebarLink from '../SidebarLink';
import DropToImport from 'components/DropToImport';
import CollectionsStore from 'stores/CollectionsStore';
type Props = {
history: Object,
collections: CollectionsStore,
};
const activeStyle = {
color: color.black,
background: color.slateDark,
};
const SidebarCollectionList = observer(({ history, collections }: Props) => {
return (
<Flex column>
<Header>Collections</Header>
{collections.data.map(collection => (
<DropToImport
key={collection.id}
history={history}
collectionId={collection.id}
activeStyle={activeStyle}
>
<SidebarLink key={collection.id} to={collection.entryUrl}>
{collection.name}
</SidebarLink>
</DropToImport>
))}
</Flex>
);
});
const Header = styled(Flex)`
font-size: 11px;
font-weight: ${fontWeight.semiBold};
text-transform: uppercase;
color: ${color.slate};
letter-spacing: 0.04em;
padding: 0 ${layout.hpadding};
`;
export default inject('collections')(SidebarCollectionList);

View File

@ -1,3 +0,0 @@
// @flow
import SidebarCollectionList from './SidebarCollectionList';
export default SidebarCollectionList;

View File

@ -0,0 +1,171 @@
// @flow
import React from 'react';
import { observable } from 'mobx';
import { observer, inject } from 'mobx-react';
import Flex from 'components/Flex';
import styled from 'styled-components';
import { color, layout, fontWeight } from 'styles/constants';
import SidebarLink from './SidebarLink';
import DropToImport from 'components/DropToImport';
import Icon from 'components/Icon';
import CollectionMenu from 'menus/CollectionMenu';
import CollectionsStore from 'stores/CollectionsStore';
import UiStore from 'stores/UiStore';
import Document from 'models/Document';
import { type NavigationNode } from 'types';
type Props = {
history: Object,
collections: CollectionsStore,
activeDocument: ?Document,
onCreateCollection: Function,
ui: UiStore,
};
const activeStyle = {
color: color.black,
background: color.slateDark,
};
@observer class SidebarCollections extends React.PureComponent {
props: Props;
render() {
const { collections, activeDocument, ui } = this.props;
return (
<Flex column>
<Header>Collections</Header>
{collections.data.map(collection => (
<CollectionLink
key={collection.id}
collection={collection}
activeDocument={activeDocument}
ui={ui}
/>
))}
{collections.isLoaded &&
<SidebarLink onClick={this.props.onCreateCollection}>
<Icon type="Plus" /> Add new collection
</SidebarLink>}
</Flex>
);
}
}
@observer class CollectionLink extends React.Component {
@observable isHovering = false;
@observable menuOpen = false;
handleHover = () => (this.isHovering = true);
handleBlur = () => {
if (!this.menuOpen) this.isHovering = false;
};
render() {
const { collection, activeDocument, ui } = this.props;
return (
<DropToImport
key={collection.id}
history={history}
collectionId={collection.id}
activeStyle={activeStyle}
onMouseEnter={this.handleHover}
onMouseLeave={this.handleBlur}
>
<SidebarLink key={collection.id} to={collection.url}>
<Flex justify="space-between">
{collection.name}
{(this.isHovering || this.menuOpen) &&
<CollectionAction>
<CollectionMenu
collection={collection}
onShow={() => (this.menuOpen = true)}
onClose={() => (this.menuOpen = false)}
/>
</CollectionAction>}
</Flex>
{collection.id === ui.activeCollectionId &&
collection.documents.map(document => (
<DocumentLink
key={document.id}
document={document}
activeDocument={activeDocument}
depth={0}
/>
))}
</SidebarLink>
</DropToImport>
);
}
}
type DocumentLinkProps = {
document: NavigationNode,
activeDocument: ?Document,
depth: number,
};
const DocumentLink = observer((props: DocumentLinkProps) => {
const { document, activeDocument, depth } = props;
const canDropToImport = depth === 0;
return (
<Flex column key={document.id}>
{canDropToImport &&
<DropToImport
history={history}
documentId={document.id}
activeStyle={activeStyle}
>
<SidebarLink to={document.url}>{document.title}</SidebarLink>
</DropToImport>}
{!canDropToImport &&
<SidebarLink to={document.url}>{document.title}</SidebarLink>}
{activeDocument &&
(activeDocument.pathToDocument
.map(entry => entry.id)
.includes(document.id) ||
activeDocument.id === document.id) &&
<Children column>
{document.children &&
document.children.map(childDocument => (
<DocumentLink
key={childDocument.id}
document={childDocument}
depth={depth + 1}
/>
))}
</Children>}
</Flex>
);
});
const Header = styled(Flex)`
font-size: 11px;
font-weight: ${fontWeight.semiBold};
text-transform: uppercase;
color: ${color.slate};
letter-spacing: 0.04em;
padding: 0 ${layout.hpadding};
`;
const CollectionAction = styled.a`
color: ${color.slate};
svg { opacity: .75; }
&:hover {
svg { opacity: 1; }
}
`;
const Children = styled(Flex)`
margin-left: 20px;
`;
export default inject('collections', 'ui')(SidebarCollections);

View File

@ -9,21 +9,24 @@ const activeStyle = {
fontWeight: fontWeight.semiBold,
};
function SidebarLink(props: Object) {
return <StyledNavLink exact activeStyle={activeStyle} {...props} />;
}
const StyledNavLink = styled(NavLink)`
// $FlowFixMe :/
const styleComponent = component => styled(component)`
display: block;
overflow: hidden;
text-overflow: ellipsis;
margin: 5px ${layout.hpadding};
color: ${color.slateDark};
font-size: 15px;
cursor: pointer;
&:hover {
color: ${color.text};
}
`;
function SidebarLink(props: Object) {
const Component = styleComponent(props.to ? NavLink : 'div');
return <Component exact activeStyle={activeStyle} {...props} />;
}
export default SidebarLink;

View File

@ -1,3 +0,0 @@
// @flow
import SidebarLink from './SidebarLink';
export default SidebarLink;

View File

@ -1,3 +0,0 @@
// @flow
import Title from './Title';
export default Title;

View File

@ -1,6 +1,5 @@
// @flow
import React, { Component } from 'react';
import { withRouter } from 'react-router-dom';
import { inject, observer } from 'mobx-react';
import Collection from 'models/Collection';
import UiStore from 'stores/UiStore';
@ -10,6 +9,8 @@ import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu';
@observer class CollectionMenu extends Component {
props: {
label?: React$Element<any>,
onShow?: Function,
onClose?: Function,
history: Object,
ui: UiStore,
collection: Collection,
@ -26,11 +27,15 @@ import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu';
};
render() {
const { collection, label } = this.props;
const { collection, label, onShow, onClose } = this.props;
const { allowDelete } = collection;
return (
<DropdownMenu label={label || <Icon type="MoreHorizontal" />}>
<DropdownMenu
label={label || <Icon type="MoreHorizontal" />}
onShow={onShow}
onClose={onClose}
>
{collection &&
<DropdownMenuItem onClick={this.onEdit}>Edit</DropdownMenuItem>}
{allowDelete &&
@ -40,4 +45,4 @@ import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu';
}
}
export default withRouter(inject('ui')(CollectionMenu));
export default inject('ui')(CollectionMenu);

View File

@ -29,12 +29,17 @@ type Props = {
@observable redirectUrl;
componentDidMount = () => {
this.fetchDocument();
this.fetchDocument(this.props.match.params.id);
};
fetchDocument = async () => {
componentWillReceiveProps(nextProps) {
if (nextProps.match.params.id !== this.props.match.params.id) {
this.fetchDocument(nextProps.match.params.id);
}
}
fetchDocument = async (id: string) => {
const { collections } = this.props;
const { id } = this.props.match.params;
this.collection = await collections.getById(id);