Collaborative editing (#1660)
This commit is contained in:
parent
0a998789a3
commit
801f6681ba
|
@ -29,6 +29,10 @@ REDIS_URL=redis://localhost:6479
|
||||||
URL=http://localhost:3000
|
URL=http://localhost:3000
|
||||||
PORT=3000
|
PORT=3000
|
||||||
|
|
||||||
|
# ALPHA – See [documentation](docs/SERVICES.md) on running the alpha version of
|
||||||
|
# the collaboration server.
|
||||||
|
COLLABORATION_URL=
|
||||||
|
|
||||||
# To support uploading of images for avatars and document attachments an
|
# To support uploading of images for avatars and document attachments an
|
||||||
# s3-compatible storage must be provided. AWS S3 is recommended for redundency
|
# s3-compatible storage must be provided. AWS S3 is recommended for redundency
|
||||||
# however if you want to keep all file storage local an alternative such as
|
# however if you want to keep all file storage local an alternative such as
|
||||||
|
|
|
@ -11,6 +11,11 @@
|
||||||
.*/node_modules/react-side-effect/.*
|
.*/node_modules/react-side-effect/.*
|
||||||
.*/node_modules/fbjs/.*
|
.*/node_modules/fbjs/.*
|
||||||
.*/node_modules/config-chain/.*
|
.*/node_modules/config-chain/.*
|
||||||
|
.*/node_modules/yjs/.*
|
||||||
|
.*/node_modules/y-prosemirror/.*
|
||||||
|
.*/node_modules/y-protocols/.*
|
||||||
|
.*/node_modules/y-indexeddb/.*
|
||||||
|
.*/node_modules/lib0/.*
|
||||||
.*/server/scripts/.*
|
.*/server/scripts/.*
|
||||||
*.test.js
|
*.test.js
|
||||||
|
|
||||||
|
|
2
Makefile
2
Makefile
|
@ -2,7 +2,7 @@ up:
|
||||||
docker-compose up -d redis postgres s3
|
docker-compose up -d redis postgres s3
|
||||||
yarn install --pure-lockfile
|
yarn install --pure-lockfile
|
||||||
yarn sequelize db:migrate
|
yarn sequelize db:migrate
|
||||||
yarn dev
|
yarn dev:watch
|
||||||
|
|
||||||
build:
|
build:
|
||||||
docker-compose build --pull outline
|
docker-compose build --pull outline
|
||||||
|
|
4
Procfile
4
Procfile
|
@ -1,2 +1,2 @@
|
||||||
web: node ./build/server/index.js --services=web,websockets
|
web: yarn start --services=web,websockets
|
||||||
worker: node ./build/server/index.js --services=worker
|
worker: yarn start --services=worker
|
||||||
|
|
47
README.md
47
README.md
|
@ -1,5 +1,3 @@
|
||||||
|
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://user-images.githubusercontent.com/31465/34380645-bd67f474-eb0b-11e7-8d03-0151c1730654.png" height="29" />
|
<img src="https://user-images.githubusercontent.com/31465/34380645-bd67f474-eb0b-11e7-8d03-0151c1730654.png" height="29" />
|
||||||
</p>
|
</p>
|
||||||
|
@ -30,7 +28,6 @@ Outline requires the following dependencies:
|
||||||
- AWS S3 bucket or compatible API for file storage
|
- AWS S3 bucket or compatible API for file storage
|
||||||
- Slack or Google developer application for authentication
|
- Slack or Google developer application for authentication
|
||||||
|
|
||||||
|
|
||||||
## Self-Hosted Production
|
## Self-Hosted Production
|
||||||
|
|
||||||
### Docker
|
### Docker
|
||||||
|
@ -41,16 +38,20 @@ For a manual self-hosted production installation these are the recommended steps
|
||||||
1. Download the latest official Docker image, new releases are available around the middle of every month:
|
1. Download the latest official Docker image, new releases are available around the middle of every month:
|
||||||
|
|
||||||
`docker pull outlinewiki/outline`
|
`docker pull outlinewiki/outline`
|
||||||
|
|
||||||
1. Using the [.env.sample](.env.sample) as a reference, set the required variables in your production environment. You can export the environment variables directly, or create a `.env` file and pass it to the docker image like so:
|
1. Using the [.env.sample](.env.sample) as a reference, set the required variables in your production environment. You can export the environment variables directly, or create a `.env` file and pass it to the docker image like so:
|
||||||
|
|
||||||
`docker run --env-file=.env outlinewiki/outline`
|
`docker run --env-file=.env outlinewiki/outline`
|
||||||
|
|
||||||
1. Setup the database with `yarn db:migrate`. Production assumes an SSL connection to the database by default, if
|
1. Setup the database with `yarn db:migrate`. Production assumes an SSL connection to the database by default, if
|
||||||
Postgres is on the same machine and is not SSL you can migrate with `yarn db:migrate --env=production-ssl-disabled`, for example:
|
Postgres is on the same machine and is not SSL you can migrate with `yarn db:migrate --env=production-ssl-disabled`, for example:
|
||||||
|
|
||||||
`docker run --rm outlinewiki/outline yarn db:migrate`
|
`docker run --rm outlinewiki/outline yarn db:migrate`
|
||||||
|
|
||||||
1. Start the container:
|
1. Start the container:
|
||||||
|
|
||||||
`docker run outlinewiki/outline`
|
`docker run outlinewiki/outline`
|
||||||
|
|
||||||
1. Visit http://you_server_ip:3000 and you should be able to see Outline page
|
1. Visit http://you_server_ip:3000 and you should be able to see Outline page
|
||||||
|
|
||||||
> Port number can be changed using the `PORT` environment variable
|
> Port number can be changed using the `PORT` environment variable
|
||||||
|
@ -79,29 +80,27 @@ If you're running Outline by cloning this repository, run the following command
|
||||||
yarn run upgrade
|
yarn run upgrade
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## Local Development
|
## Local Development
|
||||||
|
|
||||||
For contributing features and fixes you can quickly get an environment running using Docker by following these steps:
|
For contributing features and fixes you can quickly get an environment running using Docker by following these steps:
|
||||||
|
|
||||||
1. Install these dependencies if you don't already have them
|
1. Install these dependencies if you don't already have them
|
||||||
1. [Docker for Desktop](https://www.docker.com)
|
1. [Docker for Desktop](https://www.docker.com)
|
||||||
1. [Node.js](https://nodejs.org/) (v12 LTS preferred)
|
1. [Node.js](https://nodejs.org/) (v12 LTS preferred)
|
||||||
1. [Yarn](https://yarnpkg.com)
|
1. [Yarn](https://yarnpkg.com)
|
||||||
1. Clone this repo
|
1. Clone this repo
|
||||||
1. Register a Slack app at https://api.slack.com/apps
|
1. Register a Slack app at https://api.slack.com/apps
|
||||||
1. Copy the file `.env.sample` to `.env`
|
1. Copy the file `.env.sample` to `.env`
|
||||||
1. Fill out the following fields:
|
1. Fill out the following fields:
|
||||||
1. `SECRET_KEY` (follow instructions in the comments at the top of `.env`)
|
1. `SECRET_KEY` (follow instructions in the comments at the top of `.env`)
|
||||||
1. `SLACK_KEY` (this is called "Client ID" in Slack admin)
|
1. `SLACK_KEY` (this is called "Client ID" in Slack admin)
|
||||||
1. `SLACK_SECRET` (this is called "Client Secret" in Slack admin)
|
1. `SLACK_SECRET` (this is called "Client Secret" in Slack admin)
|
||||||
1. Configure your Slack app's Oauth & Permissions settings
|
1. Configure your Slack app's Oauth & Permissions settings
|
||||||
1. Slack recently prevented the use of `http` protocol for localhost. For local development, you can use a tool like [ngrok](https://ngrok.com) or a package like `mkcert`. ([How to use HTTPS for local development](https://web.dev/how-to-use-local-https/))
|
1. Slack recently prevented the use of `http` protocol for localhost. For local development, you can use a tool like [ngrok](https://ngrok.com) or a package like `mkcert`. ([How to use HTTPS for local development](https://web.dev/how-to-use-local-https/))
|
||||||
1. Add `https://my_ngrok_address/auth/slack.callback` as an Oauth redirect URL
|
1. Add `https://my_ngrok_address/auth/slack.callback` as an Oauth redirect URL
|
||||||
1. Ensure that the bot token scope contains at least `users:read`
|
1. Ensure that the bot token scope contains at least `users:read`
|
||||||
1. Run `make up`. This will download dependencies, build and launch a development version of Outline
|
1. Run `make up`. This will download dependencies, build and launch a development version of Outline
|
||||||
|
|
||||||
|
|
||||||
# Contributing
|
# Contributing
|
||||||
|
|
||||||
Outline is built and maintained by a small team – we'd love your help to fix bugs and add features!
|
Outline is built and maintained by a small team – we'd love your help to fix bugs and add features!
|
||||||
|
@ -110,18 +109,16 @@ Before submitting a pull request please let the core team know by creating or co
|
||||||
|
|
||||||
If you’re looking for ways to get started, here's a list of ways to help us improve Outline:
|
If you’re looking for ways to get started, here's a list of ways to help us improve Outline:
|
||||||
|
|
||||||
* [Translation](TRANSLATION.md) into other languages
|
- [Translation](docs/TRANSLATION.md) into other languages
|
||||||
* Issues with [`good first issue`](https://github.com/outline/outline/labels/good%20first%20issue) label
|
- Issues with [`good first issue`](https://github.com/outline/outline/labels/good%20first%20issue) label
|
||||||
* Performance improvements, both on server and frontend
|
- Performance improvements, both on server and frontend
|
||||||
* Developer happiness and documentation
|
- Developer happiness and documentation
|
||||||
* Bugs and other issues listed on GitHub
|
- Bugs and other issues listed on GitHub
|
||||||
|
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
If you're interested in contributing or learning more about the Outline codebase
|
If you're interested in contributing or learning more about the Outline codebase
|
||||||
please refer to the [architecture document](ARCHITECTURE.md) first for a high level overview of how the application is put together.
|
please refer to the [architecture document](docs/ARCHITECTURE.md) first for a high level overview of how the application is put together.
|
||||||
|
|
||||||
|
|
||||||
## Debugging
|
## Debugging
|
||||||
|
|
||||||
|
@ -145,7 +142,7 @@ make test
|
||||||
make watch
|
make watch
|
||||||
```
|
```
|
||||||
|
|
||||||
Once the test database is created with `make test` you may individually run
|
Once the test database is created with `make test` you may individually run
|
||||||
frontend and backend tests directly.
|
frontend and backend tests directly.
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
// @flow
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { DisconnectedIcon } from "outline-icons";
|
||||||
|
import * as React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import styled, { useTheme } from "styled-components";
|
||||||
|
import breakpoint from "styled-components-breakpoint";
|
||||||
|
import Fade from "components/Fade";
|
||||||
|
import NudeButton from "components/NudeButton";
|
||||||
|
import Tooltip from "components/Tooltip";
|
||||||
|
import useStores from "hooks/useStores";
|
||||||
|
|
||||||
|
function ConnectionStatus() {
|
||||||
|
const { ui } = useStores();
|
||||||
|
const theme = useTheme();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return ui.multiplayerStatus === "connecting" ||
|
||||||
|
ui.multiplayerStatus === "disconnected" ? (
|
||||||
|
<Tooltip
|
||||||
|
tooltip={
|
||||||
|
<Centered>
|
||||||
|
<strong>{t("Server connection lost")}</strong>
|
||||||
|
<br />
|
||||||
|
{t("Edits you make will sync once you’re online")}
|
||||||
|
</Centered>
|
||||||
|
}
|
||||||
|
placement="bottom"
|
||||||
|
>
|
||||||
|
<Button>
|
||||||
|
<Fade>
|
||||||
|
<DisconnectedIcon color={theme.sidebarText} />
|
||||||
|
</Fade>
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button = styled(NudeButton)`
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
right: 32px;
|
||||||
|
margin: 24px;
|
||||||
|
|
||||||
|
${breakpoint("tablet")`
|
||||||
|
display: block;
|
||||||
|
`};
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Centered = styled.div`
|
||||||
|
text-align: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default observer(ConnectionStatus);
|
|
@ -3,12 +3,13 @@ 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 { withRouter, type RouterHistory } from "react-router-dom";
|
||||||
|
import { Extension } from "rich-markdown-editor";
|
||||||
import styled, { withTheme } from "styled-components";
|
import styled, { withTheme } from "styled-components";
|
||||||
|
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 embeds from "../embeds";
|
|
||||||
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 { type Theme } from "types";
|
||||||
|
@ -30,6 +31,8 @@ export type Props = {|
|
||||||
grow?: boolean,
|
grow?: boolean,
|
||||||
disableEmbeds?: boolean,
|
disableEmbeds?: boolean,
|
||||||
ui?: UiStore,
|
ui?: UiStore,
|
||||||
|
style?: Object,
|
||||||
|
extensions?: Extension[],
|
||||||
shareId?: ?string,
|
shareId?: ?string,
|
||||||
autoFocus?: boolean,
|
autoFocus?: boolean,
|
||||||
template?: boolean,
|
template?: boolean,
|
||||||
|
@ -246,6 +249,50 @@ const StyledEditor = styled(RichMarkdownEditor)`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ProseMirror {
|
||||||
|
.ProseMirror-yjs-cursor {
|
||||||
|
position: relative;
|
||||||
|
margin-left: -1px;
|
||||||
|
margin-right: -1px;
|
||||||
|
border-left: 1px solid black;
|
||||||
|
border-right: 1px solid black;
|
||||||
|
height: 1em;
|
||||||
|
word-break: normal;
|
||||||
|
&:after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
left: -8px;
|
||||||
|
right: -8px;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
> div {
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: -1.8em;
|
||||||
|
font-size: 13px;
|
||||||
|
background-color: rgb(250, 129, 0);
|
||||||
|
font-style: normal;
|
||||||
|
line-height: normal;
|
||||||
|
user-select: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: white;
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 4px;
|
||||||
|
pointer-events: none;
|
||||||
|
left: -1px;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
> div {
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 100ms ease-in-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const EditorTooltip = ({ children, ...props }) => (
|
const EditorTooltip = ({ children, ...props }) => (
|
||||||
|
|
|
@ -36,7 +36,7 @@ function Header({ breadcrumb, title, actions }: Props) {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper align="center" isCompact={isScrolled} 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 align="center" justify="flex-start" onClick={handleClickTitle}>
|
||||||
|
@ -95,7 +95,7 @@ const Wrapper = styled(Flex)`
|
||||||
}
|
}
|
||||||
|
|
||||||
${breakpoint("tablet")`
|
${breakpoint("tablet")`
|
||||||
padding: ${(props) => (props.isCompact ? "12px" : `24px 24px 0`)};
|
padding: 16px 16px 0;
|
||||||
justify-content: "center";
|
justify-content: "center";
|
||||||
`};
|
`};
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
|
import { cdnPath } from "../../shared/utils/urls";
|
||||||
import useStores from "hooks/useStores";
|
import useStores from "hooks/useStores";
|
||||||
import { cdnPath } from "utils/urls";
|
|
||||||
|
|
||||||
type Props = {|
|
type Props = {|
|
||||||
title: string,
|
title: string,
|
||||||
|
|
|
@ -6,18 +6,45 @@ import Fade from "components/Fade";
|
||||||
import Flex from "components/Flex";
|
import Flex from "components/Flex";
|
||||||
import PlaceholderText from "components/PlaceholderText";
|
import PlaceholderText from "components/PlaceholderText";
|
||||||
|
|
||||||
export default function PlaceholderDocument(props: Object) {
|
export default function PlaceholderDocument({
|
||||||
|
includeTitle,
|
||||||
|
delay,
|
||||||
|
}: {
|
||||||
|
includeTitle?: boolean,
|
||||||
|
delay?: number,
|
||||||
|
}) {
|
||||||
|
const content = (
|
||||||
|
<>
|
||||||
|
<PlaceholderText delay={0.2} />
|
||||||
|
<PlaceholderText delay={0.4} />
|
||||||
|
<PlaceholderText delay={0.6} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (includeTitle === false) {
|
||||||
|
return (
|
||||||
|
<DelayedMount delay={delay}>
|
||||||
|
<Fade>
|
||||||
|
<Flex column auto>
|
||||||
|
{content}
|
||||||
|
</Flex>
|
||||||
|
</Fade>
|
||||||
|
</DelayedMount>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DelayedMount>
|
<DelayedMount delay={delay}>
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<Flex column auto {...props}>
|
<Fade>
|
||||||
<PlaceholderText height={34} maxWidth={70} />
|
<Flex column auto>
|
||||||
<PlaceholderText delay={0.2} maxWidth={40} />
|
<PlaceholderText height={34} maxWidth={70} />
|
||||||
<br />
|
<PlaceholderText delay={0.2} maxWidth={40} />
|
||||||
<PlaceholderText delay={0.2} />
|
<br />
|
||||||
<PlaceholderText delay={0.4} />
|
|
||||||
<PlaceholderText delay={0.6} />
|
{content}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
</Fade>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
</DelayedMount>
|
</DelayedMount>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
import { observer } from "mobx-react";
|
||||||
import { ExpandedIcon } from "outline-icons";
|
import { ExpandedIcon } from "outline-icons";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
@ -84,4 +85,4 @@ const Header = styled.button`
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default TeamButton;
|
export default observer(TeamButton);
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
// @flow
|
||||||
|
import invariant from "invariant";
|
||||||
|
import useStores from "./useStores";
|
||||||
|
|
||||||
|
export default function useCurrentToken() {
|
||||||
|
const { auth } = useStores();
|
||||||
|
invariant(auth.token, "token is required");
|
||||||
|
return auth.token;
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { MoonIcon, SunIcon } from "outline-icons";
|
import { MoonIcon, SunIcon, TrashIcon } from "outline-icons";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { MenuButton, useMenuState } from "reakit/Menu";
|
import { MenuButton, useMenuState } from "reakit/Menu";
|
||||||
|
@ -16,11 +16,14 @@ import KeyboardShortcuts from "scenes/KeyboardShortcuts";
|
||||||
import ContextMenu from "components/ContextMenu";
|
import ContextMenu from "components/ContextMenu";
|
||||||
import Template from "components/ContextMenu/Template";
|
import Template from "components/ContextMenu/Template";
|
||||||
import Guide from "components/Guide";
|
import Guide from "components/Guide";
|
||||||
|
import env from "env";
|
||||||
import useBoolean from "hooks/useBoolean";
|
import useBoolean from "hooks/useBoolean";
|
||||||
import useCurrentTeam from "hooks/useCurrentTeam";
|
import useCurrentTeam from "hooks/useCurrentTeam";
|
||||||
import usePrevious from "hooks/usePrevious";
|
import usePrevious from "hooks/usePrevious";
|
||||||
import useSessions from "hooks/useSessions";
|
import useSessions from "hooks/useSessions";
|
||||||
import useStores from "hooks/useStores";
|
import useStores from "hooks/useStores";
|
||||||
|
import useToasts from "hooks/useToasts";
|
||||||
|
import { deleteAllDatabases } from "utils/developer";
|
||||||
|
|
||||||
type Props = {|
|
type Props = {|
|
||||||
children: (props: any) => React.Node,
|
children: (props: any) => React.Node,
|
||||||
|
@ -33,11 +36,13 @@ function AccountMenu(props: Props) {
|
||||||
placement: "bottom-start",
|
placement: "bottom-start",
|
||||||
modal: true,
|
modal: true,
|
||||||
});
|
});
|
||||||
|
const { showToast } = useToasts();
|
||||||
const { auth, ui } = useStores();
|
const { auth, ui } = useStores();
|
||||||
const { theme, resolvedTheme } = ui;
|
const { theme, resolvedTheme } = ui;
|
||||||
const team = useCurrentTeam();
|
const team = useCurrentTeam();
|
||||||
const previousTheme = usePrevious(theme);
|
const previousTheme = usePrevious(theme);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [includeAlt, setIncludeAlt] = React.useState(false);
|
||||||
const [
|
const [
|
||||||
keyboardShortcutsOpen,
|
keyboardShortcutsOpen,
|
||||||
handleKeyboardShortcutsOpen,
|
handleKeyboardShortcutsOpen,
|
||||||
|
@ -50,6 +55,16 @@ function AccountMenu(props: Props) {
|
||||||
}
|
}
|
||||||
}, [menu, theme, previousTheme]);
|
}, [menu, theme, previousTheme]);
|
||||||
|
|
||||||
|
const handleDeleteAllDatabases = React.useCallback(async () => {
|
||||||
|
await deleteAllDatabases();
|
||||||
|
showToast("IndexedDB cache deleted");
|
||||||
|
menu.hide();
|
||||||
|
}, [showToast, menu]);
|
||||||
|
|
||||||
|
const handleOpenMenu = React.useCallback((event) => {
|
||||||
|
setIncludeAlt(event.altKey);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const items = React.useMemo(() => {
|
const items = React.useMemo(() => {
|
||||||
const otherSessions = sessions.filter(
|
const otherSessions = sessions.filter(
|
||||||
(session) => session.teamId !== team.id && session.url !== team.url
|
(session) => session.teamId !== team.id && session.url !== team.url
|
||||||
|
@ -83,6 +98,20 @@ function AccountMenu(props: Props) {
|
||||||
title: t("Report a bug"),
|
title: t("Report a bug"),
|
||||||
href: githubIssuesUrl(),
|
href: githubIssuesUrl(),
|
||||||
},
|
},
|
||||||
|
...(includeAlt || env.ENVIRONMENT === "development"
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
title: t("Development"),
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: "Delete IndexedDB cache",
|
||||||
|
icon: <TrashIcon />,
|
||||||
|
onClick: handleDeleteAllDatabases,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{
|
{
|
||||||
title: t("Appearance"),
|
title: t("Appearance"),
|
||||||
icon: resolvedTheme === "light" ? <SunIcon /> : <MoonIcon />,
|
icon: resolvedTheme === "light" ? <SunIcon /> : <MoonIcon />,
|
||||||
|
@ -130,7 +159,9 @@ function AccountMenu(props: Props) {
|
||||||
team.url,
|
team.url,
|
||||||
sessions,
|
sessions,
|
||||||
handleKeyboardShortcutsOpen,
|
handleKeyboardShortcutsOpen,
|
||||||
|
handleDeleteAllDatabases,
|
||||||
resolvedTheme,
|
resolvedTheme,
|
||||||
|
includeAlt,
|
||||||
theme,
|
theme,
|
||||||
t,
|
t,
|
||||||
ui,
|
ui,
|
||||||
|
@ -145,7 +176,9 @@ function AccountMenu(props: Props) {
|
||||||
>
|
>
|
||||||
<KeyboardShortcuts />
|
<KeyboardShortcuts />
|
||||||
</Guide>
|
</Guide>
|
||||||
<MenuButton {...menu}>{props.children}</MenuButton>
|
<MenuButton {...menu} onClick={handleOpenMenu}>
|
||||||
|
{props.children}
|
||||||
|
</MenuButton>
|
||||||
<ContextMenu {...menu} aria-label={t("Account")}>
|
<ContextMenu {...menu} aria-label={t("Account")}>
|
||||||
<Template {...menu} items={items} />
|
<Template {...menu} items={items} />
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
|
|
|
@ -12,6 +12,7 @@ import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton";
|
||||||
import Separator from "components/ContextMenu/Separator";
|
import Separator from "components/ContextMenu/Separator";
|
||||||
import CopyToClipboard from "components/CopyToClipboard";
|
import CopyToClipboard from "components/CopyToClipboard";
|
||||||
import MenuIconWrapper from "components/MenuIconWrapper";
|
import MenuIconWrapper from "components/MenuIconWrapper";
|
||||||
|
import useCurrentTeam from "hooks/useCurrentTeam";
|
||||||
import useToasts from "hooks/useToasts";
|
import useToasts from "hooks/useToasts";
|
||||||
import { documentHistoryUrl } from "utils/routeHelpers";
|
import { documentHistoryUrl } from "utils/routeHelpers";
|
||||||
|
|
||||||
|
@ -23,6 +24,7 @@ type Props = {|
|
||||||
|
|
||||||
function RevisionMenu({ document, revisionId, className }: Props) {
|
function RevisionMenu({ document, revisionId, className }: Props) {
|
||||||
const { showToast } = useToasts();
|
const { showToast } = useToasts();
|
||||||
|
const team = useCurrentTeam();
|
||||||
const menu = useMenuState({ modal: true });
|
const menu = useMenuState({ modal: true });
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
@ -55,7 +57,11 @@ function RevisionMenu({ document, revisionId, className }: Props) {
|
||||||
{...menu}
|
{...menu}
|
||||||
/>
|
/>
|
||||||
<ContextMenu {...menu} aria-label={t("Revision options")}>
|
<ContextMenu {...menu} aria-label={t("Revision options")}>
|
||||||
<MenuItem {...menu} onClick={handleRestore}>
|
<MenuItem
|
||||||
|
{...menu}
|
||||||
|
onClick={handleRestore}
|
||||||
|
disabled={team.collaborativeEditing}
|
||||||
|
>
|
||||||
<MenuIconWrapper>
|
<MenuIconWrapper>
|
||||||
<RestoreIcon />
|
<RestoreIcon />
|
||||||
</MenuIconWrapper>
|
</MenuIconWrapper>
|
||||||
|
|
|
@ -14,9 +14,10 @@ import useStores from "hooks/useStores";
|
||||||
|
|
||||||
type Props = {|
|
type Props = {|
|
||||||
document: Document,
|
document: Document,
|
||||||
|
onSelectTemplate: (template: Document) => void,
|
||||||
|};
|
|};
|
||||||
|
|
||||||
function TemplatesMenu({ document }: Props) {
|
function TemplatesMenu({ onSelectTemplate, document }: Props) {
|
||||||
const menu = useMenuState({ modal: true });
|
const menu = useMenuState({ modal: true });
|
||||||
const { documents } = useStores();
|
const { documents } = useStores();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
@ -36,7 +37,7 @@ function TemplatesMenu({ document }: Props) {
|
||||||
const renderTemplate = (template) => (
|
const renderTemplate = (template) => (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key={template.id}
|
key={template.id}
|
||||||
onClick={() => document.updateFromTemplate(template)}
|
onClick={() => onSelectTemplate(template)}
|
||||||
icon={<DocumentIcon />}
|
icon={<DocumentIcon />}
|
||||||
{...menu}
|
{...menu}
|
||||||
>
|
>
|
||||||
|
|
|
@ -10,17 +10,16 @@ import BaseModel from "models/BaseModel";
|
||||||
import User from "models/User";
|
import User from "models/User";
|
||||||
import View from "./View";
|
import View from "./View";
|
||||||
|
|
||||||
type SaveOptions = {
|
type SaveOptions = {|
|
||||||
publish?: boolean,
|
publish?: boolean,
|
||||||
done?: boolean,
|
done?: boolean,
|
||||||
autosave?: boolean,
|
autosave?: boolean,
|
||||||
lastRevision?: number,
|
lastRevision?: number,
|
||||||
};
|
|};
|
||||||
|
|
||||||
export default class Document extends BaseModel {
|
export default class Document extends BaseModel {
|
||||||
@observable isSaving: boolean = false;
|
@observable isSaving: boolean = false;
|
||||||
@observable embedsDisabled: boolean = false;
|
@observable embedsDisabled: boolean = false;
|
||||||
@observable injectTemplate: boolean = false;
|
|
||||||
@observable lastViewedAt: ?string;
|
@observable lastViewedAt: ?string;
|
||||||
store: DocumentsStore;
|
store: DocumentsStore;
|
||||||
|
|
||||||
|
@ -254,15 +253,28 @@ export default class Document extends BaseModel {
|
||||||
};
|
};
|
||||||
|
|
||||||
@action
|
@action
|
||||||
updateFromTemplate = async (template: Document) => {
|
update = async (options: {| ...SaveOptions, title: string |}) => {
|
||||||
this.templateId = template.id;
|
if (this.isSaving) return this;
|
||||||
this.title = template.title;
|
this.isSaving = true;
|
||||||
this.text = template.text;
|
|
||||||
this.injectTemplate = true;
|
try {
|
||||||
|
if (options.lastRevision) {
|
||||||
|
return await this.store.update({
|
||||||
|
id: this.id,
|
||||||
|
title: this.title,
|
||||||
|
lastRevision: options.lastRevision,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Attempting to update without a lastRevision");
|
||||||
|
} finally {
|
||||||
|
this.isSaving = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@action
|
@action
|
||||||
save = async (options: SaveOptions = {}) => {
|
save = async (options: ?SaveOptions) => {
|
||||||
if (this.isSaving) return this;
|
if (this.isSaving) return this;
|
||||||
|
|
||||||
const isCreating = !this.id;
|
const isCreating = !this.id;
|
||||||
|
@ -275,22 +287,22 @@ export default class Document extends BaseModel {
|
||||||
collectionId: this.collectionId,
|
collectionId: this.collectionId,
|
||||||
title: this.title,
|
title: this.title,
|
||||||
text: this.text,
|
text: this.text,
|
||||||
publish: options.publish,
|
publish: options?.publish,
|
||||||
done: options.done,
|
done: options?.done,
|
||||||
autosave: options.autosave,
|
autosave: options?.autosave,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.lastRevision) {
|
if (options?.lastRevision) {
|
||||||
return await this.store.update({
|
return await this.store.update({
|
||||||
id: this.id,
|
id: this.id,
|
||||||
title: this.title,
|
title: this.title,
|
||||||
text: this.text,
|
text: this.text,
|
||||||
templateId: this.templateId,
|
templateId: this.templateId,
|
||||||
lastRevision: options.lastRevision,
|
lastRevision: options?.lastRevision,
|
||||||
publish: options.publish,
|
publish: options?.publish,
|
||||||
done: options.done,
|
done: options?.done,
|
||||||
autosave: options.autosave,
|
autosave: options?.autosave,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ class Team extends BaseModel {
|
||||||
name: string;
|
name: string;
|
||||||
avatarUrl: string;
|
avatarUrl: string;
|
||||||
sharing: boolean;
|
sharing: boolean;
|
||||||
|
collaborativeEditing: boolean;
|
||||||
documentEmbeds: boolean;
|
documentEmbeds: boolean;
|
||||||
guestSignin: boolean;
|
guestSignin: boolean;
|
||||||
subdomain: ?string;
|
subdomain: ?string;
|
||||||
|
|
|
@ -8,6 +8,7 @@ class User extends BaseModel {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
color: string;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
isViewer: boolean;
|
isViewer: boolean;
|
||||||
lastActiveAt: string;
|
lastActiveAt: string;
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
// @flow
|
||||||
|
import { keymap } from "prosemirror-keymap";
|
||||||
|
import { Extension } from "rich-markdown-editor";
|
||||||
|
import {
|
||||||
|
ySyncPlugin,
|
||||||
|
yCursorPlugin,
|
||||||
|
yUndoPlugin,
|
||||||
|
undo,
|
||||||
|
redo,
|
||||||
|
} from "y-prosemirror";
|
||||||
|
import * as Y from "yjs";
|
||||||
|
|
||||||
|
export default class MultiplayerExtension extends Extension {
|
||||||
|
get name() {
|
||||||
|
return "multiplayer";
|
||||||
|
}
|
||||||
|
|
||||||
|
get plugins() {
|
||||||
|
const { user, provider, document: doc } = this.options;
|
||||||
|
const type = doc.get("default", Y.XmlFragment);
|
||||||
|
|
||||||
|
const assignUser = (tr) => {
|
||||||
|
const clientIds = Array.from(doc.store.clients.keys());
|
||||||
|
|
||||||
|
if (
|
||||||
|
tr.local &&
|
||||||
|
tr.changed.size > 0 &&
|
||||||
|
!clientIds.includes(doc.clientID)
|
||||||
|
) {
|
||||||
|
const permanentUserData = new Y.PermanentUserData(doc);
|
||||||
|
permanentUserData.setUserMapping(doc, doc.clientID, user.id);
|
||||||
|
doc.off("afterTransaction", assignUser);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// only once we have authenticated successfully do we initalize awareness.
|
||||||
|
// we could send this earlier, but getting authenticated faster is more important
|
||||||
|
provider.on("authenticated", () => {
|
||||||
|
provider.awareness.setLocalStateField("user", user);
|
||||||
|
});
|
||||||
|
|
||||||
|
// only once an actual change has been made do we add the userId <> clientId
|
||||||
|
// mapping, this avoids stored mappings for clients that never made a change
|
||||||
|
doc.on("afterTransaction", assignUser);
|
||||||
|
|
||||||
|
return [
|
||||||
|
ySyncPlugin(type),
|
||||||
|
yCursorPlugin(provider.awareness),
|
||||||
|
yUndoPlugin(),
|
||||||
|
keymap({
|
||||||
|
"Mod-z": undo,
|
||||||
|
"Mod-y": redo,
|
||||||
|
"Mod-Shift-z": redo,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,10 +21,8 @@ import { matchDocumentSlug as slug } from "utils/routeHelpers";
|
||||||
const SettingsRoutes = React.lazy(() =>
|
const SettingsRoutes = React.lazy(() =>
|
||||||
import(/* webpackChunkName: "settings" */ "./settings")
|
import(/* webpackChunkName: "settings" */ "./settings")
|
||||||
);
|
);
|
||||||
const KeyedDocument = React.lazy(() =>
|
const Document = React.lazy(() =>
|
||||||
import(
|
import(/* webpackChunkName: "document" */ "scenes/Document")
|
||||||
/* webpackChunkName: "keyed-document" */ "scenes/Document/KeyedDocument"
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
const NotFound = () => <Search notFound />;
|
const NotFound = () => <Search notFound />;
|
||||||
const RedirectDocument = ({ match }: { match: Match }) => (
|
const RedirectDocument = ({ match }: { match: Match }) => (
|
||||||
|
@ -64,10 +62,10 @@ export default function AuthenticatedRoutes() {
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
path={`/doc/${slug}/history/:revisionId?`}
|
path={`/doc/${slug}/history/:revisionId?`}
|
||||||
component={KeyedDocument}
|
component={Document}
|
||||||
/>
|
/>
|
||||||
<Route exact path={`/doc/${slug}/edit`} component={KeyedDocument} />
|
<Route exact path={`/doc/${slug}/edit`} component={Document} />
|
||||||
<Route path={`/doc/${slug}`} component={KeyedDocument} />
|
<Route path={`/doc/${slug}`} component={Document} />
|
||||||
<Route exact path="/search" component={Search} />
|
<Route exact path="/search" component={Search} />
|
||||||
<Route exact path="/search/:term" component={Search} />
|
<Route exact path="/search/:term" component={Search} />
|
||||||
<Route path="/404" component={Error404} />
|
<Route path="/404" component={Error404} />
|
||||||
|
|
|
@ -12,10 +12,8 @@ const Authenticated = React.lazy(() =>
|
||||||
const AuthenticatedRoutes = React.lazy(() =>
|
const AuthenticatedRoutes = React.lazy(() =>
|
||||||
import(/* webpackChunkName: "authenticated-routes" */ "./authenticated")
|
import(/* webpackChunkName: "authenticated-routes" */ "./authenticated")
|
||||||
);
|
);
|
||||||
const KeyedDocument = React.lazy(() =>
|
const SharedDocument = React.lazy(() =>
|
||||||
import(
|
import(/* webpackChunkName: "shared-document" */ "scenes/Document/Shared")
|
||||||
/* webpackChunkName: "keyed-document" */ "scenes/Document/KeyedDocument"
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
const Login = React.lazy(() =>
|
const Login = React.lazy(() =>
|
||||||
import(/* webpackChunkName: "login" */ "scenes/Login")
|
import(/* webpackChunkName: "login" */ "scenes/Login")
|
||||||
|
@ -37,11 +35,11 @@ export default function Routes() {
|
||||||
<Route exact path="/" component={Login} />
|
<Route exact path="/" component={Login} />
|
||||||
<Route exact path="/create" component={Login} />
|
<Route exact path="/create" component={Login} />
|
||||||
<Route exact path="/logout" component={Logout} />
|
<Route exact path="/logout" component={Logout} />
|
||||||
<Route exact path="/share/:shareId" component={KeyedDocument} />
|
<Route exact path="/share/:shareId" component={SharedDocument} />
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
path={`/share/:shareId/doc/${slug}`}
|
path={`/share/:shareId/doc/${slug}`}
|
||||||
component={KeyedDocument}
|
component={SharedDocument}
|
||||||
/>
|
/>
|
||||||
<Authenticated>
|
<Authenticated>
|
||||||
<AuthenticatedRoutes />
|
<AuthenticatedRoutes />
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
// @flow
|
|
||||||
import { inject } from "mobx-react";
|
|
||||||
import * as React from "react";
|
|
||||||
import DataLoader from "./components/DataLoader";
|
|
||||||
|
|
||||||
class KeyedDocument extends React.Component<*> {
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.props.ui.clearActiveDocument();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { documentSlug, revisionId } = this.props.match.params;
|
|
||||||
|
|
||||||
// the urlId portion of the url does not include the slugified title
|
|
||||||
// we only want to force a re-mount of the document component when the
|
|
||||||
// document changes, not when the title does so only this portion is used
|
|
||||||
// for the key.
|
|
||||||
const urlParts = documentSlug ? documentSlug.split("-") : [];
|
|
||||||
const urlId = urlParts.length ? urlParts[urlParts.length - 1] : undefined;
|
|
||||||
|
|
||||||
return <DataLoader key={[urlId, revisionId].join("/")} {...this.props} />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default inject("ui")(KeyedDocument);
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
// @flow
|
||||||
|
import * as React from "react";
|
||||||
|
import { type Match } from "react-router-dom";
|
||||||
|
import { useTheme } from "styled-components";
|
||||||
|
import Error404 from "scenes/Error404";
|
||||||
|
import ErrorOffline from "scenes/ErrorOffline";
|
||||||
|
import useStores from "../../hooks/useStores";
|
||||||
|
import Document from "./components/Document";
|
||||||
|
import Loading from "./components/Loading";
|
||||||
|
import { type LocationWithState } from "types";
|
||||||
|
import { OfflineError } from "utils/errors";
|
||||||
|
|
||||||
|
const EMPTY_OBJECT = {};
|
||||||
|
|
||||||
|
type Props = {|
|
||||||
|
match: Match,
|
||||||
|
location: LocationWithState,
|
||||||
|
|};
|
||||||
|
|
||||||
|
export default function SharedDocumentScene(props: Props) {
|
||||||
|
const theme = useTheme();
|
||||||
|
const [response, setResponse] = React.useState();
|
||||||
|
const [error, setError] = React.useState<?Error>();
|
||||||
|
const { documents } = useStores();
|
||||||
|
const { shareId, documentSlug } = props.match.params;
|
||||||
|
|
||||||
|
// ensure the wider page color always matches the theme
|
||||||
|
React.useEffect(() => {
|
||||||
|
window.document.body.style.background = theme.background;
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
async function fetchData() {
|
||||||
|
try {
|
||||||
|
const response = await documents.fetch(documentSlug, {
|
||||||
|
shareId,
|
||||||
|
});
|
||||||
|
setResponse(response);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchData();
|
||||||
|
}, [documents, documentSlug, shareId]);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return error instanceof OfflineError ? <ErrorOffline /> : <Error404 />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
return <Loading location={props.location} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Document
|
||||||
|
abilities={EMPTY_OBJECT}
|
||||||
|
document={response.document}
|
||||||
|
sharedTree={response.sharedTree}
|
||||||
|
location={props.location}
|
||||||
|
shareId={shareId}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -18,16 +18,15 @@ import Document from "models/Document";
|
||||||
import Revision from "models/Revision";
|
import Revision from "models/Revision";
|
||||||
import Error404 from "scenes/Error404";
|
import Error404 from "scenes/Error404";
|
||||||
import ErrorOffline from "scenes/ErrorOffline";
|
import ErrorOffline from "scenes/ErrorOffline";
|
||||||
import DocumentComponent from "./Document";
|
|
||||||
import HideSidebar from "./HideSidebar";
|
import HideSidebar from "./HideSidebar";
|
||||||
import Loading from "./Loading";
|
import Loading from "./Loading";
|
||||||
import SocketPresence from "./SocketPresence";
|
|
||||||
import { type LocationWithState, type NavigationNode } from "types";
|
import { type LocationWithState, type NavigationNode } from "types";
|
||||||
import { NotFoundError, OfflineError } from "utils/errors";
|
import { NotFoundError, OfflineError } from "utils/errors";
|
||||||
import { matchDocumentEdit, updateDocumentUrl } from "utils/routeHelpers";
|
import { matchDocumentEdit, updateDocumentUrl } from "utils/routeHelpers";
|
||||||
import { isInternalUrl } from "utils/urls";
|
import { isInternalUrl } from "utils/urls";
|
||||||
type Props = {|
|
type Props = {|
|
||||||
match: Match,
|
match: Match,
|
||||||
|
auth: AuthStore,
|
||||||
location: LocationWithState,
|
location: LocationWithState,
|
||||||
shares: SharesStore,
|
shares: SharesStore,
|
||||||
documents: DocumentsStore,
|
documents: DocumentsStore,
|
||||||
|
@ -36,6 +35,7 @@ type Props = {|
|
||||||
auth: AuthStore,
|
auth: AuthStore,
|
||||||
ui: UiStore,
|
ui: UiStore,
|
||||||
history: RouterHistory,
|
history: RouterHistory,
|
||||||
|
children: (any) => React.Node,
|
||||||
|};
|
|};
|
||||||
|
|
||||||
const sharedTreeCache = {};
|
const sharedTreeCache = {};
|
||||||
|
@ -223,7 +223,7 @@ class DataLoader extends React.Component<Props> {
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { location, policies, ui } = this.props;
|
const { location, policies, auth, ui } = this.props;
|
||||||
|
|
||||||
if (this.error) {
|
if (this.error) {
|
||||||
return this.error instanceof OfflineError ? (
|
return this.error instanceof OfflineError ? (
|
||||||
|
@ -233,10 +233,11 @@ class DataLoader extends React.Component<Props> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const team = auth.team;
|
||||||
const document = this.document;
|
const document = this.document;
|
||||||
const revision = this.revision;
|
const revision = this.revision;
|
||||||
|
|
||||||
if (!document) {
|
if (!document || !team) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Loading location={location} />
|
<Loading location={location} />
|
||||||
|
@ -247,20 +248,28 @@ class DataLoader extends React.Component<Props> {
|
||||||
|
|
||||||
const abilities = policies.abilities(document.id);
|
const abilities = policies.abilities(document.id);
|
||||||
|
|
||||||
|
// We do not want to remount the document when changing from view->edit
|
||||||
|
// on the multiplayer flag as the doc is guaranteed to be upto date.
|
||||||
|
const key = team.collaborativeEditing
|
||||||
|
? ""
|
||||||
|
: this.isEditing
|
||||||
|
? "editing"
|
||||||
|
: "read-only";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SocketPresence documentId={document.id} isEditing={this.isEditing}>
|
<React.Fragment key={key}>
|
||||||
{this.isEditing && <HideSidebar ui={ui} />}
|
{this.isEditing && <HideSidebar ui={ui} />}
|
||||||
<DocumentComponent
|
{this.props.children({
|
||||||
document={document}
|
document,
|
||||||
revision={revision}
|
revision,
|
||||||
abilities={abilities}
|
abilities,
|
||||||
location={location}
|
isEditing: this.isEditing,
|
||||||
readOnly={!this.isEditing || !abilities.update || document.isArchived}
|
readOnly: !this.isEditing || !abilities.update || document.isArchived,
|
||||||
onSearchLink={this.onSearchLink}
|
onSearchLink: this.onSearchLink,
|
||||||
onCreateLink={this.onCreateLink}
|
onCreateLink: this.onCreateLink,
|
||||||
sharedTree={this.sharedTree}
|
sharedTree: this.sharedTree,
|
||||||
/>
|
})}
|
||||||
</SocketPresence>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { debounce } from "lodash";
|
import { debounce } from "lodash";
|
||||||
import { observable } from "mobx";
|
import { action, observable } from "mobx";
|
||||||
import { observer, inject } from "mobx-react";
|
import { observer, inject } from "mobx-react";
|
||||||
import { InputIcon } from "outline-icons";
|
import { InputIcon } from "outline-icons";
|
||||||
|
import { AllSelection } from "prosemirror-state";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { type TFunction, Trans, withTranslation } from "react-i18next";
|
import { type TFunction, Trans, withTranslation } from "react-i18next";
|
||||||
import keydown from "react-keydown";
|
import keydown from "react-keydown";
|
||||||
|
@ -18,6 +19,7 @@ import Document from "models/Document";
|
||||||
import Revision from "models/Revision";
|
import Revision from "models/Revision";
|
||||||
import DocumentMove from "scenes/DocumentMove";
|
import DocumentMove from "scenes/DocumentMove";
|
||||||
import Branding from "components/Branding";
|
import Branding from "components/Branding";
|
||||||
|
import ConnectionStatus from "components/ConnectionStatus";
|
||||||
import ErrorBoundary from "components/ErrorBoundary";
|
import ErrorBoundary from "components/ErrorBoundary";
|
||||||
import Flex from "components/Flex";
|
import Flex from "components/Flex";
|
||||||
import LoadingIndicator from "components/LoadingIndicator";
|
import LoadingIndicator from "components/LoadingIndicator";
|
||||||
|
@ -113,15 +115,31 @@ class DocumentScene extends React.Component<Props> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (document.injectTemplate) {
|
|
||||||
document.injectTemplate = false;
|
|
||||||
this.title = document.title;
|
|
||||||
this.isDirty = true;
|
|
||||||
this.updateIsDirty();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onSelectTemplate = (template: Document) => {
|
||||||
|
this.title = template.title;
|
||||||
|
this.isDirty = true;
|
||||||
|
|
||||||
|
const editorRef = this.editor.current;
|
||||||
|
if (!editorRef) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { view, parser } = editorRef;
|
||||||
|
view.dispatch(
|
||||||
|
view.state.tr
|
||||||
|
.setSelection(new AllSelection(view.state.doc))
|
||||||
|
.replaceSelectionWith(parser.parse(template.text))
|
||||||
|
);
|
||||||
|
|
||||||
|
this.props.document.templateId = template.id;
|
||||||
|
this.props.document.title = template.title;
|
||||||
|
this.props.document.text = template.text;
|
||||||
|
|
||||||
|
this.updateIsDirty();
|
||||||
|
};
|
||||||
|
|
||||||
@keydown("m")
|
@keydown("m")
|
||||||
goToMove(ev) {
|
goToMove(ev) {
|
||||||
if (!this.props.readOnly) return;
|
if (!this.props.readOnly) return;
|
||||||
|
@ -197,7 +215,7 @@ class DocumentScene extends React.Component<Props> {
|
||||||
autosave?: boolean,
|
autosave?: boolean,
|
||||||
} = {}
|
} = {}
|
||||||
) => {
|
) => {
|
||||||
const { document } = this.props;
|
const { document, auth } = this.props;
|
||||||
|
|
||||||
// prevent saves when we are already saving
|
// prevent saves when we are already saving
|
||||||
if (document.isSaving) return;
|
if (document.isSaving) return;
|
||||||
|
@ -219,18 +237,29 @@ class DocumentScene extends React.Component<Props> {
|
||||||
|
|
||||||
document.title = title;
|
document.title = title;
|
||||||
document.text = text;
|
document.text = text;
|
||||||
|
document.tasks = getTasks(document.text);
|
||||||
|
|
||||||
let isNew = !document.id;
|
let isNew = !document.id;
|
||||||
this.isSaving = true;
|
this.isSaving = true;
|
||||||
this.isPublishing = !!options.publish;
|
this.isPublishing = !!options.publish;
|
||||||
|
|
||||||
document.tasks = getTasks(document.text);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const savedDocument = await document.save({
|
let savedDocument = document;
|
||||||
...options,
|
if (auth.team?.collaborativeEditing) {
|
||||||
lastRevision: this.lastRevision,
|
// update does not send "text" field to the API, this is a workaround
|
||||||
});
|
// while the multiplayer editor is toggleable. Once it's finalized
|
||||||
|
// this can be cleaned up to single code path
|
||||||
|
savedDocument = await document.update({
|
||||||
|
...options,
|
||||||
|
lastRevision: this.lastRevision,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
savedDocument = await document.save({
|
||||||
|
...options,
|
||||||
|
lastRevision: this.lastRevision,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.isDirty = false;
|
this.isDirty = false;
|
||||||
this.lastRevision = savedDocument.revision;
|
this.lastRevision = savedDocument.revision;
|
||||||
|
|
||||||
|
@ -275,8 +304,21 @@ class DocumentScene extends React.Component<Props> {
|
||||||
};
|
};
|
||||||
|
|
||||||
onChange = (getEditorText) => {
|
onChange = (getEditorText) => {
|
||||||
|
const { document, auth } = this.props;
|
||||||
|
|
||||||
this.getEditorText = getEditorText;
|
this.getEditorText = getEditorText;
|
||||||
|
|
||||||
|
// If the multiplayer editor is enabled then we still want to keep the local
|
||||||
|
// text value in sync as it is used as a cache.
|
||||||
|
if (auth.team?.collaborativeEditing) {
|
||||||
|
action(() => {
|
||||||
|
document.text = this.getEditorText();
|
||||||
|
document.tasks = getTasks(document.text);
|
||||||
|
})();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// document change while read only is presumed to be a checkbox edit,
|
// document change while read only is presumed to be a checkbox edit,
|
||||||
// in that case we don't delay in saving for a better user experience.
|
// in that case we don't delay in saving for a better user experience.
|
||||||
if (this.props.readOnly) {
|
if (this.props.readOnly) {
|
||||||
|
@ -314,7 +356,6 @@ class DocumentScene extends React.Component<Props> {
|
||||||
const isShare = !!shareId;
|
const isShare = !!shareId;
|
||||||
|
|
||||||
const value = revision ? revision.text : document.text;
|
const value = revision ? revision.text : document.text;
|
||||||
const injectTemplate = document.injectTemplate;
|
|
||||||
const disableEmbeds =
|
const disableEmbeds =
|
||||||
(team && team.documentEmbeds === false) || document.embedsDisabled;
|
(team && team.documentEmbeds === false) || document.embedsDisabled;
|
||||||
|
|
||||||
|
@ -323,6 +364,12 @@ class DocumentScene extends React.Component<Props> {
|
||||||
: [];
|
: [];
|
||||||
const showContents = ui.tocVisible && readOnly;
|
const showContents = ui.tocVisible && readOnly;
|
||||||
|
|
||||||
|
const collaborativeEditing =
|
||||||
|
team?.collaborativeEditing &&
|
||||||
|
!document.isArchived &&
|
||||||
|
!document.isDeleted &&
|
||||||
|
!revision;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Background
|
<Background
|
||||||
|
@ -332,7 +379,7 @@ class DocumentScene extends React.Component<Props> {
|
||||||
auto
|
auto
|
||||||
>
|
>
|
||||||
<Route
|
<Route
|
||||||
path={`${match.url}/move`}
|
path={`${document.url}/move`}
|
||||||
component={() => (
|
component={() => (
|
||||||
<Modal
|
<Modal
|
||||||
title={`Move ${document.noun}`}
|
title={`Move ${document.noun}`}
|
||||||
|
@ -356,7 +403,11 @@ class DocumentScene extends React.Component<Props> {
|
||||||
{!readOnly && (
|
{!readOnly && (
|
||||||
<>
|
<>
|
||||||
<Prompt
|
<Prompt
|
||||||
when={this.isDirty && !this.isUploading}
|
when={
|
||||||
|
this.isDirty &&
|
||||||
|
!this.isUploading &&
|
||||||
|
!team?.collaborativeEditing
|
||||||
|
}
|
||||||
message={t(
|
message={t(
|
||||||
`You have unsaved changes.\nAre you sure you want to discard them?`
|
`You have unsaved changes.\nAre you sure you want to discard them?`
|
||||||
)}
|
)}
|
||||||
|
@ -383,6 +434,7 @@ class DocumentScene extends React.Component<Props> {
|
||||||
savingIsDisabled={document.isSaving || this.isEmpty}
|
savingIsDisabled={document.isSaving || this.isEmpty}
|
||||||
sharedTree={this.props.sharedTree}
|
sharedTree={this.props.sharedTree}
|
||||||
goBack={this.goBack}
|
goBack={this.goBack}
|
||||||
|
onSelectTemplate={this.onSelectTemplate}
|
||||||
onSave={this.onSave}
|
onSave={this.onSave}
|
||||||
headings={headings}
|
headings={headings}
|
||||||
/>
|
/>
|
||||||
|
@ -443,11 +495,12 @@ class DocumentScene extends React.Component<Props> {
|
||||||
{showContents && <Contents headings={headings} />}
|
{showContents && <Contents headings={headings} />}
|
||||||
<Editor
|
<Editor
|
||||||
id={document.id}
|
id={document.id}
|
||||||
|
key={disableEmbeds ? "disabled" : "enabled"}
|
||||||
innerRef={this.editor}
|
innerRef={this.editor}
|
||||||
|
multiplayer={collaborativeEditing}
|
||||||
shareId={shareId}
|
shareId={shareId}
|
||||||
isDraft={document.isDraft}
|
isDraft={document.isDraft}
|
||||||
template={document.isTemplate}
|
template={document.isTemplate}
|
||||||
key={[injectTemplate, disableEmbeds].join("-")}
|
|
||||||
title={revision ? revision.title : this.title}
|
title={revision ? revision.title : this.title}
|
||||||
document={document}
|
document={document}
|
||||||
value={readOnly ? value : undefined}
|
value={readOnly ? value : undefined}
|
||||||
|
@ -492,7 +545,12 @@ class DocumentScene extends React.Component<Props> {
|
||||||
{isShare && !isCustomDomain() && (
|
{isShare && !isCustomDomain() && (
|
||||||
<Branding href="//www.getoutline.com?ref=sharelink" />
|
<Branding href="//www.getoutline.com?ref=sharelink" />
|
||||||
)}
|
)}
|
||||||
{!isShare && <KeyboardShortcutsButton />}
|
{!isShare && (
|
||||||
|
<>
|
||||||
|
<KeyboardShortcutsButton />
|
||||||
|
<ConnectionStatus />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ import Editor, { type Props as EditorProps } from "components/Editor";
|
||||||
import Flex from "components/Flex";
|
import Flex from "components/Flex";
|
||||||
import HoverPreview from "components/HoverPreview";
|
import HoverPreview from "components/HoverPreview";
|
||||||
import Star, { AnimatedStar } from "components/Star";
|
import Star, { AnimatedStar } from "components/Star";
|
||||||
|
import MultiplayerEditor from "./MultiplayerEditor";
|
||||||
import { isModKey } from "utils/keyboard";
|
import { isModKey } from "utils/keyboard";
|
||||||
import { documentHistoryUrl } from "utils/routeHelpers";
|
import { documentHistoryUrl } from "utils/routeHelpers";
|
||||||
|
|
||||||
|
@ -27,6 +28,7 @@ type Props = {|
|
||||||
document: Document,
|
document: Document,
|
||||||
isDraft: boolean,
|
isDraft: boolean,
|
||||||
shareId: ?string,
|
shareId: ?string,
|
||||||
|
multiplayer?: boolean,
|
||||||
onSave: ({ done?: boolean, autosave?: boolean, publish?: boolean }) => any,
|
onSave: ({ done?: boolean, autosave?: boolean, publish?: boolean }) => any,
|
||||||
innerRef: { current: any },
|
innerRef: { current: any },
|
||||||
children: React.Node,
|
children: React.Node,
|
||||||
|
@ -107,10 +109,12 @@ class DocumentEditor extends React.Component<Props> {
|
||||||
innerRef,
|
innerRef,
|
||||||
children,
|
children,
|
||||||
policies,
|
policies,
|
||||||
|
multiplayer,
|
||||||
t,
|
t,
|
||||||
...rest
|
...rest
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const EditorComponent = multiplayer ? MultiplayerEditor : Editor;
|
||||||
const can = policies.abilities(document.id);
|
const can = policies.abilities(document.id);
|
||||||
const { emoji } = parseTitle(title);
|
const { emoji } = parseTitle(title);
|
||||||
const startsWithEmojiAndSpace = !!(emoji && title.startsWith(`${emoji} `));
|
const startsWithEmojiAndSpace = !!(emoji && title.startsWith(`${emoji} `));
|
||||||
|
@ -162,7 +166,7 @@ class DocumentEditor extends React.Component<Props> {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Editor
|
<EditorComponent
|
||||||
ref={innerRef}
|
ref={innerRef}
|
||||||
autoFocus={!!title && !this.props.defaultValue}
|
autoFocus={!!title && !this.props.defaultValue}
|
||||||
placeholder={t("…the rest is up to you")}
|
placeholder={t("…the rest is up to you")}
|
||||||
|
|
|
@ -41,6 +41,7 @@ type Props = {|
|
||||||
isPublishing: boolean,
|
isPublishing: boolean,
|
||||||
publishingIsDisabled: boolean,
|
publishingIsDisabled: boolean,
|
||||||
savingIsDisabled: boolean,
|
savingIsDisabled: boolean,
|
||||||
|
onSelectTemplate: (template: Document) => void,
|
||||||
onDiscard: () => void,
|
onDiscard: () => void,
|
||||||
onSave: ({
|
onSave: ({
|
||||||
done?: boolean,
|
done?: boolean,
|
||||||
|
@ -61,6 +62,7 @@ function DocumentHeader({
|
||||||
savingIsDisabled,
|
savingIsDisabled,
|
||||||
publishingIsDisabled,
|
publishingIsDisabled,
|
||||||
sharedTree,
|
sharedTree,
|
||||||
|
onSelectTemplate,
|
||||||
onSave,
|
onSave,
|
||||||
headings,
|
headings,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
@ -167,7 +169,10 @@ function DocumentHeader({
|
||||||
/>
|
/>
|
||||||
{isEditing && !isTemplate && isNew && (
|
{isEditing && !isTemplate && isNew && (
|
||||||
<Action>
|
<Action>
|
||||||
<TemplatesMenu document={document} />
|
<TemplatesMenu
|
||||||
|
document={document}
|
||||||
|
onSelectTemplate={onSelectTemplate}
|
||||||
|
/>
|
||||||
</Action>
|
</Action>
|
||||||
)}
|
)}
|
||||||
{!isEditing && (!isMobile || !isTemplate) && (
|
{!isEditing && (!isMobile || !isTemplate) && (
|
||||||
|
|
|
@ -27,12 +27,7 @@ function KeyboardShortcutsButton() {
|
||||||
>
|
>
|
||||||
<KeyboardShortcuts />
|
<KeyboardShortcuts />
|
||||||
</Guide>
|
</Guide>
|
||||||
<Tooltip
|
<Tooltip tooltip={t("Keyboard shortcuts")} shortcut="?" delay={500}>
|
||||||
tooltip={t("Keyboard shortcuts")}
|
|
||||||
shortcut="?"
|
|
||||||
placement="left"
|
|
||||||
delay={500}
|
|
||||||
>
|
|
||||||
<Button onClick={handleOpenKeyboardShortcuts}>
|
<Button onClick={handleOpenKeyboardShortcuts}>
|
||||||
<KeyboardIcon />
|
<KeyboardIcon />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -0,0 +1,144 @@
|
||||||
|
// @flow
|
||||||
|
import { HocuspocusProvider } from "@hocuspocus/provider";
|
||||||
|
import * as React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useHistory } from "react-router";
|
||||||
|
import { IndexeddbPersistence } from "y-indexeddb";
|
||||||
|
import * as Y from "yjs";
|
||||||
|
import Editor, { type Props as EditorProps } from "components/Editor";
|
||||||
|
import PlaceholderDocument from "components/PlaceholderDocument";
|
||||||
|
import env from "env";
|
||||||
|
import useCurrentToken from "hooks/useCurrentToken";
|
||||||
|
import useCurrentUser from "hooks/useCurrentUser";
|
||||||
|
import useStores from "hooks/useStores";
|
||||||
|
import useToasts from "hooks/useToasts";
|
||||||
|
import useUnmount from "hooks/useUnmount";
|
||||||
|
import MultiplayerExtension from "multiplayer/MultiplayerExtension";
|
||||||
|
import { homeUrl } from "utils/routeHelpers";
|
||||||
|
|
||||||
|
type Props = {|
|
||||||
|
...EditorProps,
|
||||||
|
id: string,
|
||||||
|
|};
|
||||||
|
|
||||||
|
function MultiplayerEditor(props: Props, ref: any) {
|
||||||
|
const documentId = props.id;
|
||||||
|
const history = useHistory();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const currentUser = useCurrentUser();
|
||||||
|
const { presence, ui } = useStores();
|
||||||
|
const token = useCurrentToken();
|
||||||
|
const [localProvider, setLocalProvider] = React.useState();
|
||||||
|
const [remoteProvider, setRemoteProvider] = React.useState();
|
||||||
|
const [isLocalSynced, setLocalSynced] = React.useState(false);
|
||||||
|
const [isRemoteSynced, setRemoteSynced] = React.useState(false);
|
||||||
|
const [ydoc] = React.useState(() => new Y.Doc());
|
||||||
|
const { showToast } = useToasts();
|
||||||
|
|
||||||
|
// Provider initialization must be within useLayoutEffect rather than useState
|
||||||
|
// or useMemo as both of these are ran twice in React StrictMode resulting in
|
||||||
|
// an orphaned websocket connection.
|
||||||
|
// see: https://github.com/facebook/react/issues/20090#issuecomment-715926549
|
||||||
|
React.useLayoutEffect(() => {
|
||||||
|
const debug = env.ENVIRONMENT === "development";
|
||||||
|
const name = `document.${documentId}`;
|
||||||
|
|
||||||
|
const localProvider = new IndexeddbPersistence(name, ydoc);
|
||||||
|
const provider = new HocuspocusProvider({
|
||||||
|
url: `${env.COLLABORATION_URL}/collaboration`,
|
||||||
|
debug,
|
||||||
|
name,
|
||||||
|
document: ydoc,
|
||||||
|
token,
|
||||||
|
maxReconnectTimeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
provider.on("authenticationFailed", () => {
|
||||||
|
showToast(
|
||||||
|
t(
|
||||||
|
"Sorry, it looks like you don’t have permission to access the document"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
history.replace(homeUrl());
|
||||||
|
});
|
||||||
|
|
||||||
|
provider.on("awarenessChange", ({ states }) => {
|
||||||
|
states.forEach(({ user }) => {
|
||||||
|
if (user) {
|
||||||
|
// could know if the user is editing here using `state.cursor` but it
|
||||||
|
// feels distracting in the UI, once multiplayer is on for everyone we
|
||||||
|
// can stop diffentiating
|
||||||
|
presence.touch(documentId, user.id, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
localProvider.on("synced", () => setLocalSynced(true));
|
||||||
|
provider.on("synced", () => setRemoteSynced(true));
|
||||||
|
|
||||||
|
if (debug) {
|
||||||
|
provider.on("status", (ev) => console.log("status", ev.status));
|
||||||
|
provider.on("message", (ev) => console.log("incoming", ev.message));
|
||||||
|
provider.on("outgoingMessage", (ev) =>
|
||||||
|
console.log("outgoing", ev.message)
|
||||||
|
);
|
||||||
|
localProvider.on("synced", (ev) => console.log("local synced"));
|
||||||
|
}
|
||||||
|
|
||||||
|
provider.on("status", (ev) => ui.setMultiplayerStatus(ev.status));
|
||||||
|
|
||||||
|
setRemoteProvider(provider);
|
||||||
|
setLocalProvider(localProvider);
|
||||||
|
}, [history, showToast, t, documentId, ui, presence, token, ydoc]);
|
||||||
|
|
||||||
|
const user = React.useMemo(() => {
|
||||||
|
return {
|
||||||
|
id: currentUser.id,
|
||||||
|
name: currentUser.name,
|
||||||
|
color: currentUser.color,
|
||||||
|
};
|
||||||
|
}, [currentUser.id, currentUser.color, currentUser.name]);
|
||||||
|
|
||||||
|
const extensions = React.useMemo(() => {
|
||||||
|
if (!remoteProvider) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
new MultiplayerExtension({
|
||||||
|
user,
|
||||||
|
provider: remoteProvider,
|
||||||
|
document: ydoc,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}, [remoteProvider, user, ydoc]);
|
||||||
|
|
||||||
|
useUnmount(() => {
|
||||||
|
remoteProvider?.destroy();
|
||||||
|
localProvider?.destroy();
|
||||||
|
ui.setMultiplayerStatus(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!extensions.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLocalSynced && !isRemoteSynced && !ydoc.get("default")._start) {
|
||||||
|
return <PlaceholderDocument includeTitle={false} delay={500} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Editor
|
||||||
|
{...props}
|
||||||
|
value={undefined}
|
||||||
|
defaultValue={undefined}
|
||||||
|
extensions={extensions}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.forwardRef<any, typeof MultiplayerEditor>(
|
||||||
|
MultiplayerEditor
|
||||||
|
);
|
|
@ -1,3 +1,61 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
import * as React from "react";
|
||||||
|
import { type Match } from "react-router-dom";
|
||||||
import DataLoader from "./components/DataLoader";
|
import DataLoader from "./components/DataLoader";
|
||||||
export default DataLoader;
|
import Document from "./components/Document";
|
||||||
|
import SocketPresence from "./components/SocketPresence";
|
||||||
|
import useCurrentTeam from "hooks/useCurrentTeam";
|
||||||
|
import useCurrentUser from "hooks/useCurrentUser";
|
||||||
|
import useStores from "hooks/useStores";
|
||||||
|
import { type LocationWithState } from "types";
|
||||||
|
|
||||||
|
type Props = {|
|
||||||
|
location: LocationWithState,
|
||||||
|
match: Match,
|
||||||
|
|};
|
||||||
|
|
||||||
|
export default function DocumentScene(props: Props) {
|
||||||
|
const { ui } = useStores();
|
||||||
|
const team = useCurrentTeam();
|
||||||
|
const user = useCurrentUser();
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
return () => ui.clearActiveDocument();
|
||||||
|
}, [ui]);
|
||||||
|
|
||||||
|
const { documentSlug, revisionId } = props.match.params;
|
||||||
|
|
||||||
|
// the urlId portion of the url does not include the slugified title
|
||||||
|
// we only want to force a re-mount of the document component when the
|
||||||
|
// document changes, not when the title does so only this portion is used
|
||||||
|
// for the key.
|
||||||
|
const urlParts = documentSlug ? documentSlug.split("-") : [];
|
||||||
|
const urlId = urlParts.length ? urlParts[urlParts.length - 1] : undefined;
|
||||||
|
const key = [urlId, revisionId].join("/");
|
||||||
|
const isMultiplayer = team.collaborativeEditing;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataLoader key={key} match={props.match}>
|
||||||
|
{({ document, isEditing, ...rest }) => {
|
||||||
|
const isActive =
|
||||||
|
!document.isArchived && !document.isDeleted && !revisionId;
|
||||||
|
|
||||||
|
// TODO: Remove once multiplayer is 100% rollout, SocketPresence will
|
||||||
|
// no longer be required
|
||||||
|
if (isActive && !isMultiplayer) {
|
||||||
|
return (
|
||||||
|
<SocketPresence
|
||||||
|
documentId={document.id}
|
||||||
|
userId={user.id}
|
||||||
|
isEditing={isEditing}
|
||||||
|
>
|
||||||
|
<Document document={document} match={props.match} {...rest} />
|
||||||
|
</SocketPresence>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Document document={document} match={props.match} {...rest} />;
|
||||||
|
}}
|
||||||
|
</DataLoader>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -603,7 +603,7 @@ export default class DocumentsStore extends BaseStore<Document> {
|
||||||
async update(params: {
|
async update(params: {
|
||||||
id: string,
|
id: string,
|
||||||
title: string,
|
title: string,
|
||||||
text: string,
|
text?: string,
|
||||||
lastRevision: number,
|
lastRevision: number,
|
||||||
}) {
|
}) {
|
||||||
const document = await super.update(params);
|
const document = await super.update(params);
|
||||||
|
|
|
@ -6,6 +6,8 @@ import Document from "models/Document";
|
||||||
|
|
||||||
const UI_STORE = "UI_STORE";
|
const UI_STORE = "UI_STORE";
|
||||||
|
|
||||||
|
type Status = "connecting" | "connected" | "disconnected" | void;
|
||||||
|
|
||||||
class UiStore {
|
class UiStore {
|
||||||
// has the user seen the prompt to change the UI language and actioned it
|
// has the user seen the prompt to change the UI language and actioned it
|
||||||
@observable languagePromptDismissed: boolean;
|
@observable languagePromptDismissed: boolean;
|
||||||
|
@ -24,6 +26,7 @@ class UiStore {
|
||||||
@observable sidebarWidth: number;
|
@observable sidebarWidth: number;
|
||||||
@observable sidebarCollapsed: boolean = false;
|
@observable sidebarCollapsed: boolean = false;
|
||||||
@observable sidebarIsResizing: boolean = false;
|
@observable sidebarIsResizing: boolean = false;
|
||||||
|
@observable multiplayerStatus: Status;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// Rehydrate
|
// Rehydrate
|
||||||
|
@ -93,6 +96,11 @@ class UiStore {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@action
|
||||||
|
setMultiplayerStatus = (status: Status): void => {
|
||||||
|
this.multiplayerStatus = status;
|
||||||
|
};
|
||||||
|
|
||||||
@action
|
@action
|
||||||
setSidebarResizing = (sidebarIsResizing: boolean): void => {
|
setSidebarResizing = (sidebarIsResizing: boolean): void => {
|
||||||
this.sidebarIsResizing = sidebarIsResizing;
|
this.sidebarIsResizing = sidebarIsResizing;
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
// A function to delete all IndexedDB databases
|
||||||
|
export async function deleteAllDatabases() {
|
||||||
|
const databases = await window.indexedDB.databases();
|
||||||
|
for (const database of databases) {
|
||||||
|
if (database.name) {
|
||||||
|
await window.indexedDB.deleteDatabase(database.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { parseDomain } from "../../shared/utils/domains";
|
import { parseDomain } from "../../shared/utils/domains";
|
||||||
import env from "env";
|
|
||||||
|
|
||||||
export function isInternalUrl(href: string) {
|
export function isInternalUrl(href: string) {
|
||||||
if (href[0] === "/") return true;
|
if (href[0] === "/") return true;
|
||||||
|
@ -21,14 +20,6 @@ export function isInternalUrl(href: string) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function cdnPath(path: string): string {
|
|
||||||
return `${env.CDN_URL}${path}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function imagePath(path: string): string {
|
|
||||||
return cdnPath(`/images/${path}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function decodeURIComponentSafe(text: string) {
|
export function decodeURIComponentSafe(text: string) {
|
||||||
return text
|
return text
|
||||||
? decodeURIComponent(text.replace(/%(?![0-9][0-9a-fA-F]+)/g, "%25"))
|
? decodeURIComponent(text.replace(/%(?![0-9][0-9a-fA-F]+)/g, "%25"))
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
# Backend Services
|
||||||
|
|
||||||
|
Outline's backend is split into several distinct [services](../server/services)
|
||||||
|
that combined form the application. When running the official Docker container
|
||||||
|
it will run all of the production services by default.
|
||||||
|
|
||||||
|
You can choose which services to run through either a comma separated CLI flag,
|
||||||
|
`--services`, or the `SERVICES` environment variable. For example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn start --services=web,worker
|
||||||
|
```
|
||||||
|
|
||||||
|
## Admin
|
||||||
|
|
||||||
|
Currently this service is only used in development to view and debug the queues.
|
||||||
|
It is hosted at `/admin`.
|
||||||
|
|
||||||
|
## Web
|
||||||
|
|
||||||
|
The web server hosts the Application and API, as such this is the main service
|
||||||
|
and must be run by at least one process.
|
||||||
|
|
||||||
|
## Websockets
|
||||||
|
|
||||||
|
The websocket server is used to communicate with the frontend, it can be ran on
|
||||||
|
the same box as the web server or separately.
|
||||||
|
|
||||||
|
## Worker
|
||||||
|
|
||||||
|
At least one worker process is required to process the [queues](../server/queues).
|
||||||
|
|
||||||
|
## Collaboration
|
||||||
|
|
||||||
|
The service is in alpha and as such is not started by default. It must run
|
||||||
|
separately to the `websockets` service, and will not start in the same process.
|
||||||
|
The `COLLABORATION_URL` must be set to the publicly accessible URL when running
|
||||||
|
the service. For example, if the app is hosted at `https://docs.example.com` you
|
||||||
|
may use something like: `COLLABORATION_URL=wss://docs-collaboration.example.com`.
|
||||||
|
|
||||||
|
Start the service with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn start --services=collaboration
|
||||||
|
```
|
|
@ -0,0 +1,377 @@
|
||||||
|
// flow-typed signature: 97da878aea98698d6c06f8a696bb62af
|
||||||
|
// flow-typed version: <<STUB>>/lib0_v0.2.34/flow_v0.104.0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is an autogenerated libdef stub for:
|
||||||
|
*
|
||||||
|
* 'lib0'
|
||||||
|
*
|
||||||
|
* Fill this stub out by replacing all the `any` types.
|
||||||
|
*
|
||||||
|
* Once filled out, we encourage you to share your work with the
|
||||||
|
* community by sending a pull request to:
|
||||||
|
* https://github.com/flowtype/flow-typed
|
||||||
|
*/
|
||||||
|
|
||||||
|
// @flow
|
||||||
|
declare module "lib0" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We include stubs for each file inside this npm package in case you need to
|
||||||
|
* require those files directly. Feel free to delete any files that aren't
|
||||||
|
* needed.
|
||||||
|
*/
|
||||||
|
declare module "lib0/array" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/bin/gendocs" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/binary" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/broadcastchannel" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/buffer" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/component" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/conditions" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/decoding" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/diff" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/dist/test" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/dom" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/encoding" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/environment" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/error" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/eventloop" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/function" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/indexeddb" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/isomorphic" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/iterator" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/json" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/logging" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/map" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/math" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/metric" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/mutex" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/number" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/object" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/observable" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/pair" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/prng" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/prng/Mt19937" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/prng/Xoroshiro128plus" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/prng/Xorshift32" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/promise" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/queue" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/random" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/set" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/sort" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/statistics" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/storage" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/string" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/symbol" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/test" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/testing" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/time" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/tree" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/url" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/websocket" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filename aliases
|
||||||
|
declare module "lib0/array.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/array">;
|
||||||
|
}
|
||||||
|
declare module "lib0/bin/gendocs.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/bin/gendocs">;
|
||||||
|
}
|
||||||
|
declare module "lib0/binary.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/binary">;
|
||||||
|
}
|
||||||
|
declare module "lib0/broadcastchannel.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/broadcastchannel">;
|
||||||
|
}
|
||||||
|
declare module "lib0/buffer.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/buffer">;
|
||||||
|
}
|
||||||
|
declare module "lib0/component.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/component">;
|
||||||
|
}
|
||||||
|
declare module "lib0/conditions.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/conditions">;
|
||||||
|
}
|
||||||
|
declare module "lib0/decoding.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/decoding">;
|
||||||
|
}
|
||||||
|
declare module "lib0/dist/decoding.cjs" {
|
||||||
|
declare module.exports: $Exports<"lib0/decoding">;
|
||||||
|
}
|
||||||
|
declare module "lib0/diff.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/diff">;
|
||||||
|
}
|
||||||
|
declare module "lib0/dist/test.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/dist/test">;
|
||||||
|
}
|
||||||
|
declare module "lib0/dom.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/dom">;
|
||||||
|
}
|
||||||
|
declare module "lib0/encoding.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/encoding">;
|
||||||
|
}
|
||||||
|
declare module "lib0/dist/encoding.cjs" {
|
||||||
|
declare module.exports: $Exports<"lib0/encoding">;
|
||||||
|
}
|
||||||
|
declare module "lib0/environment.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/environment">;
|
||||||
|
}
|
||||||
|
declare module "lib0/error.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/error">;
|
||||||
|
}
|
||||||
|
declare module "lib0/eventloop.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/eventloop">;
|
||||||
|
}
|
||||||
|
declare module "lib0/function.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/function">;
|
||||||
|
}
|
||||||
|
declare module "lib0/index" {
|
||||||
|
declare module.exports: $Exports<"lib0">;
|
||||||
|
}
|
||||||
|
declare module "lib0/index.js" {
|
||||||
|
declare module.exports: $Exports<"lib0">;
|
||||||
|
}
|
||||||
|
declare module "lib0/indexeddb.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/indexeddb">;
|
||||||
|
}
|
||||||
|
declare module "lib0/isomorphic.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/isomorphic">;
|
||||||
|
}
|
||||||
|
declare module "lib0/iterator.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/iterator">;
|
||||||
|
}
|
||||||
|
declare module "lib0/json.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/json">;
|
||||||
|
}
|
||||||
|
declare module "lib0/logging.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/logging">;
|
||||||
|
}
|
||||||
|
declare module "lib0/map.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/map">;
|
||||||
|
}
|
||||||
|
declare module "lib0/math.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/math">;
|
||||||
|
}
|
||||||
|
declare module "lib0/metric.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/metric">;
|
||||||
|
}
|
||||||
|
declare module "lib0/mutex.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/mutex">;
|
||||||
|
}
|
||||||
|
declare module "lib0/dist/mutex.cjs" {
|
||||||
|
declare module.exports: $Exports<"lib0/mutex">;
|
||||||
|
}
|
||||||
|
declare module "lib0/number.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/number">;
|
||||||
|
}
|
||||||
|
declare module "lib0/object.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/object">;
|
||||||
|
}
|
||||||
|
declare module "lib0/observable.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/observable">;
|
||||||
|
}
|
||||||
|
declare module "lib0/pair.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/pair">;
|
||||||
|
}
|
||||||
|
declare module "lib0/prng.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/prng">;
|
||||||
|
}
|
||||||
|
declare module "lib0/prng/Mt19937.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/prng/Mt19937">;
|
||||||
|
}
|
||||||
|
declare module "lib0/prng/Xoroshiro128plus.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/prng/Xoroshiro128plus">;
|
||||||
|
}
|
||||||
|
declare module "lib0/prng/Xorshift32.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/prng/Xorshift32">;
|
||||||
|
}
|
||||||
|
declare module "lib0/promise.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/promise">;
|
||||||
|
}
|
||||||
|
declare module "lib0/queue.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/queue">;
|
||||||
|
}
|
||||||
|
declare module "lib0/random.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/random">;
|
||||||
|
}
|
||||||
|
declare module "lib0/set.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/set">;
|
||||||
|
}
|
||||||
|
declare module "lib0/sort.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/sort">;
|
||||||
|
}
|
||||||
|
declare module "lib0/statistics.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/statistics">;
|
||||||
|
}
|
||||||
|
declare module "lib0/storage.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/storage">;
|
||||||
|
}
|
||||||
|
declare module "lib0/string.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/string">;
|
||||||
|
}
|
||||||
|
declare module "lib0/symbol.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/symbol">;
|
||||||
|
}
|
||||||
|
declare module "lib0/test.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/test">;
|
||||||
|
}
|
||||||
|
declare module "lib0/testing.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/testing">;
|
||||||
|
}
|
||||||
|
declare module "lib0/time.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/time">;
|
||||||
|
}
|
||||||
|
declare module "lib0/tree.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/tree">;
|
||||||
|
}
|
||||||
|
declare module "lib0/url.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/url">;
|
||||||
|
}
|
||||||
|
declare module "lib0/websocket.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/websocket">;
|
||||||
|
}
|
|
@ -0,0 +1,377 @@
|
||||||
|
// flow-typed signature: 97da878aea98698d6c06f8a696bb62af
|
||||||
|
// flow-typed version: <<STUB>>/lib0_v0.2.34/flow_v0.104.0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is an autogenerated libdef stub for:
|
||||||
|
*
|
||||||
|
* 'lib0'
|
||||||
|
*
|
||||||
|
* Fill this stub out by replacing all the `any` types.
|
||||||
|
*
|
||||||
|
* Once filled out, we encourage you to share your work with the
|
||||||
|
* community by sending a pull request to:
|
||||||
|
* https://github.com/flowtype/flow-typed
|
||||||
|
*/
|
||||||
|
|
||||||
|
// @flow
|
||||||
|
declare module "lib0" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We include stubs for each file inside this npm package in case you need to
|
||||||
|
* require those files directly. Feel free to delete any files that aren't
|
||||||
|
* needed.
|
||||||
|
*/
|
||||||
|
declare module "lib0/array" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/bin/gendocs" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/binary" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/broadcastchannel" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/buffer" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/component" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/conditions" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/decoding" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/diff" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/dist/test" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/dom" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/encoding" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/environment" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/error" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/eventloop" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/function" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/indexeddb" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/isomorphic" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/iterator" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/json" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/logging" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/map" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/math" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/metric" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/mutex" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/number" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/object" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/observable" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/pair" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/prng" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/prng/Mt19937" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/prng/Xoroshiro128plus" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/prng/Xorshift32" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/promise" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/queue" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/random" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/set" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/sort" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/statistics" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/storage" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/string" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/symbol" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/test" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/testing" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/time" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/tree" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/url" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "lib0/websocket" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filename aliases
|
||||||
|
declare module "lib0/array.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/array">;
|
||||||
|
}
|
||||||
|
declare module "lib0/bin/gendocs.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/bin/gendocs">;
|
||||||
|
}
|
||||||
|
declare module "lib0/binary.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/binary">;
|
||||||
|
}
|
||||||
|
declare module "lib0/broadcastchannel.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/broadcastchannel">;
|
||||||
|
}
|
||||||
|
declare module "lib0/buffer.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/buffer">;
|
||||||
|
}
|
||||||
|
declare module "lib0/component.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/component">;
|
||||||
|
}
|
||||||
|
declare module "lib0/conditions.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/conditions">;
|
||||||
|
}
|
||||||
|
declare module "lib0/decoding.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/decoding">;
|
||||||
|
}
|
||||||
|
declare module "lib0/dist/decoding.cjs" {
|
||||||
|
declare module.exports: $Exports<"lib0/decoding">;
|
||||||
|
}
|
||||||
|
declare module "lib0/diff.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/diff">;
|
||||||
|
}
|
||||||
|
declare module "lib0/dist/test.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/dist/test">;
|
||||||
|
}
|
||||||
|
declare module "lib0/dom.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/dom">;
|
||||||
|
}
|
||||||
|
declare module "lib0/encoding.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/encoding">;
|
||||||
|
}
|
||||||
|
declare module "lib0/dist/encoding.cjs" {
|
||||||
|
declare module.exports: $Exports<"lib0/encoding">;
|
||||||
|
}
|
||||||
|
declare module "lib0/environment.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/environment">;
|
||||||
|
}
|
||||||
|
declare module "lib0/error.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/error">;
|
||||||
|
}
|
||||||
|
declare module "lib0/eventloop.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/eventloop">;
|
||||||
|
}
|
||||||
|
declare module "lib0/function.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/function">;
|
||||||
|
}
|
||||||
|
declare module "lib0/index" {
|
||||||
|
declare module.exports: $Exports<"lib0">;
|
||||||
|
}
|
||||||
|
declare module "lib0/index.js" {
|
||||||
|
declare module.exports: $Exports<"lib0">;
|
||||||
|
}
|
||||||
|
declare module "lib0/indexeddb.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/indexeddb">;
|
||||||
|
}
|
||||||
|
declare module "lib0/isomorphic.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/isomorphic">;
|
||||||
|
}
|
||||||
|
declare module "lib0/iterator.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/iterator">;
|
||||||
|
}
|
||||||
|
declare module "lib0/json.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/json">;
|
||||||
|
}
|
||||||
|
declare module "lib0/logging.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/logging">;
|
||||||
|
}
|
||||||
|
declare module "lib0/map.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/map">;
|
||||||
|
}
|
||||||
|
declare module "lib0/math.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/math">;
|
||||||
|
}
|
||||||
|
declare module "lib0/metric.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/metric">;
|
||||||
|
}
|
||||||
|
declare module "lib0/mutex.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/mutex">;
|
||||||
|
}
|
||||||
|
declare module "lib0/dist/mutex.cjs" {
|
||||||
|
declare module.exports: $Exports<"lib0/mutex">;
|
||||||
|
}
|
||||||
|
declare module "lib0/number.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/number">;
|
||||||
|
}
|
||||||
|
declare module "lib0/object.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/object">;
|
||||||
|
}
|
||||||
|
declare module "lib0/observable.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/observable">;
|
||||||
|
}
|
||||||
|
declare module "lib0/pair.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/pair">;
|
||||||
|
}
|
||||||
|
declare module "lib0/prng.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/prng">;
|
||||||
|
}
|
||||||
|
declare module "lib0/prng/Mt19937.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/prng/Mt19937">;
|
||||||
|
}
|
||||||
|
declare module "lib0/prng/Xoroshiro128plus.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/prng/Xoroshiro128plus">;
|
||||||
|
}
|
||||||
|
declare module "lib0/prng/Xorshift32.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/prng/Xorshift32">;
|
||||||
|
}
|
||||||
|
declare module "lib0/promise.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/promise">;
|
||||||
|
}
|
||||||
|
declare module "lib0/queue.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/queue">;
|
||||||
|
}
|
||||||
|
declare module "lib0/random.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/random">;
|
||||||
|
}
|
||||||
|
declare module "lib0/set.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/set">;
|
||||||
|
}
|
||||||
|
declare module "lib0/sort.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/sort">;
|
||||||
|
}
|
||||||
|
declare module "lib0/statistics.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/statistics">;
|
||||||
|
}
|
||||||
|
declare module "lib0/storage.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/storage">;
|
||||||
|
}
|
||||||
|
declare module "lib0/string.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/string">;
|
||||||
|
}
|
||||||
|
declare module "lib0/symbol.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/symbol">;
|
||||||
|
}
|
||||||
|
declare module "lib0/test.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/test">;
|
||||||
|
}
|
||||||
|
declare module "lib0/testing.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/testing">;
|
||||||
|
}
|
||||||
|
declare module "lib0/time.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/time">;
|
||||||
|
}
|
||||||
|
declare module "lib0/tree.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/tree">;
|
||||||
|
}
|
||||||
|
declare module "lib0/url.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/url">;
|
||||||
|
}
|
||||||
|
declare module "lib0/websocket.js" {
|
||||||
|
declare module.exports: $Exports<"lib0/websocket">;
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
// flow-typed signature: 71e55e30d387153cf804d226f95c0ad8
|
||||||
|
// flow-typed version: <<STUB>>/y-indexeddb_v^9.0.5/flow_v0.104.0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is an autogenerated libdef stub for:
|
||||||
|
*
|
||||||
|
* 'y-indexeddb'
|
||||||
|
*
|
||||||
|
* Fill this stub out by replacing all the `any` types.
|
||||||
|
*
|
||||||
|
* Once filled out, we encourage you to share your work with the
|
||||||
|
* community by sending a pull request to:
|
||||||
|
* https://github.com/flowtype/flow-typed
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare module 'y-indexeddb' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We include stubs for each file inside this npm package in case you need to
|
||||||
|
* require those files directly. Feel free to delete any files that aren't
|
||||||
|
* needed.
|
||||||
|
*/
|
||||||
|
declare module 'y-indexeddb/dist/test' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'y-indexeddb/src/y-indexeddb' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filename aliases
|
||||||
|
declare module 'y-indexeddb/dist/test.js' {
|
||||||
|
declare module.exports: $Exports<'y-indexeddb/dist/test'>;
|
||||||
|
}
|
||||||
|
declare module 'y-indexeddb/src/y-indexeddb.js' {
|
||||||
|
declare module.exports: $Exports<'y-indexeddb/src/y-indexeddb'>;
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
// flow-typed signature: 2db53ec5dbb577a4e27bc465cd4670f3
|
||||||
|
// flow-typed version: <<STUB>>/y-prosemirror_v^0.3.7/flow_v0.104.0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is an autogenerated libdef stub for:
|
||||||
|
*
|
||||||
|
* 'y-prosemirror'
|
||||||
|
*
|
||||||
|
* Fill this stub out by replacing all the `any` types.
|
||||||
|
*
|
||||||
|
* Once filled out, we encourage you to share your work with the
|
||||||
|
* community by sending a pull request to:
|
||||||
|
* https://github.com/flowtype/flow-typed
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare module 'y-prosemirror' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We include stubs for each file inside this npm package in case you need to
|
||||||
|
* require those files directly. Feel free to delete any files that aren't
|
||||||
|
* needed.
|
||||||
|
*/
|
||||||
|
declare module 'y-prosemirror/dist/test' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'y-prosemirror/src/lib' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'y-prosemirror/src/plugins/cursor-plugin' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'y-prosemirror/src/plugins/sync-plugin' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'y-prosemirror/src/plugins/undo-plugin' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'y-prosemirror/src/y-prosemirror' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filename aliases
|
||||||
|
declare module 'y-prosemirror/dist/test.js' {
|
||||||
|
declare module.exports: $Exports<'y-prosemirror/dist/test'>;
|
||||||
|
}
|
||||||
|
declare module 'y-prosemirror/src/lib.js' {
|
||||||
|
declare module.exports: $Exports<'y-prosemirror/src/lib'>;
|
||||||
|
}
|
||||||
|
declare module 'y-prosemirror/src/plugins/cursor-plugin.js' {
|
||||||
|
declare module.exports: $Exports<'y-prosemirror/src/plugins/cursor-plugin'>;
|
||||||
|
}
|
||||||
|
declare module 'y-prosemirror/src/plugins/sync-plugin.js' {
|
||||||
|
declare module.exports: $Exports<'y-prosemirror/src/plugins/sync-plugin'>;
|
||||||
|
}
|
||||||
|
declare module 'y-prosemirror/src/plugins/undo-plugin.js' {
|
||||||
|
declare module.exports: $Exports<'y-prosemirror/src/plugins/undo-plugin'>;
|
||||||
|
}
|
||||||
|
declare module 'y-prosemirror/src/y-prosemirror.js' {
|
||||||
|
declare module.exports: $Exports<'y-prosemirror/src/y-prosemirror'>;
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
// flow-typed signature: 3ef5e4dd42591ff15af5f507abd6aa97
|
||||||
|
// flow-typed version: <<STUB>>/y-protocols_v^1.0.1/flow_v0.104.0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is an autogenerated libdef stub for:
|
||||||
|
*
|
||||||
|
* 'y-protocols'
|
||||||
|
*
|
||||||
|
* Fill this stub out by replacing all the `any` types.
|
||||||
|
*
|
||||||
|
* Once filled out, we encourage you to share your work with the
|
||||||
|
* community by sending a pull request to:
|
||||||
|
* https://github.com/flowtype/flow-typed
|
||||||
|
*/
|
||||||
|
|
||||||
|
// @flow
|
||||||
|
declare module "y-protocols" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We include stubs for each file inside this npm package in case you need to
|
||||||
|
* require those files directly. Feel free to delete any files that aren't
|
||||||
|
* needed.
|
||||||
|
*/
|
||||||
|
declare module "y-protocols/auth" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "y-protocols/awareness" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "y-protocols/awareness.test" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "y-protocols/dist/test" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "y-protocols/sync" {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filename aliases
|
||||||
|
declare module "y-protocols/auth.js" {
|
||||||
|
declare module.exports: $Exports<"y-protocols/auth">;
|
||||||
|
}
|
||||||
|
declare module "y-protocols/awareness.js" {
|
||||||
|
declare module.exports: $Exports<"y-protocols/awareness">;
|
||||||
|
}
|
||||||
|
declare module "y-protocols/dist/awareness.cjs" {
|
||||||
|
declare module.exports: $Exports<"y-protocols/awareness">;
|
||||||
|
}
|
||||||
|
declare module "y-protocols/awareness.test.js" {
|
||||||
|
declare module.exports: $Exports<"y-protocols/awareness.test">;
|
||||||
|
}
|
||||||
|
declare module "y-protocols/dist/test.js" {
|
||||||
|
declare module.exports: $Exports<"y-protocols/dist/test">;
|
||||||
|
}
|
||||||
|
declare module "y-protocols/sync.js" {
|
||||||
|
declare module.exports: $Exports<"y-protocols/sync">;
|
||||||
|
}
|
||||||
|
declare module "y-protocols/dist/sync.cjs" {
|
||||||
|
declare module.exports: $Exports<"y-protocols/sync">;
|
||||||
|
}
|
|
@ -0,0 +1,430 @@
|
||||||
|
// flow-typed signature: ec89eac307897bef104c76ce1dd14a4d
|
||||||
|
// flow-typed version: <<STUB>>/yjs_v^13.4.1/flow_v0.104.0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is an autogenerated libdef stub for:
|
||||||
|
*
|
||||||
|
* 'yjs'
|
||||||
|
*
|
||||||
|
* Fill this stub out by replacing all the `any` types.
|
||||||
|
*
|
||||||
|
* Once filled out, we encourage you to share your work with the
|
||||||
|
* community by sending a pull request to:
|
||||||
|
* https://github.com/flowtype/flow-typed
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare module 'yjs' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We include stubs for each file inside this npm package in case you need to
|
||||||
|
* require those files directly. Feel free to delete any files that aren't
|
||||||
|
* needed.
|
||||||
|
*/
|
||||||
|
declare module 'yjs/dist/tests' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/docs/scripts/jquery.min' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/docs/scripts/linenumber' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/docs/scripts/prettify/lang-css' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/docs/scripts/prettify/prettify' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/docs/scripts/tui-doc' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/src' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/src/internals' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/src/structs/AbstractStruct' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/src/structs/ContentAny' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/src/structs/ContentBinary' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/src/structs/ContentDeleted' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/src/structs/ContentDoc' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/src/structs/ContentEmbed' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/src/structs/ContentFormat' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/src/structs/ContentJSON' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/src/structs/ContentString' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/src/structs/ContentType' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/src/structs/GC' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/src/structs/Item' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/src/types/AbstractType' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/src/types/YArray' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/src/types/YMap' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/src/types/YText' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/src/types/YXmlElement' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/src/types/YXmlEvent' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/src/types/YXmlFragment' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/src/types/YXmlHook' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/src/types/YXmlText' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/src/utils/AbstractConnector' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/src/utils/DeleteSet' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/src/utils/Doc' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/src/utils/encoding' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/src/utils/EventHandler' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/src/utils/ID' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/src/utils/isParentOf' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/src/utils/logging' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/src/utils/PermanentUserData' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/src/utils/RelativePosition' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/src/utils/Snapshot' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/src/utils/StructStore' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/src/utils/Transaction' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/src/utils/UndoManager' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/src/utils/UpdateDecoder' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/src/utils/UpdateEncoder' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/src/utils/YEvent' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/tests/compatibility.tests' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/tests/doc.tests' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/tests/encoding.tests' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/tests' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/tests/snapshot.tests' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/tests/testHelper' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/tests/undo-redo.tests' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/tests/y-array.tests' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/tests/y-map.tests' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/tests/y-text.tests' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'yjs/tests/y-xml.tests' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filename aliases
|
||||||
|
declare module 'yjs/dist/tests.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/dist/tests'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/docs/scripts/jquery.min.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/docs/scripts/jquery.min'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/docs/scripts/linenumber.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/docs/scripts/linenumber'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/docs/scripts/prettify/lang-css.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/docs/scripts/prettify/lang-css'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/docs/scripts/prettify/prettify.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/docs/scripts/prettify/prettify'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/docs/scripts/tui-doc.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/docs/scripts/tui-doc'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/src/index' {
|
||||||
|
declare module.exports: $Exports<'yjs/src'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/src/index.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/src'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/src/internals.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/src/internals'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/src/structs/AbstractStruct.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/src/structs/AbstractStruct'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/src/structs/ContentAny.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/src/structs/ContentAny'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/src/structs/ContentBinary.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/src/structs/ContentBinary'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/src/structs/ContentDeleted.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/src/structs/ContentDeleted'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/src/structs/ContentDoc.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/src/structs/ContentDoc'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/src/structs/ContentEmbed.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/src/structs/ContentEmbed'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/src/structs/ContentFormat.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/src/structs/ContentFormat'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/src/structs/ContentJSON.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/src/structs/ContentJSON'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/src/structs/ContentString.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/src/structs/ContentString'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/src/structs/ContentType.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/src/structs/ContentType'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/src/structs/GC.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/src/structs/GC'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/src/structs/Item.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/src/structs/Item'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/src/types/AbstractType.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/src/types/AbstractType'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/src/types/YArray.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/src/types/YArray'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/src/types/YMap.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/src/types/YMap'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/src/types/YText.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/src/types/YText'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/src/types/YXmlElement.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/src/types/YXmlElement'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/src/types/YXmlEvent.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/src/types/YXmlEvent'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/src/types/YXmlFragment.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/src/types/YXmlFragment'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/src/types/YXmlHook.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/src/types/YXmlHook'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/src/types/YXmlText.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/src/types/YXmlText'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/src/utils/AbstractConnector.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/src/utils/AbstractConnector'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/src/utils/DeleteSet.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/src/utils/DeleteSet'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/src/utils/Doc.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/src/utils/Doc'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/src/utils/encoding.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/src/utils/encoding'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/src/utils/EventHandler.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/src/utils/EventHandler'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/src/utils/ID.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/src/utils/ID'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/src/utils/isParentOf.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/src/utils/isParentOf'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/src/utils/logging.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/src/utils/logging'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/src/utils/PermanentUserData.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/src/utils/PermanentUserData'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/src/utils/RelativePosition.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/src/utils/RelativePosition'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/src/utils/Snapshot.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/src/utils/Snapshot'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/src/utils/StructStore.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/src/utils/StructStore'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/src/utils/Transaction.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/src/utils/Transaction'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/src/utils/UndoManager.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/src/utils/UndoManager'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/src/utils/UpdateDecoder.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/src/utils/UpdateDecoder'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/src/utils/UpdateEncoder.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/src/utils/UpdateEncoder'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/src/utils/YEvent.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/src/utils/YEvent'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/tests/compatibility.tests.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/tests/compatibility.tests'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/tests/doc.tests.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/tests/doc.tests'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/tests/encoding.tests.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/tests/encoding.tests'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/tests/index' {
|
||||||
|
declare module.exports: $Exports<'yjs/tests'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/tests/index.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/tests'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/tests/snapshot.tests.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/tests/snapshot.tests'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/tests/testHelper.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/tests/testHelper'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/tests/undo-redo.tests.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/tests/undo-redo.tests'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/tests/y-array.tests.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/tests/y-array.tests'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/tests/y-map.tests.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/tests/y-map.tests'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/tests/y-text.tests.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/tests/y-text.tests'>;
|
||||||
|
}
|
||||||
|
declare module 'yjs/tests/y-xml.tests.js' {
|
||||||
|
declare module.exports: $Exports<'yjs/tests/y-xml.tests'>;
|
||||||
|
}
|
23
package.json
23
package.json
|
@ -6,11 +6,12 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rimraf build",
|
"clean": "rimraf build",
|
||||||
"build:i18n": "i18next --silent 'app/**/*.js' 'server/**/*.js' && mkdir -p ./build/shared/i18n && cp -R ./shared/i18n/locales ./build/shared/i18n",
|
"build:i18n": "i18next --silent 'app/**/*.js' 'server/**/*.js' && mkdir -p ./build/shared/i18n && cp -R ./shared/i18n/locales ./build/shared/i18n",
|
||||||
"build:server": "babel -d ./build/server ./server && babel -d ./build/shared ./shared && cp package.json ./build && ln -sf \"$(pwd)/webpack.config.dev.js\" ./build",
|
"build:server": "babel -d ./build/server ./server && babel -d ./build/shared ./shared && cp ./server/collaboration/Procfile ./build/server/collaboration/Procfile && cp package.json ./build && ln -sf \"$(pwd)/webpack.config.dev.js\" ./build",
|
||||||
"build:webpack": "webpack --config webpack.config.prod.js",
|
"build:webpack": "webpack --config webpack.config.prod.js",
|
||||||
"build": "yarn clean && yarn build:webpack && yarn build:i18n && yarn build:server",
|
"build": "yarn clean && yarn build:webpack && yarn build:i18n && yarn build:server",
|
||||||
"start": "node ./build/server/index.js",
|
"start": "node ./build/server/index.js",
|
||||||
"dev": "nodemon --exec \"yarn build:server && yarn build:i18n && node build/server/index.js\" -e js --ignore build/ --ignore app/ --ignore flow-typed/",
|
"dev": "yarn concurrently --kill-others -n server,multiplayer \"node --inspect=0.0.0.0 build/server/index.js --services=websockets,admin,web,worker\" \"node build/server/index.js --services=collaboration --port=4000\"",
|
||||||
|
"dev:watch": "nodemon --exec \"yarn build:server && yarn build:i18n && yarn dev\" -e js --ignore build/ --ignore app/ --ignore flow-typed/",
|
||||||
"lint": "eslint app server shared",
|
"lint": "eslint app server shared",
|
||||||
"deploy": "git push heroku master",
|
"deploy": "git push heroku master",
|
||||||
"prepare": "yarn yarn-deduplicate yarn.lock",
|
"prepare": "yarn yarn-deduplicate yarn.lock",
|
||||||
|
@ -45,6 +46,11 @@
|
||||||
"@babel/preset-env": "^7.11.0",
|
"@babel/preset-env": "^7.11.0",
|
||||||
"@babel/preset-flow": "^7.10.4",
|
"@babel/preset-flow": "^7.10.4",
|
||||||
"@babel/preset-react": "^7.10.4",
|
"@babel/preset-react": "^7.10.4",
|
||||||
|
"@bull-board/api": "^3.5.0",
|
||||||
|
"@bull-board/koa": "^3.5.0",
|
||||||
|
"@hocuspocus/extension-logger": "^1.0.0-alpha.43",
|
||||||
|
"@hocuspocus/provider": "^1.0.0-alpha.13",
|
||||||
|
"@hocuspocus/server": "^1.0.0-alpha.68",
|
||||||
"@outlinewiki/koa-passport": "^4.1.4",
|
"@outlinewiki/koa-passport": "^4.1.4",
|
||||||
"@outlinewiki/passport-azure-ad-oauth2": "^0.1.0",
|
"@outlinewiki/passport-azure-ad-oauth2": "^0.1.0",
|
||||||
"@sentry/node": "^6.3.1",
|
"@sentry/node": "^6.3.1",
|
||||||
|
@ -58,7 +64,7 @@
|
||||||
"babel-plugin-styled-components": "^1.11.1",
|
"babel-plugin-styled-components": "^1.11.1",
|
||||||
"babel-plugin-transform-class-properties": "^6.24.1",
|
"babel-plugin-transform-class-properties": "^6.24.1",
|
||||||
"boundless-arrow-key-navigation": "^1.0.4",
|
"boundless-arrow-key-navigation": "^1.0.4",
|
||||||
"bull": "^3.5.2",
|
"bull": "^3.29.0",
|
||||||
"cancan": "3.1.0",
|
"cancan": "3.1.0",
|
||||||
"chalk": "^4.1.0",
|
"chalk": "^4.1.0",
|
||||||
"compressorjs": "^1.0.7",
|
"compressorjs": "^1.0.7",
|
||||||
|
@ -96,6 +102,7 @@
|
||||||
"koa-body": "^4.2.0",
|
"koa-body": "^4.2.0",
|
||||||
"koa-compress": "2.0.0",
|
"koa-compress": "2.0.0",
|
||||||
"koa-convert": "1.2.0",
|
"koa-convert": "1.2.0",
|
||||||
|
"koa-easy-ws": "^1.3.0",
|
||||||
"koa-helmet": "5.2.0",
|
"koa-helmet": "5.2.0",
|
||||||
"koa-jwt": "^3.6.0",
|
"koa-jwt": "^3.6.0",
|
||||||
"koa-logger": "^3.2.1",
|
"koa-logger": "^3.2.1",
|
||||||
|
@ -111,7 +118,7 @@
|
||||||
"mobx-react": "^6.3.1",
|
"mobx-react": "^6.3.1",
|
||||||
"natural-sort": "^1.0.0",
|
"natural-sort": "^1.0.0",
|
||||||
"nodemailer": "^6.4.16",
|
"nodemailer": "^6.4.16",
|
||||||
"outline-icons": "^1.30.0",
|
"outline-icons": "^1.31.0",
|
||||||
"oy-vey": "^0.10.0",
|
"oy-vey": "^0.10.0",
|
||||||
"passport": "^0.4.1",
|
"passport": "^0.4.1",
|
||||||
"passport-google-oauth2": "^0.2.0",
|
"passport-google-oauth2": "^0.2.0",
|
||||||
|
@ -144,7 +151,7 @@
|
||||||
"react-window": "^1.8.6",
|
"react-window": "^1.8.6",
|
||||||
"reakit": "^1.3.8",
|
"reakit": "^1.3.8",
|
||||||
"regenerator-runtime": "^0.13.7",
|
"regenerator-runtime": "^0.13.7",
|
||||||
"rich-markdown-editor": "^11.17.4",
|
"rich-markdown-editor": "^11.17.5",
|
||||||
"semver": "^7.3.2",
|
"semver": "^7.3.2",
|
||||||
"sequelize": "^6.3.4",
|
"sequelize": "^6.3.4",
|
||||||
"sequelize-cli": "^6.2.0",
|
"sequelize-cli": "^6.2.0",
|
||||||
|
@ -167,7 +174,10 @@
|
||||||
"turndown": "^7.1.1",
|
"turndown": "^7.1.1",
|
||||||
"utf8": "^3.0.0",
|
"utf8": "^3.0.0",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"validator": "5.2.0"
|
"validator": "5.2.0",
|
||||||
|
"y-indexeddb": "^9.0.6",
|
||||||
|
"y-prosemirror": "^1.0.9",
|
||||||
|
"yjs": "^13.5.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.10.5",
|
"@babel/cli": "^7.10.5",
|
||||||
|
@ -177,6 +187,7 @@
|
||||||
"babel-jest": "^26.2.2",
|
"babel-jest": "^26.2.2",
|
||||||
"babel-loader": "^8.1.0",
|
"babel-loader": "^8.1.0",
|
||||||
"babel-plugin-transform-inline-environment-variables": "^0.4.3",
|
"babel-plugin-transform-inline-environment-variables": "^0.4.3",
|
||||||
|
"concurrently": "^6.2.1",
|
||||||
"enzyme": "^3.11.0",
|
"enzyme": "^3.11.0",
|
||||||
"enzyme-adapter-react-16": "^1.15.6",
|
"enzyme-adapter-react-16": "^1.15.6",
|
||||||
"eslint": "^7.6.0",
|
"eslint": "^7.6.0",
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
web: yarn start --services=collaboration
|
|
@ -0,0 +1,46 @@
|
||||||
|
// @flow
|
||||||
|
import { AuthenticationError } from "../errors";
|
||||||
|
import { Document } from "../models";
|
||||||
|
import policy from "../policies";
|
||||||
|
import { getUserForJWT } from "../utils/jwt";
|
||||||
|
|
||||||
|
const { can } = policy;
|
||||||
|
|
||||||
|
export default class Authentication {
|
||||||
|
async onAuthenticate({
|
||||||
|
connection,
|
||||||
|
token,
|
||||||
|
documentName,
|
||||||
|
}: {
|
||||||
|
connection: { readOnly: boolean },
|
||||||
|
token: string,
|
||||||
|
documentName: string,
|
||||||
|
}) {
|
||||||
|
// allows for different entity types to use this multiplayer provider later
|
||||||
|
const [, documentId] = documentName.split(".");
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
throw new AuthenticationError("Authentication required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await getUserForJWT(token);
|
||||||
|
if (user.isSuspended) {
|
||||||
|
throw new AuthenticationError("Account suspended");
|
||||||
|
}
|
||||||
|
|
||||||
|
const document = await Document.findByPk(documentId, { userId: user.id });
|
||||||
|
if (!can(user, "read", document)) {
|
||||||
|
throw new AuthenticationError("Authorization required");
|
||||||
|
}
|
||||||
|
|
||||||
|
// set document to read only for the current user, thus changes will not be
|
||||||
|
// accepted and synced to other clients
|
||||||
|
if (!can(user, "update", document)) {
|
||||||
|
connection.readOnly = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
user,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
// @flow
|
||||||
|
import debug from "debug";
|
||||||
|
import { debounce } from "lodash";
|
||||||
|
import * as Y from "yjs";
|
||||||
|
import documentUpdater from "../commands/documentUpdater";
|
||||||
|
import { Document, User } from "../models";
|
||||||
|
import markdownToYDoc from "./utils/markdownToYDoc";
|
||||||
|
|
||||||
|
const log = debug("server");
|
||||||
|
const DELAY = 3000;
|
||||||
|
|
||||||
|
export default class Persistence {
|
||||||
|
async onCreateDocument({
|
||||||
|
documentName,
|
||||||
|
...data
|
||||||
|
}: {
|
||||||
|
documentName: string,
|
||||||
|
document: Y.Doc,
|
||||||
|
}) {
|
||||||
|
const [, documentId] = documentName.split(".");
|
||||||
|
const fieldName = "default";
|
||||||
|
|
||||||
|
// Check if the given field already exists in the given y-doc. This is import
|
||||||
|
// so we don't import a document fresh if it exists already.
|
||||||
|
if (!data.document.isEmpty(fieldName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const document = await Document.findByPk(documentId);
|
||||||
|
|
||||||
|
if (document.state) {
|
||||||
|
const ydoc = new Y.Doc();
|
||||||
|
log(`Document ${documentId} is already in state`);
|
||||||
|
Y.applyUpdate(ydoc, document.state);
|
||||||
|
return ydoc;
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`Document ${documentId} is not in state, creating state from markdown`);
|
||||||
|
const ydoc = markdownToYDoc(document.text, fieldName);
|
||||||
|
const state = Y.encodeStateAsUpdate(ydoc);
|
||||||
|
|
||||||
|
await document.update({ state: Buffer.from(state) }, { hooks: false });
|
||||||
|
return ydoc;
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange = debounce(
|
||||||
|
async ({
|
||||||
|
document,
|
||||||
|
context,
|
||||||
|
documentName,
|
||||||
|
}: {
|
||||||
|
document: Y.Doc,
|
||||||
|
context: { user: User },
|
||||||
|
documentName: string,
|
||||||
|
}) => {
|
||||||
|
const [, documentId] = documentName.split(".");
|
||||||
|
|
||||||
|
log(`persisting ${documentId}`);
|
||||||
|
|
||||||
|
await documentUpdater({
|
||||||
|
documentId,
|
||||||
|
ydoc: document,
|
||||||
|
userId: context.user.id,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
DELAY,
|
||||||
|
{
|
||||||
|
maxWait: DELAY * 3,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
// @flow
|
||||||
|
import { Node, Fragment } from "prosemirror-model";
|
||||||
|
import { parser, schema } from "rich-markdown-editor";
|
||||||
|
import { prosemirrorToYDoc } from "y-prosemirror";
|
||||||
|
import * as Y from "yjs";
|
||||||
|
import embeds from "../../../shared/embeds";
|
||||||
|
|
||||||
|
export default function markdownToYDoc(
|
||||||
|
markdown: string,
|
||||||
|
fieldName?: string = "default"
|
||||||
|
): Y.Doc {
|
||||||
|
let node = parser.parse(markdown);
|
||||||
|
|
||||||
|
// in rich-markdown-editor embeds were created at runtime by converting links
|
||||||
|
// into embeds where they match. Because we're converting to a CRDT structure
|
||||||
|
// on the server we need to mimic this behavior.
|
||||||
|
function urlsToEmbeds(node: Node): Node {
|
||||||
|
if (node.type.name === "paragraph") {
|
||||||
|
for (const textNode of node.content.content) {
|
||||||
|
for (const embed of embeds) {
|
||||||
|
if (textNode.text && embed.matcher(textNode.text)) {
|
||||||
|
return schema.nodes.embed.createAndFill({
|
||||||
|
href: textNode.text,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.content) {
|
||||||
|
const contentAsArray =
|
||||||
|
node.content instanceof Fragment ? node.content.content : node.content;
|
||||||
|
node.content = Fragment.fromArray(contentAsArray.map(urlsToEmbeds));
|
||||||
|
}
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
node = urlsToEmbeds(node);
|
||||||
|
|
||||||
|
return prosemirrorToYDoc(node, fieldName);
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
// @flow
|
||||||
|
import { uniq } from "lodash";
|
||||||
|
import { Node } from "prosemirror-model";
|
||||||
|
import { schema, serializer } from "rich-markdown-editor";
|
||||||
|
import { yDocToProsemirrorJSON } from "y-prosemirror";
|
||||||
|
import * as Y from "yjs";
|
||||||
|
import { Document, Event } from "../models";
|
||||||
|
|
||||||
|
export default async function documentUpdater({
|
||||||
|
documentId,
|
||||||
|
ydoc,
|
||||||
|
userId,
|
||||||
|
done,
|
||||||
|
}: {
|
||||||
|
documentId: string,
|
||||||
|
ydoc: Y.Doc,
|
||||||
|
userId: string,
|
||||||
|
done?: boolean,
|
||||||
|
}) {
|
||||||
|
const document = await Document.findByPk(documentId);
|
||||||
|
const state = Y.encodeStateAsUpdate(ydoc);
|
||||||
|
const node = Node.fromJSON(schema, yDocToProsemirrorJSON(ydoc, "default"));
|
||||||
|
const text = serializer.serialize(node);
|
||||||
|
|
||||||
|
const isUnchanged = document.text === text;
|
||||||
|
const hasMultiplayerState = !!document.state;
|
||||||
|
|
||||||
|
if (isUnchanged && hasMultiplayerState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract collaborators from doc user data
|
||||||
|
const pud = new Y.PermanentUserData(ydoc);
|
||||||
|
const pudIds = Array.from(pud.clients.values());
|
||||||
|
const existingIds = document.collaboratorIds;
|
||||||
|
const collaboratorIds = uniq([...pudIds, ...existingIds]);
|
||||||
|
|
||||||
|
await Document.scope("withUnpublished").update(
|
||||||
|
{
|
||||||
|
text,
|
||||||
|
state: Buffer.from(state),
|
||||||
|
updatedAt: isUnchanged ? document.updatedAt : new Date(),
|
||||||
|
lastModifiedById: isUnchanged ? document.lastModifiedById : userId,
|
||||||
|
collaboratorIds,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hooks: false,
|
||||||
|
where: {
|
||||||
|
id: documentId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isUnchanged) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Event.add({
|
||||||
|
name: "documents.update",
|
||||||
|
documentId: document.id,
|
||||||
|
collectionId: document.collectionId,
|
||||||
|
teamId: document.teamId,
|
||||||
|
actorId: userId,
|
||||||
|
data: {
|
||||||
|
multiplayer: true,
|
||||||
|
title: document.title,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
|
@ -8,30 +8,31 @@ import Koa from "koa";
|
||||||
import compress from "koa-compress";
|
import compress from "koa-compress";
|
||||||
import helmet from "koa-helmet";
|
import helmet from "koa-helmet";
|
||||||
import logger from "koa-logger";
|
import logger from "koa-logger";
|
||||||
|
import onerror from "koa-onerror";
|
||||||
import Router from "koa-router";
|
import Router from "koa-router";
|
||||||
import { uniq } from "lodash";
|
import { uniq } from "lodash";
|
||||||
import stoppable from "stoppable";
|
import stoppable from "stoppable";
|
||||||
import throng from "throng";
|
import throng from "throng";
|
||||||
import "./sentry";
|
|
||||||
import services from "./services";
|
import services from "./services";
|
||||||
|
import { getArg } from "./utils/args";
|
||||||
|
import { requestErrorHandler } from "./utils/sentry";
|
||||||
import { checkEnv, checkMigrations } from "./utils/startup";
|
import { checkEnv, checkMigrations } from "./utils/startup";
|
||||||
import { checkUpdates } from "./utils/updates";
|
import { checkUpdates } from "./utils/updates";
|
||||||
|
|
||||||
checkEnv();
|
checkEnv();
|
||||||
checkMigrations();
|
checkMigrations();
|
||||||
|
|
||||||
|
// If a --port flag is passed then it takes priority over the env variable
|
||||||
|
const normalizedPortFlag = getArg("port", "p");
|
||||||
|
|
||||||
// If a services flag is passed it takes priority over the enviroment variable
|
// If a services flag is passed it takes priority over the enviroment variable
|
||||||
// for example: --services=web,worker
|
// for example: --services=web,worker
|
||||||
const normalizedServiceFlag = process.argv
|
const normalizedServiceFlag = getArg("services");
|
||||||
.slice(2)
|
|
||||||
.filter((arg) => arg.startsWith("--services="))
|
|
||||||
.map((arg) => arg.split("=")[1])
|
|
||||||
.join(",");
|
|
||||||
|
|
||||||
// The default is to run all services to make development and OSS installations
|
// The default is to run all services to make development and OSS installations
|
||||||
// easier to deal with. Separate services are only needed at scale.
|
// easier to deal with. Separate services are only needed at scale.
|
||||||
const serviceNames = uniq(
|
const serviceNames = uniq(
|
||||||
(normalizedServiceFlag || env.SERVICES || "web,websockets,worker")
|
(normalizedServiceFlag || env.SERVICES || "websockets,worker,web")
|
||||||
.split(",")
|
.split(",")
|
||||||
.map((service) => service.trim())
|
.map((service) => service.trim())
|
||||||
);
|
);
|
||||||
|
@ -48,10 +49,23 @@ async function start(id, disconnect) {
|
||||||
app.use(compress());
|
app.use(compress());
|
||||||
app.use(helmet());
|
app.use(helmet());
|
||||||
|
|
||||||
|
// catch errors in one place, automatically set status and response headers
|
||||||
|
onerror(app);
|
||||||
|
app.on("error", requestErrorHandler);
|
||||||
|
|
||||||
// install health check endpoint for all services
|
// install health check endpoint for all services
|
||||||
router.get("/_health", (ctx) => (ctx.body = "OK"));
|
router.get("/_health", (ctx) => (ctx.body = "OK"));
|
||||||
app.use(router.routes());
|
app.use(router.routes());
|
||||||
|
|
||||||
|
if (
|
||||||
|
serviceNames.includes("websockets") &&
|
||||||
|
serviceNames.includes("collaboration")
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Cannot run websockets and collaboration services in the same process"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// loop through requestsed services at startup
|
// loop through requestsed services at startup
|
||||||
for (const name of serviceNames) {
|
for (const name of serviceNames) {
|
||||||
if (!Object.keys(services).includes(name)) {
|
if (!Object.keys(services).includes(name)) {
|
||||||
|
@ -72,7 +86,7 @@ async function start(id, disconnect) {
|
||||||
console.log(`\n> Listening on http://localhost:${address.port}\n`);
|
console.log(`\n> Listening on http://localhost:${address.port}\n`);
|
||||||
});
|
});
|
||||||
|
|
||||||
server.listen(env.PORT || "3000");
|
server.listen(normalizedPortFlag || env.PORT || "3000");
|
||||||
|
|
||||||
process.once("SIGTERM", shutdown);
|
process.once("SIGTERM", shutdown);
|
||||||
process.once("SIGINT", shutdown);
|
process.once("SIGINT", shutdown);
|
||||||
|
@ -86,8 +100,13 @@ async function start(id, disconnect) {
|
||||||
throng({
|
throng({
|
||||||
worker: start,
|
worker: start,
|
||||||
|
|
||||||
// The number of workers to run, defaults to the number of CPU's available
|
// The number of processes to run, defaults to the number of CPU's available
|
||||||
count: process.env.WEB_CONCURRENCY || undefined,
|
// for the web service, and 1 for collaboration during the beta period.
|
||||||
|
count: serviceNames.includes("web")
|
||||||
|
? process.env.WEB_CONCURRENCY || undefined
|
||||||
|
: serviceNames.includes("collaboration")
|
||||||
|
? 1
|
||||||
|
: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (env.ENABLE_UPDATES !== "false" && process.env.NODE_ENV === "production") {
|
if (env.ENABLE_UPDATES !== "false" && process.env.NODE_ENV === "production") {
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: async (queryInterface, Sequelize) => {
|
||||||
|
await queryInterface.addColumn("documents", "state", {
|
||||||
|
type: Sequelize.BLOB,
|
||||||
|
});
|
||||||
|
await queryInterface.addColumn("teams", "collaborativeEditing", {
|
||||||
|
type: Sequelize.BOOLEAN,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: async (queryInterface, Sequelize) => {
|
||||||
|
await queryInterface.removeColumn("documents", "state");
|
||||||
|
await queryInterface.removeColumn("teams", "collaborativeEditing");
|
||||||
|
},
|
||||||
|
};
|
|
@ -81,6 +81,7 @@ const Document = sequelize.define(
|
||||||
template: DataTypes.BOOLEAN,
|
template: DataTypes.BOOLEAN,
|
||||||
editorVersion: DataTypes.STRING,
|
editorVersion: DataTypes.STRING,
|
||||||
text: DataTypes.TEXT,
|
text: DataTypes.TEXT,
|
||||||
|
state: DataTypes.BLOB,
|
||||||
isWelcome: { type: DataTypes.BOOLEAN, defaultValue: false },
|
isWelcome: { type: DataTypes.BOOLEAN, defaultValue: false },
|
||||||
revisionCount: { type: DataTypes.INTEGER, defaultValue: 0 },
|
revisionCount: { type: DataTypes.INTEGER, defaultValue: 0 },
|
||||||
archivedAt: DataTypes.DATE,
|
archivedAt: DataTypes.DATE,
|
||||||
|
|
|
@ -69,6 +69,11 @@ const Team = sequelize.define(
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
},
|
},
|
||||||
|
collaborativeEditing: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
paranoid: true,
|
paranoid: true,
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { languages } from "../../shared/i18n";
|
||||||
import { ValidationError } from "../errors";
|
import { ValidationError } from "../errors";
|
||||||
import { DataTypes, sequelize, encryptedFields, Op } from "../sequelize";
|
import { DataTypes, sequelize, encryptedFields, Op } from "../sequelize";
|
||||||
import { DEFAULT_AVATAR_HOST } from "../utils/avatars";
|
import { DEFAULT_AVATAR_HOST } from "../utils/avatars";
|
||||||
|
import { palette } from "../utils/color";
|
||||||
import { publicS3Endpoint, uploadToS3FromUrl } from "../utils/s3";
|
import { publicS3Endpoint, uploadToS3FromUrl } from "../utils/s3";
|
||||||
import {
|
import {
|
||||||
UserAuthentication,
|
UserAuthentication,
|
||||||
|
@ -74,6 +75,11 @@ const User = sequelize.define(
|
||||||
.digest("hex");
|
.digest("hex");
|
||||||
return `${DEFAULT_AVATAR_HOST}/avatar/${hash}/${initial}.png`;
|
return `${DEFAULT_AVATAR_HOST}/avatar/${hash}/${initial}.png`;
|
||||||
},
|
},
|
||||||
|
color() {
|
||||||
|
const idAsHex = crypto.createHash("md5").update(this.id).digest("hex");
|
||||||
|
const idAsNumber = parseInt(idAsHex, 16);
|
||||||
|
return palette[idAsNumber % palette.length];
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
exports[`presents a user 1`] = `
|
exports[`presents a user 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"avatarUrl": undefined,
|
"avatarUrl": undefined,
|
||||||
|
"color": undefined,
|
||||||
"createdAt": undefined,
|
"createdAt": undefined,
|
||||||
"id": "123",
|
"id": "123",
|
||||||
"isAdmin": undefined,
|
"isAdmin": undefined,
|
||||||
|
@ -16,6 +17,7 @@ Object {
|
||||||
exports[`presents a user without slack data 1`] = `
|
exports[`presents a user without slack data 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"avatarUrl": undefined,
|
"avatarUrl": undefined,
|
||||||
|
"color": undefined,
|
||||||
"createdAt": undefined,
|
"createdAt": undefined,
|
||||||
"id": "123",
|
"id": "123",
|
||||||
"isAdmin": undefined,
|
"isAdmin": undefined,
|
||||||
|
|
|
@ -4,8 +4,11 @@
|
||||||
// do not add anything here that should be a secret or password
|
// do not add anything here that should be a secret or password
|
||||||
export default function present(env: Object): Object {
|
export default function present(env: Object): Object {
|
||||||
return {
|
return {
|
||||||
URL: env.URL,
|
URL: env.URL.replace(/\/$/, ""),
|
||||||
CDN_URL: env.CDN_URL || "",
|
CDN_URL: (env.CDN_URL || "").replace(/\/$/, ""),
|
||||||
|
COLLABORATION_URL: (env.COLLABORATION_URL || "")
|
||||||
|
.replace(/\/$/, "")
|
||||||
|
.replace(/^http/, "ws"),
|
||||||
DEPLOYMENT: env.DEPLOYMENT,
|
DEPLOYMENT: env.DEPLOYMENT,
|
||||||
ENVIRONMENT: env.NODE_ENV,
|
ENVIRONMENT: env.NODE_ENV,
|
||||||
SENTRY_DSN: env.SENTRY_DSN,
|
SENTRY_DSN: env.SENTRY_DSN,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
import env from "../env";
|
||||||
import { Team } from "../models";
|
import { Team } from "../models";
|
||||||
|
|
||||||
export default function present(team: Team) {
|
export default function present(team: Team) {
|
||||||
|
@ -7,6 +8,7 @@ export default function present(team: Team) {
|
||||||
name: team.name,
|
name: team.name,
|
||||||
avatarUrl: team.logoUrl,
|
avatarUrl: team.logoUrl,
|
||||||
sharing: team.sharing,
|
sharing: team.sharing,
|
||||||
|
collaborativeEditing: team.collaborativeEditing && env.COLLABORATION_URL,
|
||||||
documentEmbeds: team.documentEmbeds,
|
documentEmbeds: team.documentEmbeds,
|
||||||
guestSignin: team.guestSignin,
|
guestSignin: team.guestSignin,
|
||||||
subdomain: team.subdomain,
|
subdomain: team.subdomain,
|
||||||
|
|
|
@ -10,6 +10,7 @@ type UserPresentation = {
|
||||||
name: string,
|
name: string,
|
||||||
avatarUrl: ?string,
|
avatarUrl: ?string,
|
||||||
email?: string,
|
email?: string,
|
||||||
|
color: string,
|
||||||
isAdmin: boolean,
|
isAdmin: boolean,
|
||||||
isSuspended: boolean,
|
isSuspended: boolean,
|
||||||
isViewer: boolean,
|
isViewer: boolean,
|
||||||
|
@ -21,6 +22,7 @@ export default (user: User, options: Options = {}): ?UserPresentation => {
|
||||||
userData.id = user.id;
|
userData.id = user.id;
|
||||||
userData.createdAt = user.createdAt;
|
userData.createdAt = user.createdAt;
|
||||||
userData.name = user.name;
|
userData.name = user.name;
|
||||||
|
userData.color = user.color;
|
||||||
userData.isAdmin = user.isAdmin;
|
userData.isAdmin = user.isAdmin;
|
||||||
userData.isViewer = user.isViewer;
|
userData.isViewer = user.isViewer;
|
||||||
userData.isSuspended = user.isSuspended;
|
userData.isSuspended = user.isSuspended;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { Document, Backlink } from "../../models";
|
import { Document, Backlink, Team } from "../../models";
|
||||||
import { Op } from "../../sequelize";
|
import { Op } from "../../sequelize";
|
||||||
import type { DocumentEvent, RevisionEvent } from "../../types";
|
import type { DocumentEvent, RevisionEvent } from "../../types";
|
||||||
import parseDocumentIds from "../../utils/parseDocumentIds";
|
import parseDocumentIds from "../../utils/parseDocumentIds";
|
||||||
|
@ -78,13 +78,19 @@ export default class BacklinksProcessor {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "documents.title_change": {
|
case "documents.title_change": {
|
||||||
const document = await Document.findByPk(event.documentId);
|
|
||||||
if (!document) return;
|
|
||||||
|
|
||||||
// might as well check
|
// might as well check
|
||||||
const { title, previousTitle } = event.data;
|
const { title, previousTitle } = event.data;
|
||||||
if (!previousTitle || title === previousTitle) break;
|
if (!previousTitle || title === previousTitle) break;
|
||||||
|
|
||||||
|
const document = await Document.findByPk(event.documentId);
|
||||||
|
if (!document) return;
|
||||||
|
|
||||||
|
// TODO: Handle re-writing of titles into CRDT
|
||||||
|
const team = await Team.findByPk(document.teamId);
|
||||||
|
if (team?.collaborativeEditing) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// update any link titles in documents that lead to this one
|
// update any link titles in documents that lead to this one
|
||||||
const backlinks = await Backlink.findAll({
|
const backlinks = await Backlink.findAll({
|
||||||
where: {
|
where: {
|
||||||
|
|
|
@ -9,11 +9,12 @@ const options = {
|
||||||
},
|
},
|
||||||
// support Heroku Redis, see:
|
// support Heroku Redis, see:
|
||||||
// https://devcenter.heroku.com/articles/heroku-redis#ioredis-module
|
// https://devcenter.heroku.com/articles/heroku-redis#ioredis-module
|
||||||
tls: process.env.REDIS_URL.startsWith("rediss://")
|
tls:
|
||||||
? {
|
process.env.REDIS_URL && process.env.REDIS_URL.startsWith("rediss://")
|
||||||
rejectUnauthorized: false,
|
? {
|
||||||
}
|
rejectUnauthorized: false,
|
||||||
: undefined,
|
}
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const client = new Redis(process.env.REDIS_URL, options);
|
const client = new Redis(process.env.REDIS_URL, options);
|
||||||
|
|
|
@ -4,6 +4,7 @@ exports[`#users.activate should activate a suspended user 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"data": Object {
|
"data": Object {
|
||||||
"avatarUrl": "https://tiley.herokuapp.com/avatar/111d68d06e2d317b5a59c2c6c5bad808/U.png",
|
"avatarUrl": "https://tiley.herokuapp.com/avatar/111d68d06e2d317b5a59c2c6c5bad808/U.png",
|
||||||
|
"color": "#e600e0",
|
||||||
"createdAt": "2018-01-02T00:00:00.000Z",
|
"createdAt": "2018-01-02T00:00:00.000Z",
|
||||||
"email": "user1@example.com",
|
"email": "user1@example.com",
|
||||||
"id": "46fde1d4-0050-428f-9f0b-0bf77f4bdf61",
|
"id": "46fde1d4-0050-428f-9f0b-0bf77f4bdf61",
|
||||||
|
@ -56,6 +57,7 @@ exports[`#users.demote should demote an admin 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"data": Object {
|
"data": Object {
|
||||||
"avatarUrl": "https://tiley.herokuapp.com/avatar/111d68d06e2d317b5a59c2c6c5bad808/U.png",
|
"avatarUrl": "https://tiley.herokuapp.com/avatar/111d68d06e2d317b5a59c2c6c5bad808/U.png",
|
||||||
|
"color": "#e600e0",
|
||||||
"createdAt": "2018-01-02T00:00:00.000Z",
|
"createdAt": "2018-01-02T00:00:00.000Z",
|
||||||
"email": "user1@example.com",
|
"email": "user1@example.com",
|
||||||
"id": "46fde1d4-0050-428f-9f0b-0bf77f4bdf61",
|
"id": "46fde1d4-0050-428f-9f0b-0bf77f4bdf61",
|
||||||
|
@ -90,6 +92,7 @@ exports[`#users.demote should demote an admin to member 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"data": Object {
|
"data": Object {
|
||||||
"avatarUrl": "https://tiley.herokuapp.com/avatar/111d68d06e2d317b5a59c2c6c5bad808/U.png",
|
"avatarUrl": "https://tiley.herokuapp.com/avatar/111d68d06e2d317b5a59c2c6c5bad808/U.png",
|
||||||
|
"color": "#e600e0",
|
||||||
"createdAt": "2018-01-02T00:00:00.000Z",
|
"createdAt": "2018-01-02T00:00:00.000Z",
|
||||||
"email": "user1@example.com",
|
"email": "user1@example.com",
|
||||||
"id": "46fde1d4-0050-428f-9f0b-0bf77f4bdf61",
|
"id": "46fde1d4-0050-428f-9f0b-0bf77f4bdf61",
|
||||||
|
@ -124,6 +127,7 @@ exports[`#users.demote should demote an admin to viewer 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"data": Object {
|
"data": Object {
|
||||||
"avatarUrl": "https://tiley.herokuapp.com/avatar/111d68d06e2d317b5a59c2c6c5bad808/U.png",
|
"avatarUrl": "https://tiley.herokuapp.com/avatar/111d68d06e2d317b5a59c2c6c5bad808/U.png",
|
||||||
|
"color": "#e600e0",
|
||||||
"createdAt": "2018-01-02T00:00:00.000Z",
|
"createdAt": "2018-01-02T00:00:00.000Z",
|
||||||
"email": "user1@example.com",
|
"email": "user1@example.com",
|
||||||
"id": "46fde1d4-0050-428f-9f0b-0bf77f4bdf61",
|
"id": "46fde1d4-0050-428f-9f0b-0bf77f4bdf61",
|
||||||
|
@ -176,6 +180,7 @@ exports[`#users.promote should promote a new admin 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"data": Object {
|
"data": Object {
|
||||||
"avatarUrl": "https://tiley.herokuapp.com/avatar/111d68d06e2d317b5a59c2c6c5bad808/U.png",
|
"avatarUrl": "https://tiley.herokuapp.com/avatar/111d68d06e2d317b5a59c2c6c5bad808/U.png",
|
||||||
|
"color": "#e600e0",
|
||||||
"createdAt": "2018-01-02T00:00:00.000Z",
|
"createdAt": "2018-01-02T00:00:00.000Z",
|
||||||
"email": "user1@example.com",
|
"email": "user1@example.com",
|
||||||
"id": "46fde1d4-0050-428f-9f0b-0bf77f4bdf61",
|
"id": "46fde1d4-0050-428f-9f0b-0bf77f4bdf61",
|
||||||
|
@ -237,6 +242,7 @@ exports[`#users.suspend should suspend an user 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"data": Object {
|
"data": Object {
|
||||||
"avatarUrl": "https://tiley.herokuapp.com/avatar/111d68d06e2d317b5a59c2c6c5bad808/U.png",
|
"avatarUrl": "https://tiley.herokuapp.com/avatar/111d68d06e2d317b5a59c2c6c5bad808/U.png",
|
||||||
|
"color": "#e600e0",
|
||||||
"createdAt": "2018-01-02T00:00:00.000Z",
|
"createdAt": "2018-01-02T00:00:00.000Z",
|
||||||
"email": "user1@example.com",
|
"email": "user1@example.com",
|
||||||
"id": "46fde1d4-0050-428f-9f0b-0bf77f4bdf61",
|
"id": "46fde1d4-0050-428f-9f0b-0bf77f4bdf61",
|
||||||
|
|
|
@ -13,7 +13,7 @@ import {
|
||||||
IntegrationAuthentication,
|
IntegrationAuthentication,
|
||||||
} from "../../models";
|
} from "../../models";
|
||||||
import { presentSlackAttachment } from "../../presenters";
|
import { presentSlackAttachment } from "../../presenters";
|
||||||
import * as Slack from "../../slack";
|
import * as Slack from "../../utils/slack";
|
||||||
const router = new Router();
|
const router = new Router();
|
||||||
|
|
||||||
// triggered by a user posting a getoutline.com link in Slack
|
// triggered by a user posting a getoutline.com link in Slack
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
import TestServer from "fetch-test-server";
|
import TestServer from "fetch-test-server";
|
||||||
import { IntegrationAuthentication, SearchQuery } from "../../models";
|
import { IntegrationAuthentication, SearchQuery } from "../../models";
|
||||||
import webService from "../../services/web";
|
import webService from "../../services/web";
|
||||||
import * as Slack from "../../slack";
|
|
||||||
import { buildDocument, buildIntegration } from "../../test/factories";
|
import { buildDocument, buildIntegration } from "../../test/factories";
|
||||||
import { flushdb, seed } from "../../test/support";
|
import { flushdb, seed } from "../../test/support";
|
||||||
|
import * as Slack from "../../utils/slack";
|
||||||
|
|
||||||
const app = webService();
|
const app = webService();
|
||||||
const server = new TestServer(app.callback());
|
const server = new TestServer(app.callback());
|
||||||
|
@ -12,7 +12,7 @@ const server = new TestServer(app.callback());
|
||||||
beforeEach(() => flushdb());
|
beforeEach(() => flushdb());
|
||||||
afterAll(() => server.close());
|
afterAll(() => server.close());
|
||||||
|
|
||||||
jest.mock("../../slack", () => ({
|
jest.mock("../../utils/slack", () => ({
|
||||||
post: jest.fn(),
|
post: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
|
@ -12,8 +12,8 @@ import {
|
||||||
Integration,
|
Integration,
|
||||||
Team,
|
Team,
|
||||||
} from "../../../models";
|
} from "../../../models";
|
||||||
import * as Slack from "../../../slack";
|
|
||||||
import { StateStore } from "../../../utils/passport";
|
import { StateStore } from "../../../utils/passport";
|
||||||
|
import * as Slack from "../../../utils/slack";
|
||||||
|
|
||||||
const router = new Router();
|
const router = new Router();
|
||||||
const providerName = "slack";
|
const providerName = "slack";
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
// @flow
|
|
||||||
import * as Sentry from "@sentry/node";
|
|
||||||
import env from "./env";
|
|
||||||
|
|
||||||
if (env.SENTRY_DSN) {
|
|
||||||
Sentry.init({
|
|
||||||
dsn: env.SENTRY_DSN,
|
|
||||||
environment: env.ENVIRONMENT,
|
|
||||||
release: env.RELEASE,
|
|
||||||
maxBreadcrumbs: 0,
|
|
||||||
ignoreErrors: [
|
|
||||||
// emitted by Koa when bots attempt to snoop on paths such as wp-admin
|
|
||||||
// or the user client submits a bad request. These are expected in normal
|
|
||||||
// running of the application and don't need to be reported.
|
|
||||||
"BadRequestError",
|
|
||||||
"UnauthorizedError",
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Sentry;
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
// @flow
|
||||||
|
import http from "http";
|
||||||
|
import { createBullBoard } from "@bull-board/api";
|
||||||
|
import { BullAdapter } from "@bull-board/api/bullAdapter";
|
||||||
|
import { KoaAdapter } from "@bull-board/koa";
|
||||||
|
import Koa from "koa";
|
||||||
|
import {
|
||||||
|
emailsQueue,
|
||||||
|
globalEventQueue,
|
||||||
|
processorEventQueue,
|
||||||
|
websocketsQueue,
|
||||||
|
} from "../queues";
|
||||||
|
|
||||||
|
export default function init(app: Koa, server?: http.Server) {
|
||||||
|
const serverAdapter = new KoaAdapter();
|
||||||
|
createBullBoard({
|
||||||
|
queues: [
|
||||||
|
new BullAdapter(globalEventQueue),
|
||||||
|
new BullAdapter(processorEventQueue),
|
||||||
|
new BullAdapter(emailsQueue),
|
||||||
|
new BullAdapter(websocketsQueue),
|
||||||
|
],
|
||||||
|
serverAdapter,
|
||||||
|
});
|
||||||
|
|
||||||
|
serverAdapter.setBasePath("/admin");
|
||||||
|
app.use(serverAdapter.registerPlugin());
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
// @flow
|
||||||
|
import http from "http";
|
||||||
|
import { Logger } from "@hocuspocus/extension-logger";
|
||||||
|
import { Server } from "@hocuspocus/server";
|
||||||
|
import Koa from "koa";
|
||||||
|
import websocket from "koa-easy-ws";
|
||||||
|
import Router from "koa-router";
|
||||||
|
import AuthenticationExtension from "../collaboration/authentication";
|
||||||
|
import PersistenceExtension from "../collaboration/persistence";
|
||||||
|
|
||||||
|
export default function init(app: Koa, server: http.Server) {
|
||||||
|
const router = new Router();
|
||||||
|
|
||||||
|
const hocuspocus = Server.configure({
|
||||||
|
extensions: [
|
||||||
|
new AuthenticationExtension(),
|
||||||
|
new PersistenceExtension(),
|
||||||
|
new Logger(),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Websockets for collaborative editing
|
||||||
|
router.get("/collaboration/:documentName", async (ctx) => {
|
||||||
|
let { documentName } = ctx.params;
|
||||||
|
|
||||||
|
if (ctx.ws) {
|
||||||
|
const ws = await ctx.ws();
|
||||||
|
hocuspocus.handleConnection(ws, ctx.request, documentName);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.response.status = 101;
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use(websocket());
|
||||||
|
app.use(router.routes());
|
||||||
|
app.use(router.allowedMethods());
|
||||||
|
}
|
|
@ -1,6 +1,8 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
import admin from "./admin";
|
||||||
|
import collaboration from "./collaboration";
|
||||||
import web from "./web";
|
import web from "./web";
|
||||||
import websockets from "./websockets";
|
import websockets from "./websockets";
|
||||||
import worker from "./worker";
|
import worker from "./worker";
|
||||||
|
|
||||||
export default { web, websockets, worker };
|
export default { websockets, collaboration, admin, web, worker };
|
||||||
|
|
|
@ -7,14 +7,12 @@ import {
|
||||||
referrerPolicy,
|
referrerPolicy,
|
||||||
} from "koa-helmet";
|
} from "koa-helmet";
|
||||||
import mount from "koa-mount";
|
import mount from "koa-mount";
|
||||||
import onerror from "koa-onerror";
|
|
||||||
import enforceHttps from "koa-sslify";
|
import enforceHttps from "koa-sslify";
|
||||||
import emails from "../emails";
|
import emails from "../emails";
|
||||||
import env from "../env";
|
import env from "../env";
|
||||||
import routes from "../routes";
|
import routes from "../routes";
|
||||||
import api from "../routes/api";
|
import api from "../routes/api";
|
||||||
import auth from "../routes/auth";
|
import auth from "../routes/auth";
|
||||||
import Sentry from "../sentry";
|
|
||||||
|
|
||||||
const isProduction = env.NODE_ENV === "production";
|
const isProduction = env.NODE_ENV === "production";
|
||||||
const isTest = env.NODE_ENV === "test";
|
const isTest = env.NODE_ENV === "test";
|
||||||
|
@ -101,44 +99,6 @@ export default function init(app: Koa = new Koa(), server?: http.Server): Koa {
|
||||||
app.use(mount("/emails", emails));
|
app.use(mount("/emails", emails));
|
||||||
}
|
}
|
||||||
|
|
||||||
// catch errors in one place, automatically set status and response headers
|
|
||||||
onerror(app);
|
|
||||||
|
|
||||||
app.on("error", (error, ctx) => {
|
|
||||||
// we don't need to report every time a request stops to the bug tracker
|
|
||||||
if (error.code === "EPIPE" || error.code === "ECONNRESET") {
|
|
||||||
console.warn("Connection error", { error });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.SENTRY_DSN) {
|
|
||||||
Sentry.withScope(function (scope) {
|
|
||||||
const requestId = ctx.headers["x-request-id"];
|
|
||||||
if (requestId) {
|
|
||||||
scope.setTag("request_id", requestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
const authType = ctx.state ? ctx.state.authType : undefined;
|
|
||||||
if (authType) {
|
|
||||||
scope.setTag("auth_type", authType);
|
|
||||||
}
|
|
||||||
|
|
||||||
const userId =
|
|
||||||
ctx.state && ctx.state.user ? ctx.state.user.id : undefined;
|
|
||||||
if (userId) {
|
|
||||||
scope.setUser({ id: userId });
|
|
||||||
}
|
|
||||||
|
|
||||||
scope.addEventProcessor(function (event) {
|
|
||||||
return Sentry.Handlers.parseRequest(event, ctx.request);
|
|
||||||
});
|
|
||||||
Sentry.captureException(error);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.use(mount("/auth", auth));
|
app.use(mount("/auth", auth));
|
||||||
app.use(mount("/api", api));
|
app.use(mount("/api", api));
|
||||||
|
|
||||||
|
|
|
@ -10,14 +10,14 @@ import policy from "../policies";
|
||||||
import { websocketsQueue } from "../queues";
|
import { websocketsQueue } from "../queues";
|
||||||
import WebsocketsProcessor from "../queues/processors/websockets";
|
import WebsocketsProcessor from "../queues/processors/websockets";
|
||||||
import { client, subscriber } from "../redis";
|
import { client, subscriber } from "../redis";
|
||||||
import Sentry from "../sentry";
|
|
||||||
import { getUserForJWT } from "../utils/jwt";
|
import { getUserForJWT } from "../utils/jwt";
|
||||||
import * as metrics from "../utils/metrics";
|
import * as metrics from "../utils/metrics";
|
||||||
|
import Sentry from "../utils/sentry";
|
||||||
|
|
||||||
const { can } = policy;
|
const { can } = policy;
|
||||||
const websockets = new WebsocketsProcessor();
|
|
||||||
|
|
||||||
export default function init(app: Koa, server: http.Server) {
|
export default function init(app: Koa, server: http.Server) {
|
||||||
|
// Websockets for events and non-collaborative documents
|
||||||
const io = IO(server, {
|
const io = IO(server, {
|
||||||
path: "/realtime",
|
path: "/realtime",
|
||||||
serveClient: false,
|
serveClient: false,
|
||||||
|
@ -226,6 +226,9 @@ export default function init(app: Koa, server: http.Server) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle events from event queue that should be sent to the clients down ws
|
||||||
|
const websockets = new WebsocketsProcessor();
|
||||||
|
|
||||||
websocketsQueue.process(async function websocketEventsProcessor(job) {
|
websocketsQueue.process(async function websocketEventsProcessor(job) {
|
||||||
const event = job.data;
|
const event = job.data;
|
||||||
websockets.on(event, io).catch((error) => {
|
websockets.on(event, io).catch((error) => {
|
||||||
|
|
|
@ -16,7 +16,7 @@ import Imports from "../queues/processors/imports";
|
||||||
import Notifications from "../queues/processors/notifications";
|
import Notifications from "../queues/processors/notifications";
|
||||||
import Revisions from "../queues/processors/revisions";
|
import Revisions from "../queues/processors/revisions";
|
||||||
import Slack from "../queues/processors/slack";
|
import Slack from "../queues/processors/slack";
|
||||||
import Sentry from "../sentry";
|
import Sentry from "../utils/sentry";
|
||||||
|
|
||||||
const log = debug("queue");
|
const log = debug("queue");
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the value of a command line argument
|
||||||
|
*
|
||||||
|
* @param {string} name The name of the argument
|
||||||
|
* @param {string} shortName The optioanl short name
|
||||||
|
*
|
||||||
|
* @returns {string} The value of the argument.
|
||||||
|
*/
|
||||||
|
export function getArg(name: string, shortName?: string) {
|
||||||
|
return process.argv
|
||||||
|
.slice(2)
|
||||||
|
.filter(
|
||||||
|
(arg) =>
|
||||||
|
arg.startsWith(`--${name}=`) ||
|
||||||
|
(shortName && arg.startsWith(`-${shortName}=`))
|
||||||
|
)
|
||||||
|
.map((arg) => arg.split("=")[1])
|
||||||
|
.join(",");
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
// @flow
|
||||||
|
import { darken } from "polished";
|
||||||
|
import theme from "../../shared/theme";
|
||||||
|
|
||||||
|
export const palette = [
|
||||||
|
theme.brand.red,
|
||||||
|
theme.brand.blue,
|
||||||
|
theme.brand.purple,
|
||||||
|
theme.brand.pink,
|
||||||
|
theme.brand.marine,
|
||||||
|
theme.brand.green,
|
||||||
|
theme.brand.yellow,
|
||||||
|
darken(0.2, theme.brand.red),
|
||||||
|
darken(0.2, theme.brand.blue),
|
||||||
|
darken(0.2, theme.brand.purple),
|
||||||
|
darken(0.2, theme.brand.pink),
|
||||||
|
darken(0.2, theme.brand.marine),
|
||||||
|
darken(0.2, theme.brand.green),
|
||||||
|
darken(0.2, theme.brand.yellow),
|
||||||
|
];
|
|
@ -3,8 +3,8 @@ import Queue from "bull";
|
||||||
import Redis from "ioredis";
|
import Redis from "ioredis";
|
||||||
import { snakeCase } from "lodash";
|
import { snakeCase } from "lodash";
|
||||||
import { client, subscriber } from "../redis";
|
import { client, subscriber } from "../redis";
|
||||||
import Sentry from "../sentry";
|
|
||||||
import * as metrics from "../utils/metrics";
|
import * as metrics from "../utils/metrics";
|
||||||
|
import Sentry from "./sentry";
|
||||||
|
|
||||||
export function createQueue(name: string) {
|
export function createQueue(name: string) {
|
||||||
const prefix = `queue.${snakeCase(name)}`;
|
const prefix = `queue.${snakeCase(name)}`;
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
// @flow
|
||||||
|
import * as Sentry from "@sentry/node";
|
||||||
|
import env from "../env";
|
||||||
|
import type { ContextWithState } from "../types";
|
||||||
|
|
||||||
|
if (env.SENTRY_DSN) {
|
||||||
|
Sentry.init({
|
||||||
|
dsn: env.SENTRY_DSN,
|
||||||
|
environment: env.ENVIRONMENT,
|
||||||
|
release: env.RELEASE,
|
||||||
|
maxBreadcrumbs: 0,
|
||||||
|
ignoreErrors: [
|
||||||
|
// emitted by Koa when bots attempt to snoop on paths such as wp-admin
|
||||||
|
// or the user client submits a bad request. These are expected in normal
|
||||||
|
// running of the application and don't need to be reported.
|
||||||
|
"BadRequestError",
|
||||||
|
"UnauthorizedError",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function requestErrorHandler(error: any, ctx: ContextWithState) {
|
||||||
|
// we don't need to report every time a request stops to the bug tracker
|
||||||
|
if (error.code === "EPIPE" || error.code === "ECONNRESET") {
|
||||||
|
console.warn("Connection error", { error });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.SENTRY_DSN) {
|
||||||
|
Sentry.withScope(function (scope) {
|
||||||
|
const requestId = ctx.headers["x-request-id"];
|
||||||
|
if (requestId) {
|
||||||
|
scope.setTag("request_id", requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const authType = ctx.state ? ctx.state.authType : undefined;
|
||||||
|
if (authType) {
|
||||||
|
scope.setTag("auth_type", authType);
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId =
|
||||||
|
ctx.state && ctx.state.user ? ctx.state.user.id : undefined;
|
||||||
|
if (userId) {
|
||||||
|
scope.setUser({ id: userId });
|
||||||
|
}
|
||||||
|
|
||||||
|
scope.addEventProcessor(function (event) {
|
||||||
|
return Sentry.Handlers.parseRequest(event, ctx.request);
|
||||||
|
});
|
||||||
|
Sentry.captureException(error);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Sentry;
|
|
@ -1,7 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import querystring from "querystring";
|
import querystring from "querystring";
|
||||||
import fetch from "fetch-with-proxy";
|
import fetch from "fetch-with-proxy";
|
||||||
import { InvalidRequestError } from "./errors";
|
import { InvalidRequestError } from "../errors";
|
||||||
|
|
||||||
const SLACK_API_URL = "https://slack.com/api";
|
const SLACK_API_URL = "https://slack.com/api";
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import Image from "components/Image";
|
|
||||||
import Frame from "./components/Frame";
|
import Frame from "./components/Frame";
|
||||||
|
import Image from "./components/Image";
|
||||||
|
|
||||||
const URL_REGEX = new RegExp("^https://viewer.diagrams.net/.*(title=\\w+)?");
|
const URL_REGEX = new RegExp("^https://viewer.diagrams.net/.*(title=\\w+)?");
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import Image from "components/Image";
|
|
||||||
import Frame from "./components/Frame";
|
import Frame from "./components/Frame";
|
||||||
|
import Image from "./components/Image";
|
||||||
|
|
||||||
const URL_REGEX = new RegExp(
|
const URL_REGEX = new RegExp(
|
||||||
"^https?://datastudio.google.com/(embed|u/0)/reporting/(.*)/page/(.*)(/edit)?$"
|
"^https?://datastudio.google.com/(embed|u/0)/reporting/(.*)/page/(.*)(/edit)?$"
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue