diff --git a/app/components/DocumentList.js b/app/components/DocumentList.js
new file mode 100644
index 000000000..78c67d7f4
--- /dev/null
+++ b/app/components/DocumentList.js
@@ -0,0 +1,34 @@
+// @flow
+import * as React from 'react';
+import Document from 'models/Document';
+import DocumentPreview from 'components/DocumentPreview';
+import ArrowKeyNavigation from 'boundless-arrow-key-navigation';
+
+type Props = {
+ documents: Document[],
+ showCollection?: boolean,
+ limit?: number,
+};
+
+export default function DocumentList({
+ limit,
+ showCollection,
+ documents,
+}: Props) {
+ const items = limit ? documents.splice(0, limit) : documents;
+
+ return (
+
+ {items.map(document => (
+
+ ))}
+
+ );
+}
diff --git a/app/components/DocumentList/DocumentList.js b/app/components/DocumentList/DocumentList.js
deleted file mode 100644
index 97e53008a..000000000
--- a/app/components/DocumentList/DocumentList.js
+++ /dev/null
@@ -1,37 +0,0 @@
-// @flow
-import * as React from 'react';
-import Document from 'models/Document';
-import DocumentPreview from 'components/DocumentPreview';
-import ArrowKeyNavigation from 'boundless-arrow-key-navigation';
-
-type Props = {
- documents: Document[],
- showCollection?: boolean,
- limit?: number,
-};
-
-class DocumentList extends React.Component {
- render() {
- const { limit, showCollection } = this.props;
- const documents = limit
- ? this.props.documents.splice(0, limit)
- : this.props.documents;
-
- return (
-
- {documents.map(document => (
-
- ))}
-
- );
- }
-}
-
-export default DocumentList;
diff --git a/app/components/DocumentList/index.js b/app/components/DocumentList/index.js
deleted file mode 100644
index 35fb670b4..000000000
--- a/app/components/DocumentList/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-// @flow
-import DocumentList from './DocumentList';
-export default DocumentList;
diff --git a/app/components/DocumentPreview/components/PublishingInfo.js b/app/components/DocumentPreview/components/PublishingInfo.js
index 34ed11055..1adfcb46b 100644
--- a/app/components/DocumentPreview/components/PublishingInfo.js
+++ b/app/components/DocumentPreview/components/PublishingInfo.js
@@ -24,31 +24,38 @@ type Props = {
};
function PublishingInfo({ collection, document }: Props) {
- const { modifiedSinceViewed, updatedAt, updatedBy, publishedAt } = document;
+ const {
+ modifiedSinceViewed,
+ updatedAt,
+ updatedBy,
+ publishedAt,
+ isDraft,
+ } = document;
+ const neverUpdated = publishedAt === updatedAt;
return (
- {publishedAt && publishedAt === updatedAt ? (
+ {publishedAt && neverUpdated ? (
{updatedBy.name} published ago
) : (
{updatedBy.name}
- {publishedAt ? (
-
- modified ago
-
- ) : (
+ {isDraft ? (
saved ago
+ ) : (
+
+ modified ago
+
)}
)}
{collection && (
- in {collection.name}
+ in {isDraft ? 'Drafts' : collection.name}
)}
diff --git a/app/components/PaginatedDocumentList.js b/app/components/PaginatedDocumentList.js
new file mode 100644
index 000000000..1ad102e5c
--- /dev/null
+++ b/app/components/PaginatedDocumentList.js
@@ -0,0 +1,78 @@
+// @flow
+import * as React from 'react';
+import { observable, action } from 'mobx';
+import { observer } from 'mobx-react';
+import Waypoint from 'react-waypoint';
+
+import { DEFAULT_PAGINATION_LIMIT } from 'stores/DocumentsStore';
+import Document from 'models/Document';
+import DocumentList from 'components/DocumentList';
+import { ListPlaceholder } from 'components/LoadingPlaceholder';
+
+type Props = {
+ showCollection?: boolean,
+ documents: Document[],
+ fetch: (options: ?Object) => Promise<*>,
+ options?: Object,
+};
+
+@observer
+class PaginatedDocumentList extends React.Component {
+ @observable isLoaded: boolean = false;
+ @observable isFetching: boolean = false;
+ @observable offset: number = 0;
+ @observable allowLoadMore: boolean = true;
+
+ componentDidMount() {
+ this.fetchResults();
+ }
+
+ componentDidUpdate(prevProps: Props) {
+ if (prevProps.fetch !== this.props.fetch) {
+ this.fetchResults();
+ }
+ }
+
+ fetchResults = async () => {
+ this.isFetching = true;
+
+ const limit = DEFAULT_PAGINATION_LIMIT;
+ const results = await this.props.fetch({ limit, ...this.props.options });
+
+ if (
+ results &&
+ (results.length === 0 || results.length < DEFAULT_PAGINATION_LIMIT)
+ ) {
+ this.allowLoadMore = false;
+ } else {
+ this.offset += DEFAULT_PAGINATION_LIMIT;
+ }
+
+ this.isLoaded = true;
+ this.isFetching = false;
+ };
+
+ @action
+ loadMoreResults = async () => {
+ // Don't paginate if there aren't more results or we’re in the middle of fetching
+ if (!this.allowLoadMore || this.isFetching) return;
+ await this.fetchResults();
+ };
+
+ render() {
+ const { showCollection, documents } = this.props;
+
+ return this.isLoaded || documents.length ? (
+
+
+ {this.allowLoadMore && (
+
+ )}
+
+ ) : (
+
+ );
+ }
+}
+
+export default PaginatedDocumentList;
diff --git a/app/components/Sidebar/Main.js b/app/components/Sidebar/Main.js
index 1b126e0b1..f56d29f7b 100644
--- a/app/components/Sidebar/Main.js
+++ b/app/components/Sidebar/Main.js
@@ -56,7 +56,7 @@ class MainSidebar extends React.Component {
- }>
+ } exact={false}>
Home
}>
diff --git a/app/components/Sidebar/components/SidebarLink.js b/app/components/Sidebar/components/SidebarLink.js
index 0ca4c3c9e..cd643e8d8 100644
--- a/app/components/Sidebar/components/SidebarLink.js
+++ b/app/components/Sidebar/components/SidebarLink.js
@@ -52,6 +52,7 @@ type Props = {
iconColor?: string,
active?: boolean,
theme: Object,
+ exact?: boolean,
};
@observer
@@ -100,6 +101,7 @@ class SidebarLink extends React.Component {
menu,
menuOpen,
hideExpandToggle,
+ exact,
} = this.props;
const Component = to ? StyledNavLink : StyledDiv;
const showExpandIcon =
@@ -113,7 +115,7 @@ class SidebarLink extends React.Component {
style={active ? this.activeStyle : undefined}
onClick={onClick}
to={to}
- exact
+ exact={exact !== false}
>
{icon && {icon}}
{showExpandIcon && (
diff --git a/app/components/Tab.js b/app/components/Tab.js
new file mode 100644
index 000000000..fab0f010e
--- /dev/null
+++ b/app/components/Tab.js
@@ -0,0 +1,26 @@
+// @flow
+import * as React from 'react';
+import styled, { withTheme } from 'styled-components';
+import { NavLink } from 'react-router-dom';
+
+const NavItem = styled(NavLink)`
+ display: inline-block;
+ font-size: 11px;
+ font-weight: 500;
+ text-transform: uppercase;
+ color: ${props => props.theme.slate};
+ letter-spacing: 0.04em;
+ margin-right: 20px;
+ padding-bottom: 8px;
+`;
+
+function Tab(props: *) {
+ const activeStyle = {
+ paddingBottom: '5px',
+ borderBottom: `3px solid ${props.theme.slateLight}`,
+ };
+
+ return ;
+}
+
+export default withTheme(Tab);
diff --git a/app/components/Tabs.js b/app/components/Tabs.js
new file mode 100644
index 000000000..78fd63123
--- /dev/null
+++ b/app/components/Tabs.js
@@ -0,0 +1,10 @@
+// @flow
+import styled from 'styled-components';
+
+const Tabs = styled.nav`
+ border-bottom: 1px solid ${props => props.theme.slateLight};
+ margin-top: 22px;
+ margin-bottom: 10px;
+`;
+
+export default Tabs;
diff --git a/app/index.js b/app/index.js
index 8a92ce64c..3f9b7b277 100644
--- a/app/index.js
+++ b/app/index.js
@@ -3,54 +3,23 @@ import * as React from 'react';
import { render } from 'react-dom';
import { Provider } from 'mobx-react';
import { ThemeProvider } from 'styled-components';
-import {
- BrowserRouter as Router,
- Switch,
- Route,
- Redirect,
-} from 'react-router-dom';
+import { BrowserRouter as Router } from 'react-router-dom';
import stores from 'stores';
import theme from 'shared/styles/theme';
import globalStyles from 'shared/styles/globals';
import 'shared/styles/prism.css';
-import Home from 'scenes/Home';
-import Dashboard from 'scenes/Dashboard';
-import Starred from 'scenes/Starred';
-import Drafts from 'scenes/Drafts';
-import Collection from 'scenes/Collection';
-import Document from 'scenes/Document';
-import Search from 'scenes/Search';
-import Settings from 'scenes/Settings';
-import Details from 'scenes/Settings/Details';
-import People from 'scenes/Settings/People';
-import Slack from 'scenes/Settings/Slack';
-import Shares from 'scenes/Settings/Shares';
-import Tokens from 'scenes/Settings/Tokens';
-import Export from 'scenes/Settings/Export';
-import Error404 from 'scenes/Error404';
-
import ErrorBoundary from 'components/ErrorBoundary';
import ScrollToTop from 'components/ScrollToTop';
import ScrollToAnchor from 'components/ScrollToAnchor';
-import Layout from 'components/Layout';
-import Auth from 'components/Auth';
-import RouteSidebarHidden from 'components/RouteSidebarHidden';
-
-import { matchDocumentSlug } from 'utils/routeHelpers';
+import Routes from './routes';
let DevTools;
if (__DEV__) {
DevTools = require('mobx-react-devtools').default; // eslint-disable-line global-require
}
-const notFoundSearch = () => ;
-const DocumentNew = () => ;
-const RedirectDocument = ({ match }: { match: Object }) => (
-
-);
-
globalStyles();
const element = document.getElementById('root');
@@ -64,92 +33,7 @@ if (element) {
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/app/routes.js b/app/routes.js
new file mode 100644
index 000000000..b2be2b839
--- /dev/null
+++ b/app/routes.js
@@ -0,0 +1,78 @@
+// @flow
+import * as React from 'react';
+import { Switch, Route, Redirect } from 'react-router-dom';
+
+import Home from 'scenes/Home';
+import Dashboard from 'scenes/Dashboard';
+import Starred from 'scenes/Starred';
+import Drafts from 'scenes/Drafts';
+import Collection from 'scenes/Collection';
+import Document from 'scenes/Document';
+import Search from 'scenes/Search';
+import Settings from 'scenes/Settings';
+import Details from 'scenes/Settings/Details';
+import People from 'scenes/Settings/People';
+import Slack from 'scenes/Settings/Slack';
+import Shares from 'scenes/Settings/Shares';
+import Tokens from 'scenes/Settings/Tokens';
+import Export from 'scenes/Settings/Export';
+import Error404 from 'scenes/Error404';
+
+import Layout from 'components/Layout';
+import Auth from 'components/Auth';
+import RouteSidebarHidden from 'components/RouteSidebarHidden';
+import { matchDocumentSlug as slug } from 'utils/routeHelpers';
+
+const NotFound = () => ;
+const DocumentNew = () => ;
+const RedirectDocument = ({ match }: { match: Object }) => (
+
+);
+
+export default function Routes() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/scenes/Collection.js b/app/scenes/Collection.js
index 1de8ea2a5..99cd8e2da 100644
--- a/app/scenes/Collection.js
+++ b/app/scenes/Collection.js
@@ -61,7 +61,7 @@ class CollectionScene extends React.Component {
this.collection = collection;
await Promise.all([
- this.props.documents.fetchRecentlyModified({
+ this.props.documents.fetchRecentlyEdited({
limit: 10,
collection: id,
}),
diff --git a/app/scenes/Dashboard.js b/app/scenes/Dashboard.js
index 5b0a5ea02..38f98a1bc 100644
--- a/app/scenes/Dashboard.js
+++ b/app/scenes/Dashboard.js
@@ -1,83 +1,75 @@
// @flow
import * as React from 'react';
-import { observable } from 'mobx';
+import { Switch, Route } from 'react-router-dom';
import { observer, inject } from 'mobx-react';
import { NewDocumentIcon } from 'outline-icons';
import DocumentsStore from 'stores/DocumentsStore';
+import AuthStore from 'stores/AuthStore';
import NewDocumentMenu from 'menus/NewDocumentMenu';
import Actions, { Action } from 'components/Actions';
import CenteredContent from 'components/CenteredContent';
-import DocumentList from 'components/DocumentList';
import PageTitle from 'components/PageTitle';
-import Subheading from 'components/Subheading';
-import { ListPlaceholder } from 'components/LoadingPlaceholder';
+import Tabs from 'components/Tabs';
+import Tab from 'components/Tab';
+import PaginatedDocumentList from '../components/PaginatedDocumentList';
type Props = {
documents: DocumentsStore,
+ auth: AuthStore,
};
@observer
class Dashboard extends React.Component {
- @observable isLoaded: boolean = false;
-
- componentDidMount() {
- this.loadContent();
- }
-
- loadContent = async () => {
- await Promise.all([
- this.props.documents.fetchRecentlyModified({ limit: 5 }),
- this.props.documents.fetchRecentlyViewed({ limit: 5 }),
- ]);
- this.isLoaded = true;
- };
-
render() {
- const { documents } = this.props;
- const hasRecentlyViewed = documents.recentlyViewed.length > 0;
- const hasRecentlyEdited = documents.recentlyEdited.length > 0;
- const showContent =
- this.isLoaded || (hasRecentlyViewed && hasRecentlyEdited);
+ const { documents, auth } = this.props;
+ if (!auth.user) return;
+ const user = auth.user.id;
return (
Home
- {showContent ? (
-
- {hasRecentlyViewed && (
-
- Recently viewed
-
-
- )}
- {hasRecentlyEdited && (
-
- Recently edited
-
-
- )}
-
-
- } />
-
-
-
- ) : (
-
- )}
+
+
+ Recently updated
+
+
+ Recently viewed
+
+ Created by me
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ } />
+
+
);
}
}
-export default inject('documents')(Dashboard);
+export default inject('documents', 'auth')(Dashboard);
diff --git a/app/stores/DocumentsStore.js b/app/stores/DocumentsStore.js
index dbe746f54..eb940512b 100644
--- a/app/stores/DocumentsStore.js
+++ b/app/stores/DocumentsStore.js
@@ -21,18 +21,16 @@ type FetchOptions = {
};
class DocumentsStore extends BaseStore {
- @observable recentlyViewedIds: Array = [];
- @observable recentlyEditedIds: Array = [];
+ @observable recentlyViewedIds: string[] = [];
+ @observable recentlyEditedIds: string[] = [];
@observable data: Map = new ObservableMap([]);
@observable isLoaded: boolean = false;
@observable isFetching: boolean = false;
ui: UiStore;
- /* Computed */
-
@computed
- get recentlyViewed(): Array {
+ get recentlyViewed(): Document[] {
const docs = [];
this.recentlyViewedIds.forEach(id => {
const doc = this.getById(id);
@@ -51,6 +49,17 @@ class DocumentsStore extends BaseStore {
return docs;
}
+ createdByUser(userId: string): Document[] {
+ return _.orderBy(
+ _.filter(
+ this.data.values(),
+ document => document.createdBy.id === userId
+ ),
+ 'updatedAt',
+ 'desc'
+ );
+ }
+
pinnedInCollection(collectionId: string): Document[] {
return _.filter(
this.recentlyEditedInCollection(collectionId),
@@ -118,10 +127,10 @@ class DocumentsStore extends BaseStore {
};
@action
- fetchRecentlyModified = async (options: ?PaginationParams): Promise<*> => {
+ fetchRecentlyEdited = async (options: ?PaginationParams): Promise<*> => {
const data = await this.fetchPage('list', options);
- runInAction('DocumentsStore#fetchRecentlyModified', () => {
+ runInAction('DocumentsStore#fetchRecentlyEdited', () => {
this.recentlyEditedIds = _.map(data, 'id');
});
return data;
@@ -138,18 +147,23 @@ class DocumentsStore extends BaseStore {
};
@action
- fetchStarred = async (options: ?PaginationParams): Promise<*> => {
- await this.fetchPage('starred', options);
+ fetchStarred = (options: ?PaginationParams): Promise<*> => {
+ return this.fetchPage('starred', options);
};
@action
- fetchDrafts = async (options: ?PaginationParams): Promise<*> => {
- await this.fetchPage('drafts', options);
+ fetchDrafts = (options: ?PaginationParams): Promise<*> => {
+ return this.fetchPage('drafts', options);
};
@action
- fetchPinned = async (options: ?PaginationParams): Promise<*> => {
- await this.fetchPage('pinned', options);
+ fetchPinned = (options: ?PaginationParams): Promise<*> => {
+ return this.fetchPage('pinned', options);
+ };
+
+ @action
+ fetchOwned = (options: ?PaginationParams): Promise<*> => {
+ return this.fetchPage('list', options);
};
@action
@@ -261,11 +275,11 @@ class DocumentsStore extends BaseStore {
// Re-fetch dashboard content so that we don't show deleted documents
this.on('collections.delete', () => {
- this.fetchRecentlyModified();
+ this.fetchRecentlyEdited();
this.fetchRecentlyViewed();
});
this.on('documents.delete', () => {
- this.fetchRecentlyModified();
+ this.fetchRecentlyEdited();
this.fetchRecentlyViewed();
});
}
diff --git a/server/api/documents.js b/server/api/documents.js
index 5c3f55130..cccd902d3 100644
--- a/server/api/documents.js
+++ b/server/api/documents.js
@@ -14,14 +14,14 @@ const { authorize, cannot } = policy;
const router = new Router();
router.post('documents.list', auth(), pagination(), async ctx => {
- let { sort = 'updatedAt', direction, collection } = ctx.body;
+ let { sort = 'updatedAt', direction, collection, user } = ctx.body;
if (direction !== 'ASC') direction = 'DESC';
- const user = ctx.state.user;
- let where = { teamId: user.teamId };
+ let where = { teamId: ctx.state.user.teamId };
if (collection) where = { ...where, collectionId: collection };
+ if (user) where = { ...where, createdById: user };
- const starredScope = { method: ['withStarred', user.id] };
+ const starredScope = { method: ['withStarred', ctx.state.user.id] };
const documents = await Document.scope('defaultScope', starredScope).findAll({
where,
order: [[sort, direction]],