commit
0057d305b8
|
@ -117,7 +117,7 @@ type KeyData = {
|
|||
<Editor
|
||||
key={this.props.starred}
|
||||
ref={ref => (this.editor = ref)}
|
||||
placeholder="Start with a title…"
|
||||
placeholder="Start with a title..."
|
||||
className={cx(styles.editor, { readOnly: this.props.readOnly })}
|
||||
schema={this.schema}
|
||||
plugins={this.plugins}
|
||||
|
|
|
@ -59,7 +59,7 @@ type Props = {
|
|||
};
|
||||
|
||||
render() {
|
||||
const { user, auth, ui, collections } = this.props;
|
||||
const { user, auth, ui } = this.props;
|
||||
|
||||
return (
|
||||
<Container column auto>
|
||||
|
@ -112,7 +112,8 @@ type Props = {
|
|||
<LinkSection>
|
||||
{ui.activeCollection
|
||||
? <SidebarCollection
|
||||
collection={collections.getById(ui.activeCollection)}
|
||||
document={ui.activeDocument}
|
||||
collection={ui.activeCollection}
|
||||
/>
|
||||
: <SidebarCollectionList />}
|
||||
</LinkSection>
|
||||
|
|
|
@ -7,26 +7,49 @@ import styled from 'styled-components';
|
|||
import SidebarLink from '../SidebarLink';
|
||||
|
||||
import Collection from 'models/Collection';
|
||||
import Document from 'models/Document';
|
||||
|
||||
type Props = {
|
||||
collection: Collection,
|
||||
collection: ?Collection,
|
||||
document: ?Document,
|
||||
};
|
||||
|
||||
const SidebarCollection = ({ collection }: Props) => {
|
||||
if (collection) {
|
||||
return (
|
||||
<Flex column>
|
||||
<Header>{collection.name}</Header>
|
||||
{collection.documents.map(document => (
|
||||
<SidebarLink key={document.id} to={document.url}>
|
||||
{document.title}
|
||||
class SidebarCollection extends React.Component {
|
||||
props: Props;
|
||||
|
||||
renderDocuments(documentList) {
|
||||
const { document } = this.props;
|
||||
|
||||
if (document) {
|
||||
return documentList.map(doc => (
|
||||
<Flex column key={doc.id}>
|
||||
<SidebarLink key={doc.id} to={doc.url}>
|
||||
{doc.title}
|
||||
</SidebarLink>
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
{(document.pathToDocument.includes(doc.id) ||
|
||||
document.id === doc.id) &&
|
||||
<Children>
|
||||
{doc.children && this.renderDocuments(doc.children)}
|
||||
</Children>}
|
||||
</Flex>
|
||||
));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
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;
|
||||
|
@ -36,4 +59,8 @@ const Header = styled(Flex)`
|
|||
letter-spacing: 0.04em;
|
||||
`;
|
||||
|
||||
const Children = styled(Flex)`
|
||||
margin-left: 20px;
|
||||
`;
|
||||
|
||||
export default observer(SidebarCollection);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { NavLink, withRouter } from 'react-router-dom';
|
||||
import { Flex } from 'reflexbox';
|
||||
import styled from 'styled-components';
|
||||
|
||||
|
@ -9,11 +9,20 @@ const activeStyle = {
|
|||
color: '#000000',
|
||||
};
|
||||
|
||||
const SidebarLink = observer(props => (
|
||||
<LinkContainer>
|
||||
<NavLink {...props} activeStyle={activeStyle} />
|
||||
</LinkContainer>
|
||||
));
|
||||
@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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const LinkContainer = styled(Flex)`
|
||||
padding: 5px 0;
|
||||
|
@ -23,4 +32,4 @@ const LinkContainer = styled(Flex)`
|
|||
}
|
||||
`;
|
||||
|
||||
export default SidebarLink;
|
||||
export default withRouter(SidebarLink);
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
// @flow
|
||||
import { extendObservable, action, runInAction, computed } from 'mobx';
|
||||
import invariant from 'invariant';
|
||||
|
||||
import ApiClient, { client } from 'utils/ApiClient';
|
||||
import stores from 'stores';
|
||||
import ErrorsStore from 'stores/ErrorsStore';
|
||||
|
||||
import type { User } from 'types';
|
||||
import Collection from './Collection';
|
||||
|
||||
class Document {
|
||||
collaborators: Array<User>;
|
||||
collection: Collection;
|
||||
createdAt: string;
|
||||
createdBy: User;
|
||||
html: string;
|
||||
id: string;
|
||||
private: boolean;
|
||||
starred: boolean;
|
||||
team: string;
|
||||
text: string;
|
||||
title: string;
|
||||
updatedAt: string;
|
||||
updatedBy: User;
|
||||
url: string;
|
||||
views: number;
|
||||
|
||||
client: ApiClient;
|
||||
errors: ErrorsStore;
|
||||
|
||||
/* Computed */
|
||||
|
||||
@computed get pathToDocument(): Array<string> {
|
||||
let path;
|
||||
const traveler = (nodes, previousPath) => {
|
||||
nodes.forEach(childNode => {
|
||||
const newPath = [...previousPath, childNode.id];
|
||||
if (childNode.id === this.id) {
|
||||
path = newPath;
|
||||
return;
|
||||
} else {
|
||||
return traveler(childNode.children, newPath);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (this.collection.documents) {
|
||||
traveler(this.collection.documents, []);
|
||||
invariant(path, 'Path is not available for collection, abort');
|
||||
return path;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/* Actions */
|
||||
|
||||
@action update = async () => {
|
||||
try {
|
||||
const res = await this.client.post('/documents.info', { id: this.id });
|
||||
invariant(res && res.data, 'Document API response should be available');
|
||||
const { data } = res;
|
||||
runInAction('Document#update', () => {
|
||||
this.updateData(data);
|
||||
});
|
||||
} catch (e) {
|
||||
this.errors.add('Document failed loading');
|
||||
}
|
||||
};
|
||||
|
||||
updateData(data: Document) {
|
||||
extendObservable(this, data);
|
||||
}
|
||||
|
||||
constructor(document: Document) {
|
||||
this.updateData(document);
|
||||
this.client = client;
|
||||
this.errors = stores.errors;
|
||||
}
|
||||
}
|
||||
|
||||
export default Document;
|
|
@ -18,7 +18,7 @@ class CollectionStore {
|
|||
invariant(res && res.data, 'Data should be available');
|
||||
const { data } = res;
|
||||
|
||||
if (data.type === 'atlas') this.redirectUrl = data.recentDocuments[0].url;
|
||||
if (data.type === 'atlas') this.redirectUrl = data.documents[0].url;
|
||||
else throw new Error('TODO code up non-atlas collections');
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
|
|
|
@ -44,18 +44,27 @@ type Props = {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.newDocument) {
|
||||
this.store.collectionId = this.props.match.params.id;
|
||||
this.loadDocument(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.match.params.id !== this.props.match.params.id)
|
||||
this.loadDocument(nextProps);
|
||||
}
|
||||
|
||||
loadDocument(props) {
|
||||
if (props.newDocument) {
|
||||
this.store.collectionId = props.match.params.id;
|
||||
this.store.newDocument = true;
|
||||
} else if (this.props.match.params.edit) {
|
||||
this.store.documentId = this.props.match.params.id;
|
||||
} else if (props.match.params.edit) {
|
||||
this.store.documentId = props.match.params.id;
|
||||
this.store.fetchDocument();
|
||||
} else if (this.props.newChildDocument) {
|
||||
this.store.documentId = this.props.match.params.id;
|
||||
} else if (props.newChildDocument) {
|
||||
this.store.documentId = props.match.params.id;
|
||||
this.store.newChildDocument = true;
|
||||
this.store.fetchDocument();
|
||||
} else {
|
||||
this.store.documentId = this.props.match.params.id;
|
||||
this.store.documentId = props.match.params.id;
|
||||
this.store.newDocument = false;
|
||||
this.store.fetchDocument();
|
||||
}
|
||||
|
@ -64,7 +73,7 @@ type Props = {
|
|||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.ui.clearActiveCollection();
|
||||
this.props.ui.clearActiveDocument();
|
||||
}
|
||||
|
||||
onEdit = () => {
|
||||
|
@ -117,32 +126,31 @@ type Props = {
|
|||
);
|
||||
|
||||
return (
|
||||
<Container flex column>
|
||||
<Container column auto>
|
||||
{titleText && <PageTitle title={titleText} />}
|
||||
<Prompt when={this.store.hasPendingChanges} message={DISCARD_CHANGES} />
|
||||
|
||||
<PagePadding auto justify="center">
|
||||
<PageTitle title={titleText} />
|
||||
<Prompt
|
||||
when={this.store.hasPendingChanges}
|
||||
message={DISCARD_CHANGES}
|
||||
/>
|
||||
{this.store.isFetching &&
|
||||
<CenteredContent>
|
||||
<PreviewLoading />
|
||||
</CenteredContent>}
|
||||
{this.store.document &&
|
||||
<DocumentContainer>
|
||||
<Editor
|
||||
text={this.store.document.text}
|
||||
onImageUploadStart={this.onImageUploadStart}
|
||||
onImageUploadStop={this.onImageUploadStop}
|
||||
onChange={this.store.updateText}
|
||||
onSave={this.onSave}
|
||||
onCancel={this.onCancel}
|
||||
onStar={this.store.starDocument}
|
||||
onUnstar={this.store.unstarDocument}
|
||||
starred={this.store.document.starred}
|
||||
readOnly={!isEditing}
|
||||
/>
|
||||
</DocumentContainer>}
|
||||
{this.store.isFetching
|
||||
? <CenteredContent>
|
||||
<PreviewLoading />
|
||||
</CenteredContent>
|
||||
: this.store.document &&
|
||||
<DocumentContainer>
|
||||
<Editor
|
||||
key={this.store.document.id}
|
||||
text={this.store.document.text}
|
||||
onImageUploadStart={this.onImageUploadStart}
|
||||
onImageUploadStop={this.onImageUploadStop}
|
||||
onChange={this.store.updateText}
|
||||
onSave={this.onSave}
|
||||
onCancel={this.onCancel}
|
||||
onStar={this.store.starDocument}
|
||||
onUnstar={this.store.unstarDocument}
|
||||
starred={this.store.document.starred}
|
||||
readOnly={!isEditing}
|
||||
/>
|
||||
</DocumentContainer>}
|
||||
</PagePadding>
|
||||
{this.store.document &&
|
||||
<Meta align="center" readOnly={!isEditing}>
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
@import '~styles/constants.scss';
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
justify-content: center;
|
||||
top: $headerHeight;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
|
@ -4,7 +4,8 @@ import get from 'lodash/get';
|
|||
import invariant from 'invariant';
|
||||
import { client } from 'utils/ApiClient';
|
||||
import emojify from 'utils/emojify';
|
||||
import type { Document, NavigationNode } from 'types';
|
||||
import Document from 'models/Document';
|
||||
import UiStore from 'stores/UiStore';
|
||||
|
||||
type SaveProps = { redirect?: boolean };
|
||||
|
||||
|
@ -24,13 +25,14 @@ const parseHeader = text => {
|
|||
|
||||
type Options = {
|
||||
history: Object,
|
||||
ui: UiStore,
|
||||
};
|
||||
|
||||
class DocumentStore {
|
||||
document: Document;
|
||||
@observable collapsedNodes: string[] = [];
|
||||
@observable documentId = null;
|
||||
@observable collectionId = null;
|
||||
@observable document: Document;
|
||||
@observable parentDocument: Document;
|
||||
@observable hasPendingChanges = false;
|
||||
@observable newDocument: ?boolean;
|
||||
|
@ -42,6 +44,7 @@ class DocumentStore {
|
|||
@observable isUploading: boolean = false;
|
||||
|
||||
history: Object;
|
||||
ui: UiStore;
|
||||
|
||||
/* Computed */
|
||||
|
||||
|
@ -49,29 +52,6 @@ class DocumentStore {
|
|||
return !!this.document && this.document.collection.type === 'atlas';
|
||||
}
|
||||
|
||||
@computed get pathToDocument(): Array<NavigationNode> {
|
||||
let path;
|
||||
const traveler = (nodes, previousPath) => {
|
||||
nodes.forEach(childNode => {
|
||||
const newPath = [...previousPath, childNode];
|
||||
if (childNode.id === this.document.id) {
|
||||
path = previousPath;
|
||||
return;
|
||||
} else {
|
||||
return traveler(childNode.chilren, newPath);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (this.document && this.document.collection.documents) {
|
||||
traveler(this.document.collection.documents, []);
|
||||
invariant(path, 'Path is not available for collection, abort');
|
||||
return path.splice(1);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/* Actions */
|
||||
|
||||
@action starDocument = async () => {
|
||||
|
@ -108,18 +88,15 @@ class DocumentStore {
|
|||
this.isFetching = true;
|
||||
|
||||
try {
|
||||
const res = await client.get(
|
||||
'/documents.info',
|
||||
{
|
||||
id: this.documentId,
|
||||
},
|
||||
{ cache: true }
|
||||
);
|
||||
const res = await client.get('/documents.info', {
|
||||
id: this.documentId,
|
||||
});
|
||||
invariant(res && res.data, 'Data should be available');
|
||||
if (this.newChildDocument) {
|
||||
this.parentDocument = res.data;
|
||||
} else {
|
||||
this.document = res.data;
|
||||
this.document = new Document(res.data);
|
||||
this.ui.setActiveDocument(this.document);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Something went wrong');
|
||||
|
@ -133,20 +110,16 @@ class DocumentStore {
|
|||
this.isSaving = true;
|
||||
|
||||
try {
|
||||
const res = await client.post(
|
||||
'/documents.create',
|
||||
{
|
||||
parentDocument: get(this.parentDocument, 'id'),
|
||||
collection: get(
|
||||
this.parentDocument,
|
||||
'collection.id',
|
||||
this.collectionId
|
||||
),
|
||||
title: get(this.document, 'title', 'Untitled document'),
|
||||
text: get(this.document, 'text'),
|
||||
},
|
||||
{ cache: true }
|
||||
);
|
||||
const res = await client.post('/documents.create', {
|
||||
parentDocument: get(this.parentDocument, 'id'),
|
||||
collection: get(
|
||||
this.parentDocument,
|
||||
'collection.id',
|
||||
this.collectionId
|
||||
),
|
||||
title: get(this.document, 'title', 'Untitled document'),
|
||||
text: get(this.document, 'text'),
|
||||
});
|
||||
invariant(res && res.data, 'Data should be available');
|
||||
const { url } = res.data;
|
||||
|
||||
|
@ -164,15 +137,11 @@ class DocumentStore {
|
|||
this.isSaving = true;
|
||||
|
||||
try {
|
||||
const res = await client.post(
|
||||
'/documents.update',
|
||||
{
|
||||
id: this.documentId,
|
||||
title: get(this.document, 'title', 'Untitled document'),
|
||||
text: get(this.document, 'text'),
|
||||
},
|
||||
{ cache: true }
|
||||
);
|
||||
const res = await client.post('/documents.update', {
|
||||
id: this.documentId,
|
||||
title: get(this.document, 'title', 'Untitled document'),
|
||||
text: get(this.document, 'text'),
|
||||
});
|
||||
invariant(res && res.data, 'Data should be available');
|
||||
const { url } = res.data;
|
||||
|
||||
|
@ -210,6 +179,7 @@ class DocumentStore {
|
|||
|
||||
constructor(options: Options) {
|
||||
this.history = options.history;
|
||||
this.ui = options.ui;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,18 +1,26 @@
|
|||
// @flow
|
||||
import { observable, action } from 'mobx';
|
||||
import { observable, action, computed } from 'mobx';
|
||||
import type { Document } from 'types';
|
||||
import Collection from 'models/Collection';
|
||||
|
||||
class UiStore {
|
||||
@observable activeCollection: ?string;
|
||||
@observable activeDocument: ?Document;
|
||||
@observable editMode: boolean = false;
|
||||
|
||||
/* Computed */
|
||||
|
||||
@computed get activeCollection(): ?Collection {
|
||||
return this.activeDocument ? this.activeDocument.collection : undefined;
|
||||
}
|
||||
|
||||
/* Actions */
|
||||
|
||||
@action setActiveCollection = (id: string): void => {
|
||||
this.activeCollection = id;
|
||||
@action setActiveDocument = (document: Document): void => {
|
||||
this.activeDocument = document;
|
||||
};
|
||||
|
||||
@action clearActiveCollection = (): void => {
|
||||
this.activeCollection = null;
|
||||
@action clearActiveDocument = (): void => {
|
||||
this.activeDocument = undefined;
|
||||
};
|
||||
|
||||
@action enableEditMode() {
|
||||
|
|
|
@ -9,5 +9,6 @@ const stores = {
|
|||
ui: new UiStore(),
|
||||
errors: new ErrorsStore(),
|
||||
};
|
||||
window.stores = stores;
|
||||
|
||||
export default stores;
|
||||
|
|
|
@ -3,7 +3,7 @@ import apiError from '../../errors';
|
|||
import validator from 'validator';
|
||||
|
||||
export default function validation() {
|
||||
return function validationMiddleware(ctx, next) {
|
||||
return function validationMiddleware(ctx: Object, next: Function) {
|
||||
ctx.assertPresent = function assertPresent(value, message) {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
throw apiError(400, 'validation_error', message);
|
||||
|
|
|
@ -16,7 +16,7 @@ async function present(ctx, collection, includeRecentDocuments = false) {
|
|||
};
|
||||
|
||||
if (collection.type === 'atlas')
|
||||
data.navigationTree = collection.navigationTree;
|
||||
data.documents = await collection.getDocumentsStructure();
|
||||
|
||||
if (includeRecentDocuments) {
|
||||
const documents = await Document.findAll({
|
||||
|
|
Reference in New Issue