feat: Add ability to star collection (#3327)

* Migrations, models, commands

* ui

* Move starred hint to location state

* lint

* tsc

* refactor

* Add collection empty state in expanded sidebar

* Add empty placeholder within starred collections

* Drag and drop improves, Relative refactor

* fix: Starring untitled draft leaves empty space

* fix: Creating draft in starred collection shouldnt open main

* fix: Dupe drop cursor

* Final fixes

* fix: Canonical redirect replaces starred location state

* fix: Don't show reorder cursor at the top of collection with no permission to edit when dragging
This commit is contained in:
Tom Moor
2022-04-03 18:51:01 -07:00
committed by GitHub
parent 3de06b8005
commit 84d6bf8ddf
36 changed files with 988 additions and 635 deletions

View File

@@ -53,15 +53,6 @@ Object {
}
`;
exports[`#documents.starred should require authentication 1`] = `
Object {
"error": "authentication_required",
"message": "Authentication required",
"ok": false,
"status": 401,
}
`;
exports[`#documents.unstar should require authentication 1`] = `
Object {
"error": "authentication_required",

View File

@@ -1455,45 +1455,6 @@ describe("#documents.viewed", () => {
});
});
describe("#documents.starred", () => {
it("should return empty result if no stars", async () => {
const { user } = await seed();
const res = await server.post("/api/documents.starred", {
body: {
token: user.getJwtToken(),
},
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.length).toEqual(0);
});
it("should return starred documents", async () => {
const { user, document } = await seed();
await Star.create({
documentId: document.id,
userId: user.id,
});
const res = await server.post("/api/documents.starred", {
body: {
token: user.getJwtToken(),
},
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.length).toEqual(1);
expect(body.data[0].id).toEqual(document.id);
expect(body.policies[0].abilities.update).toEqual(true);
});
it("should require authentication", async () => {
const res = await server.post("/api/documents.starred");
const body = await res.json();
expect(res.status).toEqual(401);
expect(body).toMatchSnapshot();
});
});
describe("#documents.move", () => {
it("should move the document", async () => {
const { user, document } = await seed();

View File

@@ -313,62 +313,6 @@ router.post("documents.viewed", auth(), pagination(), async (ctx) => {
};
});
// Deprecated use stars.list instead
router.post("documents.starred", auth(), pagination(), async (ctx) => {
let { direction } = ctx.body;
const { sort = "updatedAt" } = ctx.body;
assertSort(sort, Document);
if (direction !== "ASC") {
direction = "DESC";
}
const { user } = ctx.state;
const collectionIds = await user.collectionIds();
const stars = await Star.findAll({
where: {
userId: user.id,
},
order: [[sort, direction]],
include: [
{
model: Document,
where: {
collectionId: collectionIds,
},
include: [
{
model: Collection.scope({
method: ["withMembership", user.id],
}),
as: "collection",
},
{
model: Star,
as: "starred",
where: {
userId: user.id,
},
},
],
},
],
offset: ctx.state.pagination.offset,
limit: ctx.state.pagination.limit,
});
const documents = stars.map((star) => star.document);
const data = await Promise.all(
documents.map((document) => presentDocument(document))
);
const policies = presentPolicies(user, documents);
ctx.body = {
pagination: ctx.state.pagination,
data,
policies,
};
});
router.post("documents.drafts", auth(), pagination(), async (ctx) => {
let { direction } = ctx.body;
const { collectionId, dateFilter, sort = "updatedAt" } = ctx.body;

View File

@@ -3,8 +3,9 @@ import { Sequelize } from "sequelize";
import starCreator from "@server/commands/starCreator";
import starDestroyer from "@server/commands/starDestroyer";
import starUpdater from "@server/commands/starUpdater";
import { sequelize } from "@server/database/sequelize";
import auth from "@server/middlewares/authentication";
import { Document, Star } from "@server/models";
import { Document, Star, Collection } from "@server/models";
import { authorize } from "@server/policies";
import {
presentStar,
@@ -18,27 +19,43 @@ import pagination from "./middlewares/pagination";
const router = new Router();
router.post("stars.create", auth(), async (ctx) => {
const { documentId } = ctx.body;
const { documentId, collectionId } = ctx.body;
const { index } = ctx.body;
assertUuid(documentId, "documentId is required");
const { user } = ctx.state;
const document = await Document.findByPk(documentId, {
userId: user.id,
});
authorize(user, "star", document);
assertUuid(
documentId || collectionId,
"documentId or collectionId is required"
);
if (documentId) {
const document = await Document.findByPk(documentId, {
userId: user.id,
});
authorize(user, "star", document);
}
if (collectionId) {
const collection = await Collection.scope({
method: ["withMembership", user.id],
}).findByPk(collectionId);
authorize(user, "star", collection);
}
if (index) {
assertIndexCharacters(index);
}
const star = await starCreator({
user,
documentId,
ip: ctx.request.ip,
index,
});
const star = await sequelize.transaction(async (transaction) =>
starCreator({
user,
documentId,
collectionId,
ip: ctx.request.ip,
index,
transaction,
})
);
ctx.body = {
data: presentStar(star),
policies: presentPolicies(user, [star]),
@@ -72,12 +89,17 @@ router.post("stars.list", auth(), pagination(), async (ctx) => {
});
}
const documents = await Document.defaultScopeWithUser(user.id).findAll({
where: {
id: stars.map((star) => star.documentId),
collectionId: collectionIds,
},
});
const documentIds = stars
.map((star) => star.documentId)
.filter(Boolean) as string[];
const documents = documentIds.length
? await Document.defaultScopeWithUser(user.id).findAll({
where: {
id: documentIds,
collectionId: collectionIds,
},
})
: [];
const policies = presentPolicies(user, [...documents, ...stars]);