chore: Move to Typescript (#2783)

This PR moves the entire project to Typescript. Due to the ~1000 ignores this will lead to a messy codebase for a while, but the churn is worth it – all of those ignore comments are places that were never type-safe previously.

closes #1282
This commit is contained in:
Tom Moor
2021-11-29 06:40:55 -08:00
committed by GitHub
parent 25ccfb5d04
commit 15b1069bcc
1017 changed files with 17410 additions and 54942 deletions

View File

@@ -1,9 +1,10 @@
// @flow
import BaseModel from "./BaseModel";
class ApiKey extends BaseModel {
id: string;
name: string;
secret: string;
}

View File

@@ -1,24 +1,26 @@
// @flow
import { set, observable } from "mobx";
export default class BaseModel {
@observable id: string;
@observable isSaving: boolean;
store: *;
@observable
id: string;
constructor(fields: Object, store: *) {
@observable
isSaving: boolean;
store: any;
constructor(fields: Record<string, any>, store: any) {
set(this, fields);
this.store = store;
}
save = async (params: ?Object) => {
save = async (params?: Record<string, any>) => {
this.isSaving = true;
try {
// ensure that the id is passed if the document has one
if (params) params = { ...params, id: this.id };
const model = await this.store.save(params || this.toJS());
// if saving is successful set the new values on the model itself
set(this, { ...params, ...model });
return model;
@@ -32,11 +34,14 @@ export default class BaseModel {
};
refresh = () => {
return this.fetch({ force: true });
return this.fetch({
force: true,
});
};
delete = async () => {
this.isSaving = true;
try {
return await this.store.delete(this);
} finally {
@@ -44,7 +49,7 @@ export default class BaseModel {
}
};
toJS = (): Object => {
toJS = (): Record<string, any> => {
return { ...this };
};
}

View File

@@ -1,129 +0,0 @@
// @flow
import { pick, trim } from "lodash";
import { action, computed, observable } from "mobx";
import BaseModel from "models/BaseModel";
import Document from "models/Document";
import type { NavigationNode } from "types";
import { client } from "utils/ApiClient";
export default class Collection extends BaseModel {
@observable isSaving: boolean;
@observable isLoadingUsers: boolean;
id: string;
name: string;
description: string;
icon: string;
color: string;
permission: "read" | "read_write" | void;
sharing: boolean;
index: string;
documents: NavigationNode[];
createdAt: string;
updatedAt: string;
deletedAt: ?string;
sort: { field: string, direction: "asc" | "desc" };
url: string;
urlId: string;
@computed
get isEmpty(): boolean {
return this.documents.length === 0;
}
@computed
get documentIds(): string[] {
const results = [];
const travelDocuments = (documentList, path) =>
documentList.forEach((document) => {
results.push(document.id);
travelDocuments(document.children);
});
travelDocuments(this.documents);
return results;
}
@computed
get hasDescription(): boolean {
return !!trim(this.description, "\\").trim();
}
@action
updateDocument(document: Document) {
const travelDocuments = (documentList, path) =>
documentList.forEach((d) => {
if (d.id === document.id) {
d.title = document.title;
d.url = document.url;
} else {
travelDocuments(d.children);
}
});
travelDocuments(this.documents);
}
@action
updateIndex(index: string) {
this.index = index;
}
getDocumentChildren(documentId: string): NavigationNode[] {
let result = [];
const traveler = (nodes) => {
nodes.forEach((childNode) => {
if (childNode.id === documentId) {
result = childNode.children;
return;
}
return traveler(childNode.children);
});
};
if (this.documents) {
traveler(this.documents);
}
return result;
}
pathToDocument(documentId: string) {
let path;
const traveler = (nodes, previousPath) => {
nodes.forEach((childNode) => {
const newPath = [...previousPath, childNode];
if (childNode.id === documentId) {
path = newPath;
return;
}
return traveler(childNode.children, newPath);
});
};
if (this.documents) {
traveler(this.documents, []);
if (path) return path;
}
return [];
}
toJS = () => {
return pick(this, [
"id",
"name",
"color",
"description",
"sharing",
"icon",
"permission",
"sort",
"index",
]);
};
export = () => {
return client.get("/collections.export", { id: this.id });
};
}

View File

@@ -1,12 +1,12 @@
/* eslint-disable */
import stores from '../stores';
import stores from "~/stores";
describe('Collection model', () => {
test('should initialize with data', () => {
const collection = stores.collections.add({
id: 123,
name: 'Engineering',
id: "123",
name: 'Engineering'
});
expect(collection.name).toBe('Engineering');
});
});
});

158
app/models/Collection.ts Normal file
View File

@@ -0,0 +1,158 @@
import { pick, trim } from "lodash";
import { action, computed, observable } from "mobx";
import BaseModel from "~/models/BaseModel";
import Document from "~/models/Document";
import { NavigationNode } from "~/types";
import { client } from "~/utils/ApiClient";
export default class Collection extends BaseModel {
@observable
isSaving: boolean;
@observable
isLoadingUsers: boolean;
id: string;
name: string;
description: string;
icon: string;
color: string;
permission: "read" | "read_write" | void;
sharing: boolean;
index: string;
documents: NavigationNode[];
createdAt: string;
updatedAt: string;
deletedAt: string | null | undefined;
sort: {
field: string;
direction: "asc" | "desc";
};
url: string;
urlId: string;
@computed
get isEmpty(): boolean {
return this.documents.length === 0;
}
@computed
get documentIds(): string[] {
const results: string[] = [];
const travelNodes = (nodes: NavigationNode[]) =>
nodes.forEach((node) => {
results.push(node.id);
travelNodes(node.children);
});
travelNodes(this.documents);
return results;
}
@computed
get hasDescription(): boolean {
return !!trim(this.description, "\\").trim();
}
@action
updateDocument(document: Document) {
const travelNodes = (nodes: NavigationNode[]) =>
nodes.forEach((node) => {
if (node.id === document.id) {
node.title = document.title;
node.url = document.url;
} else {
travelNodes(node.children);
}
});
travelNodes(this.documents);
}
@action
updateIndex(index: string) {
this.index = index;
}
getDocumentChildren(documentId: string) {
let result: NavigationNode[] = [];
const travelNodes = (nodes: NavigationNode[]) => {
nodes.forEach((node) => {
if (node.id === documentId) {
result = node.children;
return;
}
return travelNodes(node.children);
});
};
if (this.documents) {
travelNodes(this.documents);
}
return result;
}
pathToDocument(documentId: string) {
let path: NavigationNode[] | undefined;
const travelNodes = (
nodes: NavigationNode[],
previousPath: NavigationNode[]
) => {
nodes.forEach((node) => {
const newPath = [...previousPath, node];
if (node.id === documentId) {
path = newPath;
return;
}
return travelNodes(node.children, newPath);
});
};
if (this.documents) {
travelNodes(this.documents, []);
}
return path || [];
}
toJS = () => {
return pick(this, [
"id",
"name",
"color",
"description",
"sharing",
"icon",
"permission",
"sort",
"index",
]);
};
export = () => {
return client.get("/collections.export", {
id: this.id,
});
};
}

View File

@@ -1,11 +1,13 @@
// @flow
import { computed } from "mobx";
import BaseModel from "./BaseModel";
class CollectionGroupMembership extends BaseModel {
id: string;
groupId: string;
collectionId: string;
permission: string;
@computed

View File

@@ -1,52 +1,79 @@
// @flow
import { addDays, differenceInDays } from "date-fns";
import invariant from "invariant";
import { floor } from "lodash";
import { action, computed, observable, set } from "mobx";
import parseTitle from "shared/utils/parseTitle";
import unescape from "shared/utils/unescape";
import DocumentsStore from "stores/DocumentsStore";
import BaseModel from "models/BaseModel";
import User from "models/User";
import parseTitle from "@shared/utils/parseTitle";
import unescape from "@shared/utils/unescape";
import DocumentsStore from "~/stores/DocumentsStore";
import BaseModel from "~/models/BaseModel";
import User from "~/models/User";
import View from "./View";
type SaveOptions = {|
publish?: boolean,
done?: boolean,
autosave?: boolean,
lastRevision?: number,
|};
type SaveOptions = {
publish?: boolean;
done?: boolean;
autosave?: boolean;
lastRevision?: number;
};
export default class Document extends BaseModel {
@observable isSaving: boolean = false;
@observable embedsDisabled: boolean = false;
@observable lastViewedAt: ?string;
@observable
isSaving = false;
@observable
embedsDisabled = false;
@observable
lastViewedAt: string | undefined;
store: DocumentsStore;
collaboratorIds: string[];
collectionId: string;
createdAt: string;
createdBy: User;
updatedAt: string;
updatedBy: User;
id: string;
team: string;
pinned: boolean;
text: string;
title: string;
emoji: string;
template: boolean;
templateId: ?string;
parentDocumentId: ?string;
publishedAt: ?string;
templateId: string | undefined;
parentDocumentId: string | undefined;
publishedAt: string | undefined;
archivedAt: string;
deletedAt: ?string;
deletedAt: string | undefined;
url: string;
urlId: string;
tasks: { completed: number, total: number };
tasks: {
completed: number;
total: number;
};
revision: number;
constructor(fields: Object, store: DocumentsStore) {
constructor(fields: Record<string, any>, store: DocumentsStore) {
super(fields, store);
if (this.isNewDocument && this.isFromTemplate) {
@@ -73,10 +100,10 @@ export default class Document extends BaseModel {
// element must appear in body for direction to be computed
document.body?.appendChild(element);
const direction = window.getComputedStyle(element).direction;
document.body?.removeChild(element);
return direction;
return direction === "rtl" ? "rtl" : "ltr";
}
@computed
@@ -133,7 +160,7 @@ export default class Document extends BaseModel {
}
@computed
get permanentlyDeletedAt(): ?string {
get permanentlyDeletedAt(): string | undefined {
if (!this.deletedAt) {
return undefined;
}
@@ -161,16 +188,19 @@ export default class Document extends BaseModel {
if (!this.isTasks) {
return 0;
}
return floor((this.tasks.completed / this.tasks.total) * 100);
}
@action
share = async () => {
return this.store.rootStore.shares.create({ documentId: this.id });
return this.store.rootStore.shares.create({
documentId: this.id,
});
};
@action
updateFromJson = (data: Object) => {
updateFromJson = (data: Record<string, any>) => {
set(this, data);
};
@@ -178,7 +208,7 @@ export default class Document extends BaseModel {
return this.store.archive(this);
};
restore = (options: { revisionId?: string, collectionId?: string }) => {
restore = (options?: { revisionId?: string; collectionId?: string }) => {
return this.store.restore(this, options);
};
@@ -199,6 +229,7 @@ export default class Document extends BaseModel {
@action
pin = async () => {
this.pinned = true;
try {
const res = await this.store.pin(this);
invariant(res && res.data, "Data should be available");
@@ -212,6 +243,7 @@ export default class Document extends BaseModel {
@action
unpin = async () => {
this.pinned = false;
try {
const res = await this.store.unpin(this);
invariant(res && res.data, "Data should be available");
@@ -239,7 +271,9 @@ export default class Document extends BaseModel {
return;
}
return this.store.rootStore.views.create({ documentId: this.id });
return this.store.rootStore.views.create({
documentId: this.id,
});
};
@action
@@ -253,18 +287,26 @@ export default class Document extends BaseModel {
};
@action
update = async (options: {| ...SaveOptions, title: string |}) => {
update = async (
options: SaveOptions & {
title?: string;
lastRevision?: number;
}
) => {
if (this.isSaving) return this;
this.isSaving = true;
try {
if (options.lastRevision) {
return await this.store.update({
id: this.id,
title: this.title,
lastRevision: options.lastRevision,
...options,
});
return await this.store.update(
{
id: this.id,
title: options.title || this.title,
},
{
lastRevision: options.lastRevision,
}
);
}
throw new Error("Attempting to update without a lastRevision");
@@ -274,36 +316,43 @@ export default class Document extends BaseModel {
};
@action
save = async (options: ?SaveOptions) => {
save = async (options: SaveOptions | undefined) => {
if (this.isSaving) return this;
const isCreating = !this.id;
this.isSaving = true;
try {
if (isCreating) {
return await this.store.create({
parentDocumentId: this.parentDocumentId,
collectionId: this.collectionId,
title: this.title,
text: this.text,
publish: options?.publish,
done: options?.done,
autosave: options?.autosave,
});
return await this.store.create(
{
parentDocumentId: this.parentDocumentId,
collectionId: this.collectionId,
title: this.title,
text: this.text,
},
{
publish: options?.publish,
done: options?.done,
autosave: options?.autosave,
}
);
}
if (options?.lastRevision) {
return await this.store.update({
id: this.id,
title: this.title,
text: this.text,
templateId: this.templateId,
lastRevision: options?.lastRevision,
publish: options?.publish,
done: options?.done,
autosave: options?.autosave,
});
return await this.store.update(
{
id: this.id,
title: this.title,
text: this.text,
templateId: this.templateId,
},
{
lastRevision: options?.lastRevision,
publish: options?.publish,
done: options?.done,
autosave: options?.autosave,
}
);
}
throw new Error("Attempting to update without a lastRevision");
@@ -312,7 +361,7 @@ export default class Document extends BaseModel {
}
};
move = (collectionId: string, parentDocumentId: ?string) => {
move = (collectionId: string, parentDocumentId?: string | undefined) => {
return this.store.move(this.id, collectionId, parentDocumentId);
};
@@ -320,23 +369,20 @@ export default class Document extends BaseModel {
return this.store.duplicate(this);
};
getSummary = (paragraphs: number = 4) => {
getSummary = (paragraphs = 4) => {
const result = this.text.trim().split("\n").slice(0, paragraphs).join("\n");
return result;
};
download = async () => {
// Ensure the document is upto date with latest server contents
await this.fetch();
const body = unescape(this.text);
const blob = new Blob([`# ${this.title}\n\n${body}`], {
type: "text/markdown",
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
// Firefox support requires the anchor tag be in the DOM to trigger the dl
if (document.body) document.body.appendChild(a);
a.href = url;

View File

@@ -1,24 +1,33 @@
// @flow
import BaseModel from "./BaseModel";
import User from "./User";
class Event extends BaseModel {
id: string;
name: string;
modelId: ?string;
modelId: string | null | undefined;
actorId: string;
actorIpAddress: ?string;
actorIpAddress: string | null | undefined;
documentId: string;
collectionId: ?string;
collectionId: string | null | undefined;
userId: string;
createdAt: string;
actor: User;
data: {
name: string,
email: string,
title: string,
published: boolean,
templateId: string,
name: string;
email: string;
title: string;
published: boolean;
templateId: string;
};
}

View File

@@ -1,4 +1,3 @@
// @flow
import { computed } from "mobx";
import BaseModal from "./BaseModel";
import Collection from "./Collection";
@@ -6,16 +5,23 @@ import User from "./User";
class FileOperation extends BaseModal {
id: string;
state: string;
collection: ?Collection;
collection: Collection | null | undefined;
size: number;
type: string;
user: User;
createdAt: string;
@computed
get sizeInMB(): string {
const inKB = this.size / 1024;
if (inKB < 1024) {
return inKB.toFixed(2) + "KB";
}

View File

@@ -1,10 +1,12 @@
// @flow
import BaseModel from "./BaseModel";
class Group extends BaseModel {
id: string;
name: string;
memberCount: number;
updatedAt: string;
toJS = () => {

View File

@@ -1,9 +1,10 @@
// @flow
import BaseModel from "./BaseModel";
class GroupMembership extends BaseModel {
id: string;
userId: string;
groupId: string;
}

View File

@@ -1,27 +1,33 @@
// @flow
import { extendObservable, action } from "mobx";
import BaseModel from "models/BaseModel";
import { client } from "utils/ApiClient";
import BaseModel from "~/models/BaseModel";
import { client } from "~/utils/ApiClient";
type Settings = {
url: string,
channel: string,
channelId: string,
url: string;
channel: string;
channelId: string;
};
type Events = "documents.create" | "collections.create";
class Integration extends BaseModel {
id: string;
type: string;
service: string;
collectionId: string;
events: Events;
settings: Settings;
@action
update = async (data: Object) => {
await client.post("/integrations.update", { id: this.id, ...data });
update = async (data: Record<string, any>) => {
await client.post("/integrations.update", {
id: this.id,
...data,
});
extendObservable(this, data);
return true;
};

View File

@@ -1,11 +1,13 @@
// @flow
import { computed } from "mobx";
import BaseModel from "./BaseModel";
class Membership extends BaseModel {
id: string;
userId: string;
collectionId: string;
permission: string;
@computed

View File

@@ -1,8 +1,8 @@
// @flow
import BaseModel from "./BaseModel";
class NotificationSetting extends BaseModel {
id: string;
event: string;
}

View File

@@ -1,11 +1,9 @@
// @flow
import BaseModel from "./BaseModel";
class Policy extends BaseModel {
id: string;
abilities: {
[key: string]: boolean,
};
abilities: Record<string, boolean>;
}
export default Policy;

View File

@@ -1,13 +1,17 @@
// @flow
import BaseModel from "./BaseModel";
import User from "./User";
class Revision extends BaseModel {
id: string;
documentId: string;
title: string;
text: string;
createdAt: string;
createdBy: User;
}

View File

@@ -1,18 +1,27 @@
// @flow
import BaseModel from "./BaseModel";
import User from "./User";
class Share extends BaseModel {
id: string;
url: string;
published: boolean;
documentId: string;
documentTitle: string;
documentUrl: string;
lastAccessedAt: ?string;
lastAccessedAt: string | null | undefined;
includeChildDocuments: boolean;
createdBy: User;
createdAt: string;
updatedAt: string;
}

View File

@@ -1,18 +1,27 @@
// @flow
import { computed } from "mobx";
import BaseModel from "./BaseModel";
class Team extends BaseModel {
id: string;
name: string;
avatarUrl: string;
sharing: boolean;
collaborativeEditing: boolean;
documentEmbeds: boolean;
guestSignin: boolean;
subdomain: ?string;
domain: ?string;
subdomain: string | null | undefined;
domain: string | null | undefined;
url: string;
defaultUserRole: string;
@computed

View File

@@ -1,19 +1,28 @@
// @flow
import { computed } from "mobx";
import type { Role } from "shared/types";
import { Role } from "@shared/types";
import BaseModel from "./BaseModel";
class User extends BaseModel {
avatarUrl: string;
id: string;
name: string;
email: string;
color: string;
isAdmin: boolean;
isViewer: boolean;
lastActiveAt: string;
isSuspended: boolean;
createdAt: string;
language: string;
@computed

View File

@@ -1,14 +1,18 @@
// @flow
import { action } from "mobx";
import BaseModel from "./BaseModel";
import User from "./User";
class View extends BaseModel {
id: string;
documentId: string;
firstViewedAt: string;
lastViewedAt: string;
count: number;
user: User;
@action