mirror of
https://github.com/outline/outline.git
synced 2026-06-13 11:25:03 +03:00
Compare commits
107 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5690c5e9fd | |||
| ae1e115539 | |||
| a4e2054017 | |||
| 9189d08118 | |||
| 0a998789a3 | |||
| af2aefa8f5 | |||
| 77c86ddbfb | |||
| 7aaec29af6 | |||
| 92016bbd06 | |||
| 231ab2da03 | |||
| bd880ee984 | |||
| 995c6f90b7 | |||
| 8ac853bb8b | |||
| 26c14bcead | |||
| 55c099b895 | |||
| ebcb303d0e | |||
| befa36231c | |||
| 93611b1deb | |||
| 0a07a0dd26 | |||
| 2f5cf90cb7 | |||
| c709e54738 | |||
| 47953b3354 | |||
| d96099b5b8 | |||
| 4b2bf28531 | |||
| a3df9e868f | |||
| 136d7a85d7 | |||
| 3b4a457da9 | |||
| 476b5e03f9 | |||
| 23a6459ae8 | |||
| 2a1bc4934c | |||
| 3e0cd3b3aa | |||
| 2e721f90ea | |||
| 4929fbaccb | |||
| 26835cb7c4 | |||
| 08a8fea69a | |||
| 2024c6e64f | |||
| 3dad744179 | |||
| 3dfd336f59 | |||
| 9a875920ac | |||
| dcb80ff865 | |||
| f389ac6414 | |||
| a4ec33e80c | |||
| 8d343fae6d | |||
| 9744d5e746 | |||
| e4b7aa6761 | |||
| 00ba65f3ef | |||
| 120c699db1 | |||
| 3964c83ba6 | |||
| 0158ab7ae8 | |||
| 19cad30ac8 | |||
| 28aef82af9 | |||
| 86f008293a | |||
| 835fd26a95 | |||
| cc9468e2c5 | |||
| 22ba4d0f48 | |||
| d335670b91 | |||
| cabaee2d0a | |||
| f6d889f759 | |||
| a50471959b | |||
| d8ad2fc1a2 | |||
| 781ab7f5aa | |||
| 0c48227b57 | |||
| 72da0653cc | |||
| aa915ec52b | |||
| 32792caca8 | |||
| 7e1c5b7640 | |||
| 14974facc9 | |||
| e613ec732b | |||
| 83fcef1491 | |||
| 716ff572be | |||
| 0be40609ed | |||
| ec8fde0a5f | |||
| 80ff682ec6 | |||
| 4973298f24 | |||
| 32156a29b2 | |||
| 1579ce7c52 | |||
| 4cd9bd26d0 | |||
| 8bc77e54d3 | |||
| 9ef44ff67d | |||
| 2c52a8cb8b | |||
| 1db31eed41 | |||
| 8ba8013c6a | |||
| 1521d4dbac | |||
| a1a4fd1baf | |||
| 8843c789c5 | |||
| 31f4424018 | |||
| ee2e776175 | |||
| dbffde315e | |||
| 58b3b5db5c | |||
| 1f5b83aaeb | |||
| 77db0c2e95 | |||
| 4cbae1cf7d | |||
| e985078b80 | |||
| ea72a8f1e9 | |||
| aa90d388ef | |||
| c1f832b0a2 | |||
| 14596a11e8 | |||
| 06a9a6634b | |||
| 6f684037e2 | |||
| 2ddf0b3f7a | |||
| c8fb988d6d | |||
| 135dc356af | |||
| 859b04d6a3 | |||
| 77dfdac93f | |||
| 2fdc9b4875 | |||
| d0f4d8282c | |||
| 79b92b92aa |
+33
-15
@@ -1,6 +1,6 @@
|
||||
# 👋 Welcome, we're glad you're setting up an installation of Outline. Copy this
|
||||
# file to .env or set the variables in your local environment manually. For
|
||||
# development with docker this should mostly work out of the box other than
|
||||
# 👋 Welcome, we're glad you're setting up an installation of Outline. Copy this
|
||||
# file to .env or set the variables in your local environment manually. For
|
||||
# development with docker this should mostly work out of the box other than
|
||||
# setting the Slack keys and the SECRET_KEY.
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
# in your terminal to generate a random value.
|
||||
SECRET_KEY=generate_a_new_key
|
||||
|
||||
# Generate a unique random key. The format is not important but you could still use
|
||||
# Generate a unique random key. The format is not important but you could still use
|
||||
# `openssl rand -hex 32` in your terminal to produce this.
|
||||
UTILS_SECRET=generate_a_new_key
|
||||
|
||||
@@ -29,9 +29,13 @@ 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
|
||||
# however if you want to keep all file storage local an alternative such as
|
||||
# minio (https://github.com/minio/minio) can be used.
|
||||
|
||||
# A more detailed guide on setting up S3 is available here:
|
||||
@@ -69,24 +73,38 @@ SLACK_SECRET=get_the_secret_of_above_key
|
||||
GOOGLE_CLIENT_ID=
|
||||
GOOGLE_CLIENT_SECRET=
|
||||
|
||||
# To configure Microsoft/Azure auth, you'll need to create an OAuth Client. See
|
||||
# To configure Microsoft/Azure auth, you'll need to create an OAuth Client. See
|
||||
# the guide for details on setting up your Azure App:
|
||||
# => https://wiki.generaloutline.com/share/dfa77e56-d4d2-4b51-8ff8-84ea6608faa4
|
||||
AZURE_CLIENT_ID=
|
||||
AZURE_CLIENT_ID=
|
||||
AZURE_CLIENT_SECRET=
|
||||
AZURE_RESOURCE_APP_ID=
|
||||
|
||||
# To configure generic OIDC auth, you'll need some kind of identity provider.
|
||||
# See documentation for whichever IdP you use to acquire the following info:
|
||||
# Redirect URI is https://<URL>/auth/oidc.callback
|
||||
OIDC_CLIENT_ID=
|
||||
OIDC_CLIENT_SECRET=
|
||||
OIDC_AUTH_URI=
|
||||
OIDC_TOKEN_URI=
|
||||
OIDC_USERINFO_URI=
|
||||
|
||||
# Display name for OIDC authentication
|
||||
OIDC_DISPLAY_NAME=OpenID Connect
|
||||
|
||||
# Space separated auth scopes.
|
||||
OIDC_SCOPES=openid profile email
|
||||
|
||||
|
||||
# –––––––––––––––– OPTIONAL ––––––––––––––––
|
||||
|
||||
# If using a Cloudfront/Cloudflare distribution or similar it can be set below.
|
||||
# This will cause paths to javascript, stylesheets, and images to be updated to
|
||||
# the hostname defined in CDN_URL. In your CDN configuration the origin server
|
||||
# This will cause paths to javascript, stylesheets, and images to be updated to
|
||||
# the hostname defined in CDN_URL. In your CDN configuration the origin server
|
||||
# should be set to the same as URL.
|
||||
CDN_URL=
|
||||
|
||||
# Auto-redirect to https in production. The default is true but you may set to
|
||||
# Auto-redirect to https in production. The default is true but you may set to
|
||||
# false if you can be sure that SSL is terminated at an external loadbalancer.
|
||||
FORCE_HTTPS=true
|
||||
|
||||
@@ -104,13 +122,13 @@ MAXIMUM_IMPORT_SIZE=5120000
|
||||
|
||||
# You may enable or disable debugging categories to increase the noisiness of
|
||||
# logs. The default is a good balance
|
||||
DEBUG=cache,presenters,events,emails,mailer,utils,http,server,services
|
||||
DEBUG=cache,presenters,events,emails,mailer,utils,http,server,processors
|
||||
|
||||
# Comma separated list of domains to be allowed to signin to the wiki. If not
|
||||
# set, all domains are allowed by default when using Google OAuth to signin
|
||||
ALLOWED_DOMAINS=
|
||||
|
||||
# For a complete Slack integration with search and posting to channels the
|
||||
# For a complete Slack integration with search and posting to channels the
|
||||
# following configs are also needed, some more details
|
||||
# => https://wiki.generaloutline.com/share/be25efd1-b3ef-4450-b8e5-c4a4fc11e02a
|
||||
#
|
||||
@@ -118,13 +136,13 @@ SLACK_VERIFICATION_TOKEN=your_token
|
||||
SLACK_APP_ID=A0XXXXXXX
|
||||
SLACK_MESSAGE_ACTIONS=true
|
||||
|
||||
# Optionally enable google analytics to track pageviews in the knowledge base
|
||||
# Optionally enable google analytics to track pageviews in the knowledge base
|
||||
GOOGLE_ANALYTICS_ID=
|
||||
|
||||
# Optionally enable Sentry (sentry.io) to track errors and performance
|
||||
SENTRY_DSN=
|
||||
|
||||
# To support sending outgoing transactional emails such as "document updated" or
|
||||
# To support sending outgoing transactional emails such as "document updated" or
|
||||
# "you've been invited" you'll need to provide authentication for an SMTP server
|
||||
SMTP_HOST=
|
||||
SMTP_PORT=
|
||||
@@ -138,6 +156,6 @@ SMTP_SECURE=true
|
||||
# Custom logo that displays on the authentication screen, scaled to height: 60px
|
||||
# TEAM_LOGO=https://example.com/images/logo.png
|
||||
|
||||
# The default interface language. See translate.getoutline.com for a list of
|
||||
# The default interface language. See translate.getoutline.com for a list of
|
||||
# available language codes and their rough percentage translated.
|
||||
DEFAULT_LANGUAGE=en_US
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
.*/node_modules/yjs/.*
|
||||
.*/node_modules/y-prosemirror/.*
|
||||
.*/node_modules/y-protocols/.*
|
||||
.*/node_modules/y-indexeddb/.*
|
||||
.*/node_modules/lib0/.*
|
||||
.*/server/scripts/.*
|
||||
*.test.js
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
web: node ./build/server/index.js
|
||||
web: yarn start --services=web,websockets
|
||||
worker: yarn start --services=worker
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
|
||||
|
||||
<p align="center">
|
||||
<img src="https://user-images.githubusercontent.com/31465/34380645-bd67f474-eb0b-11e7-8d03-0151c1730654.png" height="29" />
|
||||
</p>
|
||||
@@ -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
|
||||
|
||||
@@ -128,11 +128,11 @@ export type Props = {|
|
||||
fullwidth?: boolean,
|
||||
autoFocus?: boolean,
|
||||
style?: Object,
|
||||
as?: React.ComponentType<any>,
|
||||
as?: React.ComponentType<any> | string,
|
||||
to?: string,
|
||||
onClick?: (event: SyntheticEvent<>) => mixed,
|
||||
borderOnHover?: boolean,
|
||||
|
||||
href?: string,
|
||||
"data-on"?: string,
|
||||
"data-event-category"?: string,
|
||||
"data-event-action"?: string,
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import { DisconnectedIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled, { useTheme } from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import Fade from "components/Fade";
|
||||
import NudeButton from "components/NudeButton";
|
||||
import Tooltip from "components/Tooltip";
|
||||
import useStores from "hooks/useStores";
|
||||
|
||||
function ConnectionStatus() {
|
||||
const { ui } = useStores();
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return ui.multiplayerStatus === "connecting" ||
|
||||
ui.multiplayerStatus === "disconnected" ? (
|
||||
<Tooltip
|
||||
tooltip={
|
||||
<Centered>
|
||||
<strong>{t("Server connection lost")}</strong>
|
||||
<br />
|
||||
{t("Edits you make will sync once you’re online")}
|
||||
</Centered>
|
||||
}
|
||||
placement="bottom"
|
||||
>
|
||||
<Button>
|
||||
<Fade>
|
||||
<DisconnectedIcon color={theme.sidebarText} />
|
||||
</Fade>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
) : null;
|
||||
}
|
||||
|
||||
const Button = styled(NudeButton)`
|
||||
display: none;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 32px;
|
||||
margin: 24px;
|
||||
|
||||
${breakpoint("tablet")`
|
||||
display: block;
|
||||
`};
|
||||
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const Centered = styled.div`
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
export default observer(ConnectionStatus);
|
||||
@@ -4,6 +4,7 @@ import * as React from "react";
|
||||
import { MenuItem as BaseMenuItem } from "reakit/Menu";
|
||||
import styled from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import MenuIconWrapper from "../MenuIconWrapper";
|
||||
|
||||
type Props = {|
|
||||
onClick?: (SyntheticEvent<>) => void | Promise<void>,
|
||||
@@ -16,6 +17,7 @@ type Props = {|
|
||||
as?: string | React.ComponentType<*>,
|
||||
hide?: () => void,
|
||||
level?: number,
|
||||
icon?: React.Node,
|
||||
|};
|
||||
|
||||
const MenuItem = ({
|
||||
@@ -25,6 +27,7 @@ const MenuItem = ({
|
||||
disabled,
|
||||
as,
|
||||
hide,
|
||||
icon,
|
||||
...rest
|
||||
}: Props) => {
|
||||
const handleClick = React.useCallback(
|
||||
@@ -71,6 +74,7 @@ const MenuItem = ({
|
||||
|
||||
</>
|
||||
)}
|
||||
{icon && <MenuIconWrapper>{icon}</MenuIconWrapper>}
|
||||
{children}
|
||||
</MenuAnchor>
|
||||
)}
|
||||
@@ -130,8 +134,8 @@ export const MenuAnchor = styled.a`
|
||||
`};
|
||||
|
||||
${breakpoint("tablet")`
|
||||
padding: ${(props) => (props.$toggleable ? "4px 12px" : "6px 12px")};
|
||||
font-size: 15px;
|
||||
padding: 4px 12px;
|
||||
font-size: 14px;
|
||||
`};
|
||||
`;
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
MenuItem as BaseMenuItem,
|
||||
} from "reakit/Menu";
|
||||
import styled from "styled-components";
|
||||
import Flex from "components/Flex";
|
||||
import MenuIconWrapper from "components/MenuIconWrapper";
|
||||
import Header from "./Header";
|
||||
import MenuItem, { MenuAnchor } from "./MenuItem";
|
||||
import Separator from "./Separator";
|
||||
@@ -67,7 +69,15 @@ export function filterTemplateItems(items: TMenuItem[]): TMenuItem[] {
|
||||
}
|
||||
|
||||
function Template({ items, ...menu }: Props): React.Node {
|
||||
return filterTemplateItems(items).map((item, index) => {
|
||||
const filteredTemplates = filterTemplateItems(items);
|
||||
const iconIsPresentInAnyMenuItem = filteredTemplates.find(
|
||||
(item) => !!item.icon
|
||||
);
|
||||
|
||||
return filteredTemplates.map((item, index) => {
|
||||
if (iconIsPresentInAnyMenuItem)
|
||||
item.icon = item.icon ? item.icon : <MenuIconWrapper />;
|
||||
|
||||
if (item.to) {
|
||||
return (
|
||||
<MenuItem
|
||||
@@ -76,6 +86,7 @@ function Template({ items, ...menu }: Props): React.Node {
|
||||
key={index}
|
||||
disabled={item.disabled}
|
||||
selected={item.selected}
|
||||
icon={item.icon}
|
||||
{...menu}
|
||||
>
|
||||
{item.title}
|
||||
@@ -92,6 +103,7 @@ function Template({ items, ...menu }: Props): React.Node {
|
||||
selected={item.selected}
|
||||
level={item.level}
|
||||
target={item.href.startsWith("#") ? undefined : "_blank"}
|
||||
icon={item.icon}
|
||||
{...menu}
|
||||
>
|
||||
{item.title}
|
||||
@@ -107,6 +119,7 @@ function Template({ items, ...menu }: Props): React.Node {
|
||||
disabled={item.disabled}
|
||||
selected={item.selected}
|
||||
key={index}
|
||||
icon={item.icon}
|
||||
{...menu}
|
||||
>
|
||||
{item.title}
|
||||
@@ -120,7 +133,7 @@ function Template({ items, ...menu }: Props): React.Node {
|
||||
key={index}
|
||||
as={Submenu}
|
||||
templateItems={item.items}
|
||||
title={item.title}
|
||||
title={<Title title={item.title} icon={item.icon} />}
|
||||
{...menu}
|
||||
/>
|
||||
);
|
||||
@@ -139,4 +152,13 @@ function Template({ items, ...menu }: Props): React.Node {
|
||||
});
|
||||
}
|
||||
|
||||
function Title({ title, icon }) {
|
||||
return (
|
||||
<Flex align="center">
|
||||
{icon && <MenuIconWrapper>{icon}</MenuIconWrapper>}
|
||||
{title}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo<Props>(Template);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -36,7 +36,7 @@ function Header({ breadcrumb, title, actions }: Props) {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Wrapper align="center" isCompact={isScrolled} shrink={false}>
|
||||
<Wrapper align="center" shrink={false}>
|
||||
{breadcrumb ? <Breadcrumbs>{breadcrumb}</Breadcrumbs> : null}
|
||||
{isScrolled ? (
|
||||
<Title align="center" justify="flex-start" onClick={handleClickTitle}>
|
||||
@@ -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";
|
||||
`};
|
||||
`;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// @flow
|
||||
import {
|
||||
BookmarkedIcon,
|
||||
CollectionIcon,
|
||||
CoinsIcon,
|
||||
AcademicCapIcon,
|
||||
@@ -8,9 +9,12 @@ import {
|
||||
CloudIcon,
|
||||
CodeIcon,
|
||||
EditIcon,
|
||||
EmailIcon,
|
||||
EyeIcon,
|
||||
ImageIcon,
|
||||
LeafIcon,
|
||||
LightBulbIcon,
|
||||
MathIcon,
|
||||
MoonIcon,
|
||||
NotepadIcon,
|
||||
PadlockIcon,
|
||||
@@ -18,6 +22,7 @@ import {
|
||||
QuestionMarkIcon,
|
||||
SunIcon,
|
||||
VehicleIcon,
|
||||
WarningIcon,
|
||||
} from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -39,6 +44,10 @@ const TwitterPicker = React.lazy(() =>
|
||||
);
|
||||
|
||||
export const icons = {
|
||||
bookmark: {
|
||||
component: BookmarkedIcon,
|
||||
keywords: "bookmark",
|
||||
},
|
||||
collection: {
|
||||
component: CollectionIcon,
|
||||
keywords: "collection",
|
||||
@@ -67,10 +76,18 @@ export const icons = {
|
||||
component: CodeIcon,
|
||||
keywords: "developer api code development engineering programming",
|
||||
},
|
||||
email: {
|
||||
component: EmailIcon,
|
||||
keywords: "email at",
|
||||
},
|
||||
eye: {
|
||||
component: EyeIcon,
|
||||
keywords: "eye view",
|
||||
},
|
||||
image: {
|
||||
component: ImageIcon,
|
||||
keywords: "image photo picture",
|
||||
},
|
||||
leaf: {
|
||||
component: LeafIcon,
|
||||
keywords: "leaf plant outdoors nature ecosystem climate",
|
||||
@@ -79,6 +96,10 @@ export const icons = {
|
||||
component: LightBulbIcon,
|
||||
keywords: "lightbulb idea",
|
||||
},
|
||||
math: {
|
||||
component: MathIcon,
|
||||
keywords: "math formula",
|
||||
},
|
||||
moon: {
|
||||
component: MoonIcon,
|
||||
keywords: "night moon dark",
|
||||
@@ -111,6 +132,10 @@ export const icons = {
|
||||
component: VehicleIcon,
|
||||
keywords: "truck car travel transport",
|
||||
},
|
||||
warning: {
|
||||
component: WarningIcon,
|
||||
keywords: "danger",
|
||||
},
|
||||
};
|
||||
|
||||
const colors = [
|
||||
@@ -192,7 +217,7 @@ const Label = styled.label`
|
||||
`;
|
||||
|
||||
const Icons = styled.div`
|
||||
padding: 15px 9px 9px 15px;
|
||||
padding: 16px 8px 0 16px;
|
||||
width: 276px;
|
||||
`;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { VisuallyHidden } from "reakit/VisuallyHidden";
|
||||
import styled from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import { Outline, LabelText } from "./Input";
|
||||
|
||||
const Select = styled.select`
|
||||
@@ -15,6 +16,7 @@ const Select = styled.select`
|
||||
background: none;
|
||||
color: ${(props) => props.theme.text};
|
||||
height: 30px;
|
||||
font-size: 14px;
|
||||
|
||||
option {
|
||||
background: ${(props) => props.theme.buttonNeutralBackground};
|
||||
@@ -24,6 +26,10 @@ const Select = styled.select`
|
||||
&::placeholder {
|
||||
color: ${(props) => props.theme.placeholder};
|
||||
}
|
||||
|
||||
${breakpoint("mobile", "tablet")`
|
||||
font-size: 16px;
|
||||
`};
|
||||
`;
|
||||
|
||||
const Wrapper = styled.label`
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import InputSelect, { type Props, type Option } from "components/InputSelect";
|
||||
|
||||
const InputSelectRole = (props: $Rest<Props, { options: Array<Option> }>) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<InputSelect
|
||||
label={t("Role")}
|
||||
options={[
|
||||
{ label: t("Member"), value: "member" },
|
||||
{ label: t("Viewer"), value: "viewer" },
|
||||
{ label: t("Admin"), value: "admin" },
|
||||
]}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default InputSelectRole;
|
||||
@@ -22,7 +22,6 @@ import UiStore from "stores/UiStore";
|
||||
import ErrorSuspended from "scenes/ErrorSuspended";
|
||||
import KeyboardShortcuts from "scenes/KeyboardShortcuts";
|
||||
import Button from "components/Button";
|
||||
import DocumentHistory from "components/DocumentHistory";
|
||||
import Flex from "components/Flex";
|
||||
import Guide from "components/Guide";
|
||||
import { LoadingIndicatorBar } from "components/LoadingIndicator";
|
||||
@@ -38,6 +37,12 @@ import {
|
||||
newDocumentUrl,
|
||||
} from "utils/routeHelpers";
|
||||
|
||||
const DocumentHistory = React.lazy(() =>
|
||||
import(
|
||||
/* webpackChunkName: "document-history" */ "components/DocumentHistory"
|
||||
)
|
||||
);
|
||||
|
||||
type Props = {
|
||||
documents: DocumentsStore,
|
||||
children?: ?React.Node,
|
||||
@@ -154,12 +159,14 @@ class Layout extends React.Component<Props> {
|
||||
{this.props.children}
|
||||
</Content>
|
||||
|
||||
<Switch>
|
||||
<Route
|
||||
path={`/doc/${slug}/history/:revisionId?`}
|
||||
component={DocumentHistory}
|
||||
/>
|
||||
</Switch>
|
||||
<React.Suspense>
|
||||
<Switch>
|
||||
<Route
|
||||
path={`/doc/${slug}/history/:revisionId?`}
|
||||
component={DocumentHistory}
|
||||
/>
|
||||
</Switch>
|
||||
</React.Suspense>
|
||||
</Container>
|
||||
<Guide
|
||||
isOpen={this.keyboardShortcutsOpen}
|
||||
|
||||
@@ -1,39 +1,9 @@
|
||||
// @flow
|
||||
import { format as formatDate, formatDistanceToNow } from "date-fns";
|
||||
import {
|
||||
enUS,
|
||||
de,
|
||||
faIR,
|
||||
fr,
|
||||
es,
|
||||
it,
|
||||
ja,
|
||||
ko,
|
||||
ptBR,
|
||||
pt,
|
||||
zhCN,
|
||||
zhTW,
|
||||
ru,
|
||||
} from "date-fns/locale";
|
||||
import * as React from "react";
|
||||
import Tooltip from "components/Tooltip";
|
||||
import useUserLocale from "hooks/useUserLocale";
|
||||
|
||||
const locales = {
|
||||
en_US: enUS,
|
||||
de_DE: de,
|
||||
es_ES: es,
|
||||
fa_IR: faIR,
|
||||
fr_FR: fr,
|
||||
it_IT: it,
|
||||
ja_JP: ja,
|
||||
ko_KR: ko,
|
||||
pt_BR: ptBR,
|
||||
pt_PT: pt,
|
||||
zh_CN: zhCN,
|
||||
zh_TW: zhTW,
|
||||
ru_RU: ru,
|
||||
};
|
||||
import { dateLocale } from "utils/i18n";
|
||||
|
||||
let callbacks = [];
|
||||
|
||||
@@ -59,7 +29,6 @@ type Props = {
|
||||
shorten?: boolean,
|
||||
relative?: boolean,
|
||||
format?: string,
|
||||
tooltip?: boolean,
|
||||
};
|
||||
|
||||
function LocaleTime({
|
||||
@@ -70,7 +39,6 @@ function LocaleTime({
|
||||
format,
|
||||
relative,
|
||||
tooltipDelay,
|
||||
tooltip,
|
||||
}: Props) {
|
||||
const userLocale = useUserLocale();
|
||||
const [_, setMinutesMounted] = React.useState(0); // eslint-disable-line no-unused-vars
|
||||
@@ -88,7 +56,7 @@ function LocaleTime({
|
||||
};
|
||||
}, []);
|
||||
|
||||
const locale = userLocale ? locales[userLocale] : undefined;
|
||||
const locale = dateLocale(userLocale);
|
||||
let relativeContent = formatDistanceToNow(Date.parse(dateTime), {
|
||||
addSuffix,
|
||||
locale,
|
||||
@@ -107,11 +75,8 @@ function LocaleTime({
|
||||
{ locale }
|
||||
);
|
||||
|
||||
const content = children || relative ? relativeContent : tooltipContent;
|
||||
|
||||
if (!tooltip) {
|
||||
return content;
|
||||
}
|
||||
const content =
|
||||
children || relative !== false ? relativeContent : tooltipContent;
|
||||
|
||||
return (
|
||||
<Tooltip tooltip={tooltipContent} delay={tooltipDelay} placement="bottom">
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
// @flow
|
||||
import styled from "styled-components";
|
||||
|
||||
const MenuIconWrapper = styled.span`
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 6px;
|
||||
margin-left: -4px;
|
||||
`;
|
||||
|
||||
export default MenuIconWrapper;
|
||||
@@ -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,
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
import ArrowKeyNavigation from "boundless-arrow-key-navigation";
|
||||
import { isEqual } from "lodash";
|
||||
import { observable, action } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { observer, inject } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { withTranslation, type TFunction } from "react-i18next";
|
||||
import { Waypoint } from "react-waypoint";
|
||||
import AuthStore from "stores/AuthStore";
|
||||
import { DEFAULT_PAGINATION_LIMIT } from "stores/BaseStore";
|
||||
import DelayedMount from "components/DelayedMount";
|
||||
import PlaceholderList from "components/List/Placeholder";
|
||||
@@ -17,6 +18,7 @@ type Props = {
|
||||
heading?: React.Node,
|
||||
empty?: React.Node,
|
||||
items: any[],
|
||||
auth: AuthStore,
|
||||
renderItem: (any, index: number) => React.Node,
|
||||
renderHeading?: (name: React.Element<any> | string) => React.Node,
|
||||
t: TFunction,
|
||||
@@ -105,7 +107,7 @@ class PaginatedList extends React.Component<Props> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { items, heading, empty, renderHeading } = this.props;
|
||||
const { items, heading, auth, empty, renderHeading } = this.props;
|
||||
|
||||
let previousHeading = "";
|
||||
const showLoading =
|
||||
@@ -137,7 +139,11 @@ class PaginatedList extends React.Component<Props> {
|
||||
// Get what a heading would look like for this item
|
||||
const currentDate =
|
||||
item.updatedAt || item.createdAt || previousHeading;
|
||||
const currentHeading = dateToHeading(currentDate, this.props.t);
|
||||
const currentHeading = dateToHeading(
|
||||
currentDate,
|
||||
this.props.t,
|
||||
auth.user?.language
|
||||
);
|
||||
|
||||
// If the heading is different to any previous heading then we
|
||||
// should render it, otherwise the item can go under the previous
|
||||
@@ -173,4 +179,4 @@ class PaginatedList extends React.Component<Props> {
|
||||
|
||||
export const Component = PaginatedList;
|
||||
|
||||
export default withTranslation()<PaginatedList>(PaginatedList);
|
||||
export default withTranslation()<PaginatedList>(inject("auth")(PaginatedList));
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
import { observer } from "mobx-react";
|
||||
import {
|
||||
EditIcon,
|
||||
SearchIcon,
|
||||
ShapesIcon,
|
||||
HomeIcon,
|
||||
PlusIcon,
|
||||
SearchIcon,
|
||||
SettingsIcon,
|
||||
ShapesIcon,
|
||||
StarredIcon,
|
||||
} from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { DndProvider } from "react-dnd";
|
||||
@@ -25,6 +24,7 @@ import ArchiveLink from "./components/ArchiveLink";
|
||||
import Collections from "./components/Collections";
|
||||
import Section from "./components/Section";
|
||||
import SidebarLink from "./components/SidebarLink";
|
||||
import Starred from "./components/Starred";
|
||||
import TeamButton from "./components/TeamButton";
|
||||
import TrashLink from "./components/TrashLink";
|
||||
import useCurrentTeam from "hooks/useCurrentTeam";
|
||||
@@ -109,12 +109,6 @@ function MainSidebar() {
|
||||
label={t("Search")}
|
||||
exact={false}
|
||||
/>
|
||||
<SidebarLink
|
||||
to="/starred"
|
||||
icon={<StarredIcon color="currentColor" />}
|
||||
exact={false}
|
||||
label={t("Starred")}
|
||||
/>
|
||||
{can.createDocument && (
|
||||
<SidebarLink
|
||||
to="/drafts"
|
||||
@@ -135,6 +129,7 @@ function MainSidebar() {
|
||||
/>
|
||||
)}
|
||||
</Section>
|
||||
<Starred />
|
||||
<Section auto>
|
||||
<Collections
|
||||
onCreateCollection={handleCreateCollectionModalOpen}
|
||||
|
||||
@@ -159,6 +159,7 @@ function CollectionLink({
|
||||
/>
|
||||
}
|
||||
exact={false}
|
||||
depth={0.5}
|
||||
menu={
|
||||
<>
|
||||
{can.update && (
|
||||
@@ -198,7 +199,7 @@ function CollectionLink({
|
||||
activeDocument={activeDocument}
|
||||
prefetchDocument={prefetchDocument}
|
||||
canUpdate={canUpdate}
|
||||
depth={1.5}
|
||||
depth={2}
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
// @flow
|
||||
import fractionalIndex from "fractional-index";
|
||||
import { observer } from "mobx-react";
|
||||
import { PlusIcon } from "outline-icons";
|
||||
import { PlusIcon, CollapsedIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useDrop } from "react-dnd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import Fade from "components/Fade";
|
||||
import Flex from "components/Flex";
|
||||
import useStores from "../../../hooks/useStores";
|
||||
import CollectionLink from "./CollectionLink";
|
||||
import DropCursor from "./DropCursor";
|
||||
import Header from "./Header";
|
||||
import PlaceholderCollections from "./PlaceholderCollections";
|
||||
import SidebarLink from "./SidebarLink";
|
||||
import useCurrentTeam from "hooks/useCurrentTeam";
|
||||
@@ -25,6 +25,7 @@ function Collections({ onCreateCollection }: Props) {
|
||||
const [fetchError, setFetchError] = React.useState();
|
||||
const { ui, policies, documents, collections } = useStores();
|
||||
const { showToast } = useToasts();
|
||||
const [expanded, setExpanded] = React.useState(true);
|
||||
const isPreloaded: boolean = !!collections.orderedData.length;
|
||||
const { t } = useTranslation();
|
||||
const team = useCurrentTeam();
|
||||
@@ -99,6 +100,7 @@ function Collections({ onCreateCollection }: Props) {
|
||||
icon={<PlusIcon color="currentColor" />}
|
||||
label={`${t("New collection")}…`}
|
||||
exact
|
||||
depth={0.5}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
@@ -107,7 +109,11 @@ function Collections({ onCreateCollection }: Props) {
|
||||
if (!collections.isLoaded || fetchError) {
|
||||
return (
|
||||
<Flex column>
|
||||
<Header>{t("Collections")}</Header>
|
||||
<SidebarLink
|
||||
label={t("Collections")}
|
||||
icon={<Disclosure expanded={expanded} color="currentColor" />}
|
||||
disabled
|
||||
/>
|
||||
<PlaceholderCollections />
|
||||
</Flex>
|
||||
);
|
||||
@@ -115,10 +121,19 @@ function Collections({ onCreateCollection }: Props) {
|
||||
|
||||
return (
|
||||
<Flex column>
|
||||
<Header>{t("Collections")}</Header>
|
||||
{isPreloaded ? content : <Fade>{content}</Fade>}
|
||||
<SidebarLink
|
||||
onClick={() => setExpanded((prev) => !prev)}
|
||||
label={t("Collections")}
|
||||
icon={<Disclosure expanded={expanded} color="currentColor" />}
|
||||
/>
|
||||
{expanded && (isPreloaded ? content : <Fade>{content}</Fade>)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
const Disclosure = styled(CollapsedIcon)`
|
||||
transition: transform 100ms ease, fill 50ms !important;
|
||||
${({ expanded }) => !expanded && "transform: rotate(-90deg);"};
|
||||
`;
|
||||
|
||||
export default observer(Collections);
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
// @flow
|
||||
import { CollapsedIcon } from "outline-icons";
|
||||
import styled from "styled-components";
|
||||
|
||||
const Disclosure = styled(CollapsedIcon)`
|
||||
transition: transform 100ms ease, fill 50ms !important;
|
||||
position: absolute;
|
||||
left: -24px;
|
||||
|
||||
${({ expanded }) => !expanded && "transform: rotate(-90deg);"};
|
||||
`;
|
||||
|
||||
export default Disclosure;
|
||||
@@ -1,6 +1,5 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import { CollapsedIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useDrag, useDrop } from "react-dnd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -8,6 +7,7 @@ import styled from "styled-components";
|
||||
import Collection from "models/Collection";
|
||||
import Document from "models/Document";
|
||||
import Fade from "components/Fade";
|
||||
import Disclosure from "./Disclosure";
|
||||
import DropCursor from "./DropCursor";
|
||||
import DropToImport from "./DropToImport";
|
||||
import EditableTitle from "./EditableTitle";
|
||||
@@ -210,7 +210,7 @@ function DocumentLink(
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ position: "relative" }} onDragLeave={resetHoverExpanding}>
|
||||
<Relative onDragLeave={resetHoverExpanding}>
|
||||
<Draggable
|
||||
key={node.id}
|
||||
ref={drag}
|
||||
@@ -244,6 +244,7 @@ function DocumentLink(
|
||||
depth={depth}
|
||||
exact={false}
|
||||
showActions={menuOpen}
|
||||
scrollIntoViewIfNeeded={!document?.isStarred}
|
||||
ref={ref}
|
||||
menu={
|
||||
document && !isMoving ? (
|
||||
@@ -263,7 +264,7 @@ function DocumentLink(
|
||||
{manualSort && (
|
||||
<DropCursor isActiveDrop={isOverReorder} innerRef={dropToReorder} />
|
||||
)}
|
||||
</div>
|
||||
</Relative>
|
||||
{expanded && !isDragging && (
|
||||
<>
|
||||
{node.children.map((childNode, index) => (
|
||||
@@ -285,17 +286,13 @@ function DocumentLink(
|
||||
);
|
||||
}
|
||||
|
||||
const Draggable = styled("div")`
|
||||
opacity: ${(props) => (props.$isDragging || props.$isMoving ? 0.5 : 1)};
|
||||
pointer-events: ${(props) => (props.$isMoving ? "none" : "all")};
|
||||
const Relative = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const Disclosure = styled(CollapsedIcon)`
|
||||
transition: transform 100ms ease, fill 50ms !important;
|
||||
position: absolute;
|
||||
left: -24px;
|
||||
|
||||
${({ expanded }) => !expanded && "transform: rotate(-90deg);"};
|
||||
const Draggable = styled.div`
|
||||
opacity: ${(props) => (props.$isDragging || props.$isMoving ? 0.5 : 1)};
|
||||
pointer-events: ${(props) => (props.$isMoving ? "none" : "all")};
|
||||
`;
|
||||
|
||||
const ObservedDocumentLink = observer(React.forwardRef(DocumentLink));
|
||||
|
||||
@@ -27,7 +27,7 @@ const Cursor = styled("div")`
|
||||
|
||||
width: 100%;
|
||||
height: 14px;
|
||||
${(props) => (props.from === "collections" ? "top: 15px;" : "bottom: -7px;")}
|
||||
${(props) => (props.from === "collections" ? "top: 25px;" : "bottom: -7px;")}
|
||||
background: transparent;
|
||||
|
||||
::after {
|
||||
|
||||
@@ -5,10 +5,11 @@ import Flex from "components/Flex";
|
||||
const Header = styled(Flex)`
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
user-select: none;
|
||||
text-transform: uppercase;
|
||||
color: ${(props) => props.theme.sidebarText};
|
||||
letter-spacing: 0.04em;
|
||||
margin: 4px 16px;
|
||||
margin: 4px 12px;
|
||||
`;
|
||||
|
||||
export default Header;
|
||||
|
||||
@@ -31,6 +31,7 @@ type Props = {|
|
||||
activeClassName?: String,
|
||||
activeStyle?: Object,
|
||||
className?: string,
|
||||
scrollIntoViewIfNeeded?: boolean,
|
||||
exact?: boolean,
|
||||
isActive?: any,
|
||||
location?: Location,
|
||||
@@ -52,6 +53,7 @@ const NavLink = ({
|
||||
location: locationProp,
|
||||
strict,
|
||||
style: styleProp,
|
||||
scrollIntoViewIfNeeded,
|
||||
to,
|
||||
...rest
|
||||
}: Props) => {
|
||||
@@ -83,13 +85,13 @@ const NavLink = ({
|
||||
const style = isActive ? { ...styleProp, ...activeStyle } : styleProp;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isActive && linkRef.current) {
|
||||
if (isActive && linkRef.current && scrollIntoViewIfNeeded !== false) {
|
||||
scrollIntoView(linkRef.current, {
|
||||
scrollMode: "if-needed",
|
||||
behavior: "instant",
|
||||
});
|
||||
}
|
||||
}, [linkRef, isActive]);
|
||||
}, [linkRef, scrollIntoViewIfNeeded, isActive]);
|
||||
|
||||
const props = {
|
||||
"aria-current": (isActive && ariaCurrent) || null,
|
||||
|
||||
@@ -15,6 +15,7 @@ function PlaceholderCollections() {
|
||||
|
||||
const Wrapper = styled.div`
|
||||
margin: 4px 16px;
|
||||
margin-left: 40px;
|
||||
width: 75%;
|
||||
`;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import Flex from "components/Flex";
|
||||
const Section = styled(Flex)`
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
margin: 0 8px 20px;
|
||||
margin: 0 8px 12px;
|
||||
min-width: ${(props) => props.theme.sidebarMinWidth}px;
|
||||
flex-shrink: 0;
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ type Props = {
|
||||
theme: Theme,
|
||||
exact?: boolean,
|
||||
depth?: number,
|
||||
scrollIntoViewIfNeeded?: boolean,
|
||||
};
|
||||
|
||||
function SidebarLink(
|
||||
@@ -49,12 +50,13 @@ function SidebarLink(
|
||||
history,
|
||||
match,
|
||||
className,
|
||||
scrollIntoViewIfNeeded,
|
||||
}: Props,
|
||||
ref
|
||||
) {
|
||||
const style = React.useMemo(() => {
|
||||
return {
|
||||
paddingLeft: `${(depth || 0) * 16 + 16}px`,
|
||||
paddingLeft: `${(depth || 0) * 16 + 12}px`,
|
||||
};
|
||||
}, [depth]);
|
||||
|
||||
@@ -73,6 +75,7 @@ function SidebarLink(
|
||||
<>
|
||||
<Link
|
||||
$isActiveDrop={isActiveDrop}
|
||||
scrollIntoViewIfNeeded={scrollIntoViewIfNeeded}
|
||||
activeStyle={isActiveDrop ? activeDropStyle : activeStyle}
|
||||
style={active ? activeStyle : style}
|
||||
onClick={onClick}
|
||||
@@ -131,6 +134,7 @@ const Link = styled(NavLink)`
|
||||
padding: 6px 16px;
|
||||
border-radius: 4px;
|
||||
transition: background 50ms, color 50ms;
|
||||
user-select: none;
|
||||
background: ${(props) =>
|
||||
props.$isActiveDrop ? props.theme.slateDark : "inherit"};
|
||||
color: ${(props) =>
|
||||
@@ -156,13 +160,11 @@ const Link = styled(NavLink)`
|
||||
`}
|
||||
|
||||
@media (hover: hover) {
|
||||
&:hover + ${Actions},
|
||||
&:active + ${Actions} {
|
||||
display: inline-flex;
|
||||
&:hover + ${Actions}, &:active + ${Actions} {
|
||||
display: inline-flex;
|
||||
|
||||
svg {
|
||||
opacity: 0.75;
|
||||
}
|
||||
svg {
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import { CollapsedIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import Flex from "components/Flex";
|
||||
import PlaceholderCollections from "./PlaceholderCollections";
|
||||
import Section from "./Section";
|
||||
import SidebarLink from "./SidebarLink";
|
||||
import StarredLink from "./StarredLink";
|
||||
import useStores from "hooks/useStores";
|
||||
import useToasts from "hooks/useToasts";
|
||||
|
||||
const STARRED_PAGINATION_LIMIT = 10;
|
||||
const STARRED = "STARRED";
|
||||
|
||||
function Starred() {
|
||||
const [isFetching, setIsFetching] = React.useState(false);
|
||||
const [fetchError, setFetchError] = React.useState();
|
||||
const [expanded, setExpanded] = React.useState(true);
|
||||
const [show, setShow] = React.useState("Nothing");
|
||||
const [offset, setOffset] = React.useState(0);
|
||||
const [upperBound, setUpperBound] = React.useState(STARRED_PAGINATION_LIMIT);
|
||||
const { showToast } = useToasts();
|
||||
const { documents } = useStores();
|
||||
const { t } = useTranslation();
|
||||
const { fetchStarred, starred } = documents;
|
||||
|
||||
const fetchResults = React.useCallback(async () => {
|
||||
try {
|
||||
setIsFetching(true);
|
||||
await fetchStarred({
|
||||
limit: STARRED_PAGINATION_LIMIT,
|
||||
offset,
|
||||
});
|
||||
} catch (error) {
|
||||
showToast(t("Starred documents could not be loaded"), {
|
||||
type: "error",
|
||||
});
|
||||
setFetchError(error);
|
||||
} finally {
|
||||
setIsFetching(false);
|
||||
}
|
||||
}, [fetchStarred, offset, showToast, t]);
|
||||
|
||||
useEffect(() => {
|
||||
let stateInLocal;
|
||||
|
||||
try {
|
||||
stateInLocal = localStorage.getItem(STARRED);
|
||||
} catch (_) {
|
||||
// no-op Safari private mode
|
||||
}
|
||||
|
||||
if (!stateInLocal) {
|
||||
localStorage.setItem(STARRED, expanded ? "true" : "false");
|
||||
} else {
|
||||
setExpanded(stateInLocal === "true");
|
||||
}
|
||||
}, [expanded]);
|
||||
|
||||
useEffect(() => {
|
||||
setOffset(starred.length);
|
||||
if (starred.length <= STARRED_PAGINATION_LIMIT) {
|
||||
setShow("Nothing");
|
||||
} else if (starred.length >= upperBound) {
|
||||
setShow("More");
|
||||
} else if (starred.length < upperBound) {
|
||||
setShow("Less");
|
||||
}
|
||||
}, [starred, upperBound]);
|
||||
|
||||
useEffect(() => {
|
||||
if (offset === 0) {
|
||||
fetchResults();
|
||||
}
|
||||
}, [fetchResults, offset]);
|
||||
|
||||
const handleShowMore = React.useCallback(
|
||||
async (ev) => {
|
||||
setUpperBound(
|
||||
(previousUpperBound) => previousUpperBound + STARRED_PAGINATION_LIMIT
|
||||
);
|
||||
await fetchResults();
|
||||
},
|
||||
[fetchResults]
|
||||
);
|
||||
|
||||
const handleShowLess = React.useCallback((ev) => {
|
||||
setUpperBound(STARRED_PAGINATION_LIMIT);
|
||||
setShow("More");
|
||||
}, []);
|
||||
|
||||
const handleExpandClick = React.useCallback(
|
||||
(ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
try {
|
||||
localStorage.setItem(STARRED, !expanded ? "true" : "false");
|
||||
} catch (_) {
|
||||
// no-op Safari private mode
|
||||
}
|
||||
setExpanded((prev) => !prev);
|
||||
},
|
||||
[expanded]
|
||||
);
|
||||
|
||||
const content = starred.slice(0, upperBound).map((document, index) => {
|
||||
return (
|
||||
<StarredLink
|
||||
key={document.id}
|
||||
documentId={document.id}
|
||||
collectionId={document.collectionId}
|
||||
to={document.url}
|
||||
title={document.title}
|
||||
url={document.url}
|
||||
depth={2}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
if (!starred.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<Flex column>
|
||||
<SidebarLink
|
||||
onClick={handleExpandClick}
|
||||
label={t("Starred")}
|
||||
icon={<Disclosure expanded={expanded} color="currentColor" />}
|
||||
/>
|
||||
{expanded && (
|
||||
<>
|
||||
{content}
|
||||
{show === "More" && !isFetching && (
|
||||
<SidebarLink
|
||||
onClick={handleShowMore}
|
||||
label={`${t("Show more")}…`}
|
||||
depth={2}
|
||||
/>
|
||||
)}
|
||||
{show === "Less" && !isFetching && (
|
||||
<SidebarLink
|
||||
onClick={handleShowLess}
|
||||
label={`${t("Show less")}…`}
|
||||
depth={2}
|
||||
/>
|
||||
)}
|
||||
{(isFetching || fetchError) && (
|
||||
<Flex column>
|
||||
<PlaceholderCollections />
|
||||
</Flex>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
const Disclosure = styled(CollapsedIcon)`
|
||||
transition: transform 100ms ease, fill 50ms !important;
|
||||
${({ expanded }) => !expanded && "transform: rotate(-90deg);"};
|
||||
`;
|
||||
|
||||
export default observer(Starred);
|
||||
@@ -0,0 +1,124 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import Fade from "components/Fade";
|
||||
import useStores from "../../../hooks/useStores";
|
||||
import Disclosure from "./Disclosure";
|
||||
import EditableTitle from "./EditableTitle";
|
||||
import SidebarLink from "./SidebarLink";
|
||||
import useBoolean from "hooks/useBoolean";
|
||||
import DocumentMenu from "menus/DocumentMenu";
|
||||
|
||||
type Props = {|
|
||||
depth: number,
|
||||
title: string,
|
||||
to: string,
|
||||
documentId: string,
|
||||
collectionId: string,
|
||||
|};
|
||||
|
||||
function StarredLink({ depth, title, to, documentId, collectionId }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const { collections, documents, policies } = useStores();
|
||||
const collection = collections.get(collectionId);
|
||||
const document = documents.get(documentId);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean();
|
||||
const canUpdate = policies.abilities(documentId).update;
|
||||
|
||||
const childDocuments = collection
|
||||
? collection.getDocumentChildren(documentId)
|
||||
: [];
|
||||
|
||||
const hasChildDocuments = childDocuments.length > 0;
|
||||
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
if (!document) {
|
||||
await documents.fetch(documentId);
|
||||
}
|
||||
}
|
||||
load();
|
||||
}, [collection, collectionId, collections, document, documentId, documents]);
|
||||
|
||||
const handleDisclosureClick = React.useCallback((ev: SyntheticEvent<>) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
setExpanded((prevExpanded) => !prevExpanded);
|
||||
}, []);
|
||||
|
||||
const handleTitleChange = React.useCallback(
|
||||
async (title: string) => {
|
||||
if (!document) return;
|
||||
|
||||
await documents.update({
|
||||
id: document.id,
|
||||
lastRevision: document.revision,
|
||||
text: document.text,
|
||||
title,
|
||||
});
|
||||
},
|
||||
[documents, document]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Relative>
|
||||
<SidebarLink
|
||||
depth={depth}
|
||||
to={to}
|
||||
label={
|
||||
<>
|
||||
{hasChildDocuments && (
|
||||
<Disclosure
|
||||
expanded={expanded}
|
||||
onClick={handleDisclosureClick}
|
||||
/>
|
||||
)}
|
||||
<EditableTitle
|
||||
title={title || t("Untitled")}
|
||||
onSubmit={handleTitleChange}
|
||||
canUpdate={canUpdate}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
exact={false}
|
||||
showActions={menuOpen}
|
||||
menu={
|
||||
document ? (
|
||||
<Fade>
|
||||
<DocumentMenu
|
||||
document={document}
|
||||
onOpen={handleMenuOpen}
|
||||
onClose={handleMenuClose}
|
||||
/>
|
||||
</Fade>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
</Relative>
|
||||
{expanded &&
|
||||
childDocuments.map((childDocument) => (
|
||||
<ObserveredStarredLink
|
||||
key={childDocument.id}
|
||||
depth={depth + 1}
|
||||
title={childDocument.title}
|
||||
to={childDocument.url}
|
||||
documentId={childDocument.id}
|
||||
collectionId={collectionId}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const Relative = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const ObserveredStarredLink = observer(StarredLink);
|
||||
|
||||
export default ObserveredStarredLink;
|
||||
@@ -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);
|
||||
|
||||
@@ -8,6 +8,7 @@ import AuthStore from "stores/AuthStore";
|
||||
import CollectionsStore from "stores/CollectionsStore";
|
||||
import DocumentPresenceStore from "stores/DocumentPresenceStore";
|
||||
import DocumentsStore from "stores/DocumentsStore";
|
||||
import FileOperationsStore from "stores/FileOperationsStore";
|
||||
import GroupsStore from "stores/GroupsStore";
|
||||
import MembershipsStore from "stores/MembershipsStore";
|
||||
import PoliciesStore from "stores/PoliciesStore";
|
||||
@@ -28,6 +29,7 @@ type Props = {
|
||||
views: ViewsStore,
|
||||
auth: AuthStore,
|
||||
toasts: ToastsStore,
|
||||
fileOperations: FileOperationsStore,
|
||||
};
|
||||
|
||||
@observer
|
||||
@@ -80,6 +82,7 @@ class SocketProvider extends React.Component<Props> {
|
||||
policies,
|
||||
presence,
|
||||
views,
|
||||
fileOperations,
|
||||
} = this.props;
|
||||
if (!auth.token) return;
|
||||
|
||||
@@ -287,6 +290,21 @@ class SocketProvider extends React.Component<Props> {
|
||||
}
|
||||
});
|
||||
|
||||
this.socket.on("fileOperations.update", async (event) => {
|
||||
const user = auth.user;
|
||||
let collection = null;
|
||||
|
||||
if (event.collectionId)
|
||||
collection = await collections.fetch(event.collectionId);
|
||||
if (user) {
|
||||
fileOperations.add({
|
||||
...event,
|
||||
user,
|
||||
collection,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// received a message from the API server that we should request
|
||||
// to join a specific room. Forward that to the ws server.
|
||||
this.socket.on("join", (event) => {
|
||||
@@ -345,5 +363,6 @@ export default inject(
|
||||
"memberships",
|
||||
"presence",
|
||||
"policies",
|
||||
"views"
|
||||
"views",
|
||||
"fileOperations"
|
||||
)(SocketProvider);
|
||||
|
||||
+55
-16
@@ -1,4 +1,5 @@
|
||||
// @flow
|
||||
import { isEqual } from "lodash";
|
||||
import { observer } from "mobx-react";
|
||||
import { CollapsedIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
@@ -24,6 +25,7 @@ export type Props = {|
|
||||
onChangePage: (index: number) => void,
|
||||
onChangeSort: (sort: ?string, direction: "ASC" | "DESC") => void,
|
||||
columns: any,
|
||||
defaultSortDirection: "ASC" | "DESC",
|
||||
|};
|
||||
|
||||
function Table({
|
||||
@@ -39,6 +41,7 @@ function Table({
|
||||
topRef,
|
||||
onChangeSort,
|
||||
onChangePage,
|
||||
defaultSortDirection,
|
||||
}: Props) {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
@@ -62,32 +65,52 @@ function Table({
|
||||
autoResetPage: false,
|
||||
pageCount: totalPages,
|
||||
initialState: {
|
||||
sortBy: [{ id: defaultSort, desc: false }],
|
||||
sortBy: [
|
||||
{
|
||||
id: defaultSort,
|
||||
desc: defaultSortDirection === "DESC" ? true : false,
|
||||
},
|
||||
],
|
||||
pageSize,
|
||||
pageIndex: page,
|
||||
},
|
||||
stateReducer: (newState, action, prevState) => {
|
||||
if (!isEqual(newState.sortBy, prevState.sortBy)) {
|
||||
return { ...newState, pageIndex: 0 };
|
||||
}
|
||||
return newState;
|
||||
},
|
||||
},
|
||||
useSortBy,
|
||||
usePagination
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
onChangePage(pageIndex);
|
||||
}, [pageIndex]);
|
||||
const prevSortBy = React.useRef(sortBy);
|
||||
|
||||
React.useEffect(() => {
|
||||
onChangePage(0);
|
||||
onChangeSort(
|
||||
sortBy.length ? sortBy[0].id : undefined,
|
||||
sortBy.length && sortBy[0].desc ? "DESC" : "ASC"
|
||||
);
|
||||
}, [sortBy]);
|
||||
if (!isEqual(sortBy, prevSortBy.current)) {
|
||||
prevSortBy.current = sortBy;
|
||||
onChangePage(0);
|
||||
onChangeSort(
|
||||
sortBy.length ? sortBy[0].id : undefined,
|
||||
!sortBy.length ? defaultSortDirection : sortBy[0].desc ? "DESC" : "ASC"
|
||||
);
|
||||
}
|
||||
}, [defaultSortDirection, onChangePage, onChangeSort, sortBy]);
|
||||
|
||||
const handleNextPage = () => {
|
||||
nextPage();
|
||||
onChangePage(pageIndex + 1);
|
||||
};
|
||||
|
||||
const handlePreviousPage = () => {
|
||||
previousPage();
|
||||
onChangePage(pageIndex - 1);
|
||||
};
|
||||
|
||||
const isEmpty = !isLoading && data.length === 0;
|
||||
const showPlaceholder = isLoading && data.length === 0;
|
||||
|
||||
console.log({ canNextPage, pageIndex, totalPages, rows, data });
|
||||
|
||||
return (
|
||||
<>
|
||||
<Anchor ref={topRef} />
|
||||
@@ -142,12 +165,12 @@ function Table({
|
||||
>
|
||||
{/* Note: the page > 0 check shouldn't be needed here but is */}
|
||||
{canPreviousPage && page > 0 && (
|
||||
<Button onClick={previousPage} neutral>
|
||||
<Button onClick={handlePreviousPage} neutral>
|
||||
{t("Previous page")}
|
||||
</Button>
|
||||
)}
|
||||
{canNextPage && (
|
||||
<Button onClick={nextPage} neutral>
|
||||
<Button onClick={handleNextPage} neutral>
|
||||
{t("Next page")}
|
||||
</Button>
|
||||
)}
|
||||
@@ -209,7 +232,7 @@ const SortWrapper = styled(Flex)`
|
||||
`;
|
||||
|
||||
const Cell = styled.td`
|
||||
padding: 8px 0;
|
||||
padding: 6px;
|
||||
border-bottom: 1px solid ${(props) => props.theme.divider};
|
||||
font-size: 14px;
|
||||
|
||||
@@ -226,6 +249,14 @@ const Cell = styled.td`
|
||||
`;
|
||||
|
||||
const Row = styled.tr`
|
||||
${Cell} {
|
||||
&:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
&:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
&:last-child {
|
||||
${Cell} {
|
||||
border-bottom: 0;
|
||||
@@ -237,7 +268,7 @@ const Head = styled.th`
|
||||
text-align: left;
|
||||
position: sticky;
|
||||
top: 54px;
|
||||
padding: 6px 0;
|
||||
padding: 6px;
|
||||
border-bottom: 1px solid ${(props) => props.theme.divider};
|
||||
background: ${(props) => props.theme.background};
|
||||
transition: ${(props) => props.theme.backgroundTransition};
|
||||
@@ -245,6 +276,14 @@ const Head = styled.th`
|
||||
color: ${(props) => props.theme.textSecondary};
|
||||
font-weight: 500;
|
||||
z-index: 1;
|
||||
|
||||
:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export default observer(Table);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { getCookie } from "tiny-cookie";
|
||||
|
||||
type Session = {|
|
||||
url: string,
|
||||
logoUrl: string,
|
||||
name: string,
|
||||
teamId: string,
|
||||
|};
|
||||
|
||||
function loadSessionsFromCookie(): Session[] {
|
||||
const sessions = JSON.parse(getCookie("sessions") || "{}");
|
||||
|
||||
return Object.keys(sessions).map((teamId) => ({
|
||||
teamId,
|
||||
...sessions[teamId],
|
||||
}));
|
||||
}
|
||||
|
||||
export default function useSessions() {
|
||||
const [sessions, setSessions] = React.useState(loadSessionsFromCookie);
|
||||
|
||||
const reload = React.useCallback(() => {
|
||||
setSessions(loadSessionsFromCookie());
|
||||
}, []);
|
||||
|
||||
return [sessions, reload];
|
||||
}
|
||||
+137
-82
@@ -1,85 +1,48 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import { SunIcon, MoonIcon } from "outline-icons";
|
||||
import { MoonIcon, SunIcon, TrashIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useMenuState, MenuButton } from "reakit/Menu";
|
||||
import { MenuButton, useMenuState } from "reakit/Menu";
|
||||
import styled from "styled-components";
|
||||
import {
|
||||
developers,
|
||||
changelog,
|
||||
developers,
|
||||
githubIssuesUrl,
|
||||
mailToUrl,
|
||||
settings,
|
||||
} from "shared/utils/routeHelpers";
|
||||
import KeyboardShortcuts from "scenes/KeyboardShortcuts";
|
||||
import ContextMenu from "components/ContextMenu";
|
||||
import MenuItem, { MenuAnchor } from "components/ContextMenu/MenuItem";
|
||||
import Separator from "components/ContextMenu/Separator";
|
||||
import Flex from "components/Flex";
|
||||
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,
|
||||
|};
|
||||
|
||||
const AppearanceMenu = React.forwardRef((props, ref) => {
|
||||
const { ui } = useStores();
|
||||
const { t } = useTranslation();
|
||||
const menu = useMenuState();
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuButton ref={ref} {...menu} {...props} onClick={menu.show}>
|
||||
{(props) => (
|
||||
<MenuAnchor {...props}>
|
||||
<ChangeTheme justify="space-between">
|
||||
{t("Appearance")}
|
||||
{ui.resolvedTheme === "light" ? <SunIcon /> : <MoonIcon />}
|
||||
</ChangeTheme>
|
||||
</MenuAnchor>
|
||||
)}
|
||||
</MenuButton>
|
||||
<ContextMenu {...menu} aria-label={t("Appearance")}>
|
||||
<MenuItem
|
||||
{...menu}
|
||||
onClick={() => ui.setTheme("system")}
|
||||
selected={ui.theme === "system"}
|
||||
>
|
||||
{t("System")}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
{...menu}
|
||||
onClick={() => ui.setTheme("light")}
|
||||
selected={ui.theme === "light"}
|
||||
>
|
||||
{t("Light")}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
{...menu}
|
||||
onClick={() => ui.setTheme("dark")}
|
||||
selected={ui.theme === "dark"}
|
||||
>
|
||||
{t("Dark")}
|
||||
</MenuItem>
|
||||
</ContextMenu>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
function AccountMenu(props: Props) {
|
||||
const [sessions] = useSessions();
|
||||
const menu = useMenuState({
|
||||
unstable_offset: [8, 0],
|
||||
placement: "bottom-start",
|
||||
modal: true,
|
||||
});
|
||||
const { showToast } = useToasts();
|
||||
const { auth, ui } = useStores();
|
||||
const previousTheme = usePrevious(ui.theme);
|
||||
const { theme, resolvedTheme } = ui;
|
||||
const team = useCurrentTeam();
|
||||
const previousTheme = usePrevious(theme);
|
||||
const { t } = useTranslation();
|
||||
const [includeAlt, setIncludeAlt] = React.useState(false);
|
||||
const [
|
||||
keyboardShortcutsOpen,
|
||||
handleKeyboardShortcutsOpen,
|
||||
@@ -87,10 +50,122 @@ function AccountMenu(props: Props) {
|
||||
] = useBoolean();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (ui.theme !== previousTheme) {
|
||||
if (theme !== previousTheme) {
|
||||
menu.hide();
|
||||
}
|
||||
}, [menu, ui.theme, previousTheme]);
|
||||
}, [menu, theme, previousTheme]);
|
||||
|
||||
const handleDeleteAllDatabases = React.useCallback(async () => {
|
||||
await deleteAllDatabases();
|
||||
showToast("IndexedDB cache deleted");
|
||||
menu.hide();
|
||||
}, [showToast, menu]);
|
||||
|
||||
const handleOpenMenu = React.useCallback((event) => {
|
||||
setIncludeAlt(event.altKey);
|
||||
}, []);
|
||||
|
||||
const items = React.useMemo(() => {
|
||||
const otherSessions = sessions.filter(
|
||||
(session) => session.teamId !== team.id && session.url !== team.url
|
||||
);
|
||||
|
||||
return [
|
||||
{
|
||||
title: t("Settings"),
|
||||
to: settings(),
|
||||
},
|
||||
{
|
||||
title: t("Keyboard shortcuts"),
|
||||
onClick: handleKeyboardShortcutsOpen,
|
||||
},
|
||||
{
|
||||
title: t("API documentation"),
|
||||
href: developers(),
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
title: t("Changelog"),
|
||||
href: changelog(),
|
||||
},
|
||||
{
|
||||
title: t("Send us feedback"),
|
||||
href: mailToUrl(),
|
||||
},
|
||||
{
|
||||
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 />,
|
||||
items: [
|
||||
{
|
||||
title: t("System"),
|
||||
onClick: () => ui.setTheme("system"),
|
||||
selected: theme === "system",
|
||||
},
|
||||
{
|
||||
title: t("Light"),
|
||||
onClick: () => ui.setTheme("light"),
|
||||
selected: theme === "light",
|
||||
},
|
||||
{
|
||||
title: t("Dark"),
|
||||
onClick: () => ui.setTheme("dark"),
|
||||
selected: theme === "dark",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
...(otherSessions.length
|
||||
? [
|
||||
{
|
||||
title: t("Switch team"),
|
||||
items: otherSessions.map((session) => ({
|
||||
title: session.name,
|
||||
icon: <Logo alt={session.name} src={session.logoUrl} />,
|
||||
href: session.url,
|
||||
})),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: t("Log out"),
|
||||
onClick: auth.logout,
|
||||
},
|
||||
];
|
||||
}, [
|
||||
auth.logout,
|
||||
team.id,
|
||||
team.url,
|
||||
sessions,
|
||||
handleKeyboardShortcutsOpen,
|
||||
handleDeleteAllDatabases,
|
||||
resolvedTheme,
|
||||
includeAlt,
|
||||
theme,
|
||||
t,
|
||||
ui,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -101,40 +176,20 @@ function AccountMenu(props: Props) {
|
||||
>
|
||||
<KeyboardShortcuts />
|
||||
</Guide>
|
||||
<MenuButton {...menu}>{props.children}</MenuButton>
|
||||
<MenuButton {...menu} onClick={handleOpenMenu}>
|
||||
{props.children}
|
||||
</MenuButton>
|
||||
<ContextMenu {...menu} aria-label={t("Account")}>
|
||||
<MenuItem {...menu} as={Link} to={settings()}>
|
||||
{t("Settings")}
|
||||
</MenuItem>
|
||||
<MenuItem {...menu} onClick={handleKeyboardShortcutsOpen}>
|
||||
{t("Keyboard shortcuts")}
|
||||
</MenuItem>
|
||||
<MenuItem {...menu} href={developers()} target="_blank">
|
||||
{t("API documentation")}
|
||||
</MenuItem>
|
||||
<Separator {...menu} />
|
||||
<MenuItem {...menu} href={changelog()} target="_blank">
|
||||
{t("Changelog")}
|
||||
</MenuItem>
|
||||
<MenuItem {...menu} href={mailToUrl()} target="_blank">
|
||||
{t("Send us feedback")}
|
||||
</MenuItem>
|
||||
<MenuItem {...menu} href={githubIssuesUrl()} target="_blank">
|
||||
{t("Report a bug")}
|
||||
</MenuItem>
|
||||
<Separator {...menu} />
|
||||
<MenuItem {...menu} as={AppearanceMenu} />
|
||||
<Separator {...menu} />
|
||||
<MenuItem {...menu} onClick={auth.logout}>
|
||||
{t("Log out")}
|
||||
</MenuItem>
|
||||
<Template {...menu} items={items} />
|
||||
</ContextMenu>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const ChangeTheme = styled(Flex)`
|
||||
width: 100%;
|
||||
const Logo = styled("img")`
|
||||
border-radius: 2px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
`;
|
||||
|
||||
export default observer(AccountMenu);
|
||||
|
||||
+66
-41
@@ -1,5 +1,13 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import {
|
||||
NewDocumentIcon,
|
||||
EditIcon,
|
||||
TrashIcon,
|
||||
ImportIcon,
|
||||
ExportIcon,
|
||||
PadlockIcon,
|
||||
} from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHistory } from "react-router-dom";
|
||||
@@ -12,8 +20,9 @@ import CollectionExport from "scenes/CollectionExport";
|
||||
import CollectionPermissions from "scenes/CollectionPermissions";
|
||||
import ContextMenu from "components/ContextMenu";
|
||||
import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton";
|
||||
import Template, { filterTemplateItems } from "components/ContextMenu/Template";
|
||||
import Template from "components/ContextMenu/Template";
|
||||
import Modal from "components/Modal";
|
||||
import useCurrentTeam from "hooks/useCurrentTeam";
|
||||
import useStores from "hooks/useStores";
|
||||
import useToasts from "hooks/useToasts";
|
||||
import getDataTransferFiles from "utils/getDataTransferFiles";
|
||||
@@ -38,6 +47,7 @@ function CollectionMenu({
|
||||
}: Props) {
|
||||
const menu = useMenuState({ modal, placement });
|
||||
const [renderModals, setRenderModals] = React.useState(false);
|
||||
const team = useCurrentTeam();
|
||||
const { documents, policies } = useStores();
|
||||
const { showToast } = useToasts();
|
||||
const { t } = useTranslation();
|
||||
@@ -112,47 +122,62 @@ function CollectionMenu({
|
||||
);
|
||||
|
||||
const can = policies.abilities(collection.id);
|
||||
const canUserInTeam = policies.abilities(team.id);
|
||||
|
||||
const items = React.useMemo(
|
||||
() =>
|
||||
filterTemplateItems([
|
||||
{
|
||||
title: t("New document"),
|
||||
visible: can.update,
|
||||
onClick: handleNewDocument,
|
||||
},
|
||||
{
|
||||
title: t("Import document"),
|
||||
visible: can.update,
|
||||
onClick: handleImportDocument,
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
title: `${t("Edit")}…`,
|
||||
visible: can.update,
|
||||
onClick: () => setShowCollectionEdit(true),
|
||||
},
|
||||
{
|
||||
title: `${t("Permissions")}…`,
|
||||
visible: can.update,
|
||||
onClick: () => setShowCollectionPermissions(true),
|
||||
},
|
||||
{
|
||||
title: `${t("Export")}…`,
|
||||
visible: !!(collection && can.export),
|
||||
onClick: () => setShowCollectionExport(true),
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
title: `${t("Delete")}…`,
|
||||
visible: !!(collection && can.delete),
|
||||
onClick: () => setShowCollectionDelete(true),
|
||||
},
|
||||
]),
|
||||
[can, collection, handleNewDocument, handleImportDocument, t]
|
||||
() => [
|
||||
{
|
||||
title: t("New document"),
|
||||
visible: can.update,
|
||||
onClick: handleNewDocument,
|
||||
icon: <NewDocumentIcon />,
|
||||
},
|
||||
{
|
||||
title: t("Import document"),
|
||||
visible: can.update,
|
||||
onClick: handleImportDocument,
|
||||
icon: <ImportIcon />,
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
title: `${t("Edit")}…`,
|
||||
visible: can.update,
|
||||
onClick: () => setShowCollectionEdit(true),
|
||||
icon: <EditIcon />,
|
||||
},
|
||||
{
|
||||
title: `${t("Permissions")}…`,
|
||||
visible: can.update,
|
||||
onClick: () => setShowCollectionPermissions(true),
|
||||
icon: <PadlockIcon />,
|
||||
},
|
||||
{
|
||||
title: `${t("Export")}…`,
|
||||
visible: !!(collection && canUserInTeam.export),
|
||||
onClick: () => setShowCollectionExport(true),
|
||||
icon: <ExportIcon />,
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
title: `${t("Delete")}…`,
|
||||
visible: !!(collection && can.delete),
|
||||
onClick: () => setShowCollectionDelete(true),
|
||||
icon: <TrashIcon />,
|
||||
},
|
||||
],
|
||||
[
|
||||
t,
|
||||
can.update,
|
||||
can.delete,
|
||||
handleNewDocument,
|
||||
handleImportDocument,
|
||||
collection,
|
||||
canUserInTeam.export,
|
||||
]
|
||||
);
|
||||
|
||||
if (!items.length) {
|
||||
|
||||
+63
-18
@@ -1,5 +1,25 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import {
|
||||
EditIcon,
|
||||
PinIcon,
|
||||
StarredIcon,
|
||||
UnstarredIcon,
|
||||
DuplicateIcon,
|
||||
ArchiveIcon,
|
||||
TrashIcon,
|
||||
MoveIcon,
|
||||
HistoryIcon,
|
||||
UnpublishIcon,
|
||||
ShapesIcon,
|
||||
PrintIcon,
|
||||
ImportIcon,
|
||||
NewDocumentIcon,
|
||||
DownloadIcon,
|
||||
BuildingBlocksIcon,
|
||||
RestoreIcon,
|
||||
CrossIcon,
|
||||
} from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHistory } from "react-router-dom";
|
||||
@@ -114,9 +134,13 @@ function DocumentMenu({
|
||||
[showToast, t, document]
|
||||
);
|
||||
|
||||
const handlePrint = React.useCallback((ev: SyntheticEvent<>) => {
|
||||
window.print();
|
||||
}, []);
|
||||
const handlePrint = React.useCallback(
|
||||
(ev: SyntheticEvent<>) => {
|
||||
menu.hide();
|
||||
window.print();
|
||||
},
|
||||
[menu]
|
||||
);
|
||||
|
||||
const handleStar = React.useCallback(
|
||||
(ev: SyntheticEvent<>) => {
|
||||
@@ -248,6 +272,7 @@ function DocumentMenu({
|
||||
title: t("Restore"),
|
||||
visible: (!!collection && can.restore) || can.unarchive,
|
||||
onClick: handleRestore,
|
||||
icon: <RestoreIcon />,
|
||||
},
|
||||
{
|
||||
title: t("Restore"),
|
||||
@@ -258,6 +283,7 @@ function DocumentMenu({
|
||||
position: "relative",
|
||||
top: -40,
|
||||
},
|
||||
icon: <RestoreIcon />,
|
||||
hover: true,
|
||||
items: [
|
||||
{
|
||||
@@ -271,86 +297,102 @@ function DocumentMenu({
|
||||
title: t("Unpin"),
|
||||
onClick: document.unpin,
|
||||
visible: !!(showPin && document.pinned && can.unpin),
|
||||
icon: <PinIcon />,
|
||||
},
|
||||
{
|
||||
title: t("Pin to collection"),
|
||||
onClick: document.pin,
|
||||
visible: !!(showPin && !document.pinned && can.pin),
|
||||
icon: <PinIcon />,
|
||||
},
|
||||
{
|
||||
title: t("Unstar"),
|
||||
onClick: handleUnstar,
|
||||
visible: document.isStarred && !!can.unstar,
|
||||
icon: <UnstarredIcon />,
|
||||
},
|
||||
{
|
||||
title: t("Star"),
|
||||
onClick: handleStar,
|
||||
visible: !document.isStarred && !!can.star,
|
||||
},
|
||||
{
|
||||
title: t("Enable embeds"),
|
||||
onClick: document.enableEmbeds,
|
||||
visible: !!showToggleEmbeds && document.embedsDisabled,
|
||||
},
|
||||
{
|
||||
title: t("Disable embeds"),
|
||||
onClick: document.disableEmbeds,
|
||||
visible: !!showToggleEmbeds && !document.embedsDisabled,
|
||||
icon: <StarredIcon />,
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
title: t("Edit"),
|
||||
to: editDocumentUrl(document),
|
||||
visible: !!can.update,
|
||||
icon: <EditIcon />,
|
||||
},
|
||||
{
|
||||
title: t("New nested document"),
|
||||
to: newDocumentUrl(document.collectionId, {
|
||||
parentDocumentId: document.id,
|
||||
}),
|
||||
visible: !!can.createChildDocument,
|
||||
icon: <NewDocumentIcon />,
|
||||
},
|
||||
{
|
||||
title: t("Import document"),
|
||||
visible: can.createChildDocument,
|
||||
onClick: handleImportDocument,
|
||||
icon: <ImportIcon />,
|
||||
},
|
||||
{
|
||||
title: `${t("Create template")}…`,
|
||||
onClick: () => setShowTemplateModal(true),
|
||||
visible: !!can.update && !document.isTemplate,
|
||||
},
|
||||
{
|
||||
title: t("Edit"),
|
||||
to: editDocumentUrl(document),
|
||||
visible: !!can.update,
|
||||
icon: <ShapesIcon />,
|
||||
},
|
||||
{
|
||||
title: t("Duplicate"),
|
||||
onClick: handleDuplicate,
|
||||
visible: !!can.update,
|
||||
icon: <DuplicateIcon />,
|
||||
},
|
||||
{
|
||||
title: t("Unpublish"),
|
||||
onClick: handleUnpublish,
|
||||
visible: !!can.unpublish,
|
||||
icon: <UnpublishIcon />,
|
||||
},
|
||||
{
|
||||
title: t("Archive"),
|
||||
onClick: handleArchive,
|
||||
visible: !!can.archive,
|
||||
icon: <ArchiveIcon />,
|
||||
},
|
||||
{
|
||||
title: `${t("Delete")}…`,
|
||||
onClick: () => setShowDeleteModal(true),
|
||||
visible: !!can.delete,
|
||||
icon: <TrashIcon />,
|
||||
},
|
||||
{
|
||||
title: `${t("Permanently delete")}…`,
|
||||
onClick: () => setShowPermanentDeleteModal(true),
|
||||
visible: can.permanentDelete,
|
||||
icon: <CrossIcon />,
|
||||
},
|
||||
{
|
||||
title: `${t("Move")}…`,
|
||||
onClick: () => setShowMoveModal(true),
|
||||
visible: !!can.move,
|
||||
icon: <MoveIcon />,
|
||||
},
|
||||
{
|
||||
title: t("Enable embeds"),
|
||||
onClick: document.enableEmbeds,
|
||||
visible: !!showToggleEmbeds && document.embedsDisabled,
|
||||
icon: <BuildingBlocksIcon />,
|
||||
},
|
||||
{
|
||||
title: t("Disable embeds"),
|
||||
onClick: document.disableEmbeds,
|
||||
visible: !!showToggleEmbeds && !document.embedsDisabled,
|
||||
icon: <BuildingBlocksIcon />,
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
@@ -361,16 +403,19 @@ function DocumentMenu({
|
||||
? documentUrl(document)
|
||||
: documentHistoryUrl(document),
|
||||
visible: canViewHistory,
|
||||
icon: <HistoryIcon />,
|
||||
},
|
||||
{
|
||||
title: t("Download"),
|
||||
onClick: document.download,
|
||||
visible: !!can.download,
|
||||
icon: <DownloadIcon />,
|
||||
},
|
||||
{
|
||||
title: t("Print"),
|
||||
onClick: handlePrint,
|
||||
visible: !!showPrint,
|
||||
icon: <PrintIcon />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -11,7 +11,6 @@ import CollectionIcon from "components/CollectionIcon";
|
||||
import ContextMenu from "components/ContextMenu";
|
||||
import Header from "components/ContextMenu/Header";
|
||||
import Template from "components/ContextMenu/Template";
|
||||
import Flex from "components/Flex";
|
||||
import useCurrentTeam from "hooks/useCurrentTeam";
|
||||
import useStores from "hooks/useStores";
|
||||
import { newDocumentUrl } from "utils/routeHelpers";
|
||||
@@ -31,12 +30,8 @@ function NewDocumentMenu() {
|
||||
if (can.update) {
|
||||
filtered.push({
|
||||
to: newDocumentUrl(collection.id),
|
||||
title: (
|
||||
<Flex align="center">
|
||||
<CollectionIcon collection={collection} />
|
||||
<CollectionName>{collection.name}</CollectionName>
|
||||
</Flex>
|
||||
),
|
||||
title: <CollectionName>{collection.name}</CollectionName>,
|
||||
icon: <CollectionIcon collection={collection} />,
|
||||
});
|
||||
}
|
||||
return filtered;
|
||||
|
||||
@@ -3,14 +3,13 @@ import { observer } from "mobx-react";
|
||||
import { PlusIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMenuState, MenuButton } from "reakit/Menu";
|
||||
import { MenuButton, useMenuState } from "reakit/Menu";
|
||||
import styled from "styled-components";
|
||||
import Button from "components/Button";
|
||||
import CollectionIcon from "components/CollectionIcon";
|
||||
import ContextMenu from "components/ContextMenu";
|
||||
import Header from "components/ContextMenu/Header";
|
||||
import Template from "components/ContextMenu/Template";
|
||||
import Flex from "components/Flex";
|
||||
import useCurrentTeam from "hooks/useCurrentTeam";
|
||||
import useStores from "hooks/useStores";
|
||||
import { newDocumentUrl } from "utils/routeHelpers";
|
||||
@@ -29,12 +28,8 @@ function NewTemplateMenu() {
|
||||
if (can.update) {
|
||||
filtered.push({
|
||||
to: newDocumentUrl(collection.id, { template: true }),
|
||||
title: (
|
||||
<Flex align="center">
|
||||
<CollectionIcon collection={collection} />
|
||||
<CollectionName>{collection.name}</CollectionName>
|
||||
</Flex>
|
||||
),
|
||||
title: <CollectionName>{collection.name}</CollectionName>,
|
||||
icon: <CollectionIcon collection={collection} />,
|
||||
});
|
||||
}
|
||||
return filtered;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import { RestoreIcon, LinkIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHistory } from "react-router-dom";
|
||||
@@ -10,6 +11,8 @@ import MenuItem from "components/ContextMenu/MenuItem";
|
||||
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";
|
||||
|
||||
@@ -21,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();
|
||||
@@ -53,12 +57,24 @@ 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>
|
||||
{t("Restore version")}
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<CopyToClipboard text={url} onCopy={handleCopy}>
|
||||
<MenuItem {...menu}>{t("Copy link")}</MenuItem>
|
||||
<MenuItem {...menu}>
|
||||
<MenuIconWrapper>
|
||||
<LinkIcon />
|
||||
</MenuIconWrapper>
|
||||
{t("Copy link")}
|
||||
</MenuItem>
|
||||
</CopyToClipboard>
|
||||
</ContextMenu>
|
||||
</>
|
||||
|
||||
@@ -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,10 +37,10 @@ function TemplatesMenu({ document }: Props) {
|
||||
const renderTemplate = (template) => (
|
||||
<MenuItem
|
||||
key={template.id}
|
||||
onClick={() => document.updateFromTemplate(template)}
|
||||
onClick={() => onSelectTemplate(template)}
|
||||
icon={<DocumentIcon />}
|
||||
{...menu}
|
||||
>
|
||||
<DocumentIcon />
|
||||
<TemplateItem>
|
||||
<strong>{template.titleWithDefault}</strong>
|
||||
<br />
|
||||
|
||||
@@ -49,7 +49,7 @@ function UserMenu({ user }: Props) {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
users.demote(user, "Member");
|
||||
users.demote(user, "member");
|
||||
},
|
||||
[users, user, t]
|
||||
);
|
||||
@@ -69,7 +69,7 @@ function UserMenu({ user }: Props) {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
users.demote(user, "Viewer");
|
||||
users.demote(user, "viewer");
|
||||
},
|
||||
[users, user, t]
|
||||
);
|
||||
@@ -119,21 +119,21 @@ function UserMenu({ user }: Props) {
|
||||
userName: user.name,
|
||||
}),
|
||||
onClick: handleMember,
|
||||
visible: can.demote && user.rank !== "Member",
|
||||
visible: can.demote && user.role !== "member",
|
||||
},
|
||||
{
|
||||
title: t("Make {{ userName }} a viewer", {
|
||||
userName: user.name,
|
||||
}),
|
||||
onClick: handleViewer,
|
||||
visible: can.demote && user.rank !== "Viewer",
|
||||
visible: can.demote && user.role !== "viewer",
|
||||
},
|
||||
{
|
||||
title: t("Make {{ userName }} an admin…", {
|
||||
userName: user.name,
|
||||
}),
|
||||
onClick: handlePromote,
|
||||
visible: can.promote && user.rank !== "Admin",
|
||||
visible: can.promote && user.role !== "admin",
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
|
||||
@@ -124,10 +124,6 @@ export default class Collection extends BaseModel {
|
||||
};
|
||||
|
||||
export = () => {
|
||||
return client.get(
|
||||
"/collections.export",
|
||||
{ id: this.id },
|
||||
{ download: true }
|
||||
);
|
||||
return client.get("/collections.export", { id: this.id });
|
||||
};
|
||||
}
|
||||
|
||||
+12
-21
@@ -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,7 @@ 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;
|
||||
};
|
||||
|
||||
@action
|
||||
update = async (options: SaveOptions & { title: string }) => {
|
||||
update = async (options: {| ...SaveOptions, title: string |}) => {
|
||||
if (this.isSaving) return this;
|
||||
this.isSaving = true;
|
||||
|
||||
@@ -283,7 +274,7 @@ export default class Document extends BaseModel {
|
||||
};
|
||||
|
||||
@action
|
||||
save = async (options: SaveOptions = {}) => {
|
||||
save = async (options: ?SaveOptions) => {
|
||||
if (this.isSaving) return this;
|
||||
|
||||
const isCreating = !this.id;
|
||||
@@ -296,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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// @flow
|
||||
import { computed } from "mobx";
|
||||
import BaseModal from "./BaseModel";
|
||||
import Collection from "./Collection";
|
||||
import User from "./User";
|
||||
|
||||
class FileOperation extends BaseModal {
|
||||
id: string;
|
||||
state: string;
|
||||
collection: ?Collection;
|
||||
size: number;
|
||||
type: string;
|
||||
user: User;
|
||||
createdAt: string;
|
||||
|
||||
@computed
|
||||
get sizeInMB(): string {
|
||||
const inKB = this.size / 1024;
|
||||
if (inKB < 1024) {
|
||||
return inKB.toFixed(2) + "KB";
|
||||
}
|
||||
|
||||
return (inKB / 1024).toFixed(2) + "MB";
|
||||
}
|
||||
}
|
||||
|
||||
export default FileOperation;
|
||||
+1
-1
@@ -7,7 +7,7 @@ class Team extends BaseModel {
|
||||
name: string;
|
||||
avatarUrl: string;
|
||||
sharing: boolean;
|
||||
multiplayerEditor: boolean;
|
||||
collaborativeEditing: boolean;
|
||||
documentEmbeds: boolean;
|
||||
guestSignin: boolean;
|
||||
subdomain: ?string;
|
||||
|
||||
+6
-5
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
import { computed } from "mobx";
|
||||
import type { Rank } from "shared/types";
|
||||
import type { Role } from "shared/types";
|
||||
import BaseModel from "./BaseModel";
|
||||
|
||||
class User extends BaseModel {
|
||||
@@ -8,6 +8,7 @@ class User extends BaseModel {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
color: string;
|
||||
isAdmin: boolean;
|
||||
isViewer: boolean;
|
||||
lastActiveAt: string;
|
||||
@@ -21,13 +22,13 @@ class User extends BaseModel {
|
||||
}
|
||||
|
||||
@computed
|
||||
get rank(): Rank {
|
||||
get role(): Role {
|
||||
if (this.isAdmin) {
|
||||
return "Admin";
|
||||
return "admin";
|
||||
} else if (this.isViewer) {
|
||||
return "Viewer";
|
||||
return "viewer";
|
||||
} else {
|
||||
return "Member";
|
||||
return "member";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ export default class MultiplayerExtension extends Extension {
|
||||
|
||||
get plugins() {
|
||||
const { user, provider, document: doc } = this.options;
|
||||
const type = doc.get("prosemirror", Y.XmlFragment);
|
||||
const type = doc.get("default", Y.XmlFragment);
|
||||
|
||||
const assignUser = (tr) => {
|
||||
const clientIds = Array.from(doc.store.clients.keys());
|
||||
@@ -33,12 +33,14 @@ export default class MultiplayerExtension extends Extension {
|
||||
}
|
||||
};
|
||||
|
||||
provider.awareness.setLocalStateField("user", {
|
||||
color: user.color,
|
||||
name: user.name,
|
||||
id: user.id,
|
||||
// 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 [
|
||||
|
||||
@@ -8,7 +8,6 @@ import Drafts from "scenes/Drafts";
|
||||
import Error404 from "scenes/Error404";
|
||||
import Home from "scenes/Home";
|
||||
import Search from "scenes/Search";
|
||||
import Starred from "scenes/Starred";
|
||||
import Templates from "scenes/Templates";
|
||||
import Trash from "scenes/Trash";
|
||||
|
||||
@@ -49,13 +48,12 @@ export default function AuthenticatedRoutes() {
|
||||
<Redirect from="/dashboard" to="/home" />
|
||||
<Route path="/home/:tab" component={Home} />
|
||||
<Route path="/home" component={Home} />
|
||||
<Route exact path="/starred" component={Starred} />
|
||||
<Route exact path="/starred/:sort" component={Starred} />
|
||||
<Route exact path="/templates" component={Templates} />
|
||||
<Route exact path="/templates/:sort" component={Templates} />
|
||||
<Route exact path="/drafts" component={Drafts} />
|
||||
<Route exact path="/archive" component={Archive} />
|
||||
<Route exact path="/trash" component={Trash} />
|
||||
<Redirect exact from="/starred" to="/home" />
|
||||
<Redirect exact from="/collections/*" to="/collection/*" />
|
||||
<Route exact path="/collection/:id/new" component={DocumentNew} />
|
||||
<Route exact path="/collection/:id/:tab" component={Collection} />
|
||||
|
||||
+83
-122
@@ -1,10 +1,8 @@
|
||||
// @flow
|
||||
import { observable } from "mobx";
|
||||
import { inject, observer } from "mobx-react";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { withTranslation, Trans, type TFunction } from "react-i18next";
|
||||
import AuthStore from "stores/AuthStore";
|
||||
import ToastsStore from "stores/ToastsStore";
|
||||
import { useState } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import Collection from "models/Collection";
|
||||
import Button from "components/Button";
|
||||
import Flex from "components/Flex";
|
||||
@@ -12,141 +10,104 @@ import HelpText from "components/HelpText";
|
||||
import IconPicker from "components/IconPicker";
|
||||
import Input from "components/Input";
|
||||
import InputSelect from "components/InputSelect";
|
||||
import Switch from "components/Switch";
|
||||
import useToasts from "hooks/useToasts";
|
||||
|
||||
type Props = {
|
||||
collection: Collection,
|
||||
toasts: ToastsStore,
|
||||
auth: AuthStore,
|
||||
onSubmit: () => void,
|
||||
t: TFunction,
|
||||
};
|
||||
|
||||
@observer
|
||||
class CollectionEdit extends React.Component<Props> {
|
||||
@observable name: string = this.props.collection.name;
|
||||
@observable sharing: boolean = this.props.collection.sharing;
|
||||
@observable icon: string = this.props.collection.icon;
|
||||
@observable color: string = this.props.collection.color || "#4E5C6E";
|
||||
@observable sort: { field: string, direction: "asc" | "desc" } = this.props
|
||||
.collection.sort;
|
||||
@observable isSaving: boolean;
|
||||
const CollectionEdit = ({ collection, onSubmit }: Props) => {
|
||||
const [name, setName] = useState(collection.name);
|
||||
const [icon, setIcon] = useState(collection.icon);
|
||||
const [color, setColor] = useState(collection.color || "#4E5C6E");
|
||||
const [sort, setSort] = useState<{
|
||||
field: string,
|
||||
direction: "asc" | "desc",
|
||||
}>(collection.sort);
|
||||
const [isSaving, setIsSaving] = useState();
|
||||
const { showToast } = useToasts();
|
||||
const { t } = useTranslation();
|
||||
|
||||
handleSubmit = async (ev: SyntheticEvent<*>) => {
|
||||
ev.preventDefault();
|
||||
this.isSaving = true;
|
||||
const { t } = this.props;
|
||||
const handleSubmit = React.useCallback(
|
||||
async (ev: SyntheticEvent<*>) => {
|
||||
ev.preventDefault();
|
||||
setIsSaving(true);
|
||||
|
||||
try {
|
||||
await this.props.collection.save({
|
||||
name: this.name,
|
||||
icon: this.icon,
|
||||
color: this.color,
|
||||
sharing: this.sharing,
|
||||
sort: this.sort,
|
||||
});
|
||||
this.props.onSubmit();
|
||||
this.props.toasts.showToast(t("The collection was updated"), {
|
||||
type: "success",
|
||||
});
|
||||
} catch (err) {
|
||||
this.props.toasts.showToast(err.message, { type: "error" });
|
||||
} finally {
|
||||
this.isSaving = false;
|
||||
}
|
||||
};
|
||||
try {
|
||||
await collection.save({
|
||||
name,
|
||||
icon,
|
||||
color,
|
||||
sort,
|
||||
});
|
||||
onSubmit();
|
||||
showToast(t("The collection was updated"), {
|
||||
type: "success",
|
||||
});
|
||||
} catch (err) {
|
||||
showToast(err.message, { type: "error" });
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
},
|
||||
[collection, color, icon, name, onSubmit, showToast, sort, t]
|
||||
);
|
||||
|
||||
handleSortChange = (ev: SyntheticInputEvent<HTMLSelectElement>) => {
|
||||
const handleSortChange = (ev: SyntheticInputEvent<HTMLSelectElement>) => {
|
||||
const [field, direction] = ev.target.value.split(".");
|
||||
|
||||
if (direction === "asc" || direction === "desc") {
|
||||
this.sort = { field, direction };
|
||||
setSort({ field, direction });
|
||||
}
|
||||
};
|
||||
|
||||
handleNameChange = (ev: SyntheticInputEvent<*>) => {
|
||||
this.name = ev.target.value;
|
||||
const handleNameChange = (ev: SyntheticInputEvent<*>) => {
|
||||
setName(ev.target.value.trim());
|
||||
};
|
||||
|
||||
handleChange = (color: string, icon: string) => {
|
||||
this.color = color;
|
||||
this.icon = icon;
|
||||
const handleChange = (color: string, icon: string) => {
|
||||
setColor(color);
|
||||
setIcon(icon);
|
||||
};
|
||||
|
||||
handleSharingChange = (ev: SyntheticInputEvent<*>) => {
|
||||
this.sharing = ev.target.checked;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { auth, t } = this.props;
|
||||
const teamSharingEnabled = !!auth.team && auth.team.sharing;
|
||||
|
||||
return (
|
||||
<Flex column>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<HelpText>
|
||||
<Trans>
|
||||
You can edit the name and other details at any time, however doing
|
||||
so often might confuse your team mates.
|
||||
</Trans>
|
||||
</HelpText>
|
||||
<Flex>
|
||||
<Input
|
||||
type="text"
|
||||
label={t("Name")}
|
||||
onChange={this.handleNameChange}
|
||||
value={this.name}
|
||||
required
|
||||
autoFocus
|
||||
flex
|
||||
/>
|
||||
|
||||
<IconPicker
|
||||
onChange={this.handleChange}
|
||||
color={this.color}
|
||||
icon={this.icon}
|
||||
/>
|
||||
</Flex>
|
||||
<InputSelect
|
||||
label={t("Sort in sidebar")}
|
||||
options={[
|
||||
{ label: t("Alphabetical"), value: "title.asc" },
|
||||
{ label: t("Manual sort"), value: "index.asc" },
|
||||
]}
|
||||
value={`${this.sort.field}.${this.sort.direction}`}
|
||||
onChange={this.handleSortChange}
|
||||
return (
|
||||
<Flex column>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<HelpText>
|
||||
<Trans>
|
||||
You can edit the name and other details at any time, however doing
|
||||
so often might confuse your team mates.
|
||||
</Trans>
|
||||
</HelpText>
|
||||
<Flex>
|
||||
<Input
|
||||
type="text"
|
||||
label={t("Name")}
|
||||
onChange={handleNameChange}
|
||||
value={name}
|
||||
required
|
||||
autoFocus
|
||||
flex
|
||||
/>
|
||||
<Switch
|
||||
id="sharing"
|
||||
label={t("Public document sharing")}
|
||||
onChange={this.handleSharingChange}
|
||||
checked={this.sharing && teamSharingEnabled}
|
||||
disabled={!teamSharingEnabled}
|
||||
/>
|
||||
<HelpText>
|
||||
{teamSharingEnabled ? (
|
||||
<Trans>
|
||||
When enabled, documents can be shared publicly on the internet.
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
Public sharing is currently disabled in the team security
|
||||
settings.
|
||||
</Trans>
|
||||
)}
|
||||
</HelpText>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={this.isSaving || !this.props.collection.name}
|
||||
>
|
||||
{this.isSaving ? `${t("Saving")}…` : t("Save")}
|
||||
</Button>
|
||||
</form>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
<IconPicker onChange={handleChange} color={color} icon={icon} />
|
||||
</Flex>
|
||||
<InputSelect
|
||||
label={t("Sort in sidebar")}
|
||||
options={[
|
||||
{ label: t("Alphabetical"), value: "title.asc" },
|
||||
{ label: t("Manual sort"), value: "index.asc" },
|
||||
]}
|
||||
value={`${sort.field}.${sort.direction}`}
|
||||
onChange={handleSortChange}
|
||||
/>
|
||||
<Button type="submit" disabled={isSaving || !collection.name}>
|
||||
{isSaving ? `${t("Saving")}…` : t("Save")}
|
||||
</Button>
|
||||
</form>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default withTranslation()<CollectionEdit>(
|
||||
inject("toasts", "auth")(CollectionEdit)
|
||||
);
|
||||
export default observer(CollectionEdit);
|
||||
|
||||
@@ -6,7 +6,7 @@ import Collection from "models/Collection";
|
||||
import Button from "components/Button";
|
||||
import Flex from "components/Flex";
|
||||
import HelpText from "components/HelpText";
|
||||
|
||||
import useToasts from "hooks/useToasts";
|
||||
type Props = {
|
||||
collection: Collection,
|
||||
onSubmit: () => void,
|
||||
@@ -15,6 +15,7 @@ type Props = {
|
||||
function CollectionExport({ collection, onSubmit }: Props) {
|
||||
const [isLoading, setIsLoading] = React.useState();
|
||||
const { t } = useTranslation();
|
||||
const { showToast } = useToasts();
|
||||
|
||||
const handleSubmit = React.useCallback(
|
||||
async (ev: SyntheticEvent<>) => {
|
||||
@@ -23,9 +24,12 @@ function CollectionExport({ collection, onSubmit }: Props) {
|
||||
setIsLoading(true);
|
||||
await collection.export();
|
||||
setIsLoading(false);
|
||||
showToast(
|
||||
t("Export started, you will receive an email when it’s complete.")
|
||||
);
|
||||
onSubmit();
|
||||
},
|
||||
[collection, onSubmit]
|
||||
[collection, onSubmit, showToast, t]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -33,7 +37,7 @@ function CollectionExport({ collection, onSubmit }: Props) {
|
||||
<form onSubmit={handleSubmit}>
|
||||
<HelpText>
|
||||
<Trans
|
||||
defaults="Exporting the collection <em>{{collectionName}}</em> may take a few seconds. Your documents will be downloaded as a zip of folders with files in Markdown format."
|
||||
defaults="Exporting the collection <em>{{collectionName}}</em> may take a few seconds. Your documents will be a zip of folders with files in Markdown format. Please visit the Export section on settings to get the zip."
|
||||
values={{ collectionName: collection.name }}
|
||||
components={{ em: <strong /> }}
|
||||
/>
|
||||
|
||||
@@ -13,6 +13,7 @@ import InputSelectPermission from "components/InputSelectPermission";
|
||||
import Labeled from "components/Labeled";
|
||||
import Modal from "components/Modal";
|
||||
import PaginatedList from "components/PaginatedList";
|
||||
import Switch from "components/Switch";
|
||||
import AddGroupsToCollection from "./AddGroupsToCollection";
|
||||
import AddPeopleToCollection from "./AddPeopleToCollection";
|
||||
import CollectionGroupMemberListItem from "./components/CollectionGroupMemberListItem";
|
||||
@@ -34,6 +35,7 @@ function CollectionPermissions({ collection }: Props) {
|
||||
collectionGroupMemberships,
|
||||
users,
|
||||
groups,
|
||||
auth,
|
||||
} = useStores();
|
||||
const { showToast } = useToasts();
|
||||
const [
|
||||
@@ -153,10 +155,28 @@ function CollectionPermissions({ collection }: Props) {
|
||||
collection.id,
|
||||
]);
|
||||
|
||||
const handleSharingChange = React.useCallback(
|
||||
async (ev: SyntheticInputEvent<*>) => {
|
||||
try {
|
||||
await collection.save({ sharing: ev.target.checked });
|
||||
showToast(t("Public document sharing permissions were updated"), {
|
||||
type: "success",
|
||||
});
|
||||
} catch (err) {
|
||||
showToast(t("Could not update public document sharing"), {
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
},
|
||||
[collection, showToast, t]
|
||||
);
|
||||
|
||||
const collectionName = collection.name;
|
||||
const collectionGroups = groups.inCollection(collection.id);
|
||||
const collectionUsers = users.inCollection(collection.id);
|
||||
const isEmpty = !collectionGroups.length && !collectionUsers.length;
|
||||
const sharing = collection.sharing;
|
||||
const teamSharingEnabled = !!auth.team && auth.team.sharing;
|
||||
|
||||
return (
|
||||
<Flex column>
|
||||
@@ -189,6 +209,24 @@ function CollectionPermissions({ collection }: Props) {
|
||||
/>
|
||||
)}
|
||||
</PermissionExplainer>
|
||||
<Switch
|
||||
id="sharing"
|
||||
label={t("Public document sharing")}
|
||||
onChange={handleSharingChange}
|
||||
checked={sharing && teamSharingEnabled}
|
||||
disabled={!teamSharingEnabled}
|
||||
/>
|
||||
<HelpText>
|
||||
{teamSharingEnabled ? (
|
||||
<Trans>
|
||||
When enabled, documents can be shared publicly on the internet.
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
Public sharing is currently disabled in the team security settings.
|
||||
</Trans>
|
||||
)}
|
||||
</HelpText>
|
||||
<Labeled label={t("Additional access")}>
|
||||
<Actions>
|
||||
<Button
|
||||
|
||||
@@ -10,6 +10,8 @@ import Loading from "./components/Loading";
|
||||
import { type LocationWithState } from "types";
|
||||
import { OfflineError } from "utils/errors";
|
||||
|
||||
const EMPTY_OBJECT = {};
|
||||
|
||||
type Props = {|
|
||||
match: Match,
|
||||
location: LocationWithState,
|
||||
@@ -51,6 +53,7 @@ export default function SharedDocumentScene(props: Props) {
|
||||
|
||||
return (
|
||||
<Document
|
||||
abilities={EMPTY_OBJECT}
|
||||
document={response.document}
|
||||
sharedTree={response.sharedTree}
|
||||
location={props.location}
|
||||
|
||||
@@ -248,7 +248,9 @@ class DataLoader extends React.Component<Props> {
|
||||
|
||||
const abilities = policies.abilities(document.id);
|
||||
|
||||
const key = team.multiplayerEditor
|
||||
// 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"
|
||||
|
||||
@@ -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;
|
||||
@@ -219,16 +237,15 @@ 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 {
|
||||
let savedDocument = document;
|
||||
if (auth.team && auth.team.multiplayerEditor) {
|
||||
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
|
||||
@@ -287,13 +304,21 @@ class DocumentScene extends React.Component<Props> {
|
||||
};
|
||||
|
||||
onChange = (getEditorText) => {
|
||||
const { auth } = this.props;
|
||||
if (auth.team?.multiplayerEditor) {
|
||||
return;
|
||||
}
|
||||
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) {
|
||||
@@ -331,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;
|
||||
|
||||
@@ -340,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
|
||||
@@ -376,7 +406,7 @@ class DocumentScene extends React.Component<Props> {
|
||||
when={
|
||||
this.isDirty &&
|
||||
!this.isUploading &&
|
||||
!team?.multiplayerEditor
|
||||
!team?.collaborativeEditing
|
||||
}
|
||||
message={t(
|
||||
`You have unsaved changes.\nAre you sure you want to discard them?`
|
||||
@@ -404,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}
|
||||
/>
|
||||
@@ -464,12 +495,12 @@ class DocumentScene extends React.Component<Props> {
|
||||
{showContents && <Contents headings={headings} />}
|
||||
<Editor
|
||||
id={document.id}
|
||||
key={disableEmbeds ? "disabled" : "enabled"}
|
||||
innerRef={this.editor}
|
||||
multiplayer={team?.multiplayerEditor}
|
||||
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}
|
||||
@@ -514,7 +545,12 @@ class DocumentScene extends React.Component<Props> {
|
||||
{isShare && !isCustomDomain() && (
|
||||
<Branding href="//www.getoutline.com?ref=sharelink" />
|
||||
)}
|
||||
{!isShare && <KeyboardShortcutsButton />}
|
||||
{!isShare && (
|
||||
<>
|
||||
<KeyboardShortcutsButton />
|
||||
<ConnectionStatus />
|
||||
</>
|
||||
)}
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
@@ -79,7 +81,6 @@ function DocumentHeader({
|
||||
const isNew = document.isNewDocument;
|
||||
const isTemplate = document.isTemplate;
|
||||
const can = policies.abilities(document.id);
|
||||
const canShareDocument = auth.team && auth.team.sharing && can.share;
|
||||
const canToggleEmbeds = auth.team && auth.team.documentEmbeds;
|
||||
const canEdit = can.update && !isEditing;
|
||||
|
||||
@@ -168,10 +169,13 @@ function DocumentHeader({
|
||||
/>
|
||||
{isEditing && !isTemplate && isNew && (
|
||||
<Action>
|
||||
<TemplatesMenu document={document} />
|
||||
<TemplatesMenu
|
||||
document={document}
|
||||
onSelectTemplate={onSelectTemplate}
|
||||
/>
|
||||
</Action>
|
||||
)}
|
||||
{!isEditing && canShareDocument && (!isMobile || !isTemplate) && (
|
||||
{!isEditing && (!isMobile || !isTemplate) && (
|
||||
<Action>
|
||||
<ShareButton document={document} />
|
||||
</Action>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,65 +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 from "components/Editor";
|
||||
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";
|
||||
|
||||
// TODO: typing
|
||||
export default function MultiplayerEditor(props: any) {
|
||||
const user = useCurrentUser();
|
||||
type Props = {|
|
||||
...EditorProps,
|
||||
id: string,
|
||||
|};
|
||||
|
||||
// TODO
|
||||
//const [showCachedDocument, setShowCachedDocument] = React.useState(true);
|
||||
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();
|
||||
|
||||
// React.useEffect(() => {
|
||||
// if (isRemoteSynced) {
|
||||
// setTimeout(() => setShowCachedDocument(false), 100);
|
||||
// }
|
||||
// }, [showCachedDocument, isRemoteSynced]);
|
||||
// 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(() => {
|
||||
console.log("extensions");
|
||||
|
||||
const ydoc = new Y.Doc();
|
||||
const provider = new HocuspocusProvider({
|
||||
url: env.MULTIPLAYER_URL,
|
||||
|
||||
// TODO: pipe documentId
|
||||
name: "example-document",
|
||||
document: ydoc,
|
||||
});
|
||||
if (!remoteProvider) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
new MultiplayerExtension({
|
||||
user,
|
||||
provider,
|
||||
provider: remoteProvider,
|
||||
document: ydoc,
|
||||
}),
|
||||
];
|
||||
}, [user]);
|
||||
}, [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 (
|
||||
<span style={{ position: "relative" }}>
|
||||
{true && (
|
||||
<Editor
|
||||
{...props}
|
||||
key="multiplayer"
|
||||
defaultValue={undefined}
|
||||
value={undefined}
|
||||
extensions={extensions}
|
||||
style={{ position: "absolute", width: "100%" }}
|
||||
/>
|
||||
)}
|
||||
{/* {showCachedDocument && (
|
||||
<Editor
|
||||
{...props}
|
||||
style={{ position: "absolute", width: "100%" }}
|
||||
readOnly
|
||||
/>
|
||||
)} */}
|
||||
</span>
|
||||
<Editor
|
||||
{...props}
|
||||
value={undefined}
|
||||
defaultValue={undefined}
|
||||
extensions={extensions}
|
||||
ref={ref}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.forwardRef<any, typeof MultiplayerEditor>(
|
||||
MultiplayerEditor
|
||||
);
|
||||
|
||||
@@ -61,6 +61,7 @@ function ShareButton({ document }: Props) {
|
||||
share={share}
|
||||
sharedParent={sharedParent}
|
||||
onSubmit={popover.hide}
|
||||
visible={popover.visible}
|
||||
/>
|
||||
</Popover>
|
||||
</>
|
||||
|
||||
@@ -23,22 +23,34 @@ type Props = {|
|
||||
share: Share,
|
||||
sharedParent: ?Share,
|
||||
onSubmit: () => void,
|
||||
visible: boolean,
|
||||
|};
|
||||
|
||||
function SharePopover({ document, share, sharedParent, onSubmit }: Props) {
|
||||
function SharePopover({
|
||||
document,
|
||||
share,
|
||||
sharedParent,
|
||||
onSubmit,
|
||||
visible,
|
||||
}: Props) {
|
||||
const { t } = useTranslation();
|
||||
const { policies, shares } = useStores();
|
||||
const { policies, shares, auth } = useStores();
|
||||
const { showToast } = useToasts();
|
||||
const [isCopied, setIsCopied] = React.useState(false);
|
||||
const timeout = React.useRef<?TimeoutID>();
|
||||
const can = policies.abilities(share ? share.id : "");
|
||||
const canPublish = can.update && !document.isTemplate;
|
||||
const documentAbilities = policies.abilities(document.id);
|
||||
const canPublish =
|
||||
can.update &&
|
||||
!document.isTemplate &&
|
||||
auth.team?.sharing &&
|
||||
documentAbilities.share;
|
||||
const isPubliclyShared = (share && share.published) || sharedParent;
|
||||
|
||||
React.useEffect(() => {
|
||||
document.share();
|
||||
if (visible) document.share();
|
||||
return () => clearTimeout(timeout.current);
|
||||
}, [document]);
|
||||
}, [document, visible]);
|
||||
|
||||
const handlePublishedChange = React.useCallback(
|
||||
async (event) => {
|
||||
@@ -102,7 +114,7 @@ function SharePopover({ document, share, sharedParent, onSubmit }: Props) {
|
||||
</Notice>
|
||||
)}
|
||||
|
||||
{canPublish && (
|
||||
{canPublish ? (
|
||||
<SwitchWrapper>
|
||||
<Switch
|
||||
id="published"
|
||||
@@ -132,8 +144,11 @@ function SharePopover({ document, share, sharedParent, onSubmit }: Props) {
|
||||
</SwitchText>
|
||||
</SwitchLabel>
|
||||
</SwitchWrapper>
|
||||
) : (
|
||||
<HelpText>{t("Only team members with permission can view")}</HelpText>
|
||||
)}
|
||||
{share && share.published && (
|
||||
|
||||
{canPublish && share?.published && (
|
||||
<SwitchWrapper>
|
||||
<Switch
|
||||
id="includeChildDocuments"
|
||||
|
||||
@@ -32,7 +32,7 @@ export default function DocumentScene(props: Props) {
|
||||
const urlParts = documentSlug ? documentSlug.split("-") : [];
|
||||
const urlId = urlParts.length ? urlParts[urlParts.length - 1] : undefined;
|
||||
const key = [urlId, revisionId].join("/");
|
||||
const isMultiplayer = team.multiplayerEditor;
|
||||
const isMultiplayer = team.collaborativeEditing;
|
||||
|
||||
return (
|
||||
<DataLoader key={key} match={props.match}>
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ function Home() {
|
||||
documents={documents.createdByUser(user)}
|
||||
fetch={documents.fetchOwned}
|
||||
options={{ user }}
|
||||
empty={<Empty>{t("Weird, this shouldn’t ever be empty")}</Empty>}
|
||||
empty={<Empty>{t("You haven’t created any documents yet")}</Empty>}
|
||||
showCollection
|
||||
/>
|
||||
</Route>
|
||||
|
||||
+22
-7
@@ -5,11 +5,13 @@ import * as React from "react";
|
||||
import { useTranslation, Trans } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import type { Role } from "shared/types";
|
||||
import Button from "components/Button";
|
||||
import CopyToClipboard from "components/CopyToClipboard";
|
||||
import Flex from "components/Flex";
|
||||
import HelpText from "components/HelpText";
|
||||
import Input from "components/Input";
|
||||
import InputSelectRole from "components/InputSelectRole";
|
||||
import NudeButton from "components/NudeButton";
|
||||
import Tooltip from "components/Tooltip";
|
||||
import useCurrentTeam from "hooks/useCurrentTeam";
|
||||
@@ -26,15 +28,16 @@ type Props = {|
|
||||
type InviteRequest = {
|
||||
email: string,
|
||||
name: string,
|
||||
role: Role,
|
||||
};
|
||||
|
||||
function Invite({ onSubmit }: Props) {
|
||||
const [isSaving, setIsSaving] = React.useState();
|
||||
const [linkCopied, setLinkCopied] = React.useState<boolean>(false);
|
||||
const [invites, setInvites] = React.useState<InviteRequest[]>([
|
||||
{ email: "", name: "" },
|
||||
{ email: "", name: "" },
|
||||
{ email: "", name: "" },
|
||||
{ email: "", name: "", role: "member" },
|
||||
{ email: "", name: "", role: "member" },
|
||||
{ email: "", name: "", role: "member" },
|
||||
]);
|
||||
|
||||
const { users, policies } = useStores();
|
||||
@@ -84,7 +87,7 @@ function Invite({ onSubmit }: Props) {
|
||||
|
||||
setInvites((prevInvites) => {
|
||||
const newInvites = [...prevInvites];
|
||||
newInvites.push({ email: "", name: "" });
|
||||
newInvites.push({ email: "", name: "", role: "member" });
|
||||
return newInvites;
|
||||
});
|
||||
}, [showToast, invites, t]);
|
||||
@@ -109,6 +112,14 @@ function Invite({ onSubmit }: Props) {
|
||||
});
|
||||
}, [showToast, t]);
|
||||
|
||||
const handleRoleChange = React.useCallback((ev, index) => {
|
||||
setInvites((prevInvites) => {
|
||||
const newInvites = [...prevInvites];
|
||||
newInvites[index]["role"] = ev.target.value;
|
||||
return newInvites;
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
{team.guestSignin ? (
|
||||
@@ -160,7 +171,7 @@ function Invite({ onSubmit }: Props) {
|
||||
</CopyBlock>
|
||||
)}
|
||||
{invites.map((invite, index) => (
|
||||
<Flex key={index}>
|
||||
<Flex key={index} gap={8}>
|
||||
<Input
|
||||
type="email"
|
||||
name="email"
|
||||
@@ -173,7 +184,6 @@ function Invite({ onSubmit }: Props) {
|
||||
autoFocus={index === 0}
|
||||
flex
|
||||
/>
|
||||
|
||||
<Input
|
||||
type="text"
|
||||
name="name"
|
||||
@@ -182,7 +192,12 @@ function Invite({ onSubmit }: Props) {
|
||||
onChange={(ev) => handleChange(ev, index)}
|
||||
value={invite.name}
|
||||
required={!!invite.email}
|
||||
flex
|
||||
/>
|
||||
<InputSelectRole
|
||||
onChange={(ev) => handleRoleChange(ev, index)}
|
||||
value={invite.role}
|
||||
labelHidden={index !== 0}
|
||||
short
|
||||
/>
|
||||
{index !== 0 && (
|
||||
<Remove>
|
||||
|
||||
@@ -28,6 +28,11 @@ export default function Notices() {
|
||||
an allowed team domain.
|
||||
</NoticeAlert>
|
||||
)}
|
||||
{notice === "malformed_user_info" && (
|
||||
<NoticeAlert>
|
||||
We could not read the user info supplied by your identity provider.
|
||||
</NoticeAlert>
|
||||
)}
|
||||
{notice === "email-auth-required" && (
|
||||
<NoticeAlert>
|
||||
Your account uses email sign-in, please sign-in with email to
|
||||
|
||||
@@ -11,7 +11,10 @@ import Button from "components/Button";
|
||||
import Heading from "components/Heading";
|
||||
import HelpText from "components/HelpText";
|
||||
import Notice from "components/Notice";
|
||||
import PaginatedList from "components/PaginatedList";
|
||||
import Scene from "components/Scene";
|
||||
import Subheading from "components/Subheading";
|
||||
import FileOperationListItem from "./components/FileOperationListItem";
|
||||
import useCurrentUser from "hooks/useCurrentUser";
|
||||
import useStores from "hooks/useStores";
|
||||
import useToasts from "hooks/useToasts";
|
||||
@@ -22,7 +25,7 @@ function ImportExport() {
|
||||
const { t } = useTranslation();
|
||||
const user = useCurrentUser();
|
||||
const fileRef = React.useRef();
|
||||
const { collections } = useStores();
|
||||
const { fileOperations, collections } = useStores();
|
||||
const { showToast } = useToasts();
|
||||
const [isLoading, setLoading] = React.useState(false);
|
||||
const [isImporting, setImporting] = React.useState(false);
|
||||
@@ -178,11 +181,10 @@ function ImportExport() {
|
||||
{t("Choose File")}…
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Heading>{t("Export")}</Heading>
|
||||
<HelpText>
|
||||
<Trans
|
||||
defaults="A full export might take some time, consider exporting a single document or collection if possible. We’ll put together a zip of all your documents in Markdown format and email it to <em>{{ userEmail }}</em>."
|
||||
defaults="A full export might take some time, consider exporting a single document or collection. The exported data is a zip of your documents in Markdown format. You may leave this page once the export has started – we will email a link to <em>{{ userEmail }}</em> when it’s complete."
|
||||
values={{ userEmail: user.email }}
|
||||
components={{ em: <strong /> }}
|
||||
/>
|
||||
@@ -199,6 +201,24 @@ function ImportExport() {
|
||||
? `${t("Requesting Export")}…`
|
||||
: t("Export Data")}
|
||||
</Button>
|
||||
<br />
|
||||
<br />
|
||||
<PaginatedList
|
||||
items={fileOperations.orderedDataExports}
|
||||
fetch={fileOperations.fetchPage}
|
||||
options={{ type: "export" }}
|
||||
heading={
|
||||
<Subheading>
|
||||
<Trans>Recent exports</Trans>
|
||||
</Subheading>
|
||||
}
|
||||
renderItem={(item) => (
|
||||
<FileOperationListItem
|
||||
key={item.id + item.state}
|
||||
fileOperation={item}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Scene>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -226,6 +226,7 @@ function People(props) {
|
||||
onChangePage={handleChangePage}
|
||||
page={page}
|
||||
totalPages={totalPages}
|
||||
defaultSortDirection="ASC"
|
||||
/>
|
||||
{can.inviteUser && (
|
||||
<Modal
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import FileOperation from "models/FileOperation";
|
||||
import Button from "components/Button";
|
||||
import ListItem from "components/List/Item";
|
||||
import Time from "components/Time";
|
||||
|
||||
type Props = {|
|
||||
fileOperation: FileOperation,
|
||||
|};
|
||||
|
||||
const FileOperationListItem = ({ fileOperation }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const stateMapping = {
|
||||
creating: t("Processing"),
|
||||
expired: t("Expired"),
|
||||
uploading: t("Processing"),
|
||||
error: t("Error"),
|
||||
};
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
title={
|
||||
fileOperation.collection
|
||||
? fileOperation.collection.name
|
||||
: t("All collections")
|
||||
}
|
||||
subtitle={
|
||||
<>
|
||||
{fileOperation.state !== "complete" && (
|
||||
<>{stateMapping[fileOperation.state]} • </>
|
||||
)}
|
||||
{t(`{{userName}} requested`, {
|
||||
userName:
|
||||
fileOperation.id === fileOperation.user.id
|
||||
? t("You")
|
||||
: fileOperation.user.name,
|
||||
})}
|
||||
|
||||
<Time dateTime={fileOperation.createdAt} addSuffix shorten />
|
||||
• {fileOperation.sizeInMB}
|
||||
</>
|
||||
}
|
||||
actions={
|
||||
fileOperation.state === "complete" ? (
|
||||
<Button
|
||||
as="a"
|
||||
href={`/api/fileOperations.redirect?id=${fileOperation.id}`}
|
||||
neutral
|
||||
>
|
||||
{t("Download")}
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileOperationListItem;
|
||||
@@ -1,64 +0,0 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import { StarredIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { type Match } from "react-router-dom";
|
||||
import { Action } from "components/Actions";
|
||||
import Empty from "components/Empty";
|
||||
import Heading from "components/Heading";
|
||||
import InputSearchPage from "components/InputSearchPage";
|
||||
import PaginatedDocumentList from "components/PaginatedDocumentList";
|
||||
import Scene from "components/Scene";
|
||||
import Tab from "components/Tab";
|
||||
import Tabs from "components/Tabs";
|
||||
import useStores from "hooks/useStores";
|
||||
import NewDocumentMenu from "menus/NewDocumentMenu";
|
||||
|
||||
type Props = {
|
||||
match: Match,
|
||||
};
|
||||
|
||||
function Starred(props: Props) {
|
||||
const { documents } = useStores();
|
||||
const { t } = useTranslation();
|
||||
const { fetchStarred, starred, starredAlphabetical } = documents;
|
||||
const { sort } = props.match.params;
|
||||
|
||||
return (
|
||||
<Scene
|
||||
icon={<StarredIcon color="currentColor" />}
|
||||
title={t("Starred")}
|
||||
actions={
|
||||
<>
|
||||
<Action>
|
||||
<InputSearchPage source="starred" label={t("Search documents")} />
|
||||
</Action>
|
||||
<Action>
|
||||
<NewDocumentMenu />
|
||||
</Action>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Heading>{t("Starred")}</Heading>
|
||||
<PaginatedDocumentList
|
||||
heading={
|
||||
<Tabs>
|
||||
<Tab to="/starred" exact>
|
||||
{t("Recently updated")}
|
||||
</Tab>
|
||||
<Tab to="/starred/alphabetical" exact>
|
||||
{t("Alphabetical")}
|
||||
</Tab>
|
||||
</Tabs>
|
||||
}
|
||||
empty={<Empty>{t("You’ve not starred any documents yet.")}</Empty>}
|
||||
fetch={fetchStarred}
|
||||
documents={sort === "alphabetical" ? starredAlphabetical : starred}
|
||||
showCollection
|
||||
/>
|
||||
</Scene>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(Starred);
|
||||
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
import { observer } from "mobx-react";
|
||||
import { TemplateIcon } from "outline-icons";
|
||||
import { ShapesIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { type Match } from "react-router-dom";
|
||||
@@ -29,7 +29,7 @@ function Templates(props: Props) {
|
||||
|
||||
return (
|
||||
<Scene
|
||||
icon={<TemplateIcon color="currentColor" />}
|
||||
icon={<ShapesIcon color="currentColor" />}
|
||||
title={t("Templates")}
|
||||
actions={
|
||||
<Action>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// @flow
|
||||
import { orderBy } from "lodash";
|
||||
import { computed } from "mobx";
|
||||
import FileOperation from "models/FileOperation";
|
||||
import BaseStore from "./BaseStore";
|
||||
import RootStore from "./RootStore";
|
||||
|
||||
export default class FileOperationsStore extends BaseStore<FileOperation> {
|
||||
actions = ["list", "info"];
|
||||
|
||||
constructor(rootStore: RootStore) {
|
||||
super(rootStore, FileOperation);
|
||||
}
|
||||
|
||||
@computed
|
||||
get exports(): FileOperation[] {
|
||||
return Array.from(this.data.values()).reduce(
|
||||
(acc, fileOp) => (fileOp.type === "export" ? [...acc, fileOp] : acc),
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
@computed
|
||||
get orderedDataExports(): FileOperation[] {
|
||||
return orderBy(this.exports, "createdAt", "desc");
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import CollectionsStore from "./CollectionsStore";
|
||||
import DocumentPresenceStore from "./DocumentPresenceStore";
|
||||
import DocumentsStore from "./DocumentsStore";
|
||||
import EventsStore from "./EventsStore";
|
||||
import FileOperationsStore from "./FileOperationsStore";
|
||||
import GroupMembershipsStore from "./GroupMembershipsStore";
|
||||
import GroupsStore from "./GroupsStore";
|
||||
import IntegrationsStore from "./IntegrationsStore";
|
||||
@@ -39,6 +40,7 @@ export default class RootStore {
|
||||
users: UsersStore;
|
||||
views: ViewsStore;
|
||||
toasts: ToastsStore;
|
||||
fileOperations: FileOperationsStore;
|
||||
|
||||
constructor() {
|
||||
// PoliciesStore must be initialized before AuthStore
|
||||
@@ -60,6 +62,7 @@ export default class RootStore {
|
||||
this.ui = new UiStore();
|
||||
this.users = new UsersStore(this);
|
||||
this.views = new ViewsStore(this);
|
||||
this.fileOperations = new FileOperationsStore(this);
|
||||
this.toasts = new ToastsStore();
|
||||
}
|
||||
|
||||
@@ -79,6 +82,7 @@ export default class RootStore {
|
||||
this.policies.clear();
|
||||
this.revisions.clear();
|
||||
this.shares.clear();
|
||||
this.fileOperations.clear();
|
||||
// this.ui omitted to keep ui settings between sessions
|
||||
this.users.clear();
|
||||
this.views.clear();
|
||||
|
||||
@@ -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;
|
||||
|
||||
+16
-16
@@ -2,7 +2,7 @@
|
||||
import invariant from "invariant";
|
||||
import { filter, orderBy } from "lodash";
|
||||
import { observable, computed, action, runInAction } from "mobx";
|
||||
import type { Rank } from "shared/types";
|
||||
import type { Role } from "shared/types";
|
||||
import User from "models/User";
|
||||
import BaseStore from "./BaseStore";
|
||||
import RootStore from "./RootStore";
|
||||
@@ -68,20 +68,20 @@ export default class UsersStore extends BaseStore<User> {
|
||||
@action
|
||||
promote = async (user: User) => {
|
||||
try {
|
||||
this.updateCounts("Admin", user.rank);
|
||||
this.updateCounts("admin", user.role);
|
||||
await this.actionOnUser("promote", user);
|
||||
} catch {
|
||||
this.updateCounts(user.rank, "Admin");
|
||||
this.updateCounts(user.role, "admin");
|
||||
}
|
||||
};
|
||||
|
||||
@action
|
||||
demote = async (user: User, to: Rank) => {
|
||||
demote = async (user: User, to: Role) => {
|
||||
try {
|
||||
this.updateCounts(to, user.rank);
|
||||
this.updateCounts(to, user.role);
|
||||
await this.actionOnUser("demote", user, to);
|
||||
} catch {
|
||||
this.updateCounts(user.rank, to);
|
||||
this.updateCounts(user.role, to);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -110,7 +110,7 @@ export default class UsersStore extends BaseStore<User> {
|
||||
};
|
||||
|
||||
@action
|
||||
invite = async (invites: { email: string, name: string }[]) => {
|
||||
invite = async (invites: { email: string, name: string, role: Role }[]) => {
|
||||
const res = await client.post(`/users.invite`, { invites });
|
||||
invariant(res && res.data, "Data should be available");
|
||||
runInAction(`invite`, () => {
|
||||
@@ -152,24 +152,24 @@ export default class UsersStore extends BaseStore<User> {
|
||||
}
|
||||
|
||||
@action
|
||||
updateCounts = (to: Rank, from: Rank) => {
|
||||
if (to === "Admin") {
|
||||
updateCounts = (to: Role, from: Role) => {
|
||||
if (to === "admin") {
|
||||
this.counts.admins += 1;
|
||||
if (from === "Viewer") {
|
||||
if (from === "viewer") {
|
||||
this.counts.viewers -= 1;
|
||||
}
|
||||
}
|
||||
if (to === "Viewer") {
|
||||
if (to === "viewer") {
|
||||
this.counts.viewers += 1;
|
||||
if (from === "Admin") {
|
||||
if (from === "admin") {
|
||||
this.counts.admins -= 1;
|
||||
}
|
||||
}
|
||||
if (to === "Member") {
|
||||
if (from === "Viewer") {
|
||||
if (to === "member") {
|
||||
if (from === "viewer") {
|
||||
this.counts.viewers -= 1;
|
||||
}
|
||||
if (from === "Admin") {
|
||||
if (from === "admin") {
|
||||
this.counts.admins -= 1;
|
||||
}
|
||||
}
|
||||
@@ -233,7 +233,7 @@ export default class UsersStore extends BaseStore<User> {
|
||||
return queriedUsers(users, query);
|
||||
};
|
||||
|
||||
actionOnUser = async (action: string, user: User, to?: Rank) => {
|
||||
actionOnUser = async (action: string, user: User, to?: Role) => {
|
||||
const res = await client.post(`/users.${action}`, {
|
||||
id: user.id,
|
||||
to,
|
||||
|
||||
@@ -66,6 +66,7 @@ export type MenuItem =
|
||||
visible?: boolean,
|
||||
selected?: boolean,
|
||||
disabled?: boolean,
|
||||
icon?: React.Node,
|
||||
|}
|
||||
| {|
|
||||
title: React.Node,
|
||||
@@ -73,6 +74,7 @@ export type MenuItem =
|
||||
visible?: boolean,
|
||||
selected?: boolean,
|
||||
disabled?: boolean,
|
||||
icon?: React.Node,
|
||||
|}
|
||||
| {|
|
||||
title: React.Node,
|
||||
@@ -81,6 +83,7 @@ export type MenuItem =
|
||||
selected?: boolean,
|
||||
disabled?: boolean,
|
||||
level?: number,
|
||||
icon?: React.Node,
|
||||
|}
|
||||
| {|
|
||||
title: React.Node,
|
||||
@@ -89,6 +92,7 @@ export type MenuItem =
|
||||
style?: Object,
|
||||
hover?: boolean,
|
||||
items: MenuItem[],
|
||||
icon?: React.Node,
|
||||
|}
|
||||
| {|
|
||||
type: "separator",
|
||||
|
||||
+13
-6
@@ -4,14 +4,20 @@ import {
|
||||
isYesterday,
|
||||
differenceInCalendarWeeks,
|
||||
differenceInCalendarMonths,
|
||||
differenceInCalendarYears,
|
||||
format as formatDate,
|
||||
} from "date-fns";
|
||||
import * as React from "react";
|
||||
import { type TFunction } from "react-i18next";
|
||||
import LocaleTime from "components/LocaleTime";
|
||||
import { dateLocale } from "utils/i18n";
|
||||
|
||||
export function dateToHeading(dateTime: string, t: TFunction) {
|
||||
export function dateToHeading(
|
||||
dateTime: string,
|
||||
t: TFunction,
|
||||
userLocale: ?string
|
||||
) {
|
||||
const date = Date.parse(dateTime);
|
||||
const now = new Date();
|
||||
const locale = dateLocale(userLocale);
|
||||
|
||||
if (isToday(date)) {
|
||||
return t("Today");
|
||||
@@ -26,7 +32,7 @@ export function dateToHeading(dateTime: string, t: TFunction) {
|
||||
// async bundle loading of languages
|
||||
const weekDiff = differenceInCalendarWeeks(now, date);
|
||||
if (weekDiff === 0) {
|
||||
return <LocaleTime dateTime={dateTime} tooltip={false} format="iiii" />;
|
||||
return formatDate(Date.parse(dateTime), "iiii", { locale });
|
||||
}
|
||||
|
||||
if (weekDiff === 1) {
|
||||
@@ -42,10 +48,11 @@ export function dateToHeading(dateTime: string, t: TFunction) {
|
||||
return t("Last month");
|
||||
}
|
||||
|
||||
if (monthDiff <= 12) {
|
||||
const yearDiff = differenceInCalendarYears(now, date);
|
||||
if (yearDiff === 0) {
|
||||
return t("This year");
|
||||
}
|
||||
|
||||
// If older than the current calendar year then just print the year e.g 2020
|
||||
return <LocaleTime dateTime={dateTime} tooltip={false} format="y" />;
|
||||
return formatDate(Date.parse(dateTime), "y", { locale });
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// @flow
|
||||
import {
|
||||
enUS,
|
||||
de,
|
||||
faIR,
|
||||
fr,
|
||||
es,
|
||||
it,
|
||||
ja,
|
||||
ko,
|
||||
ptBR,
|
||||
pt,
|
||||
zhCN,
|
||||
zhTW,
|
||||
ru,
|
||||
} from "date-fns/locale";
|
||||
|
||||
const locales = {
|
||||
en_US: enUS,
|
||||
de_DE: de,
|
||||
es_ES: es,
|
||||
fa_IR: faIR,
|
||||
fr_FR: fr,
|
||||
it_IT: it,
|
||||
ja_JP: ja,
|
||||
ko_KR: ko,
|
||||
pt_BR: ptBR,
|
||||
pt_PT: pt,
|
||||
zh_CN: zhCN,
|
||||
zh_TW: zhTW,
|
||||
ru_RU: ru,
|
||||
};
|
||||
|
||||
export function dateLocale(userLocale: ?string) {
|
||||
return userLocale ? locales[userLocale] : undefined;
|
||||
}
|
||||
@@ -7,10 +7,6 @@ export function homeUrl(): string {
|
||||
return "/home";
|
||||
}
|
||||
|
||||
export function starredUrl(): string {
|
||||
return "/starred";
|
||||
}
|
||||
|
||||
export function newCollectionUrl(): string {
|
||||
return "/collection/new";
|
||||
}
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
# Architecture
|
||||
|
||||
Outline is composed of a backend and frontend codebase in this monorepo. As both are written in Javascript, they share some code where possible. We utilize the latest ES6 language features, including `async`/`await`, and [Flow](https://flow.org/) typing. Prettier formatting and ESLint are enforced by CI.
|
||||
@@ -46,7 +45,9 @@ server
|
||||
├── onboarding - Markdown templates for onboarding documents
|
||||
├── policies - Authorization logic based on cancan
|
||||
├── presenters - JSON presenters for database models, the interface between backend -> frontend
|
||||
├── services - Service definitions are triggered for events and perform async jobs
|
||||
├── queues - Async queue definitions
|
||||
│ └── processors - Processors perform async jobs, usually working on events from the event bus
|
||||
├── services - Services start distinct portions of the application eg api, worker
|
||||
├── static - Static assets
|
||||
├── test - Test helpers and fixtures, tests themselves are colocated
|
||||
└── utils - Utility methods specific to the backend
|
||||
@@ -64,4 +65,4 @@ shared
|
||||
├── styles - Styles, colors and other global aesthetics
|
||||
├── utils - Shared utility methods
|
||||
└── constants - Shared constants
|
||||
```
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
Vendored
+3
@@ -3,6 +3,9 @@ declare var process: {
|
||||
exit: (code?: number) => void,
|
||||
cwd: () => string,
|
||||
argv: Array<string>,
|
||||
on: (event: string, listener: Function) => void,
|
||||
once: (event: string, listener: Function) => void,
|
||||
off: (event: string, listener: Function) => void,
|
||||
env: {
|
||||
[string]: string,
|
||||
},
|
||||
|
||||
Vendored
+377
@@ -0,0 +1,377 @@
|
||||
// flow-typed signature: 97da878aea98698d6c06f8a696bb62af
|
||||
// flow-typed version: <<STUB>>/lib0_v0.2.34/flow_v0.104.0
|
||||
|
||||
/**
|
||||
* This is an autogenerated libdef stub for:
|
||||
*
|
||||
* 'lib0'
|
||||
*
|
||||
* Fill this stub out by replacing all the `any` types.
|
||||
*
|
||||
* Once filled out, we encourage you to share your work with the
|
||||
* community by sending a pull request to:
|
||||
* https://github.com/flowtype/flow-typed
|
||||
*/
|
||||
|
||||
// @flow
|
||||
declare module "lib0" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* We include stubs for each file inside this npm package in case you need to
|
||||
* require those files directly. Feel free to delete any files that aren't
|
||||
* needed.
|
||||
*/
|
||||
declare module "lib0/array" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/bin/gendocs" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/binary" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/broadcastchannel" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/buffer" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/component" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/conditions" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/decoding" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/diff" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/dist/test" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/dom" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/encoding" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/environment" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/error" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/eventloop" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/function" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/indexeddb" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/isomorphic" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/iterator" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/json" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/logging" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/map" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/math" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/metric" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/mutex" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/number" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/object" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/observable" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/pair" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/prng" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/prng/Mt19937" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/prng/Xoroshiro128plus" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/prng/Xorshift32" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/promise" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/queue" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/random" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/set" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/sort" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/statistics" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/storage" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/string" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/symbol" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/test" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/testing" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/time" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/tree" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/url" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/websocket" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
// Filename aliases
|
||||
declare module "lib0/array.js" {
|
||||
declare module.exports: $Exports<"lib0/array">;
|
||||
}
|
||||
declare module "lib0/bin/gendocs.js" {
|
||||
declare module.exports: $Exports<"lib0/bin/gendocs">;
|
||||
}
|
||||
declare module "lib0/binary.js" {
|
||||
declare module.exports: $Exports<"lib0/binary">;
|
||||
}
|
||||
declare module "lib0/broadcastchannel.js" {
|
||||
declare module.exports: $Exports<"lib0/broadcastchannel">;
|
||||
}
|
||||
declare module "lib0/buffer.js" {
|
||||
declare module.exports: $Exports<"lib0/buffer">;
|
||||
}
|
||||
declare module "lib0/component.js" {
|
||||
declare module.exports: $Exports<"lib0/component">;
|
||||
}
|
||||
declare module "lib0/conditions.js" {
|
||||
declare module.exports: $Exports<"lib0/conditions">;
|
||||
}
|
||||
declare module "lib0/decoding.js" {
|
||||
declare module.exports: $Exports<"lib0/decoding">;
|
||||
}
|
||||
declare module "lib0/dist/decoding.cjs" {
|
||||
declare module.exports: $Exports<"lib0/decoding">;
|
||||
}
|
||||
declare module "lib0/diff.js" {
|
||||
declare module.exports: $Exports<"lib0/diff">;
|
||||
}
|
||||
declare module "lib0/dist/test.js" {
|
||||
declare module.exports: $Exports<"lib0/dist/test">;
|
||||
}
|
||||
declare module "lib0/dom.js" {
|
||||
declare module.exports: $Exports<"lib0/dom">;
|
||||
}
|
||||
declare module "lib0/encoding.js" {
|
||||
declare module.exports: $Exports<"lib0/encoding">;
|
||||
}
|
||||
declare module "lib0/dist/encoding.cjs" {
|
||||
declare module.exports: $Exports<"lib0/encoding">;
|
||||
}
|
||||
declare module "lib0/environment.js" {
|
||||
declare module.exports: $Exports<"lib0/environment">;
|
||||
}
|
||||
declare module "lib0/error.js" {
|
||||
declare module.exports: $Exports<"lib0/error">;
|
||||
}
|
||||
declare module "lib0/eventloop.js" {
|
||||
declare module.exports: $Exports<"lib0/eventloop">;
|
||||
}
|
||||
declare module "lib0/function.js" {
|
||||
declare module.exports: $Exports<"lib0/function">;
|
||||
}
|
||||
declare module "lib0/index" {
|
||||
declare module.exports: $Exports<"lib0">;
|
||||
}
|
||||
declare module "lib0/index.js" {
|
||||
declare module.exports: $Exports<"lib0">;
|
||||
}
|
||||
declare module "lib0/indexeddb.js" {
|
||||
declare module.exports: $Exports<"lib0/indexeddb">;
|
||||
}
|
||||
declare module "lib0/isomorphic.js" {
|
||||
declare module.exports: $Exports<"lib0/isomorphic">;
|
||||
}
|
||||
declare module "lib0/iterator.js" {
|
||||
declare module.exports: $Exports<"lib0/iterator">;
|
||||
}
|
||||
declare module "lib0/json.js" {
|
||||
declare module.exports: $Exports<"lib0/json">;
|
||||
}
|
||||
declare module "lib0/logging.js" {
|
||||
declare module.exports: $Exports<"lib0/logging">;
|
||||
}
|
||||
declare module "lib0/map.js" {
|
||||
declare module.exports: $Exports<"lib0/map">;
|
||||
}
|
||||
declare module "lib0/math.js" {
|
||||
declare module.exports: $Exports<"lib0/math">;
|
||||
}
|
||||
declare module "lib0/metric.js" {
|
||||
declare module.exports: $Exports<"lib0/metric">;
|
||||
}
|
||||
declare module "lib0/mutex.js" {
|
||||
declare module.exports: $Exports<"lib0/mutex">;
|
||||
}
|
||||
declare module "lib0/dist/mutex.cjs" {
|
||||
declare module.exports: $Exports<"lib0/mutex">;
|
||||
}
|
||||
declare module "lib0/number.js" {
|
||||
declare module.exports: $Exports<"lib0/number">;
|
||||
}
|
||||
declare module "lib0/object.js" {
|
||||
declare module.exports: $Exports<"lib0/object">;
|
||||
}
|
||||
declare module "lib0/observable.js" {
|
||||
declare module.exports: $Exports<"lib0/observable">;
|
||||
}
|
||||
declare module "lib0/pair.js" {
|
||||
declare module.exports: $Exports<"lib0/pair">;
|
||||
}
|
||||
declare module "lib0/prng.js" {
|
||||
declare module.exports: $Exports<"lib0/prng">;
|
||||
}
|
||||
declare module "lib0/prng/Mt19937.js" {
|
||||
declare module.exports: $Exports<"lib0/prng/Mt19937">;
|
||||
}
|
||||
declare module "lib0/prng/Xoroshiro128plus.js" {
|
||||
declare module.exports: $Exports<"lib0/prng/Xoroshiro128plus">;
|
||||
}
|
||||
declare module "lib0/prng/Xorshift32.js" {
|
||||
declare module.exports: $Exports<"lib0/prng/Xorshift32">;
|
||||
}
|
||||
declare module "lib0/promise.js" {
|
||||
declare module.exports: $Exports<"lib0/promise">;
|
||||
}
|
||||
declare module "lib0/queue.js" {
|
||||
declare module.exports: $Exports<"lib0/queue">;
|
||||
}
|
||||
declare module "lib0/random.js" {
|
||||
declare module.exports: $Exports<"lib0/random">;
|
||||
}
|
||||
declare module "lib0/set.js" {
|
||||
declare module.exports: $Exports<"lib0/set">;
|
||||
}
|
||||
declare module "lib0/sort.js" {
|
||||
declare module.exports: $Exports<"lib0/sort">;
|
||||
}
|
||||
declare module "lib0/statistics.js" {
|
||||
declare module.exports: $Exports<"lib0/statistics">;
|
||||
}
|
||||
declare module "lib0/storage.js" {
|
||||
declare module.exports: $Exports<"lib0/storage">;
|
||||
}
|
||||
declare module "lib0/string.js" {
|
||||
declare module.exports: $Exports<"lib0/string">;
|
||||
}
|
||||
declare module "lib0/symbol.js" {
|
||||
declare module.exports: $Exports<"lib0/symbol">;
|
||||
}
|
||||
declare module "lib0/test.js" {
|
||||
declare module.exports: $Exports<"lib0/test">;
|
||||
}
|
||||
declare module "lib0/testing.js" {
|
||||
declare module.exports: $Exports<"lib0/testing">;
|
||||
}
|
||||
declare module "lib0/time.js" {
|
||||
declare module.exports: $Exports<"lib0/time">;
|
||||
}
|
||||
declare module "lib0/tree.js" {
|
||||
declare module.exports: $Exports<"lib0/tree">;
|
||||
}
|
||||
declare module "lib0/url.js" {
|
||||
declare module.exports: $Exports<"lib0/url">;
|
||||
}
|
||||
declare module "lib0/websocket.js" {
|
||||
declare module.exports: $Exports<"lib0/websocket">;
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
// flow-typed signature: 97da878aea98698d6c06f8a696bb62af
|
||||
// flow-typed version: <<STUB>>/lib0_v0.2.34/flow_v0.104.0
|
||||
|
||||
/**
|
||||
* This is an autogenerated libdef stub for:
|
||||
*
|
||||
* 'lib0'
|
||||
*
|
||||
* Fill this stub out by replacing all the `any` types.
|
||||
*
|
||||
* Once filled out, we encourage you to share your work with the
|
||||
* community by sending a pull request to:
|
||||
* https://github.com/flowtype/flow-typed
|
||||
*/
|
||||
|
||||
// @flow
|
||||
declare module "lib0" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* We include stubs for each file inside this npm package in case you need to
|
||||
* require those files directly. Feel free to delete any files that aren't
|
||||
* needed.
|
||||
*/
|
||||
declare module "lib0/array" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/bin/gendocs" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/binary" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/broadcastchannel" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/buffer" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/component" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/conditions" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/decoding" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/diff" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/dist/test" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/dom" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/encoding" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/environment" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/error" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/eventloop" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/function" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/indexeddb" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/isomorphic" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/iterator" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/json" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/logging" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/map" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/math" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/metric" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/mutex" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/number" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/object" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/observable" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/pair" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/prng" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/prng/Mt19937" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/prng/Xoroshiro128plus" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/prng/Xorshift32" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/promise" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/queue" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/random" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/set" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/sort" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/statistics" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/storage" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/string" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/symbol" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/test" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/testing" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/time" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/tree" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/url" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/websocket" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
// Filename aliases
|
||||
declare module "lib0/array.js" {
|
||||
declare module.exports: $Exports<"lib0/array">;
|
||||
}
|
||||
declare module "lib0/bin/gendocs.js" {
|
||||
declare module.exports: $Exports<"lib0/bin/gendocs">;
|
||||
}
|
||||
declare module "lib0/binary.js" {
|
||||
declare module.exports: $Exports<"lib0/binary">;
|
||||
}
|
||||
declare module "lib0/broadcastchannel.js" {
|
||||
declare module.exports: $Exports<"lib0/broadcastchannel">;
|
||||
}
|
||||
declare module "lib0/buffer.js" {
|
||||
declare module.exports: $Exports<"lib0/buffer">;
|
||||
}
|
||||
declare module "lib0/component.js" {
|
||||
declare module.exports: $Exports<"lib0/component">;
|
||||
}
|
||||
declare module "lib0/conditions.js" {
|
||||
declare module.exports: $Exports<"lib0/conditions">;
|
||||
}
|
||||
declare module "lib0/decoding.js" {
|
||||
declare module.exports: $Exports<"lib0/decoding">;
|
||||
}
|
||||
declare module "lib0/dist/decoding.cjs" {
|
||||
declare module.exports: $Exports<"lib0/decoding">;
|
||||
}
|
||||
declare module "lib0/diff.js" {
|
||||
declare module.exports: $Exports<"lib0/diff">;
|
||||
}
|
||||
declare module "lib0/dist/test.js" {
|
||||
declare module.exports: $Exports<"lib0/dist/test">;
|
||||
}
|
||||
declare module "lib0/dom.js" {
|
||||
declare module.exports: $Exports<"lib0/dom">;
|
||||
}
|
||||
declare module "lib0/encoding.js" {
|
||||
declare module.exports: $Exports<"lib0/encoding">;
|
||||
}
|
||||
declare module "lib0/dist/encoding.cjs" {
|
||||
declare module.exports: $Exports<"lib0/encoding">;
|
||||
}
|
||||
declare module "lib0/environment.js" {
|
||||
declare module.exports: $Exports<"lib0/environment">;
|
||||
}
|
||||
declare module "lib0/error.js" {
|
||||
declare module.exports: $Exports<"lib0/error">;
|
||||
}
|
||||
declare module "lib0/eventloop.js" {
|
||||
declare module.exports: $Exports<"lib0/eventloop">;
|
||||
}
|
||||
declare module "lib0/function.js" {
|
||||
declare module.exports: $Exports<"lib0/function">;
|
||||
}
|
||||
declare module "lib0/index" {
|
||||
declare module.exports: $Exports<"lib0">;
|
||||
}
|
||||
declare module "lib0/index.js" {
|
||||
declare module.exports: $Exports<"lib0">;
|
||||
}
|
||||
declare module "lib0/indexeddb.js" {
|
||||
declare module.exports: $Exports<"lib0/indexeddb">;
|
||||
}
|
||||
declare module "lib0/isomorphic.js" {
|
||||
declare module.exports: $Exports<"lib0/isomorphic">;
|
||||
}
|
||||
declare module "lib0/iterator.js" {
|
||||
declare module.exports: $Exports<"lib0/iterator">;
|
||||
}
|
||||
declare module "lib0/json.js" {
|
||||
declare module.exports: $Exports<"lib0/json">;
|
||||
}
|
||||
declare module "lib0/logging.js" {
|
||||
declare module.exports: $Exports<"lib0/logging">;
|
||||
}
|
||||
declare module "lib0/map.js" {
|
||||
declare module.exports: $Exports<"lib0/map">;
|
||||
}
|
||||
declare module "lib0/math.js" {
|
||||
declare module.exports: $Exports<"lib0/math">;
|
||||
}
|
||||
declare module "lib0/metric.js" {
|
||||
declare module.exports: $Exports<"lib0/metric">;
|
||||
}
|
||||
declare module "lib0/mutex.js" {
|
||||
declare module.exports: $Exports<"lib0/mutex">;
|
||||
}
|
||||
declare module "lib0/dist/mutex.cjs" {
|
||||
declare module.exports: $Exports<"lib0/mutex">;
|
||||
}
|
||||
declare module "lib0/number.js" {
|
||||
declare module.exports: $Exports<"lib0/number">;
|
||||
}
|
||||
declare module "lib0/object.js" {
|
||||
declare module.exports: $Exports<"lib0/object">;
|
||||
}
|
||||
declare module "lib0/observable.js" {
|
||||
declare module.exports: $Exports<"lib0/observable">;
|
||||
}
|
||||
declare module "lib0/pair.js" {
|
||||
declare module.exports: $Exports<"lib0/pair">;
|
||||
}
|
||||
declare module "lib0/prng.js" {
|
||||
declare module.exports: $Exports<"lib0/prng">;
|
||||
}
|
||||
declare module "lib0/prng/Mt19937.js" {
|
||||
declare module.exports: $Exports<"lib0/prng/Mt19937">;
|
||||
}
|
||||
declare module "lib0/prng/Xoroshiro128plus.js" {
|
||||
declare module.exports: $Exports<"lib0/prng/Xoroshiro128plus">;
|
||||
}
|
||||
declare module "lib0/prng/Xorshift32.js" {
|
||||
declare module.exports: $Exports<"lib0/prng/Xorshift32">;
|
||||
}
|
||||
declare module "lib0/promise.js" {
|
||||
declare module.exports: $Exports<"lib0/promise">;
|
||||
}
|
||||
declare module "lib0/queue.js" {
|
||||
declare module.exports: $Exports<"lib0/queue">;
|
||||
}
|
||||
declare module "lib0/random.js" {
|
||||
declare module.exports: $Exports<"lib0/random">;
|
||||
}
|
||||
declare module "lib0/set.js" {
|
||||
declare module.exports: $Exports<"lib0/set">;
|
||||
}
|
||||
declare module "lib0/sort.js" {
|
||||
declare module.exports: $Exports<"lib0/sort">;
|
||||
}
|
||||
declare module "lib0/statistics.js" {
|
||||
declare module.exports: $Exports<"lib0/statistics">;
|
||||
}
|
||||
declare module "lib0/storage.js" {
|
||||
declare module.exports: $Exports<"lib0/storage">;
|
||||
}
|
||||
declare module "lib0/string.js" {
|
||||
declare module.exports: $Exports<"lib0/string">;
|
||||
}
|
||||
declare module "lib0/symbol.js" {
|
||||
declare module.exports: $Exports<"lib0/symbol">;
|
||||
}
|
||||
declare module "lib0/test.js" {
|
||||
declare module.exports: $Exports<"lib0/test">;
|
||||
}
|
||||
declare module "lib0/testing.js" {
|
||||
declare module.exports: $Exports<"lib0/testing">;
|
||||
}
|
||||
declare module "lib0/time.js" {
|
||||
declare module.exports: $Exports<"lib0/time">;
|
||||
}
|
||||
declare module "lib0/tree.js" {
|
||||
declare module.exports: $Exports<"lib0/tree">;
|
||||
}
|
||||
declare module "lib0/url.js" {
|
||||
declare module.exports: $Exports<"lib0/url">;
|
||||
}
|
||||
declare module "lib0/websocket.js" {
|
||||
declare module.exports: $Exports<"lib0/websocket">;
|
||||
}
|
||||
Vendored
+39
@@ -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'>;
|
||||
}
|
||||
+67
@@ -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'>;
|
||||
}
|
||||
Vendored
+67
@@ -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">;
|
||||
}
|
||||
Vendored
+430
@@ -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'>;
|
||||
}
|
||||
+18
-13
@@ -5,15 +5,13 @@
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"clean": "rimraf build",
|
||||
"build:i18n": "i18next '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:multiplayer": "babel -d ./build/multiplayer ./multiplayer",
|
||||
"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 ./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",
|
||||
"start:multiplayer": "node ./build/multiplayer/index.js",
|
||||
"dev": "nodemon --exec \"yarn build:server && yarn build:i18n && node --inspect=0.0.0.0 build/server/index.js\" -e js --ignore build/ --ignore app/ --ignore flow-typed/",
|
||||
"dev:multiplayer": "nodemon --exec \"yarn build:server && node --inspect=0.0.0.0 build/server/index.js --multiplayer\" -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",
|
||||
@@ -48,9 +46,11 @@
|
||||
"@babel/preset-env": "^7.11.0",
|
||||
"@babel/preset-flow": "^7.10.4",
|
||||
"@babel/preset-react": "^7.10.4",
|
||||
"@hocuspocus/extension-logger": "^1.0.0-alpha.35",
|
||||
"@hocuspocus/provider": "^1.0.0-alpha.8",
|
||||
"@hocuspocus/server": "^1.0.0-alpha.60",
|
||||
"@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",
|
||||
@@ -64,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",
|
||||
@@ -102,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",
|
||||
@@ -117,10 +118,11 @@
|
||||
"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",
|
||||
"passport-oauth2": "^1.6.0",
|
||||
"passport-slack-oauth2": "^1.1.0",
|
||||
"pg": "^8.5.1",
|
||||
"pg-hstore": "^2.3.3",
|
||||
@@ -149,7 +151,7 @@
|
||||
"react-window": "^1.8.6",
|
||||
"reakit": "^1.3.8",
|
||||
"regenerator-runtime": "^0.13.7",
|
||||
"rich-markdown-editor": "^11.17.2",
|
||||
"rich-markdown-editor": "^11.17.5",
|
||||
"semver": "^7.3.2",
|
||||
"sequelize": "^6.3.4",
|
||||
"sequelize-cli": "^6.2.0",
|
||||
@@ -161,6 +163,7 @@
|
||||
"socket.io": "^2.4.0",
|
||||
"socket.io-redis": "^5.4.0",
|
||||
"socketio-auth": "^0.1.1",
|
||||
"stoppable": "^1.1.0",
|
||||
"string-replace-to-array": "^1.0.3",
|
||||
"styled-components": "^5.2.3",
|
||||
"styled-components-breakpoint": "^2.1.1",
|
||||
@@ -172,6 +175,7 @@
|
||||
"utf8": "^3.0.0",
|
||||
"uuid": "^8.3.2",
|
||||
"validator": "5.2.0",
|
||||
"y-indexeddb": "^9.0.6",
|
||||
"y-prosemirror": "^1.0.9",
|
||||
"yjs": "^13.5.12"
|
||||
},
|
||||
@@ -183,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",
|
||||
@@ -219,5 +224,5 @@
|
||||
"dot-prop": "^5.2.0",
|
||||
"js-yaml": "^3.13.1"
|
||||
},
|
||||
"version": "0.58.0"
|
||||
"version": "0.59.0"
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
@@ -2,7 +2,6 @@
|
||||
|
||||
exports[`Mailer #welcome 1`] = `
|
||||
Object {
|
||||
"attachments": undefined,
|
||||
"from": "hello@example.com",
|
||||
"html": "
|
||||
<!DOCTYPE html PUBLIC \\"-//W3C//DTD XHTML 1.0 Strict//EN\\" \\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\\">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user