diff --git a/frontend/components/DocumentPreview/DocumentPreview.js b/frontend/components/DocumentPreview/DocumentPreview.js index 364f10fbb..409c96d43 100644 --- a/frontend/components/DocumentPreview/DocumentPreview.js +++ b/frontend/components/DocumentPreview/DocumentPreview.js @@ -50,10 +50,7 @@ class DocumentPreview extends Component {

{document.title}

diff --git a/frontend/components/PublishingInfo/PublishingInfo.js b/frontend/components/PublishingInfo/PublishingInfo.js index 66465b0ef..90b00c6d8 100644 --- a/frontend/components/PublishingInfo/PublishingInfo.js +++ b/frontend/components/PublishingInfo/PublishingInfo.js @@ -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, 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 ( @@ -68,10 +72,12 @@ class PublishingInfo extends Component { : {updatedBy.name} - {' '} - modified - {' '} - {moment(updatedAt).fromNow()} + + {' '} + modified + {' '} + {moment(updatedAt).fromNow()} + } {collection &&  in {collection.name}} diff --git a/frontend/models/Document.js b/frontend/models/Document.js index 5a9b2d31c..e244a9764 100644 --- a/frontend/models/Document.js +++ b/frontend/models/Document.js @@ -21,6 +21,9 @@ class Document { collaborators: Array; collection: $Shape; + 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 { let path; const traveler = (nodes, previousPath) => { diff --git a/frontend/scenes/Document/Document.js b/frontend/scenes/Document/Document.js index e26d3f9f1..46f353328 100644 --- a/frontend/scenes/Document/Document.js +++ b/frontend/scenes/Document/Document.js @@ -157,10 +157,7 @@ type Props = { ); diff --git a/frontend/scenes/Search/Search.js b/frontend/scenes/Search/Search.js index 0b552d8bb..2b6e0accc 100644 --- a/frontend/scenes/Search/Search.js +++ b/frontend/scenes/Search/Search.js @@ -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 ( - + + {this.store.isFetching && } {this.props.notFound &&

Not Found

diff --git a/frontend/scenes/Search/SearchStore.js b/frontend/scenes/Search/SearchStore.js index 058ca7744..68088ce80 100644 --- a/frontend/scenes/Search/SearchStore.js +++ b/frontend/scenes/Search/SearchStore.js @@ -7,7 +7,6 @@ import Document from 'models/Document'; class SearchStore { @observable documents: Array = []; @observable searchTerm: ?string = null; - @observable isFetching = false; /* Actions */ diff --git a/server/api/documents.js b/server/api/documents.js index 36accaf2d..5205e2c59 100644 --- a/server/api/documents.js +++ b/server/api/documents.js @@ -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), }; }); diff --git a/server/models/Document.js b/server/models/Document.js index 363f164bb..b912bc30f 100644 --- a/server/models/Document.js +++ b/server/models/Document.js @@ -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 }, }); }; diff --git a/server/presenters/document.js b/server/presenters/document.js index a3c9162a8..0cf8db4cb 100644 --- a/server/presenters/document.js +++ b/server/presenters/document.js @@ -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) {