diff --git a/frontend/models/Document.js b/frontend/models/Document.js index 40fbd91b..e19f7971 100644 --- a/frontend/models/Document.js +++ b/frontend/models/Document.js @@ -9,7 +9,16 @@ import ErrorsStore from 'stores/ErrorsStore'; import type { User } from 'types'; import Collection from './Collection'; +const parseHeader = text => { + const firstLine = text.split(/\r?\n/)[0]; + return firstLine.replace(/^#/, '').trim(); +}; + class Document { + isSaving: boolean; + hasPendingChanges: boolean = false; + errors: ErrorsStore; + collaborators: Array; collection: Collection; createdAt: string; @@ -20,12 +29,11 @@ class Document { starred: boolean; team: string; text: string; - title: string; + title: string = 'Untitled document'; updatedAt: string; updatedBy: User; url: string; views: number; - errors: ErrorsStore; /* Computed */ @@ -54,7 +62,44 @@ class Document { /* Actions */ - @action update = async () => { + @action star = async () => { + this.starred = true; + try { + await client.post('/documents.star', { id: this.id }); + } catch (e) { + this.starred = false; + this.errors.add('Document failed star'); + } + }; + + @action unstar = async () => { + this.starred = false; + try { + await client.post('/documents.unstar', { id: this.id }); + } catch (e) { + this.starred = false; + this.errors.add('Document failed unstar'); + } + }; + + @action view = async () => { + try { + await client.post('/views.create', { id: this.id }); + this.views++; + } catch (e) { + this.errors.add('Document failed to record view'); + } + }; + + @action delete = async () => { + try { + await client.post('/documents.delete', { id: this.id }); + } catch (e) { + this.errors.add('Document failed to delete'); + } + }; + + @action fetch = async () => { try { const res = await client.post('/documents.info', { id: this.id }); invariant(res && res.data, 'Document API response should be available'); @@ -67,7 +112,37 @@ class Document { } }; - updateData(data: Document) { + @action save = async () => { + if (this.isSaving) return; + this.isSaving = true; + + try { + let res; + if (this.id) { + res = await client.post('/documents.update', { + id: this.id, + title: this.title, + text: this.text, + }); + } else { + res = await client.post('/documents.create', { + collection: this.collection.id, + title: this.title, + text: this.text, + }); + } + + invariant(res && res.data, 'Data should be available'); + this.hasPendingChanges = false; + } catch (e) { + this.errors.add('Document failed saving'); + } finally { + this.isSaving = false; + } + }; + + updateData(data: Object | Document) { + data.title = parseHeader(data.text); extendObservable(this, data); } diff --git a/frontend/scenes/Document/Document.js b/frontend/scenes/Document/Document.js index a535432f..636dd7eb 100644 --- a/frontend/scenes/Document/Document.js +++ b/frontend/scenes/Document/Document.js @@ -7,8 +7,7 @@ import { withRouter, Prompt } from 'react-router'; import { Flex } from 'reflexbox'; import UiStore from 'stores/UiStore'; - -import DocumentStore from './DocumentStore'; +import DocumentsStore from 'stores/DocumentsStore'; import Menu from './components/Menu'; import Editor from 'components/Editor'; import { HeaderAction, SaveAction } from 'components/Layout'; @@ -27,77 +26,72 @@ type Props = { match: Object, history: Object, keydown: Object, + documents: DocumentsStore, newChildDocument?: boolean, ui: UiStore, }; @observer class Document extends Component { - store: DocumentStore; props: Props; - constructor(props: Props) { - super(props); - this.store = new DocumentStore({ - history: this.props.history, - ui: props.ui, - }); - } - componentDidMount() { this.loadDocument(this.props); } componentWillReceiveProps(nextProps) { - if (nextProps.match.params.id !== this.props.match.params.id) + 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 (props.match.params.edit) { - this.store.documentId = props.match.params.id; - this.store.fetchDocument(); - } else if (props.newChildDocument) { - this.store.documentId = props.match.params.id; - this.store.newChildDocument = true; - this.store.fetchDocument(); - } else { - this.store.documentId = props.match.params.id; - this.store.newDocument = false; - this.store.fetchDocument(); } - - this.store.viewDocument(); } componentWillUnmount() { this.props.ui.clearActiveDocument(); } - onEdit = () => { - const url = `${this.store.document.url}/edit`; + loadDocument = async props => { + await this.props.documents.fetch(props.match.params.id); + if (this.document) this.document.view(); + + if (this.props.match.params.edit) { + this.props.ui.enableEditMode(); + } else { + this.props.ui.disableEditMode(); + } + }; + + get document() { + return this.props.documents.getByUrl(`/d/${this.props.match.params.id}`); + } + + onClickEdit = () => { + if (!this.document) return; + const url = `${this.document.url}/edit`; this.props.history.push(url); this.props.ui.enableEditMode(); }; - onSave = async (options: { redirect?: boolean } = {}) => { - if (this.store.newDocument || this.store.newChildDocument) { - await this.store.saveDocument(options); - } else { - await this.store.updateDocument(options); - } + onSave = async (redirect: boolean = false) => { + if (!this.document) return; + await this.document.save(); this.props.ui.disableEditMode(); + + if (redirect) { + this.props.history.push(this.document.url); + } }; - onImageUploadStart = () => { - this.store.updateUploading(true); - }; + onImageUploadStart() { + // TODO: How to set loading bar on layout? + } - onImageUploadStop = () => { - this.store.updateUploading(false); - }; + onImageUploadStop() { + // TODO: How to set loading bar on layout? + } + + onChange(text) { + if (!this.document) return; + this.document.updateData({ text }); + } onCancel = () => { this.props.history.goBack(); @@ -106,69 +100,70 @@ type Props = { render() { const isNew = this.props.newDocument || this.props.newChildDocument; const isEditing = this.props.match.params.edit; - const titleText = this.store.document && get(this.store, 'document.title'); + const isFetching = !this.document && get(this.document, 'isFetching'); + const titleText = get(this.document, 'title', 'Loading'); - const actions = ( - - - {isEditing - ? - : Edit} - - - {!isEditing && - } - - ); + console.log('isEditing', isEditing); + console.log('isFetching', isFetching); + console.log('document', this.document); return ( {titleText && } - - - - {this.store.isFetching - ? - - - : this.store.document && - - - } - - {this.store.document && - - {!isEditing && - } - {!isEditing && - } - {actions} - } + {isFetching && + + + } + {!isFetching && + this.document && + + + + + + + {!isEditing && + } + {!isEditing && + } + + + {isEditing + ? + : Edit} + + {!isEditing && } + + + } ); } @@ -201,4 +196,4 @@ const DocumentContainer = styled.div` width: 50em; `; -export default withRouter(inject('ui')(Document)); +export default withRouter(inject('ui', 'documents')(Document)); diff --git a/frontend/scenes/Document/DocumentStore.js b/frontend/scenes/Document/DocumentStore.js deleted file mode 100644 index bcc59500..00000000 --- a/frontend/scenes/Document/DocumentStore.js +++ /dev/null @@ -1,186 +0,0 @@ -// @flow -import { observable, action, computed } from 'mobx'; -import get from 'lodash/get'; -import invariant from 'invariant'; -import { client } from 'utils/ApiClient'; -import emojify from 'utils/emojify'; -import Document from 'models/Document'; -import UiStore from 'stores/UiStore'; - -type SaveProps = { redirect?: boolean }; - -const parseHeader = text => { - const firstLine = text.split(/\r?\n/)[0]; - if (firstLine) { - const match = firstLine.match(/^#+ +(.*)$/); - - if (match) { - return emojify(match[1]); - } else { - return ''; - } - } - return ''; -}; - -type Options = { - history: Object, - ui: UiStore, -}; - -class DocumentStore { - document: Document; - @observable collapsedNodes: string[] = []; - @observable documentId = null; - @observable collectionId = null; - @observable parentDocument: Document; - @observable hasPendingChanges = false; - @observable newDocument: ?boolean; - @observable newChildDocument: ?boolean; - - @observable isEditing: boolean = false; - @observable isFetching: boolean = false; - @observable isSaving: boolean = false; - @observable isUploading: boolean = false; - - history: Object; - ui: UiStore; - - /* Computed */ - - @computed get isCollection(): boolean { - return !!this.document && this.document.collection.type === 'atlas'; - } - - /* Actions */ - - @action starDocument = async () => { - this.document.starred = true; - try { - await client.post('/documents.star', { - id: this.documentId, - }); - } catch (e) { - this.document.starred = false; - console.error('Something went wrong'); - } - }; - - @action unstarDocument = async () => { - this.document.starred = false; - try { - await client.post('/documents.unstar', { - id: this.documentId, - }); - } catch (e) { - this.document.starred = true; - console.error('Something went wrong'); - } - }; - - @action viewDocument = async () => { - await client.post('/views.create', { - id: this.documentId, - }); - }; - - @action fetchDocument = async () => { - this.isFetching = true; - - try { - 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 = new Document(res.data); - this.ui.setActiveDocument(this.document); - } - } catch (e) { - console.error('Something went wrong'); - } - this.isFetching = false; - }; - - @action saveDocument = async ({ redirect = true }: SaveProps) => { - if (this.isSaving) return; - - 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'), - }); - invariant(res && res.data, 'Data should be available'); - const { url } = res.data; - - this.hasPendingChanges = false; - if (redirect) this.history.push(url); - } catch (e) { - console.error('Something went wrong'); - } - this.isSaving = false; - }; - - @action updateDocument = async ({ redirect = true }: SaveProps) => { - if (this.isSaving) return; - - 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'), - }); - invariant(res && res.data, 'Data should be available'); - const { url } = res.data; - - this.hasPendingChanges = false; - if (redirect) this.history.push(url); - } catch (e) { - console.error('Something went wrong'); - } - this.isSaving = false; - }; - - @action deleteDocument = async () => { - this.isFetching = true; - - try { - await client.post('/documents.delete', { id: this.documentId }); - this.history.push(this.document.collection.url); - } catch (e) { - console.error('Something went wrong'); - } - this.isFetching = false; - }; - - @action updateText = (text: string) => { - if (!this.document) return; - - this.document.text = text; - this.document.title = parseHeader(text); - this.hasPendingChanges = true; - }; - - @action updateUploading = (uploading: boolean) => { - this.isUploading = uploading; - }; - - constructor(options: Options) { - this.history = options.history; - this.ui = options.ui; - } -} - -export default DocumentStore; diff --git a/frontend/stores/DocumentsStore.js b/frontend/stores/DocumentsStore.js index d72ffa5d..c33195a3 100644 --- a/frontend/stores/DocumentsStore.js +++ b/frontend/stores/DocumentsStore.js @@ -81,6 +81,10 @@ class DocumentsStore { return this.data.get(id); }; + getByUrl = (url: string): ?Document => { + return _.find(this.data.values(), { url }); + }; + constructor() { this.errors = stores.errors; }