Allow drafts to be created without requiring a collection (#4175)

* feat(server): allow document to be created without collectionId

* fix(server): policies for a draft doc without collection

* fix(app): hide share button for drafts

* feat(server): permissions around publishing a draft

* fix(server): return drafts without collection

* fix(server): handle draft deletion

* fix(server): show drafts in deleted docs

* fix(server): allow drafts without collection to be restored

* feat(server): return drafts in search results

* fix: use buildDraftDocument for drafts

* fix: remove isDraftWithoutCollection

* fix: do not return drafts for team

* fix: put invariants

* fix: query clause

* fix: check only for undefined

* fix: restore includeDrafts clause as it was before
This commit is contained in:
Apoorv Mishra
2022-10-25 18:01:57 +05:30
committed by GitHub
parent 6b74d43380
commit a89d30c735
14 changed files with 557 additions and 84 deletions

View File

@@ -1,6 +1,7 @@
import Document from "@server/models/Document";
import {
buildDocument,
buildDraftDocument,
buildCollection,
buildTeam,
buildUser,
@@ -336,6 +337,74 @@ describe("#searchForUser", () => {
expect(results.length).toBe(0);
});
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 { results } = await Document.searchForUser(user, "test", {
includeDrafts: true,
});
expect(results.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 { results } = await Document.searchForUser(user, "test", {
includeDrafts: false,
});
expect(results.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 draft",
});
await buildDraftDocument({
teamId: user.teamId,
userId: user.id,
createdById: user.id,
title: "draft",
});
const { results } = await Document.searchForUser(user, "draft", {
includeDrafts: true,
});
expect(results.length).toBe(2);
});
test("should not include results from drafts", 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",
});
const { results } = await Document.searchForUser(user, "draft", {
includeDrafts: false,
});
expect(results.length).toBe(1);
});
test("should return the total count of search results", async () => {
const team = await buildTeam();
const user = await buildUser({
@@ -445,6 +514,17 @@ describe("#delete", () => {
expect(newDocument?.lastModifiedById).toBe(user.id);
expect(newDocument?.deletedAt).toBeTruthy();
});
it("should delete draft without collection", async () => {
const user = await buildUser();
const document = await buildDraftDocument();
await document.delete(user.id);
const deletedDocument = await Document.findByPk(document.id, {
paranoid: false,
});
expect(deletedDocument?.lastModifiedById).toBe(user.id);
expect(deletedDocument?.deletedAt).toBeTruthy();
});
});
describe("#save", () => {

View File

@@ -404,7 +404,7 @@ class Document extends ParanoidModel {
teamId: string;
@BelongsTo(() => Collection, "collectionId")
collection: Collection;
collection: Collection | null | undefined;
@ForeignKey(() => Collection)
@Column(DataType.UUID)
@@ -620,14 +620,6 @@ class Document extends ParanoidModel {
collectionIds = await user.collectionIds();
}
// If the user has access to no collections then shortcircuit the rest of this
if (!collectionIds.length) {
return {
results: [],
totalCount: 0,
};
}
let dateFilter;
if (options.dateFilter) {
@@ -636,9 +628,16 @@ class Document extends ParanoidModel {
// Build the SQL query to get documentIds, ranking, and search term context
const whereClause = `
"searchVector" @@ to_tsquery('english', :query) AND
"searchVector" @@ to_tsquery('english', :query) AND
"teamId" = :teamId AND
"collectionId" IN(:collectionIds) AND
${
collectionIds.length
? `(
"collectionId" IN(:collectionIds) OR
("collectionId" IS NULL AND "createdById" = :userId)
) AND`
: '"collectionId" IS NULL AND "createdById" = :userId AND'
}
${
options.dateFilter ? '"updatedAt" > now() - interval :dateFilter AND' : ""
}
@@ -962,7 +961,7 @@ class Document extends ParanoidModel {
// Delete a document, archived or otherwise.
delete = (userId: string) => {
return this.sequelize.transaction(async (transaction: Transaction) => {
if (!this.archivedAt && !this.template) {
if (!this.archivedAt && !this.template && this.collectionId) {
// delete any children and remove from the document structure
const collection = await Collection.findByPk(this.collectionId, {
transaction,