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