diff --git a/app/components/PaginatedList.js b/app/components/PaginatedList.js index a23ae569..ba58bf5a 100644 --- a/app/components/PaginatedList.js +++ b/app/components/PaginatedList.js @@ -1,5 +1,6 @@ // @flow import ArrowKeyNavigation from "boundless-arrow-key-navigation"; +import { isEqual } from "lodash"; import { observable, action } from "mobx"; import { observer } from "mobx-react"; import * as React from "react"; @@ -40,6 +41,9 @@ class PaginatedList extends React.Component { if (prevProps.fetch !== this.props.fetch) { this.fetchResults(); } + if (!isEqual(prevProps.options, this.props.options)) { + this.fetchResults(); + } } fetchResults = async () => { diff --git a/app/components/Sidebar/Main.js b/app/components/Sidebar/Main.js index 75b7eae0..13e86d87 100644 --- a/app/components/Sidebar/Main.js +++ b/app/components/Sidebar/Main.js @@ -69,7 +69,6 @@ class MainSidebar extends React.Component { const { user, team } = auth; if (!user || !team) return null; - const draftDocumentsCount = documents.drafts.length; const can = policies.abilities(team.id); return ( @@ -123,8 +122,8 @@ class MainSidebar extends React.Component { label={ Drafts - {draftDocumentsCount > 0 && ( - + {documents.totalDrafts > 0 && ( + )} } diff --git a/app/components/Subheading.js b/app/components/Subheading.js index b7bc5be9..c0716deb 100644 --- a/app/components/Subheading.js +++ b/app/components/Subheading.js @@ -11,12 +11,11 @@ const H3 = styled.h3` margin-top: 22px; margin-bottom: 12px; line-height: 1; + position: relative; `; const Underline = styled("span")` - position: relative; - top: 1px; - + margin-top: -1px; display: inline-block; font-weight: 500; font-size: 14px; diff --git a/app/scenes/Drafts.js b/app/scenes/Drafts.js index 14ae4724..fa38201b 100644 --- a/app/scenes/Drafts.js +++ b/app/scenes/Drafts.js @@ -1,37 +1,107 @@ // @flow -import { observer, inject } from "mobx-react"; +import { observable } from "mobx"; +import { inject, observer } from "mobx-react"; +import queryString from "query-string"; import * as React from "react"; - +import { type RouterHistory } from "react-router-dom"; +import styled from "styled-components"; import DocumentsStore from "stores/DocumentsStore"; +import CollectionFilter from "scenes/Search/components/CollectionFilter"; +import DateFilter from "scenes/Search/components/DateFilter"; + import Actions, { Action } from "components/Actions"; import CenteredContent from "components/CenteredContent"; import Empty from "components/Empty"; +import Flex from "components/Flex"; import Heading from "components/Heading"; import InputSearch from "components/InputSearch"; import PageTitle from "components/PageTitle"; import PaginatedDocumentList from "components/PaginatedDocumentList"; import Subheading from "components/Subheading"; import NewDocumentMenu from "menus/NewDocumentMenu"; +import { type LocationWithState } from "types"; -type Props = { +type Props = {| documents: DocumentsStore, -}; + history: RouterHistory, + location: LocationWithState, +|}; @observer class Drafts extends React.Component { + @observable params: URLSearchParams = new URLSearchParams( + this.props.location.search + ); + + componentDidUpdate(prevProps) { + if (prevProps.location.search !== this.props.location.search) { + this.handleQueryChange(); + } + } + + handleQueryChange = () => { + this.params = new URLSearchParams(this.props.location.search); + }; + + handleFilterChange = (search) => { + this.props.history.replace({ + pathname: this.props.location.pathname, + search: queryString.stringify({ + ...queryString.parse(this.props.location.search), + ...search, + }), + }); + }; + + get collectionId() { + const id = this.params.get("collectionId"); + return id ? id : undefined; + } + + get dateFilter() { + const id = this.params.get("dateFilter"); + return id ? id : undefined; + } + render() { - const { fetchDrafts, drafts } = this.props.documents; + const { drafts, fetchDrafts } = this.props.documents; + const isFiltered = this.collectionId || this.dateFilter; + const options = { + dateFilter: this.dateFilter, + collectionId: this.collectionId, + }; return ( Drafts + + Documents + + + this.handleFilterChange({ collectionId }) + } + /> + this.handleFilterChange({ dateFilter })} + /> + + + Documents} - empty={You’ve not got any drafts at the moment.} + empty={ + + {isFiltered + ? "No documents found for your filters." + : "You’ve not got any drafts at the moment."} + + } fetch={fetchDrafts} - documents={drafts} - showDraft={false} + documents={drafts(options)} + options={options} showCollection /> @@ -48,4 +118,17 @@ class Drafts extends React.Component { } } +const Filters = styled(Flex)` + opacity: 0.85; + transition: opacity 100ms ease-in-out; + position: absolute; + right: -8px; + bottom: 0; + padding: 0 0 6px; + + &:hover { + opacity: 1; + } +`; + export default inject("documents")(Drafts); diff --git a/app/stores/DocumentsStore.js b/app/stores/DocumentsStore.js index 5c39a679..8ecb9675 100644 --- a/app/stores/DocumentsStore.js +++ b/app/stores/DocumentsStore.js @@ -3,6 +3,7 @@ import invariant from "invariant"; import { find, orderBy, filter, compact, omitBy } from "lodash"; import { observable, action, computed, runInAction } from "mobx"; import { MAX_TITLE_LENGTH } from "shared/constants"; +import { subtractDate } from "shared/utils/date"; import naturalSort from "shared/utils/naturalSort"; import BaseStore from "stores/BaseStore"; import RootStore from "stores/RootStore"; @@ -168,12 +169,31 @@ export default class DocumentsStore extends BaseStore { } @computed - get drafts(): Document[] { - return filter( + get totalDrafts(): number { + return this.drafts().length; + } + + drafts = (options = {}): Document[] => { + let drafts = filter( orderBy(this.all, "updatedAt", "desc"), (doc) => !doc.publishedAt ); - } + + if (options.dateFilter) { + drafts = filter( + drafts, + (draft) => + new Date(draft.updatedAt) >= + subtractDate(new Date(), options.dateFilter) + ); + } + + if (options.collectionId) { + drafts = filter(drafts, { collectionId: options.collectionId }); + } + + return drafts; + }; @computed get active(): ?Document { diff --git a/server/api/documents.js b/server/api/documents.js index b40a80ea..7048e1fe 100644 --- a/server/api/documents.js +++ b/server/api/documents.js @@ -1,6 +1,7 @@ // @flow import Router from "koa-router"; import Sequelize from "sequelize"; +import { subtractDate } from "../../shared/utils/date"; import documentImporter from "../commands/documentImporter"; import documentMover from "../commands/documentMover"; import { NotFoundError, InvalidRequestError } from "../errors"; @@ -359,24 +360,51 @@ router.post("documents.starred", auth(), pagination(), async (ctx) => { }); router.post("documents.drafts", auth(), pagination(), async (ctx) => { - let { sort = "updatedAt", direction } = ctx.body; + let { collectionId, dateFilter, sort = "updatedAt", direction } = ctx.body; if (direction !== "ASC") direction = "DESC"; const user = ctx.state.user; - const collectionIds = await user.collectionIds(); + + if (collectionId) { + ctx.assertUuid(collectionId, "collectionId must be a UUID"); + + const collection = await Collection.scope({ + method: ["withMembership", user.id], + }).findByPk(collectionId); + authorize(user, "read", collection); + } + + const collectionIds = !!collectionId + ? [collectionId] + : await user.collectionIds(); + + const whereConditions = { + userId: user.id, + collectionId: collectionIds, + publishedAt: { [Op.eq]: null }, + updatedAt: undefined, + }; + + if (dateFilter) { + ctx.assertIn( + dateFilter, + ["day", "week", "month", "year"], + "dateFilter must be one of day,week,month,year" + ); + + whereConditions.updatedAt = { + [Op.gte]: subtractDate(new Date(), dateFilter), + }; + } else { + delete whereConditions.updatedAt; + } const collectionScope = { method: ["withCollection", user.id] }; - const viewScope = { method: ["withViews", user.id] }; const documents = await Document.scope( "defaultScope", - collectionScope, - viewScope + collectionScope ).findAll({ - where: { - userId: user.id, - collectionId: collectionIds, - publishedAt: { [Op.eq]: null }, - }, + where: whereConditions, order: [[sort, direction]], offset: ctx.state.pagination.offset, limit: ctx.state.pagination.limit, diff --git a/shared/utils/date.js b/shared/utils/date.js new file mode 100644 index 00000000..d19518b5 --- /dev/null +++ b/shared/utils/date.js @@ -0,0 +1,23 @@ +// @flow +import subDays from "date-fns/sub_days"; +import subMonth from "date-fns/sub_months"; +import subWeek from "date-fns/sub_weeks"; +import subYear from "date-fns/sub_years"; + +export function subtractDate( + date: Date, + period: "day" | "week" | "month" | "year" +) { + switch (period) { + case "day": + return subDays(date, 1); + case "week": + return subWeek(date, 1); + case "month": + return subMonth(date, 1); + case "year": + return subYear(date, 1); + default: + return date; + } +}