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

View File

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

View File

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

View File

@ -2,7 +2,7 @@
import { extendObservable, action, runInAction, computed } from 'mobx'; import { extendObservable, action, runInAction, computed } from 'mobx';
import invariant from 'invariant'; import invariant from 'invariant';
import ApiClient, { client } from 'utils/ApiClient'; import { client } from 'utils/ApiClient';
import stores from 'stores'; import stores from 'stores';
import ErrorsStore from 'stores/ErrorsStore'; import ErrorsStore from 'stores/ErrorsStore';
@ -25,8 +25,6 @@ class Document {
updatedBy: User; updatedBy: User;
url: string; url: string;
views: number; views: number;
client: ApiClient;
errors: ErrorsStore; errors: ErrorsStore;
/* Computed */ /* Computed */
@ -58,7 +56,7 @@ class Document {
@action update = async () => { @action update = async () => {
try { 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'); invariant(res && res.data, 'Document API response should be available');
const { data } = res; const { data } = res;
runInAction('Document#update', () => { runInAction('Document#update', () => {
@ -75,7 +73,6 @@ class Document {
constructor(document: Document) { constructor(document: Document) {
this.updateData(document); this.updateData(document);
this.client = client;
this.errors = stores.errors; 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 // @flow
import React from 'react'; import React from 'react';
import { observer } from 'mobx-react'; import { observer, inject } from 'mobx-react';
import styled from 'styled-components'; import styled from 'styled-components';
import DocumentsStore from 'stores/DocumentsStore';
import DocumentList from 'components/DocumentList'; import DocumentList from 'components/DocumentList';
import PageTitle from 'components/PageTitle'; import PageTitle from 'components/PageTitle';
import CenteredContent from 'components/CenteredContent'; import CenteredContent from 'components/CenteredContent';
import ViewedDocumentsStore from './ViewedDocumentsStore';
import EditedDocumentsStore from './EditedDocumentsStore';
const Subheading = styled.h3` const Subheading = styled.h3`
font-size: 11px; font-size: 11px;
@ -20,22 +19,16 @@ const Subheading = styled.h3`
margin-top: 30px; margin-top: 30px;
`; `;
type Props = {}; type Props = {
documents: DocumentsStore,
};
@observer class Dashboard extends React.Component { @observer class Dashboard extends React.Component {
props: Props; props: Props;
viewedStore: ViewedDocumentsStore;
editedStore: EditedDocumentsStore;
constructor(props: Props) {
super(props);
this.viewedStore = new ViewedDocumentsStore();
this.editedStore = new EditedDocumentsStore();
}
componentDidMount() { componentDidMount() {
this.viewedStore.fetchDocuments(); this.props.documents.fetchAll();
this.editedStore.fetchDocuments(); this.props.documents.fetchRecentlyViewed();
} }
render() { render() {
@ -44,13 +37,13 @@ type Props = {};
<PageTitle title="Home" /> <PageTitle title="Home" />
<h1>Home</h1> <h1>Home</h1>
<Subheading>Recently viewed</Subheading> <Subheading>Recently viewed</Subheading>
<DocumentList documents={this.viewedStore.documents} /> <DocumentList documents={this.props.documents.getRecentlyViewed()} />
<Subheading>Recently edited</Subheading> <Subheading>Recently edited</Subheading>
<DocumentList documents={this.editedStore.documents} /> <DocumentList documents={this.props.documents.data.values()} />
</CenteredContent> </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 // @flow
import React, { Component } from 'react'; import React, { Component } from 'react';
import { observer } from 'mobx-react'; import { observer, inject } from 'mobx-react';
import CenteredContent from 'components/CenteredContent'; import CenteredContent from 'components/CenteredContent';
import PageTitle from 'components/PageTitle'; import PageTitle from 'components/PageTitle';
import DocumentList from 'components/DocumentList'; import DocumentList from 'components/DocumentList';
import StarredStore from './StarredStore'; import DocumentsStore from 'stores/DocumentsStore';
@observer class Starred extends Component { @observer class Starred extends Component {
store: StarredStore; props: {
documents: DocumentsStore,
constructor() { };
super();
this.store = new StarredStore();
}
componentDidMount() { componentDidMount() {
this.store.fetchDocuments(); this.props.documents.fetchStarred();
} }
render() { render() {
@ -23,10 +20,10 @@ import StarredStore from './StarredStore';
<CenteredContent column auto> <CenteredContent column auto>
<PageTitle title="Starred" /> <PageTitle title="Starred" />
<h1>Starred</h1> <h1>Starred</h1>
<DocumentList documents={this.store.documents} /> <DocumentList documents={this.props.documents.getStarred()} />
</CenteredContent> </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 */ /* Actions */
@action fetch = async (): Promise<*> => { @action fetchAll = async (): Promise<*> => {
try { try {
const res = await this.client.post('/collections.list', { const res = await this.client.post('/collections.list', {
id: this.teamId, id: this.teamId,

View File

@ -27,7 +27,7 @@ describe('CollectionsStore', () => {
})), })),
}; };
await store.fetch(); await store.fetchAll();
expect(store.client.post).toHaveBeenCalledWith('/collections.list', { expect(store.client.post).toHaveBeenCalledWith('/collections.list', {
id: 123, id: 123,
@ -44,7 +44,7 @@ describe('CollectionsStore', () => {
add: jest.fn(), add: jest.fn(),
}; };
await store.fetch(); await store.fetchAll();
expect(store.errors.add).toHaveBeenCalledWith( expect(store.errors.add).toHaveBeenCalledWith(
'Failed to load collections' '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 // @flow
import { observable, action, computed } from 'mobx'; import { observable, action, computed } from 'mobx';
import type { Document } from 'types'; import Document from 'models/Document';
import Collection from 'models/Collection'; import Collection from 'models/Collection';
class UiStore { class UiStore {

View File

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