diff --git a/.babelrc b/.babelrc index 38bb56bdf..5a7af638e 100644 --- a/.babelrc +++ b/.babelrc @@ -1,7 +1,7 @@ { "presets": [ "@babel/preset-react", - "@babel/preset-flow", + "@babel/preset-typescript", [ "@babel/preset-env", { diff --git a/.circleci/config.yml b/.circleci/config.yml index bf7e67546..e847cf3b1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -41,8 +41,8 @@ jobs: name: lint command: yarn lint - run: - name: flow - command: yarn flow check --max-workers 4 + name: typescript + command: yarn tsc - run: name: test command: yarn test diff --git a/.dockerignore b/.dockerignore index 9a2b306ca..f2dc90801 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,14 +6,13 @@ __mocks__ .DS_Store .env* .eslint* -.flowconfig .log Makefile Procfile app.json +crowdin.yml build docker-compose.yml fakes3 -flow-typed node_modules -setupJest.js \ No newline at end of file +tsconfig.json diff --git a/.eslintrc b/.eslintrc index 8eeaa95b1..7e2d5b354 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,20 +1,34 @@ { - "parser": "babel-eslint", + "parser": "@typescript-eslint/parser", + "parserOptions": { + "sourceType": "module", + "extraFileExtensions": [".json"], + "ecmaFeatures": { + "jsx": true + } + }, "extends": [ - "react-app", - "plugin:import/errors", - "plugin:import/warnings", - "plugin:flowtype/recommended", - "plugin:react-hooks/recommended" + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:import/recommended", + "plugin:import/typescript", + "plugin:react-hooks/recommended", + "plugin:prettier/recommended" ], "plugins": [ - "prettier", - "flowtype" + "@typescript-eslint", + "eslint-plugin-import", + "eslint-plugin-node", + "eslint-plugin-react", + "eslint-plugin-react-hooks", + "import" ], "rules": { "eqeqeq": 2, - "no-unused-vars": 2, "no-mixed-operators": "off", + "padding-line-between-statements": ["error", { "blankLine": "always", "prev": "*", "next": "export" }], + "lines-between-class-members": ["error", "always", { "exceptAfterSingleLine": true }], + "import/newline-after-import": 2, "import/order": [ "error", { @@ -23,53 +37,48 @@ }, "pathGroups": [ { - "pattern": "shared/**", + "pattern": "@shared/**", "group": "external", "position": "after" }, { - "pattern": "stores", + "pattern": "@server/**", "group": "external", "position": "after" }, { - "pattern": "stores/**", + "pattern": "~/stores", "group": "external", "position": "after" }, { - "pattern": "models/**", + "pattern": "~/stores/**", "group": "external", "position": "after" }, { - "pattern": "scenes/**", + "pattern": "~/models/**", "group": "external", "position": "after" }, { - "pattern": "components/**", + "pattern": "~/scenes/**", + "group": "external", + "position": "after" + }, + { + "pattern": "~/components/**", + "group": "external", + "position": "after" + }, + { + "pattern": "~/**", "group": "external", "position": "after" } ] } ], - "flowtype/require-valid-file-annotation": [ - 2, - "always", - { - "annotationStyle": "line" - } - ], - "flowtype/space-after-type-colon": [ - 2, - "always" - ], - "flowtype/space-before-type-colon": [ - 2, - "never" - ], "prettier/prettier": [ "error", { @@ -84,21 +93,13 @@ "pragma": "React", "version": "detect" }, - "import/resolver": { - "node": { - "paths": [ - "app", - "." - ] - } + "import/parsers": { + "@typescript-eslint/parser": [".ts", ".tsx"] }, - "flowtype": { - "onlyFilesWithFlowAnnotation": false + "import/resolver": { + "typescript": {} } }, - "env": { - "jest": true - }, "globals": { "EDITOR_VERSION": true } diff --git a/.flowconfig b/.flowconfig deleted file mode 100644 index 7e630b565..000000000 --- a/.flowconfig +++ /dev/null @@ -1,44 +0,0 @@ -[include] -.*/app/.* -.*/server/.* -.*/shared/.* - -[ignore] -.*/node_modules/tiny-cookie/flow/.* -.*/node_modules/styled-components/.* -.*/node_modules/polished/.* -.*/node_modules/mobx/.*.flow -.*/node_modules/react-side-effect/.* -.*/node_modules/fbjs/.* -.*/node_modules/config-chain/.* -.*/node_modules/yjs/.* -.*/node_modules/y-prosemirror/.* -.*/node_modules/y-protocols/.* -.*/node_modules/y-indexeddb/.* -.*/node_modules/lib0/.* -.*/server/scripts/.* -*.test.js - -[libs] - -[options] -emoji=true -sharedmemory.heap_size=3221225472 - -module.system.node.resolve_dirname=node_modules -module.system.node.resolve_dirname=app - -module.name_mapper='^\(.*\)\.md$' -> 'empty/object' -module.name_mapper='^shared\/\(.*\)$' -> '/shared/\1' - -module.file_ext=.js -module.file_ext=.md -module.file_ext=.json - -esproposal.decorators=ignore -esproposal.class_static_fields=enable -esproposal.class_instance_fields=enable -esproposal.optional_chaining=enable - -suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe -suppress_comment=\\(.\\|\n\\)*\\$FlowIssue diff --git a/.vscode/settings.json b/.vscode/settings.json index 041e71a9e..77b6256c8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,7 @@ { - "javascript.validate.enable": false, - "javascript.format.enable": false, - "typescript.validate.enable": false, - "typescript.format.enable": false, + "javascript.validate.enable": true, + "javascript.format.enable": true, + "typescript.validate.enable": true, + "typescript.format.enable": true, "editor.formatOnSave": true, } \ No newline at end of file diff --git a/README.md b/README.md index 018a15e9b..8c869ed40 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,10 @@

- - - + TypeScript + Prettier + Styled Components +

This is the source code that runs [**Outline**](https://www.getoutline.com) and all the associated services. If you want to use Outline then you don't need to run this code, we offer a hosted version of the app at [getoutline.com](https://www.getoutline.com). diff --git a/__mocks__/bull.js b/__mocks__/bull.js index 3837625cc..02c0a40b4 100644 --- a/__mocks__/bull.js +++ b/__mocks__/bull.js @@ -1,4 +1,3 @@ -/* eslint-disable flowtype/require-valid-file-annotation */ export default class Queue { name; diff --git a/app/.eslintrc b/app/.eslintrc new file mode 100644 index 000000000..f273da1c9 --- /dev/null +++ b/app/.eslintrc @@ -0,0 +1,9 @@ +{ + "extends": [ + "../.eslintrc" + ], + "env": { + "jest": true, + "browser": true + } +} \ No newline at end of file diff --git a/app/.jestconfig.json b/app/.jestconfig.json index f07d05a0d..515b464d3 100644 --- a/app/.jestconfig.json +++ b/app/.jestconfig.json @@ -7,14 +7,10 @@ "/shared" ], "moduleNameMapper": { - "^shared/(.*)$": "/shared/$1", + "^~/(.*)$": "/app/$1", + "^@shared/(.*)$": "/shared/$1", "^.*[.](gif|ttf|eot|svg)$": "/__test__/fileMock.js" }, - "moduleFileExtensions": [ - "js", - "jsx", - "json" - ], "moduleDirectories": [ "node_modules" ], @@ -25,6 +21,6 @@ "/__mocks__/window.js" ], "setupFilesAfterEnv": [ - "./app/test/setup.js" + "./app/test/setup.ts" ] } \ No newline at end of file diff --git a/app/actions/definitions/collections.js b/app/actions/definitions/collections.tsx similarity index 83% rename from app/actions/definitions/collections.js rename to app/actions/definitions/collections.tsx index 47331e75f..ad17a3ba6 100644 --- a/app/actions/definitions/collections.js +++ b/app/actions/definitions/collections.tsx @@ -1,13 +1,12 @@ -// @flow import { CollectionIcon, EditIcon, PlusIcon } from "outline-icons"; import * as React from "react"; -import stores from "stores"; -import CollectionEdit from "scenes/CollectionEdit"; -import CollectionNew from "scenes/CollectionNew"; -import DynamicCollectionIcon from "components/CollectionIcon"; -import { createAction } from "actions"; -import { CollectionSection } from "actions/sections"; -import history from "utils/history"; +import stores from "~/stores"; +import CollectionEdit from "~/scenes/CollectionEdit"; +import CollectionNew from "~/scenes/CollectionNew"; +import DynamicCollectionIcon from "~/components/CollectionIcon"; +import { createAction } from "~/actions"; +import { CollectionSection } from "~/actions/sections"; +import history from "~/utils/history"; export const openCollection = createAction({ name: ({ t }) => t("Open collection"), @@ -16,7 +15,6 @@ export const openCollection = createAction({ icon: , children: ({ stores }) => { const collections = stores.collections.orderedData; - return collections.map((collection) => ({ // Note: using url which includes the slug rather than id here to bust // cache if the collection is renamed @@ -39,7 +37,6 @@ export const createCollection = createAction({ perform: ({ t, event }) => { event?.preventDefault(); event?.stopPropagation(); - stores.dialogs.openModal({ title: t("Create a collection"), content: , @@ -55,6 +52,8 @@ export const editCollection = createAction({ !!activeCollectionId && stores.policies.abilities(activeCollectionId).update, perform: ({ t, activeCollectionId }) => { + if (!activeCollectionId) return; + stores.dialogs.openModal({ title: t("Edit collection"), content: ( diff --git a/app/actions/definitions/debug.js b/app/actions/definitions/debug.tsx similarity index 79% rename from app/actions/definitions/debug.js rename to app/actions/definitions/debug.tsx index 142f2735c..5f11c5687 100644 --- a/app/actions/definitions/debug.js +++ b/app/actions/definitions/debug.tsx @@ -1,11 +1,10 @@ -// @flow import { ToolsIcon, TrashIcon } from "outline-icons"; import * as React from "react"; -import stores from "stores"; -import { createAction } from "actions"; -import { DebugSection } from "actions/sections"; -import env from "env"; -import { deleteAllDatabases } from "utils/developer"; +import stores from "~/stores"; +import { createAction } from "~/actions"; +import { DebugSection } from "~/actions/sections"; +import env from "~/env"; +import { deleteAllDatabases } from "~/utils/developer"; export const clearIndexedDB = createAction({ name: ({ t }) => t("Delete IndexedDB cache"), diff --git a/app/actions/definitions/documents.js b/app/actions/definitions/documents.tsx similarity index 88% rename from app/actions/definitions/documents.js rename to app/actions/definitions/documents.tsx index f3922d8e8..32c83ab5d 100644 --- a/app/actions/definitions/documents.js +++ b/app/actions/definitions/documents.tsx @@ -1,4 +1,3 @@ -// @flow import invariant from "invariant"; import { DownloadIcon, @@ -12,12 +11,12 @@ import { ImportIcon, } from "outline-icons"; import * as React from "react"; -import DocumentTemplatize from "scenes/DocumentTemplatize"; -import { createAction } from "actions"; -import { DocumentSection } from "actions/sections"; -import getDataTransferFiles from "utils/getDataTransferFiles"; -import history from "utils/history"; -import { newDocumentPath } from "utils/routeHelpers"; +import DocumentTemplatize from "~/scenes/DocumentTemplatize"; +import { createAction } from "~/actions"; +import { DocumentSection } from "~/actions/sections"; +import getDataTransferFiles from "~/utils/getDataTransferFiles"; +import history from "~/utils/history"; +import { newDocumentPath } from "~/utils/routeHelpers"; export const openDocument = createAction({ name: ({ t }) => t("Open document"), @@ -36,9 +35,7 @@ export const openDocument = createAction({ id: path.url, name: path.title, icon: () => - stores.documents.get(path.id)?.isStarred ? ( - - ) : undefined, + stores.documents.get(path.id)?.isStarred ? : null, section: DocumentSection, perform: () => history.push(path.url), })); @@ -65,13 +62,12 @@ export const starDocument = createAction({ visible: ({ activeDocumentId, stores }) => { if (!activeDocumentId) return false; const document = stores.documents.get(activeDocumentId); - return ( !document?.isStarred && stores.policies.abilities(activeDocumentId).star ); }, perform: ({ activeDocumentId, stores }) => { - if (!activeDocumentId) return false; + if (!activeDocumentId) return; const document = stores.documents.get(activeDocumentId); document?.star(); @@ -86,14 +82,13 @@ export const unstarDocument = createAction({ visible: ({ activeDocumentId, stores }) => { if (!activeDocumentId) return false; const document = stores.documents.get(activeDocumentId); - return ( !!document?.isStarred && stores.policies.abilities(activeDocumentId).unstar ); }, perform: ({ activeDocumentId, stores }) => { - if (!activeDocumentId) return false; + if (!activeDocumentId) return; const document = stores.documents.get(activeDocumentId); document?.unstar(); @@ -109,7 +104,7 @@ export const downloadDocument = createAction({ visible: ({ activeDocumentId, stores }) => !!activeDocumentId && stores.policies.abilities(activeDocumentId).download, perform: ({ activeDocumentId, stores }) => { - if (!activeDocumentId) return false; + if (!activeDocumentId) return; const document = stores.documents.get(activeDocumentId); document?.download(); @@ -125,16 +120,16 @@ export const duplicateDocument = createAction({ visible: ({ activeDocumentId, stores }) => !!activeDocumentId && stores.policies.abilities(activeDocumentId).update, perform: async ({ activeDocumentId, t, stores }) => { - if (!activeDocumentId) return false; + if (!activeDocumentId) return; const document = stores.documents.get(activeDocumentId); invariant(document, "Document must exist"); - const duped = await document.duplicate(); - // when duplicating, go straight to the duplicated document content history.push(duped.url); - stores.toasts.showToast(t("Document duplicated"), { type: "success" }); + stores.toasts.showToast(t("Document duplicated"), { + type: "success", + }); }, }); @@ -150,7 +145,7 @@ export const printDocument = createAction({ }); export const importDocument = createAction({ - name: ({ t, activeDocumentId }) => t("Import document"), + name: ({ t }) => t("Import document"), section: DocumentSection, icon: , keywords: "upload", @@ -158,18 +153,20 @@ export const importDocument = createAction({ if (activeDocumentId) { return !!stores.policies.abilities(activeDocumentId).createChildDocument; } + if (activeCollectionId) { return !!stores.policies.abilities(activeCollectionId).update; } + return false; }, perform: ({ activeCollectionId, activeDocumentId, stores }) => { const { documents, toasts } = stores; - const input = document.createElement("input"); input.type = "file"; input.accept = documents.importFileTypes.join(", "); - input.onchange = async (ev: SyntheticEvent<>) => { + + input.onchange = async (ev: Event) => { const files = getDataTransferFiles(ev); try { @@ -187,10 +184,10 @@ export const importDocument = createAction({ toasts.showToast(err.message, { type: "error", }); - throw err; } }; + input.click(); }, }); @@ -202,9 +199,7 @@ export const createTemplate = createAction({ keywords: "new create template", visible: ({ activeCollectionId, activeDocumentId, stores }) => { if (!activeDocumentId) return false; - const document = stores.documents.get(activeDocumentId); - return ( !!activeCollectionId && stores.policies.abilities(activeCollectionId).update && @@ -212,6 +207,8 @@ export const createTemplate = createAction({ ); }, perform: ({ activeDocumentId, stores, t, event }) => { + if (!activeDocumentId) return; + event?.preventDefault(); event?.stopPropagation(); diff --git a/app/actions/definitions/navigation.js b/app/actions/definitions/navigation.tsx similarity index 92% rename from app/actions/definitions/navigation.js rename to app/actions/definitions/navigation.tsx index db5d38cf8..003865a8f 100644 --- a/app/actions/definitions/navigation.js +++ b/app/actions/definitions/navigation.tsx @@ -1,4 +1,3 @@ -// @flow import { HomeIcon, SearchIcon, @@ -17,12 +16,12 @@ import { changelogUrl, mailToUrl, githubIssuesUrl, -} from "shared/utils/routeHelpers"; -import stores from "stores"; -import KeyboardShortcuts from "scenes/KeyboardShortcuts"; -import { createAction } from "actions"; -import { NavigationSection } from "actions/sections"; -import history from "utils/history"; +} from "@shared/utils/routeHelpers"; +import stores from "~/stores"; +import KeyboardShortcuts from "~/scenes/KeyboardShortcuts"; +import { createAction } from "~/actions"; +import { NavigationSection } from "~/actions/sections"; +import history from "~/utils/history"; import { settingsPath, homePath, @@ -31,7 +30,7 @@ import { templatesPath, archivePath, trashPath, -} from "utils/routeHelpers"; +} from "~/utils/routeHelpers"; export const navigateToHome = createAction({ name: ({ t }) => t("Home"), diff --git a/app/actions/definitions/settings.js b/app/actions/definitions/settings.tsx similarity index 79% rename from app/actions/definitions/settings.js rename to app/actions/definitions/settings.tsx index c2accae34..432872807 100644 --- a/app/actions/definitions/settings.js +++ b/app/actions/definitions/settings.tsx @@ -1,9 +1,9 @@ -// @flow import { SunIcon, MoonIcon, BrowserIcon } from "outline-icons"; import * as React from "react"; -import stores from "stores"; -import { createAction } from "actions"; -import { SettingsSection } from "actions/sections"; +import stores from "~/stores"; +import { Theme } from "~/stores/UiStore"; +import { createAction } from "~/actions"; +import { SettingsSection } from "~/actions/sections"; export const changeToDarkTheme = createAction({ name: ({ t }) => t("Dark"), @@ -12,7 +12,7 @@ export const changeToDarkTheme = createAction({ keywords: "theme dark night", section: SettingsSection, selected: () => stores.ui.theme === "dark", - perform: () => stores.ui.setTheme("dark"), + perform: () => stores.ui.setTheme(Theme.Dark), }); export const changeToLightTheme = createAction({ @@ -22,7 +22,7 @@ export const changeToLightTheme = createAction({ keywords: "theme light day", section: SettingsSection, selected: () => stores.ui.theme === "light", - perform: () => stores.ui.setTheme("light"), + perform: () => stores.ui.setTheme(Theme.Light), }); export const changeToSystemTheme = createAction({ @@ -32,7 +32,7 @@ export const changeToSystemTheme = createAction({ keywords: "theme system default", section: SettingsSection, selected: () => stores.ui.theme === "system", - perform: () => stores.ui.setTheme("system"), + perform: () => stores.ui.setTheme(Theme.System), }); export const changeTheme = createAction({ diff --git a/app/actions/definitions/users.js b/app/actions/definitions/users.tsx similarity index 77% rename from app/actions/definitions/users.js rename to app/actions/definitions/users.tsx index d60f2e66f..cb10834c2 100644 --- a/app/actions/definitions/users.js +++ b/app/actions/definitions/users.tsx @@ -1,10 +1,9 @@ -// @flow import { PlusIcon } from "outline-icons"; import * as React from "react"; -import stores from "stores"; -import Invite from "scenes/Invite"; -import { createAction } from "actions"; -import { UserSection } from "actions/sections"; +import stores from "~/stores"; +import Invite from "~/scenes/Invite"; +import { createAction } from "~/actions"; +import { UserSection } from "~/actions/sections"; export const inviteUser = createAction({ name: ({ t }) => `${t("Invite people")}…`, diff --git a/app/actions/index.js b/app/actions/index.ts similarity index 76% rename from app/actions/index.js rename to app/actions/index.ts index 0e61d21f3..622f448da 100644 --- a/app/actions/index.js +++ b/app/actions/index.ts @@ -1,17 +1,22 @@ -// @flow import { flattenDeep } from "lodash"; import * as React from "react"; +import { $Diff } from "utility-types"; import { v4 as uuidv4 } from "uuid"; -import type { +import { Action, ActionContext, CommandBarAction, - MenuItemClickable, + MenuItemButton, MenuItemWithChildren, -} from "types"; +} from "~/types"; export function createAction( - definition: $Diff + definition: $Diff< + Action, + { + id?: string; + } + > ): Action { return { id: uuidv4(), @@ -22,7 +27,7 @@ export function createAction( export function actionToMenuItem( action: Action, context: ActionContext -): MenuItemClickable | MenuItemWithChildren { +): MenuItemButton | MenuItemWithChildren { function resolve(value: any): T { if (typeof value === "function") { return value(context); @@ -31,18 +36,20 @@ export function actionToMenuItem( return value; } - const resolvedIcon = resolve>(action.icon); + const resolvedIcon = resolve>(action.icon); const resolvedChildren = resolve(action.children); - const visible = action.visible ? action.visible(context) : true; const title = resolve(action.name); const icon = resolvedIcon && action.iconInContextMenu !== false - ? React.cloneElement(resolvedIcon, { color: "currentColor" }) + ? React.cloneElement(resolvedIcon, { + color: "currentColor", + }) : undefined; if (resolvedChildren) { return { + type: "submenu", title, icon, items: resolvedChildren @@ -53,6 +60,7 @@ export function actionToMenuItem( } return { + type: "button", title, icon, visible, @@ -77,12 +85,11 @@ export function actionToKBar( return []; } - const resolvedIcon = resolve>(action.icon); + const resolvedIcon = resolve>(action.icon); const resolvedChildren = resolve(action.children); const resolvedSection = resolve(action.section); const resolvedName = resolve(action.name); const resolvedPlaceholder = resolve(action.placeholder); - const children = resolvedChildren ? flattenDeep(resolvedChildren.map((a) => actionToKBar(a, context))).filter( (a) => !!a @@ -99,19 +106,17 @@ export function actionToKBar( .filter((c) => !!c.keywords) .map((c) => c.keywords) .join(" ")}`, - shortcut: action.shortcut, + shortcut: action.shortcut || [], icon: resolvedIcon - ? React.cloneElement(resolvedIcon, { color: "currentColor" }) + ? React.cloneElement(resolvedIcon, { + color: "currentColor", + }) : undefined, perform: action.perform ? () => action.perform && action.perform(context) : undefined, children: children.length ? children.map((a) => a.id) : undefined, }, - ].concat( - children.map((child) => ({ - ...child, - parent: action.id, - })) - ); + // @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call. + ].concat(children.map((child) => ({ ...child, parent: action.id }))); } diff --git a/app/actions/root.js b/app/actions/root.ts similarity index 98% rename from app/actions/root.js rename to app/actions/root.ts index 7be0013d1..9894e0d72 100644 --- a/app/actions/root.js +++ b/app/actions/root.ts @@ -1,4 +1,3 @@ -// @flow import { rootCollectionActions } from "./definitions/collections"; import { rootDebugActions } from "./definitions/debug"; import { rootDocumentActions } from "./definitions/documents"; diff --git a/app/actions/sections.js b/app/actions/sections.ts similarity index 89% rename from app/actions/sections.js rename to app/actions/sections.ts index 97fc3ae73..e8a6a6320 100644 --- a/app/actions/sections.js +++ b/app/actions/sections.ts @@ -1,5 +1,4 @@ -// @flow -import { type ActionContext } from "types"; +import { ActionContext } from "~/types"; export const CollectionSection = ({ t }: ActionContext) => t("Collection"); diff --git a/app/components/Actions.js b/app/components/Actions.ts similarity index 95% rename from app/components/Actions.js rename to app/components/Actions.ts index 444ce4968..54b1212de 100644 --- a/app/components/Actions.js +++ b/app/components/Actions.ts @@ -1,7 +1,6 @@ -// @flow import styled from "styled-components"; import breakpoint from "styled-components-breakpoint"; -import Flex from "components/Flex"; +import Flex from "~/components/Flex"; export const Action = styled(Flex)` justify-content: center; diff --git a/app/components/Analytics.js b/app/components/Analytics.ts similarity index 78% rename from app/components/Analytics.js rename to app/components/Analytics.ts index ef9e0f154..1bf1e5d94 100644 --- a/app/components/Analytics.js +++ b/app/components/Analytics.ts @@ -1,32 +1,30 @@ -// @flow /* global ga */ import * as React from "react"; -import env from "env"; +import env from "~/env"; type Props = { - children?: React.Node, + children?: React.ReactNode; }; export default class Analytics extends React.Component { componentDidMount() { if (!env.GOOGLE_ANALYTICS_ID) { - return null; + return; } // standard Google Analytics script window.ga = window.ga || - function () { - // $FlowIssue - (ga.q = ga.q || []).push(arguments); + function (...args) { + (ga.q = ga.q || []).push(args); }; - // $FlowIssue ga.l = +new Date(); ga("create", env.GOOGLE_ANALYTICS_ID, "auto"); - ga("set", { dimension1: "true" }); + ga("set", { + dimension1: "true", + }); ga("send", "pageview"); - const script = document.createElement("script"); script.src = "https://www.google-analytics.com/analytics.js"; script.async = true; diff --git a/app/components/Arrow.js b/app/components/Arrow.tsx similarity index 98% rename from app/components/Arrow.js rename to app/components/Arrow.tsx index 2b6ade160..ba462d577 100644 --- a/app/components/Arrow.js +++ b/app/components/Arrow.tsx @@ -1,4 +1,3 @@ -// @flow import * as React from "react"; export default function Arrow() { diff --git a/app/components/AuthLogo/GoogleLogo.js b/app/components/AuthLogo/GoogleLogo.tsx similarity index 93% rename from app/components/AuthLogo/GoogleLogo.js rename to app/components/AuthLogo/GoogleLogo.tsx index b4132b7a6..4854512b9 100644 --- a/app/components/AuthLogo/GoogleLogo.js +++ b/app/components/AuthLogo/GoogleLogo.tsx @@ -1,10 +1,9 @@ -// @flow import * as React from "react"; type Props = { - size?: number, - fill?: string, - className?: string, + size?: number; + fill?: string; + className?: string; }; function GoogleLogo({ size = 34, fill = "#FFF", className }: Props) { diff --git a/app/components/AuthLogo/MicrosoftLogo.js b/app/components/AuthLogo/MicrosoftLogo.tsx similarity index 94% rename from app/components/AuthLogo/MicrosoftLogo.js rename to app/components/AuthLogo/MicrosoftLogo.tsx index a73c5b84f..393b52488 100644 --- a/app/components/AuthLogo/MicrosoftLogo.js +++ b/app/components/AuthLogo/MicrosoftLogo.tsx @@ -1,10 +1,9 @@ -// @flow import * as React from "react"; type Props = { - size?: number, - fill?: string, - className?: string, + size?: number; + fill?: string; + className?: string; }; function MicrosoftLogo({ size = 34, fill = "#FFF", className }: Props) { diff --git a/app/components/AuthLogo/SlackLogo.js b/app/components/AuthLogo/SlackLogo.tsx similarity index 98% rename from app/components/AuthLogo/SlackLogo.js rename to app/components/AuthLogo/SlackLogo.tsx index 69388b921..1d19f5b8b 100644 --- a/app/components/AuthLogo/SlackLogo.js +++ b/app/components/AuthLogo/SlackLogo.tsx @@ -1,10 +1,9 @@ -// @flow import * as React from "react"; type Props = { - size?: number, - fill?: string, - className?: string, + size?: number; + fill?: string; + className?: string; }; function SlackLogo({ size = 34, fill = "#FFF", className }: Props) { diff --git a/app/components/AuthLogo/index.js b/app/components/AuthLogo/index.tsx similarity index 91% rename from app/components/AuthLogo/index.js rename to app/components/AuthLogo/index.tsx index d2b444f2a..669b6591e 100644 --- a/app/components/AuthLogo/index.js +++ b/app/components/AuthLogo/index.tsx @@ -1,14 +1,13 @@ -// @flow import * as React from "react"; import styled from "styled-components"; import GoogleLogo from "./GoogleLogo"; import MicrosoftLogo from "./MicrosoftLogo"; import SlackLogo from "./SlackLogo"; -type Props = {| - providerName: string, - size?: number, -|}; +type Props = { + providerName: string; + size?: number; +}; function AuthLogo({ providerName, size = 16 }: Props) { switch (providerName) { @@ -18,18 +17,21 @@ function AuthLogo({ providerName, size = 16 }: Props) { ); + case "google": return ( ); + case "azure": return ( ); + default: return null; } diff --git a/app/components/Authenticated.js b/app/components/Authenticated.tsx similarity index 84% rename from app/components/Authenticated.js rename to app/components/Authenticated.tsx index c2d489a50..225791c66 100644 --- a/app/components/Authenticated.js +++ b/app/components/Authenticated.tsx @@ -1,16 +1,15 @@ -// @flow import { observer } from "mobx-react"; import * as React from "react"; import { useTranslation } from "react-i18next"; import { Redirect } from "react-router-dom"; -import { isCustomSubdomain } from "shared/utils/domains"; -import LoadingIndicator from "components/LoadingIndicator"; -import useStores from "../hooks/useStores"; -import { changeLanguage } from "../utils/language"; -import env from "env"; +import { isCustomSubdomain } from "@shared/utils/domains"; +import LoadingIndicator from "~/components/LoadingIndicator"; +import env from "~/env"; +import useStores from "~/hooks/useStores"; +import { changeLanguage } from "~/utils/language"; type Props = { - children: React.Node, + children: JSX.Element; }; const Authenticated = ({ children }: Props) => { diff --git a/app/components/Avatar/Avatar.js b/app/components/Avatar/Avatar.tsx similarity index 82% rename from app/components/Avatar/Avatar.js rename to app/components/Avatar/Avatar.tsx index c8b25dafe..88601f43f 100644 --- a/app/components/Avatar/Avatar.js +++ b/app/components/Avatar/Avatar.tsx @@ -1,24 +1,24 @@ -// @flow import { observable } from "mobx"; import { observer } from "mobx-react"; import * as React from "react"; import styled from "styled-components"; -import User from "models/User"; +import User from "~/models/User"; import placeholder from "./placeholder.png"; -type Props = {| - src: string, - size: number, - icon?: React.Node, - user?: User, - alt?: string, - onClick?: () => void, - className?: string, -|}; +type Props = { + src: string; + size: number; + icon?: React.ReactNode; + user?: User; + alt?: string; + onClick?: () => void; + className?: string; +}; @observer class Avatar extends React.Component { - @observable error: boolean; + @observable + error: boolean; static defaultProps = { size: 24, @@ -30,7 +30,6 @@ class Avatar extends React.Component { render() { const { src, icon, ...rest } = this.props; - return ( ` display: block; width: ${(props) => props.size}px; height: ${(props) => props.size}px; diff --git a/app/components/Avatar/AvatarWithPresence.js b/app/components/Avatar/AvatarWithPresence.tsx similarity index 75% rename from app/components/Avatar/AvatarWithPresence.js rename to app/components/Avatar/AvatarWithPresence.tsx index 831e17d51..6055f5665 100644 --- a/app/components/Avatar/AvatarWithPresence.js +++ b/app/components/Avatar/AvatarWithPresence.tsx @@ -1,26 +1,25 @@ -// @flow import { observable } from "mobx"; import { observer } from "mobx-react"; import * as React from "react"; -import { withTranslation, type TFunction } from "react-i18next"; +import { WithTranslation, withTranslation } from "react-i18next"; import styled from "styled-components"; -import User from "models/User"; -import UserProfile from "scenes/UserProfile"; -import Avatar from "components/Avatar"; -import Tooltip from "components/Tooltip"; +import User from "~/models/User"; +import UserProfile from "~/scenes/UserProfile"; +import Avatar from "~/components/Avatar"; +import Tooltip from "~/components/Tooltip"; -type Props = { - user: User, - isPresent: boolean, - isEditing: boolean, - isCurrentUser: boolean, - profileOnClick: boolean, - t: TFunction, +type Props = WithTranslation & { + user: User; + isPresent: boolean; + isEditing: boolean; + isCurrentUser: boolean; + profileOnClick: boolean; }; @observer class AvatarWithPresence extends React.Component { - @observable isOpen: boolean = false; + @observable + isOpen = false; handleOpenProfile = () => { this.isOpen = true; @@ -32,13 +31,11 @@ class AvatarWithPresence extends React.Component { render() { const { user, isPresent, isEditing, isCurrentUser, t } = this.props; - const action = isPresent ? isEditing ? t("currently editing") : t("currently viewing") : t("previously edited"); - return ( <> ` opacity: ${(props) => (props.isPresent ? 1 : 0.5)}; transition: opacity 250ms ease-in-out; `; -export default withTranslation()(AvatarWithPresence); +export default withTranslation()(AvatarWithPresence); diff --git a/app/components/Avatar/index.js b/app/components/Avatar/index.ts similarity index 94% rename from app/components/Avatar/index.js rename to app/components/Avatar/index.ts index 0106b24db..4dc0cb18b 100644 --- a/app/components/Avatar/index.js +++ b/app/components/Avatar/index.ts @@ -1,6 +1,6 @@ -// @flow import Avatar from "./Avatar"; import AvatarWithPresence from "./AvatarWithPresence"; export { AvatarWithPresence }; + export default Avatar; diff --git a/app/components/Badge.js b/app/components/Badge.ts similarity index 89% rename from app/components/Badge.js rename to app/components/Badge.ts index 04d564b84..bd0c1d11a 100644 --- a/app/components/Badge.js +++ b/app/components/Badge.ts @@ -1,7 +1,6 @@ -// @flow import styled from "styled-components"; -const Badge = styled.span` +const Badge = styled.span<{ yellow?: boolean; primary?: boolean }>` margin-left: 10px; padding: 1px 5px 2px; background-color: ${({ yellow, primary, theme }) => diff --git a/app/components/Branding.js b/app/components/Branding.tsx similarity index 93% rename from app/components/Branding.js rename to app/components/Branding.tsx index 38a90a340..756130eee 100644 --- a/app/components/Branding.js +++ b/app/components/Branding.tsx @@ -1,11 +1,10 @@ -// @flow import * as React from "react"; import styled from "styled-components"; +import env from "~/env"; import OutlineLogo from "./OutlineLogo"; -import env from "env"; type Props = { - href?: string, + href?: string; }; function Branding({ href = env.URL }: Props) { diff --git a/app/components/Breadcrumb.js b/app/components/Breadcrumb.tsx similarity index 73% rename from app/components/Breadcrumb.js rename to app/components/Breadcrumb.tsx index 350e20ad9..fb19e8971 100644 --- a/app/components/Breadcrumb.js +++ b/app/components/Breadcrumb.tsx @@ -1,35 +1,36 @@ -// @flow import { GoToIcon } from "outline-icons"; import * as React from "react"; import { Link } from "react-router-dom"; import styled from "styled-components"; -import Flex from "components/Flex"; -import BreadcrumbMenu from "menus/BreadcrumbMenu"; +import Flex from "~/components/Flex"; +import BreadcrumbMenu from "~/menus/BreadcrumbMenu"; +import { MenuInternalLink } from "~/types"; -type MenuItem = {| - icon?: React.Node, - title: React.Node, - to?: string, -|}; +export type Crumb = { + title: React.ReactNode; + icon?: React.ReactNode; + to?: string; +}; -type Props = {| - items: MenuItem[], - max?: number, - children?: React.Node, - highlightFirstItem?: boolean, -|}; +type Props = { + items: Crumb[]; + max?: number; + children?: React.ReactNode; + highlightFirstItem?: boolean; +}; function Breadcrumb({ items, highlightFirstItem, children, max = 2 }: Props) { const totalItems = items.length; - let topLevelItems: MenuItem[] = [...items]; + const topLevelItems: Crumb[] = [...items]; let overflowItems; // chop middle breadcrumbs and present a "..." menu instead if (totalItems > max) { const halfMax = Math.floor(max / 2); overflowItems = topLevelItems.splice(halfMax, totalItems - max); + topLevelItems.splice(halfMax, 0, { - title: , + title: , }); } @@ -42,7 +43,7 @@ function Breadcrumb({ items, highlightFirstItem, children, max = 2 }: Props) { {item.title} @@ -62,7 +63,7 @@ const Slash = styled(GoToIcon)` fill: ${(props) => props.theme.divider}; `; -const Item = styled(Link)` +const Item = styled(Link)<{ $highlight: boolean; $withIcon: boolean }>` display: flex; flex-shrink: 1; min-width: 0; diff --git a/app/components/Bubble.js b/app/components/Bubble.tsx similarity index 88% rename from app/components/Bubble.js rename to app/components/Bubble.tsx index 656ba7b54..6cba8e80b 100644 --- a/app/components/Bubble.js +++ b/app/components/Bubble.tsx @@ -1,11 +1,10 @@ -// @flow import * as React from "react"; import styled from "styled-components"; -import { bounceIn } from "styles/animations"; +import { bounceIn } from "~/styles/animations"; -type Props = {| - count: number, -|}; +type Props = { + count: number; +}; const Bubble = ({ count }: Props) => { if (!count) { diff --git a/app/components/Button.js b/app/components/Button.tsx similarity index 64% rename from app/components/Button.js rename to app/components/Button.tsx index c29b8ae8c..e8c1c0d42 100644 --- a/app/components/Button.js +++ b/app/components/Button.tsx @@ -1,10 +1,15 @@ -// @flow import { ExpandedIcon } from "outline-icons"; import { darken } from "polished"; import * as React from "react"; import styled from "styled-components"; -const RealButton = styled.button` +const RealButton = styled.button<{ + fullwidth?: boolean; + borderOnHover?: boolean; + neutral?: boolean; + danger?: boolean; + iconColor?: string; +}>` display: ${(props) => (props.fullwidth ? "block" : "inline-block")}; width: ${(props) => (props.fullwidth ? "100%" : "auto")}; margin: 0; @@ -50,7 +55,7 @@ const RealButton = styled.button` } ${(props) => - props.$neutral && + props.neutral && ` background: ${props.theme.buttonNeutralBackground}; color: ${props.theme.buttonNeutralText}; @@ -87,7 +92,9 @@ const RealButton = styled.button` fill: ${props.theme.textTertiary}; } } - `} ${(props) => + `} + + ${(props) => props.danger && ` background: ${props.theme.danger}; @@ -99,7 +106,7 @@ const RealButton = styled.button` `}; `; -const Label = styled.span` +const Label = styled.span<{ hasIcon?: boolean }>` overflow: hidden; white-space: nowrap; text-overflow: ellipsis; @@ -107,7 +114,11 @@ const Label = styled.span` ${(props) => props.hasIcon && "padding-left: 4px;"}; `; -export const Inner = styled.span` +export const Inner = styled.span<{ + disclosure?: boolean; + hasIcon?: boolean; + hasText?: boolean; +}>` display: flex; padding: 0 8px; padding-right: ${(props) => (props.disclosure ? 2 : 8)}px; @@ -120,58 +131,41 @@ export const Inner = styled.span` ${(props) => props.hasIcon && !props.hasText && "padding: 0 4px;"}; `; -export type Props = {| - type?: "button" | "submit", - value?: string, - icon?: React.Node, - iconColor?: string, - className?: string, - children?: React.Node, - innerRef?: React.ElementRef, - disclosure?: boolean, - neutral?: boolean, - danger?: boolean, - primary?: boolean, - disabled?: boolean, - fullwidth?: boolean, - autoFocus?: boolean, - style?: Object, - as?: React.ComponentType | string, - to?: string, - onClick?: (event: SyntheticEvent<>) => mixed, - borderOnHover?: boolean, - href?: string, - "data-on"?: string, - "data-event-category"?: string, - "data-event-action"?: string, -|}; +export type Props = { + icon?: React.ReactNode; + iconColor?: string; + children?: React.ReactNode; + disclosure?: boolean; + neutral?: boolean; + danger?: boolean; + primary?: boolean; + fullwidth?: boolean; + as?: T; + to?: string; + borderOnHover?: boolean; + href?: string; + "data-on"?: string; + "data-event-category"?: string; + "data-event-action"?: string; +}; -const Button = React.forwardRef( - ( - { - type = "text", - icon, - children, - value, - disclosure, - neutral, - ...rest - }: Props, - innerRef - ) => { - const hasText = children !== undefined || value !== undefined; - const hasIcon = icon !== undefined; +const Button = ( + props: Props & React.ComponentPropsWithoutRef, + ref: React.Ref +) => { + const { type, icon, children, value, disclosure, neutral, ...rest } = props; + const hasText = children !== undefined || value !== undefined; + const hasIcon = icon !== undefined; - return ( - - - {hasIcon && icon} - {hasText && } - {disclosure && } - - - ); - } -); + return ( + + + {hasIcon && icon} + {hasText && } + {disclosure && } + + + ); +}; -export default Button; +export default React.forwardRef(Button); diff --git a/app/components/ButtonLarge.js b/app/components/ButtonLarge.ts similarity index 95% rename from app/components/ButtonLarge.js rename to app/components/ButtonLarge.ts index f3611bbd8..24e28947d 100644 --- a/app/components/ButtonLarge.js +++ b/app/components/ButtonLarge.ts @@ -1,4 +1,3 @@ -// @flow import styled from "styled-components"; import Button, { Inner } from "./Button"; diff --git a/app/components/ButtonLink.js b/app/components/ButtonLink.tsx similarity index 81% rename from app/components/ButtonLink.js rename to app/components/ButtonLink.tsx index b42e1efdc..89302b628 100644 --- a/app/components/ButtonLink.js +++ b/app/components/ButtonLink.tsx @@ -1,10 +1,9 @@ -// @flow import * as React from "react"; import styled from "styled-components"; type Props = { - onClick: (ev: SyntheticEvent<>) => void, - children: React.Node, + onClick: React.MouseEventHandler; + children: React.ReactNode; }; export default function ButtonLink(props: Props) { diff --git a/app/components/CenteredContent.js b/app/components/CenteredContent.tsx similarity index 73% rename from app/components/CenteredContent.js rename to app/components/CenteredContent.tsx index 3f3685cdd..5724b8285 100644 --- a/app/components/CenteredContent.js +++ b/app/components/CenteredContent.tsx @@ -1,20 +1,19 @@ -// @flow import * as React from "react"; import styled from "styled-components"; import breakpoint from "styled-components-breakpoint"; -type Props = {| - children?: React.Node, - withStickyHeader?: boolean, -|}; +type Props = { + children?: React.ReactNode; + withStickyHeader?: boolean; +}; -const Container = styled.div` +const Container = styled.div<{ withStickyHeader?: boolean }>` width: 100%; max-width: 100vw; padding: ${(props) => (props.withStickyHeader ? "4px 12px" : "60px 12px")}; ${breakpoint("tablet")` - padding: ${(props) => (props.withStickyHeader ? "4px 60px" : "60px")}; + padding: ${(props: any) => (props.withStickyHeader ? "4px 60px" : "60px")}; `}; `; diff --git a/app/components/Checkbox.js b/app/components/Checkbox.tsx similarity index 68% rename from app/components/Checkbox.js rename to app/components/Checkbox.tsx index 11e9e7ab8..ecf01d422 100644 --- a/app/components/Checkbox.js +++ b/app/components/Checkbox.tsx @@ -1,29 +1,27 @@ -// @flow import * as React from "react"; import { VisuallyHidden } from "reakit/VisuallyHidden"; import styled from "styled-components"; -import HelpText from "components/HelpText"; +import HelpText from "~/components/HelpText"; -export type Props = {| - checked?: boolean, - label?: React.Node, - labelHidden?: boolean, - className?: string, - name?: string, - disabled?: boolean, - onChange: (event: SyntheticInputEvent) => mixed, - note?: React.Node, - short?: boolean, - small?: boolean, -|}; +export type Props = { + checked?: boolean; + label?: React.ReactNode; + labelHidden?: boolean; + className?: string; + name?: string; + disabled?: boolean; + onChange: (event: React.ChangeEvent) => unknown; + note?: React.ReactNode; + small?: boolean; +}; -const LabelText = styled.span` +const LabelText = styled.span<{ small?: boolean }>` font-weight: 500; margin-left: ${(props) => (props.small ? "6px" : "10px")}; ${(props) => (props.small ? `color: ${props.theme.textSecondary}` : "")}; `; -const Wrapper = styled.div` +const Wrapper = styled.div<{ small?: boolean }>` padding-bottom: 8px; ${(props) => (props.small ? "font-size: 14px" : "")}; width: 100%; @@ -41,14 +39,13 @@ export default function Checkbox({ note, className, small, - short, ...rest }: Props) { const wrappedLabel = {label}; return ( <> - +