Base model refactor (#810)

* Big upgrades

* WIP: Stash

* Stash, 30 flow errors left

* Downgrade mobx

* WIP

* When I understand the difference between class and instance methods

* 💚

* Fixes: File import
Model saving edge cases
pinning and starring docs
Collection editing
Upgrade mobx devtools

* Notification settings saving works

* Disabled settings

* Document mailer

* Working notifications

* Colletion created notification
Ensure not notified for own actions

* Tidy up

* Document updated event only for document creation
Add indexes
Notification setting on user creation

* Commentary

* Fixed: Notification setting on signup

* Fix document move / duplicate stale data
Add BaseModel.refresh method

* Fixes: Title in sidebar not updated after editing document

* 💚

* Improve / restore error handling
Better handle offline errors

* 👕
This commit is contained in:
Tom Moor
2018-12-04 22:24:30 -08:00
committed by GitHub
parent 67cd250316
commit 8cbcb77486
222 changed files with 2273 additions and 2361 deletions

View File

@@ -1,49 +1,57 @@
// @flow
import { extendObservable, action, runInAction, computed } from 'mobx';
import { action, set, computed } from 'mobx';
import invariant from 'invariant';
import { client } from 'utils/ApiClient';
import stores from 'stores';
import parseTitle from '../../shared/utils/parseTitle';
import unescape from '../../shared/utils/unescape';
import parseTitle from 'shared/utils/parseTitle';
import unescape from 'shared/utils/unescape';
import type { NavigationNode, Revision, User } from 'types';
import BaseModel from './BaseModel';
import Collection from './Collection';
import type { NavigationNode } from 'types';
import BaseModel from 'models/BaseModel';
import Revision from 'models/Revision';
import User from 'models/User';
import Collection from 'models/Collection';
type SaveOptions = { publish?: boolean, done?: boolean, autosave?: boolean };
class Document extends BaseModel {
isSaving: boolean = false;
export default class Document extends BaseModel {
isSaving: boolean;
ui: *;
store: *;
collaborators: User[];
collection: $Shape<Collection>;
collection: Collection;
collectionId: string;
firstViewedAt: ?string;
lastViewedAt: ?string;
modifiedSinceViewed: ?boolean;
createdAt: string;
createdBy: User;
updatedAt: string;
updatedBy: User;
html: string;
id: string;
team: string;
starred: boolean;
pinned: boolean;
text: string;
title: string;
emoji: string;
starred: boolean = false;
pinned: boolean = false;
text: string = '';
title: string = '';
parentDocument: ?string;
publishedAt: ?string;
url: string;
urlId: string;
shareUrl: ?string;
views: number;
revision: number;
/* Computed */
constructor(data?: Object = {}, store: *) {
super(data, store);
this.updateTitle();
}
@action
updateTitle() {
set(this, parseTitle(this.text));
}
@computed
get modifiedSinceViewed(): boolean {
@@ -59,9 +67,8 @@ class Document extends BaseModel {
if (childNode.id === this.id) {
path = newPath;
return;
} else {
return traveler(childNode.children, newPath);
}
return traveler(childNode.children, newPath);
});
};
@@ -96,44 +103,32 @@ class Document extends BaseModel {
: null;
}
/* Actions */
@action
share = async () => {
try {
const res = await client.post('/shares.create', { documentId: this.id });
invariant(res && res.data, 'Document API response should be available');
this.shareUrl = res.data.url;
} catch (e) {
this.ui.showToast('Document failed to share');
}
const res = await client.post('/shares.create', { documentId: this.id });
invariant(res && res.data, 'Share data should be available');
this.shareUrl = res.data.url;
return this.shareUrl;
};
@action
restore = async (revision: Revision) => {
try {
const res = await client.post('/documents.restore', {
id: this.id,
revisionId: revision.id,
});
runInAction('Document#save', () => {
invariant(res && res.data, 'Data should be available');
this.updateData(res.data);
});
} catch (e) {
this.ui.showToast('Document failed to restore');
}
updateFromJson = data => {
set(this, data);
this.updateTitle();
};
restore = (revision: Revision) => {
return this.store.restore(this, revision);
};
@action
pin = async () => {
this.pinned = true;
try {
await client.post('/documents.pin', { id: this.id });
} catch (e) {
await this.store.pin(this);
} catch (err) {
this.pinned = false;
this.ui.showToast('Document failed to pin');
throw err;
}
};
@@ -141,10 +136,10 @@ class Document extends BaseModel {
unpin = async () => {
this.pinned = false;
try {
await client.post('/documents.unpin', { id: this.id });
} catch (e) {
await this.store.unpin(this);
} catch (err) {
this.pinned = true;
this.ui.showToast('Document failed to unpin');
throw err;
}
};
@@ -152,10 +147,10 @@ class Document extends BaseModel {
star = async () => {
this.starred = true;
try {
await client.post('/documents.star', { id: this.id });
} catch (e) {
await this.store.star(this);
} catch (err) {
this.starred = false;
this.ui.showToast('Document failed star');
throw err;
}
};
@@ -163,10 +158,10 @@ class Document extends BaseModel {
unstar = async () => {
this.starred = false;
try {
await client.post('/documents.unstar', { id: this.id });
} catch (e) {
this.starred = false;
this.ui.showToast('Document failed unstar');
await this.store.unstar(this);
} catch (err) {
this.starred = true;
throw err;
}
};
@@ -178,28 +173,21 @@ class Document extends BaseModel {
@action
fetch = async () => {
try {
const res = await client.post('/documents.info', { id: this.id });
invariant(res && res.data, 'Document API response should be available');
const { data } = res;
runInAction('Document#update', () => {
this.updateData(data);
});
} catch (e) {
this.ui.showToast('Document failed loading');
}
const res = await client.post('/documents.info', { id: this.id });
invariant(res && res.data, 'Data should be available');
this.updateFromJson(res.data);
};
@action
save = async (options: SaveOptions) => {
if (this.isSaving) return this;
const wasDraft = !this.publishedAt;
const isCreating = !this.id;
const wasDraft = !this.publishedAt;
this.isSaving = true;
this.updateTitle();
try {
let res;
if (isCreating) {
const data = {
parentDocument: undefined,
@@ -211,77 +199,30 @@ class Document extends BaseModel {
if (this.parentDocument) {
data.parentDocument = this.parentDocument;
}
res = await client.post('/documents.create', data);
const document = await this.store.create(data);
return document;
} else {
res = await client.post('/documents.update', {
const document = await this.store.update({
id: this.id,
title: this.title,
text: this.text,
lastRevision: this.revision,
...options,
});
return document;
}
runInAction('Document#save', () => {
invariant(res && res.data, 'Data should be available');
this.updateData(res.data);
if (isCreating) {
this.emit('documents.create', this);
}
this.emit('documents.update', {
document: this,
collectionId: this.collection.id,
});
if (wasDraft && this.publishedAt) {
this.emit('documents.publish', {
id: this.id,
collectionId: this.collection.id,
});
}
});
} catch (e) {
this.ui.showToast('Document failed to save');
} finally {
if (wasDraft && options.publish) {
this.store.rootStore.collections.fetch(this.collection.id, {
force: true,
});
}
this.isSaving = false;
}
return this;
};
@action
move = async (parentDocumentId: ?string) => {
try {
const res = await client.post('/documents.move', {
id: this.id,
parentDocument: parentDocumentId,
});
invariant(res && res.data, 'Data not available');
this.updateData(res.data);
this.emit('documents.move', {
id: this.id,
collectionId: this.collection.id,
});
} catch (e) {
this.ui.showToast('Error while moving the document');
}
return;
};
@action
delete = async () => {
try {
await client.post('/documents.delete', { id: this.id });
this.emit('documents.delete', {
id: this.id,
collectionId: this.collection.id,
});
return true;
} catch (e) {
this.ui.showToast('Error while deleting the document');
}
return false;
move = (parentDocumentId: ?string) => {
return this.store.move(this, parentDocumentId);
};
duplicate = () => {
@@ -289,6 +230,7 @@ class Document extends BaseModel {
};
download = async () => {
// Ensure the document is upto date with latest server contents
await this.fetch();
const blob = new Blob([unescape(this.text)], { type: 'text/markdown' });
@@ -301,23 +243,4 @@ class Document extends BaseModel {
a.download = `${this.title}.md`;
a.click();
};
updateData(data: Object = {}) {
if (data.text) {
const { title, emoji } = parseTitle(data.text);
data.title = title;
data.emoji = emoji;
}
extendObservable(this, data);
}
constructor(data?: Object = {}) {
super();
this.updateData(data);
this.ui = stores.ui;
this.store = stores.documents;
}
}
export default Document;