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

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