diff --git a/frontend/components/Button/Button.js b/frontend/components/Button/Button.js
index 988d6705..d0b3afff 100644
--- a/frontend/components/Button/Button.js
+++ b/frontend/components/Button/Button.js
@@ -6,10 +6,10 @@ import { darken } from 'polished';
const RealButton = styled.button`
display: inline-block;
- margin: 0 0 ${size.large};
+ margin: 0 ${size.medium} ${size.large} 0;
padding: 0;
border: 0;
- background: ${color.primary};
+ background: ${props => (props.neutral ? color.slate : props.danger ? color.danger : color.primary)};
color: ${color.white};
border-radius: 4px;
min-width: 32px;
@@ -23,7 +23,7 @@ const RealButton = styled.button`
border: 0;
}
&:hover {
- background: ${darken(0.05, color.primary)};
+ background: ${props => darken(0.05, props.neutral ? color.slate : props.danger ? color.danger : color.primary)};
}
&:disabled {
background: ${color.slateLight};
diff --git a/frontend/components/DocumentList/DocumentList.js b/frontend/components/DocumentList/DocumentList.js
index 4ca8b63a..a53a2de4 100644
--- a/frontend/components/DocumentList/DocumentList.js
+++ b/frontend/components/DocumentList/DocumentList.js
@@ -2,6 +2,7 @@
import React from 'react';
import Document from 'models/Document';
import DocumentPreview from 'components/DocumentPreview';
+import ArrowKeyNavigation from 'boundless-arrow-key-navigation';
class DocumentList extends React.Component {
props: {
@@ -10,12 +11,15 @@ class DocumentList extends React.Component {
render() {
return (
-
+
{this.props.documents &&
this.props.documents.map(document => (
))}
-
+
);
}
}
diff --git a/frontend/components/Icon/MoreIcon.js b/frontend/components/Icon/MoreIcon.js
new file mode 100644
index 00000000..5e8126ff
--- /dev/null
+++ b/frontend/components/Icon/MoreIcon.js
@@ -0,0 +1,21 @@
+// @flow
+import React from 'react';
+import Icon from './Icon';
+import type { Props } from './Icon';
+
+export default function MoreIcon(props: Props) {
+ return (
+
+
+
+ );
+}
diff --git a/frontend/components/Layout/Layout.js b/frontend/components/Layout/Layout.js
index 3aace8a7..2b8b4f2c 100644
--- a/frontend/components/Layout/Layout.js
+++ b/frontend/components/Layout/Layout.js
@@ -17,7 +17,9 @@ import Scrollable from 'components/Scrollable';
import Avatar from 'components/Avatar';
import Modal from 'components/Modal';
import AddIcon from 'components/Icon/AddIcon';
+import MoreIcon from 'components/Icon/MoreIcon';
import CollectionNew from 'scenes/CollectionNew';
+import CollectionEdit from 'scenes/CollectionEdit';
import KeyboardShortcuts from 'scenes/KeyboardShortcuts';
import Settings from 'scenes/Settings';
@@ -29,10 +31,12 @@ import UserStore from 'stores/UserStore';
import AuthStore from 'stores/AuthStore';
import UiStore from 'stores/UiStore';
import CollectionsStore from 'stores/CollectionsStore';
+import DocumentsStore from 'stores/DocumentsStore';
type Props = {
history: Object,
collections: CollectionsStore,
+ documents: DocumentsStore,
children?: ?React.Element,
actions?: ?React.Element,
title?: ?React.Element,
@@ -66,8 +70,8 @@ type Props = {
@keydown('e')
goToEdit() {
- if (!this.props.ui.activeDocument) return;
- this.props.history.push(documentEditUrl(this.props.ui.activeDocument));
+ if (!this.props.documents.active) return;
+ this.props.history.push(documentEditUrl(this.props.documents.active));
}
handleLogout = () => {
@@ -87,12 +91,16 @@ type Props = {
this.modal = 'create-collection';
};
+ handleEditCollection = () => {
+ this.modal = 'edit-collection';
+ };
+
handleCloseModal = () => {
this.modal = null;
};
render() {
- const { user, auth, collections, history, ui } = this.props;
+ const { user, auth, documents, collections, history, ui } = this.props;
return (
@@ -144,13 +152,17 @@ type Props = {
Starred
-
-
-
- {ui.activeCollection
+ {collections.active
+ ?
+
+
+ :
+
+ }
+ {collections.active
?
: }
@@ -171,9 +183,22 @@ type Props = {
+
+ {collections.active &&
+ }
+
{
authenticatedStores = {
user,
documents: new DocumentsStore({
+ ui: stores.ui,
cache,
}),
collections: new CollectionsStore({
+ ui: stores.ui,
teamId: user.team.id,
cache,
}),
diff --git a/frontend/models/Collection.js b/frontend/models/Collection.js
index 65ac1e1a..13d8e5e8 100644
--- a/frontend/models/Collection.js
+++ b/frontend/models/Collection.js
@@ -67,9 +67,11 @@ class Collection extends BaseModel {
description: this.description,
});
}
- invariant(res && res.data, 'Data should be available');
- this.updateData(res.data);
- this.hasPendingChanges = false;
+ runInAction('Collection#save', () => {
+ invariant(res && res.data, 'Data should be available');
+ this.updateData(res.data);
+ this.hasPendingChanges = false;
+ });
} catch (e) {
this.errors.add('Collection failed saving');
return false;
@@ -80,6 +82,17 @@ class Collection extends BaseModel {
return true;
};
+ @action delete = async () => {
+ try {
+ const res = await client.post('/collections.delete', { id: this.id });
+ invariant(res && res.data, 'Data should be available');
+ const { data } = res;
+ return data.success;
+ } catch (e) {
+ this.errors.add('Collection failed to delete');
+ }
+ };
+
updateData(data: Object = {}) {
this.data = data;
extendObservable(this, data);
diff --git a/frontend/models/Document.js b/frontend/models/Document.js
index 82acf88e..ea39c2f6 100644
--- a/frontend/models/Document.js
+++ b/frontend/models/Document.js
@@ -155,12 +155,11 @@ class Document extends BaseModel {
// }
res = await client.post('/documents.create', data);
}
-
- invariant(res && res.data, 'Data should be available');
- this.updateData({
- ...res.data,
+ runInAction('Document#save', () => {
+ invariant(res && res.data, 'Data should be available');
+ this.updateData(res.data);
+ this.hasPendingChanges = false;
});
- this.hasPendingChanges = false;
} catch (e) {
this.errors.add('Document failed saving');
} finally {
diff --git a/frontend/scenes/CollectionEdit/CollectionEdit.js b/frontend/scenes/CollectionEdit/CollectionEdit.js
new file mode 100644
index 00000000..e3718808
--- /dev/null
+++ b/frontend/scenes/CollectionEdit/CollectionEdit.js
@@ -0,0 +1,119 @@
+// @flow
+import React, { Component } from 'react';
+import { observable } from 'mobx';
+import { observer } from 'mobx-react';
+import { homeUrl } from 'utils/routeHelpers';
+import Button from 'components/Button';
+import Input from 'components/Input';
+import Flex from 'components/Flex';
+import HelpText from 'components/HelpText';
+import Collection from 'models/Collection';
+import CollectionsStore from 'stores/CollectionsStore';
+
+type Props = {
+ history: Object,
+ collection: Collection,
+ collections: CollectionsStore,
+ onSubmit: () => void,
+};
+
+@observer class CollectionEdit extends Component {
+ props: Props;
+ @observable name: string;
+ @observable isConfirming: boolean;
+ @observable isDeleting: boolean;
+ @observable isSaving: boolean;
+
+ componentWillMount() {
+ this.name = this.props.collection.name;
+ }
+
+ handleSubmit = async (ev: SyntheticEvent) => {
+ ev.preventDefault();
+ this.isSaving = true;
+
+ this.props.collection.updateData({ name: this.name });
+ const success = await this.props.collection.save();
+
+ if (success) {
+ this.props.onSubmit();
+ }
+
+ this.isSaving = false;
+ };
+
+ handleNameChange = (ev: SyntheticInputEvent) => {
+ this.name = ev.target.value;
+ };
+
+ confirmDelete = () => {
+ this.isConfirming = true;
+ };
+
+ cancelDelete = () => {
+ this.isConfirming = false;
+ };
+
+ confirmedDelete = async (ev: SyntheticEvent) => {
+ ev.preventDefault();
+ this.isDeleting = true;
+ const success = await this.props.collection.delete();
+
+ if (success) {
+ this.props.collections.remove(this.props.collection.id);
+ this.props.history.push(homeUrl());
+ this.props.onSubmit();
+ }
+
+ this.isDeleting = false;
+ };
+
+ render() {
+ return (
+
+
+
+
+
+ );
+ }
+}
+
+export default CollectionEdit;
diff --git a/frontend/scenes/CollectionEdit/index.js b/frontend/scenes/CollectionEdit/index.js
new file mode 100644
index 00000000..205ea254
--- /dev/null
+++ b/frontend/scenes/CollectionEdit/index.js
@@ -0,0 +1,3 @@
+// @flow
+import CollectionEdit from './CollectionEdit';
+export default CollectionEdit;
diff --git a/frontend/scenes/CollectionNew/CollectionNew.js b/frontend/scenes/CollectionNew/CollectionNew.js
index 186e8fa6..4b9f7819 100644
--- a/frontend/scenes/CollectionNew/CollectionNew.js
+++ b/frontend/scenes/CollectionNew/CollectionNew.js
@@ -12,7 +12,7 @@ import CollectionsStore from 'stores/CollectionsStore';
type Props = {
history: Object,
collections: CollectionsStore,
- onCollectionCreated: () => void,
+ onSubmit: () => void,
};
@observer class CollectionNew extends Component {
@@ -34,7 +34,7 @@ type Props = {
if (success) {
this.props.collections.add(this.collection);
- this.props.onCollectionCreated();
+ this.props.onSubmit();
this.props.history.push(this.collection.url);
}
diff --git a/frontend/stores/CollectionsStore.js b/frontend/stores/CollectionsStore.js
index 98f4e2da..d90c0985 100644
--- a/frontend/stores/CollectionsStore.js
+++ b/frontend/stores/CollectionsStore.js
@@ -1,6 +1,7 @@
// @flow
import {
observable,
+ computed,
action,
runInAction,
ObservableArray,
@@ -14,12 +15,14 @@ import stores from 'stores';
import Collection from 'models/Collection';
import ErrorsStore from 'stores/ErrorsStore';
import CacheStore from 'stores/CacheStore';
+import UiStore from 'stores/UiStore';
const COLLECTION_CACHE_KEY = 'COLLECTION_CACHE_KEY';
type Options = {
teamId: string,
cache: CacheStore,
+ ui: UiStore,
};
class CollectionsStore {
@@ -30,6 +33,13 @@ class CollectionsStore {
teamId: string;
errors: ErrorsStore;
cache: CacheStore;
+ ui: UiStore;
+
+ @computed get active(): ?Collection {
+ return this.ui.activeCollectionId
+ ? this.getById(this.ui.activeCollectionId)
+ : undefined;
+ }
/* Actions */
@@ -49,8 +59,8 @@ class CollectionsStore {
}
};
- @action getById = async (id: string): Promise => {
- let collection = _.find(this.data, { id });
+ @action fetchById = async (id: string): Promise => {
+ let collection = this.getById(id);
if (!collection) {
try {
const res = await this.client.post('/collections.info', {
@@ -79,18 +89,23 @@ class CollectionsStore {
this.data.splice(this.data.indexOf(id), 1);
};
+ getById = (id: string): ?Collection => {
+ return _.find(this.data, { id });
+ };
+
constructor(options: Options) {
this.client = client;
this.errors = stores.errors;
this.teamId = options.teamId;
this.cache = options.cache;
-
- this.cache.getItem(COLLECTION_CACHE_KEY).then(data => {
- if (data) {
- this.data.replace(data.map(collection => new Collection(collection)));
- this.isLoaded = true;
- }
- });
+ this.ui = options.ui;
+ //
+ // this.cache.getItem(COLLECTION_CACHE_KEY).then(data => {
+ // if (data) {
+ // this.data.replace(data.map(collection => new Collection(collection)));
+ // this.isLoaded = true;
+ // }
+ // });
autorunAsync('CollectionsStore.persists', () => {
if (this.data.length > 0)
diff --git a/frontend/stores/DocumentsStore.js b/frontend/stores/DocumentsStore.js
index 473d9b1d..3d839a1c 100644
--- a/frontend/stores/DocumentsStore.js
+++ b/frontend/stores/DocumentsStore.js
@@ -16,11 +16,13 @@ import stores from 'stores';
import Document from 'models/Document';
import ErrorsStore from 'stores/ErrorsStore';
import CacheStore from 'stores/CacheStore';
+import UiStore from 'stores/UiStore';
const DOCUMENTS_CACHE_KEY = 'DOCUMENTS_CACHE_KEY';
type Options = {
cache: CacheStore,
+ ui: UiStore,
};
class DocumentsStore extends BaseStore {
@@ -31,6 +33,7 @@ class DocumentsStore extends BaseStore {
errors: ErrorsStore;
cache: CacheStore;
+ ui: UiStore;
/* Computed */
@@ -49,6 +52,12 @@ class DocumentsStore extends BaseStore {
return _.filter(this.data.values(), 'starred');
}
+ @computed get active(): ?Document {
+ return this.ui.activeDocumentId
+ ? this.getById(this.ui.activeDocumentId)
+ : undefined;
+ }
+
/* Actions */
@action fetchAll = async (request: string = 'list'): Promise<*> => {
@@ -127,6 +136,7 @@ class DocumentsStore extends BaseStore {
this.errors = stores.errors;
this.cache = options.cache;
+ this.ui = options.ui;
this.cache.getItem(DOCUMENTS_CACHE_KEY).then(data => {
if (data) {
diff --git a/frontend/stores/UiStore.js b/frontend/stores/UiStore.js
index 6345af8b..e8db5c5e 100644
--- a/frontend/stores/UiStore.js
+++ b/frontend/stores/UiStore.js
@@ -1,27 +1,23 @@
// @flow
-import { observable, action, computed } from 'mobx';
+import { observable, action } from 'mobx';
import Document from 'models/Document';
-import Collection from 'models/Collection';
class UiStore {
- @observable activeDocument: ?Document;
+ @observable activeDocumentId: ?string;
+ @observable activeCollectionId: ?string;
@observable progressBarVisible: boolean = false;
@observable editMode: boolean = false;
- /* Computed */
-
- @computed get activeCollection(): ?Collection {
- return this.activeDocument ? this.activeDocument.collection : undefined;
- }
-
/* Actions */
@action setActiveDocument = (document: Document): void => {
- this.activeDocument = document;
+ this.activeDocumentId = document.id;
+ this.activeCollectionId = document.collection.id;
};
@action clearActiveDocument = (): void => {
- this.activeDocument = undefined;
+ this.activeDocumentId = undefined;
+ this.activeCollectionId = undefined;
};
@action enableEditMode() {
diff --git a/frontend/styles/constants.js b/frontend/styles/constants.js
index e4f9a9a5..56bf05aa 100644
--- a/frontend/styles/constants.js
+++ b/frontend/styles/constants.js
@@ -41,6 +41,7 @@ export const color = {
/* Brand */
primary: '#2B8FBF',
+ danger: '#D0021B',
/* Dark Grays */
slate: '#9BA6B2',
diff --git a/server/api/__snapshots__/collections.test.js.snap b/server/api/__snapshots__/collections.test.js.snap
new file mode 100644
index 00000000..5002b47e
--- /dev/null
+++ b/server/api/__snapshots__/collections.test.js.snap
@@ -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,
+}
+`;
diff --git a/server/api/collections.js b/server/api/collections.js
index 2b48f40f..2e76179b 100644
--- a/server/api/collections.js
+++ b/server/api/collections.js
@@ -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;
diff --git a/server/api/collections.test.js b/server/api/collections.test.js
new file mode 100644
index 00000000..6f6b320c
--- /dev/null
+++ b/server/api/collections.test.js
@@ -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);
+ });
+});
diff --git a/server/api/documents.test.js b/server/api/documents.test.js
index 000b45bb..97ea9b02 100644
--- a/server/api/documents.test.js
+++ b/server/api/documents.test.js
@@ -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';
diff --git a/server/migrations/20170827182423-improve-references.js b/server/migrations/20170827182423-improve-references.js
new file mode 100644
index 00000000..788f3b60
--- /dev/null
+++ b/server/migrations/20170827182423-improve-references.js
@@ -0,0 +1,56 @@
+module.exports = {
+ up: async function(queryInterface, Sequelize) {
+ await queryInterface.changeColumn('documents', 'atlasId', {
+ type: Sequelize.UUID,
+ allowNull: true,
+ onDelete: 'cascade',
+ references: {
+ model: 'collections',
+ },
+ });
+ await queryInterface.changeColumn('documents', 'userId', {
+ type: Sequelize.UUID,
+ allowNull: true,
+ references: {
+ model: 'users',
+ },
+ });
+ await queryInterface.changeColumn('documents', 'parentDocumentId', {
+ type: Sequelize.UUID,
+ allowNull: true,
+ references: {
+ model: 'documents',
+ },
+ });
+ await queryInterface.changeColumn('documents', 'teamId', {
+ type: Sequelize.UUID,
+ allowNull: true,
+ onDelete: 'cascade',
+ references: {
+ model: 'teams',
+ },
+ });
+ },
+
+ down: async function(queryInterface, Sequelize) {
+ await queryInterface.sequelize.query(
+ 'ALTER TABLE documents DROP CONSTRAINT "atlasId_foreign_idx";'
+ );
+ await queryInterface.removeIndex('documents', 'atlasId_foreign_idx');
+ await queryInterface.sequelize.query(
+ 'ALTER TABLE documents DROP CONSTRAINT "userId_foreign_idx";'
+ );
+ await queryInterface.removeIndex('documents', 'userId_foreign_idx');
+ await queryInterface.sequelize.query(
+ 'ALTER TABLE documents DROP CONSTRAINT "parentDocumentId_foreign_idx";'
+ );
+ await queryInterface.removeIndex(
+ 'documents',
+ 'parentDocumentId_foreign_idx'
+ );
+ await queryInterface.sequelize.query(
+ 'ALTER TABLE documents DROP CONSTRAINT "teamId_foreign_idx";'
+ );
+ await queryInterface.removeIndex('documents', 'teamId_foreign_idx');
+ },
+};
diff --git a/server/models/Collection.js b/server/models/Collection.js
index f124eb26..45a36a77 100644
--- a/server/models/Collection.js
+++ b/server/models/Collection.js
@@ -74,6 +74,7 @@ Collection.associate = models => {
Collection.hasMany(models.Document, {
as: 'documents',
foreignKey: 'atlasId',
+ onDelete: 'cascade',
});
Collection.belongsTo(models.Team, {
as: 'team',
diff --git a/server/models/Document.js b/server/models/Document.js
index eef624d3..7ee9e425 100644
--- a/server/models/Document.js
+++ b/server/models/Document.js
@@ -112,6 +112,7 @@ Document.associate = models => {
Document.belongsTo(models.Collection, {
as: 'collection',
foreignKey: 'atlasId',
+ onDelete: 'cascade',
});
Document.belongsTo(models.User, {
as: 'createdBy',
@@ -121,6 +122,10 @@ Document.associate = models => {
as: 'updatedBy',
foreignKey: 'lastModifiedById',
});
+ Document.hasMany(models.Revision, {
+ as: 'revisions',
+ onDelete: 'cascade',
+ });
Document.hasMany(models.Star, {
as: 'starred',
});
diff --git a/server/models/Revision.js b/server/models/Revision.js
index 83107856..d69892ea 100644
--- a/server/models/Revision.js
+++ b/server/models/Revision.js
@@ -1,3 +1,4 @@
+// @flow
import { DataTypes, sequelize } from '../sequelize';
const Revision = sequelize.define('revision', {