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:
@@ -1,4 +1,2 @@
|
||||
// @flow
|
||||
|
||||
export const validateColorHex = (color: string) =>
|
||||
/(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(color);
|
||||
@@ -1,19 +1,20 @@
|
||||
// @flow
|
||||
import { subDays, subMonths, subWeeks, subYears } from "date-fns";
|
||||
import { DateFilter } from "@shared/types";
|
||||
|
||||
export function subtractDate(
|
||||
date: Date,
|
||||
period: "day" | "week" | "month" | "year"
|
||||
) {
|
||||
export function subtractDate(date: Date, period: DateFilter) {
|
||||
switch (period) {
|
||||
case "day":
|
||||
return subDays(date, 1);
|
||||
|
||||
case "week":
|
||||
return subWeeks(date, 1);
|
||||
|
||||
case "month":
|
||||
return subMonths(date, 1);
|
||||
|
||||
case "year":
|
||||
return subYears(date, 1);
|
||||
|
||||
default:
|
||||
return date;
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
/* eslint-disable flowtype/require-valid-file-annotation */
|
||||
import { stripSubdomain, parseDomain, isCustomSubdomain } from "./domains";
|
||||
|
||||
// test suite is based on subset of parse-domain module we want to support
|
||||
// https://github.com/peerigon/parse-domain/blob/master/test/parseDomain.test.js
|
||||
describe("#parseDomain", () => {
|
||||
@@ -100,7 +98,9 @@ describe("#parseDomain", () => {
|
||||
});
|
||||
|
||||
it("should return null if the given value is not a string", () => {
|
||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'undefined' is not assignable to ... Remove this comment to see the full error message
|
||||
expect(parseDomain(undefined)).toBe(null);
|
||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type '{}' is not assignable to paramet... Remove this comment to see the full error message
|
||||
expect(parseDomain({})).toBe(null);
|
||||
expect(parseDomain("")).toBe(null);
|
||||
});
|
||||
@@ -113,35 +113,38 @@ describe("#parseDomain", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("#stripSubdomain", () => {
|
||||
test("to work with localhost", () => {
|
||||
expect(stripSubdomain("localhost")).toBe("localhost");
|
||||
});
|
||||
|
||||
test("to return domains without a subdomain", () => {
|
||||
expect(stripSubdomain("example")).toBe("example");
|
||||
expect(stripSubdomain("example.com")).toBe("example.com");
|
||||
expect(stripSubdomain("example.org:3000")).toBe("example.org");
|
||||
});
|
||||
|
||||
test("to remove subdomains", () => {
|
||||
expect(stripSubdomain("test.example.com")).toBe("example.com");
|
||||
expect(stripSubdomain("test.example.com:3000")).toBe("example.com");
|
||||
});
|
||||
});
|
||||
|
||||
describe("#isCustomSubdomain", () => {
|
||||
test("to work with localhost", () => {
|
||||
expect(isCustomSubdomain("localhost")).toBe(false);
|
||||
});
|
||||
|
||||
test("to return false for domains without a subdomain", () => {
|
||||
expect(isCustomSubdomain("example")).toBe(false);
|
||||
expect(isCustomSubdomain("example.com")).toBe(false);
|
||||
expect(isCustomSubdomain("example.org:3000")).toBe(false);
|
||||
});
|
||||
|
||||
test("to return false for www", () => {
|
||||
expect(isCustomSubdomain("www.example.com")).toBe(false);
|
||||
expect(isCustomSubdomain("www.example.com:3000")).toBe(false);
|
||||
});
|
||||
|
||||
test("to return true for subdomains", () => {
|
||||
expect(isCustomSubdomain("test.example.com")).toBe(true);
|
||||
expect(isCustomSubdomain("test.example.com:3000")).toBe(true);
|
||||
@@ -1,19 +1,17 @@
|
||||
// @flow
|
||||
import { trim } from "lodash";
|
||||
|
||||
type Domain = {
|
||||
tld: string,
|
||||
subdomain: string,
|
||||
domain: string,
|
||||
tld: string;
|
||||
subdomain: string;
|
||||
domain: string;
|
||||
};
|
||||
|
||||
// we originally used the parse-domain npm module however this includes
|
||||
// a large list of possible TLD's which increase the size of the bundle
|
||||
// unnecessarily for our usecase of trusted input.
|
||||
export function parseDomain(url: string): ?Domain {
|
||||
export function parseDomain(url: string): Domain | null | undefined {
|
||||
if (typeof url !== "string") return null;
|
||||
if (url === "") return null;
|
||||
|
||||
// strip extermeties and whitespace from input
|
||||
const normalizedDomain = trim(url.replace(/(https?:)?\/\//, ""));
|
||||
const parts = normalizedDomain.split(".");
|
||||
@@ -32,6 +30,7 @@ export function parseDomain(url: string): ?Domain {
|
||||
tld: cleanTLD(parts.slice(2).join(".")),
|
||||
};
|
||||
}
|
||||
|
||||
if (parts.length === 2) {
|
||||
return {
|
||||
subdomain: "",
|
||||
@@ -55,13 +54,13 @@ export function parseDomain(url: string): ?Domain {
|
||||
export function stripSubdomain(hostname: string) {
|
||||
const parsed = parseDomain(hostname);
|
||||
if (!parsed) return hostname;
|
||||
|
||||
if (parsed.tld) return `${parsed.domain}.${parsed.tld}`;
|
||||
return parsed.domain;
|
||||
}
|
||||
|
||||
export function isCustomSubdomain(hostname: string) {
|
||||
const parsed = parseDomain(hostname);
|
||||
|
||||
if (
|
||||
!parsed ||
|
||||
!parsed.subdomain ||
|
||||
@@ -70,6 +69,7 @@ export function isCustomSubdomain(hostname: string) {
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
// @flow
|
||||
|
||||
const CHECKBOX_REGEX = /\[(X|\s|_|-)\]\s(.*)?/gi;
|
||||
|
||||
export default function getTasks(text: string) {
|
||||
const matches = [...text.matchAll(CHECKBOX_REGEX)];
|
||||
let total = matches.length;
|
||||
const total = matches.length;
|
||||
|
||||
if (!total) {
|
||||
return {
|
||||
completed: 0,
|
||||
@@ -16,6 +15,9 @@ export default function getTasks(text: string) {
|
||||
match[1] === " " ? accumulator + 1 : accumulator,
|
||||
0
|
||||
);
|
||||
return { completed: total - notCompleted, total };
|
||||
return {
|
||||
completed: total - notCompleted,
|
||||
total,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,2 @@
|
||||
// @flow
|
||||
|
||||
export const validateIndexCharacters = (index: string) =>
|
||||
new RegExp("^[\x20-\x7E]+$").test(index);
|
||||
@@ -1,61 +0,0 @@
|
||||
// @flow
|
||||
import naturalSort from "./naturalSort";
|
||||
|
||||
describe("#naturalSort", () => {
|
||||
it("should sort a list of objects by the given key", () => {
|
||||
const items = [{ name: "Joan" }, { name: "Pedro" }, { name: "Mark" }];
|
||||
expect(naturalSort(items, "name")).toEqual([
|
||||
{ name: "Joan" },
|
||||
{ name: "Mark" },
|
||||
{ name: "Pedro" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should accept a function as the object key", () => {
|
||||
const items = [{ name: "Joan" }, { name: "Pedro" }, { name: "Mark" }];
|
||||
expect(naturalSort(items, (item) => item.name)).toEqual([
|
||||
{ name: "Joan" },
|
||||
{ name: "Mark" },
|
||||
{ name: "Pedro" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should accept natural-sort options", () => {
|
||||
const items = [
|
||||
{ name: "Joan" },
|
||||
{ name: "joan" },
|
||||
{ name: "Pedro" },
|
||||
{ name: "Mark" },
|
||||
];
|
||||
expect(
|
||||
naturalSort(items, "name", { direction: "desc", caseSensitive: true })
|
||||
).toEqual([
|
||||
{ name: "joan" },
|
||||
{ name: "Pedro" },
|
||||
{ name: "Mark" },
|
||||
{ name: "Joan" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should ignore non basic latin letters", () => {
|
||||
const items = [{ name: "Abel" }, { name: "Martín" }, { name: "Ávila" }];
|
||||
expect(naturalSort(items, "name")).toEqual([
|
||||
{ name: "Abel" },
|
||||
{ name: "Ávila" },
|
||||
{ name: "Martín" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should ignore emojis", () => {
|
||||
const items = [
|
||||
{ title: "🍔 Document 2" },
|
||||
{ title: "🐻 Document 3" },
|
||||
{ title: "🙂 Document 1" },
|
||||
];
|
||||
expect(naturalSort(items, "title")).toEqual([
|
||||
{ title: "🙂 Document 1" },
|
||||
{ title: "🍔 Document 2" },
|
||||
{ title: "🐻 Document 3" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
140
shared/utils/naturalSort.test.ts
Normal file
140
shared/utils/naturalSort.test.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import naturalSort from "./naturalSort";
|
||||
|
||||
describe("#naturalSort", () => {
|
||||
it("should sort a list of objects by the given key", () => {
|
||||
const items = [
|
||||
{
|
||||
name: "Joan",
|
||||
},
|
||||
{
|
||||
name: "Pedro",
|
||||
},
|
||||
{
|
||||
name: "Mark",
|
||||
},
|
||||
];
|
||||
expect(naturalSort(items, "name")).toEqual([
|
||||
{
|
||||
name: "Joan",
|
||||
},
|
||||
{
|
||||
name: "Mark",
|
||||
},
|
||||
{
|
||||
name: "Pedro",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should accept a function as the object key", () => {
|
||||
const items = [
|
||||
{
|
||||
name: "Joan",
|
||||
},
|
||||
{
|
||||
name: "Pedro",
|
||||
},
|
||||
{
|
||||
name: "Mark",
|
||||
},
|
||||
];
|
||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type '(item: any) => any' is not assig... Remove this comment to see the full error message
|
||||
expect(naturalSort(items, (item) => item.name)).toEqual([
|
||||
{
|
||||
name: "Joan",
|
||||
},
|
||||
{
|
||||
name: "Mark",
|
||||
},
|
||||
{
|
||||
name: "Pedro",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should accept natural-sort options", () => {
|
||||
const items = [
|
||||
{
|
||||
name: "Joan",
|
||||
},
|
||||
{
|
||||
name: "joan",
|
||||
},
|
||||
{
|
||||
name: "Pedro",
|
||||
},
|
||||
{
|
||||
name: "Mark",
|
||||
},
|
||||
];
|
||||
expect(
|
||||
naturalSort(items, "name", {
|
||||
direction: "desc",
|
||||
caseSensitive: true,
|
||||
})
|
||||
).toEqual([
|
||||
{
|
||||
name: "joan",
|
||||
},
|
||||
{
|
||||
name: "Pedro",
|
||||
},
|
||||
{
|
||||
name: "Mark",
|
||||
},
|
||||
{
|
||||
name: "Joan",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should ignore non basic latin letters", () => {
|
||||
const items = [
|
||||
{
|
||||
name: "Abel",
|
||||
},
|
||||
{
|
||||
name: "Martín",
|
||||
},
|
||||
{
|
||||
name: "Ávila",
|
||||
},
|
||||
];
|
||||
expect(naturalSort(items, "name")).toEqual([
|
||||
{
|
||||
name: "Abel",
|
||||
},
|
||||
{
|
||||
name: "Ávila",
|
||||
},
|
||||
{
|
||||
name: "Martín",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should ignore emojis", () => {
|
||||
const items = [
|
||||
{
|
||||
title: "🍔 Document 2",
|
||||
},
|
||||
{
|
||||
title: "🐻 Document 3",
|
||||
},
|
||||
{
|
||||
title: "🙂 Document 1",
|
||||
},
|
||||
];
|
||||
expect(naturalSort(items, "title")).toEqual([
|
||||
{
|
||||
title: "🙂 Document 1",
|
||||
},
|
||||
{
|
||||
title: "🍔 Document 2",
|
||||
},
|
||||
{
|
||||
title: "🐻 Document 3",
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1,40 +1,40 @@
|
||||
// @flow
|
||||
import emojiRegex from "emoji-regex";
|
||||
import { deburr } from "lodash";
|
||||
import naturalSort from "natural-sort";
|
||||
|
||||
type NaturalSortOptions = {
|
||||
caseSensitive?: boolean,
|
||||
direction?: "asc" | "desc",
|
||||
caseSensitive?: boolean;
|
||||
direction?: "asc" | "desc";
|
||||
};
|
||||
|
||||
const sorter = naturalSort();
|
||||
|
||||
const regex = emojiRegex();
|
||||
|
||||
const stripEmojis = (value: string) => value.replace(regex, "");
|
||||
|
||||
const cleanValue = (value: string) => stripEmojis(deburr(value));
|
||||
|
||||
function getSortByField<T: Object>(
|
||||
function getSortByField<T extends Record<string, any>>(
|
||||
item: T,
|
||||
keyOrCallback: string | ((obj: T) => string)
|
||||
keyOrCallback: string | (() => string)
|
||||
) {
|
||||
const field =
|
||||
typeof keyOrCallback === "string"
|
||||
? item[keyOrCallback]
|
||||
: keyOrCallback(item);
|
||||
: // @ts-expect-error ts-migrate(2554) FIXME: Expected 0 arguments, but got 1.
|
||||
keyOrCallback(item);
|
||||
return cleanValue(field);
|
||||
}
|
||||
|
||||
function naturalSortBy<T>(
|
||||
items: T[],
|
||||
key: string | ((obj: T) => string),
|
||||
key: string | (() => string),
|
||||
sortOptions?: NaturalSortOptions
|
||||
): T[] {
|
||||
if (!items) return [];
|
||||
|
||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'NaturalSortOptions' is not assig... Remove this comment to see the full error message
|
||||
const sort = sortOptions ? naturalSort(sortOptions) : sorter;
|
||||
return items.sort((a: any, b: any): -1 | 0 | 1 =>
|
||||
// @ts-expect-error ts-migrate(2322) FIXME: Type 'number' is not assignable to type '0 | 1 | -... Remove this comment to see the full error message
|
||||
sort(getSortByField(a, key), getSortByField(b, key))
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import parseDocumentSlug from "./parseDocumentSlug";
|
||||
|
||||
describe("#parseDocumentSlug", () => {
|
||||
@@ -1,7 +1,6 @@
|
||||
// @flow
|
||||
|
||||
export default function parseDocumentSlug(url: string) {
|
||||
let parsed;
|
||||
|
||||
if (url[0] === "/") {
|
||||
parsed = url;
|
||||
} else {
|
||||
@@ -1,26 +1,21 @@
|
||||
/* eslint-disable flowtype/require-valid-file-annotation */
|
||||
import parseTitle from "./parseTitle";
|
||||
|
||||
it("should trim the title", () => {
|
||||
expect(parseTitle(`# Lots of space `).title).toBe("Lots of space");
|
||||
});
|
||||
|
||||
it("should extract first title", () => {
|
||||
expect(parseTitle(`# Title one\n# Title two`).title).toBe("Title one");
|
||||
});
|
||||
|
||||
it("should remove escape characters", () => {
|
||||
expect(parseTitle(`# Thing \\- one`).title).toBe("Thing - one");
|
||||
expect(parseTitle(`# \\[wip\\] Title`).title).toBe("[wip] Title");
|
||||
expect(parseTitle(`# \\> Title`).title).toBe("> Title");
|
||||
});
|
||||
|
||||
it("should parse emoji if first character", () => {
|
||||
const parsed = parseTitle(`# 😀 Title`);
|
||||
expect(parsed.title).toBe("😀 Title");
|
||||
expect(parsed.emoji).toBe("😀");
|
||||
});
|
||||
|
||||
it("should not parse emoji if not first character", () => {
|
||||
const parsed = parseTitle(`# Title 🌈`);
|
||||
expect(parsed.title).toBe("Title 🌈");
|
||||
@@ -1,8 +1,7 @@
|
||||
// @flow
|
||||
import emojiRegex from "emoji-regex";
|
||||
import unescape from "./unescape";
|
||||
|
||||
export default function parseTitle(text: string = "") {
|
||||
export default function parseTitle(text = "") {
|
||||
const regex = emojiRegex();
|
||||
|
||||
// find and extract title
|
||||
@@ -18,5 +17,8 @@ export default function parseTitle(text: string = "") {
|
||||
const startsWithEmoji = firstEmoji && title.startsWith(firstEmoji);
|
||||
const emoji = startsWithEmoji ? firstEmoji : undefined;
|
||||
|
||||
return { title, emoji };
|
||||
return {
|
||||
title,
|
||||
emoji,
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
// @flow
|
||||
|
||||
export function slackAuth(
|
||||
state: string,
|
||||
scopes: string[] = [
|
||||
@@ -8,8 +6,9 @@ export function slackAuth(
|
||||
"identity.avatar",
|
||||
"identity.team",
|
||||
],
|
||||
// @ts-expect-error ts-migrate(2322) FIXME: Type 'string | undefined' is not assignable to typ... Remove this comment to see the full error message
|
||||
clientId: string = process.env.SLACK_KEY,
|
||||
redirectUri: string = `${process.env.URL}/auth/slack.callback`
|
||||
redirectUri = `${process.env.URL}/auth/slack.callback`
|
||||
): string {
|
||||
const baseUrl = "https://slack.com/oauth/authorize";
|
||||
const params = {
|
||||
@@ -18,11 +17,9 @@ export function slackAuth(
|
||||
redirect_uri: redirectUri,
|
||||
state,
|
||||
};
|
||||
|
||||
const urlParams = Object.keys(params)
|
||||
.map((key) => `${key}=${encodeURIComponent(params[key])}`)
|
||||
.join("&");
|
||||
|
||||
return `${baseUrl}?${urlParams}`;
|
||||
}
|
||||
|
||||
@@ -50,7 +47,7 @@ export function changelogUrl(): string {
|
||||
return `https://www.getoutline.com/changelog`;
|
||||
}
|
||||
|
||||
export function signin(service: string = "slack"): string {
|
||||
export function signin(service = "slack"): string {
|
||||
return `${process.env.URL}/auth/${service}`;
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
// @flow
|
||||
import slugify from "slugify";
|
||||
|
||||
// Slugify, escape, and remove periods from headings so that they are
|
||||
// compatible with url hashes AND dom selectors
|
||||
export default function safeSlugify(text: string) {
|
||||
return `h-${escape(slugify(text, { lower: true }).replace(".", "-"))}`;
|
||||
}
|
||||
10
shared/utils/slugify.ts
Normal file
10
shared/utils/slugify.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import slugify from "slugify"; // Slugify, escape, and remove periods from headings so that they are
|
||||
// compatible with url hashes AND dom selectors
|
||||
|
||||
export default function safeSlugify(text: string) {
|
||||
return `h-${escape(
|
||||
slugify(text, {
|
||||
lower: true,
|
||||
}).replace(".", "-")
|
||||
)}`;
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
// @flow
|
||||
|
||||
const unescape = (text: string) => {
|
||||
return text
|
||||
.replace(/\\([\\`*{}[\]()#+\-.!_>])/g, "$1")
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
const env = typeof window !== "undefined" ? window.env : process.env;
|
||||
|
||||
export function cdnPath(path: string): string {
|
||||
@@ -1,24 +1,24 @@
|
||||
// @flow
|
||||
import path from "path";
|
||||
// @ts-expect-error ts-migrate(2724) FIXME: '"jszip"' has no exported member named 'ZipObject'... Remove this comment to see the full error message
|
||||
import JSZip, { ZipObject } from "jszip";
|
||||
|
||||
export type Item = {|
|
||||
path: string,
|
||||
dir: string,
|
||||
name: string,
|
||||
depth: number,
|
||||
metadata: Object,
|
||||
type: "collection" | "document" | "attachment",
|
||||
item: ZipObject,
|
||||
|};
|
||||
export type Item = {
|
||||
path: string;
|
||||
dir: string;
|
||||
name: string;
|
||||
depth: number;
|
||||
metadata: Record<string, any>;
|
||||
type: "collection" | "document" | "attachment";
|
||||
|
||||
item: ZipObject;
|
||||
};
|
||||
|
||||
export async function parseOutlineExport(
|
||||
input: File | Buffer
|
||||
): Promise<Item[]> {
|
||||
const zip = await JSZip.loadAsync(input);
|
||||
|
||||
// this is so we can use async / await a little easier
|
||||
let items: Item[] = [];
|
||||
const items: Item[] = [];
|
||||
zip.forEach(async function (rawPath, item) {
|
||||
const itemPath = rawPath.replace(/\/$/, "");
|
||||
const dir = path.dirname(itemPath);
|
||||
@@ -32,6 +32,7 @@ export async function parseOutlineExport(
|
||||
|
||||
// attempt to parse extra metadata from zip comment
|
||||
let metadata = {};
|
||||
|
||||
try {
|
||||
metadata = item.comment ? JSON.parse(item.comment) : {};
|
||||
} catch (err) {
|
||||
@@ -47,12 +48,15 @@ export async function parseOutlineExport(
|
||||
}
|
||||
|
||||
let type;
|
||||
|
||||
if (depth === 0 && item.dir && name) {
|
||||
type = "collection";
|
||||
}
|
||||
|
||||
if (depth > 0 && !item.dir && item.name.endsWith(".md")) {
|
||||
type = "document";
|
||||
}
|
||||
|
||||
if (depth > 0 && !item.dir && itemPath.includes("uploads")) {
|
||||
type = "attachment";
|
||||
}
|
||||
@@ -66,11 +70,11 @@ export async function parseOutlineExport(
|
||||
dir,
|
||||
name,
|
||||
depth,
|
||||
// @ts-expect-error ts-migrate(2322) FIXME: Type 'string' is not assignable to type '"collecti... Remove this comment to see the full error message
|
||||
type,
|
||||
metadata,
|
||||
item,
|
||||
});
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
Reference in New Issue
Block a user