DocumentPreview Improves (#141)
* Show collection name in search results Highlight documents modified since last edited Move views to scope * Allow ESC key to work on Search page when input not focused * Update document title with search query Show loading indicator for search results * WIP * 💚 ? * 💚 * Address PR feedback
This commit is contained in:
@@ -50,10 +50,7 @@ class DocumentPreview extends Component {
|
||||
<DocumentLink to={document.url} innerRef={innerRef} {...rest}>
|
||||
<h3>{document.title}</h3>
|
||||
<PublishingInfo
|
||||
createdAt={document.createdAt}
|
||||
createdBy={document.createdBy}
|
||||
updatedAt={document.updatedAt}
|
||||
updatedBy={document.updatedBy}
|
||||
document={document}
|
||||
collection={showCollection ? document.collection : undefined}
|
||||
/>
|
||||
</DocumentLink>
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
import React, { Component } from 'react';
|
||||
import moment from 'moment';
|
||||
import styled from 'styled-components';
|
||||
import { color } from 'styles/constants';
|
||||
import Collection from 'models/Collection';
|
||||
import Document from 'models/Document';
|
||||
import type { User } from 'types';
|
||||
import Flex from 'components/Flex';
|
||||
|
||||
const Container = styled(Flex)`
|
||||
color: #bbb;
|
||||
color: ${color.slate};
|
||||
font-size: 13px;
|
||||
`;
|
||||
|
||||
@@ -21,7 +23,7 @@ const Avatar = styled.img`
|
||||
height: 26px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #FFFFFF;
|
||||
border: 2px solid ${color.white};
|
||||
margin-right: -13px;
|
||||
|
||||
&:first-child {
|
||||
@@ -29,26 +31,28 @@ const Avatar = styled.img`
|
||||
}
|
||||
`;
|
||||
|
||||
const Modified = styled.span`
|
||||
color: ${props => (props.highlight ? color.slateDark : color.slate)};
|
||||
font-weight: ${props => (props.highlight ? '600' : '400')};
|
||||
`;
|
||||
|
||||
class PublishingInfo extends Component {
|
||||
props: {
|
||||
collaborators?: Array<User>,
|
||||
collection?: Collection,
|
||||
createdAt: string,
|
||||
createdBy: User,
|
||||
updatedAt: string,
|
||||
updatedBy: User,
|
||||
document: Document,
|
||||
views?: number,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { collaborators, collection, document } = this.props;
|
||||
const {
|
||||
collaborators,
|
||||
collection,
|
||||
modifiedSinceViewed,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
createdBy,
|
||||
updatedBy,
|
||||
} = this.props;
|
||||
} = document;
|
||||
|
||||
return (
|
||||
<Container align="center">
|
||||
@@ -68,10 +72,12 @@ class PublishingInfo extends Component {
|
||||
</span>
|
||||
: <span>
|
||||
{updatedBy.name}
|
||||
{' '}
|
||||
modified
|
||||
{' '}
|
||||
{moment(updatedAt).fromNow()}
|
||||
<Modified highlight={modifiedSinceViewed}>
|
||||
{' '}
|
||||
modified
|
||||
{' '}
|
||||
{moment(updatedAt).fromNow()}
|
||||
</Modified>
|
||||
</span>}
|
||||
{collection && <span> in <strong>{collection.name}</strong></span>}
|
||||
</Container>
|
||||
|
||||
@@ -21,6 +21,9 @@ class Document {
|
||||
|
||||
collaborators: Array<User>;
|
||||
collection: $Shape<Collection>;
|
||||
firstViewedAt: ?string;
|
||||
lastViewedAt: ?string;
|
||||
modifiedSinceViewed: ?boolean;
|
||||
createdAt: string;
|
||||
createdBy: User;
|
||||
html: string;
|
||||
@@ -38,6 +41,10 @@ class Document {
|
||||
|
||||
/* Computed */
|
||||
|
||||
@computed get modifiedSinceViewed(): boolean {
|
||||
return !!this.lastViewedAt && this.lastViewedAt < this.updatedAt;
|
||||
}
|
||||
|
||||
@computed get pathToDocument(): Array<string> {
|
||||
let path;
|
||||
const traveler = (nodes, previousPath) => {
|
||||
|
||||
@@ -157,10 +157,7 @@ type Props = {
|
||||
<InfoWrapper visible={!isEditing}>
|
||||
<PublishingInfo
|
||||
collaborators={this.document.collaborators}
|
||||
createdAt={this.document.createdAt}
|
||||
createdBy={this.document.createdBy}
|
||||
updatedAt={this.document.updatedAt}
|
||||
updatedBy={this.document.updatedBy}
|
||||
document={this.document}
|
||||
/>
|
||||
</InfoWrapper>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import keydown from 'react-keydown';
|
||||
import { observer } from 'mobx-react';
|
||||
import _ from 'lodash';
|
||||
import Flex from 'components/Flex';
|
||||
@@ -10,6 +11,7 @@ import styled from 'styled-components';
|
||||
import ArrowKeyNavigation from 'boundless-arrow-key-navigation';
|
||||
|
||||
import CenteredContent from 'components/CenteredContent';
|
||||
import LoadingIndicator from 'components/LoadingIndicator';
|
||||
import SearchField from './components/SearchField';
|
||||
import SearchStore from './SearchStore';
|
||||
|
||||
@@ -65,12 +67,18 @@ const StyledArrowKeyNavigation = styled(ArrowKeyNavigation)`
|
||||
}
|
||||
}
|
||||
|
||||
@keydown('esc')
|
||||
goBack() {
|
||||
this.props.history.goBack();
|
||||
}
|
||||
|
||||
handleKeyDown = ev => {
|
||||
// ESC
|
||||
// Escape
|
||||
if (ev.which === 27) {
|
||||
ev.preventDefault();
|
||||
this.props.history.goBack();
|
||||
this.goBack();
|
||||
}
|
||||
|
||||
// Down
|
||||
if (ev.which === 40) {
|
||||
ev.preventDefault();
|
||||
@@ -94,13 +102,21 @@ const StyledArrowKeyNavigation = styled(ArrowKeyNavigation)`
|
||||
this.firstDocument = ref;
|
||||
};
|
||||
|
||||
get title() {
|
||||
const query = this.store.searchTerm;
|
||||
const title = 'Search';
|
||||
if (query) return `${query} - ${title}`;
|
||||
return title;
|
||||
}
|
||||
|
||||
render() {
|
||||
const query = this.props.match.params.query;
|
||||
const hasResults = this.store.documents.length > 0;
|
||||
|
||||
return (
|
||||
<Container auto>
|
||||
<PageTitle title="Search" />
|
||||
<PageTitle title={this.title} />
|
||||
{this.store.isFetching && <LoadingIndicator />}
|
||||
{this.props.notFound &&
|
||||
<div>
|
||||
<h1>Not Found</h1>
|
||||
|
||||
@@ -7,7 +7,6 @@ import Document from 'models/Document';
|
||||
class SearchStore {
|
||||
@observable documents: Array<Document> = [];
|
||||
@observable searchTerm: ?string = null;
|
||||
|
||||
@observable isFetching = false;
|
||||
|
||||
/* Actions */
|
||||
|
||||
@@ -101,9 +101,7 @@ router.post('documents.info', auth(), async ctx => {
|
||||
}
|
||||
|
||||
ctx.body = {
|
||||
data: await presentDocument(ctx, document, {
|
||||
includeViews: true,
|
||||
}),
|
||||
data: await presentDocument(ctx, document),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -119,6 +119,9 @@ Document.associate = models => {
|
||||
Document.hasMany(models.Star, {
|
||||
as: 'starred',
|
||||
});
|
||||
Document.hasMany(models.View, {
|
||||
as: 'views',
|
||||
});
|
||||
Document.addScope(
|
||||
'defaultScope',
|
||||
{
|
||||
@@ -130,6 +133,11 @@ Document.associate = models => {
|
||||
},
|
||||
{ override: true }
|
||||
);
|
||||
Document.addScope('withViews', userId => ({
|
||||
include: [
|
||||
{ model: models.View, as: 'views', where: { userId }, required: false },
|
||||
],
|
||||
}));
|
||||
Document.addScope('withStarred', userId => ({
|
||||
include: [
|
||||
{ model: models.Star, as: 'starred', where: { userId }, required: false },
|
||||
@@ -174,7 +182,9 @@ Document.searchForUser = async (user, query, options = {}) => {
|
||||
model: Document,
|
||||
})
|
||||
.map(document => document.id);
|
||||
return Document.findAll({
|
||||
|
||||
const withViewsScope = { method: ['withViews', user.id] };
|
||||
return Document.scope('defaultScope', withViewsScope).findAll({
|
||||
where: { id: ids },
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
// @flow
|
||||
import _ from 'lodash';
|
||||
import { User, Document, View } from '../models';
|
||||
import { User, Document } from '../models';
|
||||
import presentUser from './user';
|
||||
import presentCollection from './collection';
|
||||
|
||||
type Options = {
|
||||
includeCollaborators?: boolean,
|
||||
includeViews?: boolean,
|
||||
};
|
||||
|
||||
async function present(ctx: Object, document: Document, options: ?Options) {
|
||||
options = {
|
||||
includeCollaborators: true,
|
||||
includeViews: false,
|
||||
...options,
|
||||
};
|
||||
ctx.cache.set(document.id, document);
|
||||
@@ -28,6 +26,8 @@ async function present(ctx: Object, document: Document, options: ?Options) {
|
||||
createdBy: presentUser(ctx, document.createdBy),
|
||||
updatedAt: document.updatedAt,
|
||||
updatedBy: presentUser(ctx, document.updatedBy),
|
||||
firstViewedAt: undefined,
|
||||
lastViewedAt: undefined,
|
||||
team: document.teamId,
|
||||
collaborators: [],
|
||||
starred: !!document.starred,
|
||||
@@ -40,10 +40,10 @@ async function present(ctx: Object, document: Document, options: ?Options) {
|
||||
data.collection = await presentCollection(ctx, document.collection);
|
||||
}
|
||||
|
||||
if (options.includeViews) {
|
||||
data.views = await View.sum('count', {
|
||||
where: { documentId: document.id },
|
||||
});
|
||||
if (document.views && document.views.length === 1) {
|
||||
data.views = document.views[0].count;
|
||||
data.firstViewedAt = document.views[0].createdAt;
|
||||
data.lastViewedAt = document.views[0].updatedAt;
|
||||
}
|
||||
|
||||
if (options.includeCollaborators) {
|
||||
|
||||
Reference in New Issue
Block a user