diff --git a/frontend/components/DropToImport/DropToImport.js b/frontend/components/DropToImport/DropToImport.js new file mode 100644 index 00000000..944dde6f --- /dev/null +++ b/frontend/components/DropToImport/DropToImport.js @@ -0,0 +1,108 @@ +// @flow +import React, { Component } from 'react'; +import { inject } from 'mobx-react'; +import invariant from 'invariant'; +import _ from 'lodash'; +import Dropzone from 'react-dropzone'; +import Document from 'models/Document'; +import DocumentsStore from 'stores/DocumentsStore'; +import LoadingIndicator from 'components/LoadingIndicator'; + +class DropToImport extends Component { + state: { + isImporting: boolean, + }; + props: { + children?: React$Element, + collectionId: string, + documentId?: string, + activeClassName?: string, + rejectClassName?: string, + documents: DocumentsStore, + history: Object, + }; + state = { + isImporting: false, + }; + + importFile = async ({ file, documentId, collectionId, redirect }) => { + const reader = new FileReader(); + + reader.onload = async ev => { + const text = ev.target.result; + let data = { + parentDocument: undefined, + collection: { id: collectionId }, + text, + }; + + if (documentId) { + data.parentDocument = { + id: documentId, + }; + } + + let document = new Document(data); + document = await document.save(); + this.props.documents.add(document); + + if (redirect && this.props.history) { + this.props.history.push(document.url); + } + }; + reader.readAsText(file); + }; + + onDropAccepted = async (files = []) => { + this.setState({ isImporting: true }); + + try { + let collectionId = this.props.collectionId; + const documentId = this.props.documentId; + const redirect = files.length === 1; + + if (documentId && !collectionId) { + const document = await this.props.documents.fetch(documentId); + invariant(document, 'Document not available'); + collectionId = document.collection.id; + } + + for (const file of files) { + await this.importFile({ file, documentId, collectionId, redirect }); + } + } catch (err) { + // TODO: show error alert. + } finally { + this.setState({ isImporting: false }); + } + }; + + render() { + const props = _.omit( + this.props, + 'history', + 'documentId', + 'collectionId', + 'documents' + ); + + return ( + + + {this.state.isImporting && } + {this.props.children} + + + ); + } +} + +export default inject('documents')(DropToImport); diff --git a/frontend/components/DropToImport/index.js b/frontend/components/DropToImport/index.js new file mode 100644 index 00000000..a95a2a44 --- /dev/null +++ b/frontend/components/DropToImport/index.js @@ -0,0 +1,3 @@ +// @flow +import DropToImport from './DropToImport'; +export default DropToImport; diff --git a/frontend/components/Layout/Layout.js b/frontend/components/Layout/Layout.js index cb0b7648..eba7cac6 100644 --- a/frontend/components/Layout/Layout.js +++ b/frontend/components/Layout/Layout.js @@ -7,11 +7,12 @@ import { observer, inject } from 'mobx-react'; import _ from 'lodash'; import keydown from 'react-keydown'; import Flex from 'components/Flex'; -import { textColor } from 'styles/constants.scss'; +import { color, layout } from 'styles/constants'; import DropdownMenu, { MenuItem } from 'components/DropdownMenu'; import { LoadingIndicatorBar } from 'components/LoadingIndicator'; import Scrollable from 'components/Scrollable'; +import Avatar from 'components/Avatar'; import SidebarCollection from './components/SidebarCollection'; import SidebarCollectionList from './components/SidebarCollectionList'; @@ -115,8 +116,9 @@ type Props = { ? - : } + : } @@ -141,19 +143,13 @@ const LogoLink = styled(Link)` margin-top: 15px; font-family: 'Atlas Grotesk'; font-weight: bold; - color: ${textColor}; + color: ${color.text}; text-decoration: none; font-size: 16px; `; -const Avatar = styled.img` - width: 24px; - height: 24px; - border-radius: 50%; -`; - const MenuLink = styled(Link)` - color: ${textColor}; + color: ${color.text}; `; const Content = styled(Flex)` @@ -162,13 +158,13 @@ const Content = styled(Flex)` top: 0; bottom: 0; right: 0; - left: ${props => (props.editMode ? 0 : '250px')}; + left: ${props => (props.editMode ? 0 : layout.sidebarWidth)}; transition: left 200ms ease-in-out; `; const Sidebar = styled(Flex)` - width: 250px; - margin-left: ${props => (props.editMode ? '-250px' : 0)}; + width: ${layout.sidebarWidth}; + margin-left: ${props => (props.editMode ? `-${layout.sidebarWidth}` : 0)}; background: rgba(250, 251, 252, 0.71); border-right: 1px solid #eceff3; transition: margin-left 200ms ease-in-out; @@ -176,12 +172,12 @@ const Sidebar = styled(Flex)` const Header = styled(Flex)` flex-shrink: 0; - padding: 10px 20px; + padding: ${layout.padding}; `; const LinkSection = styled(Flex)` flex-direction: column; - padding: 10px 20px; + padding: 10px 0; `; export default withRouter(inject('user', 'auth', 'ui', 'collections')(Layout)); diff --git a/frontend/components/Layout/components/SidebarCollection/SidebarCollection.js b/frontend/components/Layout/components/SidebarCollection/SidebarCollection.js index 3b9329e3..60a5a05d 100644 --- a/frontend/components/Layout/components/SidebarCollection/SidebarCollection.js +++ b/frontend/components/Layout/components/SidebarCollection/SidebarCollection.js @@ -2,8 +2,9 @@ import React from 'react'; import Flex from 'components/Flex'; import styled from 'styled-components'; - +import { layout } from 'styles/constants'; import SidebarLink from '../SidebarLink'; +import DropToImport from 'components/DropToImport'; import Collection from 'models/Collection'; import Document from 'models/Document'; @@ -12,24 +13,39 @@ import type { NavigationNode } from 'types'; type Props = { collection: ?Collection, document: ?Document, + history: Object, +}; + +const activeStyle = { + color: '#000', + background: '#E1E1E1', }; class SidebarCollection extends React.Component { props: Props; - renderDocuments(documentList: Array) { - const { document } = this.props; + renderDocuments(documentList: Array, depth = 0) { + const { document, history } = this.props; + const canDropToImport = depth === 0; if (document) { return documentList.map(doc => ( - - {doc.title} - + {canDropToImport && + + {doc.title} + } + {!canDropToImport && + {doc.title}} + {(document.pathToDocument.includes(doc.id) || document.id === doc.id) && - {doc.children && this.renderDocuments(doc.children)} + {doc.children && this.renderDocuments(doc.children, depth + 1)} } )); @@ -57,6 +73,7 @@ const Header = styled(Flex)` text-transform: uppercase; color: #9FA6AB; letter-spacing: 0.04em; + padding: 0 ${layout.hpadding}; `; const Children = styled(Flex)` diff --git a/frontend/components/Layout/components/SidebarCollectionList/SidebarCollectionList.js b/frontend/components/Layout/components/SidebarCollectionList/SidebarCollectionList.js index 0f865e3d..6d292dbe 100644 --- a/frontend/components/Layout/components/SidebarCollectionList/SidebarCollectionList.js +++ b/frontend/components/Layout/components/SidebarCollectionList/SidebarCollectionList.js @@ -3,23 +3,37 @@ import React from 'react'; import { observer, inject } from 'mobx-react'; import Flex from 'components/Flex'; import styled from 'styled-components'; +import { layout } from 'styles/constants'; import SidebarLink from '../SidebarLink'; +import DropToImport from 'components/DropToImport'; import CollectionsStore from 'stores/CollectionsStore'; type Props = { + history: Object, collections: CollectionsStore, }; -const SidebarCollectionList = observer(({ collections }: Props) => { +const activeStyle = { + color: '#000', + background: '#E1E1E1', +}; + +const SidebarCollectionList = observer(({ history, collections }: Props) => { return (
Collections
{collections.data.map(collection => ( - - {collection.name} - + + + {collection.name} + + ))}
); @@ -31,6 +45,7 @@ const Header = styled(Flex)` text-transform: uppercase; color: #9FA6AB; letter-spacing: 0.04em; + padding: 0 ${layout.hpadding}; `; export default inject('collections')(SidebarCollectionList); diff --git a/frontend/components/Layout/components/SidebarLink/SidebarLink.js b/frontend/components/Layout/components/SidebarLink/SidebarLink.js index 1e73d5e1..7df81e86 100644 --- a/frontend/components/Layout/components/SidebarLink/SidebarLink.js +++ b/frontend/components/Layout/components/SidebarLink/SidebarLink.js @@ -1,7 +1,8 @@ // @flow import React from 'react'; import { NavLink } from 'react-router-dom'; -import Flex from 'components/Flex'; +import { layout, color } from 'styles/constants'; +import { darken } from 'polished'; import styled from 'styled-components'; const activeStyle = { @@ -9,18 +10,16 @@ const activeStyle = { }; function SidebarLink(props: Object) { - return ( - - - - ); + return ; } -const LinkContainer = styled(Flex)` - padding: 5px 0; +const StyledNavLink = styled(NavLink)` + display: block; + padding: 5px ${layout.hpadding}; + color: ${color.slateDark}; - a { - color: #848484; + &:hover { + color: ${darken(0.1, color.slateDark)}; } `; diff --git a/frontend/models/Document.js b/frontend/models/Document.js index 097e4563..db35d742 100644 --- a/frontend/models/Document.js +++ b/frontend/models/Document.js @@ -10,7 +10,7 @@ import type { User } from 'types'; import Collection from './Collection'; const parseHeader = text => { - const firstLine = text.split(/\r?\n/)[0]; + const firstLine = text.trim().split(/\r?\n/)[0]; return firstLine.replace(/^#/, '').trim(); }; @@ -20,7 +20,7 @@ class Document { errors: ErrorsStore; collaborators: Array; - collection: Collection; + collection: $Shape; createdAt: string; createdBy: User; html: string; @@ -113,7 +113,7 @@ class Document { }; @action save = async () => { - if (this.isSaving) return; + if (this.isSaving) return this; this.isSaving = true; try { @@ -125,11 +125,16 @@ class Document { text: this.text, }); } else { - res = await client.post('/documents.create', { + const data = { + parentDocument: undefined, collection: this.collection.id, title: this.title, text: this.text, - }); + }; + if (this.parentDocument) { + data.parentDocument = this.parentDocument.id; + } + res = await client.post('/documents.create', data); } invariant(res && res.data, 'Data should be available'); diff --git a/frontend/scenes/Document/Document.js b/frontend/scenes/Document/Document.js index e653ab27..017e6f1f 100644 --- a/frontend/scenes/Document/Document.js +++ b/frontend/scenes/Document/Document.js @@ -5,12 +5,14 @@ import styled from 'styled-components'; import { observer, inject } from 'mobx-react'; import { withRouter, Prompt } from 'react-router'; import Flex from 'components/Flex'; +import { layout } from 'styles/constants'; import Document from 'models/Document'; import UiStore from 'stores/UiStore'; import DocumentsStore from 'stores/DocumentsStore'; import Menu from './components/Menu'; import Editor from 'components/Editor'; +import DropToImport from 'components/DropToImport'; import { HeaderAction, SaveAction } from 'components/Layout'; import LoadingIndicator from 'components/LoadingIndicator'; import PublishingInfo from 'components/PublishingInfo'; @@ -39,6 +41,7 @@ type Props = { newDocument?: Document, }; state = { + isDragging: false, isLoading: false, newDocument: undefined, }; @@ -125,6 +128,14 @@ type Props = { this.props.history.goBack(); }; + onStartDragging = () => { + this.setState({ isDragging: true }); + }; + + onStopDragging = () => { + this.setState({ isDragging: false }); + }; + render() { const isNew = this.props.newDocument; const isEditing = this.props.match.params.edit || isNew; @@ -133,6 +144,10 @@ type Props = { return ( + {this.state.isDragging && + + Drop files here to import into Atlas. + } {titleText && } {this.state.isLoading && } {isFetching && @@ -141,66 +156,86 @@ type Props = { } {!isFetching && this.document && - - - - + + - - - {!isEditing && - } - {!isEditing && - } - - - {isEditing - ? - : Edit} - - {!isEditing && } - - - } + + + + + {!isEditing && + } + {!isEditing && + } + + + {isEditing + ? + : Edit} + + {!isEditing && } + + + + } ); } } +const DropHere = styled(Flex)` + pointer-events: none; + position: fixed; + top: 0; + left: ${layout.sidebarWidth}; + bottom: 0; + right: 0; + text-align: center; + background: rgba(255,255,255,.9); + z-index: 1; +`; + const Meta = styled(Flex)` justify-content: ${props => (props.readOnly ? 'space-between' : 'flex-end')}; align-items: flex-start; width: 100%; position: absolute; top: 0; - padding: 10px 20px; + padding: ${layout.padding}; `; const Container = styled(Flex)` diff --git a/frontend/stores/DocumentsStore.js b/frontend/stores/DocumentsStore.js index c33195a3..aaffa4b0 100644 --- a/frontend/stores/DocumentsStore.js +++ b/frontend/stores/DocumentsStore.js @@ -50,10 +50,14 @@ class DocumentsStore { const res = await client.post('/documents.info', { id }); invariant(res && res.data, 'Document not available'); const { data } = res; + const document = new Document(data); + runInAction('DocumentsStore#fetch', () => { - this.data.set(data.id, new Document(data)); + this.data.set(data.id, document); this.isLoaded = true; }); + + return document; } catch (e) { this.errors.add('Failed to load documents'); } diff --git a/frontend/styles/constants.js b/frontend/styles/constants.js index cc9cbcc1..d3a09f95 100644 --- a/frontend/styles/constants.js +++ b/frontend/styles/constants.js @@ -1,5 +1,14 @@ // @flow +export const layout = { + padding: '1.5vw 1.875vw', + vpadding: '1.5vw', + hpadding: '1.875vw', + sidebarWidth: '22%', + sidebarMinWidth: '250px', + sidebarMaxWidth: '350px', +}; + export const size = { tiny: '2px', small: '4px', @@ -28,6 +37,8 @@ export const fontWeight = { }; export const color = { + text: '#171B35', + /* Brand */ primary: '#73DF7B',