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,8 @@
// @flow
import retry from "fetch-retry";
import invariant from "invariant";
import { map, trim } from "lodash";
import { getCookie } from "tiny-cookie";
import stores from "stores";
import stores from "~/stores";
import download from "./download";
import {
AuthorizationError,
@@ -17,18 +16,18 @@ import {
} from "./errors";
type Options = {
baseUrl?: string,
baseUrl?: string;
};
// authorization cookie set by a Cloudflare Access proxy
const CF_AUTHORIZATION = getCookie("CF_Authorization");
// if the cookie is set, we must pass it with all ApiClient requests
const CREDENTIALS = CF_AUTHORIZATION ? "same-origin" : "omit";
const fetchWithRetry = retry(fetch);
class ApiClient {
baseUrl: string;
userAgent: string;
constructor(options: Options = {}) {
@@ -39,8 +38,8 @@ class ApiClient {
fetch = async (
path: string,
method: string,
data: ?Object | FormData | void,
options: Object = {}
data: (Record<string, any> | undefined) | FormData,
options: Record<string, any> = {}
) => {
let body;
let modifiedPath;
@@ -74,18 +73,21 @@ class ApiClient {
urlToFetch = this.baseUrl + (modifiedPath || path);
}
let headerOptions: any = {
const headerOptions: any = {
Accept: "application/json",
"cache-control": "no-cache",
// @ts-expect-error ts-migrate(2304) FIXME: Cannot find name 'EDITOR_VERSION'.
"x-editor-version": EDITOR_VERSION,
pragma: "no-cache",
};
// for multipart forms or other non JSON requests fetch
// populates the Content-Type without needing to explicitly
// set it.
if (isJson) {
headerOptions["Content-Type"] = "application/json";
}
const headers = new Headers(headerOptions);
if (stores.auth.authenticated) {
@@ -94,9 +96,11 @@ class ApiClient {
}
let response;
try {
response = await fetchWithRetry(urlToFetch, {
method,
// @ts-expect-error ts-migrate(2322) FIXME: Type 'string | Record<string, any> | undefined' is... Remove this comment to see the full error message
body,
headers,
redirect: "follow",
@@ -118,7 +122,6 @@ class ApiClient {
const fileName = (
response.headers.get("content-disposition") || ""
).split("filename=")[1];
download(blob, trim(fileName, '"'));
return;
} else if (success && response.status === 204) {
@@ -135,56 +138,78 @@ class ApiClient {
// Handle failed responses
const error = {};
// @ts-expect-error ts-migrate(2339) FIXME: Property 'statusCode' does not exist on type '{}'.
error.statusCode = response.status;
// @ts-expect-error ts-migrate(2339) FIXME: Property 'response' does not exist on type '{}'.
error.response = response;
try {
const parsed = await response.json();
// @ts-expect-error ts-migrate(2339) FIXME: Property 'message' does not exist on type '{}'.
error.message = parsed.message || "";
// @ts-expect-error ts-migrate(2339) FIXME: Property 'error' does not exist on type '{}'.
error.error = parsed.error;
// @ts-expect-error ts-migrate(2339) FIXME: Property 'data' does not exist on type '{}'.
error.data = parsed.data;
} catch (_err) {
// we're trying to parse an error so JSON may not be valid
}
// @ts-expect-error ts-migrate(2339) FIXME: Property 'error' does not exist on type '{}'.
if (response.status === 400 && error.error === "editor_update_required") {
window.location.reload(true);
window.location.reload();
// @ts-expect-error ts-migrate(2339) FIXME: Property 'message' does not exist on type '{}'.
throw new UpdateRequiredError(error.message);
}
if (response.status === 400) {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'message' does not exist on type '{}'.
throw new BadRequestError(error.message);
}
if (response.status === 403) {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'error' does not exist on type '{}'.
if (error.error === "user_suspended") {
stores.auth.logout();
return;
}
// @ts-expect-error ts-migrate(2339) FIXME: Property 'message' does not exist on type '{}'.
throw new AuthorizationError(error.message);
}
if (response.status === 404) {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'message' does not exist on type '{}'.
throw new NotFoundError(error.message);
}
if (response.status === 503) {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'message' does not exist on type '{}'.
throw new ServiceUnavailableError(error.message);
}
// @ts-expect-error ts-migrate(2339) FIXME: Property 'message' does not exist on type '{}'.
throw new RequestError(error.message);
};
get = (path: string, data: ?Object, options?: Object) => {
get = (
path: string,
data: Record<string, any> | undefined,
options?: Record<string, any>
) => {
return this.fetch(path, "GET", data, options);
};
post = (path: string, data: ?Object, options?: Object) => {
post = (
path: string,
data?: Record<string, any> | undefined,
options?: Record<string, any>
) => {
return this.fetch(path, "POST", data, options);
};
// Helpers
constructQueryString = (data: { [key: string]: string }) => {
constructQueryString = (data: Record<string, any>) => {
return map(
data,
(v, k) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`
@@ -192,7 +217,6 @@ class ApiClient {
};
}
export default ApiClient;
export default ApiClient; // In case you don't want to always initiate, just import with `import { client } ...`
// In case you don't want to always initiate, just import with `import { client } ...`
export const client = new ApiClient();

View File

@@ -1,6 +1,6 @@
/* eslint-disable */
export default {
client: {
post: jest.fn(() => Promise.resolve),
},
};
post: jest.fn(() => Promise.resolve)
}
};

View File

@@ -1,17 +1,15 @@
// @flow
import Compressor from "compressorjs";
type Options = { maxWidth?: number, maxHeight?: number };
type Options = {
maxWidth?: number;
maxHeight?: number;
};
export const compressImage = async (
file: File | Blob,
options?: Options
): Promise<Blob> => {
return new Promise((resolve, reject) => {
new Compressor(file, {
...options,
success: resolve,
error: reject,
});
new Compressor(file, { ...options, success: resolve, error: reject });
});
};

View File

@@ -1,4 +1,3 @@
// @flow
import {
isToday,
isYesterday,
@@ -7,13 +6,13 @@ import {
differenceInCalendarYears,
format as formatDate,
} from "date-fns";
import { type TFunction } from "react-i18next";
import { dateLocale } from "utils/i18n";
import { TFunction } from "react-i18next";
import { dateLocale } from "~/utils/i18n";
export function dateToHeading(
dateTime: string,
t: TFunction,
userLocale: ?string
userLocale: string | null | undefined
) {
const date = Date.parse(dateTime);
const now = new Date();
@@ -31,8 +30,11 @@ export function dateToHeading(
// of the week as a string. We use the LocaleTime component here to gain
// async bundle loading of languages
const weekDiff = differenceInCalendarWeeks(now, date);
if (weekDiff === 0) {
return formatDate(Date.parse(dateTime), "iiii", { locale });
return formatDate(Date.parse(dateTime), "iiii", {
locale,
});
}
if (weekDiff === 1) {
@@ -40,6 +42,7 @@ export function dateToHeading(
}
const monthDiff = differenceInCalendarMonths(now, date);
if (monthDiff === 0) {
return t("This month");
}
@@ -49,10 +52,13 @@ export function dateToHeading(
}
const yearDiff = differenceInCalendarYears(now, date);
if (yearDiff === 0) {
return t("This year");
}
// If older than the current calendar year then just print the year e.g 2020
return formatDate(Date.parse(dateTime), "y", { locale });
return formatDate(Date.parse(dateTime), "y", {
locale,
});
}

View File

@@ -1,8 +1,7 @@
// @flow
// A function to delete all IndexedDB databases
export async function deleteAllDatabases() {
const databases = await window.indexedDB.databases();
for (const database of databases) {
if (database.name) {
await window.indexedDB.deleteDatabase(database.name);

View File

@@ -1,6 +1,5 @@
// @flow
import { parseDomain, stripSubdomain } from "shared/utils/domains";
import env from "env";
import { parseDomain, stripSubdomain } from "@shared/utils/domains";
import env from "~/env";
export function getCookieDomain(domain: string) {
return env.SUBDOMAINS_ENABLED ? stripSubdomain(domain) : domain;
@@ -9,7 +8,6 @@ export function getCookieDomain(domain: string) {
export function isCustomDomain() {
const parsed = parseDomain(window.location.origin);
const main = parseDomain(env.URL);
return (
parsed && main && (main.domain !== parsed.domain || main.tld !== parsed.tld)
);

View File

@@ -1,77 +1,96 @@
// @flow
// download.js v3.0, by dandavis; 2008-2014. [CCBY2] see http://danml.com/download.html for tests/usage
// v1 landed a FF+Chrome compat way of downloading strings to local un-named files, upgraded to use a hidden frame and optional mime
// v2 added named files via a[download], msSaveBlob, IE (10+) support, and window.URL support for larger+faster saves than dataURLs
// v3 added dataURL and Blob Input, bind-toggle arity, and legacy dataURL fallback was improved with force-download mime and base64 support
// data can be a string, Blob, File, or dataURL
export default function download(
data: Blob | string | File,
strFileName: string,
strMimeType?: string
) {
var self = window, // this script is only for browsers anyway...
u = "application/octet-stream", // this default mime also triggers iframe downloads
const self = window,
// this script is only for browsers anyway...
u = "application/octet-stream",
// this default mime also triggers iframe downloads
m = strMimeType || u,
x = data,
D = document,
a = D.createElement("a"),
z = function (a, o) {
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'a' implicitly has an 'any' type.
z = function (a) {
return String(a);
},
// @ts-expect-error ts-migrate(2339) FIXME: Property 'MozBlob' does not exist on type 'Window ... Remove this comment to see the full error message
B = self.Blob || self.MozBlob || self.WebKitBlob || z,
// @ts-expect-error ts-migrate(2339) FIXME: Property 'WebKitBlobBuilder' does not exist on typ... Remove this comment to see the full error message
BB = self.MSBlobBuilder || self.WebKitBlobBuilder || self.BlobBuilder,
fn = strFileName || "download",
// @ts-expect-error ts-migrate(1155) FIXME: 'const' declarations must be initialized.
blob,
// @ts-expect-error ts-migrate(1155) FIXME: 'const' declarations must be initialized.
b,
// @ts-expect-error ts-migrate(1155) FIXME: 'const' declarations must be initialized.
fr;
if (String(this) === "true") {
//reverse arguments, allowing download.bind(true, "text/xml", "export.xml") to act as a callback
// @ts-expect-error ts-migrate(2588) FIXME: Cannot assign to 'x' because it is a constant.
x = [x, m];
// @ts-expect-error ts-migrate(2588) FIXME: Cannot assign to 'm' because it is a constant.
m = x[0];
// @ts-expect-error ts-migrate(2588) FIXME: Cannot assign to 'x' because it is a constant.
x = x[1];
}
//go ahead and download dataURLs right away
// go ahead and download dataURLs right away
if (String(x).match(/^data:[\w+-]+\/[\w+-]+[,;]/)) {
// $FlowIssue
return navigator.msSaveBlob // IE10 can't do a[download], only Blobs:
? // $FlowIssue
navigator.msSaveBlob(d2b(x), fn)
: saver(x); // everyone else can save dataURLs un-processed
} //end if dataURL passed?
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1.
return saver(x); // everyone else can save dataURLs un-processed
}
//end if dataURL passed?
try {
blob = x instanceof B ? x : new B([x], { type: m });
// @ts-expect-error ts-migrate(2588) FIXME: Cannot assign to 'blob' because it is a constant.
blob =
x instanceof B
? x
: new B([x], {
type: m,
});
} catch (y) {
if (BB) {
// @ts-expect-error ts-migrate(2588) FIXME: Cannot assign to 'b' because it is a constant.
b = new BB();
b.append([x]);
// @ts-expect-error ts-migrate(2588) FIXME: Cannot assign to 'blob' because it is a constant.
blob = b.getBlob(m); // the blob
}
}
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'u' implicitly has an 'any' type.
function d2b(u) {
if (typeof u !== "string") {
throw Error("Attempted to pass non-string to d2b");
}
var p = u.split(/[:;,]/),
const p = u.split(/[:;,]/),
t = p[1],
dec = p[2] === "base64" ? atob : decodeURIComponent,
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
bin = dec(p.pop()),
mx = bin.length,
i = 0,
uia = new Uint8Array(mx);
// @ts-expect-error ts-migrate(2588) FIXME: Cannot assign to 'i' because it is a constant.
for (i; i < mx; ++i) uia[i] = bin.charCodeAt(i);
return new B([uia], { type: t });
return new B([uia], {
type: t,
});
}
function saver(url, winMode) {
function saver(url: string, winMode: boolean) {
if (typeof url !== "string") {
throw Error("Attempted to pass non-string url to saver");
}
@@ -84,6 +103,7 @@ export default function download(
setTimeout(function () {
a.click();
D.body && D.body.removeChild(a);
if (winMode === true) {
setTimeout(function () {
self.URL.revokeObjectURL(a.href);
@@ -94,8 +114,9 @@ export default function download(
}
//do iframe dataURL download (old ch+FF):
var f = D.createElement("iframe");
const f = D.createElement("iframe");
D.body && D.body.appendChild(f);
if (!winMode) {
// force a mime that will download:
url = "data:" + url.replace(/^data:([\w/\-+]+)/, u);
@@ -105,13 +126,8 @@ export default function download(
setTimeout(function () {
D.body && D.body.removeChild(f);
}, 333);
}
// $FlowIssue
if (navigator.msSaveBlob) {
// IE10+ : (has Blob, but not a[download] or URL)
// $FlowIssue
return navigator.msSaveBlob(blob, fn);
return true;
}
if (self.URL) {
@@ -125,16 +141,21 @@ export default function download(
typeof m === "string"
) {
try {
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1.
return saver("data:" + m + ";base64," + self.btoa(blob));
} catch (y) {
// $FlowIssue
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1.
return saver("data:" + m + "," + encodeURIComponent(blob));
}
}
// Blob but not URL:
// @ts-expect-error ts-migrate(2588) FIXME: Cannot assign to 'fr' because it is a constant.
fr = new FileReader();
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'e' implicitly has an 'any' type.
fr.onload = function (e) {
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1.
saver(this.result);
};
@@ -142,5 +163,6 @@ export default function download(
fr.readAsDataURL(blob);
}
}
return true;
}

View File

@@ -1,5 +1,3 @@
// @flow
export function emojiToUrl(text: string) {
return `data:image/svg+xml;data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>${text}</text></svg>`;
}

View File

@@ -1,11 +1,17 @@
// @flow
import ExtendableError from "es6-error";
export class AuthorizationError extends ExtendableError {}
export class BadRequestError extends ExtendableError {}
export class NetworkError extends ExtendableError {}
export class NotFoundError extends ExtendableError {}
export class OfflineError extends ExtendableError {}
export class ServiceUnavailableError extends ExtendableError {}
export class RequestError extends ExtendableError {}
export class UpdateRequiredError extends ExtendableError {}

View File

@@ -1,22 +0,0 @@
// @flow
export default function getDataTransferFiles(event: SyntheticEvent<>): File[] {
let dataTransferItemsList = [];
// $FlowFixMe
if (event.dataTransfer) {
const dt = event.dataTransfer;
if (dt.files && dt.files.length) {
dataTransferItemsList = dt.files;
} else if (dt.items && dt.items.length) {
// During the drag even the dataTransfer.files is null
// but Chrome implements some drag store, which is accesible via dataTransfer.items
dataTransferItemsList = dt.items;
}
// $FlowFixMe
} else if (event.target && event.target.files) {
dataTransferItemsList = event.target.files;
}
// Convert from DataTransferItemsList to the native Array
return Array.prototype.slice.call(dataTransferItemsList);
}

View File

@@ -0,0 +1,29 @@
export default function getDataTransferFiles(
event:
| Event
| React.FormEvent<HTMLInputElement>
| React.DragEvent<HTMLElement>
): File[] {
let dataTransferItemsList = [];
// @ts-expect-error ts-migrate(2339) FIXME: Property 'dataTransfer' does not exist on type 'Sy... Remove this comment to see the full error message
if (event.dataTransfer) {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'dataTransfer' does not exist on type 'Sy... Remove this comment to see the full error message
const dt = event.dataTransfer;
if (dt.files && dt.files.length) {
dataTransferItemsList = dt.files;
} else if (dt.items && dt.items.length) {
// During the drag even the dataTransfer.files is null
// but Chrome implements some drag store, which is accesible via dataTransfer.items
dataTransferItemsList = dt.items;
}
// @ts-expect-error ts-migrate(2339) FIXME: Property 'files' does not exist on type 'EventTarg... Remove this comment to see the full error message
} else if (event.target && event.target.files) {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'files' does not exist on type 'EventTarg... Remove this comment to see the full error message
dataTransferItemsList = event.target.files;
}
// Convert from DataTransferItemsList to the native Array
return Array.prototype.slice.call(dataTransferItemsList);
}

View File

@@ -1,4 +1,3 @@
// @flow
import { createBrowserHistory } from "history";
const history = createBrowserHistory();

View File

@@ -1,4 +1,3 @@
// @flow
import {
enUS,
de,
@@ -33,6 +32,6 @@ const locales = {
pl_PL: pl,
};
export function dateLocale(userLocale: ?string) {
export function dateLocale(userLocale: string | null | undefined) {
return userLocale ? locales[userLocale] : undefined;
}

View File

@@ -1,7 +1,5 @@
// @flow
const inputs = ["input", "select", "button", "textarea"];
const inputs = ["input", "select", "button", "textarea"]; // detect if node is a text input element
// detect if node is a text input element
export default function isTextInput(element: HTMLElement): boolean {
return (
element &&

View File

@@ -1,4 +1,3 @@
// @flow
const isMac = window.navigator.platform === "MacIntel";
export const metaDisplay = isMac ? "⌘" : "Ctrl";
@@ -6,7 +5,7 @@ export const metaDisplay = isMac ? "⌘" : "Ctrl";
export const meta = isMac ? "cmd" : "ctrl";
export function isModKey(
event: KeyboardEvent | MouseEvent | SyntheticKeyboardEvent<>
event: KeyboardEvent | MouseEvent | React.KeyboardEvent
) {
return isMac ? event.metaKey : event.ctrlKey;
}

View File

@@ -1,12 +1,13 @@
// @flow
export function detectLanguage() {
const [ln, r] = navigator.language.split("-");
const region = (r || ln).toUpperCase();
return `${ln}_${region}`;
}
export function changeLanguage(toLanguageString: ?string, i18n: any) {
export function changeLanguage(
toLanguageString: string | null | undefined,
i18n: any
) {
if (toLanguageString && i18n.language !== toLanguageString) {
// Languages are stored in en_US format in the database, however the
// frontend translation framework (i18next) expects en-US

View File

@@ -1,3 +1,3 @@
// @flow
import { domMax } from "framer-motion";
export default domMax;

View File

@@ -1,4 +1,3 @@
// @flow
let hidden = "hidden";
let visibilityChange = "visibilitychange";
@@ -20,6 +19,5 @@ export function getVisibilityListener(): string {
}
export function getPageVisible(): boolean {
// $FlowFixMe
return !document[hidden];
}

View File

@@ -1,7 +1,6 @@
// @flow
import queryString from "query-string";
import Collection from "models/Collection";
import Document from "models/Document";
import Collection from "~/models/Collection";
import Document from "~/models/Document";
export function homePath(): string {
return "/home";
@@ -31,7 +30,7 @@ export function groupSettingsPath(): string {
return "/settings/groups";
}
export function collectionUrl(url: string, section: ?string): string {
export function collectionUrl(url: string, section?: string): string {
if (section) return `${url}/${section}`;
return url;
}
@@ -76,21 +75,21 @@ export function updateDocumentUrl(oldUrl: string, document: Document): string {
export function newDocumentPath(
collectionId: string,
params?: {
parentDocumentId?: string,
templateId?: string,
template?: boolean,
}
params: {
parentDocumentId?: string;
templateId?: string;
template?: boolean;
} = {}
): string {
return `/collection/${collectionId}/new?${queryString.stringify(params)}`;
}
export function searchUrl(
query?: string,
params?: {
collectionId?: string,
ref?: string,
}
params: {
collectionId?: string;
ref?: string;
} = {}
): string {
let search = queryString.stringify(params);
let route = "/search";

View File

@@ -1,10 +1,9 @@
// @flow
import * as Sentry from "@sentry/react";
import { Integrations } from "@sentry/tracing";
import { type RouterHistory } from "react-router-dom";
import env from "env";
import { History } from "history";
import env from "~/env";
export function initSentry(history: RouterHistory) {
export function initSentry(history: History) {
Sentry.init({
dsn: env.SENTRY_DSN,
environment: env.ENVIRONMENT,

View File

@@ -1,16 +1,17 @@
// @flow
import invariant from "invariant";
import { client } from "./ApiClient";
type Options = {
name?: string,
documentId?: string,
public?: boolean,
name?: string;
documentId?: string;
public?: boolean;
};
export const uploadFile = async (
file: File | Blob,
options?: Options = { name: "" }
options: Options = {
name: "",
}
) => {
const name = file instanceof File ? file.name : options.name;
const response = await client.post("/attachments.create", {
@@ -20,9 +21,7 @@ export const uploadFile = async (
size: file.size,
name,
});
invariant(response, "Response should be available");
const data = response.data;
const attachment = data.attachment;
const formData = new FormData();
@@ -31,9 +30,9 @@ export const uploadFile = async (
formData.append(key, data.form[key]);
}
// $FlowFixMe
// @ts-expect-error ts-migrate(2339) FIXME: Property 'blob' does not exist on type 'File | Blo... Remove this comment to see the full error message
if (file.blob) {
// $FlowFixMe
// @ts-expect-error ts-migrate(2339) FIXME: Property 'file' does not exist on type 'File | Blo... Remove this comment to see the full error message
formData.append("file", file.file);
} else {
formData.append("file", file);
@@ -43,18 +42,20 @@ export const uploadFile = async (
method: "post",
body: formData,
});
invariant(uploadResponse.ok, "Upload failed, try again?");
return attachment;
};
export const dataUrlToBlob = (dataURL: string) => {
var blobBin = atob(dataURL.split(",")[1]);
var array = [];
for (var i = 0; i < blobBin.length; i++) {
const blobBin = atob(dataURL.split(",")[1]);
const array = [];
for (let i = 0; i < blobBin.length; i++) {
array.push(blobBin.charCodeAt(i));
}
const file = new Blob([new Uint8Array(array)], { type: "image/png" });
const file = new Blob([new Uint8Array(array)], {
type: "image/png",
});
return file;
};

View File

@@ -1,4 +1,3 @@
// @flow
import { decodeURIComponentSafe } from "./urls";
describe("decodeURIComponentSafe", () => {

View File

@@ -1,9 +1,7 @@
// @flow
import { parseDomain } from "../../shared/utils/domains";
import { parseDomain } from "@shared/utils/domains";
export function isInternalUrl(href: string) {
if (href[0] === "/") return true;
const outline = parseDomain(window.location.href);
const parsed = parseDomain(href);