Merge pull request #843 from outline/collection-sort

Add new document sort options to collections
This commit is contained in:
Tom Moor
2019-01-08 09:19:51 -08:00
committed by GitHub
11 changed files with 156 additions and 31 deletions

View File

@@ -7,12 +7,14 @@ import ArrowKeyNavigation from 'boundless-arrow-key-navigation';
type Props = { type Props = {
documents: Document[], documents: Document[],
showCollection?: boolean, showCollection?: boolean,
showPublished?: boolean,
limit?: number, limit?: number,
}; };
export default function DocumentList({ export default function DocumentList({
limit, limit,
showCollection, showCollection,
showPublished,
documents, documents,
}: Props) { }: Props) {
const items = limit ? documents.splice(0, limit) : documents; const items = limit ? documents.splice(0, limit) : documents;
@@ -27,6 +29,7 @@ export default function DocumentList({
key={document.id} key={document.id}
document={document} document={document}
showCollection={showCollection} showCollection={showCollection}
showPublished={showPublished}
/> />
))} ))}
</ArrowKeyNavigation> </ArrowKeyNavigation>

View File

@@ -15,6 +15,7 @@ type Props = {
highlight?: ?string, highlight?: ?string,
context?: ?string, context?: ?string,
showCollection?: boolean, showCollection?: boolean,
showPublished?: boolean,
ref?: *, ref?: *,
}; };
@@ -133,6 +134,7 @@ class DocumentPreview extends React.Component<Props> {
const { const {
document, document,
showCollection, showCollection,
showPublished,
highlight, highlight,
context, context,
...rest ...rest
@@ -173,6 +175,7 @@ class DocumentPreview extends React.Component<Props> {
<PublishingInfo <PublishingInfo
document={document} document={document}
collection={showCollection ? document.collection : undefined} collection={showCollection ? document.collection : undefined}
showPublished={showPublished}
/> />
</DocumentLink> </DocumentLink>
); );

View File

@@ -19,11 +19,12 @@ const Modified = styled.span`
type Props = { type Props = {
collection?: Collection, collection?: Collection,
showPublished?: boolean,
document: Document, document: Document,
views?: number, views?: number,
}; };
function PublishingInfo({ collection, document }: Props) { function PublishingInfo({ collection, showPublished, document }: Props) {
const { const {
modifiedSinceViewed, modifiedSinceViewed,
updatedAt, updatedAt,
@@ -35,7 +36,7 @@ function PublishingInfo({ collection, document }: Props) {
return ( return (
<Container align="center"> <Container align="center">
{publishedAt && neverUpdated ? ( {publishedAt && (neverUpdated || showPublished) ? (
<span> <span>
{updatedBy.name} published <Time dateTime={publishedAt} /> ago {updatedBy.name} published <Time dateTime={publishedAt} /> ago
</span> </span>
@@ -48,7 +49,7 @@ function PublishingInfo({ collection, document }: Props) {
</span> </span>
) : ( ) : (
<Modified highlight={modifiedSinceViewed}> <Modified highlight={modifiedSinceViewed}>
&nbsp;modified <Time dateTime={updatedAt} /> ago &nbsp;updated <Time dateTime={updatedAt} /> ago
</Modified> </Modified>
)} )}
</React.Fragment> </React.Fragment>

View File

@@ -11,6 +11,7 @@ import { ListPlaceholder } from 'components/LoadingPlaceholder';
type Props = { type Props = {
showCollection?: boolean, showCollection?: boolean,
showPublished?: boolean,
documents: Document[], documents: Document[],
fetch: (options: ?Object) => Promise<*>, fetch: (options: ?Object) => Promise<*>,
options?: Object, options?: Object,
@@ -64,11 +65,15 @@ class PaginatedDocumentList extends React.Component<Props> {
}; };
render() { render() {
const { showCollection, documents } = this.props; const { showCollection, showPublished, documents } = this.props;
return this.isLoaded || documents.length ? ( return this.isLoaded || documents.length ? (
<React.Fragment> <React.Fragment>
<DocumentList documents={documents} showCollection={showCollection} /> <DocumentList
documents={documents}
showCollection={showCollection}
showPublished={showPublished}
/>
{this.allowLoadMore && ( {this.allowLoadMore && (
<Waypoint key={this.offset} onEnter={this.loadMoreResults} /> <Waypoint key={this.offset} onEnter={this.loadMoreResults} />
)} )}

View File

@@ -59,6 +59,7 @@ class CollectionLink extends React.Component<Props> {
hideDisclosure hideDisclosure
menuOpen={this.menuOpen} menuOpen={this.menuOpen}
label={collection.name} label={collection.name}
exact={false}
menu={ menu={
<CollectionMenu <CollectionMenu
history={history} history={history}

View File

@@ -10,14 +10,19 @@ const NavItem = styled(NavLink)`
text-transform: uppercase; text-transform: uppercase;
color: ${props => props.theme.slate}; color: ${props => props.theme.slate};
letter-spacing: 0.04em; letter-spacing: 0.04em;
margin-right: 20px; margin-right: 24px;
padding-bottom: 8px; padding-bottom: 8px;
&:hover {
color: ${props => props.theme.slateDark};
}
`; `;
function Tab(props: *) { function Tab(props: *) {
const activeStyle = { const activeStyle = {
paddingBottom: '5px', paddingBottom: '5px',
borderBottom: `3px solid ${props.theme.slateLight}`, borderBottom: `3px solid ${props.theme.slateLight}`,
color: props.theme.slate,
}; };
return <NavItem {...props} activeStyle={activeStyle} />; return <NavItem {...props} activeStyle={activeStyle} />;

View File

@@ -68,6 +68,7 @@ export default function Routes() {
component={Zapier} component={Zapier}
/> />
<Route exact path="/settings/export" component={Export} /> <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="/collections/:id" component={Collection} />
<Route exact path={`/d/${slug}`} component={RedirectDocument} /> <Route exact path={`/d/${slug}`} component={RedirectDocument} />
<Route <Route

View File

@@ -2,7 +2,8 @@
import * as React from 'react'; import * as React from 'react';
import { observable } from 'mobx'; import { observable } from 'mobx';
import { observer, inject } from 'mobx-react'; 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 styled from 'styled-components';
import { import {
CollectionIcon, CollectionIcon,
@@ -12,7 +13,7 @@ import {
} from 'outline-icons'; } from 'outline-icons';
import RichMarkdownEditor from 'rich-markdown-editor'; import RichMarkdownEditor from 'rich-markdown-editor';
import { newDocumentUrl } from 'utils/routeHelpers'; import { newDocumentUrl, collectionUrl } from 'utils/routeHelpers';
import CollectionsStore from 'stores/CollectionsStore'; import CollectionsStore from 'stores/CollectionsStore';
import DocumentsStore from 'stores/DocumentsStore'; import DocumentsStore from 'stores/DocumentsStore';
import UiStore from 'stores/UiStore'; import UiStore from 'stores/UiStore';
@@ -33,6 +34,9 @@ import PageTitle from 'components/PageTitle';
import Flex from 'shared/components/Flex'; import Flex from 'shared/components/Flex';
import Modal from 'components/Modal'; import Modal from 'components/Modal';
import CollectionPermissions from 'scenes/CollectionPermissions'; import CollectionPermissions from 'scenes/CollectionPermissions';
import Tabs from 'components/Tabs';
import Tab from 'components/Tab';
import PaginatedDocumentList from 'components/PaginatedDocumentList';
type Props = { type Props = {
ui: UiStore, ui: UiStore,
@@ -69,15 +73,9 @@ class CollectionScene extends React.Component<Props> {
this.props.ui.setActiveCollection(collection); this.props.ui.setActiveCollection(collection);
this.collection = collection; this.collection = collection;
await Promise.all([ await this.props.documents.fetchPinned({
this.props.documents.fetchRecentlyUpdated({ collection: id,
limit: 10, });
collection: id,
}),
this.props.documents.fetchPinned({
collection: id,
}),
]);
} }
this.isFetching = false; this.isFetching = false;
@@ -124,15 +122,14 @@ class CollectionScene extends React.Component<Props> {
} }
render() { render() {
const { documents } = this.props;
if (!this.isFetching && !this.collection) { if (!this.isFetching && !this.collection) {
return this.renderNotFound(); return this.renderNotFound();
} }
const pinnedDocuments = this.collection const pinnedDocuments = this.collection
? this.props.documents.pinnedInCollection(this.collection.id) ? documents.pinnedInCollection(this.collection.id)
: [];
const recentDocuments = this.collection
? this.props.documents.recentlyUpdatedInCollection(this.collection.id)
: []; : [];
const hasPinnedDocuments = !!pinnedDocuments.length; const hasPinnedDocuments = !!pinnedDocuments.length;
const collection = this.collection; const collection = this.collection;
@@ -207,8 +204,62 @@ class CollectionScene extends React.Component<Props> {
</React.Fragment> </React.Fragment>
)} )}
<Subheading>Recently edited</Subheading> <Tabs>
<DocumentList documents={recentDocuments} limit={10} /> <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>
AZ
</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> </React.Fragment>
)} )}

View File

@@ -12,8 +12,8 @@ import CenteredContent from 'components/CenteredContent';
import PageTitle from 'components/PageTitle'; import PageTitle from 'components/PageTitle';
import Tabs from 'components/Tabs'; import Tabs from 'components/Tabs';
import Tab from 'components/Tab'; import Tab from 'components/Tab';
import TipInvite from 'components/TipInvite';
import PaginatedDocumentList from '../components/PaginatedDocumentList'; import PaginatedDocumentList from '../components/PaginatedDocumentList';
import TipInvite from 'components/TipInvite';
type Props = { type Props = {
documents: DocumentsStore, documents: DocumentsStore,

View File

@@ -55,18 +55,42 @@ export default class DocumentsStore extends BaseStore<Document> {
); );
} }
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[] { recentlyUpdatedInCollection(collectionId: string): Document[] {
return orderBy( return orderBy(
filter( this.publishedInCollection(collectionId),
Array.from(this.data.values()),
document =>
document.collectionId === collectionId && !!document.publishedAt
),
'updatedAt', 'updatedAt',
'desc' 'desc'
); );
} }
recentlyPublishedInCollection(collectionId: string): Document[] {
return orderBy(
this.publishedInCollection(collectionId),
'publishedAt',
'desc'
);
}
alphabeticalInCollection(collectionId: string): Document[] {
return naturalSort(this.publishedInCollection(collectionId), 'title');
}
@computed @computed
get starred(): Document[] { get starred(): Document[] {
return filter(this.orderedData, d => d.starred); return filter(this.orderedData, d => d.starred);
@@ -126,6 +150,35 @@ export default class DocumentsStore extends BaseStore<Document> {
return data; 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 @action
fetchRecentlyViewed = async (options: ?PaginationParams): Promise<*> => { fetchRecentlyViewed = async (options: ?PaginationParams): Promise<*> => {
const data = await this.fetchNamedPage('viewed', options); const data = await this.fetchNamedPage('viewed', options);

View File

@@ -14,8 +14,10 @@ export function newCollectionUrl(): string {
return '/collections/new'; return '/collections/new';
} }
export function collectionUrl(collectionId: string): string { export function collectionUrl(collectionId: string, section: ?string): string {
return `/collections/${collectionId}`; const path = `/collections/${collectionId}`;
if (section) return `${path}/${section}`;
return path;
} }
export function documentUrl(doc: Document): string { export function documentUrl(doc: Document): string {