feat: Pin to home (#2880)

This commit is contained in:
Tom Moor
2021-12-30 16:54:02 -08:00
committed by GitHub
parent 5be2eb75f3
commit eb0c324da8
57 changed files with 1884 additions and 819 deletions

View File

@@ -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;
}

View 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);
});
});

View 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;
}

View 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);
});
});

View 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;
}

View 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;
}