Merge pull request #843 from outline/collection-sort
Add new document sort options to collections
This commit is contained in:
commit
4ddd7d4dfe
|
@ -7,12 +7,14 @@ import ArrowKeyNavigation from 'boundless-arrow-key-navigation';
|
|||
type Props = {
|
||||
documents: Document[],
|
||||
showCollection?: boolean,
|
||||
showPublished?: boolean,
|
||||
limit?: number,
|
||||
};
|
||||
|
||||
export default function DocumentList({
|
||||
limit,
|
||||
showCollection,
|
||||
showPublished,
|
||||
documents,
|
||||
}: Props) {
|
||||
const items = limit ? documents.splice(0, limit) : documents;
|
||||
|
@ -27,6 +29,7 @@ export default function DocumentList({
|
|||
key={document.id}
|
||||
document={document}
|
||||
showCollection={showCollection}
|
||||
showPublished={showPublished}
|
||||
/>
|
||||
))}
|
||||
</ArrowKeyNavigation>
|
||||
|
|
|
@ -15,6 +15,7 @@ type Props = {
|
|||
highlight?: ?string,
|
||||
context?: ?string,
|
||||
showCollection?: boolean,
|
||||
showPublished?: boolean,
|
||||
ref?: *,
|
||||
};
|
||||
|
||||
|
@ -133,6 +134,7 @@ class DocumentPreview extends React.Component<Props> {
|
|||
const {
|
||||
document,
|
||||
showCollection,
|
||||
showPublished,
|
||||
highlight,
|
||||
context,
|
||||
...rest
|
||||
|
@ -173,6 +175,7 @@ class DocumentPreview extends React.Component<Props> {
|
|||
<PublishingInfo
|
||||
document={document}
|
||||
collection={showCollection ? document.collection : undefined}
|
||||
showPublished={showPublished}
|
||||
/>
|
||||
</DocumentLink>
|
||||
);
|
||||
|
|
|
@ -19,11 +19,12 @@ const Modified = styled.span`
|
|||
|
||||
type Props = {
|
||||
collection?: Collection,
|
||||
showPublished?: boolean,
|
||||
document: Document,
|
||||
views?: number,
|
||||
};
|
||||
|
||||
function PublishingInfo({ collection, document }: Props) {
|
||||
function PublishingInfo({ collection, showPublished, document }: Props) {
|
||||
const {
|
||||
modifiedSinceViewed,
|
||||
updatedAt,
|
||||
|
@ -35,7 +36,7 @@ function PublishingInfo({ collection, document }: Props) {
|
|||
|
||||
return (
|
||||
<Container align="center">
|
||||
{publishedAt && neverUpdated ? (
|
||||
{publishedAt && (neverUpdated || showPublished) ? (
|
||||
<span>
|
||||
{updatedBy.name} published <Time dateTime={publishedAt} /> ago
|
||||
</span>
|
||||
|
@ -48,7 +49,7 @@ function PublishingInfo({ collection, document }: Props) {
|
|||
</span>
|
||||
) : (
|
||||
<Modified highlight={modifiedSinceViewed}>
|
||||
modified <Time dateTime={updatedAt} /> ago
|
||||
updated <Time dateTime={updatedAt} /> ago
|
||||
</Modified>
|
||||
)}
|
||||
</React.Fragment>
|
||||
|
|
|
@ -11,6 +11,7 @@ import { ListPlaceholder } from 'components/LoadingPlaceholder';
|
|||
|
||||
type Props = {
|
||||
showCollection?: boolean,
|
||||
showPublished?: boolean,
|
||||
documents: Document[],
|
||||
fetch: (options: ?Object) => Promise<*>,
|
||||
options?: Object,
|
||||
|
@ -64,11 +65,15 @@ class PaginatedDocumentList extends React.Component<Props> {
|
|||
};
|
||||
|
||||
render() {
|
||||
const { showCollection, documents } = this.props;
|
||||
const { showCollection, showPublished, documents } = this.props;
|
||||
|
||||
return this.isLoaded || documents.length ? (
|
||||
<React.Fragment>
|
||||
<DocumentList documents={documents} showCollection={showCollection} />
|
||||
<DocumentList
|
||||
documents={documents}
|
||||
showCollection={showCollection}
|
||||
showPublished={showPublished}
|
||||
/>
|
||||
{this.allowLoadMore && (
|
||||
<Waypoint key={this.offset} onEnter={this.loadMoreResults} />
|
||||
)}
|
||||
|
|
|
@ -59,6 +59,7 @@ class CollectionLink extends React.Component<Props> {
|
|||
hideDisclosure
|
||||
menuOpen={this.menuOpen}
|
||||
label={collection.name}
|
||||
exact={false}
|
||||
menu={
|
||||
<CollectionMenu
|
||||
history={history}
|
||||
|
|
|
@ -10,14 +10,19 @@ const NavItem = styled(NavLink)`
|
|||
text-transform: uppercase;
|
||||
color: ${props => props.theme.slate};
|
||||
letter-spacing: 0.04em;
|
||||
margin-right: 20px;
|
||||
margin-right: 24px;
|
||||
padding-bottom: 8px;
|
||||
|
||||
&:hover {
|
||||
color: ${props => props.theme.slateDark};
|
||||
}
|
||||
`;
|
||||
|
||||
function Tab(props: *) {
|
||||
const activeStyle = {
|
||||
paddingBottom: '5px',
|
||||
borderBottom: `3px solid ${props.theme.slateLight}`,
|
||||
color: props.theme.slate,
|
||||
};
|
||||
|
||||
return <NavItem {...props} activeStyle={activeStyle} />;
|
||||
|
|
|
@ -68,6 +68,7 @@ export default function Routes() {
|
|||
component={Zapier}
|
||||
/>
|
||||
<Route exact path="/settings/export" component={Export} />
|
||||
<Route exact path="/collections/:id/:tab" component={Collection} />
|
||||
<Route exact path="/collections/:id" component={Collection} />
|
||||
<Route exact path={`/d/${slug}`} component={RedirectDocument} />
|
||||
<Route
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
import * as React from 'react';
|
||||
import { observable } from 'mobx';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import { withRouter, Link } from 'react-router-dom';
|
||||
import { withRouter, Link, Switch, Route } from 'react-router-dom';
|
||||
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
CollectionIcon,
|
||||
|
@ -12,7 +13,7 @@ import {
|
|||
} from 'outline-icons';
|
||||
import RichMarkdownEditor from 'rich-markdown-editor';
|
||||
|
||||
import { newDocumentUrl } from 'utils/routeHelpers';
|
||||
import { newDocumentUrl, collectionUrl } from 'utils/routeHelpers';
|
||||
import CollectionsStore from 'stores/CollectionsStore';
|
||||
import DocumentsStore from 'stores/DocumentsStore';
|
||||
import UiStore from 'stores/UiStore';
|
||||
|
@ -33,6 +34,9 @@ import PageTitle from 'components/PageTitle';
|
|||
import Flex from 'shared/components/Flex';
|
||||
import Modal from 'components/Modal';
|
||||
import CollectionPermissions from 'scenes/CollectionPermissions';
|
||||
import Tabs from 'components/Tabs';
|
||||
import Tab from 'components/Tab';
|
||||
import PaginatedDocumentList from 'components/PaginatedDocumentList';
|
||||
|
||||
type Props = {
|
||||
ui: UiStore,
|
||||
|
@ -69,15 +73,9 @@ class CollectionScene extends React.Component<Props> {
|
|||
this.props.ui.setActiveCollection(collection);
|
||||
this.collection = collection;
|
||||
|
||||
await Promise.all([
|
||||
this.props.documents.fetchRecentlyUpdated({
|
||||
limit: 10,
|
||||
await this.props.documents.fetchPinned({
|
||||
collection: id,
|
||||
}),
|
||||
this.props.documents.fetchPinned({
|
||||
collection: id,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
this.isFetching = false;
|
||||
|
@ -124,15 +122,14 @@ class CollectionScene extends React.Component<Props> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { documents } = this.props;
|
||||
|
||||
if (!this.isFetching && !this.collection) {
|
||||
return this.renderNotFound();
|
||||
}
|
||||
|
||||
const pinnedDocuments = this.collection
|
||||
? this.props.documents.pinnedInCollection(this.collection.id)
|
||||
: [];
|
||||
const recentDocuments = this.collection
|
||||
? this.props.documents.recentlyUpdatedInCollection(this.collection.id)
|
||||
? documents.pinnedInCollection(this.collection.id)
|
||||
: [];
|
||||
const hasPinnedDocuments = !!pinnedDocuments.length;
|
||||
const collection = this.collection;
|
||||
|
@ -207,8 +204,62 @@ class CollectionScene extends React.Component<Props> {
|
|||
</React.Fragment>
|
||||
)}
|
||||
|
||||
<Subheading>Recently edited</Subheading>
|
||||
<DocumentList documents={recentDocuments} limit={10} />
|
||||
<Tabs>
|
||||
<Tab to={collectionUrl(collection.id)} exact>
|
||||
Recently updated
|
||||
</Tab>
|
||||
<Tab to={collectionUrl(collection.id, 'recent')} exact>
|
||||
Recently published
|
||||
</Tab>
|
||||
<Tab to={collectionUrl(collection.id, 'old')} exact>
|
||||
Least recently updated
|
||||
</Tab>
|
||||
<Tab to={collectionUrl(collection.id, 'alphabetical')} exact>
|
||||
A–Z
|
||||
</Tab>
|
||||
</Tabs>
|
||||
<Switch>
|
||||
<Route path={collectionUrl(collection.id, 'alphabetical')}>
|
||||
<PaginatedDocumentList
|
||||
key="alphabetical"
|
||||
documents={documents.alphabeticalInCollection(
|
||||
collection.id
|
||||
)}
|
||||
fetch={documents.fetchAlphabetical}
|
||||
options={{ collection: collection.id }}
|
||||
/>
|
||||
</Route>
|
||||
<Route path={collectionUrl(collection.id, 'old')}>
|
||||
<PaginatedDocumentList
|
||||
key="old"
|
||||
documents={documents.leastRecentlyUpdatedInCollection(
|
||||
collection.id
|
||||
)}
|
||||
fetch={documents.fetchLeastRecentlyUpdated}
|
||||
options={{ collection: collection.id }}
|
||||
/>
|
||||
</Route>
|
||||
<Route path={collectionUrl(collection.id, 'recent')}>
|
||||
<PaginatedDocumentList
|
||||
key="recent"
|
||||
documents={documents.recentlyPublishedInCollection(
|
||||
collection.id
|
||||
)}
|
||||
fetch={documents.fetchRecentlyPublished}
|
||||
options={{ collection: collection.id }}
|
||||
showPublished
|
||||
/>
|
||||
</Route>
|
||||
<Route path={collectionUrl(collection.id)}>
|
||||
<PaginatedDocumentList
|
||||
documents={documents.recentlyUpdatedInCollection(
|
||||
collection.id
|
||||
)}
|
||||
fetch={documents.fetchRecentlyUpdated}
|
||||
options={{ collection: collection.id }}
|
||||
/>
|
||||
</Route>
|
||||
</Switch>
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
|
|
|
@ -12,8 +12,8 @@ import CenteredContent from 'components/CenteredContent';
|
|||
import PageTitle from 'components/PageTitle';
|
||||
import Tabs from 'components/Tabs';
|
||||
import Tab from 'components/Tab';
|
||||
import TipInvite from 'components/TipInvite';
|
||||
import PaginatedDocumentList from '../components/PaginatedDocumentList';
|
||||
import TipInvite from 'components/TipInvite';
|
||||
|
||||
type Props = {
|
||||
documents: DocumentsStore,
|
||||
|
|
|
@ -55,18 +55,42 @@ export default class DocumentsStore extends BaseStore<Document> {
|
|||
);
|
||||
}
|
||||
|
||||
recentlyUpdatedInCollection(collectionId: string): Document[] {
|
||||
return orderBy(
|
||||
filter(
|
||||
publishedInCollection(collectionId: string): Document[] {
|
||||
return filter(
|
||||
Array.from(this.data.values()),
|
||||
document =>
|
||||
document.collectionId === collectionId && !!document.publishedAt
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
leastRecentlyUpdatedInCollection(collectionId: string): Document[] {
|
||||
return orderBy(
|
||||
this.publishedInCollection(collectionId),
|
||||
'updatedAt',
|
||||
'asc'
|
||||
);
|
||||
}
|
||||
|
||||
recentlyUpdatedInCollection(collectionId: string): Document[] {
|
||||
return orderBy(
|
||||
this.publishedInCollection(collectionId),
|
||||
'updatedAt',
|
||||
'desc'
|
||||
);
|
||||
}
|
||||
|
||||
recentlyPublishedInCollection(collectionId: string): Document[] {
|
||||
return orderBy(
|
||||
this.publishedInCollection(collectionId),
|
||||
'publishedAt',
|
||||
'desc'
|
||||
);
|
||||
}
|
||||
|
||||
alphabeticalInCollection(collectionId: string): Document[] {
|
||||
return naturalSort(this.publishedInCollection(collectionId), 'title');
|
||||
}
|
||||
|
||||
@computed
|
||||
get starred(): Document[] {
|
||||
return filter(this.orderedData, d => d.starred);
|
||||
|
@ -126,6 +150,35 @@ export default class DocumentsStore extends BaseStore<Document> {
|
|||
return data;
|
||||
};
|
||||
|
||||
@action
|
||||
fetchAlphabetical = async (options: ?PaginationParams): Promise<*> => {
|
||||
return this.fetchNamedPage('list', {
|
||||
sort: 'title',
|
||||
direction: 'ASC',
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
@action
|
||||
fetchLeastRecentlyUpdated = async (
|
||||
options: ?PaginationParams
|
||||
): Promise<*> => {
|
||||
return this.fetchNamedPage('list', {
|
||||
sort: 'updatedAt',
|
||||
direction: 'ASC',
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
@action
|
||||
fetchRecentlyPublished = async (options: ?PaginationParams): Promise<*> => {
|
||||
return this.fetchNamedPage('list', {
|
||||
sort: 'publishedAt',
|
||||
direction: 'DESC',
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
@action
|
||||
fetchRecentlyViewed = async (options: ?PaginationParams): Promise<*> => {
|
||||
const data = await this.fetchNamedPage('viewed', options);
|
||||
|
|
|
@ -14,8 +14,10 @@ export function newCollectionUrl(): string {
|
|||
return '/collections/new';
|
||||
}
|
||||
|
||||
export function collectionUrl(collectionId: string): string {
|
||||
return `/collections/${collectionId}`;
|
||||
export function collectionUrl(collectionId: string, section: ?string): string {
|
||||
const path = `/collections/${collectionId}`;
|
||||
if (section) return `${path}/${section}`;
|
||||
return path;
|
||||
}
|
||||
|
||||
export function documentUrl(doc: Document): string {
|
||||
|
|
Reference in New Issue