diff --git a/__mocks__/localStorage.js b/__mocks__/localStorage.js
new file mode 100644
index 00000000..3d8394b9
--- /dev/null
+++ b/__mocks__/localStorage.js
@@ -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;
+ },
+};
diff --git a/frontend/components/DocumentList/DocumentList.js b/frontend/components/DocumentList/DocumentList.js
index bfd0b2d4..4ca8b63a 100644
--- a/frontend/components/DocumentList/DocumentList.js
+++ b/frontend/components/DocumentList/DocumentList.js
@@ -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 {
{this.props.documents &&
this.props.documents.map(document => (
-
+
))}
);
diff --git a/frontend/components/DocumentPreview/DocumentPreview.js b/frontend/components/DocumentPreview/DocumentPreview.js
index 6b974e20..c6d34b89 100644
--- a/frontend/components/DocumentPreview/DocumentPreview.js
+++ b/frontend/components/DocumentPreview/DocumentPreview.js
@@ -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';
diff --git a/frontend/index.js b/frontend/index.js
index c09fac02..0f050e1a 100644
--- a/frontend/index.js
+++ b/frontend/index.js
@@ -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(
,
document.getElementById('root')
);
+
+window.authenticatedStores = authenticatedStores;
diff --git a/frontend/models/Document.js b/frontend/models/Document.js
index 1c69baee..40fbd91b 100644
--- a/frontend/models/Document.js
+++ b/frontend/models/Document.js
@@ -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;
}
}
diff --git a/frontend/models/Document.test.js b/frontend/models/Document.test.js
new file mode 100644
index 00000000..e7db05b7
--- /dev/null
+++ b/frontend/models/Document.test.js
@@ -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');
+ });
+});
diff --git a/frontend/scenes/Dashboard/Dashboard.js b/frontend/scenes/Dashboard/Dashboard.js
index c36f4239..54410b2f 100644
--- a/frontend/scenes/Dashboard/Dashboard.js
+++ b/frontend/scenes/Dashboard/Dashboard.js
@@ -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 = {};
Home
Recently viewed
-
+
Recently edited
-
+
);
}
}
-export default Dashboard;
+export default inject('documents')(Dashboard);
diff --git a/frontend/scenes/Dashboard/EditedDocumentsStore.js b/frontend/scenes/Dashboard/EditedDocumentsStore.js
deleted file mode 100644
index 477500e1..00000000
--- a/frontend/scenes/Dashboard/EditedDocumentsStore.js
+++ /dev/null
@@ -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 = [];
- @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;
diff --git a/frontend/scenes/Dashboard/ViewedDocumentsStore.js b/frontend/scenes/Dashboard/ViewedDocumentsStore.js
deleted file mode 100644
index 147c85a1..00000000
--- a/frontend/scenes/Dashboard/ViewedDocumentsStore.js
+++ /dev/null
@@ -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 = [];
- @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;
diff --git a/frontend/scenes/Starred/Starred.js b/frontend/scenes/Starred/Starred.js
index 5c51b244..a0cbd919 100644
--- a/frontend/scenes/Starred/Starred.js
+++ b/frontend/scenes/Starred/Starred.js
@@ -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';
Starred
-
+
);
}
}
-export default Starred;
+export default inject('documents')(Starred);
diff --git a/frontend/scenes/Starred/StarredStore.js b/frontend/scenes/Starred/StarredStore.js
deleted file mode 100644
index 8fe6bd02..00000000
--- a/frontend/scenes/Starred/StarredStore.js
+++ /dev/null
@@ -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 = [];
- @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;
diff --git a/frontend/stores/CollectionsStore.js b/frontend/stores/CollectionsStore.js
index 2f9d2daf..ae8e2644 100644
--- a/frontend/stores/CollectionsStore.js
+++ b/frontend/stores/CollectionsStore.js
@@ -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,
diff --git a/frontend/stores/CollectionsStore.test.js b/frontend/stores/CollectionsStore.test.js
index 2694a9dc..ef39ccbe 100644
--- a/frontend/stores/CollectionsStore.test.js
+++ b/frontend/stores/CollectionsStore.test.js
@@ -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'
diff --git a/frontend/stores/DocumentsStore.js b/frontend/stores/DocumentsStore.js
new file mode 100644
index 00000000..d72ffa5d
--- /dev/null
+++ b/frontend/stores/DocumentsStore.js
@@ -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 = [];
+ @observable data: Map = 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;
diff --git a/frontend/stores/UiStore.js b/frontend/stores/UiStore.js
index 74068d0d..48327a7b 100644
--- a/frontend/stores/UiStore.js
+++ b/frontend/stores/UiStore.js
@@ -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 {
diff --git a/frontend/utils/setupJest.js b/frontend/utils/setupJest.js
index dcd1f075..61b717c2 100644
--- a/frontend/utils/setupJest.js
+++ b/frontend/utils/setupJest.js
@@ -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;