From 801f6681ba36c18cdb899a55b071f0ac33890361 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Fri, 10 Sep 2021 22:46:57 -0700 Subject: [PATCH] Collaborative editing (#1660) --- .env.sample | 4 + .flowconfig | 5 + Makefile | 2 +- Procfile | 4 +- README.md | 47 +- app/components/ConnectionStatus.js | 59 ++ app/components/Editor.js | 49 +- app/components/Header.js | 4 +- app/components/PageTitle.js | 2 +- app/components/PlaceholderDocument.js | 47 +- .../Sidebar/components/TeamButton.js | 3 +- app/hooks/useCurrentToken.js | 9 + app/menus/AccountMenu.js | 37 +- app/menus/RevisionMenu.js | 8 +- app/menus/TemplatesMenu.js | 5 +- app/models/Document.js | 46 +- app/models/Team.js | 1 + app/models/User.js | 1 + app/multiplayer/MultiplayerExtension.js | 57 ++ app/routes/authenticated.js | 12 +- app/routes/index.js | 10 +- app/scenes/Document/KeyedDocument.js | 25 - app/scenes/Document/Shared.js | 64 ++ app/scenes/Document/components/DataLoader.js | 41 +- app/scenes/Document/components/Document.js | 98 +- app/scenes/Document/components/Editor.js | 6 +- app/scenes/Document/components/Header.js | 7 +- .../components/KeyboardShortcutsButton.js | 7 +- .../Document/components/MultiplayerEditor.js | 144 +++ app/scenes/Document/index.js | 60 +- app/stores/DocumentsStore.js | 2 +- app/stores/UiStore.js | 8 + app/utils/developer.js | 11 + app/utils/urls.js | 9 - ARCHITECTURE.md => docs/ARCHITECTURE.md | 0 CODE_OF_CONDUCT.md => docs/CODE_OF_CONDUCT.md | 0 SECURITY.md => docs/SECURITY.md | 0 docs/SERVICES.md | 45 + TRANSLATION.md => docs/TRANSLATION.md | 0 flow-typed/npm/lib0_vx.x.x.js | 377 +++++++ flow-typed/npm/lib0_vx.x.x.js~4256e7ec (flow) | 377 +++++++ flow-typed/npm/y-indexeddb_vx.x.x.js | 39 + flow-typed/npm/y-prosemirror_vx.x.x.js | 67 ++ flow-typed/npm/y-protocols_vx.x.x.js | 67 ++ flow-typed/npm/yjs_vx.x.x.js | 430 ++++++++ package.json | 23 +- server/collaboration/Procfile | 1 + server/collaboration/authentication.js | 46 + server/collaboration/persistence.js | 71 ++ server/collaboration/utils/markdownToYDoc.js | 42 + server/commands/documentUpdater.js | 69 ++ server/index.js | 39 +- .../20210730044248-create-realtime.js | 17 + server/models/Document.js | 1 + server/models/Team.js | 5 + server/models/User.js | 6 + .../__snapshots__/user.test.js.snap | 2 + server/presenters/env.js | 7 +- server/presenters/team.js | 2 + server/presenters/user.js | 2 + server/queues/processors/backlinks.js | 14 +- server/redis.js | 11 +- .../api/__snapshots__/users.test.js.snap | 6 + server/routes/api/hooks.js | 2 +- server/routes/api/hooks.test.js | 4 +- server/routes/auth/providers/slack.js | 2 +- server/sentry.js | 21 - server/services/admin.js | 28 + server/services/collaboration.js | 37 + server/services/index.js | 4 +- server/services/web.js | 40 - server/services/websockets.js | 7 +- server/services/worker.js | 2 +- server/utils/args.js | 21 + server/utils/color.js | 20 + server/utils/queue.js | 2 +- server/utils/sentry.js | 57 ++ server/{ => utils}/slack.js | 2 +- {app => shared}/embeds/Abstract.js | 0 {app => shared}/embeds/Abstract.test.js | 0 {app => shared}/embeds/Airtable.js | 0 {app => shared}/embeds/Airtable.test.js | 0 {app => shared}/embeds/Cawemo.js | 0 {app => shared}/embeds/Cawemo.test.js | 0 {app => shared}/embeds/ClickUp.js | 0 {app => shared}/embeds/ClickUp.test.js | 0 {app => shared}/embeds/Codepen.js | 0 {app => shared}/embeds/Codepen.test.js | 0 {app => shared}/embeds/Descript.js | 0 {app => shared}/embeds/Diagrams.js | 2 +- {app => shared}/embeds/Diagrams.test.js | 0 {app => shared}/embeds/Figma.js | 0 {app => shared}/embeds/Figma.test.js | 0 {app => shared}/embeds/Framer.js | 0 {app => shared}/embeds/Framer.test.js | 0 {app => shared}/embeds/Gist.js | 0 {app => shared}/embeds/Gist.test.js | 0 {app => shared}/embeds/GoogleCalendar.js | 0 {app => shared}/embeds/GoogleCalendar.test.js | 0 {app => shared}/embeds/GoogleDataStudio.js | 2 +- .../embeds/GoogleDataStudio.test.js | 0 {app => shared}/embeds/GoogleDocs.js | 2 +- {app => shared}/embeds/GoogleDocs.test.js | 0 {app => shared}/embeds/GoogleDrawings.js | 2 +- {app => shared}/embeds/GoogleDrawings.test.js | 0 {app => shared}/embeds/GoogleDrive.js | 2 +- {app => shared}/embeds/GoogleDrive.test.js | 0 {app => shared}/embeds/GoogleSheets.js | 2 +- {app => shared}/embeds/GoogleSheets.test.js | 0 {app => shared}/embeds/GoogleSlides.js | 2 +- {app => shared}/embeds/GoogleSlides.test.js | 0 {app => shared}/embeds/InVision.js | 0 {app => shared}/embeds/InVision.test.js | 0 {app => shared}/embeds/Loom.js | 0 {app => shared}/embeds/Loom.test.js | 0 {app => shared}/embeds/Lucidchart.js | 0 {app => shared}/embeds/Lucidchart.test.js | 0 {app => shared}/embeds/Marvel.js | 0 {app => shared}/embeds/Marvel.test.js | 0 {app => shared}/embeds/Mindmeister.js | 0 {app => shared}/embeds/Mindmeister.test.js | 0 {app => shared}/embeds/Miro.js | 0 {app => shared}/embeds/Miro.test.js | 0 {app => shared}/embeds/ModeAnalytics.js | 0 {app => shared}/embeds/ModeAnalytics.test.js | 0 {app => shared}/embeds/Prezi.js | 0 {app => shared}/embeds/Prezi.test.js | 0 {app => shared}/embeds/Spotify.js | 0 {app => shared}/embeds/Spotify.test.js | 0 {app => shared}/embeds/Trello.js | 0 {app => shared}/embeds/Typeform.js | 0 {app => shared}/embeds/Typeform.test.js | 0 {app => shared}/embeds/Vimeo.js | 0 {app => shared}/embeds/Vimeo.test.js | 0 {app => shared}/embeds/YouTube.js | 0 {app => shared}/embeds/YouTube.test.js | 0 {app => shared}/embeds/components/Frame.js | 5 +- {app => shared/embeds}/components/Image.js | 2 +- {app => shared}/embeds/index.js | 2 +- shared/i18n/locales/en_US/translation.json | 4 + shared/theme.js | 1 + shared/utils/domains.js | 2 + shared/utils/urls.js | 6 + yarn.lock | 917 +++++++++++++++++- 144 files changed, 3552 insertions(+), 310 deletions(-) create mode 100644 app/components/ConnectionStatus.js create mode 100644 app/hooks/useCurrentToken.js create mode 100644 app/multiplayer/MultiplayerExtension.js delete mode 100644 app/scenes/Document/KeyedDocument.js create mode 100644 app/scenes/Document/Shared.js create mode 100644 app/scenes/Document/components/MultiplayerEditor.js create mode 100644 app/utils/developer.js rename ARCHITECTURE.md => docs/ARCHITECTURE.md (100%) rename CODE_OF_CONDUCT.md => docs/CODE_OF_CONDUCT.md (100%) rename SECURITY.md => docs/SECURITY.md (100%) create mode 100644 docs/SERVICES.md rename TRANSLATION.md => docs/TRANSLATION.md (100%) create mode 100644 flow-typed/npm/lib0_vx.x.x.js create mode 100644 flow-typed/npm/lib0_vx.x.x.js~4256e7ec (flow) create mode 100644 flow-typed/npm/y-indexeddb_vx.x.x.js create mode 100644 flow-typed/npm/y-prosemirror_vx.x.x.js create mode 100644 flow-typed/npm/y-protocols_vx.x.x.js create mode 100644 flow-typed/npm/yjs_vx.x.x.js create mode 100644 server/collaboration/Procfile create mode 100644 server/collaboration/authentication.js create mode 100644 server/collaboration/persistence.js create mode 100644 server/collaboration/utils/markdownToYDoc.js create mode 100644 server/commands/documentUpdater.js create mode 100644 server/migrations/20210730044248-create-realtime.js delete mode 100644 server/sentry.js create mode 100644 server/services/admin.js create mode 100644 server/services/collaboration.js create mode 100644 server/utils/args.js create mode 100644 server/utils/color.js create mode 100644 server/utils/sentry.js rename server/{ => utils}/slack.js (96%) rename {app => shared}/embeds/Abstract.js (100%) rename {app => shared}/embeds/Abstract.test.js (100%) rename {app => shared}/embeds/Airtable.js (100%) rename {app => shared}/embeds/Airtable.test.js (100%) rename {app => shared}/embeds/Cawemo.js (100%) rename {app => shared}/embeds/Cawemo.test.js (100%) rename {app => shared}/embeds/ClickUp.js (100%) rename {app => shared}/embeds/ClickUp.test.js (100%) rename {app => shared}/embeds/Codepen.js (100%) rename {app => shared}/embeds/Codepen.test.js (100%) rename {app => shared}/embeds/Descript.js (100%) rename {app => shared}/embeds/Diagrams.js (96%) rename {app => shared}/embeds/Diagrams.test.js (100%) rename {app => shared}/embeds/Figma.js (100%) rename {app => shared}/embeds/Figma.test.js (100%) rename {app => shared}/embeds/Framer.js (100%) rename {app => shared}/embeds/Framer.test.js (100%) rename {app => shared}/embeds/Gist.js (100%) rename {app => shared}/embeds/Gist.test.js (100%) rename {app => shared}/embeds/GoogleCalendar.js (100%) rename {app => shared}/embeds/GoogleCalendar.test.js (100%) rename {app => shared}/embeds/GoogleDataStudio.js (95%) rename {app => shared}/embeds/GoogleDataStudio.test.js (100%) rename {app => shared}/embeds/GoogleDocs.js (94%) rename {app => shared}/embeds/GoogleDocs.test.js (100%) rename {app => shared}/embeds/GoogleDrawings.js (95%) rename {app => shared}/embeds/GoogleDrawings.test.js (100%) rename {app => shared}/embeds/GoogleDrive.js (94%) rename {app => shared}/embeds/GoogleDrive.test.js (100%) rename {app => shared}/embeds/GoogleSheets.js (95%) rename {app => shared}/embeds/GoogleSheets.test.js (100%) rename {app => shared}/embeds/GoogleSlides.js (95%) rename {app => shared}/embeds/GoogleSlides.test.js (100%) rename {app => shared}/embeds/InVision.js (100%) rename {app => shared}/embeds/InVision.test.js (100%) rename {app => shared}/embeds/Loom.js (100%) rename {app => shared}/embeds/Loom.test.js (100%) rename {app => shared}/embeds/Lucidchart.js (100%) rename {app => shared}/embeds/Lucidchart.test.js (100%) rename {app => shared}/embeds/Marvel.js (100%) rename {app => shared}/embeds/Marvel.test.js (100%) rename {app => shared}/embeds/Mindmeister.js (100%) rename {app => shared}/embeds/Mindmeister.test.js (100%) rename {app => shared}/embeds/Miro.js (100%) rename {app => shared}/embeds/Miro.test.js (100%) rename {app => shared}/embeds/ModeAnalytics.js (100%) rename {app => shared}/embeds/ModeAnalytics.test.js (100%) rename {app => shared}/embeds/Prezi.js (100%) rename {app => shared}/embeds/Prezi.test.js (100%) rename {app => shared}/embeds/Spotify.js (100%) rename {app => shared}/embeds/Spotify.test.js (100%) rename {app => shared}/embeds/Trello.js (100%) rename {app => shared}/embeds/Typeform.js (100%) rename {app => shared}/embeds/Typeform.test.js (100%) rename {app => shared}/embeds/Vimeo.js (100%) rename {app => shared}/embeds/Vimeo.test.js (100%) rename {app => shared}/embeds/YouTube.js (100%) rename {app => shared}/embeds/YouTube.test.js (100%) rename {app => shared}/embeds/components/Frame.js (98%) rename {app => shared/embeds}/components/Image.js (85%) rename {app => shared}/embeds/index.js (99%) create mode 100644 shared/utils/urls.js diff --git a/.env.sample b/.env.sample index 98c24159..640e03fc 100644 --- a/.env.sample +++ b/.env.sample @@ -29,6 +29,10 @@ REDIS_URL=redis://localhost:6479 URL=http://localhost: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 # 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 diff --git a/.flowconfig b/.flowconfig index 0a37806b..7e630b56 100644 --- a/.flowconfig +++ b/.flowconfig @@ -11,6 +11,11 @@ .*/node_modules/react-side-effect/.* .*/node_modules/fbjs/.* .*/node_modules/config-chain/.* +.*/node_modules/yjs/.* +.*/node_modules/y-prosemirror/.* +.*/node_modules/y-protocols/.* +.*/node_modules/y-indexeddb/.* +.*/node_modules/lib0/.* .*/server/scripts/.* *.test.js diff --git a/Makefile b/Makefile index a10b9261..dc597047 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ up: docker-compose up -d redis postgres s3 yarn install --pure-lockfile yarn sequelize db:migrate - yarn dev + yarn dev:watch build: docker-compose build --pull outline diff --git a/Procfile b/Procfile index 0795ef08..0de779fb 100644 --- a/Procfile +++ b/Procfile @@ -1,2 +1,2 @@ -web: node ./build/server/index.js --services=web,websockets -worker: node ./build/server/index.js --services=worker \ No newline at end of file +web: yarn start --services=web,websockets +worker: yarn start --services=worker diff --git a/README.md b/README.md index d9b0b8fe..9a9b2112 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,3 @@ - -

@@ -30,7 +28,6 @@ Outline requires the following dependencies: - AWS S3 bucket or compatible API for file storage - Slack or Google developer application for authentication - ## Self-Hosted Production ### 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: `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: `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 -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` + 1. Start the container: `docker run outlinewiki/outline` + 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 @@ -79,29 +80,27 @@ If you're running Outline by cloning this repository, run the following command yarn run upgrade ``` - ## Local Development 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. [Docker for Desktop](https://www.docker.com) - 1. [Node.js](https://nodejs.org/) (v12 LTS preferred) - 1. [Yarn](https://yarnpkg.com) + 1. [Docker for Desktop](https://www.docker.com) + 1. [Node.js](https://nodejs.org/) (v12 LTS preferred) + 1. [Yarn](https://yarnpkg.com) 1. Clone this repo 1. Register a Slack app at https://api.slack.com/apps 1. Copy the file `.env.sample` to `.env` 1. Fill out the following fields: - 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_SECRET` (this is called "Client Secret" in Slack admin) -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. 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. `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_SECRET` (this is called "Client Secret" in Slack admin) +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. 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. Run `make up`. This will download dependencies, build and launch a development version of Outline - # Contributing 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: -* [Translation](TRANSLATION.md) into other languages -* Issues with [`good first issue`](https://github.com/outline/outline/labels/good%20first%20issue) label -* Performance improvements, both on server and frontend -* Developer happiness and documentation -* Bugs and other issues listed on GitHub - +- [Translation](docs/TRANSLATION.md) into other languages +- Issues with [`good first issue`](https://github.com/outline/outline/labels/good%20first%20issue) label +- Performance improvements, both on server and frontend +- Developer happiness and documentation +- Bugs and other issues listed on GitHub ## Architecture 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 @@ -145,7 +142,7 @@ make test 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. ```shell diff --git a/app/components/ConnectionStatus.js b/app/components/ConnectionStatus.js new file mode 100644 index 00000000..99fbf05a --- /dev/null +++ b/app/components/ConnectionStatus.js @@ -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" ? ( + + {t("Server connection lost")} +
+ {t("Edits you make will sync once you’re online")} + + } + placement="bottom" + > + +
+ ) : 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); diff --git a/app/components/Editor.js b/app/components/Editor.js index 562f3c80..eb948094 100644 --- a/app/components/Editor.js +++ b/app/components/Editor.js @@ -3,12 +3,13 @@ import { lighten } from "polished"; import * as React from "react"; import { useTranslation } from "react-i18next"; import { withRouter, type RouterHistory } from "react-router-dom"; +import { Extension } from "rich-markdown-editor"; import styled, { withTheme } from "styled-components"; +import embeds from "shared/embeds"; import { light } from "shared/theme"; import UiStore from "stores/UiStore"; import ErrorBoundary from "components/ErrorBoundary"; import Tooltip from "components/Tooltip"; -import embeds from "../embeds"; import useMediaQuery from "hooks/useMediaQuery"; import useToasts from "hooks/useToasts"; import { type Theme } from "types"; @@ -30,6 +31,8 @@ export type Props = {| grow?: boolean, disableEmbeds?: boolean, ui?: UiStore, + style?: Object, + extensions?: Extension[], shareId?: ?string, autoFocus?: 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 }) => ( diff --git a/app/components/Header.js b/app/components/Header.js index 828fb431..5fcb6747 100644 --- a/app/components/Header.js +++ b/app/components/Header.js @@ -36,7 +36,7 @@ function Header({ breadcrumb, title, actions }: Props) { }, []); return ( - + {breadcrumb ? {breadcrumb} : null} {isScrolled ? ( @@ -95,7 +95,7 @@ const Wrapper = styled(Flex)` } ${breakpoint("tablet")` - padding: ${(props) => (props.isCompact ? "12px" : `24px 24px 0`)}; + padding: 16px 16px 0; justify-content: "center"; `}; `; diff --git a/app/components/PageTitle.js b/app/components/PageTitle.js index 435d1e5d..b33a0f1c 100644 --- a/app/components/PageTitle.js +++ b/app/components/PageTitle.js @@ -2,8 +2,8 @@ import { observer } from "mobx-react"; import * as React from "react"; import { Helmet } from "react-helmet"; +import { cdnPath } from "../../shared/utils/urls"; import useStores from "hooks/useStores"; -import { cdnPath } from "utils/urls"; type Props = {| title: string, diff --git a/app/components/PlaceholderDocument.js b/app/components/PlaceholderDocument.js index d3f93d07..a99b76d5 100644 --- a/app/components/PlaceholderDocument.js +++ b/app/components/PlaceholderDocument.js @@ -6,18 +6,45 @@ import Fade from "components/Fade"; import Flex from "components/Flex"; 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 ( - <DelayedMount> + <DelayedMount delay={delay}> <Wrapper> - <Flex column auto {...props}> - <PlaceholderText height={34} maxWidth={70} /> - <PlaceholderText delay={0.2} maxWidth={40} /> - <br /> - <PlaceholderText delay={0.2} /> - <PlaceholderText delay={0.4} /> - <PlaceholderText delay={0.6} /> - </Flex> + <Fade> + <Flex column auto> + <PlaceholderText height={34} maxWidth={70} /> + <PlaceholderText delay={0.2} maxWidth={40} /> + <br /> + + {content} + </Flex> + </Fade> </Wrapper> </DelayedMount> ); diff --git a/app/components/Sidebar/components/TeamButton.js b/app/components/Sidebar/components/TeamButton.js index 04738a47..96418763 100644 --- a/app/components/Sidebar/components/TeamButton.js +++ b/app/components/Sidebar/components/TeamButton.js @@ -1,4 +1,5 @@ // @flow +import { observer } from "mobx-react"; import { ExpandedIcon } from "outline-icons"; import * as React from "react"; import styled from "styled-components"; @@ -84,4 +85,4 @@ const Header = styled.button` } `; -export default TeamButton; +export default observer(TeamButton); diff --git a/app/hooks/useCurrentToken.js b/app/hooks/useCurrentToken.js new file mode 100644 index 00000000..bdcac380 --- /dev/null +++ b/app/hooks/useCurrentToken.js @@ -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; +} diff --git a/app/menus/AccountMenu.js b/app/menus/AccountMenu.js index 59f59e42..a6dd053c 100644 --- a/app/menus/AccountMenu.js +++ b/app/menus/AccountMenu.js @@ -1,6 +1,6 @@ // @flow import { observer } from "mobx-react"; -import { MoonIcon, SunIcon } from "outline-icons"; +import { MoonIcon, SunIcon, TrashIcon } from "outline-icons"; import * as React from "react"; import { useTranslation } from "react-i18next"; import { MenuButton, useMenuState } from "reakit/Menu"; @@ -16,11 +16,14 @@ import KeyboardShortcuts from "scenes/KeyboardShortcuts"; import ContextMenu from "components/ContextMenu"; import Template from "components/ContextMenu/Template"; import Guide from "components/Guide"; +import env from "env"; import useBoolean from "hooks/useBoolean"; import useCurrentTeam from "hooks/useCurrentTeam"; import usePrevious from "hooks/usePrevious"; import useSessions from "hooks/useSessions"; import useStores from "hooks/useStores"; +import useToasts from "hooks/useToasts"; +import { deleteAllDatabases } from "utils/developer"; type Props = {| children: (props: any) => React.Node, @@ -33,11 +36,13 @@ function AccountMenu(props: Props) { placement: "bottom-start", modal: true, }); + const { showToast } = useToasts(); const { auth, ui } = useStores(); const { theme, resolvedTheme } = ui; const team = useCurrentTeam(); const previousTheme = usePrevious(theme); const { t } = useTranslation(); + const [includeAlt, setIncludeAlt] = React.useState(false); const [ keyboardShortcutsOpen, handleKeyboardShortcutsOpen, @@ -50,6 +55,16 @@ function AccountMenu(props: Props) { } }, [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 otherSessions = sessions.filter( (session) => session.teamId !== team.id && session.url !== team.url @@ -83,6 +98,20 @@ function AccountMenu(props: Props) { title: t("Report a bug"), href: githubIssuesUrl(), }, + ...(includeAlt || env.ENVIRONMENT === "development" + ? [ + { + title: t("Development"), + items: [ + { + title: "Delete IndexedDB cache", + icon: <TrashIcon />, + onClick: handleDeleteAllDatabases, + }, + ], + }, + ] + : []), { title: t("Appearance"), icon: resolvedTheme === "light" ? <SunIcon /> : <MoonIcon />, @@ -130,7 +159,9 @@ function AccountMenu(props: Props) { team.url, sessions, handleKeyboardShortcutsOpen, + handleDeleteAllDatabases, resolvedTheme, + includeAlt, theme, t, ui, @@ -145,7 +176,9 @@ function AccountMenu(props: Props) { > <KeyboardShortcuts /> </Guide> - <MenuButton {...menu}>{props.children}</MenuButton> + <MenuButton {...menu} onClick={handleOpenMenu}> + {props.children} + </MenuButton> <ContextMenu {...menu} aria-label={t("Account")}> <Template {...menu} items={items} /> </ContextMenu> diff --git a/app/menus/RevisionMenu.js b/app/menus/RevisionMenu.js index 5e680321..ea4c47f4 100644 --- a/app/menus/RevisionMenu.js +++ b/app/menus/RevisionMenu.js @@ -12,6 +12,7 @@ import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton"; import Separator from "components/ContextMenu/Separator"; import CopyToClipboard from "components/CopyToClipboard"; import MenuIconWrapper from "components/MenuIconWrapper"; +import useCurrentTeam from "hooks/useCurrentTeam"; import useToasts from "hooks/useToasts"; import { documentHistoryUrl } from "utils/routeHelpers"; @@ -23,6 +24,7 @@ type Props = {| function RevisionMenu({ document, revisionId, className }: Props) { const { showToast } = useToasts(); + const team = useCurrentTeam(); const menu = useMenuState({ modal: true }); const { t } = useTranslation(); const history = useHistory(); @@ -55,7 +57,11 @@ function RevisionMenu({ document, revisionId, className }: Props) { {...menu} /> <ContextMenu {...menu} aria-label={t("Revision options")}> - <MenuItem {...menu} onClick={handleRestore}> + <MenuItem + {...menu} + onClick={handleRestore} + disabled={team.collaborativeEditing} + > <MenuIconWrapper> <RestoreIcon /> </MenuIconWrapper> diff --git a/app/menus/TemplatesMenu.js b/app/menus/TemplatesMenu.js index 311530fc..a3f2cf92 100644 --- a/app/menus/TemplatesMenu.js +++ b/app/menus/TemplatesMenu.js @@ -14,9 +14,10 @@ import useStores from "hooks/useStores"; type Props = {| document: Document, + onSelectTemplate: (template: Document) => void, |}; -function TemplatesMenu({ document }: Props) { +function TemplatesMenu({ onSelectTemplate, document }: Props) { const menu = useMenuState({ modal: true }); const { documents } = useStores(); const { t } = useTranslation(); @@ -36,7 +37,7 @@ function TemplatesMenu({ document }: Props) { const renderTemplate = (template) => ( <MenuItem key={template.id} - onClick={() => document.updateFromTemplate(template)} + onClick={() => onSelectTemplate(template)} icon={<DocumentIcon />} {...menu} > diff --git a/app/models/Document.js b/app/models/Document.js index 93286dca..262c24cf 100644 --- a/app/models/Document.js +++ b/app/models/Document.js @@ -10,17 +10,16 @@ import BaseModel from "models/BaseModel"; import User from "models/User"; import View from "./View"; -type SaveOptions = { +type SaveOptions = {| publish?: boolean, done?: boolean, autosave?: boolean, lastRevision?: number, -}; +|}; export default class Document extends BaseModel { @observable isSaving: boolean = false; @observable embedsDisabled: boolean = false; - @observable injectTemplate: boolean = false; @observable lastViewedAt: ?string; store: DocumentsStore; @@ -254,15 +253,28 @@ export default class Document extends BaseModel { }; @action - updateFromTemplate = async (template: Document) => { - this.templateId = template.id; - this.title = template.title; - this.text = template.text; - this.injectTemplate = true; + update = async (options: {| ...SaveOptions, title: string |}) => { + if (this.isSaving) return this; + this.isSaving = 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 - save = async (options: SaveOptions = {}) => { + save = async (options: ?SaveOptions) => { if (this.isSaving) return this; const isCreating = !this.id; @@ -275,22 +287,22 @@ export default class Document extends BaseModel { collectionId: this.collectionId, title: this.title, text: this.text, - publish: options.publish, - done: options.done, - autosave: options.autosave, + publish: options?.publish, + done: options?.done, + autosave: options?.autosave, }); } - if (options.lastRevision) { + if (options?.lastRevision) { return await this.store.update({ id: this.id, title: this.title, text: this.text, templateId: this.templateId, - lastRevision: options.lastRevision, - publish: options.publish, - done: options.done, - autosave: options.autosave, + lastRevision: options?.lastRevision, + publish: options?.publish, + done: options?.done, + autosave: options?.autosave, }); } diff --git a/app/models/Team.js b/app/models/Team.js index fbe18bf9..b0865b8e 100644 --- a/app/models/Team.js +++ b/app/models/Team.js @@ -7,6 +7,7 @@ class Team extends BaseModel { name: string; avatarUrl: string; sharing: boolean; + collaborativeEditing: boolean; documentEmbeds: boolean; guestSignin: boolean; subdomain: ?string; diff --git a/app/models/User.js b/app/models/User.js index 7fc03c5b..86c9a6c9 100644 --- a/app/models/User.js +++ b/app/models/User.js @@ -8,6 +8,7 @@ class User extends BaseModel { id: string; name: string; email: string; + color: string; isAdmin: boolean; isViewer: boolean; lastActiveAt: string; diff --git a/app/multiplayer/MultiplayerExtension.js b/app/multiplayer/MultiplayerExtension.js new file mode 100644 index 00000000..66a7f0ab --- /dev/null +++ b/app/multiplayer/MultiplayerExtension.js @@ -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, + }), + ]; + } +} diff --git a/app/routes/authenticated.js b/app/routes/authenticated.js index 7b9dd1bb..f6a141a1 100644 --- a/app/routes/authenticated.js +++ b/app/routes/authenticated.js @@ -21,10 +21,8 @@ import { matchDocumentSlug as slug } from "utils/routeHelpers"; const SettingsRoutes = React.lazy(() => import(/* webpackChunkName: "settings" */ "./settings") ); -const KeyedDocument = React.lazy(() => - import( - /* webpackChunkName: "keyed-document" */ "scenes/Document/KeyedDocument" - ) +const Document = React.lazy(() => + import(/* webpackChunkName: "document" */ "scenes/Document") ); const NotFound = () => <Search notFound />; const RedirectDocument = ({ match }: { match: Match }) => ( @@ -64,10 +62,10 @@ export default function AuthenticatedRoutes() { <Route exact path={`/doc/${slug}/history/:revisionId?`} - component={KeyedDocument} + component={Document} /> - <Route exact path={`/doc/${slug}/edit`} component={KeyedDocument} /> - <Route path={`/doc/${slug}`} component={KeyedDocument} /> + <Route exact path={`/doc/${slug}/edit`} component={Document} /> + <Route path={`/doc/${slug}`} component={Document} /> <Route exact path="/search" component={Search} /> <Route exact path="/search/:term" component={Search} /> <Route path="/404" component={Error404} /> diff --git a/app/routes/index.js b/app/routes/index.js index f3d93a35..5ef41d7d 100644 --- a/app/routes/index.js +++ b/app/routes/index.js @@ -12,10 +12,8 @@ const Authenticated = React.lazy(() => const AuthenticatedRoutes = React.lazy(() => import(/* webpackChunkName: "authenticated-routes" */ "./authenticated") ); -const KeyedDocument = React.lazy(() => - import( - /* webpackChunkName: "keyed-document" */ "scenes/Document/KeyedDocument" - ) +const SharedDocument = React.lazy(() => + import(/* webpackChunkName: "shared-document" */ "scenes/Document/Shared") ); const Login = React.lazy(() => import(/* webpackChunkName: "login" */ "scenes/Login") @@ -37,11 +35,11 @@ export default function Routes() { <Route exact path="/" component={Login} /> <Route exact path="/create" component={Login} /> <Route exact path="/logout" component={Logout} /> - <Route exact path="/share/:shareId" component={KeyedDocument} /> + <Route exact path="/share/:shareId" component={SharedDocument} /> <Route exact path={`/share/:shareId/doc/${slug}`} - component={KeyedDocument} + component={SharedDocument} /> <Authenticated> <AuthenticatedRoutes /> diff --git a/app/scenes/Document/KeyedDocument.js b/app/scenes/Document/KeyedDocument.js deleted file mode 100644 index aefd1a78..00000000 --- a/app/scenes/Document/KeyedDocument.js +++ /dev/null @@ -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); diff --git a/app/scenes/Document/Shared.js b/app/scenes/Document/Shared.js new file mode 100644 index 00000000..ae9be691 --- /dev/null +++ b/app/scenes/Document/Shared.js @@ -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 + /> + ); +} diff --git a/app/scenes/Document/components/DataLoader.js b/app/scenes/Document/components/DataLoader.js index df8fc3b5..522c3661 100644 --- a/app/scenes/Document/components/DataLoader.js +++ b/app/scenes/Document/components/DataLoader.js @@ -18,16 +18,15 @@ import Document from "models/Document"; import Revision from "models/Revision"; import Error404 from "scenes/Error404"; import ErrorOffline from "scenes/ErrorOffline"; -import DocumentComponent from "./Document"; import HideSidebar from "./HideSidebar"; import Loading from "./Loading"; -import SocketPresence from "./SocketPresence"; import { type LocationWithState, type NavigationNode } from "types"; import { NotFoundError, OfflineError } from "utils/errors"; import { matchDocumentEdit, updateDocumentUrl } from "utils/routeHelpers"; import { isInternalUrl } from "utils/urls"; type Props = {| match: Match, + auth: AuthStore, location: LocationWithState, shares: SharesStore, documents: DocumentsStore, @@ -36,6 +35,7 @@ type Props = {| auth: AuthStore, ui: UiStore, history: RouterHistory, + children: (any) => React.Node, |}; const sharedTreeCache = {}; @@ -223,7 +223,7 @@ class DataLoader extends React.Component<Props> { }; render() { - const { location, policies, ui } = this.props; + const { location, policies, auth, ui } = this.props; if (this.error) { return this.error instanceof OfflineError ? ( @@ -233,10 +233,11 @@ class DataLoader extends React.Component<Props> { ); } + const team = auth.team; const document = this.document; const revision = this.revision; - if (!document) { + if (!document || !team) { return ( <> <Loading location={location} /> @@ -247,20 +248,28 @@ class DataLoader extends React.Component<Props> { 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 ( - <SocketPresence documentId={document.id} isEditing={this.isEditing}> + <React.Fragment key={key}> {this.isEditing && <HideSidebar ui={ui} />} - <DocumentComponent - document={document} - revision={revision} - abilities={abilities} - location={location} - readOnly={!this.isEditing || !abilities.update || document.isArchived} - onSearchLink={this.onSearchLink} - onCreateLink={this.onCreateLink} - sharedTree={this.sharedTree} - /> - </SocketPresence> + {this.props.children({ + document, + revision, + abilities, + isEditing: this.isEditing, + readOnly: !this.isEditing || !abilities.update || document.isArchived, + onSearchLink: this.onSearchLink, + onCreateLink: this.onCreateLink, + sharedTree: this.sharedTree, + })} + </React.Fragment> ); } } diff --git a/app/scenes/Document/components/Document.js b/app/scenes/Document/components/Document.js index d825da7a..13628d2f 100644 --- a/app/scenes/Document/components/Document.js +++ b/app/scenes/Document/components/Document.js @@ -1,8 +1,9 @@ // @flow import { debounce } from "lodash"; -import { observable } from "mobx"; +import { action, observable } from "mobx"; import { observer, inject } from "mobx-react"; import { InputIcon } from "outline-icons"; +import { AllSelection } from "prosemirror-state"; import * as React from "react"; import { type TFunction, Trans, withTranslation } from "react-i18next"; import keydown from "react-keydown"; @@ -18,6 +19,7 @@ import Document from "models/Document"; import Revision from "models/Revision"; import DocumentMove from "scenes/DocumentMove"; import Branding from "components/Branding"; +import ConnectionStatus from "components/ConnectionStatus"; import ErrorBoundary from "components/ErrorBoundary"; import Flex from "components/Flex"; 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") goToMove(ev) { if (!this.props.readOnly) return; @@ -197,7 +215,7 @@ class DocumentScene extends React.Component<Props> { autosave?: boolean, } = {} ) => { - const { document } = this.props; + const { document, auth } = this.props; // prevent saves when we are already saving if (document.isSaving) return; @@ -219,18 +237,29 @@ class DocumentScene extends React.Component<Props> { document.title = title; document.text = text; + document.tasks = getTasks(document.text); let isNew = !document.id; this.isSaving = true; this.isPublishing = !!options.publish; - document.tasks = getTasks(document.text); - try { - const savedDocument = await document.save({ - ...options, - lastRevision: this.lastRevision, - }); + let savedDocument = document; + if (auth.team?.collaborativeEditing) { + // 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.lastRevision = savedDocument.revision; @@ -275,8 +304,21 @@ class DocumentScene extends React.Component<Props> { }; onChange = (getEditorText) => { + const { document, auth } = this.props; + 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, // in that case we don't delay in saving for a better user experience. if (this.props.readOnly) { @@ -314,7 +356,6 @@ class DocumentScene extends React.Component<Props> { const isShare = !!shareId; const value = revision ? revision.text : document.text; - const injectTemplate = document.injectTemplate; const disableEmbeds = (team && team.documentEmbeds === false) || document.embedsDisabled; @@ -323,6 +364,12 @@ class DocumentScene extends React.Component<Props> { : []; const showContents = ui.tocVisible && readOnly; + const collaborativeEditing = + team?.collaborativeEditing && + !document.isArchived && + !document.isDeleted && + !revision; + return ( <ErrorBoundary> <Background @@ -332,7 +379,7 @@ class DocumentScene extends React.Component<Props> { auto > <Route - path={`${match.url}/move`} + path={`${document.url}/move`} component={() => ( <Modal title={`Move ${document.noun}`} @@ -356,7 +403,11 @@ class DocumentScene extends React.Component<Props> { {!readOnly && ( <> <Prompt - when={this.isDirty && !this.isUploading} + when={ + this.isDirty && + !this.isUploading && + !team?.collaborativeEditing + } message={t( `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} sharedTree={this.props.sharedTree} goBack={this.goBack} + onSelectTemplate={this.onSelectTemplate} onSave={this.onSave} headings={headings} /> @@ -443,11 +495,12 @@ class DocumentScene extends React.Component<Props> { {showContents && <Contents headings={headings} />} <Editor id={document.id} + key={disableEmbeds ? "disabled" : "enabled"} innerRef={this.editor} + multiplayer={collaborativeEditing} shareId={shareId} isDraft={document.isDraft} template={document.isTemplate} - key={[injectTemplate, disableEmbeds].join("-")} title={revision ? revision.title : this.title} document={document} value={readOnly ? value : undefined} @@ -492,7 +545,12 @@ class DocumentScene extends React.Component<Props> { {isShare && !isCustomDomain() && ( <Branding href="//www.getoutline.com?ref=sharelink" /> )} - {!isShare && <KeyboardShortcutsButton />} + {!isShare && ( + <> + <KeyboardShortcutsButton /> + <ConnectionStatus /> + </> + )} </ErrorBoundary> ); } diff --git a/app/scenes/Document/components/Editor.js b/app/scenes/Document/components/Editor.js index df442c7e..9da6b2b3 100644 --- a/app/scenes/Document/components/Editor.js +++ b/app/scenes/Document/components/Editor.js @@ -17,6 +17,7 @@ import Editor, { type Props as EditorProps } from "components/Editor"; import Flex from "components/Flex"; import HoverPreview from "components/HoverPreview"; import Star, { AnimatedStar } from "components/Star"; +import MultiplayerEditor from "./MultiplayerEditor"; import { isModKey } from "utils/keyboard"; import { documentHistoryUrl } from "utils/routeHelpers"; @@ -27,6 +28,7 @@ type Props = {| document: Document, isDraft: boolean, shareId: ?string, + multiplayer?: boolean, onSave: ({ done?: boolean, autosave?: boolean, publish?: boolean }) => any, innerRef: { current: any }, children: React.Node, @@ -107,10 +109,12 @@ class DocumentEditor extends React.Component<Props> { innerRef, children, policies, + multiplayer, t, ...rest } = this.props; + const EditorComponent = multiplayer ? MultiplayerEditor : Editor; const can = policies.abilities(document.id); const { emoji } = parseTitle(title); const startsWithEmojiAndSpace = !!(emoji && title.startsWith(`${emoji} `)); @@ -162,7 +166,7 @@ class DocumentEditor extends React.Component<Props> { } /> )} - <Editor + <EditorComponent ref={innerRef} autoFocus={!!title && !this.props.defaultValue} placeholder={t("…the rest is up to you")} diff --git a/app/scenes/Document/components/Header.js b/app/scenes/Document/components/Header.js index 0804bf15..b766f864 100644 --- a/app/scenes/Document/components/Header.js +++ b/app/scenes/Document/components/Header.js @@ -41,6 +41,7 @@ type Props = {| isPublishing: boolean, publishingIsDisabled: boolean, savingIsDisabled: boolean, + onSelectTemplate: (template: Document) => void, onDiscard: () => void, onSave: ({ done?: boolean, @@ -61,6 +62,7 @@ function DocumentHeader({ savingIsDisabled, publishingIsDisabled, sharedTree, + onSelectTemplate, onSave, headings, }: Props) { @@ -167,7 +169,10 @@ function DocumentHeader({ /> {isEditing && !isTemplate && isNew && ( <Action> - <TemplatesMenu document={document} /> + <TemplatesMenu + document={document} + onSelectTemplate={onSelectTemplate} + /> </Action> )} {!isEditing && (!isMobile || !isTemplate) && ( diff --git a/app/scenes/Document/components/KeyboardShortcutsButton.js b/app/scenes/Document/components/KeyboardShortcutsButton.js index fe3bcb11..1fd92380 100644 --- a/app/scenes/Document/components/KeyboardShortcutsButton.js +++ b/app/scenes/Document/components/KeyboardShortcutsButton.js @@ -27,12 +27,7 @@ function KeyboardShortcutsButton() { > <KeyboardShortcuts /> </Guide> - <Tooltip - tooltip={t("Keyboard shortcuts")} - shortcut="?" - placement="left" - delay={500} - > + <Tooltip tooltip={t("Keyboard shortcuts")} shortcut="?" delay={500}> <Button onClick={handleOpenKeyboardShortcuts}> <KeyboardIcon /> </Button> diff --git a/app/scenes/Document/components/MultiplayerEditor.js b/app/scenes/Document/components/MultiplayerEditor.js new file mode 100644 index 00000000..94fc414f --- /dev/null +++ b/app/scenes/Document/components/MultiplayerEditor.js @@ -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 +); diff --git a/app/scenes/Document/index.js b/app/scenes/Document/index.js index 7af896b2..75edaaf4 100644 --- a/app/scenes/Document/index.js +++ b/app/scenes/Document/index.js @@ -1,3 +1,61 @@ // @flow +import * as React from "react"; +import { type Match } from "react-router-dom"; 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> + ); +} diff --git a/app/stores/DocumentsStore.js b/app/stores/DocumentsStore.js index a0aed5af..374fad76 100644 --- a/app/stores/DocumentsStore.js +++ b/app/stores/DocumentsStore.js @@ -603,7 +603,7 @@ export default class DocumentsStore extends BaseStore<Document> { async update(params: { id: string, title: string, - text: string, + text?: string, lastRevision: number, }) { const document = await super.update(params); diff --git a/app/stores/UiStore.js b/app/stores/UiStore.js index 1a0280f5..d44ac883 100644 --- a/app/stores/UiStore.js +++ b/app/stores/UiStore.js @@ -6,6 +6,8 @@ import Document from "models/Document"; const UI_STORE = "UI_STORE"; +type Status = "connecting" | "connected" | "disconnected" | void; + class UiStore { // has the user seen the prompt to change the UI language and actioned it @observable languagePromptDismissed: boolean; @@ -24,6 +26,7 @@ class UiStore { @observable sidebarWidth: number; @observable sidebarCollapsed: boolean = false; @observable sidebarIsResizing: boolean = false; + @observable multiplayerStatus: Status; constructor() { // Rehydrate @@ -93,6 +96,11 @@ class UiStore { } }; + @action + setMultiplayerStatus = (status: Status): void => { + this.multiplayerStatus = status; + }; + @action setSidebarResizing = (sidebarIsResizing: boolean): void => { this.sidebarIsResizing = sidebarIsResizing; diff --git a/app/utils/developer.js b/app/utils/developer.js new file mode 100644 index 00000000..537e9fff --- /dev/null +++ b/app/utils/developer.js @@ -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); + } + } +} diff --git a/app/utils/urls.js b/app/utils/urls.js index 6f38a6e7..7e19f3df 100644 --- a/app/utils/urls.js +++ b/app/utils/urls.js @@ -1,6 +1,5 @@ // @flow import { parseDomain } from "../../shared/utils/domains"; -import env from "env"; export function isInternalUrl(href: string) { if (href[0] === "/") return true; @@ -21,14 +20,6 @@ export function isInternalUrl(href: string) { 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) { return text ? decodeURIComponent(text.replace(/%(?![0-9][0-9a-fA-F]+)/g, "%25")) diff --git a/ARCHITECTURE.md b/docs/ARCHITECTURE.md similarity index 100% rename from ARCHITECTURE.md rename to docs/ARCHITECTURE.md diff --git a/CODE_OF_CONDUCT.md b/docs/CODE_OF_CONDUCT.md similarity index 100% rename from CODE_OF_CONDUCT.md rename to docs/CODE_OF_CONDUCT.md diff --git a/SECURITY.md b/docs/SECURITY.md similarity index 100% rename from SECURITY.md rename to docs/SECURITY.md diff --git a/docs/SERVICES.md b/docs/SERVICES.md new file mode 100644 index 00000000..67522d07 --- /dev/null +++ b/docs/SERVICES.md @@ -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 +``` diff --git a/TRANSLATION.md b/docs/TRANSLATION.md similarity index 100% rename from TRANSLATION.md rename to docs/TRANSLATION.md diff --git a/flow-typed/npm/lib0_vx.x.x.js b/flow-typed/npm/lib0_vx.x.x.js new file mode 100644 index 00000000..6c73c4e7 --- /dev/null +++ b/flow-typed/npm/lib0_vx.x.x.js @@ -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">; +} diff --git a/flow-typed/npm/lib0_vx.x.x.js~4256e7ec (flow) b/flow-typed/npm/lib0_vx.x.x.js~4256e7ec (flow) new file mode 100644 index 00000000..6c73c4e7 --- /dev/null +++ b/flow-typed/npm/lib0_vx.x.x.js~4256e7ec (flow) @@ -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">; +} diff --git a/flow-typed/npm/y-indexeddb_vx.x.x.js b/flow-typed/npm/y-indexeddb_vx.x.x.js new file mode 100644 index 00000000..03dd378c --- /dev/null +++ b/flow-typed/npm/y-indexeddb_vx.x.x.js @@ -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'>; +} diff --git a/flow-typed/npm/y-prosemirror_vx.x.x.js b/flow-typed/npm/y-prosemirror_vx.x.x.js new file mode 100644 index 00000000..d299fd87 --- /dev/null +++ b/flow-typed/npm/y-prosemirror_vx.x.x.js @@ -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'>; +} diff --git a/flow-typed/npm/y-protocols_vx.x.x.js b/flow-typed/npm/y-protocols_vx.x.x.js new file mode 100644 index 00000000..8ef44753 --- /dev/null +++ b/flow-typed/npm/y-protocols_vx.x.x.js @@ -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">; +} diff --git a/flow-typed/npm/yjs_vx.x.x.js b/flow-typed/npm/yjs_vx.x.x.js new file mode 100644 index 00000000..6f506208 --- /dev/null +++ b/flow-typed/npm/yjs_vx.x.x.js @@ -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'>; +} diff --git a/package.json b/package.json index 7b6d60c4..0260c0ab 100644 --- a/package.json +++ b/package.json @@ -6,11 +6,12 @@ "scripts": { "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: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": "yarn clean && yarn build:webpack && yarn build:i18n && yarn build:server", "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", "deploy": "git push heroku master", "prepare": "yarn yarn-deduplicate yarn.lock", @@ -45,6 +46,11 @@ "@babel/preset-env": "^7.11.0", "@babel/preset-flow": "^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/passport-azure-ad-oauth2": "^0.1.0", "@sentry/node": "^6.3.1", @@ -58,7 +64,7 @@ "babel-plugin-styled-components": "^1.11.1", "babel-plugin-transform-class-properties": "^6.24.1", "boundless-arrow-key-navigation": "^1.0.4", - "bull": "^3.5.2", + "bull": "^3.29.0", "cancan": "3.1.0", "chalk": "^4.1.0", "compressorjs": "^1.0.7", @@ -96,6 +102,7 @@ "koa-body": "^4.2.0", "koa-compress": "2.0.0", "koa-convert": "1.2.0", + "koa-easy-ws": "^1.3.0", "koa-helmet": "5.2.0", "koa-jwt": "^3.6.0", "koa-logger": "^3.2.1", @@ -111,7 +118,7 @@ "mobx-react": "^6.3.1", "natural-sort": "^1.0.0", "nodemailer": "^6.4.16", - "outline-icons": "^1.30.0", + "outline-icons": "^1.31.0", "oy-vey": "^0.10.0", "passport": "^0.4.1", "passport-google-oauth2": "^0.2.0", @@ -144,7 +151,7 @@ "react-window": "^1.8.6", "reakit": "^1.3.8", "regenerator-runtime": "^0.13.7", - "rich-markdown-editor": "^11.17.4", + "rich-markdown-editor": "^11.17.5", "semver": "^7.3.2", "sequelize": "^6.3.4", "sequelize-cli": "^6.2.0", @@ -167,7 +174,10 @@ "turndown": "^7.1.1", "utf8": "^3.0.0", "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": { "@babel/cli": "^7.10.5", @@ -177,6 +187,7 @@ "babel-jest": "^26.2.2", "babel-loader": "^8.1.0", "babel-plugin-transform-inline-environment-variables": "^0.4.3", + "concurrently": "^6.2.1", "enzyme": "^3.11.0", "enzyme-adapter-react-16": "^1.15.6", "eslint": "^7.6.0", diff --git a/server/collaboration/Procfile b/server/collaboration/Procfile new file mode 100644 index 00000000..913cf469 --- /dev/null +++ b/server/collaboration/Procfile @@ -0,0 +1 @@ +web: yarn start --services=collaboration diff --git a/server/collaboration/authentication.js b/server/collaboration/authentication.js new file mode 100644 index 00000000..b334971b --- /dev/null +++ b/server/collaboration/authentication.js @@ -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, + }; + } +} diff --git a/server/collaboration/persistence.js b/server/collaboration/persistence.js new file mode 100644 index 00000000..3d361f37 --- /dev/null +++ b/server/collaboration/persistence.js @@ -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, + } + ); +} diff --git a/server/collaboration/utils/markdownToYDoc.js b/server/collaboration/utils/markdownToYDoc.js new file mode 100644 index 00000000..ab9605cc --- /dev/null +++ b/server/collaboration/utils/markdownToYDoc.js @@ -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); +} diff --git a/server/commands/documentUpdater.js b/server/commands/documentUpdater.js new file mode 100644 index 00000000..f38bc0c5 --- /dev/null +++ b/server/commands/documentUpdater.js @@ -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, + }, + }); +} diff --git a/server/index.js b/server/index.js index f7a5b3eb..eeffabdc 100644 --- a/server/index.js +++ b/server/index.js @@ -8,30 +8,31 @@ import Koa from "koa"; import compress from "koa-compress"; import helmet from "koa-helmet"; import logger from "koa-logger"; +import onerror from "koa-onerror"; import Router from "koa-router"; import { uniq } from "lodash"; import stoppable from "stoppable"; import throng from "throng"; -import "./sentry"; import services from "./services"; +import { getArg } from "./utils/args"; +import { requestErrorHandler } from "./utils/sentry"; import { checkEnv, checkMigrations } from "./utils/startup"; import { checkUpdates } from "./utils/updates"; checkEnv(); 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 // for example: --services=web,worker -const normalizedServiceFlag = process.argv - .slice(2) - .filter((arg) => arg.startsWith("--services=")) - .map((arg) => arg.split("=")[1]) - .join(","); +const normalizedServiceFlag = getArg("services"); // The default is to run all services to make development and OSS installations // easier to deal with. Separate services are only needed at scale. const serviceNames = uniq( - (normalizedServiceFlag || env.SERVICES || "web,websockets,worker") + (normalizedServiceFlag || env.SERVICES || "websockets,worker,web") .split(",") .map((service) => service.trim()) ); @@ -48,10 +49,23 @@ async function start(id, disconnect) { app.use(compress()); 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 router.get("/_health", (ctx) => (ctx.body = "OK")); 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 for (const name of serviceNames) { 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`); }); - server.listen(env.PORT || "3000"); + server.listen(normalizedPortFlag || env.PORT || "3000"); process.once("SIGTERM", shutdown); process.once("SIGINT", shutdown); @@ -86,8 +100,13 @@ async function start(id, disconnect) { throng({ worker: start, - // The number of workers to run, defaults to the number of CPU's available - count: process.env.WEB_CONCURRENCY || undefined, + // The number of processes to run, defaults to the number of CPU's available + // 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") { diff --git a/server/migrations/20210730044248-create-realtime.js b/server/migrations/20210730044248-create-realtime.js new file mode 100644 index 00000000..6750914d --- /dev/null +++ b/server/migrations/20210730044248-create-realtime.js @@ -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"); + }, +}; diff --git a/server/models/Document.js b/server/models/Document.js index 52b9f9f3..06fe8fcd 100644 --- a/server/models/Document.js +++ b/server/models/Document.js @@ -81,6 +81,7 @@ const Document = sequelize.define( template: DataTypes.BOOLEAN, editorVersion: DataTypes.STRING, text: DataTypes.TEXT, + state: DataTypes.BLOB, isWelcome: { type: DataTypes.BOOLEAN, defaultValue: false }, revisionCount: { type: DataTypes.INTEGER, defaultValue: 0 }, archivedAt: DataTypes.DATE, diff --git a/server/models/Team.js b/server/models/Team.js index 6fcfb191..20f229e3 100644 --- a/server/models/Team.js +++ b/server/models/Team.js @@ -69,6 +69,11 @@ const Team = sequelize.define( allowNull: false, defaultValue: true, }, + collaborativeEditing: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false, + }, }, { paranoid: true, diff --git a/server/models/User.js b/server/models/User.js index af987c60..9d592072 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -7,6 +7,7 @@ import { languages } from "../../shared/i18n"; import { ValidationError } from "../errors"; import { DataTypes, sequelize, encryptedFields, Op } from "../sequelize"; import { DEFAULT_AVATAR_HOST } from "../utils/avatars"; +import { palette } from "../utils/color"; import { publicS3Endpoint, uploadToS3FromUrl } from "../utils/s3"; import { UserAuthentication, @@ -74,6 +75,11 @@ const User = sequelize.define( .digest("hex"); 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]; + }, }, } ); diff --git a/server/presenters/__snapshots__/user.test.js.snap b/server/presenters/__snapshots__/user.test.js.snap index 238edb8f..f080b50c 100644 --- a/server/presenters/__snapshots__/user.test.js.snap +++ b/server/presenters/__snapshots__/user.test.js.snap @@ -3,6 +3,7 @@ exports[`presents a user 1`] = ` Object { "avatarUrl": undefined, + "color": undefined, "createdAt": undefined, "id": "123", "isAdmin": undefined, @@ -16,6 +17,7 @@ Object { exports[`presents a user without slack data 1`] = ` Object { "avatarUrl": undefined, + "color": undefined, "createdAt": undefined, "id": "123", "isAdmin": undefined, diff --git a/server/presenters/env.js b/server/presenters/env.js index e5b42ea6..3b361f6b 100644 --- a/server/presenters/env.js +++ b/server/presenters/env.js @@ -4,8 +4,11 @@ // do not add anything here that should be a secret or password export default function present(env: Object): Object { return { - URL: env.URL, - CDN_URL: env.CDN_URL || "", + URL: env.URL.replace(/\/$/, ""), + CDN_URL: (env.CDN_URL || "").replace(/\/$/, ""), + COLLABORATION_URL: (env.COLLABORATION_URL || "") + .replace(/\/$/, "") + .replace(/^http/, "ws"), DEPLOYMENT: env.DEPLOYMENT, ENVIRONMENT: env.NODE_ENV, SENTRY_DSN: env.SENTRY_DSN, diff --git a/server/presenters/team.js b/server/presenters/team.js index 58eb5b14..3f91c1e3 100644 --- a/server/presenters/team.js +++ b/server/presenters/team.js @@ -1,4 +1,5 @@ // @flow +import env from "../env"; import { Team } from "../models"; export default function present(team: Team) { @@ -7,6 +8,7 @@ export default function present(team: Team) { name: team.name, avatarUrl: team.logoUrl, sharing: team.sharing, + collaborativeEditing: team.collaborativeEditing && env.COLLABORATION_URL, documentEmbeds: team.documentEmbeds, guestSignin: team.guestSignin, subdomain: team.subdomain, diff --git a/server/presenters/user.js b/server/presenters/user.js index eec34e63..3c5022f2 100644 --- a/server/presenters/user.js +++ b/server/presenters/user.js @@ -10,6 +10,7 @@ type UserPresentation = { name: string, avatarUrl: ?string, email?: string, + color: string, isAdmin: boolean, isSuspended: boolean, isViewer: boolean, @@ -21,6 +22,7 @@ export default (user: User, options: Options = {}): ?UserPresentation => { userData.id = user.id; userData.createdAt = user.createdAt; userData.name = user.name; + userData.color = user.color; userData.isAdmin = user.isAdmin; userData.isViewer = user.isViewer; userData.isSuspended = user.isSuspended; diff --git a/server/queues/processors/backlinks.js b/server/queues/processors/backlinks.js index 2d6ac4f0..f33f2256 100644 --- a/server/queues/processors/backlinks.js +++ b/server/queues/processors/backlinks.js @@ -1,5 +1,5 @@ // @flow -import { Document, Backlink } from "../../models"; +import { Document, Backlink, Team } from "../../models"; import { Op } from "../../sequelize"; import type { DocumentEvent, RevisionEvent } from "../../types"; import parseDocumentIds from "../../utils/parseDocumentIds"; @@ -78,13 +78,19 @@ export default class BacklinksProcessor { break; } case "documents.title_change": { - const document = await Document.findByPk(event.documentId); - if (!document) return; - // might as well check const { title, previousTitle } = event.data; 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 const backlinks = await Backlink.findAll({ where: { diff --git a/server/redis.js b/server/redis.js index 1d207617..42b10a9f 100644 --- a/server/redis.js +++ b/server/redis.js @@ -9,11 +9,12 @@ const options = { }, // support Heroku Redis, see: // https://devcenter.heroku.com/articles/heroku-redis#ioredis-module - tls: process.env.REDIS_URL.startsWith("rediss://") - ? { - rejectUnauthorized: false, - } - : undefined, + tls: + process.env.REDIS_URL && process.env.REDIS_URL.startsWith("rediss://") + ? { + rejectUnauthorized: false, + } + : undefined, }; const client = new Redis(process.env.REDIS_URL, options); diff --git a/server/routes/api/__snapshots__/users.test.js.snap b/server/routes/api/__snapshots__/users.test.js.snap index 626a5eab..060fd066 100644 --- a/server/routes/api/__snapshots__/users.test.js.snap +++ b/server/routes/api/__snapshots__/users.test.js.snap @@ -4,6 +4,7 @@ exports[`#users.activate should activate a suspended user 1`] = ` Object { "data": Object { "avatarUrl": "https://tiley.herokuapp.com/avatar/111d68d06e2d317b5a59c2c6c5bad808/U.png", + "color": "#e600e0", "createdAt": "2018-01-02T00:00:00.000Z", "email": "user1@example.com", "id": "46fde1d4-0050-428f-9f0b-0bf77f4bdf61", @@ -56,6 +57,7 @@ exports[`#users.demote should demote an admin 1`] = ` Object { "data": Object { "avatarUrl": "https://tiley.herokuapp.com/avatar/111d68d06e2d317b5a59c2c6c5bad808/U.png", + "color": "#e600e0", "createdAt": "2018-01-02T00:00:00.000Z", "email": "user1@example.com", "id": "46fde1d4-0050-428f-9f0b-0bf77f4bdf61", @@ -90,6 +92,7 @@ exports[`#users.demote should demote an admin to member 1`] = ` Object { "data": Object { "avatarUrl": "https://tiley.herokuapp.com/avatar/111d68d06e2d317b5a59c2c6c5bad808/U.png", + "color": "#e600e0", "createdAt": "2018-01-02T00:00:00.000Z", "email": "user1@example.com", "id": "46fde1d4-0050-428f-9f0b-0bf77f4bdf61", @@ -124,6 +127,7 @@ exports[`#users.demote should demote an admin to viewer 1`] = ` Object { "data": Object { "avatarUrl": "https://tiley.herokuapp.com/avatar/111d68d06e2d317b5a59c2c6c5bad808/U.png", + "color": "#e600e0", "createdAt": "2018-01-02T00:00:00.000Z", "email": "user1@example.com", "id": "46fde1d4-0050-428f-9f0b-0bf77f4bdf61", @@ -176,6 +180,7 @@ exports[`#users.promote should promote a new admin 1`] = ` Object { "data": Object { "avatarUrl": "https://tiley.herokuapp.com/avatar/111d68d06e2d317b5a59c2c6c5bad808/U.png", + "color": "#e600e0", "createdAt": "2018-01-02T00:00:00.000Z", "email": "user1@example.com", "id": "46fde1d4-0050-428f-9f0b-0bf77f4bdf61", @@ -237,6 +242,7 @@ exports[`#users.suspend should suspend an user 1`] = ` Object { "data": Object { "avatarUrl": "https://tiley.herokuapp.com/avatar/111d68d06e2d317b5a59c2c6c5bad808/U.png", + "color": "#e600e0", "createdAt": "2018-01-02T00:00:00.000Z", "email": "user1@example.com", "id": "46fde1d4-0050-428f-9f0b-0bf77f4bdf61", diff --git a/server/routes/api/hooks.js b/server/routes/api/hooks.js index 0c36cb16..3d9448b1 100644 --- a/server/routes/api/hooks.js +++ b/server/routes/api/hooks.js @@ -13,7 +13,7 @@ import { IntegrationAuthentication, } from "../../models"; import { presentSlackAttachment } from "../../presenters"; -import * as Slack from "../../slack"; +import * as Slack from "../../utils/slack"; const router = new Router(); // triggered by a user posting a getoutline.com link in Slack diff --git a/server/routes/api/hooks.test.js b/server/routes/api/hooks.test.js index 82108977..b2df3fed 100644 --- a/server/routes/api/hooks.test.js +++ b/server/routes/api/hooks.test.js @@ -2,9 +2,9 @@ import TestServer from "fetch-test-server"; import { IntegrationAuthentication, SearchQuery } from "../../models"; import webService from "../../services/web"; -import * as Slack from "../../slack"; import { buildDocument, buildIntegration } from "../../test/factories"; import { flushdb, seed } from "../../test/support"; +import * as Slack from "../../utils/slack"; const app = webService(); const server = new TestServer(app.callback()); @@ -12,7 +12,7 @@ const server = new TestServer(app.callback()); beforeEach(() => flushdb()); afterAll(() => server.close()); -jest.mock("../../slack", () => ({ +jest.mock("../../utils/slack", () => ({ post: jest.fn(), })); diff --git a/server/routes/auth/providers/slack.js b/server/routes/auth/providers/slack.js index ede4e2e7..4cd0e52e 100644 --- a/server/routes/auth/providers/slack.js +++ b/server/routes/auth/providers/slack.js @@ -12,8 +12,8 @@ import { Integration, Team, } from "../../../models"; -import * as Slack from "../../../slack"; import { StateStore } from "../../../utils/passport"; +import * as Slack from "../../../utils/slack"; const router = new Router(); const providerName = "slack"; diff --git a/server/sentry.js b/server/sentry.js deleted file mode 100644 index b2bc8283..00000000 --- a/server/sentry.js +++ /dev/null @@ -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; diff --git a/server/services/admin.js b/server/services/admin.js new file mode 100644 index 00000000..90470285 --- /dev/null +++ b/server/services/admin.js @@ -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()); +} diff --git a/server/services/collaboration.js b/server/services/collaboration.js new file mode 100644 index 00000000..ff2886d2 --- /dev/null +++ b/server/services/collaboration.js @@ -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()); +} diff --git a/server/services/index.js b/server/services/index.js index ab4383ca..4af106b0 100644 --- a/server/services/index.js +++ b/server/services/index.js @@ -1,6 +1,8 @@ // @flow +import admin from "./admin"; +import collaboration from "./collaboration"; import web from "./web"; import websockets from "./websockets"; import worker from "./worker"; -export default { web, websockets, worker }; +export default { websockets, collaboration, admin, web, worker }; diff --git a/server/services/web.js b/server/services/web.js index a2df0eb5..ce6b7544 100644 --- a/server/services/web.js +++ b/server/services/web.js @@ -7,14 +7,12 @@ import { referrerPolicy, } from "koa-helmet"; import mount from "koa-mount"; -import onerror from "koa-onerror"; import enforceHttps from "koa-sslify"; import emails from "../emails"; import env from "../env"; import routes from "../routes"; import api from "../routes/api"; import auth from "../routes/auth"; -import Sentry from "../sentry"; const isProduction = env.NODE_ENV === "production"; 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)); } - // 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("/api", api)); diff --git a/server/services/websockets.js b/server/services/websockets.js index 46547a8a..0179f43a 100644 --- a/server/services/websockets.js +++ b/server/services/websockets.js @@ -10,14 +10,14 @@ import policy from "../policies"; import { websocketsQueue } from "../queues"; import WebsocketsProcessor from "../queues/processors/websockets"; import { client, subscriber } from "../redis"; -import Sentry from "../sentry"; import { getUserForJWT } from "../utils/jwt"; import * as metrics from "../utils/metrics"; +import Sentry from "../utils/sentry"; const { can } = policy; -const websockets = new WebsocketsProcessor(); export default function init(app: Koa, server: http.Server) { + // Websockets for events and non-collaborative documents const io = IO(server, { path: "/realtime", 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) { const event = job.data; websockets.on(event, io).catch((error) => { diff --git a/server/services/worker.js b/server/services/worker.js index ba4da6f7..2c86da3c 100644 --- a/server/services/worker.js +++ b/server/services/worker.js @@ -16,7 +16,7 @@ import Imports from "../queues/processors/imports"; import Notifications from "../queues/processors/notifications"; import Revisions from "../queues/processors/revisions"; import Slack from "../queues/processors/slack"; -import Sentry from "../sentry"; +import Sentry from "../utils/sentry"; const log = debug("queue"); diff --git a/server/utils/args.js b/server/utils/args.js new file mode 100644 index 00000000..d1f89d5c --- /dev/null +++ b/server/utils/args.js @@ -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(","); +} diff --git a/server/utils/color.js b/server/utils/color.js new file mode 100644 index 00000000..02b2c09a --- /dev/null +++ b/server/utils/color.js @@ -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), +]; diff --git a/server/utils/queue.js b/server/utils/queue.js index 54e079bd..8cfb826b 100644 --- a/server/utils/queue.js +++ b/server/utils/queue.js @@ -3,8 +3,8 @@ import Queue from "bull"; import Redis from "ioredis"; import { snakeCase } from "lodash"; import { client, subscriber } from "../redis"; -import Sentry from "../sentry"; import * as metrics from "../utils/metrics"; +import Sentry from "./sentry"; export function createQueue(name: string) { const prefix = `queue.${snakeCase(name)}`; diff --git a/server/utils/sentry.js b/server/utils/sentry.js new file mode 100644 index 00000000..662eb219 --- /dev/null +++ b/server/utils/sentry.js @@ -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; diff --git a/server/slack.js b/server/utils/slack.js similarity index 96% rename from server/slack.js rename to server/utils/slack.js index 9045108f..ed78b507 100644 --- a/server/slack.js +++ b/server/utils/slack.js @@ -1,7 +1,7 @@ // @flow import querystring from "querystring"; import fetch from "fetch-with-proxy"; -import { InvalidRequestError } from "./errors"; +import { InvalidRequestError } from "../errors"; const SLACK_API_URL = "https://slack.com/api"; diff --git a/app/embeds/Abstract.js b/shared/embeds/Abstract.js similarity index 100% rename from app/embeds/Abstract.js rename to shared/embeds/Abstract.js diff --git a/app/embeds/Abstract.test.js b/shared/embeds/Abstract.test.js similarity index 100% rename from app/embeds/Abstract.test.js rename to shared/embeds/Abstract.test.js diff --git a/app/embeds/Airtable.js b/shared/embeds/Airtable.js similarity index 100% rename from app/embeds/Airtable.js rename to shared/embeds/Airtable.js diff --git a/app/embeds/Airtable.test.js b/shared/embeds/Airtable.test.js similarity index 100% rename from app/embeds/Airtable.test.js rename to shared/embeds/Airtable.test.js diff --git a/app/embeds/Cawemo.js b/shared/embeds/Cawemo.js similarity index 100% rename from app/embeds/Cawemo.js rename to shared/embeds/Cawemo.js diff --git a/app/embeds/Cawemo.test.js b/shared/embeds/Cawemo.test.js similarity index 100% rename from app/embeds/Cawemo.test.js rename to shared/embeds/Cawemo.test.js diff --git a/app/embeds/ClickUp.js b/shared/embeds/ClickUp.js similarity index 100% rename from app/embeds/ClickUp.js rename to shared/embeds/ClickUp.js diff --git a/app/embeds/ClickUp.test.js b/shared/embeds/ClickUp.test.js similarity index 100% rename from app/embeds/ClickUp.test.js rename to shared/embeds/ClickUp.test.js diff --git a/app/embeds/Codepen.js b/shared/embeds/Codepen.js similarity index 100% rename from app/embeds/Codepen.js rename to shared/embeds/Codepen.js diff --git a/app/embeds/Codepen.test.js b/shared/embeds/Codepen.test.js similarity index 100% rename from app/embeds/Codepen.test.js rename to shared/embeds/Codepen.test.js diff --git a/app/embeds/Descript.js b/shared/embeds/Descript.js similarity index 100% rename from app/embeds/Descript.js rename to shared/embeds/Descript.js diff --git a/app/embeds/Diagrams.js b/shared/embeds/Diagrams.js similarity index 96% rename from app/embeds/Diagrams.js rename to shared/embeds/Diagrams.js index 0669ed78..eca9c54f 100644 --- a/app/embeds/Diagrams.js +++ b/shared/embeds/Diagrams.js @@ -1,7 +1,7 @@ // @flow import * as React from "react"; -import Image from "components/Image"; import Frame from "./components/Frame"; +import Image from "./components/Image"; const URL_REGEX = new RegExp("^https://viewer.diagrams.net/.*(title=\\w+)?"); diff --git a/app/embeds/Diagrams.test.js b/shared/embeds/Diagrams.test.js similarity index 100% rename from app/embeds/Diagrams.test.js rename to shared/embeds/Diagrams.test.js diff --git a/app/embeds/Figma.js b/shared/embeds/Figma.js similarity index 100% rename from app/embeds/Figma.js rename to shared/embeds/Figma.js diff --git a/app/embeds/Figma.test.js b/shared/embeds/Figma.test.js similarity index 100% rename from app/embeds/Figma.test.js rename to shared/embeds/Figma.test.js diff --git a/app/embeds/Framer.js b/shared/embeds/Framer.js similarity index 100% rename from app/embeds/Framer.js rename to shared/embeds/Framer.js diff --git a/app/embeds/Framer.test.js b/shared/embeds/Framer.test.js similarity index 100% rename from app/embeds/Framer.test.js rename to shared/embeds/Framer.test.js diff --git a/app/embeds/Gist.js b/shared/embeds/Gist.js similarity index 100% rename from app/embeds/Gist.js rename to shared/embeds/Gist.js diff --git a/app/embeds/Gist.test.js b/shared/embeds/Gist.test.js similarity index 100% rename from app/embeds/Gist.test.js rename to shared/embeds/Gist.test.js diff --git a/app/embeds/GoogleCalendar.js b/shared/embeds/GoogleCalendar.js similarity index 100% rename from app/embeds/GoogleCalendar.js rename to shared/embeds/GoogleCalendar.js diff --git a/app/embeds/GoogleCalendar.test.js b/shared/embeds/GoogleCalendar.test.js similarity index 100% rename from app/embeds/GoogleCalendar.test.js rename to shared/embeds/GoogleCalendar.test.js diff --git a/app/embeds/GoogleDataStudio.js b/shared/embeds/GoogleDataStudio.js similarity index 95% rename from app/embeds/GoogleDataStudio.js rename to shared/embeds/GoogleDataStudio.js index 24c00b6b..2ca4dd29 100644 --- a/app/embeds/GoogleDataStudio.js +++ b/shared/embeds/GoogleDataStudio.js @@ -1,7 +1,7 @@ // @flow import * as React from "react"; -import Image from "components/Image"; import Frame from "./components/Frame"; +import Image from "./components/Image"; const URL_REGEX = new RegExp( "^https?://datastudio.google.com/(embed|u/0)/reporting/(.*)/page/(.*)(/edit)?$" diff --git a/app/embeds/GoogleDataStudio.test.js b/shared/embeds/GoogleDataStudio.test.js similarity index 100% rename from app/embeds/GoogleDataStudio.test.js rename to shared/embeds/GoogleDataStudio.test.js diff --git a/app/embeds/GoogleDocs.js b/shared/embeds/GoogleDocs.js similarity index 94% rename from app/embeds/GoogleDocs.js rename to shared/embeds/GoogleDocs.js index fd5a4263..a185b531 100644 --- a/app/embeds/GoogleDocs.js +++ b/shared/embeds/GoogleDocs.js @@ -1,7 +1,7 @@ // @flow import * as React from "react"; -import Image from "components/Image"; import Frame from "./components/Frame"; +import Image from "./components/Image"; const URL_REGEX = new RegExp("^https?://docs.google.com/document/(.*)$"); diff --git a/app/embeds/GoogleDocs.test.js b/shared/embeds/GoogleDocs.test.js similarity index 100% rename from app/embeds/GoogleDocs.test.js rename to shared/embeds/GoogleDocs.test.js diff --git a/app/embeds/GoogleDrawings.js b/shared/embeds/GoogleDrawings.js similarity index 95% rename from app/embeds/GoogleDrawings.js rename to shared/embeds/GoogleDrawings.js index 6be5db51..79053976 100644 --- a/app/embeds/GoogleDrawings.js +++ b/shared/embeds/GoogleDrawings.js @@ -1,7 +1,7 @@ // @flow import * as React from "react"; -import Image from "components/Image"; import Frame from "./components/Frame"; +import Image from "./components/Image"; const URL_REGEX = new RegExp( "^https://docs.google.com/drawings/d/(.*)/(edit|preview)(.*)$" diff --git a/app/embeds/GoogleDrawings.test.js b/shared/embeds/GoogleDrawings.test.js similarity index 100% rename from app/embeds/GoogleDrawings.test.js rename to shared/embeds/GoogleDrawings.test.js diff --git a/app/embeds/GoogleDrive.js b/shared/embeds/GoogleDrive.js similarity index 94% rename from app/embeds/GoogleDrive.js rename to shared/embeds/GoogleDrive.js index 12bb07a2..e75eb33b 100644 --- a/app/embeds/GoogleDrive.js +++ b/shared/embeds/GoogleDrive.js @@ -1,7 +1,7 @@ // @flow import * as React from "react"; -import Image from "components/Image"; import Frame from "./components/Frame"; +import Image from "./components/Image"; const URL_REGEX = new RegExp("^https?://drive.google.com/file/d/(.*)$"); diff --git a/app/embeds/GoogleDrive.test.js b/shared/embeds/GoogleDrive.test.js similarity index 100% rename from app/embeds/GoogleDrive.test.js rename to shared/embeds/GoogleDrive.test.js diff --git a/app/embeds/GoogleSheets.js b/shared/embeds/GoogleSheets.js similarity index 95% rename from app/embeds/GoogleSheets.js rename to shared/embeds/GoogleSheets.js index 70a3d53e..070964eb 100644 --- a/app/embeds/GoogleSheets.js +++ b/shared/embeds/GoogleSheets.js @@ -1,7 +1,7 @@ // @flow import * as React from "react"; -import Image from "components/Image"; import Frame from "./components/Frame"; +import Image from "./components/Image"; const URL_REGEX = new RegExp("^https?://docs.google.com/spreadsheets/d/(.*)$"); diff --git a/app/embeds/GoogleSheets.test.js b/shared/embeds/GoogleSheets.test.js similarity index 100% rename from app/embeds/GoogleSheets.test.js rename to shared/embeds/GoogleSheets.test.js diff --git a/app/embeds/GoogleSlides.js b/shared/embeds/GoogleSlides.js similarity index 95% rename from app/embeds/GoogleSlides.js rename to shared/embeds/GoogleSlides.js index cc961a4f..05a3da6a 100644 --- a/app/embeds/GoogleSlides.js +++ b/shared/embeds/GoogleSlides.js @@ -1,7 +1,7 @@ // @flow import * as React from "react"; -import Image from "components/Image"; import Frame from "./components/Frame"; +import Image from "./components/Image"; const URL_REGEX = new RegExp("^https?://docs.google.com/presentation/d/(.*)$"); diff --git a/app/embeds/GoogleSlides.test.js b/shared/embeds/GoogleSlides.test.js similarity index 100% rename from app/embeds/GoogleSlides.test.js rename to shared/embeds/GoogleSlides.test.js diff --git a/app/embeds/InVision.js b/shared/embeds/InVision.js similarity index 100% rename from app/embeds/InVision.js rename to shared/embeds/InVision.js diff --git a/app/embeds/InVision.test.js b/shared/embeds/InVision.test.js similarity index 100% rename from app/embeds/InVision.test.js rename to shared/embeds/InVision.test.js diff --git a/app/embeds/Loom.js b/shared/embeds/Loom.js similarity index 100% rename from app/embeds/Loom.js rename to shared/embeds/Loom.js diff --git a/app/embeds/Loom.test.js b/shared/embeds/Loom.test.js similarity index 100% rename from app/embeds/Loom.test.js rename to shared/embeds/Loom.test.js diff --git a/app/embeds/Lucidchart.js b/shared/embeds/Lucidchart.js similarity index 100% rename from app/embeds/Lucidchart.js rename to shared/embeds/Lucidchart.js diff --git a/app/embeds/Lucidchart.test.js b/shared/embeds/Lucidchart.test.js similarity index 100% rename from app/embeds/Lucidchart.test.js rename to shared/embeds/Lucidchart.test.js diff --git a/app/embeds/Marvel.js b/shared/embeds/Marvel.js similarity index 100% rename from app/embeds/Marvel.js rename to shared/embeds/Marvel.js diff --git a/app/embeds/Marvel.test.js b/shared/embeds/Marvel.test.js similarity index 100% rename from app/embeds/Marvel.test.js rename to shared/embeds/Marvel.test.js diff --git a/app/embeds/Mindmeister.js b/shared/embeds/Mindmeister.js similarity index 100% rename from app/embeds/Mindmeister.js rename to shared/embeds/Mindmeister.js diff --git a/app/embeds/Mindmeister.test.js b/shared/embeds/Mindmeister.test.js similarity index 100% rename from app/embeds/Mindmeister.test.js rename to shared/embeds/Mindmeister.test.js diff --git a/app/embeds/Miro.js b/shared/embeds/Miro.js similarity index 100% rename from app/embeds/Miro.js rename to shared/embeds/Miro.js diff --git a/app/embeds/Miro.test.js b/shared/embeds/Miro.test.js similarity index 100% rename from app/embeds/Miro.test.js rename to shared/embeds/Miro.test.js diff --git a/app/embeds/ModeAnalytics.js b/shared/embeds/ModeAnalytics.js similarity index 100% rename from app/embeds/ModeAnalytics.js rename to shared/embeds/ModeAnalytics.js diff --git a/app/embeds/ModeAnalytics.test.js b/shared/embeds/ModeAnalytics.test.js similarity index 100% rename from app/embeds/ModeAnalytics.test.js rename to shared/embeds/ModeAnalytics.test.js diff --git a/app/embeds/Prezi.js b/shared/embeds/Prezi.js similarity index 100% rename from app/embeds/Prezi.js rename to shared/embeds/Prezi.js diff --git a/app/embeds/Prezi.test.js b/shared/embeds/Prezi.test.js similarity index 100% rename from app/embeds/Prezi.test.js rename to shared/embeds/Prezi.test.js diff --git a/app/embeds/Spotify.js b/shared/embeds/Spotify.js similarity index 100% rename from app/embeds/Spotify.js rename to shared/embeds/Spotify.js diff --git a/app/embeds/Spotify.test.js b/shared/embeds/Spotify.test.js similarity index 100% rename from app/embeds/Spotify.test.js rename to shared/embeds/Spotify.test.js diff --git a/app/embeds/Trello.js b/shared/embeds/Trello.js similarity index 100% rename from app/embeds/Trello.js rename to shared/embeds/Trello.js diff --git a/app/embeds/Typeform.js b/shared/embeds/Typeform.js similarity index 100% rename from app/embeds/Typeform.js rename to shared/embeds/Typeform.js diff --git a/app/embeds/Typeform.test.js b/shared/embeds/Typeform.test.js similarity index 100% rename from app/embeds/Typeform.test.js rename to shared/embeds/Typeform.test.js diff --git a/app/embeds/Vimeo.js b/shared/embeds/Vimeo.js similarity index 100% rename from app/embeds/Vimeo.js rename to shared/embeds/Vimeo.js diff --git a/app/embeds/Vimeo.test.js b/shared/embeds/Vimeo.test.js similarity index 100% rename from app/embeds/Vimeo.test.js rename to shared/embeds/Vimeo.test.js diff --git a/app/embeds/YouTube.js b/shared/embeds/YouTube.js similarity index 100% rename from app/embeds/YouTube.js rename to shared/embeds/YouTube.js diff --git a/app/embeds/YouTube.test.js b/shared/embeds/YouTube.test.js similarity index 100% rename from app/embeds/YouTube.test.js rename to shared/embeds/YouTube.test.js diff --git a/app/embeds/components/Frame.js b/shared/embeds/components/Frame.js similarity index 98% rename from app/embeds/components/Frame.js rename to shared/embeds/components/Frame.js index 2cd79624..0cc1e5d0 100644 --- a/app/embeds/components/Frame.js +++ b/shared/embeds/components/Frame.js @@ -4,7 +4,6 @@ import { observer } from "mobx-react"; import { OpenIcon } from "outline-icons"; import * as React from "react"; import styled from "styled-components"; -import Flex from "components/Flex"; // This wrapper allows us to pass non-standard HTML attributes through to the DOM element // https://www.styled-components.com/docs/basics#passed-props @@ -130,7 +129,9 @@ const Title = styled.span` padding-left: 4px; `; -const Bar = styled(Flex)` +const Bar = styled.div` + display: flex; + align-items: center; border-top: 1px solid ${(props) => props.theme.embedBorder}; background: ${(props) => props.theme.secondaryBackground}; color: ${(props) => props.theme.textSecondary}; diff --git a/app/components/Image.js b/shared/embeds/components/Image.js similarity index 85% rename from app/components/Image.js rename to shared/embeds/components/Image.js index a8bf88a8..8fc67ab8 100644 --- a/app/components/Image.js +++ b/shared/embeds/components/Image.js @@ -1,6 +1,6 @@ // @flow import * as React from "react"; -import { cdnPath } from "utils/urls"; +import { cdnPath } from "../../utils/urls"; type Props = {| alt: string, diff --git a/app/embeds/index.js b/shared/embeds/index.js similarity index 99% rename from app/embeds/index.js rename to shared/embeds/index.js index 2ee824d5..3f53c865 100644 --- a/app/embeds/index.js +++ b/shared/embeds/index.js @@ -1,7 +1,6 @@ // @flow import * as React from "react"; import styled from "styled-components"; -import Image from "components/Image"; import Abstract from "./Abstract"; import Airtable from "./Airtable"; import Cawemo from "./Cawemo"; @@ -32,6 +31,7 @@ import Trello from "./Trello"; import Typeform from "./Typeform"; import Vimeo from "./Vimeo"; import YouTube from "./YouTube"; +import Image from "./components/Image"; function matcher(Component) { return (url: string) => { diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json index 63bd46b8..d7395722 100644 --- a/shared/i18n/locales/en_US/translation.json +++ b/shared/i18n/locales/en_US/translation.json @@ -8,6 +8,8 @@ "Add a description": "Add a description", "Collapse": "Collapse", "Expand": "Expand", + "Server connection lost": "Server connection lost", + "Edits you make will sync once you’re online": "Edits you make will sync once you’re online", "Submenu": "Submenu", "Trash": "Trash", "Archive": "Archive", @@ -178,6 +180,7 @@ "Changelog": "Changelog", "Send us feedback": "Send us feedback", "Report a bug": "Report a bug", + "Development": "Development", "Appearance": "Appearance", "System": "System", "Light": "Light", @@ -350,6 +353,7 @@ "New from template": "New from template", "Publish": "Publish", "Publishing": "Publishing", + "Sorry, it looks like you don’t have permission to access the document": "Sorry, it looks like you don’t have permission to access the document", "Nested documents": "Nested documents", "Anyone with the link <1></1>can view this document": "Anyone with the link <1></1>can view this document", "Share": "Share", diff --git a/shared/theme.js b/shared/theme.js index 0e355085..c0b9f7b6 100644 --- a/shared/theme.js +++ b/shared/theme.js @@ -40,6 +40,7 @@ const colors = { blue: "#3633FF", marine: "#2BC2FF", green: "#42DED1", + yellow: "#F5BE31", }, }; diff --git a/shared/utils/domains.js b/shared/utils/domains.js index 25eb8679..496dae05 100644 --- a/shared/utils/domains.js +++ b/shared/utils/domains.js @@ -102,6 +102,7 @@ export const RESERVED_SUBDOMAINS = [ "mail", "marketing", "mobile", + "multiplayer", "new", "news", "newsletter", @@ -111,6 +112,7 @@ export const RESERVED_SUBDOMAINS = [ "ns4", "password", "profile", + "realtime", "sandbox", "script", "scripts", diff --git a/shared/utils/urls.js b/shared/utils/urls.js new file mode 100644 index 00000000..c7de2298 --- /dev/null +++ b/shared/utils/urls.js @@ -0,0 +1,6 @@ +// @flow +const env = typeof window !== "undefined" ? window.env : process.env; + +export function cdnPath(path: string): string { + return `${env.CDN_URL}${path}`; +} diff --git a/yarn.lock b/yarn.lock index 0da83c2d..fb6e1b1d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -978,10 +978,10 @@ core-js-pure "^3.0.0" regenerator-runtime "^0.13.4" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.3.1", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.5.tgz#665450911c6031af38f81db530f387ec04cd9a98" - integrity sha512-121rumjddw9c3NCQ55KGkyE1h/nzWhU/owjhw0l4mQrkzz4x9SGS1X8gFLraHwX7td3Yo4QTL+qj0NcIzN87BA== +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.3.1", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": + version "7.15.3" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.15.3.tgz#2e1c2880ca118e5b2f9988322bd8a7656a32502b" + integrity sha512-OvwMLqNXkCXSz1kSm58sEsNuhqOx/fKpnUnKnFB5v8uDda5bLNEHNgKPvhDN6IU0LDcnHQ90LlJ0Q6jnyBSIBA== dependencies: regenerator-runtime "^0.13.4" @@ -1022,6 +1022,36 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@bull-board/api@3.5.0", "@bull-board/api@^3.5.0": + version "3.5.0" + resolved "https://registry.yarnpkg.com/@bull-board/api/-/api-3.5.0.tgz#9a678a3fc89663bf7cb2cade7c99f3ab7deeff71" + integrity sha512-IeNH3UMvxxfxmT5zimUfFps4093tIaWO5L7Mpxb2OdFvbcP/Xd8Nm+f1w/dpKj+aLd2acQbPD4OCra35GLqgOQ== + dependencies: + redis-info "^3.0.8" + +"@bull-board/koa@^3.5.0": + version "3.5.0" + resolved "https://registry.yarnpkg.com/@bull-board/koa/-/koa-3.5.0.tgz#7e0683525483c61f08e9ca96e75eec5497eb2316" + integrity sha512-0JGUtWtV5I1jVZqW7JYLiCMyPImGyv6B5xOGxT2pyWQHQAfe7NJBdNBl8CsJjU4FwV70LWlLg2rL+kpzNAJdJQ== + dependencies: + "@bull-board/api" "3.5.0" + "@bull-board/ui" "3.5.0" + ejs "^3.1.6" + koa "^2.13.1" + koa-mount "^4.0.0" + koa-router "^10.0.0" + koa-static "^5.0.0" + koa-views "^7.0.1" + +"@bull-board/ui@3.5.0": + version "3.5.0" + resolved "https://registry.yarnpkg.com/@bull-board/ui/-/ui-3.5.0.tgz#7dec7ea4987b65a06abce91952ed8f923eb58d20" + integrity sha512-D7ScfrleLuTsXs19yteqT7EmesEfBgercXl7hSA1LFdevesAwW/u+qdupBBom/Dit9ejTYafaOzzxFDPvszWxw== + dependencies: + "@bull-board/api" "3.5.0" + "@radix-ui/react-alert-dialog" "^0.0.19" + "@radix-ui/react-dropdown-menu" "^0.0.22" + "@bundle-stats/utils@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@bundle-stats/utils/-/utils-3.0.0.tgz#8578b957bbad3a71b6137ca6541f2ee81f1f176b" @@ -1116,6 +1146,36 @@ dependencies: "@hapi/hoek" "^8.3.0" +"@hocuspocus/extension-logger@^1.0.0-alpha.43": + version "1.0.0-alpha.43" + resolved "https://registry.yarnpkg.com/@hocuspocus/extension-logger/-/extension-logger-1.0.0-alpha.43.tgz#66224ba5bf634ba1a1b6be6156d5d55d515efba6" + integrity sha512-PdIG7egACqNpRzWR/vX5BRVlvTRjOsT1MwK1h+6T/ICI8aP6WOsJ5ruqPwJGN0vLbffZK+IH9mfxALHa6NHVlA== + dependencies: + "@hocuspocus/server" "^1.0.0-alpha.68" + +"@hocuspocus/provider@^1.0.0-alpha.13": + version "1.0.0-alpha.13" + resolved "https://registry.yarnpkg.com/@hocuspocus/provider/-/provider-1.0.0-alpha.13.tgz#40c65168d1bed35bef0196df40512326825bfa51" + integrity sha512-/L6GmV7bEEwd8VIiodOynQjQUaEHUT4hQIkzNMtsICUkiu3OQs35ZrC973BxSp0t7df6KOo3kSULBpp/TW8wGg== + dependencies: + lib0 "^0.2.42" + y-protocols "^1.0.5" + yjs "^13.5.8" + +"@hocuspocus/server@^1.0.0-alpha.68": + version "1.0.0-alpha.68" + resolved "https://registry.yarnpkg.com/@hocuspocus/server/-/server-1.0.0-alpha.68.tgz#19a061718f0cdebab83fe0847386a556e7753e5c" + integrity sha512-phuuW8KFHmcufhatvIysq3iiXYH1Utd8+dWW0YNM90BA79A6rmx32DcQT3fton/wjcqU8rzoeZQomemghfSxBQ== + dependencies: + "@types/async-lock" "^1.1.2" + "@types/uuid" "^8.3.0" + "@types/ws" "^7.4.0" + async-lock "^1.2.8" + lib0 "^0.2.41" + uuid "^8.3.2" + ws "^7.4.3" + yjs "^13.5.0" + "@icons/material@^0.2.4": version "0.2.4" resolved "https://registry.yarnpkg.com/@icons/material/-/material-0.2.4.tgz#e90c9f71768b3736e76d7dd6783fc6c2afa88bc8" @@ -1800,6 +1860,306 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= +"@radix-ui/popper@0.0.10": + version "0.0.10" + resolved "https://registry.yarnpkg.com/@radix-ui/popper/-/popper-0.0.10.tgz#9f707d9cec8762423f81acaf8e650e40a554cb73" + integrity sha512-YFKuPqQPKscreQid7NuB4it3PMzSwGg03vgrud6sVliHkI43QNAOHyrHyMNo015jg6QK5GVDn+7J2W5uygqSGA== + dependencies: + "@babel/runtime" "^7.13.10" + csstype "^3.0.4" + +"@radix-ui/primitive@0.0.5": + version "0.0.5" + resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-0.0.5.tgz#8464fb4db04401bde72d36e27e05714080668d40" + integrity sha512-VeL6A5LpKYRJhDDj5tCTnzP3zm+FnvybsAkgBHQ4LUPPBnqRdWLoyKpZhlwFze/z22QHINaTIcE9Z/fTcrUR1g== + dependencies: + "@babel/runtime" "^7.13.10" + +"@radix-ui/react-alert-dialog@^0.0.19": + version "0.0.19" + resolved "https://registry.yarnpkg.com/@radix-ui/react-alert-dialog/-/react-alert-dialog-0.0.19.tgz#5b69bfe063cdb13f49630ad2705e71228505d147" + integrity sha512-SJRUT2s0/WLCvCEbfuKL5EM6QNXjZQkX9ZgkwKvgRNYu5zYEmCmlCUWDJbPIX1Y7w/a6tuEm24f3Uywd8VcBxw== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "0.0.5" + "@radix-ui/react-compose-refs" "0.0.5" + "@radix-ui/react-context" "0.0.5" + "@radix-ui/react-dialog" "0.0.19" + "@radix-ui/react-polymorphic" "0.0.12" + "@radix-ui/react-primitive" "0.0.14" + "@radix-ui/react-slot" "0.0.12" + +"@radix-ui/react-arrow@0.0.14": + version "0.0.14" + resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-0.0.14.tgz#70b2c66efbf3cde0c9dd0895417e39f6cdf31805" + integrity sha512-ZWfQM3hTCXN6ub2dMUmMv2ttEB/1zuBlOZdeWFeCCJHwZ+yy7iPzWF6ixaNygBa6t451PImq/dC7sqOnDJCfqQ== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-polymorphic" "0.0.12" + "@radix-ui/react-primitive" "0.0.14" + +"@radix-ui/react-collection@0.0.14": + version "0.0.14" + resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-0.0.14.tgz#c51c790100407e8eb38be2b67538a5ff7c52915e" + integrity sha512-IM3ttcjArKWItPY7vaCHyqWFSGK+k4Lb0V7RC8vCgB0LXEep5YQ1d30o4TNAqiRX3+Mz7T0zg+gDCwK86visdg== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-compose-refs" "0.0.5" + "@radix-ui/react-slot" "0.0.12" + +"@radix-ui/react-compose-refs@0.0.5": + version "0.0.5" + resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-0.0.5.tgz#0f71f0de1dec341f30cebd420b6bc3d12a3037dd" + integrity sha512-O9mH9X/2EwuAEEoZXrU4alcrRbAhhZHGpIJ5bOH6rmRcokhaoWRBY1tOEe2lgHdb/bkKrY+viLi4Zq8Ju6/09Q== + dependencies: + "@babel/runtime" "^7.13.10" + +"@radix-ui/react-context@0.0.5": + version "0.0.5" + resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-0.0.5.tgz#7c15f46795d7765dabfaf6f9c53791ad28c521c2" + integrity sha512-bwrzAc0qc2EPepSTLBT4+93uCiI9wP78VSmPg2K+k71O/vpx7dPs0VqrewwCBNCHT54NIwaRr2hEsm2uqYi02A== + dependencies: + "@babel/runtime" "^7.13.10" + +"@radix-ui/react-dialog@0.0.19": + version "0.0.19" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-0.0.19.tgz#5a76fa380142a7a97c15c585ab071f63fba5297d" + integrity sha512-7FbWaj/C/TDpfJ+VJ4wNAQIjENDNfwAqNvAfeb+TEtBjgjmsfRDgA1AMenlA5N1QuRtAokRMTHUs3ukW49oQ+g== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "0.0.5" + "@radix-ui/react-compose-refs" "0.0.5" + "@radix-ui/react-context" "0.0.5" + "@radix-ui/react-dismissable-layer" "0.0.14" + "@radix-ui/react-focus-guards" "0.0.7" + "@radix-ui/react-focus-scope" "0.0.14" + "@radix-ui/react-id" "0.0.6" + "@radix-ui/react-polymorphic" "0.0.12" + "@radix-ui/react-portal" "0.0.14" + "@radix-ui/react-presence" "0.0.14" + "@radix-ui/react-primitive" "0.0.14" + "@radix-ui/react-slot" "0.0.12" + "@radix-ui/react-use-controllable-state" "0.0.6" + aria-hidden "^1.1.1" + react-remove-scroll "^2.4.0" + +"@radix-ui/react-dismissable-layer@0.0.14": + version "0.0.14" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-0.0.14.tgz#9d8a3415a2830688070c6596dec18b43c33df7b2" + integrity sha512-0pmRuGYYvWlEaED1igGFLjic0+hD0OqvsnrZaN3n1nDOkoCd7H5CA2geaShSrlBF5riI2Dr9jIZPGLbDRhs4DA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "0.0.5" + "@radix-ui/react-polymorphic" "0.0.12" + "@radix-ui/react-primitive" "0.0.14" + "@radix-ui/react-use-body-pointer-events" "0.0.6" + "@radix-ui/react-use-callback-ref" "0.0.5" + "@radix-ui/react-use-escape-keydown" "0.0.6" + +"@radix-ui/react-dropdown-menu@^0.0.22": + version "0.0.22" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-0.0.22.tgz#914dbbb12d31b4379df697f1262150b3f50916ae" + integrity sha512-/2EnuNKPZjM+MZOsL7X+EOZGBXbJsWH6Rkrrk7etoajZzng48nINVm67aJQERue44LSO8zRJa+XbxS9/g9SvLg== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "0.0.5" + "@radix-ui/react-compose-refs" "0.0.5" + "@radix-ui/react-context" "0.0.5" + "@radix-ui/react-id" "0.0.6" + "@radix-ui/react-menu" "0.0.21" + "@radix-ui/react-polymorphic" "0.0.12" + "@radix-ui/react-primitive" "0.0.14" + "@radix-ui/react-use-controllable-state" "0.0.6" + +"@radix-ui/react-focus-guards@0.0.7": + version "0.0.7" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-0.0.7.tgz#285ed081c877587acd4ee7e6d8260bdf9044e922" + integrity sha512-enAsmrUunptHVzPLTuZqwTP/X3WFBnyJ/jP9W+0g+bRvI3o7V1kxNc+T2Rp1eRTFBW+lUNnt08qkugPytyTRog== + dependencies: + "@babel/runtime" "^7.13.10" + +"@radix-ui/react-focus-scope@0.0.14": + version "0.0.14" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-0.0.14.tgz#778e2a3ea607621d82e0139616d7ea6d517d9533" + integrity sha512-D3v6Tw8vzpIBNd2I32Q2G4LCiXMIlmc6Pl2VV9CZjSatDOjkV/ckGbhkQyQ7QxnD/0CmiSxNo5hTeGRmZDjwmA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-compose-refs" "0.0.5" + "@radix-ui/react-polymorphic" "0.0.12" + "@radix-ui/react-primitive" "0.0.14" + "@radix-ui/react-use-callback-ref" "0.0.5" + +"@radix-ui/react-id@0.0.6": + version "0.0.6" + resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-0.0.6.tgz#c4b27d11861805e91ac296e7758ab47e3947b65c" + integrity sha512-PzmraF34fYggsYvTIZVJ5S68WMp3aKUN3HkSmGnz4zn9zpRjkAbbg7Xn3ueQI3FQsLWKgyUfnpsmWFDndpcqYg== + dependencies: + "@babel/runtime" "^7.13.10" + +"@radix-ui/react-menu@0.0.21": + version "0.0.21" + resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-0.0.21.tgz#a0a2402e1bd11537af5a43f82a7a6f113abf00c6" + integrity sha512-zUhG4pFcHhvsCTHTqGfrury9FQTSbvkclxVIfZ0I6pkCA5wEXJNMmkztCQe1QZT6lZlTuQRPybbbLrt8j3DVAA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "0.0.5" + "@radix-ui/react-collection" "0.0.14" + "@radix-ui/react-compose-refs" "0.0.5" + "@radix-ui/react-context" "0.0.5" + "@radix-ui/react-dismissable-layer" "0.0.14" + "@radix-ui/react-focus-guards" "0.0.7" + "@radix-ui/react-focus-scope" "0.0.14" + "@radix-ui/react-id" "0.0.6" + "@radix-ui/react-polymorphic" "0.0.12" + "@radix-ui/react-popper" "0.0.17" + "@radix-ui/react-portal" "0.0.14" + "@radix-ui/react-presence" "0.0.14" + "@radix-ui/react-primitive" "0.0.14" + "@radix-ui/react-roving-focus" "0.0.15" + "@radix-ui/react-slot" "0.0.12" + "@radix-ui/react-use-callback-ref" "0.0.5" + "@radix-ui/react-use-direction" "0.0.1" + aria-hidden "^1.1.1" + react-remove-scroll "^2.4.0" + +"@radix-ui/react-polymorphic@0.0.12": + version "0.0.12" + resolved "https://registry.yarnpkg.com/@radix-ui/react-polymorphic/-/react-polymorphic-0.0.12.tgz#bf4ae516669b68e059549538104d97322f7c876b" + integrity sha512-/GYNMicBnGzjD1d2fCAuzql1VeFrp8mqM3xfzT1kxhnV85TKdURO45jBfMgqo17XNXoNhWIAProUsCO4qFAAIg== + +"@radix-ui/react-popper@0.0.17": + version "0.0.17" + resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-0.0.17.tgz#a73486e19a628cb3fecaf3fb6eecf6e2cab9d0be" + integrity sha512-2Lk5AjlCcN9B6fQwQ+y1Zb93f1LfyCK6u0oOAUsPtwbnYEHCdC6wuCFBOZ7AonpjHbrHbY6QDrLGcC43TY6ihw== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/popper" "0.0.10" + "@radix-ui/react-arrow" "0.0.14" + "@radix-ui/react-compose-refs" "0.0.5" + "@radix-ui/react-context" "0.0.5" + "@radix-ui/react-polymorphic" "0.0.12" + "@radix-ui/react-primitive" "0.0.14" + "@radix-ui/react-use-rect" "0.0.7" + "@radix-ui/react-use-size" "0.0.6" + "@radix-ui/rect" "0.0.5" + +"@radix-ui/react-portal@0.0.14": + version "0.0.14" + resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-0.0.14.tgz#31513d8777cf5e50a3a30ebc9deb34821e890e9e" + integrity sha512-Wi9arVwVenonjZIX6znCBYaasua03Tb+UtrBZShepJkLGtbGxDlzExijiGIaIRNetl46Oc2pw0F6Y6HffDnUww== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-polymorphic" "0.0.12" + "@radix-ui/react-primitive" "0.0.14" + "@radix-ui/react-use-layout-effect" "0.0.5" + +"@radix-ui/react-presence@0.0.14": + version "0.0.14" + resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-0.0.14.tgz#6a86058bbbf46234dd8840dacd620b3ac5797025" + integrity sha512-ufof9B76DHXV0sC8H7Lswh2AepdJFG8qEtF32JWrbA9N1bl2Jnf9px76KsagyC0MA8crGEZO5A96wizGuSgGWQ== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-compose-refs" "0.0.5" + +"@radix-ui/react-primitive@0.0.14": + version "0.0.14" + resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-0.0.14.tgz#752a967cb05d4c5643634fe20274e7dc905d1cce" + integrity sha512-FYOWGCrxFpLdB534aWTwMK4Pjg8cxFb+745qWhPfI+cYi+aYUddJQD3ilRHHXxCBD72ve7/PufqeB7Y/QlKqgg== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-polymorphic" "0.0.12" + +"@radix-ui/react-roving-focus@0.0.15": + version "0.0.15" + resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-0.0.15.tgz#9bc238ba705fbeed74ef04ebdbb77b35e5faca06" + integrity sha512-KtbWAqqxJRSxVKM+dhOc1rrq6eNWLqjmp7LVj3eKhw62nyV/z5G1lzzT54Zr5GD7+SH/QDyDvBFmPzRomXf1bQ== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "0.0.5" + "@radix-ui/react-collection" "0.0.14" + "@radix-ui/react-compose-refs" "0.0.5" + "@radix-ui/react-context" "0.0.5" + "@radix-ui/react-id" "0.0.6" + "@radix-ui/react-polymorphic" "0.0.12" + "@radix-ui/react-primitive" "0.0.14" + "@radix-ui/react-use-callback-ref" "0.0.5" + "@radix-ui/react-use-controllable-state" "0.0.6" + +"@radix-ui/react-slot@0.0.12": + version "0.0.12" + resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-0.0.12.tgz#c4d8a75fffca561aeeca2ed9603384d86757f60a" + integrity sha512-Em8P/xYyh3O/32IhrmARJNH+J/XCAVnw6h2zGu6oeReliIX7ktU67pMSeyyIZiU2hNXzaXYB/xDdixizQe/DGA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-compose-refs" "0.0.5" + +"@radix-ui/react-use-body-pointer-events@0.0.6": + version "0.0.6" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-body-pointer-events/-/react-use-body-pointer-events-0.0.6.tgz#30b21301880417e7dbb345871ff5a83f2abe0d8d" + integrity sha512-ouYb7u1+K9TsiEcNs3HceNUBUgB2PV41EyD5O6y6ZPMxl1lW/QAy5dfyfJMRyaRWQ6kxwmGoKlCSb4uPTruzuQ== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-use-layout-effect" "0.0.5" + +"@radix-ui/react-use-callback-ref@0.0.5": + version "0.0.5" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-0.0.5.tgz#fa8db050229cda573dfeeae213d74ef06f6130db" + integrity sha512-z1AI221vmq9f3vsDyrCsGLCatKagbM1YeCGdRMhMsUBzFFCaJ+Axyoe/ndVqW8wwsraGWr1zYVAhIEdlC0GvPg== + dependencies: + "@babel/runtime" "^7.13.10" + +"@radix-ui/react-use-controllable-state@0.0.6": + version "0.0.6" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-0.0.6.tgz#c4b16bc911a25889333388a684a04df937e5fec7" + integrity sha512-fBk4hUSKc4N7X/NAaifWYfKKfNuOB9xvj0MBQQYS5oOTNRgg4y8/Ax3jZ0adsplXDm7ix75sxqWm0nrvUoAjcw== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-use-callback-ref" "0.0.5" + +"@radix-ui/react-use-direction@0.0.1": + version "0.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-direction/-/react-use-direction-0.0.1.tgz#9ac72eb6d9902ed505c8a34048981d94f9433e14" + integrity sha512-sU+tkP09uEI1m+YJAR1ZAZLVFY1h/JD+jLSSQt5Wo3b9SYrJA889i2hH1P3DNRyWbbbisweiEQdK3MWILhFCig== + dependencies: + "@babel/runtime" "^7.13.10" + +"@radix-ui/react-use-escape-keydown@0.0.6": + version "0.0.6" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-0.0.6.tgz#1ad1c81b99961b7dbe376ef54151ebc8bef627a0" + integrity sha512-MJpVj21BYwWllmp2xbXPqpKPssJ1WWrZi+Qx7PY5hVcBhQr5Jo6yKwIX677pH5Yql95ENTTT5LW3q+LVFYIISw== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-use-callback-ref" "0.0.5" + +"@radix-ui/react-use-layout-effect@0.0.5": + version "0.0.5" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-0.0.5.tgz#cbbd059090edc765749da00d9f562a9abd43cbac" + integrity sha512-bNPW2JNOr/p2hXr0hfKKqrEy5deNSRF17sw3l9Z7qlEnvIbBtQ7iwY/wrxIz5P7XFyYGoXodIUDH5G8PEucE3A== + dependencies: + "@babel/runtime" "^7.13.10" + +"@radix-ui/react-use-rect@0.0.7": + version "0.0.7" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-0.0.7.tgz#e3a55fa7183ef436042198787bf38f8c9befcc14" + integrity sha512-OmaeFTgyiGNAchaxzDu+kFLz4Ly8RUcT5nwfoz4Nddd86I8Zdq93iNFnOpVLoVYqBnFEmvR6zexHXNFATrMbbQ== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/rect" "0.0.5" + +"@radix-ui/react-use-size@0.0.6": + version "0.0.6" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-0.0.6.tgz#998eaf6e8871b868f81f3b7faac06c3e896c37a0" + integrity sha512-kP4RIb2I5oHQzwzXJ21Hu8htNqf+sdaRzywxQpbj+hmqeUhpvIkhoq+ShNWV9wE/3c1T7gPnka8/nKYsKaKdCg== + dependencies: + "@babel/runtime" "^7.13.10" + +"@radix-ui/rect@0.0.5": + version "0.0.5" + resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-0.0.5.tgz#6000d8d800288114af4bbc5863e6b58755d7d978" + integrity sha512-gXw171KbjyttA7K1DRIvPguLmKsg8raitB67MIcsdZwcquy+a1O2w3xY21NIKEqGhJwqJkECPUmMJDXgMNYuAg== + dependencies: + "@babel/runtime" "^7.13.10" + "@react-dnd/asap@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-4.0.0.tgz#b300eeed83e9801f51bd66b0337c9a6f04548651" @@ -2012,6 +2372,18 @@ resolved "https://registry.yarnpkg.com/@tommoor/remove-markdown/-/remove-markdown-0.3.2.tgz#5288ddd0e26b6b173e76ebb31c94653b0dcff45d" integrity sha512-awcc9hfLZqyyZHOGzAHbnjgZJpQGS1W1oZZ5GXOTTnbKVdKQ4OWYbrRWPUvXI2YAKJazrcS8rxPh67PX3rpGkQ== +"@types/accepts@*": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/accepts/-/accepts-1.3.5.tgz#c34bec115cfc746e04fe5a059df4ce7e7b391575" + integrity sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ== + dependencies: + "@types/node" "*" + +"@types/async-lock@^1.1.2": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@types/async-lock/-/async-lock-1.1.3.tgz#0d86017cf87abbcb941c55360e533d37a3f23b3d" + integrity sha512-UpeDcjGKsYEQMeqEbfESm8OWJI305I7b9KE4ji3aBjoKWyN5CTdn8izcA1FM1DVDne30R5fNEnIy89vZw5LXJQ== + "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7": version "7.1.12" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.12.tgz#4d8e9e51eb265552a7e4f1ff2219ab6133bdfb2d" @@ -2045,6 +2417,14 @@ dependencies: "@babel/types" "^7.3.0" +"@types/body-parser@*": + version "1.19.1" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.1.tgz#0c0174c42a7d017b818303d4b5d969cb0b75929c" + integrity sha512-a6bTJ21vFOGIkwM0kzh9Yr89ziVxq4vYH2fQ6N8AeipEzai/cFK6aGMArIkUeIdRIgpwQa+2bXiLuUJCpSf2Cg== + dependencies: + "@types/connect" "*" + "@types/node" "*" + "@types/cacheable-request@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.1.tgz#5d22f3dded1fd3a84c0bbeb5039a7419c2c91976" @@ -2055,6 +2435,28 @@ "@types/node" "*" "@types/responselike" "*" +"@types/connect@*": + version "3.4.35" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" + integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ== + dependencies: + "@types/node" "*" + +"@types/content-disposition@*": + version "0.5.4" + resolved "https://registry.yarnpkg.com/@types/content-disposition/-/content-disposition-0.5.4.tgz#de48cf01c79c9f1560bcfd8ae43217ab028657f8" + integrity sha512-0mPF08jn9zYI0n0Q/Pnz7C4kThdSt+6LD4amsrYDDpgBfrVWa3TcCOxKX1zkGgYniGagRv8heN2cbh+CAn+uuQ== + +"@types/cookies@*": + version "0.7.7" + resolved "https://registry.yarnpkg.com/@types/cookies/-/cookies-0.7.7.tgz#7a92453d1d16389c05a5301eef566f34946cfd81" + integrity sha512-h7BcvPUogWbKCzBR2lY4oqaZbO3jXZksexYJVFvkrFeLgbZjQkU4x8pRq6eg2MHXQhY0McQdqmmsxRWlVAHooA== + dependencies: + "@types/connect" "*" + "@types/express" "*" + "@types/keygrip" "*" + "@types/node" "*" + "@types/estree@0.0.39": version "0.0.39" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" @@ -2065,6 +2467,25 @@ resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== +"@types/express-serve-static-core@^4.17.18": + version "4.17.24" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.24.tgz#ea41f93bf7e0d59cd5a76665068ed6aab6815c07" + integrity sha512-3UJuW+Qxhzwjq3xhwXm2onQcFHn76frIYVbTu+kn24LFxI+dEhdfISDFovPB8VpEgW8oQCTpRuCe+0zJxB7NEA== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + +"@types/express@*": + version "4.17.13" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034" + integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.18" + "@types/qs" "*" + "@types/serve-static" "*" + "@types/formidable@^1.0.31": version "1.0.31" resolved "https://registry.yarnpkg.com/@types/formidable/-/formidable-1.0.31.tgz#274f9dc2d0a1a9ce1feef48c24ca0859e7ec947b" @@ -2087,11 +2508,21 @@ dependencies: "@types/unist" "*" +"@types/http-assert@*": + version "1.5.2" + resolved "https://registry.yarnpkg.com/@types/http-assert/-/http-assert-1.5.2.tgz#a7fb59a7ca366e141789a084555a633801b9af3b" + integrity sha512-Ddzuzv/bB2prZnJKlS1sEYhaeT50wfJjhcTTTQLjEsEZJlk3XB4Xohieyq+P4VXIzg7lrQ1Spd/PfRnBpQsdqA== + "@types/http-cache-semantics@*": version "4.0.0" resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz#9140779736aa2655635ee756e2467d787cfe8a2a" integrity sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A== +"@types/http-errors@*": + version "1.8.1" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-1.8.1.tgz#e81ad28a60bee0328c6d2384e029aec626f1ae67" + integrity sha512-e+2rjEwK6KDaNOm5Aa9wNGgyS9oSZU/4pfSMMPYNOfjvFI0WVXm29+ITRFr6aKDvvKo7uU1jV68MW4ScsfDi7Q== + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.3" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762" @@ -2121,6 +2552,11 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= +"@types/keygrip@*": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.2.tgz#513abfd256d7ad0bf1ee1873606317b33b1b2a72" + integrity sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw== + "@types/keyv@*", "@types/keyv@^3.1.1": version "3.1.1" resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.1.tgz#e45a45324fca9dab716ab1230ee249c9fb52cfa7" @@ -2128,11 +2564,37 @@ dependencies: "@types/node" "*" +"@types/koa-compose@*": + version "3.2.5" + resolved "https://registry.yarnpkg.com/@types/koa-compose/-/koa-compose-3.2.5.tgz#85eb2e80ac50be95f37ccf8c407c09bbe3468e9d" + integrity sha512-B8nG/OoE1ORZqCkBVsup/AKcvjdgoHnfi4pZMn5UwAPCbhk/96xyv284eBYW8JlQbQ7zDmnpFr68I/40mFoIBQ== + dependencies: + "@types/koa" "*" + +"@types/koa@*", "@types/koa@^2.13.1": + version "2.13.4" + resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.13.4.tgz#10620b3f24a8027ef5cbae88b393d1b31205726b" + integrity sha512-dfHYMfU+z/vKtQB7NUrthdAEiSvnLebvBjwHtfFmpZmB7em2N3WVQdHgnFq+xvyVgxW5jKDmjWfLD3lw4g4uTw== + dependencies: + "@types/accepts" "*" + "@types/content-disposition" "*" + "@types/cookies" "*" + "@types/http-assert" "*" + "@types/http-errors" "*" + "@types/keygrip" "*" + "@types/koa-compose" "*" + "@types/node" "*" + "@types/long@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w== +"@types/mime@^1": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" + integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== + "@types/node@*", "@types/node@>= 8": version "14.14.7" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.7.tgz#8ea1e8f8eae2430cf440564b98c6dfce1ec5945d" @@ -2163,6 +2625,16 @@ resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.1.5.tgz#b6ab3bba29e16b821d84e09ecfaded462b816b00" integrity sha512-UEyp8LwZ4Dg30kVU2Q3amHHyTn1jEdhCIE59ANed76GaT1Vp76DD3ZWSAxgCrw6wJ0TqeoBpqmfUHiUDPs//HQ== +"@types/qs@*": + version "6.9.7" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" + integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== + +"@types/range-parser@*": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" + integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== + "@types/resolve@1.17.1": version "1.17.1" resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" @@ -2177,6 +2649,14 @@ dependencies: "@types/node" "*" +"@types/serve-static@*": + version "1.13.10" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.10.tgz#f5e0ce8797d2d7cc5ebeda48a52c96c4fa47a8d9" + integrity sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + "@types/stack-utils@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.0.tgz#7036640b4e21cc2f259ae826ce843d277dad8cff" @@ -2187,6 +2667,18 @@ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e" integrity sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ== +"@types/uuid@^8.3.0": + version "8.3.1" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.1.tgz#1a32969cf8f0364b3d8c8af9cc3555b7805df14f" + integrity sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg== + +"@types/ws@^7.4.0": + version "7.4.7" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.7.tgz#f7c390a36f7a0679aa69de2d501319f4f8d9b702" + integrity sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww== + dependencies: + "@types/node" "*" + "@types/yargs-parser@*": version "15.0.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d" @@ -2601,6 +3093,13 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== +aria-hidden@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.1.3.tgz#bb48de18dc84787a3c6eee113709c473c64ec254" + integrity sha512-RhVWFtKH5BiGMycI72q2RAFMLQi8JP9bLuQXgR5a8Znp7P5KOIADSJeyfI8PCVxLEp067B2HbP5JIiI/PXIZeA== + dependencies: + tslib "^1.0.0" + aria-query@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b" @@ -2739,6 +3238,16 @@ async-each@^1.0.1: resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== +async-lock@^1.2.8: + version "1.3.0" + resolved "https://registry.yarnpkg.com/async-lock/-/async-lock-1.3.0.tgz#0fba111bea8b9693020857eba4f9adca173df3e5" + integrity sha512-8A7SkiisnEgME2zEedtDYPxUPzdv3x//E7n5IFktPAtMYSEAV7eNJF0rMwrVyUFj6d/8rgajLantbjcNRQYXIg== + +async@0.9.x: + version "0.9.2" + resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" + integrity sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0= + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -3458,16 +3967,16 @@ builtin-status-codes@^3.0.0: resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= -bull@^3.5.2: - version "3.18.1" - resolved "https://registry.yarnpkg.com/bull/-/bull-3.18.1.tgz#49eb8fd9844a3dc0a12a851b132e508890763a31" - integrity sha512-g3gHFZ0qMo0wpecoNmd2W+F1Gj48l6phVCTdsQPKxDk1bB7kzD0nY5FAFnBFiaWxNmh5lb5X9TMB64uNXFKFDg== +bull@^3.29.0: + version "3.29.1" + resolved "https://registry.yarnpkg.com/bull/-/bull-3.29.1.tgz#7c5d7c557ebbf856892dee576ca5a19b46bb983e" + integrity sha512-ASNnorakKCV4hmgHABfn8Ir+gy24a4gaGnXH/0bm1Msq+djOnNfM5XW7Igzsa5iTjpboWXhY9dHFVjiWRKsSGw== dependencies: cron-parser "^2.13.0" debuglog "^1.0.0" get-port "^5.1.1" - ioredis "^4.14.1" - lodash "^4.17.19" + ioredis "^4.27.0" + lodash "^4.17.21" p-timeout "^3.2.0" promise.prototype.finally "^3.1.2" semver "^7.3.2" @@ -3881,6 +4390,15 @@ cliui@^6.0.0: strip-ansi "^6.0.0" wrap-ansi "^6.2.0" +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + clone-buffer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" @@ -4103,6 +4621,30 @@ concat-stream@^1.5.0, concat-stream@^1.5.2, concat-stream@~1.6.0: readable-stream "^2.2.2" typedarray "^0.0.6" +concurrently@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-6.2.1.tgz#d880fc1d77559084732fa514092a3d5109a0d5bf" + integrity sha512-emgwhH+ezkuYKSHZQ+AkgEpoUZZlbpPVYCVv7YZx0r+T7fny1H03r2nYRebpi2DudHR4n1Rgbo2YTxKOxVJ4+g== + dependencies: + chalk "^4.1.0" + date-fns "^2.16.1" + lodash "^4.17.21" + read-pkg "^5.2.0" + rxjs "^6.6.3" + spawn-command "^0.0.2-1" + supports-color "^8.1.0" + tree-kill "^1.2.2" + yargs "^16.2.0" + +condense-newlines@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/condense-newlines/-/condense-newlines-0.2.1.tgz#3de985553139475d32502c83b02f60684d24c55f" + integrity sha1-PemFVTE5R10yUCyDsC9gaE0kxV8= + dependencies: + extend-shallow "^2.0.1" + is-whitespace "^0.3.0" + kind-of "^3.0.2" + config-chain@^1.1.12: version "1.1.12" resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.12.tgz#0fde8d091200eb5e808caf25fe618c02f48e4efa" @@ -4133,6 +4675,13 @@ console-browserify@^1.1.0: resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== +consolidate@^0.16.0: + version "0.16.0" + resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.16.0.tgz#a11864768930f2f19431660a65906668f5fbdc16" + integrity sha512-Nhl1wzCslqXYTJVDyJCu3ODohy9OfBMB5uD2BiBTzd7w+QY0lBzafkR8y8755yMYHAaMD4NuzbAw03/xzfw+eQ== + dependencies: + bluebird "^3.7.2" + "consolidated-events@^1.1.0 || ^2.0.0": version "2.0.2" resolved "https://registry.yarnpkg.com/consolidated-events/-/consolidated-events-2.0.2.tgz#da8d8f8c2b232831413d9e190dc11669c79f4a91" @@ -4437,6 +4986,11 @@ cssstyle@^2.2.0: dependencies: cssom "~0.3.6" +csstype@^3.0.4: + version "3.0.8" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.8.tgz#d2266a792729fb227cd216fb572f43728e1ad340" + integrity sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw== + cyclist@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" @@ -4489,6 +5043,11 @@ date-fns@2.22.1: resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.22.1.tgz#1e5af959831ebb1d82992bf67b765052d8f0efc4" integrity sha512-yUFPQjrxEmIsMqlHhAhmxkuH769baF21Kk+nZwZGyrMoyLA+LugaQtC0+Tqf9CBUUULWwUJt6Q5ySI3LJDDCGg== +date-fns@^2.16.1: + version "2.23.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.23.0.tgz#4e886c941659af0cf7b30fafdd1eaa37e88788a9" + integrity sha512-5ycpauovVyAk0kXNZz6ZoB9AYMZB4DObse7P3BPWmyEjXNORTI8EJ6X0uaSAq4sCHzM1uajzrkr6HnsLQpxGXA== + dd-trace@^0.32.2: version "0.32.2" resolved "https://registry.yarnpkg.com/dd-trace/-/dd-trace-0.32.2.tgz#c5844f75f3c15bf88f4992b266def062a254102a" @@ -4660,12 +5219,12 @@ denque@^1.1.0, denque@^1.5.0: resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.0.tgz#773de0686ff2d8ec2ff92914316a47b73b1c73de" integrity sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ== -depd@2.0.0, depd@~2.0.0: +depd@2.0.0, depd@^2.0.0, depd@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== -depd@^1.1.2, depd@~1.1.2: +depd@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= @@ -4698,6 +5257,11 @@ detect-newline@^3.0.0: resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== +detect-node-es@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493" + integrity sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ== + dicer@0.2.5: version "0.2.5" resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.2.5.tgz#5996c086bb33218c812c090bddc09cd12facb70f" @@ -4973,6 +5537,13 @@ ejs@^2.6.1: resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.7.4.tgz#48661287573dcc53e366c7a1ae52c3a120eec9ba" integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA== +ejs@^3.1.6: + version "3.1.6" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.6.tgz#5bfd0a0689743bb5268b3550cceeebbc1702822a" + integrity sha512-9lt9Zse4hPucPkoP7FHDF0LQAlGyF9JVpnClFLFH3aSSbxmyoqINRpp/9wePWJTUl4KOQwRL72Iw3InHPDkoGw== + dependencies: + jake "^10.6.1" + electron-to-chromium@^1.3.723: version "1.3.736" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.736.tgz#f632d900a1f788dab22fec9c62ec5c9c8f0c4052" @@ -5179,9 +5750,9 @@ eol@^0.9.1: integrity sha512-Ds/TEoZjwggRoz/Q2O7SE3i4Jm66mqTDfmdHdq/7DKVk3bro9Q8h6WdXKdPqFLMoqxrDK5SVRzHVPOS6uuGtrg== errno@^0.1.3, errno@~0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618" - integrity sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg== + version "0.1.8" + resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f" + integrity sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A== dependencies: prr "~1.0.1" @@ -5808,6 +6379,13 @@ file-uri-to-path@1.0.0: resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== +filelist@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.2.tgz#80202f21462d4d1c2e214119b1807c1bc0380e5b" + integrity sha512-z7O0IS8Plc39rTCq6i6iHxk43duYOn8uFJiWSewIq0Bww1RNybVHSCjahmcC87ZqAm4OTvFzlzeGu3XAzG1ctQ== + dependencies: + minimatch "^3.0.4" + fill-range@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" @@ -6174,7 +6752,7 @@ gensync@^1.0.0-beta.1: resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== -get-caller-file@^2.0.1: +get-caller-file@^2.0.1, get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== @@ -6188,6 +6766,11 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.1: has "^1.0.3" has-symbols "^1.0.1" +get-nonce@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3" + integrity sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q== + get-own-enumerable-property-symbols@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" @@ -6198,6 +6781,13 @@ get-package-type@^0.1.0: resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== +get-paths@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/get-paths/-/get-paths-0.0.7.tgz#15331086752077cf130166ccd233a1cdbeefcf38" + integrity sha512-0wdJt7C1XKQxuCgouqd+ZvLJ56FQixKoki9MrFaO4EriqzXOiH9gbukaDE1ou08S8Ns3/yDzoBAISNPqj6e6tA== + dependencies: + pify "^4.0.1" + get-port@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193" @@ -6784,7 +7374,7 @@ http-errors@1.7.3, http-errors@~1.7.2: statuses ">= 1.5.0 < 2" toidentifier "1.0.0" -http-errors@^1.3.1, http-errors@^1.6.1, http-errors@^1.6.3: +http-errors@^1.3.1, http-errors@^1.6.1, http-errors@^1.6.3, http-errors@^1.7.3: version "1.8.0" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.0.tgz#75d1bbe497e1044f51e4ee9e704a62f28d336507" integrity sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A== @@ -7034,23 +7624,24 @@ interpret@^1.0.0, interpret@^1.4.0: resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== -invariant@^2.2.1, invariant@^2.2.2: +invariant@^2.2.1, invariant@^2.2.2, invariant@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== dependencies: loose-envify "^1.0.0" -ioredis@^4.14.1, ioredis@^4.24.3: - version "4.24.3" - resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.24.3.tgz#ba14c3621f751727f1b6c41c55ab26a7794018d5" - integrity sha512-ANE2YT2fCa+KE0EUmx8VMZuJ+LaTNVXhjyAUDAxom9nqJGAXzCbNBMcujrUSJbz6xc2ZMaMxGB5y10cfYo/0IA== +ioredis@^4.24.3, ioredis@^4.27.0: + version "4.27.8" + resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.27.8.tgz#822c2d1ac44067a8f7b92fb673070fc9d661c56e" + integrity sha512-AcMEevap2wKxNcYEybZ/Qp+MR2HbNNUwGjG4sVCC3cAJ/zR9HXKAkolXOuR6YcOGPf7DHx9mWb/JKtAGujyPow== dependencies: cluster-key-slot "^1.1.0" debug "^4.3.1" denque "^1.1.0" lodash.defaults "^4.2.0" lodash.flatten "^4.4.0" + lodash.isarguments "^3.1.0" p-map "^2.1.0" redis-commands "1.7.0" redis-errors "^1.2.0" @@ -7447,6 +8038,11 @@ is-valid-glob@^1.0.0: resolved "https://registry.yarnpkg.com/is-valid-glob/-/is-valid-glob-1.0.0.tgz#29bf3eff701be2d4d315dbacc39bc39fe8f601aa" integrity sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao= +is-whitespace@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/is-whitespace/-/is-whitespace-0.3.0.tgz#1639ecb1be036aec69a54cbb401cfbed7114ab7f" + integrity sha1-Fjnssb4DauxppUy7QBz77XEUq38= + is-windows@^1.0.1, is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" @@ -7504,6 +8100,11 @@ isomorphic-fetch@^3.0.0: node-fetch "^2.6.1" whatwg-fetch "^3.4.1" +isomorphic.js@^0.2.4: + version "0.2.4" + resolved "https://registry.yarnpkg.com/isomorphic.js/-/isomorphic.js-0.2.4.tgz#24ca374163ae54a7ce3b86ce63b701b91aa84969" + integrity sha512-Y4NjZceAwaPXctwsHgNsmfuPxR8lJ3f8X7QTAkhltrX4oGIv+eTlgHLXn4tWysC9zGTi929gapnPp+8F8cg7nA== + isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" @@ -7550,6 +8151,16 @@ istanbul-reports@^3.0.2: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" +jake@^10.6.1: + version "10.8.2" + resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.2.tgz#ebc9de8558160a66d82d0eadc6a2e58fbc500a7b" + integrity sha512-eLpKyrfG3mzvGE2Du8VoPbeSkRry093+tyNjdYaBbJS9v17knImYGNXQCUV0gLxQtF82m3E8iRb/wdSQZLoq7A== + dependencies: + async "0.9.x" + chalk "^2.4.2" + filelist "^1.0.1" + minimatch "^3.0.4" + java-properties@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/java-properties/-/java-properties-1.0.2.tgz#ccd1fa73907438a5b5c38982269d0e771fe78211" @@ -7953,15 +8564,14 @@ jpeg-js@0.4.2: resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.2.tgz#8b345b1ae4abde64c2da2fe67ea216a114ac279d" integrity sha512-+az2gi/hvex7eLTMTlbRLOhH6P6WFdk2ITI8HJsaH2VqYO0I594zXSYEP+tf4FW+8Cy68ScDXoAsQdyQanv3sw== -js-beautify@^1.8.8: - version "1.13.0" - resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.13.0.tgz#a056d5d3acfd4918549aae3ab039f9f3c51eebb2" - integrity sha512-/Tbp1OVzZjbwzwJQFIlYLm9eWQ+3aYbBXLSaqb1mEJzhcQAfrqMMQYtjb6io+U6KpD0ID4F+Id3/xcjH3l/sqA== +js-beautify@^1.6.12, js-beautify@^1.8.8: + version "1.14.0" + resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.14.0.tgz#2ce790c555d53ce1e3d7363227acf5dc69024c2d" + integrity sha512-yuck9KirNSCAwyNJbqW+BxJqJ0NLJ4PwBUzQQACl5O3qHMBXVkXb/rD0ilh/Lat/tn88zSZ+CAHOlk0DsY7GuQ== dependencies: config-chain "^1.1.12" editorconfig "^0.15.3" glob "^7.1.3" - mkdirp "^1.0.4" nopt "^5.0.0" js-search@^1.4.2: @@ -8278,6 +8888,14 @@ koa-convert@1.2.0, koa-convert@^1.2.0: co "^4.6.0" koa-compose "^3.0.0" +koa-easy-ws@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/koa-easy-ws/-/koa-easy-ws-1.3.0.tgz#dbf8eeb126ca11eed4c4e3af7904a47c25be2347" + integrity sha512-06lHAwm25HBdplTpwHiZqKcl39vS4KiVzRPneUExCHVvQ9TPHmFlUFO/gjQrUdLj3+kvqQpqdlIPNb91wn6EwA== + dependencies: + debug "^4.1.1" + ws "^7.3.1" + koa-helmet@5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/koa-helmet/-/koa-helmet-5.2.0.tgz#6529f64dd4539261a9bb0a56e201e4976f0200f0" @@ -8317,6 +8935,14 @@ koa-mount@^3.0.0: debug "^2.6.1" koa-compose "^3.2.1" +koa-mount@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/koa-mount/-/koa-mount-4.0.0.tgz#e0265e58198e1a14ef889514c607254ff386329c" + integrity sha512-rm71jaA/P+6HeCpoRhmCv8KVBIi0tfGuO/dMKicbQnQW/YJntJ6MnnspkodoA4QstMVEZArsCphmd0bJEtoMjQ== + dependencies: + debug "^4.0.1" + koa-compose "^4.1.0" + koa-onerror@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/koa-onerror/-/koa-onerror-4.1.0.tgz#7949c7651941e67b11813bf1fad03c2d34470b1c" @@ -8336,6 +8962,17 @@ koa-router@7.0.1: methods "^1.0.1" path-to-regexp "^1.1.1" +koa-router@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/koa-router/-/koa-router-10.0.0.tgz#7bc76a031085731e61fc92c1683687b2f44de6a4" + integrity sha512-gAE5J1gBQTvfR8rMMtMUkE26+1MbO3DGpGmvfmM2pR9Z7w2VIb2Ecqeal98yVO7+4ltffby7gWOzpCmdNOQe0w== + dependencies: + debug "^4.1.1" + http-errors "^1.7.3" + koa-compose "^4.1.0" + methods "^1.1.2" + path-to-regexp "^6.1.0" + koa-send@^4.1.3: version "4.1.3" resolved "https://registry.yarnpkg.com/koa-send/-/koa-send-4.1.3.tgz#0822207bbf5253a414c8f1765ebc29fa41353cb6" @@ -8346,6 +8983,15 @@ koa-send@^4.1.3: mz "^2.6.0" resolve-path "^1.4.0" +koa-send@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/koa-send/-/koa-send-5.0.1.tgz#39dceebfafb395d0d60beaffba3a70b4f543fe79" + integrity sha512-tmcyQ/wXXuxpDxyNXv5yNNkdAMdFRqwtegBXUaowiQzUKqJehttS0x2j0eOZDQAyloAth5w6wwBImnFzkUz3pQ== + dependencies: + debug "^4.1.1" + http-errors "^1.7.3" + resolve-path "^1.4.0" + koa-sendfile@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/koa-sendfile/-/koa-sendfile-2.0.0.tgz#446e2a35ba7074ed03afda2c667c2359406e1082" @@ -8368,11 +9014,33 @@ koa-static@^4.0.1: debug "^3.1.0" koa-send "^4.1.3" +koa-static@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/koa-static/-/koa-static-5.0.0.tgz#5e92fc96b537ad5219f425319c95b64772776943" + integrity sha512-UqyYyH5YEXaJrf9S8E23GoJFQZXkBVJ9zYYMPGz919MSX1KuvAcycIuS0ci150HCoPf4XQVhQ84Qf8xRPWxFaQ== + dependencies: + debug "^3.1.0" + koa-send "^5.0.0" + koa-unless@1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/koa-unless/-/koa-unless-1.0.7.tgz#b9df375e2b4da3043918d48622520c2c0b79f032" integrity sha1-ud83XitNowQ5GNSGIlIMLAt58DI= +koa-views@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/koa-views/-/koa-views-7.0.1.tgz#0c8f8e65d5cd2e08249430cb83dc361e49a17a5a" + integrity sha512-yS8751DXHXXDbdl/oUZd0PsgnxR0MLiguu77Eqrgu6yawE9Hi99wNKiVENb0Kfgsmvq/8px7YCI+USgxaTB1LA== + dependencies: + "@types/koa" "^2.13.1" + consolidate "^0.16.0" + debug "^4.1.0" + get-paths "0.0.7" + koa-send "^5.0.0" + mz "^2.4.0" + pretty "^2.0.0" + resolve-path "^1.4.0" + koa-webpack-dev-middleware@^1.4.5: version "1.4.6" resolved "https://registry.yarnpkg.com/koa-webpack-dev-middleware/-/koa-webpack-dev-middleware-1.4.6.tgz#6ec20d3648c3c80b5edb0b721a6838f66a1fc47a" @@ -8387,10 +9055,10 @@ koa-webpack-hot-middleware@^1.0.3: dependencies: webpack-hot-middleware "2.x" -koa@^2.10.0: - version "2.13.0" - resolved "https://registry.yarnpkg.com/koa/-/koa-2.13.0.tgz#25217e05efd3358a7e5ddec00f0a380c9b71b501" - integrity sha512-i/XJVOfPw7npbMv67+bOeXr3gPqOAw6uh5wFyNs3QvJ47tUx3M3V9rIE0//WytY42MKz4l/MXKyGkQ2LQTfLUQ== +koa@^2.10.0, koa@^2.13.1: + version "2.13.1" + resolved "https://registry.yarnpkg.com/koa/-/koa-2.13.1.tgz#6275172875b27bcfe1d454356a5b6b9f5a9b1051" + integrity sha512-Lb2Dloc72auj5vK4X4qqL7B5jyDPQaZucc9sR/71byg7ryoD1NCaCm63CShk9ID9quQvDEi1bGR/iGjCG7As3w== dependencies: accepts "^1.3.5" cache-content-type "^1.0.0" @@ -8399,7 +9067,7 @@ koa@^2.10.0: cookies "~0.8.0" debug "~3.1.0" delegates "^1.0.0" - depd "^1.1.2" + depd "^2.0.0" destroy "^1.0.4" encodeurl "^1.0.2" escape-html "^1.0.3" @@ -8475,6 +9143,13 @@ levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +lib0@^0.2.34, lib0@^0.2.35, lib0@^0.2.41, lib0@^0.2.42: + version "0.2.42" + resolved "https://registry.yarnpkg.com/lib0/-/lib0-0.2.42.tgz#6d8bf1fb8205dec37a953c521c5ee403fd8769b0" + integrity sha512-8BNM4MiokEKzMvSxTOC3gnCBisJH+jL67CnSnqzHv3jli3pUvGC8wz+0DQ2YvGr4wVQdb2R2uNNPw9LEpVvJ4Q== + dependencies: + isomorphic.js "^0.2.4" + lie@~3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" @@ -8737,6 +9412,11 @@ lodash.includes@^4.3.0: resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8= +lodash.isarguments@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" + integrity sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo= + lodash.isboolean@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" @@ -9344,7 +10024,7 @@ multer@^1.4.2: type-is "^1.6.4" xtend "^4.0.0" -mz@2, mz@^2.6.0: +mz@2, mz@^2.4.0, mz@^2.6.0: version "2.7.0" resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q== @@ -9814,10 +10494,10 @@ os-name@^3.1.0: macos-release "^2.2.0" windows-release "^3.1.0" -outline-icons@^1.26.1, outline-icons@^1.30.0: - version "1.30.0" - resolved "https://registry.yarnpkg.com/outline-icons/-/outline-icons-1.30.0.tgz#e6bb779d43fc89df4b7bda8dfe886c28c72e494d" - integrity sha512-JLL0hiBB7cKBrpQ7ejvLORUVYNpx2ULngoWJHL8vpsMwMha+ChF7z1MmJQ1dD6/yDX3dgIzeMfMASD8weVRloQ== +outline-icons@^1.26.1, outline-icons@^1.31.0: + version "1.31.0" + resolved "https://registry.yarnpkg.com/outline-icons/-/outline-icons-1.31.0.tgz#c373667442300afdb1a9fd6059180833c6123fcb" + integrity sha512-pe2vSI1mR4bJaWpmTitniyNpu4oq8dVObOweh/H6EON+12w2QDRtJZ6A6Bkc2vjlb2ZRYcZasisJI9VBUuTnHQ== oy-vey@^0.10.0: version "0.10.0" @@ -10206,6 +10886,11 @@ path-to-regexp@^1.1.1, path-to-regexp@^1.7.0: dependencies: isarray "0.0.1" +path-to-regexp@^6.1.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.2.0.tgz#f7b3803336104c346889adece614669230645f38" + integrity sha512-f66KywYG6+43afgE/8j/GoiNyygk/bnoCbps++3ErRKsIYkGGupyv07R2Ok5m9i67Iqc+T2g1eAUGUPzWhYTyg== + path-type@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" @@ -10476,6 +11161,15 @@ pretty-format@^26.6.2: ansi-styles "^4.0.0" react-is "^17.0.1" +pretty@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pretty/-/pretty-2.0.0.tgz#adbc7960b7bbfe289a557dc5f737619a220d06a5" + integrity sha1-rbx5YLe7/iiaVX3F9zdhmiINBqU= + dependencies: + condense-newlines "^0.2.1" + extend-shallow "^2.0.1" + js-beautify "^1.6.12" + prismjs@~1.23.0: version "1.23.0" resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.23.0.tgz#d3b3967f7d72440690497652a9d40ff046067f33" @@ -11021,6 +11715,25 @@ react-refresh@^0.9.0: resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.9.0.tgz#71863337adc3e5c2f8a6bfddd12ae3bfe32aafbf" integrity sha512-Gvzk7OZpiqKSkxsQvO/mbTN1poglhmAV7gR/DdIrRrSMXraRQQlfikRJOr3Nb9GTMPC5kof948Zy6jJZIFtDvQ== +react-remove-scroll-bar@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.2.0.tgz#d4d545a7df024f75d67e151499a6ab5ac97c8cdd" + integrity sha512-UU9ZBP1wdMR8qoUs7owiVcpaPwsQxUDC2lypP6mmixaGlARZa7ZIBx1jcuObLdhMOvCsnZcvetOho0wzPa9PYg== + dependencies: + react-style-singleton "^2.1.0" + tslib "^1.0.0" + +react-remove-scroll@^2.4.0: + version "2.4.3" + resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.4.3.tgz#83d19b02503b04bd8141ed6e0b9e6691a2e935a6" + integrity sha512-lGWYXfV6jykJwbFpsuPdexKKzp96f3RbvGapDSIdcyGvHb7/eqyn46C7/6h+rUzYar1j5mdU+XECITHXCKBk9Q== + dependencies: + react-remove-scroll-bar "^2.1.0" + react-style-singleton "^2.1.0" + tslib "^1.0.0" + use-callback-ref "^1.2.3" + use-sidecar "^1.0.1" + react-router-dom@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.2.0.tgz#9e65a4d0c45e13289e66c7b17c7e175d0ea15662" @@ -11055,6 +11768,15 @@ react-side-effect@^2.1.0: resolved "https://registry.yarnpkg.com/react-side-effect/-/react-side-effect-2.1.1.tgz#66c5701c3e7560ab4822a4ee2742dee215d72eb3" integrity sha512-2FoTQzRNTncBVtnzxFOk2mCpcfxQpenBMbk5kSVBg5UcPqV9fRbgY2zhb7GTWWOlpFmAxhClBDlIq8Rsubz1yQ== +react-style-singleton@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.1.1.tgz#ce7f90b67618be2b6b94902a30aaea152ce52e66" + integrity sha512-jNRp07Jza6CBqdRKNgGhT3u9umWvils1xsuMOjZlghBDH2MU0PL2WZor4PGYjXpnRCa9DQSlHMs/xnABWOwYbA== + dependencies: + get-nonce "^1.0.0" + invariant "^2.2.4" + tslib "^1.0.0" + react-table@^7.7.0: version "7.7.0" resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.7.0.tgz#e2ce14d7fe3a559f7444e9ecfe8231ea8373f912" @@ -11239,6 +11961,13 @@ redis-errors@^1.0.0, redis-errors@^1.2.0: resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" integrity sha1-62LSrbFeTq9GEMBK/hUpOEJQq60= +redis-info@^3.0.8: + version "3.0.8" + resolved "https://registry.yarnpkg.com/redis-info/-/redis-info-3.0.8.tgz#27e696778c100d716324fa68b92cccf390487e02" + integrity sha512-L7yPuGzRq+gu+ZYl/aO0TDgc4nNcMpDTaTN4P3bBi8ZENp1fk8gvtZQpidrYL5uAJYMIcMN81fgUz28qUpTeVA== + dependencies: + lodash "^4.17.11" + redis-parser@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4" @@ -11589,10 +12318,10 @@ retry-as-promised@^3.2.0: dependencies: any-promise "^1.3.0" -rich-markdown-editor@^11.17.4: - version "11.17.4" - resolved "https://registry.yarnpkg.com/rich-markdown-editor/-/rich-markdown-editor-11.17.4.tgz#a3ce79ad74291a792526a488f183a3aa047603ce" - integrity sha512-T9i9jGeq8aOyEl/3u+K169/kCpaOYh+kVwRDFiZfR13DVQxGKrK/MQ9VXjX6yxTBTEDIGy2RSuYxUUjCnJ25RA== +rich-markdown-editor@^11.17.5: + version "11.17.5" + resolved "https://registry.yarnpkg.com/rich-markdown-editor/-/rich-markdown-editor-11.17.5.tgz#90041c4b2b6fed64dc7402aac6dec378cc4a7c22" + integrity sha512-NhKmQyOqV0FYK4BMjIdjWLC2J0NGEibebWVyJgJE43fLIFm4LOg5ql4QszsOFLX79CVVBPAysHCFYns+XJ+qGw== dependencies: copy-to-clipboard "^3.0.8" lodash "^4.17.11" @@ -11717,6 +12446,13 @@ run-queue@^1.0.0, run-queue@^1.0.3: dependencies: aproba "^1.1.1" +rxjs@^6.6.3: + version "6.6.7" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" + integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== + dependencies: + tslib "^1.9.0" + safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" @@ -12318,6 +13054,11 @@ space-separated-tokens@^1.0.0: resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz#85f32c3d10d9682007e917414ddc5c26d1aa6899" integrity sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA== +spawn-command@^0.0.2-1: + version "0.0.2-1" + resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2-1.tgz#62f5e9466981c1b796dc5929937e11c9c6921bd0" + integrity sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A= + spdx-correct@^3.0.0: version "3.1.1" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" @@ -12733,6 +13474,13 @@ supports-color@^7.0.0, supports-color@^7.1.0: dependencies: has-flag "^4.0.0" +supports-color@^8.1.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + supports-hyperlinks@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.1.0.tgz#f663df252af5f37c5d49bbd7eeefa9e0b9e59e47" @@ -13141,6 +13889,11 @@ tr46@^2.0.2: resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9" integrity sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk= +tree-kill@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" + integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== + tsconfig-paths@^3.9.0: version "3.9.0" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b" @@ -13151,7 +13904,7 @@ tsconfig-paths@^3.9.0: minimist "^1.2.0" strip-bom "^3.0.0" -tslib@^1.9.0, tslib@^1.9.3: +tslib@^1.0.0, tslib@^1.9.0, tslib@^1.9.3: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== @@ -13557,6 +14310,19 @@ url@^0.11.0: punycode "1.3.2" querystring "0.2.0" +use-callback-ref@^1.2.3: + version "1.2.5" + resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.2.5.tgz#6115ed242cfbaed5915499c0a9842ca2912f38a5" + integrity sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg== + +use-sidecar@^1.0.1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.0.5.tgz#ffff2a17c1df42e348624b699ba6e5c220527f2b" + integrity sha512-k9jnrjYNwN6xYLj1iaGhonDghfvmeTmYjAiGvOr7clwKfPjMXJf4/HOr7oT5tJwYafgp2tG2l3eZEOfoELiMcA== + dependencies: + detect-node-es "^1.1.0" + tslib "^1.9.3" + use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" @@ -14226,6 +14992,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -14257,7 +15032,12 @@ write@1.0.3: dependencies: mkdirp "^0.5.1" -ws@^7.2.3, ws@~7.4.2: +ws@^7.2.3, ws@^7.3.1, ws@^7.4.3: + version "7.5.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.3.tgz#160835b63c7d97bfab418fc1b8a9fced2ac01a74" + integrity sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg== + +ws@~7.4.2: version "7.4.6" resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== @@ -14338,11 +15118,37 @@ xtend@^4.0.0, xtend@~4.0.0, xtend@~4.0.1: resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== +y-indexeddb@^9.0.6: + version "9.0.6" + resolved "https://registry.yarnpkg.com/y-indexeddb/-/y-indexeddb-9.0.6.tgz#49aecac11bc229571fb134e0ec0717c0330b731f" + integrity sha512-8mdCYdzZDWS2lGiB9Reaz67ZqvnV6EXH/F7L+TmBC+3mWjIBrPw4UcI79nOhEOh+y9lHXzNpSda4YJ06M13F1A== + dependencies: + lib0 "^0.2.35" + +y-prosemirror@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/y-prosemirror/-/y-prosemirror-1.0.9.tgz#c0b5bf4e2c6620093ba0658c2aca52055346a683" + integrity sha512-OM12aPx04lwiIy1IOBidb6ONAof2KFxQE/Gww26SEsMQuA2dibrJkjaMwXwY1KnYY7yOpwbIFRdwecdNXLU9yQ== + dependencies: + lib0 "^0.2.34" + +y-protocols@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/y-protocols/-/y-protocols-1.0.5.tgz#91d574250060b29fcac8f8eb5e276fbad594245e" + integrity sha512-Wil92b7cGk712lRHDqS4T90IczF6RkcvCwAD0A2OPg+adKmOe+nOiT/N2hvpQIWS3zfjmtL4CPaH5sIW1Hkm/A== + dependencies: + lib0 "^0.2.42" + y18n@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + yallist@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" @@ -14387,6 +15193,11 @@ yargs-parser@^18.1.2: camelcase "^5.0.0" decamelize "^1.2.0" +yargs-parser@^20.2.2: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + yargs@^13.1.0, yargs@^13.3.2: version "13.3.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" @@ -14420,6 +15231,19 @@ yargs@^15.1.0, yargs@^15.4.1: y18n "^4.0.0" yargs-parser "^18.1.2" +yargs@^16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + yarn-deduplicate@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/yarn-deduplicate/-/yarn-deduplicate-3.1.0.tgz#3018d93e95f855f236a215b591fe8bc4bcabba3e" @@ -14434,6 +15258,13 @@ yeast@0.1.2: resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" integrity sha1-AI4G2AlDIMNy28L47XagymyKxBk= +yjs@^13.5.0, yjs@^13.5.12, yjs@^13.5.8: + version "13.5.12" + resolved "https://registry.yarnpkg.com/yjs/-/yjs-13.5.12.tgz#7a0cf3119fb368c07243825e989a55de164b3f9c" + integrity sha512-/buy1kh8Ls+t733Lgov9hiNxCsjHSCymTuZNahj2hsPNoGbvnSdDmCz9Z4F19Yr1eUAAXQLJF3q7fiBcvPC6Qg== + dependencies: + lib0 "^0.2.41" + ylru@^1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/ylru/-/ylru-1.2.1.tgz#f576b63341547989c1de7ba288760923b27fe84f"