Deleting a collection should detach associated drafts from it (#5082)

Co-authored-by: Tom Moor <tom.moor@gmail.com>
This commit is contained in:
Apoorv Mishra
2023-04-24 00:50:44 +05:30
committed by GitHub
parent 7250c0ed64
commit 86062f396d
39 changed files with 363 additions and 112 deletions

View File

@@ -39,7 +39,7 @@ function CollectionDeleteDialog({ collection, onSubmit }: Props) {
<>
<Text type="secondary">
<Trans
defaults="Are you sure about that? Deleting the <em>{{collectionName}}</em> collection is permanent and cannot be restored, however documents within will be moved to the trash."
defaults="Are you sure about that? Deleting the <em>{{collectionName}}</em> collection is permanent and cannot be restored, however all published documents within will be moved to the trash."
values={{
collectionName: collection.name,
}}

View File

@@ -62,7 +62,9 @@ const DocumentBreadcrumb: React.FC<Props> = ({
const { collections } = useStores();
const { t } = useTranslation();
const category = useCategory(document);
const collection = collections.get(document.collectionId);
const collection = document.collectionId
? collections.get(document.collectionId)
: undefined;
let collectionNode: MenuInternalLink | undefined;

View File

@@ -36,7 +36,9 @@ function DocumentCard(props: Props) {
const { collections } = useStores();
const theme = useTheme();
const { document, pin, canUpdatePin, isDraggable } = props;
const collection = collections.get(document.collectionId);
const collection = document.collectionId
? collections.get(document.collectionId)
: undefined;
const {
attributes,
listeners,

View File

@@ -57,7 +57,9 @@ const DocumentMeta: React.FC<Props> = ({
return null;
}
const collection = collections.get(document.collectionId);
const collection = document.collectionId
? collections.get(document.collectionId)
: undefined;
const lastUpdatedByCurrentUser = user.id === updatedBy.id;
const userName = updatedBy.name;
let content;

View File

@@ -152,7 +152,9 @@ function StarredLink({ star }: Props) {
return null;
}
const collection = collections.get(document.collectionId);
const collection = document.collectionId
? collections.get(document.collectionId)
: undefined;
const childDocuments = collection
? collection.getDocumentChildren(documentId)
: [];

View File

@@ -175,7 +175,7 @@ class WebsocketProvider extends React.Component<Props> {
id: document.collectionId,
});
if (!existing) {
if (!existing && document.collectionId) {
event.collectionIds.push({
id: document.collectionId,
});
@@ -298,10 +298,14 @@ class WebsocketProvider extends React.Component<Props> {
action((event: WebsocketEntityDeletedEvent) => {
const collectionId = event.modelId;
const deletedAt = new Date().toISOString();
const deletedDocuments = documents.inCollection(collectionId);
deletedDocuments.forEach((doc) => {
doc.deletedAt = deletedAt;
if (!doc.publishedAt) {
// draft is to be detached from collection, not deleted
doc.collectionId = null;
} else {
doc.deletedAt = deletedAt;
}
policies.remove(doc.id);
});
documents.removeCollectionDocuments(collectionId);

View File

@@ -8,7 +8,7 @@ import useToasts from "~/hooks/useToasts";
let importingLock = false;
export default function useImportDocument(
collectionId?: string,
collectionId?: string | null,
documentId?: string
): {
handleFiles: (files: File[]) => Promise<void>;

View File

@@ -9,7 +9,7 @@ import useStores from "./useStores";
* @param entity The model or model id
* @returns The policy for the model
*/
export default function usePolicy(entity: string | BaseModel | undefined) {
export default function usePolicy(entity?: string | BaseModel | null) {
const { policies } = useStores();
const triggered = React.useRef(false);
const entityId = entity

View File

@@ -123,7 +123,9 @@ function DocumentMenu({
[showToast, t, document]
);
const collection = collections.get(document.collectionId);
const collection = document.collectionId
? collections.get(document.collectionId)
: undefined;
const can = usePolicy(document);
const restoreItems = React.useMemo(
() => [

View File

@@ -19,7 +19,9 @@ function NewChildDocumentMenu({ document, label }: Props) {
});
const { collections } = useStores();
const { t } = useTranslation();
const collection = collections.get(document.collectionId);
const collection = document.collectionId
? collections.get(document.collectionId)
: undefined;
const collectionName = collection ? collection.name : t("collection");
return (

View File

@@ -46,7 +46,7 @@ export default class Document extends ParanoidModel {
@Field
@observable
collectionId: string;
collectionId?: string | null;
@Field
@observable
@@ -261,7 +261,7 @@ export default class Document extends ParanoidModel {
};
@action
pin = (collectionId?: string) =>
pin = (collectionId?: string | null) =>
this.store.rootStore.pins.create({
documentId: this.id,
...(collectionId ? { collectionId } : {}),

View File

@@ -23,7 +23,9 @@ function References({ document }: Props) {
}, [documents, document.id]);
const backlinks = documents.getBacklinkedDocuments(document.id);
const collection = collections.get(document.collectionId);
const collection = document.collectionId
? collections.get(document.collectionId)
: undefined;
const children = collection
? collection.getDocumentChildren(document.id)
: [];

View File

@@ -23,7 +23,9 @@ function DocumentDelete({ document, onSubmit }: Props) {
const [isArchiving, setArchiving] = React.useState(false);
const { showToast } = useToasts();
const canArchive = !document.isDraft && !document.isArchived;
const collection = collections.get(document.collectionId);
const collection = document.collectionId
? collections.get(document.collectionId)
: undefined;
const nestedDocumentsCount = collection
? collection.getDocumentChildren(document.id).length
: 0;

View File

@@ -645,7 +645,11 @@ export default class DocumentsStore extends BaseStore<Document> {
@action
removeCollectionDocuments(collectionId: string) {
const documents = this.inCollection(collectionId);
// drafts are to be detached from collection rather than deleted, hence excluded here
const documents = filter(
this.inCollection(collectionId),
(d) => !!d.publishedAt
);
const documentIds = documents.map((doc) => doc.id);
documentIds.forEach((id) => this.remove(id));
}
@@ -795,6 +799,8 @@ export default class DocumentsStore extends BaseStore<Document> {
find(this.orderedData, (doc) => url.endsWith(doc.urlId));
getCollectionForDocument(document: Document) {
return this.rootStore.collections.data.get(document.collectionId);
return document.collectionId
? this.rootStore.collections.get(document.collectionId)
: undefined;
}
}

View File

@@ -78,7 +78,9 @@ export default class SharesStore extends BaseStore<Share> {
return;
}
const collection = this.rootStore.collections.get(document.collectionId);
const collection = document.collectionId
? this.rootStore.collections.get(document.collectionId)
: undefined;
if (!collection) {
return;
}

View File

@@ -37,7 +37,7 @@ class UiStore {
activeDocumentId: string | undefined;
@observable
activeCollectionId: string | undefined;
activeCollectionId?: string | null;
@observable
observingUserId: string | undefined;

View File

@@ -76,7 +76,7 @@ export type ActionContext = {
isCommandBar: boolean;
isButton: boolean;
inStarredSection?: boolean;
activeCollectionId: string | undefined;
activeCollectionId?: string | null;
activeDocumentId: string | undefined;
currentUserId: string | undefined;
currentTeamId: string | undefined;

View File

@@ -101,7 +101,7 @@ export function updateDocumentPath(oldUrl: string, document: Document): string {
}
export function newDocumentPath(
collectionId?: string,
collectionId?: string | null,
params: {
parentDocumentId?: string;
templateId?: string;

View File

@@ -109,7 +109,7 @@ export default async function documentCreator({
);
if (publish) {
await document.publish(user.id, { transaction });
await document.publish(user.id, collectionId!, { transaction });
await Event.create(
{
name: "documents.publish",

View File

@@ -126,7 +126,9 @@ export default async function loadDocument({
if (canReadDocument) {
// Cannot use document.collection here as it does not include the
// documentStructure by default through the relationship.
collection = await Collection.findByPk(document.collectionId);
if (document.collectionId) {
collection = await Collection.findByPk(document.collectionId);
}
if (!collection) {
throw NotFoundError("Collection could not be found for document");
}
@@ -146,7 +148,9 @@ export default async function loadDocument({
}
// It is possible to disable sharing at the collection so we must check
collection = await Collection.findByPk(document.collectionId);
if (document.collectionId) {
collection = await Collection.findByPk(document.collectionId);
}
invariant(collection, "collection not found");
if (!collection.sharing) {

View File

@@ -153,4 +153,27 @@ describe("documentMover", () => {
expect(response.documents[0].collection?.id).toEqual(newCollection.id);
expect(response.documents[0].updatedBy.id).toEqual(user.id);
});
it("should detach document from collection and move it to drafts", async () => {
const { document, user, collection } = await seed();
const response = await sequelize.transaction(async (transaction) =>
documentMover({
user,
document,
collectionId: null,
index: 0,
ip,
transaction,
})
);
expect(response.collections[0].id).toBe(collection.id);
expect(response.collections.length).toEqual(1);
expect(response.documents.length).toEqual(1);
expect(response.documents[0].collection).toBeNull();
expect(response.documents[0].updatedBy.id).toEqual(user.id);
expect(response.documents[0].publishedAt).toBeNull();
});
});

View File

@@ -5,12 +5,19 @@ import { User, Document, Collection, Pin, Event } from "@server/models";
import pinDestroyer from "./pinDestroyer";
type Props = {
/** User attempting to move the document */
user: User;
/** Document which is being moved */
document: Document;
collectionId: string;
/** Destination collection to which the document is moved */
collectionId: string | null;
/** ID of parent under which the document is moved */
parentDocumentId?: string | null;
/** Position of moved document within document structure */
index?: number;
/** The IP address of the user moving the document */
ip: string;
/** The database transaction to run within */
transaction?: Transaction;
};
@@ -23,7 +30,7 @@ type Result = {
async function documentMover({
user,
document,
collectionId,
collectionId = null,
parentDocumentId = null,
// convert undefined to null so parentId comparison treats them as equal
index,
@@ -43,6 +50,7 @@ async function documentMover({
}
if (document.template) {
invariant(collectionId, "collectionId should exist");
document.collectionId = collectionId;
document.parentDocumentId = null;
document.lastModifiedById = user.id;
@@ -51,20 +59,23 @@ async function documentMover({
result.documents.push(document);
} else {
// Load the current and the next collection upfront and lock them
const collection = await Collection.findByPk(document.collectionId, {
const collection = await Collection.findByPk(document.collectionId!, {
transaction,
lock: Transaction.LOCK.UPDATE,
paranoid: false,
});
let newCollection = collectionChanged
? await Collection.findByPk(collectionId, {
let newCollection = collection;
if (collectionChanged) {
if (collectionId) {
newCollection = await Collection.findByPk(collectionId, {
transaction,
lock: Transaction.LOCK.UPDATE,
})
: collection;
invariant(newCollection, "collection should exist");
});
} else {
newCollection = null;
}
}
if (document.publishedAt) {
// Remove the document from the current collection
@@ -99,11 +110,13 @@ async function documentMover({
document.lastModifiedById = user.id;
document.updatedBy = user;
// Add the document and it's tree to the new collection
await newCollection.addDocumentToStructure(document, toIndex, {
documentJson,
transaction,
});
if (newCollection) {
// Add the document and it's tree to the new collection
await newCollection.addDocumentToStructure(document, toIndex, {
documentJson,
transaction,
});
}
} else {
document.collectionId = collectionId;
document.parentDocumentId = parentDocumentId;
@@ -118,30 +131,48 @@ async function documentMover({
// If the collection has changed then we also need to update the properties
// on all of the documents children to reflect the new collectionId
if (collectionChanged) {
// Reload the collection to get relationship data
newCollection = await Collection.scope({
method: ["withMembership", user.id],
}).findByPk(collectionId, {
transaction,
});
invariant(newCollection, "collection should exist");
result.collections.push(newCollection);
// Efficiently find the ID's of all the documents that are children of
// the moved document and update in one query
const childDocumentIds = await document.getChildDocumentIds();
await Document.update(
{
collectionId: newCollection.id,
},
{
if (collectionId) {
// Reload the collection to get relationship data
newCollection = await Collection.scope({
method: ["withMembership", user.id],
}).findByPk(collectionId, {
transaction,
where: {
id: childDocumentIds,
});
invariant(newCollection, "collection should exist");
result.collections.push(newCollection);
await Document.update(
{
collectionId: newCollection.id,
},
}
);
{
transaction,
where: {
id: childDocumentIds,
},
}
);
} else {
// document will be moved to drafts
document.publishedAt = null;
// point children's parent to moved document's parent
await Document.update(
{
parentDocumentId: document.parentDocumentId,
},
{
transaction,
where: {
id: childDocumentIds,
},
}
);
}
// We must reload from the database to get the relationship data
const documents = await Document.findAll({

View File

@@ -74,7 +74,7 @@ export default async function documentUpdater({
if (!document.collectionId) {
document.collectionId = collectionId as string;
}
await document.publish(user.id, { transaction });
await document.publish(user.id, collectionId!, { transaction });
await Event.create(
{

View File

@@ -11,7 +11,7 @@ type Props = {
/** The document to pin */
documentId: string;
/** The collection to pin the document in. If no collection is provided then it will be pinned to home */
collectionId?: string | undefined;
collectionId?: string | null;
/** The index to pin the document at. If no index is provided then it will be pinned to the end of the collection */
index?: string;
/** The IP address of the user creating the pin */

View File

@@ -267,7 +267,8 @@ class Document extends ParanoidModel {
model.archivedAt ||
model.template ||
!model.publishedAt ||
!model.changed("title")
!model.changed("title") ||
!model.collectionId
) {
return;
}
@@ -286,12 +287,17 @@ class Document extends ParanoidModel {
@AfterCreate
static async addDocumentToCollectionStructure(model: Document) {
if (model.archivedAt || model.template || !model.publishedAt) {
if (
model.archivedAt ||
model.template ||
!model.publishedAt ||
!model.collectionId
) {
return;
}
return this.sequelize!.transaction(async (transaction: Transaction) => {
const collection = await Collection.findByPk(model.collectionId, {
const collection = await Collection.findByPk(model.collectionId!, {
transaction,
lock: transaction.LOCK.UPDATE,
});
@@ -399,7 +405,7 @@ class Document extends ParanoidModel {
@ForeignKey(() => Collection)
@Column(DataType.UUID)
collectionId: string;
collectionId?: string | null;
@HasMany(() => Revision)
revisions: Revision[];
@@ -555,13 +561,21 @@ class Document extends ParanoidModel {
return this.save(options);
};
publish = async (userId: string, { transaction }: SaveOptions<Document>) => {
publish = async (
userId: string,
collectionId: string,
{ transaction }: SaveOptions<Document>
) => {
// If the document is already published then calling publish should act like
// a regular save
if (this.publishedAt) {
return this.save({ transaction });
}
if (!this.collectionId) {
this.collectionId = collectionId;
}
if (!this.template) {
const collection = await Collection.findByPk(this.collectionId, {
transaction,
@@ -587,10 +601,12 @@ class Document extends ParanoidModel {
}
await this.sequelize.transaction(async (transaction: Transaction) => {
const collection = await Collection.findByPk(this.collectionId, {
transaction,
lock: transaction.LOCK.UPDATE,
});
const collection = this.collectionId
? await Collection.findByPk(this.collectionId, {
transaction,
lock: transaction.LOCK.UPDATE,
})
: undefined;
if (collection) {
await collection.removeDocumentInStructure(this, { transaction });
@@ -610,10 +626,12 @@ class Document extends ParanoidModel {
// to the archived area, where it can be subsequently restored.
archive = async (userId: string) => {
await this.sequelize.transaction(async (transaction: Transaction) => {
const collection = await Collection.findByPk(this.collectionId, {
transaction,
lock: transaction.LOCK.UPDATE,
});
const collection = this.collectionId
? await Collection.findByPk(this.collectionId, {
transaction,
lock: transaction.LOCK.UPDATE,
})
: undefined;
if (collection) {
await collection.removeDocumentInStructure(this, { transaction });
@@ -628,10 +646,12 @@ class Document extends ParanoidModel {
// Restore an archived document back to being visible to the team
unarchive = async (userId: string) => {
await this.sequelize.transaction(async (transaction: Transaction) => {
const collection = await Collection.findByPk(this.collectionId, {
transaction,
lock: transaction.LOCK.UPDATE,
});
const collection = this.collectionId
? await Collection.findByPk(this.collectionId, {
transaction,
lock: transaction.LOCK.UPDATE,
})
: undefined;
// check to see if the documents parent hasn't been archived also
// If it has then restore the document to the collection root.
@@ -675,6 +695,7 @@ class Document extends ParanoidModel {
const collection = await Collection.findByPk(this.collectionId, {
transaction,
lock: transaction.LOCK.UPDATE,
paranoid: false,
});
await collection?.deleteDocument(this, { transaction });
} else {

View File

@@ -252,7 +252,9 @@ class Team extends ParanoidModel {
},
{ transaction }
);
await document.publish(collection.createdById, { transaction });
await document.publish(collection.createdById, collection.id, {
transaction,
});
}
});
};

View File

@@ -164,11 +164,12 @@ export default class NotificationHelper {
const collectionIds = await recipient.collectionIds();
// Check the recipient has access to the collection this document is in. Just
// because they are subscribed doesn't meant they "still have access to read
// because they are subscribed doesn't mean they still have access to read
// the document.
if (
recipient.email &&
!recipient.isSuspended &&
document.collectionId &&
collectionIds.includes(document.collectionId)
) {
filtered.push(recipient);

View File

@@ -31,7 +31,7 @@ type SearchOptions = {
/** The query offset for pagination */
offset?: number;
/** Limit results to a collection. Authorization is presumed to have been done before passing to this helper. */
collectionId?: string;
collectionId?: string | null;
/** Limit results to a shared document. */
share?: Share;
/** Limit results to a date range. */

View File

@@ -18,7 +18,7 @@ describe("documents.publish", () => {
await processor.perform({
name: "documents.publish",
documentId: document.id,
collectionId: document.collectionId,
collectionId: document.collectionId!,
teamId: document.teamId,
actorId: document.createdById,
data: { title: document.title },
@@ -46,7 +46,7 @@ describe("documents.publish", () => {
await processor.perform({
name: "documents.publish",
documentId: document.id,
collectionId: document.collectionId,
collectionId: document.collectionId!,
teamId: document.teamId,
actorId: document.createdById,
data: { title: document.title },
@@ -72,7 +72,7 @@ describe("documents.update", () => {
await processor.perform({
name: "documents.update",
documentId: document.id,
collectionId: document.collectionId,
collectionId: document.collectionId!,
teamId: document.teamId,
actorId: document.createdById,
createdAt: new Date().toISOString(),
@@ -100,7 +100,7 @@ describe("documents.update", () => {
await processor.perform({
name: "documents.update",
documentId: document.id,
collectionId: document.collectionId,
collectionId: document.collectionId!,
teamId: document.teamId,
actorId: document.createdById,
createdAt: new Date().toISOString(),
@@ -125,7 +125,7 @@ describe("documents.update", () => {
await processor.perform({
name: "documents.update",
documentId: document.id,
collectionId: document.collectionId,
collectionId: document.collectionId!,
teamId: document.teamId,
actorId: document.createdById,
createdAt: new Date().toISOString(),
@@ -153,7 +153,7 @@ describe("documents.update", () => {
await processor.perform({
name: "documents.publish",
documentId: document.id,
collectionId: document.collectionId,
collectionId: document.collectionId!,
teamId: document.teamId,
actorId: document.createdById,
data: { title: document.title },
@@ -167,7 +167,7 @@ describe("documents.update", () => {
await processor.perform({
name: "documents.update",
documentId: document.id,
collectionId: document.collectionId,
collectionId: document.collectionId!,
teamId: document.teamId,
actorId: document.createdById,
createdAt: new Date().toISOString(),
@@ -195,7 +195,7 @@ describe("documents.delete", () => {
await processor.perform({
name: "documents.update",
documentId: document.id,
collectionId: document.collectionId,
collectionId: document.collectionId!,
teamId: document.teamId,
actorId: document.createdById,
createdAt: new Date().toISOString(),
@@ -206,7 +206,7 @@ describe("documents.delete", () => {
await processor.perform({
name: "documents.delete",
documentId: document.id,
collectionId: document.collectionId,
collectionId: document.collectionId!,
teamId: document.teamId,
actorId: document.createdById,
data: { title: document.title },

View File

@@ -0,0 +1,23 @@
import { CollectionEvent, Event } from "@server/types";
import DetachDraftsFromCollectionTask from "../tasks/DetachDraftsFromCollectionTask";
import BaseProcessor from "./BaseProcessor";
export default class CollectionsProcessor extends BaseProcessor {
static applicableEvents: Event["name"][] = ["collections.delete"];
async perform(event: CollectionEvent) {
switch (event.name) {
case "collections.delete":
return this.collectionDeleted(event);
default:
}
}
async collectionDeleted(event: CollectionEvent) {
await DetachDraftsFromCollectionTask.schedule({
collectionId: event.collectionId,
actorId: event.actorId,
ip: event.ip,
});
}
}

View File

@@ -15,7 +15,7 @@ describe("documents.update.debounced", () => {
await processor.perform({
name: "documents.update.debounced",
documentId: document.id,
collectionId: document.collectionId,
collectionId: document.collectionId!,
teamId: document.teamId,
actorId: document.createdById,
createdAt: new Date().toISOString(),
@@ -38,7 +38,7 @@ describe("documents.update.debounced", () => {
await processor.perform({
name: "documents.update.debounced",
documentId: document.id,
collectionId: document.collectionId,
collectionId: document.collectionId!,
teamId: document.teamId,
actorId: document.createdById,
createdAt: new Date().toISOString(),

View File

@@ -0,0 +1,33 @@
import { Document } from "@server/models";
import { buildCollection, buildDocument } from "@server/test/factories";
import { setupTestDatabase } from "@server/test/support";
import DetachDraftsFromCollectionTask from "./DetachDraftsFromCollectionTask";
setupTestDatabase();
describe("DetachDraftsFromCollectionTask", () => {
const ip = "127.0.0.1";
it("should detach drafts from deleted collection", async () => {
const collection = await buildCollection();
const document = await buildDocument({
title: "test",
collectionId: collection.id,
publishedAt: null,
createdById: collection.createdById,
teamId: collection.teamId,
});
await collection.destroy();
const task = new DetachDraftsFromCollectionTask();
await task.perform({
collectionId: collection.id,
ip,
actorId: collection.createdById,
});
const draft = await Document.findByPk(document.id);
expect(draft).not.toBe(null);
expect(draft?.deletedAt).toBe(null);
expect(draft?.collectionId).toBe(null);
});
});

View File

@@ -0,0 +1,48 @@
import { Op } from "sequelize";
import documentMover from "@server/commands/documentMover";
import { sequelize } from "@server/database/sequelize";
import { Collection, Document, User } from "@server/models";
import BaseTask from "./BaseTask";
type Props = {
collectionId: string;
actorId: string;
ip: string;
};
export default class DetachDraftsFromCollectionTask extends BaseTask<Props> {
async perform(props: Props) {
const [collection, actor] = await Promise.all([
Collection.findByPk(props.collectionId, {
paranoid: false,
}),
User.findByPk(props.actorId),
]);
if (!actor || !collection || !collection.deletedAt) {
return;
}
const documents = await Document.scope("withDrafts").findAll({
where: {
collectionId: props.collectionId,
publishedAt: {
[Op.is]: null,
},
},
paranoid: false,
});
return sequelize.transaction(async (transaction) => {
for (const document of documents) {
await documentMover({
document,
user: actor,
ip: props.ip,
collectionId: null,
transaction,
});
}
});
}
}

View File

@@ -31,7 +31,7 @@ describe("documents.publish", () => {
await processor.perform({
name: "documents.publish",
documentId: document.id,
collectionId: document.collectionId,
collectionId: document.collectionId!,
teamId: document.teamId,
actorId: document.createdById,
data: {
@@ -55,7 +55,7 @@ describe("documents.publish", () => {
await processor.perform({
name: "documents.publish",
documentId: document.id,
collectionId: document.collectionId,
collectionId: document.collectionId!,
teamId: document.teamId,
actorId: document.createdById,
data: {
@@ -95,7 +95,7 @@ describe("documents.publish", () => {
await processor.perform({
name: "documents.publish",
documentId: document.id,
collectionId: document.collectionId,
collectionId: document.collectionId!,
teamId: document.teamId,
actorId: document.createdById,
data: {
@@ -124,7 +124,7 @@ describe("documents.publish", () => {
await processor.perform({
name: "documents.publish",
documentId: document.id,
collectionId: document.collectionId,
collectionId: document.collectionId!,
teamId: document.teamId,
actorId: document.createdById,
data: {

View File

@@ -34,7 +34,7 @@ describe("revisions.create", () => {
await task.perform({
name: "revisions.create",
documentId: document.id,
collectionId: document.collectionId,
collectionId: document.collectionId!,
teamId: document.teamId,
actorId: collaborator.id,
modelId: revision.id,
@@ -63,7 +63,7 @@ describe("revisions.create", () => {
await task.perform({
name: "revisions.create",
documentId: document.id,
collectionId: document.collectionId,
collectionId: document.collectionId!,
teamId: document.teamId,
actorId: collaborator.id,
modelId: revision.id,
@@ -88,7 +88,7 @@ describe("revisions.create", () => {
await task.perform({
name: "revisions.create",
documentId: document.id,
collectionId: document.collectionId,
collectionId: document.collectionId!,
teamId: document.teamId,
actorId: user.id,
modelId: revision.id,
@@ -123,7 +123,7 @@ describe("revisions.create", () => {
await task.perform({
name: "revisions.create",
documentId: document.id,
collectionId: document.collectionId,
collectionId: document.collectionId!,
teamId: document.teamId,
actorId: collaborator.id,
modelId: revision.id,
@@ -152,7 +152,7 @@ describe("revisions.create", () => {
await task.perform({
name: "revisions.create",
documentId: document.id,
collectionId: document.collectionId,
collectionId: document.collectionId!,
teamId: document.teamId,
actorId: collaborator0.id,
modelId: revision.id,
@@ -202,7 +202,7 @@ describe("revisions.create", () => {
await task.perform({
name: "revisions.create",
documentId: document.id,
collectionId: document.collectionId,
collectionId: document.collectionId!,
teamId: document.teamId,
actorId: collaborator0.id,
modelId: revision.id,
@@ -247,7 +247,7 @@ describe("revisions.create", () => {
await task.perform({
name: "revisions.create",
documentId: document.id,
collectionId: document.collectionId,
collectionId: document.collectionId!,
teamId: document.teamId,
actorId: collaborator0.id,
modelId: revision.id,
@@ -303,7 +303,7 @@ describe("revisions.create", () => {
await task.perform({
name: "revisions.create",
documentId: document.id,
collectionId: document.collectionId,
collectionId: document.collectionId!,
teamId: document.teamId,
actorId: collaborator.id,
modelId: revision.id,
@@ -345,7 +345,7 @@ describe("revisions.create", () => {
await task.perform({
name: "revisions.create",
documentId: document.id,
collectionId: document.collectionId,
collectionId: document.collectionId!,
teamId: document.teamId,
actorId: collaborator.id,
modelId: revision.id,
@@ -391,7 +391,7 @@ describe("revisions.create", () => {
await task.perform({
name: "revisions.create",
documentId: document.id,
collectionId: document.collectionId,
collectionId: document.collectionId!,
teamId: document.teamId,
actorId: collaborator.id,
modelId: revision.id,
@@ -421,7 +421,7 @@ describe("revisions.create", () => {
await task.perform({
name: "revisions.create",
documentId: document.id,
collectionId: document.collectionId,
collectionId: document.collectionId!,
teamId: document.teamId,
actorId: collaborator.id,
modelId: revision.id,
@@ -444,7 +444,7 @@ describe("revisions.create", () => {
await task.perform({
name: "revisions.create",
documentId: document.id,
collectionId: document.collectionId,
collectionId: document.collectionId!,
teamId: document.teamId,
actorId: user.id,
modelId: revision.id,

View File

@@ -2928,6 +2928,35 @@ describe("#documents.delete", () => {
expect(deletedDoc?.deletedAt).toBeTruthy();
});
it("should delete a draft under deleted collection", async () => {
const user = await buildUser();
const collection = await buildCollection({
createdById: user.id,
teamId: user.teamId,
deletedAt: new Date(),
});
const document = await buildDocument({
createdById: user.id,
teamId: user.teamId,
collectionId: collection.id,
publishedAt: null,
});
const res = await server.post("/api/documents.delete", {
body: {
token: user.getJwtToken(),
id: document.id,
},
});
expect(res.status).toBe(200);
const deletedDoc = await Document.findByPk(document.id, {
paranoid: false,
});
expect(deletedDoc).not.toBe(null);
expect(deletedDoc?.deletedAt).not.toBe(null);
});
it("should allow permanently deleting a document", async () => {
const user = await buildUser();
const document = await buildDocument({

View File

@@ -582,9 +582,11 @@ router.post(
document.collectionId = collectionId;
}
const collection = await Collection.scope({
method: ["withMembership", user.id],
}).findByPk(document.collectionId);
const collection = document.collectionId
? await Collection.scope({
method: ["withMembership", user.id],
}).findByPk(document.collectionId)
: undefined;
// if the collectionId was provided in the request and isn't valid then it will
// be caught as a 403 on the authorize call below. Otherwise we're checking here
@@ -938,6 +940,10 @@ router.post(
ip: ctx.request.ip,
});
if (!document.collectionId) {
return null;
}
return await Collection.scope({
method: ["withMembership", user.id],
}).findByPk(document.collectionId, { transaction });

View File

@@ -87,7 +87,9 @@ export const seed = async () =>
},
{ transaction }
);
await document.publish(collection.createdById, { transaction });
await document.publish(collection.createdById, collection.id, {
transaction,
});
await collection.reload({ transaction });
return {
user,

View File

@@ -98,7 +98,7 @@
"Viewers": "Viewers",
"Im sure Delete": "Im sure Delete",
"Deleting": "Deleting",
"Are you sure about that? Deleting the <em>{{collectionName}}</em> collection is permanent and cannot be restored, however documents within will be moved to the trash.": "Are you sure about that? Deleting the <em>{{collectionName}}</em> collection is permanent and cannot be restored, however documents within will be moved to the trash.",
"Are you sure about that? Deleting the <em>{{collectionName}}</em> collection is permanent and cannot be restored, however all published documents within will be moved to the trash.": "Are you sure about that? Deleting the <em>{{collectionName}}</em> collection is permanent and cannot be restored, however all published documents within will be moved to the trash.",
"Also, <em>{{collectionName}}</em> is being used as the start view deleting it will reset the start view to the Home page.": "Also, <em>{{collectionName}}</em> is being used as the start view deleting it will reset the start view to the Home page.",
"Sorry, an error occurred saving the collection": "Sorry, an error occurred saving the collection",
"Add a description": "Add a description",