feat: add filters to drafts (#1631)

* feat: add filters to drafts

Co-authored-by: Guilherme Diniz <guilhermedassumpcao@gmail.com>
This commit is contained in:
Tom Moor
2020-11-08 22:33:52 -08:00
committed by GitHub
parent 14e0ed8108
commit 4e7a1cd121
7 changed files with 184 additions and 28 deletions

View File

@ -1,5 +1,6 @@
// @flow // @flow
import ArrowKeyNavigation from "boundless-arrow-key-navigation"; import ArrowKeyNavigation from "boundless-arrow-key-navigation";
import { isEqual } from "lodash";
import { observable, action } from "mobx"; import { observable, action } from "mobx";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import * as React from "react"; import * as React from "react";
@ -40,6 +41,9 @@ class PaginatedList extends React.Component<Props> {
if (prevProps.fetch !== this.props.fetch) { if (prevProps.fetch !== this.props.fetch) {
this.fetchResults(); this.fetchResults();
} }
if (!isEqual(prevProps.options, this.props.options)) {
this.fetchResults();
}
} }
fetchResults = async () => { fetchResults = async () => {

View File

@ -69,7 +69,6 @@ class MainSidebar extends React.Component<Props> {
const { user, team } = auth; const { user, team } = auth;
if (!user || !team) return null; if (!user || !team) return null;
const draftDocumentsCount = documents.drafts.length;
const can = policies.abilities(team.id); const can = policies.abilities(team.id);
return ( return (
@ -123,8 +122,8 @@ class MainSidebar extends React.Component<Props> {
label={ label={
<Drafts align="center"> <Drafts align="center">
Drafts Drafts
{draftDocumentsCount > 0 && ( {documents.totalDrafts > 0 && (
<Bubble count={draftDocumentsCount} /> <Bubble count={documents.totalDrafts} />
)} )}
</Drafts> </Drafts>
} }

View File

@ -11,12 +11,11 @@ const H3 = styled.h3`
margin-top: 22px; margin-top: 22px;
margin-bottom: 12px; margin-bottom: 12px;
line-height: 1; line-height: 1;
position: relative;
`; `;
const Underline = styled("span")` const Underline = styled("span")`
position: relative; margin-top: -1px;
top: 1px;
display: inline-block; display: inline-block;
font-weight: 500; font-weight: 500;
font-size: 14px; font-size: 14px;

View File

@ -1,37 +1,107 @@
// @flow // @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 * as React from "react";
import { type RouterHistory } from "react-router-dom";
import styled from "styled-components";
import DocumentsStore from "stores/DocumentsStore"; 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 Actions, { Action } from "components/Actions";
import CenteredContent from "components/CenteredContent"; import CenteredContent from "components/CenteredContent";
import Empty from "components/Empty"; import Empty from "components/Empty";
import Flex from "components/Flex";
import Heading from "components/Heading"; import Heading from "components/Heading";
import InputSearch from "components/InputSearch"; import InputSearch from "components/InputSearch";
import PageTitle from "components/PageTitle"; import PageTitle from "components/PageTitle";
import PaginatedDocumentList from "components/PaginatedDocumentList"; import PaginatedDocumentList from "components/PaginatedDocumentList";
import Subheading from "components/Subheading"; import Subheading from "components/Subheading";
import NewDocumentMenu from "menus/NewDocumentMenu"; import NewDocumentMenu from "menus/NewDocumentMenu";
import { type LocationWithState } from "types";
type Props = { type Props = {|
documents: DocumentsStore, documents: DocumentsStore,
}; history: RouterHistory,
location: LocationWithState,
|};
@observer @observer
class Drafts extends React.Component<Props> { class Drafts extends React.Component<Props> {
@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() { 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 ( return (
<CenteredContent column auto> <CenteredContent column auto>
<PageTitle title="Drafts" /> <PageTitle title="Drafts" />
<Heading>Drafts</Heading> <Heading>Drafts</Heading>
<Subheading>
Documents
<Filters>
<CollectionFilter
collectionId={this.collectionId}
onSelect={(collectionId) =>
this.handleFilterChange({ collectionId })
}
/>
<DateFilter
dateFilter={this.dateFilter}
onSelect={(dateFilter) => this.handleFilterChange({ dateFilter })}
/>
</Filters>
</Subheading>
<PaginatedDocumentList <PaginatedDocumentList
heading={<Subheading>Documents</Subheading>} empty={
empty={<Empty>Youve not got any drafts at the moment.</Empty>} <Empty>
{isFiltered
? "No documents found for your filters."
: "Youve not got any drafts at the moment."}
</Empty>
}
fetch={fetchDrafts} fetch={fetchDrafts}
documents={drafts} documents={drafts(options)}
showDraft={false} options={options}
showCollection showCollection
/> />
@ -48,4 +118,17 @@ class Drafts extends React.Component<Props> {
} }
} }
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); export default inject("documents")(Drafts);

View File

@ -3,6 +3,7 @@ import invariant from "invariant";
import { find, orderBy, filter, compact, omitBy } from "lodash"; import { find, orderBy, filter, compact, omitBy } from "lodash";
import { observable, action, computed, runInAction } from "mobx"; import { observable, action, computed, runInAction } from "mobx";
import { MAX_TITLE_LENGTH } from "shared/constants"; import { MAX_TITLE_LENGTH } from "shared/constants";
import { subtractDate } from "shared/utils/date";
import naturalSort from "shared/utils/naturalSort"; import naturalSort from "shared/utils/naturalSort";
import BaseStore from "stores/BaseStore"; import BaseStore from "stores/BaseStore";
import RootStore from "stores/RootStore"; import RootStore from "stores/RootStore";
@ -168,12 +169,31 @@ export default class DocumentsStore extends BaseStore<Document> {
} }
@computed @computed
get drafts(): Document[] { get totalDrafts(): number {
return filter( return this.drafts().length;
}
drafts = (options = {}): Document[] => {
let drafts = filter(
orderBy(this.all, "updatedAt", "desc"), orderBy(this.all, "updatedAt", "desc"),
(doc) => !doc.publishedAt (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 @computed
get active(): ?Document { get active(): ?Document {

View File

@ -1,6 +1,7 @@
// @flow // @flow
import Router from "koa-router"; import Router from "koa-router";
import Sequelize from "sequelize"; import Sequelize from "sequelize";
import { subtractDate } from "../../shared/utils/date";
import documentImporter from "../commands/documentImporter"; import documentImporter from "../commands/documentImporter";
import documentMover from "../commands/documentMover"; import documentMover from "../commands/documentMover";
import { NotFoundError, InvalidRequestError } from "../errors"; 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) => { 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"; if (direction !== "ASC") direction = "DESC";
const user = ctx.state.user; 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 collectionScope = { method: ["withCollection", user.id] };
const viewScope = { method: ["withViews", user.id] };
const documents = await Document.scope( const documents = await Document.scope(
"defaultScope", "defaultScope",
collectionScope, collectionScope
viewScope
).findAll({ ).findAll({
where: { where: whereConditions,
userId: user.id,
collectionId: collectionIds,
publishedAt: { [Op.eq]: null },
},
order: [[sort, direction]], order: [[sort, direction]],
offset: ctx.state.pagination.offset, offset: ctx.state.pagination.offset,
limit: ctx.state.pagination.limit, limit: ctx.state.pagination.limit,

23
shared/utils/date.js Normal file
View File

@ -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;
}
}