mirror of
https://github.com/outline/outline.git
synced 2026-01-06 11:09:55 -06:00
* 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>
731 lines
22 KiB
TypeScript
731 lines
22 KiB
TypeScript
import { DocumentPermission, StatusFilter } from "@shared/types";
|
|
import SearchHelper from "@server/models/helpers/SearchHelper";
|
|
import {
|
|
buildDocument,
|
|
buildDraftDocument,
|
|
buildCollection,
|
|
buildTeam,
|
|
buildUser,
|
|
buildShare,
|
|
} from "@server/test/factories";
|
|
import UserMembership from "../UserMembership";
|
|
|
|
beforeEach(async () => {
|
|
jest.resetAllMocks();
|
|
await buildDocument();
|
|
});
|
|
|
|
describe("SearchHelper", () => {
|
|
describe("#searchForTeam", () => {
|
|
test("should return search results from public collections", async () => {
|
|
const team = await buildTeam();
|
|
const collection = await buildCollection({
|
|
teamId: team.id,
|
|
});
|
|
const document = await buildDocument({
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
title: "test",
|
|
});
|
|
const { results } = await SearchHelper.searchForTeam(team, "test");
|
|
expect(results.length).toBe(1);
|
|
expect(results[0].document?.id).toBe(document.id);
|
|
});
|
|
|
|
test("should not return results from private collections without providing collectionId", async () => {
|
|
const team = await buildTeam();
|
|
const collection = await buildCollection({
|
|
permission: null,
|
|
teamId: team.id,
|
|
});
|
|
await buildDocument({
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
title: "test",
|
|
});
|
|
const { results } = await SearchHelper.searchForTeam(team, "test");
|
|
expect(results.length).toBe(0);
|
|
});
|
|
|
|
test("should return results from private collections when collectionId is provided", async () => {
|
|
const team = await buildTeam();
|
|
const collection = await buildCollection({
|
|
permission: null,
|
|
teamId: team.id,
|
|
});
|
|
await buildDocument({
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
title: "test",
|
|
});
|
|
const { results } = await SearchHelper.searchForTeam(team, "test", {
|
|
collectionId: collection.id,
|
|
});
|
|
expect(results.length).toBe(1);
|
|
});
|
|
|
|
test("should return results from document tree of shared document", async () => {
|
|
const team = await buildTeam();
|
|
const collection = await buildCollection({
|
|
permission: null,
|
|
teamId: team.id,
|
|
});
|
|
const document = await buildDocument({
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
title: "test 1",
|
|
});
|
|
await buildDocument({
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
title: "test 2",
|
|
});
|
|
|
|
const share = await buildShare({
|
|
documentId: document.id,
|
|
includeChildDocuments: true,
|
|
});
|
|
|
|
const { results } = await SearchHelper.searchForTeam(team, "test", {
|
|
collectionId: collection.id,
|
|
share,
|
|
});
|
|
expect(results.length).toBe(1);
|
|
});
|
|
|
|
test("should handle no collections", async () => {
|
|
const team = await buildTeam();
|
|
const { results } = await SearchHelper.searchForTeam(team, "test");
|
|
expect(results.length).toBe(0);
|
|
});
|
|
|
|
test("should handle backslashes in search term", async () => {
|
|
const team = await buildTeam();
|
|
const { results } = await SearchHelper.searchForTeam(team, "\\\\");
|
|
expect(results.length).toBe(0);
|
|
});
|
|
|
|
test("should return the total count of search results", async () => {
|
|
const team = await buildTeam();
|
|
const collection = await buildCollection({
|
|
teamId: team.id,
|
|
});
|
|
await buildDocument({
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
title: "test number 1",
|
|
});
|
|
await buildDocument({
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
title: "test number 2",
|
|
});
|
|
const { totalCount } = await SearchHelper.searchForTeam(team, "test");
|
|
expect(totalCount).toBe(2);
|
|
});
|
|
|
|
test("should return the document when searched with their previous titles", async () => {
|
|
const team = await buildTeam();
|
|
const collection = await buildCollection({
|
|
teamId: team.id,
|
|
});
|
|
const document = await buildDocument({
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
title: "test number 1",
|
|
});
|
|
document.title = "change";
|
|
await document.save();
|
|
const { totalCount } = await SearchHelper.searchForTeam(
|
|
team,
|
|
"test number"
|
|
);
|
|
expect(totalCount).toBe(1);
|
|
});
|
|
|
|
test("should not return the document when searched with neither the titles nor the previous titles", async () => {
|
|
const team = await buildTeam();
|
|
const collection = await buildCollection({
|
|
teamId: team.id,
|
|
});
|
|
const document = await buildDocument({
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
title: "test number 1",
|
|
});
|
|
document.title = "change";
|
|
await document.save();
|
|
const { totalCount } = await SearchHelper.searchForTeam(
|
|
team,
|
|
"title doesn't exist"
|
|
);
|
|
expect(totalCount).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("#searchForUser", () => {
|
|
test("should return search results from collections", async () => {
|
|
const team = await buildTeam();
|
|
const user = await buildUser({ teamId: team.id });
|
|
const collection = await buildCollection({
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
});
|
|
const document = await buildDocument({
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
title: "test",
|
|
});
|
|
await buildDocument({
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
deletedAt: new Date(),
|
|
title: "test",
|
|
});
|
|
const { results } = await SearchHelper.searchForUser(user, "test");
|
|
expect(results.length).toBe(1);
|
|
expect(results[0].ranking).toBeTruthy();
|
|
expect(results[0].document?.id).toBe(document.id);
|
|
});
|
|
|
|
test("should handle no collections", async () => {
|
|
const team = await buildTeam();
|
|
const user = await buildUser({ teamId: team.id });
|
|
const { results } = await SearchHelper.searchForUser(user, "test");
|
|
expect(results.length).toBe(0);
|
|
});
|
|
|
|
test("should search only drafts created by user", async () => {
|
|
const user = await buildUser();
|
|
await buildDraftDocument({
|
|
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",
|
|
archivedAt: new Date(),
|
|
});
|
|
const { results } = await SearchHelper.searchForUser(user, "test", {
|
|
statusFilter: [StatusFilter.Draft],
|
|
});
|
|
expect(results.length).toBe(1);
|
|
});
|
|
|
|
test("should not include drafts with user read permission", async () => {
|
|
const user = await buildUser();
|
|
await buildDraftDocument({
|
|
title: "test",
|
|
});
|
|
const draft = await buildDraftDocument({
|
|
teamId: user.teamId,
|
|
userId: user.id,
|
|
createdById: user.id,
|
|
title: "test",
|
|
});
|
|
await UserMembership.create({
|
|
createdById: user.id,
|
|
documentId: draft.id,
|
|
userId: user.id,
|
|
permission: DocumentPermission.Read,
|
|
});
|
|
|
|
const { results } = await SearchHelper.searchForUser(user, "test", {
|
|
statusFilter: [StatusFilter.Published, StatusFilter.Archived],
|
|
});
|
|
expect(results.length).toBe(0);
|
|
});
|
|
|
|
test("should search only published 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",
|
|
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 return results from drafts and published", 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.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 () => {
|
|
const team = await buildTeam();
|
|
const user = await buildUser({ teamId: team.id });
|
|
const collection = await buildCollection({
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
});
|
|
await buildDocument({
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
title: "test number 1",
|
|
});
|
|
await buildDocument({
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
title: "test number 2",
|
|
});
|
|
const { totalCount } = await SearchHelper.searchForUser(user, "test");
|
|
expect(totalCount).toBe(2);
|
|
});
|
|
|
|
test("should return the document when searched with their previous titles", async () => {
|
|
const team = await buildTeam();
|
|
const user = await buildUser({ teamId: team.id });
|
|
const collection = await buildCollection({
|
|
teamId: team.id,
|
|
userId: user.id,
|
|
});
|
|
const document = await buildDocument({
|
|
teamId: team.id,
|
|
userId: user.id,
|
|
collectionId: collection.id,
|
|
title: "test number 1",
|
|
});
|
|
document.title = "change";
|
|
await document.save();
|
|
const { totalCount } = await SearchHelper.searchForUser(
|
|
user,
|
|
"test number"
|
|
);
|
|
expect(totalCount).toBe(1);
|
|
});
|
|
|
|
test("should not return the document when searched with neither the titles nor the previous titles", async () => {
|
|
const team = await buildTeam();
|
|
const user = await buildUser({ teamId: team.id });
|
|
const collection = await buildCollection({
|
|
teamId: team.id,
|
|
userId: user.id,
|
|
});
|
|
const document = await buildDocument({
|
|
teamId: team.id,
|
|
userId: user.id,
|
|
collectionId: collection.id,
|
|
title: "test number 1",
|
|
});
|
|
document.title = "change";
|
|
await document.save();
|
|
const { totalCount } = await SearchHelper.searchForUser(
|
|
user,
|
|
"title doesn't exist"
|
|
);
|
|
expect(totalCount).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("#searchTitlesForUser", () => {
|
|
test("should return search results from collections", async () => {
|
|
const team = await buildTeam();
|
|
const user = await buildUser({ teamId: team.id });
|
|
const collection = await buildCollection({
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
});
|
|
const document = await buildDocument({
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
title: "test",
|
|
});
|
|
const documents = await SearchHelper.searchTitlesForUser(user, "test");
|
|
expect(documents.length).toBe(1);
|
|
expect(documents[0]?.id).toBe(document.id);
|
|
});
|
|
|
|
test("should filter to specific collection", async () => {
|
|
const team = await buildTeam();
|
|
const user = await buildUser({ teamId: team.id });
|
|
const collection = await buildCollection({
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
});
|
|
const collection1 = await buildCollection({
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
});
|
|
const document = await buildDocument({
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
collectionId: collection.id,
|
|
title: "test",
|
|
});
|
|
await buildDraftDocument({
|
|
teamId: team.id,
|
|
userId: user.id,
|
|
title: "test",
|
|
});
|
|
await buildDocument({
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
collectionId: collection1.id,
|
|
title: "test",
|
|
});
|
|
const documents = await SearchHelper.searchTitlesForUser(user, "test", {
|
|
collectionId: collection.id,
|
|
});
|
|
expect(documents.length).toBe(1);
|
|
expect(documents[0]?.id).toBe(document.id);
|
|
});
|
|
|
|
test("should handle no collections", async () => {
|
|
const team = await buildTeam();
|
|
const user = await buildUser({ teamId: team.id });
|
|
const documents = await SearchHelper.searchTitlesForUser(user, "test");
|
|
expect(documents.length).toBe(0);
|
|
});
|
|
|
|
test("should search only drafts created by user", async () => {
|
|
const user = await buildUser();
|
|
await buildDraftDocument({
|
|
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",
|
|
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,
|
|
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.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 return results from drafts and published", 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.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);
|
|
});
|
|
});
|
|
|
|
describe("webSearchQuery", () => {
|
|
test("should correctly sanitize query", () => {
|
|
expect(SearchHelper.webSearchQuery("one/two")).toBe("one/two:*");
|
|
expect(SearchHelper.webSearchQuery("one\\two")).toBe("one\\\\two:*");
|
|
expect(SearchHelper.webSearchQuery("test''")).toBe("test");
|
|
});
|
|
test("should wildcard unquoted queries", () => {
|
|
expect(SearchHelper.webSearchQuery("test")).toBe("test:*");
|
|
expect(SearchHelper.webSearchQuery("'")).toBe("");
|
|
expect(SearchHelper.webSearchQuery("'quoted'")).toBe(`"quoted":*`);
|
|
});
|
|
test("should wildcard multi-word queries", () => {
|
|
expect(SearchHelper.webSearchQuery("this is a test")).toBe(
|
|
"this&is&a&test:*"
|
|
);
|
|
});
|
|
test("should now wildcard quoted queries", () => {
|
|
expect(SearchHelper.webSearchQuery(`"this is a test"`)).toBe(
|
|
`"this<->is<->a<->test"`
|
|
);
|
|
});
|
|
});
|
|
});
|