Merge master

This commit is contained in:
Tom Moor
2017-07-08 22:30:20 -07:00
58 changed files with 828 additions and 524 deletions

View File

@@ -1,6 +1,6 @@
// @flow
import React, { PropTypes } from 'react';
import { Flex } from 'reflexbox';
import Flex from 'components/Flex';
import classNames from 'classnames/bind';
import styles from './Alert.scss';

View File

@@ -4,8 +4,6 @@ import styled from 'styled-components';
type Props = {
children?: React.Element<any>,
style?: Object,
maxWidth?: string,
};
const Container = styled.div`
@@ -13,20 +11,17 @@ const Container = styled.div`
margin: 40px 20px;
`;
const CenteredContent = ({
children,
maxWidth = '740px',
style,
...rest
}: Props) => {
const styles = {
maxWidth,
...style,
};
const Content = styled.div`
max-width: 740px;
margin: 0 auto;
`;
const CenteredContent = ({ children, ...rest }: Props) => {
return (
<Container style={styles} {...rest}>
{children}
<Container {...rest}>
<Content>
{children}
</Content>
</Container>
);
};

View File

@@ -18,7 +18,7 @@ class DocumentViewersStore {
this.isFetching = true;
try {
const res = await client.get(
const res = await client.post(
'/views.list',
{
id: this.documentId,

View File

@@ -5,7 +5,7 @@ import Popover from 'components/Popover';
import styled from 'styled-components';
import DocumentViewers from './components/DocumentViewers';
import DocumentViewersStore from './DocumentViewersStore';
import { Flex } from 'reflexbox';
import Flex from 'components/Flex';
const Container = styled(Flex)`
font-size: 13px;

View File

@@ -1,6 +1,6 @@
// @flow
import React, { Component } from 'react';
import { Flex } from 'reflexbox';
import Flex from 'components/Flex';
import styled from 'styled-components';
import map from 'lodash/map';
import Avatar from 'components/Avatar';

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

@@ -61,6 +61,12 @@ type KeyData = {
}
}
componentDidUpdate(prevProps: Props) {
if (prevProps.readOnly && !this.props.readOnly) {
this.focusAtEnd();
}
}
getChildContext() {
return { starred: this.props.starred };
}

View File

@@ -0,0 +1,41 @@
// @flow
import React from 'react';
import styled from 'styled-components';
type JustifyValues =
| 'center'
| 'space-around'
| 'space-between'
| 'flex-start'
| 'flex-end';
type AlignValues =
| 'stretch'
| 'center'
| 'baseline'
| 'flex-start'
| 'flex-end';
type Props = {
column?: ?boolean,
align?: AlignValues,
justify?: JustifyValues,
auto?: ?boolean,
className?: string,
children?: React.Element<any>,
};
const Flex = (props: Props) => {
const { children, ...restProps } = props;
return <Container {...restProps}>{children}</Container>;
};
const Container = styled.div`
display: flex;
flex: ${({ auto }) => (auto ? '1 1 auto' : 'initial')};
flex-direction: ${({ column }) => (column ? 'column' : 'row')};
align-items: ${({ align }) => align};
justify-content: ${({ justify }) => justify};
`;
export default Flex;

View File

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

View File

