Edit collection (#173)

* Collection edit modal

* Add icon

* 💚

* Oh look, some specs

* Delete collection

* Remove from collection

* Handle error responses
Protect against deleting last collection

* Fix key

* 💚

* Keyboard navigate documents list

* Add missing database constraints
This commit is contained in:
Tom Moor
2017-08-29 08:37:17 -07:00
committed by GitHub
parent e0b1c259e8
commit 8558b92cae
22 changed files with 515 additions and 53 deletions

View File

@@ -0,0 +1,37 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`#collections.create should require authentication 1`] = `
Object {
"error": "authentication_required",
"message": "Authentication required",
"ok": false,
"status": 401,
}
`;
exports[`#collections.delete should require authentication 1`] = `
Object {
"error": "authentication_required",
"message": "Authentication required",
"ok": false,
"status": 401,
}
`;
exports[`#collections.info should require authentication 1`] = `
Object {
"error": "authentication_required",
"message": "Authentication required",
"ok": false,
"status": 401,
}
`;
exports[`#collections.list should require authentication 1`] = `
Object {
"error": "authentication_required",
"message": "Authentication required",
"ok": false,
"status": 401,
}
`;

View File

@@ -1,3 +1,4 @@
// @flow
import Router from 'koa-router';
import httpErrors from 'http-errors';
import _ from 'lodash';
@@ -15,7 +16,7 @@ router.post('collections.create', auth(), async ctx => {
const user = ctx.state.user;
const atlas = await Collection.create({
const collection = await Collection.create({
name,
description,
type: type || 'atlas',
@@ -24,7 +25,20 @@ router.post('collections.create', auth(), async ctx => {
});
ctx.body = {
data: await presentCollection(ctx, atlas),
data: await presentCollection(ctx, collection),
};
});
router.post('collections.update', auth(), async ctx => {
const { id, name } = ctx.body;
ctx.assertPresent(name, 'name is required');
const collection = await Collection.findById(id);
collection.name = name;
await collection.save();
ctx.body = {
data: await presentCollection(ctx, collection),
};
});
@@ -33,17 +47,17 @@ router.post('collections.info', auth(), async ctx => {
ctx.assertPresent(id, 'id is required');
const user = ctx.state.user;
const atlas = await Collection.scope('withRecentDocuments').findOne({
const collection = await Collection.scope('withRecentDocuments').findOne({
where: {
id,
teamId: user.teamId,
},
});
if (!atlas) throw httpErrors.NotFound();
if (!collection) throw httpErrors.NotFound();
ctx.body = {
data: await presentCollection(ctx, atlas),
data: await presentCollection(ctx, collection),
};
});
@@ -59,7 +73,9 @@ router.post('collections.list', auth(), pagination(), async ctx => {
});
const data = await Promise.all(
collections.map(async atlas => await presentCollection(ctx, atlas))
collections.map(
async collection => await presentCollection(ctx, collection)
)
);
ctx.body = {
@@ -68,4 +84,28 @@ router.post('collections.list', auth(), pagination(), async ctx => {
};
});
router.post('collections.delete', auth(), async ctx => {
const { id } = ctx.body;
ctx.assertPresent(id, 'id is required');
const user = ctx.state.user;
const collection = await Collection.findById(id);
const total = await Collection.count();
if (total === 1) throw httpErrors.BadRequest('Cannot delete last collection');
if (!collection || collection.teamId !== user.teamId)
throw httpErrors.BadRequest();
try {
await collection.destroy();
} catch (e) {
throw httpErrors.BadRequest('Error while deleting collection');
}
ctx.body = {
success: true,
};
});
export default router;

View File

@@ -0,0 +1,111 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import TestServer from 'fetch-test-server';
import app from '..';
import { flushdb, seed } from '../test/support';
import Collection from '../models/Collection';
const server = new TestServer(app.callback());
beforeEach(flushdb);
afterAll(server.close);
describe('#collections.list', async () => {
it('should require authentication', async () => {
const res = await server.post('/api/collections.list');
const body = await res.json();
expect(res.status).toEqual(401);
expect(body).toMatchSnapshot();
});
it('should return collections', async () => {
const { user, collection } = await seed();
const res = await server.post('/api/collections.list', {
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(collection.id);
});
});
describe('#collections.info', async () => {
it('should require authentication', async () => {
const res = await server.post('/api/collections.info');
const body = await res.json();
expect(res.status).toEqual(401);
expect(body).toMatchSnapshot();
});
it('should return collection', async () => {
const { user, collection } = await seed();
const res = await server.post('/api/collections.info', {
body: { token: user.getJwtToken(), id: collection.id },
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.id).toEqual(collection.id);
});
});
describe('#collections.create', async () => {
it('should require authentication', async () => {
const res = await server.post('/api/collections.create');
const body = await res.json();
expect(res.status).toEqual(401);
expect(body).toMatchSnapshot();
});
it('should create collection', async () => {
const { user } = await seed();
const res = await server.post('/api/collections.create', {
body: { token: user.getJwtToken(), name: 'Test', type: 'atlas' },
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.id).toBeTruthy();
expect(body.data.name).toBe('Test');
});
});
describe('#collections.delete', async () => {
it('should require authentication', async () => {
const res = await server.post('/api/collections.delete');
const body = await res.json();
expect(res.status).toEqual(401);
expect(body).toMatchSnapshot();
});
it('should not delete last collection', async () => {
const { user, collection } = await seed();
const res = await server.post('/api/collections.delete', {
body: { token: user.getJwtToken(), id: collection.id },
});
expect(res.status).toEqual(400);
});
it('should delete collection', async () => {
const { user, collection } = await seed();
await Collection.create({
name: 'Blah',
urlId: 'blah',
teamId: user.teamId,
creatorId: user.id,
type: 'atlas',
});
const res = await server.post('/api/collections.delete', {
body: { token: user.getJwtToken(), id: collection.id },
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.success).toBe(true);
});
});

View File

@@ -1,3 +1,4 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import TestServer from 'fetch-test-server';
import app from '..';
import { View, Star } from '../models';