Merge pull request #434 from outline/collection-home

Collection Home
This commit is contained in:
Tom Moor
2017-11-26 18:27:21 -08:00
committed by GitHub
25 changed files with 373 additions and 253 deletions

View File

@@ -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;

View File

@@ -0,0 +1,4 @@
// @flow
import Actions from './Actions';
export { Action, Separator } from './Actions';
export default Actions;

View File

@@ -12,7 +12,7 @@ const Container = styled.div`
`;
const Content = styled.div`
max-width: 50em;
max-width: 46em;
margin: 0 auto;
`;

View File

@@ -157,6 +157,10 @@ const SwatchInset = styled(Flex)`
const StyledOutline = styled(Outline)`
padding: 5px;
strong {
font-weight: 500;
}
`;
const HexHash = styled.div`

View File

@@ -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 {

View File

@@ -6,7 +6,7 @@ import type { Props } from './Icon';
export default function CollectionIcon({
expanded,
...rest
}: Props & { expanded: boolean }) {
}: Props & { expanded?: boolean }) {
return (
<Icon {...rest}>
{expanded ? (

View File

@@ -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 (
<Fade>
<Item column auto>
<Mask style={{ width: randomValues[0] }} header />
<Mask style={{ width: randomValues[1] }} />
</Item>
<Item column auto>
<Mask style={{ width: randomValues[2] }} header />
<Mask style={{ width: randomValues[3] }} />
</Item>
</Fade>
);
};
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;
`;

View File

@@ -1,3 +0,0 @@
// @flow
import LoadingListPlaceholder from './LoadingListPlaceholder';
export default LoadingListPlaceholder;

View File

@@ -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;

View File

@@ -0,0 +1,3 @@
// @flow
import Subheading from './Subheading';
export default Subheading;

View File

@@ -39,6 +39,11 @@ class Collection extends BaseModel {
return true;
}
@computed
get isEmpty(): boolean {
return this.documents.length === 0;
}
/* Actions */
@action

View File

@@ -20,6 +20,7 @@ class Document extends BaseModel {
collaborators: Array<User>;
collection: $Shape<Collection>;
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) {

View File

@@ -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 (
<NewDocumentContainer auto column justify="center">
<h1>Create a document</h1>
<CenteredContent>
<PageTitle title={this.collection.name} />
<Heading>
<CollectionIcon color={this.collection.color} size={40} expanded />{' '}
{this.collection.name}
</Heading>
<HelpText>
Publish your first document to start building the{' '}
<strong>{this.collection.name}</strong> collection.
Publish your first document to start building this collection.
</HelpText>
<Action>
<Wrapper>
<Link to={newDocumentUrl(this.collection)}>
<Button>Create new document</Button>
</Link>
</Action>
</NewDocumentContainer>
</Wrapper>
</CenteredContent>
);
}
renderNotFound() {
return <Search notFound />;
}
render() {
if (this.redirectUrl) return <Redirect to={this.redirectUrl} />;
if (!this.isFetching && !this.collection) {
return this.renderNotFound();
}
if (this.collection && this.collection.isEmpty) {
return this.renderEmptyCollection();
}
return (
<CenteredContent>
{this.isFetching ? (
<LoadingListPlaceholder />
{this.collection ? (
<span>
<PageTitle title={this.collection.name} />
<Heading>
<CollectionIcon
color={this.collection.color}
size={40}
expanded
/>{' '}
{this.collection.name}
</Heading>
<Subheading>Recently edited</Subheading>
<DocumentList
documents={this.props.documents.recentlyEditedInCollection(
this.collection.id
)}
/>
<Actions align="center" justify="flex-end">
<Action>
<CollectionMenu collection={this.collection} />
</Action>
<Separator />
<Action>
<a onClick={this.onNewDocument}>
<NewDocumentIcon />
</a>
</Action>
</Actions>
</span>
) : (
this.renderEmptyCollection()
<ListPlaceholder count={5} />
)}
</CenteredContent>
);
}
}
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)
);

View File

@@ -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 (
<CenteredContent>
<PageTitle title="Home" />
<h1>Home</h1>
{showContent ? (
<Flex column>
{recentlyViewedLoaded && (
<Flex column>
<Subheading>Recently viewed</Subheading>
<DocumentList documents={documents.recentlyViewed} />
</Flex>
)}
{recentlyEditedLoaded && (
<Flex column>
<Subheading>Recently edited</Subheading>
<DocumentList documents={documents.recentlyEdited} />
</Flex>
)}
</Flex>
<span>
{hasRecentlyViewed && [
<Subheading>Recently viewed</Subheading>,
<DocumentList documents={documents.recentlyViewed} />,
]}
{hasRecentlyEdited && [
<Subheading>Recently edited</Subheading>,
<DocumentList documents={documents.recentlyEdited} />,
]}
</span>
) : (
<ListPlaceholder count={5} />
)}

View File

@@ -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}
/>
<Editor
key={document.id}
key={`${document.id}-${document.revision}`}
text={document.text}
emoji={document.emoji}
onImageUploadStart={this.onImageUploadStart}
@@ -243,49 +242,47 @@ class DocumentScene extends Component {
onCancel={this.onDiscard}
readOnly={!this.isEditing}
/>
<Meta
<Actions
align="center"
justify="flex-end"
readOnly={!this.isEditing}
>
<Flex align="center">
{!isNew &&
!this.isEditing && <Collaborators document={document} />}
<HeaderAction>
{this.isEditing ? (
<SaveAction
isSaving={this.isSaving}
onClick={this.onSave.bind(this, true)}
disabled={
!(this.document && this.document.allowSave) ||
this.isSaving
}
isNew={!!isNew}
/>
) : (
<a onClick={this.onClickEdit}>Edit</a>
)}
</HeaderAction>
{this.isEditing && (
<HeaderAction>
<a onClick={this.onDiscard}>Discard</a>
</HeaderAction>
{!isNew &&
!this.isEditing && <Collaborators document={document} />}
<Action>
{this.isEditing ? (
<SaveAction
isSaving={this.isSaving}
onClick={this.onSave.bind(this, true)}
disabled={
!(this.document && this.document.allowSave) ||
this.isSaving
}
isNew={!!isNew}
/>
) : (
<a onClick={this.onClickEdit}>Edit</a>
)}
</Action>
{this.isEditing && (
<Action>
<a onClick={this.onDiscard}>Discard</a>
</Action>
)}
{!this.isEditing && (
<Action>
<DocumentMenu document={document} />
</Action>
)}
{!this.isEditing && <Separator />}
<Action>
{!this.isEditing && (
<HeaderAction>
<DocumentMenu document={document} />
</HeaderAction>
<a onClick={this.onClickNew}>
<NewDocumentIcon />
</a>
)}
{!this.isEditing && <Separator />}
<HeaderAction>
{!this.isEditing && (
<a onClick={this.onClickNew}>
<NewDocumentIcon />
</a>
)}
</HeaderAction>
</Flex>
</Meta>
</Action>
</Actions>
</Flex>
)}
</Container>
@@ -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%;

View File

@@ -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<DocumentPathItem>,
path: DocumentPathItem[],
};
class CollectionsStore {
@observable data: ObservableArray<Collection> = observable.array([]);
@observable data: Map<string, Collection> = 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<?Collection> => {
fetch = async (id: string): Promise<?Collection> => {
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;
}
}

View File

@@ -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();

View File

@@ -52,6 +52,17 @@ class DocumentsStore extends BaseStore {
return _.take(_.orderBy(this.data.values(), 'updatedAt', 'desc'), 5);
}
recentlyEditedInCollection(collectionId: string): Array<Document> {
return _.orderBy(
_.filter(
this.data.values(),
document => document.collection.id === collectionId
),
'updatedAt',
'desc'
);
}
@computed
get starred(): Array<Document> {
return _.filter(this.data.values(), 'starred');

View File

@@ -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;

View File

@@ -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",

View File

@@ -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

View File

@@ -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 = [

View File

@@ -128,9 +128,9 @@ export default function Pricing() {
<p>
To authenticate with Outline API, you can supply the API key as a
header (<code>Authorization: Bearer YOUR_API_KEY</code>) or as part of
the payload using <code>token</code> parameter. If you're making{' '}
the payload using <code>token</code> parameter. If youre making{' '}
<code>GET</code> requests, header based authentication is recommended
so that your keys don't leak into logs.
so that your keys dont leak into logs.
</p>
<p>
@@ -241,7 +241,7 @@ export default function Pricing() {
<Method method="collections.delete" label="Delete a collection">
<Description>
Delete a collection and all of its documents. This action can`t be
Delete a collection and all of its documents. This action cant be
undone so please be careful.
</Description>
<Arguments>
@@ -249,6 +249,16 @@ export default function Pricing() {
</Arguments>
</Method>
<Method method="documents.list" label="List your documents">
<Description>List all your documents.</Description>
<Arguments pagination>
<Argument
id="collection"
description="Collection id to filter by"
/>
</Arguments>
</Method>
<Method method="documents.info" label="Get a document">
<Description>
<p>
@@ -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 (
<MethodContainer>
<h3 id={props.method}>
<code>{props.method}</code> - {props.label}
</h3>
<div>{description}</div>
<Request>HTTP request & arguments</Request>
<p>
<code>{`${process.env.URL}/api/${props.method}`}</code>
</p>
{apiArgs}
</MethodContainer>
);
};
const Description = (props: { children: React.Element<*> }) => (
<p>{props.children}</p>
);
@@ -536,6 +526,26 @@ const Arguments = (props: ArgumentsProps) => (
</table>
);
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 (
<MethodContainer>
<h3 id={props.method}>
<code>{props.method}</code> - {props.label}
</h3>
<div>{description}</div>
<Request>HTTP request & arguments</Request>
<p>
<code>{`${process.env.URL}/api/${props.method}`}</code>
</p>
{apiArgs}
</MethodContainer>
);
};
type ArgumentProps = {
id: string,
required?: boolean,

View File

@@ -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,

View File

@@ -45,7 +45,7 @@ export const color = {
text: '#171B35',
/* Brand */
primary: '#2B8FBF',
primary: '#1AB6FF',
danger: '#D0021B',
warning: '#f08a24' /* replace */,
success: '#43AC6A' /* replace */,