improv: use statusFilter instead of includeArchive,includeDrafts for document search (#6537)

* improv: use statusFilter instead of includeArchive,includeDrafts for document search

* improv: update FilterComponent to add support for multiple selected items

* feat: update document type search ui

* fix test

* Restore support for old parameters to avoid breaking change

---------

Co-authored-by: Tom Moor <tom.moor@gmail.com>
This commit is contained in:
Pranav Joglekar
2024-02-25 00:32:19 +05:30
committed by GitHub
parent b7f0af9b85
commit 50b90b8878
15 changed files with 426 additions and 191 deletions

View File

@@ -16,7 +16,7 @@ type TFilterOption = {
type Props = {
options: TFilterOption[];
activeKey: string | null | undefined;
selectedKeys: (string | null | undefined)[];
defaultLabel?: string;
selectedPrefix?: string;
className?: string;
@@ -25,7 +25,7 @@ type Props = {
const FilterOptions = ({
options,
activeKey = "",
selectedKeys = [],
defaultLabel = "Filter options",
selectedPrefix = "",
className,
@@ -34,17 +34,22 @@ const FilterOptions = ({
const menu = useMenuState({
modal: true,
});
const selected =
options.find((option) => option.key === activeKey) || options[0];
const selectedItems = options.filter((option) =>
selectedKeys.includes(option.key)
);
const selectedLabel = selected ? `${selectedPrefix} ${selected.label}` : "";
const selectedLabel = selectedItems.length
? selectedItems
.map((selected) => `${selectedPrefix} ${selected.label}`)
.join(", ")
: "";
return (
<Wrapper>
<MenuButton {...menu}>
{(props) => (
<StyledButton {...props} className={className} neutral disclosure>
{activeKey ? selectedLabel : defaultLabel}
{selectedItems.length ? selectedLabel : defaultLabel}
</StyledButton>
)}
</MenuButton>
@@ -56,7 +61,7 @@ const FilterOptions = ({
onSelect(option.key);
menu.hide();
}}
selected={option.key === activeKey}
selected={selectedKeys.includes(option.key)}
{...menu}
>
{option.icon && <Icon>{option.icon}</Icon>}

View File

@@ -9,7 +9,10 @@ import breakpoint from "styled-components-breakpoint";
import { v4 as uuidv4 } from "uuid";
import { Pagination } from "@shared/constants";
import { hideScrollbars } from "@shared/styles";
import { DateFilter as TDateFilter } from "@shared/types";
import {
DateFilter as TDateFilter,
StatusFilter as TStatusFilter,
} from "@shared/types";
import ArrowKeyNavigation from "~/components/ArrowKeyNavigation";
import DocumentListItem from "~/components/DocumentListItem";
import Empty from "~/components/Empty";
@@ -54,18 +57,18 @@ function Search(props: Props) {
// filters
const query = decodeURIComponentSafe(routeMatch.params.term ?? "");
const includeArchived = params.get("includeArchived") === "true";
const includeDrafts = params.get("includeDrafts") !== "false";
const collectionId = params.get("collectionId") ?? undefined;
const userId = params.get("userId") ?? undefined;
const dateFilter = (params.get("dateFilter") as TDateFilter) ?? undefined;
const statusFilter = params.getAll("statusFilter")?.length
? (params.getAll("statusFilter") as TStatusFilter[])
: [TStatusFilter.Published, TStatusFilter.Draft];
const titleFilter = params.get("titleFilter") === "true";
const filters = React.useMemo(
() => ({
query,
includeArchived,
includeDrafts,
statusFilter,
collectionId,
userId,
dateFilter,
@@ -73,8 +76,7 @@ function Search(props: Props) {
}),
[
query,
includeArchived,
includeDrafts,
JSON.stringify(statusFilter),
collectionId,
userId,
dateFilter,
@@ -118,8 +120,7 @@ function Search(props: Props) {
collectionId?: string | undefined;
userId?: string | undefined;
dateFilter?: TDateFilter;
includeArchived?: boolean | undefined;
includeDrafts?: boolean | undefined;
statusFilter?: TStatusFilter[];
titleFilter?: boolean | undefined;
}) => {
history.replace({
@@ -214,10 +215,9 @@ function Search(props: Props) {
<>
<Filters>
<DocumentTypeFilter
includeArchived={includeArchived}
includeDrafts={includeDrafts}
onSelect={({ includeArchived, includeDrafts }) =>
handleFilterChange({ includeArchived, includeDrafts })
statusFilter={statusFilter}
onSelect={({ statusFilter }) =>
handleFilterChange({ statusFilter })
}
/>
<CollectionFilter

View File

@@ -29,7 +29,7 @@ function CollectionFilter(props: Props) {
return (
<FilterOptions
options={options}
activeKey={collectionId}
selectedKeys={[collectionId]}
onSelect={onSelect}
defaultLabel={t("Any collection")}
selectedPrefix={`${t("Collection")}:`}

View File

@@ -39,7 +39,7 @@ const DateFilter = ({ dateFilter, onSelect }: Props) => {
return (
<FilterOptions
options={options}
activeKey={dateFilter}
selectedKeys={[dateFilter]}
onSelect={onSelect}
defaultLabel={t("Any time")}
/>

View File

@@ -1,80 +1,50 @@
import * as React from "react";
import { useTranslation } from "react-i18next";
import { StatusFilter as TStatusFilter } from "@shared/types";
import FilterOptions from "~/components/FilterOptions";
type Props = {
includeArchived?: boolean;
includeDrafts?: boolean;
onSelect: (option: {
includeArchived?: boolean;
includeDrafts?: boolean;
}) => void;
statusFilter: TStatusFilter[];
onSelect: (option: { statusFilter: TStatusFilter[] }) => void;
};
enum DocumentType {
Published = "published",
Active = "active",
All = "all",
}
const DocumentTypeFilter = ({
includeArchived,
includeDrafts,
onSelect,
}: Props) => {
const DocumentTypeFilter = ({ statusFilter, onSelect }: Props) => {
const { t } = useTranslation();
const options = React.useMemo(
() => [
{
key: DocumentType.Published,
key: TStatusFilter.Published,
label: t("Published documents"),
note: t("Documents you have access to, excluding drafts"),
},
{
key: DocumentType.Active,
label: t("Active documents"),
note: t("Documents you have access to, including drafts"),
key: TStatusFilter.Archived,
label: t("Archived documents"),
},
{
key: DocumentType.All,
label: t("All documents"),
note: t("Documents you have access to, including drafts and archived"),
key: TStatusFilter.Draft,
label: t("Draft documents"),
},
],
[t]
);
const getActiveKey = () => {
if (includeArchived && includeDrafts) {
return DocumentType.All;
const handleSelect = (key: TStatusFilter) => {
let modifiedStatusFilter;
if (statusFilter.includes(key)) {
modifiedStatusFilter = statusFilter.filter((status) => status !== key);
} else {
modifiedStatusFilter = [...statusFilter, key];
}
if (includeDrafts) {
return DocumentType.Active;
}
return DocumentType.Published;
};
const handleSelect = (key: DocumentType) => {
switch (key) {
case DocumentType.Published:
return onSelect({ includeArchived: false, includeDrafts: false });
case DocumentType.Active:
return onSelect({ includeArchived: false, includeDrafts: true });
case DocumentType.All:
return onSelect({ includeArchived: true, includeDrafts: true });
default:
onSelect({ includeArchived: false, includeDrafts: false });
}
onSelect({ statusFilter: modifiedStatusFilter });
};
return (
<FilterOptions
options={options}
activeKey={getActiveKey()}
selectedKeys={statusFilter}
onSelect={handleSelect}
defaultLabel={t("Document type")}
defaultLabel={t("Any status")}
/>
);
};

View File

@@ -43,7 +43,7 @@ function UserFilter(props: Props) {
return (
<FilterOptions
options={options}
activeKey={userId}
selectedKeys={[userId]}
onSelect={onSelect}
defaultLabel={t("Any author")}
selectedPrefix={`${t("Author")}:`}

View File

@@ -56,7 +56,7 @@ const UserStatusFilter = ({ activeKey, onSelect, ...rest }: Props) => {
return (
<FilterOptions
options={options}
activeKey={activeKey}
selectedKeys={[activeKey]}
onSelect={onSelect}
defaultLabel={t("Active")}
{...rest}

View File

@@ -10,6 +10,7 @@ import type {
JSONObject,
NavigationNode,
PublicTeam,
StatusFilter,
} from "@shared/types";
import { subtractDate } from "@shared/utils/date";
import { bytesToHumanReadable } from "@shared/utils/files";
@@ -36,8 +37,7 @@ export type SearchParams = {
offset?: number;
limit?: number;
dateFilter?: DateFilter;
includeArchived?: boolean;
includeDrafts?: boolean;
statusFilter?: StatusFilter[];
collectionId?: string;
userId?: string;
shareId?: string;

View File

@@ -1,4 +1,4 @@
import { DocumentPermission } from "@shared/types";
import { DocumentPermission, StatusFilter } from "@shared/types";
import SearchHelper from "@server/models/helpers/SearchHelper";
import {
buildDocument,
@@ -208,28 +208,30 @@ describe("SearchHelper", () => {
createdById: user.id,
title: "test",
});
await buildDocument({
userId: user.id,
teamId: user.teamId,
createdById: user.id,
title: "test",
});
await buildDocument({
userId: user.id,
teamId: user.teamId,
createdById: user.id,
title: "test",
archivedAt: new Date(),
});
const { results } = await SearchHelper.searchForUser(user, "test", {
includeDrafts: true,
statusFilter: [StatusFilter.Draft],
});
expect(results.length).toBe(1);
});
test("should not include drafts", async () => {
test("should not include drafts with user read permission", async () => {
const user = await buildUser();
await buildDraftDocument({
teamId: user.teamId,
userId: user.id,
createdById: user.id,
title: "test",
});
const { results } = await SearchHelper.searchForUser(user, "test", {
includeDrafts: false,
});
expect(results.length).toBe(0);
});
test("should not include drafts with user permission", async () => {
const user = await buildUser();
const draft = await buildDraftDocument({
teamId: user.teamId,
userId: user.id,
@@ -244,32 +246,105 @@ describe("SearchHelper", () => {
});
const { results } = await SearchHelper.searchForUser(user, "test", {
includeDrafts: false,
statusFilter: [StatusFilter.Published, StatusFilter.Archived],
});
expect(results.length).toBe(0);
});
test("should include results from drafts as well", async () => {
test("should search only published created by user", async () => {
const user = await buildUser();
await buildDocument({
userId: user.id,
teamId: user.teamId,
createdById: user.id,
title: "not draft",
title: "test",
});
await buildDraftDocument({
teamId: user.teamId,
userId: user.id,
createdById: user.id,
title: "draft",
title: "test",
});
const { results } = await SearchHelper.searchForUser(user, "draft", {
includeDrafts: true,
await buildDocument({
userId: user.id,
teamId: user.teamId,
createdById: user.id,
title: "test",
});
await buildDocument({
userId: user.id,
teamId: user.teamId,
createdById: user.id,
title: "test",
archivedAt: new Date(),
});
const { results } = await SearchHelper.searchForUser(user, "test", {
statusFilter: [StatusFilter.Published],
});
expect(results.length).toBe(1);
});
test("should search only archived documents created by user", async () => {
const user = await buildUser();
await buildDocument({
title: "test",
});
await buildDraftDocument({
teamId: user.teamId,
userId: user.id,
createdById: user.id,
title: "test",
});
await buildDocument({
userId: user.id,
teamId: user.teamId,
createdById: user.id,
title: "test",
});
await buildDocument({
userId: user.id,
teamId: user.teamId,
createdById: user.id,
title: "test",
});
await buildDocument({
userId: user.id,
teamId: user.teamId,
createdById: user.id,
title: "test",
archivedAt: new Date(),
});
const { results } = await SearchHelper.searchForUser(user, "test", {
statusFilter: [StatusFilter.Archived],
});
expect(results.length).toBe(1);
});
test("should return results from archived and published", async () => {
const user = await buildUser();
await buildDraftDocument({
teamId: user.teamId,
userId: user.id,
createdById: user.id,
title: "test",
});
await buildDocument({
userId: user.id,
teamId: user.teamId,
createdById: user.id,
title: "test",
});
await buildDocument({
userId: user.id,
teamId: user.teamId,
createdById: user.id,
title: "test",
archivedAt: new Date(),
});
const { results } = await SearchHelper.searchForUser(user, "test", {
statusFilter: [StatusFilter.Archived, StatusFilter.Published],
});
expect(results.length).toBe(2);
});
test("should not include results from drafts", async () => {
test("should return results from drafts and published", async () => {
const user = await buildUser();
await buildDocument({
userId: user.id,
@@ -283,10 +358,44 @@ describe("SearchHelper", () => {
createdById: user.id,
title: "draft",
});
const { results } = await SearchHelper.searchForUser(user, "draft", {
includeDrafts: false,
await buildDocument({
userId: user.id,
teamId: user.teamId,
createdById: user.id,
title: "archived not draft",
archivedAt: new Date(),
});
expect(results.length).toBe(1);
const { results } = await SearchHelper.searchForUser(user, "draft", {
statusFilter: [StatusFilter.Published, StatusFilter.Draft],
});
expect(results.length).toBe(2);
});
test("should include results from drafts and archived", async () => {
const user = await buildUser();
await buildDocument({
userId: user.id,
teamId: user.teamId,
createdById: user.id,
title: "not draft",
});
await buildDraftDocument({
teamId: user.teamId,
userId: user.id,
createdById: user.id,
title: "draft",
});
await buildDocument({
userId: user.id,
teamId: user.teamId,
createdById: user.id,
title: "archived not draft",
archivedAt: new Date(),
});
const { results } = await SearchHelper.searchForUser(user, "draft", {
statusFilter: [StatusFilter.Draft, StatusFilter.Archived],
});
expect(results.length).toBe(2);
});
test("should return the total count of search results", async () => {
@@ -421,38 +530,37 @@ describe("SearchHelper", () => {
test("should search only drafts created by user", async () => {
const user = await buildUser();
await buildDraftDocument({
teamId: user.teamId,
userId: user.id,
createdById: user.id,
title: "test",
});
const documents = await SearchHelper.searchTitlesForUser(user, "test", {
includeDrafts: true,
});
expect(documents.length).toBe(1);
});
test("should not include drafts", async () => {
const user = await buildUser();
await buildDraftDocument({
teamId: user.teamId,
userId: user.id,
createdById: user.id,
title: "test",
});
const documents = await SearchHelper.searchTitlesForUser(user, "test", {
includeDrafts: false,
});
expect(documents.length).toBe(0);
});
test("should include results from drafts as well", async () => {
const user = await buildUser();
await buildDocument({
userId: user.id,
teamId: user.teamId,
createdById: user.id,
title: "not test",
title: "test",
});
await buildDocument({
userId: user.id,
teamId: user.teamId,
createdById: user.id,
title: "test",
archivedAt: new Date(),
});
const documents = await SearchHelper.searchTitlesForUser(user, "test", {
statusFilter: [StatusFilter.Draft],
});
expect(documents.length).toBe(1);
});
test("should search only published created by user", async () => {
const user = await buildUser();
await buildDocument({
title: "test",
});
await buildDraftDocument({
teamId: user.teamId,
@@ -460,30 +568,140 @@ describe("SearchHelper", () => {
createdById: user.id,
title: "test",
});
await buildDocument({
userId: user.id,
teamId: user.teamId,
createdById: user.id,
title: "test",
});
await buildDocument({
userId: user.id,
teamId: user.teamId,
createdById: user.id,
title: "test",
archivedAt: new Date(),
});
const documents = await SearchHelper.searchTitlesForUser(user, "test", {
includeDrafts: true,
statusFilter: [StatusFilter.Published],
});
expect(documents.length).toBe(1);
});
test("should search only archived documents created by user", async () => {
const user = await buildUser();
await buildDocument({
title: "test",
});
await buildDraftDocument({
teamId: user.teamId,
userId: user.id,
createdById: user.id,
title: "test",
});
await buildDocument({
userId: user.id,
teamId: user.teamId,
createdById: user.id,
title: "test",
});
await buildDocument({
userId: user.id,
teamId: user.teamId,
createdById: user.id,
title: "test",
});
await buildDocument({
userId: user.id,
teamId: user.teamId,
createdById: user.id,
title: "test",
archivedAt: new Date(),
});
const documents = await SearchHelper.searchTitlesForUser(user, "test", {
statusFilter: [StatusFilter.Archived],
});
expect(documents.length).toBe(1);
});
test("should return results from archived and published", async () => {
const user = await buildUser();
await buildDraftDocument({
teamId: user.teamId,
userId: user.id,
createdById: user.id,
title: "test",
});
await buildDocument({
userId: user.id,
teamId: user.teamId,
createdById: user.id,
title: "test",
});
await buildDocument({
userId: user.id,
teamId: user.teamId,
createdById: user.id,
title: "test",
archivedAt: new Date(),
});
const documents = await SearchHelper.searchTitlesForUser(user, "test", {
statusFilter: [StatusFilter.Archived, StatusFilter.Published],
});
expect(documents.length).toBe(2);
});
test("should not include results from drafts", async () => {
test("should return results from drafts and published", async () => {
const user = await buildUser();
await buildDocument({
userId: user.id,
teamId: user.teamId,
createdById: user.id,
title: "not test",
title: "not draft",
});
await buildDraftDocument({
teamId: user.teamId,
userId: user.id,
createdById: user.id,
title: "test",
title: "draft",
});
const documents = await SearchHelper.searchTitlesForUser(user, "test", {
includeDrafts: false,
await buildDocument({
userId: user.id,
teamId: user.teamId,
createdById: user.id,
title: "archived not draft",
archivedAt: new Date(),
});
expect(documents.length).toBe(1);
const documents = await SearchHelper.searchTitlesForUser(user, "draft", {
statusFilter: [StatusFilter.Published, StatusFilter.Draft],
});
expect(documents.length).toBe(2);
});
test("should include results from drafts and archived", async () => {
const user = await buildUser();
await buildDocument({
userId: user.id,
teamId: user.teamId,
createdById: user.id,
title: "not draft",
});
await buildDraftDocument({
teamId: user.teamId,
userId: user.id,
createdById: user.id,
title: "draft",
});
await buildDocument({
userId: user.id,
teamId: user.teamId,
createdById: user.id,
title: "archived not draft",
archivedAt: new Date(),
});
const documents = await SearchHelper.searchTitlesForUser(user, "draft", {
statusFilter: [StatusFilter.Draft, StatusFilter.Archived],
});
expect(documents.length).toBe(2);
});
});

View File

@@ -4,7 +4,7 @@ import find from "lodash/find";
import map from "lodash/map";
import queryParser from "pg-tsquery";
import { Op, Sequelize, WhereOptions } from "sequelize";
import { DateFilter } from "@shared/types";
import { DateFilter, StatusFilter } from "@shared/types";
import Collection from "@server/models/Collection";
import Document from "@server/models/Document";
import Share from "@server/models/Share";
@@ -36,12 +36,10 @@ type SearchOptions = {
share?: Share;
/** Limit results to a date range. */
dateFilter?: DateFilter;
/** Status of the documents to return */
statusFilter?: StatusFilter[];
/** Limit results to a list of users that collaborated on the document. */
collaboratorIds?: string[];
/** Include archived documents in the results */
includeArchived?: boolean;
/** Include draft documents in the results (will only ever return your own) */
includeDrafts?: boolean;
/** The minimum number of words to be returned in the contextual snippet */
snippetMinWords?: number;
/** The maximum number of words to be returned in the contextual snippet */
@@ -356,36 +354,59 @@ export default class SearchHelper {
});
}
if (!options.includeArchived) {
where[Op.and].push({
archivedAt: {
[Op.eq]: null,
},
});
}
if (options.includeDrafts && model instanceof User) {
where[Op.and].push({
[Op.or]: [
const statusQuery = [];
if (options.statusFilter?.includes(StatusFilter.Published)) {
statusQuery.push({
[Op.and]: [
{
publishedAt: {
[Op.ne]: null,
},
archivedAt: {
[Op.eq]: null,
},
},
{
createdById: model.id,
},
{ "$memberships.id$": { [Op.ne]: null } },
],
});
} else {
where[Op.and].push({
publishedAt: {
}
if (
options.statusFilter?.includes(StatusFilter.Draft) &&
// Only ever include draft results for the user's own documents
model instanceof User
) {
statusQuery.push({
[Op.and]: [
{
publishedAt: {
[Op.eq]: null,
},
archivedAt: {
[Op.eq]: null,
},
[Op.or]: [
{ createdById: model.id },
{ "$memberships.id$": { [Op.ne]: null } },
],
},
],
});
}
if (options.statusFilter?.includes(StatusFilter.Archived)) {
statusQuery.push({
archivedAt: {
[Op.ne]: null,
},
});
}
if (options.statusFilter?.length) {
where[Op.and].push({
[Op.or]: statusQuery,
});
}
return where;
}

View File

@@ -1,6 +1,10 @@
import { faker } from "@faker-js/faker";
import { addMinutes, subDays } from "date-fns";
import { CollectionPermission, DocumentPermission } from "@shared/types";
import {
CollectionPermission,
DocumentPermission,
StatusFilter,
} from "@shared/types";
import {
Document,
View,
@@ -1104,7 +1108,7 @@ describe("#documents.search_titles", () => {
body: {
token: member.getJwtToken(),
query: "title",
includeDrafts: true,
statusFilter: [StatusFilter.Draft],
},
});
const body = await res.json();
@@ -1215,7 +1219,7 @@ describe("#documents.search_titles", () => {
body: {
token: user.getJwtToken(),
query: "SECRET",
includeArchived: true,
statusFilter: [StatusFilter.Archived],
},
});
const body = await res.json();
@@ -1235,7 +1239,7 @@ describe("#documents.search_titles", () => {
body: {
token: user.getJwtToken(),
query: "SECRET",
includeDrafts: true,
statusFilter: [StatusFilter.Draft],
},
});
const body = await res.json();
@@ -1282,6 +1286,7 @@ describe("#documents.search_titles", () => {
body: {
token: user.getJwtToken(),
query: "SECRET",
statusFilter: [StatusFilter.Published, StatusFilter.Draft],
},
});
const body = await res.json();
@@ -1371,7 +1376,7 @@ describe("#documents.search", () => {
body: {
token: user.getJwtToken(),
shareId: share.id,
includeDrafts: true,
statusFilter: [StatusFilter.Draft],
query: "test",
},
});
@@ -1540,6 +1545,7 @@ describe("#documents.search", () => {
body: {
token: user.getJwtToken(),
query: "search term",
statusFilter: [StatusFilter.Published, StatusFilter.Archived],
},
});
const body = await res.json();
@@ -1574,7 +1580,7 @@ describe("#documents.search", () => {
body: {
token: user.getJwtToken(),
query: "search term",
includeDrafts: true,
statusFilter: [StatusFilter.Draft],
},
});
const body = await res.json();
@@ -1595,7 +1601,7 @@ describe("#documents.search", () => {
const res = await server.post("/api/documents.search", {
body: {
token: user.getJwtToken(),
includeDrafts: true,
statusFilter: [StatusFilter.Draft],
query: "text",
},
});
@@ -1616,7 +1622,7 @@ describe("#documents.search", () => {
body: {
token: user.getJwtToken(),
query: "search term",
includeDrafts: true,
statusFilter: [StatusFilter.Draft],
},
});
const body = await res.json();
@@ -1636,6 +1642,7 @@ describe("#documents.search", () => {
body: {
token: user.getJwtToken(),
query: "search term",
statusFilter: [StatusFilter.Published, StatusFilter.Draft],
},
});
const body = await res.json();
@@ -1655,7 +1662,7 @@ describe("#documents.search", () => {
body: {
token: user.getJwtToken(),
query: "search term",
includeArchived: true,
statusFilter: [StatusFilter.Archived],
},
});
const body = await res.json();
@@ -1899,7 +1906,7 @@ describe("#documents.search", () => {
body: {
token: member.getJwtToken(),
query: "title",
includeDrafts: true,
statusFilter: [StatusFilter.Draft],
},
});
const body = await res.json();

View File

@@ -7,7 +7,7 @@ import Router from "koa-router";
import escapeRegExp from "lodash/escapeRegExp";
import mime from "mime-types";
import { Op, ScopeOptions, Sequelize, WhereOptions } from "sequelize";
import { TeamPreference } from "@shared/types";
import { StatusFilter, TeamPreference } from "@shared/types";
import { subtractDate } from "@shared/utils/date";
import slugify from "@shared/utils/slugify";
import documentCreator from "@server/commands/documentCreator";
@@ -732,14 +732,8 @@ router.post(
rateLimiter(RateLimiterStrategy.OneHundredPerMinute),
validate(T.DocumentsSearchSchema),
async (ctx: APIContext<T.DocumentsSearchReq>) => {
const {
query,
includeArchived,
includeDrafts,
dateFilter,
collectionId,
userId,
} = ctx.input.body;
const { query, statusFilter, dateFilter, collectionId, userId } =
ctx.input.body;
const { offset, limit } = ctx.state.pagination;
const { user } = ctx.state.auth;
let collaboratorIds = undefined;
@@ -756,9 +750,8 @@ router.post(
}
const documents = await SearchHelper.searchTitlesForUser(user, query, {
includeArchived,
includeDrafts,
dateFilter,
statusFilter,
collectionId,
collaboratorIds,
offset,
@@ -786,11 +779,12 @@ router.post(
async (ctx: APIContext<T.DocumentsSearchReq>) => {
const {
query,
includeArchived,
includeDrafts,
collectionId,
userId,
dateFilter,
statusFilter = [],
includeArchived,
includeDrafts,
shareId,
snippetMinWords,
snippetMaxWords,
@@ -800,6 +794,14 @@ router.post(
// Unfortunately, this still doesn't adequately handle cases when auth is optional
const { user } = ctx.state.auth;
// TODO: Deprecated filter options, remove in a few versions
if (includeArchived && !statusFilter.includes(StatusFilter.Archived)) {
statusFilter.push(StatusFilter.Archived);
}
if (includeDrafts && !statusFilter.includes(StatusFilter.Draft)) {
statusFilter.push(StatusFilter.Draft);
}
let teamId;
let response;
let share;
@@ -823,11 +825,10 @@ router.post(
invariant(team, "Share must belong to a team");
response = await SearchHelper.searchForTeam(team, query, {
includeArchived,
includeDrafts,
collectionId: document.collectionId,
share,
dateFilter,
statusFilter,
offset,
limit,
snippetMinWords,
@@ -854,11 +855,10 @@ router.post(
}
response = await SearchHelper.searchForUser(user, query, {
includeArchived,
includeDrafts,
collaboratorIds,
collectionId,
dateFilter,
statusFilter,
offset,
limit,
snippetMinWords,

View File

@@ -3,7 +3,7 @@ import formidable from "formidable";
import isEmpty from "lodash/isEmpty";
import isUUID from "validator/lib/isUUID";
import { z } from "zod";
import { DocumentPermission } from "@shared/types";
import { DocumentPermission, StatusFilter } from "@shared/types";
import { SHARE_URL_SLUG_REGEX } from "@shared/utils/urlHelpers";
import { BaseSchema } from "@server/routes/api/schema";
@@ -147,18 +147,29 @@ export type DocumentsRestoreReq = z.infer<typeof DocumentsRestoreSchema>;
export const DocumentsSearchSchema = BaseSchema.extend({
body: SearchQuerySchema.merge(DateFilterSchema).extend({
/** Whether to include archived docs in results */
includeArchived: z.boolean().optional(),
/** Whether to include drafts in results */
includeDrafts: z.boolean().optional(),
/** Filter results for team based on the collection */
collectionId: z.string().uuid().optional(),
/** Filter results based on user */
userId: z.string().uuid().optional(),
/**
* Whether to include archived documents in results
*
* @deprecated Use `statusFilter` instead
*/
includeArchived: z.boolean().optional(),
/**
* Whether to include draft documents in results
*
* @deprecated Use `statusFilter` instead
*/
includeDrafts: z.boolean().optional(),
/** Document statuses to include in results */
statusFilter: z.nativeEnum(StatusFilter).array().optional(),
/** Filter results for the team derived from shareId */
shareId: z
.string()

View File

@@ -740,12 +740,9 @@
"Past month": "Past month",
"Past year": "Past year",
"Published documents": "Published documents",
"Documents you have access to, excluding drafts": "Documents you have access to, excluding drafts",
"Active documents": "Active documents",
"Documents you have access to, including drafts": "Documents you have access to, including drafts",
"All documents": "All documents",
"Documents you have access to, including drafts and archived": "Documents you have access to, including drafts and archived",
"Document type": "Document type",
"Archived documents": "Archived documents",
"Draft documents": "Draft documents",
"Any status": "Any status",
"Search Results": "Search Results",
"Remove search": "Remove search",
"Any author": "Any author",

View File

@@ -6,6 +6,12 @@ export enum UserRole {
export type DateFilter = "day" | "week" | "month" | "year";
export enum StatusFilter {
Published = "published",
Archived = "archived",
Draft = "draft",
}
export enum Client {
Web = "web",
Desktop = "desktop",