Combined documents and collections in the sidepanel
This commit is contained in:
@ -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};
|
||||
|
@ -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%;
|
||||
|
@ -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;
|
@ -1,3 +0,0 @@
|
||||
// @flow
|
||||
import SidebarCollection from './SidebarCollection';
|
||||
export default SidebarCollection;
|
@ -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);
|
@ -1,3 +0,0 @@
|
||||
// @flow
|
||||
import SidebarCollectionList from './SidebarCollectionList';
|
||||
export default SidebarCollectionList;
|
171
frontend/components/Layout/components/SidebarCollections.js
Normal file
171
frontend/components/Layout/components/SidebarCollections.js
Normal 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);
|
@ -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;
|
@ -1,3 +0,0 @@
|
||||
// @flow
|
||||
import SidebarLink from './SidebarLink';
|
||||
export default SidebarLink;
|
@ -1,3 +0,0 @@
|
||||
// @flow
|
||||
import Title from './Title';
|
||||
export default Title;
|
@ -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);
|
||||
|
@ -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);
|
||||
|
||||
|
Reference in New Issue
Block a user