From 9808c1cdf5d04f7b3c792e20beacf64ba14d71c1 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Mon, 11 Sep 2017 23:18:44 -0700 Subject: [PATCH 01/17] Bring back extract text webpack plugin --- package.json | 1 + webpack.config.dev.js | 2 ++ webpack.config.js | 3 ++- webpack.config.prod.js | 3 +++ yarn.lock | 21 ++++++++++++++++++--- 5 files changed, 26 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 7fc4d76e..bd9d3faa 100644 --- a/package.json +++ b/package.json @@ -96,6 +96,7 @@ "eslint-plugin-prettier": "^2.0.1", "eslint-plugin-react": "^6.10.3", "exports-loader": "0.6.3", + "extract-text-webpack-plugin": "1.0.1", "fbemitter": "^2.1.1", "file-loader": "0.9.0", "flow-typed": "^2.1.2", diff --git a/webpack.config.dev.js b/webpack.config.dev.js index 2a569fed..a09773b9 100644 --- a/webpack.config.dev.js +++ b/webpack.config.dev.js @@ -1,6 +1,7 @@ /* eslint-disable */ var webpack = require('webpack'); var HtmlWebpackPlugin = require('html-webpack-plugin'); +var ExtractTextPlugin = require('extract-text-webpack-plugin'); const commonWebpackConfig = require('./webpack.config'); @@ -18,6 +19,7 @@ const developmentWebpackConfig = Object.assign(commonWebpackConfig, { developmentWebpackConfig.plugins.push( new webpack.optimize.OccurenceOrderPlugin() ); +developmentWebpackConfig.plugins.push(new ExtractTextPlugin('styles.css')); developmentWebpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin()); developmentWebpackConfig.plugins.push(new webpack.NoErrorsPlugin()); developmentWebpackConfig.plugins.push( diff --git a/webpack.config.js b/webpack.config.js index 1be0fdfd..f99b890c 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,6 +1,7 @@ /* eslint-disable */ const path = require('path'); const webpack = require('webpack'); +var ExtractTextPlugin = require('extract-text-webpack-plugin'); require('dotenv').config({ silent: true }); @@ -40,7 +41,7 @@ module.exports = { }, { test: /\.css$/, - loader: 'style-loader!css-loader?sourceMap', + loader: ExtractTextPlugin.extract('style-loader', 'css-loader'), }, { test: /\.md/, loader: 'raw-loader' }, ], diff --git a/webpack.config.prod.js b/webpack.config.prod.js index 5f593eb5..0dc51e6c 100644 --- a/webpack.config.prod.js +++ b/webpack.config.prod.js @@ -20,6 +20,9 @@ productionWebpackConfig.plugins.push( template: 'server/static/index.html', }) ); +productionWebpackConfig.plugins.push( + new ExtractTextPlugin('styles.[hash].css') +); productionWebpackConfig.plugins.push( new webpack.optimize.OccurenceOrderPlugin() ); diff --git a/yarn.lock b/yarn.lock index f446a475..3b83cf3b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -306,7 +306,7 @@ async@^0.9.0: version "0.9.2" resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" -async@^1.3.0, async@^1.4.0: +async@^1.3.0, async@^1.4.0, async@^1.5.0: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" @@ -3035,6 +3035,14 @@ extglob@^0.3.1: dependencies: is-extglob "^1.0.0" +extract-text-webpack-plugin@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/extract-text-webpack-plugin/-/extract-text-webpack-plugin-1.0.1.tgz#c95bf3cbaac49dc96f1dc6e072549fbb654ccd2c" + dependencies: + async "^1.5.0" + loader-utils "^0.2.3" + webpack-sources "^0.1.0" + extsprintf@1.3.0, extsprintf@^1.2.0: version "1.3.0" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" @@ -5298,7 +5306,7 @@ load-json-file@^2.0.0: pify "^2.0.0" strip-bom "^3.0.0" -loader-utils@0.2.x, loader-utils@^0.2.11, loader-utils@^0.2.14, loader-utils@~0.2.5: +loader-utils@0.2.x, loader-utils@^0.2.11, loader-utils@^0.2.14, loader-utils@^0.2.3, loader-utils@~0.2.5: version "0.2.17" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.17.tgz#f86e6374d43205a6e6c60e9196f17c0299bfb348" dependencies: @@ -8190,7 +8198,7 @@ source-map@0.5.6: version "0.5.6" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" -source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.0, source-map@~0.5.1: +source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.0, source-map@~0.5.1, source-map@~0.5.3: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" @@ -9100,6 +9108,13 @@ webpack-hot-middleware@2.x: querystring "^0.2.0" strip-ansi "^3.0.0" +webpack-sources@^0.1.0: + version "0.1.5" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-0.1.5.tgz#aa1f3abf0f0d74db7111c40e500b84f966640750" + dependencies: + source-list-map "~0.1.7" + source-map "~0.5.3" + webpack@1.13.2: version "1.13.2" resolved "https://registry.yarnpkg.com/webpack/-/webpack-1.13.2.tgz#f11a96f458eb752970a86abe746c0704fabafaf3" From f3cb17046d38a4bd0d9402b63e2fc37dcd358300 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Mon, 11 Sep 2017 23:32:26 -0700 Subject: [PATCH 02/17] Fix to webpack plugin --- webpack.config.prod.js | 1 + 1 file changed, 1 insertion(+) diff --git a/webpack.config.prod.js b/webpack.config.prod.js index 0dc51e6c..f3e0b0df 100644 --- a/webpack.config.prod.js +++ b/webpack.config.prod.js @@ -2,6 +2,7 @@ var path = require('path'); var webpack = require('webpack'); var HtmlWebpackPlugin = require('html-webpack-plugin'); +var ExtractTextPlugin = require('extract-text-webpack-plugin'); commonWebpackConfig = require('./webpack.config'); From 0071e09d7eb61b83843820f505d7890e15d20607 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Mon, 11 Sep 2017 23:59:35 -0700 Subject: [PATCH 03/17] refactored document setState to mobx & cancel reload --- frontend/scenes/Document/Document.js | 65 +++++++++++++--------------- 1 file changed, 31 insertions(+), 34 deletions(-) diff --git a/frontend/scenes/Document/Document.js b/frontend/scenes/Document/Document.js index 4bc09784..e55a15e0 100644 --- a/frontend/scenes/Document/Document.js +++ b/frontend/scenes/Document/Document.js @@ -2,6 +2,7 @@ import React, { Component } from 'react'; import get from 'lodash/get'; import styled from 'styled-components'; +import { observable } from 'mobx'; import { observer, inject } from 'mobx-react'; import { withRouter, Prompt } from 'react-router'; import Flex from 'components/Flex'; @@ -39,17 +40,13 @@ type Props = { @observer class DocumentScene extends Component { props: Props; savedTimeout: number; - state: { - newDocument?: Document, - }; - state = { - isDragging: false, - isLoading: false, - isSaving: false, - newDocument: undefined, - showAsSaved: false, - notFound: false, - }; + + @observable newDocument: ?Document; + @observable isDragging = false; + @observable isLoading = false; + @observable isSaving = false; + @observable showAsSaved = false; + @observable notFound = false; componentDidMount() { this.loadDocument(this.props); @@ -60,7 +57,7 @@ type Props = { nextProps.match.params.documentSlug !== this.props.match.params.documentSlug ) { - this.setState({ notFound: false }); + this.notFound = false; this.loadDocument(nextProps); } } @@ -77,7 +74,7 @@ type Props = { title: '', text: '', }); - this.setState({ newDocument }); + this.newDocument = newDocument; } else { let document = this.document; if (document) { @@ -92,13 +89,13 @@ type Props = { document.view(); } else { // Render 404 with search - this.setState({ notFound: true }); + this.notFound = true; } } }; get document() { - if (this.state.newDocument) return this.state.newDocument; + if (this.newDocument) return this.newDocument; return this.props.documents.getByUrl( `/doc/${this.props.match.params.documentSlug}` ); @@ -120,31 +117,30 @@ type Props = { let document = this.document; if (!document) return; - this.setState({ isLoading: true, isSaving: true }); + this.isLoading = true; + this.isSaving = true; document = await document.save(); - this.setState({ isLoading: false }); + this.isLoading = false; if (redirect || this.props.newDocument) { this.props.history.push(document.url); } else { - this.showAsSaved(); + this.toggleShowAsSaved(); } }; - showAsSaved() { - this.setState({ showAsSaved: true, isSaving: false }); - this.savedTimeout = setTimeout( - () => this.setState({ showAsSaved: false }), - 2000 - ); + toggleShowAsSaved() { + this.showAsSaved = true; + this.isSaving = false; + this.savedTimeout = setTimeout(() => (this.showAsSaved = false), 2000); } onImageUploadStart = () => { - this.setState({ isLoading: true }); + this.isLoading = true; }; onImageUploadStop = () => { - this.setState({ isLoading: false }); + this.isLoading = false; }; onChange = text => { @@ -152,10 +148,11 @@ type Props = { this.document.updateData({ text }, true); }; - onCancel = () => { + onCancel = async () => { let url; if (this.document && this.document.url) { url = this.document.url; + await this.document.fetch(); } else { url = collectionUrl(this.props.match.params.id); } @@ -163,11 +160,11 @@ type Props = { }; onStartDragging = () => { - this.setState({ isDragging: true }); + this.isDragging = true; }; onStopDragging = () => { - this.setState({ isDragging: false }); + this.isDragging = false; }; renderNotFound() { @@ -181,18 +178,18 @@ type Props = { const titleText = get(this.document, 'title', ''); const document = this.document; - if (this.state.notFound) { + if (this.notFound) { return this.renderNotFound(); } return ( - {this.state.isDragging && + {this.isDragging && Drop files here to import into Atlas. } {titleText && } - {this.state.isLoading && } + {this.isLoading && } {isFetching && @@ -232,11 +229,11 @@ type Props = { {isEditing ? From 917c5c4923ee52d478caa31efddd1b7b06a96968 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Tue, 12 Sep 2017 00:10:02 -0700 Subject: [PATCH 04/17] Return updated collection on document create --- server/api/documents.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/api/documents.js b/server/api/documents.js index 178e590a..3eb5efb8 100644 --- a/server/api/documents.js +++ b/server/api/documents.js @@ -211,6 +211,8 @@ router.post('documents.create', auth(), async ctx => { await ownerCollection.addDocumentToStructure(document, index); } + document.collection = ownerCollection; + ctx.body = { data: await presentDocument(ctx, document), }; From ce5b6e6ac86bb654a8c9ebf4504ea717e0d947a5 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Tue, 12 Sep 2017 00:11:53 -0700 Subject: [PATCH 05/17] Return updated collection on document move --- server/api/documents.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/api/documents.js b/server/api/documents.js index 3eb5efb8..a179f983 100644 --- a/server/api/documents.js +++ b/server/api/documents.js @@ -282,6 +282,8 @@ router.post('documents.move', auth(), async ctx => { await collection.addDocumentToStructure(document, index); } + document.collection = collection; + ctx.body = { data: await presentDocument(ctx, document), }; From 406e94372f5f3723e894aae311f68a2304b16b93 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Tue, 12 Sep 2017 07:59:01 -0700 Subject: [PATCH 06/17] Fixes no formatting toolbar on selecting heading --- .../components/Editor/components/Heading.js | 56 ++++++++++--------- frontend/components/Editor/schema.js | 21 ++++--- 2 files changed, 43 insertions(+), 34 deletions(-) diff --git a/frontend/components/Editor/components/Heading.js b/frontend/components/Editor/components/Heading.js index 9aa1431f..3a7aaa10 100644 --- a/frontend/components/Editor/components/Heading.js +++ b/frontend/components/Editor/components/Heading.js @@ -17,32 +17,6 @@ type Props = { component?: string, }; -const Wrapper = styled.div` - display: inline; - margin-left: ${props => (props.hasEmoji ? '-1.2em' : 0)} -`; - -const Anchor = styled.a` - visibility: hidden; - padding-left: .25em; - color: #dedede; - - &:hover { - color: #cdcdcd; - } -`; - -// $FlowIssue I don't know -const titleStyles = component => styled(component)` - position: relative; - - &:hover { - ${Anchor} { - visibility: visible; - } - } -`; - function Heading(props: Props) { const { parent, @@ -58,7 +32,7 @@ function Heading(props: Props) { const showPlaceholder = placeholder && firstHeading && !node.text; const slugish = _.escape(`${component}-${slug(node.text)}`); const showHash = readOnly && !!slugish; - const Component = titleStyles(component); + const Component = component; const emoji = editor.props.emoji || ''; const title = node.text.trim(); const startsWithEmojiAndSpace = @@ -76,4 +50,32 @@ function Heading(props: Props) { ); } +const Wrapper = styled.div` + display: inline; + margin-left: ${props => (props.hasEmoji ? '-1.2em' : 0)} +`; + +const Anchor = styled.a` + visibility: hidden; + padding-left: .25em; + color: #dedede; + + &:hover { + color: #cdcdcd; + } +`; + +export const Heading1 = styled(Heading)` + &:hover { + ${Anchor} { + visibility: visible; + } + } +`; +export const Heading2 = Heading1.withComponent('h2'); +export const Heading3 = Heading1.withComponent('h3'); +export const Heading4 = Heading1.withComponent('h4'); +export const Heading5 = Heading1.withComponent('h5'); +export const Heading6 = Heading1.withComponent('h6'); + export default Heading; diff --git a/frontend/components/Editor/schema.js b/frontend/components/Editor/schema.js index e90f2e84..6c95d36a 100644 --- a/frontend/components/Editor/schema.js +++ b/frontend/components/Editor/schema.js @@ -6,7 +6,14 @@ import InlineCode from './components/InlineCode'; import Image from './components/Image'; import Link from './components/Link'; import ListItem from './components/ListItem'; -import Heading from './components/Heading'; +import { + Heading1, + Heading2, + Heading3, + Heading4, + Heading5, + Heading6, +} from './components/Heading'; import Paragraph from './components/Paragraph'; import type { Props, Node, Transform } from './types'; @@ -47,12 +54,12 @@ const createSchema = () => { image: Image, link: Link, 'list-item': ListItem, - heading1: (props: Props) => , - heading2: (props: Props) => , - heading3: (props: Props) => , - heading4: (props: Props) => , - heading5: (props: Props) => , - heading6: (props: Props) => , + heading1: (props: Props) => , + heading2: (props: Props) => , + heading3: (props: Props) => , + heading4: (props: Props) => , + heading5: (props: Props) => , + heading6: (props: Props) => , }, rules: [ From e40d9cebdaf95d9b2d9c3093efeb108d91db4bf6 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Tue, 12 Sep 2017 19:54:22 -0700 Subject: [PATCH 07/17] Trigger collection update when document gets updated (renamed) --- frontend/models/Collection.js | 10 +++++++++- frontend/models/Document.js | 5 +++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/frontend/models/Collection.js b/frontend/models/Collection.js index 13d8e5e8..80d0ba39 100644 --- a/frontend/models/Collection.js +++ b/frontend/models/Collection.js @@ -93,7 +93,7 @@ class Collection extends BaseModel { } }; - updateData(data: Object = {}) { + @action updateData(data: Object = {}) { this.data = data; extendObservable(this, data); } @@ -107,6 +107,14 @@ class Collection extends BaseModel { this.on('documents.delete', (data: { collectionId: string }) => { if (data.collectionId === this.id) this.fetch(); }); + this.on( + 'collections.update', + (data: { id: string, collection: Collection }) => { + // FIXME: calling this.updateData won't update the + // UI. Some mobx issue + if (data.id === this.id) this.fetch(); + } + ); } } diff --git a/frontend/models/Document.js b/frontend/models/Document.js index ea39c2f6..eade3779 100644 --- a/frontend/models/Document.js +++ b/frontend/models/Document.js @@ -160,6 +160,11 @@ class Document extends BaseModel { this.updateData(res.data); this.hasPendingChanges = false; }); + + this.emit('collections.update', { + id: this.collection.id, + collection: this.collection, + }); } catch (e) { this.errors.add('Document failed saving'); } finally { From 5c43e1221837d3b11c88a15b01d5a7cd7ea6f700 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Tue, 12 Sep 2017 19:55:01 -0700 Subject: [PATCH 08/17] Moved document matching to use urlId to prevent issues with renamed documents --- frontend/scenes/Document/Document.js | 4 ++++ frontend/stores/DocumentsStore.js | 5 ++++- server/presenters/document.js | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/frontend/scenes/Document/Document.js b/frontend/scenes/Document/Document.js index 4bc09784..b7f80090 100644 --- a/frontend/scenes/Document/Document.js +++ b/frontend/scenes/Document/Document.js @@ -90,6 +90,10 @@ type Props = { if (document) { this.props.ui.setActiveDocument(document); document.view(); + + // Update url to match the current one + const urlParts = this.props.match.url.split('/'); + this.props.history.replace([document.url, urlParts.slice(3)].join('/')); } else { // Render 404 with search this.setState({ notFound: true }); diff --git a/frontend/stores/DocumentsStore.js b/frontend/stores/DocumentsStore.js index 7ccedcad..cfbb70f9 100644 --- a/frontend/stores/DocumentsStore.js +++ b/frontend/stores/DocumentsStore.js @@ -138,8 +138,11 @@ class DocumentsStore extends BaseStore { return this.data.get(id); }; + /** + * Match documents by the url ID as the title slug can change + */ getByUrl = (url: string): ?Document => { - return _.find(this.data.values(), { url }); + return _.find(this.data.values(), doc => url.endsWith(doc.urlId)); }; constructor(options: Options) { diff --git a/server/presenters/document.js b/server/presenters/document.js index 0ca0ea18..5927a83d 100644 --- a/server/presenters/document.js +++ b/server/presenters/document.js @@ -17,6 +17,7 @@ async function present(ctx: Object, document: Document, options: ?Options) { const data = { id: document.id, url: document.getUrl(), + urlId: document.urlId, private: document.private, title: document.title, text: document.text, From 76afaacc6eddc944268399caca0b814836b065bf Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Tue, 12 Sep 2017 20:14:14 -0700 Subject: [PATCH 09/17] Moved url updating to a helper function --- frontend/scenes/Document/Document.js | 7 ++++--- frontend/utils/routeHelpers.js | 10 ++++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/frontend/scenes/Document/Document.js b/frontend/scenes/Document/Document.js index b7f80090..b64c0023 100644 --- a/frontend/scenes/Document/Document.js +++ b/frontend/scenes/Document/Document.js @@ -6,7 +6,7 @@ import { observer, inject } from 'mobx-react'; import { withRouter, Prompt } from 'react-router'; import Flex from 'components/Flex'; import { color, layout } from 'styles/constants'; -import { collectionUrl } from 'utils/routeHelpers'; +import { collectionUrl, updateDocumentUrl } from 'utils/routeHelpers'; import Document from 'models/Document'; import UiStore from 'stores/UiStore'; @@ -92,8 +92,9 @@ type Props = { document.view(); // Update url to match the current one - const urlParts = this.props.match.url.split('/'); - this.props.history.replace([document.url, urlParts.slice(3)].join('/')); + this.props.history.replace( + updateDocumentUrl(this.props.match.url, document.url) + ); } else { // Render 404 with search this.setState({ notFound: true }); diff --git a/frontend/utils/routeHelpers.js b/frontend/utils/routeHelpers.js index ce11b20f..2a273bd5 100644 --- a/frontend/utils/routeHelpers.js +++ b/frontend/utils/routeHelpers.js @@ -38,3 +38,13 @@ export function searchUrl(query?: string): string { export function notFoundUrl(): string { return '/404'; } + +/** + * Replace full url's document part with the new one in case + * the document slug has been updated + */ +export function updateDocumentUrl(oldUrl: string, newUrl: string): string { + // Update url to match the current one + const urlParts = oldUrl.split('/'); + return [newUrl, urlParts.slice(3)].join('/'); +} From e6f99bc3fb87f5dcf7d6298982080b3d4ca3719e Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Tue, 12 Sep 2017 20:48:33 -0700 Subject: [PATCH 10/17] Added edit cache for canceling edit changes --- frontend/scenes/Document/Document.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/scenes/Document/Document.js b/frontend/scenes/Document/Document.js index e55a15e0..fea42c42 100644 --- a/frontend/scenes/Document/Document.js +++ b/frontend/scenes/Document/Document.js @@ -41,6 +41,7 @@ type Props = { props: Props; savedTimeout: number; + @observable editCache: ?string; @observable newDocument: ?Document; @observable isDragging = false; @observable isLoading = false; @@ -86,6 +87,8 @@ type Props = { if (document) { this.props.ui.setActiveDocument(document); + // Cache data if user enters edit mode and cancels + this.editCache = document.text; document.view(); } else { // Render 404 with search @@ -104,6 +107,7 @@ type Props = { onClickEdit = () => { if (!this.document) return; const url = `${this.document.url}/edit`; + this.editCache = document.text; this.props.history.push(url); }; @@ -152,7 +156,7 @@ type Props = { let url; if (this.document && this.document.url) { url = this.document.url; - await this.document.fetch(); + if (this.editCache) this.document.updateData({ text: this.editCache }); } else { url = collectionUrl(this.props.match.params.id); } From c194667b6f019662ed3abfa37bb4a3fe9126001f Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Tue, 12 Sep 2017 20:52:17 -0700 Subject: [PATCH 11/17] removed useless async --- frontend/scenes/Document/Document.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/scenes/Document/Document.js b/frontend/scenes/Document/Document.js index fea42c42..6b646291 100644 --- a/frontend/scenes/Document/Document.js +++ b/frontend/scenes/Document/Document.js @@ -152,7 +152,7 @@ type Props = { this.document.updateData({ text }, true); }; - onCancel = async () => { + onCancel = () => { let url; if (this.document && this.document.url) { url = this.document.url; From 688d84392b81c1b48dce832c8451c231575854a4 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Tue, 12 Sep 2017 20:55:05 -0700 Subject: [PATCH 12/17] removed useless editCache set --- frontend/scenes/Document/Document.js | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/scenes/Document/Document.js b/frontend/scenes/Document/Document.js index 6b646291..b8dbff7b 100644 --- a/frontend/scenes/Document/Document.js +++ b/frontend/scenes/Document/Document.js @@ -107,7 +107,6 @@ type Props = { onClickEdit = () => { if (!this.document) return; const url = `${this.document.url}/edit`; - this.editCache = document.text; this.props.history.push(url); }; From 70d352e193da4bd7824342b76ceee9813e2cb046 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Mon, 4 Sep 2017 14:38:53 -0700 Subject: [PATCH 13/17] ChevronIcon --- frontend/components/Icon/ChevronIcon.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 frontend/components/Icon/ChevronIcon.js diff --git a/frontend/components/Icon/ChevronIcon.js b/frontend/components/Icon/ChevronIcon.js new file mode 100644 index 00000000..88453bbc --- /dev/null +++ b/frontend/components/Icon/ChevronIcon.js @@ -0,0 +1,21 @@ +// @flow +import React from 'react'; +import Icon from './Icon'; +import type { Props } from './Icon'; + +export default function NextIcon(props: Props) { + return ( + + + + + + + ); +} From 483bf29cc46443adba3c6d1739ed507da06b74da Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Mon, 4 Sep 2017 14:48:56 -0700 Subject: [PATCH 14/17] Workable document moving --- frontend/components/Labeled/Labeled.js | 29 ++ frontend/components/Labeled/index.js | 3 + frontend/components/Modal/Modal.js | 3 +- frontend/index.js | 11 +- frontend/models/Collection.js | 3 + frontend/models/Document.js | 27 +- frontend/scenes/Document/Document.js | 17 +- .../components/DocumentMove/DocumentMove.js | 252 ++++++++++++++++++ .../Document/components/DocumentMove/index.js | 3 + frontend/scenes/Document/components/Menu.js | 5 + frontend/scenes/Search/Search.js | 82 ++++-- frontend/scenes/Search/SearchStore.js | 37 --- frontend/utils/routeHelpers.js | 6 + server/api/documents.js | 2 + 14 files changed, 414 insertions(+), 66 deletions(-) create mode 100644 frontend/components/Labeled/Labeled.js create mode 100644 frontend/components/Labeled/index.js create mode 100644 frontend/scenes/Document/components/DocumentMove/DocumentMove.js create mode 100644 frontend/scenes/Document/components/DocumentMove/index.js delete mode 100644 frontend/scenes/Search/SearchStore.js diff --git a/frontend/components/Labeled/Labeled.js b/frontend/components/Labeled/Labeled.js new file mode 100644 index 00000000..bfff39c9 --- /dev/null +++ b/frontend/components/Labeled/Labeled.js @@ -0,0 +1,29 @@ +// @flow +import React from 'react'; +import { observer } from 'mobx-react'; +import Flex from 'components/Flex'; +import styled from 'styled-components'; +import { size } from 'styles/constants'; + +type Props = { + label: React.Element<*> | string, + children: React.Element<*>, +}; + +const Labeled = ({ label, children, ...props }: Props) => ( + +
{label}
+ {children} +
+); + +const Header = styled(Flex)` + margin-bottom: ${size.medium}; + font-size: 13px; + font-weight: 500; + text-transform: uppercase; + color: #9FA6AB; + letter-spacing: 0.04em; +`; + +export default observer(Labeled); diff --git a/frontend/components/Labeled/index.js b/frontend/components/Labeled/index.js new file mode 100644 index 00000000..544f6a7b --- /dev/null +++ b/frontend/components/Labeled/index.js @@ -0,0 +1,3 @@ +// @flow +import Labeled from './Labeled'; +export default Labeled; diff --git a/frontend/components/Modal/Modal.js b/frontend/components/Modal/Modal.js index cde11308..d0b208e9 100644 --- a/frontend/components/Modal/Modal.js +++ b/frontend/components/Modal/Modal.js @@ -1,5 +1,6 @@ // @flow import React from 'react'; +import { observer } from 'mobx-react'; import styled from 'styled-components'; import ReactModal from 'react-modal'; import { color } from 'styles/constants'; @@ -75,4 +76,4 @@ const Close = styled.a` } `; -export default Modal; +export default observer(Modal); diff --git a/frontend/index.js b/frontend/index.js index 9754daaa..e6784ea7 100644 --- a/frontend/index.js +++ b/frontend/index.js @@ -39,6 +39,8 @@ import RouteSidebarHidden from 'components/RouteSidebarHidden'; import flatpages from 'static/flatpages'; +import { matchDocumentSlug } from 'utils/routeHelpers'; + let DevTools; if (__DEV__) { DevTools = require('mobx-react-devtools').default; // eslint-disable-line global-require @@ -93,8 +95,6 @@ const RedirectDocument = ({ match }: { match: Object }) => ( ); -const matchDocumentSlug = ':documentSlug([0-9a-zA-Z-]*-[a-zA-z0-9]{10,15})'; - render(
@@ -123,6 +123,11 @@ render( path={`/doc/${matchDocumentSlug}`} component={Document} /> + @@ -132,7 +137,7 @@ render( { + if (data.collectionId === this.id) this.fetch(); + }); } } diff --git a/frontend/models/Document.js b/frontend/models/Document.js index eade3779..39cfdb0a 100644 --- a/frontend/models/Document.js +++ b/frontend/models/Document.js @@ -47,11 +47,17 @@ class Document extends BaseModel { return !!this.lastViewedAt && this.lastViewedAt < this.updatedAt; } - @computed get pathToDocument(): Array { + @computed get pathToDocument(): Array<{ id: string, title: string }> { let path; const traveler = (nodes, previousPath) => { nodes.forEach(childNode => { - const newPath = [...previousPath, childNode.id]; + const newPath = [ + ...previousPath, + { + id: childNode.id, + title: childNode.title, + }, + ]; if (childNode.id === this.id) { path = newPath; return; @@ -174,6 +180,23 @@ class Document extends BaseModel { return this; }; + @action move = async (parentDocumentId: ?string) => { + try { + const res = await client.post('/documents.move', { + id: this.id, + parentDocument: parentDocumentId, + }); + this.updateData(res.data); + this.emit('documents.move', { + id: this.id, + collectionId: this.collection.id, + }); + } catch (e) { + this.errors.add('Error while moving the document'); + } + return; + }; + @action delete = async () => { try { await client.post('/documents.delete', { id: this.id }); diff --git a/frontend/scenes/Document/Document.js b/frontend/scenes/Document/Document.js index b64c0023..78a015c6 100644 --- a/frontend/scenes/Document/Document.js +++ b/frontend/scenes/Document/Document.js @@ -2,13 +2,16 @@ import React, { Component } from 'react'; import get from 'lodash/get'; import styled from 'styled-components'; +import { observable } from 'mobx'; import { observer, inject } from 'mobx-react'; -import { withRouter, Prompt } from 'react-router'; +import { withRouter, Prompt, Route } from 'react-router'; import Flex from 'components/Flex'; import { color, layout } from 'styles/constants'; import { collectionUrl, updateDocumentUrl } from 'utils/routeHelpers'; import Document from 'models/Document'; +import Modal from 'components/Modal'; +import DocumentMove from './components/DocumentMove'; import UiStore from 'stores/UiStore'; import DocumentsStore from 'stores/DocumentsStore'; import Menu from './components/Menu'; @@ -22,6 +25,8 @@ import CenteredContent from 'components/CenteredContent'; import PageTitle from 'components/PageTitle'; import Search from 'scenes/Search'; +import { matchDocumentEdit, matchDocumentMove } from 'utils/routeHelpers'; + const DISCARD_CHANGES = ` You have unsaved changes. Are you sure you want to discard them? @@ -51,6 +56,8 @@ type Props = { notFound: false, }; + @observable moveModalOpen: boolean = false; + componentDidMount() { this.loadDocument(this.props); } @@ -120,6 +127,9 @@ type Props = { this.props.history.push(`${this.document.collection.url}/new`); }; + handleCloseMoveModal = () => (this.moveModalOpen = false); + handleOpenMoveModal = () => (this.moveModalOpen = true); + onSave = async (redirect: boolean = false) => { if (this.document && !this.document.allowSave) return; let document = this.document; @@ -181,7 +191,8 @@ type Props = { render() { const isNew = this.props.newDocument; - const isEditing = !!this.props.match.params.edit || isNew; + const isMoving = this.props.match.path === matchDocumentMove; + const isEditing = this.props.match.path === matchDocumentEdit || isNew; const isFetching = !this.document; const titleText = get(this.document, 'title', ''); const document = this.document; @@ -192,6 +203,8 @@ type Props = { return ( + {isMoving && document && } + {this.state.isDragging && Drop files here to import into Atlas. diff --git a/frontend/scenes/Document/components/DocumentMove/DocumentMove.js b/frontend/scenes/Document/components/DocumentMove/DocumentMove.js new file mode 100644 index 00000000..9999ab4c --- /dev/null +++ b/frontend/scenes/Document/components/DocumentMove/DocumentMove.js @@ -0,0 +1,252 @@ +// @flow +import React, { Component } from 'react'; +import ReactDOM from 'react-dom'; +import { observable, runInAction, action } from 'mobx'; +import { observer, inject } from 'mobx-react'; +import { withRouter } from 'react-router'; +import ArrowKeyNavigation from 'boundless-arrow-key-navigation'; +import _ from 'lodash'; +import invariant from 'invariant'; +import { client } from 'utils/ApiClient'; +import styled from 'styled-components'; +import { size, color } from 'styles/constants'; + +import Modal from 'components/Modal'; +import Button from 'components/Button'; +import Input from 'components/Input'; +import HelpText from 'components/HelpText'; +import Labeled from 'components/Labeled'; +import Flex from 'components/Flex'; +import ChevronIcon from 'components/Icon/ChevronIcon'; + +import Document from 'models/Document'; +import DocumentsStore from 'stores/DocumentsStore'; + +type Props = { + match: Object, + history: Object, + document: Document, + documents: DocumentsStore, +}; + +@observer class DocumentMove extends Component { + props: Props; + store: DocumentMoveStore; + firstDocument: HTMLElement; + + @observable isSaving: boolean; + @observable resultIds: Array = []; // Document IDs + @observable searchTerm: ?string = null; + @observable isFetching = false; + + handleKeyDown = ev => { + // Down + if (ev.which === 40) { + ev.preventDefault(); + if (this.firstDocument) { + const element = ReactDOM.findDOMNode(this.firstDocument); + // $FlowFixMe + if (element && element.focus) element.focus(); + } + } + }; + + handleClose = () => { + this.props.history.push(this.props.document.url); + }; + + handleFilter = (e: SyntheticEvent) => { + const value = e.target.value; + this.searchTerm = value; + this.updateSearchResults(); + }; + + updateSearchResults = _.debounce(() => { + this.search(); + }, 250); + + setFirstDocumentRef = ref => { + this.firstDocument = ref; + }; + + @action search = async () => { + this.isFetching = true; + + if (this.searchTerm) { + try { + const res = await client.get('/documents.search', { + query: this.searchTerm, + }); + invariant(res && res.data, 'res or res.data missing'); + const { data } = res; + runInAction('search document', () => { + // Fill documents store + data.forEach(documentData => + this.props.documents.add(new Document(documentData)) + ); + this.resultIds = data.map(documentData => documentData.id); + }); + } catch (e) { + console.error('Something went wrong'); + } + } else { + this.resultIds = []; + } + + this.isFetching = false; + }; + + render() { + const { document, documents } = this.props; + + return ( + + +
+ + + +
+ +
+ + + + + + this.setFirstDocumentRef(ref)} + onSuccess={this.handleClose} + /> + {this.resultIds.map((documentId, index) => ( + + ))} + + +
+ + {false && + } +
+ ); + } +} + +const Section = styled(Flex)` + margin-bottom: ${size.huge}; +`; + +const StyledArrowKeyNavigation = styled(ArrowKeyNavigation)` + display: flex; + flex-direction: column; + flex: 1; +`; + +type PathToDocumentProps = { + documentId: string, + onSuccess?: Function, + documents: DocumentsStore, + document?: Document, + ref?: Function, + selectable?: boolean, +}; + +class PathToDocument extends React.Component { + props: PathToDocumentProps; + + get resultDocument(): ?Document { + return this.props.documents.getById(this.props.documentId); + } + + handleSelect = async event => { + const { document } = this.props; + invariant(this.props.onSuccess, 'onSuccess unavailable'); + event.preventDefault(); + await document.move(this.resultDocument ? this.resultDocument.id : null); + this.props.onSuccess(); + }; + + render() { + const { document, onSuccess, ref } = this.props; + const { collection } = document || this.resultDocument; + const Component = onSuccess ? ResultWrapperLink : ResultWrapper; + + return ( + + {collection.name} + {this.resultDocument && + + {' '} + + {' '} + {this.resultDocument.pathToDocument + .map(doc => {doc.title}) + .reduce((prev, curr) => [prev, , curr])} + } + {document && + + {' '} + + {' '}{document.title} + } + + ); + } +} + +const ResultWrapper = styled.div` + display: flex; + margin-bottom: 10px; + + color: ${color.text}; + cursor: default; +`; + +const ResultWrapperLink = ResultWrapper.withComponent('a').extend` + padding-top: 3px; + + &:hover, + &:active, + &:focus { + margin-left: -8px; + padding-left: 6px; + background: ${color.smokeLight}; + border-left: 2px solid ${color.primary}; + outline: none; + cursor: pointer; + } +`; + +export default withRouter(inject('documents')(DocumentMove)); diff --git a/frontend/scenes/Document/components/DocumentMove/index.js b/frontend/scenes/Document/components/DocumentMove/index.js new file mode 100644 index 00000000..3f3eb8bf --- /dev/null +++ b/frontend/scenes/Document/components/DocumentMove/index.js @@ -0,0 +1,3 @@ +// @flow +import DocumentMove from './DocumentMove'; +export default DocumentMove; diff --git a/frontend/scenes/Document/components/Menu.js b/frontend/scenes/Document/components/Menu.js index 9bae6f0b..7eca8f58 100644 --- a/frontend/scenes/Document/components/Menu.js +++ b/frontend/scenes/Document/components/Menu.js @@ -51,6 +51,10 @@ type Props = { } }; + onMove = () => { + this.props.history.push(`${this.props.document.url}/move`); + }; + render() { const document = get(this.props, 'document'); if (document) { @@ -69,6 +73,7 @@ type Props = { New document
} + Move Export {allowDelete && Delete} diff --git a/frontend/scenes/Search/Search.js b/frontend/scenes/Search/Search.js index 2b6e0acc..7665ff79 100644 --- a/frontend/scenes/Search/Search.js +++ b/frontend/scenes/Search/Search.js @@ -2,18 +2,23 @@ import React from 'react'; import ReactDOM from 'react-dom'; import keydown from 'react-keydown'; -import { observer } from 'mobx-react'; +import { observable, action, runInAction } from 'mobx'; +import { observer, inject } from 'mobx-react'; import _ from 'lodash'; -import Flex from 'components/Flex'; +import invariant from 'invariant'; +import { client } from 'utils/ApiClient'; +import Document from 'models/Document'; +import DocumentsStore from 'stores/DocumentsStore'; + import { withRouter } from 'react-router'; import { searchUrl } from 'utils/routeHelpers'; import styled from 'styled-components'; import ArrowKeyNavigation from 'boundless-arrow-key-navigation'; +import Flex from 'components/Flex'; import CenteredContent from 'components/CenteredContent'; import LoadingIndicator from 'components/LoadingIndicator'; import SearchField from './components/SearchField'; -import SearchStore from './SearchStore'; import DocumentPreview from 'components/DocumentPreview'; import PageTitle from 'components/PageTitle'; @@ -21,6 +26,7 @@ import PageTitle from 'components/PageTitle'; type Props = { history: Object, match: Object, + documents: DocumentsStore, notFound: ?boolean, }; @@ -55,9 +61,11 @@ const StyledArrowKeyNavigation = styled(ArrowKeyNavigation)` props: Props; store: SearchStore; - constructor(props: Props) { - super(props); - this.store = new SearchStore(); + @observable resultIds: Array = []; // Document IDs + @observable searchTerm: ?string = null; + @observable isFetching = false; + + componentDidMount() { this.updateSearchResults(); } @@ -91,9 +99,35 @@ const StyledArrowKeyNavigation = styled(ArrowKeyNavigation)` }; updateSearchResults = _.debounce(() => { - this.store.search(this.props.match.params.query); + this.search(this.props.match.params.query); }, 250); + @action search = async (query: string) => { + this.searchTerm = query; + this.isFetching = true; + + if (query) { + try { + const res = await client.get('/documents.search', { query }); + invariant(res && res.data, 'res or res.data missing'); + const { data } = res; + runInAction('search document', () => { + // Fill documents store + data.forEach(documentData => + this.props.documents.add(new Document(documentData)) + ); + this.resultIds = data.map(documentData => documentData.id); + }); + } catch (e) { + console.error('Something went wrong'); + } + } else { + this.resultIds = []; + } + + this.isFetching = false; + }; + updateQuery = query => { this.props.history.replace(searchUrl(query)); }; @@ -103,20 +137,21 @@ const StyledArrowKeyNavigation = styled(ArrowKeyNavigation)` }; get title() { - const query = this.store.searchTerm; + const query = this.searchTerm; const title = 'Search'; if (query) return `${query} - ${title}`; return title; } render() { + const { documents } = this.props; const query = this.props.match.params.query; - const hasResults = this.store.documents.length > 0; + const hasResults = this.resultIds.length > 0; return ( - {this.store.isFetching && } + {this.isFetching && } {this.props.notFound &&

Not Found

@@ -125,7 +160,7 @@ const StyledArrowKeyNavigation = styled(ArrowKeyNavigation)`
} - {this.store.documents.map((document, index) => ( - index === 0 && this.setFirstDocumentRef(ref)} - key={document.id} - document={document} - highlight={this.store.searchTerm} - showCollection - /> - ))} + {this.resultIds.map((documentId, index) => { + const document = documents.getById(documentId); + if (document) + return ( + + index === 0 && this.setFirstDocumentRef(ref)} + key={documentId} + document={document} + highlight={this.searchTerm} + showCollection + /> + ); + })} @@ -152,4 +192,4 @@ const StyledArrowKeyNavigation = styled(ArrowKeyNavigation)` } } -export default withRouter(Search); +export default withRouter(inject('documents')(Search)); diff --git a/frontend/scenes/Search/SearchStore.js b/frontend/scenes/Search/SearchStore.js deleted file mode 100644 index 68088ce8..00000000 --- a/frontend/scenes/Search/SearchStore.js +++ /dev/null @@ -1,37 +0,0 @@ -// @flow -import { observable, action, runInAction } from 'mobx'; -import invariant from 'invariant'; -import { client } from 'utils/ApiClient'; -import Document from 'models/Document'; - -class SearchStore { - @observable documents: Array = []; - @observable searchTerm: ?string = null; - @observable isFetching = false; - - /* Actions */ - - @action search = async (query: string) => { - this.searchTerm = query; - this.isFetching = true; - - if (query) { - try { - const res = await client.get('/documents.search', { query }); - invariant(res && res.data, 'res or res.data missing'); - const { data } = res; - runInAction('search document', () => { - this.documents = data.map(documentData => new Document(documentData)); - }); - } catch (e) { - console.error('Something went wrong'); - } - } else { - this.documents = []; - } - - this.isFetching = false; - }; -} - -export default SearchStore; diff --git a/frontend/utils/routeHelpers.js b/frontend/utils/routeHelpers.js index 2a273bd5..e501f2a9 100644 --- a/frontend/utils/routeHelpers.js +++ b/frontend/utils/routeHelpers.js @@ -39,6 +39,12 @@ export function notFoundUrl(): string { return '/404'; } +export const matchDocumentSlug = + ':documentSlug([0-9a-zA-Z-]*-[a-zA-z0-9]{10,15})'; + +export const matchDocumentEdit = `/doc/${matchDocumentSlug}/edit`; +export const matchDocumentMove = `/doc/${matchDocumentSlug}/move`; + /** * Replace full url's document part with the new one in case * the document slug has been updated diff --git a/server/api/documents.js b/server/api/documents.js index a179f983..2eb51b4f 100644 --- a/server/api/documents.js +++ b/server/api/documents.js @@ -281,6 +281,8 @@ router.post('documents.move', auth(), async ctx => { await collection.deleteDocument(document); await collection.addDocumentToStructure(document, index); } + // Update collection + document.collection = collection; document.collection = collection; From 86a1792c8a0656c4b1106491471ef98d6c4a0188 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Mon, 4 Sep 2017 15:08:23 -0700 Subject: [PATCH 15/17] Added keyboard shortcut for move --- frontend/scenes/Document/Document.js | 10 ++++++++-- frontend/static/flatpages/keyboard.md | 3 ++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/frontend/scenes/Document/Document.js b/frontend/scenes/Document/Document.js index 78a015c6..3e254346 100644 --- a/frontend/scenes/Document/Document.js +++ b/frontend/scenes/Document/Document.js @@ -4,13 +4,13 @@ import get from 'lodash/get'; import styled from 'styled-components'; import { observable } from 'mobx'; import { observer, inject } from 'mobx-react'; -import { withRouter, Prompt, Route } from 'react-router'; +import { withRouter, Prompt } from 'react-router'; +import keydown from 'react-keydown'; import Flex from 'components/Flex'; import { color, layout } from 'styles/constants'; import { collectionUrl, updateDocumentUrl } from 'utils/routeHelpers'; import Document from 'models/Document'; -import Modal from 'components/Modal'; import DocumentMove from './components/DocumentMove'; import UiStore from 'stores/UiStore'; import DocumentsStore from 'stores/DocumentsStore'; @@ -77,6 +77,12 @@ type Props = { this.props.ui.clearActiveDocument(); } + @keydown('m') + goToMove(event) { + event.preventDefault(); + this.props.history.push(`${this.document.url}/move`); + } + loadDocument = async props => { if (props.newDocument) { const newDocument = new Document({ diff --git a/frontend/static/flatpages/keyboard.md b/frontend/static/flatpages/keyboard.md index 8d0eeeb0..56e604a0 100644 --- a/frontend/static/flatpages/keyboard.md +++ b/frontend/static/flatpages/keyboard.md @@ -1,8 +1,9 @@ - `Cmd+Enter` - Save and exit document editor -- `Cmd+S` - Save document and continue editing +- `Cmd+s` - Save document and continue editing - `Cmd+Esc` - Cancel edit - `/` or `t` - Jump to search - `d` - Jump to dashboard - `c` - Compose within a collection - `e` - Edit document +- `m` - Move document - `?` - This guide From 2cfe36dd35d701cc8c306fe33b1b06e344ab174f Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Sun, 10 Sep 2017 17:28:37 -0400 Subject: [PATCH 16/17] styling --- frontend/components/Input/Input.js | 2 +- .../components/DocumentMove/DocumentMove.js | 118 +++--------------- .../DocumentMove/components/PathToDocument.js | 99 +++++++++++++++ 3 files changed, 116 insertions(+), 103 deletions(-) create mode 100644 frontend/scenes/Document/components/DocumentMove/components/PathToDocument.js diff --git a/frontend/components/Input/Input.js b/frontend/components/Input/Input.js index b8420ed1..3dc278a2 100644 --- a/frontend/components/Input/Input.js +++ b/frontend/components/Input/Input.js @@ -24,7 +24,7 @@ const RealInput = styled.input` background: none; &::placeholder { - color: ${color.slateLight}; + color: ${color.slate}; } `; diff --git a/frontend/scenes/Document/components/DocumentMove/DocumentMove.js b/frontend/scenes/Document/components/DocumentMove/DocumentMove.js index 9999ab4c..bed6b4ea 100644 --- a/frontend/scenes/Document/components/DocumentMove/DocumentMove.js +++ b/frontend/scenes/Document/components/DocumentMove/DocumentMove.js @@ -9,15 +9,13 @@ import _ from 'lodash'; import invariant from 'invariant'; import { client } from 'utils/ApiClient'; import styled from 'styled-components'; -import { size, color } from 'styles/constants'; +import { size } from 'styles/constants'; import Modal from 'components/Modal'; -import Button from 'components/Button'; import Input from 'components/Input'; -import HelpText from 'components/HelpText'; import Labeled from 'components/Labeled'; import Flex from 'components/Flex'; -import ChevronIcon from 'components/Icon/ChevronIcon'; +import PathToDocument from './components/PathToDocument'; import Document from 'models/Document'; import DocumentsStore from 'stores/DocumentsStore'; @@ -31,7 +29,6 @@ type Props = { @observer class DocumentMove extends Component { props: Props; - store: DocumentMoveStore; firstDocument: HTMLElement; @observable isSaving: boolean; @@ -39,6 +36,10 @@ type Props = { @observable searchTerm: ?string = null; @observable isFetching = false; + componentDidMount() { + this.setDefaultResult(); + } + handleKeyDown = ev => { // Down if (ev.which === 40) { @@ -55,7 +56,7 @@ type Props = { this.props.history.push(this.props.document.url); }; - handleFilter = (e: SyntheticEvent) => { + handleFilter = (e: SyntheticInputEvent) => { const value = e.target.value; this.searchTerm = value; this.updateSearchResults(); @@ -69,6 +70,12 @@ type Props = { this.firstDocument = ref; }; + @action setDefaultResult() { + this.resultIds = this.props.document.collection.documents.map( + doc => doc.id + ); + } + @action search = async () => { this.isFetching = true; @@ -90,7 +97,7 @@ type Props = { console.error('Something went wrong'); } } else { - this.resultIds = []; + this.setDefaultResult(); } this.isFetching = false; @@ -100,12 +107,7 @@ type Props = { const { document, documents } = this.props; return ( - - +
@@ -113,7 +115,7 @@ type Props = {
- +
- - {false && - }
); } @@ -169,84 +163,4 @@ const StyledArrowKeyNavigation = styled(ArrowKeyNavigation)` flex: 1; `; -type PathToDocumentProps = { - documentId: string, - onSuccess?: Function, - documents: DocumentsStore, - document?: Document, - ref?: Function, - selectable?: boolean, -}; - -class PathToDocument extends React.Component { - props: PathToDocumentProps; - - get resultDocument(): ?Document { - return this.props.documents.getById(this.props.documentId); - } - - handleSelect = async event => { - const { document } = this.props; - invariant(this.props.onSuccess, 'onSuccess unavailable'); - event.preventDefault(); - await document.move(this.resultDocument ? this.resultDocument.id : null); - this.props.onSuccess(); - }; - - render() { - const { document, onSuccess, ref } = this.props; - const { collection } = document || this.resultDocument; - const Component = onSuccess ? ResultWrapperLink : ResultWrapper; - - return ( - - {collection.name} - {this.resultDocument && - - {' '} - - {' '} - {this.resultDocument.pathToDocument - .map(doc => {doc.title}) - .reduce((prev, curr) => [prev, , curr])} - } - {document && - - {' '} - - {' '}{document.title} - } - - ); - } -} - -const ResultWrapper = styled.div` - display: flex; - margin-bottom: 10px; - - color: ${color.text}; - cursor: default; -`; - -const ResultWrapperLink = ResultWrapper.withComponent('a').extend` - padding-top: 3px; - - &:hover, - &:active, - &:focus { - margin-left: -8px; - padding-left: 6px; - background: ${color.smokeLight}; - border-left: 2px solid ${color.primary}; - outline: none; - cursor: pointer; - } -`; - export default withRouter(inject('documents')(DocumentMove)); diff --git a/frontend/scenes/Document/components/DocumentMove/components/PathToDocument.js b/frontend/scenes/Document/components/DocumentMove/components/PathToDocument.js new file mode 100644 index 00000000..972f29d5 --- /dev/null +++ b/frontend/scenes/Document/components/DocumentMove/components/PathToDocument.js @@ -0,0 +1,99 @@ +// @flow +import React from 'react'; +import { observer } from 'mobx-react'; +import _ from 'lodash'; +import invariant from 'invariant'; +import styled from 'styled-components'; +import { color } from 'styles/constants'; + +import Flex from 'components/Flex'; +import ChevronIcon from 'components/Icon/ChevronIcon'; + +import Document from 'models/Document'; +import DocumentsStore from 'stores/DocumentsStore'; + +type Props = { + documentId?: string, + onSuccess?: Function, + documents: DocumentsStore, + document?: Document, + ref?: Function, + selectable?: boolean, +}; + +@observer class PathToDocument extends React.Component { + props: Props; + + get resultDocument(): ?Document { + const { documentId } = this.props; + if (documentId) return this.props.documents.getById(documentId); + } + + handleSelect = async (event: SyntheticEvent) => { + const { document, onSuccess } = this.props; + + invariant(onSuccess && document, 'onSuccess unavailable'); + event.preventDefault(); + await document.move(this.resultDocument ? this.resultDocument.id : null); + onSuccess(); + }; + + render() { + const { document, onSuccess, ref } = this.props; + // $FlowIssue we'll always have a document + const { collection } = document || this.resultDocument; + const Component = onSuccess ? ResultWrapperLink : ResultWrapper; + + return ( + + {collection.name} + {this.resultDocument && + + {' '} + + {' '} + {this.resultDocument.pathToDocument + .map(doc => {doc.title}) + .reduce((prev, curr) => [prev, , curr])} + } + {document && + + {' '} + + {' '}{document.title} + } + + ); + } +} + +const ResultWrapper = styled.div` + display: flex; + margin-bottom: 10px; + + color: ${color.text}; + cursor: default; +`; + +const ResultWrapperLink = ResultWrapper.withComponent('a').extend` + padding-top: 3px; + padding-left: 5px; + + &:hover, + &:active, + &:focus { + margin-left: 0px; + border-radius: 2px; + background: ${color.black}; + color: ${color.smokeLight}; + outline: none; + cursor: pointer; + } +`; + +export default PathToDocument; From c02bc04fd2e26f7080507d77a1aa9c7be41b2329 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Tue, 12 Sep 2017 23:30:18 -0700 Subject: [PATCH 17/17] small refactor, lint and fixes --- frontend/components/Input/Input.js | 2 +- .../SidebarCollection/SidebarCollection.js | 2 +- frontend/models/Document.js | 1 + frontend/scenes/Document/Document.js | 11 ++-- .../components/DocumentMove/DocumentMove.js | 17 +------ .../DocumentMove/components/PathToDocument.js | 50 +++++++++---------- frontend/scenes/Search/Search.js | 17 +------ frontend/stores/DocumentsStore.js | 8 +++ 8 files changed, 47 insertions(+), 61 deletions(-) diff --git a/frontend/components/Input/Input.js b/frontend/components/Input/Input.js index 3dc278a2..6b25d24f 100644 --- a/frontend/components/Input/Input.js +++ b/frontend/components/Input/Input.js @@ -55,7 +55,7 @@ const LabelText = styled.div` export type Props = { type: string, - value: string, + value?: string, label?: string, className?: string, }; diff --git a/frontend/components/Layout/components/SidebarCollection/SidebarCollection.js b/frontend/components/Layout/components/SidebarCollection/SidebarCollection.js index 9d1cefdd..4ada3fa9 100644 --- a/frontend/components/Layout/components/SidebarCollection/SidebarCollection.js +++ b/frontend/components/Layout/components/SidebarCollection/SidebarCollection.js @@ -43,7 +43,7 @@ const activeStyle = { {!canDropToImport && {doc.title}} - {(document.pathToDocument.includes(doc.id) || + {(document.pathToDocument.map(entry => entry.id).includes(doc.id) || document.id === doc.id) && {doc.children && this.renderDocuments(doc.children, depth + 1)} diff --git a/frontend/models/Document.js b/frontend/models/Document.js index 39cfdb0a..cafcdefb 100644 --- a/frontend/models/Document.js +++ b/frontend/models/Document.js @@ -186,6 +186,7 @@ class Document extends BaseModel { id: this.id, parentDocument: parentDocumentId, }); + invariant(res && res.data, 'Data not available'); this.updateData(res.data); this.emit('documents.move', { id: this.id, diff --git a/frontend/scenes/Document/Document.js b/frontend/scenes/Document/Document.js index 3e254346..6cb6316a 100644 --- a/frontend/scenes/Document/Document.js +++ b/frontend/scenes/Document/Document.js @@ -8,7 +8,12 @@ import { withRouter, Prompt } from 'react-router'; import keydown from 'react-keydown'; import Flex from 'components/Flex'; import { color, layout } from 'styles/constants'; -import { collectionUrl, updateDocumentUrl } from 'utils/routeHelpers'; +import { + collectionUrl, + updateDocumentUrl, + matchDocumentEdit, + matchDocumentMove, +} from 'utils/routeHelpers'; import Document from 'models/Document'; import DocumentMove from './components/DocumentMove'; @@ -25,8 +30,6 @@ import CenteredContent from 'components/CenteredContent'; import PageTitle from 'components/PageTitle'; import Search from 'scenes/Search'; -import { matchDocumentEdit, matchDocumentMove } from 'utils/routeHelpers'; - const DISCARD_CHANGES = ` You have unsaved changes. Are you sure you want to discard them? @@ -80,7 +83,7 @@ type Props = { @keydown('m') goToMove(event) { event.preventDefault(); - this.props.history.push(`${this.document.url}/move`); + if (this.document) this.props.history.push(`${this.document.url}/move`); } loadDocument = async props => { diff --git a/frontend/scenes/Document/components/DocumentMove/DocumentMove.js b/frontend/scenes/Document/components/DocumentMove/DocumentMove.js index bed6b4ea..d6268a03 100644 --- a/frontend/scenes/Document/components/DocumentMove/DocumentMove.js +++ b/frontend/scenes/Document/components/DocumentMove/DocumentMove.js @@ -1,13 +1,11 @@ // @flow import React, { Component } from 'react'; import ReactDOM from 'react-dom'; -import { observable, runInAction, action } from 'mobx'; +import { observable, action } from 'mobx'; import { observer, inject } from 'mobx-react'; import { withRouter } from 'react-router'; import ArrowKeyNavigation from 'boundless-arrow-key-navigation'; import _ from 'lodash'; -import invariant from 'invariant'; -import { client } from 'utils/ApiClient'; import styled from 'styled-components'; import { size } from 'styles/constants'; @@ -81,18 +79,7 @@ type Props = { if (this.searchTerm) { try { - const res = await client.get('/documents.search', { - query: this.searchTerm, - }); - invariant(res && res.data, 'res or res.data missing'); - const { data } = res; - runInAction('search document', () => { - // Fill documents store - data.forEach(documentData => - this.props.documents.add(new Document(documentData)) - ); - this.resultIds = data.map(documentData => documentData.id); - }); + this.resultIds = await this.props.documents.search(this.searchTerm); } catch (e) { console.error('Something went wrong'); } diff --git a/frontend/scenes/Document/components/DocumentMove/components/PathToDocument.js b/frontend/scenes/Document/components/DocumentMove/components/PathToDocument.js index 972f29d5..1c2ec5ff 100644 --- a/frontend/scenes/Document/components/DocumentMove/components/PathToDocument.js +++ b/frontend/scenes/Document/components/DocumentMove/components/PathToDocument.js @@ -12,6 +12,30 @@ import ChevronIcon from 'components/Icon/ChevronIcon'; import Document from 'models/Document'; import DocumentsStore from 'stores/DocumentsStore'; +const ResultWrapper = styled.div` +display: flex; +margin-bottom: 10px; + +color: ${color.text}; +cursor: default; +`; + +const ResultWrapperLink = ResultWrapper.withComponent('a').extend` +padding-top: 3px; +padding-left: 5px; + +&:hover, +&:active, +&:focus { + margin-left: 0px; + border-radius: 2px; + background: ${color.black}; + color: ${color.smokeLight}; + outline: none; + cursor: pointer; +} +`; + type Props = { documentId?: string, onSuccess?: Function, @@ -58,7 +82,7 @@ type Props = { {' '} {this.resultDocument.pathToDocument - .map(doc => {doc.title}) + .map(doc => {doc.title}) .reduce((prev, curr) => [prev, , curr])} } {document && @@ -72,28 +96,4 @@ type Props = { } } -const ResultWrapper = styled.div` - display: flex; - margin-bottom: 10px; - - color: ${color.text}; - cursor: default; -`; - -const ResultWrapperLink = ResultWrapper.withComponent('a').extend` - padding-top: 3px; - padding-left: 5px; - - &:hover, - &:active, - &:focus { - margin-left: 0px; - border-radius: 2px; - background: ${color.black}; - color: ${color.smokeLight}; - outline: none; - cursor: pointer; - } -`; - export default PathToDocument; diff --git a/frontend/scenes/Search/Search.js b/frontend/scenes/Search/Search.js index 7665ff79..b6ca7c8c 100644 --- a/frontend/scenes/Search/Search.js +++ b/frontend/scenes/Search/Search.js @@ -2,12 +2,9 @@ import React from 'react'; import ReactDOM from 'react-dom'; import keydown from 'react-keydown'; -import { observable, action, runInAction } from 'mobx'; +import { observable, action } from 'mobx'; import { observer, inject } from 'mobx-react'; import _ from 'lodash'; -import invariant from 'invariant'; -import { client } from 'utils/ApiClient'; -import Document from 'models/Document'; import DocumentsStore from 'stores/DocumentsStore'; import { withRouter } from 'react-router'; @@ -59,7 +56,6 @@ const StyledArrowKeyNavigation = styled(ArrowKeyNavigation)` @observer class Search extends React.Component { firstDocument: HTMLElement; props: Props; - store: SearchStore; @observable resultIds: Array = []; // Document IDs @observable searchTerm: ?string = null; @@ -108,16 +104,7 @@ const StyledArrowKeyNavigation = styled(ArrowKeyNavigation)` if (query) { try { - const res = await client.get('/documents.search', { query }); - invariant(res && res.data, 'res or res.data missing'); - const { data } = res; - runInAction('search document', () => { - // Fill documents store - data.forEach(documentData => - this.props.documents.add(new Document(documentData)) - ); - this.resultIds = data.map(documentData => documentData.id); - }); + this.resultIds = await this.props.documents.search(query); } catch (e) { console.error('Something went wrong'); } diff --git a/frontend/stores/DocumentsStore.js b/frontend/stores/DocumentsStore.js index cfbb70f9..c7e03e97 100644 --- a/frontend/stores/DocumentsStore.js +++ b/frontend/stores/DocumentsStore.js @@ -104,6 +104,14 @@ class DocumentsStore extends BaseStore { await this.fetchAll('starred'); }; + @action search = async (query: string): Promise<*> => { + const res = await client.get('/documents.search', { query }); + invariant(res && res.data, 'res or res.data missing'); + const { data } = res; + data.forEach(documentData => this.add(new Document(documentData))); + return data.map(documentData => documentData.id); + }; + @action fetch = async (id: string): Promise<*> => { this.isFetching = true;