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/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/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/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/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/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/models/Document.js b/app/models/Document.js index 00397a5de..5bd9392f6 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; @@ -38,6 +39,7 @@ class Document extends BaseModel { updatedBy: User; url: string; views: number; + revision: number; data: Object; @@ -167,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/Collection/Collection.js b/app/scenes/Collection/Collection.js index 193e58de3..5d08074e7 100644 --- a/app/scenes/Collection/Collection.js +++ b/app/scenes/Collection/Collection.js @@ -2,21 +2,34 @@ import React, { Component } from 'react'; import { observable } from 'mobx'; import { observer, inject } from 'mobx-react'; -import { Link, Redirect } from 'react-router-dom'; +import { withRouter, Link } from 'react-router-dom'; 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'; +import Search from 'scenes/Search'; +import CollectionMenu from 'menus/CollectionMenu'; +import Actions, { Action, Separator } from 'components/Actions'; import CenteredContent from 'components/CenteredContent'; -import LoadingListPlaceholder from 'components/LoadingListPlaceholder'; +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'; +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, + history: Object, match: Object, }; @@ -24,73 +37,129 @@ 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); - }; + componentDidMount() { + this.loadContent(this.props.match.params.id); + } componentWillReceiveProps(nextProps) { if (nextProps.match.params.id !== this.props.match.params.id) { - this.fetchDocument(nextProps.match.params.id); + this.loadContent(nextProps.match.params.id); } } - fetchDocument = async (id: string) => { + loadContent = async (id: string) => { const { collections } = this.props; - this.collection = await collections.fetchById(id); + const collection = collections.getById(id) || (await collections.fetch(id)); - if (!this.collection) this.redirectUrl = '/404'; - - if (this.collection && this.collection.documents.length > 0) { - this.redirectUrl = this.collection.documents[0].url; + if (collection) { + this.props.ui.setActiveCollection(collection); + this.collection = collection; + await this.props.documents.fetchRecentlyModified({ + limit: 10, + collection: collection.id, + }); } + this.isFetching = false; }; + onNewDocument = (ev: SyntheticEvent) => { + ev.preventDefault(); + + if (this.collection) { + this.props.history.push(`${this.collection.url}/new`); + } + }; + renderEmptyCollection() { 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 && !this.collection) { + return this.renderNotFound(); + } + if (this.collection && this.collection.isEmpty) { + return this.renderEmptyCollection(); + } return ( - {this.isFetching ? ( - + {this.collection ? ( + + + + {' '} + {this.collection.name} + + Recently edited + + + + + + + + + + + + + ) : ( - this.renderEmptyCollection() + )} ); } } -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)` +const Wrapper = styled(Flex)` margin: 10px 0; `; -export default inject('collections')(CollectionScene); +export default withRouter( + inject('collections', 'documents', '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..7aa08b9cb 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 = ` @@ -44,7 +44,6 @@ type Props = { match: Object, history: Object, location: Location, - keydown: Object, documents: DocumentsStore, collections: CollectionsStore, newDocument?: boolean, @@ -233,7 +232,7 @@ class DocumentScene extends Component { message={DISCARD_CHANGES} /> - - - {!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 && ( - - - - )} - - - + + )} @@ -293,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%; diff --git a/app/stores/CollectionsStore.js b/app/stores/CollectionsStore.js index 18b205af4..67973f4b7 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,9 @@ 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 +21,16 @@ 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 +42,7 @@ class CollectionsStore { @computed get orderedData(): Collection[] { - return _.sortBy(this.data, 'name'); + return _.sortBy(this.data.values(), 'name'); } /** @@ -100,63 +90,70 @@ class CollectionsStore { @action fetchAll = async (): Promise<*> => { + 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', () => { - this.data.replace(data.map(collection => new Collection(collection))); + runInAction('CollectionsStore#fetchAll', () => { + 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/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(); diff --git a/app/stores/DocumentsStore.js b/app/stores/DocumentsStore.js index 99235facd..f03d025ed 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.collection.id === collectionId + ), + 'updatedAt', + 'desc' + ); + } + @computed get starred(): Array { return _.filter(this.data.values(), 'starred'); 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/server/api/__snapshots__/documents.test.js.snap b/server/api/__snapshots__/documents.test.js.snap index 3400de1c4..d7d09f8ae 100644 --- a/server/api/__snapshots__/documents.test.js.snap +++ b/server/api/__snapshots__/documents.test.js.snap @@ -45,6 +45,15 @@ Object { } `; +exports[`#documents.update should fail if document lastRevision does not match 1`] = ` +Object { + "error": "document_has_changed_since_last_revision", + "message": "Document has changed since last revision", + "ok": false, + "status": 400, +} +`; + exports[`#documents.viewed should require authentication 1`] = ` Object { "error": "authentication_required", diff --git a/server/api/documents.js b/server/api/documents.js index 5e098ca8b..2aeae2787 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, collection } = ctx.body; if (direction !== 'ASC') direction = 'DESC'; const user = ctx.state.user; + let where = { teamId: user.teamId }; + if (collection) where = { ...where, atlasId: collection }; + 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, @@ -239,7 +242,7 @@ router.post('documents.create', auth(), async ctx => { }); 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'); @@ -247,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 7d2bc37df..e1eec7bbe 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(), collection: 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(); @@ -266,6 +277,7 @@ describe('#documents.update', async () => { id: document.id, title: 'Updated title', text: 'Updated text', + lastRevision: document.revision, }, }); const body = await res.json(); @@ -276,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/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, diff --git a/server/presenters/document.js b/server/presenters/document.js index 575d18f3b..a665b307d 100644 --- a/server/presenters/document.js +++ b/server/presenters/document.js @@ -36,6 +36,8 @@ 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, views: 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 */,