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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user