Compare commits

..

107 Commits

Author SHA1 Message Date
Tom Moor 5690c5e9fd fix: Embed frame alignment 2021-09-10 22:15:36 -07:00
Tom Moor ae1e115539 docs 2021-09-10 22:15:36 -07:00
Tom Moor a4e2054017 Merge branch 'main' into collaborative-editing 2021-09-10 21:22:01 -07:00
Tom Moor 9189d08118 lint 2021-09-10 21:16:56 -07:00
Tom Moor 0a998789a3 chore: Support Redis v6 on Heroku 2021-09-10 21:05:06 -07:00
Tom Moor af2aefa8f5 simplify feature switch 2021-09-10 21:02:44 -07:00
Tom Moor 77c86ddbfb Sentry location 2021-09-10 20:08:15 -07:00
Tom Moor 7aaec29af6 Merge main 2021-09-10 20:06:28 -07:00
Tom Moor 92016bbd06 fix: List behavior when ordered list starts at number other than 1
fix: Image improvements
fix: Image upload race condition
2021-09-10 19:06:54 -07:00
Tom Moor 231ab2da03 fix: Add recording of job errors, remove from queues on failure, centralize options 2021-09-09 22:38:34 -07:00
Tom Moor bd880ee984 chore: Add basic logging of metrics to event queue (#2545)
* chore: Add basic logging of metrics to event queue
closes #2524

* Better naming for multiple queue types

* Add stalled event
2021-09-09 21:55:45 -07:00
Tom Moor 995c6f90b7 fix: Mount _health route before catch-alls
closes #2536
2021-09-09 21:08:34 -07:00
Tom Moor 8ac853bb8b fix: Printing from doc menu is blank in Firefox
closes #2543
2021-09-09 20:41:56 -07:00
Tom Moor 26c14bcead fix: Make collaborative config more lenient 2021-09-09 18:16:09 -07:00
Tom Moor 55c099b895 fix: Handle empty text nodes 2021-09-09 18:15:56 -07:00
Tom Moor ebcb303d0e fix: Allow Redis v6 on Heroku 2021-09-08 21:03:12 -07:00
Tom Moor befa36231c Copy Procfile in build process 2021-09-08 20:20:41 -07:00
Tom Moor 93611b1deb Add collaboration Procfile 2021-09-08 20:00:43 -07:00
Tom Moor 0a07a0dd26 Merge branch 'main' into wvds-feat/twitter-embed 2021-09-08 19:34:14 -07:00
Tom Moor 2f5cf90cb7 0.59.0 2021-09-07 22:10:29 -07:00
Tom Moor c709e54738 fix: Init dd trace sooner, closes #2528 2021-09-02 23:22:31 -07:00
Tom Moor 47953b3354 Yarn.lock 2021-09-02 23:17:41 -07:00
Tom Moor d96099b5b8 Move OIDC provider to routes directory 2021-09-02 19:55:06 -07:00
Greg Linklater 4b2bf28531 feat: Generic OAuth2 Authentication (#2388)
* chore: additional dependency

* feat: OAuth2 authentication provider

* docs: add env vars

* chore: lock file

* feat: add malformed user info error and notice

* feat: configurable scopes

* fix: explicitly enable state and disable pkce

* chore: remove externally supplied username from account provisioner use

* chore: remove upstream error

* chore: add explicit import for fetch

* chore: remove unused env var from sample

* docs: openid connect claims

* fix: forward fetch errors

* feat: configurable team claim name

* docs: move OIDC env vars together

* refactor: change provider name

* refactor: rename error to match provider

* fix: resolve claim using lodash.get

* refactor: remove OIDC_TEAM_CLAIM and hard code team name
2021-09-02 19:50:17 -07:00
Tom Moor a3df9e868f fix: Server error when loading documents.info with shareId and user token and child documents shared
closes #2527
2021-09-01 23:38:43 -07:00
Tom Moor 136d7a85d7 docs 2021-09-01 21:20:53 -07:00
Tom Moor 3b4a457da9 Merge branch 'main' into collaborative-editing 2021-09-01 07:40:20 -07:00
Tom Moor 476b5e03f9 perf: Move exports to worker service (#2514)
* first pass

* fixes

* fix: Move export related emails to queue

* i18n
2021-08-31 17:41:57 -07:00
Tom Moor 23a6459ae8 fix: Make GoogleDrive embed links more lenient
closes #2405
2021-08-30 22:43:13 -07:00
Tom Moor 2a1bc4934c wip 2021-08-30 22:05:18 -07:00
Tom Moor 3e0cd3b3aa fix: Content duplication 2021-08-30 18:22:27 -07:00
Tom Moor 2e721f90ea fix: Embeds not converted in collaborative editor 2021-08-30 18:17:09 -07:00
Saumya Pandey 4929fbaccb fix: Move "public document sharing" to "Permissions" (#2496)
* Convert to functional component

* Move public sharing to permissions

* Add collections.permission_changed event

* Account for null

* Update server/events.js

Co-authored-by: Tom Moor <tom.moor@gmail.com>

* Add collections.permission_changed event

* Remove name

Co-authored-by: Tom Moor <tom.moor@gmail.com>
2021-08-30 11:43:42 +05:30
Tom Moor 26835cb7c4 Merge branch 'main' into collaborative-editing 2021-08-29 21:16:00 -07:00
Tom Moor 08a8fea69a chore: Add /_health endpoint to all services
closes #2506
2021-08-29 19:44:06 -07:00
Tom Moor 2024c6e64f chore: Graceful server shutdown, closes #2507 2021-08-29 14:48:12 -07:00
Tom Moor 3dad744179 Merge branch 'main' into feat/issue-2230 2021-08-29 13:45:26 -07:00
Tom Moor 3dfd336f59 chore: Move all routes under routes directory (#2513)
closes #2504
2021-08-29 13:25:06 -07:00
Tom Moor 9a875920ac chore: Remove 'attachments' option from mailer 2021-08-29 12:35:55 -07:00
Tom Moor dcb80ff865 Bump Hocuspocus for context fix 2021-08-29 10:59:07 -07:00
Saumya Pandey f389ac6414 fix: Improvements in share feat (#2502)
* Make request only when popover is visible

* Update policies required for shares.create shares.update

* Create withCollection scope

* Remove team share check from shares.create

* Update tests
2021-08-29 10:44:09 +05:30
Tom Moor a4ec33e80c Add debugging facility for local cache 2021-08-28 18:11:34 -07:00
Tom Moor 8d343fae6d fix: Enforce single process for collaboration 2021-08-28 17:30:59 -07:00
Tom Moor 9744d5e746 Merge branch 'main' of github.com:outline/outline into collaborative-editing 2021-08-28 15:40:39 -07:00
Saumya Pandey e4b7aa6761 fix: Add ability to choose user permission level when inviting (#2473)
* Select user role while sending invite

* Add tests to check for role

* Update app/scenes/Invite.js

Co-authored-by: Tom Moor <tom.moor@gmail.com>

* Use select

* Use inviteUser policy

* Remove unnecessary code

* Normalize rank/role
Fix text sizing of select input, fix alignment on users invite form

* Move component to root

* cleanup

Co-authored-by: Tom Moor <tom.moor@gmail.com>
2021-08-29 03:05:37 +05:30
Saumya Pandey 00ba65f3ef fix: Refactor collection exports to not send email attachment (#2460)
Co-authored-by: Tom Moor <tom.moor@gmail.com>
2021-08-29 02:57:07 +05:30
Tom Moor 120c699db1 test 2021-08-28 12:16:30 -07:00
Tom Moor 3964c83ba6 fix: Add error handling to all services 2021-08-28 11:55:00 -07:00
Tom Moor 0158ab7ae8 refactor for new services structure 2021-08-28 11:43:38 -07:00
Tom Moor 19cad30ac8 Merge without enabling multiplayer 2021-08-27 22:24:46 -07:00
Tom Moor 28aef82af9 chore: Refactoring event processors and service architecture (#2495) 2021-08-27 21:42:13 -07:00
Saumya Pandey 86f008293a fix: Return memberships of collections (#2501) 2021-08-27 20:03:57 -07:00
Tom Moor 835fd26a95 Squashed commit of the following:
commit ebe2fe07d1a9110a99a21772b79f189dd13b4ca8
Author: Tom Moor <tom.moor@gmail.com>
Date:   Thu Aug 26 20:18:52 2021 -0700

    fix: regex, formatting

commit 1fd17b6f8a
Author: Matheus Breguêz <matbrgz@gmail.com>
Date:   Thu Aug 26 09:37:12 2021 -0300

    fix: change image size

commit 30e9bad0f5
Merge: ef99201c cc9468e2
Author: Matheus Breguêz <matbrgz@mail.com>
Date:   Thu Aug 26 09:28:34 2021 -0300

    Merge branch 'main' into feat/google-calendar-embed

commit ef99201c9d
Author: Matheus Breguêz <matbrgz@mail.com>
Date:   Fri Jul 9 11:53:56 2021 -0300

    Update GoogleCalendar.js

commit 0e91084756
Merge: e98f94c0 ec5c47e0
Author: Matheus Breguêz <matbrgz@gmail.com>
Date:   Wed Jul 7 14:49:06 2021 -0300

    Merge remote-tracking branch 'origin/feat/google-calendar-embed' into feat/google-calendar-embed

commit e98f94c02d
Author: Matheus Breguêz <matbrgz@gmail.com>
Date:   Wed Jul 7 14:29:49 2021 -0300

    feat: Add Google Calendar Embed

commit ec5c47e0c8
Author: Matheus Breguêz <matbrgz@gmail.com>
Date:   Wed Jul 7 14:29:49 2021 -0300

    feat: Add Google Calendar Embed
2021-08-26 20:20:15 -07:00
Tom Moor cc9468e2c5 Add 4 additional collection icons, closes #2482 2021-08-25 21:44:30 -07:00
Saumya Pandey 22ba4d0f48 fix: prevent access to docs in trash from deleted private collections (#2431)
* Check for collection in deleted document

* Add tests

* Use update policy

* Set paranoid to false when fetching deleted doc

* Update policy
2021-08-26 09:35:59 +05:30
Tom Moor d335670b91 fix: Starred untitled draft has no title in sidebar
fix: Double click to edit starred document titles
2021-08-24 23:30:55 -07:00
Tom Moor cabaee2d0a Bump editor
closes #2441
closes #2459
2021-08-24 23:23:38 -07:00
Saumya Pandey f6d889f759 fix: Show starred docs in sidebar (#2317)
Co-authored-by: Tom Moor <tom.moor@gmail.com>
2021-08-23 13:07:28 +05:30
Wesley a50471959b feat: Always show share button (#2469)
This is to enable the share page also for internal team members.

closes #2444
2021-08-22 23:20:29 -07:00
Tom Moor d8ad2fc1a2 fix: Theme in account menu does not update 2021-08-22 22:19:20 -07:00
Tom Moor 781ab7f5aa fix: Broken queues 2021-08-21 13:17:21 -07:00
Wesley 0c48227b57 Feat: add diagrams.net/draw.io embed (#2464)
* feat: Add diagrams.net/draw.io embed

* Rename Diagrams integration to include .net

Co-authored-by: Tom Moor <tom.moor@gmail.com>
2021-08-21 11:11:47 -07:00
Tom Moor 72da0653cc Revert "feat: Add hosted domain hint when signing in through Google SSO from subdomain (#2458)" (#2467)
This reverts commit e613ec732b.
2021-08-21 11:11:01 -07:00
Tom Moor aa915ec52b fix persistence debounce 2021-08-20 19:27:29 -07:00
Tom Moor 32792caca8 Add loading placeholder when exceeding 500ms 2021-08-20 17:36:11 -07:00
Tom Moor 7e1c5b7640 Move connection status to within doc area, otherwise you cant see it when sidebar is collapsed 2021-08-20 15:56:39 -07:00
Tom Moor 14974facc9 Handle permission denied when loading doc 2021-08-20 15:38:47 -07:00
Tom Moor e613ec732b feat: Add hosted domain hint when signing in through Google SSO from subdomain (#2458)
* feat: Add hosted domain hint when signing in through Google SSO from subdomain

closes #2454
2021-08-20 14:03:52 -07:00
Tom Moor 83fcef1491 refactor 2021-08-20 13:45:22 -07:00
Tom Moor 716ff572be Use onAuthenticate hook 2021-08-19 14:53:44 -07:00
Tom Moor 0be40609ed feat: Add UI to switch teams where signed in to multiple (#2457)
* feat: Add UI to switch teams where signed in to multiple

* fix: Do not display current team in switch menu

* Refactor to hook
2021-08-18 18:37:50 -07:00
Saumya Pandey ec8fde0a5f fix: Improvements in table component (#2450) 2021-08-18 03:27:23 +05:30
Tom Moor 80ff682ec6 fix: Use static docs for archived,trash,revisions 2021-08-13 18:14:55 -07:00
Tom Moor 4973298f24 fix: JS errors in shared document 2021-08-13 17:43:15 -07:00
Tom Moor 32156a29b2 Disable 'restore document' functionality during multiplayer beta 2021-08-13 17:38:41 -07:00
Tom Moor 1579ce7c52 avoid calculations when doc is unchanged 2021-08-13 17:36:19 -07:00
Tom Moor 4cd9bd26d0 Update deps, security fix 2021-08-13 16:11:39 -07:00
Tom Moor 8bc77e54d3 Local document storage 2021-08-13 15:25:43 -07:00
Tom Moor 9ef44ff67d Merge main 2021-08-13 15:09:56 -07:00
Saumya Pandey 2c52a8cb8b fix: Add icons to menu items (#2373)
Co-authored-by: Tom Moor <tom.moor@gmail.com>
2021-08-13 14:21:25 -07:00
Tom Moor 1db31eed41 fix: Incorrect empty state text for /created Home tab 2021-08-13 09:57:20 -07:00
Tom Moor 8ba8013c6a fix: Suppressed notification causes missing notifications for other users on the same team 2021-08-13 09:40:43 -07:00
David Herman 1521d4dbac fix: Suppress notifications for suspended users (#2448)
* fix: Supress notifications for suspended users

* spelling

Co-authored-by: Tom Moor <tom.moor@gmail.com>
2021-08-13 09:32:19 -07:00
Saumya Pandey a1a4fd1baf fix: Redirect to collection on self-hosted (#2438) 2021-08-13 12:32:18 +05:30
Tom Moor 8843c789c5 Merge branch 'main' into collaborative-editing 2021-08-12 22:48:00 -07:00
Tom Moor 31f4424018 fix: Time/LocaleTime should default to relative humanized timestamps (regression) 2021-08-12 22:47:14 -07:00
Tom Moor ee2e776175 Add display when websocket connection is lost 2021-08-12 19:50:10 -07:00
Tom Moor dbffde315e Add additional restricted domains 2021-08-12 15:41:12 -07:00
Tom Moor 58b3b5db5c Add Procfile 2021-08-12 15:40:59 -07:00
Translate-O-Tron 1f5b83aaeb New Crowdin updates (#2413) 2021-08-12 15:24:40 -07:00
Tom Moor 77db0c2e95 fix: Document history event headings (#2433)
* fix: Document history events from last year but within 12 months shown as 'this year'
fix: Events older than a year have repeated headings

* lint
2021-08-12 15:24:13 -07:00
dependabot[bot] 4cbae1cf7d chore(deps): bump path-parse from 1.0.6 to 1.0.7 (#2439)
Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7.
- [Release notes](https://github.com/jbgutierrez/path-parse/releases)
- [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7)

---
updated-dependencies:
- dependency-name: path-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-08-12 15:23:43 -07:00
Tom Moor e985078b80 fix: JS error scrolling overflowed templates page
closes #2445
2021-08-12 11:26:28 -07:00
Tom Moor ea72a8f1e9 chore: Add queue debugging UI in development 2021-08-12 10:22:14 -07:00
Tom Moor aa90d388ef fix: Locally update text value and tasks summary with multiplayer editor 2021-08-11 17:10:13 -07:00
Tom Moor c1f832b0a2 Add team.features validation 2021-08-11 16:19:33 -07:00
Tom Moor 14596a11e8 fix: Template injection working with multiplayer and drafts 2021-08-11 15:47:17 -07:00
Tom Moor 06a9a6634b refactor template injection to be multiplayer-friendly 2021-08-10 23:16:38 -07:00
Tom Moor 6f684037e2 feat: Hook up awareness to presence store 2021-08-10 20:48:38 -07:00
Tom Moor 2ddf0b3f7a fix: Content duplication if server restarts while viewing document without CRDT state and multiplayer flag on 2021-08-10 17:59:03 -07:00
Tom Moor c8fb988d6d Create features object to allow enabling future features 2021-08-10 17:40:16 -07:00
Tom Moor 135dc356af wip 2021-08-10 16:51:16 -07:00
Tom Moor 859b04d6a3 fix: Double network connection 2021-08-10 11:43:41 -07:00
Tom Moor 77dfdac93f flow-typed 2021-08-09 12:36:36 -07:00
Tom Moor 2fdc9b4875 flow-types 2021-08-09 12:31:40 -07:00
Tom Moor d0f4d8282c wip 2021-08-08 18:49:29 -07:00
Tom Moor 79b92b92aa iteration 2021-08-07 18:11:09 -07:00
317 changed files with 8189 additions and 3005 deletions
+33 -15
View File
@@ -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
+1
View File
@@ -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
+1 -1
View File
@@ -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
+2 -1
View File
@@ -1 +1,2 @@
web: node ./build/server/index.js
web: yarn start --services=web,websockets
worker: yarn start --services=worker
+22 -25
View File
@@ -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 youre 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
+2 -2
View File
@@ -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,
+59
View File
@@ -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 youre 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);
+6 -2
View File
@@ -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 = ({
&nbsp;
</>
)}
{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;
`};
`;
+24 -2
View File
@@ -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);
+4 -1
View File
@@ -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,
+2 -2
View File
@@ -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";
`};
`;
+26 -1
View File
@@ -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;
`;
+6
View File
@@ -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`
+22
View File
@@ -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;
+14 -7
View File
@@ -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}
+4 -39
View File
@@ -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">
+11
View File
@@ -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;
+1 -1
View File
@@ -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,
+10 -4
View File
@@ -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));
+37 -10
View File
@@ -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>
);
+4 -9
View File
@@ -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 {
+2 -1
View File
@@ -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;
+4 -2
View File
@@ -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%;
`;
+1 -1
View File
@@ -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);
+20 -1
View File
@@ -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
View File
@@ -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);
+9
View File
@@ -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;
}
+29
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 />,
},
]}
/>
+2 -7
View File
@@ -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 -8
View File
@@ -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;
+18 -2
View File
@@ -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>
</>
+4 -3
View File
@@ -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 />
+5 -5
View File
@@ -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",
+1 -5
View File
@@ -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
View File
@@ -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,
});
}
+27
View File
@@ -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
View File
@@ -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
View File
@@ -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";
}
}
}
+7 -5
View File
@@ -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 [
+1 -3
View File
@@ -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
View File
@@ -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
/>
&nbsp;
<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>
);
}
}
&nbsp;
<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);
+7 -3
View File
@@ -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 its 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 /> }}
/>
+38
View File
@@ -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
+3
View File
@@ -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}
+3 -1
View File
@@ -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"
+56 -20
View File
@@ -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>
);
}
+7 -3
View File
@@ -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 dont 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>
</>
+22 -7
View File
@@ -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"
+1 -1
View File
@@ -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
View File
@@ -64,7 +64,7 @@ function Home() {
documents={documents.createdByUser(user)}
fetch={documents.fetchOwned}
options={{ user }}
empty={<Empty>{t("Weird, this shouldnt ever be empty")}</Empty>}
empty={<Empty>{t("You havent created any documents yet")}</Empty>}
showCollection
/>
</Route>
+22 -7
View File
@@ -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
/>
&nbsp;&nbsp;
<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>
+5
View File
@@ -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
+23 -3
View File
@@ -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. Well 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 its 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>
);
}
+1
View File
@@ -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]}&nbsp;&nbsp;</>
)}
{t(`{{userName}} requested`, {
userName:
fileOperation.id === fileOperation.user.id
? t("You")
: fileOperation.user.name,
})}
&nbsp;
<Time dateTime={fileOperation.createdAt} addSuffix shorten />
&nbsp;&nbsp;{fileOperation.sizeInMB}
</>
}
actions={
fileOperation.state === "complete" ? (
<Button
as="a"
href={`/api/fileOperations.redirect?id=${fileOperation.id}`}
neutral
>
{t("Download")}
</Button>
) : undefined
}
/>
);
};
export default FileOperationListItem;
-64
View File
@@ -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("Youve not starred any documents yet.")}</Empty>}
fetch={fetchStarred}
documents={sort === "alphabetical" ? starredAlphabetical : starred}
showCollection
/>
</Scene>
);
}
export default observer(Starred);
+2 -2
View File
@@ -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>
+1 -1
View File
@@ -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);
+27
View File
@@ -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");
}
}
+4
View File
@@ -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();
+8
View File
@@ -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
View File
@@ -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,
+4
View File
@@ -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
View File
@@ -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 });
}
+11
View File
@@ -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);
}
}
}
+36
View File
@@ -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;
}
-4
View File
@@ -7,10 +7,6 @@ export function homeUrl(): string {
return "/home";
}
export function starredUrl(): string {
return "/starred";
}
export function newCollectionUrl(): string {
return "/collection/new";
}
-9
View File
@@ -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"))
+4 -3
View File
@@ -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
```
```
View File
+45
View File
@@ -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
```
+3
View File
@@ -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,
},
+377
View File
@@ -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">;
}
+39
View File
@@ -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
View File
@@ -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'>;
}
+67
View File
@@ -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">;
}
+430
View File
@@ -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
View File
@@ -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

-1
View File
@@ -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