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,10 +1,9 @@
// @flow
import ApiKey from "models/ApiKey";
import BaseStore from "./BaseStore";
import ApiKey from "~/models/ApiKey";
import BaseStore, { RPCAction } from "./BaseStore";
import RootStore from "./RootStore";
export default class ApiKeysStore extends BaseStore<ApiKey> {
actions = ["list", "create", "delete"];
actions = [RPCAction.List, RPCAction.Create, RPCAction.Delete];
constructor(rootStore: RootStore) {
super(rootStore, ApiKey);

View File

@@ -1,54 +1,71 @@
// @flow
import * as Sentry from "@sentry/react";
import invariant from "invariant";
import { observable, action, computed, autorun, runInAction } from "mobx";
import { getCookie, setCookie, removeCookie } from "tiny-cookie";
import RootStore from "stores/RootStore";
import Policy from "models/Policy";
import Team from "models/Team";
import User from "models/User";
import env from "env";
import { client } from "utils/ApiClient";
import { getCookieDomain } from "utils/domains";
import RootStore from "~/stores/RootStore";
import Policy from "~/models/Policy";
import Team from "~/models/Team";
import User from "~/models/User";
import env from "~/env";
import { client } from "~/utils/ApiClient";
import { getCookieDomain } from "~/utils/domains";
const AUTH_STORE = "AUTH_STORE";
const NO_REDIRECT_PATHS = ["/", "/create", "/home"];
type PersistedData = {
user?: User,
team?: Team,
policies?: Policy[],
user?: User;
team?: Team;
policies?: Policy[];
};
type Provider = {|
id: string,
name: string,
authUrl: string,
|};
type Provider = {
id: string;
name: string;
authUrl: string;
};
type Config = {|
name?: string,
hostname?: string,
providers: Provider[],
|};
type Config = {
name?: string;
hostname?: string;
providers: Provider[];
};
export default class AuthStore {
@observable user: ?User;
@observable team: ?Team;
@observable token: ?string;
@observable policies: Policy[] = [];
@observable lastSignedIn: ?string;
@observable isSaving: boolean = false;
@observable isSuspended: boolean = false;
@observable suspendedContactEmail: ?string;
@observable config: ?Config;
@observable
user: User | null | undefined;
@observable
team: Team | null | undefined;
@observable
token: string | null | undefined;
@observable
policies: Policy[] = [];
@observable
lastSignedIn: string | null | undefined;
@observable
isSaving = false;
@observable
isSuspended = false;
@observable
suspendedContactEmail: string | null | undefined;
@observable
config: Config | null | undefined;
rootStore: RootStore;
constructor(rootStore: RootStore) {
this.rootStore = rootStore;
// attempt to load the previous state of this store from localstorage
let data: PersistedData = {};
try {
data = JSON.parse(localStorage.getItem(AUTH_STORE) || "{}");
} catch (_) {
@@ -56,7 +73,6 @@ export default class AuthStore {
}
this.rehydrate(data);
// persists this entire store to localstorage whenever any keys are changed
autorun(() => {
try {
@@ -65,13 +81,13 @@ export default class AuthStore {
// no-op Safari private mode
}
});
// listen to the localstorage value changing in other tabs to react to
// signin/signout events in other tabs and follow suite.
window.addEventListener("storage", (event) => {
if (event.key === AUTH_STORE) {
const data: ?PersistedData = JSON.parse(event.newValue);
if (event.key === AUTH_STORE && event.newValue) {
const data: PersistedData | null | undefined = JSON.parse(
event.newValue
);
// data may be null if key is deleted in localStorage
if (!data) return;
@@ -90,8 +106,8 @@ export default class AuthStore {
@action
rehydrate(data: PersistedData) {
this.user = new User(data.user);
this.team = new Team(data.team);
this.user = data.user ? new User(data.user, this) : undefined;
this.team = data.team ? new Team(data.team, this) : undefined;
this.token = getCookie("accessToken");
this.lastSignedIn = getCookie("lastSignedIn");
this.addPolicies(data.policies);
@@ -135,16 +151,17 @@ export default class AuthStore {
try {
const res = await client.post("/auth.info");
invariant(res && res.data, "Auth not available");
runInAction("AuthStore#fetch", () => {
this.addPolicies(res.policies);
const { user, team } = res.data;
this.user = new User(user);
this.team = new Team(team);
this.user = new User(user, this);
this.team = new Team(team, this);
if (env.SENTRY_DSN) {
Sentry.configureScope(function (scope) {
scope.setUser({ id: user.id });
scope.setUser({
id: user.id,
});
scope.setExtra("team", team.name);
scope.setExtra("teamId", team.id);
});
@@ -152,6 +169,7 @@ export default class AuthStore {
// If we came from a redirect then send the user immediately there
const postLoginRedirectPath = getCookie("postLoginRedirectPath");
if (postLoginRedirectPath) {
removeCookie("postLoginRedirectPath");
@@ -170,8 +188,9 @@ export default class AuthStore {
@action
deleteUser = async () => {
await client.post(`/users.delete`, { confirmation: true });
await client.post(`/users.delete`, {
confirmation: true,
});
runInAction("AuthStore#updateUser", () => {
this.user = null;
this.team = null;
@@ -180,16 +199,19 @@ export default class AuthStore {
};
@action
updateUser = async (params: { name?: string, avatarUrl: ?string }) => {
updateUser = async (params: {
name?: string;
avatarUrl?: string | null;
language?: string;
}) => {
this.isSaving = true;
try {
const res = await client.post(`/users.update`, params);
invariant(res && res.data, "User response not available");
runInAction("AuthStore#updateUser", () => {
this.addPolicies(res.policies);
this.user = new User(res.data);
this.user = new User(res.data, this);
});
} finally {
this.isSaving = false;
@@ -198,19 +220,20 @@ export default class AuthStore {
@action
updateTeam = async (params: {
name?: string,
avatarUrl?: ?string,
sharing?: boolean,
name?: string;
avatarUrl?: string | null | undefined;
sharing?: boolean;
collaborativeEditing?: boolean;
subdomain?: string | null | undefined;
}) => {
this.isSaving = true;
try {
const res = await client.post(`/team.update`, params);
invariant(res && res.data, "Team response not available");
runInAction("AuthStore#updateTeam", () => {
this.addPolicies(res.policies);
this.team = new Team(res.data);
this.team = new Team(res.data, this);
});
} finally {
this.isSaving = false;
@@ -218,7 +241,7 @@ export default class AuthStore {
};
@action
logout = async (savePath: boolean = false) => {
logout = async (savePath = false) => {
// remove user and team from localStorage
localStorage.setItem(
AUTH_STORE,
@@ -228,7 +251,6 @@ export default class AuthStore {
policies: [],
})
);
this.token = null;
// if this logout was forced from an authenticated route then
@@ -242,14 +264,15 @@ export default class AuthStore {
}
// remove authentication token itself
removeCookie("accessToken", { path: "/" });
removeCookie("accessToken", {
path: "/",
});
// remove session record on apex cookie
const team = this.team;
if (team) {
const sessions = JSON.parse(getCookie("sessions") || "{}");
delete sessions[team.id];
setCookie("sessions", JSON.stringify(sessions), {
domain: getCookieDomain(window.location.hostname),
});

View File

@@ -1,15 +1,31 @@
// @flow
import invariant from "invariant";
import { orderBy } from "lodash";
import { observable, set, action, computed, runInAction } from "mobx";
import RootStore from "stores/RootStore";
import BaseModel from "../models/BaseModel";
import type { PaginationParams } from "types";
import { client } from "utils/ApiClient";
import { Class } from "utility-types";
import RootStore from "~/stores/RootStore";
import BaseModel from "~/models/BaseModel";
import Policy from "~/models/Policy";
import { PaginationParams } from "~/types";
import { client } from "~/utils/ApiClient";
type Action = "list" | "info" | "create" | "update" | "delete" | "count";
type PartialWithId<T> = Partial<T> & { id: string };
function modelNameFromClassName(string) {
export enum RPCAction {
Info = "info",
List = "list",
Create = "create",
Update = "update",
Delete = "delete",
Count = "count",
}
type FetchPageParams = PaginationParams & {
documentId?: string;
query?: string;
filter?: string;
};
function modelNameFromClassName(string: string) {
return string.charAt(0).toLowerCase() + string.slice(1);
}
@@ -17,16 +33,33 @@ export const DEFAULT_PAGINATION_LIMIT = 25;
export const PAGINATION_SYMBOL = Symbol.for("pagination");
export default class BaseStore<T: BaseModel> {
@observable data: Map<string, T> = new Map();
@observable isFetching: boolean = false;
@observable isSaving: boolean = false;
@observable isLoaded: boolean = false;
export default class BaseStore<T extends BaseModel> {
@observable
data: Map<string, T> = new Map();
@observable
isFetching = false;
@observable
isSaving = false;
@observable
isLoaded = false;
model: Class<T>;
modelName: string;
rootStore: RootStore;
actions: Action[] = ["list", "info", "create", "update", "delete", "count"];
actions = [
RPCAction.Info,
RPCAction.List,
RPCAction.Create,
RPCAction.Update,
RPCAction.Delete,
RPCAction.Count,
];
constructor(rootStore: RootStore, model: Class<T>) {
this.rootStore = rootStore;
@@ -39,24 +72,27 @@ export default class BaseStore<T: BaseModel> {
this.data.clear();
}
addPolicies = (policies) => {
addPolicies = (policies: Policy[]) => {
if (policies) {
policies.forEach((policy) => this.rootStore.policies.add(policy));
}
};
@action
add = (item: Object): T => {
const Model = this.model;
add = (item: PartialWithId<T> | T): T => {
const ModelClass = this.model;
if (!(item instanceof Model)) {
const existing: ?T = this.data.get(item.id);
if (existing) {
set(existing, item);
return existing;
} else {
item = new Model(item, this);
if (!(item instanceof ModelClass)) {
const existingModel = this.data.get(item.id);
if (existingModel) {
set(existingModel, item);
return existingModel;
}
const newModel = new ModelClass(item, this);
this.data.set(newModel.id, newModel);
return newModel;
}
this.data.set(item.id, item);
@@ -68,27 +104,33 @@ export default class BaseStore<T: BaseModel> {
this.data.delete(id);
}
save(params: Object) {
save(params: Partial<T>): Promise<T> {
if (params.id) return this.update(params);
return this.create(params);
}
get(id: string): ?T {
get(id: string): T | undefined {
return this.data.get(id);
}
@action
async create(params: Object) {
if (!this.actions.includes("create")) {
async create(
params: Partial<T>,
options?: Record<string, string | boolean | number | undefined>
): Promise<T> {
if (!this.actions.includes(RPCAction.Create)) {
throw new Error(`Cannot create ${this.modelName}`);
}
this.isSaving = true;
try {
const res = await client.post(`/${this.modelName}s.create`, params);
const res = await client.post(`/${this.modelName}s.create`, {
...params,
...options,
});
invariant(res && res.data, "Data should be available");
this.addPolicies(res.policies);
return this.add(res.data);
} finally {
@@ -97,17 +139,23 @@ export default class BaseStore<T: BaseModel> {
}
@action
async update(params: Object): * {
if (!this.actions.includes("update")) {
async update(
params: Partial<T>,
options?: Record<string, string | boolean | number | undefined>
): Promise<T> {
if (!this.actions.includes(RPCAction.Update)) {
throw new Error(`Cannot update ${this.modelName}`);
}
this.isSaving = true;
try {
const res = await client.post(`/${this.modelName}s.update`, params);
const res = await client.post(`/${this.modelName}s.update`, {
...params,
...options,
});
invariant(res && res.data, "Data should be available");
this.addPolicies(res.policies);
return this.add(res.data);
} finally {
@@ -116,10 +164,11 @@ export default class BaseStore<T: BaseModel> {
}
@action
async delete(item: T, options: Object = {}) {
if (!this.actions.includes("delete")) {
async delete(item: T, options: Record<string, any> = {}) {
if (!this.actions.includes(RPCAction.Delete)) {
throw new Error(`Cannot delete ${this.modelName}`);
}
this.isSaving = true;
try {
@@ -134,27 +183,27 @@ export default class BaseStore<T: BaseModel> {
}
@action
async fetch(id: string, options: Object = {}): Promise<*> {
if (!this.actions.includes("info")) {
async fetch(id: string, options: Record<string, any> = {}): Promise<T> {
if (!this.actions.includes(RPCAction.Info)) {
throw new Error(`Cannot fetch ${this.modelName}`);
}
const item = this.data.get(id);
if (item && !options.force) return item;
this.isFetching = true;
try {
const res = await client.post(`/${this.modelName}s.info`, { id });
const res = await client.post(`/${this.modelName}s.info`, {
id,
});
invariant(res && res.data, "Data should be available");
this.addPolicies(res.policies);
return this.add(res.data);
} catch (err) {
if (err.statusCode === 403) {
this.remove(id);
}
throw err;
} finally {
this.isFetching = false;
@@ -162,15 +211,15 @@ export default class BaseStore<T: BaseModel> {
}
@action
fetchPage = async (params: ?PaginationParams): Promise<*> => {
if (!this.actions.includes("list")) {
fetchPage = async (params: FetchPageParams | undefined): Promise<any> => {
if (!this.actions.includes(RPCAction.List)) {
throw new Error(`Cannot list ${this.modelName}`);
}
this.isFetching = true;
try {
const res = await client.post(`/${this.modelName}s.list`, params);
invariant(res && res.data, "Data not available");
runInAction(`list#${this.modelName}`, () => {
@@ -179,7 +228,7 @@ export default class BaseStore<T: BaseModel> {
this.isLoaded = true;
});
let response = res.data;
const response = res.data;
response[PAGINATION_SYMBOL] = res.pagination;
return response;
} finally {

View File

@@ -1,28 +1,27 @@
// @flow
import invariant from "invariant";
import { action, runInAction } from "mobx";
import CollectionGroupMembership from "models/CollectionGroupMembership";
import BaseStore from "./BaseStore";
import CollectionGroupMembership from "~/models/CollectionGroupMembership";
import { PaginationParams } from "~/types";
import { client } from "~/utils/ApiClient";
import BaseStore, { RPCAction } from "./BaseStore";
import RootStore from "./RootStore";
import type { PaginationParams } from "types";
import { client } from "utils/ApiClient";
export default class CollectionGroupMembershipsStore extends BaseStore<CollectionGroupMembership> {
actions = ["create", "delete"];
export default class CollectionGroupMembershipsStore extends BaseStore<
CollectionGroupMembership
> {
actions = [RPCAction.Create, RPCAction.Delete];
constructor(rootStore: RootStore) {
super(rootStore, CollectionGroupMembership);
}
@action
fetchPage = async (params: ?PaginationParams): Promise<*> => {
fetchPage = async (params: PaginationParams | undefined): Promise<any> => {
this.isFetching = true;
try {
const res = await client.post(`/collections.group_memberships`, params);
invariant(res && res.data, "Data not available");
runInAction(`CollectionGroupMembershipsStore#fetchPage`, () => {
res.data.groups.forEach(this.rootStore.groups.add);
res.data.collectionGroupMemberships.forEach(this.add);
@@ -40,9 +39,9 @@ export default class CollectionGroupMembershipsStore extends BaseStore<Collectio
groupId,
permission,
}: {
collectionId: string,
groupId: string,
permission: string,
collectionId: string;
groupId: string;
permission: string;
}) {
const res = await client.post("/collections.add_group", {
id: collectionId,
@@ -51,7 +50,8 @@ export default class CollectionGroupMembershipsStore extends BaseStore<Collectio
});
invariant(res && res.data, "Membership data should be available");
res.data.collectionGroupMemberships.forEach(this.add);
const cgm = res.data.collectionGroupMemberships.map(this.add);
return cgm[0];
}
@action
@@ -59,14 +59,13 @@ export default class CollectionGroupMembershipsStore extends BaseStore<Collectio
collectionId,
groupId,
}: {
collectionId: string,
groupId: string,
collectionId: string;
groupId: string;
}) {
await client.post("/collections.remove_group", {
id: collectionId,
groupId,
});
this.remove(`${groupId}-${collectionId}`);
}

View File

@@ -1,22 +1,27 @@
// @flow
import invariant from "invariant";
import { concat, find, last } from "lodash";
import { computed, action } from "mobx";
import Collection from "models/Collection";
import Collection from "~/models/Collection";
import { NavigationNode } from "~/types";
import { client } from "~/utils/ApiClient";
import BaseStore from "./BaseStore";
import RootStore from "./RootStore";
import { client } from "utils/ApiClient";
enum DocumentPathItemType {
Collection = "collection",
Document = "document",
}
export type DocumentPathItem = {
id: string,
collectionId: string,
title: string,
url: string,
type: "collection" | "document",
type: DocumentPathItemType;
id: string;
collectionId: string;
title: string;
url: string;
};
export type DocumentPath = DocumentPathItem & {
path: DocumentPathItem[],
path: DocumentPathItem[];
};
export default class CollectionsStore extends BaseStore<Collection> {
@@ -25,7 +30,7 @@ export default class CollectionsStore extends BaseStore<Collection> {
}
@computed
get active(): ?Collection {
get active(): Collection | null | undefined {
return this.rootStore.ui.activeCollectionId
? this.data.get(this.rootStore.ui.activeCollectionId)
: undefined;
@@ -34,15 +39,14 @@ export default class CollectionsStore extends BaseStore<Collection> {
@computed
get orderedData(): Collection[] {
let collections = Array.from(this.data.values());
collections = collections.filter((collection) =>
collection.deletedAt ? false : true
);
return collections.sort((a, b) => {
if (a.index === b.index) {
return a.updatedAt > b.updatedAt ? -1 : 1;
}
return a.index < b.index ? -1 : 1;
});
}
@@ -52,11 +56,22 @@ export default class CollectionsStore extends BaseStore<Collection> {
*/
@computed
get pathsToDocuments(): DocumentPath[] {
let results = [];
const travelDocuments = (documentList, collectionId, path) =>
documentList.forEach((document) => {
const results: DocumentPathItem[][] = [];
const travelDocuments = (
documentList: NavigationNode[],
collectionId: string,
path: DocumentPathItem[]
) =>
documentList.forEach((document: NavigationNode) => {
const { id, title, url } = document;
const node = { id, collectionId, title, url, type: "document" };
const node = {
type: DocumentPathItemType.Document,
id,
collectionId,
title,
url,
};
results.push(concat(path, node));
travelDocuments(document.children, collectionId, concat(path, [node]));
});
@@ -65,11 +80,11 @@ export default class CollectionsStore extends BaseStore<Collection> {
this.data.forEach((collection) => {
const { id, name, url } = collection;
const node = {
type: DocumentPathItemType.Collection,
id,
collectionId: id,
title: name,
url,
type: "collection",
};
results.push([node]);
travelDocuments(collection.documents, id, [node]);
@@ -77,11 +92,8 @@ export default class CollectionsStore extends BaseStore<Collection> {
}
return results.map((result) => {
const tail = last(result);
return {
...tail,
path: result,
};
const tail = last(result) as DocumentPathItem;
return { ...tail, path: result };
});
}
@@ -100,7 +112,6 @@ export default class CollectionsStore extends BaseStore<Collection> {
index,
});
invariant(res && res.success, "Collection could not be moved");
const collection = this.get(collectionId);
if (collection) {
@@ -108,7 +119,7 @@ export default class CollectionsStore extends BaseStore<Collection> {
}
};
async update(params: Object): Promise<Collection> {
async update(params: Record<string, any>): Promise<Collection> {
const result = await super.update(params);
// If we're changing sharing permissions on the collection then we need to
@@ -116,6 +127,7 @@ export default class CollectionsStore extends BaseStore<Collection> {
// are now invalid
if (params.sharing !== undefined) {
const collection = this.get(params.id);
if (collection) {
collection.documentIds.forEach((id) => {
this.rootStore.policies.remove(id);
@@ -127,45 +139,48 @@ export default class CollectionsStore extends BaseStore<Collection> {
}
@action
async fetch(id: string, options: Object = {}): Promise<*> {
async fetch(id: string, options: Record<string, any> = {}): Promise<any> {
const item = this.get(id) || this.getByUrl(id);
if (item && !options.force) return item;
this.isFetching = true;
try {
const res = await client.post(`/collections.info`, { id });
const res = await client.post(`/collections.info`, {
id,
});
invariant(res && res.data, "Collection not available");
this.addPolicies(res.policies);
return this.add(res.data);
} catch (err) {
if (err.statusCode === 403) {
this.remove(id);
}
throw err;
} finally {
this.isFetching = false;
}
}
getPathForDocument(documentId: string): ?DocumentPath {
getPathForDocument(documentId: string): DocumentPath | undefined {
return this.pathsToDocuments.find((path) => path.id === documentId);
}
titleForDocument(documentUrl: string): ?string {
titleForDocument(documentUrl: string): string | undefined {
const path = this.pathsToDocuments.find((path) => path.url === documentUrl);
if (path) return path.title;
if (path) {
return path.title;
}
return;
}
getByUrl(url: string): ?Collection {
getByUrl(url: string): Collection | null | undefined {
return find(this.orderedData, (col: Collection) => url.endsWith(col.urlId));
}
delete = async (collection: Collection) => {
await super.delete(collection);
this.rootStore.documents.fetchRecentlyUpdated();
this.rootStore.documents.fetchRecentlyViewed();
};

View File

@@ -1,27 +1,39 @@
// @flow
import { observable, action } from "mobx";
import * as React from "react";
import { v4 as uuidv4 } from "uuid";
export default class DialogsStore {
@observable guide: {
title: string,
content: React.Node,
isOpen: boolean,
@observable
guide: {
title: string;
content: React.ReactNode;
isOpen: boolean;
};
@observable modalStack = new Map<
@observable
modalStack = new Map<
string,
{
title: string,
content: React.Node,
isOpen: boolean,
title: string;
content: React.ReactNode;
isOpen: boolean;
}
>();
openGuide = ({ title, content }: { title: string, content: React.Node }) => {
openGuide = ({
title,
content,
}: {
title: string;
content: React.ReactNode;
}) => {
setTimeout(
action(() => {
this.guide = { title, content, isOpen: true };
this.guide = {
title,
content,
isOpen: true,
};
}),
0
);
@@ -39,9 +51,9 @@ export default class DialogsStore {
content,
replace,
}: {
title: string,
content: React.Node,
replace?: boolean,
title: string;
content: React.ReactNode;
replace?: boolean;
}) => {
setTimeout(
action(() => {

View File

@@ -1,12 +1,19 @@
// @flow
import { observable, action } from "mobx";
import { USER_PRESENCE_INTERVAL } from "shared/constants";
import { USER_PRESENCE_INTERVAL } from "@shared/constants";
type DocumentPresence = Map<string, { isEditing: boolean, userId: string }>;
type DocumentPresence = Map<
string,
{
isEditing: boolean;
userId: string;
}
>;
export default class PresenceStore {
@observable data: Map<string, DocumentPresence> = new Map();
timeouts: Map<string, TimeoutID> = new Map();
@observable
data: Map<string, DocumentPresence> = new Map();
timeouts: Map<string, ReturnType<typeof setTimeout>> = new Map();
// called to setup when we get the initial state from document.presence
// websocket message. overrides any existing state
@@ -22,6 +29,7 @@ export default class PresenceStore {
@action
leave(documentId: string, userId: string) {
const existing = this.data.get(documentId);
if (existing) {
existing.delete(userId);
}
@@ -30,7 +38,10 @@ export default class PresenceStore {
@action
update(documentId: string, userId: string, isEditing: boolean) {
const existing = this.data.get(documentId) || new Map();
existing.set(userId, { isEditing, userId });
existing.set(userId, {
isEditing,
userId,
});
this.data.set(documentId, existing);
}
@@ -43,6 +54,7 @@ export default class PresenceStore {
touch(documentId: string, userId: string, isEditing: boolean) {
const id = `${documentId}-${userId}`;
let timeout = this.timeouts.get(id);
if (timeout) {
clearTimeout(timeout);
this.timeouts.delete(id);
@@ -58,7 +70,7 @@ export default class PresenceStore {
}
}
get(documentId: string): ?DocumentPresence {
get(documentId: string): DocumentPresence | null | undefined {
return this.data.get(documentId);
}

View File

@@ -1,32 +1,43 @@
// @flow
import path from "path";
import invariant from "invariant";
import { find, orderBy, filter, compact, omitBy } from "lodash";
import { observable, action, computed, runInAction } from "mobx";
import { MAX_TITLE_LENGTH } from "shared/constants";
import { subtractDate } from "shared/utils/date";
import naturalSort from "shared/utils/naturalSort";
import BaseStore from "stores/BaseStore";
import RootStore from "stores/RootStore";
import Document from "models/Document";
import env from "env";
import type {
import { MAX_TITLE_LENGTH } from "@shared/constants";
import { DateFilter } from "@shared/types";
import { subtractDate } from "@shared/utils/date";
import naturalSort from "@shared/utils/naturalSort";
import BaseStore from "~/stores/BaseStore";
import RootStore from "~/stores/RootStore";
import Document from "~/models/Document";
import env from "~/env";
import {
NavigationNode,
FetchOptions,
PaginationParams,
SearchResult,
} from "types";
import { client } from "utils/ApiClient";
} from "~/types";
import { client } from "~/utils/ApiClient";
type FetchParams = PaginationParams & { collectionId: string };
type FetchPageParams = PaginationParams & { template?: boolean };
type ImportOptions = {
publish?: boolean,
publish?: boolean;
};
export default class DocumentsStore extends BaseStore<Document> {
@observable searchCache: Map<string, SearchResult[]> = new Map();
@observable starredIds: Map<string, boolean> = new Map();
@observable backlinks: Map<string, string[]> = new Map();
@observable movingDocumentId: ?string;
@observable
searchCache: Map<string, SearchResult[]> = new Map();
@observable
starredIds: Map<string, boolean> = new Map();
@observable
backlinks: Map<string, string[]> = new Map();
@observable
movingDocumentId: string | null | undefined;
importFileTypes: string[] = [
".md",
@@ -54,7 +65,7 @@ export default class DocumentsStore extends BaseStore<Document> {
@computed
get recentlyViewed(): Document[] {
return orderBy(
filter(this.all, (d) => d.lastViewedAt),
this.all.filter((d) => d.lastViewedAt),
"lastViewedAt",
"desc"
);
@@ -123,6 +134,7 @@ export default class DocumentsStore extends BaseStore<Document> {
rootInCollection(collectionId: string): Document[] {
const collection = this.rootStore.collections.get(collectionId);
if (!collection) {
return [];
}
@@ -156,7 +168,7 @@ export default class DocumentsStore extends BaseStore<Document> {
get starred(): Document[] {
return orderBy(
filter(this.all, (d) => d.isStarred),
this.all.filter((d) => d.isStarred),
"updatedAt",
"desc"
);
@@ -164,16 +176,14 @@ export default class DocumentsStore extends BaseStore<Document> {
@computed
get archived(): Document[] {
return filter(
orderBy(this.orderedData, "archivedAt", "desc"),
return orderBy(this.orderedData, "archivedAt", "desc").filter(
(d) => d.archivedAt && !d.deletedAt
);
}
@computed
get deleted(): Document[] {
return filter(
orderBy(this.orderedData, "deletedAt", "desc"),
return orderBy(this.orderedData, "deletedAt", "desc").filter(
(d) => d.deletedAt
);
}
@@ -194,10 +204,9 @@ export default class DocumentsStore extends BaseStore<Document> {
}
drafts = (
options: {
...PaginationParams,
dateFilter?: "day" | "week" | "month" | "year",
collectionId?: string,
options: PaginationParams & {
dateFilter?: DateFilter;
collectionId?: string;
} = {}
): Document[] => {
let drafts = filter(
@@ -215,31 +224,36 @@ export default class DocumentsStore extends BaseStore<Document> {
}
if (options.collectionId) {
drafts = filter(drafts, { collectionId: options.collectionId });
drafts = filter(drafts, {
collectionId: options.collectionId,
});
}
return drafts;
};
@computed
get active(): ?Document {
get active(): Document | null | undefined {
return this.rootStore.ui.activeDocumentId
? this.data.get(this.rootStore.ui.activeDocumentId)
: undefined;
}
@action
fetchBacklinks = async (documentId: string): Promise<?(Document[])> => {
fetchBacklinks = async (documentId: string): Promise<void> => {
const res = await client.post(`/documents.list`, {
backlinkDocumentId: documentId,
});
invariant(res && res.data, "Document list not available");
const { data } = res;
runInAction("DocumentsStore#fetchBacklinks", () => {
data.forEach(this.add);
this.addPolicies(res.policies);
this.backlinks.set(
documentId,
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'doc' implicitly has an 'any' type.
data.map((doc) => doc.id)
);
});
@@ -255,12 +269,13 @@ export default class DocumentsStore extends BaseStore<Document> {
}
@action
fetchChildDocuments = async (documentId: string): Promise<?(Document[])> => {
fetchChildDocuments = async (documentId: string): Promise<void> => {
const res = await client.post(`/documents.list`, {
parentDocumentId: documentId,
});
invariant(res && res.data, "Document list not available");
const { data } = res;
runInAction("DocumentsStore#fetchChildDocuments", () => {
data.forEach(this.add);
this.addPolicies(res.policies);
@@ -269,9 +284,9 @@ export default class DocumentsStore extends BaseStore<Document> {
@action
fetchNamedPage = async (
request: string = "list",
options: ?Object
): Promise<?(Document[])> => {
request = "list",
options: FetchPageParams | undefined
): Promise<Document[] | undefined> => {
this.isFetching = true;
try {
@@ -289,27 +304,27 @@ export default class DocumentsStore extends BaseStore<Document> {
};
@action
fetchArchived = async (options: ?PaginationParams): Promise<*> => {
fetchArchived = async (options?: PaginationParams): Promise<any> => {
return this.fetchNamedPage("archived", options);
};
@action
fetchDeleted = async (options: ?PaginationParams): Promise<*> => {
fetchDeleted = async (options?: PaginationParams): Promise<any> => {
return this.fetchNamedPage("deleted", options);
};
@action
fetchRecentlyUpdated = async (options: ?PaginationParams): Promise<*> => {
fetchRecentlyUpdated = async (options?: PaginationParams): Promise<any> => {
return this.fetchNamedPage("list", options);
};
@action
fetchTemplates = async (options: ?PaginationParams): Promise<*> => {
fetchTemplates = async (options?: PaginationParams): Promise<any> => {
return this.fetchNamedPage("list", { ...options, template: true });
};
@action
fetchAlphabetical = async (options: ?PaginationParams): Promise<*> => {
fetchAlphabetical = async (options?: PaginationParams): Promise<any> => {
return this.fetchNamedPage("list", {
sort: "title",
direction: "ASC",
@@ -319,8 +334,8 @@ export default class DocumentsStore extends BaseStore<Document> {
@action
fetchLeastRecentlyUpdated = async (
options: ?PaginationParams
): Promise<*> => {
options?: PaginationParams
): Promise<any> => {
return this.fetchNamedPage("list", {
sort: "updatedAt",
direction: "ASC",
@@ -329,7 +344,7 @@ export default class DocumentsStore extends BaseStore<Document> {
};
@action
fetchRecentlyPublished = async (options: ?PaginationParams): Promise<*> => {
fetchRecentlyPublished = async (options?: PaginationParams): Promise<any> => {
return this.fetchNamedPage("list", {
sort: "publishedAt",
direction: "DESC",
@@ -338,27 +353,27 @@ export default class DocumentsStore extends BaseStore<Document> {
};
@action
fetchRecentlyViewed = async (options: ?PaginationParams): Promise<*> => {
fetchRecentlyViewed = async (options?: PaginationParams): Promise<any> => {
return this.fetchNamedPage("viewed", options);
};
@action
fetchStarred = (options: ?PaginationParams): Promise<*> => {
fetchStarred = (options?: PaginationParams): Promise<any> => {
return this.fetchNamedPage("starred", options);
};
@action
fetchDrafts = (options: ?PaginationParams): Promise<*> => {
fetchDrafts = (options?: PaginationParams): Promise<any> => {
return this.fetchNamedPage("drafts", options);
};
@action
fetchPinned = (options: ?PaginationParams): Promise<*> => {
fetchPinned = (options?: FetchParams): Promise<any> => {
return this.fetchNamedPage("pinned", options);
};
@action
fetchOwned = (options: ?PaginationParams): Promise<*> => {
fetchOwned = (options?: PaginationParams): Promise<any> => {
return this.fetchNamedPage("list", options);
};
@@ -368,7 +383,6 @@ export default class DocumentsStore extends BaseStore<Document> {
query,
});
invariant(res && res.data, "Search response should be available");
// add the documents and associated policies to the store
res.data.forEach(this.add);
this.addPolicies(res.policies);
@@ -379,13 +393,13 @@ export default class DocumentsStore extends BaseStore<Document> {
search = async (
query: string,
options: {
offset?: number,
limit?: number,
dateFilter?: "day" | "week" | "month" | "year",
includeArchived?: boolean,
includeDrafts?: boolean,
collectionId?: string,
userId?: string,
offset?: number;
limit?: number;
dateFilter?: DateFilter;
includeArchived?: boolean;
includeDrafts?: boolean;
collectionId?: string;
userId?: string;
}
): Promise<SearchResult[]> => {
const compactedOptions = omitBy(options, (o) => !o);
@@ -396,16 +410,17 @@ export default class DocumentsStore extends BaseStore<Document> {
invariant(res && res.data, "Search response should be available");
// add the documents and associated policies to the store
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'result' implicitly has an 'any' type.
res.data.forEach((result) => this.add(result.document));
this.addPolicies(res.policies);
// store a reference to the document model in the search cache instead
// of the original result from the API.
const results: SearchResult[] = compact(
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'result' implicitly has an 'any' type.
res.data.map((result) => {
const document = this.data.get(result.document.id);
if (!document) return null;
return {
ranking: result.ranking,
context: result.context,
@@ -413,53 +428,61 @@ export default class DocumentsStore extends BaseStore<Document> {
};
})
);
let existing = this.searchCache.get(query) || [];
const existing = this.searchCache.get(query) || [];
// splice modifies any existing results, taking into account pagination
existing.splice(options.offset || 0, options.limit || 0, ...results);
this.searchCache.set(query, existing);
return res.data;
};
@action
prefetchDocument = (id: string) => {
prefetchDocument = async (id: string) => {
if (!this.data.get(id) && !this.getByUrl(id)) {
return this.fetch(id, { prefetch: true });
return this.fetch(id, {
prefetch: true,
});
}
return;
};
@action
templatize = async (id: string): Promise<?Document> => {
const doc: ?Document = this.data.get(id);
templatize = async (id: string): Promise<Document | null | undefined> => {
const doc: Document | null | undefined = this.data.get(id);
invariant(doc, "Document should exist");
if (doc.template) {
return;
}
const res = await client.post("/documents.templatize", { id });
const res = await client.post("/documents.templatize", {
id,
});
invariant(res && res.data, "Document not available");
this.addPolicies(res.policies);
this.add(res.data);
return this.data.get(res.data.id);
};
@action
fetch = async (
fetchWithSharedTree = async (
id: string,
options: FetchOptions = {}
): Promise<{ document: ?Document, sharedTree?: NavigationNode }> => {
): Promise<{
document: Document;
sharedTree?: NavigationNode;
}> => {
if (!options.prefetch) this.isFetching = true;
try {
const doc: ?Document = this.data.get(id) || this.getByUrl(id);
const doc: Document | null | undefined =
this.data.get(id) || this.getByUrl(id);
const policy = doc ? this.rootStore.policies.get(doc.id) : undefined;
if (doc && policy && !options.force) {
return { document: doc };
return {
document: doc,
};
}
const res = await client.post("/documents.info", {
@@ -467,13 +490,16 @@ export default class DocumentsStore extends BaseStore<Document> {
shareId: options.shareId,
apiVersion: 2,
});
invariant(res && res.data, "Document not available");
invariant(res && res.data, "Document not available");
this.addPolicies(res.policies);
this.add(res.data.document);
const document = this.data.get(res.data.document.id);
invariant(document, "Document not available");
return {
document: this.data.get(res.data.document.id),
document,
sharedTree: res.data.sharedTree,
};
} finally {
@@ -485,8 +511,8 @@ export default class DocumentsStore extends BaseStore<Document> {
move = async (
documentId: string,
collectionId: string,
parentDocumentId: ?string,
index: ?number
parentDocumentId?: string | null,
index?: number | null
) => {
this.movingDocumentId = documentId;
@@ -498,7 +524,6 @@ export default class DocumentsStore extends BaseStore<Document> {
index: index,
});
invariant(res && res.data, "Data not available");
res.data.documents.forEach(this.add);
res.data.collections.forEach(this.rootStore.collections.add);
this.addPolicies(res.policies);
@@ -508,9 +533,8 @@ export default class DocumentsStore extends BaseStore<Document> {
};
@action
duplicate = async (document: Document): * => {
duplicate = async (document: Document): Promise<Document> => {
const append = " (duplicate)";
const res = await client.post("/documents.create", {
publish: !!document.publishedAt,
parentDocumentId: document.parentDocumentId,
@@ -523,10 +547,8 @@ export default class DocumentsStore extends BaseStore<Document> {
text: document.text,
});
invariant(res && res.data, "Data should be available");
const collection = this.getCollectionForDocument(document);
if (collection) collection.refresh();
this.addPolicies(res.policies);
return this.add(res.data);
};
@@ -534,8 +556,8 @@ export default class DocumentsStore extends BaseStore<Document> {
@action
import = async (
file: File,
parentDocumentId: string,
collectionId: string,
parentDocumentId: string | null | undefined,
collectionId: string | null | undefined,
options: ImportOptions
) => {
// file.type can be an empty string sometimes
@@ -553,28 +575,42 @@ export default class DocumentsStore extends BaseStore<Document> {
const title = file.name.replace(/\.[^/.]+$/, "");
const formData = new FormData();
[
{ key: "parentDocumentId", value: parentDocumentId },
{ key: "collectionId", value: collectionId },
{ key: "title", value: title },
{ key: "publish", value: options.publish },
{ key: "file", value: file },
{
key: "parentDocumentId",
value: parentDocumentId,
},
{
key: "collectionId",
value: collectionId,
},
{
key: "title",
value: title,
},
{
key: "publish",
value: options.publish,
},
{
key: "file",
value: file,
},
].forEach((info) => {
if (typeof info.value === "string" && info.value) {
formData.append(info.key, info.value);
}
if (typeof info.value === "boolean") {
formData.append(info.key, info.value.toString());
}
if (info.value instanceof File) {
formData.append(info.key, info.value);
}
});
const res = await client.post("/documents.import", formData);
invariant(res && res.data, "Data should be available");
this.addPolicies(res.policies);
return this.add(res.data);
};
@@ -582,7 +618,7 @@ export default class DocumentsStore extends BaseStore<Document> {
_add = this.add;
@action
add = (item: Object) => {
add = (item: Record<string, any>): Document => {
const document = this._add(item);
if (item.starred !== undefined) {
@@ -600,13 +636,21 @@ export default class DocumentsStore extends BaseStore<Document> {
}
@action
async update(params: {
id: string,
title: string,
text?: string,
lastRevision: number,
}) {
const document = await super.update(params);
async update(
params: {
id: string;
title: string;
text?: string;
templateId?: string;
},
options?: {
publish?: boolean;
done?: boolean;
autosave?: boolean;
lastRevision: number;
}
) {
const document = await super.update(params, options);
// Because the collection object contains the url and title
// we need to ensure they are updated there as well.
@@ -616,12 +660,17 @@ export default class DocumentsStore extends BaseStore<Document> {
}
@action
async delete(document: Document, options?: {| permanent: boolean |}) {
async delete(
document: Document,
options?: {
permanent: boolean;
}
) {
await super.delete(document, options);
// check to see if we have any shares related to this document already
// loaded in local state. If so we can go ahead and remove those too.
const share = this.rootStore.shares.getByDocumentId(document.id);
if (share) {
this.rootStore.shares.remove(share.id);
}
@@ -640,7 +689,6 @@ export default class DocumentsStore extends BaseStore<Document> {
document.updateFromJson(res.data);
this.addPolicies(res.policies);
});
const collection = this.getCollectionForDocument(document);
if (collection) collection.refresh();
};
@@ -648,7 +696,10 @@ export default class DocumentsStore extends BaseStore<Document> {
@action
restore = async (
document: Document,
options: { revisionId?: string, collectionId?: string } = {}
options: {
revisionId?: string;
collectionId?: string;
} = {}
) => {
const res = await client.post("/documents.restore", {
id: document.id,
@@ -660,7 +711,6 @@ export default class DocumentsStore extends BaseStore<Document> {
document.updateFromJson(res.data);
this.addPolicies(res.policies);
});
const collection = this.getCollectionForDocument(document);
if (collection) collection.refresh();
};
@@ -670,46 +720,52 @@ export default class DocumentsStore extends BaseStore<Document> {
const res = await client.post("/documents.unpublish", {
id: document.id,
});
runInAction("Document#unpublish", () => {
invariant(res && res.data, "Data should be available");
document.updateFromJson(res.data);
this.addPolicies(res.policies);
});
const collection = this.getCollectionForDocument(document);
if (collection) collection.refresh();
};
pin = (document: Document) => {
return client.post("/documents.pin", { id: document.id });
return client.post("/documents.pin", {
id: document.id,
});
};
unpin = (document: Document) => {
return client.post("/documents.unpin", { id: document.id });
return client.post("/documents.unpin", {
id: document.id,
});
};
star = async (document: Document) => {
this.starredIds.set(document.id, true);
try {
return client.post("/documents.star", { id: document.id });
return client.post("/documents.star", {
id: document.id,
});
} catch (err) {
this.starredIds.set(document.id, false);
}
};
unstar = (document: Document) => {
unstar = async (document: Document) => {
this.starredIds.set(document.id, false);
try {
return client.post("/documents.unstar", { id: document.id });
return client.post("/documents.unstar", {
id: document.id,
});
} catch (err) {
this.starredIds.set(document.id, false);
}
};
getByUrl = (url: string = ""): ?Document => {
getByUrl = (url = ""): Document | null | undefined => {
return find(this.orderedData, (doc) => url.endsWith(doc.urlId));
};

View File

@@ -1,12 +1,11 @@
// @flow
import { sortBy, filter } from "lodash";
import { computed } from "mobx";
import Event from "models/Event";
import BaseStore from "./BaseStore";
import Event from "~/models/Event";
import BaseStore, { RPCAction } from "./BaseStore";
import RootStore from "./RootStore";
export default class EventsStore extends BaseStore<Event> {
actions = ["list"];
actions = [RPCAction.List];
constructor(rootStore: RootStore) {
super(rootStore, Event);

View File

@@ -1,12 +1,11 @@
// @flow
import { orderBy } from "lodash";
import { computed } from "mobx";
import FileOperation from "models/FileOperation";
import BaseStore from "./BaseStore";
import FileOperation from "~/models/FileOperation";
import BaseStore, { RPCAction } from "./BaseStore";
import RootStore from "./RootStore";
export default class FileOperationsStore extends BaseStore<FileOperation> {
actions = ["list", "info", "delete"];
actions = [RPCAction.List, RPCAction.Info, RPCAction.Delete];
constructor(rootStore: RootStore) {
super(rootStore, FileOperation);

View File

@@ -1,29 +1,26 @@
// @flow
import invariant from "invariant";
import { filter } from "lodash";
import { action, runInAction } from "mobx";
import GroupMembership from "models/GroupMembership";
import BaseStore from "./BaseStore";
import GroupMembership from "~/models/GroupMembership";
import { PaginationParams } from "~/types";
import { client } from "~/utils/ApiClient";
import BaseStore, { RPCAction } from "./BaseStore";
import RootStore from "./RootStore";
import type { PaginationParams } from "types";
import { client } from "utils/ApiClient";
export default class GroupMembershipsStore extends BaseStore<GroupMembership> {
actions = ["create", "delete"];
actions = [RPCAction.Create, RPCAction.Delete];
constructor(rootStore: RootStore) {
super(rootStore, GroupMembership);
}
@action
fetchPage = async (params: ?PaginationParams): Promise<*> => {
fetchPage = async (params: PaginationParams | undefined): Promise<any> => {
this.isFetching = true;
try {
const res = await client.post(`/groups.memberships`, params);
invariant(res && res.data, "Data not available");
runInAction(`GroupMembershipsStore#fetchPage`, () => {
res.data.users.forEach(this.rootStore.users.add);
res.data.groupMemberships.forEach(this.add);
@@ -36,28 +33,27 @@ export default class GroupMembershipsStore extends BaseStore<GroupMembership> {
};
@action
async create({ groupId, userId }: { groupId: string, userId: string }) {
async create({ groupId, userId }: { groupId: string; userId: string }) {
const res = await client.post("/groups.add_user", {
id: groupId,
userId,
});
invariant(res && res.data, "Group Membership data should be available");
res.data.users.forEach(this.rootStore.users.add);
res.data.groups.forEach(this.rootStore.groups.add);
res.data.groupMemberships.forEach(this.add);
const groupMemberships = res.data.groupMemberships.map(this.add);
return groupMemberships[0];
}
@action
async delete({ groupId, userId }: { groupId: string, userId: string }) {
async delete({ groupId, userId }: { groupId: string; userId: string }) {
const res = await client.post("/groups.remove_user", {
id: groupId,
userId,
});
invariant(res && res.data, "Group Membership data should be available");
this.remove(`${userId}-${groupId}`);
runInAction(`GroupMembershipsStore#delete`, () => {
res.data.groups.forEach(this.rootStore.groups.add);
this.isLoaded = true;

View File

@@ -1,13 +1,14 @@
// @flow
import invariant from "invariant";
import { filter } from "lodash";
import { action, runInAction, computed } from "mobx";
import naturalSort from "shared/utils/naturalSort";
import Group from "models/Group";
import naturalSort from "@shared/utils/naturalSort";
import Group from "~/models/Group";
import { PaginationParams } from "~/types";
import { client } from "~/utils/ApiClient";
import BaseStore from "./BaseStore";
import RootStore from "./RootStore";
import type { PaginationParams } from "types";
import { client } from "utils/ApiClient";
type FetchPageParams = PaginationParams & { query?: string };
export default class GroupsStore extends BaseStore<Group> {
constructor(rootStore: RootStore) {
@@ -20,14 +21,12 @@ export default class GroupsStore extends BaseStore<Group> {
}
@action
fetchPage = async (params: ?PaginationParams): Promise<*> => {
fetchPage = async (params: FetchPageParams | undefined): Promise<any> => {
this.isFetching = true;
try {
const res = await client.post(`/groups.list`, params);
invariant(res && res.data, "Data not available");
runInAction(`GroupsStore#fetchPage`, () => {
this.addPolicies(res.policies);
res.data.groups.forEach(this.add);
@@ -40,7 +39,7 @@ export default class GroupsStore extends BaseStore<Group> {
}
};
inCollection = (collectionId: string, query: string) => {
inCollection = (collectionId: string, query?: string) => {
const memberships = filter(
this.rootStore.collectionGroupMemberships.orderedData,
(member) => member.collectionId === collectionId
@@ -49,12 +48,11 @@ export default class GroupsStore extends BaseStore<Group> {
const groups = filter(this.orderedData, (group) =>
groupIds.includes(group.id)
);
if (!query) return groups;
return queriedGroups(groups, query);
};
notInCollection = (collectionId: string, query: string = "") => {
notInCollection = (collectionId: string, query = "") => {
const memberships = filter(
this.rootStore.collectionGroupMemberships.orderedData,
(member) => member.collectionId === collectionId
@@ -64,13 +62,12 @@ export default class GroupsStore extends BaseStore<Group> {
this.orderedData,
(group) => !groupIds.includes(group.id)
);
if (!query) return groups;
return queriedGroups(groups, query);
};
}
function queriedGroups(groups, query) {
function queriedGroups(groups: Group[], query: string) {
return filter(groups, (group) =>
group.name.toLowerCase().match(query.toLowerCase())
);

View File

@@ -1,11 +1,9 @@
// @flow
import { filter } from "lodash";
import { computed } from "mobx";
import naturalSort from "shared/utils/naturalSort";
import BaseStore from "stores/BaseStore";
import RootStore from "stores/RootStore";
import Integration from "models/Integration";
import naturalSort from "@shared/utils/naturalSort";
import BaseStore from "~/stores/BaseStore";
import RootStore from "~/stores/RootStore";
import Integration from "~/models/Integration";
class IntegrationsStore extends BaseStore<Integration> {
constructor(rootStore: RootStore) {
@@ -19,7 +17,9 @@ class IntegrationsStore extends BaseStore<Integration> {
@computed
get slackIntegrations(): Integration[] {
return filter(this.orderedData, { service: "slack" });
return filter(this.orderedData, {
service: "slack",
});
}
}

View File

@@ -1,28 +1,25 @@
// @flow
import invariant from "invariant";
import { action, runInAction } from "mobx";
import Membership from "models/Membership";
import BaseStore from "./BaseStore";
import Membership from "~/models/Membership";
import { PaginationParams } from "~/types";
import { client } from "~/utils/ApiClient";
import BaseStore, { RPCAction } from "./BaseStore";
import RootStore from "./RootStore";
import type { PaginationParams } from "types";
import { client } from "utils/ApiClient";
export default class MembershipsStore extends BaseStore<Membership> {
actions = ["create", "delete"];
actions = [RPCAction.Create, RPCAction.Delete];
constructor(rootStore: RootStore) {
super(rootStore, Membership);
}
@action
fetchPage = async (params: ?PaginationParams): Promise<*> => {
fetchPage = async (params: PaginationParams | undefined): Promise<any> => {
this.isFetching = true;
try {
const res = await client.post(`/collections.memberships`, params);
invariant(res && res.data, "Data not available");
runInAction(`/collections.memberships`, () => {
res.data.users.forEach(this.rootStore.users.add);
res.data.memberships.forEach(this.add);
@@ -40,9 +37,9 @@ export default class MembershipsStore extends BaseStore<Membership> {
userId,
permission,
}: {
collectionId: string,
userId: string,
permission: string,
collectionId: string;
userId: string;
permission: string;
}) {
const res = await client.post("/collections.add_user", {
id: collectionId,
@@ -50,9 +47,10 @@ export default class MembershipsStore extends BaseStore<Membership> {
permission,
});
invariant(res && res.data, "Membership data should be available");
res.data.users.forEach(this.rootStore.users.add);
res.data.memberships.forEach(this.add);
const memberships = res.data.memberships.map(this.add);
return memberships[0];
}
@action
@@ -60,14 +58,13 @@ export default class MembershipsStore extends BaseStore<Membership> {
collectionId,
userId,
}: {
collectionId: string,
userId: string,
collectionId: string;
userId: string;
}) {
await client.post("/collections.remove_user", {
id: collectionId,
userId,
});
this.remove(`${userId}-${collectionId}`);
}

View File

@@ -1,17 +0,0 @@
// @flow
import { find } from "lodash";
import NotificationSetting from "models/NotificationSetting";
import BaseStore from "./BaseStore";
import RootStore from "./RootStore";
export default class NotificationSettingsStore extends BaseStore<NotificationSetting> {
actions = ["list", "create", "delete"];
constructor(rootStore: RootStore) {
super(rootStore, NotificationSetting);
}
getByEvent = (event: string) => {
return find(this.orderedData, { event });
};
}

View File

@@ -0,0 +1,20 @@
import { find } from "lodash";
import NotificationSetting from "~/models/NotificationSetting";
import BaseStore, { RPCAction } from "./BaseStore";
import RootStore from "./RootStore";
export default class NotificationSettingsStore extends BaseStore<
NotificationSetting
> {
actions = [RPCAction.List, RPCAction.Create, RPCAction.Delete];
constructor(rootStore: RootStore) {
super(rootStore, NotificationSetting);
}
getByEvent = (event: string) => {
return find(this.orderedData, {
event,
});
};
}

View File

@@ -1,5 +1,4 @@
// @flow
import Policy from "models/Policy";
import Policy from "~/models/Policy";
import BaseStore from "./BaseStore";
import RootStore from "./RootStore";

View File

@@ -1,87 +0,0 @@
// @flow
import invariant from "invariant";
import { filter } from "lodash";
import { action, runInAction } from "mobx";
import BaseStore from "stores/BaseStore";
import RootStore from "stores/RootStore";
import Revision from "models/Revision";
import type { FetchOptions, PaginationParams } from "types";
import { client } from "utils/ApiClient";
export default class RevisionsStore extends BaseStore<Revision> {
actions = ["list"];
constructor(rootStore: RootStore) {
super(rootStore, Revision);
}
getDocumentRevisions(documentId: string): Revision[] {
let revisions = filter(this.orderedData, { documentId });
const latestRevision = revisions[0];
const document = this.rootStore.documents.get(documentId);
// There is no guarantee that we have a revision that represents the latest
// state of the document. This pushes a fake revision in at the top if there
// isn't one
if (
latestRevision &&
document &&
latestRevision.createdAt !== document.updatedAt
) {
revisions.unshift(
new Revision({
id: "latest",
documentId: document.id,
title: document.title,
text: document.text,
createdAt: document.updatedAt,
createdBy: document.createdBy,
})
);
}
return revisions;
}
@action
fetch = async (id: string, options?: FetchOptions): Promise<?Revision> => {
this.isFetching = true;
invariant(id, "Id is required");
try {
const rev = this.data.get(id);
if (rev) return rev;
const res = await client.post("/revisions.info", {
id,
});
invariant(res && res.data, "Revision not available");
this.add(res.data);
runInAction("RevisionsStore#fetch", () => {
this.isLoaded = true;
});
return this.data.get(res.data.id);
} finally {
this.isFetching = false;
}
};
@action
fetchPage = async (options: ?PaginationParams): Promise<*> => {
this.isFetching = true;
try {
const res = await client.post("/revisions.list", options);
invariant(res && res.data, "Document revisions not available");
runInAction("RevisionsStore#fetchPage", () => {
res.data.forEach((revision) => this.add(revision));
this.isLoaded = true;
});
return res.data;
} finally {
this.isFetching = false;
}
};
}

View File

@@ -0,0 +1,66 @@
import invariant from "invariant";
import { filter } from "lodash";
import { action, runInAction } from "mobx";
import BaseStore, { RPCAction } from "~/stores/BaseStore";
import RootStore from "~/stores/RootStore";
import Revision from "~/models/Revision";
import { PaginationParams } from "~/types";
import { client } from "~/utils/ApiClient";
export default class RevisionsStore extends BaseStore<Revision> {
actions = [RPCAction.List];
constructor(rootStore: RootStore) {
super(rootStore, Revision);
}
getDocumentRevisions(documentId: string): Revision[] {
const revisions = filter(this.orderedData, {
documentId,
});
const latestRevision = revisions[0];
const document = this.rootStore.documents.get(documentId);
// There is no guarantee that we have a revision that represents the latest
// state of the document. This pushes a fake revision in at the top if there
// isn't one
if (
latestRevision &&
document &&
latestRevision.createdAt !== document.updatedAt
) {
revisions.unshift(
new Revision(
{
id: "latest",
documentId: document.id,
title: document.title,
text: document.text,
createdAt: document.updatedAt,
createdBy: document.createdBy,
},
this
)
);
}
return revisions;
}
@action
fetchPage = async (options: PaginationParams | undefined): Promise<any> => {
this.isFetching = true;
try {
const res = await client.post("/revisions.list", options);
invariant(res && res.data, "Document revisions not available");
runInAction("RevisionsStore#fetchPage", () => {
res.data.forEach(this.add);
this.isLoaded = true;
});
return res.data;
} finally {
this.isFetching = false;
}
};
}

View File

@@ -1,4 +1,3 @@
// @flow
import ApiKeysStore from "./ApiKeysStore";
import AuthStore from "./AuthStore";
import CollectionGroupMembershipsStore from "./CollectionGroupMembershipsStore";

View File

@@ -1,14 +1,18 @@
// @flow
import invariant from "invariant";
import { sortBy, filter, find, isUndefined } from "lodash";
import { action, computed } from "mobx";
import Share from "models/Share";
import BaseStore from "./BaseStore";
import Share from "~/models/Share";
import { client } from "~/utils/ApiClient";
import BaseStore, { RPCAction } from "./BaseStore";
import RootStore from "./RootStore";
import { client } from "utils/ApiClient";
export default class SharesStore extends BaseStore<Share> {
actions = ["info", "list", "create", "update"];
actions = [
RPCAction.Info,
RPCAction.List,
RPCAction.Create,
RPCAction.Update,
];
constructor(rootStore: RootStore) {
super(rootStore, Share);
@@ -26,23 +30,26 @@ export default class SharesStore extends BaseStore<Share> {
@action
revoke = async (share: Share) => {
await client.post("/shares.revoke", { id: share.id });
await client.post("/shares.revoke", {
id: share.id,
});
this.remove(share.id);
};
@action
async create(params: Object) {
let item = this.getByDocumentId(params.documentId);
async create(params: Record<string, any>) {
const item = this.getByDocumentId(params.documentId);
if (item) return item;
return super.create(params);
}
@action
async fetch(documentId: string, options?: Object = {}): Promise<*> {
let item = this.getByDocumentId(documentId);
async fetch(
documentId: string,
options: Record<string, any> = {}
): Promise<any> {
const item = this.getByDocumentId(documentId);
if (item && !options.force) return item;
this.isFetching = true;
try {
@@ -50,10 +57,9 @@ export default class SharesStore extends BaseStore<Share> {
documentId,
apiVersion: 2,
});
if (isUndefined(res)) return;
invariant(res && res.data, "Data should be available");
this.addPolicies(res.policies);
return res.data.shares.map(this.add);
} finally {
@@ -61,7 +67,7 @@ export default class SharesStore extends BaseStore<Share> {
}
}
getByDocumentParents = (documentId: string): ?Share => {
getByDocumentParents = (documentId: string): Share | null | undefined => {
const document = this.rootStore.documents.get(documentId);
if (!document) return;
@@ -75,13 +81,16 @@ export default class SharesStore extends BaseStore<Share> {
for (const parentId of parentIds) {
const share = this.getByDocumentId(parentId);
if (share && share.includeChildDocuments && share.published) {
return share;
}
}
return undefined;
};
getByDocumentId = (documentId: string): ?Share => {
getByDocumentId = (documentId: string): Share | null | undefined => {
return find(this.orderedData, (share) => share.documentId === documentId);
};
}

View File

@@ -1,28 +0,0 @@
/* eslint-disable */
import stores from '.';
// Actions
describe('ToastsStore', () => {
let store;
beforeEach(() => {
store = stores.toasts;
});
test('#add should add messages', () => {
expect(store.orderedData.length).toBe(0);
store.showToast('first error');
store.showToast('second error');
expect(store.orderedData.length).toBe(2);
});
test('#remove should remove messages', () => {
store.toasts.clear();
const id = store.showToast('first error');
store.showToast('second error');
expect(store.orderedData.length).toBe(2);
store.hideToast(id);
expect(store.orderedData.length).toBe(1);
expect(store.orderedData[0].message).toBe('second error');
});
});

View File

@@ -0,0 +1,25 @@
import stores from ".";
describe("ToastsStore", () => {
const store = stores.toasts;
test("#add should add messages", () => {
expect(store.orderedData.length).toBe(0);
store.showToast("first error");
store.showToast("second error");
expect(store.orderedData.length).toBe(2);
});
test("#remove should remove messages", () => {
store.toasts.clear();
const id = store.showToast("first error");
store.showToast("second error");
expect(store.orderedData.length).toBe(2);
id && store.hideToast(id);
expect(store.orderedData.length).toBe(1);
expect(store.orderedData[0].message).toBe("second error");
});
});

View File

@@ -1,11 +1,12 @@
// @flow
import { orderBy } from "lodash";
import { observable, action, computed } from "mobx";
import { v4 as uuidv4 } from "uuid";
import type { Toast, ToastOptions } from "types";
import { Toast, ToastOptions } from "~/types";
export default class ToastsStore {
@observable toasts: Map<string, Toast> = new Map();
@observable
toasts: Map<string, Toast> = new Map();
lastToastId: string;
@action
@@ -16,8 +17,8 @@ export default class ToastsStore {
}
) => {
if (!message) return;
const lastToast = this.toasts.get(this.lastToastId);
if (lastToast && lastToast.message === message) {
this.toasts.set(this.lastToastId, {
...lastToast,

View File

@@ -1,36 +1,69 @@
// @flow
import { action, autorun, computed, observable } from "mobx";
import { light as defaultTheme } from "shared/theme";
import Collection from "models/Collection";
import Document from "models/Document";
import { light as defaultTheme } from "@shared/theme";
import Collection from "~/models/Collection";
import Document from "~/models/Document";
import { ConnectionStatus } from "~/scenes/Document/components/MultiplayerEditor";
const UI_STORE = "UI_STORE";
type Status = "connecting" | "connected" | "disconnected" | void;
export enum Theme {
Light = "light",
Dark = "dark",
System = "system",
}
export enum SystemTheme {
Light = "light",
Dark = "dark",
}
class UiStore {
// has the user seen the prompt to change the UI language and actioned it
@observable languagePromptDismissed: boolean;
@observable
languagePromptDismissed: boolean | undefined;
// theme represents the users UI preference (defaults to system)
@observable theme: "light" | "dark" | "system";
@observable
theme: Theme;
// systemTheme represents the system UI theme (Settings -> General in macOS)
@observable systemTheme: "light" | "dark";
@observable activeDocumentId: ?string;
@observable activeCollectionId: ?string;
@observable progressBarVisible: boolean = false;
@observable isEditing: boolean = false;
@observable tocVisible: boolean = false;
@observable mobileSidebarVisible: boolean = false;
@observable sidebarWidth: number;
@observable sidebarCollapsed: boolean = false;
@observable sidebarIsResizing: boolean = false;
@observable multiplayerStatus: Status;
@observable
systemTheme: SystemTheme;
@observable
activeDocumentId: string | undefined;
@observable
activeCollectionId: string | undefined;
@observable
progressBarVisible = false;
@observable
isEditing = false;
@observable
tocVisible = false;
@observable
mobileSidebarVisible = false;
@observable
sidebarWidth: number;
@observable
sidebarCollapsed = false;
@observable
sidebarIsResizing = false;
@observable
multiplayerStatus: ConnectionStatus;
constructor() {
// Rehydrate
let data = {};
let data: Partial<UiStore> = {};
try {
data = JSON.parse(localStorage.getItem(UI_STORE) || "{}");
} catch (_) {
@@ -43,10 +76,13 @@ class UiStore {
"(prefers-color-scheme: dark)"
);
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'event' implicitly has an 'any' type.
const setSystemTheme = (event) => {
this.systemTheme = event.matches ? "dark" : "light";
this.systemTheme = event.matches ? SystemTheme.Dark : SystemTheme.Light;
};
setSystemTheme(colorSchemeQueryList);
if (colorSchemeQueryList.addListener) {
colorSchemeQueryList.addListener(setSystemTheme);
}
@@ -54,10 +90,10 @@ class UiStore {
// persisted keys
this.languagePromptDismissed = data.languagePromptDismissed;
this.sidebarCollapsed = data.sidebarCollapsed;
this.sidebarCollapsed = !!data.sidebarCollapsed;
this.sidebarWidth = data.sidebarWidth || defaultTheme.sidebarWidth;
this.tocVisible = data.tocVisible;
this.theme = data.theme || "system";
this.tocVisible = !!data.tocVisible;
this.theme = data.theme || Theme.System;
autorun(() => {
try {
@@ -69,7 +105,7 @@ class UiStore {
}
@action
setTheme = (theme: "light" | "dark" | "system") => {
setTheme = (theme: Theme) => {
this.theme = theme;
if (window.localStorage) {
@@ -97,7 +133,7 @@ class UiStore {
};
@action
setMultiplayerStatus = (status: Status): void => {
setMultiplayerStatus = (status: ConnectionStatus): void => {
this.multiplayerStatus = status;
};
@@ -177,7 +213,7 @@ class UiStore {
};
@computed
get resolvedTheme(): "dark" | "light" {
get resolvedTheme(): Theme | SystemTheme {
if (this.theme === "system") {
return this.systemTheme;
}

View File

@@ -1,22 +1,29 @@
// @flow
import invariant from "invariant";
import { filter, orderBy } from "lodash";
import { observable, computed, action, runInAction } from "mobx";
import type { Role } from "shared/types";
import User from "models/User";
import { Role } from "@shared/types";
import User from "~/models/User";
import { client } from "~/utils/ApiClient";
import BaseStore from "./BaseStore";
import RootStore from "./RootStore";
import { client } from "utils/ApiClient";
export default class UsersStore extends BaseStore<User> {
@observable counts: {
active: number,
admins: number,
all: number,
invited: number,
suspended: number,
viewers: number,
} = {};
@observable
counts: {
active: number;
admins: number;
all: number;
invited: number;
suspended: number;
viewers: number;
} = {
active: 0,
admins: 0,
all: 0,
invited: 0,
suspended: 0,
viewers: 0,
};
constructor(rootStore: RootStore) {
super(rootStore, User);
@@ -24,40 +31,39 @@ export default class UsersStore extends BaseStore<User> {
@computed
get active(): User[] {
return filter(
this.orderedData,
return this.orderedData.filter(
(user) => !user.isSuspended && user.lastActiveAt
);
}
@computed
get suspended(): User[] {
return filter(this.orderedData, (user) => user.isSuspended);
return this.orderedData.filter((user) => user.isSuspended);
}
@computed
get activeOrInvited(): User[] {
return filter(this.orderedData, (user) => !user.isSuspended);
return this.orderedData.filter((user) => !user.isSuspended);
}
@computed
get invited(): User[] {
return filter(this.orderedData, (user) => user.isInvited);
return this.orderedData.filter((user) => user.isInvited);
}
@computed
get admins(): User[] {
return filter(this.orderedData, (user) => user.isAdmin);
return this.orderedData.filter((user) => user.isAdmin);
}
@computed
get viewers(): User[] {
return filter(this.orderedData, (user) => user.isViewer);
return this.orderedData.filter((user) => user.isViewer);
}
@computed
get all(): User[] {
return filter(this.orderedData, (user) => user.lastActiveAt);
return this.orderedData.filter((user) => user.lastActiveAt);
}
@computed
@@ -110,8 +116,16 @@ export default class UsersStore extends BaseStore<User> {
};
@action
invite = async (invites: { email: string, name: string, role: Role }[]) => {
const res = await client.post(`/users.invite`, { invites });
invite = async (
invites: {
email: string;
name: string;
role: Role;
}[]
) => {
const res = await client.post(`/users.invite`, {
invites,
});
invariant(res && res.data, "Data should be available");
runInAction(`invite`, () => {
res.data.users.forEach(this.add);
@@ -122,32 +136,39 @@ export default class UsersStore extends BaseStore<User> {
};
@action
fetchCounts = async (teamId: string): Promise<*> => {
const res = await client.post(`/users.count`, { teamId });
fetchCounts = async (teamId: string): Promise<any> => {
const res = await client.post(`/users.count`, {
teamId,
});
invariant(res && res.data, "Data should be available");
this.counts = res.data.counts;
return res.data;
};
@action
async delete(user: User, options: Object = {}) {
async delete(user: User, options: Record<string, any> = {}) {
super.delete(user, options);
if (!user.isSuspended && user.lastActiveAt) {
this.counts.active -= 1;
}
if (user.isInvited) {
this.counts.invited -= 1;
}
if (user.isAdmin) {
this.counts.admins -= 1;
}
if (user.isSuspended) {
this.counts.suspended -= 1;
}
if (user.isViewer) {
this.counts.viewers -= 1;
}
this.counts.all -= 1;
}
@@ -155,27 +176,32 @@ export default class UsersStore extends BaseStore<User> {
updateCounts = (to: Role, from: Role) => {
if (to === "admin") {
this.counts.admins += 1;
if (from === "viewer") {
this.counts.viewers -= 1;
}
}
if (to === "viewer") {
this.counts.viewers += 1;
if (from === "admin") {
this.counts.admins -= 1;
}
}
if (to === "member") {
if (from === "viewer") {
this.counts.viewers -= 1;
}
if (from === "admin") {
this.counts.admins -= 1;
}
}
};
notInCollection = (collectionId: string, query: string = "") => {
notInCollection = (collectionId: string, query = "") => {
const memberships = filter(
this.rootStore.memberships.orderedData,
(member) => member.collectionId === collectionId
@@ -185,12 +211,11 @@ export default class UsersStore extends BaseStore<User> {
this.activeOrInvited,
(user) => !userIds.includes(user.id)
);
if (!query) return users;
return queriedUsers(users, query);
};
inCollection = (collectionId: string, query: string) => {
inCollection = (collectionId: string, query?: string) => {
const memberships = filter(
this.rootStore.memberships.orderedData,
(member) => member.collectionId === collectionId
@@ -199,12 +224,11 @@ export default class UsersStore extends BaseStore<User> {
const users = filter(this.activeOrInvited, (user) =>
userIds.includes(user.id)
);
if (!query) return users;
return queriedUsers(users, query);
};
notInGroup = (groupId: string, query: string = "") => {
notInGroup = (groupId: string, query = "") => {
const memberships = filter(
this.rootStore.groupMemberships.orderedData,
(member) => member.groupId === groupId
@@ -214,12 +238,11 @@ export default class UsersStore extends BaseStore<User> {
this.activeOrInvited,
(user) => !userIds.includes(user.id)
);
if (!query) return users;
return queriedUsers(users, query);
};
inGroup = (groupId: string, query: string) => {
inGroup = (groupId: string, query?: string) => {
const groupMemberships = filter(
this.rootStore.groupMemberships.orderedData,
(member) => member.groupId === groupId
@@ -228,7 +251,6 @@ export default class UsersStore extends BaseStore<User> {
const users = filter(this.activeOrInvited, (user) =>
userIds.includes(user.id)
);
if (!query) return users;
return queriedUsers(users, query);
};
@@ -239,7 +261,6 @@ export default class UsersStore extends BaseStore<User> {
to,
});
invariant(res && res.data, "Data should be available");
runInAction(`UsersStore#${action}`, () => {
this.addPolicies(res.policies);
this.add(res.data);
@@ -247,7 +268,7 @@ export default class UsersStore extends BaseStore<User> {
};
}
function queriedUsers(users, query) {
function queriedUsers(users: User[], query: string) {
return filter(users, (user) =>
user.name.toLowerCase().includes(query.toLowerCase())
);

View File

@@ -1,11 +1,10 @@
// @flow
import { reduce, filter, find, orderBy } from "lodash";
import View from "models/View";
import BaseStore from "./BaseStore";
import View from "~/models/View";
import BaseStore, { RPCAction } from "./BaseStore";
import RootStore from "./RootStore";
export default class ViewsStore extends BaseStore<View> {
actions = ["list", "create"];
actions = [RPCAction.List, RPCAction.Create];
constructor(rootStore: RootStore) {
super(rootStore, View);

View File

@@ -1,5 +1,4 @@
// @flow
import RootStore from "stores/RootStore";
import RootStore from "~/stores/RootStore";
const stores = new RootStore();