feat: Pin to home (#2880)
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { Transaction } from "sequelize";
|
||||
import { Document, Attachment, Collection, User, Event } from "@server/models";
|
||||
import { Document, Attachment, Collection, Pin, Event } from "@server/models";
|
||||
import parseAttachmentIds from "@server/utils/parseAttachmentIds";
|
||||
import { sequelize } from "../sequelize";
|
||||
import pinDestroyer from "./pinDestroyer";
|
||||
|
||||
async function copyAttachments(
|
||||
document: Document,
|
||||
@@ -51,36 +52,30 @@ export default async function documentMover({
|
||||
}: {
|
||||
// @ts-expect-error ts-migrate(2749) FIXME: 'User' refers to a value, but is being used as a t... Remove this comment to see the full error message
|
||||
user: User;
|
||||
document: Document;
|
||||
document: any;
|
||||
collectionId: string;
|
||||
parentDocumentId?: string | null;
|
||||
index?: number;
|
||||
ip: string;
|
||||
}) {
|
||||
let transaction: Transaction | undefined;
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'collectionId' does not exist on type 'Do... Remove this comment to see the full error message
|
||||
const collectionChanged = collectionId !== document.collectionId;
|
||||
const previousCollectionId = document.collectionId;
|
||||
const result = {
|
||||
collections: [],
|
||||
documents: [],
|
||||
collectionChanged,
|
||||
};
|
||||
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'template' does not exist on type 'Docume... Remove this comment to see the full error message
|
||||
if (document.template) {
|
||||
if (!collectionChanged) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'collectionId' does not exist on type 'Do... Remove this comment to see the full error message
|
||||
document.collectionId = collectionId;
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'parentDocumentId' does not exist on type... Remove this comment to see the full error message
|
||||
document.parentDocumentId = null;
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'lastModifiedById' does not exist on type... Remove this comment to see the full error message
|
||||
document.lastModifiedById = user.id;
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'updatedBy' does not exist on type 'Docum... Remove this comment to see the full error message
|
||||
document.updatedBy = user;
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'save' does not exist on type 'Document'.
|
||||
await document.save();
|
||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'Document' is not assignable to p... Remove this comment to see the full error message
|
||||
result.documents.push(document);
|
||||
@@ -89,7 +84,6 @@ export default async function documentMover({
|
||||
transaction = await sequelize.transaction();
|
||||
|
||||
// remove from original collection
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'collectionId' does not exist on type 'Do... Remove this comment to see the full error message
|
||||
const collection = await Collection.findByPk(document.collectionId, {
|
||||
transaction,
|
||||
paranoid: false,
|
||||
@@ -107,9 +101,7 @@ export default async function documentMover({
|
||||
// We need to compensate for this when reordering
|
||||
const toIndex =
|
||||
index !== undefined &&
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'parentDocumentId' does not exist on type... Remove this comment to see the full error message
|
||||
document.parentDocumentId === parentDocumentId &&
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'collectionId' does not exist on type 'Do... Remove this comment to see the full error message
|
||||
document.collectionId === collectionId &&
|
||||
fromIndex < index
|
||||
? index - 1
|
||||
@@ -121,21 +113,17 @@ export default async function documentMover({
|
||||
await collection.save({
|
||||
transaction,
|
||||
});
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'text' does not exist on type 'Document'.
|
||||
document.text = await copyAttachments(document, {
|
||||
transaction,
|
||||
});
|
||||
}
|
||||
|
||||
// add to new collection (may be the same)
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'collectionId' does not exist on type 'Do... Remove this comment to see the full error message
|
||||
document.collectionId = collectionId;
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'parentDocumentId' does not exist on type... Remove this comment to see the full error message
|
||||
document.parentDocumentId = parentDocumentId;
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'lastModifiedById' does not exist on type... Remove this comment to see the full error message
|
||||
document.lastModifiedById = user.id;
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'updatedBy' does not exist on type 'Docum... Remove this comment to see the full error message
|
||||
document.updatedBy = user;
|
||||
|
||||
// @ts-expect-error ts-migrate(2749) FIXME: 'Collection' refers to a value, but is being used ... Remove this comment to see the full error message
|
||||
const newCollection: Collection = collectionChanged
|
||||
? await Collection.scope({
|
||||
@@ -180,15 +168,27 @@ export default async function documentMover({
|
||||
);
|
||||
};
|
||||
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'Document'.
|
||||
await loopChildren(document.id);
|
||||
|
||||
const pin = await Pin.findOne({
|
||||
where: {
|
||||
documentId: document.id,
|
||||
collectionId: previousCollectionId,
|
||||
},
|
||||
});
|
||||
|
||||
if (pin) {
|
||||
await pinDestroyer({
|
||||
user,
|
||||
pin,
|
||||
ip,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'save' does not exist on type 'Document'.
|
||||
await document.save({
|
||||
transaction,
|
||||
});
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'collection' does not exist on type 'Docu... Remove this comment to see the full error message
|
||||
document.collection = newCollection;
|
||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'Document' is not assignable to p... Remove this comment to see the full error message
|
||||
result.documents.push(document);
|
||||
@@ -208,10 +208,8 @@ export default async function documentMover({
|
||||
await Event.create({
|
||||
name: "documents.move",
|
||||
actorId: user.id,
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'Document'.
|
||||
documentId: document.id,
|
||||
collectionId,
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'teamId' does not exist on type 'Document... Remove this comment to see the full error message
|
||||
teamId: document.teamId,
|
||||
data: {
|
||||
title: document.title,
|
||||
@@ -222,6 +220,7 @@ export default async function documentMover({
|
||||
},
|
||||
ip,
|
||||
});
|
||||
|
||||
// we need to send all updated models back to the client
|
||||
return result;
|
||||
}
|
||||
|
||||
55
server/commands/pinCreator.test.ts
Normal file
55
server/commands/pinCreator.test.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { Event } from "@server/models";
|
||||
import { buildDocument, buildUser } from "@server/test/factories";
|
||||
import { flushdb } from "@server/test/support";
|
||||
import pinCreator from "./pinCreator";
|
||||
|
||||
beforeEach(() => flushdb());
|
||||
describe("pinCreator", () => {
|
||||
const ip = "127.0.0.1";
|
||||
|
||||
it("should create pin to home", async () => {
|
||||
const user = await buildUser();
|
||||
const document = await buildDocument({
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
});
|
||||
|
||||
const pin = await pinCreator({
|
||||
documentId: document.id,
|
||||
user,
|
||||
ip,
|
||||
});
|
||||
|
||||
const event = await Event.findOne();
|
||||
expect(pin.documentId).toEqual(document.id);
|
||||
expect(pin.collectionId).toEqual(null);
|
||||
expect(pin.createdById).toEqual(user.id);
|
||||
expect(pin.index).toEqual("P");
|
||||
expect(event.name).toEqual("pins.create");
|
||||
expect(event.modelId).toEqual(pin.id);
|
||||
});
|
||||
|
||||
it("should create pin to collection", async () => {
|
||||
const user = await buildUser();
|
||||
const document = await buildDocument({
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
});
|
||||
|
||||
const pin = await pinCreator({
|
||||
documentId: document.id,
|
||||
collectionId: document.collectionId,
|
||||
user,
|
||||
ip,
|
||||
});
|
||||
|
||||
const event = await Event.findOne();
|
||||
expect(pin.documentId).toEqual(document.id);
|
||||
expect(pin.collectionId).toEqual(document.collectionId);
|
||||
expect(pin.createdById).toEqual(user.id);
|
||||
expect(pin.index).toEqual("P");
|
||||
expect(event.name).toEqual("pins.create");
|
||||
expect(event.modelId).toEqual(pin.id);
|
||||
expect(event.collectionId).toEqual(pin.collectionId);
|
||||
});
|
||||
});
|
||||
97
server/commands/pinCreator.ts
Normal file
97
server/commands/pinCreator.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import fractionalIndex from "fractional-index";
|
||||
import { ValidationError } from "@server/errors";
|
||||
import { Pin, Event } from "@server/models";
|
||||
import { sequelize, Op } from "@server/sequelize";
|
||||
|
||||
const MAX_PINS = 8;
|
||||
|
||||
type Props = {
|
||||
/** The user creating the pin */
|
||||
user: any;
|
||||
/** 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;
|
||||
/** 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 */
|
||||
ip: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* This command creates a "pinned" document via the pin relation. A document can
|
||||
* be pinned to a collection or to the home screen.
|
||||
*
|
||||
* @param Props The properties of the pin to create
|
||||
* @returns Pin The pin that was created
|
||||
*/
|
||||
export default async function pinCreator({
|
||||
user,
|
||||
documentId,
|
||||
collectionId,
|
||||
ip,
|
||||
...rest
|
||||
}: Props): Promise<any> {
|
||||
let { index } = rest;
|
||||
const where = {
|
||||
teamId: user.teamId,
|
||||
...(collectionId ? { collectionId } : { collectionId: { [Op.eq]: null } }),
|
||||
};
|
||||
|
||||
const count = await Pin.count({ where });
|
||||
if (count >= MAX_PINS) {
|
||||
throw ValidationError(`You cannot pin more than ${MAX_PINS} documents`);
|
||||
}
|
||||
|
||||
if (!index) {
|
||||
const pins = await Pin.findAll({
|
||||
where,
|
||||
attributes: ["id", "index", "updatedAt"],
|
||||
limit: 1,
|
||||
order: [
|
||||
// using LC_COLLATE:"C" because we need byte order to drive the sorting
|
||||
// find only the last pin so we can create an index after it
|
||||
sequelize.literal('"pins"."index" collate "C" DESC'),
|
||||
["updatedAt", "ASC"],
|
||||
],
|
||||
});
|
||||
|
||||
// create a pin at the end of the list
|
||||
index = fractionalIndex(pins.length ? pins[0].index : null, null);
|
||||
}
|
||||
|
||||
const transaction = await sequelize.transaction();
|
||||
let pin;
|
||||
|
||||
try {
|
||||
pin = await Pin.create(
|
||||
{
|
||||
createdById: user.id,
|
||||
teamId: user.teamId,
|
||||
collectionId,
|
||||
documentId,
|
||||
index,
|
||||
},
|
||||
{ transaction }
|
||||
);
|
||||
|
||||
await Event.create(
|
||||
{
|
||||
name: "pins.create",
|
||||
modelId: pin.id,
|
||||
teamId: user.teamId,
|
||||
actorId: user.id,
|
||||
documentId,
|
||||
collectionId,
|
||||
ip,
|
||||
},
|
||||
{ transaction }
|
||||
);
|
||||
await transaction.commit();
|
||||
} catch (err) {
|
||||
await transaction.rollback();
|
||||
throw err;
|
||||
}
|
||||
|
||||
return pin;
|
||||
}
|
||||
38
server/commands/pinDestroyer.test.ts
Normal file
38
server/commands/pinDestroyer.test.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Pin, Event } from "@server/models";
|
||||
import { buildDocument, buildUser } from "@server/test/factories";
|
||||
import { flushdb } from "@server/test/support";
|
||||
import pinDestroyer from "./pinDestroyer";
|
||||
|
||||
beforeEach(() => flushdb());
|
||||
describe("pinCreator", () => {
|
||||
const ip = "127.0.0.1";
|
||||
|
||||
it("should destroy existing pin", async () => {
|
||||
const user = await buildUser();
|
||||
const document = await buildDocument({
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
});
|
||||
|
||||
const pin = await Pin.create({
|
||||
teamId: document.teamId,
|
||||
documentId: document.id,
|
||||
collectionId: document.collectionId,
|
||||
createdById: user.id,
|
||||
index: "P",
|
||||
});
|
||||
|
||||
await pinDestroyer({
|
||||
pin,
|
||||
user,
|
||||
ip,
|
||||
});
|
||||
|
||||
const count = await Pin.count();
|
||||
expect(count).toEqual(0);
|
||||
|
||||
const event = await Event.findOne();
|
||||
expect(event.name).toEqual("pins.delete");
|
||||
expect(event.modelId).toEqual(pin.id);
|
||||
});
|
||||
});
|
||||
54
server/commands/pinDestroyer.ts
Normal file
54
server/commands/pinDestroyer.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Transaction } from "sequelize";
|
||||
import { Event } from "@server/models";
|
||||
import { sequelize } from "@server/sequelize";
|
||||
|
||||
type Props = {
|
||||
/** The user destroying the pin */
|
||||
user: any;
|
||||
/** The pin to destroy */
|
||||
pin: any;
|
||||
/** The IP address of the user creating the pin */
|
||||
ip: string;
|
||||
/** Optional existing transaction */
|
||||
transaction?: Transaction;
|
||||
};
|
||||
|
||||
/**
|
||||
* This command destroys a document pin. This just removes the pin itself and
|
||||
* does not touch the document
|
||||
*
|
||||
* @param Props The properties of the pin to destroy
|
||||
* @returns void
|
||||
*/
|
||||
export default async function pinDestroyer({
|
||||
user,
|
||||
pin,
|
||||
ip,
|
||||
transaction: t,
|
||||
}: Props): Promise<any> {
|
||||
const transaction = t || (await sequelize.transaction());
|
||||
|
||||
try {
|
||||
await Event.create(
|
||||
{
|
||||
name: "pins.delete",
|
||||
modelId: pin.id,
|
||||
teamId: user.teamId,
|
||||
actorId: user.id,
|
||||
documentId: pin.documentId,
|
||||
collectionId: pin.collectionId,
|
||||
ip,
|
||||
},
|
||||
{ transaction }
|
||||
);
|
||||
|
||||
await pin.destroy({ transaction });
|
||||
|
||||
await transaction.commit();
|
||||
} catch (err) {
|
||||
await transaction.rollback();
|
||||
throw err;
|
||||
}
|
||||
|
||||
return pin;
|
||||
}
|
||||
53
server/commands/pinUpdater.ts
Normal file
53
server/commands/pinUpdater.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { Event } from "@server/models";
|
||||
import { sequelize } from "@server/sequelize";
|
||||
|
||||
type Props = {
|
||||
/** The user updating the pin */
|
||||
user: any;
|
||||
/** The existing pin */
|
||||
pin: any;
|
||||
/** The index to pin the document at */
|
||||
index?: string;
|
||||
/** The IP address of the user creating the pin */
|
||||
ip: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* This command updates a "pinned" document. A pin can only be moved to a new
|
||||
* index (reordered) once created.
|
||||
*
|
||||
* @param Props The properties of the pin to update
|
||||
* @returns Pin The updated pin
|
||||
*/
|
||||
export default async function pinUpdater({
|
||||
user,
|
||||
pin,
|
||||
index,
|
||||
ip,
|
||||
}: Props): Promise<any> {
|
||||
const transaction = await sequelize.transaction();
|
||||
|
||||
try {
|
||||
pin.index = index;
|
||||
await pin.save({ transaction });
|
||||
|
||||
await Event.create(
|
||||
{
|
||||
name: "pins.update",
|
||||
modelId: pin.id,
|
||||
teamId: user.teamId,
|
||||
actorId: user.id,
|
||||
documentId: pin.documentId,
|
||||
collectionId: pin.collectionId,
|
||||
ip,
|
||||
},
|
||||
{ transaction }
|
||||
);
|
||||
await transaction.commit();
|
||||
} catch (err) {
|
||||
await transaction.rollback();
|
||||
throw err;
|
||||
}
|
||||
|
||||
return pin;
|
||||
}
|
||||
Reference in New Issue
Block a user