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,7 +1,7 @@
{ {
"presets": [ "presets": [
"@babel/preset-react", "@babel/preset-react",
"@babel/preset-flow", "@babel/preset-typescript",
[ [
"@babel/preset-env", "@babel/preset-env",
{ {

View File

@@ -41,8 +41,8 @@ jobs:
name: lint name: lint
command: yarn lint command: yarn lint
- run: - run:
name: flow name: typescript
command: yarn flow check --max-workers 4 command: yarn tsc
- run: - run:
name: test name: test
command: yarn test command: yarn test

View File

@@ -6,14 +6,13 @@ __mocks__
.DS_Store .DS_Store
.env* .env*
.eslint* .eslint*
.flowconfig
.log .log
Makefile Makefile
Procfile Procfile
app.json app.json
crowdin.yml
build build
docker-compose.yml docker-compose.yml
fakes3 fakes3
flow-typed
node_modules node_modules
setupJest.js tsconfig.json

View File

@@ -1,20 +1,34 @@
{ {
"parser": "babel-eslint", "parser": "@typescript-eslint/parser",
"parserOptions": {
"sourceType": "module",
"extraFileExtensions": [".json"],
"ecmaFeatures": {
"jsx": true
}
},
"extends": [ "extends": [
"react-app", "eslint:recommended",
"plugin:import/errors", "plugin:@typescript-eslint/recommended",
"plugin:import/warnings", "plugin:import/recommended",
"plugin:flowtype/recommended", "plugin:import/typescript",
"plugin:react-hooks/recommended" "plugin:react-hooks/recommended",
"plugin:prettier/recommended"
], ],
"plugins": [ "plugins": [
"prettier", "@typescript-eslint",
"flowtype" "eslint-plugin-import",
"eslint-plugin-node",
"eslint-plugin-react",
"eslint-plugin-react-hooks",
"import"
], ],
"rules": { "rules": {
"eqeqeq": 2, "eqeqeq": 2,
"no-unused-vars": 2,
"no-mixed-operators": "off", "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": [ "import/order": [
"error", "error",
{ {
@@ -23,53 +37,48 @@
}, },
"pathGroups": [ "pathGroups": [
{ {
"pattern": "shared/**", "pattern": "@shared/**",
"group": "external", "group": "external",
"position": "after" "position": "after"
}, },
{ {
"pattern": "stores", "pattern": "@server/**",
"group": "external", "group": "external",
"position": "after" "position": "after"
}, },
{ {
"pattern": "stores/**", "pattern": "~/stores",
"group": "external", "group": "external",
"position": "after" "position": "after"
}, },
{ {
"pattern": "models/**", "pattern": "~/stores/**",
"group": "external", "group": "external",
"position": "after" "position": "after"
}, },
{ {
"pattern": "scenes/**", "pattern": "~/models/**",
"group": "external", "group": "external",
"position": "after" "position": "after"
}, },
{ {
"pattern": "components/**", "pattern": "~/scenes/**",
"group": "external",
"position": "after"
},
{
"pattern": "~/components/**",
"group": "external",
"position": "after"
},
{
"pattern": "~/**",
"group": "external", "group": "external",
"position": "after" "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": [ "prettier/prettier": [
"error", "error",
{ {
@@ -84,21 +93,13 @@
"pragma": "React", "pragma": "React",
"version": "detect" "version": "detect"
}, },
"import/resolver": { "import/parsers": {
"node": { "@typescript-eslint/parser": [".ts", ".tsx"]
"paths": [
"app",
"."
]
}
}, },
"flowtype": { "import/resolver": {
"onlyFilesWithFlowAnnotation": false "typescript": {}
} }
}, },
"env": {
"jest": true
},
"globals": { "globals": {
"EDITOR_VERSION": true "EDITOR_VERSION": true
} }

View File

@@ -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\/\(.*\)$' -> '<PROJECT_ROOT>/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

View File

@@ -1,7 +1,7 @@
{ {
"javascript.validate.enable": false, "javascript.validate.enable": true,
"javascript.format.enable": false, "javascript.format.enable": true,
"typescript.validate.enable": false, "typescript.validate.enable": true,
"typescript.format.enable": false, "typescript.format.enable": true,
"editor.formatOnSave": true, "editor.formatOnSave": true,
} }

View File

@@ -8,9 +8,10 @@
</p> </p>
<p align="center"> <p align="center">
<a href="https://circleci.com/gh/outline/outline" rel="nofollow"><img src="https://circleci.com/gh/outline/outline.svg?style=shield&amp;circle-token=c0c4c2f39990e277385d5c1ae96169c409eb887a"></a> <a href="https://circleci.com/gh/outline/outline" rel="nofollow"><img src="https://circleci.com/gh/outline/outline.svg?style=shield&amp;circle-token=c0c4c2f39990e277385d5c1ae96169c409eb887a"></a>
<a href="https://github.com/prettier/prettier"><img src="https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat"></a> <a href="http://www.typescriptlang.org" rel="nofollow"><img src="https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg" alt="TypeScript"></a>
<a href="https://github.com/styled-components/styled-components"><img src="https://img.shields.io/badge/style-%F0%9F%92%85%20styled--components-orange.svg"></a> <a href="https://github.com/prettier/prettier"><img src="https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat" alt="Prettier"></a>
<a href="https://translate.getoutline.com/project/outline"><img src="https://badges.crowdin.net/outline/localized.svg"></a> <a href="https://github.com/styled-components/styled-components"><img src="https://img.shields.io/badge/style-%F0%9F%92%85%20styled--components-orange.svg" alt="Styled Components"></a>
<a href="https://translate.getoutline.com/project/outline" alt="Localized"><img src="https://badges.crowdin.net/outline/localized.svg"></a>
</p> </p>
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). 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).

View File

@@ -1,4 +1,3 @@
/* eslint-disable flowtype/require-valid-file-annotation */
export default class Queue { export default class Queue {
name; name;

9
app/.eslintrc Normal file
View File

@@ -0,0 +1,9 @@
{
"extends": [
"../.eslintrc"
],
"env": {
"jest": true,
"browser": true
}
}

View File

@@ -7,14 +7,10 @@
"<rootDir>/shared" "<rootDir>/shared"
], ],
"moduleNameMapper": { "moduleNameMapper": {
"^shared/(.*)$": "<rootDir>/shared/$1", "^~/(.*)$": "<rootDir>/app/$1",
"^@shared/(.*)$": "<rootDir>/shared/$1",
"^.*[.](gif|ttf|eot|svg)$": "<rootDir>/__test__/fileMock.js" "^.*[.](gif|ttf|eot|svg)$": "<rootDir>/__test__/fileMock.js"
}, },
"moduleFileExtensions": [
"js",
"jsx",
"json"
],
"moduleDirectories": [ "moduleDirectories": [
"node_modules" "node_modules"
], ],
@@ -25,6 +21,6 @@
"<rootDir>/__mocks__/window.js" "<rootDir>/__mocks__/window.js"
], ],
"setupFilesAfterEnv": [ "setupFilesAfterEnv": [
"./app/test/setup.js" "./app/test/setup.ts"
] ]
} }

View File

@@ -1,13 +1,12 @@
// @flow
import { CollectionIcon, EditIcon, PlusIcon } from "outline-icons"; import { CollectionIcon, EditIcon, PlusIcon } from "outline-icons";
import * as React from "react"; import * as React from "react";
import stores from "stores"; import stores from "~/stores";
import CollectionEdit from "scenes/CollectionEdit"; import CollectionEdit from "~/scenes/CollectionEdit";
import CollectionNew from "scenes/CollectionNew"; import CollectionNew from "~/scenes/CollectionNew";
import DynamicCollectionIcon from "components/CollectionIcon"; import DynamicCollectionIcon from "~/components/CollectionIcon";
import { createAction } from "actions"; import { createAction } from "~/actions";
import { CollectionSection } from "actions/sections"; import { CollectionSection } from "~/actions/sections";
import history from "utils/history"; import history from "~/utils/history";
export const openCollection = createAction({ export const openCollection = createAction({
name: ({ t }) => t("Open collection"), name: ({ t }) => t("Open collection"),
@@ -16,7 +15,6 @@ export const openCollection = createAction({
icon: <CollectionIcon />, icon: <CollectionIcon />,
children: ({ stores }) => { children: ({ stores }) => {
const collections = stores.collections.orderedData; const collections = stores.collections.orderedData;
return collections.map((collection) => ({ return collections.map((collection) => ({
// Note: using url which includes the slug rather than id here to bust // Note: using url which includes the slug rather than id here to bust
// cache if the collection is renamed // cache if the collection is renamed
@@ -39,7 +37,6 @@ export const createCollection = createAction({
perform: ({ t, event }) => { perform: ({ t, event }) => {
event?.preventDefault(); event?.preventDefault();
event?.stopPropagation(); event?.stopPropagation();
stores.dialogs.openModal({ stores.dialogs.openModal({
title: t("Create a collection"), title: t("Create a collection"),
content: <CollectionNew onSubmit={stores.dialogs.closeAllModals} />, content: <CollectionNew onSubmit={stores.dialogs.closeAllModals} />,
@@ -55,6 +52,8 @@ export const editCollection = createAction({
!!activeCollectionId && !!activeCollectionId &&
stores.policies.abilities(activeCollectionId).update, stores.policies.abilities(activeCollectionId).update,
perform: ({ t, activeCollectionId }) => { perform: ({ t, activeCollectionId }) => {
if (!activeCollectionId) return;
stores.dialogs.openModal({ stores.dialogs.openModal({
title: t("Edit collection"), title: t("Edit collection"),
content: ( content: (

View File

@@ -1,11 +1,10 @@
// @flow
import { ToolsIcon, TrashIcon } from "outline-icons"; import { ToolsIcon, TrashIcon } from "outline-icons";
import * as React from "react"; import * as React from "react";
import stores from "stores"; import stores from "~/stores";
import { createAction } from "actions"; import { createAction } from "~/actions";
import { DebugSection } from "actions/sections"; import { DebugSection } from "~/actions/sections";
import env from "env"; import env from "~/env";
import { deleteAllDatabases } from "utils/developer"; import { deleteAllDatabases } from "~/utils/developer";
export const clearIndexedDB = createAction({ export const clearIndexedDB = createAction({
name: ({ t }) => t("Delete IndexedDB cache"), name: ({ t }) => t("Delete IndexedDB cache"),

View File

@@ -1,4 +1,3 @@
// @flow
import invariant from "invariant"; import invariant from "invariant";
import { import {
DownloadIcon, DownloadIcon,
@@ -12,12 +11,12 @@ import {
ImportIcon, ImportIcon,
} from "outline-icons"; } from "outline-icons";
import * as React from "react"; import * as React from "react";
import DocumentTemplatize from "scenes/DocumentTemplatize"; import DocumentTemplatize from "~/scenes/DocumentTemplatize";
import { createAction } from "actions"; import { createAction } from "~/actions";
import { DocumentSection } from "actions/sections"; import { DocumentSection } from "~/actions/sections";
import getDataTransferFiles from "utils/getDataTransferFiles"; import getDataTransferFiles from "~/utils/getDataTransferFiles";
import history from "utils/history"; import history from "~/utils/history";
import { newDocumentPath } from "utils/routeHelpers"; import { newDocumentPath } from "~/utils/routeHelpers";
export const openDocument = createAction({ export const openDocument = createAction({
name: ({ t }) => t("Open document"), name: ({ t }) => t("Open document"),
@@ -36,9 +35,7 @@ export const openDocument = createAction({
id: path.url, id: path.url,
name: path.title, name: path.title,
icon: () => icon: () =>
stores.documents.get(path.id)?.isStarred ? ( stores.documents.get(path.id)?.isStarred ? <StarredIcon /> : null,
<StarredIcon />
) : undefined,
section: DocumentSection, section: DocumentSection,
perform: () => history.push(path.url), perform: () => history.push(path.url),
})); }));
@@ -65,13 +62,12 @@ export const starDocument = createAction({
visible: ({ activeDocumentId, stores }) => { visible: ({ activeDocumentId, stores }) => {
if (!activeDocumentId) return false; if (!activeDocumentId) return false;
const document = stores.documents.get(activeDocumentId); const document = stores.documents.get(activeDocumentId);
return ( return (
!document?.isStarred && stores.policies.abilities(activeDocumentId).star !document?.isStarred && stores.policies.abilities(activeDocumentId).star
); );
}, },
perform: ({ activeDocumentId, stores }) => { perform: ({ activeDocumentId, stores }) => {
if (!activeDocumentId) return false; if (!activeDocumentId) return;
const document = stores.documents.get(activeDocumentId); const document = stores.documents.get(activeDocumentId);
document?.star(); document?.star();
@@ -86,14 +82,13 @@ export const unstarDocument = createAction({
visible: ({ activeDocumentId, stores }) => { visible: ({ activeDocumentId, stores }) => {
if (!activeDocumentId) return false; if (!activeDocumentId) return false;
const document = stores.documents.get(activeDocumentId); const document = stores.documents.get(activeDocumentId);
return ( return (
!!document?.isStarred && !!document?.isStarred &&
stores.policies.abilities(activeDocumentId).unstar stores.policies.abilities(activeDocumentId).unstar
); );
}, },
perform: ({ activeDocumentId, stores }) => { perform: ({ activeDocumentId, stores }) => {
if (!activeDocumentId) return false; if (!activeDocumentId) return;
const document = stores.documents.get(activeDocumentId); const document = stores.documents.get(activeDocumentId);
document?.unstar(); document?.unstar();
@@ -109,7 +104,7 @@ export const downloadDocument = createAction({
visible: ({ activeDocumentId, stores }) => visible: ({ activeDocumentId, stores }) =>
!!activeDocumentId && stores.policies.abilities(activeDocumentId).download, !!activeDocumentId && stores.policies.abilities(activeDocumentId).download,
perform: ({ activeDocumentId, stores }) => { perform: ({ activeDocumentId, stores }) => {
if (!activeDocumentId) return false; if (!activeDocumentId) return;
const document = stores.documents.get(activeDocumentId); const document = stores.documents.get(activeDocumentId);
document?.download(); document?.download();
@@ -125,16 +120,16 @@ export const duplicateDocument = createAction({
visible: ({ activeDocumentId, stores }) => visible: ({ activeDocumentId, stores }) =>
!!activeDocumentId && stores.policies.abilities(activeDocumentId).update, !!activeDocumentId && stores.policies.abilities(activeDocumentId).update,
perform: async ({ activeDocumentId, t, stores }) => { perform: async ({ activeDocumentId, t, stores }) => {
if (!activeDocumentId) return false; if (!activeDocumentId) return;
const document = stores.documents.get(activeDocumentId); const document = stores.documents.get(activeDocumentId);
invariant(document, "Document must exist"); invariant(document, "Document must exist");
const duped = await document.duplicate(); const duped = await document.duplicate();
// when duplicating, go straight to the duplicated document content // when duplicating, go straight to the duplicated document content
history.push(duped.url); 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({ export const importDocument = createAction({
name: ({ t, activeDocumentId }) => t("Import document"), name: ({ t }) => t("Import document"),
section: DocumentSection, section: DocumentSection,
icon: <ImportIcon />, icon: <ImportIcon />,
keywords: "upload", keywords: "upload",
@@ -158,18 +153,20 @@ export const importDocument = createAction({
if (activeDocumentId) { if (activeDocumentId) {
return !!stores.policies.abilities(activeDocumentId).createChildDocument; return !!stores.policies.abilities(activeDocumentId).createChildDocument;
} }
if (activeCollectionId) { if (activeCollectionId) {
return !!stores.policies.abilities(activeCollectionId).update; return !!stores.policies.abilities(activeCollectionId).update;
} }
return false; return false;
}, },
perform: ({ activeCollectionId, activeDocumentId, stores }) => { perform: ({ activeCollectionId, activeDocumentId, stores }) => {
const { documents, toasts } = stores; const { documents, toasts } = stores;
const input = document.createElement("input"); const input = document.createElement("input");
input.type = "file"; input.type = "file";
input.accept = documents.importFileTypes.join(", "); input.accept = documents.importFileTypes.join(", ");
input.onchange = async (ev: SyntheticEvent<>) => {
input.onchange = async (ev: Event) => {
const files = getDataTransferFiles(ev); const files = getDataTransferFiles(ev);
try { try {
@@ -187,10 +184,10 @@ export const importDocument = createAction({
toasts.showToast(err.message, { toasts.showToast(err.message, {
type: "error", type: "error",
}); });
throw err; throw err;
} }
}; };
input.click(); input.click();
}, },
}); });
@@ -202,9 +199,7 @@ export const createTemplate = createAction({
keywords: "new create template", keywords: "new create template",
visible: ({ activeCollectionId, activeDocumentId, stores }) => { visible: ({ activeCollectionId, activeDocumentId, stores }) => {
if (!activeDocumentId) return false; if (!activeDocumentId) return false;
const document = stores.documents.get(activeDocumentId); const document = stores.documents.get(activeDocumentId);
return ( return (
!!activeCollectionId && !!activeCollectionId &&
stores.policies.abilities(activeCollectionId).update && stores.policies.abilities(activeCollectionId).update &&
@@ -212,6 +207,8 @@ export const createTemplate = createAction({
); );
}, },
perform: ({ activeDocumentId, stores, t, event }) => { perform: ({ activeDocumentId, stores, t, event }) => {
if (!activeDocumentId) return;
event?.preventDefault(); event?.preventDefault();
event?.stopPropagation(); event?.stopPropagation();

View File

@@ -1,4 +1,3 @@
// @flow
import { import {
HomeIcon, HomeIcon,
SearchIcon, SearchIcon,
@@ -17,12 +16,12 @@ import {
changelogUrl, changelogUrl,
mailToUrl, mailToUrl,
githubIssuesUrl, githubIssuesUrl,
} from "shared/utils/routeHelpers"; } from "@shared/utils/routeHelpers";
import stores from "stores"; import stores from "~/stores";
import KeyboardShortcuts from "scenes/KeyboardShortcuts"; import KeyboardShortcuts from "~/scenes/KeyboardShortcuts";
import { createAction } from "actions"; import { createAction } from "~/actions";
import { NavigationSection } from "actions/sections"; import { NavigationSection } from "~/actions/sections";
import history from "utils/history"; import history from "~/utils/history";
import { import {
settingsPath, settingsPath,
homePath, homePath,
@@ -31,7 +30,7 @@ import {
templatesPath, templatesPath,
archivePath, archivePath,
trashPath, trashPath,
} from "utils/routeHelpers"; } from "~/utils/routeHelpers";
export const navigateToHome = createAction({ export const navigateToHome = createAction({
name: ({ t }) => t("Home"), name: ({ t }) => t("Home"),

View File

@@ -1,9 +1,9 @@
// @flow
import { SunIcon, MoonIcon, BrowserIcon } from "outline-icons"; import { SunIcon, MoonIcon, BrowserIcon } from "outline-icons";
import * as React from "react"; import * as React from "react";
import stores from "stores"; import stores from "~/stores";
import { createAction } from "actions"; import { Theme } from "~/stores/UiStore";
import { SettingsSection } from "actions/sections"; import { createAction } from "~/actions";
import { SettingsSection } from "~/actions/sections";
export const changeToDarkTheme = createAction({ export const changeToDarkTheme = createAction({
name: ({ t }) => t("Dark"), name: ({ t }) => t("Dark"),
@@ -12,7 +12,7 @@ export const changeToDarkTheme = createAction({
keywords: "theme dark night", keywords: "theme dark night",
section: SettingsSection, section: SettingsSection,
selected: () => stores.ui.theme === "dark", selected: () => stores.ui.theme === "dark",
perform: () => stores.ui.setTheme("dark"), perform: () => stores.ui.setTheme(Theme.Dark),
}); });
export const changeToLightTheme = createAction({ export const changeToLightTheme = createAction({
@@ -22,7 +22,7 @@ export const changeToLightTheme = createAction({
keywords: "theme light day", keywords: "theme light day",
section: SettingsSection, section: SettingsSection,
selected: () => stores.ui.theme === "light", selected: () => stores.ui.theme === "light",
perform: () => stores.ui.setTheme("light"), perform: () => stores.ui.setTheme(Theme.Light),
}); });
export const changeToSystemTheme = createAction({ export const changeToSystemTheme = createAction({
@@ -32,7 +32,7 @@ export const changeToSystemTheme = createAction({
keywords: "theme system default", keywords: "theme system default",
section: SettingsSection, section: SettingsSection,
selected: () => stores.ui.theme === "system", selected: () => stores.ui.theme === "system",
perform: () => stores.ui.setTheme("system"), perform: () => stores.ui.setTheme(Theme.System),
}); });
export const changeTheme = createAction({ export const changeTheme = createAction({

View File

@@ -1,10 +1,9 @@
// @flow
import { PlusIcon } from "outline-icons"; import { PlusIcon } from "outline-icons";
import * as React from "react"; import * as React from "react";
import stores from "stores"; import stores from "~/stores";
import Invite from "scenes/Invite"; import Invite from "~/scenes/Invite";
import { createAction } from "actions"; import { createAction } from "~/actions";
import { UserSection } from "actions/sections"; import { UserSection } from "~/actions/sections";
export const inviteUser = createAction({ export const inviteUser = createAction({
name: ({ t }) => `${t("Invite people")}`, name: ({ t }) => `${t("Invite people")}`,

View File

@@ -1,17 +1,22 @@
// @flow
import { flattenDeep } from "lodash"; import { flattenDeep } from "lodash";
import * as React from "react"; import * as React from "react";
import { $Diff } from "utility-types";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import type { import {
Action, Action,
ActionContext, ActionContext,
CommandBarAction, CommandBarAction,
MenuItemClickable, MenuItemButton,
MenuItemWithChildren, MenuItemWithChildren,
} from "types"; } from "~/types";
export function createAction( export function createAction(
definition: $Diff<Action, { id?: string }> definition: $Diff<
Action,
{
id?: string;
}
>
): Action { ): Action {
return { return {
id: uuidv4(), id: uuidv4(),
@@ -22,7 +27,7 @@ export function createAction(
export function actionToMenuItem( export function actionToMenuItem(
action: Action, action: Action,
context: ActionContext context: ActionContext
): MenuItemClickable | MenuItemWithChildren { ): MenuItemButton | MenuItemWithChildren {
function resolve<T>(value: any): T { function resolve<T>(value: any): T {
if (typeof value === "function") { if (typeof value === "function") {
return value(context); return value(context);
@@ -31,18 +36,20 @@ export function actionToMenuItem(
return value; return value;
} }
const resolvedIcon = resolve<React.Element<any>>(action.icon); const resolvedIcon = resolve<React.ReactElement<any>>(action.icon);
const resolvedChildren = resolve<Action[]>(action.children); const resolvedChildren = resolve<Action[]>(action.children);
const visible = action.visible ? action.visible(context) : true; const visible = action.visible ? action.visible(context) : true;
const title = resolve<string>(action.name); const title = resolve<string>(action.name);
const icon = const icon =
resolvedIcon && action.iconInContextMenu !== false resolvedIcon && action.iconInContextMenu !== false
? React.cloneElement(resolvedIcon, { color: "currentColor" }) ? React.cloneElement(resolvedIcon, {
color: "currentColor",
})
: undefined; : undefined;
if (resolvedChildren) { if (resolvedChildren) {
return { return {
type: "submenu",
title, title,
icon, icon,
items: resolvedChildren items: resolvedChildren
@@ -53,6 +60,7 @@ export function actionToMenuItem(
} }
return { return {
type: "button",
title, title,
icon, icon,
visible, visible,
@@ -77,12 +85,11 @@ export function actionToKBar(
return []; return [];
} }
const resolvedIcon = resolve<React.Element<any>>(action.icon); const resolvedIcon = resolve<React.ReactElement<any>>(action.icon);
const resolvedChildren = resolve<Action[]>(action.children); const resolvedChildren = resolve<Action[]>(action.children);
const resolvedSection = resolve<string>(action.section); const resolvedSection = resolve<string>(action.section);
const resolvedName = resolve<string>(action.name); const resolvedName = resolve<string>(action.name);
const resolvedPlaceholder = resolve<string>(action.placeholder); const resolvedPlaceholder = resolve<string>(action.placeholder);
const children = resolvedChildren const children = resolvedChildren
? flattenDeep(resolvedChildren.map((a) => actionToKBar(a, context))).filter( ? flattenDeep(resolvedChildren.map((a) => actionToKBar(a, context))).filter(
(a) => !!a (a) => !!a
@@ -99,19 +106,17 @@ export function actionToKBar(
.filter((c) => !!c.keywords) .filter((c) => !!c.keywords)
.map((c) => c.keywords) .map((c) => c.keywords)
.join(" ")}`, .join(" ")}`,
shortcut: action.shortcut, shortcut: action.shortcut || [],
icon: resolvedIcon icon: resolvedIcon
? React.cloneElement(resolvedIcon, { color: "currentColor" }) ? React.cloneElement(resolvedIcon, {
color: "currentColor",
})
: undefined, : undefined,
perform: action.perform perform: action.perform
? () => action.perform && action.perform(context) ? () => action.perform && action.perform(context)
: undefined, : undefined,
children: children.length ? children.map((a) => a.id) : undefined, children: children.length ? children.map((a) => a.id) : undefined,
}, },
].concat( // @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
children.map((child) => ({ ].concat(children.map((child) => ({ ...child, parent: action.id })));
...child,
parent: action.id,
}))
);
} }

View File

@@ -1,4 +1,3 @@
// @flow
import { rootCollectionActions } from "./definitions/collections"; import { rootCollectionActions } from "./definitions/collections";
import { rootDebugActions } from "./definitions/debug"; import { rootDebugActions } from "./definitions/debug";
import { rootDocumentActions } from "./definitions/documents"; import { rootDocumentActions } from "./definitions/documents";

View File

@@ -1,5 +1,4 @@
// @flow import { ActionContext } from "~/types";
import { type ActionContext } from "types";
export const CollectionSection = ({ t }: ActionContext) => t("Collection"); export const CollectionSection = ({ t }: ActionContext) => t("Collection");

View File

@@ -1,7 +1,6 @@
// @flow
import styled from "styled-components"; import styled from "styled-components";
import breakpoint from "styled-components-breakpoint"; import breakpoint from "styled-components-breakpoint";
import Flex from "components/Flex"; import Flex from "~/components/Flex";
export const Action = styled(Flex)` export const Action = styled(Flex)`
justify-content: center; justify-content: center;

View File

@@ -1,32 +1,30 @@
// @flow
/* global ga */ /* global ga */
import * as React from "react"; import * as React from "react";
import env from "env"; import env from "~/env";
type Props = { type Props = {
children?: React.Node, children?: React.ReactNode;
}; };
export default class Analytics extends React.Component<Props> { export default class Analytics extends React.Component<Props> {
componentDidMount() { componentDidMount() {
if (!env.GOOGLE_ANALYTICS_ID) { if (!env.GOOGLE_ANALYTICS_ID) {
return null; return;
} }
// standard Google Analytics script // standard Google Analytics script
window.ga = window.ga =
window.ga || window.ga ||
function () { function (...args) {
// $FlowIssue (ga.q = ga.q || []).push(args);
(ga.q = ga.q || []).push(arguments);
}; };
// $FlowIssue
ga.l = +new Date(); ga.l = +new Date();
ga("create", env.GOOGLE_ANALYTICS_ID, "auto"); ga("create", env.GOOGLE_ANALYTICS_ID, "auto");
ga("set", { dimension1: "true" }); ga("set", {
dimension1: "true",
});
ga("send", "pageview"); ga("send", "pageview");
const script = document.createElement("script"); const script = document.createElement("script");
script.src = "https://www.google-analytics.com/analytics.js"; script.src = "https://www.google-analytics.com/analytics.js";
script.async = true; script.async = true;

View File

@@ -1,4 +1,3 @@
// @flow
import * as React from "react"; import * as React from "react";
export default function Arrow() { export default function Arrow() {

View File

@@ -1,10 +1,9 @@
// @flow
import * as React from "react"; import * as React from "react";
type Props = { type Props = {
size?: number, size?: number;
fill?: string, fill?: string;
className?: string, className?: string;
}; };
function GoogleLogo({ size = 34, fill = "#FFF", className }: Props) { function GoogleLogo({ size = 34, fill = "#FFF", className }: Props) {

View File

@@ -1,10 +1,9 @@
// @flow
import * as React from "react"; import * as React from "react";
type Props = { type Props = {
size?: number, size?: number;
fill?: string, fill?: string;
className?: string, className?: string;
}; };
function MicrosoftLogo({ size = 34, fill = "#FFF", className }: Props) { function MicrosoftLogo({ size = 34, fill = "#FFF", className }: Props) {

View File

@@ -1,10 +1,9 @@
// @flow
import * as React from "react"; import * as React from "react";
type Props = { type Props = {
size?: number, size?: number;
fill?: string, fill?: string;
className?: string, className?: string;
}; };
function SlackLogo({ size = 34, fill = "#FFF", className }: Props) { function SlackLogo({ size = 34, fill = "#FFF", className }: Props) {

View File

@@ -1,14 +1,13 @@
// @flow
import * as React from "react"; import * as React from "react";
import styled from "styled-components"; import styled from "styled-components";
import GoogleLogo from "./GoogleLogo"; import GoogleLogo from "./GoogleLogo";
import MicrosoftLogo from "./MicrosoftLogo"; import MicrosoftLogo from "./MicrosoftLogo";
import SlackLogo from "./SlackLogo"; import SlackLogo from "./SlackLogo";
type Props = {| type Props = {
providerName: string, providerName: string;
size?: number, size?: number;
|}; };
function AuthLogo({ providerName, size = 16 }: Props) { function AuthLogo({ providerName, size = 16 }: Props) {
switch (providerName) { switch (providerName) {
@@ -18,18 +17,21 @@ function AuthLogo({ providerName, size = 16 }: Props) {
<SlackLogo size={size} /> <SlackLogo size={size} />
</Logo> </Logo>
); );
case "google": case "google":
return ( return (
<Logo> <Logo>
<GoogleLogo size={size} /> <GoogleLogo size={size} />
</Logo> </Logo>
); );
case "azure": case "azure":
return ( return (
<Logo> <Logo>
<MicrosoftLogo size={size} /> <MicrosoftLogo size={size} />
</Logo> </Logo>
); );
default: default:
return null; return null;
} }

View File

@@ -1,16 +1,15 @@
// @flow
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import * as React from "react"; import * as React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Redirect } from "react-router-dom"; import { Redirect } from "react-router-dom";
import { isCustomSubdomain } from "shared/utils/domains"; import { isCustomSubdomain } from "@shared/utils/domains";
import LoadingIndicator from "components/LoadingIndicator"; import LoadingIndicator from "~/components/LoadingIndicator";
import useStores from "../hooks/useStores"; import env from "~/env";
import { changeLanguage } from "../utils/language"; import useStores from "~/hooks/useStores";
import env from "env"; import { changeLanguage } from "~/utils/language";
type Props = { type Props = {
children: React.Node, children: JSX.Element;
}; };
const Authenticated = ({ children }: Props) => { const Authenticated = ({ children }: Props) => {

View File

@@ -1,24 +1,24 @@
// @flow
import { observable } from "mobx"; import { observable } from "mobx";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import * as React from "react"; import * as React from "react";
import styled from "styled-components"; import styled from "styled-components";
import User from "models/User"; import User from "~/models/User";
import placeholder from "./placeholder.png"; import placeholder from "./placeholder.png";
type Props = {| type Props = {
src: string, src: string;
size: number, size: number;
icon?: React.Node, icon?: React.ReactNode;
user?: User, user?: User;
alt?: string, alt?: string;
onClick?: () => void, onClick?: () => void;
className?: string, className?: string;
|}; };
@observer @observer
class Avatar extends React.Component<Props> { class Avatar extends React.Component<Props> {
@observable error: boolean; @observable
error: boolean;
static defaultProps = { static defaultProps = {
size: 24, size: 24,
@@ -30,7 +30,6 @@ class Avatar extends React.Component<Props> {
render() { render() {
const { src, icon, ...rest } = this.props; const { src, icon, ...rest } = this.props;
return ( return (
<AvatarWrapper> <AvatarWrapper>
<CircleImg <CircleImg
@@ -60,7 +59,7 @@ const IconWrapper = styled.div`
height: 20px; height: 20px;
`; `;
const CircleImg = styled.img` const CircleImg = styled.img<{ size: number }>`
display: block; display: block;
width: ${(props) => props.size}px; width: ${(props) => props.size}px;
height: ${(props) => props.size}px; height: ${(props) => props.size}px;

View File

@@ -1,26 +1,25 @@
// @flow
import { observable } from "mobx"; import { observable } from "mobx";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import * as React from "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 styled from "styled-components";
import User from "models/User"; import User from "~/models/User";
import UserProfile from "scenes/UserProfile"; import UserProfile from "~/scenes/UserProfile";
import Avatar from "components/Avatar"; import Avatar from "~/components/Avatar";
import Tooltip from "components/Tooltip"; import Tooltip from "~/components/Tooltip";
type Props = { type Props = WithTranslation & {
user: User, user: User;
isPresent: boolean, isPresent: boolean;
isEditing: boolean, isEditing: boolean;
isCurrentUser: boolean, isCurrentUser: boolean;
profileOnClick: boolean, profileOnClick: boolean;
t: TFunction,
}; };
@observer @observer
class AvatarWithPresence extends React.Component<Props> { class AvatarWithPresence extends React.Component<Props> {
@observable isOpen: boolean = false; @observable
isOpen = false;
handleOpenProfile = () => { handleOpenProfile = () => {
this.isOpen = true; this.isOpen = true;
@@ -32,13 +31,11 @@ class AvatarWithPresence extends React.Component<Props> {
render() { render() {
const { user, isPresent, isEditing, isCurrentUser, t } = this.props; const { user, isPresent, isEditing, isCurrentUser, t } = this.props;
const action = isPresent const action = isPresent
? isEditing ? isEditing
? t("currently editing") ? t("currently editing")
: t("currently viewing") : t("currently viewing")
: t("previously edited"); : t("previously edited");
return ( return (
<> <>
<Tooltip <Tooltip
@@ -81,9 +78,9 @@ const Centered = styled.div`
text-align: center; text-align: center;
`; `;
const AvatarWrapper = styled.div` const AvatarWrapper = styled.div<{ isPresent: boolean }>`
opacity: ${(props) => (props.isPresent ? 1 : 0.5)}; opacity: ${(props) => (props.isPresent ? 1 : 0.5)};
transition: opacity 250ms ease-in-out; transition: opacity 250ms ease-in-out;
`; `;
export default withTranslation()<AvatarWithPresence>(AvatarWithPresence); export default withTranslation()(AvatarWithPresence);

View File

@@ -1,6 +1,6 @@
// @flow
import Avatar from "./Avatar"; import Avatar from "./Avatar";
import AvatarWithPresence from "./AvatarWithPresence"; import AvatarWithPresence from "./AvatarWithPresence";
export { AvatarWithPresence }; export { AvatarWithPresence };
export default Avatar; export default Avatar;

View File

@@ -1,7 +1,6 @@
// @flow
import styled from "styled-components"; import styled from "styled-components";
const Badge = styled.span` const Badge = styled.span<{ yellow?: boolean; primary?: boolean }>`
margin-left: 10px; margin-left: 10px;
padding: 1px 5px 2px; padding: 1px 5px 2px;
background-color: ${({ yellow, primary, theme }) => background-color: ${({ yellow, primary, theme }) =>

View File

@@ -1,11 +1,10 @@
// @flow
import * as React from "react"; import * as React from "react";
import styled from "styled-components"; import styled from "styled-components";
import env from "~/env";
import OutlineLogo from "./OutlineLogo"; import OutlineLogo from "./OutlineLogo";
import env from "env";
type Props = { type Props = {
href?: string, href?: string;
}; };
function Branding({ href = env.URL }: Props) { function Branding({ href = env.URL }: Props) {

View File

@@ -1,35 +1,36 @@
// @flow
import { GoToIcon } from "outline-icons"; import { GoToIcon } from "outline-icons";
import * as React from "react"; import * as React from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import styled from "styled-components"; import styled from "styled-components";
import Flex from "components/Flex"; import Flex from "~/components/Flex";
import BreadcrumbMenu from "menus/BreadcrumbMenu"; import BreadcrumbMenu from "~/menus/BreadcrumbMenu";
import { MenuInternalLink } from "~/types";
type MenuItem = {| export type Crumb = {
icon?: React.Node, title: React.ReactNode;
title: React.Node, icon?: React.ReactNode;
to?: string, to?: string;
|}; };
type Props = {| type Props = {
items: MenuItem[], items: Crumb[];
max?: number, max?: number;
children?: React.Node, children?: React.ReactNode;
highlightFirstItem?: boolean, highlightFirstItem?: boolean;
|}; };
function Breadcrumb({ items, highlightFirstItem, children, max = 2 }: Props) { function Breadcrumb({ items, highlightFirstItem, children, max = 2 }: Props) {
const totalItems = items.length; const totalItems = items.length;
let topLevelItems: MenuItem[] = [...items]; const topLevelItems: Crumb[] = [...items];
let overflowItems; let overflowItems;
// chop middle breadcrumbs and present a "..." menu instead // chop middle breadcrumbs and present a "..." menu instead
if (totalItems > max) { if (totalItems > max) {
const halfMax = Math.floor(max / 2); const halfMax = Math.floor(max / 2);
overflowItems = topLevelItems.splice(halfMax, totalItems - max); overflowItems = topLevelItems.splice(halfMax, totalItems - max);
topLevelItems.splice(halfMax, 0, { topLevelItems.splice(halfMax, 0, {
title: <BreadcrumbMenu items={overflowItems} />, title: <BreadcrumbMenu items={overflowItems as MenuInternalLink[]} />,
}); });
} }
@@ -42,7 +43,7 @@ function Breadcrumb({ items, highlightFirstItem, children, max = 2 }: Props) {
<Item <Item
to={item.to} to={item.to}
$withIcon={!!item.icon} $withIcon={!!item.icon}
$highlight={highlightFirstItem && index === 0} $highlight={!!highlightFirstItem && index === 0}
> >
{item.title} {item.title}
</Item> </Item>
@@ -62,7 +63,7 @@ const Slash = styled(GoToIcon)`
fill: ${(props) => props.theme.divider}; fill: ${(props) => props.theme.divider};
`; `;
const Item = styled(Link)` const Item = styled(Link)<{ $highlight: boolean; $withIcon: boolean }>`
display: flex; display: flex;
flex-shrink: 1; flex-shrink: 1;
min-width: 0; min-width: 0;

View File

@@ -1,11 +1,10 @@
// @flow
import * as React from "react"; import * as React from "react";
import styled from "styled-components"; import styled from "styled-components";
import { bounceIn } from "styles/animations"; import { bounceIn } from "~/styles/animations";
type Props = {| type Props = {
count: number, count: number;
|}; };
const Bubble = ({ count }: Props) => { const Bubble = ({ count }: Props) => {
if (!count) { if (!count) {

View File

@@ -1,10 +1,15 @@
// @flow
import { ExpandedIcon } from "outline-icons"; import { ExpandedIcon } from "outline-icons";
import { darken } from "polished"; import { darken } from "polished";
import * as React from "react"; import * as React from "react";
import styled from "styled-components"; 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")}; display: ${(props) => (props.fullwidth ? "block" : "inline-block")};
width: ${(props) => (props.fullwidth ? "100%" : "auto")}; width: ${(props) => (props.fullwidth ? "100%" : "auto")};
margin: 0; margin: 0;
@@ -50,7 +55,7 @@ const RealButton = styled.button`
} }
${(props) => ${(props) =>
props.$neutral && props.neutral &&
` `
background: ${props.theme.buttonNeutralBackground}; background: ${props.theme.buttonNeutralBackground};
color: ${props.theme.buttonNeutralText}; color: ${props.theme.buttonNeutralText};
@@ -87,7 +92,9 @@ const RealButton = styled.button`
fill: ${props.theme.textTertiary}; fill: ${props.theme.textTertiary};
} }
} }
`} ${(props) => `}
${(props) =>
props.danger && props.danger &&
` `
background: ${props.theme.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; overflow: hidden;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -107,7 +114,11 @@ const Label = styled.span`
${(props) => props.hasIcon && "padding-left: 4px;"}; ${(props) => props.hasIcon && "padding-left: 4px;"};
`; `;
export const Inner = styled.span` export const Inner = styled.span<{
disclosure?: boolean;
hasIcon?: boolean;
hasText?: boolean;
}>`
display: flex; display: flex;
padding: 0 8px; padding: 0 8px;
padding-right: ${(props) => (props.disclosure ? 2 : 8)}px; 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;"}; ${(props) => props.hasIcon && !props.hasText && "padding: 0 4px;"};
`; `;
export type Props = {| export type Props<T> = {
type?: "button" | "submit", icon?: React.ReactNode;
value?: string, iconColor?: string;
icon?: React.Node, children?: React.ReactNode;
iconColor?: string, disclosure?: boolean;
className?: string, neutral?: boolean;
children?: React.Node, danger?: boolean;
innerRef?: React.ElementRef<any>, primary?: boolean;
disclosure?: boolean, fullwidth?: boolean;
neutral?: boolean, as?: T;
danger?: boolean, to?: string;
primary?: boolean, borderOnHover?: boolean;
disabled?: boolean, href?: string;
fullwidth?: boolean, "data-on"?: string;
autoFocus?: boolean, "data-event-category"?: string;
style?: Object, "data-event-action"?: string;
as?: React.ComponentType<any> | string, };
to?: string,
onClick?: (event: SyntheticEvent<>) => mixed,
borderOnHover?: boolean,
href?: string,
"data-on"?: string,
"data-event-category"?: string,
"data-event-action"?: string,
|};
const Button = React.forwardRef<Props, HTMLButtonElement>( const Button = <T extends React.ElementType = "button">(
( props: Props<T> & React.ComponentPropsWithoutRef<T>,
{ ref: React.Ref<HTMLButtonElement>
type = "text", ) => {
icon, const { type, icon, children, value, disclosure, neutral, ...rest } = props;
children, const hasText = children !== undefined || value !== undefined;
value, const hasIcon = icon !== undefined;
disclosure,
neutral,
...rest
}: Props,
innerRef
) => {
const hasText = children !== undefined || value !== undefined;
const hasIcon = icon !== undefined;
return ( return (
<RealButton type={type} ref={innerRef} $neutral={neutral} {...rest}> <RealButton type={type || "button"} ref={ref} neutral={neutral} {...rest}>
<Inner hasIcon={hasIcon} hasText={hasText} disclosure={disclosure}> <Inner hasIcon={hasIcon} hasText={hasText} disclosure={disclosure}>
{hasIcon && icon} {hasIcon && icon}
{hasText && <Label hasIcon={hasIcon}>{children || value}</Label>} {hasText && <Label hasIcon={hasIcon}>{children || value}</Label>}
{disclosure && <ExpandedIcon />} {disclosure && <ExpandedIcon />}
</Inner> </Inner>
</RealButton> </RealButton>
); );
} };
);
export default Button; export default React.forwardRef(Button);

View File

@@ -1,4 +1,3 @@
// @flow
import styled from "styled-components"; import styled from "styled-components";
import Button, { Inner } from "./Button"; import Button, { Inner } from "./Button";

View File

@@ -1,10 +1,9 @@
// @flow
import * as React from "react"; import * as React from "react";
import styled from "styled-components"; import styled from "styled-components";
type Props = { type Props = {
onClick: (ev: SyntheticEvent<>) => void, onClick: React.MouseEventHandler<HTMLButtonElement>;
children: React.Node, children: React.ReactNode;
}; };
export default function ButtonLink(props: Props) { export default function ButtonLink(props: Props) {

View File

@@ -1,20 +1,19 @@
// @flow
import * as React from "react"; import * as React from "react";
import styled from "styled-components"; import styled from "styled-components";
import breakpoint from "styled-components-breakpoint"; import breakpoint from "styled-components-breakpoint";
type Props = {| type Props = {
children?: React.Node, children?: React.ReactNode;
withStickyHeader?: boolean, withStickyHeader?: boolean;
|}; };
const Container = styled.div` const Container = styled.div<{ withStickyHeader?: boolean }>`
width: 100%; width: 100%;
max-width: 100vw; max-width: 100vw;
padding: ${(props) => (props.withStickyHeader ? "4px 12px" : "60px 12px")}; padding: ${(props) => (props.withStickyHeader ? "4px 12px" : "60px 12px")};
${breakpoint("tablet")` ${breakpoint("tablet")`
padding: ${(props) => (props.withStickyHeader ? "4px 60px" : "60px")}; padding: ${(props: any) => (props.withStickyHeader ? "4px 60px" : "60px")};
`}; `};
`; `;

View File

@@ -1,29 +1,27 @@
// @flow
import * as React from "react"; import * as React from "react";
import { VisuallyHidden } from "reakit/VisuallyHidden"; import { VisuallyHidden } from "reakit/VisuallyHidden";
import styled from "styled-components"; import styled from "styled-components";
import HelpText from "components/HelpText"; import HelpText from "~/components/HelpText";
export type Props = {| export type Props = {
checked?: boolean, checked?: boolean;
label?: React.Node, label?: React.ReactNode;
labelHidden?: boolean, labelHidden?: boolean;
className?: string, className?: string;
name?: string, name?: string;
disabled?: boolean, disabled?: boolean;
onChange: (event: SyntheticInputEvent<HTMLInputElement>) => mixed, onChange: (event: React.ChangeEvent<HTMLInputElement>) => unknown;
note?: React.Node, note?: React.ReactNode;
short?: boolean, small?: boolean;
small?: boolean, };
|};
const LabelText = styled.span` const LabelText = styled.span<{ small?: boolean }>`
font-weight: 500; font-weight: 500;
margin-left: ${(props) => (props.small ? "6px" : "10px")}; margin-left: ${(props) => (props.small ? "6px" : "10px")};
${(props) => (props.small ? `color: ${props.theme.textSecondary}` : "")}; ${(props) => (props.small ? `color: ${props.theme.textSecondary}` : "")};
`; `;
const Wrapper = styled.div` const Wrapper = styled.div<{ small?: boolean }>`
padding-bottom: 8px; padding-bottom: 8px;
${(props) => (props.small ? "font-size: 14px" : "")}; ${(props) => (props.small ? "font-size: 14px" : "")};
width: 100%; width: 100%;
@@ -41,14 +39,13 @@ export default function Checkbox({
note, note,
className, className,
small, small,
short,
...rest ...rest
}: Props) { }: Props) {
const wrappedLabel = <LabelText small={small}>{label}</LabelText>; const wrappedLabel = <LabelText small={small}>{label}</LabelText>;
return ( return (
<> <>
<Wrapper small={small}> <Wrapper small={small} className={className}>
<Label> <Label>
<input type="checkbox" {...rest} /> <input type="checkbox" {...rest} />
{label && {label &&

View File

@@ -1,8 +1,7 @@
// @flow
import React from "react"; import React from "react";
import styled, { useTheme } from "styled-components"; import styled, { useTheme } from "styled-components";
const cleanPercentage = (percentage) => { const cleanPercentage = (percentage: number) => {
const tooLow = !Number.isFinite(+percentage) || percentage < 0; const tooLow = !Number.isFinite(+percentage) || percentage < 0;
const tooHigh = percentage > 100; const tooHigh = percentage > 100;
return tooLow ? 0 : tooHigh ? 100 : +percentage; return tooLow ? 0 : tooHigh ? 100 : +percentage;
@@ -13,13 +12,14 @@ const Circle = ({
percentage, percentage,
offset, offset,
}: { }: {
color: string, color: string;
percentage?: number, percentage?: number;
offset: number, offset: number;
}) => { }) => {
const radius = offset * 0.7; const radius = offset * 0.7;
const circumference = 2 * Math.PI * radius; const circumference = 2 * Math.PI * radius;
let strokePercentage; let strokePercentage;
if (percentage) { if (percentage) {
// because the circle is so small, anything greater than 85% appears like 100% // because the circle is so small, anything greater than 85% appears like 100%
percentage = percentage > 85 && percentage < 100 ? 85 : percentage; percentage = percentage > 85 && percentage < 100 ? 85 : percentage;
@@ -39,7 +39,9 @@ const Circle = ({
strokeDasharray={circumference} strokeDasharray={circumference}
strokeDashoffset={percentage ? strokePercentage : 0} strokeDashoffset={percentage ? strokePercentage : 0}
strokeLinecap="round" strokeLinecap="round"
style={{ transition: "stroke-dashoffset 0.6s ease 0s" }} style={{
transition: "stroke-dashoffset 0.6s ease 0s",
}}
></circle> ></circle>
); );
}; };
@@ -48,8 +50,8 @@ const CircularProgressBar = ({
percentage, percentage,
size = 16, size = 16,
}: { }: {
percentage: number, percentage: number;
size?: number, size?: number;
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
percentage = cleanPercentage(percentage); percentage = cleanPercentage(percentage);

View File

@@ -1,7 +1,6 @@
// @flow
import styled from "styled-components"; import styled from "styled-components";
const ClickablePadding = styled.div` const ClickablePadding = styled.div<{ grow?: boolean }>`
min-height: 10em; min-height: 10em;
cursor: ${({ onClick }) => (onClick ? "text" : "default")}; cursor: ${({ onClick }) => (onClick ? "text" : "default")};
${({ grow }) => grow && `flex-grow: 100;`}; ${({ grow }) => grow && `flex-grow: 100;`};

View File

@@ -1,4 +1,3 @@
// @flow
import { sortBy, filter, uniq, isEqual } from "lodash"; import { sortBy, filter, uniq, isEqual } from "lodash";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import * as React from "react"; import * as React from "react";
@@ -6,18 +5,18 @@ import { useTranslation } from "react-i18next";
import { usePopoverState, PopoverDisclosure } from "reakit/Popover"; import { usePopoverState, PopoverDisclosure } from "reakit/Popover";
import styled from "styled-components"; import styled from "styled-components";
import breakpoint from "styled-components-breakpoint"; import breakpoint from "styled-components-breakpoint";
import Document from "models/Document"; import Document from "~/models/Document";
import { AvatarWithPresence } from "components/Avatar"; import { AvatarWithPresence } from "~/components/Avatar";
import DocumentViews from "components/DocumentViews"; import DocumentViews from "~/components/DocumentViews";
import Facepile from "components/Facepile"; import Facepile from "~/components/Facepile";
import NudeButton from "components/NudeButton"; import NudeButton from "~/components/NudeButton";
import Popover from "components/Popover"; import Popover from "~/components/Popover";
import useCurrentUser from "hooks/useCurrentUser"; import useCurrentUser from "~/hooks/useCurrentUser";
import useStores from "hooks/useStores"; import useStores from "~/hooks/useStores";
type Props = {| type Props = {
document: Document, document: Document;
|}; };
function Collaborators(props: Props) { function Collaborators(props: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -26,14 +25,13 @@ function Collaborators(props: Props) {
const [requestedUserIds, setRequestedUserIds] = React.useState<string[]>([]); const [requestedUserIds, setRequestedUserIds] = React.useState<string[]>([]);
const { users, presence } = useStores(); const { users, presence } = useStores();
const { document } = props; const { document } = props;
const documentPresence = presence.get(document.id);
let documentPresence = presence.get(document.id); const documentPresenceArray = documentPresence
documentPresence = documentPresence
? Array.from(documentPresence.values()) ? Array.from(documentPresence.values())
: []; : [];
const presentIds = documentPresence.map((p) => p.userId); const presentIds = documentPresenceArray.map((p) => p.userId);
const editingIds = documentPresence const editingIds = documentPresenceArray
.filter((p) => p.isEditing) .filter((p) => p.isEditing)
.map((p) => p.userId); .map((p) => p.userId);
@@ -83,7 +81,6 @@ function Collaborators(props: Props) {
renderAvatar={(user) => { renderAvatar={(user) => {
const isPresent = presentIds.includes(user.id); const isPresent = presentIds.includes(user.id);
const isEditing = editingIds.includes(user.id); const isEditing = editingIds.includes(user.id);
return ( return (
<AvatarWithPresence <AvatarWithPresence
key={user.id} key={user.id}

View File

@@ -1,22 +1,21 @@
// @flow
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { transparentize } from "polished"; import { transparentize } from "polished";
import * as React from "react"; import * as React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import styled from "styled-components"; import styled from "styled-components";
import Collection from "models/Collection"; import Collection from "~/models/Collection";
import Arrow from "components/Arrow"; import Arrow from "~/components/Arrow";
import ButtonLink from "components/ButtonLink"; import ButtonLink from "~/components/ButtonLink";
import Editor from "components/Editor"; import Editor from "~/components/Editor";
import LoadingIndicator from "components/LoadingIndicator"; import LoadingIndicator from "~/components/LoadingIndicator";
import NudeButton from "components/NudeButton"; import NudeButton from "~/components/NudeButton";
import useDebouncedCallback from "hooks/useDebouncedCallback"; import useDebouncedCallback from "~/hooks/useDebouncedCallback";
import useStores from "hooks/useStores"; import useStores from "~/hooks/useStores";
import useToasts from "hooks/useToasts"; import useToasts from "~/hooks/useToasts";
type Props = {| type Props = {
collection: Collection, collection: Collection;
|}; };
function CollectionDescription({ collection }: Props) { function CollectionDescription({ collection }: Props) {
const { collections, policies } = useStores(); const { collections, policies } = useStores();
@@ -40,6 +39,7 @@ function CollectionDescription({ collection }: Props) {
event.preventDefault(); event.preventDefault();
if (isExpanded && document.activeElement) { if (isExpanded && document.activeElement) {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'blur' does not exist on type 'Element'.
document.activeElement.blur(); document.activeElement.blur();
} }
@@ -75,20 +75,16 @@ function CollectionDescription({ collection }: Props) {
React.useEffect(() => { React.useEffect(() => {
setEditing(false); setEditing(false);
}, [collection.id]); }, [collection.id]);
const placeholder = `${t("Add a description")}`; const placeholder = `${t("Add a description")}`;
const key = isEditing || isDirty ? "draft" : collection.updatedAt; const key = isEditing || isDirty ? "draft" : collection.updatedAt;
return ( return (
<MaxHeight data-editing={isEditing} data-expanded={isExpanded}> <MaxHeight data-editing={isEditing} data-expanded={isExpanded}>
<Input <Input data-editing={isEditing} data-expanded={isExpanded}>
$isEditable={can.update}
data-editing={isEditing}
data-expanded={isExpanded}
>
<span onClick={can.update ? handleStartEditing : undefined}> <span onClick={can.update ? handleStartEditing : undefined}>
{collections.isSaving && <LoadingIndicator />} {collections.isSaving && <LoadingIndicator />}
{collection.hasDescription || isEditing || isDirty ? ( {collection.hasDescription || isEditing || isDirty ? (
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
<React.Suspense fallback={<Placeholder>Loading</Placeholder>}> <React.Suspense fallback={<Placeholder>Loading</Placeholder>}>
<Editor <Editor
key={key} key={key}
@@ -105,7 +101,15 @@ function CollectionDescription({ collection }: Props) {
/> />
</React.Suspense> </React.Suspense>
) : ( ) : (
can.update && <Placeholder>{placeholder}</Placeholder> can.update && (
<Placeholder
onClick={() => {
//
}}
>
{placeholder}
</Placeholder>
)
)} )}
</span> </span>
</Input> </Input>

View File

@@ -1,16 +1,15 @@
// @flow
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { CollectionIcon } from "outline-icons"; import { CollectionIcon } from "outline-icons";
import { getLuminance } from "polished"; import { getLuminance } from "polished";
import * as React from "react"; import * as React from "react";
import Collection from "models/Collection"; import Collection from "~/models/Collection";
import { icons } from "components/IconPicker"; import { icons } from "~/components/IconPicker";
import useStores from "hooks/useStores"; import useStores from "~/hooks/useStores";
type Props = { type Props = {
collection: Collection, collection: Collection;
expanded?: boolean, expanded?: boolean;
size?: number, size?: number;
}; };
function ResolvedCollectionIcon({ collection, expanded, size }: Props) { function ResolvedCollectionIcon({ collection, expanded, size }: Props) {

View File

@@ -1,4 +1,3 @@
// @flow
import { useKBar, KBarPositioner, KBarAnimator, KBarSearch } from "kbar"; import { useKBar, KBarPositioner, KBarAnimator, KBarSearch } from "kbar";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import * as React from "react"; import * as React from "react";
@@ -6,9 +5,10 @@ import { useTranslation } from "react-i18next";
import { Portal } from "react-portal"; import { Portal } from "react-portal";
import styled from "styled-components"; import styled from "styled-components";
import breakpoint from "styled-components-breakpoint"; import breakpoint from "styled-components-breakpoint";
import CommandBarResults from "components/CommandBarResults"; import CommandBarResults from "~/components/CommandBarResults";
import rootActions from "actions/root"; import rootActions from "~/actions/root";
import useCommandBarActions from "hooks/useCommandBarActions"; import useCommandBarActions from "~/hooks/useCommandBarActions";
import { CommandBarAction } from "~/types";
export const CommandBarOptions = { export const CommandBarOptions = {
animations: { animations: {
@@ -19,11 +19,12 @@ export const CommandBarOptions = {
function CommandBar() { function CommandBar() {
const { t } = useTranslation(); const { t } = useTranslation();
useCommandBarActions(rootActions); useCommandBarActions(rootActions);
const { rootAction } = useKBar((state) => ({ const { rootAction } = useKBar((state) => ({
rootAction: state.actions[state.currentRootActionId], rootAction: state.currentRootActionId
? (state.actions[state.currentRootActionId] as CommandBarAction)
: undefined,
})); }));
return ( return (
@@ -44,7 +45,7 @@ function CommandBar() {
); );
} }
function KBarPortal({ children }: { children: React.Node }) { function KBarPortal({ children }: { children: React.ReactNode }) {
const { showing } = useKBar((state) => ({ const { showing } = useKBar((state) => ({
showing: state.visualState !== "hidden", showing: state.visualState !== "hidden",
})); }));

View File

@@ -1,23 +1,27 @@
// @flow
import { BackIcon } from "outline-icons"; import { BackIcon } from "outline-icons";
import * as React from "react"; import * as React from "react";
import styled from "styled-components"; import styled from "styled-components";
import Flex from "components/Flex"; import Flex from "~/components/Flex";
import Key from "components/Key"; import Key from "~/components/Key";
import type { CommandBarAction } from "types"; import { CommandBarAction } from "~/types";
type Props = {| type Props = {
action: CommandBarAction, action: CommandBarAction;
active: Boolean, active: boolean;
|}; };
function CommandBarItem({ action, active }: Props, ref) { function CommandBarItem(
{ action, active }: Props,
ref: React.RefObject<HTMLDivElement>
) {
return ( return (
<Item active={active} ref={ref}> <Item active={active} ref={ref}>
<Text align="center" gap={8}> <Text align="center" gap={8}>
<Icon> <Icon>
{action.icon ? ( {action.icon ? (
React.cloneElement(action.icon, { size: 22 }) React.cloneElement(action.icon, {
size: 22,
})
) : ( ) : (
<ForwardIcon color="currentColor" size={22} /> <ForwardIcon color="currentColor" size={22} />
)} )}
@@ -26,8 +30,14 @@ function CommandBarItem({ action, active }: Props, ref) {
{action.children?.length ? "…" : ""} {action.children?.length ? "…" : ""}
</Text> </Text>
{action.shortcut?.length ? ( {action.shortcut?.length ? (
<div style={{ display: "grid", gridAutoFlow: "column", gap: "4px" }}> <div
{action.shortcut.map((sc) => ( style={{
display: "grid",
gridAutoFlow: "column",
gap: "4px",
}}
>
{action.shortcut.map((sc: string) => (
<Key key={sc}>{sc}</Key> <Key key={sc}>{sc}</Key>
))} ))}
</div> </div>
@@ -48,7 +58,7 @@ const Text = styled(Flex)`
flex-shrink: 1; flex-shrink: 1;
`; `;
const Item = styled.div` const Item = styled.div<{ active?: boolean }>`
font-size: 15px; font-size: 15px;
padding: 12px 16px; padding: 12px 16px;
background: ${(props) => background: ${(props) =>
@@ -68,4 +78,4 @@ const ForwardIcon = styled(BackIcon)`
transform: rotate(180deg); transform: rotate(180deg);
`; `;
export default React.forwardRef<Props, HTMLDivElement>(CommandBarItem); export default React.forwardRef<HTMLDivElement, Props>(CommandBarItem);

View File

@@ -1,8 +1,8 @@
// @flow import { useMatches, KBarResults, Action } from "kbar";
import { useMatches, KBarResults, NO_GROUP } from "kbar";
import * as React from "react"; import * as React from "react";
import styled from "styled-components"; import styled from "styled-components";
import CommandBarItem from "components/CommandBarItem"; import CommandBarItem from "~/components/CommandBarItem";
import { CommandBarAction } from "~/types";
export default function CommandBarResults() { export default function CommandBarResults() {
const matches = useMatches(); const matches = useMatches();
@@ -14,8 +14,8 @@ export default function CommandBarResults() {
acc.push(name); acc.push(name);
acc.push(...actions); acc.push(...actions);
return acc; return acc;
}, []) }, [] as (Action | string)[])
.filter((i) => i !== NO_GROUP), .filter((i) => i !== "none"),
[matches] [matches]
); );
@@ -27,7 +27,7 @@ export default function CommandBarResults() {
typeof item === "string" ? ( typeof item === "string" ? (
<Header>{item}</Header> <Header>{item}</Header>
) : ( ) : (
<CommandBarItem action={item} active={active} /> <CommandBarItem action={item as CommandBarAction} active={active} />
) )
} }
/> />

View File

@@ -1,14 +1,13 @@
// @flow
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { DisconnectedIcon } from "outline-icons"; import { DisconnectedIcon } from "outline-icons";
import * as React from "react"; import * as React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import styled, { useTheme } from "styled-components"; import styled, { useTheme } from "styled-components";
import breakpoint from "styled-components-breakpoint"; import breakpoint from "styled-components-breakpoint";
import Fade from "components/Fade"; import Fade from "~/components/Fade";
import NudeButton from "components/NudeButton"; import NudeButton from "~/components/NudeButton";
import Tooltip from "components/Tooltip"; import Tooltip from "~/components/Tooltip";
import useStores from "hooks/useStores"; import useStores from "~/hooks/useStores";
function ConnectionStatus() { function ConnectionStatus() {
const { ui } = useStores(); const { ui } = useStores();

View File

@@ -1,22 +1,20 @@
// @flow
import isPrintableKeyEvent from "is-printable-key-event"; import isPrintableKeyEvent from "is-printable-key-event";
import * as React from "react"; import * as React from "react";
import styled from "styled-components"; import styled from "styled-components";
type Props = {| type Props = Omit<React.HTMLAttributes<HTMLSpanElement>, "ref" | "onChange"> & {
disabled?: boolean, disabled?: boolean;
readOnly?: boolean, readOnly?: boolean;
onChange?: (text: string) => void, onChange?: (text: string) => void;
onBlur?: (event: SyntheticInputEvent<>) => void, onBlur?: React.FocusEventHandler<HTMLSpanElement> | undefined;
onInput?: (event: SyntheticInputEvent<>) => void, onInput?: React.FormEventHandler<HTMLSpanElement> | undefined;
onKeyDown?: (event: SyntheticInputEvent<>) => void, onKeyDown?: React.KeyboardEventHandler<HTMLSpanElement> | undefined;
placeholder?: string, placeholder?: string;
maxLength?: number, maxLength?: number;
autoFocus?: boolean, autoFocus?: boolean;
className?: string, children?: React.ReactNode;
children?: React.Node, value: string;
value: string, };
|};
/** /**
* Defines a content editable component with the same interface as a native * Defines a content editable component with the same interface as a native
@@ -37,18 +35,22 @@ function ContentEditable({
readOnly, readOnly,
...rest ...rest
}: Props) { }: Props) {
const ref = React.useRef<?HTMLSpanElement>(); const ref = React.useRef<HTMLSpanElement>(null);
const [innerHTML, setInnerHTML] = React.useState<string>(value); const [innerHTML, setInnerHTML] = React.useState<string>(value);
const lastValue = React.useRef(""); const lastValue = React.useRef("");
const wrappedEvent = (callback) => ( const wrappedEvent = (
event: SyntheticInputEvent<HTMLInputElement> callback:
) => { | React.FocusEventHandler<HTMLSpanElement>
| React.FormEventHandler<HTMLSpanElement>
| React.KeyboardEventHandler<HTMLSpanElement>
| undefined
) => (event: any) => {
const text = ref.current?.innerText || ""; const text = ref.current?.innerText || "";
if (maxLength && isPrintableKeyEvent(event) && text.length >= maxLength) { if (maxLength && isPrintableKeyEvent(event) && text.length >= maxLength) {
event.preventDefault(); event?.preventDefault();
return false; return;
} }
if (text !== lastValue.current) { if (text !== lastValue.current) {
@@ -56,7 +58,7 @@ function ContentEditable({
onChange && onChange(text); onChange && onChange(text);
} }
callback && callback(event); callback?.(event);
}; };
React.useLayoutEffect(() => { React.useLayoutEffect(() => {
@@ -74,14 +76,16 @@ function ContentEditable({
return ( return (
<div className={className}> <div className={className}>
<Content <Content
ref={ref}
contentEditable={!disabled && !readOnly} contentEditable={!disabled && !readOnly}
onInput={wrappedEvent(onInput)} onInput={wrappedEvent(onInput)}
onBlur={wrappedEvent(onBlur)} onBlur={wrappedEvent(onBlur)}
onKeyDown={wrappedEvent(onKeyDown)} onKeyDown={wrappedEvent(onKeyDown)}
ref={ref}
data-placeholder={placeholder} data-placeholder={placeholder}
role="textbox" role="textbox"
dangerouslySetInnerHTML={{ __html: innerHTML }} dangerouslySetInnerHTML={{
__html: innerHTML,
}}
{...rest} {...rest}
/> />
{children} {children}

View File

@@ -1,4 +1,3 @@
// @flow
import styled from "styled-components"; import styled from "styled-components";
const Header = styled.h3` const Header = styled.h3`

View File

@@ -1,4 +1,3 @@
// @flow
import { CheckmarkIcon } from "outline-icons"; import { CheckmarkIcon } from "outline-icons";
import * as React from "react"; import * as React from "react";
import { MenuItem as BaseMenuItem } from "reakit/Menu"; import { MenuItem as BaseMenuItem } from "reakit/Menu";
@@ -6,19 +5,19 @@ import styled, { css } from "styled-components";
import breakpoint from "styled-components-breakpoint"; import breakpoint from "styled-components-breakpoint";
import MenuIconWrapper from "../MenuIconWrapper"; import MenuIconWrapper from "../MenuIconWrapper";
type Props = {| type Props = {
onClick?: (SyntheticEvent<>) => void | Promise<void>, onClick?: (arg0: React.SyntheticEvent) => void | Promise<void>;
children?: React.Node, children?: React.ReactNode;
selected?: boolean, selected?: boolean;
disabled?: boolean, disabled?: boolean;
to?: string, to?: string;
href?: string, href?: string;
target?: "_blank", target?: "_blank";
as?: string | React.ComponentType<*>, as?: string | React.ComponentType<any>;
hide?: () => void, hide?: () => void;
level?: number, level?: number;
icon?: React.Node, icon?: React.ReactNode;
|}; };
const MenuItem = ({ const MenuItem = ({
onClick, onClick,
@@ -88,7 +87,7 @@ const Spacer = styled.svg`
flex-shrink: 0; flex-shrink: 0;
`; `;
export const MenuAnchorCSS = css` export const MenuAnchorCSS = css<{ level?: number; disabled?: boolean }>`
display: flex; display: flex;
margin: 0; margin: 0;
border: 0; border: 0;
@@ -138,6 +137,7 @@ export const MenuAnchorCSS = css`
font-size: 14px; font-size: 14px;
`}; `};
`; `;
export const MenuAnchor = styled.a` export const MenuAnchor = styled.a`
${MenuAnchorCSS} ${MenuAnchorCSS}
`; `;

View File

@@ -1,14 +1,18 @@
// @flow
import { MoreIcon } from "outline-icons"; import { MoreIcon } from "outline-icons";
import * as React from "react"; import * as React from "react";
import { MenuButton } from "reakit/Menu"; import { MenuButton } from "reakit/Menu";
import NudeButton from "components/NudeButton"; import NudeButton from "~/components/NudeButton";
type Props = React.ComponentProps<typeof MenuButton> & {
className?: string;
iconColor?: string;
};
export default function OverflowMenuButton({ export default function OverflowMenuButton({
iconColor, iconColor,
className, className,
...rest ...rest
}: any) { }: Props) {
return ( return (
<MenuButton {...rest}> <MenuButton {...rest}>
{(props) => ( {(props) => (

View File

@@ -1,9 +1,8 @@
// @flow
import * as React from "react"; import * as React from "react";
import { MenuSeparator } from "reakit/Menu"; import { MenuSeparator } from "reakit/Menu";
import styled from "styled-components"; import styled from "styled-components";
export default function Separator(rest: {}) { export default function Separator(rest: any) {
return ( return (
<MenuSeparator {...rest}> <MenuSeparator {...rest}>
{(props) => <HorizontalRule {...props} />} {(props) => <HorizontalRule {...props} />}

View File

@@ -1,197 +0,0 @@
// @flow
import { ExpandedIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { Link, useLocation } from "react-router-dom";
import {
useMenuState,
MenuButton,
MenuItem as BaseMenuItem,
} from "reakit/Menu";
import styled from "styled-components";
import Flex from "components/Flex";
import MenuIconWrapper from "components/MenuIconWrapper";
import Header from "./Header";
import MenuItem, { MenuAnchor } from "./MenuItem";
import Separator from "./Separator";
import ContextMenu from ".";
import { actionToMenuItem } from "actions";
import useStores from "hooks/useStores";
import type {
MenuItem as TMenuItem,
Action,
ActionContext,
MenuSeparator,
MenuHeading,
} from "types";
type Props = {|
items: TMenuItem[],
actions: (Action | MenuSeparator | MenuHeading)[],
context?: $Shape<ActionContext>,
|};
const Disclosure = styled(ExpandedIcon)`
transform: rotate(270deg);
position: absolute;
right: 8px;
`;
const Submenu = React.forwardRef(({ templateItems, title, ...rest }, ref) => {
const { t } = useTranslation();
const menu = useMenuState({ modal: true });
return (
<>
<MenuButton ref={ref} {...menu} {...rest}>
{(props) => (
<MenuAnchor {...props}>
{title} <Disclosure color="currentColor" />
</MenuAnchor>
)}
</MenuButton>
<ContextMenu {...menu} aria-label={t("Submenu")}>
<Template {...menu} items={templateItems} />
</ContextMenu>
</>
);
});
export function filterTemplateItems(items: TMenuItem[]): TMenuItem[] {
let filtered = items.filter((item) => item.visible !== false);
// this block literally just trims unnecessary separators
filtered = filtered.reduce((acc, item, index) => {
// trim separators from start / end
if (item.type === "separator" && index === 0) return acc;
if (item.type === "separator" && index === filtered.length - 1) return acc;
// trim double separators looking ahead / behind
const prev = filtered[index - 1];
if (prev && prev.type === "separator" && item.type === "separator")
return acc;
// otherwise, continue
return [...acc, item];
}, []);
return filtered;
}
function Template({ items, actions, context, ...menu }: Props): React.Node {
const { t } = useTranslation();
const location = useLocation();
const stores = useStores();
const { ui } = stores;
const ctx = {
t,
isCommandBar: false,
isContextMenu: true,
activeCollectionId: ui.activeCollectionId,
activeDocumentId: ui.activeDocumentId,
location,
stores,
...context,
};
const filteredTemplates = filterTemplateItems(
actions
? actions.map((action) =>
action.type ? action : actionToMenuItem(action, ctx)
)
: items
);
const iconIsPresentInAnyMenuItem = filteredTemplates.find(
(item) => !item.type && !!item.icon
);
return filteredTemplates.map((item, index) => {
if (iconIsPresentInAnyMenuItem && !item.type) {
item.icon = item.icon || <MenuIconWrapper />;
}
if (item.to) {
return (
<MenuItem
as={Link}
to={item.to}
key={index}
disabled={item.disabled}
selected={item.selected}
icon={item.icon}
{...menu}
>
{item.title}
</MenuItem>
);
}
if (item.href) {
return (
<MenuItem
href={item.href}
key={index}
disabled={item.disabled}
selected={item.selected}
level={item.level}
target={item.href.startsWith("#") ? undefined : "_blank"}
icon={item.icon}
{...menu}
>
{item.title}
</MenuItem>
);
}
if (item.onClick) {
return (
<MenuItem
as="button"
onClick={item.onClick}
disabled={item.disabled}
selected={item.selected}
key={index}
icon={item.icon}
{...menu}
>
{item.title}
</MenuItem>
);
}
if (item.items) {
return (
<BaseMenuItem
key={index}
as={Submenu}
templateItems={item.items}
title={<Title title={item.title} icon={item.icon} />}
{...menu}
/>
);
}
if (item.type === "separator") {
return <Separator key={index} />;
}
if (item.type === "heading") {
return <Header>{item.title}</Header>;
}
console.warn("Unrecognized menu item", item);
return null;
});
}
function Title({ title, icon }) {
return (
<Flex align="center">
{icon && <MenuIconWrapper>{icon}</MenuIconWrapper>}
{title}
</Flex>
);
}
export default React.memo<Props>(Template);

View File

@@ -0,0 +1,225 @@
import { ExpandedIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { Link, useLocation } from "react-router-dom";
import {
useMenuState,
MenuButton,
MenuItem as BaseMenuItem,
} from "reakit/Menu";
import styled from "styled-components";
import { $Shape } from "utility-types";
import Flex from "~/components/Flex";
import MenuIconWrapper from "~/components/MenuIconWrapper";
import { actionToMenuItem } from "~/actions";
import useStores from "~/hooks/useStores";
import {
Action,
ActionContext,
MenuSeparator,
MenuHeading,
MenuItem as TMenuItem,
} from "~/types";
import Header from "./Header";
import MenuItem, { MenuAnchor } from "./MenuItem";
import Separator from "./Separator";
import ContextMenu from ".";
type Props = {
actions?: (Action | MenuSeparator | MenuHeading)[];
context?: $Shape<ActionContext>;
items?: TMenuItem[];
};
const Disclosure = styled(ExpandedIcon)`
transform: rotate(270deg);
position: absolute;
right: 8px;
`;
const Submenu = React.forwardRef(
(
{
templateItems,
title,
...rest
}: { templateItems: TMenuItem[]; title: React.ReactNode },
ref: React.LegacyRef<HTMLButtonElement>
) => {
const { t } = useTranslation();
const menu = useMenuState({
modal: true,
});
return (
<>
<MenuButton ref={ref} {...menu} {...rest}>
{(props) => (
<MenuAnchor {...props}>
{title} <Disclosure color="currentColor" />
</MenuAnchor>
)}
</MenuButton>
<ContextMenu {...menu} aria-label={t("Submenu")}>
<Template {...menu} items={templateItems} />
</ContextMenu>
</>
);
}
);
export function filterTemplateItems(items: TMenuItem[]): TMenuItem[] {
let filtered = items.filter((item) => item.visible !== false);
// this block literally just trims unnecessary separators
filtered = filtered.reduce((acc, item, index) => {
// trim separators from start / end
if (item.type === "separator" && index === 0) return acc;
if (item.type === "separator" && index === filtered.length - 1) return acc;
// trim double separators looking ahead / behind
const prev = filtered[index - 1];
if (prev && prev.type === "separator" && item.type === "separator")
return acc;
// otherwise, continue
return [...acc, item];
}, []);
return filtered;
}
function Template({ items, actions, context, ...menu }: Props) {
const { t } = useTranslation();
const location = useLocation();
const stores = useStores();
const { ui } = stores;
const ctx = {
t,
isCommandBar: false,
isContextMenu: true,
activeCollectionId: ui.activeCollectionId,
activeDocumentId: ui.activeDocumentId,
location,
stores,
...context,
};
const templateItems = actions
? actions.map((item) =>
item.type === "separator" || item.type === "heading"
? item
: actionToMenuItem(item, ctx)
)
: items || [];
const filteredTemplates = filterTemplateItems(templateItems);
const iconIsPresentInAnyMenuItem = filteredTemplates.find(
(item) =>
item.type !== "separator" && item.type !== "heading" && !!item.icon
);
return (
<>
{filteredTemplates.map((item, index) => {
if (
iconIsPresentInAnyMenuItem &&
item.type !== "separator" &&
item.type !== "heading"
) {
item.icon = item.icon || <MenuIconWrapper />;
}
if (item.type === "route") {
return (
<MenuItem
as={Link}
to={item.to}
key={index}
disabled={item.disabled}
selected={item.selected}
icon={item.icon}
{...menu}
>
{item.title}
</MenuItem>
);
}
if (item.type === "link") {
return (
<MenuItem
href={item.href}
key={index}
disabled={item.disabled}
selected={item.selected}
level={item.level}
target={item.href.startsWith("#") ? undefined : "_blank"}
icon={item.icon}
{...menu}
>
{item.title}
</MenuItem>
);
}
if (item.type === "button") {
return (
<MenuItem
as="button"
onClick={item.onClick}
disabled={item.disabled}
selected={item.selected}
key={index}
icon={item.icon}
{...menu}
>
{item.title}
</MenuItem>
);
}
if (item.type === "submenu") {
return (
<BaseMenuItem
key={index}
as={Submenu}
templateItems={item.items}
title={<Title title={item.title} icon={item.icon} />}
{...menu}
/>
);
}
if (item.type === "separator") {
return <Separator key={index} />;
}
if (item.type === "heading") {
return <Header>{item.title}</Header>;
}
const _exhaustiveCheck: never = item;
return _exhaustiveCheck;
})}
</>
);
}
function Title({
title,
icon,
}: {
title: React.ReactNode;
icon?: React.ReactNode;
}) {
return (
<Flex align="center">
{icon && <MenuIconWrapper>{icon}</MenuIconWrapper>}
{title}
</Flex>
);
}
export default React.memo<Props>(Template);

View File

@@ -1,31 +1,45 @@
// @flow
import * as React from "react"; import * as React from "react";
import { Portal } from "react-portal"; import { Portal } from "react-portal";
import { Menu } from "reakit/Menu"; import { Menu } from "reakit/Menu";
import styled from "styled-components"; import styled from "styled-components";
import breakpoint from "styled-components-breakpoint"; import breakpoint from "styled-components-breakpoint";
import useMenuHeight from "hooks/useMenuHeight"; import useMenuHeight from "~/hooks/useMenuHeight";
import usePrevious from "hooks/usePrevious"; import usePrevious from "~/hooks/usePrevious";
import { import {
fadeIn, fadeIn,
fadeAndSlideUp, fadeAndSlideUp,
fadeAndSlideDown, fadeAndSlideDown,
mobileContextMenu, mobileContextMenu,
} from "styles/animations"; } from "~/styles/animations";
type Props = {| export type Placement =
"aria-label": string, | "auto-start"
visible?: boolean, | "auto"
placement?: string, | "auto-end"
animating?: boolean, | "top-start"
children: React.Node, | "top"
unstable_disclosureRef?: { | "top-end"
current: null | React.ElementRef<"button">, | "right-start"
}, | "right"
onOpen?: () => void, | "right-end"
onClose?: () => void, | "bottom-end"
hide?: () => void, | "bottom"
|}; | "bottom-start"
| "left-end"
| "left"
| "left-start";
type Props = {
"aria-label": string;
visible?: boolean;
placement?: Placement;
animating?: boolean;
children: React.ReactNode;
unstable_disclosureRef?: React.RefObject<HTMLElement | null>;
onOpen?: () => void;
onClose?: () => void;
hide?: () => void;
};
export default function ContextMenu({ export default function ContextMenu({
children, children,
@@ -43,6 +57,7 @@ export default function ContextMenu({
onOpen(); onOpen();
} }
} }
if (!rest.visible && previousVisible) { if (!rest.visible && previousVisible) {
if (onClose) { if (onClose) {
onClose(); onClose();
@@ -50,6 +65,11 @@ export default function ContextMenu({
} }
}, [onOpen, onClose, previousVisible, rest.visible]); }, [onOpen, onClose, previousVisible, rest.visible]);
// Perf win don't render anything until the menu has been opened
if (!rest.visible && !previousVisible) {
return null;
}
// sets the menu height based on the available space between the disclosure/ // sets the menu height based on the available space between the disclosure/
// trigger and the bottom of the window // trigger and the bottom of the window
return ( return (
@@ -59,7 +79,9 @@ export default function ContextMenu({
// kind of hacky, but this is an effective way of telling which way // kind of hacky, but this is an effective way of telling which way
// the menu will _actually_ be placed when taking into account screen // the menu will _actually_ be placed when taking into account screen
// positioning. // positioning.
// @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'.
const topAnchor = props.style.top === "0"; const topAnchor = props.style.top === "0";
// @ts-expect-error ts-migrate(2339) FIXME: Property 'placement' does not exist on type 'Extra... Remove this comment to see the full error message
const rightAnchor = props.placement === "bottom-end"; const rightAnchor = props.placement === "bottom-end";
return ( return (
@@ -68,8 +90,15 @@ export default function ContextMenu({
dir="auto" dir="auto"
topAnchor={topAnchor} topAnchor={topAnchor}
rightAnchor={rightAnchor} rightAnchor={rightAnchor}
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
ref={backgroundRef} ref={backgroundRef}
style={maxHeight && topAnchor ? { maxHeight } : undefined} style={
maxHeight && topAnchor
? {
maxHeight,
}
: undefined
}
> >
{rest.visible || rest.animating ? children : null} {rest.visible || rest.animating ? children : null}
</Background> </Background>
@@ -115,7 +144,10 @@ export const Position = styled.div`
`}; `};
`; `;
export const Background = styled.div` export const Background = styled.div<{
topAnchor?: boolean;
rightAnchor?: boolean;
}>`
animation: ${mobileContextMenu} 200ms ease; animation: ${mobileContextMenu} 200ms ease;
transform-origin: 50% 100%; transform-origin: 50% 100%;
max-width: 100%; max-width: 100%;
@@ -135,11 +167,11 @@ export const Background = styled.div`
} }
${breakpoint("tablet")` ${breakpoint("tablet")`
animation: ${(props) => animation: ${(props: any) =>
props.topAnchor ? fadeAndSlideDown : fadeAndSlideUp} 200ms ease; props.topAnchor ? fadeAndSlideDown : fadeAndSlideUp} 200ms ease;
transform-origin: ${(props) => (props.rightAnchor ? "75%" : "25%")} 0; transform-origin: ${(props: any) => (props.rightAnchor ? "75%" : "25%")} 0;
max-width: 276px; max-width: 276px;
background: ${(props) => props.theme.menuBackground}; background: ${(props: any) => props.theme.menuBackground};
box-shadow: ${(props) => props.theme.menuShadow}; box-shadow: ${(props: any) => props.theme.menuShadow};
`}; `};
`; `;

View File

@@ -1,24 +1,25 @@
// @flow
import copy from "copy-to-clipboard"; import copy from "copy-to-clipboard";
import * as React from "react"; import * as React from "react";
type Props = { type Props = {
text: string, text: string;
children?: React.Node, children?: React.ReactElement;
onClick?: () => void, onClick?: React.MouseEventHandler<HTMLButtonElement>;
onCopy: () => void, onCopy: () => void;
}; };
class CopyToClipboard extends React.PureComponent<Props> { class CopyToClipboard extends React.PureComponent<Props> {
onClick = (ev: SyntheticEvent<>) => { onClick = (ev: React.SyntheticEvent) => {
const { text, onCopy, children } = this.props; const { text, onCopy, children } = this.props;
const elem = React.Children.only(children); const elem = React.Children.only(children);
copy(text, { copy(text, {
debug: process.env.NODE_ENV !== "production", debug: process.env.NODE_ENV !== "production",
format: "text/plain", format: "text/plain",
}); });
if (onCopy) {
if (onCopy) onCopy(); onCopy();
}
if (elem && elem.props && typeof elem.props.onClick === "function") { if (elem && elem.props && typeof elem.props.onClick === "function") {
elem.props.onClick(ev); elem.props.onClick(ev);
@@ -28,6 +29,10 @@ class CopyToClipboard extends React.PureComponent<Props> {
render() { render() {
const { text: _text, onCopy: _onCopy, children, ...rest } = this.props; const { text: _text, onCopy: _onCopy, children, ...rest } = this.props;
const elem = React.Children.only(children); const elem = React.Children.only(children);
if (!elem) {
return null;
}
return React.cloneElement(elem, { ...rest, onClick: this.onClick }); return React.cloneElement(elem, { ...rest, onClick: this.onClick });
} }
} }

View File

@@ -1,9 +1,8 @@
// @flow
import * as React from "react"; import * as React from "react";
type Props = { type Props = {
delay?: number, delay?: number;
children: React.Node, children: JSX.Element;
}; };
export default function DelayedMount({ delay = 250, children }: Props) { export default function DelayedMount({ delay = 250, children }: Props) {

View File

@@ -1,14 +1,12 @@
// @flow
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import * as React from "react"; import * as React from "react";
import Guide from "components/Guide"; import Guide from "~/components/Guide";
import Modal from "components/Modal"; import Modal from "~/components/Modal";
import useStores from "hooks/useStores"; import useStores from "~/hooks/useStores";
function Dialogs() { function Dialogs() {
const { dialogs } = useStores(); const { dialogs } = useStores();
const { guide, modalStack } = dialogs; const { guide, modalStack } = dialogs;
return ( return (
<> <>
{guide ? ( {guide ? (

View File

@@ -1,4 +1,3 @@
// @flow
import styled from "styled-components"; import styled from "styled-components";
const Divider = styled.hr` const Divider = styled.hr`

View File

@@ -1,4 +1,3 @@
// @flow
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { import {
ArchiveIcon, ArchiveIcon,
@@ -10,19 +9,20 @@ import {
import * as React from "react"; import * as React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import styled from "styled-components"; import styled from "styled-components";
import Document from "models/Document"; import Document from "~/models/Document";
import Breadcrumb from "components/Breadcrumb"; import Breadcrumb, { Crumb } from "~/components/Breadcrumb";
import CollectionIcon from "components/CollectionIcon"; import CollectionIcon from "~/components/CollectionIcon";
import useStores from "hooks/useStores"; import useStores from "~/hooks/useStores";
import { collectionUrl } from "utils/routeHelpers"; import { NavigationNode } from "~/types";
import { collectionUrl } from "~/utils/routeHelpers";
type Props = {| type Props = {
document: Document, document: Document;
children?: React.Node, children?: React.ReactNode;
onlyText: boolean, onlyText?: boolean;
|}; };
function useCategory(document) { function useCategory(document: Document) {
const { t } = useTranslation(); const { t } = useTranslation();
if (document.isDeleted) { if (document.isDeleted) {
@@ -32,6 +32,7 @@ function useCategory(document) {
to: "/trash", to: "/trash",
}; };
} }
if (document.isArchived) { if (document.isArchived) {
return { return {
icon: <ArchiveIcon color="currentColor" />, icon: <ArchiveIcon color="currentColor" />,
@@ -39,6 +40,7 @@ function useCategory(document) {
to: "/archive", to: "/archive",
}; };
} }
if (document.isDraft) { if (document.isDraft) {
return { return {
icon: <EditIcon color="currentColor" />, icon: <EditIcon color="currentColor" />,
@@ -46,6 +48,7 @@ function useCategory(document) {
to: "/drafts", to: "/drafts",
}; };
} }
if (document.isTemplate) { if (document.isTemplate) {
return { return {
icon: <ShapesIcon color="currentColor" />, icon: <ShapesIcon color="currentColor" />,
@@ -53,6 +56,7 @@ function useCategory(document) {
to: "/templates", to: "/templates",
}; };
} }
return null; return null;
} }
@@ -60,49 +64,46 @@ const DocumentBreadcrumb = ({ document, children, onlyText }: Props) => {
const { collections } = useStores(); const { collections } = useStores();
const { t } = useTranslation(); const { t } = useTranslation();
const category = useCategory(document); const category = useCategory(document);
const collection = collections.get(document.collectionId);
let collection = collections.get(document.collectionId); let collectionNode: Crumb;
if (!collection) {
collection = { if (collection) {
id: document.collectionId, collectionNode = {
name: t("Deleted Collection"), title: collection.name,
color: "currentColor", icon: <CollectionIcon collection={collection} expanded />,
url: "deleted-collection", to: collectionUrl(collection.url),
};
} else {
collectionNode = {
title: t("Deleted Collection"),
icon: undefined,
to: collectionUrl("deleted-collection"),
}; };
} }
const path = React.useMemo( const path = React.useMemo(
() => () => collection?.pathToDocument?.(document.id).slice(0, -1) || [],
collection && collection.pathToDocument
? collection.pathToDocument(document.id).slice(0, -1)
: [],
[collection, document.id] [collection, document.id]
); );
const items = React.useMemo(() => { const items = React.useMemo(() => {
let output = []; const output: Crumb[] = [];
if (category) { if (category) {
output.push(category); output.push(category);
} }
if (collection) { output.push(collectionNode);
output.push({
icon: <CollectionIcon collection={collection} expanded />,
title: collection.name,
to: collectionUrl(collection.url),
});
}
path.forEach((p) => { path.forEach((p: NavigationNode) => {
output.push({ output.push({
title: p.title, title: p.title,
to: p.url, to: p.url,
}); });
}); });
return output; return output;
}, [path, category, collection]); }, [path, category, collectionNode]);
if (!collections.isLoaded) { if (!collections.isLoaded) {
return null; return null;
@@ -111,8 +112,8 @@ const DocumentBreadcrumb = ({ document, children, onlyText }: Props) => {
if (onlyText === true) { if (onlyText === true) {
return ( return (
<> <>
{collection.name} {collection?.name}
{path.map((n) => ( {path.map((n: any) => (
<React.Fragment key={n.id}> <React.Fragment key={n.id}>
<SmallSlash /> <SmallSlash />
{n.title} {n.title}

View File

@@ -1,4 +1,3 @@
// @flow
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { CloseIcon } from "outline-icons"; import { CloseIcon } from "outline-icons";
import * as React from "react"; import * as React from "react";
@@ -6,30 +5,34 @@ import { useTranslation } from "react-i18next";
import { useHistory, useRouteMatch } from "react-router-dom"; import { useHistory, useRouteMatch } from "react-router-dom";
import styled from "styled-components"; import styled from "styled-components";
import breakpoint from "styled-components-breakpoint"; import breakpoint from "styled-components-breakpoint";
import Event from "models/Event"; import Event from "~/models/Event";
import Button from "components/Button"; import Button from "~/components/Button";
import Empty from "components/Empty"; import Empty from "~/components/Empty";
import Flex from "components/Flex"; import Flex from "~/components/Flex";
import PaginatedEventList from "components/PaginatedEventList"; import PaginatedEventList from "~/components/PaginatedEventList";
import Scrollable from "components/Scrollable"; import Scrollable from "~/components/Scrollable";
import useStores from "hooks/useStores"; import useStores from "~/hooks/useStores";
import { documentUrl } from "utils/routeHelpers"; import { documentUrl } from "~/utils/routeHelpers";
const EMPTY_ARRAY = []; const EMPTY_ARRAY: Event[] = [];
function DocumentHistory() { function DocumentHistory() {
const { events, documents } = useStores(); const { events, documents } = useStores();
const { t } = useTranslation(); const { t } = useTranslation();
const match = useRouteMatch(); const match = useRouteMatch<{ documentSlug: string }>();
const history = useHistory(); const history = useHistory();
const document = documents.getByUrl(match.params.documentSlug); const document = documents.getByUrl(match.params.documentSlug);
const eventsInDocument = document const eventsInDocument = document
? events.inDocument(document.id) ? events.inDocument(document.id)
: EMPTY_ARRAY; : EMPTY_ARRAY;
const onCloseHistory = () => { const onCloseHistory = () => {
history.push(documentUrl(document)); if (document) {
history.push(documentUrl(document));
} else {
history.goBack();
}
}; };
const items = React.useMemo(() => { const items = React.useMemo(() => {
@@ -39,17 +42,20 @@ function DocumentHistory() {
eventsInDocument[0].createdAt !== document.updatedAt eventsInDocument[0].createdAt !== document.updatedAt
) { ) {
eventsInDocument.unshift( eventsInDocument.unshift(
new Event({ new Event(
name: "documents.latest_version", {
documentId: document.id, name: "documents.latest_version",
createdAt: document.updatedAt, documentId: document.id,
actor: document.updatedBy, createdAt: document.updatedAt,
}) actor: document.updatedBy,
},
events
)
); );
} }
return eventsInDocument; return eventsInDocument;
}, [eventsInDocument, document]); }, [eventsInDocument, events, document]);
return ( return (
<Sidebar> <Sidebar>
@@ -68,7 +74,9 @@ function DocumentHistory() {
<PaginatedEventList <PaginatedEventList
fetch={events.fetchPage} fetch={events.fetchPage}
events={items} events={items}
options={{ documentId: document.id }} options={{
documentId: document.id,
}}
document={document} document={document}
empty={<Empty>{t("Oh weird, there's nothing here")}</Empty>} empty={<Empty>{t("Oh weird, there's nothing here")}</Empty>}
/> />

View File

@@ -1,22 +1,20 @@
// @flow
import ArrowKeyNavigation from "boundless-arrow-key-navigation"; import ArrowKeyNavigation from "boundless-arrow-key-navigation";
import * as React from "react"; import * as React from "react";
import Document from "models/Document"; import Document from "~/models/Document";
import DocumentListItem from "components/DocumentListItem"; import DocumentListItem from "~/components/DocumentListItem";
type Props = {| type Props = {
documents: Document[], documents: Document[];
limit?: number, limit?: number;
showCollection?: boolean, showCollection?: boolean;
showPublished?: boolean, showPublished?: boolean;
showPin?: boolean, showPin?: boolean;
showDraft?: boolean, showDraft?: boolean;
showTemplate?: boolean, showTemplate?: boolean;
|}; };
export default function DocumentList({ limit, documents, ...rest }: Props) { export default function DocumentList({ limit, documents, ...rest }: Props) {
const items = limit ? documents.splice(0, limit) : documents; const items = limit ? documents.splice(0, limit) : documents;
return ( return (
<ArrowKeyNavigation <ArrowKeyNavigation
mode={ArrowKeyNavigation.mode.VERTICAL} mode={ArrowKeyNavigation.mode.VERTICAL}

View File

@@ -1,4 +1,3 @@
// @flow
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { PlusIcon } from "outline-icons"; import { PlusIcon } from "outline-icons";
import * as React from "react"; import * as React from "react";
@@ -6,34 +5,33 @@ import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import styled, { css } from "styled-components"; import styled, { css } from "styled-components";
import breakpoint from "styled-components-breakpoint"; import breakpoint from "styled-components-breakpoint";
import Document from "models/Document"; import Document from "~/models/Document";
import Badge from "components/Badge"; import Badge from "~/components/Badge";
import Button from "components/Button"; import Button from "~/components/Button";
import DocumentMeta from "components/DocumentMeta"; import DocumentMeta from "~/components/DocumentMeta";
import EventBoundary from "components/EventBoundary"; import EventBoundary from "~/components/EventBoundary";
import Flex from "components/Flex"; import Flex from "~/components/Flex";
import Highlight from "components/Highlight"; import Highlight from "~/components/Highlight";
import StarButton, { AnimatedStar } from "components/Star"; import StarButton, { AnimatedStar } from "~/components/Star";
import Tooltip from "components/Tooltip"; import Tooltip from "~/components/Tooltip";
import useBoolean from "hooks/useBoolean"; import useBoolean from "~/hooks/useBoolean";
import useCurrentTeam from "hooks/useCurrentTeam"; import useCurrentTeam from "~/hooks/useCurrentTeam";
import useCurrentUser from "hooks/useCurrentUser"; import useCurrentUser from "~/hooks/useCurrentUser";
import useStores from "hooks/useStores"; import useStores from "~/hooks/useStores";
import DocumentMenu from "menus/DocumentMenu"; import DocumentMenu from "~/menus/DocumentMenu";
import { newDocumentPath } from "utils/routeHelpers"; import { newDocumentPath } from "~/utils/routeHelpers";
type Props = {|
document: Document,
highlight?: ?string,
context?: ?string,
showNestedDocuments?: boolean,
showCollection?: boolean,
showPublished?: boolean,
showPin?: boolean,
showDraft?: boolean,
showTemplate?: boolean,
|};
type Props = {
document: Document;
highlight?: string | undefined;
context?: string | undefined;
showNestedDocuments?: boolean;
showCollection?: boolean;
showPublished?: boolean;
showPin?: boolean;
showDraft?: boolean;
showTemplate?: boolean;
};
const SEARCH_RESULT_REGEX = /<b\b[^>]*>(.*?)<\/b>/gi; const SEARCH_RESULT_REGEX = /<b\b[^>]*>(.*?)<\/b>/gi;
function replaceResultMarks(tag: string) { function replaceResultMarks(tag: string) {
@@ -42,12 +40,16 @@ function replaceResultMarks(tag: string) {
return tag.replace(/<b\b[^>]*>(.*?)<\/b>/gi, "$1"); return tag.replace(/<b\b[^>]*>(.*?)<\/b>/gi, "$1");
} }
function DocumentListItem(props: Props, ref) { function DocumentListItem(
props: Props,
ref: React.RefObject<HTMLAnchorElement>
) {
const { t } = useTranslation(); const { t } = useTranslation();
const { policies } = useStores(); const { policies } = useStores();
const currentUser = useCurrentUser(); const currentUser = useCurrentUser();
const currentTeam = useCurrentTeam(); const currentTeam = useCurrentTeam();
const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean(); const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean();
const { const {
document, document,
showNestedDocuments, showNestedDocuments,
@@ -59,7 +61,6 @@ function DocumentListItem(props: Props, ref) {
highlight, highlight,
context, context,
} = props; } = props;
const queryIsInTitle = const queryIsInTitle =
!!highlight && !!highlight &&
!!document.title.toLowerCase().includes(highlight.toLowerCase()); !!document.title.toLowerCase().includes(highlight.toLowerCase());
@@ -76,7 +77,9 @@ function DocumentListItem(props: Props, ref) {
$menuOpen={menuOpen} $menuOpen={menuOpen}
to={{ to={{
pathname: document.url, pathname: document.url,
state: { title: document.titleWithDefault }, state: {
title: document.titleWithDefault,
},
}} }}
> >
<Content> <Content>
@@ -173,7 +176,10 @@ const Actions = styled(EventBoundary)`
`}; `};
`; `;
const DocumentLink = styled(Link)` const DocumentLink = styled(Link)<{
$isStarred?: boolean;
$menuOpen?: boolean;
}>`
display: flex; display: flex;
align-items: center; align-items: center;
margin: 10px -8px; margin: 10px -8px;
@@ -228,7 +234,7 @@ const DocumentLink = styled(Link)`
`} `}
`; `;
const Heading = styled.h3` const Heading = styled.h3<{ rtl?: boolean }>`
display: flex; display: flex;
justify-content: ${(props) => (props.rtl ? "flex-end" : "flex-start")}; justify-content: ${(props) => (props.rtl ? "flex-end" : "flex-start")};
align-items: center; align-items: center;

View File

@@ -1,18 +1,17 @@
// @flow
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import * as React from "react"; import * as React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import styled from "styled-components"; import styled from "styled-components";
import Document from "models/Document"; import Document from "~/models/Document";
import DocumentBreadcrumb from "components/DocumentBreadcrumb"; import DocumentBreadcrumb from "~/components/DocumentBreadcrumb";
import DocumentTasks from "components/DocumentTasks"; import DocumentTasks from "~/components/DocumentTasks";
import Flex from "components/Flex"; import Flex from "~/components/Flex";
import Time from "components/Time"; import Time from "~/components/Time";
import useCurrentUser from "hooks/useCurrentUser"; import useCurrentUser from "~/hooks/useCurrentUser";
import useStores from "hooks/useStores"; import useStores from "~/hooks/useStores";
const Container = styled(Flex)` const Container = styled(Flex)<{ rtl?: boolean }>`
justify-content: ${(props) => (props.rtl ? "flex-end" : "flex-start")}; justify-content: ${(props) => (props.rtl ? "flex-end" : "flex-start")};
color: ${(props) => props.theme.textTertiary}; color: ${(props) => props.theme.textTertiary};
font-size: 13px; font-size: 13px;
@@ -26,20 +25,20 @@ const Viewed = styled.span`
overflow: hidden; overflow: hidden;
`; `;
const Modified = styled.span` const Modified = styled.span<{ highlight?: boolean }>`
color: ${(props) => props.theme.textTertiary}; color: ${(props) => props.theme.textTertiary};
font-weight: ${(props) => (props.highlight ? "600" : "400")}; font-weight: ${(props) => (props.highlight ? "600" : "400")};
`; `;
type Props = {| type Props = {
showCollection?: boolean, showCollection?: boolean;
showPublished?: boolean, showPublished?: boolean;
showLastViewed?: boolean, showLastViewed?: boolean;
showNestedDocuments?: boolean, showNestedDocuments?: boolean;
document: Document, document: Document;
children: React.Node, children?: React.ReactNode;
to?: string, to?: string;
|}; };
function DocumentMeta({ function DocumentMeta({
showPublished, showPublished,
@@ -54,7 +53,6 @@ function DocumentMeta({
const { t } = useTranslation(); const { t } = useTranslation();
const { collections } = useStores(); const { collections } = useStores();
const user = useCurrentUser(); const user = useCurrentUser();
const { const {
modifiedSinceViewed, modifiedSinceViewed,
updatedAt, updatedAt,
@@ -126,6 +124,7 @@ function DocumentMeta({
if (isDraft || !showLastViewed) { if (isDraft || !showLastViewed) {
return null; return null;
} }
if (!lastViewedAt) { if (!lastViewedAt) {
return ( return (
<Viewed> <Viewed>
@@ -156,7 +155,9 @@ function DocumentMeta({
{showNestedDocuments && nestedDocumentsCount > 0 && ( {showNestedDocuments && nestedDocumentsCount > 0 && (
<span> <span>
&nbsp; {nestedDocumentsCount}{" "} &nbsp; {nestedDocumentsCount}{" "}
{t("nested document", { count: nestedDocumentsCount })} {t("nested document", {
count: nestedDocumentsCount,
})}
</span> </span>
)} )}
&nbsp;{timeSinceNow()} &nbsp;{timeSinceNow()}

View File

@@ -1,21 +1,20 @@
// @flow
import { useObserver } from "mobx-react"; import { useObserver } from "mobx-react";
import * as React from "react"; import * as React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { usePopoverState, PopoverDisclosure } from "reakit/Popover"; import { usePopoverState, PopoverDisclosure } from "reakit/Popover";
import styled from "styled-components"; import styled from "styled-components";
import Document from "models/Document"; import Document from "~/models/Document";
import DocumentMeta from "components/DocumentMeta"; import DocumentMeta from "~/components/DocumentMeta";
import DocumentViews from "components/DocumentViews"; import DocumentViews from "~/components/DocumentViews";
import Popover from "components/Popover"; import Popover from "~/components/Popover";
import useStores from "../hooks/useStores"; import useStores from "~/hooks/useStores";
type Props = {| type Props = {
document: Document, document: Document;
isDraft: boolean, isDraft: boolean;
to?: string, to?: string;
rtl?: boolean, rtl?: boolean;
|}; };
function DocumentMetaWithViews({ to, isDraft, document, ...rest }: Props) { function DocumentMetaWithViews({ to, isDraft, document, ...rest }: Props) {
const { views } = useStores(); const { views } = useStores();
@@ -26,7 +25,9 @@ function DocumentMetaWithViews({ to, isDraft, document, ...rest }: Props) {
React.useEffect(() => { React.useEffect(() => {
if (!document.isDeleted) { if (!document.isDeleted) {
views.fetchPage({ documentId: document.id }); views.fetchPage({
documentId: document.id,
});
} }
}, [views, document.id, document.isDeleted]); }, [views, document.id, document.isDeleted]);
@@ -62,7 +63,7 @@ function DocumentMetaWithViews({ to, isDraft, document, ...rest }: Props) {
); );
} }
const Meta = styled(DocumentMeta)` const Meta = styled(DocumentMeta)<{ rtl?: boolean }>`
justify-content: ${(props) => (props.rtl ? "flex-end" : "flex-start")}; justify-content: ${(props) => (props.rtl ? "flex-end" : "flex-start")};
margin: -12px 0 2em 0; margin: -12px 0 2em 0;
font-size: 14px; font-size: 14px;

View File

@@ -1,22 +1,27 @@
// @flow
import { DoneIcon } from "outline-icons"; import { DoneIcon } from "outline-icons";
import * as React from "react"; import * as React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation, TFunction } from "react-i18next";
import styled, { useTheme } from "styled-components"; import styled, { useTheme } from "styled-components";
import CircularProgressBar from "components/CircularProgressBar"; import Document from "~/models/Document";
import usePrevious from "../hooks/usePrevious"; import CircularProgressBar from "~/components/CircularProgressBar";
import Document from "../models/Document"; import usePrevious from "~/hooks/usePrevious";
import { bounceIn } from "styles/animations"; import { bounceIn } from "~/styles/animations";
type Props = {| type Props = {
document: Document, document: Document;
|}; };
function getMessage(t, total, completed) { function getMessage(t: TFunction, total: number, completed: number): string {
if (completed === 0) { if (completed === 0) {
return t(`{{ total }} task`, { total, count: total }); return t(`{{ total }} task`, {
total,
count: total,
});
} else if (completed === total) { } else if (completed === total) {
return t(`{{ completed }} task done`, { completed, count: completed }); return t(`{{ completed }} task done`, {
completed,
count: completed,
});
} else { } else {
return t(`{{ completed }} of {{ total }} tasks`, { return t(`{{ completed }} of {{ total }} tasks`, {
total, total,
@@ -33,7 +38,6 @@ function DocumentTasks({ document }: Props) {
const done = completed === total; const done = completed === total;
const previousDone = usePrevious(done); const previousDone = usePrevious(done);
const message = getMessage(t, total, completed); const message = getMessage(t, total, completed);
return ( return (
<> <>
{completed === total ? ( {completed === total ? (

View File

@@ -1,31 +1,28 @@
// @flow
import { formatDistanceToNow } from "date-fns"; import { formatDistanceToNow } from "date-fns";
import { sortBy } from "lodash"; import { sortBy } from "lodash";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import * as React from "react"; import * as React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Document from "models/Document"; import Document from "~/models/Document";
import Avatar from "components/Avatar"; import Avatar from "~/components/Avatar";
import ListItem from "components/List/Item"; import ListItem from "~/components/List/Item";
import PaginatedList from "components/PaginatedList"; import PaginatedList from "~/components/PaginatedList";
import useStores from "hooks/useStores"; import useStores from "~/hooks/useStores";
type Props = {| type Props = {
document: Document, document: Document;
isOpen?: boolean, isOpen?: boolean;
|}; };
function DocumentViews({ document, isOpen }: Props) { function DocumentViews({ document, isOpen }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const { views, presence } = useStores(); const { views, presence } = useStores();
const documentPresence = presence.get(document.id);
let documentPresence = presence.get(document.id); const documentPresenceArray = documentPresence
documentPresence = documentPresence
? Array.from(documentPresence.values()) ? Array.from(documentPresence.values())
: []; : [];
const presentIds = documentPresenceArray.map((p) => p.userId);
const presentIds = documentPresence.map((p) => p.userId); const editingIds = documentPresenceArray
const editingIds = documentPresence
.filter((p) => p.isEditing) .filter((p) => p.isEditing)
.map((p) => p.userId); .map((p) => p.userId);
@@ -35,7 +32,6 @@ function DocumentViews({ document, isOpen }: Props) {
documentViews, documentViews,
(view) => !presentIds.includes(view.user.id) (view) => !presentIds.includes(view.user.id)
); );
const users = React.useMemo(() => sortedViews.map((v) => v.user), [ const users = React.useMemo(() => sortedViews.map((v) => v.user), [
sortedViews, sortedViews,
]); ]);
@@ -49,7 +45,6 @@ function DocumentViews({ document, isOpen }: Props) {
const view = documentViews.find((v) => v.user.id === item.id); const view = documentViews.find((v) => v.user.id === item.id);
const isPresent = presentIds.includes(item.id); const isPresent = presentIds.includes(item.id);
const isEditing = editingIds.includes(item.id); const isEditing = editingIds.includes(item.id);
const subtitle = isPresent const subtitle = isPresent
? isEditing ? isEditing
? t("Currently editing") ? t("Currently editing")
@@ -59,7 +54,6 @@ function DocumentViews({ document, isOpen }: Props) {
view ? Date.parse(view.lastViewedAt) : new Date() view ? Date.parse(view.lastViewedAt) : new Date()
), ),
}); });
return ( return (
<ListItem <ListItem
key={item.id} key={item.id}

View File

@@ -1,76 +1,84 @@
// @flow
import { lighten } from "polished"; import { lighten } from "polished";
import * as React from "react"; import * as React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { withRouter, type RouterHistory } from "react-router-dom";
import { Extension } from "rich-markdown-editor"; import { Extension } from "rich-markdown-editor";
import styled, { withTheme } from "styled-components"; import styled, { DefaultTheme, withTheme } from "styled-components";
import embeds from "shared/embeds"; import embeds from "@shared/embeds";
import { light } from "shared/theme"; import { light } from "@shared/theme";
import UiStore from "stores/UiStore"; import UiStore from "~/stores/UiStore";
import ErrorBoundary from "components/ErrorBoundary"; import ErrorBoundary from "~/components/ErrorBoundary";
import Tooltip from "components/Tooltip"; import Tooltip from "~/components/Tooltip";
import useMediaQuery from "hooks/useMediaQuery"; import useMediaQuery from "~/hooks/useMediaQuery";
import useToasts from "hooks/useToasts"; import useToasts from "~/hooks/useToasts";
import { type Theme } from "types"; import history from "~/utils/history";
import { isModKey } from "utils/keyboard"; import { isModKey } from "~/utils/keyboard";
import { uploadFile } from "utils/uploadFile"; import { uploadFile } from "~/utils/uploadFile";
import { isInternalUrl, isHash } from "utils/urls"; import { isInternalUrl, isHash } from "~/utils/urls";
const RichMarkdownEditor = React.lazy(() => const RichMarkdownEditor = React.lazy(
import(/* webpackChunkName: "rich-markdown-editor" */ "rich-markdown-editor") () =>
import(
/* webpackChunkName: "rich-markdown-editor" */
"rich-markdown-editor"
)
); );
// @ts-expect-error ts-migrate(7034) FIXME: Variable 'EMPTY_ARRAY' implicitly has type 'any[]'... Remove this comment to see the full error message
const EMPTY_ARRAY = []; const EMPTY_ARRAY = [];
export type Props = {| export type Props = {
id?: string, id?: string;
value?: string, value?: string;
defaultValue?: string, defaultValue?: string;
readOnly?: boolean, readOnly?: boolean;
grow?: boolean, grow?: boolean;
disableEmbeds?: boolean, disableEmbeds?: boolean;
ui?: UiStore, ui?: UiStore;
style?: Object, style?: React.CSSProperties;
extensions?: Extension[], extensions?: Extension[];
shareId?: ?string, shareId?: string | null | undefined;
autoFocus?: boolean, autoFocus?: boolean;
template?: boolean, template?: boolean;
placeholder?: string, placeholder?: string;
maxLength?: number, maxLength?: number;
scrollTo?: string, scrollTo?: string;
theme?: Theme, theme?: DefaultTheme;
className?: string, className?: string;
handleDOMEvents?: Object, readOnlyWriteCheckboxes?: boolean;
readOnlyWriteCheckboxes?: boolean, onBlur?: () => void;
onBlur?: (event: SyntheticEvent<>) => any, onFocus?: () => void;
onFocus?: (event: SyntheticEvent<>) => any, onPublish?: (event: React.SyntheticEvent) => any;
onPublish?: (event: SyntheticEvent<>) => any, onSave?: (arg0: {
onSave?: ({ done?: boolean, autosave?: boolean, publish?: boolean }) => any, done?: boolean;
onCancel?: () => any, autosave?: boolean;
onDoubleClick?: () => any, publish?: boolean;
onChange?: (getValue: () => string) => any, }) => any;
onSearchLink?: (title: string) => any, onSynced?: () => Promise<void>;
onHoverLink?: (event: MouseEvent) => any, onCancel?: () => any;
onCreateLink?: (title: string) => Promise<string>, onDoubleClick?: () => any;
onImageUploadStart?: () => any, onChange?: (getValue: () => string) => any;
onImageUploadStop?: () => any, onSearchLink?: (title: string) => any;
|}; onHoverLink?: (event: MouseEvent) => any;
onCreateLink?: (title: string) => Promise<string>;
onImageUploadStart?: () => any;
onImageUploadStop?: () => any;
};
type PropsWithRef = Props & { type PropsWithRef = Props & {
forwardedRef: React.Ref<any>, forwardedRef: React.Ref<any>;
history: RouterHistory,
}; };
function Editor(props: PropsWithRef) { function Editor(props: PropsWithRef) {
const { id, shareId, history } = props; const { id, shareId } = props;
const { t } = useTranslation(); const { t } = useTranslation();
const { showToast } = useToasts(); const { showToast } = useToasts();
const isPrinting = useMediaQuery("print"); const isPrinting = useMediaQuery("print");
const onUploadImage = React.useCallback( const onUploadImage = React.useCallback(
async (file: File) => { async (file: File) => {
const result = await uploadFile(file, { documentId: id }); const result = await uploadFile(file, {
documentId: id,
});
return result.url; return result.url;
}, },
[id] [id]
@@ -166,7 +174,9 @@ function Editor(props: PropsWithRef) {
pageBreak: t("Page break"), pageBreak: t("Page break"),
pasteLink: `${t("Paste a link")}`, pasteLink: `${t("Paste a link")}`,
pasteLinkWithTitle: (service: string) => pasteLinkWithTitle: (service: string) =>
t("Paste a {{service}} link…", { service }), t("Paste a {{service}} link…", {
service,
}),
placeholder: t("Placeholder"), placeholder: t("Placeholder"),
quote: t("Quote"), quote: t("Quote"),
removeLink: t("Remove link"), removeLink: t("Remove link"),
@@ -189,17 +199,20 @@ function Editor(props: PropsWithRef) {
uploadImage={onUploadImage} uploadImage={onUploadImage}
onClickLink={onClickLink} onClickLink={onClickLink}
onShowToast={onShowToast} onShowToast={onShowToast}
// @ts-expect-error ts-migrate(7005) FIXME: Variable 'EMPTY_ARRAY' implicitly has an 'any[]' t... Remove this comment to see the full error message
embeds={props.disableEmbeds ? EMPTY_ARRAY : embeds} embeds={props.disableEmbeds ? EMPTY_ARRAY : embeds}
tooltip={EditorTooltip} tooltip={EditorTooltip}
dictionary={dictionary} dictionary={dictionary}
{...props} {...props}
placeholder={props.placeholder || ""}
defaultValue={props.defaultValue || ""}
theme={isPrinting ? light : props.theme} theme={isPrinting ? light : props.theme}
/> />
</ErrorBoundary> </ErrorBoundary>
); );
} }
const StyledEditor = styled(RichMarkdownEditor)` const StyledEditor = styled(RichMarkdownEditor)<{ grow?: boolean }>`
flex-grow: ${(props) => (props.grow ? 1 : 0)}; flex-grow: ${(props) => (props.grow ? 1 : 0)};
justify-content: start; justify-content: start;
@@ -307,7 +320,9 @@ const StyledEditor = styled(RichMarkdownEditor)`
} }
`; `;
// @ts-expect-error ts-migrate(7031) FIXME: Binding element 'children' implicitly has an 'any'... Remove this comment to see the full error message
const EditorTooltip = ({ children, ...props }) => ( const EditorTooltip = ({ children, ...props }) => (
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
<Tooltip offset="0, 16" delay={150} {...props}> <Tooltip offset="0, 16" delay={150} {...props}>
<Span>{children}</Span> <Span>{children}</Span>
</Tooltip> </Tooltip>
@@ -317,8 +332,8 @@ const Span = styled.span`
outline: none; outline: none;
`; `;
const EditorWithRouterAndTheme = withRouter(withTheme(Editor)); const EditorWithTheme = withTheme(Editor);
export default React.forwardRef<Props, typeof Editor>((props, ref) => ( export default React.forwardRef<typeof Editor, Props>((props, ref) => (
<EditorWithRouterAndTheme {...props} forwardedRef={ref} /> <EditorWithTheme {...props} forwardedRef={ref} />
)); ));

View File

@@ -1,4 +1,3 @@
// @flow
import styled from "styled-components"; import styled from "styled-components";
const Empty = styled.p` const Empty = styled.p`

View File

@@ -1,29 +1,30 @@
// @flow
import * as Sentry from "@sentry/react"; import * as Sentry from "@sentry/react";
import { observable } from "mobx"; import { observable } from "mobx";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import * as React from "react"; import * as React from "react";
import { withTranslation, type TFunction, Trans } from "react-i18next"; import { withTranslation, Trans, WithTranslation } from "react-i18next";
import styled from "styled-components"; import styled from "styled-components";
import Button from "components/Button"; import { githubIssuesUrl } from "@shared/utils/routeHelpers";
import CenteredContent from "components/CenteredContent"; import Button from "~/components/Button";
import HelpText from "components/HelpText"; import CenteredContent from "~/components/CenteredContent";
import PageTitle from "components/PageTitle"; import HelpText from "~/components/HelpText";
import { githubIssuesUrl } from "../../shared/utils/routeHelpers"; import PageTitle from "~/components/PageTitle";
import env from "env"; import env from "~/env";
type Props = {| type Props = WithTranslation & {
children: React.Node, children: React.ReactNode;
reloadOnChunkMissing?: boolean, reloadOnChunkMissing?: boolean;
t: TFunction, };
|};
@observer @observer
class ErrorBoundary extends React.Component<Props> { class ErrorBoundary extends React.Component<Props> {
@observable error: ?Error; @observable
@observable showDetails: boolean = false; error: Error | null | undefined;
componentDidCatch(error: Error, info: Object) { @observable
showDetails = false;
componentDidCatch(error: Error) {
this.error = error; this.error = error;
console.error(error); console.error(error);
@@ -35,7 +36,7 @@ class ErrorBoundary extends React.Component<Props> {
// If the editor bundle fails to load then reload the entire window. This // If the editor bundle fails to load then reload the entire window. This
// can happen if a deploy happens between the user loading the initial JS // can happen if a deploy happens between the user loading the initial JS
// bundle and the async-loaded editor JS bundle as the hash will change. // bundle and the async-loaded editor JS bundle as the hash will change.
window.location.reload(true); window.location.reload();
return; return;
} }
@@ -45,7 +46,7 @@ class ErrorBoundary extends React.Component<Props> {
} }
handleReload = () => { handleReload = () => {
window.location.reload(true); window.location.reload();
}; };
handleShowDetails = () => { handleShowDetails = () => {
@@ -79,9 +80,7 @@ class ErrorBoundary extends React.Component<Props> {
</Trans> </Trans>
</HelpText> </HelpText>
<p> <p>
<Button onClick={this.handleReload}> <Button onClick={this.handleReload}>{t("Reload")}</Button>
<Trans>Reload</Trans>
</Button>
</p> </p>
</CenteredContent> </CenteredContent>
); );
@@ -105,9 +104,7 @@ class ErrorBoundary extends React.Component<Props> {
</HelpText> </HelpText>
{this.showDetails && <Pre>{error.toString()}</Pre>} {this.showDetails && <Pre>{error.toString()}</Pre>}
<p> <p>
<Button onClick={this.handleReload}> <Button onClick={this.handleReload}>{t("Reload")}</Button>{" "}
<Trans>Reload</Trans>
</Button>{" "}
{this.showDetails ? ( {this.showDetails ? (
<Button onClick={this.handleReportBug} neutral> <Button onClick={this.handleReportBug} neutral>
<Trans>Report a Bug</Trans> <Trans>Report a Bug</Trans>
@@ -121,6 +118,7 @@ class ErrorBoundary extends React.Component<Props> {
</CenteredContent> </CenteredContent>
); );
} }
return this.props.children; return this.props.children;
} }
} }
@@ -133,4 +131,4 @@ const Pre = styled.pre`
white-space: pre-wrap; white-space: pre-wrap;
`; `;
export default withTranslation()<ErrorBoundary>(ErrorBoundary); export default withTranslation()(ErrorBoundary);

View File

@@ -1,14 +1,12 @@
// @flow
import * as React from "react"; import * as React from "react";
type Props = { type Props = {
children: React.Node, children: React.ReactNode;
className?: string, className?: string;
}; };
export default function EventBoundary({ children, className }: Props) { export default function EventBoundary({ children, className }: Props) {
const handleClick = React.useCallback((event: SyntheticEvent<>) => { const handleClick = React.useCallback((event: React.SyntheticEvent) => {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
}, []); }, []);

View File

@@ -1,4 +1,3 @@
// @flow
import { import {
TrashIcon, TrashIcon,
ArchiveIcon, ArchiveIcon,
@@ -10,23 +9,25 @@ import {
import * as React from "react"; import * as React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import styled from "styled-components"; import styled from "styled-components";
import Document from "models/Document"; import Document from "~/models/Document";
import Event from "models/Event"; import Event from "~/models/Event";
import Avatar from "components/Avatar"; import Avatar from "~/components/Avatar";
import Item, { Actions } from "components/List/Item"; import Item, { Actions } from "~/components/List/Item";
import Time from "components/Time"; import Time from "~/components/Time";
import RevisionMenu from "menus/RevisionMenu"; import RevisionMenu from "~/menus/RevisionMenu";
import { documentHistoryUrl } from "utils/routeHelpers"; import { documentHistoryUrl } from "~/utils/routeHelpers";
type Props = {| type Props = {
document: Document, document: Document;
event: Event, event: Event;
latest?: boolean, latest?: boolean;
|}; };
const EventListItem = ({ event, latest, document }: Props) => { const EventListItem = ({ event, latest, document }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const opts = { userName: event.actor.name }; const opts = {
userName: event.actor.name,
};
const isRevision = event.name === "revisions.create"; const isRevision = event.name === "revisions.create";
let meta, icon, to; let meta, icon, to;
@@ -45,28 +46,35 @@ const EventListItem = ({ event, latest, document }: Props) => {
break; break;
} }
} }
case "documents.archive": case "documents.archive":
icon = <ArchiveIcon color="currentColor" size={16} />; icon = <ArchiveIcon color="currentColor" size={16} />;
meta = t("{{userName}} archived", opts); meta = t("{{userName}} archived", opts);
break; break;
case "documents.unarchive": case "documents.unarchive":
meta = t("{{userName}} restored", opts); meta = t("{{userName}} restored", opts);
break; break;
case "documents.delete": case "documents.delete":
icon = <TrashIcon color="currentColor" size={16} />; icon = <TrashIcon color="currentColor" size={16} />;
meta = t("{{userName}} deleted", opts); meta = t("{{userName}} deleted", opts);
break; break;
case "documents.restore": case "documents.restore":
meta = t("{{userName}} moved from trash", opts); meta = t("{{userName}} moved from trash", opts);
break; break;
case "documents.publish": case "documents.publish":
icon = <PublishIcon color="currentColor" size={16} />; icon = <PublishIcon color="currentColor" size={16} />;
meta = t("{{userName}} published", opts); meta = t("{{userName}} published", opts);
break; break;
case "documents.move": case "documents.move":
icon = <MoveIcon color="currentColor" size={16} />; icon = <MoveIcon color="currentColor" size={16} />;
meta = t("{{userName}} moved", opts); meta = t("{{userName}} moved", opts);
break; break;
default: default:
console.warn("Unhandled event: ", event.name); console.warn("Unhandled event: ", event.name);
} }
@@ -78,7 +86,6 @@ const EventListItem = ({ event, latest, document }: Props) => {
return ( return (
<ListItem <ListItem
small small
exact
to={to} to={to}
title={ title={
<Time <Time
@@ -97,7 +104,7 @@ const EventListItem = ({ event, latest, document }: Props) => {
</Subtitle> </Subtitle>
} }
actions={ actions={
isRevision ? ( isRevision && event.modelId ? (
<RevisionMenu document={document} revisionId={event.modelId} /> <RevisionMenu document={document} revisionId={event.modelId} />
) : undefined ) : undefined
} }

View File

@@ -1,22 +1,21 @@
// @flow
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import * as React from "react"; import * as React from "react";
import styled from "styled-components"; import styled from "styled-components";
import User from "models/User"; import User from "~/models/User";
import Avatar from "components/Avatar"; import Avatar from "~/components/Avatar";
import Flex from "components/Flex"; import Flex from "~/components/Flex";
type Props = {| type Props = {
users: User[], users: User[];
size?: number, size?: number;
overflow: number, overflow?: number;
onClick?: (event: SyntheticEvent<>) => mixed, onClick?: React.MouseEventHandler<HTMLDivElement>;
renderAvatar?: (user: User) => React.Node, renderAvatar?: (user: User) => React.ReactNode;
|}; };
function Facepile({ function Facepile({
users, users,
overflow, overflow = 0,
size = 32, size = 32,
renderAvatar = DefaultAvatar, renderAvatar = DefaultAvatar,
...rest ...rest
@@ -47,7 +46,7 @@ const AvatarWrapper = styled.div`
} }
`; `;
const More = styled.div` const More = styled.div<{ size: number }>`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;

View File

@@ -1,8 +1,7 @@
// @flow
import styled from "styled-components"; import styled from "styled-components";
import { fadeIn } from "styles/animations"; import { fadeIn } from "~/styles/animations";
const Fade = styled.span` const Fade = styled.span<{ timing?: number | string }>`
animation: ${fadeIn} ${(props) => props.timing || "250ms"} ease-in-out; animation: ${fadeIn} ${(props) => props.timing || "250ms"} ease-in-out;
`; `;

View File

@@ -1,51 +1,51 @@
// @flow
import { find } from "lodash"; import { find } from "lodash";
import * as React from "react"; import * as React from "react";
import { useMenuState, MenuButton } from "reakit/Menu"; import { useMenuState, MenuButton } from "reakit/Menu";
import styled from "styled-components"; import styled from "styled-components";
import Button, { Inner } from "components/Button"; import Button, { Inner } from "~/components/Button";
import ContextMenu from "components/ContextMenu"; import ContextMenu from "~/components/ContextMenu";
import MenuItem from "components/ContextMenu/MenuItem"; import MenuItem from "~/components/ContextMenu/MenuItem";
import HelpText from "components/HelpText"; import HelpText from "~/components/HelpText";
type TFilterOption = {| type TFilterOption = {
key: string, key: string;
label: string, label: string;
note?: string, note?: string;
|}; };
type Props = {| type Props = {
options: TFilterOption[], options: TFilterOption[];
activeKey: ?string, activeKey: string | null | undefined;
defaultLabel?: string, defaultLabel?: string;
selectedPrefix?: string, selectedPrefix?: string;
className?: string, className?: string;
onSelect: (key: ?string) => void, onSelect: (key: string | null | undefined) => void;
|}; };
const FilterOptions = ({ const FilterOptions = ({
options, options,
activeKey = "", activeKey = "",
defaultLabel, defaultLabel = "Filter options",
selectedPrefix = "", selectedPrefix = "",
className, className,
onSelect, onSelect,
}: Props) => { }: Props) => {
const menu = useMenuState({ modal: true }); const menu = useMenuState({
const selected = find(options, { key: activeKey }) || options[0]; modal: true,
});
const selected =
find(options, {
key: activeKey,
}) || options[0];
// @ts-expect-error ts-migrate(2339) FIXME: Property 'label' does not exist on type 'number | ... Remove this comment to see the full error message
const selectedLabel = selected ? `${selectedPrefix} ${selected.label}` : ""; const selectedLabel = selected ? `${selectedPrefix} ${selected.label}` : "";
return ( return (
<Wrapper> <Wrapper>
<MenuButton {...menu}> <MenuButton {...menu}>
{(props) => ( {(props) => (
<StyledButton <StyledButton {...props} className={className} neutral disclosure>
{...props}
className={className}
neutral
disclosure
small
>
{activeKey ? selectedLabel : defaultLabel} {activeKey ? selectedLabel : defaultLabel}
</StyledButton> </StyledButton>
)} )}

View File

@@ -1,5 +1,3 @@
// @flow
import * as React from "react";
import styled from "styled-components"; import styled from "styled-components";
type JustifyValues = type JustifyValues =
@@ -16,29 +14,14 @@ type AlignValues =
| "flex-start" | "flex-start"
| "flex-end"; | "flex-end";
type Props = {| const Flex = styled.div<{
column?: ?boolean, auto?: boolean;
shrink?: ?boolean, column?: boolean;
align?: AlignValues, align?: AlignValues;
justify?: JustifyValues, justify?: JustifyValues;
auto?: ?boolean, shrink?: boolean;
className?: string, gap?: number;
children?: React.Node, }>`
role?: string,
gap?: number,
|};
const Flex = React.forwardRef<Props, HTMLDivElement>((props: Props, ref) => {
const { children, ...restProps } = props;
return (
<Container ref={ref} {...restProps}>
{children}
</Container>
);
});
const Container = styled.div`
display: flex; display: flex;
flex: ${({ auto }) => (auto ? "1 1 auto" : "initial")}; flex: ${({ auto }) => (auto ? "1 1 auto" : "initial")};
flex-direction: ${({ column }) => (column ? "column" : "row")}; flex-direction: ${({ column }) => (column ? "column" : "row")};

View File

@@ -1,9 +1,8 @@
// @flow
import * as React from "react"; import * as React from "react";
import styled from "styled-components"; import styled from "styled-components";
import Empty from "components/Empty"; import Empty from "~/components/Empty";
import Fade from "components/Fade"; import Fade from "~/components/Fade";
import Flex from "components/Flex"; import Flex from "~/components/Flex";
export default function FullscreenLoading() { export default function FullscreenLoading() {
return ( return (

View File

@@ -1,10 +1,9 @@
// @flow
import * as React from "react"; import * as React from "react";
type Props = { type Props = {
size?: number, size?: number;
fill?: string, fill?: string;
className?: string, className?: string;
}; };
function GithubLogo({ size = 34, fill = "#FFF", className }: Props) { function GithubLogo({ size = 34, fill = "#FFF", className }: Props) {

View File

@@ -1,31 +1,31 @@
// @flow
import { observable } from "mobx"; import { observable } from "mobx";
import { observer, inject } from "mobx-react"; import { observer } from "mobx-react";
import { GroupIcon } from "outline-icons"; import { GroupIcon } from "outline-icons";
import * as React from "react"; import * as React from "react";
import styled from "styled-components"; import styled from "styled-components";
import { MAX_AVATAR_DISPLAY } from "shared/constants"; import { MAX_AVATAR_DISPLAY } from "@shared/constants";
import GroupMembershipsStore from "stores/GroupMembershipsStore"; import RootStore from "~/stores/RootStore";
import CollectionGroupMembership from "models/CollectionGroupMembership"; import CollectionGroupMembership from "~/models/CollectionGroupMembership";
import Group from "models/Group"; import Group from "~/models/Group";
import GroupMembers from "scenes/GroupMembers"; import GroupMembers from "~/scenes/GroupMembers";
import Facepile from "components/Facepile"; import Facepile from "~/components/Facepile";
import Flex from "components/Flex"; import Flex from "~/components/Flex";
import ListItem from "components/List/Item"; import ListItem from "~/components/List/Item";
import Modal from "components/Modal"; import Modal from "~/components/Modal";
import withStores from "~/components/withStores";
type Props = { type Props = RootStore & {
group: Group, group: Group;
groupMemberships: GroupMembershipsStore, membership?: CollectionGroupMembership;
membership?: CollectionGroupMembership, showFacepile?: boolean;
showFacepile?: boolean, showAvatar?: boolean;
showAvatar?: boolean, renderActions: (arg0: { openMembersModal: () => void }) => React.ReactNode;
renderActions: ({ openMembersModal: () => void }) => React.Node,
}; };
@observer @observer
class GroupListItem extends React.Component<Props> { class GroupListItem extends React.Component<Props> {
@observable membersModalOpen: boolean = false; @observable
membersModalOpen = false;
handleMembersModalOpen = () => { handleMembersModalOpen = () => {
this.membersModalOpen = true; this.membersModalOpen = true;
@@ -37,14 +37,12 @@ class GroupListItem extends React.Component<Props> {
render() { render() {
const { group, groupMemberships, showFacepile, renderActions } = this.props; const { group, groupMemberships, showFacepile, renderActions } = this.props;
const memberCount = group.memberCount; const memberCount = group.memberCount;
const membershipsInGroup = groupMemberships.inGroup(group.id); const membershipsInGroup = groupMemberships.inGroup(group.id);
const users = membershipsInGroup const users = membershipsInGroup
.slice(0, MAX_AVATAR_DISPLAY) .slice(0, MAX_AVATAR_DISPLAY)
// @ts-expect-error ts-migrate(2339) FIXME: Property 'user' does not exist on type 'GroupMembe... Remove this comment to see the full error message
.map((gm) => gm.user); .map((gm) => gm.user);
const overflow = memberCount - users.length; const overflow = memberCount - users.length;
return ( return (
@@ -84,7 +82,7 @@ class GroupListItem extends React.Component<Props> {
onRequestClose={this.handleMembersModalClose} onRequestClose={this.handleMembersModalClose}
isOpen={this.membersModalOpen} isOpen={this.membersModalOpen}
> >
<GroupMembers group={group} onSubmit={this.handleMembersModalClose} /> <GroupMembers group={group} />
</Modal> </Modal>
</> </>
); );
@@ -107,4 +105,4 @@ const Title = styled.span`
} }
`; `;
export default inject("groupMemberships")(GroupListItem); export default withStores(GroupListItem);

View File

@@ -1,17 +1,16 @@
// @flow
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import * as React from "react"; import * as React from "react";
import { Dialog, DialogBackdrop, useDialogState } from "reakit/Dialog"; import { Dialog, DialogBackdrop, useDialogState } from "reakit/Dialog";
import styled from "styled-components"; import styled from "styled-components";
import Scrollable from "components/Scrollable"; import Scrollable from "~/components/Scrollable";
import usePrevious from "hooks/usePrevious"; import usePrevious from "~/hooks/usePrevious";
type Props = {| type Props = {
children?: React.Node, children?: React.ReactNode;
isOpen: boolean, isOpen: boolean;
title?: string, title?: string;
onRequestClose: () => void, onRequestClose: () => void;
|}; };
const Guide = ({ const Guide = ({
children, children,
@@ -20,13 +19,16 @@ const Guide = ({
onRequestClose, onRequestClose,
...rest ...rest
}: Props) => { }: Props) => {
const dialog = useDialogState({ animated: 250 }); const dialog = useDialogState({
animated: 250,
});
const wasOpen = usePrevious(isOpen); const wasOpen = usePrevious(isOpen);
React.useEffect(() => { React.useEffect(() => {
if (!wasOpen && isOpen) { if (!wasOpen && isOpen) {
dialog.show(); dialog.show();
} }
if (wasOpen && !isOpen) { if (wasOpen && !isOpen) {
dialog.hide(); dialog.hide();
} }

View File

@@ -1,22 +1,20 @@
// @flow
import { throttle } from "lodash"; import { throttle } from "lodash";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { transparentize } from "polished"; import { transparentize } from "polished";
import * as React from "react"; import * as React from "react";
import styled from "styled-components"; import styled from "styled-components";
import breakpoint from "styled-components-breakpoint"; import breakpoint from "styled-components-breakpoint";
import Fade from "components/Fade"; import Fade from "~/components/Fade";
import Flex from "components/Flex"; import Flex from "~/components/Flex";
type Props = {| type Props = {
breadcrumb?: React.Node, breadcrumb?: React.ReactNode;
title: React.Node, title: React.ReactNode;
actions?: React.Node, actions?: React.ReactNode;
|}; };
function Header({ breadcrumb, title, actions }: Props) { function Header({ breadcrumb, title, actions }: Props) {
const [isScrolled, setScrolled] = React.useState(false); const [isScrolled, setScrolled] = React.useState(false);
const handleScroll = React.useCallback( const handleScroll = React.useCallback(
throttle(() => setScrolled(window.scrollY > 75), 50), throttle(() => setScrolled(window.scrollY > 75), 50),
[] []
@@ -24,7 +22,6 @@ function Header({ breadcrumb, title, actions }: Props) {
React.useEffect(() => { React.useEffect(() => {
window.addEventListener("scroll", handleScroll); window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll); return () => window.removeEventListener("scroll", handleScroll);
}, [handleScroll]); }, [handleScroll]);
@@ -39,7 +36,7 @@ function Header({ breadcrumb, title, actions }: Props) {
<Wrapper align="center" shrink={false}> <Wrapper align="center" shrink={false}>
{breadcrumb ? <Breadcrumbs>{breadcrumb}</Breadcrumbs> : null} {breadcrumb ? <Breadcrumbs>{breadcrumb}</Breadcrumbs> : null}
{isScrolled ? ( {isScrolled ? (
<Title align="center" justify="flex-start" onClick={handleClickTitle}> <Title onClick={handleClickTitle}>
<Fade>{title}</Fade> <Fade>{title}</Fade>
</Title> </Title>
) : ( ) : (

View File

@@ -1,7 +1,6 @@
// @flow
import styled from "styled-components"; import styled from "styled-components";
const Heading = styled.h1` const Heading = styled.h1<{ centered?: boolean }>`
display: flex; display: flex;
align-items: center; align-items: center;
${(props) => (props.centered ? "text-align: center;" : "")} ${(props) => (props.centered ? "text-align: center;" : "")}

View File

@@ -1,7 +1,6 @@
// @flow
import styled from "styled-components"; import styled from "styled-components";
const HelpText = styled.p` const HelpText = styled.p<{ small?: boolean }>`
margin-top: 0; margin-top: 0;
color: ${(props) => props.theme.textSecondary}; color: ${(props) => props.theme.textSecondary};
font-size: ${(props) => (props.small ? "13px" : "inherit")}; font-size: ${(props) => (props.small ? "13px" : "inherit")};

View File

@@ -1,24 +1,24 @@
// @flow
import * as React from "react"; import * as React from "react";
import replace from "string-replace-to-array"; import replace from "string-replace-to-array";
import styled from "styled-components"; import styled from "styled-components";
type Props = { type Props = React.HTMLAttributes<HTMLSpanElement> & {
highlight: ?string | RegExp, highlight: (string | null | undefined) | RegExp;
processResult?: (tag: string) => string, processResult?: (tag: string) => string;
text: string, text: string | undefined;
caseSensitive?: boolean, caseSensitive?: boolean;
}; };
function Highlight({ function Highlight({
highlight, highlight,
processResult, processResult,
caseSensitive, caseSensitive,
text, text = "",
...rest ...rest
}: Props) { }: Props) {
let regex; let regex;
let index = 0; let index = 0;
if (highlight instanceof RegExp) { if (highlight instanceof RegExp) {
regex = highlight; regex = highlight;
} else { } else {
@@ -27,10 +27,11 @@ function Highlight({
caseSensitive ? "g" : "gi" caseSensitive ? "g" : "gi"
); );
} }
return ( return (
<span {...rest}> <span {...rest}>
{highlight {highlight
? replace(text, regex, (tag) => ( ? replace(text, regex, (tag: string) => (
<Mark key={index++}> <Mark key={index++}>
{processResult ? processResult(tag) : tag} {processResult ? processResult(tag) : tag}
</Mark> </Mark>

View File

@@ -1,32 +1,29 @@
// @flow
import { inject } from "mobx-react";
import { transparentize } from "polished"; import { transparentize } from "polished";
import * as React from "react"; import * as React from "react";
import { Portal } from "react-portal"; import { Portal } from "react-portal";
import styled from "styled-components"; import styled from "styled-components";
import parseDocumentSlug from "shared/utils/parseDocumentSlug"; import parseDocumentSlug from "@shared/utils/parseDocumentSlug";
import DocumentsStore from "stores/DocumentsStore"; import HoverPreviewDocument from "~/components/HoverPreviewDocument";
import HoverPreviewDocument from "components/HoverPreviewDocument"; import useStores from "~/hooks/useStores";
import { fadeAndSlideDown } from "styles/animations"; import { fadeAndSlideDown } from "~/styles/animations";
import { isInternalUrl } from "utils/urls"; import { isInternalUrl } from "~/utils/urls";
const DELAY_OPEN = 300; const DELAY_OPEN = 300;
const DELAY_CLOSE = 300; const DELAY_CLOSE = 300;
type Props = { type Props = {
node: HTMLAnchorElement, node: HTMLAnchorElement;
event: MouseEvent, event: MouseEvent;
documents: DocumentsStore, onClose: () => void;
onClose: () => void,
}; };
function HoverPreviewInternal({ node, documents, onClose, event }: Props) { function HoverPreviewInternal({ node, onClose }: Props) {
const { documents } = useStores();
const slug = parseDocumentSlug(node.href); const slug = parseDocumentSlug(node.href);
const [isVisible, setVisible] = React.useState(false); const [isVisible, setVisible] = React.useState(false);
const timerClose = React.useRef(); const timerClose = React.useRef<ReturnType<typeof setTimeout>>();
const timerOpen = React.useRef(); const timerOpen = React.useRef<ReturnType<typeof setTimeout>>();
const cardRef = React.useRef<?HTMLDivElement>(); const cardRef = React.useRef<HTMLDivElement>(null);
const startCloseTimer = () => { const startCloseTimer = () => {
stopOpenTimer(); stopOpenTimer();
@@ -54,9 +51,7 @@ function HoverPreviewInternal({ node, documents, onClose, event }: Props) {
React.useEffect(() => { React.useEffect(() => {
if (slug) { if (slug) {
documents.prefetchDocument(slug, { documents.prefetchDocument(slug);
prefetch: true,
});
} }
startOpenTimer(); startOpenTimer();
@@ -64,6 +59,7 @@ function HoverPreviewInternal({ node, documents, onClose, event }: Props) {
if (cardRef.current) { if (cardRef.current) {
cardRef.current.addEventListener("mouseenter", stopCloseTimer); cardRef.current.addEventListener("mouseenter", stopCloseTimer);
} }
if (cardRef.current) { if (cardRef.current) {
cardRef.current.addEventListener("mouseleave", startCloseTimer); cardRef.current.addEventListener("mouseleave", startCloseTimer);
} }
@@ -71,7 +67,6 @@ function HoverPreviewInternal({ node, documents, onClose, event }: Props) {
node.addEventListener("mouseout", startCloseTimer); node.addEventListener("mouseout", startCloseTimer);
node.addEventListener("mouseover", stopCloseTimer); node.addEventListener("mouseover", stopCloseTimer);
node.addEventListener("mouseover", startOpenTimer); node.addEventListener("mouseover", startOpenTimer);
return () => { return () => {
node.removeEventListener("mouseout", startCloseTimer); node.removeEventListener("mouseout", startCloseTimer);
node.removeEventListener("mouseover", stopCloseTimer); node.removeEventListener("mouseover", stopCloseTimer);
@@ -80,6 +75,7 @@ function HoverPreviewInternal({ node, documents, onClose, event }: Props) {
if (cardRef.current) { if (cardRef.current) {
cardRef.current.removeEventListener("mouseenter", stopCloseTimer); cardRef.current.removeEventListener("mouseenter", stopCloseTimer);
} }
if (cardRef.current) { if (cardRef.current) {
cardRef.current.removeEventListener("mouseleave", startCloseTimer); cardRef.current.removeEventListener("mouseleave", startCloseTimer);
} }
@@ -88,12 +84,10 @@ function HoverPreviewInternal({ node, documents, onClose, event }: Props) {
clearTimeout(timerClose.current); clearTimeout(timerClose.current);
} }
}; };
}, [node]); }, [node, slug]);
const anchorBounds = node.getBoundingClientRect(); const anchorBounds = node.getBoundingClientRect();
const cardBounds = cardRef.current const cardBounds = cardRef.current?.getBoundingClientRect();
? cardRef.current.getBoundingClientRect()
: undefined;
const left = cardBounds const left = cardBounds
? Math.min(anchorBounds.left, window.innerWidth - 16 - 350) ? Math.min(anchorBounds.left, window.innerWidth - 16 - 350)
: anchorBounds.left; : anchorBounds.left;
@@ -108,7 +102,7 @@ function HoverPreviewInternal({ node, documents, onClose, event }: Props) {
> >
<div ref={cardRef}> <div ref={cardRef}>
<HoverPreviewDocument url={node.href}> <HoverPreviewDocument url={node.href}>
{(content) => {(content: React.ReactNode) =>
isVisible ? ( isVisible ? (
<Animate> <Animate>
<Card> <Card>
@@ -196,7 +190,7 @@ const Card = styled.div`
} }
`; `;
const Position = styled.div` const Position = styled.div<{ fixed?: boolean; top?: number; left?: number }>`
margin-top: 10px; margin-top: 10px;
position: ${({ fixed }) => (fixed ? "fixed" : "absolute")}; position: ${({ fixed }) => (fixed ? "fixed" : "absolute")};
z-index: ${(props) => props.theme.depths.hoverPreview}; z-index: ${(props) => props.theme.depths.hoverPreview};
@@ -207,7 +201,7 @@ const Position = styled.div`
${({ left }) => (left !== undefined ? `left: ${left}px` : "")}; ${({ left }) => (left !== undefined ? `left: ${left}px` : "")};
`; `;
const Pointer = styled.div` const Pointer = styled.div<{ offset: number }>`
top: -22px; top: -22px;
left: ${(props) => props.offset}px; left: ${(props) => props.offset}px;
width: 22px; width: 22px;
@@ -238,4 +232,4 @@ const Pointer = styled.div`
} }
`; `;
export default inject("documents")(HoverPreview); export default HoverPreview;

View File

@@ -1,25 +1,24 @@
// @flow
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import * as React from "react"; import * as React from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import styled from "styled-components"; import styled from "styled-components";
import parseDocumentSlug from "shared/utils/parseDocumentSlug"; import parseDocumentSlug from "@shared/utils/parseDocumentSlug";
import DocumentMetaWithViews from "components/DocumentMetaWithViews"; import DocumentMetaWithViews from "~/components/DocumentMetaWithViews";
import Editor from "components/Editor"; import Editor from "~/components/Editor";
import useStores from "hooks/useStores"; import useStores from "~/hooks/useStores";
type Props = { type Props = {
url: string, url: string;
children: (React.Node) => React.Node, children: (arg0: React.ReactNode) => React.ReactNode;
}; };
function HoverPreviewDocument({ url, children }: Props) { function HoverPreviewDocument({ url, children }: Props) {
const { documents } = useStores(); const { documents } = useStores();
const slug = parseDocumentSlug(url); const slug = parseDocumentSlug(url);
documents.prefetchDocument(slug, { if (slug) {
prefetch: true, documents.prefetchDocument(slug);
}); }
const document = slug ? documents.getByUrl(slug) : undefined; const document = slug ? documents.getByUrl(slug) : undefined;
if (!document) return null; if (!document) return null;
@@ -50,4 +49,5 @@ const Heading = styled.h2`
color: ${(props) => props.theme.text}; color: ${(props) => props.theme.text};
`; `;
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type '({ url, children }: Props) => Re... Remove this comment to see the full error message
export default observer(HoverPreviewDocument); export default observer(HoverPreviewDocument);

View File

@@ -1,4 +1,3 @@
// @flow
import { import {
BookmarkedIcon, BookmarkedIcon,
CollectionIcon, CollectionIcon,
@@ -36,19 +35,22 @@ import { useTranslation } from "react-i18next";
import { useMenuState, MenuButton, MenuItem } from "reakit/Menu"; import { useMenuState, MenuButton, MenuItem } from "reakit/Menu";
import styled from "styled-components"; import styled from "styled-components";
import breakpoint from "styled-components-breakpoint"; import breakpoint from "styled-components-breakpoint";
import ContextMenu from "components/ContextMenu"; import ContextMenu from "~/components/ContextMenu";
import Flex from "components/Flex"; import Flex from "~/components/Flex";
import HelpText from "components/HelpText"; import HelpText from "~/components/HelpText";
import { LabelText } from "components/Input"; import { LabelText } from "~/components/Input";
import NudeButton from "components/NudeButton"; import NudeButton from "~/components/NudeButton";
const style = { width: 30, height: 30 }; const style = {
width: 30,
const TwitterPicker = React.lazy(() => height: 30,
import( };
/* webpackChunkName: "twitter-picker" */ const TwitterPicker = React.lazy(
"react-color/lib/components/twitter/Twitter" () =>
) import(
/* webpackChunkName: "twitter-picker" */
"react-color/lib/components/twitter/Twitter"
)
); );
export const icons = { export const icons = {
@@ -173,7 +175,6 @@ export const icons = {
keywords: "warning alert error", keywords: "warning alert error",
}, },
}; };
const colors = [ const colors = [
"#4E5C6E", "#4E5C6E",
"#0366d6", "#0366d6",
@@ -186,14 +187,13 @@ const colors = [
"#FF4DFA", "#FF4DFA",
"#2F362F", "#2F362F",
]; ];
type Props = {
type Props = {| onOpen?: () => void;
onOpen?: () => void, onClose?: () => void;
onClose?: () => void, onChange: (color: string, icon: string) => void;
onChange: (color: string, icon: string) => void, icon: string;
icon: string, color: string;
color: string, };
|};
function IconPicker({ onOpen, onClose, icon, color, onChange }: Props) { function IconPicker({ onOpen, onClose, icon, color, onChange }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();

View File

@@ -1,13 +1,12 @@
// @flow
import { observable } from "mobx"; import { observable } from "mobx";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import * as React from "react"; import * as React from "react";
import { VisuallyHidden } from "reakit/VisuallyHidden"; import { VisuallyHidden } from "reakit/VisuallyHidden";
import styled from "styled-components"; import styled from "styled-components";
import breakpoint from "styled-components-breakpoint"; import breakpoint from "styled-components-breakpoint";
import Flex from "components/Flex"; import Flex from "~/components/Flex";
const RealTextarea = styled.textarea` const RealTextarea = styled.textarea<{ hasIcon?: boolean }>`
border: 0; border: 0;
flex: 1; flex: 1;
padding: 8px 12px 8px ${(props) => (props.hasIcon ? "8px" : "12px")}; padding: 8px 12px 8px ${(props) => (props.hasIcon ? "8px" : "12px")};
@@ -21,7 +20,7 @@ const RealTextarea = styled.textarea`
} }
`; `;
const RealInput = styled.input` const RealInput = styled.input<{ hasIcon?: boolean }>`
border: 0; border: 0;
flex: 1; flex: 1;
padding: 8px 12px 8px ${(props) => (props.hasIcon ? "8px" : "12px")}; padding: 8px 12px 8px ${(props) => (props.hasIcon ? "8px" : "12px")};
@@ -48,7 +47,12 @@ const RealInput = styled.input`
`}; `};
`; `;
const Wrapper = styled.div` const Wrapper = styled.div<{
flex?: boolean;
short?: boolean;
minHeight?: number;
maxHeight?: number;
}>`
flex: ${(props) => (props.flex ? "1" : "0")}; flex: ${(props) => (props.flex ? "1" : "0")};
width: ${(props) => (props.short ? "49%" : "auto")}; width: ${(props) => (props.short ? "49%" : "auto")};
max-width: ${(props) => (props.short ? "350px" : "100%")}; max-width: ${(props) => (props.short ? "350px" : "100%")};
@@ -63,7 +67,11 @@ const IconWrapper = styled.span`
height: 24px; height: 24px;
`; `;
export const Outline = styled(Flex)` export const Outline = styled(Flex)<{
margin?: string | number;
hasError?: boolean;
focused?: boolean;
}>`
flex: 1; flex: 1;
margin: ${(props) => margin: ${(props) =>
props.margin !== undefined ? props.margin : "0 0 16px"}; props.margin !== undefined ? props.margin : "0 0 16px"};
@@ -88,56 +96,58 @@ export const LabelText = styled.div`
display: inline-block; display: inline-block;
`; `;
export type Props = {| export type Props = {
type?: "text" | "email" | "checkbox" | "search" | "textarea", type?: "text" | "email" | "checkbox" | "search" | "textarea";
value?: string, value?: string;
label?: string, label?: string;
className?: string, className?: string;
labelHidden?: boolean, labelHidden?: boolean;
flex?: boolean, flex?: boolean;
short?: boolean, short?: boolean;
margin?: string | number, margin?: string | number;
icon?: React.Node, icon?: React.ReactNode;
name?: string, name?: string;
minLength?: number, minLength?: number;
maxLength?: number, maxLength?: number;
autoFocus?: boolean, autoFocus?: boolean;
autoComplete?: boolean | string, autoComplete?: boolean | string;
readOnly?: boolean, readOnly?: boolean;
required?: boolean, required?: boolean;
disabled?: boolean, disabled?: boolean;
placeholder?: string, placeholder?: string;
onChange?: ( onChange?: (
ev: SyntheticInputEvent<HTMLInputElement | HTMLTextAreaElement> ev: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => mixed, ) => unknown;
onKeyDown?: (ev: SyntheticKeyboardEvent<HTMLInputElement>) => mixed, onKeyDown?: (ev: React.KeyboardEvent<HTMLInputElement>) => unknown;
onFocus?: (ev: SyntheticEvent<>) => mixed, onFocus?: (ev: React.SyntheticEvent) => unknown;
onBlur?: (ev: SyntheticEvent<>) => mixed, onBlur?: (ev: React.SyntheticEvent) => unknown;
|}; };
@observer @observer
class Input extends React.Component<Props> { class Input extends React.Component<Props> {
input: ?HTMLInputElement; input = React.createRef<HTMLInputElement | HTMLTextAreaElement>();
@observable focused: boolean = false;
handleBlur = (ev: SyntheticEvent<>) => { @observable
focused = false;
handleBlur = (ev: React.SyntheticEvent) => {
this.focused = false; this.focused = false;
if (this.props.onBlur) { if (this.props.onBlur) {
this.props.onBlur(ev); this.props.onBlur(ev);
} }
}; };
handleFocus = (ev: SyntheticEvent<>) => { handleFocus = (ev: React.SyntheticEvent) => {
this.focused = true; this.focused = true;
if (this.props.onFocus) { if (this.props.onFocus) {
this.props.onFocus(ev); this.props.onFocus(ev);
} }
}; };
focus() { focus() {
if (this.input) { this.input.current?.focus();
this.input.focus();
}
} }
render() { render() {
@@ -155,7 +165,8 @@ class Input extends React.Component<Props> {
...rest ...rest
} = this.props; } = this.props;
const InputComponent = type === "textarea" ? RealTextarea : RealInput; const InputComponent: React.ComponentType =
type === "textarea" ? RealTextarea : RealInput;
const wrappedLabel = <LabelText>{label}</LabelText>; const wrappedLabel = <LabelText>{label}</LabelText>;
return ( return (
@@ -170,11 +181,12 @@ class Input extends React.Component<Props> {
<Outline focused={this.focused} margin={margin}> <Outline focused={this.focused} margin={margin}>
{icon && <IconWrapper>{icon}</IconWrapper>} {icon && <IconWrapper>{icon}</IconWrapper>}
<InputComponent <InputComponent
ref={(ref) => (this.input = ref)} // @ts-expect-error no idea why this is not working
ref={this.input}
onBlur={this.handleBlur} onBlur={this.handleBlur}
onFocus={this.handleFocus} onFocus={this.handleFocus}
type={type === "textarea" ? undefined : type}
hasIcon={!!icon} hasIcon={!!icon}
type={type === "textarea" ? undefined : type}
{...rest} {...rest}
/> />
</Outline> </Outline>

View File

@@ -1,4 +1,3 @@
// @flow
import styled from "styled-components"; import styled from "styled-components";
import Input from "./Input"; import Input from "./Input";

View File

@@ -1,28 +1,25 @@
// @flow
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import * as React from "react"; import * as React from "react";
import { Trans } from "react-i18next"; import { Trans } from "react-i18next";
import styled, { withTheme } from "styled-components"; import styled from "styled-components";
import Editor from "components/Editor"; import Editor from "~/components/Editor";
import HelpText from "components/HelpText"; import HelpText from "~/components/HelpText";
import { LabelText, Outline } from "components/Input"; import { LabelText, Outline } from "~/components/Input";
import useStores from "hooks/useStores"; import useStores from "~/hooks/useStores";
type Props = {| type Props = {
label: string, label: string;
minHeight?: number, minHeight?: number;
maxHeight?: number, maxHeight?: number;
readOnly?: boolean, readOnly?: boolean;
|}; };
function InputRich({ label, minHeight, maxHeight, ...rest }: Props) { function InputRich({ label, minHeight, maxHeight, ...rest }: Props) {
const [focused, setFocused] = React.useState<boolean>(false); const [focused, setFocused] = React.useState<boolean>(false);
const { ui } = useStores(); const { ui } = useStores();
const handleBlur = React.useCallback(() => { const handleBlur = React.useCallback(() => {
setFocused(false); setFocused(false);
}, []); }, []);
const handleFocus = React.useCallback(() => { const handleFocus = React.useCallback(() => {
setFocused(true); setFocused(true);
}, []); }, []);
@@ -55,7 +52,11 @@ function InputRich({ label, minHeight, maxHeight, ...rest }: Props) {
); );
} }
const StyledOutline = styled(Outline)` const StyledOutline = styled(Outline)<{
minHeight?: number;
maxHeight?: number;
focused?: boolean;
}>`
display: block; display: block;
padding: 8px 12px; padding: 8px 12px;
min-height: ${({ minHeight }) => (minHeight ? `${minHeight}px` : "0")}; min-height: ${({ minHeight }) => (minHeight ? `${minHeight}px` : "0")};
@@ -67,4 +68,4 @@ const StyledOutline = styled(Outline)`
} }
`; `;
export default observer(withTheme(InputRich)); export default observer(InputRich);

View File

@@ -1,23 +1,20 @@
// @flow
import { SearchIcon } from "outline-icons"; import { SearchIcon } from "outline-icons";
import * as React from "react"; import * as React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useTheme } from "styled-components"; import { useTheme } from "styled-components";
import Input, { type Props as InputProps } from "./Input"; import Input, { Props as InputProps } from "./Input";
type Props = {| type Props = InputProps & {
...InputProps, placeholder?: string;
placeholder?: string, value?: string;
value?: string, onChange: (event: React.ChangeEvent<HTMLInputElement>) => unknown;
onChange: (event: SyntheticInputEvent<>) => mixed, onKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => unknown;
onKeyDown?: (event: SyntheticKeyboardEvent<HTMLInputElement>) => mixed, };
|};
export default function InputSearch(props: Props) { export default function InputSearch(props: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const theme = useTheme(); const theme = useTheme();
const [isFocused, setIsFocused] = React.useState(false); const [isFocused, setIsFocused] = React.useState(false);
const handleFocus = React.useCallback(() => { const handleFocus = React.useCallback(() => {
setIsFocused(true); setIsFocused(true);
}, []); }, []);

View File

@@ -1,26 +1,25 @@
// @flow
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { SearchIcon } from "outline-icons"; import { SearchIcon } from "outline-icons";
import * as React from "react"; import * as React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import styled, { useTheme } from "styled-components"; import styled, { useTheme } from "styled-components";
import useBoolean from "~/hooks/useBoolean";
import useKeyDown from "~/hooks/useKeyDown";
import { isModKey } from "~/utils/keyboard";
import { searchUrl } from "~/utils/routeHelpers";
import Input from "./Input"; import Input from "./Input";
import useBoolean from "hooks/useBoolean";
import useKeyDown from "hooks/useKeyDown";
import { isModKey } from "utils/keyboard";
import { searchUrl } from "utils/routeHelpers";
type Props = {| type Props = {
source: string, source: string;
placeholder?: string, placeholder?: string;
label?: string, label?: string;
labelHidden?: boolean, labelHidden?: boolean;
collectionId?: string, collectionId?: string;
value: string, value?: string;
onChange: (event: SyntheticInputEvent<>) => mixed, onChange?: (event: React.ChangeEvent<HTMLInputElement>) => unknown;
onKeyDown: (event: SyntheticKeyboardEvent<HTMLInputElement>) => mixed, onKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => unknown;
|}; };
function InputSearchPage({ function InputSearchPage({
onKeyDown, onKeyDown,
@@ -31,12 +30,11 @@ function InputSearchPage({
collectionId, collectionId,
source, source,
}: Props) { }: Props) {
const inputRef = React.useRef(); const inputRef = React.useRef<Input>(null);
const theme = useTheme(); const theme = useTheme();
const history = useHistory(); const history = useHistory();
const { t } = useTranslation(); const { t } = useTranslation();
const [isFocused, setFocused, setUnfocused] = useBoolean(false); const [isFocused, setFocused, setUnfocused] = useBoolean(false);
const focus = React.useCallback(() => { const focus = React.useCallback(() => {
inputRef.current?.focus(); inputRef.current?.focus();
}, []); }, []);
@@ -49,7 +47,7 @@ function InputSearchPage({
}); });
const handleKeyDown = React.useCallback( const handleKeyDown = React.useCallback(
(ev: SyntheticKeyboardEvent<HTMLInputElement>) => { (ev: React.KeyboardEvent<HTMLInputElement>) => {
if (ev.key === "Enter") { if (ev.key === "Enter") {
ev.preventDefault(); ev.preventDefault();
history.push( history.push(

View File

@@ -1,4 +1,3 @@
// @flow
import { import {
Select, Select,
SelectOption, SelectOption,
@@ -11,32 +10,38 @@ import * as React from "react";
import { VisuallyHidden } from "reakit/VisuallyHidden"; import { VisuallyHidden } from "reakit/VisuallyHidden";
import scrollIntoView from "smooth-scroll-into-view-if-needed"; import scrollIntoView from "smooth-scroll-into-view-if-needed";
import styled, { css } from "styled-components"; import styled, { css } from "styled-components";
import Button, { Inner } from "components/Button"; import Button, { Inner } from "~/components/Button";
import HelpText from "components/HelpText"; import HelpText from "~/components/HelpText";
import { Position, Background, Backdrop } from "./ContextMenu"; import useMenuHeight from "~/hooks/useMenuHeight";
import { Position, Background, Backdrop, Placement } from "./ContextMenu";
import { MenuAnchorCSS } from "./ContextMenu/MenuItem"; import { MenuAnchorCSS } from "./ContextMenu/MenuItem";
import { LabelText } from "./Input"; import { LabelText } from "./Input";
import useMenuHeight from "hooks/useMenuHeight";
export type Option = { label: string, value: string }; export type Option = {
label: string;
export type Props = { value: string;
value?: string,
label?: string,
nude?: boolean,
ariaLabel: string,
short?: boolean,
disabled?: boolean,
className?: string,
labelHidden?: boolean,
icon?: React.Node,
options: Option[],
note?: React.Node,
onChange: (string) => Promise<void> | void,
}; };
const getOptionFromValue = (options: Option[], value) => { export type Props = {
return options.find((option) => option.value === value) || {}; value?: string;
label?: string;
nude?: boolean;
ariaLabel: string;
short?: boolean;
disabled?: boolean;
className?: string;
labelHidden?: boolean;
icon?: React.ReactNode;
options: Option[];
note?: React.ReactNode;
onChange: (value: string | null) => void;
};
const getOptionFromValue = (
options: Option[],
value: string | undefined | null
) => {
return options.find((option) => option.value === value);
}; };
const InputSelect = (props: Props) => { const InputSelect = (props: Props) => {
@@ -50,7 +55,6 @@ const InputSelect = (props: Props) => {
ariaLabel, ariaLabel,
onChange, onChange,
disabled, disabled,
nude,
note, note,
icon, icon,
} = props; } = props;
@@ -69,13 +73,12 @@ const InputSelect = (props: Props) => {
disabled, disabled,
}); });
const previousValue = React.useRef(value); const previousValue = React.useRef<string | undefined | null>(value);
const contentRef = React.useRef(); const contentRef = React.useRef<HTMLDivElement>(null);
const selectedRef = React.useRef(); const selectedRef = React.useRef<HTMLDivElement>(null);
const buttonRef = React.useRef(); const buttonRef = React.useRef<HTMLButtonElement>(null);
const [offset, setOffset] = React.useState(0); const [offset, setOffset] = React.useState(0);
const minWidth = buttonRef.current?.offsetWidth || 0; const minWidth = buttonRef.current?.offsetWidth || 0;
const maxHeight = useMenuHeight( const maxHeight = useMenuHeight(
select.visible, select.visible,
select.unstable_disclosureRef select.unstable_disclosureRef
@@ -83,16 +86,15 @@ const InputSelect = (props: Props) => {
React.useEffect(() => { React.useEffect(() => {
if (previousValue.current === select.selectedValue) return; if (previousValue.current === select.selectedValue) return;
previousValue.current = select.selectedValue; previousValue.current = select.selectedValue;
async function load() { async function load() {
await onChange(select.selectedValue); await onChange(select.selectedValue);
} }
load(); load();
}, [onChange, select.selectedValue]); }, [onChange, select.selectedValue]);
const wrappedLabel = <LabelText>{label}</LabelText>; const wrappedLabel = <LabelText>{label}</LabelText>;
const selectedValueIndex = options.findIndex( const selectedValueIndex = options.findIndex(
(option) => option.value === select.selectedValue (option) => option.value === select.selectedValue
); );
@@ -102,7 +104,7 @@ const InputSelect = (props: Props) => {
if (!select.animating && selectedRef.current) { if (!select.animating && selectedRef.current) {
scrollIntoView(selectedRef.current, { scrollIntoView(selectedRef.current, {
scrollMode: "if-needed", scrollMode: "if-needed",
behavior: "instant", behavior: "auto",
block: "start", block: "start",
}); });
} }
@@ -134,18 +136,24 @@ const InputSelect = (props: Props) => {
neutral neutral
disclosure disclosure
className={className} className={className}
nude={nude}
icon={icon} icon={icon}
{...props} {...props}
> >
{getOptionFromValue(options, select.selectedValue).label || ( {getOptionFromValue(options, select.selectedValue)?.label || (
<Placeholder>Select a {ariaLabel.toLowerCase()}</Placeholder> <Placeholder>Select a {ariaLabel.toLowerCase()}</Placeholder>
)} )}
</StyledButton> </StyledButton>
)} )}
</Select> </Select>
<SelectPopover {...select} {...popOver} aria-label={ariaLabel}> <SelectPopover {...select} {...popOver} aria-label={ariaLabel}>
{(props) => { {(
props: React.HTMLAttributes<HTMLDivElement> & {
placement: Placement;
}
) => {
if (!props.style) {
props.style = {};
}
const topAnchor = props.style.top === "0"; const topAnchor = props.style.top === "0";
const rightAnchor = props.placement === "bottom-end"; const rightAnchor = props.placement === "bottom-end";
@@ -163,8 +171,13 @@ const InputSelect = (props: Props) => {
rightAnchor={rightAnchor} rightAnchor={rightAnchor}
style={ style={
maxHeight && topAnchor maxHeight && topAnchor
? { maxHeight, minWidth } ? {
: { minWidth } maxHeight,
minWidth,
}
: {
minWidth,
}
} }
> >
{select.visible || select.animating {select.visible || select.animating
@@ -173,7 +186,7 @@ const InputSelect = (props: Props) => {
{...select} {...select}
value={option.value} value={option.value}
key={option.value} key={option.value}
animating={select.animating} $animating={select.animating}
ref={ ref={
select.selectedValue === option.value select.selectedValue === option.value
? selectedRef ? selectedRef
@@ -201,7 +214,6 @@ const InputSelect = (props: Props) => {
</SelectPopover> </SelectPopover>
</Wrapper> </Wrapper>
{note && <HelpText small>{note}</HelpText>} {note && <HelpText small>{note}</HelpText>}
{(select.visible || select.animating) && <Backdrop />} {(select.visible || select.animating) && <Backdrop />}
</> </>
); );
@@ -217,7 +229,7 @@ const Spacer = styled.div`
flex-shrink: 0; flex-shrink: 0;
`; `;
const StyledButton = styled(Button)` const StyledButton = styled(Button)<{ nude?: boolean }>`
font-weight: normal; font-weight: normal;
text-transform: none; text-transform: none;
margin-bottom: 16px; margin-bottom: 16px;
@@ -243,17 +255,17 @@ const StyledButton = styled(Button)`
} }
`; `;
export const StyledSelectOption = styled(SelectOption)` export const StyledSelectOption = styled(SelectOption)<{ $animating: boolean }>`
${MenuAnchorCSS} ${MenuAnchorCSS}
${(props) => ${(props) =>
props.animating && props.$animating &&
css` css`
pointer-events: none; pointer-events: none;
`} `}
`; `;
const Wrapper = styled.label` const Wrapper = styled.label<{ short?: boolean }>`
display: block; display: block;
max-width: ${(props) => (props.short ? "350px" : "100%")}; max-width: ${(props) => (props.short ? "350px" : "100%")};
`; `;

View File

@@ -1,19 +1,25 @@
// @flow
import * as React from "react"; import * as React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import InputSelect, { type Props, type Option } from "./InputSelect"; import { $Diff } from "utility-types";
import InputSelect, { Props, Option } from "./InputSelect";
export default function InputSelectPermission( export default function InputSelectPermission(
props: $Rest<$Exact<Props>, {| options: Array<Option>, ariaLabel: string |}> props: $Diff<
Props,
{
options: Array<Option>;
ariaLabel: string;
}
>
) { ) {
const { value, onChange, ...rest } = props; const { value, onChange, ...rest } = props;
const { t } = useTranslation(); const { t } = useTranslation();
const handleChange = React.useCallback( const handleChange = React.useCallback(
(value) => { (value) => {
if (value === "no_access") { if (value === "no_access") {
value = ""; value = "";
} }
onChange(value); onChange(value);
}, },
[onChange] [onChange]
@@ -23,9 +29,18 @@ export default function InputSelectPermission(
<InputSelect <InputSelect
label={t("Default access")} label={t("Default access")}
options={[ options={[
{ label: t("View and edit"), value: "read_write" }, {
{ label: t("View only"), value: "read" }, label: t("View and edit"),
{ label: t("No access"), value: "no_access" }, value: "read_write",
},
{
label: t("View only"),
value: "read",
},
{
label: t("No access"),
value: "no_access",
},
]} ]}
ariaLabel={t("Default access")} ariaLabel={t("Default access")}
value={value || "no_access"} value={value || "no_access"}

View File

@@ -1,25 +0,0 @@
// @flow
import * as React from "react";
import { useTranslation } from "react-i18next";
import InputSelect, { type Props, type Option } from "components/InputSelect";
const InputSelectRole = (
props: $Rest<$Exact<Props>, {| options: Array<Option>, ariaLabel: string |}>
) => {
const { t } = useTranslation();
return (
<InputSelect
label={t("Role")}
options={[
{ label: t("Member"), value: "member" },
{ label: t("Viewer"), value: "viewer" },
{ label: t("Admin"), value: "admin" },
]}
ariaLabel={t("Role")}
{...props}
/>
);
};
export default InputSelectRole;

View File

@@ -0,0 +1,39 @@
import * as React from "react";
import { useTranslation } from "react-i18next";
import { $Diff } from "utility-types";
import InputSelect, { Props, Option } from "~/components/InputSelect";
const InputSelectRole = (
props: $Diff<
Props,
{
options: Array<Option>;
ariaLabel: string;
}
>
) => {
const { t } = useTranslation();
return (
<InputSelect
label={t("Role")}
options={[
{
label: t("Member"),
value: "member",
},
{
label: t("Viewer"),
value: "viewer",
},
{
label: t("Admin"),
value: "admin",
},
]}
ariaLabel={t("Role")}
{...props}
/>
);
};
export default InputSelectRole;

View File

@@ -1,4 +1,3 @@
// @flow
import styled from "styled-components"; import styled from "styled-components";
const Key = styled.kbd` const Key = styled.kbd`

View File

@@ -1,13 +1,12 @@
// @flow
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import * as React from "react"; import * as React from "react";
import styled from "styled-components"; import styled from "styled-components";
import Flex from "components/Flex"; import Flex from "~/components/Flex";
type Props = {| type Props = {
label: React.Node | string, label: React.ReactNode | string;
children: React.Node, children?: React.ReactNode;
|}; };
const Labeled = ({ label, children, ...props }: Props) => ( const Labeled = ({ label, children, ...props }: Props) => (
<Flex column {...props}> <Flex column {...props}>

View File

@@ -1,17 +1,16 @@
// @flow
import { find } from "lodash"; import { find } from "lodash";
import * as React from "react"; import * as React from "react";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import styled from "styled-components"; import styled from "styled-components";
import { languages, languageOptions } from "shared/i18n"; import { languages, languageOptions } from "@shared/i18n";
import ButtonLink from "components/ButtonLink"; import ButtonLink from "~/components/ButtonLink";
import Flex from "components/Flex"; import Flex from "~/components/Flex";
import NoticeTip from "components/NoticeTip"; import NoticeTip from "~/components/NoticeTip";
import useCurrentUser from "hooks/useCurrentUser"; import useCurrentUser from "~/hooks/useCurrentUser";
import useStores from "hooks/useStores"; import useStores from "~/hooks/useStores";
import { detectLanguage } from "utils/language"; import { detectLanguage } from "~/utils/language";
function Icon(props) { function Icon(props: any) {
return ( return (
<svg <svg
width="32" width="32"
@@ -65,8 +64,11 @@ export default function LanguagePrompt() {
<LanguageIcon /> <LanguageIcon />
<span> <span>
<Trans> <Trans>
Outline is available in your language {{ optionLabel }}, would you Outline is available in your language{" "}
like to change? {{
optionLabel,
}}
, would you like to change?
</Trans> </Trans>
<br /> <br />
<Link <Link

Some files were not shown because too many files have changed in this diff Show More