feat: Add reordering to starred documents (#2953)
* draft * reordering * JIT Index stars on first load * test * Remove unused code on client * small unrefactor
This commit is contained in:
58
server/commands/starCreator.test.ts
Normal file
58
server/commands/starCreator.test.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Star, Event } from "@server/models";
|
||||
import { buildDocument, buildUser } from "@server/test/factories";
|
||||
import { flushdb } from "@server/test/support";
|
||||
import starCreator from "./starCreator";
|
||||
|
||||
beforeEach(() => flushdb());
|
||||
describe("starCreator", () => {
|
||||
const ip = "127.0.0.1";
|
||||
|
||||
it("should create star", async () => {
|
||||
const user = await buildUser();
|
||||
const document = await buildDocument({
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
});
|
||||
|
||||
const star = await starCreator({
|
||||
documentId: document.id,
|
||||
user,
|
||||
ip,
|
||||
});
|
||||
|
||||
const event = await Event.findOne();
|
||||
expect(star.documentId).toEqual(document.id);
|
||||
expect(star.userId).toEqual(user.id);
|
||||
expect(star.index).toEqual("P");
|
||||
expect(event!.name).toEqual("stars.create");
|
||||
expect(event!.modelId).toEqual(star.id);
|
||||
});
|
||||
|
||||
it("should not record event if star is existing", async () => {
|
||||
const user = await buildUser();
|
||||
const document = await buildDocument({
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
});
|
||||
|
||||
await Star.create({
|
||||
teamId: document.teamId,
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
createdById: user.id,
|
||||
index: "P",
|
||||
});
|
||||
|
||||
const star = await starCreator({
|
||||
documentId: document.id,
|
||||
user,
|
||||
ip,
|
||||
});
|
||||
|
||||
const events = await Event.count();
|
||||
expect(star.documentId).toEqual(document.id);
|
||||
expect(star.userId).toEqual(user.id);
|
||||
expect(star.index).toEqual("P");
|
||||
expect(events).toEqual(0);
|
||||
});
|
||||
});
|
||||
89
server/commands/starCreator.ts
Normal file
89
server/commands/starCreator.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import fractionalIndex from "fractional-index";
|
||||
import { Sequelize, WhereOptions } from "sequelize";
|
||||
import { sequelize } from "@server/database/sequelize";
|
||||
import { Star, User, Event } from "@server/models";
|
||||
|
||||
type Props = {
|
||||
/** The user creating the star */
|
||||
user: User;
|
||||
/** The document to star */
|
||||
documentId: string;
|
||||
/** The sorted index for the star in the sidebar If no index is provided then it will be at the end */
|
||||
index?: string;
|
||||
/** The IP address of the user creating the star */
|
||||
ip: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* This command creates a "starred" document via the star relation. Stars are
|
||||
* only visible to the user that created them.
|
||||
*
|
||||
* @param Props The properties of the star to create
|
||||
* @returns Star The star that was created
|
||||
*/
|
||||
export default async function starCreator({
|
||||
user,
|
||||
documentId,
|
||||
ip,
|
||||
...rest
|
||||
}: Props): Promise<Star> {
|
||||
let { index } = rest;
|
||||
const where: WhereOptions<Star> = {
|
||||
userId: user.id,
|
||||
};
|
||||
|
||||
if (!index) {
|
||||
const stars = await Star.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 first star so we can create an index before it
|
||||
Sequelize.literal('"star"."index" collate "C"'),
|
||||
["updatedAt", "DESC"],
|
||||
],
|
||||
});
|
||||
|
||||
// create a star at the beginning of the list
|
||||
index = fractionalIndex(null, stars.length ? stars[0].index : null);
|
||||
}
|
||||
|
||||
const transaction = await sequelize.transaction();
|
||||
let star;
|
||||
|
||||
try {
|
||||
const response = await Star.findOrCreate({
|
||||
where: {
|
||||
userId: user.id,
|
||||
documentId,
|
||||
},
|
||||
defaults: {
|
||||
index,
|
||||
},
|
||||
transaction,
|
||||
});
|
||||
star = response[0];
|
||||
|
||||
if (response[1]) {
|
||||
await Event.create(
|
||||
{
|
||||
name: "stars.create",
|
||||
modelId: star.id,
|
||||
userId: user.id,
|
||||
actorId: user.id,
|
||||
documentId,
|
||||
ip,
|
||||
},
|
||||
{ transaction }
|
||||
);
|
||||
}
|
||||
|
||||
await transaction.commit();
|
||||
} catch (err) {
|
||||
await transaction.rollback();
|
||||
throw err;
|
||||
}
|
||||
|
||||
return star;
|
||||
}
|
||||
39
server/commands/starDestroyer.test.ts
Normal file
39
server/commands/starDestroyer.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Star, Event } from "@server/models";
|
||||
import { buildDocument, buildUser } from "@server/test/factories";
|
||||
import { flushdb } from "@server/test/support";
|
||||
import starDestroyer from "./starDestroyer";
|
||||
|
||||
beforeEach(() => flushdb());
|
||||
|
||||
describe("starDestroyer", () => {
|
||||
const ip = "127.0.0.1";
|
||||
|
||||
it("should destroy existing star", async () => {
|
||||
const user = await buildUser();
|
||||
const document = await buildDocument({
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
});
|
||||
|
||||
const star = await Star.create({
|
||||
teamId: document.teamId,
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
createdById: user.id,
|
||||
index: "P",
|
||||
});
|
||||
|
||||
await starDestroyer({
|
||||
star,
|
||||
user,
|
||||
ip,
|
||||
});
|
||||
|
||||
const count = await Star.count();
|
||||
expect(count).toEqual(0);
|
||||
|
||||
const event = await Event.findOne();
|
||||
expect(event!.name).toEqual("stars.delete");
|
||||
expect(event!.modelId).toEqual(star.id);
|
||||
});
|
||||
});
|
||||
54
server/commands/starDestroyer.ts
Normal file
54
server/commands/starDestroyer.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Transaction } from "sequelize";
|
||||
import { sequelize } from "@server/database/sequelize";
|
||||
import { Event, Star, User } from "@server/models";
|
||||
|
||||
type Props = {
|
||||
/** The user destroying the star */
|
||||
user: User;
|
||||
/** The star to destroy */
|
||||
star: Star;
|
||||
/** The IP address of the user creating the star */
|
||||
ip: string;
|
||||
/** Optional existing transaction */
|
||||
transaction?: Transaction;
|
||||
};
|
||||
|
||||
/**
|
||||
* This command destroys a document star. This just removes the star itself and
|
||||
* does not touch the document
|
||||
*
|
||||
* @param Props The properties of the star to destroy
|
||||
* @returns void
|
||||
*/
|
||||
export default async function starDestroyer({
|
||||
user,
|
||||
star,
|
||||
ip,
|
||||
transaction: t,
|
||||
}: Props): Promise<Star> {
|
||||
const transaction = t || (await sequelize.transaction());
|
||||
|
||||
try {
|
||||
await star.destroy({ transaction });
|
||||
|
||||
await Event.create(
|
||||
{
|
||||
name: "stars.delete",
|
||||
modelId: star.id,
|
||||
teamId: user.teamId,
|
||||
actorId: user.id,
|
||||
userId: star.userId,
|
||||
documentId: star.documentId,
|
||||
ip,
|
||||
},
|
||||
{ transaction }
|
||||
);
|
||||
|
||||
await transaction.commit();
|
||||
} catch (err) {
|
||||
await transaction.rollback();
|
||||
throw err;
|
||||
}
|
||||
|
||||
return star;
|
||||
}
|
||||
40
server/commands/starUpdater.test.ts
Normal file
40
server/commands/starUpdater.test.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Star, Event } from "@server/models";
|
||||
import { buildDocument, buildUser } from "@server/test/factories";
|
||||
import { flushdb } from "@server/test/support";
|
||||
import starUpdater from "./starUpdater";
|
||||
|
||||
beforeEach(() => flushdb());
|
||||
|
||||
describe("starUpdater", () => {
|
||||
const ip = "127.0.0.1";
|
||||
|
||||
it("should update (move) existing star", async () => {
|
||||
const user = await buildUser();
|
||||
const document = await buildDocument({
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
});
|
||||
|
||||
let star = await Star.create({
|
||||
teamId: document.teamId,
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
createdById: user.id,
|
||||
index: "P",
|
||||
});
|
||||
|
||||
star = await starUpdater({
|
||||
star,
|
||||
index: "h",
|
||||
user,
|
||||
ip,
|
||||
});
|
||||
|
||||
const event = await Event.findOne();
|
||||
expect(star.documentId).toEqual(document.id);
|
||||
expect(star.userId).toEqual(user.id);
|
||||
expect(star.index).toEqual("h");
|
||||
expect(event!.name).toEqual("stars.update");
|
||||
expect(event!.modelId).toEqual(star.id);
|
||||
});
|
||||
});
|
||||
53
server/commands/starUpdater.ts
Normal file
53
server/commands/starUpdater.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { sequelize } from "@server/database/sequelize";
|
||||
import { Event, Star, User } from "@server/models";
|
||||
|
||||
type Props = {
|
||||
/** The user updating the star */
|
||||
user: User;
|
||||
/** The existing star */
|
||||
star: Star;
|
||||
/** The index to star the document at */
|
||||
index: string;
|
||||
/** The IP address of the user creating the star */
|
||||
ip: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* This command updates a "starred" document. A star can only be moved to a new
|
||||
* index (reordered) once created.
|
||||
*
|
||||
* @param Props The properties of the star to update
|
||||
* @returns Star The updated star
|
||||
*/
|
||||
export default async function starUpdater({
|
||||
user,
|
||||
star,
|
||||
index,
|
||||
ip,
|
||||
}: Props): Promise<Star> {
|
||||
const transaction = await sequelize.transaction();
|
||||
|
||||
try {
|
||||
star.index = index;
|
||||
await star.save({ transaction });
|
||||
|
||||
await Event.create(
|
||||
{
|
||||
name: "stars.update",
|
||||
modelId: star.id,
|
||||
userId: star.userId,
|
||||
teamId: user.teamId,
|
||||
actorId: user.id,
|
||||
documentId: star.documentId,
|
||||
ip,
|
||||
},
|
||||
{ transaction }
|
||||
);
|
||||
await transaction.commit();
|
||||
} catch (err) {
|
||||
await transaction.rollback();
|
||||
throw err;
|
||||
}
|
||||
|
||||
return star;
|
||||
}
|
||||
Reference in New Issue
Block a user