mirror of
https://github.com/outline/outline.git
synced 2025-12-20 10:09:43 -06:00
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:
2
.babelrc
2
.babelrc
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"presets": [
|
||||
"@babel/preset-react",
|
||||
"@babel/preset-flow",
|
||||
"@babel/preset-typescript",
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
|
||||
@@ -41,8 +41,8 @@ jobs:
|
||||
name: lint
|
||||
command: yarn lint
|
||||
- run:
|
||||
name: flow
|
||||
command: yarn flow check --max-workers 4
|
||||
name: typescript
|
||||
command: yarn tsc
|
||||
- run:
|
||||
name: test
|
||||
command: yarn test
|
||||
|
||||
@@ -6,14 +6,13 @@ __mocks__
|
||||
.DS_Store
|
||||
.env*
|
||||
.eslint*
|
||||
.flowconfig
|
||||
.log
|
||||
Makefile
|
||||
Procfile
|
||||
app.json
|
||||
crowdin.yml
|
||||
build
|
||||
docker-compose.yml
|
||||
fakes3
|
||||
flow-typed
|
||||
node_modules
|
||||
setupJest.js
|
||||
tsconfig.json
|
||||
|
||||
85
.eslintrc
85
.eslintrc
@@ -1,20 +1,34 @@
|
||||
{
|
||||
"parser": "babel-eslint",
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"extraFileExtensions": [".json"],
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"extends": [
|
||||
"react-app",
|
||||
"plugin:import/errors",
|
||||
"plugin:import/warnings",
|
||||
"plugin:flowtype/recommended",
|
||||
"plugin:react-hooks/recommended"
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:import/recommended",
|
||||
"plugin:import/typescript",
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:prettier/recommended"
|
||||
],
|
||||
"plugins": [
|
||||
"prettier",
|
||||
"flowtype"
|
||||
"@typescript-eslint",
|
||||
"eslint-plugin-import",
|
||||
"eslint-plugin-node",
|
||||
"eslint-plugin-react",
|
||||
"eslint-plugin-react-hooks",
|
||||
"import"
|
||||
],
|
||||
"rules": {
|
||||
"eqeqeq": 2,
|
||||
"no-unused-vars": 2,
|
||||
"no-mixed-operators": "off",
|
||||
"padding-line-between-statements": ["error", { "blankLine": "always", "prev": "*", "next": "export" }],
|
||||
"lines-between-class-members": ["error", "always", { "exceptAfterSingleLine": true }],
|
||||
"import/newline-after-import": 2,
|
||||
"import/order": [
|
||||
"error",
|
||||
{
|
||||
@@ -23,53 +37,48 @@
|
||||
},
|
||||
"pathGroups": [
|
||||
{
|
||||
"pattern": "shared/**",
|
||||
"pattern": "@shared/**",
|
||||
"group": "external",
|
||||
"position": "after"
|
||||
},
|
||||
{
|
||||
"pattern": "stores",
|
||||
"pattern": "@server/**",
|
||||
"group": "external",
|
||||
"position": "after"
|
||||
},
|
||||
{
|
||||
"pattern": "stores/**",
|
||||
"pattern": "~/stores",
|
||||
"group": "external",
|
||||
"position": "after"
|
||||
},
|
||||
{
|
||||
"pattern": "models/**",
|
||||
"pattern": "~/stores/**",
|
||||
"group": "external",
|
||||
"position": "after"
|
||||
},
|
||||
{
|
||||
"pattern": "scenes/**",
|
||||
"pattern": "~/models/**",
|
||||
"group": "external",
|
||||
"position": "after"
|
||||
},
|
||||
{
|
||||
"pattern": "components/**",
|
||||
"pattern": "~/scenes/**",
|
||||
"group": "external",
|
||||
"position": "after"
|
||||
},
|
||||
{
|
||||
"pattern": "~/components/**",
|
||||
"group": "external",
|
||||
"position": "after"
|
||||
},
|
||||
{
|
||||
"pattern": "~/**",
|
||||
"group": "external",
|
||||
"position": "after"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"flowtype/require-valid-file-annotation": [
|
||||
2,
|
||||
"always",
|
||||
{
|
||||
"annotationStyle": "line"
|
||||
}
|
||||
],
|
||||
"flowtype/space-after-type-colon": [
|
||||
2,
|
||||
"always"
|
||||
],
|
||||
"flowtype/space-before-type-colon": [
|
||||
2,
|
||||
"never"
|
||||
],
|
||||
"prettier/prettier": [
|
||||
"error",
|
||||
{
|
||||
@@ -84,21 +93,13 @@
|
||||
"pragma": "React",
|
||||
"version": "detect"
|
||||
},
|
||||
"import/resolver": {
|
||||
"node": {
|
||||
"paths": [
|
||||
"app",
|
||||
"."
|
||||
]
|
||||
}
|
||||
"import/parsers": {
|
||||
"@typescript-eslint/parser": [".ts", ".tsx"]
|
||||
},
|
||||
"flowtype": {
|
||||
"onlyFilesWithFlowAnnotation": false
|
||||
"import/resolver": {
|
||||
"typescript": {}
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"jest": true
|
||||
},
|
||||
"globals": {
|
||||
"EDITOR_VERSION": true
|
||||
}
|
||||
|
||||
44
.flowconfig
44
.flowconfig
@@ -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
|
||||
8
.vscode/settings.json
vendored
8
.vscode/settings.json
vendored
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"javascript.validate.enable": false,
|
||||
"javascript.format.enable": false,
|
||||
"typescript.validate.enable": false,
|
||||
"typescript.format.enable": false,
|
||||
"javascript.validate.enable": true,
|
||||
"javascript.format.enable": true,
|
||||
"typescript.validate.enable": true,
|
||||
"typescript.format.enable": true,
|
||||
"editor.formatOnSave": true,
|
||||
}
|
||||
@@ -8,9 +8,10 @@
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://circleci.com/gh/outline/outline" rel="nofollow"><img src="https://circleci.com/gh/outline/outline.svg?style=shield&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="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://translate.getoutline.com/project/outline"><img src="https://badges.crowdin.net/outline/localized.svg"></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/prettier/prettier"><img src="https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat" alt="Prettier"></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>
|
||||
|
||||
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).
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable flowtype/require-valid-file-annotation */
|
||||
export default class Queue {
|
||||
name;
|
||||
|
||||
|
||||
9
app/.eslintrc
Normal file
9
app/.eslintrc
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": [
|
||||
"../.eslintrc"
|
||||
],
|
||||
"env": {
|
||||
"jest": true,
|
||||
"browser": true
|
||||
}
|
||||
}
|
||||
@@ -7,14 +7,10 @@
|
||||
"<rootDir>/shared"
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
"^shared/(.*)$": "<rootDir>/shared/$1",
|
||||
"^~/(.*)$": "<rootDir>/app/$1",
|
||||
"^@shared/(.*)$": "<rootDir>/shared/$1",
|
||||
"^.*[.](gif|ttf|eot|svg)$": "<rootDir>/__test__/fileMock.js"
|
||||
},
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"jsx",
|
||||
"json"
|
||||
],
|
||||
"moduleDirectories": [
|
||||
"node_modules"
|
||||
],
|
||||
@@ -25,6 +21,6 @@
|
||||
"<rootDir>/__mocks__/window.js"
|
||||
],
|
||||
"setupFilesAfterEnv": [
|
||||
"./app/test/setup.js"
|
||||
"./app/test/setup.ts"
|
||||
]
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
// @flow
|
||||
import { CollectionIcon, EditIcon, PlusIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import stores from "stores";
|
||||
import CollectionEdit from "scenes/CollectionEdit";
|
||||
import CollectionNew from "scenes/CollectionNew";
|
||||
import DynamicCollectionIcon from "components/CollectionIcon";
|
||||
import { createAction } from "actions";
|
||||
import { CollectionSection } from "actions/sections";
|
||||
import history from "utils/history";
|
||||
import stores from "~/stores";
|
||||
import CollectionEdit from "~/scenes/CollectionEdit";
|
||||
import CollectionNew from "~/scenes/CollectionNew";
|
||||
import DynamicCollectionIcon from "~/components/CollectionIcon";
|
||||
import { createAction } from "~/actions";
|
||||
import { CollectionSection } from "~/actions/sections";
|
||||
import history from "~/utils/history";
|
||||
|
||||
export const openCollection = createAction({
|
||||
name: ({ t }) => t("Open collection"),
|
||||
@@ -16,7 +15,6 @@ export const openCollection = createAction({
|
||||
icon: <CollectionIcon />,
|
||||
children: ({ stores }) => {
|
||||
const collections = stores.collections.orderedData;
|
||||
|
||||
return collections.map((collection) => ({
|
||||
// Note: using url which includes the slug rather than id here to bust
|
||||
// cache if the collection is renamed
|
||||
@@ -39,7 +37,6 @@ export const createCollection = createAction({
|
||||
perform: ({ t, event }) => {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
|
||||
stores.dialogs.openModal({
|
||||
title: t("Create a collection"),
|
||||
content: <CollectionNew onSubmit={stores.dialogs.closeAllModals} />,
|
||||
@@ -55,6 +52,8 @@ export const editCollection = createAction({
|
||||
!!activeCollectionId &&
|
||||
stores.policies.abilities(activeCollectionId).update,
|
||||
perform: ({ t, activeCollectionId }) => {
|
||||
if (!activeCollectionId) return;
|
||||
|
||||
stores.dialogs.openModal({
|
||||
title: t("Edit collection"),
|
||||
content: (
|
||||
@@ -1,11 +1,10 @@
|
||||
// @flow
|
||||
import { ToolsIcon, TrashIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import stores from "stores";
|
||||
import { createAction } from "actions";
|
||||
import { DebugSection } from "actions/sections";
|
||||
import env from "env";
|
||||
import { deleteAllDatabases } from "utils/developer";
|
||||
import stores from "~/stores";
|
||||
import { createAction } from "~/actions";
|
||||
import { DebugSection } from "~/actions/sections";
|
||||
import env from "~/env";
|
||||
import { deleteAllDatabases } from "~/utils/developer";
|
||||
|
||||
export const clearIndexedDB = createAction({
|
||||
name: ({ t }) => t("Delete IndexedDB cache"),
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import invariant from "invariant";
|
||||
import {
|
||||
DownloadIcon,
|
||||
@@ -12,12 +11,12 @@ import {
|
||||
ImportIcon,
|
||||
} from "outline-icons";
|
||||
import * as React from "react";
|
||||
import DocumentTemplatize from "scenes/DocumentTemplatize";
|
||||
import { createAction } from "actions";
|
||||
import { DocumentSection } from "actions/sections";
|
||||
import getDataTransferFiles from "utils/getDataTransferFiles";
|
||||
import history from "utils/history";
|
||||
import { newDocumentPath } from "utils/routeHelpers";
|
||||
import DocumentTemplatize from "~/scenes/DocumentTemplatize";
|
||||
import { createAction } from "~/actions";
|
||||
import { DocumentSection } from "~/actions/sections";
|
||||
import getDataTransferFiles from "~/utils/getDataTransferFiles";
|
||||
import history from "~/utils/history";
|
||||
import { newDocumentPath } from "~/utils/routeHelpers";
|
||||
|
||||
export const openDocument = createAction({
|
||||
name: ({ t }) => t("Open document"),
|
||||
@@ -36,9 +35,7 @@ export const openDocument = createAction({
|
||||
id: path.url,
|
||||
name: path.title,
|
||||
icon: () =>
|
||||
stores.documents.get(path.id)?.isStarred ? (
|
||||
<StarredIcon />
|
||||
) : undefined,
|
||||
stores.documents.get(path.id)?.isStarred ? <StarredIcon /> : null,
|
||||
section: DocumentSection,
|
||||
perform: () => history.push(path.url),
|
||||
}));
|
||||
@@ -65,13 +62,12 @@ export const starDocument = createAction({
|
||||
visible: ({ activeDocumentId, stores }) => {
|
||||
if (!activeDocumentId) return false;
|
||||
const document = stores.documents.get(activeDocumentId);
|
||||
|
||||
return (
|
||||
!document?.isStarred && stores.policies.abilities(activeDocumentId).star
|
||||
);
|
||||
},
|
||||
perform: ({ activeDocumentId, stores }) => {
|
||||
if (!activeDocumentId) return false;
|
||||
if (!activeDocumentId) return;
|
||||
|
||||
const document = stores.documents.get(activeDocumentId);
|
||||
document?.star();
|
||||
@@ -86,14 +82,13 @@ export const unstarDocument = createAction({
|
||||
visible: ({ activeDocumentId, stores }) => {
|
||||
if (!activeDocumentId) return false;
|
||||
const document = stores.documents.get(activeDocumentId);
|
||||
|
||||
return (
|
||||
!!document?.isStarred &&
|
||||
stores.policies.abilities(activeDocumentId).unstar
|
||||
);
|
||||
},
|
||||
perform: ({ activeDocumentId, stores }) => {
|
||||
if (!activeDocumentId) return false;
|
||||
if (!activeDocumentId) return;
|
||||
|
||||
const document = stores.documents.get(activeDocumentId);
|
||||
document?.unstar();
|
||||
@@ -109,7 +104,7 @@ export const downloadDocument = createAction({
|
||||
visible: ({ activeDocumentId, stores }) =>
|
||||
!!activeDocumentId && stores.policies.abilities(activeDocumentId).download,
|
||||
perform: ({ activeDocumentId, stores }) => {
|
||||
if (!activeDocumentId) return false;
|
||||
if (!activeDocumentId) return;
|
||||
|
||||
const document = stores.documents.get(activeDocumentId);
|
||||
document?.download();
|
||||
@@ -125,16 +120,16 @@ export const duplicateDocument = createAction({
|
||||
visible: ({ activeDocumentId, stores }) =>
|
||||
!!activeDocumentId && stores.policies.abilities(activeDocumentId).update,
|
||||
perform: async ({ activeDocumentId, t, stores }) => {
|
||||
if (!activeDocumentId) return false;
|
||||
if (!activeDocumentId) return;
|
||||
|
||||
const document = stores.documents.get(activeDocumentId);
|
||||
invariant(document, "Document must exist");
|
||||
|
||||
const duped = await document.duplicate();
|
||||
|
||||
// when duplicating, go straight to the duplicated document content
|
||||
history.push(duped.url);
|
||||
stores.toasts.showToast(t("Document duplicated"), { type: "success" });
|
||||
stores.toasts.showToast(t("Document duplicated"), {
|
||||
type: "success",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -150,7 +145,7 @@ export const printDocument = createAction({
|
||||
});
|
||||
|
||||
export const importDocument = createAction({
|
||||
name: ({ t, activeDocumentId }) => t("Import document"),
|
||||
name: ({ t }) => t("Import document"),
|
||||
section: DocumentSection,
|
||||
icon: <ImportIcon />,
|
||||
keywords: "upload",
|
||||
@@ -158,18 +153,20 @@ export const importDocument = createAction({
|
||||
if (activeDocumentId) {
|
||||
return !!stores.policies.abilities(activeDocumentId).createChildDocument;
|
||||
}
|
||||
|
||||
if (activeCollectionId) {
|
||||
return !!stores.policies.abilities(activeCollectionId).update;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
perform: ({ activeCollectionId, activeDocumentId, stores }) => {
|
||||
const { documents, toasts } = stores;
|
||||
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = documents.importFileTypes.join(", ");
|
||||
input.onchange = async (ev: SyntheticEvent<>) => {
|
||||
|
||||
input.onchange = async (ev: Event) => {
|
||||
const files = getDataTransferFiles(ev);
|
||||
|
||||
try {
|
||||
@@ -187,10 +184,10 @@ export const importDocument = createAction({
|
||||
toasts.showToast(err.message, {
|
||||
type: "error",
|
||||
});
|
||||
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
input.click();
|
||||
},
|
||||
});
|
||||
@@ -202,9 +199,7 @@ export const createTemplate = createAction({
|
||||
keywords: "new create template",
|
||||
visible: ({ activeCollectionId, activeDocumentId, stores }) => {
|
||||
if (!activeDocumentId) return false;
|
||||
|
||||
const document = stores.documents.get(activeDocumentId);
|
||||
|
||||
return (
|
||||
!!activeCollectionId &&
|
||||
stores.policies.abilities(activeCollectionId).update &&
|
||||
@@ -212,6 +207,8 @@ export const createTemplate = createAction({
|
||||
);
|
||||
},
|
||||
perform: ({ activeDocumentId, stores, t, event }) => {
|
||||
if (!activeDocumentId) return;
|
||||
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import {
|
||||
HomeIcon,
|
||||
SearchIcon,
|
||||
@@ -17,12 +16,12 @@ import {
|
||||
changelogUrl,
|
||||
mailToUrl,
|
||||
githubIssuesUrl,
|
||||
} from "shared/utils/routeHelpers";
|
||||
import stores from "stores";
|
||||
import KeyboardShortcuts from "scenes/KeyboardShortcuts";
|
||||
import { createAction } from "actions";
|
||||
import { NavigationSection } from "actions/sections";
|
||||
import history from "utils/history";
|
||||
} from "@shared/utils/routeHelpers";
|
||||
import stores from "~/stores";
|
||||
import KeyboardShortcuts from "~/scenes/KeyboardShortcuts";
|
||||
import { createAction } from "~/actions";
|
||||
import { NavigationSection } from "~/actions/sections";
|
||||
import history from "~/utils/history";
|
||||
import {
|
||||
settingsPath,
|
||||
homePath,
|
||||
@@ -31,7 +30,7 @@ import {
|
||||
templatesPath,
|
||||
archivePath,
|
||||
trashPath,
|
||||
} from "utils/routeHelpers";
|
||||
} from "~/utils/routeHelpers";
|
||||
|
||||
export const navigateToHome = createAction({
|
||||
name: ({ t }) => t("Home"),
|
||||
@@ -1,9 +1,9 @@
|
||||
// @flow
|
||||
import { SunIcon, MoonIcon, BrowserIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import stores from "stores";
|
||||
import { createAction } from "actions";
|
||||
import { SettingsSection } from "actions/sections";
|
||||
import stores from "~/stores";
|
||||
import { Theme } from "~/stores/UiStore";
|
||||
import { createAction } from "~/actions";
|
||||
import { SettingsSection } from "~/actions/sections";
|
||||
|
||||
export const changeToDarkTheme = createAction({
|
||||
name: ({ t }) => t("Dark"),
|
||||
@@ -12,7 +12,7 @@ export const changeToDarkTheme = createAction({
|
||||
keywords: "theme dark night",
|
||||
section: SettingsSection,
|
||||
selected: () => stores.ui.theme === "dark",
|
||||
perform: () => stores.ui.setTheme("dark"),
|
||||
perform: () => stores.ui.setTheme(Theme.Dark),
|
||||
});
|
||||
|
||||
export const changeToLightTheme = createAction({
|
||||
@@ -22,7 +22,7 @@ export const changeToLightTheme = createAction({
|
||||
keywords: "theme light day",
|
||||
section: SettingsSection,
|
||||
selected: () => stores.ui.theme === "light",
|
||||
perform: () => stores.ui.setTheme("light"),
|
||||
perform: () => stores.ui.setTheme(Theme.Light),
|
||||
});
|
||||
|
||||
export const changeToSystemTheme = createAction({
|
||||
@@ -32,7 +32,7 @@ export const changeToSystemTheme = createAction({
|
||||
keywords: "theme system default",
|
||||
section: SettingsSection,
|
||||
selected: () => stores.ui.theme === "system",
|
||||
perform: () => stores.ui.setTheme("system"),
|
||||
perform: () => stores.ui.setTheme(Theme.System),
|
||||
});
|
||||
|
||||
export const changeTheme = createAction({
|
||||
@@ -1,10 +1,9 @@
|
||||
// @flow
|
||||
import { PlusIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import stores from "stores";
|
||||
import Invite from "scenes/Invite";
|
||||
import { createAction } from "actions";
|
||||
import { UserSection } from "actions/sections";
|
||||
import stores from "~/stores";
|
||||
import Invite from "~/scenes/Invite";
|
||||
import { createAction } from "~/actions";
|
||||
import { UserSection } from "~/actions/sections";
|
||||
|
||||
export const inviteUser = createAction({
|
||||
name: ({ t }) => `${t("Invite people")}…`,
|
||||
@@ -1,17 +1,22 @@
|
||||
// @flow
|
||||
import { flattenDeep } from "lodash";
|
||||
import * as React from "react";
|
||||
import { $Diff } from "utility-types";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import type {
|
||||
import {
|
||||
Action,
|
||||
ActionContext,
|
||||
CommandBarAction,
|
||||
MenuItemClickable,
|
||||
MenuItemButton,
|
||||
MenuItemWithChildren,
|
||||
} from "types";
|
||||
} from "~/types";
|
||||
|
||||
export function createAction(
|
||||
definition: $Diff<Action, { id?: string }>
|
||||
definition: $Diff<
|
||||
Action,
|
||||
{
|
||||
id?: string;
|
||||
}
|
||||
>
|
||||
): Action {
|
||||
return {
|
||||
id: uuidv4(),
|
||||
@@ -22,7 +27,7 @@ export function createAction(
|
||||
export function actionToMenuItem(
|
||||
action: Action,
|
||||
context: ActionContext
|
||||
): MenuItemClickable | MenuItemWithChildren {
|
||||
): MenuItemButton | MenuItemWithChildren {
|
||||
function resolve<T>(value: any): T {
|
||||
if (typeof value === "function") {
|
||||
return value(context);
|
||||
@@ -31,18 +36,20 @@ export function actionToMenuItem(
|
||||
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 visible = action.visible ? action.visible(context) : true;
|
||||
const title = resolve<string>(action.name);
|
||||
const icon =
|
||||
resolvedIcon && action.iconInContextMenu !== false
|
||||
? React.cloneElement(resolvedIcon, { color: "currentColor" })
|
||||
? React.cloneElement(resolvedIcon, {
|
||||
color: "currentColor",
|
||||
})
|
||||
: undefined;
|
||||
|
||||
if (resolvedChildren) {
|
||||
return {
|
||||
type: "submenu",
|
||||
title,
|
||||
icon,
|
||||
items: resolvedChildren
|
||||
@@ -53,6 +60,7 @@ export function actionToMenuItem(
|
||||
}
|
||||
|
||||
return {
|
||||
type: "button",
|
||||
title,
|
||||
icon,
|
||||
visible,
|
||||
@@ -77,12 +85,11 @@ export function actionToKBar(
|
||||
return [];
|
||||
}
|
||||
|
||||
const resolvedIcon = resolve<React.Element<any>>(action.icon);
|
||||
const resolvedIcon = resolve<React.ReactElement<any>>(action.icon);
|
||||
const resolvedChildren = resolve<Action[]>(action.children);
|
||||
const resolvedSection = resolve<string>(action.section);
|
||||
const resolvedName = resolve<string>(action.name);
|
||||
const resolvedPlaceholder = resolve<string>(action.placeholder);
|
||||
|
||||
const children = resolvedChildren
|
||||
? flattenDeep(resolvedChildren.map((a) => actionToKBar(a, context))).filter(
|
||||
(a) => !!a
|
||||
@@ -99,19 +106,17 @@ export function actionToKBar(
|
||||
.filter((c) => !!c.keywords)
|
||||
.map((c) => c.keywords)
|
||||
.join(" ")}`,
|
||||
shortcut: action.shortcut,
|
||||
shortcut: action.shortcut || [],
|
||||
icon: resolvedIcon
|
||||
? React.cloneElement(resolvedIcon, { color: "currentColor" })
|
||||
? React.cloneElement(resolvedIcon, {
|
||||
color: "currentColor",
|
||||
})
|
||||
: undefined,
|
||||
perform: action.perform
|
||||
? () => action.perform && action.perform(context)
|
||||
: undefined,
|
||||
children: children.length ? children.map((a) => a.id) : undefined,
|
||||
},
|
||||
].concat(
|
||||
children.map((child) => ({
|
||||
...child,
|
||||
parent: action.id,
|
||||
}))
|
||||
);
|
||||
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
|
||||
].concat(children.map((child) => ({ ...child, parent: action.id })));
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import { rootCollectionActions } from "./definitions/collections";
|
||||
import { rootDebugActions } from "./definitions/debug";
|
||||
import { rootDocumentActions } from "./definitions/documents";
|
||||
@@ -1,5 +1,4 @@
|
||||
// @flow
|
||||
import { type ActionContext } from "types";
|
||||
import { ActionContext } from "~/types";
|
||||
|
||||
export const CollectionSection = ({ t }: ActionContext) => t("Collection");
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// @flow
|
||||
import styled from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import Flex from "components/Flex";
|
||||
import Flex from "~/components/Flex";
|
||||
|
||||
export const Action = styled(Flex)`
|
||||
justify-content: center;
|
||||
@@ -1,32 +1,30 @@
|
||||
// @flow
|
||||
/* global ga */
|
||||
import * as React from "react";
|
||||
import env from "env";
|
||||
import env from "~/env";
|
||||
|
||||
type Props = {
|
||||
children?: React.Node,
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export default class Analytics extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
if (!env.GOOGLE_ANALYTICS_ID) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
|
||||
// standard Google Analytics script
|
||||
window.ga =
|
||||
window.ga ||
|
||||
function () {
|
||||
// $FlowIssue
|
||||
(ga.q = ga.q || []).push(arguments);
|
||||
function (...args) {
|
||||
(ga.q = ga.q || []).push(args);
|
||||
};
|
||||
|
||||
// $FlowIssue
|
||||
ga.l = +new Date();
|
||||
ga("create", env.GOOGLE_ANALYTICS_ID, "auto");
|
||||
ga("set", { dimension1: "true" });
|
||||
ga("set", {
|
||||
dimension1: "true",
|
||||
});
|
||||
ga("send", "pageview");
|
||||
|
||||
const script = document.createElement("script");
|
||||
script.src = "https://www.google-analytics.com/analytics.js";
|
||||
script.async = true;
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
|
||||
export default function Arrow() {
|
||||
@@ -1,10 +1,9 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
|
||||
type Props = {
|
||||
size?: number,
|
||||
fill?: string,
|
||||
className?: string,
|
||||
size?: number;
|
||||
fill?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function GoogleLogo({ size = 34, fill = "#FFF", className }: Props) {
|
||||
@@ -1,10 +1,9 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
|
||||
type Props = {
|
||||
size?: number,
|
||||
fill?: string,
|
||||
className?: string,
|
||||
size?: number;
|
||||
fill?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function MicrosoftLogo({ size = 34, fill = "#FFF", className }: Props) {
|
||||
@@ -1,10 +1,9 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
|
||||
type Props = {
|
||||
size?: number,
|
||||
fill?: string,
|
||||
className?: string,
|
||||
size?: number;
|
||||
fill?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function SlackLogo({ size = 34, fill = "#FFF", className }: Props) {
|
||||
@@ -1,14 +1,13 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import GoogleLogo from "./GoogleLogo";
|
||||
import MicrosoftLogo from "./MicrosoftLogo";
|
||||
import SlackLogo from "./SlackLogo";
|
||||
|
||||
type Props = {|
|
||||
providerName: string,
|
||||
size?: number,
|
||||
|};
|
||||
type Props = {
|
||||
providerName: string;
|
||||
size?: number;
|
||||
};
|
||||
|
||||
function AuthLogo({ providerName, size = 16 }: Props) {
|
||||
switch (providerName) {
|
||||
@@ -18,18 +17,21 @@ function AuthLogo({ providerName, size = 16 }: Props) {
|
||||
<SlackLogo size={size} />
|
||||
</Logo>
|
||||
);
|
||||
|
||||
case "google":
|
||||
return (
|
||||
<Logo>
|
||||
<GoogleLogo size={size} />
|
||||
</Logo>
|
||||
);
|
||||
|
||||
case "azure":
|
||||
return (
|
||||
<Logo>
|
||||
<MicrosoftLogo size={size} />
|
||||
</Logo>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Redirect } from "react-router-dom";
|
||||
import { isCustomSubdomain } from "shared/utils/domains";
|
||||
import LoadingIndicator from "components/LoadingIndicator";
|
||||
import useStores from "../hooks/useStores";
|
||||
import { changeLanguage } from "../utils/language";
|
||||
import env from "env";
|
||||
import { isCustomSubdomain } from "@shared/utils/domains";
|
||||
import LoadingIndicator from "~/components/LoadingIndicator";
|
||||
import env from "~/env";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import { changeLanguage } from "~/utils/language";
|
||||
|
||||
type Props = {
|
||||
children: React.Node,
|
||||
children: JSX.Element;
|
||||
};
|
||||
|
||||
const Authenticated = ({ children }: Props) => {
|
||||
@@ -1,24 +1,24 @@
|
||||
// @flow
|
||||
import { observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import User from "models/User";
|
||||
import User from "~/models/User";
|
||||
import placeholder from "./placeholder.png";
|
||||
|
||||
type Props = {|
|
||||
src: string,
|
||||
size: number,
|
||||
icon?: React.Node,
|
||||
user?: User,
|
||||
alt?: string,
|
||||
onClick?: () => void,
|
||||
className?: string,
|
||||
|};
|
||||
type Props = {
|
||||
src: string;
|
||||
size: number;
|
||||
icon?: React.ReactNode;
|
||||
user?: User;
|
||||
alt?: string;
|
||||
onClick?: () => void;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
@observer
|
||||
class Avatar extends React.Component<Props> {
|
||||
@observable error: boolean;
|
||||
@observable
|
||||
error: boolean;
|
||||
|
||||
static defaultProps = {
|
||||
size: 24,
|
||||
@@ -30,7 +30,6 @@ class Avatar extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { src, icon, ...rest } = this.props;
|
||||
|
||||
return (
|
||||
<AvatarWrapper>
|
||||
<CircleImg
|
||||
@@ -60,7 +59,7 @@ const IconWrapper = styled.div`
|
||||
height: 20px;
|
||||
`;
|
||||
|
||||
const CircleImg = styled.img`
|
||||
const CircleImg = styled.img<{ size: number }>`
|
||||
display: block;
|
||||
width: ${(props) => props.size}px;
|
||||
height: ${(props) => props.size}px;
|
||||
@@ -1,26 +1,25 @@
|
||||
// @flow
|
||||
import { observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { withTranslation, type TFunction } from "react-i18next";
|
||||
import { WithTranslation, withTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import User from "models/User";
|
||||
import UserProfile from "scenes/UserProfile";
|
||||
import Avatar from "components/Avatar";
|
||||
import Tooltip from "components/Tooltip";
|
||||
import User from "~/models/User";
|
||||
import UserProfile from "~/scenes/UserProfile";
|
||||
import Avatar from "~/components/Avatar";
|
||||
import Tooltip from "~/components/Tooltip";
|
||||
|
||||
type Props = {
|
||||
user: User,
|
||||
isPresent: boolean,
|
||||
isEditing: boolean,
|
||||
isCurrentUser: boolean,
|
||||
profileOnClick: boolean,
|
||||
t: TFunction,
|
||||
type Props = WithTranslation & {
|
||||
user: User;
|
||||
isPresent: boolean;
|
||||
isEditing: boolean;
|
||||
isCurrentUser: boolean;
|
||||
profileOnClick: boolean;
|
||||
};
|
||||
|
||||
@observer
|
||||
class AvatarWithPresence extends React.Component<Props> {
|
||||
@observable isOpen: boolean = false;
|
||||
@observable
|
||||
isOpen = false;
|
||||
|
||||
handleOpenProfile = () => {
|
||||
this.isOpen = true;
|
||||
@@ -32,13 +31,11 @@ class AvatarWithPresence extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { user, isPresent, isEditing, isCurrentUser, t } = this.props;
|
||||
|
||||
const action = isPresent
|
||||
? isEditing
|
||||
? t("currently editing")
|
||||
: t("currently viewing")
|
||||
: t("previously edited");
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip
|
||||
@@ -81,9 +78,9 @@ const Centered = styled.div`
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const AvatarWrapper = styled.div`
|
||||
const AvatarWrapper = styled.div<{ isPresent: boolean }>`
|
||||
opacity: ${(props) => (props.isPresent ? 1 : 0.5)};
|
||||
transition: opacity 250ms ease-in-out;
|
||||
`;
|
||||
|
||||
export default withTranslation()<AvatarWithPresence>(AvatarWithPresence);
|
||||
export default withTranslation()(AvatarWithPresence);
|
||||
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
import Avatar from "./Avatar";
|
||||
import AvatarWithPresence from "./AvatarWithPresence";
|
||||
|
||||
export { AvatarWithPresence };
|
||||
|
||||
export default Avatar;
|
||||
@@ -1,7 +1,6 @@
|
||||
// @flow
|
||||
import styled from "styled-components";
|
||||
|
||||
const Badge = styled.span`
|
||||
const Badge = styled.span<{ yellow?: boolean; primary?: boolean }>`
|
||||
margin-left: 10px;
|
||||
padding: 1px 5px 2px;
|
||||
background-color: ${({ yellow, primary, theme }) =>
|
||||
@@ -1,11 +1,10 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import env from "~/env";
|
||||
import OutlineLogo from "./OutlineLogo";
|
||||
import env from "env";
|
||||
|
||||
type Props = {
|
||||
href?: string,
|
||||
href?: string;
|
||||
};
|
||||
|
||||
function Branding({ href = env.URL }: Props) {
|
||||
@@ -1,35 +1,36 @@
|
||||
// @flow
|
||||
import { GoToIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import Flex from "components/Flex";
|
||||
import BreadcrumbMenu from "menus/BreadcrumbMenu";
|
||||
import Flex from "~/components/Flex";
|
||||
import BreadcrumbMenu from "~/menus/BreadcrumbMenu";
|
||||
import { MenuInternalLink } from "~/types";
|
||||
|
||||
type MenuItem = {|
|
||||
icon?: React.Node,
|
||||
title: React.Node,
|
||||
to?: string,
|
||||
|};
|
||||
export type Crumb = {
|
||||
title: React.ReactNode;
|
||||
icon?: React.ReactNode;
|
||||
to?: string;
|
||||
};
|
||||
|
||||
type Props = {|
|
||||
items: MenuItem[],
|
||||
max?: number,
|
||||
children?: React.Node,
|
||||
highlightFirstItem?: boolean,
|
||||
|};
|
||||
type Props = {
|
||||
items: Crumb[];
|
||||
max?: number;
|
||||
children?: React.ReactNode;
|
||||
highlightFirstItem?: boolean;
|
||||
};
|
||||
|
||||
function Breadcrumb({ items, highlightFirstItem, children, max = 2 }: Props) {
|
||||
const totalItems = items.length;
|
||||
let topLevelItems: MenuItem[] = [...items];
|
||||
const topLevelItems: Crumb[] = [...items];
|
||||
let overflowItems;
|
||||
|
||||
// chop middle breadcrumbs and present a "..." menu instead
|
||||
if (totalItems > max) {
|
||||
const halfMax = Math.floor(max / 2);
|
||||
overflowItems = topLevelItems.splice(halfMax, totalItems - max);
|
||||
|
||||
topLevelItems.splice(halfMax, 0, {
|
||||
title: <BreadcrumbMenu items={overflowItems} />,
|
||||
title: <BreadcrumbMenu items={overflowItems as MenuInternalLink[]} />,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -42,7 +43,7 @@ function Breadcrumb({ items, highlightFirstItem, children, max = 2 }: Props) {
|
||||
<Item
|
||||
to={item.to}
|
||||
$withIcon={!!item.icon}
|
||||
$highlight={highlightFirstItem && index === 0}
|
||||
$highlight={!!highlightFirstItem && index === 0}
|
||||
>
|
||||
{item.title}
|
||||
</Item>
|
||||
@@ -62,7 +63,7 @@ const Slash = styled(GoToIcon)`
|
||||
fill: ${(props) => props.theme.divider};
|
||||
`;
|
||||
|
||||
const Item = styled(Link)`
|
||||
const Item = styled(Link)<{ $highlight: boolean; $withIcon: boolean }>`
|
||||
display: flex;
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
@@ -1,11 +1,10 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import { bounceIn } from "styles/animations";
|
||||
import { bounceIn } from "~/styles/animations";
|
||||
|
||||
type Props = {|
|
||||
count: number,
|
||||
|};
|
||||
type Props = {
|
||||
count: number;
|
||||
};
|
||||
|
||||
const Bubble = ({ count }: Props) => {
|
||||
if (!count) {
|
||||
@@ -1,10 +1,15 @@
|
||||
// @flow
|
||||
import { ExpandedIcon } from "outline-icons";
|
||||
import { darken } from "polished";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
const RealButton = styled.button`
|
||||
const RealButton = styled.button<{
|
||||
fullwidth?: boolean;
|
||||
borderOnHover?: boolean;
|
||||
neutral?: boolean;
|
||||
danger?: boolean;
|
||||
iconColor?: string;
|
||||
}>`
|
||||
display: ${(props) => (props.fullwidth ? "block" : "inline-block")};
|
||||
width: ${(props) => (props.fullwidth ? "100%" : "auto")};
|
||||
margin: 0;
|
||||
@@ -50,7 +55,7 @@ const RealButton = styled.button`
|
||||
}
|
||||
|
||||
${(props) =>
|
||||
props.$neutral &&
|
||||
props.neutral &&
|
||||
`
|
||||
background: ${props.theme.buttonNeutralBackground};
|
||||
color: ${props.theme.buttonNeutralText};
|
||||
@@ -87,7 +92,9 @@ const RealButton = styled.button`
|
||||
fill: ${props.theme.textTertiary};
|
||||
}
|
||||
}
|
||||
`} ${(props) =>
|
||||
`}
|
||||
|
||||
${(props) =>
|
||||
props.danger &&
|
||||
`
|
||||
background: ${props.theme.danger};
|
||||
@@ -99,7 +106,7 @@ const RealButton = styled.button`
|
||||
`};
|
||||
`;
|
||||
|
||||
const Label = styled.span`
|
||||
const Label = styled.span<{ hasIcon?: boolean }>`
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
@@ -107,7 +114,11 @@ const Label = styled.span`
|
||||
${(props) => props.hasIcon && "padding-left: 4px;"};
|
||||
`;
|
||||
|
||||
export const Inner = styled.span`
|
||||
export const Inner = styled.span<{
|
||||
disclosure?: boolean;
|
||||
hasIcon?: boolean;
|
||||
hasText?: boolean;
|
||||
}>`
|
||||
display: flex;
|
||||
padding: 0 8px;
|
||||
padding-right: ${(props) => (props.disclosure ? 2 : 8)}px;
|
||||
@@ -120,58 +131,41 @@ export const Inner = styled.span`
|
||||
${(props) => props.hasIcon && !props.hasText && "padding: 0 4px;"};
|
||||
`;
|
||||
|
||||
export type Props = {|
|
||||
type?: "button" | "submit",
|
||||
value?: string,
|
||||
icon?: React.Node,
|
||||
iconColor?: string,
|
||||
className?: string,
|
||||
children?: React.Node,
|
||||
innerRef?: React.ElementRef<any>,
|
||||
disclosure?: boolean,
|
||||
neutral?: boolean,
|
||||
danger?: boolean,
|
||||
primary?: boolean,
|
||||
disabled?: boolean,
|
||||
fullwidth?: boolean,
|
||||
autoFocus?: boolean,
|
||||
style?: Object,
|
||||
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,
|
||||
|};
|
||||
export type Props<T> = {
|
||||
icon?: React.ReactNode;
|
||||
iconColor?: string;
|
||||
children?: React.ReactNode;
|
||||
disclosure?: boolean;
|
||||
neutral?: boolean;
|
||||
danger?: boolean;
|
||||
primary?: boolean;
|
||||
fullwidth?: boolean;
|
||||
as?: T;
|
||||
to?: string;
|
||||
borderOnHover?: boolean;
|
||||
href?: string;
|
||||
"data-on"?: string;
|
||||
"data-event-category"?: string;
|
||||
"data-event-action"?: string;
|
||||
};
|
||||
|
||||
const Button = React.forwardRef<Props, HTMLButtonElement>(
|
||||
(
|
||||
{
|
||||
type = "text",
|
||||
icon,
|
||||
children,
|
||||
value,
|
||||
disclosure,
|
||||
neutral,
|
||||
...rest
|
||||
}: Props,
|
||||
innerRef
|
||||
) => {
|
||||
const hasText = children !== undefined || value !== undefined;
|
||||
const hasIcon = icon !== undefined;
|
||||
const Button = <T extends React.ElementType = "button">(
|
||||
props: Props<T> & React.ComponentPropsWithoutRef<T>,
|
||||
ref: React.Ref<HTMLButtonElement>
|
||||
) => {
|
||||
const { type, icon, children, value, disclosure, neutral, ...rest } = props;
|
||||
const hasText = children !== undefined || value !== undefined;
|
||||
const hasIcon = icon !== undefined;
|
||||
|
||||
return (
|
||||
<RealButton type={type} ref={innerRef} $neutral={neutral} {...rest}>
|
||||
<Inner hasIcon={hasIcon} hasText={hasText} disclosure={disclosure}>
|
||||
{hasIcon && icon}
|
||||
{hasText && <Label hasIcon={hasIcon}>{children || value}</Label>}
|
||||
{disclosure && <ExpandedIcon />}
|
||||
</Inner>
|
||||
</RealButton>
|
||||
);
|
||||
}
|
||||
);
|
||||
return (
|
||||
<RealButton type={type || "button"} ref={ref} neutral={neutral} {...rest}>
|
||||
<Inner hasIcon={hasIcon} hasText={hasText} disclosure={disclosure}>
|
||||
{hasIcon && icon}
|
||||
{hasText && <Label hasIcon={hasIcon}>{children || value}</Label>}
|
||||
{disclosure && <ExpandedIcon />}
|
||||
</Inner>
|
||||
</RealButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default Button;
|
||||
export default React.forwardRef(Button);
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import styled from "styled-components";
|
||||
import Button, { Inner } from "./Button";
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
type Props = {
|
||||
onClick: (ev: SyntheticEvent<>) => void,
|
||||
children: React.Node,
|
||||
onClick: React.MouseEventHandler<HTMLButtonElement>;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function ButtonLink(props: Props) {
|
||||
@@ -1,20 +1,19 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
|
||||
type Props = {|
|
||||
children?: React.Node,
|
||||
withStickyHeader?: boolean,
|
||||
|};
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
withStickyHeader?: boolean;
|
||||
};
|
||||
|
||||
const Container = styled.div`
|
||||
const Container = styled.div<{ withStickyHeader?: boolean }>`
|
||||
width: 100%;
|
||||
max-width: 100vw;
|
||||
padding: ${(props) => (props.withStickyHeader ? "4px 12px" : "60px 12px")};
|
||||
|
||||
${breakpoint("tablet")`
|
||||
padding: ${(props) => (props.withStickyHeader ? "4px 60px" : "60px")};
|
||||
padding: ${(props: any) => (props.withStickyHeader ? "4px 60px" : "60px")};
|
||||
`};
|
||||
`;
|
||||
|
||||
@@ -1,29 +1,27 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { VisuallyHidden } from "reakit/VisuallyHidden";
|
||||
import styled from "styled-components";
|
||||
import HelpText from "components/HelpText";
|
||||
import HelpText from "~/components/HelpText";
|
||||
|
||||
export type Props = {|
|
||||
checked?: boolean,
|
||||
label?: React.Node,
|
||||
labelHidden?: boolean,
|
||||
className?: string,
|
||||
name?: string,
|
||||
disabled?: boolean,
|
||||
onChange: (event: SyntheticInputEvent<HTMLInputElement>) => mixed,
|
||||
note?: React.Node,
|
||||
short?: boolean,
|
||||
small?: boolean,
|
||||
|};
|
||||
export type Props = {
|
||||
checked?: boolean;
|
||||
label?: React.ReactNode;
|
||||
labelHidden?: boolean;
|
||||
className?: string;
|
||||
name?: string;
|
||||
disabled?: boolean;
|
||||
onChange: (event: React.ChangeEvent<HTMLInputElement>) => unknown;
|
||||
note?: React.ReactNode;
|
||||
small?: boolean;
|
||||
};
|
||||
|
||||
const LabelText = styled.span`
|
||||
const LabelText = styled.span<{ small?: boolean }>`
|
||||
font-weight: 500;
|
||||
margin-left: ${(props) => (props.small ? "6px" : "10px")};
|
||||
${(props) => (props.small ? `color: ${props.theme.textSecondary}` : "")};
|
||||
`;
|
||||
|
||||
const Wrapper = styled.div`
|
||||
const Wrapper = styled.div<{ small?: boolean }>`
|
||||
padding-bottom: 8px;
|
||||
${(props) => (props.small ? "font-size: 14px" : "")};
|
||||
width: 100%;
|
||||
@@ -41,14 +39,13 @@ export default function Checkbox({
|
||||
note,
|
||||
className,
|
||||
small,
|
||||
short,
|
||||
...rest
|
||||
}: Props) {
|
||||
const wrappedLabel = <LabelText small={small}>{label}</LabelText>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Wrapper small={small}>
|
||||
<Wrapper small={small} className={className}>
|
||||
<Label>
|
||||
<input type="checkbox" {...rest} />
|
||||
{label &&
|
||||
@@ -1,8 +1,7 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import styled, { useTheme } from "styled-components";
|
||||
|
||||
const cleanPercentage = (percentage) => {
|
||||
const cleanPercentage = (percentage: number) => {
|
||||
const tooLow = !Number.isFinite(+percentage) || percentage < 0;
|
||||
const tooHigh = percentage > 100;
|
||||
return tooLow ? 0 : tooHigh ? 100 : +percentage;
|
||||
@@ -13,13 +12,14 @@ const Circle = ({
|
||||
percentage,
|
||||
offset,
|
||||
}: {
|
||||
color: string,
|
||||
percentage?: number,
|
||||
offset: number,
|
||||
color: string;
|
||||
percentage?: number;
|
||||
offset: number;
|
||||
}) => {
|
||||
const radius = offset * 0.7;
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
let strokePercentage;
|
||||
|
||||
if (percentage) {
|
||||
// because the circle is so small, anything greater than 85% appears like 100%
|
||||
percentage = percentage > 85 && percentage < 100 ? 85 : percentage;
|
||||
@@ -39,7 +39,9 @@ const Circle = ({
|
||||
strokeDasharray={circumference}
|
||||
strokeDashoffset={percentage ? strokePercentage : 0}
|
||||
strokeLinecap="round"
|
||||
style={{ transition: "stroke-dashoffset 0.6s ease 0s" }}
|
||||
style={{
|
||||
transition: "stroke-dashoffset 0.6s ease 0s",
|
||||
}}
|
||||
></circle>
|
||||
);
|
||||
};
|
||||
@@ -48,8 +50,8 @@ const CircularProgressBar = ({
|
||||
percentage,
|
||||
size = 16,
|
||||
}: {
|
||||
percentage: number,
|
||||
size?: number,
|
||||
percentage: number;
|
||||
size?: number;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
percentage = cleanPercentage(percentage);
|
||||
@@ -1,7 +1,6 @@
|
||||
// @flow
|
||||
import styled from "styled-components";
|
||||
|
||||
const ClickablePadding = styled.div`
|
||||
const ClickablePadding = styled.div<{ grow?: boolean }>`
|
||||
min-height: 10em;
|
||||
cursor: ${({ onClick }) => (onClick ? "text" : "default")};
|
||||
${({ grow }) => grow && `flex-grow: 100;`};
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import { sortBy, filter, uniq, isEqual } from "lodash";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
@@ -6,18 +5,18 @@ import { useTranslation } from "react-i18next";
|
||||
import { usePopoverState, PopoverDisclosure } from "reakit/Popover";
|
||||
import styled from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import Document from "models/Document";
|
||||
import { AvatarWithPresence } from "components/Avatar";
|
||||
import DocumentViews from "components/DocumentViews";
|
||||
import Facepile from "components/Facepile";
|
||||
import NudeButton from "components/NudeButton";
|
||||
import Popover from "components/Popover";
|
||||
import useCurrentUser from "hooks/useCurrentUser";
|
||||
import useStores from "hooks/useStores";
|
||||
import Document from "~/models/Document";
|
||||
import { AvatarWithPresence } from "~/components/Avatar";
|
||||
import DocumentViews from "~/components/DocumentViews";
|
||||
import Facepile from "~/components/Facepile";
|
||||
import NudeButton from "~/components/NudeButton";
|
||||
import Popover from "~/components/Popover";
|
||||
import useCurrentUser from "~/hooks/useCurrentUser";
|
||||
import useStores from "~/hooks/useStores";
|
||||
|
||||
type Props = {|
|
||||
document: Document,
|
||||
|};
|
||||
type Props = {
|
||||
document: Document;
|
||||
};
|
||||
|
||||
function Collaborators(props: Props) {
|
||||
const { t } = useTranslation();
|
||||
@@ -26,14 +25,13 @@ function Collaborators(props: Props) {
|
||||
const [requestedUserIds, setRequestedUserIds] = React.useState<string[]>([]);
|
||||
const { users, presence } = useStores();
|
||||
const { document } = props;
|
||||
|
||||
let documentPresence = presence.get(document.id);
|
||||
documentPresence = documentPresence
|
||||
const documentPresence = presence.get(document.id);
|
||||
const documentPresenceArray = documentPresence
|
||||
? Array.from(documentPresence.values())
|
||||
: [];
|
||||
|
||||
const presentIds = documentPresence.map((p) => p.userId);
|
||||
const editingIds = documentPresence
|
||||
const presentIds = documentPresenceArray.map((p) => p.userId);
|
||||
const editingIds = documentPresenceArray
|
||||
.filter((p) => p.isEditing)
|
||||
.map((p) => p.userId);
|
||||
|
||||
@@ -83,7 +81,6 @@ function Collaborators(props: Props) {
|
||||
renderAvatar={(user) => {
|
||||
const isPresent = presentIds.includes(user.id);
|
||||
const isEditing = editingIds.includes(user.id);
|
||||
|
||||
return (
|
||||
<AvatarWithPresence
|
||||
key={user.id}
|
||||
@@ -1,22 +1,21 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import { transparentize } from "polished";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import Collection from "models/Collection";
|
||||
import Arrow from "components/Arrow";
|
||||
import ButtonLink from "components/ButtonLink";
|
||||
import Editor from "components/Editor";
|
||||
import LoadingIndicator from "components/LoadingIndicator";
|
||||
import NudeButton from "components/NudeButton";
|
||||
import useDebouncedCallback from "hooks/useDebouncedCallback";
|
||||
import useStores from "hooks/useStores";
|
||||
import useToasts from "hooks/useToasts";
|
||||
import Collection from "~/models/Collection";
|
||||
import Arrow from "~/components/Arrow";
|
||||
import ButtonLink from "~/components/ButtonLink";
|
||||
import Editor from "~/components/Editor";
|
||||
import LoadingIndicator from "~/components/LoadingIndicator";
|
||||
import NudeButton from "~/components/NudeButton";
|
||||
import useDebouncedCallback from "~/hooks/useDebouncedCallback";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import useToasts from "~/hooks/useToasts";
|
||||
|
||||
type Props = {|
|
||||
collection: Collection,
|
||||
|};
|
||||
type Props = {
|
||||
collection: Collection;
|
||||
};
|
||||
|
||||
function CollectionDescription({ collection }: Props) {
|
||||
const { collections, policies } = useStores();
|
||||
@@ -40,6 +39,7 @@ function CollectionDescription({ collection }: Props) {
|
||||
event.preventDefault();
|
||||
|
||||
if (isExpanded && document.activeElement) {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'blur' does not exist on type 'Element'.
|
||||
document.activeElement.blur();
|
||||
}
|
||||
|
||||
@@ -75,20 +75,16 @@ function CollectionDescription({ collection }: Props) {
|
||||
React.useEffect(() => {
|
||||
setEditing(false);
|
||||
}, [collection.id]);
|
||||
|
||||
const placeholder = `${t("Add a description")}…`;
|
||||
const key = isEditing || isDirty ? "draft" : collection.updatedAt;
|
||||
|
||||
return (
|
||||
<MaxHeight data-editing={isEditing} data-expanded={isExpanded}>
|
||||
<Input
|
||||
$isEditable={can.update}
|
||||
data-editing={isEditing}
|
||||
data-expanded={isExpanded}
|
||||
>
|
||||
<Input data-editing={isEditing} data-expanded={isExpanded}>
|
||||
<span onClick={can.update ? handleStartEditing : undefined}>
|
||||
{collections.isSaving && <LoadingIndicator />}
|
||||
{collection.hasDescription || isEditing || isDirty ? (
|
||||
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
|
||||
<React.Suspense fallback={<Placeholder>Loading…</Placeholder>}>
|
||||
<Editor
|
||||
key={key}
|
||||
@@ -105,7 +101,15 @@ function CollectionDescription({ collection }: Props) {
|
||||
/>
|
||||
</React.Suspense>
|
||||
) : (
|
||||
can.update && <Placeholder>{placeholder}</Placeholder>
|
||||
can.update && (
|
||||
<Placeholder
|
||||
onClick={() => {
|
||||
//
|
||||
}}
|
||||
>
|
||||
{placeholder}
|
||||
</Placeholder>
|
||||
)
|
||||
)}
|
||||
</span>
|
||||
</Input>
|
||||
@@ -1,16 +1,15 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import { CollectionIcon } from "outline-icons";
|
||||
import { getLuminance } from "polished";
|
||||
import * as React from "react";
|
||||
import Collection from "models/Collection";
|
||||
import { icons } from "components/IconPicker";
|
||||
import useStores from "hooks/useStores";
|
||||
import Collection from "~/models/Collection";
|
||||
import { icons } from "~/components/IconPicker";
|
||||
import useStores from "~/hooks/useStores";
|
||||
|
||||
type Props = {
|
||||
collection: Collection,
|
||||
expanded?: boolean,
|
||||
size?: number,
|
||||
collection: Collection;
|
||||
expanded?: boolean;
|
||||
size?: number;
|
||||
};
|
||||
|
||||
function ResolvedCollectionIcon({ collection, expanded, size }: Props) {
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import { useKBar, KBarPositioner, KBarAnimator, KBarSearch } from "kbar";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
@@ -6,9 +5,10 @@ import { useTranslation } from "react-i18next";
|
||||
import { Portal } from "react-portal";
|
||||
import styled from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import CommandBarResults from "components/CommandBarResults";
|
||||
import rootActions from "actions/root";
|
||||
import useCommandBarActions from "hooks/useCommandBarActions";
|
||||
import CommandBarResults from "~/components/CommandBarResults";
|
||||
import rootActions from "~/actions/root";
|
||||
import useCommandBarActions from "~/hooks/useCommandBarActions";
|
||||
import { CommandBarAction } from "~/types";
|
||||
|
||||
export const CommandBarOptions = {
|
||||
animations: {
|
||||
@@ -19,11 +19,12 @@ export const CommandBarOptions = {
|
||||
|
||||
function CommandBar() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
useCommandBarActions(rootActions);
|
||||
|
||||
const { rootAction } = useKBar((state) => ({
|
||||
rootAction: state.actions[state.currentRootActionId],
|
||||
rootAction: state.currentRootActionId
|
||||
? (state.actions[state.currentRootActionId] as CommandBarAction)
|
||||
: undefined,
|
||||
}));
|
||||
|
||||
return (
|
||||
@@ -44,7 +45,7 @@ function CommandBar() {
|
||||
);
|
||||
}
|
||||
|
||||
function KBarPortal({ children }: { children: React.Node }) {
|
||||
function KBarPortal({ children }: { children: React.ReactNode }) {
|
||||
const { showing } = useKBar((state) => ({
|
||||
showing: state.visualState !== "hidden",
|
||||
}));
|
||||
@@ -1,23 +1,27 @@
|
||||
// @flow
|
||||
import { BackIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import Flex from "components/Flex";
|
||||
import Key from "components/Key";
|
||||
import type { CommandBarAction } from "types";
|
||||
import Flex from "~/components/Flex";
|
||||
import Key from "~/components/Key";
|
||||
import { CommandBarAction } from "~/types";
|
||||
|
||||
type Props = {|
|
||||
action: CommandBarAction,
|
||||
active: Boolean,
|
||||
|};
|
||||
type Props = {
|
||||
action: CommandBarAction;
|
||||
active: boolean;
|
||||
};
|
||||
|
||||
function CommandBarItem({ action, active }: Props, ref) {
|
||||
function CommandBarItem(
|
||||
{ action, active }: Props,
|
||||
ref: React.RefObject<HTMLDivElement>
|
||||
) {
|
||||
return (
|
||||
<Item active={active} ref={ref}>
|
||||
<Text align="center" gap={8}>
|
||||
<Icon>
|
||||
{action.icon ? (
|
||||
React.cloneElement(action.icon, { size: 22 })
|
||||
React.cloneElement(action.icon, {
|
||||
size: 22,
|
||||
})
|
||||
) : (
|
||||
<ForwardIcon color="currentColor" size={22} />
|
||||
)}
|
||||
@@ -26,8 +30,14 @@ function CommandBarItem({ action, active }: Props, ref) {
|
||||
{action.children?.length ? "…" : ""}
|
||||
</Text>
|
||||
{action.shortcut?.length ? (
|
||||
<div style={{ display: "grid", gridAutoFlow: "column", gap: "4px" }}>
|
||||
{action.shortcut.map((sc) => (
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridAutoFlow: "column",
|
||||
gap: "4px",
|
||||
}}
|
||||
>
|
||||
{action.shortcut.map((sc: string) => (
|
||||
<Key key={sc}>{sc}</Key>
|
||||
))}
|
||||
</div>
|
||||
@@ -48,7 +58,7 @@ const Text = styled(Flex)`
|
||||
flex-shrink: 1;
|
||||
`;
|
||||
|
||||
const Item = styled.div`
|
||||
const Item = styled.div<{ active?: boolean }>`
|
||||
font-size: 15px;
|
||||
padding: 12px 16px;
|
||||
background: ${(props) =>
|
||||
@@ -68,4 +78,4 @@ const ForwardIcon = styled(BackIcon)`
|
||||
transform: rotate(180deg);
|
||||
`;
|
||||
|
||||
export default React.forwardRef<Props, HTMLDivElement>(CommandBarItem);
|
||||
export default React.forwardRef<HTMLDivElement, Props>(CommandBarItem);
|
||||
@@ -1,8 +1,8 @@
|
||||
// @flow
|
||||
import { useMatches, KBarResults, NO_GROUP } from "kbar";
|
||||
import { useMatches, KBarResults, Action } from "kbar";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import CommandBarItem from "components/CommandBarItem";
|
||||
import CommandBarItem from "~/components/CommandBarItem";
|
||||
import { CommandBarAction } from "~/types";
|
||||
|
||||
export default function CommandBarResults() {
|
||||
const matches = useMatches();
|
||||
@@ -14,8 +14,8 @@ export default function CommandBarResults() {
|
||||
acc.push(name);
|
||||
acc.push(...actions);
|
||||
return acc;
|
||||
}, [])
|
||||
.filter((i) => i !== NO_GROUP),
|
||||
}, [] as (Action | string)[])
|
||||
.filter((i) => i !== "none"),
|
||||
[matches]
|
||||
);
|
||||
|
||||
@@ -27,7 +27,7 @@ export default function CommandBarResults() {
|
||||
typeof item === "string" ? (
|
||||
<Header>{item}</Header>
|
||||
) : (
|
||||
<CommandBarItem action={item} active={active} />
|
||||
<CommandBarItem action={item as CommandBarAction} active={active} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
@@ -1,14 +1,13 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import { DisconnectedIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled, { useTheme } from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import Fade from "components/Fade";
|
||||
import NudeButton from "components/NudeButton";
|
||||
import Tooltip from "components/Tooltip";
|
||||
import useStores from "hooks/useStores";
|
||||
import Fade from "~/components/Fade";
|
||||
import NudeButton from "~/components/NudeButton";
|
||||
import Tooltip from "~/components/Tooltip";
|
||||
import useStores from "~/hooks/useStores";
|
||||
|
||||
function ConnectionStatus() {
|
||||
const { ui } = useStores();
|
||||
@@ -1,22 +1,20 @@
|
||||
// @flow
|
||||
import isPrintableKeyEvent from "is-printable-key-event";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
type Props = {|
|
||||
disabled?: boolean,
|
||||
readOnly?: boolean,
|
||||
onChange?: (text: string) => void,
|
||||
onBlur?: (event: SyntheticInputEvent<>) => void,
|
||||
onInput?: (event: SyntheticInputEvent<>) => void,
|
||||
onKeyDown?: (event: SyntheticInputEvent<>) => void,
|
||||
placeholder?: string,
|
||||
maxLength?: number,
|
||||
autoFocus?: boolean,
|
||||
className?: string,
|
||||
children?: React.Node,
|
||||
value: string,
|
||||
|};
|
||||
type Props = Omit<React.HTMLAttributes<HTMLSpanElement>, "ref" | "onChange"> & {
|
||||
disabled?: boolean;
|
||||
readOnly?: boolean;
|
||||
onChange?: (text: string) => void;
|
||||
onBlur?: React.FocusEventHandler<HTMLSpanElement> | undefined;
|
||||
onInput?: React.FormEventHandler<HTMLSpanElement> | undefined;
|
||||
onKeyDown?: React.KeyboardEventHandler<HTMLSpanElement> | undefined;
|
||||
placeholder?: string;
|
||||
maxLength?: number;
|
||||
autoFocus?: boolean;
|
||||
children?: React.ReactNode;
|
||||
value: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Defines a content editable component with the same interface as a native
|
||||
@@ -37,18 +35,22 @@ function ContentEditable({
|
||||
readOnly,
|
||||
...rest
|
||||
}: Props) {
|
||||
const ref = React.useRef<?HTMLSpanElement>();
|
||||
const ref = React.useRef<HTMLSpanElement>(null);
|
||||
const [innerHTML, setInnerHTML] = React.useState<string>(value);
|
||||
const lastValue = React.useRef("");
|
||||
|
||||
const wrappedEvent = (callback) => (
|
||||
event: SyntheticInputEvent<HTMLInputElement>
|
||||
) => {
|
||||
const wrappedEvent = (
|
||||
callback:
|
||||
| React.FocusEventHandler<HTMLSpanElement>
|
||||
| React.FormEventHandler<HTMLSpanElement>
|
||||
| React.KeyboardEventHandler<HTMLSpanElement>
|
||||
| undefined
|
||||
) => (event: any) => {
|
||||
const text = ref.current?.innerText || "";
|
||||
|
||||
if (maxLength && isPrintableKeyEvent(event) && text.length >= maxLength) {
|
||||
event.preventDefault();
|
||||
return false;
|
||||
event?.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if (text !== lastValue.current) {
|
||||
@@ -56,7 +58,7 @@ function ContentEditable({
|
||||
onChange && onChange(text);
|
||||
}
|
||||
|
||||
callback && callback(event);
|
||||
callback?.(event);
|
||||
};
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
@@ -74,14 +76,16 @@ function ContentEditable({
|
||||
return (
|
||||
<div className={className}>
|
||||
<Content
|
||||
ref={ref}
|
||||
contentEditable={!disabled && !readOnly}
|
||||
onInput={wrappedEvent(onInput)}
|
||||
onBlur={wrappedEvent(onBlur)}
|
||||
onKeyDown={wrappedEvent(onKeyDown)}
|
||||
ref={ref}
|
||||
data-placeholder={placeholder}
|
||||
role="textbox"
|
||||
dangerouslySetInnerHTML={{ __html: innerHTML }}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: innerHTML,
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
{children}
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import styled from "styled-components";
|
||||
|
||||
const Header = styled.h3`
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import { CheckmarkIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { MenuItem as BaseMenuItem } from "reakit/Menu";
|
||||
@@ -6,19 +5,19 @@ import styled, { css } from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import MenuIconWrapper from "../MenuIconWrapper";
|
||||
|
||||
type Props = {|
|
||||
onClick?: (SyntheticEvent<>) => void | Promise<void>,
|
||||
children?: React.Node,
|
||||
selected?: boolean,
|
||||
disabled?: boolean,
|
||||
to?: string,
|
||||
href?: string,
|
||||
target?: "_blank",
|
||||
as?: string | React.ComponentType<*>,
|
||||
hide?: () => void,
|
||||
level?: number,
|
||||
icon?: React.Node,
|
||||
|};
|
||||
type Props = {
|
||||
onClick?: (arg0: React.SyntheticEvent) => void | Promise<void>;
|
||||
children?: React.ReactNode;
|
||||
selected?: boolean;
|
||||
disabled?: boolean;
|
||||
to?: string;
|
||||
href?: string;
|
||||
target?: "_blank";
|
||||
as?: string | React.ComponentType<any>;
|
||||
hide?: () => void;
|
||||
level?: number;
|
||||
icon?: React.ReactNode;
|
||||
};
|
||||
|
||||
const MenuItem = ({
|
||||
onClick,
|
||||
@@ -88,7 +87,7 @@ const Spacer = styled.svg`
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
export const MenuAnchorCSS = css`
|
||||
export const MenuAnchorCSS = css<{ level?: number; disabled?: boolean }>`
|
||||
display: flex;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
@@ -138,6 +137,7 @@ export const MenuAnchorCSS = css`
|
||||
font-size: 14px;
|
||||
`};
|
||||
`;
|
||||
|
||||
export const MenuAnchor = styled.a`
|
||||
${MenuAnchorCSS}
|
||||
`;
|
||||
@@ -1,14 +1,18 @@
|
||||
// @flow
|
||||
import { MoreIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
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({
|
||||
iconColor,
|
||||
className,
|
||||
...rest
|
||||
}: any) {
|
||||
}: Props) {
|
||||
return (
|
||||
<MenuButton {...rest}>
|
||||
{(props) => (
|
||||
@@ -1,9 +1,8 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { MenuSeparator } from "reakit/Menu";
|
||||
import styled from "styled-components";
|
||||
|
||||
export default function Separator(rest: {}) {
|
||||
export default function Separator(rest: any) {
|
||||
return (
|
||||
<MenuSeparator {...rest}>
|
||||
{(props) => <HorizontalRule {...props} />}
|
||||
@@ -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);
|
||||
225
app/components/ContextMenu/Template.tsx
Normal file
225
app/components/ContextMenu/Template.tsx
Normal 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);
|
||||
@@ -1,31 +1,45 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { Portal } from "react-portal";
|
||||
import { Menu } from "reakit/Menu";
|
||||
import styled from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import useMenuHeight from "hooks/useMenuHeight";
|
||||
import usePrevious from "hooks/usePrevious";
|
||||
import useMenuHeight from "~/hooks/useMenuHeight";
|
||||
import usePrevious from "~/hooks/usePrevious";
|
||||
import {
|
||||
fadeIn,
|
||||
fadeAndSlideUp,
|
||||
fadeAndSlideDown,
|
||||
mobileContextMenu,
|
||||
} from "styles/animations";
|
||||
} from "~/styles/animations";
|
||||
|
||||
type Props = {|
|
||||
"aria-label": string,
|
||||
visible?: boolean,
|
||||
placement?: string,
|
||||
animating?: boolean,
|
||||
children: React.Node,
|
||||
unstable_disclosureRef?: {
|
||||
current: null | React.ElementRef<"button">,
|
||||
},
|
||||
onOpen?: () => void,
|
||||
onClose?: () => void,
|
||||
hide?: () => void,
|
||||
|};
|
||||
export type Placement =
|
||||
| "auto-start"
|
||||
| "auto"
|
||||
| "auto-end"
|
||||
| "top-start"
|
||||
| "top"
|
||||
| "top-end"
|
||||
| "right-start"
|
||||
| "right"
|
||||
| "right-end"
|
||||
| "bottom-end"
|
||||
| "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({
|
||||
children,
|
||||
@@ -43,6 +57,7 @@ export default function ContextMenu({
|
||||
onOpen();
|
||||
}
|
||||
}
|
||||
|
||||
if (!rest.visible && previousVisible) {
|
||||
if (onClose) {
|
||||
onClose();
|
||||
@@ -50,6 +65,11 @@ export default function ContextMenu({
|
||||
}
|
||||
}, [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/
|
||||
// trigger and the bottom of the window
|
||||
return (
|
||||
@@ -59,7 +79,9 @@ export default function ContextMenu({
|
||||
// kind of hacky, but this is an effective way of telling which way
|
||||
// the menu will _actually_ be placed when taking into account screen
|
||||
// positioning.
|
||||
// @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'.
|
||||
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";
|
||||
|
||||
return (
|
||||
@@ -68,8 +90,15 @@ export default function ContextMenu({
|
||||
dir="auto"
|
||||
topAnchor={topAnchor}
|
||||
rightAnchor={rightAnchor}
|
||||
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
|
||||
ref={backgroundRef}
|
||||
style={maxHeight && topAnchor ? { maxHeight } : undefined}
|
||||
style={
|
||||
maxHeight && topAnchor
|
||||
? {
|
||||
maxHeight,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{rest.visible || rest.animating ? children : null}
|
||||
</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;
|
||||
transform-origin: 50% 100%;
|
||||
max-width: 100%;
|
||||
@@ -135,11 +167,11 @@ export const Background = styled.div`
|
||||
}
|
||||
|
||||
${breakpoint("tablet")`
|
||||
animation: ${(props) =>
|
||||
animation: ${(props: any) =>
|
||||
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;
|
||||
background: ${(props) => props.theme.menuBackground};
|
||||
box-shadow: ${(props) => props.theme.menuShadow};
|
||||
background: ${(props: any) => props.theme.menuBackground};
|
||||
box-shadow: ${(props: any) => props.theme.menuShadow};
|
||||
`};
|
||||
`;
|
||||
@@ -1,24 +1,25 @@
|
||||
// @flow
|
||||
import copy from "copy-to-clipboard";
|
||||
import * as React from "react";
|
||||
|
||||
type Props = {
|
||||
text: string,
|
||||
children?: React.Node,
|
||||
onClick?: () => void,
|
||||
onCopy: () => void,
|
||||
text: string;
|
||||
children?: React.ReactElement;
|
||||
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
onCopy: () => void;
|
||||
};
|
||||
|
||||
class CopyToClipboard extends React.PureComponent<Props> {
|
||||
onClick = (ev: SyntheticEvent<>) => {
|
||||
onClick = (ev: React.SyntheticEvent) => {
|
||||
const { text, onCopy, children } = this.props;
|
||||
const elem = React.Children.only(children);
|
||||
|
||||
copy(text, {
|
||||
debug: process.env.NODE_ENV !== "production",
|
||||
format: "text/plain",
|
||||
});
|
||||
|
||||
if (onCopy) onCopy();
|
||||
if (onCopy) {
|
||||
onCopy();
|
||||
}
|
||||
|
||||
if (elem && elem.props && typeof elem.props.onClick === "function") {
|
||||
elem.props.onClick(ev);
|
||||
@@ -28,6 +29,10 @@ class CopyToClipboard extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const { text: _text, onCopy: _onCopy, children, ...rest } = this.props;
|
||||
const elem = React.Children.only(children);
|
||||
if (!elem) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return React.cloneElement(elem, { ...rest, onClick: this.onClick });
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
|
||||
type Props = {
|
||||
delay?: number,
|
||||
children: React.Node,
|
||||
delay?: number;
|
||||
children: JSX.Element;
|
||||
};
|
||||
|
||||
export default function DelayedMount({ delay = 250, children }: Props) {
|
||||
@@ -1,14 +1,12 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react-lite";
|
||||
import * as React from "react";
|
||||
import Guide from "components/Guide";
|
||||
import Modal from "components/Modal";
|
||||
import useStores from "hooks/useStores";
|
||||
import Guide from "~/components/Guide";
|
||||
import Modal from "~/components/Modal";
|
||||
import useStores from "~/hooks/useStores";
|
||||
|
||||
function Dialogs() {
|
||||
const { dialogs } = useStores();
|
||||
const { guide, modalStack } = dialogs;
|
||||
|
||||
return (
|
||||
<>
|
||||
{guide ? (
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import styled from "styled-components";
|
||||
|
||||
const Divider = styled.hr`
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import {
|
||||
ArchiveIcon,
|
||||
@@ -10,19 +9,20 @@ import {
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import Document from "models/Document";
|
||||
import Breadcrumb from "components/Breadcrumb";
|
||||
import CollectionIcon from "components/CollectionIcon";
|
||||
import useStores from "hooks/useStores";
|
||||
import { collectionUrl } from "utils/routeHelpers";
|
||||
import Document from "~/models/Document";
|
||||
import Breadcrumb, { Crumb } from "~/components/Breadcrumb";
|
||||
import CollectionIcon from "~/components/CollectionIcon";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import { NavigationNode } from "~/types";
|
||||
import { collectionUrl } from "~/utils/routeHelpers";
|
||||
|
||||
type Props = {|
|
||||
document: Document,
|
||||
children?: React.Node,
|
||||
onlyText: boolean,
|
||||
|};
|
||||
type Props = {
|
||||
document: Document;
|
||||
children?: React.ReactNode;
|
||||
onlyText?: boolean;
|
||||
};
|
||||
|
||||
function useCategory(document) {
|
||||
function useCategory(document: Document) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (document.isDeleted) {
|
||||
@@ -32,6 +32,7 @@ function useCategory(document) {
|
||||
to: "/trash",
|
||||
};
|
||||
}
|
||||
|
||||
if (document.isArchived) {
|
||||
return {
|
||||
icon: <ArchiveIcon color="currentColor" />,
|
||||
@@ -39,6 +40,7 @@ function useCategory(document) {
|
||||
to: "/archive",
|
||||
};
|
||||
}
|
||||
|
||||
if (document.isDraft) {
|
||||
return {
|
||||
icon: <EditIcon color="currentColor" />,
|
||||
@@ -46,6 +48,7 @@ function useCategory(document) {
|
||||
to: "/drafts",
|
||||
};
|
||||
}
|
||||
|
||||
if (document.isTemplate) {
|
||||
return {
|
||||
icon: <ShapesIcon color="currentColor" />,
|
||||
@@ -53,6 +56,7 @@ function useCategory(document) {
|
||||
to: "/templates",
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -60,49 +64,46 @@ const DocumentBreadcrumb = ({ document, children, onlyText }: Props) => {
|
||||
const { collections } = useStores();
|
||||
const { t } = useTranslation();
|
||||
const category = useCategory(document);
|
||||
const collection = collections.get(document.collectionId);
|
||||
|
||||
let collection = collections.get(document.collectionId);
|
||||
if (!collection) {
|
||||
collection = {
|
||||
id: document.collectionId,
|
||||
name: t("Deleted Collection"),
|
||||
color: "currentColor",
|
||||
url: "deleted-collection",
|
||||
let collectionNode: Crumb;
|
||||
|
||||
if (collection) {
|
||||
collectionNode = {
|
||||
title: collection.name,
|
||||
icon: <CollectionIcon collection={collection} expanded />,
|
||||
to: collectionUrl(collection.url),
|
||||
};
|
||||
} else {
|
||||
collectionNode = {
|
||||
title: t("Deleted Collection"),
|
||||
icon: undefined,
|
||||
to: collectionUrl("deleted-collection"),
|
||||
};
|
||||
}
|
||||
|
||||
const path = React.useMemo(
|
||||
() =>
|
||||
collection && collection.pathToDocument
|
||||
? collection.pathToDocument(document.id).slice(0, -1)
|
||||
: [],
|
||||
() => collection?.pathToDocument?.(document.id).slice(0, -1) || [],
|
||||
[collection, document.id]
|
||||
);
|
||||
|
||||
const items = React.useMemo(() => {
|
||||
let output = [];
|
||||
const output: Crumb[] = [];
|
||||
|
||||
if (category) {
|
||||
output.push(category);
|
||||
}
|
||||
|
||||
if (collection) {
|
||||
output.push({
|
||||
icon: <CollectionIcon collection={collection} expanded />,
|
||||
title: collection.name,
|
||||
to: collectionUrl(collection.url),
|
||||
});
|
||||
}
|
||||
output.push(collectionNode);
|
||||
|
||||
path.forEach((p) => {
|
||||
path.forEach((p: NavigationNode) => {
|
||||
output.push({
|
||||
title: p.title,
|
||||
to: p.url,
|
||||
});
|
||||
});
|
||||
|
||||
return output;
|
||||
}, [path, category, collection]);
|
||||
}, [path, category, collectionNode]);
|
||||
|
||||
if (!collections.isLoaded) {
|
||||
return null;
|
||||
@@ -111,8 +112,8 @@ const DocumentBreadcrumb = ({ document, children, onlyText }: Props) => {
|
||||
if (onlyText === true) {
|
||||
return (
|
||||
<>
|
||||
{collection.name}
|
||||
{path.map((n) => (
|
||||
{collection?.name}
|
||||
{path.map((n: any) => (
|
||||
<React.Fragment key={n.id}>
|
||||
<SmallSlash />
|
||||
{n.title}
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import { CloseIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
@@ -6,30 +5,34 @@ import { useTranslation } from "react-i18next";
|
||||
import { useHistory, useRouteMatch } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import Event from "models/Event";
|
||||
import Button from "components/Button";
|
||||
import Empty from "components/Empty";
|
||||
import Flex from "components/Flex";
|
||||
import PaginatedEventList from "components/PaginatedEventList";
|
||||
import Scrollable from "components/Scrollable";
|
||||
import useStores from "hooks/useStores";
|
||||
import { documentUrl } from "utils/routeHelpers";
|
||||
import Event from "~/models/Event";
|
||||
import Button from "~/components/Button";
|
||||
import Empty from "~/components/Empty";
|
||||
import Flex from "~/components/Flex";
|
||||
import PaginatedEventList from "~/components/PaginatedEventList";
|
||||
import Scrollable from "~/components/Scrollable";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import { documentUrl } from "~/utils/routeHelpers";
|
||||
|
||||
const EMPTY_ARRAY = [];
|
||||
const EMPTY_ARRAY: Event[] = [];
|
||||
|
||||
function DocumentHistory() {
|
||||
const { events, documents } = useStores();
|
||||
const { t } = useTranslation();
|
||||
const match = useRouteMatch();
|
||||
const match = useRouteMatch<{ documentSlug: string }>();
|
||||
const history = useHistory();
|
||||
|
||||
const document = documents.getByUrl(match.params.documentSlug);
|
||||
|
||||
const eventsInDocument = document
|
||||
? events.inDocument(document.id)
|
||||
: EMPTY_ARRAY;
|
||||
|
||||
const onCloseHistory = () => {
|
||||
history.push(documentUrl(document));
|
||||
if (document) {
|
||||
history.push(documentUrl(document));
|
||||
} else {
|
||||
history.goBack();
|
||||
}
|
||||
};
|
||||
|
||||
const items = React.useMemo(() => {
|
||||
@@ -39,17 +42,20 @@ function DocumentHistory() {
|
||||
eventsInDocument[0].createdAt !== document.updatedAt
|
||||
) {
|
||||
eventsInDocument.unshift(
|
||||
new Event({
|
||||
name: "documents.latest_version",
|
||||
documentId: document.id,
|
||||
createdAt: document.updatedAt,
|
||||
actor: document.updatedBy,
|
||||
})
|
||||
new Event(
|
||||
{
|
||||
name: "documents.latest_version",
|
||||
documentId: document.id,
|
||||
createdAt: document.updatedAt,
|
||||
actor: document.updatedBy,
|
||||
},
|
||||
events
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return eventsInDocument;
|
||||
}, [eventsInDocument, document]);
|
||||
}, [eventsInDocument, events, document]);
|
||||
|
||||
return (
|
||||
<Sidebar>
|
||||
@@ -68,7 +74,9 @@ function DocumentHistory() {
|
||||
<PaginatedEventList
|
||||
fetch={events.fetchPage}
|
||||
events={items}
|
||||
options={{ documentId: document.id }}
|
||||
options={{
|
||||
documentId: document.id,
|
||||
}}
|
||||
document={document}
|
||||
empty={<Empty>{t("Oh weird, there's nothing here")}</Empty>}
|
||||
/>
|
||||
@@ -1,22 +1,20 @@
|
||||
// @flow
|
||||
import ArrowKeyNavigation from "boundless-arrow-key-navigation";
|
||||
import * as React from "react";
|
||||
import Document from "models/Document";
|
||||
import DocumentListItem from "components/DocumentListItem";
|
||||
import Document from "~/models/Document";
|
||||
import DocumentListItem from "~/components/DocumentListItem";
|
||||
|
||||
type Props = {|
|
||||
documents: Document[],
|
||||
limit?: number,
|
||||
showCollection?: boolean,
|
||||
showPublished?: boolean,
|
||||
showPin?: boolean,
|
||||
showDraft?: boolean,
|
||||
showTemplate?: boolean,
|
||||
|};
|
||||
type Props = {
|
||||
documents: Document[];
|
||||
limit?: number;
|
||||
showCollection?: boolean;
|
||||
showPublished?: boolean;
|
||||
showPin?: boolean;
|
||||
showDraft?: boolean;
|
||||
showTemplate?: boolean;
|
||||
};
|
||||
|
||||
export default function DocumentList({ limit, documents, ...rest }: Props) {
|
||||
const items = limit ? documents.splice(0, limit) : documents;
|
||||
|
||||
return (
|
||||
<ArrowKeyNavigation
|
||||
mode={ArrowKeyNavigation.mode.VERTICAL}
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import { PlusIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
@@ -6,34 +5,33 @@ import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import styled, { css } from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import Document from "models/Document";
|
||||
import Badge from "components/Badge";
|
||||
import Button from "components/Button";
|
||||
import DocumentMeta from "components/DocumentMeta";
|
||||
import EventBoundary from "components/EventBoundary";
|
||||
import Flex from "components/Flex";
|
||||
import Highlight from "components/Highlight";
|
||||
import StarButton, { AnimatedStar } from "components/Star";
|
||||
import Tooltip from "components/Tooltip";
|
||||
import useBoolean from "hooks/useBoolean";
|
||||
import useCurrentTeam from "hooks/useCurrentTeam";
|
||||
import useCurrentUser from "hooks/useCurrentUser";
|
||||
import useStores from "hooks/useStores";
|
||||
import DocumentMenu from "menus/DocumentMenu";
|
||||
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,
|
||||
|};
|
||||
import Document from "~/models/Document";
|
||||
import Badge from "~/components/Badge";
|
||||
import Button from "~/components/Button";
|
||||
import DocumentMeta from "~/components/DocumentMeta";
|
||||
import EventBoundary from "~/components/EventBoundary";
|
||||
import Flex from "~/components/Flex";
|
||||
import Highlight from "~/components/Highlight";
|
||||
import StarButton, { AnimatedStar } from "~/components/Star";
|
||||
import Tooltip from "~/components/Tooltip";
|
||||
import useBoolean from "~/hooks/useBoolean";
|
||||
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
||||
import useCurrentUser from "~/hooks/useCurrentUser";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import DocumentMenu from "~/menus/DocumentMenu";
|
||||
import { newDocumentPath } from "~/utils/routeHelpers";
|
||||
|
||||
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;
|
||||
|
||||
function replaceResultMarks(tag: string) {
|
||||
@@ -42,12 +40,16 @@ function replaceResultMarks(tag: string) {
|
||||
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 { policies } = useStores();
|
||||
const currentUser = useCurrentUser();
|
||||
const currentTeam = useCurrentTeam();
|
||||
const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean();
|
||||
|
||||
const {
|
||||
document,
|
||||
showNestedDocuments,
|
||||
@@ -59,7 +61,6 @@ function DocumentListItem(props: Props, ref) {
|
||||
highlight,
|
||||
context,
|
||||
} = props;
|
||||
|
||||
const queryIsInTitle =
|
||||
!!highlight &&
|
||||
!!document.title.toLowerCase().includes(highlight.toLowerCase());
|
||||
@@ -76,7 +77,9 @@ function DocumentListItem(props: Props, ref) {
|
||||
$menuOpen={menuOpen}
|
||||
to={{
|
||||
pathname: document.url,
|
||||
state: { title: document.titleWithDefault },
|
||||
state: {
|
||||
title: document.titleWithDefault,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Content>
|
||||
@@ -173,7 +176,10 @@ const Actions = styled(EventBoundary)`
|
||||
`};
|
||||
`;
|
||||
|
||||
const DocumentLink = styled(Link)`
|
||||
const DocumentLink = styled(Link)<{
|
||||
$isStarred?: boolean;
|
||||
$menuOpen?: boolean;
|
||||
}>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 10px -8px;
|
||||
@@ -228,7 +234,7 @@ const DocumentLink = styled(Link)`
|
||||
`}
|
||||
`;
|
||||
|
||||
const Heading = styled.h3`
|
||||
const Heading = styled.h3<{ rtl?: boolean }>`
|
||||
display: flex;
|
||||
justify-content: ${(props) => (props.rtl ? "flex-end" : "flex-start")};
|
||||
align-items: center;
|
||||
@@ -1,18 +1,17 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import Document from "models/Document";
|
||||
import DocumentBreadcrumb from "components/DocumentBreadcrumb";
|
||||
import DocumentTasks from "components/DocumentTasks";
|
||||
import Flex from "components/Flex";
|
||||
import Time from "components/Time";
|
||||
import useCurrentUser from "hooks/useCurrentUser";
|
||||
import useStores from "hooks/useStores";
|
||||
import Document from "~/models/Document";
|
||||
import DocumentBreadcrumb from "~/components/DocumentBreadcrumb";
|
||||
import DocumentTasks from "~/components/DocumentTasks";
|
||||
import Flex from "~/components/Flex";
|
||||
import Time from "~/components/Time";
|
||||
import useCurrentUser from "~/hooks/useCurrentUser";
|
||||
import useStores from "~/hooks/useStores";
|
||||
|
||||
const Container = styled(Flex)`
|
||||
const Container = styled(Flex)<{ rtl?: boolean }>`
|
||||
justify-content: ${(props) => (props.rtl ? "flex-end" : "flex-start")};
|
||||
color: ${(props) => props.theme.textTertiary};
|
||||
font-size: 13px;
|
||||
@@ -26,20 +25,20 @@ const Viewed = styled.span`
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const Modified = styled.span`
|
||||
const Modified = styled.span<{ highlight?: boolean }>`
|
||||
color: ${(props) => props.theme.textTertiary};
|
||||
font-weight: ${(props) => (props.highlight ? "600" : "400")};
|
||||
`;
|
||||
|
||||
type Props = {|
|
||||
showCollection?: boolean,
|
||||
showPublished?: boolean,
|
||||
showLastViewed?: boolean,
|
||||
showNestedDocuments?: boolean,
|
||||
document: Document,
|
||||
children: React.Node,
|
||||
to?: string,
|
||||
|};
|
||||
type Props = {
|
||||
showCollection?: boolean;
|
||||
showPublished?: boolean;
|
||||
showLastViewed?: boolean;
|
||||
showNestedDocuments?: boolean;
|
||||
document: Document;
|
||||
children?: React.ReactNode;
|
||||
to?: string;
|
||||
};
|
||||
|
||||
function DocumentMeta({
|
||||
showPublished,
|
||||
@@ -54,7 +53,6 @@ function DocumentMeta({
|
||||
const { t } = useTranslation();
|
||||
const { collections } = useStores();
|
||||
const user = useCurrentUser();
|
||||
|
||||
const {
|
||||
modifiedSinceViewed,
|
||||
updatedAt,
|
||||
@@ -126,6 +124,7 @@ function DocumentMeta({
|
||||
if (isDraft || !showLastViewed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!lastViewedAt) {
|
||||
return (
|
||||
<Viewed>
|
||||
@@ -156,7 +155,9 @@ function DocumentMeta({
|
||||
{showNestedDocuments && nestedDocumentsCount > 0 && (
|
||||
<span>
|
||||
• {nestedDocumentsCount}{" "}
|
||||
{t("nested document", { count: nestedDocumentsCount })}
|
||||
{t("nested document", {
|
||||
count: nestedDocumentsCount,
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
{timeSinceNow()}
|
||||
@@ -1,21 +1,20 @@
|
||||
// @flow
|
||||
import { useObserver } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { usePopoverState, PopoverDisclosure } from "reakit/Popover";
|
||||
import styled from "styled-components";
|
||||
import Document from "models/Document";
|
||||
import DocumentMeta from "components/DocumentMeta";
|
||||
import DocumentViews from "components/DocumentViews";
|
||||
import Popover from "components/Popover";
|
||||
import useStores from "../hooks/useStores";
|
||||
import Document from "~/models/Document";
|
||||
import DocumentMeta from "~/components/DocumentMeta";
|
||||
import DocumentViews from "~/components/DocumentViews";
|
||||
import Popover from "~/components/Popover";
|
||||
import useStores from "~/hooks/useStores";
|
||||
|
||||
type Props = {|
|
||||
document: Document,
|
||||
isDraft: boolean,
|
||||
to?: string,
|
||||
rtl?: boolean,
|
||||
|};
|
||||
type Props = {
|
||||
document: Document;
|
||||
isDraft: boolean;
|
||||
to?: string;
|
||||
rtl?: boolean;
|
||||
};
|
||||
|
||||
function DocumentMetaWithViews({ to, isDraft, document, ...rest }: Props) {
|
||||
const { views } = useStores();
|
||||
@@ -26,7 +25,9 @@ function DocumentMetaWithViews({ to, isDraft, document, ...rest }: Props) {
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!document.isDeleted) {
|
||||
views.fetchPage({ documentId: document.id });
|
||||
views.fetchPage({
|
||||
documentId: document.id,
|
||||
});
|
||||
}
|
||||
}, [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")};
|
||||
margin: -12px 0 2em 0;
|
||||
font-size: 14px;
|
||||
@@ -1,22 +1,27 @@
|
||||
// @flow
|
||||
import { DoneIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation, TFunction } from "react-i18next";
|
||||
import styled, { useTheme } from "styled-components";
|
||||
import CircularProgressBar from "components/CircularProgressBar";
|
||||
import usePrevious from "../hooks/usePrevious";
|
||||
import Document from "../models/Document";
|
||||
import { bounceIn } from "styles/animations";
|
||||
import Document from "~/models/Document";
|
||||
import CircularProgressBar from "~/components/CircularProgressBar";
|
||||
import usePrevious from "~/hooks/usePrevious";
|
||||
import { bounceIn } from "~/styles/animations";
|
||||
|
||||
type Props = {|
|
||||
document: Document,
|
||||
|};
|
||||
type Props = {
|
||||
document: Document;
|
||||
};
|
||||
|
||||
function getMessage(t, total, completed) {
|
||||
function getMessage(t: TFunction, total: number, completed: number): string {
|
||||
if (completed === 0) {
|
||||
return t(`{{ total }} task`, { total, count: total });
|
||||
return t(`{{ total }} task`, {
|
||||
total,
|
||||
count: total,
|
||||
});
|
||||
} else if (completed === total) {
|
||||
return t(`{{ completed }} task done`, { completed, count: completed });
|
||||
return t(`{{ completed }} task done`, {
|
||||
completed,
|
||||
count: completed,
|
||||
});
|
||||
} else {
|
||||
return t(`{{ completed }} of {{ total }} tasks`, {
|
||||
total,
|
||||
@@ -33,7 +38,6 @@ function DocumentTasks({ document }: Props) {
|
||||
const done = completed === total;
|
||||
const previousDone = usePrevious(done);
|
||||
const message = getMessage(t, total, completed);
|
||||
|
||||
return (
|
||||
<>
|
||||
{completed === total ? (
|
||||
@@ -1,31 +1,28 @@
|
||||
// @flow
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { sortBy } from "lodash";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Document from "models/Document";
|
||||
import Avatar from "components/Avatar";
|
||||
import ListItem from "components/List/Item";
|
||||
import PaginatedList from "components/PaginatedList";
|
||||
import useStores from "hooks/useStores";
|
||||
import Document from "~/models/Document";
|
||||
import Avatar from "~/components/Avatar";
|
||||
import ListItem from "~/components/List/Item";
|
||||
import PaginatedList from "~/components/PaginatedList";
|
||||
import useStores from "~/hooks/useStores";
|
||||
|
||||
type Props = {|
|
||||
document: Document,
|
||||
isOpen?: boolean,
|
||||
|};
|
||||
type Props = {
|
||||
document: Document;
|
||||
isOpen?: boolean;
|
||||
};
|
||||
|
||||
function DocumentViews({ document, isOpen }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const { views, presence } = useStores();
|
||||
|
||||
let documentPresence = presence.get(document.id);
|
||||
documentPresence = documentPresence
|
||||
const documentPresence = presence.get(document.id);
|
||||
const documentPresenceArray = documentPresence
|
||||
? Array.from(documentPresence.values())
|
||||
: [];
|
||||
|
||||
const presentIds = documentPresence.map((p) => p.userId);
|
||||
const editingIds = documentPresence
|
||||
const presentIds = documentPresenceArray.map((p) => p.userId);
|
||||
const editingIds = documentPresenceArray
|
||||
.filter((p) => p.isEditing)
|
||||
.map((p) => p.userId);
|
||||
|
||||
@@ -35,7 +32,6 @@ function DocumentViews({ document, isOpen }: Props) {
|
||||
documentViews,
|
||||
(view) => !presentIds.includes(view.user.id)
|
||||
);
|
||||
|
||||
const users = React.useMemo(() => sortedViews.map((v) => v.user), [
|
||||
sortedViews,
|
||||
]);
|
||||
@@ -49,7 +45,6 @@ function DocumentViews({ document, isOpen }: Props) {
|
||||
const view = documentViews.find((v) => v.user.id === item.id);
|
||||
const isPresent = presentIds.includes(item.id);
|
||||
const isEditing = editingIds.includes(item.id);
|
||||
|
||||
const subtitle = isPresent
|
||||
? isEditing
|
||||
? t("Currently editing")
|
||||
@@ -59,7 +54,6 @@ function DocumentViews({ document, isOpen }: Props) {
|
||||
view ? Date.parse(view.lastViewedAt) : new Date()
|
||||
),
|
||||
});
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
key={item.id}
|
||||
@@ -1,76 +1,84 @@
|
||||
// @flow
|
||||
import { lighten } from "polished";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { withRouter, type RouterHistory } from "react-router-dom";
|
||||
import { Extension } from "rich-markdown-editor";
|
||||
import styled, { withTheme } from "styled-components";
|
||||
import embeds from "shared/embeds";
|
||||
import { light } from "shared/theme";
|
||||
import UiStore from "stores/UiStore";
|
||||
import ErrorBoundary from "components/ErrorBoundary";
|
||||
import Tooltip from "components/Tooltip";
|
||||
import useMediaQuery from "hooks/useMediaQuery";
|
||||
import useToasts from "hooks/useToasts";
|
||||
import { type Theme } from "types";
|
||||
import { isModKey } from "utils/keyboard";
|
||||
import { uploadFile } from "utils/uploadFile";
|
||||
import { isInternalUrl, isHash } from "utils/urls";
|
||||
import styled, { DefaultTheme, withTheme } from "styled-components";
|
||||
import embeds from "@shared/embeds";
|
||||
import { light } from "@shared/theme";
|
||||
import UiStore from "~/stores/UiStore";
|
||||
import ErrorBoundary from "~/components/ErrorBoundary";
|
||||
import Tooltip from "~/components/Tooltip";
|
||||
import useMediaQuery from "~/hooks/useMediaQuery";
|
||||
import useToasts from "~/hooks/useToasts";
|
||||
import history from "~/utils/history";
|
||||
import { isModKey } from "~/utils/keyboard";
|
||||
import { uploadFile } from "~/utils/uploadFile";
|
||||
import { isInternalUrl, isHash } from "~/utils/urls";
|
||||
|
||||
const RichMarkdownEditor = React.lazy(() =>
|
||||
import(/* webpackChunkName: "rich-markdown-editor" */ "rich-markdown-editor")
|
||||
const RichMarkdownEditor = React.lazy(
|
||||
() =>
|
||||
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 = [];
|
||||
|
||||
export type Props = {|
|
||||
id?: string,
|
||||
value?: string,
|
||||
defaultValue?: string,
|
||||
readOnly?: boolean,
|
||||
grow?: boolean,
|
||||
disableEmbeds?: boolean,
|
||||
ui?: UiStore,
|
||||
style?: Object,
|
||||
extensions?: Extension[],
|
||||
shareId?: ?string,
|
||||
autoFocus?: boolean,
|
||||
template?: boolean,
|
||||
placeholder?: string,
|
||||
maxLength?: number,
|
||||
scrollTo?: string,
|
||||
theme?: Theme,
|
||||
className?: string,
|
||||
handleDOMEvents?: Object,
|
||||
readOnlyWriteCheckboxes?: boolean,
|
||||
onBlur?: (event: SyntheticEvent<>) => any,
|
||||
onFocus?: (event: SyntheticEvent<>) => any,
|
||||
onPublish?: (event: SyntheticEvent<>) => any,
|
||||
onSave?: ({ done?: boolean, autosave?: boolean, publish?: boolean }) => any,
|
||||
onCancel?: () => any,
|
||||
onDoubleClick?: () => any,
|
||||
onChange?: (getValue: () => string) => any,
|
||||
onSearchLink?: (title: string) => any,
|
||||
onHoverLink?: (event: MouseEvent) => any,
|
||||
onCreateLink?: (title: string) => Promise<string>,
|
||||
onImageUploadStart?: () => any,
|
||||
onImageUploadStop?: () => any,
|
||||
|};
|
||||
export type Props = {
|
||||
id?: string;
|
||||
value?: string;
|
||||
defaultValue?: string;
|
||||
readOnly?: boolean;
|
||||
grow?: boolean;
|
||||
disableEmbeds?: boolean;
|
||||
ui?: UiStore;
|
||||
style?: React.CSSProperties;
|
||||
extensions?: Extension[];
|
||||
shareId?: string | null | undefined;
|
||||
autoFocus?: boolean;
|
||||
template?: boolean;
|
||||
placeholder?: string;
|
||||
maxLength?: number;
|
||||
scrollTo?: string;
|
||||
theme?: DefaultTheme;
|
||||
className?: string;
|
||||
readOnlyWriteCheckboxes?: boolean;
|
||||
onBlur?: () => void;
|
||||
onFocus?: () => void;
|
||||
onPublish?: (event: React.SyntheticEvent) => any;
|
||||
onSave?: (arg0: {
|
||||
done?: boolean;
|
||||
autosave?: boolean;
|
||||
publish?: boolean;
|
||||
}) => any;
|
||||
onSynced?: () => Promise<void>;
|
||||
onCancel?: () => any;
|
||||
onDoubleClick?: () => any;
|
||||
onChange?: (getValue: () => string) => any;
|
||||
onSearchLink?: (title: string) => any;
|
||||
onHoverLink?: (event: MouseEvent) => any;
|
||||
onCreateLink?: (title: string) => Promise<string>;
|
||||
onImageUploadStart?: () => any;
|
||||
onImageUploadStop?: () => any;
|
||||
};
|
||||
|
||||
type PropsWithRef = Props & {
|
||||
forwardedRef: React.Ref<any>,
|
||||
history: RouterHistory,
|
||||
forwardedRef: React.Ref<any>;
|
||||
};
|
||||
|
||||
function Editor(props: PropsWithRef) {
|
||||
const { id, shareId, history } = props;
|
||||
const { id, shareId } = props;
|
||||
const { t } = useTranslation();
|
||||
const { showToast } = useToasts();
|
||||
const isPrinting = useMediaQuery("print");
|
||||
|
||||
const onUploadImage = React.useCallback(
|
||||
async (file: File) => {
|
||||
const result = await uploadFile(file, { documentId: id });
|
||||
const result = await uploadFile(file, {
|
||||
documentId: id,
|
||||
});
|
||||
return result.url;
|
||||
},
|
||||
[id]
|
||||
@@ -166,7 +174,9 @@ function Editor(props: PropsWithRef) {
|
||||
pageBreak: t("Page break"),
|
||||
pasteLink: `${t("Paste a link")}…`,
|
||||
pasteLinkWithTitle: (service: string) =>
|
||||
t("Paste a {{service}} link…", { service }),
|
||||
t("Paste a {{service}} link…", {
|
||||
service,
|
||||
}),
|
||||
placeholder: t("Placeholder"),
|
||||
quote: t("Quote"),
|
||||
removeLink: t("Remove link"),
|
||||
@@ -189,17 +199,20 @@ function Editor(props: PropsWithRef) {
|
||||
uploadImage={onUploadImage}
|
||||
onClickLink={onClickLink}
|
||||
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}
|
||||
tooltip={EditorTooltip}
|
||||
dictionary={dictionary}
|
||||
{...props}
|
||||
placeholder={props.placeholder || ""}
|
||||
defaultValue={props.defaultValue || ""}
|
||||
theme={isPrinting ? light : props.theme}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledEditor = styled(RichMarkdownEditor)`
|
||||
const StyledEditor = styled(RichMarkdownEditor)<{ grow?: boolean }>`
|
||||
flex-grow: ${(props) => (props.grow ? 1 : 0)};
|
||||
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 }) => (
|
||||
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
|
||||
<Tooltip offset="0, 16" delay={150} {...props}>
|
||||
<Span>{children}</Span>
|
||||
</Tooltip>
|
||||
@@ -317,8 +332,8 @@ const Span = styled.span`
|
||||
outline: none;
|
||||
`;
|
||||
|
||||
const EditorWithRouterAndTheme = withRouter(withTheme(Editor));
|
||||
const EditorWithTheme = withTheme(Editor);
|
||||
|
||||
export default React.forwardRef<Props, typeof Editor>((props, ref) => (
|
||||
<EditorWithRouterAndTheme {...props} forwardedRef={ref} />
|
||||
export default React.forwardRef<typeof Editor, Props>((props, ref) => (
|
||||
<EditorWithTheme {...props} forwardedRef={ref} />
|
||||
));
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import styled from "styled-components";
|
||||
|
||||
const Empty = styled.p`
|
||||
@@ -1,29 +1,30 @@
|
||||
// @flow
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { observable } from "mobx";
|
||||
import { observer } from "mobx-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 Button from "components/Button";
|
||||
import CenteredContent from "components/CenteredContent";
|
||||
import HelpText from "components/HelpText";
|
||||
import PageTitle from "components/PageTitle";
|
||||
import { githubIssuesUrl } from "../../shared/utils/routeHelpers";
|
||||
import env from "env";
|
||||
import { githubIssuesUrl } from "@shared/utils/routeHelpers";
|
||||
import Button from "~/components/Button";
|
||||
import CenteredContent from "~/components/CenteredContent";
|
||||
import HelpText from "~/components/HelpText";
|
||||
import PageTitle from "~/components/PageTitle";
|
||||
import env from "~/env";
|
||||
|
||||
type Props = {|
|
||||
children: React.Node,
|
||||
reloadOnChunkMissing?: boolean,
|
||||
t: TFunction,
|
||||
|};
|
||||
type Props = WithTranslation & {
|
||||
children: React.ReactNode;
|
||||
reloadOnChunkMissing?: boolean;
|
||||
};
|
||||
|
||||
@observer
|
||||
class ErrorBoundary extends React.Component<Props> {
|
||||
@observable error: ?Error;
|
||||
@observable showDetails: boolean = false;
|
||||
@observable
|
||||
error: Error | null | undefined;
|
||||
|
||||
componentDidCatch(error: Error, info: Object) {
|
||||
@observable
|
||||
showDetails = false;
|
||||
|
||||
componentDidCatch(error: Error) {
|
||||
this.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
|
||||
// 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.
|
||||
window.location.reload(true);
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -45,7 +46,7 @@ class ErrorBoundary extends React.Component<Props> {
|
||||
}
|
||||
|
||||
handleReload = () => {
|
||||
window.location.reload(true);
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
handleShowDetails = () => {
|
||||
@@ -79,9 +80,7 @@ class ErrorBoundary extends React.Component<Props> {
|
||||
</Trans>
|
||||
</HelpText>
|
||||
<p>
|
||||
<Button onClick={this.handleReload}>
|
||||
<Trans>Reload</Trans>
|
||||
</Button>
|
||||
<Button onClick={this.handleReload}>{t("Reload")}</Button>
|
||||
</p>
|
||||
</CenteredContent>
|
||||
);
|
||||
@@ -105,9 +104,7 @@ class ErrorBoundary extends React.Component<Props> {
|
||||
</HelpText>
|
||||
{this.showDetails && <Pre>{error.toString()}</Pre>}
|
||||
<p>
|
||||
<Button onClick={this.handleReload}>
|
||||
<Trans>Reload</Trans>
|
||||
</Button>{" "}
|
||||
<Button onClick={this.handleReload}>{t("Reload")}</Button>{" "}
|
||||
{this.showDetails ? (
|
||||
<Button onClick={this.handleReportBug} neutral>
|
||||
<Trans>Report a Bug</Trans>…
|
||||
@@ -121,6 +118,7 @@ class ErrorBoundary extends React.Component<Props> {
|
||||
</CenteredContent>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
@@ -133,4 +131,4 @@ const Pre = styled.pre`
|
||||
white-space: pre-wrap;
|
||||
`;
|
||||
|
||||
export default withTranslation()<ErrorBoundary>(ErrorBoundary);
|
||||
export default withTranslation()(ErrorBoundary);
|
||||
@@ -1,14 +1,12 @@
|
||||
// @flow
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
type Props = {
|
||||
children: React.Node,
|
||||
className?: string,
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export default function EventBoundary({ children, className }: Props) {
|
||||
const handleClick = React.useCallback((event: SyntheticEvent<>) => {
|
||||
const handleClick = React.useCallback((event: React.SyntheticEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}, []);
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import {
|
||||
TrashIcon,
|
||||
ArchiveIcon,
|
||||
@@ -10,23 +9,25 @@ import {
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import Document from "models/Document";
|
||||
import Event from "models/Event";
|
||||
import Avatar from "components/Avatar";
|
||||
import Item, { Actions } from "components/List/Item";
|
||||
import Time from "components/Time";
|
||||
import RevisionMenu from "menus/RevisionMenu";
|
||||
import { documentHistoryUrl } from "utils/routeHelpers";
|
||||
import Document from "~/models/Document";
|
||||
import Event from "~/models/Event";
|
||||
import Avatar from "~/components/Avatar";
|
||||
import Item, { Actions } from "~/components/List/Item";
|
||||
import Time from "~/components/Time";
|
||||
import RevisionMenu from "~/menus/RevisionMenu";
|
||||
import { documentHistoryUrl } from "~/utils/routeHelpers";
|
||||
|
||||
type Props = {|
|
||||
document: Document,
|
||||
event: Event,
|
||||
latest?: boolean,
|
||||
|};
|
||||
type Props = {
|
||||
document: Document;
|
||||
event: Event;
|
||||
latest?: boolean;
|
||||
};
|
||||
|
||||
const EventListItem = ({ event, latest, document }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const opts = { userName: event.actor.name };
|
||||
const opts = {
|
||||
userName: event.actor.name,
|
||||
};
|
||||
const isRevision = event.name === "revisions.create";
|
||||
let meta, icon, to;
|
||||
|
||||
@@ -45,28 +46,35 @@ const EventListItem = ({ event, latest, document }: Props) => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
case "documents.archive":
|
||||
icon = <ArchiveIcon color="currentColor" size={16} />;
|
||||
meta = t("{{userName}} archived", opts);
|
||||
break;
|
||||
|
||||
case "documents.unarchive":
|
||||
meta = t("{{userName}} restored", opts);
|
||||
break;
|
||||
|
||||
case "documents.delete":
|
||||
icon = <TrashIcon color="currentColor" size={16} />;
|
||||
meta = t("{{userName}} deleted", opts);
|
||||
break;
|
||||
|
||||
case "documents.restore":
|
||||
meta = t("{{userName}} moved from trash", opts);
|
||||
break;
|
||||
|
||||
case "documents.publish":
|
||||
icon = <PublishIcon color="currentColor" size={16} />;
|
||||
meta = t("{{userName}} published", opts);
|
||||
break;
|
||||
|
||||
case "documents.move":
|
||||
icon = <MoveIcon color="currentColor" size={16} />;
|
||||
meta = t("{{userName}} moved", opts);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn("Unhandled event: ", event.name);
|
||||
}
|
||||
@@ -78,7 +86,6 @@ const EventListItem = ({ event, latest, document }: Props) => {
|
||||
return (
|
||||
<ListItem
|
||||
small
|
||||
exact
|
||||
to={to}
|
||||
title={
|
||||
<Time
|
||||
@@ -97,7 +104,7 @@ const EventListItem = ({ event, latest, document }: Props) => {
|
||||
</Subtitle>
|
||||
}
|
||||
actions={
|
||||
isRevision ? (
|
||||
isRevision && event.modelId ? (
|
||||
<RevisionMenu document={document} revisionId={event.modelId} />
|
||||
) : undefined
|
||||
}
|
||||
@@ -1,22 +1,21 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import User from "models/User";
|
||||
import Avatar from "components/Avatar";
|
||||
import Flex from "components/Flex";
|
||||
import User from "~/models/User";
|
||||
import Avatar from "~/components/Avatar";
|
||||
import Flex from "~/components/Flex";
|
||||
|
||||
type Props = {|
|
||||
users: User[],
|
||||
size?: number,
|
||||
overflow: number,
|
||||
onClick?: (event: SyntheticEvent<>) => mixed,
|
||||
renderAvatar?: (user: User) => React.Node,
|
||||
|};
|
||||
type Props = {
|
||||
users: User[];
|
||||
size?: number;
|
||||
overflow?: number;
|
||||
onClick?: React.MouseEventHandler<HTMLDivElement>;
|
||||
renderAvatar?: (user: User) => React.ReactNode;
|
||||
};
|
||||
|
||||
function Facepile({
|
||||
users,
|
||||
overflow,
|
||||
overflow = 0,
|
||||
size = 32,
|
||||
renderAvatar = DefaultAvatar,
|
||||
...rest
|
||||
@@ -47,7 +46,7 @@ const AvatarWrapper = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
const More = styled.div`
|
||||
const More = styled.div<{ size: number }>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@@ -1,8 +1,7 @@
|
||||
// @flow
|
||||
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;
|
||||
`;
|
||||
|
||||
@@ -1,51 +1,51 @@
|
||||
// @flow
|
||||
import { find } from "lodash";
|
||||
import * as React from "react";
|
||||
import { useMenuState, MenuButton } from "reakit/Menu";
|
||||
import styled from "styled-components";
|
||||
import Button, { Inner } from "components/Button";
|
||||
import ContextMenu from "components/ContextMenu";
|
||||
import MenuItem from "components/ContextMenu/MenuItem";
|
||||
import HelpText from "components/HelpText";
|
||||
import Button, { Inner } from "~/components/Button";
|
||||
import ContextMenu from "~/components/ContextMenu";
|
||||
import MenuItem from "~/components/ContextMenu/MenuItem";
|
||||
import HelpText from "~/components/HelpText";
|
||||
|
||||
type TFilterOption = {|
|
||||
key: string,
|
||||
label: string,
|
||||
note?: string,
|
||||
|};
|
||||
type TFilterOption = {
|
||||
key: string;
|
||||
label: string;
|
||||
note?: string;
|
||||
};
|
||||
|
||||
type Props = {|
|
||||
options: TFilterOption[],
|
||||
activeKey: ?string,
|
||||
defaultLabel?: string,
|
||||
selectedPrefix?: string,
|
||||
className?: string,
|
||||
onSelect: (key: ?string) => void,
|
||||
|};
|
||||
type Props = {
|
||||
options: TFilterOption[];
|
||||
activeKey: string | null | undefined;
|
||||
defaultLabel?: string;
|
||||
selectedPrefix?: string;
|
||||
className?: string;
|
||||
onSelect: (key: string | null | undefined) => void;
|
||||
};
|
||||
|
||||
const FilterOptions = ({
|
||||
options,
|
||||
activeKey = "",
|
||||
defaultLabel,
|
||||
defaultLabel = "Filter options",
|
||||
selectedPrefix = "",
|
||||
className,
|
||||
onSelect,
|
||||
}: Props) => {
|
||||
const menu = useMenuState({ modal: true });
|
||||
const selected = find(options, { key: activeKey }) || options[0];
|
||||
const menu = useMenuState({
|
||||
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}` : "";
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<MenuButton {...menu}>
|
||||
{(props) => (
|
||||
<StyledButton
|
||||
{...props}
|
||||
className={className}
|
||||
neutral
|
||||
disclosure
|
||||
small
|
||||
>
|
||||
<StyledButton {...props} className={className} neutral disclosure>
|
||||
{activeKey ? selectedLabel : defaultLabel}
|
||||
</StyledButton>
|
||||
)}
|
||||
@@ -1,5 +1,3 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
type JustifyValues =
|
||||
@@ -16,29 +14,14 @@ type AlignValues =
|
||||
| "flex-start"
|
||||
| "flex-end";
|
||||
|
||||
type Props = {|
|
||||
column?: ?boolean,
|
||||
shrink?: ?boolean,
|
||||
align?: AlignValues,
|
||||
justify?: JustifyValues,
|
||||
auto?: ?boolean,
|
||||
className?: string,
|
||||
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`
|
||||
const Flex = styled.div<{
|
||||
auto?: boolean;
|
||||
column?: boolean;
|
||||
align?: AlignValues;
|
||||
justify?: JustifyValues;
|
||||
shrink?: boolean;
|
||||
gap?: number;
|
||||
}>`
|
||||
display: flex;
|
||||
flex: ${({ auto }) => (auto ? "1 1 auto" : "initial")};
|
||||
flex-direction: ${({ column }) => (column ? "column" : "row")};
|
||||
@@ -1,9 +1,8 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import Empty from "components/Empty";
|
||||
import Fade from "components/Fade";
|
||||
import Flex from "components/Flex";
|
||||
import Empty from "~/components/Empty";
|
||||
import Fade from "~/components/Fade";
|
||||
import Flex from "~/components/Flex";
|
||||
|
||||
export default function FullscreenLoading() {
|
||||
return (
|
||||
@@ -1,10 +1,9 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
|
||||
type Props = {
|
||||
size?: number,
|
||||
fill?: string,
|
||||
className?: string,
|
||||
size?: number;
|
||||
fill?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function GithubLogo({ size = 34, fill = "#FFF", className }: Props) {
|
||||
@@ -1,31 +1,31 @@
|
||||
// @flow
|
||||
import { observable } from "mobx";
|
||||
import { observer, inject } from "mobx-react";
|
||||
import { observer } from "mobx-react";
|
||||
import { GroupIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import { MAX_AVATAR_DISPLAY } from "shared/constants";
|
||||
import GroupMembershipsStore from "stores/GroupMembershipsStore";
|
||||
import CollectionGroupMembership from "models/CollectionGroupMembership";
|
||||
import Group from "models/Group";
|
||||
import GroupMembers from "scenes/GroupMembers";
|
||||
import Facepile from "components/Facepile";
|
||||
import Flex from "components/Flex";
|
||||
import ListItem from "components/List/Item";
|
||||
import Modal from "components/Modal";
|
||||
import { MAX_AVATAR_DISPLAY } from "@shared/constants";
|
||||
import RootStore from "~/stores/RootStore";
|
||||
import CollectionGroupMembership from "~/models/CollectionGroupMembership";
|
||||
import Group from "~/models/Group";
|
||||
import GroupMembers from "~/scenes/GroupMembers";
|
||||
import Facepile from "~/components/Facepile";
|
||||
import Flex from "~/components/Flex";
|
||||
import ListItem from "~/components/List/Item";
|
||||
import Modal from "~/components/Modal";
|
||||
import withStores from "~/components/withStores";
|
||||
|
||||
type Props = {
|
||||
group: Group,
|
||||
groupMemberships: GroupMembershipsStore,
|
||||
membership?: CollectionGroupMembership,
|
||||
showFacepile?: boolean,
|
||||
showAvatar?: boolean,
|
||||
renderActions: ({ openMembersModal: () => void }) => React.Node,
|
||||
type Props = RootStore & {
|
||||
group: Group;
|
||||
membership?: CollectionGroupMembership;
|
||||
showFacepile?: boolean;
|
||||
showAvatar?: boolean;
|
||||
renderActions: (arg0: { openMembersModal: () => void }) => React.ReactNode;
|
||||
};
|
||||
|
||||
@observer
|
||||
class GroupListItem extends React.Component<Props> {
|
||||
@observable membersModalOpen: boolean = false;
|
||||
@observable
|
||||
membersModalOpen = false;
|
||||
|
||||
handleMembersModalOpen = () => {
|
||||
this.membersModalOpen = true;
|
||||
@@ -37,14 +37,12 @@ class GroupListItem extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { group, groupMemberships, showFacepile, renderActions } = this.props;
|
||||
|
||||
const memberCount = group.memberCount;
|
||||
|
||||
const membershipsInGroup = groupMemberships.inGroup(group.id);
|
||||
const users = membershipsInGroup
|
||||
.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);
|
||||
|
||||
const overflow = memberCount - users.length;
|
||||
|
||||
return (
|
||||
@@ -84,7 +82,7 @@ class GroupListItem extends React.Component<Props> {
|
||||
onRequestClose={this.handleMembersModalClose}
|
||||
isOpen={this.membersModalOpen}
|
||||
>
|
||||
<GroupMembers group={group} onSubmit={this.handleMembersModalClose} />
|
||||
<GroupMembers group={group} />
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
@@ -107,4 +105,4 @@ const Title = styled.span`
|
||||
}
|
||||
`;
|
||||
|
||||
export default inject("groupMemberships")(GroupListItem);
|
||||
export default withStores(GroupListItem);
|
||||
@@ -1,17 +1,16 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { Dialog, DialogBackdrop, useDialogState } from "reakit/Dialog";
|
||||
import styled from "styled-components";
|
||||
import Scrollable from "components/Scrollable";
|
||||
import usePrevious from "hooks/usePrevious";
|
||||
import Scrollable from "~/components/Scrollable";
|
||||
import usePrevious from "~/hooks/usePrevious";
|
||||
|
||||
type Props = {|
|
||||
children?: React.Node,
|
||||
isOpen: boolean,
|
||||
title?: string,
|
||||
onRequestClose: () => void,
|
||||
|};
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
isOpen: boolean;
|
||||
title?: string;
|
||||
onRequestClose: () => void;
|
||||
};
|
||||
|
||||
const Guide = ({
|
||||
children,
|
||||
@@ -20,13 +19,16 @@ const Guide = ({
|
||||
onRequestClose,
|
||||
...rest
|
||||
}: Props) => {
|
||||
const dialog = useDialogState({ animated: 250 });
|
||||
const dialog = useDialogState({
|
||||
animated: 250,
|
||||
});
|
||||
const wasOpen = usePrevious(isOpen);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!wasOpen && isOpen) {
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
if (wasOpen && !isOpen) {
|
||||
dialog.hide();
|
||||
}
|
||||
@@ -1,22 +1,20 @@
|
||||
// @flow
|
||||
import { throttle } from "lodash";
|
||||
import { observer } from "mobx-react";
|
||||
import { transparentize } from "polished";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import Fade from "components/Fade";
|
||||
import Flex from "components/Flex";
|
||||
import Fade from "~/components/Fade";
|
||||
import Flex from "~/components/Flex";
|
||||
|
||||
type Props = {|
|
||||
breadcrumb?: React.Node,
|
||||
title: React.Node,
|
||||
actions?: React.Node,
|
||||
|};
|
||||
type Props = {
|
||||
breadcrumb?: React.ReactNode;
|
||||
title: React.ReactNode;
|
||||
actions?: React.ReactNode;
|
||||
};
|
||||
|
||||
function Header({ breadcrumb, title, actions }: Props) {
|
||||
const [isScrolled, setScrolled] = React.useState(false);
|
||||
|
||||
const handleScroll = React.useCallback(
|
||||
throttle(() => setScrolled(window.scrollY > 75), 50),
|
||||
[]
|
||||
@@ -24,7 +22,6 @@ function Header({ breadcrumb, title, actions }: Props) {
|
||||
|
||||
React.useEffect(() => {
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
|
||||
return () => window.removeEventListener("scroll", handleScroll);
|
||||
}, [handleScroll]);
|
||||
|
||||
@@ -39,7 +36,7 @@ function Header({ breadcrumb, title, actions }: Props) {
|
||||
<Wrapper align="center" shrink={false}>
|
||||
{breadcrumb ? <Breadcrumbs>{breadcrumb}</Breadcrumbs> : null}
|
||||
{isScrolled ? (
|
||||
<Title align="center" justify="flex-start" onClick={handleClickTitle}>
|
||||
<Title onClick={handleClickTitle}>
|
||||
<Fade>{title}</Fade>
|
||||
</Title>
|
||||
) : (
|
||||
@@ -1,7 +1,6 @@
|
||||
// @flow
|
||||
import styled from "styled-components";
|
||||
|
||||
const Heading = styled.h1`
|
||||
const Heading = styled.h1<{ centered?: boolean }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
${(props) => (props.centered ? "text-align: center;" : "")}
|
||||
@@ -1,7 +1,6 @@
|
||||
// @flow
|
||||
import styled from "styled-components";
|
||||
|
||||
const HelpText = styled.p`
|
||||
const HelpText = styled.p<{ small?: boolean }>`
|
||||
margin-top: 0;
|
||||
color: ${(props) => props.theme.textSecondary};
|
||||
font-size: ${(props) => (props.small ? "13px" : "inherit")};
|
||||
@@ -1,24 +1,24 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import replace from "string-replace-to-array";
|
||||
import styled from "styled-components";
|
||||
|
||||
type Props = {
|
||||
highlight: ?string | RegExp,
|
||||
processResult?: (tag: string) => string,
|
||||
text: string,
|
||||
caseSensitive?: boolean,
|
||||
type Props = React.HTMLAttributes<HTMLSpanElement> & {
|
||||
highlight: (string | null | undefined) | RegExp;
|
||||
processResult?: (tag: string) => string;
|
||||
text: string | undefined;
|
||||
caseSensitive?: boolean;
|
||||
};
|
||||
|
||||
function Highlight({
|
||||
highlight,
|
||||
processResult,
|
||||
caseSensitive,
|
||||
text,
|
||||
text = "",
|
||||
...rest
|
||||
}: Props) {
|
||||
let regex;
|
||||
let index = 0;
|
||||
|
||||
if (highlight instanceof RegExp) {
|
||||
regex = highlight;
|
||||
} else {
|
||||
@@ -27,10 +27,11 @@ function Highlight({
|
||||
caseSensitive ? "g" : "gi"
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span {...rest}>
|
||||
{highlight
|
||||
? replace(text, regex, (tag) => (
|
||||
? replace(text, regex, (tag: string) => (
|
||||
<Mark key={index++}>
|
||||
{processResult ? processResult(tag) : tag}
|
||||
</Mark>
|
||||
@@ -1,32 +1,29 @@
|
||||
// @flow
|
||||
import { inject } from "mobx-react";
|
||||
import { transparentize } from "polished";
|
||||
import * as React from "react";
|
||||
import { Portal } from "react-portal";
|
||||
import styled from "styled-components";
|
||||
import parseDocumentSlug from "shared/utils/parseDocumentSlug";
|
||||
import DocumentsStore from "stores/DocumentsStore";
|
||||
import HoverPreviewDocument from "components/HoverPreviewDocument";
|
||||
import { fadeAndSlideDown } from "styles/animations";
|
||||
import { isInternalUrl } from "utils/urls";
|
||||
import parseDocumentSlug from "@shared/utils/parseDocumentSlug";
|
||||
import HoverPreviewDocument from "~/components/HoverPreviewDocument";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import { fadeAndSlideDown } from "~/styles/animations";
|
||||
import { isInternalUrl } from "~/utils/urls";
|
||||
|
||||
const DELAY_OPEN = 300;
|
||||
const DELAY_CLOSE = 300;
|
||||
|
||||
type Props = {
|
||||
node: HTMLAnchorElement,
|
||||
event: MouseEvent,
|
||||
documents: DocumentsStore,
|
||||
onClose: () => void,
|
||||
node: HTMLAnchorElement;
|
||||
event: MouseEvent;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
function HoverPreviewInternal({ node, documents, onClose, event }: Props) {
|
||||
function HoverPreviewInternal({ node, onClose }: Props) {
|
||||
const { documents } = useStores();
|
||||
const slug = parseDocumentSlug(node.href);
|
||||
|
||||
const [isVisible, setVisible] = React.useState(false);
|
||||
const timerClose = React.useRef();
|
||||
const timerOpen = React.useRef();
|
||||
const cardRef = React.useRef<?HTMLDivElement>();
|
||||
const timerClose = React.useRef<ReturnType<typeof setTimeout>>();
|
||||
const timerOpen = React.useRef<ReturnType<typeof setTimeout>>();
|
||||
const cardRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const startCloseTimer = () => {
|
||||
stopOpenTimer();
|
||||
@@ -54,9 +51,7 @@ function HoverPreviewInternal({ node, documents, onClose, event }: Props) {
|
||||
|
||||
React.useEffect(() => {
|
||||
if (slug) {
|
||||
documents.prefetchDocument(slug, {
|
||||
prefetch: true,
|
||||
});
|
||||
documents.prefetchDocument(slug);
|
||||
}
|
||||
|
||||
startOpenTimer();
|
||||
@@ -64,6 +59,7 @@ function HoverPreviewInternal({ node, documents, onClose, event }: Props) {
|
||||
if (cardRef.current) {
|
||||
cardRef.current.addEventListener("mouseenter", stopCloseTimer);
|
||||
}
|
||||
|
||||
if (cardRef.current) {
|
||||
cardRef.current.addEventListener("mouseleave", startCloseTimer);
|
||||
}
|
||||
@@ -71,7 +67,6 @@ function HoverPreviewInternal({ node, documents, onClose, event }: Props) {
|
||||
node.addEventListener("mouseout", startCloseTimer);
|
||||
node.addEventListener("mouseover", stopCloseTimer);
|
||||
node.addEventListener("mouseover", startOpenTimer);
|
||||
|
||||
return () => {
|
||||
node.removeEventListener("mouseout", startCloseTimer);
|
||||
node.removeEventListener("mouseover", stopCloseTimer);
|
||||
@@ -80,6 +75,7 @@ function HoverPreviewInternal({ node, documents, onClose, event }: Props) {
|
||||
if (cardRef.current) {
|
||||
cardRef.current.removeEventListener("mouseenter", stopCloseTimer);
|
||||
}
|
||||
|
||||
if (cardRef.current) {
|
||||
cardRef.current.removeEventListener("mouseleave", startCloseTimer);
|
||||
}
|
||||
@@ -88,12 +84,10 @@ function HoverPreviewInternal({ node, documents, onClose, event }: Props) {
|
||||
clearTimeout(timerClose.current);
|
||||
}
|
||||
};
|
||||
}, [node]);
|
||||
}, [node, slug]);
|
||||
|
||||
const anchorBounds = node.getBoundingClientRect();
|
||||
const cardBounds = cardRef.current
|
||||
? cardRef.current.getBoundingClientRect()
|
||||
: undefined;
|
||||
const cardBounds = cardRef.current?.getBoundingClientRect();
|
||||
const left = cardBounds
|
||||
? Math.min(anchorBounds.left, window.innerWidth - 16 - 350)
|
||||
: anchorBounds.left;
|
||||
@@ -108,7 +102,7 @@ function HoverPreviewInternal({ node, documents, onClose, event }: Props) {
|
||||
>
|
||||
<div ref={cardRef}>
|
||||
<HoverPreviewDocument url={node.href}>
|
||||
{(content) =>
|
||||
{(content: React.ReactNode) =>
|
||||
isVisible ? (
|
||||
<Animate>
|
||||
<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;
|
||||
position: ${({ fixed }) => (fixed ? "fixed" : "absolute")};
|
||||
z-index: ${(props) => props.theme.depths.hoverPreview};
|
||||
@@ -207,7 +201,7 @@ const Position = styled.div`
|
||||
${({ left }) => (left !== undefined ? `left: ${left}px` : "")};
|
||||
`;
|
||||
|
||||
const Pointer = styled.div`
|
||||
const Pointer = styled.div<{ offset: number }>`
|
||||
top: -22px;
|
||||
left: ${(props) => props.offset}px;
|
||||
width: 22px;
|
||||
@@ -238,4 +232,4 @@ const Pointer = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
export default inject("documents")(HoverPreview);
|
||||
export default HoverPreview;
|
||||
@@ -1,25 +1,24 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import parseDocumentSlug from "shared/utils/parseDocumentSlug";
|
||||
import DocumentMetaWithViews from "components/DocumentMetaWithViews";
|
||||
import Editor from "components/Editor";
|
||||
import useStores from "hooks/useStores";
|
||||
import parseDocumentSlug from "@shared/utils/parseDocumentSlug";
|
||||
import DocumentMetaWithViews from "~/components/DocumentMetaWithViews";
|
||||
import Editor from "~/components/Editor";
|
||||
import useStores from "~/hooks/useStores";
|
||||
|
||||
type Props = {
|
||||
url: string,
|
||||
children: (React.Node) => React.Node,
|
||||
url: string;
|
||||
children: (arg0: React.ReactNode) => React.ReactNode;
|
||||
};
|
||||
|
||||
function HoverPreviewDocument({ url, children }: Props) {
|
||||
const { documents } = useStores();
|
||||
const slug = parseDocumentSlug(url);
|
||||
|
||||
documents.prefetchDocument(slug, {
|
||||
prefetch: true,
|
||||
});
|
||||
if (slug) {
|
||||
documents.prefetchDocument(slug);
|
||||
}
|
||||
|
||||
const document = slug ? documents.getByUrl(slug) : undefined;
|
||||
if (!document) return null;
|
||||
@@ -50,4 +49,5 @@ const Heading = styled.h2`
|
||||
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);
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import {
|
||||
BookmarkedIcon,
|
||||
CollectionIcon,
|
||||
@@ -36,19 +35,22 @@ import { useTranslation } from "react-i18next";
|
||||
import { useMenuState, MenuButton, MenuItem } from "reakit/Menu";
|
||||
import styled from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import ContextMenu from "components/ContextMenu";
|
||||
import Flex from "components/Flex";
|
||||
import HelpText from "components/HelpText";
|
||||
import { LabelText } from "components/Input";
|
||||
import NudeButton from "components/NudeButton";
|
||||
import ContextMenu from "~/components/ContextMenu";
|
||||
import Flex from "~/components/Flex";
|
||||
import HelpText from "~/components/HelpText";
|
||||
import { LabelText } from "~/components/Input";
|
||||
import NudeButton from "~/components/NudeButton";
|
||||
|
||||
const style = { width: 30, height: 30 };
|
||||
|
||||
const TwitterPicker = React.lazy(() =>
|
||||
import(
|
||||
/* webpackChunkName: "twitter-picker" */
|
||||
"react-color/lib/components/twitter/Twitter"
|
||||
)
|
||||
const style = {
|
||||
width: 30,
|
||||
height: 30,
|
||||
};
|
||||
const TwitterPicker = React.lazy(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "twitter-picker" */
|
||||
"react-color/lib/components/twitter/Twitter"
|
||||
)
|
||||
);
|
||||
|
||||
export const icons = {
|
||||
@@ -173,7 +175,6 @@ export const icons = {
|
||||
keywords: "warning alert error",
|
||||
},
|
||||
};
|
||||
|
||||
const colors = [
|
||||
"#4E5C6E",
|
||||
"#0366d6",
|
||||
@@ -186,14 +187,13 @@ const colors = [
|
||||
"#FF4DFA",
|
||||
"#2F362F",
|
||||
];
|
||||
|
||||
type Props = {|
|
||||
onOpen?: () => void,
|
||||
onClose?: () => void,
|
||||
onChange: (color: string, icon: string) => void,
|
||||
icon: string,
|
||||
color: string,
|
||||
|};
|
||||
type Props = {
|
||||
onOpen?: () => void;
|
||||
onClose?: () => void;
|
||||
onChange: (color: string, icon: string) => void;
|
||||
icon: string;
|
||||
color: string;
|
||||
};
|
||||
|
||||
function IconPicker({ onOpen, onClose, icon, color, onChange }: Props) {
|
||||
const { t } = useTranslation();
|
||||
@@ -1,13 +1,12 @@
|
||||
// @flow
|
||||
import { observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { VisuallyHidden } from "reakit/VisuallyHidden";
|
||||
import styled from "styled-components";
|
||||
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;
|
||||
flex: 1;
|
||||
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;
|
||||
flex: 1;
|
||||
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")};
|
||||
width: ${(props) => (props.short ? "49%" : "auto")};
|
||||
max-width: ${(props) => (props.short ? "350px" : "100%")};
|
||||
@@ -63,7 +67,11 @@ const IconWrapper = styled.span`
|
||||
height: 24px;
|
||||
`;
|
||||
|
||||
export const Outline = styled(Flex)`
|
||||
export const Outline = styled(Flex)<{
|
||||
margin?: string | number;
|
||||
hasError?: boolean;
|
||||
focused?: boolean;
|
||||
}>`
|
||||
flex: 1;
|
||||
margin: ${(props) =>
|
||||
props.margin !== undefined ? props.margin : "0 0 16px"};
|
||||
@@ -88,56 +96,58 @@ export const LabelText = styled.div`
|
||||
display: inline-block;
|
||||
`;
|
||||
|
||||
export type Props = {|
|
||||
type?: "text" | "email" | "checkbox" | "search" | "textarea",
|
||||
value?: string,
|
||||
label?: string,
|
||||
className?: string,
|
||||
labelHidden?: boolean,
|
||||
flex?: boolean,
|
||||
short?: boolean,
|
||||
margin?: string | number,
|
||||
icon?: React.Node,
|
||||
name?: string,
|
||||
minLength?: number,
|
||||
maxLength?: number,
|
||||
autoFocus?: boolean,
|
||||
autoComplete?: boolean | string,
|
||||
readOnly?: boolean,
|
||||
required?: boolean,
|
||||
disabled?: boolean,
|
||||
placeholder?: string,
|
||||
export type Props = {
|
||||
type?: "text" | "email" | "checkbox" | "search" | "textarea";
|
||||
value?: string;
|
||||
label?: string;
|
||||
className?: string;
|
||||
labelHidden?: boolean;
|
||||
flex?: boolean;
|
||||
short?: boolean;
|
||||
margin?: string | number;
|
||||
icon?: React.ReactNode;
|
||||
name?: string;
|
||||
minLength?: number;
|
||||
maxLength?: number;
|
||||
autoFocus?: boolean;
|
||||
autoComplete?: boolean | string;
|
||||
readOnly?: boolean;
|
||||
required?: boolean;
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
onChange?: (
|
||||
ev: SyntheticInputEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||
) => mixed,
|
||||
onKeyDown?: (ev: SyntheticKeyboardEvent<HTMLInputElement>) => mixed,
|
||||
onFocus?: (ev: SyntheticEvent<>) => mixed,
|
||||
onBlur?: (ev: SyntheticEvent<>) => mixed,
|
||||
|};
|
||||
ev: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||
) => unknown;
|
||||
onKeyDown?: (ev: React.KeyboardEvent<HTMLInputElement>) => unknown;
|
||||
onFocus?: (ev: React.SyntheticEvent) => unknown;
|
||||
onBlur?: (ev: React.SyntheticEvent) => unknown;
|
||||
};
|
||||
|
||||
@observer
|
||||
class Input extends React.Component<Props> {
|
||||
input: ?HTMLInputElement;
|
||||
@observable focused: boolean = false;
|
||||
input = React.createRef<HTMLInputElement | HTMLTextAreaElement>();
|
||||
|
||||
handleBlur = (ev: SyntheticEvent<>) => {
|
||||
@observable
|
||||
focused = false;
|
||||
|
||||
handleBlur = (ev: React.SyntheticEvent) => {
|
||||
this.focused = false;
|
||||
|
||||
if (this.props.onBlur) {
|
||||
this.props.onBlur(ev);
|
||||
}
|
||||
};
|
||||
|
||||
handleFocus = (ev: SyntheticEvent<>) => {
|
||||
handleFocus = (ev: React.SyntheticEvent) => {
|
||||
this.focused = true;
|
||||
|
||||
if (this.props.onFocus) {
|
||||
this.props.onFocus(ev);
|
||||
}
|
||||
};
|
||||
|
||||
focus() {
|
||||
if (this.input) {
|
||||
this.input.focus();
|
||||
}
|
||||
this.input.current?.focus();
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -155,7 +165,8 @@ class Input extends React.Component<Props> {
|
||||
...rest
|
||||
} = this.props;
|
||||
|
||||
const InputComponent = type === "textarea" ? RealTextarea : RealInput;
|
||||
const InputComponent: React.ComponentType =
|
||||
type === "textarea" ? RealTextarea : RealInput;
|
||||
const wrappedLabel = <LabelText>{label}</LabelText>;
|
||||
|
||||
return (
|
||||
@@ -170,11 +181,12 @@ class Input extends React.Component<Props> {
|
||||
<Outline focused={this.focused} margin={margin}>
|
||||
{icon && <IconWrapper>{icon}</IconWrapper>}
|
||||
<InputComponent
|
||||
ref={(ref) => (this.input = ref)}
|
||||
// @ts-expect-error no idea why this is not working
|
||||
ref={this.input}
|
||||
onBlur={this.handleBlur}
|
||||
onFocus={this.handleFocus}
|
||||
type={type === "textarea" ? undefined : type}
|
||||
hasIcon={!!icon}
|
||||
type={type === "textarea" ? undefined : type}
|
||||
{...rest}
|
||||
/>
|
||||
</Outline>
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import styled from "styled-components";
|
||||
import Input from "./Input";
|
||||
|
||||
@@ -1,28 +1,25 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { Trans } from "react-i18next";
|
||||
import styled, { withTheme } from "styled-components";
|
||||
import Editor from "components/Editor";
|
||||
import HelpText from "components/HelpText";
|
||||
import { LabelText, Outline } from "components/Input";
|
||||
import useStores from "hooks/useStores";
|
||||
import styled from "styled-components";
|
||||
import Editor from "~/components/Editor";
|
||||
import HelpText from "~/components/HelpText";
|
||||
import { LabelText, Outline } from "~/components/Input";
|
||||
import useStores from "~/hooks/useStores";
|
||||
|
||||
type Props = {|
|
||||
label: string,
|
||||
minHeight?: number,
|
||||
maxHeight?: number,
|
||||
readOnly?: boolean,
|
||||
|};
|
||||
type Props = {
|
||||
label: string;
|
||||
minHeight?: number;
|
||||
maxHeight?: number;
|
||||
readOnly?: boolean;
|
||||
};
|
||||
|
||||
function InputRich({ label, minHeight, maxHeight, ...rest }: Props) {
|
||||
const [focused, setFocused] = React.useState<boolean>(false);
|
||||
const { ui } = useStores();
|
||||
|
||||
const handleBlur = React.useCallback(() => {
|
||||
setFocused(false);
|
||||
}, []);
|
||||
|
||||
const handleFocus = React.useCallback(() => {
|
||||
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;
|
||||
padding: 8px 12px;
|
||||
min-height: ${({ minHeight }) => (minHeight ? `${minHeight}px` : "0")};
|
||||
@@ -67,4 +68,4 @@ const StyledOutline = styled(Outline)`
|
||||
}
|
||||
`;
|
||||
|
||||
export default observer(withTheme(InputRich));
|
||||
export default observer(InputRich);
|
||||
@@ -1,23 +1,20 @@
|
||||
// @flow
|
||||
import { SearchIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTheme } from "styled-components";
|
||||
import Input, { type Props as InputProps } from "./Input";
|
||||
import Input, { Props as InputProps } from "./Input";
|
||||
|
||||
type Props = {|
|
||||
...InputProps,
|
||||
placeholder?: string,
|
||||
value?: string,
|
||||
onChange: (event: SyntheticInputEvent<>) => mixed,
|
||||
onKeyDown?: (event: SyntheticKeyboardEvent<HTMLInputElement>) => mixed,
|
||||
|};
|
||||
type Props = InputProps & {
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
onChange: (event: React.ChangeEvent<HTMLInputElement>) => unknown;
|
||||
onKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => unknown;
|
||||
};
|
||||
|
||||
export default function InputSearch(props: Props) {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const [isFocused, setIsFocused] = React.useState(false);
|
||||
|
||||
const handleFocus = React.useCallback(() => {
|
||||
setIsFocused(true);
|
||||
}, []);
|
||||
@@ -1,26 +1,25 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import { SearchIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHistory } from "react-router-dom";
|
||||
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 useBoolean from "hooks/useBoolean";
|
||||
import useKeyDown from "hooks/useKeyDown";
|
||||
import { isModKey } from "utils/keyboard";
|
||||
import { searchUrl } from "utils/routeHelpers";
|
||||
|
||||
type Props = {|
|
||||
source: string,
|
||||
placeholder?: string,
|
||||
label?: string,
|
||||
labelHidden?: boolean,
|
||||
collectionId?: string,
|
||||
value: string,
|
||||
onChange: (event: SyntheticInputEvent<>) => mixed,
|
||||
onKeyDown: (event: SyntheticKeyboardEvent<HTMLInputElement>) => mixed,
|
||||
|};
|
||||
type Props = {
|
||||
source: string;
|
||||
placeholder?: string;
|
||||
label?: string;
|
||||
labelHidden?: boolean;
|
||||
collectionId?: string;
|
||||
value?: string;
|
||||
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => unknown;
|
||||
onKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => unknown;
|
||||
};
|
||||
|
||||
function InputSearchPage({
|
||||
onKeyDown,
|
||||
@@ -31,12 +30,11 @@ function InputSearchPage({
|
||||
collectionId,
|
||||
source,
|
||||
}: Props) {
|
||||
const inputRef = React.useRef();
|
||||
const inputRef = React.useRef<Input>(null);
|
||||
const theme = useTheme();
|
||||
const history = useHistory();
|
||||
const { t } = useTranslation();
|
||||
const [isFocused, setFocused, setUnfocused] = useBoolean(false);
|
||||
|
||||
const focus = React.useCallback(() => {
|
||||
inputRef.current?.focus();
|
||||
}, []);
|
||||
@@ -49,7 +47,7 @@ function InputSearchPage({
|
||||
});
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
(ev: SyntheticKeyboardEvent<HTMLInputElement>) => {
|
||||
(ev: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (ev.key === "Enter") {
|
||||
ev.preventDefault();
|
||||
history.push(
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import {
|
||||
Select,
|
||||
SelectOption,
|
||||
@@ -11,32 +10,38 @@ import * as React from "react";
|
||||
import { VisuallyHidden } from "reakit/VisuallyHidden";
|
||||
import scrollIntoView from "smooth-scroll-into-view-if-needed";
|
||||
import styled, { css } from "styled-components";
|
||||
import Button, { Inner } from "components/Button";
|
||||
import HelpText from "components/HelpText";
|
||||
import { Position, Background, Backdrop } from "./ContextMenu";
|
||||
import Button, { Inner } from "~/components/Button";
|
||||
import HelpText from "~/components/HelpText";
|
||||
import useMenuHeight from "~/hooks/useMenuHeight";
|
||||
import { Position, Background, Backdrop, Placement } from "./ContextMenu";
|
||||
import { MenuAnchorCSS } from "./ContextMenu/MenuItem";
|
||||
import { LabelText } from "./Input";
|
||||
import useMenuHeight from "hooks/useMenuHeight";
|
||||
|
||||
export type Option = { label: string, value: string };
|
||||
|
||||
export type Props = {
|
||||
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,
|
||||
export type Option = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
const getOptionFromValue = (options: Option[], value) => {
|
||||
return options.find((option) => option.value === value) || {};
|
||||
export type Props = {
|
||||
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) => {
|
||||
@@ -50,7 +55,6 @@ const InputSelect = (props: Props) => {
|
||||
ariaLabel,
|
||||
onChange,
|
||||
disabled,
|
||||
nude,
|
||||
note,
|
||||
icon,
|
||||
} = props;
|
||||
@@ -69,13 +73,12 @@ const InputSelect = (props: Props) => {
|
||||
disabled,
|
||||
});
|
||||
|
||||
const previousValue = React.useRef(value);
|
||||
const contentRef = React.useRef();
|
||||
const selectedRef = React.useRef();
|
||||
const buttonRef = React.useRef();
|
||||
const previousValue = React.useRef<string | undefined | null>(value);
|
||||
const contentRef = React.useRef<HTMLDivElement>(null);
|
||||
const selectedRef = React.useRef<HTMLDivElement>(null);
|
||||
const buttonRef = React.useRef<HTMLButtonElement>(null);
|
||||
const [offset, setOffset] = React.useState(0);
|
||||
const minWidth = buttonRef.current?.offsetWidth || 0;
|
||||
|
||||
const maxHeight = useMenuHeight(
|
||||
select.visible,
|
||||
select.unstable_disclosureRef
|
||||
@@ -83,16 +86,15 @@ const InputSelect = (props: Props) => {
|
||||
|
||||
React.useEffect(() => {
|
||||
if (previousValue.current === select.selectedValue) return;
|
||||
|
||||
previousValue.current = select.selectedValue;
|
||||
|
||||
async function load() {
|
||||
await onChange(select.selectedValue);
|
||||
}
|
||||
|
||||
load();
|
||||
}, [onChange, select.selectedValue]);
|
||||
|
||||
const wrappedLabel = <LabelText>{label}</LabelText>;
|
||||
|
||||
const selectedValueIndex = options.findIndex(
|
||||
(option) => option.value === select.selectedValue
|
||||
);
|
||||
@@ -102,7 +104,7 @@ const InputSelect = (props: Props) => {
|
||||
if (!select.animating && selectedRef.current) {
|
||||
scrollIntoView(selectedRef.current, {
|
||||
scrollMode: "if-needed",
|
||||
behavior: "instant",
|
||||
behavior: "auto",
|
||||
block: "start",
|
||||
});
|
||||
}
|
||||
@@ -134,18 +136,24 @@ const InputSelect = (props: Props) => {
|
||||
neutral
|
||||
disclosure
|
||||
className={className}
|
||||
nude={nude}
|
||||
icon={icon}
|
||||
{...props}
|
||||
>
|
||||
{getOptionFromValue(options, select.selectedValue).label || (
|
||||
{getOptionFromValue(options, select.selectedValue)?.label || (
|
||||
<Placeholder>Select a {ariaLabel.toLowerCase()}</Placeholder>
|
||||
)}
|
||||
</StyledButton>
|
||||
)}
|
||||
</Select>
|
||||
<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 rightAnchor = props.placement === "bottom-end";
|
||||
|
||||
@@ -163,8 +171,13 @@ const InputSelect = (props: Props) => {
|
||||
rightAnchor={rightAnchor}
|
||||
style={
|
||||
maxHeight && topAnchor
|
||||
? { maxHeight, minWidth }
|
||||
: { minWidth }
|
||||
? {
|
||||
maxHeight,
|
||||
minWidth,
|
||||
}
|
||||
: {
|
||||
minWidth,
|
||||
}
|
||||
}
|
||||
>
|
||||
{select.visible || select.animating
|
||||
@@ -173,7 +186,7 @@ const InputSelect = (props: Props) => {
|
||||
{...select}
|
||||
value={option.value}
|
||||
key={option.value}
|
||||
animating={select.animating}
|
||||
$animating={select.animating}
|
||||
ref={
|
||||
select.selectedValue === option.value
|
||||
? selectedRef
|
||||
@@ -201,7 +214,6 @@ const InputSelect = (props: Props) => {
|
||||
</SelectPopover>
|
||||
</Wrapper>
|
||||
{note && <HelpText small>{note}</HelpText>}
|
||||
|
||||
{(select.visible || select.animating) && <Backdrop />}
|
||||
</>
|
||||
);
|
||||
@@ -217,7 +229,7 @@ const Spacer = styled.div`
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
const StyledButton = styled(Button)`
|
||||
const StyledButton = styled(Button)<{ nude?: boolean }>`
|
||||
font-weight: normal;
|
||||
text-transform: none;
|
||||
margin-bottom: 16px;
|
||||
@@ -243,17 +255,17 @@ const StyledButton = styled(Button)`
|
||||
}
|
||||
`;
|
||||
|
||||
export const StyledSelectOption = styled(SelectOption)`
|
||||
export const StyledSelectOption = styled(SelectOption)<{ $animating: boolean }>`
|
||||
${MenuAnchorCSS}
|
||||
|
||||
${(props) =>
|
||||
props.animating &&
|
||||
props.$animating &&
|
||||
css`
|
||||
pointer-events: none;
|
||||
`}
|
||||
`;
|
||||
|
||||
const Wrapper = styled.label`
|
||||
const Wrapper = styled.label<{ short?: boolean }>`
|
||||
display: block;
|
||||
max-width: ${(props) => (props.short ? "350px" : "100%")};
|
||||
`;
|
||||
@@ -1,19 +1,25 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
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(
|
||||
props: $Rest<$Exact<Props>, {| options: Array<Option>, ariaLabel: string |}>
|
||||
props: $Diff<
|
||||
Props,
|
||||
{
|
||||
options: Array<Option>;
|
||||
ariaLabel: string;
|
||||
}
|
||||
>
|
||||
) {
|
||||
const { value, onChange, ...rest } = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleChange = React.useCallback(
|
||||
(value) => {
|
||||
if (value === "no_access") {
|
||||
value = "";
|
||||
}
|
||||
|
||||
onChange(value);
|
||||
},
|
||||
[onChange]
|
||||
@@ -23,9 +29,18 @@ export default function InputSelectPermission(
|
||||
<InputSelect
|
||||
label={t("Default access")}
|
||||
options={[
|
||||
{ label: t("View and edit"), value: "read_write" },
|
||||
{ label: t("View only"), value: "read" },
|
||||
{ label: t("No access"), value: "no_access" },
|
||||
{
|
||||
label: t("View and edit"),
|
||||
value: "read_write",
|
||||
},
|
||||
{
|
||||
label: t("View only"),
|
||||
value: "read",
|
||||
},
|
||||
{
|
||||
label: t("No access"),
|
||||
value: "no_access",
|
||||
},
|
||||
]}
|
||||
ariaLabel={t("Default access")}
|
||||
value={value || "no_access"}
|
||||
@@ -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;
|
||||
39
app/components/InputSelectRole.tsx
Normal file
39
app/components/InputSelectRole.tsx
Normal 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;
|
||||
@@ -1,4 +1,3 @@
|
||||
// @flow
|
||||
import styled from "styled-components";
|
||||
|
||||
const Key = styled.kbd`
|
||||
@@ -1,13 +1,12 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import Flex from "components/Flex";
|
||||
import Flex from "~/components/Flex";
|
||||
|
||||
type Props = {|
|
||||
label: React.Node | string,
|
||||
children: React.Node,
|
||||
|};
|
||||
type Props = {
|
||||
label: React.ReactNode | string;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
const Labeled = ({ label, children, ...props }: Props) => (
|
||||
<Flex column {...props}>
|
||||
@@ -1,17 +1,16 @@
|
||||
// @flow
|
||||
import { find } from "lodash";
|
||||
import * as React from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import { languages, languageOptions } from "shared/i18n";
|
||||
import ButtonLink from "components/ButtonLink";
|
||||
import Flex from "components/Flex";
|
||||
import NoticeTip from "components/NoticeTip";
|
||||
import useCurrentUser from "hooks/useCurrentUser";
|
||||
import useStores from "hooks/useStores";
|
||||
import { detectLanguage } from "utils/language";
|
||||
import { languages, languageOptions } from "@shared/i18n";
|
||||
import ButtonLink from "~/components/ButtonLink";
|
||||
import Flex from "~/components/Flex";
|
||||
import NoticeTip from "~/components/NoticeTip";
|
||||
import useCurrentUser from "~/hooks/useCurrentUser";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import { detectLanguage } from "~/utils/language";
|
||||
|
||||
function Icon(props) {
|
||||
function Icon(props: any) {
|
||||
return (
|
||||
<svg
|
||||
width="32"
|
||||
@@ -65,8 +64,11 @@ export default function LanguagePrompt() {
|
||||
<LanguageIcon />
|
||||
<span>
|
||||
<Trans>
|
||||
Outline is available in your language {{ optionLabel }}, would you
|
||||
like to change?
|
||||
Outline is available in your language{" "}
|
||||
{{
|
||||
optionLabel,
|
||||
}}
|
||||
, would you like to change?
|
||||
</Trans>
|
||||
<br />
|
||||
<Link
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user