diff --git a/.vscode/settings.json b/.vscode/settings.json index f5852ab56705..48305b633ae6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,6 @@ { "javascript.validate.enable": false, "typescript.validate.enable": false, - "editor.formatOnSave": true, + "editor.formatOnSave": false, "typescript.format.enable": false } \ No newline at end of file diff --git a/app/scenes/Drafts.js b/app/scenes/Drafts.js index f514f0a123a1..51326f93d3f8 100644 --- a/app/scenes/Drafts.js +++ b/app/scenes/Drafts.js @@ -1,13 +1,24 @@ // @flow -import { observer, inject } from "mobx-react"; +import { debounce } from "lodash"; +import { observable, action } from "mobx"; +import { inject, observer } from "mobx-react"; +import queryString from "query-string"; import * as React from "react"; - +import styled from "styled-components"; +import breakpoint from "styled-components-breakpoint"; import DocumentsStore from "stores/DocumentsStore"; +import Document from "models/Document"; +import CollectionFilter from "scenes/Search/components/CollectionFilter"; +import DateFilter from "scenes/Search/components/DateFilter"; +import UserFilter from "scenes/Search/components/UserFilter"; + 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 LoadingIndicator from "components/LoadingIndicator"; import PageTitle from "components/PageTitle"; import PaginatedDocumentList from "components/PaginatedDocumentList"; import Subheading from "components/Subheading"; @@ -19,21 +30,115 @@ type Props = { @observer class Drafts extends React.Component { + @observable params: URLSearchParams = new URLSearchParams(); + @observable isFetching: boolean = false; + @observable drafts: Document[] = []; + + componentDidMount() { + this.handleQueryChange(); + } + + componentDidUpdate(prevProps) { + if (prevProps.location.search !== this.props.location.search) { + this.handleQueryChange(); + } + } + + handleQueryChange = () => { + this.params = new URLSearchParams(this.props.location.search); + this.fetchResultsDebounced(); + }; + + handleFilterChange = (search) => { + this.props.history.replace({ + pathname: this.props.location.pathname, + search: queryString.stringify({ + ...queryString.parse(this.props.location.search), + ...search, + }), + }); + + this.fetchResultsDebounced(); + }; + + @action + fetchResults = async () => { + this.isFetching = true; + + try { + const result = await this.props.documents.fetchDrafts({ + dateFilter: this.dateFilter, + collectionId: this.collectionId, + userId: this.userId, + }); + + this.drafts = result.map( + (item) => new Document(item, this.props.documents) + ); + } finally { + this.isFetching = false; + } + }; + + fetchResultsDebounced = debounce(this.fetchResults, 350, { + leading: false, + trailing: true, + }); + + get collectionId() { + const id = this.params.get("collectionId"); + return id ? id : undefined; + } + + get userId() { + const id = this.params.get("userId"); + return id ? id : undefined; + } + + get dateFilter() { + const id = this.params.get("dateFilter"); + return id ? id : undefined; + } + render() { - const { fetchDrafts, drafts } = this.props.documents; + const { fetchDrafts } = this.props.documents; return ( Drafts - Documents} - empty={You’ve not got any drafts at the moment.} - fetch={fetchDrafts} - documents={drafts} - showDraft={false} - showCollection - /> + + + this.handleFilterChange({ collectionId }) + } + /> + this.handleFilterChange({ userId })} + /> + this.handleFilterChange({ dateFilter })} + /> + + {this.isFetching ? ( + + ) : ( + Documents} + empty={You’ve not got any drafts at the moment.} + fetch={fetchDrafts} + documents={this.drafts} + options={{ + dateFilter: this.dateFilter, + collectionId: this.collectionId, + userId: this.userId, + }} + showCollection + /> + )} @@ -48,4 +153,18 @@ class Drafts extends React.Component { } } +const Filters = styled(Flex)` + opacity: 0.85; + transition: opacity 100ms ease-in-out; + padding: 8px 0; + + ${breakpoint("tablet")` + padding: 0; + `}; + + &:hover { + opacity: 1; + } +`; + export default inject("documents")(Drafts); diff --git a/server/api/documents.js b/server/api/documents.js index 90f88e1a7529..c8a4a342fd11 100644 --- a/server/api/documents.js +++ b/server/api/documents.js @@ -5,23 +5,24 @@ import documentMover from "../commands/documentMover"; import { InvalidRequestError } from "../errors"; import auth from "../middlewares/authentication"; import { + Backlink, Collection, Document, Event, + Revision, Share, Star, - View, - Revision, - Backlink, User, + View, } from "../models"; import policy from "../policies"; import { - presentDocument, presentCollection, + presentDocument, presentPolicies, } from "../presenters"; import { sequelize } from "../sequelize"; +import { subtractDate } from "../utils/date"; import pagination from "./middlewares/pagination"; const Op = Sequelize.Op; @@ -341,22 +342,61 @@ router.post("documents.starred", auth(), pagination(), async (ctx) => { }); router.post("documents.drafts", auth(), pagination(), async (ctx) => { - let { sort = "updatedAt", direction } = ctx.body; + let { + collectionId, + userId, + 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); + } + + if (userId) { + ctx.assertUuid(userId, "userId must be a UUID"); + } + + const collectionIds = !!collectionId + ? [collectionId] + : await user.collectionIds(); + + const whereConditions = { + userId: 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 documents = await Document.scope( "defaultScope", 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/server/utils/date.js b/server/utils/date.js new file mode 100644 index 000000000000..2ce867671380 --- /dev/null +++ b/server/utils/date.js @@ -0,0 +1,24 @@ +// @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; + } +}