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:
@@ -237,7 +237,8 @@ function DocumentHeader({
|
|||||||
{!isEditing &&
|
{!isEditing &&
|
||||||
!isDeleted &&
|
!isDeleted &&
|
||||||
!isRevision &&
|
!isRevision &&
|
||||||
(!isMobile || !isTemplate) && (
|
(!isMobile || !isTemplate) &&
|
||||||
|
document.collectionId && (
|
||||||
<Action>
|
<Action>
|
||||||
<ShareButton document={document} />
|
<ShareButton document={document} />
|
||||||
</Action>
|
</Action>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ type Props = {
|
|||||||
type Result = {
|
type Result = {
|
||||||
document: Document;
|
document: Document;
|
||||||
share?: Share;
|
share?: Share;
|
||||||
collection: Collection;
|
collection?: Collection | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function loadDocument({
|
export default async function loadDocument({
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ describe("documentMover", () => {
|
|||||||
);
|
);
|
||||||
expect(response.collections.length).toEqual(1);
|
expect(response.collections.length).toEqual(1);
|
||||||
expect(response.documents.length).toEqual(1);
|
expect(response.documents.length).toEqual(1);
|
||||||
expect(response.documents[0].collection.id).toEqual(collection.id);
|
expect(response.documents[0].collection?.id).toEqual(collection.id);
|
||||||
expect(response.documents[0].updatedBy.id).toEqual(user.id);
|
expect(response.documents[0].updatedBy.id).toEqual(user.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -112,9 +112,9 @@ describe("documentMover", () => {
|
|||||||
expect(response.collections.length).toEqual(2);
|
expect(response.collections.length).toEqual(2);
|
||||||
expect(response.documents.length).toEqual(2);
|
expect(response.documents.length).toEqual(2);
|
||||||
|
|
||||||
expect(response.documents[0].collection.id).toEqual(newCollection.id);
|
expect(response.documents[0].collection?.id).toEqual(newCollection.id);
|
||||||
expect(response.documents[0].updatedBy.id).toEqual(user.id);
|
expect(response.documents[0].updatedBy.id).toEqual(user.id);
|
||||||
expect(response.documents[1].collection.id).toEqual(newCollection.id);
|
expect(response.documents[1].collection?.id).toEqual(newCollection.id);
|
||||||
expect(response.documents[1].updatedBy.id).toEqual(user.id);
|
expect(response.documents[1].updatedBy.id).toEqual(user.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -151,7 +151,7 @@ describe("documentMover", () => {
|
|||||||
expect(response.collections.length).toEqual(2);
|
expect(response.collections.length).toEqual(2);
|
||||||
expect(response.documents.length).toEqual(1);
|
expect(response.documents.length).toEqual(1);
|
||||||
|
|
||||||
expect(response.documents[0].collection.id).toEqual(newCollection.id);
|
expect(response.documents[0].collection?.id).toEqual(newCollection.id);
|
||||||
expect(response.documents[0].updatedBy.id).toEqual(user.id);
|
expect(response.documents[0].updatedBy.id).toEqual(user.id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ type Props = {
|
|||||||
append?: boolean;
|
append?: boolean;
|
||||||
/** Whether the document should be published to the collection */
|
/** Whether the document should be published to the collection */
|
||||||
publish?: boolean;
|
publish?: boolean;
|
||||||
|
/** The ID of the collection to publish the document to */
|
||||||
|
collectionId?: string;
|
||||||
/** The IP address of the user creating the document */
|
/** The IP address of the user creating the document */
|
||||||
ip: string;
|
ip: string;
|
||||||
/** The database transaction to run within */
|
/** The database transaction to run within */
|
||||||
@@ -44,6 +46,7 @@ export default async function documentUpdater({
|
|||||||
fullWidth,
|
fullWidth,
|
||||||
append,
|
append,
|
||||||
publish,
|
publish,
|
||||||
|
collectionId,
|
||||||
transaction,
|
transaction,
|
||||||
ip,
|
ip,
|
||||||
}: Props): Promise<Document> {
|
}: Props): Promise<Document> {
|
||||||
@@ -74,7 +77,9 @@ export default async function documentUpdater({
|
|||||||
const changed = document.changed();
|
const changed = document.changed();
|
||||||
|
|
||||||
if (publish) {
|
if (publish) {
|
||||||
document.lastModifiedById = user.id;
|
if (!document.collectionId) {
|
||||||
|
document.collectionId = collectionId as string;
|
||||||
|
}
|
||||||
await document.publish(user.id, { transaction });
|
await document.publish(user.id, { transaction });
|
||||||
|
|
||||||
await Event.create(
|
await Event.create(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import Document from "@server/models/Document";
|
import Document from "@server/models/Document";
|
||||||
import {
|
import {
|
||||||
buildDocument,
|
buildDocument,
|
||||||
|
buildDraftDocument,
|
||||||
buildCollection,
|
buildCollection,
|
||||||
buildTeam,
|
buildTeam,
|
||||||
buildUser,
|
buildUser,
|
||||||
@@ -336,6 +337,74 @@ describe("#searchForUser", () => {
|
|||||||
expect(results.length).toBe(0);
|
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 () => {
|
test("should return the total count of search results", async () => {
|
||||||
const team = await buildTeam();
|
const team = await buildTeam();
|
||||||
const user = await buildUser({
|
const user = await buildUser({
|
||||||
@@ -445,6 +514,17 @@ describe("#delete", () => {
|
|||||||
expect(newDocument?.lastModifiedById).toBe(user.id);
|
expect(newDocument?.lastModifiedById).toBe(user.id);
|
||||||
expect(newDocument?.deletedAt).toBeTruthy();
|
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", () => {
|
describe("#save", () => {
|
||||||
|
|||||||
@@ -404,7 +404,7 @@ class Document extends ParanoidModel {
|
|||||||
teamId: string;
|
teamId: string;
|
||||||
|
|
||||||
@BelongsTo(() => Collection, "collectionId")
|
@BelongsTo(() => Collection, "collectionId")
|
||||||
collection: Collection;
|
collection: Collection | null | undefined;
|
||||||
|
|
||||||
@ForeignKey(() => Collection)
|
@ForeignKey(() => Collection)
|
||||||
@Column(DataType.UUID)
|
@Column(DataType.UUID)
|
||||||
@@ -620,14 +620,6 @@ class Document extends ParanoidModel {
|
|||||||
collectionIds = await user.collectionIds();
|
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;
|
let dateFilter;
|
||||||
|
|
||||||
if (options.dateFilter) {
|
if (options.dateFilter) {
|
||||||
@@ -636,9 +628,16 @@ class Document extends ParanoidModel {
|
|||||||
|
|
||||||
// Build the SQL query to get documentIds, ranking, and search term context
|
// Build the SQL query to get documentIds, ranking, and search term context
|
||||||
const whereClause = `
|
const whereClause = `
|
||||||
"searchVector" @@ to_tsquery('english', :query) AND
|
"searchVector" @@ to_tsquery('english', :query) AND
|
||||||
"teamId" = :teamId 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' : ""
|
options.dateFilter ? '"updatedAt" > now() - interval :dateFilter AND' : ""
|
||||||
}
|
}
|
||||||
@@ -962,7 +961,7 @@ class Document extends ParanoidModel {
|
|||||||
// Delete a document, archived or otherwise.
|
// Delete a document, archived or otherwise.
|
||||||
delete = (userId: string) => {
|
delete = (userId: string) => {
|
||||||
return this.sequelize.transaction(async (transaction: Transaction) => {
|
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
|
// delete any children and remove from the document structure
|
||||||
const collection = await Collection.findByPk(this.collectionId, {
|
const collection = await Collection.findByPk(this.collectionId, {
|
||||||
transaction,
|
transaction,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
buildUser,
|
buildUser,
|
||||||
buildTeam,
|
buildTeam,
|
||||||
buildDocument,
|
buildDocument,
|
||||||
|
buildDraftDocument,
|
||||||
buildCollection,
|
buildCollection,
|
||||||
} from "@server/test/factories";
|
} from "@server/test/factories";
|
||||||
import { setupTestDatabase } from "@server/test/support";
|
import { setupTestDatabase } from "@server/test/support";
|
||||||
@@ -112,3 +113,35 @@ describe("private collection", () => {
|
|||||||
expect(abilities.move).toEqual(false);
|
expect(abilities.move).toEqual(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("no collection", () => {
|
||||||
|
it("should grant same permissions as that on a draft document except the share permission", async () => {
|
||||||
|
const team = await buildTeam();
|
||||||
|
const user = await buildUser({
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
const document = await buildDraftDocument({
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
const abilities = serialize(user, document);
|
||||||
|
expect(abilities.archive).toEqual(false);
|
||||||
|
expect(abilities.createChildDocument).toEqual(false);
|
||||||
|
expect(abilities.delete).toEqual(true);
|
||||||
|
expect(abilities.download).toEqual(true);
|
||||||
|
expect(abilities.move).toEqual(false);
|
||||||
|
expect(abilities.permanentDelete).toEqual(false);
|
||||||
|
expect(abilities.pin).toEqual(false);
|
||||||
|
expect(abilities.pinToHome).toEqual(false);
|
||||||
|
expect(abilities.read).toEqual(true);
|
||||||
|
expect(abilities.restore).toEqual(false);
|
||||||
|
expect(abilities.share).toEqual(false);
|
||||||
|
expect(abilities.star).toEqual(true);
|
||||||
|
expect(abilities.subscribe).toEqual(false);
|
||||||
|
expect(abilities.unarchive).toEqual(false);
|
||||||
|
expect(abilities.unpin).toEqual(false);
|
||||||
|
expect(abilities.unpublish).toEqual(false);
|
||||||
|
expect(abilities.unstar).toEqual(true);
|
||||||
|
expect(abilities.unsubscribe).toEqual(false);
|
||||||
|
expect(abilities.update).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -35,13 +35,17 @@ allow(User, "star", Document, (user, document) => {
|
|||||||
if (document.template) {
|
if (document.template) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
invariant(
|
|
||||||
document.collection,
|
if (document.collectionId) {
|
||||||
"collection is missing, did you forget to include in the query scope?"
|
invariant(
|
||||||
);
|
document.collection,
|
||||||
if (cannot(user, "read", document.collection)) {
|
"collection is missing, did you forget to include in the query scope?"
|
||||||
return false;
|
);
|
||||||
|
if (cannot(user, "read", document.collection)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return user.teamId === document.teamId;
|
return user.teamId === document.teamId;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -52,13 +56,17 @@ allow(User, "unstar", Document, (user, document) => {
|
|||||||
if (document.template) {
|
if (document.template) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
invariant(
|
|
||||||
document.collection,
|
if (document.collectionId) {
|
||||||
"collection is missing, did you forget to include in the query scope?"
|
invariant(
|
||||||
);
|
document.collection,
|
||||||
if (cannot(user, "read", document.collection)) {
|
"collection is missing, did you forget to include in the query scope?"
|
||||||
return false;
|
);
|
||||||
|
if (cannot(user, "read", document.collection)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return user.teamId === document.teamId;
|
return user.teamId === document.teamId;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -95,8 +103,15 @@ allow(User, "update", Document, (user, document) => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cannot(user, "update", document.collection)) {
|
if (document.collectionId) {
|
||||||
return false;
|
invariant(
|
||||||
|
document.collection,
|
||||||
|
"collection is missing, did you forget to include in the query scope?"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (cannot(user, "update", document.collection)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return user.teamId === document.teamId;
|
return user.teamId === document.teamId;
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ type Action = {
|
|||||||
|
|
||||||
function present(
|
function present(
|
||||||
document: Document,
|
document: Document,
|
||||||
collection: Collection,
|
|
||||||
team: Team,
|
team: Team,
|
||||||
|
collection?: Collection | null,
|
||||||
context?: string,
|
context?: string,
|
||||||
actions?: Action[]
|
actions?: Action[]
|
||||||
) {
|
) {
|
||||||
@@ -22,10 +22,10 @@ function present(
|
|||||||
: document.getSummary();
|
: document.getSummary();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
color: collection.color,
|
color: collection?.color,
|
||||||
title: document.title,
|
title: document.title,
|
||||||
title_link: `${team.url}${document.url}`,
|
title_link: `${team.url}${document.url}`,
|
||||||
footer: collection.name,
|
footer: collection?.name,
|
||||||
callback_id: document.id,
|
callback_id: document.id,
|
||||||
text,
|
text,
|
||||||
ts: document.getTimestamp(),
|
ts: document.getTimestamp(),
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export default class SlackProcessor extends BaseProcessor {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
text,
|
text,
|
||||||
attachments: [
|
attachments: [
|
||||||
presentSlackAttachment(document, document.collection, team),
|
presentSlackAttachment(document, team, document.collection),
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ import {
|
|||||||
buildCollection,
|
buildCollection,
|
||||||
buildUser,
|
buildUser,
|
||||||
buildDocument,
|
buildDocument,
|
||||||
|
buildDraftDocument,
|
||||||
buildViewer,
|
buildViewer,
|
||||||
|
buildTeam,
|
||||||
} from "@server/test/factories";
|
} from "@server/test/factories";
|
||||||
import { seed, getTestServer } from "@server/test/support";
|
import { seed, getTestServer } from "@server/test/support";
|
||||||
|
|
||||||
@@ -865,6 +867,30 @@ describe("#documents.drafts", () => {
|
|||||||
expect(body.data.length).toEqual(1);
|
expect(body.data.length).toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should return drafts, including ones without collectionIds", async () => {
|
||||||
|
const drafts = [];
|
||||||
|
const { user, document } = await seed();
|
||||||
|
document.publishedAt = null;
|
||||||
|
await document.save();
|
||||||
|
drafts.push(document);
|
||||||
|
const draftDocument = await buildDraftDocument({
|
||||||
|
title: "draft title",
|
||||||
|
text: "draft text",
|
||||||
|
createdById: user.id,
|
||||||
|
teamId: user.teamId,
|
||||||
|
});
|
||||||
|
drafts.push(draftDocument);
|
||||||
|
|
||||||
|
const res = await server.post("/api/documents.drafts", {
|
||||||
|
body: {
|
||||||
|
token: user.getJwtToken(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const body = await res.json();
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(body.data.length).toEqual(drafts.length);
|
||||||
|
});
|
||||||
|
|
||||||
it("should not return documents in private collections not a member of", async () => {
|
it("should not return documents in private collections not a member of", async () => {
|
||||||
const { user, document, collection } = await seed();
|
const { user, document, collection } = await seed();
|
||||||
document.publishedAt = null;
|
document.publishedAt = null;
|
||||||
@@ -980,6 +1006,29 @@ describe("#documents.search", () => {
|
|||||||
expect(body.data[0].document.id).toEqual(share.documentId);
|
expect(body.data[0].document.id).toEqual(share.documentId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should not return drafts using shareId", async () => {
|
||||||
|
const { user, document } = await seed();
|
||||||
|
document.publishedAt = null;
|
||||||
|
await document.save();
|
||||||
|
const share = await buildShare({
|
||||||
|
documentId: document.id,
|
||||||
|
includeChildDocuments: true,
|
||||||
|
teamId: user.teamId,
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
const res = await server.post("/api/documents.search", {
|
||||||
|
body: {
|
||||||
|
token: user.getJwtToken(),
|
||||||
|
shareId: share.id,
|
||||||
|
includeDrafts: true,
|
||||||
|
query: "test",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const body = await res.json();
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(body.data.length).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
it("should not allow search if child documents are not included", async () => {
|
it("should not allow search if child documents are not included", async () => {
|
||||||
const findableDocument = await buildDocument({
|
const findableDocument = await buildDocument({
|
||||||
title: "search term",
|
title: "search term",
|
||||||
@@ -1140,7 +1189,7 @@ describe("#documents.search", () => {
|
|||||||
body: {
|
body: {
|
||||||
token: user.getJwtToken(),
|
token: user.getJwtToken(),
|
||||||
query: "search term",
|
query: "search term",
|
||||||
includeDrafts: "true",
|
includeDrafts: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
@@ -1149,6 +1198,27 @@ describe("#documents.search", () => {
|
|||||||
expect(body.data[0].document.id).toEqual(document.id);
|
expect(body.data[0].document.id).toEqual(document.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should return drafts without collection too", async () => {
|
||||||
|
const user = await buildUser();
|
||||||
|
await buildDraftDocument({
|
||||||
|
title: "some title",
|
||||||
|
text: "some text",
|
||||||
|
createdById: user.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: user.teamId,
|
||||||
|
});
|
||||||
|
const res = await server.post("/api/documents.search", {
|
||||||
|
body: {
|
||||||
|
token: user.getJwtToken(),
|
||||||
|
includeDrafts: true,
|
||||||
|
query: "text",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const body = await res.json();
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(body.data.length).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
it("should not return draft documents created by other users", async () => {
|
it("should not return draft documents created by other users", async () => {
|
||||||
const user = await buildUser();
|
const user = await buildUser();
|
||||||
await buildDocument({
|
await buildDocument({
|
||||||
@@ -1161,7 +1231,7 @@ describe("#documents.search", () => {
|
|||||||
body: {
|
body: {
|
||||||
token: user.getJwtToken(),
|
token: user.getJwtToken(),
|
||||||
query: "search term",
|
query: "search term",
|
||||||
includeDrafts: "true",
|
includeDrafts: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
@@ -1200,7 +1270,7 @@ describe("#documents.search", () => {
|
|||||||
body: {
|
body: {
|
||||||
token: user.getJwtToken(),
|
token: user.getJwtToken(),
|
||||||
query: "search term",
|
query: "search term",
|
||||||
includeArchived: "true",
|
includeArchived: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
@@ -1470,6 +1540,30 @@ describe("#documents.deleted", () => {
|
|||||||
expect(body.data.length).toEqual(1);
|
expect(body.data.length).toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should return deleted documents, including drafts without collection", async () => {
|
||||||
|
const { user } = await seed();
|
||||||
|
const document = await buildDocument({
|
||||||
|
userId: user.id,
|
||||||
|
teamId: user.teamId,
|
||||||
|
});
|
||||||
|
const draftDocument = await buildDraftDocument({
|
||||||
|
userId: user.id,
|
||||||
|
teamId: user.teamId,
|
||||||
|
});
|
||||||
|
await Promise.all([
|
||||||
|
document.delete(user.id),
|
||||||
|
draftDocument.delete(user.id),
|
||||||
|
]);
|
||||||
|
const res = await server.post("/api/documents.deleted", {
|
||||||
|
body: {
|
||||||
|
token: user.getJwtToken(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const body = await res.json();
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(body.data.length).toEqual(2);
|
||||||
|
});
|
||||||
|
|
||||||
it("should not return documents in private collections not a member of", async () => {
|
it("should not return documents in private collections not a member of", async () => {
|
||||||
const { user } = await seed();
|
const { user } = await seed();
|
||||||
const collection = await buildCollection({
|
const collection = await buildCollection({
|
||||||
@@ -1646,6 +1740,24 @@ describe("#documents.restore", () => {
|
|||||||
expect(body.data.deletedAt).toEqual(null);
|
expect(body.data.deletedAt).toEqual(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should allow restore of trashed drafts without collection", async () => {
|
||||||
|
const { user } = await seed();
|
||||||
|
const document = await buildDraftDocument({
|
||||||
|
userId: user.id,
|
||||||
|
teamId: user.teamId,
|
||||||
|
});
|
||||||
|
await document.delete(user.id);
|
||||||
|
const res = await server.post("/api/documents.restore", {
|
||||||
|
body: {
|
||||||
|
token: user.getJwtToken(),
|
||||||
|
id: document.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const body = await res.json();
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(body.data.deletedAt).toEqual(null);
|
||||||
|
});
|
||||||
|
|
||||||
it("should allow restore of trashed documents with collectionId", async () => {
|
it("should allow restore of trashed documents with collectionId", async () => {
|
||||||
const { user, document } = await seed();
|
const { user, document } = await seed();
|
||||||
const collection = await buildCollection({
|
const collection = await buildCollection({
|
||||||
@@ -1864,6 +1976,81 @@ describe("#documents.create", () => {
|
|||||||
expect(body.policies[0].abilities.update).toEqual(true);
|
expect(body.policies[0].abilities.update).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should create a draft document not belonging to any collection", async () => {
|
||||||
|
const team = await buildTeam();
|
||||||
|
const user = await buildUser({ teamId: team.id });
|
||||||
|
const res = await server.post("/api/documents.create", {
|
||||||
|
body: {
|
||||||
|
token: user.getJwtToken(),
|
||||||
|
title: "draft document",
|
||||||
|
text: "draft document without collection",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const body = await res.json();
|
||||||
|
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(body.data.title).toBe("draft document");
|
||||||
|
expect(body.data.text).toBe("draft document without collection");
|
||||||
|
expect(body.data.collectionId).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not allow creating a template without a collection", async () => {
|
||||||
|
const team = await buildTeam();
|
||||||
|
const user = await buildUser({ teamId: team.id });
|
||||||
|
const res = await server.post("/api/documents.create", {
|
||||||
|
body: {
|
||||||
|
template: true,
|
||||||
|
token: user.getJwtToken(),
|
||||||
|
title: "template",
|
||||||
|
text: "template without collection",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const body = await res.json();
|
||||||
|
|
||||||
|
expect(body.message).toBe(
|
||||||
|
"collectionId is required to create a nested doc or a template"
|
||||||
|
);
|
||||||
|
expect(res.status).toEqual(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not allow publishing without specifying the collection", async () => {
|
||||||
|
const team = await buildTeam();
|
||||||
|
const user = await buildUser({ teamId: team.id });
|
||||||
|
const res = await server.post("/api/documents.create", {
|
||||||
|
body: {
|
||||||
|
token: user.getJwtToken(),
|
||||||
|
title: "title",
|
||||||
|
text: "text",
|
||||||
|
publish: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const body = await res.json();
|
||||||
|
|
||||||
|
expect(body.message).toBe(
|
||||||
|
"collectionId is required to publish a draft without collection"
|
||||||
|
);
|
||||||
|
expect(res.status).toEqual(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not allow creating a nested doc without a collection", async () => {
|
||||||
|
const team = await buildTeam();
|
||||||
|
const user = await buildUser({ teamId: team.id });
|
||||||
|
const res = await server.post("/api/documents.create", {
|
||||||
|
body: {
|
||||||
|
token: user.getJwtToken(),
|
||||||
|
parentDocumentId: "d7a4eb73-fac1-4028-af45-d7e34d54db8e",
|
||||||
|
title: "nested doc",
|
||||||
|
text: "nested doc without collection",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const body = await res.json();
|
||||||
|
|
||||||
|
expect(body.message).toBe(
|
||||||
|
"collectionId is required to create a nested doc or a template"
|
||||||
|
);
|
||||||
|
expect(res.status).toEqual(400);
|
||||||
|
});
|
||||||
|
|
||||||
it("should not allow very long titles", async () => {
|
it("should not allow very long titles", async () => {
|
||||||
const { user, collection } = await seed();
|
const { user, collection } = await seed();
|
||||||
const res = await server.post("/api/documents.create", {
|
const res = await server.post("/api/documents.create", {
|
||||||
@@ -1969,6 +2156,73 @@ describe("#documents.update", () => {
|
|||||||
expect(events.length).toEqual(1);
|
expect(events.length).toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should not allow publishing a draft without specifying the collection", async () => {
|
||||||
|
const { user, team } = await seed();
|
||||||
|
const document = await buildDraftDocument({
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
const res = await server.post("/api/documents.update", {
|
||||||
|
body: {
|
||||||
|
token: user.getJwtToken(),
|
||||||
|
id: document.id,
|
||||||
|
title: "Updated title",
|
||||||
|
text: "Updated text",
|
||||||
|
lastRevision: document.revisionCount,
|
||||||
|
publish: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const body = await res.json();
|
||||||
|
expect(res.status).toEqual(400);
|
||||||
|
expect(body.message).toBe(
|
||||||
|
"collectionId is required to publish a draft without collection"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should successfully publish a draft", async () => {
|
||||||
|
const { user, team, collection } = await seed();
|
||||||
|
const document = await buildDraftDocument({
|
||||||
|
title: "title",
|
||||||
|
text: "text",
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await server.post("/api/documents.update", {
|
||||||
|
body: {
|
||||||
|
token: user.getJwtToken(),
|
||||||
|
id: document.id,
|
||||||
|
title: "Updated title",
|
||||||
|
text: "Updated text",
|
||||||
|
lastRevision: document.revisionCount,
|
||||||
|
collectionId: collection.id,
|
||||||
|
publish: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const body = await res.json();
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(body.data.collectionId).toBe(collection.id);
|
||||||
|
expect(body.data.title).toBe("Updated title");
|
||||||
|
expect(body.data.text).toBe("Updated text");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not allow publishing by another collection's user", async () => {
|
||||||
|
const { user, document, collection } = await seed();
|
||||||
|
const anotherTeam = await buildTeam();
|
||||||
|
user.teamId = anotherTeam.id;
|
||||||
|
await user.save();
|
||||||
|
const res = await server.post("/api/documents.update", {
|
||||||
|
body: {
|
||||||
|
token: user.getJwtToken(),
|
||||||
|
id: document.id,
|
||||||
|
title: "Updated title",
|
||||||
|
text: "Updated text",
|
||||||
|
lastRevision: document.revisionCount,
|
||||||
|
collectionId: collection.id,
|
||||||
|
publish: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(res.status).toEqual(403);
|
||||||
|
});
|
||||||
|
|
||||||
it("should not add template to collection structure when publishing", async () => {
|
it("should not add template to collection structure when publishing", async () => {
|
||||||
const user = await buildUser();
|
const user = await buildUser();
|
||||||
const collection = await buildCollection({
|
const collection = await buildCollection({
|
||||||
@@ -2282,6 +2536,31 @@ describe("#documents.delete", () => {
|
|||||||
expect(body.success).toEqual(true);
|
expect(body.success).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should delete a draft without collection", async () => {
|
||||||
|
const { user } = await seed();
|
||||||
|
const document = await buildDraftDocument({
|
||||||
|
teamId: user.teamId,
|
||||||
|
deletedAt: null,
|
||||||
|
});
|
||||||
|
const res = await server.post("/api/documents.delete", {
|
||||||
|
body: {
|
||||||
|
token: user.getJwtToken(),
|
||||||
|
id: document.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const body = await res.json();
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(body.success).toEqual(true);
|
||||||
|
|
||||||
|
const deletedDoc = await Document.findByPk(document.id, {
|
||||||
|
paranoid: false,
|
||||||
|
});
|
||||||
|
expect(deletedDoc).toBeDefined();
|
||||||
|
expect(deletedDoc).not.toBeNull();
|
||||||
|
expect(deletedDoc?.deletedAt).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
it("should allow permanently deleting a document", async () => {
|
it("should allow permanently deleting a document", async () => {
|
||||||
const user = await buildUser();
|
const user = await buildUser();
|
||||||
const document = await buildDocument({
|
const document = await buildDocument({
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import {
|
|||||||
assertPresent,
|
assertPresent,
|
||||||
assertPositiveInteger,
|
assertPositiveInteger,
|
||||||
assertNotEmpty,
|
assertNotEmpty,
|
||||||
|
assertBoolean,
|
||||||
} from "@server/validation";
|
} from "@server/validation";
|
||||||
import env from "../../env";
|
import env from "../../env";
|
||||||
import pagination from "./middlewares/pagination";
|
import pagination from "./middlewares/pagination";
|
||||||
@@ -244,7 +245,9 @@ router.post(
|
|||||||
]).findAll({
|
]).findAll({
|
||||||
where: {
|
where: {
|
||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
collectionId: collectionIds,
|
collectionId: {
|
||||||
|
[Op.or]: [{ [Op.in]: collectionIds }, { [Op.is]: null }],
|
||||||
|
},
|
||||||
deletedAt: {
|
deletedAt: {
|
||||||
[Op.ne]: null,
|
[Op.ne]: null,
|
||||||
},
|
},
|
||||||
@@ -353,9 +356,11 @@ router.post("documents.drafts", auth(), pagination(), async (ctx) => {
|
|||||||
const collectionIds = collectionId
|
const collectionIds = collectionId
|
||||||
? [collectionId]
|
? [collectionId]
|
||||||
: await user.collectionIds();
|
: await user.collectionIds();
|
||||||
const where: WhereOptions<Document> = {
|
const where: WhereOptions = {
|
||||||
createdById: user.id,
|
createdById: user.id,
|
||||||
collectionId: collectionIds,
|
collectionId: {
|
||||||
|
[Op.or]: [{ [Op.in]: collectionIds }, { [Op.is]: null }],
|
||||||
|
},
|
||||||
publishedAt: {
|
publishedAt: {
|
||||||
[Op.is]: null,
|
[Op.is]: null,
|
||||||
},
|
},
|
||||||
@@ -425,7 +430,7 @@ router.post(
|
|||||||
document: serializedDocument,
|
document: serializedDocument,
|
||||||
sharedTree:
|
sharedTree:
|
||||||
share && share.includeChildDocuments
|
share && share.includeChildDocuments
|
||||||
? collection.getDocumentTree(share.documentId)
|
? collection?.getDocumentTree(share.documentId)
|
||||||
: undefined,
|
: undefined,
|
||||||
}
|
}
|
||||||
: serializedDocument;
|
: serializedDocument;
|
||||||
@@ -516,13 +521,15 @@ router.post("documents.restore", auth({ member: true }), async (ctx) => {
|
|||||||
// be caught as a 403 on the authorize call below. Otherwise we're checking here
|
// be caught as a 403 on the authorize call below. Otherwise we're checking here
|
||||||
// that the original collection still exists and advising to pass collectionId
|
// that the original collection still exists and advising to pass collectionId
|
||||||
// if not.
|
// if not.
|
||||||
if (!collectionId && !collection) {
|
if (document.collection && !collectionId && !collection) {
|
||||||
throw ValidationError(
|
throw ValidationError(
|
||||||
"Unable to restore to original collection, it may have been deleted"
|
"Unable to restore to original collection, it may have been deleted"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
authorize(user, "update", collection);
|
if (document.collection) {
|
||||||
|
authorize(user, "update", collection);
|
||||||
|
}
|
||||||
|
|
||||||
if (document.deletedAt) {
|
if (document.deletedAt) {
|
||||||
authorize(user, "restore", document);
|
authorize(user, "restore", document);
|
||||||
@@ -655,6 +662,14 @@ router.post(
|
|||||||
} = ctx.request.body;
|
} = ctx.request.body;
|
||||||
assertNotEmpty(query, "query is required");
|
assertNotEmpty(query, "query is required");
|
||||||
|
|
||||||
|
if (includeDrafts) {
|
||||||
|
assertBoolean(includeDrafts);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeArchived) {
|
||||||
|
assertBoolean(includeArchived);
|
||||||
|
}
|
||||||
|
|
||||||
const { offset, limit } = ctx.state.pagination;
|
const { offset, limit } = ctx.state.pagination;
|
||||||
const snippetMinWords = parseInt(
|
const snippetMinWords = parseInt(
|
||||||
ctx.request.body.snippetMinWords || 20,
|
ctx.request.body.snippetMinWords || 20,
|
||||||
@@ -687,8 +702,8 @@ router.post(
|
|||||||
invariant(team, "Share must belong to a team");
|
invariant(team, "Share must belong to a team");
|
||||||
|
|
||||||
response = await Document.searchForTeam(team, query, {
|
response = await Document.searchForTeam(team, query, {
|
||||||
includeArchived: includeArchived === "true",
|
includeArchived,
|
||||||
includeDrafts: includeDrafts === "true",
|
includeDrafts,
|
||||||
collectionId: document.collectionId,
|
collectionId: document.collectionId,
|
||||||
share,
|
share,
|
||||||
dateFilter,
|
dateFilter,
|
||||||
@@ -728,8 +743,8 @@ router.post(
|
|||||||
}
|
}
|
||||||
|
|
||||||
response = await Document.searchForUser(user, query, {
|
response = await Document.searchForUser(user, query, {
|
||||||
includeArchived: includeArchived === "true",
|
includeArchived,
|
||||||
includeDrafts: includeDrafts === "true",
|
includeDrafts,
|
||||||
collaboratorIds,
|
collaboratorIds,
|
||||||
collectionId,
|
collectionId,
|
||||||
dateFilter,
|
dateFilter,
|
||||||
@@ -827,6 +842,7 @@ router.post("documents.update", auth(), async (ctx) => {
|
|||||||
publish,
|
publish,
|
||||||
lastRevision,
|
lastRevision,
|
||||||
templateId,
|
templateId,
|
||||||
|
collectionId,
|
||||||
append,
|
append,
|
||||||
} = ctx.request.body;
|
} = ctx.request.body;
|
||||||
const editorVersion = ctx.headers["x-editor-version"] as string | undefined;
|
const editorVersion = ctx.headers["x-editor-version"] as string | undefined;
|
||||||
@@ -834,24 +850,39 @@ router.post("documents.update", auth(), async (ctx) => {
|
|||||||
if (append) {
|
if (append) {
|
||||||
assertPresent(text, "Text is required while appending");
|
assertPresent(text, "Text is required while appending");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (collectionId) {
|
||||||
|
assertUuid(collectionId, "collectionId must be an uuid");
|
||||||
|
}
|
||||||
|
|
||||||
const { user } = ctx.state;
|
const { user } = ctx.state;
|
||||||
|
|
||||||
let collection: Collection | null | undefined;
|
let collection: Collection | null | undefined;
|
||||||
|
|
||||||
const document = await sequelize.transaction(async (transaction) => {
|
const document = await Document.findByPk(id, {
|
||||||
const document = await Document.findByPk(id, {
|
userId: user.id,
|
||||||
userId: user.id,
|
includeState: true,
|
||||||
includeState: true,
|
});
|
||||||
transaction,
|
authorize(user, "update", document);
|
||||||
});
|
|
||||||
authorize(user, "update", document);
|
|
||||||
|
|
||||||
collection = document.collection;
|
if (publish) {
|
||||||
|
if (!document.collectionId) {
|
||||||
if (lastRevision && lastRevision !== document.revisionCount) {
|
assertPresent(
|
||||||
throw InvalidRequestError("Document has changed since last revision");
|
collectionId,
|
||||||
|
"collectionId is required to publish a draft without collection"
|
||||||
|
);
|
||||||
|
collection = await Collection.findByPk(collectionId);
|
||||||
|
} else {
|
||||||
|
collection = document.collection;
|
||||||
}
|
}
|
||||||
|
authorize(user, "publish", collection);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastRevision && lastRevision !== document.revisionCount) {
|
||||||
|
throw InvalidRequestError("Document has changed since last revision");
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedDocument = await sequelize.transaction(async (transaction) => {
|
||||||
return documentUpdater({
|
return documentUpdater({
|
||||||
document,
|
document,
|
||||||
user,
|
user,
|
||||||
@@ -859,6 +890,7 @@ router.post("documents.update", auth(), async (ctx) => {
|
|||||||
text,
|
text,
|
||||||
fullWidth,
|
fullWidth,
|
||||||
publish,
|
publish,
|
||||||
|
collectionId,
|
||||||
append,
|
append,
|
||||||
templateId,
|
templateId,
|
||||||
editorVersion,
|
editorVersion,
|
||||||
@@ -867,14 +899,12 @@ router.post("documents.update", auth(), async (ctx) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
invariant(collection, "collection not found");
|
updatedDocument.updatedBy = user;
|
||||||
|
updatedDocument.collection = collection;
|
||||||
document.updatedBy = user;
|
|
||||||
document.collection = collection;
|
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
data: await presentDocument(document),
|
data: await presentDocument(updatedDocument),
|
||||||
policies: presentPolicies(user, [document]),
|
policies: presentPolicies(user, [updatedDocument]),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1169,7 +1199,19 @@ router.post("documents.create", auth(), async (ctx) => {
|
|||||||
index,
|
index,
|
||||||
} = ctx.request.body;
|
} = ctx.request.body;
|
||||||
const editorVersion = ctx.headers["x-editor-version"] as string | undefined;
|
const editorVersion = ctx.headers["x-editor-version"] as string | undefined;
|
||||||
assertUuid(collectionId, "collectionId must be an uuid");
|
|
||||||
|
if (parentDocumentId || template || publish) {
|
||||||
|
assertPresent(
|
||||||
|
collectionId,
|
||||||
|
publish
|
||||||
|
? "collectionId is required to publish a draft without collection"
|
||||||
|
: "collectionId is required to create a nested doc or a template"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (collectionId) {
|
||||||
|
assertUuid(collectionId, "collectionId must be an uuid");
|
||||||
|
}
|
||||||
|
|
||||||
if (parentDocumentId) {
|
if (parentDocumentId) {
|
||||||
assertUuid(parentDocumentId, "parentDocumentId must be an uuid");
|
assertUuid(parentDocumentId, "parentDocumentId must be an uuid");
|
||||||
@@ -1180,15 +1222,19 @@ router.post("documents.create", auth(), async (ctx) => {
|
|||||||
}
|
}
|
||||||
const { user } = ctx.state;
|
const { user } = ctx.state;
|
||||||
|
|
||||||
const collection = await Collection.scope({
|
let collection;
|
||||||
method: ["withMembership", user.id],
|
|
||||||
}).findOne({
|
if (collectionId) {
|
||||||
where: {
|
collection = await Collection.scope({
|
||||||
id: collectionId,
|
method: ["withMembership", user.id],
|
||||||
teamId: user.teamId,
|
}).findOne({
|
||||||
},
|
where: {
|
||||||
});
|
id: collectionId,
|
||||||
authorize(user, "publish", collection);
|
teamId: user.teamId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
authorize(user, "publish", collection);
|
||||||
|
}
|
||||||
|
|
||||||
let parentDocument;
|
let parentDocument;
|
||||||
|
|
||||||
@@ -1196,7 +1242,7 @@ router.post("documents.create", auth(), async (ctx) => {
|
|||||||
parentDocument = await Document.findOne({
|
parentDocument = await Document.findOne({
|
||||||
where: {
|
where: {
|
||||||
id: parentDocumentId,
|
id: parentDocumentId,
|
||||||
collectionId: collection.id,
|
collectionId: collection?.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
authorize(user, "read", parentDocument, {
|
authorize(user, "read", parentDocument, {
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ router.post("hooks.unfurl", async (ctx) => {
|
|||||||
unfurls[link.url] = {
|
unfurls[link.url] = {
|
||||||
title: doc.title,
|
title: doc.title,
|
||||||
text: doc.getSummary(),
|
text: doc.getSummary(),
|
||||||
color: doc.collection.color,
|
color: doc.collection?.color,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,8 +131,8 @@ router.post("hooks.interactive", async (ctx) => {
|
|||||||
attachments: [
|
attachments: [
|
||||||
presentSlackAttachment(
|
presentSlackAttachment(
|
||||||
document,
|
document,
|
||||||
document.collection,
|
|
||||||
team,
|
team,
|
||||||
|
document.collection,
|
||||||
document.getSummary()
|
document.getSummary()
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -303,8 +303,8 @@ router.post("hooks.slack", async (ctx) => {
|
|||||||
attachments.push(
|
attachments.push(
|
||||||
presentSlackAttachment(
|
presentSlackAttachment(
|
||||||
result.document,
|
result.document,
|
||||||
result.document.collection,
|
|
||||||
team,
|
team,
|
||||||
|
result.document.collection,
|
||||||
queryIsInTitle ? undefined : result.context,
|
queryIsInTitle ? undefined : result.context,
|
||||||
env.SLACK_MESSAGE_ACTIONS
|
env.SLACK_MESSAGE_ACTIONS
|
||||||
? [
|
? [
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { isNull } from "lodash";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { CollectionPermission } from "@shared/types";
|
import { CollectionPermission } from "@shared/types";
|
||||||
import {
|
import {
|
||||||
@@ -323,8 +324,22 @@ export async function buildGroupUser(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function buildDocument(
|
export async function buildDraftDocument(
|
||||||
overrides: Partial<Document> & { userId?: string } = {}
|
overrides: Partial<Document> & { userId?: string } = {}
|
||||||
|
) {
|
||||||
|
return buildDocument({ ...overrides, collectionId: null });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function buildDocument(
|
||||||
|
// Omission first, addition later?
|
||||||
|
// This is actually a workaround to allow
|
||||||
|
// passing collectionId as null. Ideally, it
|
||||||
|
// should be updated in the Document model itself
|
||||||
|
// but that'd cascade and require further changes
|
||||||
|
// beyond the scope of what's required now
|
||||||
|
overrides: Omit<Partial<Document>, "collectionId"> & { userId?: string } & {
|
||||||
|
collectionId?: string | null;
|
||||||
|
} = {}
|
||||||
) {
|
) {
|
||||||
if (!overrides.teamId) {
|
if (!overrides.teamId) {
|
||||||
const team = await buildTeam();
|
const team = await buildTeam();
|
||||||
@@ -336,7 +351,7 @@ export async function buildDocument(
|
|||||||
overrides.userId = user.id;
|
overrides.userId = user.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!overrides.collectionId) {
|
if (overrides.collectionId === undefined) {
|
||||||
const collection = await buildCollection({
|
const collection = await buildCollection({
|
||||||
teamId: overrides.teamId,
|
teamId: overrides.teamId,
|
||||||
userId: overrides.userId,
|
userId: overrides.userId,
|
||||||
@@ -348,7 +363,7 @@ export async function buildDocument(
|
|||||||
return Document.create({
|
return Document.create({
|
||||||
title: `Document ${count}`,
|
title: `Document ${count}`,
|
||||||
text: "This is the text in an example document",
|
text: "This is the text in an example document",
|
||||||
publishedAt: new Date(),
|
publishedAt: isNull(overrides.collectionId) ? null : new Date(),
|
||||||
lastModifiedById: overrides.userId,
|
lastModifiedById: overrides.userId,
|
||||||
createdById: overrides.userId,
|
createdById: overrides.userId,
|
||||||
...overrides,
|
...overrides,
|
||||||
|
|||||||
Reference in New Issue
Block a user