@@ -1,7 +1,7 @@
// @flow
import React from 'react';
import styled from 'styled-components';
import { Flex } from 'reflexbox';
import Flex from 'components/Flex';
import { size } from 'styles/constants';
const RealTextarea = styled.textarea`

View File

@@ -6,11 +6,13 @@ import styled from 'styled-components';
import { observer, inject } from 'mobx-react';
import _ from 'lodash';
import keydown from 'react-keydown';
import { Flex } from 'reflexbox';
import { textColor } from 'styles/constants.scss';
import Flex from 'components/Flex';
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';
@@ -101,21 +103,24 @@ type Props = {
</Header>
<Flex column>
<LinkSection>
<SidebarLink to="/search">Search</SidebarLink>
</LinkSection>
<LinkSection>
<SidebarLink to="/dashboard">Home</SidebarLink>
<SidebarLink to="/starred">Starred</SidebarLink>
</LinkSection>
<LinkSection>
{ui.activeCollection
? <SidebarCollection
document={ui.activeDocument}
collection={ui.activeCollection}
/>
: <SidebarCollectionList />}
</LinkSection>
<Scrollable>
<LinkSection>
<SidebarLink to="/search">Search</SidebarLink>
</LinkSection>
<LinkSection>
<SidebarLink to="/dashboard">Home</SidebarLink>
<SidebarLink to="/starred">Starred</SidebarLink>
</LinkSection>
<LinkSection>
{ui.activeCollection
? <SidebarCollection
document={ui.activeDocument}
collection={ui.activeCollection}
history={this.props.history}
/>
: <SidebarCollectionList history={this.props.history} />}
</LinkSection>
</Scrollable>
</Flex>
</Sidebar>}
@@ -135,22 +140,16 @@ const Container = styled(Flex)`
`;
const LogoLink = styled(Link)`
margin-top: 5px;
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)`
@@ -159,26 +158,27 @@ 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)};
padding: 10px 20px;
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;
`;
const Header = styled(Flex)`
margin-bottom: 20px;
flex-shrink: 0;
padding: ${layout.padding};
padding-bottom: 10px;
`;
const LinkSection = styled(Flex)`
margin-bottom: 20px;
flex-direction: column;
padding: 10px 0;
`;
export default withRouter(inject('user', 'auth', 'ui', 'collections')(Layout));

View File

@@ -1,35 +1,51 @@
// @flow
import React from 'react';
import { observer } from 'mobx-react';
import { Flex } from 'reflexbox';
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';
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) {
const { document } = this.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}>
<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>
{doc.children && this.renderDocuments(doc.children)}
<Children column>
{doc.children && this.renderDocuments(doc.children, depth + 1)}
</Children>}
</Flex>
));
@@ -57,10 +73,11 @@ const Header = styled(Flex)`
text-transform: uppercase;
color: #9FA6AB;
letter-spacing: 0.04em;
padding: 0 ${layout.hpadding};
`;
const Children = styled(Flex)`
margin-left: 20px;
`;
export default observer(SidebarCollection);
export default SidebarCollection;

View File

