Drag and Drop Import (#95)

* Drag and drop files into collection first pass

* Allow import of sub documents
Fix up UI styles

* Import Loading indicator

* Drag onto document to import
This commit is contained in:
Tom Moor
2017-07-08 22:12:14 -07:00
committed by GitHub
parent b7e1ac8a36
commit 444c8afb2a
10 changed files with 283 additions and 90 deletions

View File

@ -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<any>,
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 (
<Dropzone
accept="text/markdown, text/plain"
onDropAccepted={this.onDropAccepted}
style={{}}
disableClick
disablePreview
multiple
{...props}
>
<span>
{this.state.isImporting && <LoadingIndicator />}
{this.props.children}
</span>
</Dropzone>
);
}
}
export default inject('documents')(DropToImport);

View File

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

View File

@ -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 = {
? <SidebarCollection
document={ui.activeDocument}
collection={ui.activeCollection}
history={this.props.history}
/>
: <SidebarCollectionList />}
: <SidebarCollectionList history={this.props.history} />}
</LinkSection>
</Scrollable>
</Flex>
@ -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));

View File

@ -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<NavigationNode>) {
const { document } = this.props;
renderDocuments(documentList: Array<NavigationNode>, depth = 0) {
const { document, history } = this.props;
const canDropToImport = depth === 0;
if (document) {
return documentList.map(doc => (
<Flex column key={doc.id}>
<SidebarLink key={doc.id} to={doc.url}>
{doc.title}
</SidebarLink>
{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.includes(doc.id) ||
document.id === doc.id) &&
<Children column>
{doc.children && this.renderDocuments(doc.children)}
{doc.children && this.renderDocuments(doc.children, depth + 1)}
</Children>}
</Flex>
));
@ -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)`

View File

@ -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 (
<Flex column>
<Header>Collections</Header>
{collections.data.map(collection => (
<DropToImport
history={history}
collectionId={collection.id}
activeStyle={activeStyle}
>
<SidebarLink key={collection.id} to={collection.url}>
{collection.name}
</SidebarLink>
</DropToImport>
))}
</Flex>
);
@ -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);

View File

@ -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 (
<LinkContainer>
<NavLink exact {...props} activeStyle={activeStyle} />
</LinkContainer>
);
return <StyledNavLink exact {...props} activeStyle={activeStyle} />;
}
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)};
}
`;

View File

@ -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<User>;
collection: Collection;
collection: $Shape<Collection>;
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');

View File

@ -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 (
<Container column auto>
{this.state.isDragging &&
<DropHere align="center" justify="center">
Drop files here to import into Atlas.
</DropHere>}
{titleText && <PageTitle title={titleText} />}
{this.state.isLoading && <LoadingIndicator />}
{isFetching &&
@ -141,6 +156,13 @@ type Props = {
</CenteredContent>}
{!isFetching &&
this.document &&
<DropToImport
documentId={this.document.id}
history={this.props.history}
onDragEnter={this.onStartDragging}
onDragLeave={this.onStopDragging}
onDrop={this.onStopDragging}
>
<PagePadding justify="center" auto>
<Prompt
when={this.document.hasPendingChanges}
@ -188,19 +210,32 @@ type Props = {
{!isEditing && <Menu document={this.document} />}
</Flex>
</Meta>
</PagePadding>}
</PagePadding>
</DropToImport>}
</Container>
);
}
}
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)`

View File

@ -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');
}

View File

@ -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',