From 70d352e193da4bd7824342b76ceee9813e2cb046 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Mon, 4 Sep 2017 14:38:53 -0700 Subject: [PATCH 1/5] 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 000000000..88453bbce --- /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 2/5] 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 000000000..bfff39c91 --- /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 000000000..544f6a7b3 --- /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 cde113089..d0b208e93 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 9754daaa2..e6784ea7e 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 eade37798..39cfdb0a4 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 b64c0023f..78a015c63 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 000000000..9999ab4ca --- /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 000000000..3f3eb8bf1 --- /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 9bae6f0b8..7eca8f58c 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 2b6e0accc..7665ff79e 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 68088ce80..000000000 --- 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 2a273bd51..e501f2a98 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 a179f9830..2eb51b4f9 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 3/5] 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 78a015c63..3e2543468 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 8d0eeeb02..56e604a0c 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 4/5] 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 b8420ed1a..3dc278a2a 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 9999ab4ca..bed6b4ea3 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 000000000..972f29d5b --- /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 5/5] 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 3dc278a2a..6b25d24f7 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 9d1cefdda..4ada3fa99 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 39cfdb0a4..cafcdefb5 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 3e2543468..6cb6316ae 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 bed6b4ea3..d6268a039 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 972f29d5b..1c2ec5ff2 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 7665ff79e..b6ca7c8ca 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 cfbb70f99..c7e03e975 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;