diff --git a/.sequelizerc b/.sequelizerc index 0f26a4e15..aa05bd025 100644 --- a/.sequelizerc +++ b/.sequelizerc @@ -1,4 +1,4 @@ -require('localenv'); +require('dotenv').config({ silent: true }); var path = require('path'); diff --git a/README.md b/README.md index b0154c0b8..b4b11cf29 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Atlas +![](https://circleci.com/gh/jorilallo/atlas.svg?style=shield&circle-token=c0c4c2f39990e277385d5c1ae96169c409eb887a) + ## Installation 1. Install dependencies with `yarn` diff --git a/circle.yml b/circle.yml new file mode 100644 index 000000000..51020211c --- /dev/null +++ b/circle.yml @@ -0,0 +1,24 @@ +machine: + node: + version: 7.6 + services: + - redis + environment: + ENVIRONMENT: test + PATH: "${PATH}:${HOME}/${CIRCLE_PROJECT_REPONAME}/node_modules/.bin" + SEQUELIZE_SECRET: F0E5AD933D7F6FD8F4DBB3E038C501C052DC0593C686D21ACB30AE205D2F634B + DATABASE_URL_TEST: postgres://ubuntu@localhost:5432/circle_test + DATABASE_URL: postgres://ubuntu@localhost:5432/circle_test + +dependencies: + override: + - yarn + cache_directories: + - ~/.cache/yarn + +test: + pre: + - sequelize db:migrate --url postgres://ubuntu@localhost:5432/circle_test + override: + - yarn test + - yarn lint diff --git a/frontend/components/Alert/Alert.js b/frontend/components/Alert/Alert.js index 830227569..70c2081d5 100644 --- a/frontend/components/Alert/Alert.js +++ b/frontend/components/Alert/Alert.js @@ -1,6 +1,6 @@ // @flow import React, { PropTypes } from 'react'; -import { Flex } from 'reflexbox'; +import Flex from 'components/Flex'; import classNames from 'classnames/bind'; import styles from './Alert.scss'; diff --git a/frontend/components/CenteredContent/CenteredContent.js b/frontend/components/CenteredContent/CenteredContent.js index 2d6cdd5a6..6f135e1e0 100644 --- a/frontend/components/CenteredContent/CenteredContent.js +++ b/frontend/components/CenteredContent/CenteredContent.js @@ -4,8 +4,6 @@ import styled from 'styled-components'; type Props = { children?: React.Element, - style?: Object, - maxWidth?: string, }; const Container = styled.div` @@ -13,20 +11,17 @@ const Container = styled.div` margin: 40px 20px; `; -const CenteredContent = ({ - children, - maxWidth = '740px', - style, - ...rest -}: Props) => { - const styles = { - maxWidth, - ...style, - }; +const Content = styled.div` + max-width: 740px; + margin: 0 auto; +`; +const CenteredContent = ({ children, ...rest }: Props) => { return ( - - {children} + + + {children} + ); }; diff --git a/frontend/components/DocumentViews/DocumentViewersStore.js b/frontend/components/DocumentViews/DocumentViewersStore.js index 8fda92231..0f620ffb4 100644 --- a/frontend/components/DocumentViews/DocumentViewersStore.js +++ b/frontend/components/DocumentViews/DocumentViewersStore.js @@ -18,7 +18,7 @@ class DocumentViewersStore { this.isFetching = true; try { - const res = await client.get( + const res = await client.post( '/views.list', { id: this.documentId, diff --git a/frontend/components/DocumentViews/DocumentViews.js b/frontend/components/DocumentViews/DocumentViews.js index 7d0c67d0a..0ff1b2d16 100644 --- a/frontend/components/DocumentViews/DocumentViews.js +++ b/frontend/components/DocumentViews/DocumentViews.js @@ -5,7 +5,7 @@ import Popover from 'components/Popover'; import styled from 'styled-components'; import DocumentViewers from './components/DocumentViewers'; import DocumentViewersStore from './DocumentViewersStore'; -import { Flex } from 'reflexbox'; +import Flex from 'components/Flex'; const Container = styled(Flex)` font-size: 13px; diff --git a/frontend/components/DocumentViews/components/DocumentViewers/DocumentViewers.js b/frontend/components/DocumentViews/components/DocumentViewers/DocumentViewers.js index ecd4289f8..e545a8d3c 100644 --- a/frontend/components/DocumentViews/components/DocumentViewers/DocumentViewers.js +++ b/frontend/components/DocumentViews/components/DocumentViewers/DocumentViewers.js @@ -1,6 +1,6 @@ // @flow import React, { Component } from 'react'; -import { Flex } from 'reflexbox'; +import Flex from 'components/Flex'; import styled from 'styled-components'; import map from 'lodash/map'; import Avatar from 'components/Avatar'; diff --git a/frontend/components/DropToImport/DropToImport.js b/frontend/components/DropToImport/DropToImport.js new file mode 100644 index 000000000..944dde6f1 --- /dev/null +++ b/frontend/components/DropToImport/DropToImport.js @@ -0,0 +1,108 @@ +// @flow +import React, { Component } from 'react'; +import { inject } from 'mobx-react'; +import invariant from 'invariant'; +import _ from 'lodash'; +import Dropzone from 'react-dropzone'; +import Document from 'models/Document'; +import DocumentsStore from 'stores/DocumentsStore'; +import LoadingIndicator from 'components/LoadingIndicator'; + +class DropToImport extends Component { + state: { + isImporting: boolean, + }; + props: { + children?: React$Element, + collectionId: string, + documentId?: string, + activeClassName?: string, + rejectClassName?: string, + documents: DocumentsStore, + history: Object, + }; + state = { + isImporting: false, + }; + + importFile = async ({ file, documentId, collectionId, redirect }) => { + const reader = new FileReader(); + + reader.onload = async ev => { + const text = ev.target.result; + let data = { + parentDocument: undefined, + collection: { id: collectionId }, + text, + }; + + if (documentId) { + data.parentDocument = { + id: documentId, + }; + } + + let document = new Document(data); + document = await document.save(); + this.props.documents.add(document); + + if (redirect && this.props.history) { + this.props.history.push(document.url); + } + }; + reader.readAsText(file); + }; + + onDropAccepted = async (files = []) => { + this.setState({ isImporting: true }); + + try { + let collectionId = this.props.collectionId; + const documentId = this.props.documentId; + const redirect = files.length === 1; + + if (documentId && !collectionId) { + const document = await this.props.documents.fetch(documentId); + invariant(document, 'Document not available'); + collectionId = document.collection.id; + } + + for (const file of files) { + await this.importFile({ file, documentId, collectionId, redirect }); + } + } catch (err) { + // TODO: show error alert. + } finally { + this.setState({ isImporting: false }); + } + }; + + render() { + const props = _.omit( + this.props, + 'history', + 'documentId', + 'collectionId', + 'documents' + ); + + return ( + + + {this.state.isImporting && } + {this.props.children} + + + ); + } +} + +export default inject('documents')(DropToImport); diff --git a/frontend/components/DropToImport/index.js b/frontend/components/DropToImport/index.js new file mode 100644 index 000000000..a95a2a44d --- /dev/null +++ b/frontend/components/DropToImport/index.js @@ -0,0 +1,3 @@ +// @flow +import DropToImport from './DropToImport'; +export default DropToImport; diff --git a/frontend/components/Editor/Editor.js b/frontend/components/Editor/Editor.js index 6a25ebcf2..cd22cce58 100644 --- a/frontend/components/Editor/Editor.js +++ b/frontend/components/Editor/Editor.js @@ -61,6 +61,12 @@ type KeyData = { } } + componentDidUpdate(prevProps: Props) { + if (prevProps.readOnly && !this.props.readOnly) { + this.focusAtEnd(); + } + } + getChildContext() { return { starred: this.props.starred }; } diff --git a/frontend/components/Flex/Flex.js b/frontend/components/Flex/Flex.js new file mode 100644 index 000000000..d14c0ce15 --- /dev/null +++ b/frontend/components/Flex/Flex.js @@ -0,0 +1,41 @@ +// @flow +import React from 'react'; +import styled from 'styled-components'; + +type JustifyValues = + | 'center' + | 'space-around' + | 'space-between' + | 'flex-start' + | 'flex-end'; + +type AlignValues = + | 'stretch' + | 'center' + | 'baseline' + | 'flex-start' + | 'flex-end'; + +type Props = { + column?: ?boolean, + align?: AlignValues, + justify?: JustifyValues, + auto?: ?boolean, + className?: string, + children?: React.Element, +}; + +const Flex = (props: Props) => { + const { children, ...restProps } = props; + return {children}; +}; + +const Container = styled.div` + display: flex; + flex: ${({ auto }) => (auto ? '1 1 auto' : 'initial')}; + flex-direction: ${({ column }) => (column ? 'column' : 'row')}; + align-items: ${({ align }) => align}; + justify-content: ${({ justify }) => justify}; +`; + +export default Flex; diff --git a/frontend/components/Flex/index.js b/frontend/components/Flex/index.js new file mode 100644 index 000000000..d798e8cd8 --- /dev/null +++ b/frontend/components/Flex/index.js @@ -0,0 +1,3 @@ +// @flow +import Flex from './Flex'; +export default Flex; diff --git a/frontend/components/Input/Input.js b/frontend/components/Input/Input.js index da09d5417..9ac8641e6 100644 --- a/frontend/components/Input/Input.js +++ b/frontend/components/Input/Input.js @@ -1,7 +1,7 @@ // @flow import React from 'react'; import styled from 'styled-components'; -import { Flex } from 'reflexbox'; +import Flex from 'components/Flex'; import { size } from 'styles/constants'; const RealTextarea = styled.textarea` diff --git a/frontend/components/Layout/Layout.js b/frontend/components/Layout/Layout.js index b04a95469..c022c6301 100644 --- a/frontend/components/Layout/Layout.js +++ b/frontend/components/Layout/Layout.js @@ -6,11 +6,13 @@ import styled from 'styled-components'; import { observer, inject } from 'mobx-react'; import _ from 'lodash'; import keydown from 'react-keydown'; -import { Flex } from 'reflexbox'; -import { textColor } from 'styles/constants.scss'; +import Flex from 'components/Flex'; +import { color, layout } from 'styles/constants'; import DropdownMenu, { MenuItem } from 'components/DropdownMenu'; import { LoadingIndicatorBar } from 'components/LoadingIndicator'; +import Scrollable from 'components/Scrollable'; +import Avatar from 'components/Avatar'; import SidebarCollection from './components/SidebarCollection'; import SidebarCollectionList from './components/SidebarCollectionList'; @@ -101,21 +103,24 @@ type Props = { - - Search - - - Home - Starred - - - {ui.activeCollection - ? - : } - + + + Search + + + Home + Starred + + + {ui.activeCollection + ? + : } + + } @@ -135,22 +140,16 @@ const Container = styled(Flex)` `; const LogoLink = styled(Link)` - margin-top: 5px; + margin-top: 15px; font-family: 'Atlas Grotesk'; font-weight: bold; - color: ${textColor}; + color: ${color.text}; text-decoration: none; font-size: 16px; `; -const Avatar = styled.img` - width: 24px; - height: 24px; - border-radius: 50%; -`; - const MenuLink = styled(Link)` - color: ${textColor}; + color: ${color.text}; `; const Content = styled(Flex)` @@ -159,26 +158,27 @@ const Content = styled(Flex)` top: 0; bottom: 0; right: 0; - left: ${props => (props.editMode ? 0 : '250px')}; + left: ${props => (props.editMode ? 0 : layout.sidebarWidth)}; transition: left 200ms ease-in-out; `; const Sidebar = styled(Flex)` - width: 250px; - margin-left: ${props => (props.editMode ? '-250px' : 0)}; - padding: 10px 20px; + width: ${layout.sidebarWidth}; + margin-left: ${props => (props.editMode ? `-${layout.sidebarWidth}` : 0)}; background: rgba(250, 251, 252, 0.71); border-right: 1px solid #eceff3; transition: margin-left 200ms ease-in-out; `; const Header = styled(Flex)` - margin-bottom: 20px; + flex-shrink: 0; + padding: ${layout.padding}; + padding-bottom: 10px; `; const LinkSection = styled(Flex)` - margin-bottom: 20px; flex-direction: column; + padding: 10px 0; `; export default withRouter(inject('user', 'auth', 'ui', 'collections')(Layout)); diff --git a/frontend/components/Layout/components/SidebarCollection/SidebarCollection.js b/frontend/components/Layout/components/SidebarCollection/SidebarCollection.js index 828221186..9d02cd85b 100644 --- a/frontend/components/Layout/components/SidebarCollection/SidebarCollection.js +++ b/frontend/components/Layout/components/SidebarCollection/SidebarCollection.js @@ -1,35 +1,51 @@ // @flow import React from 'react'; -import { observer } from 'mobx-react'; -import { Flex } from 'reflexbox'; +import Flex from 'components/Flex'; import styled from 'styled-components'; - +import { layout } from 'styles/constants'; import SidebarLink from '../SidebarLink'; +import DropToImport from 'components/DropToImport'; import Collection from 'models/Collection'; import Document from 'models/Document'; +import type { NavigationNode } from 'types'; type Props = { collection: ?Collection, document: ?Document, + history: Object, +}; + +const activeStyle = { + color: '#000', + background: '#E1E1E1', }; class SidebarCollection extends React.Component { props: Props; - renderDocuments(documentList) { - const { document } = this.props; + renderDocuments(documentList: Array, depth: number = 0) { + const { document, history } = this.props; + const canDropToImport = depth === 0; if (document) { return documentList.map(doc => ( - - {doc.title} - + {canDropToImport && + + {doc.title} + } + {!canDropToImport && + {doc.title}} + {(document.pathToDocument.includes(doc.id) || document.id === doc.id) && - - {doc.children && this.renderDocuments(doc.children)} + + {doc.children && this.renderDocuments(doc.children, depth + 1)} } )); @@ -57,10 +73,11 @@ const Header = styled(Flex)` text-transform: uppercase; color: #9FA6AB; letter-spacing: 0.04em; + padding: 0 ${layout.hpadding}; `; const Children = styled(Flex)` margin-left: 20px; `; -export default observer(SidebarCollection); +export default SidebarCollection; diff --git a/frontend/components/Layout/components/SidebarCollectionList/SidebarCollectionList.js b/frontend/components/Layout/components/SidebarCollectionList/SidebarCollectionList.js index b2880ff63..7d6055869 100644 --- a/frontend/components/Layout/components/SidebarCollectionList/SidebarCollectionList.js +++ b/frontend/components/Layout/components/SidebarCollectionList/SidebarCollectionList.js @@ -1,25 +1,39 @@ // @flow import React from 'react'; import { observer, inject } from 'mobx-react'; -import { Flex } from 'reflexbox'; +import Flex from 'components/Flex'; import styled from 'styled-components'; +import { layout } from 'styles/constants'; import SidebarLink from '../SidebarLink'; +import DropToImport from 'components/DropToImport'; import CollectionsStore from 'stores/CollectionsStore'; type Props = { + history: Object, collections: CollectionsStore, }; -const SidebarCollectionList = observer(({ collections }: Props) => { +const activeStyle = { + color: '#000', + background: '#E1E1E1', +}; + +const SidebarCollectionList = observer(({ history, collections }: Props) => { return (
Collections
{collections.data.map(collection => ( - - {collection.name} - + + + {collection.name} + + ))}
); @@ -31,6 +45,7 @@ const Header = styled(Flex)` text-transform: uppercase; color: #9FA6AB; letter-spacing: 0.04em; + padding: 0 ${layout.hpadding}; `; export default inject('collections')(SidebarCollectionList); diff --git a/frontend/components/Layout/components/SidebarLink/SidebarLink.js b/frontend/components/Layout/components/SidebarLink/SidebarLink.js index df60708c4..7df81e86c 100644 --- a/frontend/components/Layout/components/SidebarLink/SidebarLink.js +++ b/frontend/components/Layout/components/SidebarLink/SidebarLink.js @@ -1,35 +1,26 @@ // @flow import React from 'react'; -import { observer } from 'mobx-react'; -import { NavLink, withRouter } from 'react-router-dom'; -import { Flex } from 'reflexbox'; +import { NavLink } from 'react-router-dom'; +import { layout, color } from 'styles/constants'; +import { darken } from 'polished'; import styled from 'styled-components'; const activeStyle = { color: '#000000', }; -@observer class SidebarLink extends React.Component { - shouldComponentUpdate(nextProps) { - // Navlink is having issues updating, forcing update on URL changes - return this.props.match !== nextProps.match; - } - - render() { - return ( - - - - ); - } +function SidebarLink(props: Object) { + return ; } -const LinkContainer = styled(Flex)` - padding: 5px 0; - - a { - color: #848484; +const StyledNavLink = styled(NavLink)` + display: block; + padding: 5px ${layout.hpadding}; + color: ${color.slateDark}; + + &:hover { + color: ${darken(0.1, color.slateDark)}; } `; -export default withRouter(SidebarLink); +export default SidebarLink; diff --git a/frontend/components/PreviewLoading/PreviewLoading.js b/frontend/components/PreviewLoading/PreviewLoading.js index 43ddae04e..150e1c8ef 100644 --- a/frontend/components/PreviewLoading/PreviewLoading.js +++ b/frontend/components/PreviewLoading/PreviewLoading.js @@ -2,7 +2,7 @@ import React from 'react'; import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; import styled, { keyframes } from 'styled-components'; -import { Flex } from 'reflexbox'; +import Flex from 'components/Flex'; import { randomInteger } from 'utils/random'; @@ -11,7 +11,7 @@ const randomValues = Array.from( () => `${randomInteger(85, 100)}%` ); -export default () => { +export default (props: {}) => { return ( { transitionEnterTimeout={0} transitionLeaveTimeout={0} > - + diff --git a/frontend/components/PublishingInfo/PublishingInfo.js b/frontend/components/PublishingInfo/PublishingInfo.js index 9b4d245cc..708474cb6 100644 --- a/frontend/components/PublishingInfo/PublishingInfo.js +++ b/frontend/components/PublishingInfo/PublishingInfo.js @@ -3,7 +3,7 @@ import React, { Component } from 'react'; import moment from 'moment'; import styled from 'styled-components'; import type { User } from 'types'; -import { Flex } from 'reflexbox'; +import Flex from 'components/Flex'; const Container = styled(Flex)` justify-content: space-between; @@ -51,7 +51,6 @@ class PublishingInfo extends Component { ))} } - {createdAt === updatedAt ? {createdBy.name} diff --git a/frontend/components/ScrollToTop/ScrollToTop.js b/frontend/components/ScrollToTop/ScrollToTop.js new file mode 100644 index 000000000..44e52f7f7 --- /dev/null +++ b/frontend/components/ScrollToTop/ScrollToTop.js @@ -0,0 +1,18 @@ +// @flow +// based on: https://reacttraining.com/react-router/web/guides/scroll-restoration +import { Component } from 'react'; +import { withRouter } from 'react-router'; + +class ScrollToTop extends Component { + componentDidUpdate(prevProps) { + if (this.props.location !== prevProps.location) { + window.scrollTo(0, 0); + } + } + + render() { + return this.props.children; + } +} + +export default withRouter(ScrollToTop); diff --git a/frontend/components/ScrollToTop/index.js b/frontend/components/ScrollToTop/index.js new file mode 100644 index 000000000..0f8823a18 --- /dev/null +++ b/frontend/components/ScrollToTop/index.js @@ -0,0 +1,3 @@ +// @flow +import ScrollToTop from './ScrollToTop'; +export default ScrollToTop; diff --git a/frontend/components/SidebarHidden/SidebarHidden.js b/frontend/components/SidebarHidden/SidebarHidden.js new file mode 100644 index 000000000..adf26b443 --- /dev/null +++ b/frontend/components/SidebarHidden/SidebarHidden.js @@ -0,0 +1,25 @@ +// @flow +import { Component } from 'react'; +import { inject } from 'mobx-react'; +import UiStore from 'stores/UiStore'; + +class SidebarHidden extends Component { + props: { + ui: UiStore, + children: React$Element, + }; + + componentDidMount() { + this.props.ui.enableEditMode(); + } + + componentWillUnmount() { + this.props.ui.disableEditMode(); + } + + render() { + return this.props.children; + } +} + +export default inject('ui')(SidebarHidden); diff --git a/frontend/components/SidebarHidden/index.js b/frontend/components/SidebarHidden/index.js new file mode 100644 index 000000000..b63c0b7a1 --- /dev/null +++ b/frontend/components/SidebarHidden/index.js @@ -0,0 +1,3 @@ +// @flow +import SidebarHidden from './SidebarHidden'; +export default SidebarHidden; diff --git a/frontend/index.js b/frontend/index.js index 0f050e1ab..065c1ff8a 100644 --- a/frontend/index.js +++ b/frontend/index.js @@ -8,7 +8,7 @@ import { Route, Redirect, } from 'react-router-dom'; -import { Flex } from 'reflexbox'; +import Flex from 'components/Flex'; import stores from 'stores'; import DocumentsStore from 'stores/DocumentsStore'; @@ -33,7 +33,9 @@ import Flatpage from 'scenes/Flatpage'; import ErrorAuth from 'scenes/ErrorAuth'; import Error404 from 'scenes/Error404'; +import ScrollToTop from 'components/ScrollToTop'; import Layout from 'components/Layout'; +import SidebarHidden from 'components/SidebarHidden'; import flatpages from 'static/flatpages'; @@ -85,52 +87,73 @@ const KeyboardShortcuts = () => ( ); const Api = () => ; const DocumentNew = () => ; -const DocumentNewChild = () => ; +const RedirectDocument = ({ match }: { match: Object }) => ( + +); + +const matchDocumentSlug = ':documentSlug([0-9a-zA-Z-]*-[a-zA-z0-9]{10,15})'; render(
- - + + + - - - + + + - - - - - - - + + + + + + + + + + + + + + - - - + + + - - - + + - - - - - - - - - + + + + + + + {DevTools && } diff --git a/frontend/models/Document.js b/frontend/models/Document.js index e19f79711..db35d7423 100644 --- a/frontend/models/Document.js +++ b/frontend/models/Document.js @@ -10,25 +10,25 @@ import type { User } from 'types'; import Collection from './Collection'; const parseHeader = text => { - const firstLine = text.split(/\r?\n/)[0]; + const firstLine = text.trim().split(/\r?\n/)[0]; return firstLine.replace(/^#/, '').trim(); }; class Document { - isSaving: boolean; + isSaving: boolean = false; hasPendingChanges: boolean = false; errors: ErrorsStore; collaborators: Array; - collection: Collection; + collection: $Shape; createdAt: string; createdBy: User; html: string; id: string; - private: boolean; - starred: boolean; team: string; - text: string; + private: boolean = false; + starred: boolean = false; + text: string = ''; title: string = 'Untitled document'; updatedAt: string; updatedBy: User; @@ -83,9 +83,9 @@ class Document { }; @action view = async () => { + this.views++; try { await client.post('/views.create', { id: this.id }); - this.views++; } catch (e) { this.errors.add('Document failed to record view'); } @@ -113,7 +113,7 @@ class Document { }; @action save = async () => { - if (this.isSaving) return; + if (this.isSaving) return this; this.isSaving = true; try { @@ -125,28 +125,38 @@ class Document { text: this.text, }); } else { - res = await client.post('/documents.create', { + const data = { + parentDocument: undefined, collection: this.collection.id, title: this.title, text: this.text, - }); + }; + if (this.parentDocument) { + data.parentDocument = this.parentDocument.id; + } + res = await client.post('/documents.create', data); } invariant(res && res.data, 'Data should be available'); - this.hasPendingChanges = false; + this.updateData({ + ...res.data, + hasPendingChanges: false, + }); } catch (e) { this.errors.add('Document failed saving'); } finally { this.isSaving = false; } + + return this; }; updateData(data: Object | Document) { - data.title = parseHeader(data.text); + if (data.text) data.title = parseHeader(data.text); extendObservable(this, data); } - constructor(document: Document) { + constructor(document?: Object = {}) { this.updateData(document); this.errors = stores.errors; } diff --git a/frontend/models/Document.test.js b/frontend/models/Document.test.js index e7db05b7d..f84327064 100644 --- a/frontend/models/Document.test.js +++ b/frontend/models/Document.test.js @@ -5,8 +5,7 @@ describe('Document model', () => { test('should initialize with data', () => { const document = new Document({ id: 123, - title: 'Onboarding', - text: 'Some body text' + text: '# Onboarding\nSome body text', }); expect(document.title).toBe('Onboarding'); }); diff --git a/frontend/scenes/Document/Document.js b/frontend/scenes/Document/Document.js index db5d81bb6..d04c08c52 100644 --- a/frontend/scenes/Document/Document.js +++ b/frontend/scenes/Document/Document.js @@ -4,16 +4,18 @@ import get from 'lodash/get'; import styled from 'styled-components'; import { observer, inject } from 'mobx-react'; import { withRouter, Prompt } from 'react-router'; -import { Flex } from 'reflexbox'; +import Flex from 'components/Flex'; +import { layout } from 'styles/constants'; +import Document from 'models/Document'; import UiStore from 'stores/UiStore'; import DocumentsStore from 'stores/DocumentsStore'; import Menu from './components/Menu'; import Editor from 'components/Editor'; +import DropToImport from 'components/DropToImport'; import { HeaderAction, SaveAction } from 'components/Layout'; import LoadingIndicator from 'components/LoadingIndicator'; import PublishingInfo from 'components/PublishingInfo'; -import AuthorInfo from 'components/AuthorInfo'; import PreviewLoading from 'components/PreviewLoading'; import CenteredContent from 'components/CenteredContent'; import PageTitle from 'components/PageTitle'; @@ -28,15 +30,19 @@ type Props = { history: Object, keydown: Object, documents: DocumentsStore, - newChildDocument?: boolean, + newDocument?: boolean, ui: UiStore, }; -@observer class Document extends Component { +@observer class DocumentScene extends Component { props: Props; - + state: { + newDocument?: Document, + }; state = { + isDragging: false, isLoading: false, + newDocument: undefined, }; componentDidMount() { @@ -44,7 +50,10 @@ type Props = { } componentWillReceiveProps(nextProps) { - if (nextProps.match.params.id !== this.props.match.params.id) { + if ( + nextProps.match.params.documentSlug !== + this.props.match.params.documentSlug + ) { this.loadDocument(nextProps); } } @@ -54,42 +63,49 @@ type Props = { } loadDocument = async props => { - await this.props.documents.fetch(props.match.params.id); - const document = this.document; - - if (document) { - this.props.ui.setActiveDocument(document); - document.view(); - } - - if (this.props.match.params.edit) { - this.props.ui.enableEditMode(); + if (props.newDocument) { + const newDocument = new Document({ + collection: { id: props.match.params.id }, + }); + this.setState({ newDocument }); } else { - this.props.ui.disableEditMode(); + let document = this.document; + if (document) { + this.props.ui.setActiveDocument(document); + } + + await this.props.documents.fetch(props.match.params.documentSlug); + document = this.document; + + if (document) { + this.props.ui.setActiveDocument(document); + document.view(); + } } }; get document() { - return this.props.documents.getByUrl(`/d/${this.props.match.params.id}`); + if (this.state.newDocument) return this.state.newDocument; + return this.props.documents.getByUrl( + `/doc/${this.props.match.params.documentSlug}` + ); } onClickEdit = () => { if (!this.document) return; const url = `${this.document.url}/edit`; this.props.history.push(url); - this.props.ui.enableEditMode(); }; onSave = async (redirect: boolean = false) => { - const document = this.document; + let document = this.document; if (!document) return; this.setState({ isLoading: true }); - await document.save(); + document = await document.save(); this.setState({ isLoading: false }); - this.props.ui.disableEditMode(); - if (redirect) { + if (redirect || this.props.newDocument) { this.props.history.push(document.url); } }; @@ -111,86 +127,111 @@ type Props = { this.props.history.goBack(); }; + onStartDragging = () => { + this.setState({ isDragging: true }); + }; + + onStopDragging = () => { + this.setState({ isDragging: false }); + }; + render() { - const isNew = this.props.newDocument || this.props.newChildDocument; - const isEditing = this.props.match.params.edit; - const isFetching = !this.document && get(this.document, 'isFetching'); + const isNew = this.props.newDocument; + const isEditing = this.props.match.params.edit || isNew; + const isFetching = !this.document; const titleText = get(this.document, 'title', 'Loading'); return ( + {this.state.isDragging && + + Drop files here to import into Atlas. + } {titleText && } {this.state.isLoading && } {isFetching && - + } {!isFetching && this.document && - - - - - - - - - - - - - - - - - {isEditing - ? - : Edit} - - {!isEditing && } - - - } + + + + + + + + + + + + + + + {isEditing + ? + : Edit} + + {!isEditing && } + + + + } ); } } +const DropHere = styled(Flex)` + pointer-events: none; + position: fixed; + top: 0; + left: ${layout.sidebarWidth}; + bottom: 0; + right: 0; + text-align: center; + background: rgba(255,255,255,.9); + z-index: 1; +`; + const Meta = styled(Flex)` - justify-content: ${props => (props.readOnly ? 'space-between' : 'flex-end')}; align-items: flex-start; width: 100%; position: absolute; top: 0; - padding: 10px 20px; + padding: ${layout.padding}; `; const Content = styled(Flex)` @@ -207,6 +248,10 @@ const Container = styled.div` width: 100%; `; +const LoadingState = styled(PreviewLoading)` + margin: 80px 20px; +`; + const PagePadding = styled(Flex)` padding: 80px 20px; position: relative; @@ -220,4 +265,4 @@ const DocumentContainer = styled.div` width: 50em; `; -export default withRouter(inject('ui', 'documents')(Document)); +export default withRouter(inject('ui', 'user', 'documents')(DocumentScene)); diff --git a/frontend/scenes/Document/components/Menu.js b/frontend/scenes/Document/components/Menu.js index 76ac51a91..a5fe09765 100644 --- a/frontend/scenes/Document/components/Menu.js +++ b/frontend/scenes/Document/components/Menu.js @@ -16,9 +16,7 @@ type Props = { props: Props; onCreateDocument = () => { - // Disabled until created a better API - // invariant(this.props.collectionTree, 'collectionTree is not available'); - // this.props.history.push(`${this.props.collectionTree.url}/new`); + this.props.history.push(`${this.props.document.collection.url}/new`); }; onCreateChild = () => { @@ -68,7 +66,6 @@ type Props = { New document - New child
} Export {allowDelete && Delete} diff --git a/frontend/scenes/Flatpage/Flatpage.scss b/frontend/scenes/Flatpage/Flatpage.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/frontend/scenes/Home/Home.js b/frontend/scenes/Home/Home.js index 022f1b943..549fb4435 100644 --- a/frontend/scenes/Home/Home.js +++ b/frontend/scenes/Home/Home.js @@ -2,7 +2,7 @@ import React from 'react'; import { observer, inject } from 'mobx-react'; import { Redirect } from 'react-router'; -import { Flex } from 'reflexbox'; +import Flex from 'components/Flex'; import styled from 'styled-components'; import AuthStore from 'stores/AuthStore'; diff --git a/frontend/scenes/Search/Search.js b/frontend/scenes/Search/Search.js index 4ec9cc78d..6a9f184c9 100644 --- a/frontend/scenes/Search/Search.js +++ b/frontend/scenes/Search/Search.js @@ -3,7 +3,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { observer } from 'mobx-react'; import _ from 'lodash'; -import { Flex } from 'reflexbox'; +import Flex from 'components/Flex'; import { withRouter } from 'react-router'; import { searchUrl } from 'utils/routeHelpers'; import styled from 'styled-components'; diff --git a/frontend/scenes/Search/components/SearchField/SearchField.js b/frontend/scenes/Search/components/SearchField/SearchField.js index 9046e1dbc..785a3a49f 100644 --- a/frontend/scenes/Search/components/SearchField/SearchField.js +++ b/frontend/scenes/Search/components/SearchField/SearchField.js @@ -1,6 +1,6 @@ // @flow import React, { Component } from 'react'; -import { Flex } from 'reflexbox'; +import Flex from 'components/Flex'; import styled from 'styled-components'; import searchImg from 'assets/icons/search.svg'; diff --git a/frontend/scenes/Settings/Settings.js b/frontend/scenes/Settings/Settings.js index 9c27d69e6..38204d7e4 100644 --- a/frontend/scenes/Settings/Settings.js +++ b/frontend/scenes/Settings/Settings.js @@ -1,7 +1,7 @@ // @flow import React from 'react'; import { observer } from 'mobx-react'; -import { Flex } from 'reflexbox'; +import Flex from 'components/Flex'; import ApiKeyRow from './components/ApiKeyRow'; import styles from './Settings.scss'; diff --git a/frontend/stores/DocumentsStore.js b/frontend/stores/DocumentsStore.js index c33195a3c..aaffa4b06 100644 --- a/frontend/stores/DocumentsStore.js +++ b/frontend/stores/DocumentsStore.js @@ -50,10 +50,14 @@ class DocumentsStore { const res = await client.post('/documents.info', { id }); invariant(res && res.data, 'Document not available'); const { data } = res; + const document = new Document(data); + runInAction('DocumentsStore#fetch', () => { - this.data.set(data.id, new Document(data)); + this.data.set(data.id, document); this.isLoaded = true; }); + + return document; } catch (e) { this.errors.add('Failed to load documents'); } diff --git a/frontend/styles/constants.js b/frontend/styles/constants.js index cc9cbcc19..d3a09f953 100644 --- a/frontend/styles/constants.js +++ b/frontend/styles/constants.js @@ -1,5 +1,14 @@ // @flow +export const layout = { + padding: '1.5vw 1.875vw', + vpadding: '1.5vw', + hpadding: '1.875vw', + sidebarWidth: '22%', + sidebarMinWidth: '250px', + sidebarMaxWidth: '350px', +}; + export const size = { tiny: '2px', small: '4px', @@ -28,6 +37,8 @@ export const fontWeight = { }; export const color = { + text: '#171B35', + /* Brand */ primary: '#73DF7B', diff --git a/index.js b/index.js index 0ac7c2d07..9f822e6c2 100644 --- a/index.js +++ b/index.js @@ -1,13 +1,13 @@ require('./init'); -var app = require('./server').default; -var http = require('http'); +const app = require('./server').default; +const http = require('http'); -var server = http.createServer(app.callback()); +const server = http.createServer(app.callback()); server.listen(process.env.PORT || '3000'); -server.on('error', (err) => { +server.on('error', err => { throw err; }); server.on('listening', () => { - var address = server.address(); - console.log('Listening on %s%s', address.address, address.port); -}); \ No newline at end of file + const address = server.address(); + console.log(`Listening on http://localhost:${address.port}`); +}); diff --git a/init.js b/init.js index 2905afe7d..8c5d982b4 100644 --- a/init.js +++ b/init.js @@ -3,4 +3,4 @@ require('safestart')(__dirname, { }); require('babel-core/register'); require('babel-polyfill'); -require('localenv'); +require('dotenv').config({ silent: true }); diff --git a/package.json b/package.json index 3ce6affae..d068d9d5b 100644 --- a/package.json +++ b/package.json @@ -4,11 +4,11 @@ "main": "index.js", "scripts": { "clean": "rimraf dist", - "build:webpack": "cross-env NODE_ENV=production webpack --config webpack.config.prod.js", - "build:analyze": "cross-env NODE_ENV=production webpack --config webpack.config.prod.js --json | webpack-bundle-size-analyzer", + "build:webpack": "NODE_ENV=production webpack --config webpack.config.prod.js", + "build:analyze": "NODE_ENV=production webpack --config webpack.config.prod.js --json | webpack-bundle-size-analyzer", "build": "npm run clean && npm run build:webpack", "start": "node index.js", - "dev": "cross-env NODE_ENV=development DEBUG=sql,cache,presenters ./node_modules/.bin/nodemon --watch server index.js", + "dev": "NODE_ENV=development DEBUG=sql,cache,presenters ./node_modules/.bin/nodemon --watch server index.js", "lint": "npm run lint:js && npm run lint:flow", "lint:js": "eslint frontend", "lint:flow": "flow check", @@ -80,7 +80,6 @@ "boundless-popover": "^1.0.4", "bugsnag": "^1.7.0", "classnames": "2.2.3", - "cross-env": "1.0.7", "css-loader": "0.23.1", "debug": "2.2.0", "dotenv": "^4.0.0", @@ -119,7 +118,6 @@ "koa-mount": "^3.0.0", "koa-router": "7.0.1", "koa-sendfile": "2.0.0", - "localenv": "0.2.2", "lodash": "^4.17.4", "lodash.orderby": "4.4.0", "marked": "0.3.6", @@ -148,7 +146,6 @@ "react-router-dom": "^4.1.1", "redis": "^2.6.2", "redis-lock": "^0.1.0", - "reflexbox": "^2.2.3", "rimraf": "^2.5.4", "safestart": "1.1.0", "sass-loader": "4.0.0", @@ -181,7 +178,6 @@ "fetch-test-server": "^1.1.0", "flow-bin": "^0.45.0", "identity-obj-proxy": "^3.0.0", - "ignore-loader": "0.1.1", "jest-cli": "^20.0.0", "koa-webpack-dev-middleware": "1.4.5", "koa-webpack-hot-middleware": "1.0.3", diff --git a/server/.jestconfig.json b/server/.jestconfig.json index 56da3efdc..383aded9b 100644 --- a/server/.jestconfig.json +++ b/server/.jestconfig.json @@ -5,8 +5,7 @@ "/server" ], "setupFiles": [ - "/__mocks__/console.js", - "./server/test/helper.js" + "/__mocks__/console.js" ], "testEnvironment": "node" -} +} \ No newline at end of file diff --git a/server/api/collections.js b/server/api/collections.js index b129c0852..2b48f40fb 100644 --- a/server/api/collections.js +++ b/server/api/collections.js @@ -24,7 +24,7 @@ router.post('collections.create', auth(), async ctx => { }); ctx.body = { - data: await presentCollection(ctx, atlas, true), + data: await presentCollection(ctx, atlas), }; }); @@ -33,7 +33,7 @@ router.post('collections.info', auth(), async ctx => { ctx.assertPresent(id, 'id is required'); const user = ctx.state.user; - const atlas = await Collection.findOne({ + const atlas = await Collection.scope('withRecentDocuments').findOne({ where: { id, teamId: user.teamId, @@ -43,7 +43,7 @@ router.post('collections.info', auth(), async ctx => { if (!atlas) throw httpErrors.NotFound(); ctx.body = { - data: await presentCollection(ctx, atlas, true), + data: await presentCollection(ctx, atlas), }; }); @@ -58,16 +58,10 @@ router.post('collections.list', auth(), pagination(), async ctx => { limit: ctx.state.pagination.limit, }); - // Collectiones - let data = []; - await Promise.all( - collections.map(async atlas => { - return data.push(await presentCollection(ctx, atlas, true)); - }) + const data = await Promise.all( + collections.map(async atlas => await presentCollection(ctx, atlas)) ); - data = _.orderBy(data, ['updatedAt'], ['desc']); - ctx.body = { pagination: ctx.state.pagination, data, diff --git a/server/api/documents.js b/server/api/documents.js index 5c8e3d7b7..cf5258611 100644 --- a/server/api/documents.js +++ b/server/api/documents.js @@ -8,7 +8,6 @@ import { presentDocument } from '../presenters'; import { Document, Collection, Star, View } from '../models'; const router = new Router(); - router.post('documents.list', auth(), pagination(), async ctx => { let { sort = 'updatedAt', direction } = ctx.body; if (direction !== 'ASC') direction = 'DESC'; @@ -19,9 +18,12 @@ router.post('documents.list', auth(), pagination(), async ctx => { order: [[sort, direction]], offset: ctx.state.pagination.offset, limit: ctx.state.pagination.limit, + include: [{ model: Star, as: 'starred', where: { userId: user.id } }], }); - let data = await Promise.all(documents.map(doc => presentDocument(ctx, doc))); + const data = await Promise.all( + documents.map(document => presentDocument(ctx, document)) + ); ctx.body = { pagination: ctx.state.pagination, @@ -42,7 +44,7 @@ router.post('documents.viewed', auth(), pagination(), async ctx => { limit: ctx.state.pagination.limit, }); - let data = await Promise.all( + const data = await Promise.all( views.map(view => presentDocument(ctx, view.document)) ); @@ -60,12 +62,17 @@ router.post('documents.starred', auth(), pagination(), async ctx => { const views = await Star.findAll({ where: { userId: user.id }, order: [[sort, direction]], - include: [{ model: Document }], + include: [ + { + model: Document, + include: [{ model: Star, as: 'starred', where: { userId: user.id } }], + }, + ], offset: ctx.state.pagination.offset, limit: ctx.state.pagination.limit, }); - let data = await Promise.all( + const data = await Promise.all( views.map(view => presentDocument(ctx, view.document)) ); @@ -94,8 +101,7 @@ router.post('documents.info', auth(), async ctx => { ctx.body = { data: await presentDocument(ctx, document, { - includeCollection: document.private, - includeCollaborators: true, + includeViews: true, }), }; }); @@ -108,16 +114,8 @@ router.post('documents.search', auth(), async ctx => { const documents = await Document.searchForUser(user, query); - const data = []; - await Promise.all( - documents.map(async document => { - data.push( - await presentDocument(ctx, document, { - includeCollection: true, - includeCollaborators: true, - }) - ); - }) + const data = await Promise.all( + documents.map(async document => await presentDocument(ctx, document)) ); ctx.body = { @@ -200,11 +198,7 @@ router.post('documents.create', auth(), async ctx => { } ctx.body = { - data: await presentDocument(ctx, newDocument, { - includeCollection: true, - includeCollaborators: true, - collection: ownerCollection, - }), + data: await presentDocument(ctx, newDocument), }; }); @@ -230,11 +224,7 @@ router.post('documents.update', auth(), async ctx => { } ctx.body = { - data: await presentDocument(ctx, document, { - includeCollection: true, - includeCollaborators: true, - collection: collection, - }), + data: await presentDocument(ctx, document), }; }); @@ -273,11 +263,7 @@ router.post('documents.move', auth(), async ctx => { } ctx.body = { - data: await presentDocument(ctx, document, { - includeCollection: true, - includeCollaborators: true, - collection: collection, - }), + data: await presentDocument(ctx, document), }; }); diff --git a/server/config/database.json b/server/config/database.json index bff4eeb07..7fdc75998 100644 --- a/server/config/database.json +++ b/server/config/database.json @@ -11,4 +11,4 @@ "use_env_variable": "DATABASE_URL", "dialect": "postgres" } -} +} \ No newline at end of file diff --git a/server/middlewares/cache.js b/server/middlewares/cache.js index 7a10ad642..22068f621 100644 --- a/server/middlewares/cache.js +++ b/server/middlewares/cache.js @@ -1,9 +1,10 @@ +// @flow import debug from 'debug'; const debugCache = debug('cache'); export default function cache() { - return async function cacheMiddleware(ctx, next) { + return async function cacheMiddleware(ctx: Object, next: Function) { ctx.cache = {}; ctx.cache.set = async (id, value) => { diff --git a/server/migrations/20170603185012-add-collection-documentStructure-migration.js b/server/migrations/20170603185012-add-collection-documentStructure-migration.js index aa92416d2..671b2e679 100644 --- a/server/migrations/20170603185012-add-collection-documentStructure-migration.js +++ b/server/migrations/20170603185012-add-collection-documentStructure-migration.js @@ -1,14 +1,16 @@ module.exports = { up: (queryInterface, Sequelize) => { - queryInterface.renameTable('atlases', 'collections'); - queryInterface.addColumn('collections', 'documentStructure', { - type: Sequelize.JSONB, - allowNull: true, + queryInterface.renameTable('atlases', 'collections').then(() => { + queryInterface.addColumn('collections', 'documentStructure', { + type: Sequelize.JSONB, + allowNull: true, + }); }); }, down: (queryInterface, _Sequelize) => { - queryInterface.renameTable('collections', 'atlases'); - queryInterface.removeColumn('atlases', 'documentStructure'); + queryInterface.renameTable('collections', 'atlases').then(() => { + queryInterface.removeColumn('atlases', 'documentStructure'); + }); }, }; diff --git a/server/migrations/20170604052346-add-views.js b/server/migrations/20170604052346-add-views.js index cd143c726..cbaea8083 100644 --- a/server/migrations/20170604052346-add-views.js +++ b/server/migrations/20170604052346-add-views.js @@ -1,40 +1,44 @@ module.exports = { up: function(queryInterface, Sequelize) { - queryInterface.createTable('views', { - id: { - type: Sequelize.UUID, - allowNull: false, - primaryKey: true, - }, - documentId: { - type: Sequelize.UUID, - allowNull: false, - }, - userId: { - type: Sequelize.UUID, - allowNull: false, - }, - count: { - type: Sequelize.INTEGER, - allowNull: false, - defaultValue: 1, - }, - createdAt: { - type: Sequelize.DATE, - allowNull: false, - }, - updatedAt: { - type: Sequelize.DATE, - allowNull: false, - }, - }); - queryInterface.addIndex('views', ['documentId', 'userId'], { - indicesType: 'UNIQUE', - }); + queryInterface + .createTable('views', { + id: { + type: Sequelize.UUID, + allowNull: false, + primaryKey: true, + }, + documentId: { + type: Sequelize.UUID, + allowNull: false, + }, + userId: { + type: Sequelize.UUID, + allowNull: false, + }, + count: { + type: Sequelize.INTEGER, + allowNull: false, + defaultValue: 1, + }, + createdAt: { + type: Sequelize.DATE, + allowNull: false, + }, + updatedAt: { + type: Sequelize.DATE, + allowNull: false, + }, + }) + .then(() => { + queryInterface.addIndex('views', ['documentId', 'userId'], { + indicesType: 'UNIQUE', + }); + }); }, down: function(queryInterface, Sequelize) { - queryInterface.removeIndex('views', ['documentId', 'userId']); - queryInterface.dropTable('views'); + queryInterface.removeIndex('views', ['documentId', 'userId']).then(() => { + queryInterface.dropTable('views'); + }); }, }; diff --git a/server/migrations/20170604052347-add-stars.js b/server/migrations/20170604052347-add-stars.js index 78ad77125..1046022d6 100644 --- a/server/migrations/20170604052347-add-stars.js +++ b/server/migrations/20170604052347-add-stars.js @@ -1,35 +1,39 @@ module.exports = { up: function(queryInterface, Sequelize) { - queryInterface.createTable('stars', { - id: { - type: Sequelize.UUID, - allowNull: false, - primaryKey: true, - }, - documentId: { - type: Sequelize.UUID, - allowNull: false, - }, - userId: { - type: Sequelize.UUID, - allowNull: false, - }, - createdAt: { - type: Sequelize.DATE, - allowNull: false, - }, - updatedAt: { - type: Sequelize.DATE, - allowNull: false, - }, - }); - queryInterface.addIndex('stars', ['documentId', 'userId'], { - indicesType: 'UNIQUE', - }); + queryInterface + .createTable('stars', { + id: { + type: Sequelize.UUID, + allowNull: false, + primaryKey: true, + }, + documentId: { + type: Sequelize.UUID, + allowNull: false, + }, + userId: { + type: Sequelize.UUID, + allowNull: false, + }, + createdAt: { + type: Sequelize.DATE, + allowNull: false, + }, + updatedAt: { + type: Sequelize.DATE, + allowNull: false, + }, + }) + .then(() => { + queryInterface.addIndex('stars', ['documentId', 'userId'], { + indicesType: 'UNIQUE', + }); + }); }, down: function(queryInterface, Sequelize) { - queryInterface.removeIndex('stars', ['documentId', 'userId']); - queryInterface.dropTable('stars'); + queryInterface.removeIndex('stars', ['documentId', 'userId']).then(() => { + queryInterface.dropTable('stars'); + }); }, }; diff --git a/server/models/Collection.js b/server/models/Collection.js index 953a57aeb..382b54768 100644 --- a/server/models/Collection.js +++ b/server/models/Collection.js @@ -60,6 +60,16 @@ const Collection = sequelize.define( as: 'documents', foreignKey: 'atlasId', }); + Collection.addScope('withRecentDocuments', { + include: [ + { + as: 'documents', + limit: 10, + model: models.Document, + order: [['updatedAt', 'DESC']], + }, + ], + }); }, }, instanceMethods: { diff --git a/server/models/Document.js b/server/models/Document.js index f0f5e9001..9e7c44a36 100644 --- a/server/models/Document.js +++ b/server/models/Document.js @@ -100,7 +100,7 @@ const Document = sequelize.define( instanceMethods: { getUrl() { const slugifiedTitle = slugify(this.title); - return `/d/${slugifiedTitle}-${this.urlId}`; + return `/doc/${slugifiedTitle}-${this.urlId}`; }, toJSON() { // Warning: only use for new documents as order of children is @@ -115,7 +115,32 @@ const Document = sequelize.define( }, classMethods: { associate: models => { - Document.belongsTo(models.User); + Document.belongsTo(models.Collection, { + as: 'collection', + foreignKey: 'atlasId', + }); + Document.belongsTo(models.User, { + as: 'createdBy', + foreignKey: 'createdById', + }); + Document.belongsTo(models.User, { + as: 'updatedBy', + foreignKey: 'lastModifiedById', + }); + Document.hasMany(models.Star, { + as: 'starred', + }); + Document.addScope( + 'defaultScope', + { + include: [ + { model: models.Collection, as: 'collection' }, + { model: models.User, as: 'createdBy' }, + { model: models.User, as: 'updatedBy' }, + ], + }, + { override: true } + ); }, findById: async id => { if (isUUID(id)) { diff --git a/server/presenters/collection.js b/server/presenters/collection.js index 8dc1777db..a6fdbc8ee 100644 --- a/server/presenters/collection.js +++ b/server/presenters/collection.js @@ -1,8 +1,9 @@ +// @flow import _ from 'lodash'; -import { Document } from '../models'; +import { Collection } from '../models'; import presentDocument from './document'; -async function present(ctx, collection, includeRecentDocuments = false) { +async function present(ctx: Object, collection: Collection) { ctx.cache.set(collection.id, collection); const data = { @@ -13,31 +14,21 @@ async function present(ctx, collection, includeRecentDocuments = false) { type: collection.type, createdAt: collection.createdAt, updatedAt: collection.updatedAt, + recentDocuments: undefined, + documents: undefined, }; - if (collection.type === 'atlas') + if (collection.type === 'atlas') { data.documents = await collection.getDocumentsStructure(); + } - if (includeRecentDocuments) { - const documents = await Document.findAll({ - where: { - atlasId: collection.id, - }, - limit: 10, - order: [['updatedAt', 'DESC']], - }); - - const recentDocuments = []; - await Promise.all( - documents.map(async document => { - recentDocuments.push( - await presentDocument(ctx, document, { - includeCollaborators: true, - }) - ); - }) + if (collection.documents) { + data.recentDocuments = await Promise.all( + collection.documents.map( + async document => + await presentDocument(ctx, document, { includeCollaborators: true }) + ) ); - data.recentDocuments = _.orderBy(recentDocuments, ['updatedAt'], ['desc']); } return data; diff --git a/server/presenters/document.js b/server/presenters/document.js index 8fa79a18d..586072978 100644 --- a/server/presenters/document.js +++ b/server/presenters/document.js @@ -1,26 +1,22 @@ // @flow -import { Collection, Star, User, View, Document } from '../models'; +import _ from 'lodash'; +import { User, Document, View } from '../models'; import presentUser from './user'; import presentCollection from './collection'; -import _ from 'lodash'; type Options = { - includeCollection?: boolean, includeCollaborators?: boolean, includeViews?: boolean, }; -async function present(ctx: Object, document: Document, options: Options) { +async function present(ctx: Object, document: Document, options: ?Options) { options = { - includeCollection: true, includeCollaborators: true, - includeViews: true, + includeViews: false, ...options, }; ctx.cache.set(document.id, document); - - const userId = ctx.state.user.id; - let data = { + const data = { id: document.id, url: document.getUrl(), private: document.private, @@ -29,36 +25,23 @@ async function present(ctx: Object, document: Document, options: Options) { html: document.html, preview: document.preview, createdAt: document.createdAt, - createdBy: undefined, - starred: false, + createdBy: presentUser(ctx, document.createdBy), updatedAt: document.updatedAt, - updatedBy: undefined, + updatedBy: presentUser(ctx, document.updatedBy), team: document.teamId, collaborators: [], + starred: !!document.starred, + collection: undefined, + views: undefined, }; - data.starred = !!await Star.findOne({ - where: { documentId: document.id, userId }, - }); - - if (options.includeViews) { - // $FlowIssue not found in object literal? - data.views = await View.sum('count', { - where: { documentId: document.id }, - }); + if (document.private) { + data.collection = await presentCollection(ctx, document.collection); } - if (options.includeCollection) { - // $FlowIssue not found in object literal? - data.collection = await ctx.cache.get(document.atlasId, async () => { - const collection = - options.collection || - (await Collection.findOne({ - where: { - id: document.atlasId, - }, - })); - return presentCollection(ctx, collection); + if (options.includeViews) { + data.views = await View.sum('count', { + where: { documentId: document.id }, }); } @@ -66,27 +49,13 @@ async function present(ctx: Object, document: Document, options: Options) { // This could be further optimized by using ctx.cache data['collaborators'] = await User.findAll({ where: { - id: { - $in: _.takeRight(document.collaboratorIds, 10) || [], - }, + id: { $in: _.takeRight(document.collaboratorIds, 10) || [] }, }, }).map(user => presentUser(ctx, user)); // $FlowIssue not found in object literal? data.collaboratorCount = document.collaboratorIds.length; } - const createdBy = await ctx.cache.get( - document.createdById, - async () => await User.findById(document.createdById) - ); - data.createdBy = await presentUser(ctx, createdBy); - - const updatedBy = await ctx.cache.get( - document.lastModifiedById, - async () => await User.findById(document.lastModifiedById) - ); - data.updatedBy = await presentUser(ctx, updatedBy); - return data; } diff --git a/server/presenters/team.js b/server/presenters/team.js index c418192b2..6587798ad 100644 --- a/server/presenters/team.js +++ b/server/presenters/team.js @@ -1,4 +1,7 @@ -function present(ctx, team) { +// @flow +import { Team } from '../models'; + +function present(ctx: Object, team: Team) { ctx.cache.set(team.id, team); return { diff --git a/server/static/dev.html b/server/static/dev.html index 04b8c94dd..1933c80b3 100644 --- a/server/static/dev.html +++ b/server/static/dev.html @@ -1,28 +1,33 @@ - - Atlas - - - - -
- - - + body { + display: flex; + width: 100%; + height: 100%; + } + + #root { + flex: 1; + height: 100%; + } + + + + +
+ + + + \ No newline at end of file diff --git a/server/static/index.html b/server/static/index.html index 01e0eca11..0b3a902c5 100644 --- a/server/static/index.html +++ b/server/static/index.html @@ -1,16 +1,30 @@ - - Atlas - - - - - + + + Atlas + + + + +
+ + + \ No newline at end of file diff --git a/server/test/helper.js b/server/test/helper.js index ed178251c..05d110c66 100644 --- a/server/test/helper.js +++ b/server/test/helper.js @@ -1,4 +1,5 @@ -require('localenv'); +// @flow +require('../../init'); // test environment variables process.env.DATABASE_URL = process.env.DATABASE_URL_TEST; diff --git a/webpack.config.js b/webpack.config.js index 4d69a7a44..54b66792a 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -36,13 +36,6 @@ module.exports = { loader: 'url-loader?limit=1&mimetype=application/font-woff&name=public/fonts/[name].[ext]', }, { test: /\.md/, loader: 'raw-loader' }, - - // Excludes - { - // slug - test: /unicode/, - loader: 'ignore-loader', - }, ], }, resolve: { diff --git a/webpack.config.prod.js b/webpack.config.prod.js index 1c4606963..2658cbd8e 100644 --- a/webpack.config.prod.js +++ b/webpack.config.prod.js @@ -41,12 +41,5 @@ productionWebpackConfig.plugins.push( }, }) ); -productionWebpackConfig.plugins.push( - new webpack.DefinePlugin({ - 'process.env': { - NODE_ENV: JSON.stringify('production'), - }, - }) -); module.exports = productionWebpackConfig; diff --git a/yarn.lock b/yarn.lock index 27043076b..28fe5d82c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1639,10 +1639,6 @@ combined-stream@^1.0.5, combined-stream@~1.0.5: dependencies: delayed-stream "~1.0.0" -commander@2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.5.0.tgz#d777b6a4d847d423e5d475da864294ac1ff5aa9d" - commander@2.8.x: version "2.8.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.8.1.tgz#06be367febfda0c330aa1e2a072d3dc9762425d4" @@ -1899,20 +1895,6 @@ create-hmac@^1.1.0, create-hmac@^1.1.2: create-hash "^1.1.0" inherits "^2.0.1" -cross-env@1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-1.0.7.tgz#dd6cea13b31df4ffab4591343e605e370182647e" - dependencies: - cross-spawn-async "2.0.0" - lodash.assign "^3.2.0" - -cross-spawn-async@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/cross-spawn-async/-/cross-spawn-async-2.0.0.tgz#4af143df4156900d012be41cabf4da3abfc797c0" - dependencies: - lru-cache "^2.6.5" - which "^1.1.1" - cross-spawn@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982" @@ -3970,10 +3952,6 @@ ignore-by-default@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" -ignore-loader@0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ignore-loader/-/ignore-loader-0.1.1.tgz#187c846c661afcdee269ef4c3f42c73888903334" - ignore@^3.2.0: version "3.2.7" resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.2.7.tgz#4810ca5f1d8eca5595213a34b94f2eb4ed926bbd" @@ -5165,12 +5143,6 @@ loader-utils@0.2.x, loader-utils@^0.2.11, loader-utils@^0.2.14, loader-utils@^0. json5 "^0.5.0" object-assign "^4.0.1" -localenv@0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/localenv/-/localenv-0.2.2.tgz#c508f29d3485bdc9341d3ead17f61c5abd1b0bab" - dependencies: - commander "2.5.0" - locate-path@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" @@ -5293,7 +5265,7 @@ lodash._topath@^3.0.0: dependencies: lodash.isarray "^3.0.0" -lodash.assign@^3.0.0, lodash.assign@^3.2.0: +lodash.assign@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-3.2.0.tgz#3ce9f0234b4b2223e296b8fa0ac1fee8ebca64fa" dependencies: @@ -5564,7 +5536,7 @@ lowercase-keys@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" -lru-cache@2, lru-cache@^2.6.5: +lru-cache@2: version "2.7.3" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952" @@ -8969,7 +8941,7 @@ which-module@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" -which@1, which@^1.0.5, which@^1.1.1, which@^1.2.10, which@^1.2.12, which@^1.2.9: +which@1, which@^1.0.5, which@^1.2.10, which@^1.2.12, which@^1.2.9: version "1.2.14" resolved "https://registry.yarnpkg.com/which/-/which-1.2.14.tgz#9a87c4378f03e827cecaf1acdf56c736c01c14e5" dependencies: