Pinned documents (#608)
* Migrations and API for pinned documents * Documentation * Add pin icon * Fin. * v0.2.0 * Remove pin from DocumentPreview, add general menu Add Pinned documents header * Tidy * Fixed: Drafts appearing on collection home
This commit is contained in:
@@ -17,6 +17,15 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`#documents.pin should require authentication 1`] = `
|
||||
Object {
|
||||
"error": "authentication_required",
|
||||
"message": "Authentication required",
|
||||
"ok": false,
|
||||
"status": 401,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`#documents.search should require authentication 1`] = `
|
||||
Object {
|
||||
"error": "authentication_required",
|
||||
@@ -44,6 +53,15 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`#documents.unpin 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",
|
||||
|
||||
@@ -19,8 +19,7 @@ router.post('documents.list', auth(), pagination(), async ctx => {
|
||||
let where = { teamId: user.teamId };
|
||||
if (collection) where = { ...where, atlasId: collection };
|
||||
|
||||
const userId = user.id;
|
||||
const starredScope = { method: ['withStarred', userId] };
|
||||
const starredScope = { method: ['withStarred', user.id] };
|
||||
const documents = await Document.scope('defaultScope', starredScope).findAll({
|
||||
where,
|
||||
order: [[sort, direction]],
|
||||
@@ -38,6 +37,36 @@ router.post('documents.list', auth(), pagination(), async ctx => {
|
||||
};
|
||||
});
|
||||
|
||||
router.post('documents.pinned', auth(), pagination(), async ctx => {
|
||||
let { sort = 'updatedAt', direction, collection } = ctx.body;
|
||||
if (direction !== 'ASC') direction = 'DESC';
|
||||
ctx.assertPresent(collection, 'collection is required');
|
||||
|
||||
const user = ctx.state.user;
|
||||
const starredScope = { method: ['withStarred', user.id] };
|
||||
const documents = await Document.scope('defaultScope', starredScope).findAll({
|
||||
where: {
|
||||
teamId: user.teamId,
|
||||
atlasId: collection,
|
||||
pinnedById: {
|
||||
[Op.ne]: null,
|
||||
},
|
||||
},
|
||||
order: [[sort, direction]],
|
||||
offset: ctx.state.pagination.offset,
|
||||
limit: ctx.state.pagination.limit,
|
||||
});
|
||||
|
||||
const data = await Promise.all(
|
||||
documents.map(document => presentDocument(ctx, document))
|
||||
);
|
||||
|
||||
ctx.body = {
|
||||
pagination: ctx.state.pagination,
|
||||
data,
|
||||
};
|
||||
});
|
||||
|
||||
router.post('documents.viewed', auth(), pagination(), async ctx => {
|
||||
let { sort = 'updatedAt', direction } = ctx.body;
|
||||
if (direction !== 'ASC') direction = 'DESC';
|
||||
@@ -166,8 +195,7 @@ router.post('documents.search', auth(), pagination(), async ctx => {
|
||||
const { offset, limit } = ctx.state.pagination;
|
||||
ctx.assertPresent(query, 'query is required');
|
||||
|
||||
const user = await ctx.state.user;
|
||||
|
||||
const user = ctx.state.user;
|
||||
const documents = await Document.searchForUser(user, query, {
|
||||
offset,
|
||||
limit,
|
||||
@@ -183,13 +211,45 @@ router.post('documents.search', auth(), pagination(), async ctx => {
|
||||
};
|
||||
});
|
||||
|
||||
router.post('documents.pin', auth(), async ctx => {
|
||||
const { id } = ctx.body;
|
||||
ctx.assertPresent(id, 'id is required');
|
||||
const user = ctx.state.user;
|
||||
const document = await Document.findById(id);
|
||||
|
||||
authorize(user, 'update', document);
|
||||
|
||||
document.pinnedById = user.id;
|
||||
await document.save();
|
||||
|
||||
ctx.body = {
|
||||
data: await presentDocument(ctx, document),
|
||||
};
|
||||
});
|
||||
|
||||
router.post('documents.unpin', auth(), async ctx => {
|
||||
const { id } = ctx.body;
|
||||
ctx.assertPresent(id, 'id is required');
|
||||
const user = ctx.state.user;
|
||||
const document = await Document.findById(id);
|
||||
|
||||
authorize(user, 'update', document);
|
||||
|
||||
document.pinnedById = null;
|
||||
await document.save();
|
||||
|
||||
ctx.body = {
|
||||
data: await presentDocument(ctx, document),
|
||||
};
|
||||
});
|
||||
|
||||
router.post('documents.star', auth(), async ctx => {
|
||||
const { id } = ctx.body;
|
||||
ctx.assertPresent(id, 'id is required');
|
||||
const user = await ctx.state.user;
|
||||
const user = ctx.state.user;
|
||||
const document = await Document.findById(id);
|
||||
|
||||
authorize(ctx.state.user, 'read', document);
|
||||
authorize(user, 'read', document);
|
||||
|
||||
await Star.findOrCreate({
|
||||
where: { documentId: document.id, userId: user.id },
|
||||
@@ -199,10 +259,10 @@ router.post('documents.star', auth(), async ctx => {
|
||||
router.post('documents.unstar', auth(), async ctx => {
|
||||
const { id } = ctx.body;
|
||||
ctx.assertPresent(id, 'id is required');
|
||||
const user = await ctx.state.user;
|
||||
const user = ctx.state.user;
|
||||
const document = await Document.findById(id);
|
||||
|
||||
authorize(ctx.state.user, 'read', document);
|
||||
authorize(user, 'read', document);
|
||||
|
||||
await Star.destroy({
|
||||
where: { documentId: document.id, userId: user.id },
|
||||
|
||||
@@ -248,6 +248,66 @@ describe('#documents.starred', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#documents.pin', async () => {
|
||||
it('should pin the document', async () => {
|
||||
const { user, document } = await seed();
|
||||
|
||||
const res = await server.post('/api/documents.pin', {
|
||||
body: { token: user.getJwtToken(), id: document.id },
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(body.data.pinned).toEqual(true);
|
||||
});
|
||||
|
||||
it('should require authentication', async () => {
|
||||
const res = await server.post('/api/documents.pin');
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(401);
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should require authorization', async () => {
|
||||
const { document } = await seed();
|
||||
const user = await buildUser();
|
||||
const res = await server.post('/api/documents.pin', {
|
||||
body: { token: user.getJwtToken(), id: document.id },
|
||||
});
|
||||
expect(res.status).toEqual(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#documents.unpin', async () => {
|
||||
it('should unpin the document', async () => {
|
||||
const { user, document } = await seed();
|
||||
document.pinnedBy = user;
|
||||
await document.save();
|
||||
|
||||
const res = await server.post('/api/documents.unpin', {
|
||||
body: { token: user.getJwtToken(), id: document.id },
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(body.data.pinned).toEqual(false);
|
||||
});
|
||||
|
||||
it('should require authentication', async () => {
|
||||
const res = await server.post('/api/documents.unpin');
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(401);
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should require authorization', async () => {
|
||||
const { document } = await seed();
|
||||
const user = await buildUser();
|
||||
const res = await server.post('/api/documents.unpin', {
|
||||
body: { token: user.getJwtToken(), id: document.id },
|
||||
});
|
||||
expect(res.status).toEqual(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#documents.star', async () => {
|
||||
it('should star the document', async () => {
|
||||
const { user, document } = await seed();
|
||||
|
||||
13
server/migrations/20180225203847-document-pinning.js
Normal file
13
server/migrations/20180225203847-document-pinning.js
Normal file
@@ -0,0 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.addColumn('documents', 'pinnedById', {
|
||||
type: Sequelize.UUID,
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.removeColumn('documents', 'pinnedById');
|
||||
}
|
||||
};
|
||||
@@ -85,20 +85,6 @@ const Document = sequelize.define(
|
||||
revisionCount: { type: DataTypes.INTEGER, defaultValue: 0 },
|
||||
publishedAt: DataTypes.DATE,
|
||||
parentDocumentId: DataTypes.UUID,
|
||||
createdById: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'users',
|
||||
},
|
||||
},
|
||||
lastModifiedById: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'users',
|
||||
},
|
||||
},
|
||||
collaboratorIds: DataTypes.ARRAY(DataTypes.UUID),
|
||||
},
|
||||
{
|
||||
@@ -129,6 +115,10 @@ Document.associate = models => {
|
||||
as: 'updatedBy',
|
||||
foreignKey: 'lastModifiedById',
|
||||
});
|
||||
Document.belongsTo(models.User, {
|
||||
as: 'pinnedBy',
|
||||
foreignKey: 'pinnedById',
|
||||
});
|
||||
Document.hasMany(models.Revision, {
|
||||
as: 'revisions',
|
||||
onDelete: 'cascade',
|
||||
|
||||
@@ -427,6 +427,34 @@ export default function Pricing() {
|
||||
</Arguments>
|
||||
</Method>
|
||||
|
||||
<Method method="documents.pin" label="Pin a document">
|
||||
<Description>
|
||||
Pins a document to the collection home. The pinned document is
|
||||
visible to all members of the team.
|
||||
</Description>
|
||||
<Arguments>
|
||||
<Argument
|
||||
id="id"
|
||||
description="Document ID or URI identifier"
|
||||
required
|
||||
/>
|
||||
</Arguments>
|
||||
</Method>
|
||||
|
||||
<Method method="documents.unpin" label="Unpin a document">
|
||||
<Description>
|
||||
Unpins a document from the collection home. It will still remain
|
||||
in the collection itself.
|
||||
</Description>
|
||||
<Arguments>
|
||||
<Argument
|
||||
id="id"
|
||||
description="Document ID or URI identifier"
|
||||
required
|
||||
/>
|
||||
</Arguments>
|
||||
</Method>
|
||||
|
||||
<Method method="documents.star" label="Star a document">
|
||||
<Description>
|
||||
Star (favorite) a document for authenticated user.
|
||||
@@ -442,7 +470,7 @@ export default function Pricing() {
|
||||
|
||||
<Method method="documents.unstar" label="Unstar a document">
|
||||
<Description>
|
||||
Unstar as starred (favorited) a document for authenticated user.
|
||||
Unstar a starred (favorited) document for authenticated user.
|
||||
</Description>
|
||||
<Arguments>
|
||||
<Argument
|
||||
@@ -473,6 +501,14 @@ export default function Pricing() {
|
||||
<Arguments pagination />
|
||||
</Method>
|
||||
|
||||
<Method
|
||||
method="documents.pinned"
|
||||
label="Get pinned documents for a collection"
|
||||
>
|
||||
<Description>Return pinned documents for a collection</Description>
|
||||
<Arguments pagination />
|
||||
</Method>
|
||||
|
||||
<Method
|
||||
method="documents.revisions"
|
||||
label="Get revisions for a document"
|
||||
|
||||
@@ -39,6 +39,7 @@ async function present(ctx: Object, document: Document, options: ?Options) {
|
||||
team: document.teamId,
|
||||
collaborators: [],
|
||||
starred: !!(document.starred && document.starred.length),
|
||||
pinned: !!document.pinnedById,
|
||||
revision: document.revisionCount,
|
||||
collectionId: document.atlasId,
|
||||
collaboratorCount: undefined,
|
||||
|
||||
Reference in New Issue
Block a user