Merge documents

This commit is contained in:
Tom Moor
2017-06-27 22:15:29 -07:00
16 changed files with 156 additions and 128 deletions

20
__mocks__/localStorage.js Normal file
View File

@ -0,0 +1,20 @@
const storage = {};
export default {
setItem: function(key, value) {
storage[key] = value || '';
},
getItem: function(key) {
return key in storage ? storage[key] : null;
},
removeItem: function(key) {
delete storage[key];
},
get length() {
return Object.keys(storage).length;
},
key: function(i) {
var keys = Object.keys(storage);
return keys[i] || null;
},
};

View File

@ -1,6 +1,6 @@
// @flow
import React from 'react';
import type { Document } from 'types';
import Document from 'models/Document';
import DocumentPreview from 'components/DocumentPreview';
class DocumentList extends React.Component {
@ -13,7 +13,7 @@ class DocumentList extends React.Component {
<div>
{this.props.documents &&
this.props.documents.map(document => (
<DocumentPreview document={document} />
<DocumentPreview key={document.id} document={document} />
))}
</div>
);

View File

@ -1,7 +1,7 @@
// @flow
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import type { Document } from 'types';
import Document from 'models/Document';
import styled from 'styled-components';
import { color } from 'styles/constants';
import PublishingInfo from 'components/PublishingInfo';

View File

@ -11,6 +11,7 @@ import {
import { Flex } from 'reflexbox';
import stores from 'stores';
import DocumentsStore from 'stores/DocumentsStore';
import CollectionsStore from 'stores/CollectionsStore';
import 'normalize.css/normalize.css';
@ -57,12 +58,13 @@ const Auth = ({ children }: AuthProps) => {
const user = stores.auth.getUserStore();
authenticatedStores = {
user,
documents: new DocumentsStore(),
collections: new CollectionsStore({
teamId: user.team.id,
}),
};
authenticatedStores.collections.fetch();
authenticatedStores.collections.fetchAll();
}
return (
@ -135,3 +137,5 @@ render(
</div>,
document.getElementById('root')
);
window.authenticatedStores = authenticatedStores;

View File

@ -2,7 +2,7 @@
import { extendObservable, action, runInAction, computed } from 'mobx';
import invariant from 'invariant';
import ApiClient, { client } from 'utils/ApiClient';
import { client } from 'utils/ApiClient';
import stores from 'stores';
import ErrorsStore from 'stores/ErrorsStore';
@ -25,8 +25,6 @@ class Document {
updatedBy: User;
url: string;
views: number;
client: ApiClient;
errors: ErrorsStore;
/* Computed */
@ -58,7 +56,7 @@ class Document {
@action update = async () => {
try {
const res = await this.client.post('/documents.info', { id: this.id });
const res = await client.post('/documents.info', { id: this.id });
invariant(res && res.data, 'Document API response should be available');
const { data } = res;
runInAction('Document#update', () => {
@ -75,7 +73,6 @@ class Document {
constructor(document: Document) {
this.updateData(document);
this.client = client;
this.errors = stores.errors;
}
}

View File

@ -0,0 +1,13 @@
/* eslint-disable */
import Document from './Document';
describe('Document model', () => {
test('should initialize with data', () => {
const document = new Document({
id: 123,
title: 'Onboarding',
text: 'Some body text'
});
expect(document.title).toBe('Onboarding');
});
});

View File

@ -1,13 +1,12 @@
// @flow
import React from 'react';
import { observer } from 'mobx-react';
import { observer, inject } from 'mobx-react';
import styled from 'styled-components';
import DocumentsStore from 'stores/DocumentsStore';
import DocumentList from 'components/DocumentList';
import PageTitle from 'components/PageTitle';
import CenteredContent from 'components/CenteredContent';
import ViewedDocumentsStore from './ViewedDocumentsStore';
import EditedDocumentsStore from './EditedDocumentsStore';
const Subheading = styled.h3`
font-size: 11px;
@ -20,22 +19,16 @@ const Subheading = styled.h3`
margin-top: 30px;
`;
type Props = {};
type Props = {
documents: DocumentsStore,
};
@observer class Dashboard extends React.Component {
props: Props;
viewedStore: ViewedDocumentsStore;
editedStore: EditedDocumentsStore;
constructor(props: Props) {
super(props);
this.viewedStore = new ViewedDocumentsStore();
this.editedStore = new EditedDocumentsStore();
}
componentDidMount() {
this.viewedStore.fetchDocuments();
this.editedStore.fetchDocuments();
this.props.documents.fetchAll();
this.props.documents.fetchRecentlyViewed();
}
render() {
@ -44,13 +37,13 @@ type Props = {};
<PageTitle title="Home" />
<h1>Home</h1>
<Subheading>Recently viewed</Subheading>
<DocumentList documents={this.viewedStore.documents} />
<DocumentList documents={this.props.documents.getRecentlyViewed()} />
<Subheading>Recently edited</Subheading>
<DocumentList documents={this.editedStore.documents} />
<DocumentList documents={this.props.documents.data.values()} />
</CenteredContent>
);
}
}
export default Dashboard;
export default inject('documents')(Dashboard);

View File

@ -1,29 +0,0 @@
// @flow
import { observable, action, runInAction } from 'mobx';
import invariant from 'invariant';
import { client } from 'utils/ApiClient';
import type { Document } from 'types';
class EditedDocumentsStore {
@observable documents: Array<Document> = [];
@observable isFetching = false;
@action fetchDocuments = async () => {
this.isFetching = true;
try {
const res = await client.get('/documents.list');
invariant(res && res.data, 'res or res.data missing');
const { data } = res;
runInAction('update state after fetching data', () => {
this.documents = data;
});
} catch (e) {
console.error('Something went wrong');
}
this.isFetching = false;
};
}
export default EditedDocumentsStore;

View File

@ -1,29 +0,0 @@
// @flow
import { observable, action, runInAction } from 'mobx';
import invariant from 'invariant';
import { client } from 'utils/ApiClient';
import type { Document } from 'types';
class ViewedDocumentsStore {
@observable documents: Array<Document> = [];
@observable isFetching = false;
@action fetchDocuments = async () => {
this.isFetching = true;
try {
const res = await client.get('/documents.viewed');
invariant(res && res.data, 'res or res.data missing');
const { data } = res;
runInAction('update state after fetching data', () => {
this.documents = data;
});
} catch (e) {
console.error('Something went wrong');
}
this.isFetching = false;
};
}
export default ViewedDocumentsStore;

View File

@ -1,21 +1,18 @@
// @flow
import React, { Component } from 'react';
import { observer } from 'mobx-react';
import { observer, inject } from 'mobx-react';
import CenteredContent from 'components/CenteredContent';
import PageTitle from 'components/PageTitle';
import DocumentList from 'components/DocumentList';
import StarredStore from './StarredStore';
import DocumentsStore from 'stores/DocumentsStore';
@observer class Starred extends Component {
store: StarredStore;
constructor() {
super();
this.store = new StarredStore();
}
props: {
documents: DocumentsStore,
};
componentDidMount() {
this.store.fetchDocuments();
this.props.documents.fetchStarred();
}
render() {
@ -23,10 +20,10 @@ import StarredStore from './StarredStore';
<CenteredContent column auto>
<PageTitle title="Starred" />
<h1>Starred</h1>
<DocumentList documents={this.store.documents} />
<DocumentList documents={this.props.documents.getStarred()} />
</CenteredContent>
);
}
}
export default Starred;
export default inject('documents')(Starred);

View File

@ -1,29 +0,0 @@
// @flow
import { observable, action, runInAction } from 'mobx';
import invariant from 'invariant';
import { client } from 'utils/ApiClient';
import type { Document } from 'types';
class StarredDocumentsStore {
@observable documents: Array<Document> = [];
@observable isFetching = false;
@action fetchDocuments = async () => {
this.isFetching = true;
try {
const res = await client.get('/documents.starred');
invariant(res && res.data, 'res or res.data missing');
const { data } = res;
runInAction('update state after fetching data', () => {
this.documents = data;
});
} catch (e) {
console.error('Something went wrong');
}
this.isFetching = false;
};
}
export default StarredDocumentsStore;

View File

@ -22,7 +22,7 @@ class CollectionsStore {
/* Actions */
@action fetch = async (): Promise<*> => {
@action fetchAll = async (): Promise<*> => {
try {
const res = await this.client.post('/collections.list', {
id: this.teamId,

View File

@ -27,7 +27,7 @@ describe('CollectionsStore', () => {
})),
};
await store.fetch();
await store.fetchAll();
expect(store.client.post).toHaveBeenCalledWith('/collections.list', {
id: 123,
@ -44,7 +44,7 @@ describe('CollectionsStore', () => {
add: jest.fn(),
};
await store.fetch();
await store.fetchAll();
expect(store.errors.add).toHaveBeenCalledWith(
'Failed to load collections'

View File

@ -0,0 +1,89 @@
// @flow
import { observable, action, ObservableMap, runInAction } from 'mobx';
import { client } from 'utils/ApiClient';
import _ from 'lodash';
import invariant from 'invariant';
import stores from 'stores';
import Document from 'models/Document';
import ErrorsStore from 'stores/ErrorsStore';
class DocumentsStore {
@observable recentlyViewedIds: Array<string> = [];
@observable data: Map<string, Document> = new ObservableMap([]);
@observable isLoaded: boolean = false;
errors: ErrorsStore;
/* Actions */
@action fetchAll = async (request: string = 'list'): Promise<*> => {
try {
const res = await client.post(`/documents.${request}`);
invariant(res && res.data, 'Document list not available');
const { data } = res;
runInAction('DocumentsStore#fetchAll', () => {
data.forEach(document => {
this.data.set(document.id, new Document(document));
});
this.isLoaded = true;
});
return data;
} catch (e) {
this.errors.add('Failed to load documents');
}
};
@action fetchRecentlyViewed = async (): Promise<*> => {
const data = await this.fetchAll('viewed');
runInAction('DocumentsStore#fetchRecentlyViewed', () => {
this.recentlyViewedIds = _.map(data, 'id');
});
};
@action fetchStarred = async (): Promise<*> => {
await this.fetchAll('starred');
};
@action fetch = async (id: string): Promise<*> => {
try {
const res = await client.post('/documents.info', { id });
invariant(res && res.data, 'Document not available');
const { data } = res;
runInAction('DocumentsStore#fetch', () => {
this.data.set(data.id, new Document(data));
this.isLoaded = true;
});
} catch (e) {
this.errors.add('Failed to load documents');
}
};
@action add = (document: Document): void => {
this.data.set(document.id, document);
};
@action remove = (id: string): void => {
this.data.delete(id);
};
getStarred = () => {
return _.filter(this.data.values(), 'starred');
};
getRecentlyViewed = () => {
return _.filter(this.data.values(), ({ id }) =>
this.recentlyViewedIds.includes(id)
);
};
getById = (id: string): ?Document => {
return this.data.get(id);
};
constructor() {
this.errors = stores.errors;
}
}
export default DocumentsStore;

View File

@ -1,6 +1,6 @@
// @flow
import { observable, action, computed } from 'mobx';
import type { Document } from 'types';
import Document from 'models/Document';
import Collection from 'models/Collection';
class UiStore {

View File

@ -2,10 +2,12 @@
import React from 'react';
import { shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
import localStorage from '../../__mocks__/localStorage';
const snap = children => {
const wrapper = shallow(children);
expect(toJson(wrapper)).toMatchSnapshot();
};
global.localStorage = localStorage;
global.snap = snap;