From 8f5869c48d7fdcb78099930dbae0a75481737661 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sun, 19 Nov 2017 20:16:49 -0800 Subject: [PATCH 01/16] WIP: Collection home --- .../CenteredContent/CenteredContent.js | 2 +- app/components/Editor/Editor.js | 6 +- app/components/Icon/CollectionIcon.js | 2 +- app/components/Subheading/Subheading.js | 15 ++++ app/components/Subheading/index.js | 3 + app/models/Collection.js | 5 ++ app/scenes/Collection/Collection.js | 72 +++++++++++------- app/scenes/Dashboard/Dashboard.js | 46 ++++-------- app/scenes/Document/Document.js | 1 - app/stores/CollectionsStore.js | 74 ++++++++++--------- app/stores/UiStore.js | 6 ++ shared/styles/constants.js | 2 +- 12 files changed, 134 insertions(+), 100 deletions(-) create mode 100644 app/components/Subheading/Subheading.js create mode 100644 app/components/Subheading/index.js diff --git a/app/components/CenteredContent/CenteredContent.js b/app/components/CenteredContent/CenteredContent.js index 578d00556..bcfaedb8f 100644 --- a/app/components/CenteredContent/CenteredContent.js +++ b/app/components/CenteredContent/CenteredContent.js @@ -12,7 +12,7 @@ const Container = styled.div` `; const Content = styled.div` - max-width: 50em; + max-width: 46em; margin: 0 auto; `; diff --git a/app/components/Editor/Editor.js b/app/components/Editor/Editor.js index 50a5805fe..4e4d71293 100644 --- a/app/components/Editor/Editor.js +++ b/app/components/Editor/Editor.js @@ -228,8 +228,8 @@ class MarkdownEditor extends Component { } const MaxWidth = styled(Flex)` - padding: 0 60px; - max-width: 50em; + margin: 0 60px; + max-width: 46em; height: 100%; `; @@ -281,6 +281,8 @@ const StyledEditor = styled(Editor)` p { position: relative; + margin-top: 1.2em; + margin-bottom: 1.2em; } a:hover { diff --git a/app/components/Icon/CollectionIcon.js b/app/components/Icon/CollectionIcon.js index f419be0dc..251824194 100644 --- a/app/components/Icon/CollectionIcon.js +++ b/app/components/Icon/CollectionIcon.js @@ -6,7 +6,7 @@ import type { Props } from './Icon'; export default function CollectionIcon({ expanded, ...rest -}: Props & { expanded: boolean }) { +}: Props & { expanded?: boolean }) { return ( {expanded ? ( diff --git a/app/components/Subheading/Subheading.js b/app/components/Subheading/Subheading.js new file mode 100644 index 000000000..fdc716489 --- /dev/null +++ b/app/components/Subheading/Subheading.js @@ -0,0 +1,15 @@ +// @flow +import styled from 'styled-components'; + +const Subheading = styled.h3` + font-size: 11px; + font-weight: 500; + text-transform: uppercase; + color: #9fa6ab; + letter-spacing: 0.04em; + border-bottom: 1px solid #ddd; + padding-bottom: 10px; + margin-top: 30px; +`; + +export default Subheading; diff --git a/app/components/Subheading/index.js b/app/components/Subheading/index.js new file mode 100644 index 000000000..d0f6a76e7 --- /dev/null +++ b/app/components/Subheading/index.js @@ -0,0 +1,3 @@ +// @flow +import Subheading from './Subheading'; +export default Subheading; diff --git a/app/models/Collection.js b/app/models/Collection.js index 96a1c57e2..e7a4013f2 100644 --- a/app/models/Collection.js +++ b/app/models/Collection.js @@ -39,6 +39,11 @@ class Collection extends BaseModel { return true; } + @computed + get isEmpty(): boolean { + return this.documents.length === 0; + } + /* Actions */ @action diff --git a/app/scenes/Collection/Collection.js b/app/scenes/Collection/Collection.js index 193e58de3..6cd13ab77 100644 --- a/app/scenes/Collection/Collection.js +++ b/app/scenes/Collection/Collection.js @@ -2,20 +2,26 @@ import React, { Component } from 'react'; import { observable } from 'mobx'; import { observer, inject } from 'mobx-react'; -import { Link, Redirect } from 'react-router-dom'; +import { Link } from 'react-router-dom'; import styled from 'styled-components'; import { newDocumentUrl } from 'utils/routeHelpers'; import CollectionsStore from 'stores/CollectionsStore'; +import UiStore from 'stores/UiStore'; import Collection from 'models/Collection'; +import Search from 'scenes/Search'; import CenteredContent from 'components/CenteredContent'; +import CollectionIcon from 'components/Icon/CollectionIcon'; import LoadingListPlaceholder from 'components/LoadingListPlaceholder'; import Button from 'components/Button'; import HelpText from 'components/HelpText'; +import Subheading from 'components/Subheading'; +import PageTitle from 'components/PageTitle'; import Flex from 'shared/components/Flex'; type Props = { + ui: UiStore, collections: CollectionsStore, match: Object, }; @@ -24,29 +30,27 @@ type Props = { class CollectionScene extends Component { props: Props; @observable collection: ?Collection; - @observable isFetching = true; - @observable redirectUrl; + @observable isFetching: boolean = true; componentDidMount = () => { - this.fetchDocument(this.props.match.params.id); + this.fetchCollection(this.props.match.params.id); }; componentWillReceiveProps(nextProps) { if (nextProps.match.params.id !== this.props.match.params.id) { - this.fetchDocument(nextProps.match.params.id); + this.fetchCollection(nextProps.match.params.id); } } - fetchDocument = async (id: string) => { + fetchCollection = async (id: string) => { const { collections } = this.props; - this.collection = await collections.fetchById(id); - - if (!this.collection) this.redirectUrl = '/404'; - - if (this.collection && this.collection.documents.length > 0) { - this.redirectUrl = this.collection.documents[0].url; + const collection = await collections.fetch(id); + if (collection) { + this.props.ui.setActiveCollection(collection); + this.collection = collection; } + this.isFetching = false; }; @@ -54,43 +58,57 @@ class CollectionScene extends Component { if (!this.collection) return; return ( - -

Create a document

+ + + + {' '} + {this.collection.name} + - Publish your first document to start building the{' '} - {this.collection.name} collection. + Publish your first document to start building this collection. -
+ ); } + renderNotFound() { + return ; + } + render() { - if (this.redirectUrl) return ; + if (this.isFetching) return ; + if (!this.collection) return this.renderNotFound(); + if (this.collection.isEmpty) return this.renderEmptyCollection(); return ( - {this.isFetching ? ( - - ) : ( - this.renderEmptyCollection() - )} + + + {' '} + {this.collection.name} + + Recently edited ); } } -const NewDocumentContainer = styled(Flex)` - padding-top: 50%; - transform: translateY(-50%); +const Heading = styled.h1` + display: flex; + + svg { + margin-left: -6px; + margin-right: 6px; + } `; const Action = styled(Flex)` margin: 10px 0; `; -export default inject('collections')(CollectionScene); +export default inject('collections', 'ui')(CollectionScene); diff --git a/app/scenes/Dashboard/Dashboard.js b/app/scenes/Dashboard/Dashboard.js index 19a195c98..1eb9d07f5 100644 --- a/app/scenes/Dashboard/Dashboard.js +++ b/app/scenes/Dashboard/Dashboard.js @@ -2,26 +2,14 @@ import React, { Component } from 'react'; import { observable } from 'mobx'; import { observer, inject } from 'mobx-react'; -import styled from 'styled-components'; import DocumentsStore from 'stores/DocumentsStore'; -import Flex from 'shared/components/Flex'; +import CenteredContent from 'components/CenteredContent'; import DocumentList from 'components/DocumentList'; import PageTitle from 'components/PageTitle'; -import CenteredContent from 'components/CenteredContent'; +import Subheading from 'components/Subheading'; import { ListPlaceholder } from 'components/LoadingPlaceholder'; -const Subheading = styled.h3` - font-size: 11px; - font-weight: 500; - text-transform: uppercase; - color: #9fa6ab; - letter-spacing: 0.04em; - border-bottom: 1px solid #ddd; - padding-bottom: 10px; - margin-top: 30px; -`; - type Props = { documents: DocumentsStore, }; @@ -45,30 +33,26 @@ class Dashboard extends Component { render() { const { documents } = this.props; - const recentlyViewedLoaded = documents.recentlyViewed.length > 0; - const recentlyEditedLoaded = documents.recentlyEdited.length > 0; + const hasRecentlyViewed = documents.recentlyViewed.length > 0; + const hasRecentlyEdited = documents.recentlyEdited.length > 0; const showContent = - this.isLoaded || (recentlyViewedLoaded && recentlyEditedLoaded); + this.isLoaded || (hasRecentlyViewed && hasRecentlyEdited); return (

Home

