Refactor, paginate on scroll
New PaginatedDocumentList component
This commit is contained in:
parent
266b4d735c
commit
d308442fef
34
app/components/DocumentList.js
Normal file
34
app/components/DocumentList.js
Normal file
@ -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 (
|
||||
<ArrowKeyNavigation
|
||||
mode={ArrowKeyNavigation.mode.VERTICAL}
|
||||
defaultActiveChildIndex={0}
|
||||
>
|
||||
{items.map(document => (
|
||||
<DocumentPreview
|
||||
key={document.id}
|
||||
document={document}
|
||||
showCollection={showCollection}
|
||||
/>
|
||||
))}
|
||||
</ArrowKeyNavigation>
|
||||
);
|
||||
}
|
@ -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<Props> {
|
||||
render() {
|
||||
const { limit, showCollection } = this.props;
|
||||
const documents = limit
|
||||
? this.props.documents.splice(0, limit)
|
||||
: this.props.documents;
|
||||
|
||||
return (
|
||||
<ArrowKeyNavigation
|
||||
mode={ArrowKeyNavigation.mode.VERTICAL}
|
||||
defaultActiveChildIndex={0}
|
||||
>
|
||||
{documents.map(document => (
|
||||
<DocumentPreview
|
||||
key={document.id}
|
||||
document={document}
|
||||
showCollection={showCollection}
|
||||
/>
|
||||
))}
|
||||
</ArrowKeyNavigation>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default DocumentList;
|
@ -1,3 +0,0 @@
|
||||
// @flow
|
||||
import DocumentList from './DocumentList';
|
||||
export default DocumentList;
|
78
app/components/PaginatedDocumentList.js
Normal file
78
app/components/PaginatedDocumentList.js
Normal file
@ -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<Props> {
|
||||
@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 ? (
|
||||
<React.Fragment>
|
||||
<DocumentList documents={documents} showCollection={showCollection} />
|
||||
{this.allowLoadMore && (
|
||||
<Waypoint key={this.offset} onEnter={this.loadMoreResults} />
|
||||
)}
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<ListPlaceholder count={5} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PaginatedDocumentList;
|
@ -61,7 +61,7 @@ class CollectionScene extends React.Component<Props> {
|
||||
this.collection = collection;
|
||||
|
||||
await Promise.all([
|
||||
this.props.documents.fetchRecentlyModified({
|
||||
this.props.documents.fetchRecentlyEdited({
|
||||
limit: 10,
|
||||
collection: id,
|
||||
}),
|
||||
|
@ -1,7 +1,6 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { Switch, Route } from 'react-router-dom';
|
||||
import { observable } from 'mobx';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import { NewDocumentIcon } from 'outline-icons';
|
||||
|
||||
@ -10,11 +9,10 @@ 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 Tabs from 'components/Tabs';
|
||||
import Tab from 'components/Tab';
|
||||
import { ListPlaceholder } from 'components/LoadingPlaceholder';
|
||||
import PaginatedDocumentList from '../components/PaginatedDocumentList';
|
||||
|
||||
type Props = {
|
||||
documents: DocumentsStore,
|
||||
@ -23,41 +21,10 @@ type Props = {
|
||||
|
||||
@observer
|
||||
class Dashboard extends React.Component<Props> {
|
||||
@observable isLoaded: boolean = false;
|
||||
|
||||
componentDidMount() {
|
||||
this.loadContent();
|
||||
}
|
||||
|
||||
loadContent = async () => {
|
||||
const { auth } = this.props;
|
||||
const user = auth.user ? auth.user.id : undefined;
|
||||
|
||||
await Promise.all([
|
||||
this.props.documents.fetchRecentlyModified({ limit: 15 }),
|
||||
this.props.documents.fetchRecentlyViewed({ limit: 15 }),
|
||||
this.props.documents.fetchOwned({ limit: 15, user }),
|
||||
]);
|
||||
this.isLoaded = true;
|
||||
};
|
||||
|
||||
renderTab = (path, documents) => {
|
||||
return (
|
||||
<Route path={path}>
|
||||
{this.isLoaded || documents.length ? (
|
||||
<DocumentList documents={documents} showCollection />
|
||||
) : (
|
||||
<ListPlaceholder count={5} />
|
||||
)}
|
||||
</Route>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { documents, auth } = this.props;
|
||||
if (!auth.user) return;
|
||||
|
||||
const createdDocuments = documents.owned(auth.user.id);
|
||||
const user = auth.user.id;
|
||||
|
||||
return (
|
||||
<CenteredContent>
|
||||
@ -73,9 +40,27 @@ class Dashboard extends React.Component<Props> {
|
||||
<Tab to="/dashboard/created">Created by me</Tab>
|
||||
</Tabs>
|
||||
<Switch>
|
||||
{this.renderTab('/dashboard/recent', documents.recentlyViewed)}
|
||||
{this.renderTab('/dashboard/created', createdDocuments)}
|
||||
{this.renderTab('/dashboard', documents.recentlyEdited)}
|
||||
<Route path="/dashboard/recent">
|
||||
<PaginatedDocumentList
|
||||
key="recent"
|
||||
documents={documents.recentlyViewed}
|
||||
fetch={documents.fetchRecentlyViewed}
|
||||
/>
|
||||
</Route>
|
||||
<Route path="/dashboard/created">
|
||||
<PaginatedDocumentList
|
||||
key="created"
|
||||
documents={documents.createdByUser(user)}
|
||||
fetch={documents.fetchOwned}
|
||||
options={{ user }}
|
||||
/>
|
||||
</Route>
|
||||
<Route path="/dashboard">
|
||||
<PaginatedDocumentList
|
||||
documents={documents.recentlyEdited}
|
||||
fetch={documents.fetchRecentlyEdited}
|
||||
/>
|
||||
</Route>
|
||||
</Switch>
|
||||
<Actions align="center" justify="flex-end">
|
||||
<Action>
|
||||
|
@ -21,8 +21,8 @@ type FetchOptions = {
|
||||
};
|
||||
|
||||
class DocumentsStore extends BaseStore {
|
||||
@observable recentlyViewedIds: Array<string> = [];
|
||||
@observable recentlyEditedIds: Array<string> = [];
|
||||
@observable recentlyViewedIds: string[] = [];
|
||||
@observable recentlyEditedIds: string[] = [];
|
||||
@observable data: Map<string, Document> = new ObservableMap([]);
|
||||
@observable isLoaded: boolean = false;
|
||||
@observable isFetching: boolean = false;
|
||||
@ -49,7 +49,7 @@ class DocumentsStore extends BaseStore {
|
||||
return docs;
|
||||
}
|
||||
|
||||
owned(userId: string): Document[] {
|
||||
createdByUser(userId: string): Document[] {
|
||||
return _.orderBy(
|
||||
_.filter(
|
||||
this.data.values(),
|
||||
@ -127,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;
|
||||
@ -147,23 +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 = async (options: ?PaginationParams): Promise<*> => {
|
||||
await this.fetchPage('list', options);
|
||||
fetchOwned = (options: ?PaginationParams): Promise<*> => {
|
||||
return this.fetchPage('list', options);
|
||||
};
|
||||
|
||||
@action
|
||||
@ -275,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();
|
||||
});
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ router.post('documents.list', auth(), pagination(), async ctx => {
|
||||
|
||||
let where = { teamId: ctx.state.user.teamId };
|
||||
if (collection) where = { ...where, collectionId: collection };
|
||||
if (user) where = { ...where, userId: collection };
|
||||
if (user) where = { ...where, createdById: user };
|
||||
|
||||
const starredScope = { method: ['withStarred', ctx.state.user.id] };
|
||||
const documents = await Document.scope('defaultScope', starredScope).findAll({
|
||||
|
Reference in New Issue
Block a user