@@ -1,25 +1,39 @@
// @flow
import React from 'react';
import { observer, inject } from 'mobx-react';
import { Flex } from 'reflexbox';
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 => (
<SidebarLink key={collection.id} to={collection.entryUrl}>
{collection.name}
</SidebarLink>
<DropToImport
history={history}
collectionId={collection.id}
activeStyle={activeStyle}
>
<SidebarLink key={collection.id} to={collection.entryUrl}>
{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,35 +1,26 @@
// @flow
import React from 'react';
import { observer } from 'mobx-react';
import { NavLink, withRouter } from 'react-router-dom';
import { Flex } from 'reflexbox';
import { NavLink } from 'react-router-dom';
import { layout, color } from 'styles/constants';
import { darken } from 'polished';
import styled from 'styled-components';
const activeStyle = {
color: '#000000',
};
@observer class SidebarLink extends React.Component {
shouldComponentUpdate(nextProps) {
// Navlink is having issues updating, forcing update on URL changes
return this.props.match !== nextProps.match;
}
render() {
return (
<LinkContainer>
<NavLink exact {...this.props} activeStyle={activeStyle} />
</LinkContainer>
);
}
function SidebarLink(props: Object) {
return <StyledNavLink exact {...props} activeStyle={activeStyle} />;
}
const LinkContainer = styled(Flex)`
padding: 5px 0;
a {
color: #848484;
const StyledNavLink = styled(NavLink)`
display: block;
padding: 5px ${layout.hpadding};
color: ${color.slateDark};
&:hover {
color: ${darken(0.1, color.slateDark)};
}
`;
export default withRouter(SidebarLink);
export default SidebarLink;

View File

@@ -2,7 +2,7 @@
import React from 'react';
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
import styled, { keyframes } from 'styled-components';
import { Flex } from 'reflexbox';
import Flex from 'components/Flex';
import { randomInteger } from 'utils/random';
@@ -11,7 +11,7 @@ const randomValues = Array.from(
() => `${randomInteger(85, 100)}%`
);
export default () => {
export default (props: {}) => {
return (
<ReactCSSTransitionGroup
transitionName="fadeIn"
@@ -22,7 +22,7 @@ export default () => {
transitionEnterTimeout={0}
transitionLeaveTimeout={0}
>
<Flex column auto>
<Flex column auto {...props}>
<Mask style={{ width: randomValues[0] }} header />
<Mask style={{ width: randomValues[1] }} />
<Mask style={{ width: randomValues[2] }} />

View File

@@ -3,7 +3,7 @@ import React, { Component } from 'react';
import moment from 'moment';
import styled from 'styled-components';
import type { User } from 'types';
import { Flex } from 'reflexbox';
import Flex from 'components/Flex';
const Container = styled(Flex)`
justify-content: space-between;
@@ -51,7 +51,6 @@ class PublishingInfo extends Component {
<Avatar key={user.id} src={user.avatarUrl} title={user.name} />
))}
</Avatars>}
{createdAt === updatedAt
? <span>
{createdBy.name}

View File

@@ -0,0 +1,18 @@
// @flow
// based on: https://reacttraining.com/react-router/web/guides/scroll-restoration
import { Component } from 'react';
import { withRouter } from 'react-router';
class ScrollToTop extends Component {
componentDidUpdate(prevProps) {
if (this.props.location !== prevProps.location) {
window.scrollTo(0, 0);
}
}
render() {
return this.props.children;
}
}
export default withRouter(ScrollToTop);

View File

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

View File

@@ -0,0 +1,25 @@
// @flow
import { Component } from 'react';
import { inject } from 'mobx-react';
import UiStore from 'stores/UiStore';
class SidebarHidden extends Component {
props: {
ui: UiStore,
children: React$Element<any>,
};
componentDidMount() {
this.props.ui.enableEditMode();
}
componentWillUnmount() {
this.props.ui.disableEditMode();
}
render() {
return this.props.children;
}
}
export default inject('ui')(SidebarHidden);

View File

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

View File

@@ -8,7 +8,7 @@ import {
Route,
Redirect,
} from 'react-router-dom';
import { Flex } from 'reflexbox';
import Flex from 'components/Flex';
import stores from 'stores';
import DocumentsStore from 'stores/DocumentsStore';
@@ -33,7 +33,9 @@ import Flatpage from 'scenes/Flatpage';
import ErrorAuth from 'scenes/ErrorAuth';
import Error404 from 'scenes/Error404';
import ScrollToTop from 'components/ScrollToTop';
import Layout from 'components/Layout';
import SidebarHidden from 'components/SidebarHidden';
import flatpages from 'static/flatpages';
@@ -85,52 +87,73 @@ const KeyboardShortcuts = () => (
);
const Api = () => <Flatpage title="API" content={flatpages.api} />;
const DocumentNew = () => <Document newDocument />;
const DocumentNewChild = () => <Document newChildDocument />;
const RedirectDocument = ({ match }: { match: Object }) => (
<Redirect to={`/doc/${match.params.documentSlug}`} />
);
const matchDocumentSlug = ':documentSlug([0-9a-zA-Z-]*-[a-zA-z0-9]{10,15})';
render(
<div style={{ display: 'flex', flex: 1, height: '100%' }}>
<Provider {...stores}>
<Router>
<Switch>
<Route exact path="/" component={Home} />
<ScrollToTop>
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/auth/slack" component={SlackAuth} />
<Route exact path="/auth/slack/commands" component={SlackAuth} />
<Route exact path="/auth/error" component={ErrorAuth} />
<Route exact path="/auth/slack" component={SlackAuth} />
<Route exact path="/auth/slack/commands" component={SlackAuth} />
<Route exact path="/auth/error" component={ErrorAuth} />
<Auth>
<Layout>
<Switch>
<Route exact path="/dashboard" component={Dashboard} />
<Route exact path="/starred" component={Starred} />
<Route exact path="/collections/:id" component={Collection} />
<Route exact path="/d/:id" component={Document} />
<Auth>
<Layout>
<Switch>
<Route exact path="/dashboard" component={Dashboard} />
<Route exact path="/starred" component={Starred} />
<Route exact path="/collections/:id" component={Collection} />
<Route
exact
path={`/d/${matchDocumentSlug}`}
component={RedirectDocument}
/>
<Route
exact
path={`/doc/${matchDocumentSlug}`}
component={Document}
/>
<SidebarHidden>
<Switch>
<Route
exact
path={`/doc/${matchDocumentSlug}/:edit`}
component={Document}
/>
<Route
exact
path="/collections/:id/new"
component={DocumentNew}
/>
</Switch>
</SidebarHidden>
<Route exact path="/d/:id/:edit" component={Document} />
<Route
exact
path="/collections/:id/new"
component={DocumentNew}
/>
<Route exact path="/d/:id/new" component={DocumentNewChild} />
<Route exact path="/search" component={Search} />
<Route exact path="/search/:query" component={Search} />
<Route exact path="/settings" component={Settings} />
<Route exact path="/search" component={Search} />
<Route exact path="/search/:query" component={Search} />
<Route exact path="/settings" component={Settings} />
<Route
exact
path="/keyboard-shortcuts"
component={KeyboardShortcuts}
/>
<Route exact path="/developers" component={Api} />
<Route
exact
path="/keyboard-shortcuts"
component={KeyboardShortcuts}
/>
<Route exact path="/developers" component={Api} />
<Route path="/404" component={Error404} />
<Route component={notFoundSearch} />
</Switch>
</Layout>
</Auth>
</Switch>
<Route path="/404" component={Error404} />
<Route component={notFoundSearch} />
</Switch>
</Layout>
</Auth>
</Switch>
</ScrollToTop>
</Router>
</Provider>
{DevTools && <DevTools position={{ bottom: 0, right: 0 }} />}

View File

@@ -10,25 +10,25 @@ 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();
};
class Document {
isSaving: boolean;
isSaving: boolean = false;
hasPendingChanges: boolean = false;
errors: ErrorsStore;
collaborators: Array<User>;
collection: Collection;
collection: $Shape<Collection>;
createdAt: string;
createdBy: User;
html: string;
id: string;
private: boolean;
starred: boolean;
team: string;
text: string;
private: boolean = false;
starred: boolean = false;
text: string = '';
title: string = 'Untitled document';
updatedAt: string;
updatedBy: User;
@@ -83,9 +83,9 @@ class Document {
};
@action view = async () => {
this.views++;
try {
await client.post('/views.create', { id: this.id });
this.views++;
} catch (e) {
this.errors.add('Document failed to record view');
}
@@ -113,7 +113,7 @@ class Document {
};
@action save = async () => {
if (this.isSaving) return;
if (this.isSaving) return this;
this.isSaving = true;
try {
@@ -125,28 +125,38 @@ 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');
this.hasPendingChanges = false;
this.updateData({
...res.data,
hasPendingChanges: false,
});
} catch (e) {
this.errors.add('Document failed saving');
} finally {
this.isSaving = false;
}
return this;
};
updateData(data: Object | Document) {
data.title = parseHeader(data.text);
if (data.text) data.title = parseHeader(data.text);
extendObservable(this, data);
}
constructor(document: Document) {
constructor(document?: Object = {}) {
this.updateData(document);
this.errors = stores.errors;
}

View File

@@ -5,8 +5,7 @@ describe('Document model', () => {
test('should initialize with data', () => {
const document = new Document({
id: 123,
title: 'Onboarding',
text: 'Some body text'
text: '# Onboarding\nSome body text',
});
expect(document.title).toBe('Onboarding');
});

View File

@@ -4,16 +4,18 @@ import get from 'lodash/get';
import styled from 'styled-components';
import { observer, inject } from 'mobx-react';
import { withRouter, Prompt } from 'react-router';
import { Flex } from 'reflexbox';
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';
import AuthorInfo from 'components/AuthorInfo';
import PreviewLoading from 'components/PreviewLoading';
import CenteredContent from 'components/CenteredContent';
import PageTitle from 'components/PageTitle';
@@ -28,15 +30,19 @@ type Props = {
history: Object,
keydown: Object,
documents: DocumentsStore,
newChildDocument?: boolean,
newDocument?: boolean,
ui: UiStore,
};
@observer class Document extends Component {
@observer class DocumentScene extends Component {
props: Props;
state: {
newDocument?: Document,
};
state = {
isDragging: false,
isLoading: false,
newDocument: undefined,
};
componentDidMount() {
@@ -44,7 +50,10 @@ type Props = {
}
componentWillReceiveProps(nextProps) {
if (nextProps.match.params.id !== this.props.match.params.id) {
if (
nextProps.match.params.documentSlug !==
this.props.match.params.documentSlug
) {
this.loadDocument(nextProps);
}
}
@@ -54,42 +63,49 @@ type Props = {
}
loadDocument = async props => {
await this.props.documents.fetch(props.match.params.id);
const document = this.document;
if (document) {
this.props.ui.setActiveDocument(document);
document.view();
}
if (this.props.match.params.edit) {
this.props.ui.enableEditMode();
if (props.newDocument) {
const newDocument = new Document({
collection: { id: props.match.params.id },
});
this.setState({ newDocument });
} else {
this.props.ui.disableEditMode();
let document = this.document;
if (document) {
this.props.ui.setActiveDocument(document);
}
await this.props.documents.fetch(props.match.params.documentSlug);
document = this.document;
if (document) {
this.props.ui.setActiveDocument(document);
document.view();
}
}
};
get document() {
return this.props.documents.getByUrl(`/d/${this.props.match.params.id}`);
if (this.state.newDocument) return this.state.newDocument;
return this.props.documents.getByUrl(
`/doc/${this.props.match.params.documentSlug}`
);
}
onClickEdit = () => {
if (!this.document) return;
const url = `${this.document.url}/edit`;
this.props.history.push(url);
this.props.ui.enableEditMode();
};
onSave = async (redirect: boolean = false) => {
const document = this.document;
let document = this.document;
if (!document) return;
this.setState({ isLoading: true });
await document.save();
document = await document.save();
this.setState({ isLoading: false });
this.props.ui.disableEditMode();
if (redirect) {
if (redirect || this.props.newDocument) {
this.props.history.push(document.url);
}
};
@@ -111,86 +127,111 @@ type Props = {
this.props.history.goBack();
};
onStartDragging = () => {
this.setState({ isDragging: true });
};
onStopDragging = () => {
this.setState({ isDragging: false });
};
render() {
const isNew = this.props.newDocument || this.props.newChildDocument;
const isEditing = this.props.match.params.edit;
const isFetching = !this.document && get(this.document, 'isFetching');
const isNew = this.props.newDocument;
const isEditing = this.props.match.params.edit || isNew;
const isFetching = !this.document;
const titleText = get(this.document, 'title', 'Loading');
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 &&
<CenteredContent>
<PreviewLoading />
<LoadingState />
</CenteredContent>}
{!isFetching &&
this.document &&
<PagePadding justify="center" auto>
<Prompt
when={this.document.hasPendingChanges}
message={DISCARD_CHANGES}
/>
<DocumentContainer>
<InfoWrapper visible={!isEditing}>
<PublishingInfo
collaborators={this.document.collaborators}
createdAt={this.document.createdAt}
createdBy={this.document.createdBy}
updatedAt={this.document.updatedAt}
updatedBy={this.document.updatedBy}
/>
</InfoWrapper>
<Content>
<Editor
key={this.document.id}
text={this.document.text}
onImageUploadStart={this.onImageUploadStart}
onImageUploadStop={this.onImageUploadStop}
onChange={this.onChange}
onSave={this.onSave}
onCancel={this.onCancel}
onStar={this.document.star}
onUnstar={this.document.unstar}
starred={this.document.starred}
readOnly={!isEditing}
/>
</Content>
<InfoWrapper visible={!isEditing} bottom>
<AuthorInfo
collaborators={this.document.collaborators}
views={this.document.views}
/>
</InfoWrapper>
</DocumentContainer>
<Meta align="center" justify="flex-end" readOnly={!isEditing}>
<Flex align="center">
<HeaderAction>
{isEditing
? <SaveAction
onClick={this.onSave.bind(this, true)}
disabled={get(this.document, 'isSaving')}
isNew={!!isNew}
/>
: <a onClick={this.onClickEdit}>Edit</a>}
</HeaderAction>
{!isEditing && <Menu document={this.document} />}
</Flex>
</Meta>
</PagePadding>}
<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}
message={DISCARD_CHANGES}
/>
<DocumentContainer>
<InfoWrapper visible={!isEditing}>
<PublishingInfo
collaborators={this.document.collaborators}
createdAt={this.document.createdAt}
createdBy={this.document.createdBy}
updatedAt={this.document.updatedAt}
updatedBy={this.document.updatedBy}
/>
</InfoWrapper>
<Content>
<Editor
key={this.document.id}
text={this.document.text}
onImageUploadStart={this.onImageUploadStart}
onImageUploadStop={this.onImageUploadStop}
onChange={this.onChange}
onSave={this.onSave}
onCancel={this.onCancel}
onStar={this.document.star}
onUnstar={this.document.unstar}
starred={this.document.starred}
readOnly={!isEditing}
/>
</Content>
</DocumentContainer>
<Meta align="center" justify="flex-end" readOnly={!isEditing}>
<Flex align="center">
<HeaderAction>
{isEditing
? <SaveAction
onClick={this.onSave.bind(this, true)}
disabled={get(this.document, 'isSaving')}
isNew={!!isNew}
/>
: <a onClick={this.onClickEdit}>Edit</a>}
</HeaderAction>
{!isEditing && <Menu document={this.document} />}
</Flex>
</Meta>
</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 Content = styled(Flex)`
@@ -207,6 +248,10 @@ const Container = styled.div`
width: 100%;
`;
const LoadingState = styled(PreviewLoading)`
margin: 80px 20px;
`;
const PagePadding = styled(Flex)`
padding: 80px 20px;
position: relative;
@@ -220,4 +265,4 @@ const DocumentContainer = styled.div`
width: 50em;
`;
export default withRouter(inject('ui', 'documents')(Document));
export default withRouter(inject('ui', 'user', 'documents')(DocumentScene));

View File

@@ -16,9 +16,7 @@ type Props = {
props: Props;
onCreateDocument = () => {
// Disabled until created a better API
// invariant(this.props.collectionTree, 'collectionTree is not available');
// this.props.history.push(`${this.props.collectionTree.url}/new`);
this.props.history.push(`${this.props.document.collection.url}/new`);
};
onCreateChild = () => {
@@ -68,7 +66,6 @@ type Props = {
<MenuItem onClick={this.onCreateDocument}>
New document
</MenuItem>
<MenuItem onClick={this.onCreateChild}>New child</MenuItem>
</div>}
<MenuItem onClick={this.onExport}>Export</MenuItem>
{allowDelete && <MenuItem onClick={this.onDelete}>Delete</MenuItem>}

View File

@@ -2,7 +2,7 @@
import React from 'react';
import { observer, inject } from 'mobx-react';
import { Redirect } from 'react-router';
import { Flex } from 'reflexbox';
import Flex from 'components/Flex';
import styled from 'styled-components';
import AuthStore from 'stores/AuthStore';

View File

@@ -3,7 +3,7 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { observer } from 'mobx-react';
import _ from 'lodash';
import { Flex } from 'reflexbox';
import Flex from 'components/Flex';
import { withRouter } from 'react-router';
import { searchUrl } from 'utils/routeHelpers';
import styled from 'styled-components';

View File

@@ -1,6 +1,6 @@
// @flow
import React, { Component } from 'react';
import { Flex } from 'reflexbox';
import Flex from 'components/Flex';
import styled from 'styled-components';
import searchImg from 'assets/icons/search.svg';

View File

@@ -1,7 +1,7 @@
// @flow
import React from 'react';
import { observer } from 'mobx-react';
import { Flex } from 'reflexbox';
import Flex from 'components/Flex';
import ApiKeyRow from './components/ApiKeyRow';
import styles from './Settings.scss';

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