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,5 +1,5 @@
// @flow
export const USER_PRESENCE_INTERVAL = 5000;
export const MAX_AVATAR_DISPLAY = 6;
export const MAX_TITLE_LENGTH = 100;

View File

@@ -1,16 +1,15 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import Abstract from "./Abstract";
describe("Abstract", () => {
const match = Abstract.ENABLED[0];
const match2 = Abstract.ENABLED[1];
test("to be enabled on share subdomain link", () => {
expect(
"https://share.goabstract.com/aaec8bba-f473-4f64-96e7-bff41c70ff8a".match(
match
)
).toBeTruthy();
expect(
"https://share.abstract.com/aaec8bba-f473-4f64-96e7-bff41c70ff8a".match(
match
@@ -24,7 +23,6 @@ describe("Abstract", () => {
match2
)
).toBeTruthy();
expect(
"https://app.abstract.com/share/aaec8bba-f473-4f64-96e7-bff41c70ff8a".match(
match2

View File

@@ -1,13 +1,12 @@
// @flow
import * as React from "react";
import Frame from "./components/Frame";
type Props = {|
attrs: {|
href: string,
matches: string[],
|},
|};
type Props = {
attrs: {
href: string;
matches: string[];
};
};
export default class Abstract extends React.Component<Props> {
static ENABLED = [
@@ -18,10 +17,10 @@ export default class Abstract extends React.Component<Props> {
render() {
const { matches } = this.props.attrs;
const shareId = matches[1];
return (
<Frame
{...this.props}
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ src: string; title: string; attrs: { href:... Remove this comment to see the full error message
src={`https://app.goabstract.com/embed/${shareId}`}
title={`Abstract (${shareId})`}
/>

View File

@@ -1,8 +1,8 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import Airtable from "./Airtable";
describe("Airtable", () => {
const match = Airtable.ENABLED[0];
test("to be enabled on share link", () => {
expect("https://airtable.com/shrEoQs3erLnppMie".match(match)).toBeTruthy();
});

View File

@@ -1,15 +1,13 @@
// @flow
import * as React from "react";
import Frame from "./components/Frame";
const URL_REGEX = new RegExp("https://airtable.com/(?:embed/)?(shr.*)$");
type Props = {|
attrs: {|
href: string,
matches: string[],
|},
|};
type Props = {
attrs: {
href: string;
matches: string[];
};
};
export default class Airtable extends React.Component<Props> {
static ENABLED = [URL_REGEX];
@@ -17,10 +15,10 @@ export default class Airtable extends React.Component<Props> {
render() {
const { matches } = this.props.attrs;
const shareId = matches[1];
return (
<Frame
{...this.props}
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ src: string; title: string; border: true; ... Remove this comment to see the full error message
src={`https://airtable.com/embed/${shareId}`}
title={`Airtable (${shareId})`}
border

View File

@@ -1,8 +1,8 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import Bilibili from "./Bilibili";
describe("Bilibili", () => {
const match = Bilibili.ENABLED[0];
test("to be enabled on video link", () => {
expect(
"https://www.bilibili.com/video/BV1CV411s7jd?spm_id_from=333.999.0.0".match(

View File

@@ -1,15 +1,13 @@
// @flow
import * as React from "react";
import Frame from "./components/Frame";
const URL_REGEX = /(?:https?:\/\/)?(www\.bilibili\.com)\/video\/([\w\d]+)?(\?\S+)?/i;
type Props = {|
attrs: {|
href: string,
matches: string[],
|},
|};
type Props = {
attrs: {
href: string;
matches: string[];
};
};
export default class Vimeo extends React.Component<Props> {
static ENABLED = [URL_REGEX];
@@ -17,10 +15,10 @@ export default class Vimeo extends React.Component<Props> {
render() {
const { matches } = this.props.attrs;
const videoId = matches[2];
return (
<Frame
{...this.props}
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ src: string; title: string; attrs: { href:... Remove this comment to see the full error message
src={`https://player.bilibili.com/player.html?bvid=${videoId}&page=1&high_quality=1`}
title={`Bilibili Embed (${videoId})`}
/>

View File

@@ -1,8 +1,8 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import Cawemo from "./Cawemo";
describe("Cawemo", () => {
const match = Cawemo.ENABLED[0];
test("to be enabled on embed link", () => {
expect(
"https://cawemo.com/embed/a82e9f22-e283-4253-8d11".match(match)

View File

@@ -1,15 +1,13 @@
// @flow
import * as React from "react";
import Frame from "./components/Frame";
const URL_REGEX = new RegExp("https?://cawemo.com/(?:share|embed)/(.*)$");
type Props = {|
attrs: {|
href: string,
matches: string[],
|},
|};
type Props = {
attrs: {
href: string;
matches: string[];
};
};
export default class Cawemo extends React.Component<Props> {
static ENABLED = [URL_REGEX];
@@ -17,10 +15,10 @@ export default class Cawemo extends React.Component<Props> {
render() {
const { matches } = this.props.attrs;
const shareId = matches[1];
return (
<Frame
{...this.props}
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ src: string; title: string; border: true; ... Remove this comment to see the full error message
src={`https://cawemo.com/embed/${shareId}`}
title={"Cawemo Embed"}
border

View File

@@ -1,8 +1,8 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import ClickUp from "./ClickUp";
describe("ClickUp", () => {
const match = ClickUp.ENABLED[0];
test("to be enabled on share link", () => {
expect(
"https://share.clickup.com/b/h/6-9310960-2/c9d837d74182317".match(match)

View File

@@ -1,17 +1,15 @@
// @flow
import * as React from "react";
import Frame from "./components/Frame";
const URL_REGEX = new RegExp(
"^https?://share.clickup.com/[a-z]/[a-z]/(.*)/(.*)$"
);
type Props = {|
attrs: {|
href: string,
matches: string[],
|},
|};
type Props = {
attrs: {
href: string;
matches: string[];
};
};
export default class ClickUp extends React.Component<Props> {
static ENABLED = [URL_REGEX];
@@ -20,6 +18,7 @@ export default class ClickUp extends React.Component<Props> {
return (
<Frame
{...this.props}
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ src: string; title: string; attrs: { href:... Remove this comment to see the full error message
src={this.props.attrs.href}
title="ClickUp Embed"
/>

View File

@@ -1,8 +1,8 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import Codepen from "./Codepen";
describe("Codepen", () => {
const match = Codepen.ENABLED[0];
test("to be enabled on pen link", () => {
expect(
"https://codepen.io/chriscoyier/pen/gfdDu".match(match)

View File

@@ -1,22 +1,20 @@
// @flow
import * as React from "react";
import Frame from "./components/Frame";
const URL_REGEX = new RegExp("^https://codepen.io/(.*?)/(pen|embed)/(.*)$");
type Props = {|
attrs: {|
href: string,
matches: string[],
|},
|};
type Props = {
attrs: {
href: string;
matches: string[];
};
};
export default class Codepen extends React.Component<Props> {
static ENABLED = [URL_REGEX];
render() {
const normalizedUrl = this.props.attrs.href.replace(/\/pen\//, "/embed/");
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ src: string; title: string; attrs: { href:... Remove this comment to see the full error message
return <Frame {...this.props} src={normalizedUrl} title="Codepen Embed" />;
}
}

View File

@@ -1,13 +1,12 @@
// @flow
import * as React from "react";
import Frame from "./components/Frame";
type Props = {|
attrs: {|
href: string,
matches: string[],
|},
|};
type Props = {
attrs: {
href: string;
matches: string[];
};
};
export default class Descript extends React.Component<Props> {
static ENABLED = [new RegExp("https?://share.descript.com/view/(\\w+)$")];
@@ -15,10 +14,10 @@ export default class Descript extends React.Component<Props> {
render() {
const { matches } = this.props.attrs;
const shareId = matches[1];
return (
<Frame
{...this.props}
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ src: string; title: string; width: string;... Remove this comment to see the full error message
src={`https://share.descript.com/embed/${shareId}`}
title={`Descript (${shareId})`}
width="400px"

View File

@@ -1,4 +1,3 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import Diagrams from "./Diagrams";
describe("Diagrams", () => {

View File

@@ -1,16 +1,14 @@
// @flow
import * as React from "react";
import Frame from "./components/Frame";
import Image from "./components/Image";
const URL_REGEX = /^https:\/\/viewer\.diagrams\.net\/.*(title=\\w+)?/;
type Props = {|
attrs: {|
href: string,
matches: string[],
|},
|};
type Props = {
attrs: {
href: string;
matches: string[];
};
};
export default class Diagrams extends React.Component<Props> {
static ENABLED = [URL_REGEX];
@@ -35,6 +33,7 @@ export default class Diagrams extends React.Component<Props> {
return (
<Frame
{...this.props}
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ src: string; title: string; border: true; ... Remove this comment to see the full error message
src={this.embedUrl}
title={this.title}
border

View File

@@ -1,8 +1,8 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import Figma from "./Figma";
describe("Figma", () => {
const match = Figma.ENABLED[0];
test("to be enabled on file link", () => {
expect(
"https://www.figma.com/file/LKQ4FJ4bTnCSjedbRpk931".match(match)

View File

@@ -1,17 +1,15 @@
// @flow
import * as React from "react";
import Frame from "./components/Frame";
const URL_REGEX = new RegExp(
"https://([w.-]+.)?figma.com/(file|proto)/([0-9a-zA-Z]{22,128})(?:/.*)?$"
);
type Props = {|
attrs: {|
href: string,
matches: string[],
|},
|};
type Props = {
attrs: {
href: string;
matches: string[];
};
};
export default class Figma extends React.Component<Props> {
static ENABLED = [URL_REGEX];
@@ -20,6 +18,7 @@ export default class Figma extends React.Component<Props> {
return (
<Frame
{...this.props}
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ src: string; title: string; border: true; ... Remove this comment to see the full error message
src={`https://www.figma.com/embed?embed_host=outline&url=${this.props.attrs.href}`}
title="Figma Embed"
border

View File

@@ -1,8 +1,8 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import Framer from "./Framer";
describe("Framer", () => {
const match = Framer.ENABLED[0];
test("to be enabled on share link", () => {
expect("https://framer.cloud/PVwJO".match(match)).toBeTruthy();
});

View File

@@ -1,15 +1,13 @@
// @flow
import * as React from "react";
import Frame from "./components/Frame";
const URL_REGEX = new RegExp("^https://framer.cloud/(.*)$");
type Props = {|
attrs: {|
href: string,
matches: string[],
|},
|};
type Props = {
attrs: {
href: string;
matches: string[];
};
};
export default class Framer extends React.Component<Props> {
static ENABLED = [URL_REGEX];
@@ -18,6 +16,7 @@ export default class Framer extends React.Component<Props> {
return (
<Frame
{...this.props}
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ src: string; title: string; border: true; ... Remove this comment to see the full error message
src={this.props.attrs.href}
title="Framer Embed"
border

View File

@@ -1,21 +1,19 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import Gist from "./Gist";
describe("Gist", () => {
const match = Gist.ENABLED[0];
test("to be enabled on gist link", () => {
expect(
"https://gist.github.com/wmertens/0b4fd66ca7055fd290ecc4b9d95271a9".match(
match
)
).toBeTruthy();
expect(
"https://gist.github.com/n3n/eb51ada6308b539d388c8ff97711adfa".match(
match
)
).toBeTruthy();
expect(
"https://gist.github.com/ShubhanjanMedhi-dev/900c9c14093611898a4a085938bb90d9".match(
match

View File

@@ -1,20 +1,19 @@
// @flow
import * as React from "react";
const URL_REGEX = new RegExp(
"^https://gist.github.com/([a-zA-Z\\d](?:[a-zA-Z\\d]|-(?=[a-zA-Z\\d])){0,38})/(.*)$"
);
type Props = {|
isSelected: boolean,
attrs: {|
href: string,
matches: string[],
|},
|};
type Props = {
isSelected: boolean;
attrs: {
href: string;
matches: string[];
};
};
class Gist extends React.Component<Props> {
static ENABLED = [URL_REGEX];
ref = React.createRef<HTMLIFrameElement>();
get id() {
@@ -35,8 +34,9 @@ class Gist extends React.Component<Props> {
if (!iframe) return;
const id = this.id;
// $FlowFixMe
// @ts-expect-error ts-migrate(2339) FIXME: Property 'document' does not exist on type 'HTMLIF... Remove this comment to see the full error message
let doc = iframe.document;
if (iframe.contentDocument) {
doc = iframe.contentDocument;
} else if (iframe.contentWindow) {
@@ -48,9 +48,7 @@ class Gist extends React.Component<Props> {
const styles =
"<style>*{ font-size:12px; } body { margin: 0; } .gist .blob-wrapper.data { max-height:150px; overflow:auto; }</style>";
const iframeHtml = `<html><head><base target="_parent">${styles}</head><body>${gistScript}</body></html>`;
if (!doc) return;
doc.open();
doc.writeln(iframeHtml);
doc.close();
@@ -58,11 +56,11 @@ class Gist extends React.Component<Props> {
render() {
const id = this.id;
return (
<iframe
className={this.props.isSelected ? "ProseMirror-selectednode" : ""}
ref={this.ref}
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ className: string; ref: RefObject<HTMLIFra... Remove this comment to see the full error message
type="text/html"
frameBorder="0"
width="100%"

View File

@@ -1,8 +1,8 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import GoogleCalendar from "./GoogleCalendar";
describe("GoogleCalendar", () => {
const match = GoogleCalendar.ENABLED[0];
test("to be enabled on share link", () => {
expect(
"https://calendar.google.com/calendar/embed?src=tom%40outline.com&ctz=America%2FSao_Paulo".match(

View File

@@ -1,17 +1,15 @@
// @flow
import * as React from "react";
import Frame from "./components/Frame";
const URL_REGEX = new RegExp(
"^https?://calendar.google.com/calendar/embed\\?src=(.*)$"
);
type Props = {|
attrs: {|
href: string,
matches: string[],
|},
|};
type Props = {
attrs: {
href: string;
matches: string[];
};
};
export default class GoogleCalendar extends React.Component<Props> {
static ENABLED = [URL_REGEX];
@@ -20,6 +18,7 @@ export default class GoogleCalendar extends React.Component<Props> {
return (
<Frame
{...this.props}
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ src: string; title: string; border: true; ... Remove this comment to see the full error message
src={this.props.attrs.href}
title="Google Calendar"
border

View File

@@ -1,8 +1,8 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import GoogleDataStudio from "./GoogleDataStudio";
describe("GoogleDataStudio", () => {
const match = GoogleDataStudio.ENABLED[0];
test("to be enabled on share link", () => {
expect(
"https://datastudio.google.com/embed/reporting/aab01789-f3a2-4ff3-9cba-c4c94c4a92e8/page/7zFD".match(

View File

@@ -1,4 +1,3 @@
// @flow
import * as React from "react";
import Frame from "./components/Frame";
import Image from "./components/Image";
@@ -6,13 +5,12 @@ import Image from "./components/Image";
const URL_REGEX = new RegExp(
"^https?://datastudio.google.com/(embed|u/0)/reporting/(.*)/page/(.*)(/edit)?$"
);
type Props = {|
attrs: {|
href: string,
matches: string[],
|},
|};
type Props = {
attrs: {
href: string;
matches: string[];
};
};
export default class GoogleDataStudio extends React.Component<Props> {
static ENABLED = [URL_REGEX];
@@ -21,6 +19,7 @@ export default class GoogleDataStudio extends React.Component<Props> {
return (
<Frame
{...this.props}
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ src: string; icon: Element; canonicalUrl: ... Remove this comment to see the full error message
src={this.props.attrs.href.replace("u/0", "embed").replace("/edit", "")}
icon={
<Image

View File

@@ -1,8 +1,8 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import GoogleDocs from "./GoogleDocs";
describe("GoogleDocs", () => {
const match = GoogleDocs.ENABLED[0];
test("to be enabled on share link", () => {
expect(
"https://docs.google.com/document/d/e/2PACX-1vTdddHPoZ5M_47wmSHCoigRIt2cj_Pd-kgtaNQY6H0Jzn0_CVGbxC1GcK5IoNzU615lzguexFwxasAW/pubhtml".match(

View File

@@ -1,16 +1,14 @@
// @flow
import * as React from "react";
import Frame from "./components/Frame";
import Image from "./components/Image";
const URL_REGEX = new RegExp("^https?://docs.google.com/document/(.*)$");
type Props = {|
attrs: {|
href: string,
matches: string[],
|},
|};
type Props = {
attrs: {
href: string;
matches: string[];
};
};
export default class GoogleDocs extends React.Component<Props> {
static ENABLED = [URL_REGEX];
@@ -19,6 +17,7 @@ export default class GoogleDocs extends React.Component<Props> {
return (
<Frame
{...this.props}
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ src: string; icon: Element; canonicalUrl: ... Remove this comment to see the full error message
src={this.props.attrs.href.replace("/edit", "/preview")}
icon={
<Image

View File

@@ -1,8 +1,8 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import GoogleDrawings from "./GoogleDrawings";
describe("GoogleDrawings", () => {
const match = GoogleDrawings.ENABLED[0];
test("to be enabled on share link", () => {
expect(
"https://docs.google.com/drawings/d/1zDLtJ4HSCnjGCGSoCgqGe3F8p6o7R8Vjk8MDR6dKf-U/edit".match(

View File

@@ -1,4 +1,3 @@
// @flow
import * as React from "react";
import Frame from "./components/Frame";
import Image from "./components/Image";
@@ -6,13 +5,12 @@ import Image from "./components/Image";
const URL_REGEX = new RegExp(
"^https://docs.google.com/drawings/d/(.*)/(edit|preview)(.*)$"
);
type Props = {|
attrs: {|
href: string,
matches: string[],
|},
|};
type Props = {
attrs: {
href: string;
matches: string[];
};
};
export default class GoogleDrawings extends React.Component<Props> {
static ENABLED = [URL_REGEX];
@@ -21,6 +19,7 @@ export default class GoogleDrawings extends React.Component<Props> {
return (
<Frame
{...this.props}
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ src: string; icon: Element; canonicalUrl: ... Remove this comment to see the full error message
src={this.props.attrs.href.replace("/edit", "/preview")}
icon={
<Image

View File

@@ -1,4 +1,3 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import GoogleDrive from "./GoogleDrive";
describe("GoogleDrive", () => {

View File

@@ -1,16 +1,14 @@
// @flow
import * as React from "react";
import Frame from "./components/Frame";
import Image from "./components/Image";
const URL_REGEX = new RegExp("^https?://drive.google.com/file/d/(.*)$");
type Props = {|
attrs: {|
href: string,
matches: string[],
|},
|};
type Props = {
attrs: {
href: string;
matches: string[];
};
};
export default class GoogleDrive extends React.Component<Props> {
static ENABLED = [URL_REGEX];
@@ -18,6 +16,7 @@ export default class GoogleDrive extends React.Component<Props> {
render() {
return (
<Frame
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ src: string; icon: Element; title: string;... Remove this comment to see the full error message
src={this.props.attrs.href.replace("/view", "/preview")}
icon={
<Image

View File

@@ -1,8 +1,8 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import GoogleSheets from "./GoogleSheets";
describe("GoogleSheets", () => {
const match = GoogleSheets.ENABLED[0];
test("to be enabled on share link", () => {
expect(
"https://docs.google.com/spreadsheets/d/e/2PACX-1vTdddHPoZ5M_47wmSHCoigRIt2cj_Pd-kgtaNQY6H0Jzn0_CVGbxC1GcK5IoNzU615lzguexFwxasAW/pub".match(

View File

@@ -1,16 +1,14 @@
// @flow
import * as React from "react";
import Frame from "./components/Frame";
import Image from "./components/Image";
const URL_REGEX = new RegExp("^https?://docs.google.com/spreadsheets/d/(.*)$");
type Props = {|
attrs: {|
href: string,
matches: string[],
|},
|};
type Props = {
attrs: {
href: string;
matches: string[];
};
};
export default class GoogleSheets extends React.Component<Props> {
static ENABLED = [URL_REGEX];
@@ -19,6 +17,7 @@ export default class GoogleSheets extends React.Component<Props> {
return (
<Frame
{...this.props}
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ src: string; icon: Element; canonicalUrl: ... Remove this comment to see the full error message
src={this.props.attrs.href.replace("/edit", "/preview")}
icon={
<Image

View File

@@ -1,8 +1,8 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import GoogleSlides from "./GoogleSlides";
describe("GoogleSlides", () => {
const match = GoogleSlides.ENABLED[0];
test("to be enabled on share link", () => {
expect(
"https://docs.google.com/presentation/d/e/2PACX-1vTdddHPoZ5M_47wmSHCoigRIt2cj_Pd-kgtaNQY6H0Jzn0_CVGbxC1GcK5IoNzU615lzguexFwxasAW/pub?start=false&loop=false&delayms=3000".match(

View File

@@ -1,16 +1,14 @@
// @flow
import * as React from "react";
import Frame from "./components/Frame";
import Image from "./components/Image";
const URL_REGEX = new RegExp("^https?://docs.google.com/presentation/d/(.*)$");
type Props = {|
attrs: {|
href: string,
matches: string[],
|},
|};
type Props = {
attrs: {
href: string;
matches: string[];
};
};
export default class GoogleSlides extends React.Component<Props> {
static ENABLED = [URL_REGEX];
@@ -19,6 +17,7 @@ export default class GoogleSlides extends React.Component<Props> {
return (
<Frame
{...this.props}
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ src: string; icon: Element; canonicalUrl: ... Remove this comment to see the full error message
src={this.props.attrs.href
.replace("/edit", "/preview")
.replace("/pub", "/embed")}

View File

@@ -1,8 +1,8 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import InVision from "./InVision";
describe("InVision", () => {
const match = InVision.ENABLED[0];
test("to be enabled on shortlink", () => {
expect("https://invis.io/69PG07QYQTE".match(match)).toBeTruthy();
});

View File

@@ -1,19 +1,16 @@
// @flow
import * as React from "react";
import ImageZoom from "react-medium-image-zoom";
import Frame from "./components/Frame";
const IFRAME_REGEX = /^https:\/\/(invis\.io\/.*)|(projects\.invisionapp\.com\/share\/.*)$/;
const IMAGE_REGEX = /^https:\/\/(opal\.invisionapp\.com\/static-signed\/live-embed\/.*)$/;
type Props = {|
isSelected: boolean,
attrs: {|
href: string,
matches: string[],
|},
|};
type Props = {
isSelected: boolean;
attrs: {
href: string;
matches: string[];
};
};
export default class InVision extends React.Component<Props> {
static ENABLED = [IFRAME_REGEX, IMAGE_REGEX];
@@ -22,6 +19,7 @@ export default class InVision extends React.Component<Props> {
if (IMAGE_REGEX.test(this.props.attrs.href)) {
return (
<ImageZoom
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
className={this.props.isSelected ? "ProseMirror-selectednode" : ""}
image={{
src: this.props.attrs.href,
@@ -35,9 +33,11 @@ export default class InVision extends React.Component<Props> {
/>
);
}
return (
<Frame
{...this.props}
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ src: string; title: string; isSelected: bo... Remove this comment to see the full error message
src={this.props.attrs.href}
title="InVision Embed"
/>

View File

@@ -1,8 +1,8 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import Loom from "./Loom";
describe("Loom", () => {
const match = Loom.ENABLED[0];
test("to be enabled on share link", () => {
expect(
"https://www.loom.com/share/55327cbb265743f39c2c442c029277e0".match(match)

View File

@@ -1,22 +1,20 @@
// @flow
import * as React from "react";
import Frame from "./components/Frame";
const URL_REGEX = /^https:\/\/(www\.)?(use)?loom.com\/(embed|share)\/(.*)$/;
type Props = {|
attrs: {|
href: string,
matches: string[],
|},
|};
type Props = {
attrs: {
href: string;
matches: string[];
};
};
export default class Loom extends React.Component<Props> {
static ENABLED = [URL_REGEX];
render() {
const normalizedUrl = this.props.attrs.href.replace("share", "embed");
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ src: string; title: string; attrs: { href:... Remove this comment to see the full error message
return <Frame {...this.props} src={normalizedUrl} title="Loom Embed" />;
}
}

View File

@@ -1,8 +1,8 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import Lucidchart from "./Lucidchart";
describe("Lucidchart", () => {
const match = Lucidchart.ENABLED[0];
test("to be enabled on view link", () => {
expect(
"https://www.lucidchart.com/documents/view/2f4a79cb-7637-433d-8ffb-27cce65a05e7".match(
@@ -10,6 +10,7 @@ describe("Lucidchart", () => {
)
).toBeTruthy();
});
test("to be enabled on root link", () => {
expect(
"https://lucidchart.com/documents/view/2f4a79cb-7637-433d-8ffb-27cce65a05e7".match(
@@ -17,6 +18,7 @@ describe("Lucidchart", () => {
)
).toBeTruthy();
});
test("to be enabled on app link", () => {
expect(
"https://app.lucidchart.com/documents/view/2f4a79cb-7637-433d-8ffb-27cce65a05e7".match(
@@ -24,6 +26,7 @@ describe("Lucidchart", () => {
)
).toBeTruthy();
});
test("to be enabled on visited link", () => {
expect(
"https://www.lucidchart.com/documents/view/2f4a79cb-7637-433d-8ffb-27cce65a05e7/0".match(
@@ -31,6 +34,7 @@ describe("Lucidchart", () => {
)
).toBeTruthy();
});
test("to be enabled on embedded link", () => {
expect(
"https://app.lucidchart.com/documents/embeddedchart/1af2bdfa-da7d-4ea1-aa1d-bec5677a9837".match(

View File

@@ -1,12 +1,12 @@
// @flow
import * as React from "react";
import Frame from "./components/Frame";
type Props = {|
attrs: {|
href: string,
matches: Object,
|},
|};
type Props = {
attrs: {
href: string;
matches: Record<string, any>;
};
};
export default class Lucidchart extends React.Component<Props> {
static ENABLED = [
@@ -17,10 +17,10 @@ export default class Lucidchart extends React.Component<Props> {
render() {
const { matches } = this.props.attrs;
const { chartId } = matches.groups;
return (
<Frame
{...this.props}
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ src: string; title: string; attrs: { href:... Remove this comment to see the full error message
src={`https://lucidchart.com/documents/embeddedchart/${chartId}`}
title="Lucidchart Embed"
/>

View File

@@ -1,8 +1,8 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import Marvel from "./Marvel";
describe("Marvel", () => {
const match = Marvel.ENABLED[0];
test("to be enabled on share link", () => {
expect("https://marvelapp.com/75hj91".match(match)).toBeTruthy();
});

View File

@@ -1,15 +1,13 @@
// @flow
import * as React from "react";
import Frame from "./components/Frame";
const URL_REGEX = new RegExp("^https://marvelapp.com/([A-Za-z0-9-]{6})/?$");
type Props = {|
attrs: {|
href: string,
matches: string[],
|},
|};
type Props = {
attrs: {
href: string;
matches: string[];
};
};
export default class Marvel extends React.Component<Props> {
static ENABLED = [URL_REGEX];
@@ -18,6 +16,7 @@ export default class Marvel extends React.Component<Props> {
return (
<Frame
{...this.props}
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ src: string; title: string; border: true; ... Remove this comment to see the full error message
src={this.props.attrs.href}
title="Marvel Embed"
border

View File

@@ -1,4 +1,3 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import Mindmeister from "./Mindmeister";
describe("Mindmeister", () => {

View File

@@ -1,17 +1,15 @@
// @flow
import * as React from "react";
import Frame from "./components/Frame";
const URL_REGEX = new RegExp(
"^https://([w.-]+.)?(mindmeister.com|mm.tt)(/maps/public_map_shell)?/(\\d+)(\\?t=.*)?(/.*)?$"
);
type Props = {|
attrs: {|
href: string,
matches: string[],
|},
|};
type Props = {
attrs: {
href: string;
matches: string[];
};
};
export default class Mindmeister extends React.Component<Props> {
static ENABLED = [URL_REGEX];
@@ -21,10 +19,10 @@ export default class Mindmeister extends React.Component<Props> {
this.props.attrs.matches[4] +
(this.props.attrs.matches[5] || "") +
(this.props.attrs.matches[6] || "");
return (
<Frame
{...this.props}
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ src: string; title: string; border: true; ... Remove this comment to see the full error message
src={`https://www.mindmeister.com/maps/public_map_shell/${chartId}`}
title="Mindmeister Embed"
border

View File

@@ -1,8 +1,8 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import Miro from "./Miro";
describe("Miro", () => {
const match = Miro.ENABLED[0];
test("to be enabled on old domain share link", () => {
expect(
"https://realtimeboard.com/app/board/o9J_k0fwiss=".match(match)
@@ -15,6 +15,7 @@ describe("Miro", () => {
test("to extract the domain as part of the match for later use", () => {
expect(
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
"https://realtimeboard.com/app/board/o9J_k0fwiss=".match(match)[1]
).toBe("realtimeboard");
});

View File

@@ -1,15 +1,13 @@
// @flow
import * as React from "react";
import Frame from "./components/Frame";
const URL_REGEX = /^https:\/\/(realtimeboard|miro).com\/app\/board\/(.*)$/;
type Props = {|
attrs: {|
href: string,
matches: string[],
|},
|};
type Props = {
attrs: {
href: string;
matches: string[];
};
};
export default class RealtimeBoard extends React.Component<Props> {
static ENABLED = [URL_REGEX];
@@ -19,10 +17,10 @@ export default class RealtimeBoard extends React.Component<Props> {
const domain = matches[1];
const boardId = matches[2];
const titleName = domain === "realtimeboard" ? "RealtimeBoard" : "Miro";
return (
<Frame
{...this.props}
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ src: string; title: string; attrs: { href:... Remove this comment to see the full error message
src={`https://${domain}.com/app/embed/${boardId}`}
title={`${titleName} (${boardId})`}
/>

View File

@@ -1,8 +1,8 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import ModeAnalytics from "./ModeAnalytics";
describe("ModeAnalytics", () => {
const match = ModeAnalytics.ENABLED[0];
test("to be enabled on report link", () => {
expect(
"https://modeanalytics.com/outline/reports/5aca06064f56".match(match)

View File

@@ -1,17 +1,15 @@
// @flow
import * as React from "react";
import Frame from "./components/Frame";
const URL_REGEX = new RegExp(
"^https://([w.-]+.)?modeanalytics.com/(.*)/reports/(.*)$"
);
type Props = {|
attrs: {|
href: string,
matches: string[],
|},
|};
type Props = {
attrs: {
href: string;
matches: string[];
};
};
export default class ModeAnalytics extends React.Component<Props> {
static ENABLED = [URL_REGEX];
@@ -19,10 +17,10 @@ export default class ModeAnalytics extends React.Component<Props> {
render() {
// Allow users to paste embed or standard urls and handle them the same
const normalizedUrl = this.props.attrs.href.replace(/\/embed$/, "");
return (
<Frame
{...this.props}
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ src: string; title: string; attrs: { href:... Remove this comment to see the full error message
src={`${normalizedUrl}/embed`}
title="Mode Analytics Embed"
/>

View File

@@ -1,27 +1,25 @@
// @flow
import * as React from "react";
import Frame from "./components/Frame";
const URL_REGEX = new RegExp(
"^https?://app.pitch.com/app/(?:presentation/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|public/player)/(.*)$"
);
type Props = {|
attrs: {|
href: string,
matches: any,
|},
|};
type Props = {
attrs: {
href: string;
matches: any;
};
};
export default class Pitch extends React.Component<Props> {
static ENABLED = [URL_REGEX];
render() {
const shareId = this.props.attrs.matches[1];
return (
<Frame
{...this.props}
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ src: string; title: string; height: string... Remove this comment to see the full error message
src={`https://pitch.com/embed/${shareId}`}
title="Pitch Embed"
height="414px"

View File

@@ -1,8 +1,8 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import Prezi from "./Prezi";
describe("Prezi", () => {
const match = Prezi.ENABLED[0];
test("to be enabled on share link", () => {
expect(
"https://prezi.com/view/39mn8Rn1ZkoeEKQCgk5C".match(match)

View File

@@ -1,23 +1,21 @@
// @flow
import * as React from "react";
import Frame from "./components/Frame";
const URL_REGEX = new RegExp("^https://prezi.com/view/(.*)$");
type Props = {|
attrs: {|
href: string,
matches: string[],
|},
|};
type Props = {
attrs: {
href: string;
matches: string[];
};
};
export default class Prezi extends React.Component<Props> {
static ENABLED = [URL_REGEX];
render() {
const url = this.props.attrs.href.replace(/\/embed$/, "");
return (
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ src: string; title: string; border: true; ... Remove this comment to see the full error message
<Frame {...this.props} src={`${url}/embed`} title="Prezi Embed" border />
);
}

View File

@@ -1,8 +1,8 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import Spotify from "./Spotify";
describe("Spotify", () => {
const match = Spotify.ENABLED[0];
test("to be enabled on song link", () => {
expect(
"https://open.spotify.com/track/29G1ScCUhgjgI0H72qN4DE?si=DxjEUxV2Tjmk6pSVckPDRg".match(

View File

@@ -1,15 +1,14 @@
// @flow
import * as React from "react";
import Frame from "./components/Frame";
const URL_REGEX = new RegExp("https?://open.spotify.com/(.*)$");
type Props = {
attrs: {
href: string;
matches: string[];
};
};
type Props = {|
attrs: {|
href: string,
matches: string[],
|},
|};
export default class Spotify extends React.Component<Props> {
static ENABLED = [URL_REGEX];
@@ -24,8 +23,7 @@ export default class Spotify extends React.Component<Props> {
render() {
const normalizedPath = this.pathname.replace(/^\/embed/, "/");
var height;
let height;
if (normalizedPath.includes("episode") || normalizedPath.includes("show")) {
height = 232;
@@ -38,6 +36,7 @@ export default class Spotify extends React.Component<Props> {
return (
<Frame
{...this.props}
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ width: string; height: string; src: string... Remove this comment to see the full error message
width="100%"
height={`${height}px`}
src={`https://open.spotify.com/embed${normalizedPath}`}

View File

@@ -1,15 +1,13 @@
// @flow
import * as React from "react";
import Frame from "./components/Frame";
const URL_REGEX = /^https:\/\/trello.com\/(c|b)\/([^/]*)(.*)?$/;
type Props = {|
attrs: {|
href: string,
matches: string[],
|},
|};
type Props = {
attrs: {
href: string;
matches: string[];
};
};
export default class Trello extends React.Component<Props> {
static ENABLED = [URL_REGEX];
@@ -21,6 +19,7 @@ export default class Trello extends React.Component<Props> {
if (matches[1] === "c") {
return (
<Frame
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ width: string; height: string; src: string... Remove this comment to see the full error message
width="316px"
height="158px"
src={`https://trello.com/embed/card?id=${objectId}`}
@@ -32,6 +31,7 @@ export default class Trello extends React.Component<Props> {
return (
<Frame
{...this.props}
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ width: string; height: string; src: string... Remove this comment to see the full error message
width="248px"
height="185px"
src={`https://trello.com/embed/board?id=${objectId}`}

View File

@@ -1,8 +1,8 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import Typeform from "./Typeform";
describe("Typeform", () => {
const match = Typeform.ENABLED[0];
test("to be enabled on share link", () => {
expect(
"https://beardyman.typeform.com/to/zvlr4L".match(match)

View File

@@ -1,17 +1,15 @@
// @flow
import * as React from "react";
import Frame from "./components/Frame";
const URL_REGEX = new RegExp(
"^https://([A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?).typeform.com/to/(.*)$"
);
type Props = {|
attrs: {|
href: string,
matches: string[],
|},
|};
type Props = {
attrs: {
href: string;
matches: string[];
};
};
export default class Typeform extends React.Component<Props> {
static ENABLED = [URL_REGEX];
@@ -20,6 +18,7 @@ export default class Typeform extends React.Component<Props> {
return (
<Frame
{...this.props}
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ src: string; title: string; attrs: { href:... Remove this comment to see the full error message
src={this.props.attrs.href}
title="Typeform Embed"
/>

View File

@@ -1,8 +1,8 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import Vimeo from "./Vimeo";
describe("Vimeo", () => {
const match = Vimeo.ENABLED[0];
test("to be enabled on video link", () => {
expect("https://vimeo.com/265045525".match(match)).toBeTruthy();
});

View File

@@ -1,15 +1,13 @@
// @flow
import * as React from "react";
import Frame from "./components/Frame";
const URL_REGEX = /(http|https)?:\/\/(www\.)?vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/([^/]*)\/videos\/|)(\d+)(?:|\/\?)/;
type Props = {|
attrs: {|
href: string,
matches: string[],
|},
|};
type Props = {
attrs: {
href: string;
matches: string[];
};
};
export default class Vimeo extends React.Component<Props> {
static ENABLED = [URL_REGEX];
@@ -17,10 +15,10 @@ export default class Vimeo extends React.Component<Props> {
render() {
const { matches } = this.props.attrs;
const videoId = matches[4];
return (
<Frame
{...this.props}
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ src: string; title: string; attrs: { href:... Remove this comment to see the full error message
src={`https://player.vimeo.com/video/${videoId}?byline=0`}
title={`Vimeo Embed (${videoId})`}
/>

View File

@@ -1,8 +1,8 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import YouTube from "./YouTube";
describe("YouTube", () => {
const match = YouTube.ENABLED[0];
test("to be enabled on video link", () => {
expect(
"https://www.youtube.com/watch?v=dQw4w9WgXcQ".match(match)

View File

@@ -1,16 +1,14 @@
// @flow
import * as React from "react";
import Frame from "./components/Frame";
const URL_REGEX = /(?:https?:\/\/)?(?:www\.)?youtu\.?be(?:\.com)?\/?.*(?:watch|embed)?(?:.*v=|v\/|\/)([a-zA-Z0-9_-]{11})$/i;
type Props = {|
isSelected: boolean,
attrs: {|
href: string,
matches: string[],
|},
|};
type Props = {
isSelected: boolean;
attrs: {
href: string;
matches: string[];
};
};
export default class YouTube extends React.Component<Props> {
static ENABLED = [URL_REGEX];
@@ -18,10 +16,10 @@ export default class YouTube extends React.Component<Props> {
render() {
const { matches } = this.props.attrs;
const videoId = matches[1];
return (
<Frame
{...this.props}
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ src: string; title: string; isSelected: bo... Remove this comment to see the full error message
src={`https://www.youtube.com/embed/${videoId}?modestbranding=1`}
title={`YouTube (${videoId})`}
/>

View File

@@ -1,4 +1,3 @@
// @flow
import { observable } from "mobx";
import { observer } from "mobx-react";
import { OpenIcon } from "outline-icons";
@@ -7,32 +6,34 @@ import styled from "styled-components";
// This wrapper allows us to pass non-standard HTML attributes through to the DOM element
// https://www.styled-components.com/docs/basics#passed-props
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'props' implicitly has an 'any' type.
const Iframe = (props) => <iframe title="Embed" {...props} />;
const StyledIframe = styled(Iframe)`
border-radius: ${(props) => (props.$withBar ? "3px 3px 0 0" : "3px")};
display: block;
`;
type Props = {
src?: string,
border?: boolean,
title?: string,
icon?: React.Node,
canonicalUrl?: string,
isSelected?: boolean,
width?: string,
height?: string,
src?: string;
border?: boolean;
title?: string;
icon?: React.ReactNode;
canonicalUrl?: string;
isSelected?: boolean;
width?: string;
height?: string;
};
type PropsWithRef = Props & {
forwardedRef: React.Ref<typeof StyledIframe>,
forwardedRef: React.Ref<typeof StyledIframe>;
};
@observer
class Frame extends React.Component<PropsWithRef> {
mounted: boolean;
@observable isLoaded: boolean = false;
@observable
isLoaded = false;
componentDidMount() {
this.mounted = true;
@@ -72,6 +73,7 @@ class Frame extends React.Component<PropsWithRef> {
>
{this.isLoaded && (
<Component
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
ref={forwardedRef}
$withBar={withBar}
sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
@@ -86,7 +88,7 @@ class Frame extends React.Component<PropsWithRef> {
/>
)}
{withBar && (
<Bar align="center">
<Bar>
{icon} <Title>{title}</Title>
{canonicalUrl && (
<Open
@@ -104,7 +106,11 @@ class Frame extends React.Component<PropsWithRef> {
}
}
const Rounded = styled.div`
const Rounded = styled.div<{
width: string;
height: string;
$withBar: boolean;
}>`
border: 1px solid ${(props) => props.theme.embedBorder};
border-radius: 6px;
overflow: hidden;
@@ -141,6 +147,7 @@ const Bar = styled.div`
user-select: none;
`;
export default React.forwardRef<Props, typeof Frame>((props, ref) => (
export default React.forwardRef((props, ref) => (
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
<Frame {...props} forwardedRef={ref} />
));

View File

@@ -1,14 +1,13 @@
// @flow
import * as React from "react";
import { cdnPath } from "../../utils/urls";
type Props = {|
alt: string,
src: string,
title?: string,
width?: number,
height?: number,
|};
type Props = {
alt: string;
src: string;
title?: string;
width?: number;
height?: number;
};
export default function Image({ src, alt, ...rest }: Props) {
return <img src={cdnPath(src)} alt={alt} {...rest} />;

View File

@@ -1,4 +1,3 @@
// @flow
import * as React from "react";
import styled from "styled-components";
import Abstract from "./Abstract";
@@ -35,11 +34,15 @@ import Vimeo from "./Vimeo";
import YouTube from "./YouTube";
import Image from "./components/Image";
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'Component' implicitly has an 'any' type... Remove this comment to see the full error message
function matcher(Component) {
// @ts-expect-error ts-migrate(7030) FIXME: Not all code paths return a value.
return (url: string) => {
const regexes = Component.ENABLED;
for (const regex of regexes) {
const result = url.match(regex);
if (result) {
return result;
}
@@ -57,6 +60,7 @@ export default [
{
title: "Abstract",
keywords: "design",
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
icon: () => <Img src="/images/abstract.png" />,
component: Abstract,
matcher: matcher(Abstract),
@@ -64,6 +68,7 @@ export default [
{
title: "Airtable",
keywords: "spreadsheet",
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
icon: () => <Img src="/images/airtable.png" />,
component: Airtable,
matcher: matcher(Airtable),
@@ -72,6 +77,7 @@ export default [
title: "Bilibili",
keywords: "video",
defaultHidden: true,
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
icon: () => <Img src="/images/bilibili.png" />,
component: Bilibili,
matcher: matcher(Bilibili),
@@ -80,6 +86,7 @@ export default [
title: "Cawemo",
keywords: "bpmn process",
defaultHidden: true,
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
icon: () => <Img src="/images/cawemo.png" />,
component: Cawemo,
matcher: matcher(Cawemo),
@@ -88,6 +95,7 @@ export default [
title: "ClickUp",
keywords: "project",
defaultHidden: true,
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
icon: () => <Img src="/images/clickup.png" />,
component: ClickUp,
matcher: matcher(ClickUp),
@@ -95,6 +103,7 @@ export default [
{
title: "Codepen",
keywords: "code editor",
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
icon: () => <Img src="/images/codepen.png" />,
component: Codepen,
matcher: matcher(Codepen),
@@ -102,6 +111,7 @@ export default [
{
title: "Descript",
keywords: "audio",
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
icon: () => <Img src="/images/descript.png" />,
component: Descript,
matcher: matcher(Descript),
@@ -109,6 +119,7 @@ export default [
{
title: "Figma",
keywords: "design svg vector",
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
icon: () => <Img src="/images/figma.png" />,
component: Figma,
matcher: matcher(Figma),
@@ -116,6 +127,7 @@ export default [
{
title: "Framer",
keywords: "design prototyping",
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
icon: () => <Img src="/images/framer.png" />,
component: Framer,
matcher: matcher(Framer),
@@ -123,6 +135,7 @@ export default [
{
title: "GitHub Gist",
keywords: "code",
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
icon: () => <Img src="/images/github-gist.png" />,
component: Gist,
matcher: matcher(Gist),
@@ -130,6 +143,7 @@ export default [
{
title: "Diagrams.net",
keywords: "diagrams drawio",
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
icon: () => <Img src="/images/diagrams.png" />,
component: Diagrams,
matcher: matcher(Diagrams),
@@ -137,6 +151,7 @@ export default [
{
title: "Google Drawings",
keywords: "drawings",
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
icon: () => <Img src="/images/google-drawings.png" />,
component: GoogleDrawings,
matcher: matcher(GoogleDrawings),
@@ -144,12 +159,14 @@ export default [
{
title: "Google Drive",
keywords: "drive",
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
icon: () => <Img src="/images/google-drive.png" />,
component: GoogleDrive,
matcher: matcher(GoogleDrive),
},
{
title: "Google Docs",
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
icon: () => <Img src="/images/google-docs.png" />,
component: GoogleDocs,
matcher: matcher(GoogleDocs),
@@ -157,6 +174,7 @@ export default [
{
title: "Google Sheets",
keywords: "excel spreadsheet",
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
icon: () => <Img src="/images/google-sheets.png" />,
component: GoogleSheets,
matcher: matcher(GoogleSheets),
@@ -164,6 +182,7 @@ export default [
{
title: "Google Slides",
keywords: "presentation slideshow",
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
icon: () => <Img src="/images/google-slides.png" />,
component: GoogleSlides,
matcher: matcher(GoogleSlides),
@@ -171,6 +190,7 @@ export default [
{
title: "Google Calendar",
keywords: "calendar",
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
icon: () => <Img src="/images/google-calendar.png" />,
component: GoogleCalendar,
matcher: matcher(GoogleCalendar),
@@ -178,6 +198,7 @@ export default [
{
title: "Google Data Studio",
keywords: "business intelligence",
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
icon: () => <Img src="/images/google-datastudio.png" />,
component: GoogleDataStudio,
matcher: matcher(GoogleDataStudio),
@@ -186,6 +207,7 @@ export default [
title: "InVision",
keywords: "design prototype",
defaultHidden: true,
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
icon: () => <Img src="/images/invision.png" />,
component: InVision,
matcher: matcher(InVision),
@@ -193,6 +215,7 @@ export default [
{
title: "Loom",
keywords: "video screencast",
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
icon: () => <Img src="/images/loom.png" />,
component: Loom,
matcher: matcher(Loom),
@@ -200,6 +223,7 @@ export default [
{
title: "Lucidchart",
keywords: "chart",
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
icon: () => <Img src="/images/lucidchart.png" />,
component: Lucidchart,
matcher: matcher(Lucidchart),
@@ -207,6 +231,7 @@ export default [
{
title: "Marvel",
keywords: "design prototype",
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
icon: () => <Img src="/images/marvel.png" />,
component: Marvel,
matcher: matcher(Marvel),
@@ -214,6 +239,7 @@ export default [
{
title: "Mindmeister",
keywords: "mindmap",
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
icon: () => <Img src="/images/mindmeister.png" />,
component: Mindmeister,
matcher: matcher(Mindmeister),
@@ -221,6 +247,7 @@ export default [
{
title: "Miro",
keywords: "whiteboard",
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
icon: () => <Img src="/images/miro.png" />,
component: Miro,
matcher: matcher(Miro),
@@ -229,6 +256,7 @@ export default [
title: "Mode",
keywords: "analytics",
defaultHidden: true,
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
icon: () => <Img src="/images/mode-analytics.png" />,
component: ModeAnalytics,
matcher: matcher(ModeAnalytics),
@@ -237,6 +265,7 @@ export default [
title: "Pitch",
keywords: "presentation",
defaultHidden: true,
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
icon: () => <Img src="/images/pitch.png" />,
component: Pitch,
matcher: matcher(Pitch),
@@ -244,6 +273,7 @@ export default [
{
title: "Prezi",
keywords: "presentation",
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
icon: () => <Img src="/images/prezi.png" />,
component: Prezi,
matcher: matcher(Prezi),
@@ -251,6 +281,7 @@ export default [
{
title: "Spotify",
keywords: "music",
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
icon: () => <Img src="/images/spotify.png" />,
component: Spotify,
matcher: matcher(Spotify),
@@ -258,6 +289,7 @@ export default [
{
title: "Trello",
keywords: "kanban",
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
icon: () => <Img src="/images/trello.png" />,
component: Trello,
matcher: matcher(Trello),
@@ -265,6 +297,7 @@ export default [
{
title: "Typeform",
keywords: "form survey",
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
icon: () => <Img src="/images/typeform.png" />,
component: Typeform,
matcher: matcher(Typeform),
@@ -272,6 +305,7 @@ export default [
{
title: "Vimeo",
keywords: "video",
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
icon: () => <Img src="/images/vimeo.png" />,
component: Vimeo,
matcher: matcher(Vimeo),
@@ -279,6 +313,7 @@ export default [
{
title: "YouTube",
keywords: "google video",
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
icon: () => <Img src="/images/youtube.png" />,
component: YouTube,
matcher: matcher(YouTube),

View File

@@ -1,4 +1,3 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import i18n from "i18next";
import de_DE from "./locales/de_DE/translation.json";
import en_US from "./locales/en_US/translation.json";
@@ -27,7 +26,6 @@ describe("i18n process.env is unset", () => {
expect(i18n.t("Saving")).toBe("A guardar");
});
});
describe("i18n process.env is en-US", () => {
beforeEach(() => {
process.env.DEFAULT_LANGUAGE = "en-US";
@@ -50,7 +48,6 @@ describe("i18n process.env is en-US", () => {
expect(i18n.t("Saving")).toBe("A guardar");
});
});
describe("i18n process.env is de-DE", () => {
beforeEach(() => {
process.env.DEFAULT_LANGUAGE = "de-DE";
@@ -73,7 +70,6 @@ describe("i18n process.env is de-DE", () => {
expect(i18n.t("Saving")).toBe("A guardar");
});
});
describe("i18n process.env is pt-PT", () => {
beforeEach(() => {
process.env.DEFAULT_LANGUAGE = "pt-PT";

View File

@@ -1,4 +1,3 @@
// @flow
import i18n from "i18next";
import backend from "i18next-http-backend";
import { initReactI18next } from "react-i18next";
@@ -6,20 +5,62 @@ import { initReactI18next } from "react-i18next";
// Note: Updating the available languages? Make sure to also update the
// locales array in app/utils/i18n.js to enable translation for timestamps.
export const languageOptions = [
{ label: "English (US)", value: "en_US" },
{ label: "简体中文 (Chinese, Simplified)", value: "zh_CN" },
{ label: "繁體中文 (Chinese, Traditional)", value: "zh_TW" },
{ label: "Deutsch (Deutschland)", value: "de_DE" },
{ label: "Español (España)", value: "es_ES" },
{ label: "فارسی (Persian)", value: "fa_IR" },
{ label: "Français (France)", value: "fr_FR" },
{ label: "Italiano (Italia)", value: "it_IT" },
{ label: "日本語 (Japanese)", value: "ja_JP" },
{ label: "한국어 (Korean)", value: "ko_KR" },
{ label: "Português (Brazil)", value: "pt_BR" },
{ label: "Português (Portugal)", value: "pt_PT" },
{ label: "Pусский (Россия)", value: "ru_RU" },
{ label: "Polskie (Polska)", value: "pl_PL" },
{
label: "English (US)",
value: "en_US",
},
{
label: "简体中文 (Chinese, Simplified)",
value: "zh_CN",
},
{
label: "繁體中文 (Chinese, Traditional)",
value: "zh_TW",
},
{
label: "Deutsch (Deutschland)",
value: "de_DE",
},
{
label: "Español (España)",
value: "es_ES",
},
{
label: "فارسی (Persian)",
value: "fa_IR",
},
{
label: "Français (France)",
value: "fr_FR",
},
{
label: "Italiano (Italia)",
value: "it_IT",
},
{
label: "日本語 (Japanese)",
value: "ja_JP",
},
{
label: "한국어 (Korean)",
value: "ko_KR",
},
{
label: "Português (Brazil)",
value: "pt_BR",
},
{
label: "Português (Portugal)",
value: "pt_PT",
},
{
label: "Pусский (Россия)",
value: "ru_RU",
},
{
label: "Polskie (Polska)",
value: "pl_PL",
},
];
export const languages: string[] = languageOptions.map((i) => i.value);
@@ -27,13 +68,14 @@ export const languages: string[] = languageOptions.map((i) => i.value);
// Languages are stored in en_US format in the database, however the
// frontend translation framework (i18next) expects en-US
const underscoreToDash = (text: string) => text.replace("_", "-");
const dashToUnderscore = (text: string) => text.replace("-", "_");
export const initI18n = () => {
const lng = underscoreToDash(
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'string | undefined' is not assig... Remove this comment to see the full error message
"DEFAULT_LANGUAGE" in process.env ? process.env.DEFAULT_LANGUAGE : "en_US"
);
i18n
.use(backend)
.use(initReactI18next)
@@ -41,7 +83,7 @@ export const initI18n = () => {
backend: {
// this must match the path defined in routes. It's the path that the
// frontend UI code will hit to load missing translations.
loadPath: (languages: string[], namespaces: string[]) =>
loadPath: (languages: string[]) =>
`/locales/${dashToUnderscore(languages[0])}.json`,
},
interpolation: {
@@ -57,6 +99,5 @@ export const initI18n = () => {
// debug: process.env.NODE_ENV === "development",
keySeparator: false,
});
return i18n;
};

View File

@@ -1,4 +1,3 @@
// @flow
const randomInteger = (min: number, max: number) => {
return Math.floor(Math.random() * (max - min + 1) + min);
};

View File

@@ -1,5 +1,5 @@
// @flow
import { darken, lighten } from "polished";
import { DefaultTheme } from "styled-components";
const colors = {
transparent: "transparent",
@@ -7,15 +7,12 @@ const colors = {
lightBlack: "#2F3336",
almostWhite: "#E6E6E6",
veryDarkBlue: "#08090C",
slate: "#9BA6B2",
slateLight: "#DAE1E9",
slateDark: "#394351",
smoke: "#F4F7FA",
smokeLight: "#F9FBFC",
smokeDark: "#E8EBED",
white: "#FFF",
white10: "rgba(255, 255, 255, 0.1)",
white50: "rgba(255, 255, 255, 0.5)",
@@ -26,13 +23,11 @@ const colors = {
primary: "#0366d6",
yellow: "#FBCA04",
warmGrey: "#EDF2F7",
searchHighlight: "#FDEA9B",
danger: "#ff476f",
warning: "#f08a24",
success: "#2f3336",
info: "#a0d3e8",
brand: {
red: "#FF5C80",
pink: "#FF4DFA",
@@ -64,13 +59,11 @@ export const base = {
fontWeight: 400,
backgroundTransition: "background 100ms ease-in-out",
zIndex: 800,
selected: colors.primary,
buttonBackground: colors.primary,
buttonText: colors.white,
textHighlight: "#FDEA9B",
textHighlightForeground: colors.almostBlack,
codeComment: "#6a737d",
codePunctuation: "#5e6687",
codeNumber: "#d73a49",
@@ -86,7 +79,6 @@ export const base = {
codePlaceholder: "#3d8fd1",
codeInserted: "#202746",
codeImportant: "#c94922",
blockToolbarBackground: colors.white,
blockToolbarTrigger: colors.slate,
blockToolbarTriggerIcon: colors.white,
@@ -98,21 +90,21 @@ export const base = {
blockToolbarDivider: colors.slateLight,
blockToolbarIcon: undefined,
blockToolbarIconSelected: colors.black,
noticeInfoBackground: colors.warmGrey,
noticeInfoText: colors.almostBlack,
noticeTipBackground: "#fce5bb",
noticeTipText: colors.almostBlack,
noticeWarningBackground: "#ffadbf",
noticeWarningText: colors.almostBlack,
breakpoints: {
mobile: 0, // targeting all devices
tablet: 737, // targeting devices that are larger than the iPhone 6 Plus (which is 736px in landscape mode)
desktop: 1025, // targeting devices that are larger than the iPad (which is 1024px in landscape mode)
mobile: 0,
// targeting all devices
tablet: 737,
// targeting devices that are larger than the iPhone 6 Plus (which is 736px in landscape mode)
desktop: 1025,
// targeting devices that are larger than the iPad (which is 1024px in landscape mode)
desktopLarge: 1600,
},
depths: {
header: 800,
sidebar: 900,
@@ -130,24 +122,21 @@ export const base = {
},
};
export const light = {
export const light: DefaultTheme = {
...base,
background: colors.white,
secondaryBackground: colors.warmGrey,
link: colors.primary,
text: colors.almostBlack,
cursor: colors.almostBlack,
text: colors.almostBlack,
textSecondary: colors.slateDark,
textTertiary: colors.slate,
placeholder: "#a2b2c3",
sidebarBackground: colors.warmGrey,
sidebarItemBackground: "#d7e0ea",
sidebarText: "rgb(78, 92, 110)",
backdrop: "rgba(0, 0, 0, 0.2)",
shadow: "rgba(0, 0, 0, 0.2)",
menuItemSelected: colors.warmGrey,
menuBackground: colors.white,
menuShadow:
@@ -156,58 +145,46 @@ export const light = {
titleBarDivider: colors.slateLight,
inputBorder: colors.slateLight,
inputBorderFocused: colors.slate,
listItemHoverBackground: colors.warmGrey,
toolbarHoverBackground: colors.black,
toolbarBackground: colors.lightBlack,
toolbarInput: colors.white10,
toolbarItem: colors.white,
tableDivider: colors.smokeDark,
tableSelected: colors.primary,
tableSelectedBackground: "#E5F7FF",
buttonNeutralBackground: colors.white,
buttonNeutralText: colors.almostBlack,
buttonNeutralBorder: darken(0.15, colors.white),
tooltipBackground: colors.almostBlack,
tooltipText: colors.white,
toastBackground: colors.almostBlack,
toastText: colors.white,
quote: colors.slateLight,
codeBackground: colors.smoke,
codeBorder: colors.smokeDark,
embedBorder: colors.slateLight,
horizontalRule: colors.smokeDark,
progressBarBackground: colors.slateLight,
scrollbarBackground: colors.smoke,
scrollbarThumb: darken(0.15, colors.smokeDark),
};
export const dark = {
export const dark: DefaultTheme = {
...base,
background: colors.almostBlack,
secondaryBackground: colors.black50,
link: "#137FFB",
text: colors.almostWhite,
cursor: colors.almostWhite,
textSecondary: lighten(0.1, colors.slate),
textTertiary: colors.slate,
placeholder: colors.slateDark,
sidebarBackground: colors.veryDarkBlue,
sidebarItemBackground: lighten(0.015, colors.almostBlack),
sidebarText: colors.slate,
backdrop: "rgba(255, 255, 255, 0.3)",
shadow: "rgba(0, 0, 0, 0.6)",
menuItemSelected: lighten(0.1, "#1f2128"),
menuBackground: "#1f2128",
menuShadow:
@@ -216,54 +193,43 @@ export const dark = {
titleBarDivider: darken(0.4, colors.slate),
inputBorder: colors.slateDark,
inputBorderFocused: colors.slate,
listItemHoverBackground: colors.black50,
toolbarHoverBackground: colors.slate,
toolbarBackground: colors.white,
toolbarInput: colors.black10,
toolbarItem: colors.lightBlack,
tableDivider: colors.lightBlack,
tableSelected: colors.primary,
tableSelectedBackground: "#002333",
buttonNeutralBackground: colors.almostBlack,
buttonNeutralText: colors.white,
buttonNeutralBorder: colors.slateDark,
tooltipBackground: colors.white,
tooltipText: colors.lightBlack,
toastBackground: colors.white,
toastText: colors.lightBlack,
quote: colors.almostWhite,
codeBackground: colors.black,
codeBorder: colors.black50,
codeString: "#3d8fd1",
embedBorder: colors.black50,
horizontalRule: lighten(0.1, colors.almostBlack),
noticeInfoBackground: "#252a37",
noticeInfoText: colors.white,
noticeTipBackground: "#a3840a",
noticeTipText: colors.white,
noticeWarningBackground: "#7a001b",
noticeWarningText: colors.white,
progressBarBackground: colors.slate,
scrollbarBackground: colors.black,
scrollbarThumb: colors.lightBlack,
};
export const lightMobile = {
background: colors.white,
};
export const lightMobile = light;
export const darkMobile = {
export const darkMobile = (theme: DefaultTheme): DefaultTheme => ({
...theme,
background: colors.black,
};
});
export default light;

View File

@@ -1,2 +0,0 @@
// @flow
export type Role = "admin" | "viewer" | "member";

20
shared/types.ts Normal file
View File

@@ -0,0 +1,20 @@
export type Role = "admin" | "viewer" | "member";
export type DateFilter = "day" | "week" | "month" | "year";
export type PublicEnv = {
URL: string;
CDN_URL: string;
COLLABORATION_URL: string;
DEPLOYMENT: "hosted" | "";
ENVIRONMENT: "production" | "development";
SENTRY_DSN: string | undefined;
TEAM_LOGO: string | undefined;
SLACK_KEY: string | undefined;
SLACK_APP_ID: string | undefined;
MAXIMUM_IMPORT_SIZE: number;
SUBDOMAINS_ENABLED: boolean;
EMAIL_ENABLED: boolean;
GOOGLE_ANALYTICS_ID: string | undefined;
RELEASE: string | undefined;
};

View File

@@ -1,4 +1,2 @@
// @flow
export const validateColorHex = (color: string) =>
/(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(color);

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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,
};
}
}

View File

@@ -1,4 +1,2 @@
// @flow
export const validateIndexCharacters = (index: string) =>
new RegExp("^[\x20-\x7E]+$").test(index);

View File

@@ -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" },
]);
});
});

View 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",
},
]);
});
});

View File

@@ -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))
);
}

View File

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

View File

@@ -1,7 +1,6 @@
// @flow
export default function parseDocumentSlug(url: string) {
let parsed;
if (url[0] === "/") {
parsed = url;
} else {

View File

@@ -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 🌈");

View File

@@ -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,
};
}

View File

@@ -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}`;
}

View File

@@ -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
View 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(".", "-")
)}`;
}

View File

@@ -1,5 +1,3 @@
// @flow
const unescape = (text: string) => {
return text
.replace(/\\([\\`*{}[\]()#+\-.!_>])/g, "$1")

View File

@@ -1,4 +1,3 @@
// @flow
const env = typeof window !== "undefined" ? window.env : process.env;
export function cdnPath(path: string): string {

View File

@@ -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;
}