{showContent ? ( - - {recentlyViewedLoaded && ( - - Recently viewed - - - )} - {recentlyEditedLoaded && ( - - Recently edited - - - )} - + + {hasRecentlyViewed && [ + Recently viewed, + , + ]} + {hasRecentlyEdited && [ + Recently edited, + , + ]} + ) : ( )} diff --git a/app/scenes/Document/Document.js b/app/scenes/Document/Document.js index a95340d23..ee86195da 100644 --- a/app/scenes/Document/Document.js +++ b/app/scenes/Document/Document.js @@ -44,7 +44,6 @@ type Props = { match: Object, history: Object, location: Location, - keydown: Object, documents: DocumentsStore, collections: CollectionsStore, newDocument?: boolean, diff --git a/app/stores/CollectionsStore.js b/app/stores/CollectionsStore.js index 18b205af4..18e7c2ff7 100644 --- a/app/stores/CollectionsStore.js +++ b/app/stores/CollectionsStore.js @@ -1,11 +1,5 @@ // @flow -import { - observable, - computed, - action, - runInAction, - ObservableArray, -} from 'mobx'; +import { observable, computed, action, runInAction, ObservableMap } from 'mobx'; import ApiClient, { client } from 'utils/ApiClient'; import _ from 'lodash'; import invariant from 'invariant'; @@ -13,12 +7,10 @@ import invariant from 'invariant'; import stores from 'stores'; import Collection from 'models/Collection'; import ErrorsStore from 'stores/ErrorsStore'; -import CacheStore from 'stores/CacheStore'; import UiStore from 'stores/UiStore'; type Options = { teamId: string, - cache: CacheStore, ui: UiStore, }; @@ -30,17 +22,17 @@ type DocumentPathItem = { }; export type DocumentPath = DocumentPathItem & { - path: Array, + path: DocumentPathItem[], }; class CollectionsStore { - @observable data: ObservableArray = observable.array([]); + @observable data: Map = new ObservableMap([]); @observable isLoaded: boolean = false; + @observable isFetching: boolean = false; client: ApiClient; teamId: string; errors: ErrorsStore; - cache: CacheStore; ui: UiStore; @computed @@ -52,7 +44,7 @@ class CollectionsStore { @computed get orderedData(): Collection[] { - return _.sortBy(this.data, 'name'); + return _.sortBy(this.data.values(), 'name'); } /** @@ -100,6 +92,8 @@ class CollectionsStore { @action fetchAll = async (): Promise<*> => { + this.isFetching = true; + try { const res = await this.client.post('/collections.list', { id: this.teamId, @@ -107,56 +101,64 @@ class CollectionsStore { invariant(res && res.data, 'Collection list not available'); const { data } = res; runInAction('CollectionsStore#fetch', () => { - this.data.replace(data.map(collection => new Collection(collection))); + data.forEach(collection => { + this.data.set(collection.id, new Collection(collection)); + }); this.isLoaded = true; }); } catch (e) { this.errors.add('Failed to load collections'); + } finally { + this.isFetching = false; } }; @action - fetchById = async (id: string): Promise => { + fetch = async (id: string): Promise => { let collection = this.getById(id); - if (!collection) { - try { - const res = await this.client.post('/collections.info', { - id, - }); - invariant(res && res.data, 'Collection not available'); - const { data } = res; - runInAction('CollectionsStore#getById', () => { - collection = new Collection(data); - this.add(collection); - }); - } catch (e) { - Bugsnag.notify(e); - this.errors.add('Something went wrong'); - } - } + if (collection) return collection; - return collection; + this.isFetching = true; + + try { + const res = await this.client.post('/collections.info', { + id, + }); + invariant(res && res.data, 'Collection not available'); + const { data } = res; + const collection = new Collection(data); + + runInAction('CollectionsStore#fetch', () => { + this.data.set(data.id, collection); + this.isLoaded = true; + }); + + return collection; + } catch (e) { + this.errors.add('Something went wrong'); + } finally { + this.isFetching = false; + } }; @action add = (collection: Collection): void => { - this.data.push(collection); + this.data.set(collection.id, collection); }; @action remove = (id: string): void => { - this.data.splice(this.data.indexOf(id), 1); + this.data.delete(id); }; getById = (id: string): ?Collection => { - return _.find(this.data, { id }); + return this.data.get(id); }; constructor(options: Options) { this.client = client; this.errors = stores.errors; this.teamId = options.teamId; - this.cache = options.cache; this.ui = options.ui; } } diff --git a/app/stores/UiStore.js b/app/stores/UiStore.js index 10acfe2fd..3fc05269d 100644 --- a/app/stores/UiStore.js +++ b/app/stores/UiStore.js @@ -1,6 +1,7 @@ // @flow import { observable, action } from 'mobx'; import Document from 'models/Document'; +import Collection from 'models/Collection'; class UiStore { @observable activeModalName: ?string; @@ -29,6 +30,11 @@ class UiStore { this.activeCollectionId = document.collection.id; }; + @action + setActiveCollection = (collection: Collection): void => { + this.activeCollectionId = collection.id; + }; + @action clearActiveDocument = (): void => { this.activeDocumentId = undefined; diff --git a/shared/styles/constants.js b/shared/styles/constants.js index 3a1496567..303d6beee 100644 --- a/shared/styles/constants.js +++ b/shared/styles/constants.js @@ -45,7 +45,7 @@ export const color = { text: '#171B35', /* Brand */ - primary: '#2B8FBF', + primary: '#1AB6FF', danger: '#D0021B', warning: '#f08a24' /* replace */, success: '#43AC6A' /* replace */, From c0b2accb18cf6c2a33674a0ed8712b11ed51540d Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sun, 19 Nov 2017 20:44:01 -0800 Subject: [PATCH 02/16] Remove unused teamId --- app/components/ColorPicker/ColorPicker.js | 4 ++++ app/stores/CollectionsStore.js | 7 +------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/app/components/ColorPicker/ColorPicker.js b/app/components/ColorPicker/ColorPicker.js index cd005ede6..f359312d8 100644 --- a/app/components/ColorPicker/ColorPicker.js +++ b/app/components/ColorPicker/ColorPicker.js @@ -157,6 +157,10 @@ const SwatchInset = styled(Flex)` const StyledOutline = styled(Outline)` padding: 5px; + + strong { + font-weight: 500; + } `; const HexHash = styled.div` diff --git a/app/stores/CollectionsStore.js b/app/stores/CollectionsStore.js index 18e7c2ff7..0a3880d28 100644 --- a/app/stores/CollectionsStore.js +++ b/app/stores/CollectionsStore.js @@ -10,7 +10,6 @@ import ErrorsStore from 'stores/ErrorsStore'; import UiStore from 'stores/UiStore'; type Options = { - teamId: string, ui: UiStore, }; @@ -31,7 +30,6 @@ class CollectionsStore { @observable isFetching: boolean = false; client: ApiClient; - teamId: string; errors: ErrorsStore; ui: UiStore; @@ -95,9 +93,7 @@ class CollectionsStore { this.isFetching = true; try { - const res = await this.client.post('/collections.list', { - id: this.teamId, - }); + const res = await this.client.post('/collections.list'); invariant(res && res.data, 'Collection list not available'); const { data } = res; runInAction('CollectionsStore#fetch', () => { @@ -158,7 +154,6 @@ class CollectionsStore { constructor(options: Options) { this.client = client; this.errors = stores.errors; - this.teamId = options.teamId; this.ui = options.ui; } } From eb8b12d3275ccd322732bd73e32522a248247eca Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sun, 19 Nov 2017 21:32:18 -0800 Subject: [PATCH 03/16] Working recently edited list --- app/models/Document.js | 1 + app/scenes/Collection/Collection.js | 25 +++++++++++++++++++------ app/stores/CollectionsStore.js | 2 +- app/stores/DocumentsStore.js | 11 +++++++++++ server/api/documents.js | 7 +++++-- server/api/documents.test.js | 11 +++++++++++ server/presenters/document.js | 1 + 7 files changed, 49 insertions(+), 9 deletions(-) diff --git a/app/models/Document.js b/app/models/Document.js index 00397a5de..c67b1c580 100644 --- a/app/models/Document.js +++ b/app/models/Document.js @@ -20,6 +20,7 @@ class Document extends BaseModel { collaborators: Array; collection: $Shape; + collectionId: string; firstViewedAt: ?string; lastViewedAt: ?string; modifiedSinceViewed: ?boolean; diff --git a/app/scenes/Collection/Collection.js b/app/scenes/Collection/Collection.js index 6cd13ab77..f26503379 100644 --- a/app/scenes/Collection/Collection.js +++ b/app/scenes/Collection/Collection.js @@ -7,6 +7,7 @@ import styled from 'styled-components'; import { newDocumentUrl } from 'utils/routeHelpers'; import CollectionsStore from 'stores/CollectionsStore'; +import DocumentsStore from 'stores/DocumentsStore'; import UiStore from 'stores/UiStore'; import Collection from 'models/Collection'; @@ -16,12 +17,14 @@ import CollectionIcon from 'components/Icon/CollectionIcon'; import LoadingListPlaceholder from 'components/LoadingListPlaceholder'; import Button from 'components/Button'; import HelpText from 'components/HelpText'; +import DocumentList from 'components/DocumentList'; import Subheading from 'components/Subheading'; import PageTitle from 'components/PageTitle'; import Flex from 'shared/components/Flex'; type Props = { ui: UiStore, + documents: DocumentsStore, collections: CollectionsStore, match: Object, }; @@ -32,23 +35,28 @@ class CollectionScene extends Component { @observable collection: ?Collection; @observable isFetching: boolean = true; - componentDidMount = () => { - this.fetchCollection(this.props.match.params.id); - }; + componentDidMount() { + this.loadContent(this.props.match.params.id); + } componentWillReceiveProps(nextProps) { if (nextProps.match.params.id !== this.props.match.params.id) { - this.fetchCollection(nextProps.match.params.id); + this.loadContent(nextProps.match.params.id); } } - fetchCollection = async (id: string) => { + loadContent = async (id: string) => { const { collections } = this.props; const collection = await collections.fetch(id); + if (collection) { this.props.ui.setActiveCollection(collection); this.collection = collection; + await this.props.documents.fetchRecentlyModified({ + limit: 5, + collectionId: collection.id, + }); } this.isFetching = false; @@ -93,6 +101,11 @@ class CollectionScene extends Component { {this.collection.name} Recently edited +
); } @@ -111,4 +124,4 @@ const Action = styled(Flex)` margin: 10px 0; `; -export default inject('collections', 'ui')(CollectionScene); +export default inject('collections', 'documents', 'ui')(CollectionScene); diff --git a/app/stores/CollectionsStore.js b/app/stores/CollectionsStore.js index 0a3880d28..67973f4b7 100644 --- a/app/stores/CollectionsStore.js +++ b/app/stores/CollectionsStore.js @@ -96,7 +96,7 @@ class CollectionsStore { const res = await this.client.post('/collections.list'); invariant(res && res.data, 'Collection list not available'); const { data } = res; - runInAction('CollectionsStore#fetch', () => { + runInAction('CollectionsStore#fetchAll', () => { data.forEach(collection => { this.data.set(collection.id, new Collection(collection)); }); diff --git a/app/stores/DocumentsStore.js b/app/stores/DocumentsStore.js index 99235facd..4c05276b1 100644 --- a/app/stores/DocumentsStore.js +++ b/app/stores/DocumentsStore.js @@ -52,6 +52,17 @@ class DocumentsStore extends BaseStore { return _.take(_.orderBy(this.data.values(), 'updatedAt', 'desc'), 5); } + recentlyEditedInCollection(collectionId: string): Array { + return _.orderBy( + _.filter( + this.data.values(), + document => document.collectionId === collectionId + ), + 'updatedAt', + 'desc' + ); + } + @computed get starred(): Array { return _.filter(this.data.values(), 'starred'); diff --git a/server/api/documents.js b/server/api/documents.js index 5e098ca8b..f3f112827 100644 --- a/server/api/documents.js +++ b/server/api/documents.js @@ -15,14 +15,17 @@ const authDocumentForUser = (ctx, document) => { const router = new Router(); router.post('documents.list', auth(), pagination(), async ctx => { - let { sort = 'updatedAt', direction } = ctx.body; + let { sort = 'updatedAt', direction, collectionId } = ctx.body; if (direction !== 'ASC') direction = 'DESC'; const user = ctx.state.user; + let where = { teamId: user.teamId }; + if (collectionId) where = { ...where, atlasId: collectionId }; + const userId = user.id; const starredScope = { method: ['withStarred', userId] }; const documents = await Document.scope('defaultScope', starredScope).findAll({ - where: { teamId: user.teamId }, + where, order: [[sort, direction]], offset: ctx.state.pagination.offset, limit: ctx.state.pagination.limit, diff --git a/server/api/documents.test.js b/server/api/documents.test.js index 7d2bc37df..82380eed4 100644 --- a/server/api/documents.test.js +++ b/server/api/documents.test.js @@ -34,6 +34,17 @@ describe('#documents.list', async () => { expect(body.data[1].id).toEqual(document.id); }); + it('should allow filtering by collection', async () => { + const { user, document } = await seed(); + const res = await server.post('/api/documents.list', { + body: { token: user.getJwtToken(), collectionId: document.atlasId }, + }); + const body = await res.json(); + + expect(res.status).toEqual(200); + expect(body.data.length).toEqual(2); + }); + it('should require authentication', async () => { const res = await server.post('/api/documents.list'); const body = await res.json(); diff --git a/server/presenters/document.js b/server/presenters/document.js index 575d18f3b..29ddff81d 100644 --- a/server/presenters/document.js +++ b/server/presenters/document.js @@ -36,6 +36,7 @@ async function present(ctx: Object, document: Document, options: ?Options) { team: document.teamId, collaborators: [], starred: !!(document.starred && document.starred.length), + collectionId: document.atlasId, collaboratorCount: undefined, collection: undefined, views: undefined, From 113974ee190e6eb505d509b5b76943f8aecc8494 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sun, 19 Nov 2017 21:50:42 -0800 Subject: [PATCH 04/16] Improved loading state --- .../LoadingListPlaceholder.js | 40 ----------------- .../LoadingListPlaceholder/index.js | 3 -- app/scenes/Collection/Collection.js | 45 ++++++++++++------- 3 files changed, 29 insertions(+), 59 deletions(-) delete mode 100644 app/components/LoadingListPlaceholder/LoadingListPlaceholder.js delete mode 100644 app/components/LoadingListPlaceholder/index.js diff --git a/app/components/LoadingListPlaceholder/LoadingListPlaceholder.js b/app/components/LoadingListPlaceholder/LoadingListPlaceholder.js deleted file mode 100644 index 2f81df862..000000000 --- a/app/components/LoadingListPlaceholder/LoadingListPlaceholder.js +++ /dev/null @@ -1,40 +0,0 @@ -// @flow -import React from 'react'; -import styled from 'styled-components'; -import { pulsate } from 'shared/styles/animations'; -import { color } from 'shared/styles/constants'; -import Flex from 'shared/components/Flex'; -import Fade from 'components/Fade'; - -import { randomInteger } from 'shared/random'; - -const randomValues = Array.from( - new Array(5), - () => `${randomInteger(85, 100)}%` -); - -export default (props: Object) => { - return ( - - - - - - - - - - - ); -}; - -const Item = styled(Flex)` - padding: 18px 0; -`; - -const Mask = styled(Flex)` - height: ${props => (props.header ? 28 : 18)}px; - margin-bottom: ${props => (props.header ? 18 : 0)}px; - background-color: ${color.smoke}; - animation: ${pulsate} 1.3s infinite; -`; diff --git a/app/components/LoadingListPlaceholder/index.js b/app/components/LoadingListPlaceholder/index.js deleted file mode 100644 index 17588c5a6..000000000 --- a/app/components/LoadingListPlaceholder/index.js +++ /dev/null @@ -1,3 +0,0 @@ -// @flow -import LoadingListPlaceholder from './LoadingListPlaceholder'; -export default LoadingListPlaceholder; diff --git a/app/scenes/Collection/Collection.js b/app/scenes/Collection/Collection.js index f26503379..f176004b3 100644 --- a/app/scenes/Collection/Collection.js +++ b/app/scenes/Collection/Collection.js @@ -14,7 +14,7 @@ import Collection from 'models/Collection'; import Search from 'scenes/Search'; import CenteredContent from 'components/CenteredContent'; import CollectionIcon from 'components/Icon/CollectionIcon'; -import LoadingListPlaceholder from 'components/LoadingListPlaceholder'; +import { ListPlaceholder } from 'components/LoadingPlaceholder'; import Button from 'components/Button'; import HelpText from 'components/HelpText'; import DocumentList from 'components/DocumentList'; @@ -48,7 +48,7 @@ class CollectionScene extends Component { loadContent = async (id: string) => { const { collections } = this.props; - const collection = await collections.fetch(id); + const collection = collections.getById(id) || (await collections.fetch(id)); if (collection) { this.props.ui.setActiveCollection(collection); @@ -89,23 +89,36 @@ class CollectionScene extends Component { } render() { - if (this.isFetching) return ; - if (!this.collection) return this.renderNotFound(); - if (this.collection.isEmpty) return this.renderEmptyCollection(); + if (!this.isFetching && !this.collection) { + return this.renderNotFound(); + } + if (this.collection && this.collection.isEmpty) { + return this.renderEmptyCollection(); + } return ( - - - {' '} - {this.collection.name} - - Recently edited - + {this.collection ? ( + + + + {' '} + {this.collection.name} + + Recently edited + + + ) : ( + + )} ); } From a8b6b51aa6cee739af457e2028c091c247128c5a Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sun, 19 Nov 2017 23:39:56 -0800 Subject: [PATCH 05/16] Add collection actions --- app/components/Actions/Actions.js | 34 ++++++++++ app/components/Actions/index.js | 4 ++ app/scenes/Collection/Collection.js | 35 ++++++++-- app/scenes/Document/Document.js | 101 ++++++++++------------------ 4 files changed, 103 insertions(+), 71 deletions(-) create mode 100644 app/components/Actions/Actions.js create mode 100644 app/components/Actions/index.js diff --git a/app/components/Actions/Actions.js b/app/components/Actions/Actions.js new file mode 100644 index 000000000..7296f1d55 --- /dev/null +++ b/app/components/Actions/Actions.js @@ -0,0 +1,34 @@ +// @flow +import styled from 'styled-components'; +import Flex from 'shared/components/Flex'; +import { layout, color } from 'shared/styles/constants'; + +export const Action = styled(Flex)` + justify-content: center; + align-items: center; + padding: 0 0 0 10px; + + a { + color: ${color.text}; + height: 24px; + } +`; + +export const Separator = styled.div` + margin-left: 12px; + width: 1px; + height: 20px; + background: ${color.slateLight}; +`; + +const Actions = styled(Flex)` + position: fixed; + top: 0; + right: 0; + padding: ${layout.vpadding} ${layout.hpadding} 8px 8px; + border-radius: 3px; + background: rgba(255, 255, 255, 0.9); + -webkit-backdrop-filter: blur(20px); +`; + +export default Actions; diff --git a/app/components/Actions/index.js b/app/components/Actions/index.js new file mode 100644 index 000000000..71c56fa85 --- /dev/null +++ b/app/components/Actions/index.js @@ -0,0 +1,4 @@ +// @flow +import Actions from './Actions'; +export { Action, Separator } from './Actions'; +export default Actions; diff --git a/app/scenes/Collection/Collection.js b/app/scenes/Collection/Collection.js index f176004b3..67717ba61 100644 --- a/app/scenes/Collection/Collection.js +++ b/app/scenes/Collection/Collection.js @@ -2,7 +2,7 @@ import React, { Component } from 'react'; import { observable } from 'mobx'; import { observer, inject } from 'mobx-react'; -import { Link } from 'react-router-dom'; +import { withRouter, Link } from 'react-router-dom'; import styled from 'styled-components'; import { newDocumentUrl } from 'utils/routeHelpers'; @@ -12,8 +12,11 @@ import UiStore from 'stores/UiStore'; import Collection from 'models/Collection'; import Search from 'scenes/Search'; +import CollectionMenu from 'menus/CollectionMenu'; +import Actions, { Action, Separator } from 'components/Actions'; import CenteredContent from 'components/CenteredContent'; import CollectionIcon from 'components/Icon/CollectionIcon'; +import NewDocumentIcon from 'components/Icon/NewDocumentIcon'; import { ListPlaceholder } from 'components/LoadingPlaceholder'; import Button from 'components/Button'; import HelpText from 'components/HelpText'; @@ -26,6 +29,7 @@ type Props = { ui: UiStore, documents: DocumentsStore, collections: CollectionsStore, + history: Object, match: Object, }; @@ -62,6 +66,14 @@ class CollectionScene extends Component { this.isFetching = false; }; + onNewDocument = (ev: SyntheticEvent) => { + ev.preventDefault(); + + if (this.collection) { + this.props.history.push(`${this.collection.url}/new`); + } + }; + renderEmptyCollection() { if (!this.collection) return; @@ -75,11 +87,11 @@ class CollectionScene extends Component { Publish your first document to start building this collection. - + - + ); } @@ -115,6 +127,17 @@ class CollectionScene extends Component { this.collection.id )} /> + + + + + + + + + + + ) : ( @@ -133,8 +156,10 @@ const Heading = styled.h1` } `; -const Action = styled(Flex)` +const Wrapper = styled(Flex)` margin: 10px 0; `; -export default inject('collections', 'documents', 'ui')(CollectionScene); +export default withRouter( + inject('collections', 'documents', 'ui')(CollectionScene) +); diff --git a/app/scenes/Document/Document.js b/app/scenes/Document/Document.js index ee86195da..60f6e2f63 100644 --- a/app/scenes/Document/Document.js +++ b/app/scenes/Document/Document.js @@ -8,7 +8,6 @@ import { withRouter, Prompt } from 'react-router-dom'; import type { Location } from 'react-router-dom'; import keydown from 'react-keydown'; import Flex from 'shared/components/Flex'; -import { color, layout } from 'shared/styles/constants'; import { collectionUrl, updateDocumentUrl, @@ -33,6 +32,7 @@ import Collaborators from 'components/Collaborators'; import CenteredContent from 'components/CenteredContent'; import PageTitle from 'components/PageTitle'; import NewDocumentIcon from 'components/Icon/NewDocumentIcon'; +import Actions, { Action, Separator } from 'components/Actions'; import Search from 'scenes/Search'; const DISCARD_CHANGES = ` @@ -241,49 +241,47 @@ class DocumentScene extends Component { onCancel={this.onDiscard} readOnly={!this.isEditing} /> - - - {!isNew && - !this.isEditing && } - - {this.isEditing ? ( - - ) : ( - Edit - )} - - {this.isEditing && ( - - Discard - + {!isNew && + !this.isEditing && } + + {this.isEditing ? ( + + ) : ( + Edit )} + + {this.isEditing && ( + + Discard + + )} + {!this.isEditing && ( + + + + )} + {!this.isEditing && } + {!this.isEditing && ( - - - + + + )} - {!this.isEditing && } - - {!this.isEditing && ( - - - - )} - - - + + )} @@ -291,35 +289,6 @@ class DocumentScene extends Component { } } -const Separator = styled.div` - margin-left: 12px; - width: 1px; - height: 20px; - background: ${color.slateLight}; -`; - -const HeaderAction = styled(Flex)` - justify-content: center; - align-items: center; - padding: 0 0 0 10px; - - a { - color: ${color.text}; - height: 24px; - } -`; - -const Meta = styled(Flex)` - align-items: flex-start; - position: fixed; - top: 0; - right: 0; - padding: ${layout.vpadding} ${layout.hpadding} 8px 8px; - border-radius: 3px; - background: rgba(255, 255, 255, 0.9); - -webkit-backdrop-filter: blur(20px); -`; - const Container = styled(Flex)` position: relative; width: 100%; From 1e75ce74e2ffbbfa8334fe937d3b30a3cce6c20b Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Mon, 20 Nov 2017 00:09:46 -0800 Subject: [PATCH 06/16] :green_heart: --- app/stores/CollectionsStore.test.js | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/app/stores/CollectionsStore.test.js b/app/stores/CollectionsStore.test.js index 358f5bd5b..674843847 100644 --- a/app/stores/CollectionsStore.test.js +++ b/app/stores/CollectionsStore.test.js @@ -4,29 +4,24 @@ import CollectionsStore from './CollectionsStore'; jest.mock('utils/ApiClient', () => ({ client: { post: {} }, })); -jest.mock('stores', () => ({ errors: {} })); +jest.mock('stores', () => ({ + errors: { add: jest.fn() } +})); describe('CollectionsStore', () => { let store; beforeEach(() => { - const cache = { - getItem: jest.fn(() => Promise.resolve()), - setItem: jest.fn(() => {}), - }; - - store = new CollectionsStore({ - teamId: 123, - cache, - }); + store = new CollectionsStore({}); }); - describe('#fetch', () => { + describe('#fetchAll', () => { test('should load stores', async () => { store.client = { post: jest.fn(() => ({ data: [ { + id: 123, name: 'New collection', }, ], @@ -35,20 +30,15 @@ describe('CollectionsStore', () => { await store.fetchAll(); - expect(store.client.post).toHaveBeenCalledWith('/collections.list', { - id: 123, - }); - expect(store.data.length).toBe(1); - expect(store.data[0].name).toBe('New collection'); + expect(store.client.post).toHaveBeenCalledWith('/collections.list'); + expect(store.data.size).toBe(1); + expect(store.data.values()[0].name).toBe('New collection'); }); test('should report errors', async () => { store.client = { post: jest.fn(() => Promise.reject), }; - store.errors = { - add: jest.fn(), - }; await store.fetchAll(); From 7945abbe543747b62993117f1d89b9da0752ec2a Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Wed, 22 Nov 2017 18:40:37 -0800 Subject: [PATCH 07/16] Added documents.list endpoint to documentation (fixed some bad formatting) Updated collection parameter Increased fetch to 10 records, 5 looks dumb on larger screens --- app/scenes/Collection/Collection.js | 4 +-- server/api/documents.js | 4 +-- server/pages/Api.js | 56 +++++++++++++++++------------ 3 files changed, 37 insertions(+), 27 deletions(-) diff --git a/app/scenes/Collection/Collection.js b/app/scenes/Collection/Collection.js index 67717ba61..5d08074e7 100644 --- a/app/scenes/Collection/Collection.js +++ b/app/scenes/Collection/Collection.js @@ -58,8 +58,8 @@ class CollectionScene extends Component { this.props.ui.setActiveCollection(collection); this.collection = collection; await this.props.documents.fetchRecentlyModified({ - limit: 5, - collectionId: collection.id, + limit: 10, + collection: collection.id, }); } diff --git a/server/api/documents.js b/server/api/documents.js index f3f112827..9fe74459c 100644 --- a/server/api/documents.js +++ b/server/api/documents.js @@ -15,12 +15,12 @@ const authDocumentForUser = (ctx, document) => { const router = new Router(); router.post('documents.list', auth(), pagination(), async ctx => { - let { sort = 'updatedAt', direction, collectionId } = ctx.body; + let { sort = 'updatedAt', direction, collection } = ctx.body; if (direction !== 'ASC') direction = 'DESC'; const user = ctx.state.user; let where = { teamId: user.teamId }; - if (collectionId) where = { ...where, atlasId: collectionId }; + if (collection) where = { ...where, atlasId: collection }; const userId = user.id; const starredScope = { method: ['withStarred', userId] }; diff --git a/server/pages/Api.js b/server/pages/Api.js index e41b9b52a..b546f2520 100644 --- a/server/pages/Api.js +++ b/server/pages/Api.js @@ -128,9 +128,9 @@ export default function Pricing() {

To authenticate with Outline API, you can supply the API key as a header (Authorization: Bearer YOUR_API_KEY) or as part of - the payload using token parameter. If you're making{' '} + the payload using token parameter. If you’re making{' '} GET requests, header based authentication is recommended - so that your keys don't leak into logs. + so that your keys don’t leak into logs.

@@ -241,7 +241,7 @@ export default function Pricing() { - Delete a collection and all of its documents. This action can`t be + Delete a collection and all of its documents. This action can’t be undone so please be careful. @@ -249,6 +249,16 @@ export default function Pricing() { + + List all your documents. + + + + +

@@ -487,26 +497,6 @@ type MethodProps = { children: React.Element<*>, }; -const Method = (props: MethodProps) => { - const children = React.Children.toArray(props.children); - const description = children.find(child => child.type === Description); - const apiArgs = children.find(child => child.type === Arguments); - - return ( - -

- {props.method} - {props.label} -

-
{description}
- HTTP request & arguments -

- {`${process.env.URL}/api/${props.method}`} -

- {apiArgs} - - ); -}; - const Description = (props: { children: React.Element<*> }) => (

{props.children}

); @@ -536,6 +526,26 @@ const Arguments = (props: ArgumentsProps) => ( ); +const Method = (props: MethodProps) => { + const children = React.Children.toArray(props.children); + const description = children.find(child => child.type === Description); + const apiArgs = children.find(child => child.type === Arguments); + + return ( + +

+ {props.method} - {props.label} +

+
{description}
+ HTTP request & arguments +

+ {`${process.env.URL}/api/${props.method}`} +

+ {apiArgs} +
+ ); +}; + type ArgumentProps = { id: string, required?: boolean, From 98f317c74adb413564e63815698cc30c6891c1a5 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sat, 25 Nov 2017 13:04:54 -0800 Subject: [PATCH 08/16] Prevents accidental overwriting of another users work --- app/models/Document.js | 2 ++ app/scenes/Document/Document.js | 2 +- .../api/__snapshots__/documents.test.js.snap | 9 +++++++++ server/api/documents.js | 6 +++++- server/api/documents.test.js | 18 ++++++++++++++++++ server/presenters/document.js | 1 + 6 files changed, 36 insertions(+), 2 deletions(-) diff --git a/app/models/Document.js b/app/models/Document.js index c67b1c580..5bd9392f6 100644 --- a/app/models/Document.js +++ b/app/models/Document.js @@ -39,6 +39,7 @@ class Document extends BaseModel { updatedBy: User; url: string; views: number; + revision: number; data: Object; @@ -168,6 +169,7 @@ class Document extends BaseModel { id: this.id, title: this.title, text: this.text, + lastRevision: this.revision, }); } else { if (!this.title) { diff --git a/app/scenes/Document/Document.js b/app/scenes/Document/Document.js index 8e75a4849..7aa08b9cb 100644 --- a/app/scenes/Document/Document.js +++ b/app/scenes/Document/Document.js @@ -232,7 +232,7 @@ class DocumentScene extends Component { message={DISCARD_CHANGES} /> { }); router.post('documents.update', auth(), async ctx => { - const { id, title, text } = ctx.body; + const { id, title, text, lastRevision } = ctx.body; ctx.assertPresent(id, 'id is required'); ctx.assertPresent(title || text, 'title or text is required'); @@ -250,6 +250,10 @@ router.post('documents.update', auth(), async ctx => { const document = await Document.findById(id); const collection = document.collection; + if (lastRevision && lastRevision !== document.revisionCount) { + throw httpErrors.BadRequest('Document has changed since last revision'); + } + authDocumentForUser(ctx, document); // Update document diff --git a/server/api/documents.test.js b/server/api/documents.test.js index 82380eed4..fb5759ac1 100644 --- a/server/api/documents.test.js +++ b/server/api/documents.test.js @@ -277,6 +277,7 @@ describe('#documents.update', async () => { id: document.id, title: 'Updated title', text: 'Updated text', + lastRevision: document.revision, }, }); const body = await res.json(); @@ -287,6 +288,23 @@ describe('#documents.update', async () => { expect(body.data.collection.documents[1].title).toBe('Updated title'); }); + it('should fail if document lastRevision does not match', async () => { + const { user, document } = await seed(); + + const res = await server.post('/api/documents.update', { + body: { + token: user.getJwtToken(), + id: document.id, + text: 'Updated text', + lastRevision: 123, + }, + }); + const body = await res.json(); + + expect(res.status).toEqual(400); + expect(body).toMatchSnapshot(); + }); + it('should update document details for children', async () => { const { user, document, collection } = await seed(); collection.documentStructure = [ diff --git a/server/presenters/document.js b/server/presenters/document.js index 29ddff81d..a665b307d 100644 --- a/server/presenters/document.js +++ b/server/presenters/document.js @@ -36,6 +36,7 @@ async function present(ctx: Object, document: Document, options: ?Options) { team: document.teamId, collaborators: [], starred: !!(document.starred && document.starred.length), + revision: document.revisionCount, collectionId: document.atlasId, collaboratorCount: undefined, collection: undefined, From 2143a87671f38ddcb8300abe395a8cc6275ccf16 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sun, 19 Nov 2017 20:16:49 -0800 Subject: [PATCH 09/16] WIP: Collection home --- .../CenteredContent/CenteredContent.js | 2 +- app/components/Editor/Editor.js | 6 +- app/components/Icon/CollectionIcon.js | 2 +- app/components/Subheading/Subheading.js | 15 ++++ app/components/Subheading/index.js | 3 + app/models/Collection.js | 5 ++ app/scenes/Collection/Collection.js | 72 +++++++++++------- app/scenes/Dashboard/Dashboard.js | 33 ++++----- app/scenes/Document/Document.js | 1 - app/stores/CollectionsStore.js | 74 ++++++++++--------- app/stores/UiStore.js | 6 ++ shared/styles/constants.js | 2 +- 12 files changed, 133 insertions(+), 88 deletions(-) create mode 100644 app/components/Subheading/Subheading.js create mode 100644 app/components/Subheading/index.js diff --git a/app/components/CenteredContent/CenteredContent.js b/app/components/CenteredContent/CenteredContent.js index 578d00556..bcfaedb8f 100644 --- a/app/components/CenteredContent/CenteredContent.js +++ b/app/components/CenteredContent/CenteredContent.js @@ -12,7 +12,7 @@ const Container = styled.div` `; const Content = styled.div` - max-width: 50em; + max-width: 46em; margin: 0 auto; `; diff --git a/app/components/Editor/Editor.js b/app/components/Editor/Editor.js index 50a5805fe..4e4d71293 100644 --- a/app/components/Editor/Editor.js +++ b/app/components/Editor/Editor.js @@ -228,8 +228,8 @@ class MarkdownEditor extends Component { } const MaxWidth = styled(Flex)` - padding: 0 60px; - max-width: 50em; + margin: 0 60px; + max-width: 46em; height: 100%; `; @@ -281,6 +281,8 @@ const StyledEditor = styled(Editor)` p { position: relative; + margin-top: 1.2em; + margin-bottom: 1.2em; } a:hover { diff --git a/app/components/Icon/CollectionIcon.js b/app/components/Icon/CollectionIcon.js index f419be0dc..251824194 100644 --- a/app/components/Icon/CollectionIcon.js +++ b/app/components/Icon/CollectionIcon.js @@ -6,7 +6,7 @@ import type { Props } from './Icon'; export default function CollectionIcon({ expanded, ...rest -}: Props & { expanded: boolean }) { +}: Props & { expanded?: boolean }) { return ( {expanded ? ( diff --git a/app/components/Subheading/Subheading.js b/app/components/Subheading/Subheading.js new file mode 100644 index 000000000..fdc716489 --- /dev/null +++ b/app/components/Subheading/Subheading.js @@ -0,0 +1,15 @@ +// @flow +import styled from 'styled-components'; + +const Subheading = styled.h3` + font-size: 11px; + font-weight: 500; + text-transform: uppercase; + color: #9fa6ab; + letter-spacing: 0.04em; + border-bottom: 1px solid #ddd; + padding-bottom: 10px; + margin-top: 30px; +`; + +export default Subheading; diff --git a/app/components/Subheading/index.js b/app/components/Subheading/index.js new file mode 100644 index 000000000..d0f6a76e7 --- /dev/null +++ b/app/components/Subheading/index.js @@ -0,0 +1,3 @@ +// @flow +import Subheading from './Subheading'; +export default Subheading; diff --git a/app/models/Collection.js b/app/models/Collection.js index 96a1c57e2..e7a4013f2 100644 --- a/app/models/Collection.js +++ b/app/models/Collection.js @@ -39,6 +39,11 @@ class Collection extends BaseModel { return true; } + @computed + get isEmpty(): boolean { + return this.documents.length === 0; + } + /* Actions */ @action diff --git a/app/scenes/Collection/Collection.js b/app/scenes/Collection/Collection.js index 193e58de3..6cd13ab77 100644 --- a/app/scenes/Collection/Collection.js +++ b/app/scenes/Collection/Collection.js @@ -2,20 +2,26 @@ import React, { Component } from 'react'; import { observable } from 'mobx'; import { observer, inject } from 'mobx-react'; -import { Link, Redirect } from 'react-router-dom'; +import { Link } from 'react-router-dom'; import styled from 'styled-components'; import { newDocumentUrl } from 'utils/routeHelpers'; import CollectionsStore from 'stores/CollectionsStore'; +import UiStore from 'stores/UiStore'; import Collection from 'models/Collection'; +import Search from 'scenes/Search'; import CenteredContent from 'components/CenteredContent'; +import CollectionIcon from 'components/Icon/CollectionIcon'; import LoadingListPlaceholder from 'components/LoadingListPlaceholder'; import Button from 'components/Button'; import HelpText from 'components/HelpText'; +import Subheading from 'components/Subheading'; +import PageTitle from 'components/PageTitle'; import Flex from 'shared/components/Flex'; type Props = { + ui: UiStore, collections: CollectionsStore, match: Object, }; @@ -24,29 +30,27 @@ type Props = { class CollectionScene extends Component { props: Props; @observable collection: ?Collection; - @observable isFetching = true; - @observable redirectUrl; + @observable isFetching: boolean = true; componentDidMount = () => { - this.fetchDocument(this.props.match.params.id); + this.fetchCollection(this.props.match.params.id); }; componentWillReceiveProps(nextProps) { if (nextProps.match.params.id !== this.props.match.params.id) { - this.fetchDocument(nextProps.match.params.id); + this.fetchCollection(nextProps.match.params.id); } } - fetchDocument = async (id: string) => { + fetchCollection = async (id: string) => { const { collections } = this.props; - this.collection = await collections.fetchById(id); - - if (!this.collection) this.redirectUrl = '/404'; - - if (this.collection && this.collection.documents.length > 0) { - this.redirectUrl = this.collection.documents[0].url; + const collection = await collections.fetch(id); + if (collection) { + this.props.ui.setActiveCollection(collection); + this.collection = collection; } + this.isFetching = false; }; @@ -54,43 +58,57 @@ class CollectionScene extends Component { if (!this.collection) return; return ( - -

Create a document

+ + + + {' '} + {this.collection.name} + - Publish your first document to start building the{' '} - {this.collection.name} collection. + Publish your first document to start building this collection. -
+ ); } + renderNotFound() { + return ; + } + render() { - if (this.redirectUrl) return ; + if (this.isFetching) return ; + if (!this.collection) return this.renderNotFound(); + if (this.collection.isEmpty) return this.renderEmptyCollection(); return ( - {this.isFetching ? ( - - ) : ( - this.renderEmptyCollection() - )} + + + {' '} + {this.collection.name} + + Recently edited ); } } -const NewDocumentContainer = styled(Flex)` - padding-top: 50%; - transform: translateY(-50%); +const Heading = styled.h1` + display: flex; + + svg { + margin-left: -6px; + margin-right: 6px; + } `; const Action = styled(Flex)` margin: 10px 0; `; -export default inject('collections')(CollectionScene); +export default inject('collections', 'ui')(CollectionScene); diff --git a/app/scenes/Dashboard/Dashboard.js b/app/scenes/Dashboard/Dashboard.js index b33042fc6..1eb9d07f5 100644 --- a/app/scenes/Dashboard/Dashboard.js +++ b/app/scenes/Dashboard/Dashboard.js @@ -4,11 +4,10 @@ import { observable } from 'mobx'; import { observer, inject } from 'mobx-react'; import DocumentsStore from 'stores/DocumentsStore'; -import Flex from 'shared/components/Flex'; +import CenteredContent from 'components/CenteredContent'; import DocumentList from 'components/DocumentList'; import PageTitle from 'components/PageTitle'; import Subheading from 'components/Subheading'; -import CenteredContent from 'components/CenteredContent'; import { ListPlaceholder } from 'components/LoadingPlaceholder'; type Props = { @@ -34,30 +33,26 @@ class Dashboard extends Component { render() { const { documents } = this.props; - const recentlyViewedLoaded = documents.recentlyViewed.length > 0; - const recentlyEditedLoaded = documents.recentlyEdited.length > 0; + const hasRecentlyViewed = documents.recentlyViewed.length > 0; + const hasRecentlyEdited = documents.recentlyEdited.length > 0; const showContent = - this.isLoaded || (recentlyViewedLoaded && recentlyEditedLoaded); + this.isLoaded || (hasRecentlyViewed && hasRecentlyEdited); return (

Home

{showContent ? ( - - {recentlyViewedLoaded && ( - - Recently viewed - - - )} - {recentlyEditedLoaded && ( - - Recently edited - - - )} - + + {hasRecentlyViewed && [ + Recently viewed, + , + ]} + {hasRecentlyEdited && [ + Recently edited, + , + ]} + ) : ( )} diff --git a/app/scenes/Document/Document.js b/app/scenes/Document/Document.js index cdcc83fee..9fb48a94e 100644 --- a/app/scenes/Document/Document.js +++ b/app/scenes/Document/Document.js @@ -44,7 +44,6 @@ type Props = { match: Object, history: Object, location: Location, - keydown: Object, documents: DocumentsStore, collections: CollectionsStore, newDocument?: boolean, diff --git a/app/stores/CollectionsStore.js b/app/stores/CollectionsStore.js index 18b205af4..18e7c2ff7 100644 --- a/app/stores/CollectionsStore.js +++ b/app/stores/CollectionsStore.js @@ -1,11 +1,5 @@ // @flow -import { - observable, - computed, - action, - runInAction, - ObservableArray, -} from 'mobx'; +import { observable, computed, action, runInAction, ObservableMap } from 'mobx'; import ApiClient, { client } from 'utils/ApiClient'; import _ from 'lodash'; import invariant from 'invariant'; @@ -13,12 +7,10 @@ import invariant from 'invariant'; import stores from 'stores'; import Collection from 'models/Collection'; import ErrorsStore from 'stores/ErrorsStore'; -import CacheStore from 'stores/CacheStore'; import UiStore from 'stores/UiStore'; type Options = { teamId: string, - cache: CacheStore, ui: UiStore, }; @@ -30,17 +22,17 @@ type DocumentPathItem = { }; export type DocumentPath = DocumentPathItem & { - path: Array, + path: DocumentPathItem[], }; class CollectionsStore { - @observable data: ObservableArray = observable.array([]); + @observable data: Map = new ObservableMap([]); @observable isLoaded: boolean = false; + @observable isFetching: boolean = false; client: ApiClient; teamId: string; errors: ErrorsStore; - cache: CacheStore; ui: UiStore; @computed @@ -52,7 +44,7 @@ class CollectionsStore { @computed get orderedData(): Collection[] { - return _.sortBy(this.data, 'name'); + return _.sortBy(this.data.values(), 'name'); } /** @@ -100,6 +92,8 @@ class CollectionsStore { @action fetchAll = async (): Promise<*> => { + this.isFetching = true; + try { const res = await this.client.post('/collections.list', { id: this.teamId, @@ -107,56 +101,64 @@ class CollectionsStore { invariant(res && res.data, 'Collection list not available'); const { data } = res; runInAction('CollectionsStore#fetch', () => { - this.data.replace(data.map(collection => new Collection(collection))); + data.forEach(collection => { + this.data.set(collection.id, new Collection(collection)); + }); this.isLoaded = true; }); } catch (e) { this.errors.add('Failed to load collections'); + } finally { + this.isFetching = false; } }; @action - fetchById = async (id: string): Promise => { + fetch = async (id: string): Promise => { let collection = this.getById(id); - if (!collection) { - try { - const res = await this.client.post('/collections.info', { - id, - }); - invariant(res && res.data, 'Collection not available'); - const { data } = res; - runInAction('CollectionsStore#getById', () => { - collection = new Collection(data); - this.add(collection); - }); - } catch (e) { - Bugsnag.notify(e); - this.errors.add('Something went wrong'); - } - } + if (collection) return collection; - return collection; + this.isFetching = true; + + try { + const res = await this.client.post('/collections.info', { + id, + }); + invariant(res && res.data, 'Collection not available'); + const { data } = res; + const collection = new Collection(data); + + runInAction('CollectionsStore#fetch', () => { + this.data.set(data.id, collection); + this.isLoaded = true; + }); + + return collection; + } catch (e) { + this.errors.add('Something went wrong'); + } finally { + this.isFetching = false; + } }; @action add = (collection: Collection): void => { - this.data.push(collection); + this.data.set(collection.id, collection); }; @action remove = (id: string): void => { - this.data.splice(this.data.indexOf(id), 1); + this.data.delete(id); }; getById = (id: string): ?Collection => { - return _.find(this.data, { id }); + return this.data.get(id); }; constructor(options: Options) { this.client = client; this.errors = stores.errors; this.teamId = options.teamId; - this.cache = options.cache; this.ui = options.ui; } } diff --git a/app/stores/UiStore.js b/app/stores/UiStore.js index 10acfe2fd..3fc05269d 100644 --- a/app/stores/UiStore.js +++ b/app/stores/UiStore.js @@ -1,6 +1,7 @@ // @flow import { observable, action } from 'mobx'; import Document from 'models/Document'; +import Collection from 'models/Collection'; class UiStore { @observable activeModalName: ?string; @@ -29,6 +30,11 @@ class UiStore { this.activeCollectionId = document.collection.id; }; + @action + setActiveCollection = (collection: Collection): void => { + this.activeCollectionId = collection.id; + }; + @action clearActiveDocument = (): void => { this.activeDocumentId = undefined; diff --git a/shared/styles/constants.js b/shared/styles/constants.js index 3a1496567..303d6beee 100644 --- a/shared/styles/constants.js +++ b/shared/styles/constants.js @@ -45,7 +45,7 @@ export const color = { text: '#171B35', /* Brand */ - primary: '#2B8FBF', + primary: '#1AB6FF', danger: '#D0021B', warning: '#f08a24' /* replace */, success: '#43AC6A' /* replace */, From dc242038ff6e3b02dbfed4343dc99f359e016c6f Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sun, 19 Nov 2017 20:44:01 -0800 Subject: [PATCH 10/16] Remove unused teamId --- app/components/ColorPicker/ColorPicker.js | 4 ++++ app/stores/CollectionsStore.js | 7 +------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/app/components/ColorPicker/ColorPicker.js b/app/components/ColorPicker/ColorPicker.js index cd005ede6..f359312d8 100644 --- a/app/components/ColorPicker/ColorPicker.js +++ b/app/components/ColorPicker/ColorPicker.js @@ -157,6 +157,10 @@ const SwatchInset = styled(Flex)` const StyledOutline = styled(Outline)` padding: 5px; + + strong { + font-weight: 500; + } `; const HexHash = styled.div` diff --git a/app/stores/CollectionsStore.js b/app/stores/CollectionsStore.js index 18e7c2ff7..0a3880d28 100644 --- a/app/stores/CollectionsStore.js +++ b/app/stores/CollectionsStore.js @@ -10,7 +10,6 @@ import ErrorsStore from 'stores/ErrorsStore'; import UiStore from 'stores/UiStore'; type Options = { - teamId: string, ui: UiStore, }; @@ -31,7 +30,6 @@ class CollectionsStore { @observable isFetching: boolean = false; client: ApiClient; - teamId: string; errors: ErrorsStore; ui: UiStore; @@ -95,9 +93,7 @@ class CollectionsStore { this.isFetching = true; try { - const res = await this.client.post('/collections.list', { - id: this.teamId, - }); + const res = await this.client.post('/collections.list'); invariant(res && res.data, 'Collection list not available'); const { data } = res; runInAction('CollectionsStore#fetch', () => { @@ -158,7 +154,6 @@ class CollectionsStore { constructor(options: Options) { this.client = client; this.errors = stores.errors; - this.teamId = options.teamId; this.ui = options.ui; } } From 81edb55d56a2d0f6d4868de0ab9047c6aa8bb98c Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sun, 19 Nov 2017 21:32:18 -0800 Subject: [PATCH 11/16] Working recently edited list --- app/models/Document.js | 1 + app/scenes/Collection/Collection.js | 25 +++++++++++++++++++------ app/stores/CollectionsStore.js | 2 +- app/stores/DocumentsStore.js | 11 +++++++++++ server/api/documents.js | 7 +++++-- server/api/documents.test.js | 11 +++++++++++ server/presenters/document.js | 1 + 7 files changed, 49 insertions(+), 9 deletions(-) diff --git a/app/models/Document.js b/app/models/Document.js index 00397a5de..c67b1c580 100644 --- a/app/models/Document.js +++ b/app/models/Document.js @@ -20,6 +20,7 @@ class Document extends BaseModel { collaborators: Array; collection: $Shape; + collectionId: string; firstViewedAt: ?string; lastViewedAt: ?string; modifiedSinceViewed: ?boolean; diff --git a/app/scenes/Collection/Collection.js b/app/scenes/Collection/Collection.js index 6cd13ab77..f26503379 100644 --- a/app/scenes/Collection/Collection.js +++ b/app/scenes/Collection/Collection.js @@ -7,6 +7,7 @@ import styled from 'styled-components'; import { newDocumentUrl } from 'utils/routeHelpers'; import CollectionsStore from 'stores/CollectionsStore'; +import DocumentsStore from 'stores/DocumentsStore'; import UiStore from 'stores/UiStore'; import Collection from 'models/Collection'; @@ -16,12 +17,14 @@ import CollectionIcon from 'components/Icon/CollectionIcon'; import LoadingListPlaceholder from 'components/LoadingListPlaceholder'; import Button from 'components/Button'; import HelpText from 'components/HelpText'; +import DocumentList from 'components/DocumentList'; import Subheading from 'components/Subheading'; import PageTitle from 'components/PageTitle'; import Flex from 'shared/components/Flex'; type Props = { ui: UiStore, + documents: DocumentsStore, collections: CollectionsStore, match: Object, }; @@ -32,23 +35,28 @@ class CollectionScene extends Component { @observable collection: ?Collection; @observable isFetching: boolean = true; - componentDidMount = () => { - this.fetchCollection(this.props.match.params.id); - }; + componentDidMount() { + this.loadContent(this.props.match.params.id); + } componentWillReceiveProps(nextProps) { if (nextProps.match.params.id !== this.props.match.params.id) { - this.fetchCollection(nextProps.match.params.id); + this.loadContent(nextProps.match.params.id); } } - fetchCollection = async (id: string) => { + loadContent = async (id: string) => { const { collections } = this.props; const collection = await collections.fetch(id); + if (collection) { this.props.ui.setActiveCollection(collection); this.collection = collection; + await this.props.documents.fetchRecentlyModified({ + limit: 5, + collectionId: collection.id, + }); } this.isFetching = false; @@ -93,6 +101,11 @@ class CollectionScene extends Component { {this.collection.name} Recently edited +
); } @@ -111,4 +124,4 @@ const Action = styled(Flex)` margin: 10px 0; `; -export default inject('collections', 'ui')(CollectionScene); +export default inject('collections', 'documents', 'ui')(CollectionScene); diff --git a/app/stores/CollectionsStore.js b/app/stores/CollectionsStore.js index 0a3880d28..67973f4b7 100644 --- a/app/stores/CollectionsStore.js +++ b/app/stores/CollectionsStore.js @@ -96,7 +96,7 @@ class CollectionsStore { const res = await this.client.post('/collections.list'); invariant(res && res.data, 'Collection list not available'); const { data } = res; - runInAction('CollectionsStore#fetch', () => { + runInAction('CollectionsStore#fetchAll', () => { data.forEach(collection => { this.data.set(collection.id, new Collection(collection)); }); diff --git a/app/stores/DocumentsStore.js b/app/stores/DocumentsStore.js index 99235facd..4c05276b1 100644 --- a/app/stores/DocumentsStore.js +++ b/app/stores/DocumentsStore.js @@ -52,6 +52,17 @@ class DocumentsStore extends BaseStore { return _.take(_.orderBy(this.data.values(), 'updatedAt', 'desc'), 5); } + recentlyEditedInCollection(collectionId: string): Array { + return _.orderBy( + _.filter( + this.data.values(), + document => document.collectionId === collectionId + ), + 'updatedAt', + 'desc' + ); + } + @computed get starred(): Array { return _.filter(this.data.values(), 'starred'); diff --git a/server/api/documents.js b/server/api/documents.js index 5e098ca8b..f3f112827 100644 --- a/server/api/documents.js +++ b/server/api/documents.js @@ -15,14 +15,17 @@ const authDocumentForUser = (ctx, document) => { const router = new Router(); router.post('documents.list', auth(), pagination(), async ctx => { - let { sort = 'updatedAt', direction } = ctx.body; + let { sort = 'updatedAt', direction, collectionId } = ctx.body; if (direction !== 'ASC') direction = 'DESC'; const user = ctx.state.user; + let where = { teamId: user.teamId }; + if (collectionId) where = { ...where, atlasId: collectionId }; + const userId = user.id; const starredScope = { method: ['withStarred', userId] }; const documents = await Document.scope('defaultScope', starredScope).findAll({ - where: { teamId: user.teamId }, + where, order: [[sort, direction]], offset: ctx.state.pagination.offset, limit: ctx.state.pagination.limit, diff --git a/server/api/documents.test.js b/server/api/documents.test.js index 7d2bc37df..82380eed4 100644 --- a/server/api/documents.test.js +++ b/server/api/documents.test.js @@ -34,6 +34,17 @@ describe('#documents.list', async () => { expect(body.data[1].id).toEqual(document.id); }); + it('should allow filtering by collection', async () => { + const { user, document } = await seed(); + const res = await server.post('/api/documents.list', { + body: { token: user.getJwtToken(), collectionId: document.atlasId }, + }); + const body = await res.json(); + + expect(res.status).toEqual(200); + expect(body.data.length).toEqual(2); + }); + it('should require authentication', async () => { const res = await server.post('/api/documents.list'); const body = await res.json(); diff --git a/server/presenters/document.js b/server/presenters/document.js index 575d18f3b..29ddff81d 100644 --- a/server/presenters/document.js +++ b/server/presenters/document.js @@ -36,6 +36,7 @@ async function present(ctx: Object, document: Document, options: ?Options) { team: document.teamId, collaborators: [], starred: !!(document.starred && document.starred.length), + collectionId: document.atlasId, collaboratorCount: undefined, collection: undefined, views: undefined, From a626b786168795ea27a1e0f29f5bb37a10830846 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sun, 19 Nov 2017 21:50:42 -0800 Subject: [PATCH 12/16] Improved loading state --- .../LoadingListPlaceholder.js | 40 ----------------- .../LoadingListPlaceholder/index.js | 3 -- app/scenes/Collection/Collection.js | 45 ++++++++++++------- 3 files changed, 29 insertions(+), 59 deletions(-) delete mode 100644 app/components/LoadingListPlaceholder/LoadingListPlaceholder.js delete mode 100644 app/components/LoadingListPlaceholder/index.js diff --git a/app/components/LoadingListPlaceholder/LoadingListPlaceholder.js b/app/components/LoadingListPlaceholder/LoadingListPlaceholder.js deleted file mode 100644 index 2f81df862..000000000 --- a/app/components/LoadingListPlaceholder/LoadingListPlaceholder.js +++ /dev/null @@ -1,40 +0,0 @@ -// @flow -import React from 'react'; -import styled from 'styled-components'; -import { pulsate } from 'shared/styles/animations'; -import { color } from 'shared/styles/constants'; -import Flex from 'shared/components/Flex'; -import Fade from 'components/Fade'; - -import { randomInteger } from 'shared/random'; - -const randomValues = Array.from( - new Array(5), - () => `${randomInteger(85, 100)}%` -); - -export default (props: Object) => { - return ( - - - - - - - - - - - ); -}; - -const Item = styled(Flex)` - padding: 18px 0; -`; - -const Mask = styled(Flex)` - height: ${props => (props.header ? 28 : 18)}px; - margin-bottom: ${props => (props.header ? 18 : 0)}px; - background-color: ${color.smoke}; - animation: ${pulsate} 1.3s infinite; -`; diff --git a/app/components/LoadingListPlaceholder/index.js b/app/components/LoadingListPlaceholder/index.js deleted file mode 100644 index 17588c5a6..000000000 --- a/app/components/LoadingListPlaceholder/index.js +++ /dev/null @@ -1,3 +0,0 @@ -// @flow -import LoadingListPlaceholder from './LoadingListPlaceholder'; -export default LoadingListPlaceholder; diff --git a/app/scenes/Collection/Collection.js b/app/scenes/Collection/Collection.js index f26503379..f176004b3 100644 --- a/app/scenes/Collection/Collection.js +++ b/app/scenes/Collection/Collection.js @@ -14,7 +14,7 @@ import Collection from 'models/Collection'; import Search from 'scenes/Search'; import CenteredContent from 'components/CenteredContent'; import CollectionIcon from 'components/Icon/CollectionIcon'; -import LoadingListPlaceholder from 'components/LoadingListPlaceholder'; +import { ListPlaceholder } from 'components/LoadingPlaceholder'; import Button from 'components/Button'; import HelpText from 'components/HelpText'; import DocumentList from 'components/DocumentList'; @@ -48,7 +48,7 @@ class CollectionScene extends Component { loadContent = async (id: string) => { const { collections } = this.props; - const collection = await collections.fetch(id); + const collection = collections.getById(id) || (await collections.fetch(id)); if (collection) { this.props.ui.setActiveCollection(collection); @@ -89,23 +89,36 @@ class CollectionScene extends Component { } render() { - if (this.isFetching) return ; - if (!this.collection) return this.renderNotFound(); - if (this.collection.isEmpty) return this.renderEmptyCollection(); + if (!this.isFetching && !this.collection) { + return this.renderNotFound(); + } + if (this.collection && this.collection.isEmpty) { + return this.renderEmptyCollection(); + } return ( - - - {' '} - {this.collection.name} - - Recently edited - + {this.collection ? ( + + + + {' '} + {this.collection.name} + + Recently edited + + + ) : ( + + )} ); } From 3e01d4813e9fd18615ac1e8a779caebb31b9e868 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sun, 19 Nov 2017 23:39:56 -0800 Subject: [PATCH 13/16] Add collection actions --- app/components/Actions/Actions.js | 34 ++++++++++ app/components/Actions/index.js | 4 ++ app/scenes/Collection/Collection.js | 35 ++++++++-- app/scenes/Document/Document.js | 101 ++++++++++------------------ 4 files changed, 103 insertions(+), 71 deletions(-) create mode 100644 app/components/Actions/Actions.js create mode 100644 app/components/Actions/index.js diff --git a/app/components/Actions/Actions.js b/app/components/Actions/Actions.js new file mode 100644 index 000000000..7296f1d55 --- /dev/null +++ b/app/components/Actions/Actions.js @@ -0,0 +1,34 @@ +// @flow +import styled from 'styled-components'; +import Flex from 'shared/components/Flex'; +import { layout, color } from 'shared/styles/constants'; + +export const Action = styled(Flex)` + justify-content: center; + align-items: center; + padding: 0 0 0 10px; + + a { + color: ${color.text}; + height: 24px; + } +`; + +export const Separator = styled.div` + margin-left: 12px; + width: 1px; + height: 20px; + background: ${color.slateLight}; +`; + +const Actions = styled(Flex)` + position: fixed; + top: 0; + right: 0; + padding: ${layout.vpadding} ${layout.hpadding} 8px 8px; + border-radius: 3px; + background: rgba(255, 255, 255, 0.9); + -webkit-backdrop-filter: blur(20px); +`; + +export default Actions; diff --git a/app/components/Actions/index.js b/app/components/Actions/index.js new file mode 100644 index 000000000..71c56fa85 --- /dev/null +++ b/app/components/Actions/index.js @@ -0,0 +1,4 @@ +// @flow +import Actions from './Actions'; +export { Action, Separator } from './Actions'; +export default Actions; diff --git a/app/scenes/Collection/Collection.js b/app/scenes/Collection/Collection.js index f176004b3..67717ba61 100644 --- a/app/scenes/Collection/Collection.js +++ b/app/scenes/Collection/Collection.js @@ -2,7 +2,7 @@ import React, { Component } from 'react'; import { observable } from 'mobx'; import { observer, inject } from 'mobx-react'; -import { Link } from 'react-router-dom'; +import { withRouter, Link } from 'react-router-dom'; import styled from 'styled-components'; import { newDocumentUrl } from 'utils/routeHelpers'; @@ -12,8 +12,11 @@ import UiStore from 'stores/UiStore'; import Collection from 'models/Collection'; import Search from 'scenes/Search'; +import CollectionMenu from 'menus/CollectionMenu'; +import Actions, { Action, Separator } from 'components/Actions'; import CenteredContent from 'components/CenteredContent'; import CollectionIcon from 'components/Icon/CollectionIcon'; +import NewDocumentIcon from 'components/Icon/NewDocumentIcon'; import { ListPlaceholder } from 'components/LoadingPlaceholder'; import Button from 'components/Button'; import HelpText from 'components/HelpText'; @@ -26,6 +29,7 @@ type Props = { ui: UiStore, documents: DocumentsStore, collections: CollectionsStore, + history: Object, match: Object, }; @@ -62,6 +66,14 @@ class CollectionScene extends Component { this.isFetching = false; }; + onNewDocument = (ev: SyntheticEvent) => { + ev.preventDefault(); + + if (this.collection) { + this.props.history.push(`${this.collection.url}/new`); + } + }; + renderEmptyCollection() { if (!this.collection) return; @@ -75,11 +87,11 @@ class CollectionScene extends Component { Publish your first document to start building this collection. - + - + ); } @@ -115,6 +127,17 @@ class CollectionScene extends Component { this.collection.id )} /> + + + + + + + + + + + ) : ( @@ -133,8 +156,10 @@ const Heading = styled.h1` } `; -const Action = styled(Flex)` +const Wrapper = styled(Flex)` margin: 10px 0; `; -export default inject('collections', 'documents', 'ui')(CollectionScene); +export default withRouter( + inject('collections', 'documents', 'ui')(CollectionScene) +); diff --git a/app/scenes/Document/Document.js b/app/scenes/Document/Document.js index 9fb48a94e..8e75a4849 100644 --- a/app/scenes/Document/Document.js +++ b/app/scenes/Document/Document.js @@ -8,7 +8,6 @@ import { withRouter, Prompt } from 'react-router-dom'; import type { Location } from 'react-router-dom'; import keydown from 'react-keydown'; import Flex from 'shared/components/Flex'; -import { color, layout } from 'shared/styles/constants'; import { collectionUrl, updateDocumentUrl, @@ -33,6 +32,7 @@ import Collaborators from 'components/Collaborators'; import CenteredContent from 'components/CenteredContent'; import PageTitle from 'components/PageTitle'; import NewDocumentIcon from 'components/Icon/NewDocumentIcon'; +import Actions, { Action, Separator } from 'components/Actions'; import Search from 'scenes/Search'; const DISCARD_CHANGES = ` @@ -242,49 +242,47 @@ class DocumentScene extends Component { onCancel={this.onDiscard} readOnly={!this.isEditing} /> - - - {!isNew && - !this.isEditing && } - - {this.isEditing ? ( - - ) : ( - Edit - )} - - {this.isEditing && ( - - Discard - + {!isNew && + !this.isEditing && } + + {this.isEditing ? ( + + ) : ( + Edit )} + + {this.isEditing && ( + + Discard + + )} + {!this.isEditing && ( + + + + )} + {!this.isEditing && } + {!this.isEditing && ( - - - + + + )} - {!this.isEditing && } - - {!this.isEditing && ( - - - - )} - - - + + )} @@ -292,35 +290,6 @@ class DocumentScene extends Component { } } -const Separator = styled.div` - margin-left: 12px; - width: 1px; - height: 20px; - background: ${color.slateLight}; -`; - -const HeaderAction = styled(Flex)` - justify-content: center; - align-items: center; - padding: 0 0 0 10px; - - a { - color: ${color.text}; - height: 24px; - } -`; - -const Meta = styled(Flex)` - align-items: flex-start; - position: fixed; - top: 0; - right: 0; - padding: ${layout.vpadding} ${layout.hpadding} 8px 8px; - border-radius: 3px; - background: rgba(255, 255, 255, 0.9); - -webkit-backdrop-filter: blur(20px); -`; - const Container = styled(Flex)` position: relative; width: 100%; From bca940bedc2700ce8d490d2ea030fd21b940d0b5 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Mon, 20 Nov 2017 00:09:46 -0800 Subject: [PATCH 14/16] :green_heart: --- app/stores/CollectionsStore.test.js | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/app/stores/CollectionsStore.test.js b/app/stores/CollectionsStore.test.js index 358f5bd5b..674843847 100644 --- a/app/stores/CollectionsStore.test.js +++ b/app/stores/CollectionsStore.test.js @@ -4,29 +4,24 @@ import CollectionsStore from './CollectionsStore'; jest.mock('utils/ApiClient', () => ({ client: { post: {} }, })); -jest.mock('stores', () => ({ errors: {} })); +jest.mock('stores', () => ({ + errors: { add: jest.fn() } +})); describe('CollectionsStore', () => { let store; beforeEach(() => { - const cache = { - getItem: jest.fn(() => Promise.resolve()), - setItem: jest.fn(() => {}), - }; - - store = new CollectionsStore({ - teamId: 123, - cache, - }); + store = new CollectionsStore({}); }); - describe('#fetch', () => { + describe('#fetchAll', () => { test('should load stores', async () => { store.client = { post: jest.fn(() => ({ data: [ { + id: 123, name: 'New collection', }, ], @@ -35,20 +30,15 @@ describe('CollectionsStore', () => { await store.fetchAll(); - expect(store.client.post).toHaveBeenCalledWith('/collections.list', { - id: 123, - }); - expect(store.data.length).toBe(1); - expect(store.data[0].name).toBe('New collection'); + expect(store.client.post).toHaveBeenCalledWith('/collections.list'); + expect(store.data.size).toBe(1); + expect(store.data.values()[0].name).toBe('New collection'); }); test('should report errors', async () => { store.client = { post: jest.fn(() => Promise.reject), }; - store.errors = { - add: jest.fn(), - }; await store.fetchAll(); From 94c2cc09eeb3ebedb0d492480d4906547e96e2b3 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Wed, 22 Nov 2017 18:40:37 -0800 Subject: [PATCH 15/16] Added documents.list endpoint to documentation (fixed some bad formatting) Updated collection parameter Increased fetch to 10 records, 5 looks dumb on larger screens --- app/scenes/Collection/Collection.js | 4 +-- server/api/documents.js | 4 +-- server/pages/Api.js | 56 +++++++++++++++++------------ 3 files changed, 37 insertions(+), 27 deletions(-) diff --git a/app/scenes/Collection/Collection.js b/app/scenes/Collection/Collection.js index 67717ba61..5d08074e7 100644 --- a/app/scenes/Collection/Collection.js +++ b/app/scenes/Collection/Collection.js @@ -58,8 +58,8 @@ class CollectionScene extends Component { this.props.ui.setActiveCollection(collection); this.collection = collection; await this.props.documents.fetchRecentlyModified({ - limit: 5, - collectionId: collection.id, + limit: 10, + collection: collection.id, }); } diff --git a/server/api/documents.js b/server/api/documents.js index f3f112827..9fe74459c 100644 --- a/server/api/documents.js +++ b/server/api/documents.js @@ -15,12 +15,12 @@ const authDocumentForUser = (ctx, document) => { const router = new Router(); router.post('documents.list', auth(), pagination(), async ctx => { - let { sort = 'updatedAt', direction, collectionId } = ctx.body; + let { sort = 'updatedAt', direction, collection } = ctx.body; if (direction !== 'ASC') direction = 'DESC'; const user = ctx.state.user; let where = { teamId: user.teamId }; - if (collectionId) where = { ...where, atlasId: collectionId }; + if (collection) where = { ...where, atlasId: collection }; const userId = user.id; const starredScope = { method: ['withStarred', userId] }; diff --git a/server/pages/Api.js b/server/pages/Api.js index e41b9b52a..b546f2520 100644 --- a/server/pages/Api.js +++ b/server/pages/Api.js @@ -128,9 +128,9 @@ export default function Pricing() {

To authenticate with Outline API, you can supply the API key as a header (Authorization: Bearer YOUR_API_KEY) or as part of - the payload using token parameter. If you're making{' '} + the payload using token parameter. If you’re making{' '} GET requests, header based authentication is recommended - so that your keys don't leak into logs. + so that your keys don’t leak into logs.

@@ -241,7 +241,7 @@ export default function Pricing() { - Delete a collection and all of its documents. This action can`t be + Delete a collection and all of its documents. This action can’t be undone so please be careful. @@ -249,6 +249,16 @@ export default function Pricing() { + + List all your documents. + + + + +

@@ -487,26 +497,6 @@ type MethodProps = { children: React.Element<*>, }; -const Method = (props: MethodProps) => { - const children = React.Children.toArray(props.children); - const description = children.find(child => child.type === Description); - const apiArgs = children.find(child => child.type === Arguments); - - return ( - -

- {props.method} - {props.label} -

-
{description}
- HTTP request & arguments -

- {`${process.env.URL}/api/${props.method}`} -

- {apiArgs} - - ); -}; - const Description = (props: { children: React.Element<*> }) => (

{props.children}

); @@ -536,6 +526,26 @@ const Arguments = (props: ArgumentsProps) => ( ); +const Method = (props: MethodProps) => { + const children = React.Children.toArray(props.children); + const description = children.find(child => child.type === Description); + const apiArgs = children.find(child => child.type === Arguments); + + return ( + +

+ {props.method} - {props.label} +

+
{description}
+ HTTP request & arguments +

+ {`${process.env.URL}/api/${props.method}`} +

+ {apiArgs} +
+ ); +}; + type ArgumentProps = { id: string, required?: boolean, From 33fea77a86836890aef19aed7be3a0e4e49bb85e Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sun, 26 Nov 2017 18:18:04 -0800 Subject: [PATCH 16/16] PR feedback, remove collectionId --- app/models/Document.js | 1 - app/stores/DocumentsStore.js | 2 +- server/api/documents.test.js | 2 +- server/presenters/document.js | 1 - 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/models/Document.js b/app/models/Document.js index c67b1c580..00397a5de 100644 --- a/app/models/Document.js +++ b/app/models/Document.js @@ -20,7 +20,6 @@ class Document extends BaseModel { collaborators: Array; collection: $Shape; - collectionId: string; firstViewedAt: ?string; lastViewedAt: ?string; modifiedSinceViewed: ?boolean; diff --git a/app/stores/DocumentsStore.js b/app/stores/DocumentsStore.js index 4c05276b1..f03d025ed 100644 --- a/app/stores/DocumentsStore.js +++ b/app/stores/DocumentsStore.js @@ -56,7 +56,7 @@ class DocumentsStore extends BaseStore { return _.orderBy( _.filter( this.data.values(), - document => document.collectionId === collectionId + document => document.collection.id === collectionId ), 'updatedAt', 'desc' diff --git a/server/api/documents.test.js b/server/api/documents.test.js index 82380eed4..a7dbb96d5 100644 --- a/server/api/documents.test.js +++ b/server/api/documents.test.js @@ -37,7 +37,7 @@ describe('#documents.list', async () => { it('should allow filtering by collection', async () => { const { user, document } = await seed(); const res = await server.post('/api/documents.list', { - body: { token: user.getJwtToken(), collectionId: document.atlasId }, + body: { token: user.getJwtToken(), collection: document.atlasId }, }); const body = await res.json(); diff --git a/server/presenters/document.js b/server/presenters/document.js index 29ddff81d..575d18f3b 100644 --- a/server/presenters/document.js +++ b/server/presenters/document.js @@ -36,7 +36,6 @@ async function present(ctx: Object, document: Document, options: ?Options) { team: document.teamId, collaborators: [], starred: !!(document.starred && document.starred.length), - collectionId: document.atlasId, collaboratorCount: undefined, collection: undefined, views: undefined,