Compare commits

...

105 Commits

Author SHA1 Message Date
Tom Moor 0ed4ee4295 ts: 415 errors 2020-12-29 13:44:16 -08:00
Tom Moor 5f6bea5cde ts: server and shared compiles in non-strict 2020-12-29 13:31:59 -08:00
Tom Moor 78d7dd077e ts: root, presenters, test 2020-12-29 13:11:36 -08:00
Tom Moor aa29a96cd0 ts: auth, api, utils 2020-12-29 13:07:18 -08:00
Tom Moor e81605fb9d ts: middlewares, emails, commands 2020-12-29 13:05:55 -08:00
Tom Moor d67d1e3f9b ts: policies, services 2020-12-29 13:04:32 -08:00
Tom Moor db79a2cd1e ts: models 2020-12-29 13:03:20 -08:00
Tom Moor d4bb04e921 fix: Handle linked documents destroyed when document is published
closes #1739
2020-12-29 10:32:09 -08:00
Nan Yu 8a3a279c0e Merge branch 'develop' of github.com:outline/outline into develop 2020-12-28 21:35:37 -08:00
Nan Yu 37f2cc8d55 closes #1752 2020-12-28 21:35:13 -08:00
Gustavo Maronato 89903b4bbe feat: Compress avatar images before upload (#1751)
* compress avatar images before upload

* move compressImage to dedicated file

* Update ImageUpload.js
2020-12-28 21:08:10 -08:00
Malek Hijazi b6ab816bb3 feat: command to upgrade outline (#1727)
* Add upgrade script to package.json

* Update the docs to include docker and yarn guides
2020-12-25 15:23:55 -08:00
Tom Moor ac1120914a fix: Unable to delete archived and templated documents (#1749)
closes #1746
2020-12-24 13:28:08 -08:00
Tom Moor ea57cef89c fix: Reduce double reporting of errors 2020-12-21 21:10:25 -08:00
Translate-O-Tron 7d44e1aeeb New Crowdin updates (#1725)
* fix: New Japanese translations from Crowdin [ci skip]

* fix: New Japanese translations from Crowdin [ci skip]

* fix: New Japanese translations from Crowdin [ci skip]

* fix: New Japanese translations from Crowdin [ci skip]

* fix: New French translations from Crowdin [ci skip]

* fix: New Spanish translations from Crowdin [ci skip]

* fix: New German translations from Crowdin [ci skip]

* fix: New Japanese translations from Crowdin [ci skip]

* fix: New Portuguese translations from Crowdin [ci skip]

* fix: New Russian translations from Crowdin [ci skip]

* fix: New Chinese Simplified translations from Crowdin [ci skip]

* fix: New Portuguese, Brazilian translations from Crowdin [ci skip]

* fix: New Korean translations from Crowdin [ci skip]

* fix: New Japanese translations from Crowdin [ci skip]

* fix: New Japanese translations from Crowdin [ci skip]

* fix: New German translations from Crowdin [ci skip]

* fix: New Japanese translations from Crowdin [ci skip]

* fix: New Portuguese translations from Crowdin [ci skip]

* fix: New Portuguese, Brazilian translations from Crowdin [ci skip]

* fix: New Spanish translations from Crowdin [ci skip]

* fix: New Japanese translations from Crowdin [ci skip]

* fix: New Japanese translations from Crowdin [ci skip]

* fix: New Portuguese, Brazilian translations from Crowdin [ci skip]

* fix: New Spanish translations from Crowdin [ci skip]

* fix: New Spanish translations from Crowdin [ci skip]
2020-12-21 19:28:41 -08:00
Tom Moor 25d5ad8a7e chore: Enable automatic generation of email server in non production environments (#1731) 2020-12-21 19:27:14 -08:00
dependabot[bot] e34ba1457e chore(deps): bump node-notifier from 8.0.0 to 8.0.1 (#1734)
Bumps [node-notifier](https://github.com/mikaelbr/node-notifier) from 8.0.0 to 8.0.1.
- [Release notes](https://github.com/mikaelbr/node-notifier/releases)
- [Changelog](https://github.com/mikaelbr/node-notifier/blob/v8.0.1/CHANGELOG.md)
- [Commits](https://github.com/mikaelbr/node-notifier/compare/v8.0.0...v8.0.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-21 19:26:12 -08:00
Tom Moor e966eb8c9a fix: Error notice not displayed to user when exceeding rate limit on signin attempt 2020-12-20 13:05:16 -08:00
Tom Moor 4684b3a3f3 fix: Server error when invalid JSON passed to API endpoint
Fix is to ensure that the errorHandling middleware is mounted before the body parser so that it can catch and return an error response
2020-12-20 12:08:47 -08:00
Tom Moor 47ce8afcc5 fix: Server Error when requesting invalid locale 2020-12-20 11:53:09 -08:00
Tom Moor decbe4f643 fix: Allow deleting attachments not linked to documents when owned by user
closes #1729
2020-12-20 11:39:09 -08:00
Tom Moor 117d278d16 fix: Deprecated Buffer usage, closes #1726 2020-12-19 15:58:21 -08:00
Tom Moor 40ca73e684 feat: Collapsible sidebar (#1721)
* wip

* styling, add keyboard shortcut

* tweak styling
2020-12-17 22:26:04 -08:00
Nan Yu 051ecab0fc feat: Moving documents via drag and drop in sidebar (#1717)
* wip: added some basic drag and drop UI for combining items

* refactor: pathToDocument to accept only id

* fix: Multiple drop backends error
fix: Incorrect styling dragging over active collection
fix: Stay in disabled state until save is complete

* Improving display while moving doc

* fix: update by user should be changed when moving a doc

* add move guard to drag

Co-authored-by: Tom Moor <tom.moor@gmail.com>
2020-12-15 19:07:29 -08:00
Tom Moor 3469b82beb feat: Add Korean as available language choice 2020-12-15 08:11:50 -08:00
Tom Moor f2c3481670 test 2020-12-14 23:04:39 -08:00
Tom Moor bc141dc40c New Crowdin updates (#1718)
* fix: New French translations from Crowdin [ci skip]

* fix: New Spanish translations from Crowdin [ci skip]

* fix: New German translations from Crowdin [ci skip]

* fix: New Japanese translations from Crowdin [ci skip]

* fix: New Portuguese translations from Crowdin [ci skip]

* fix: New Russian translations from Crowdin [ci skip]

* fix: New Chinese Simplified translations from Crowdin [ci skip]

* fix: New Portuguese, Brazilian translations from Crowdin [ci skip]

* fix: New Korean translations from Crowdin [ci skip]

* fix: New French translations from Crowdin [ci skip]

* fix: New Spanish translations from Crowdin [ci skip]

* fix: New German translations from Crowdin [ci skip]

* fix: New Korean translations from Crowdin [ci skip]

* fix: New Korean translations from Crowdin [ci skip]

* fix: New Korean translations from Crowdin [ci skip]

* fix: New Korean translations from Crowdin [ci skip]

* fix: New Korean translations from Crowdin [ci skip]

* fix: New Korean translations from Crowdin [ci skip]

* fix: New Korean translations from Crowdin [ci skip]

* fix: New Korean translations from Crowdin [ci skip]

* fix: New French translations from Crowdin [ci skip]

* fix: New Spanish translations from Crowdin [ci skip]

* fix: New German translations from Crowdin [ci skip]

* fix: New Japanese translations from Crowdin [ci skip]

* fix: New Portuguese translations from Crowdin [ci skip]

* fix: New Russian translations from Crowdin [ci skip]

* fix: New Chinese Simplified translations from Crowdin [ci skip]

* fix: New Portuguese, Brazilian translations from Crowdin [ci skip]

* fix: New Korean translations from Crowdin [ci skip]
2020-12-14 22:28:47 -08:00
Translate-O-Tron 99814f6e2f fix: New Korean translations from Crowdin [ci skip] 2020-12-14 22:19:57 -08:00
Translate-O-Tron 3737f0b42c fix: New Portuguese translations from Crowdin [ci skip] 2020-12-14 22:19:53 -08:00
Translate-O-Tron 9ef27cb436 fix: New German translations from Crowdin [ci skip] 2020-12-14 22:19:50 -08:00
Translate-O-Tron c9fa3f93f2 fix: New Spanish translations from Crowdin [ci skip] 2020-12-14 22:19:48 -08:00
Translate-O-Tron 24ed96c9a5 fix: New French translations from Crowdin [ci skip] 2020-12-14 22:19:46 -08:00
Translate-O-Tron 956cf401bd fix: New Korean translations from Crowdin [ci skip] 2020-12-14 21:17:46 -08:00
Translate-O-Tron 7cb837f478 fix: New Portuguese, Brazilian translations from Crowdin [ci skip] 2020-12-14 21:17:44 -08:00
Translate-O-Tron a3209e9d23 fix: New Chinese Simplified translations from Crowdin [ci skip] 2020-12-14 21:17:43 -08:00
Translate-O-Tron 29582a1bb1 fix: New Russian translations from Crowdin [ci skip] 2020-12-14 21:17:41 -08:00
Translate-O-Tron 4084c91769 fix: New Portuguese translations from Crowdin [ci skip] 2020-12-14 21:17:39 -08:00
Translate-O-Tron 6772f28226 fix: New Japanese translations from Crowdin [ci skip] 2020-12-14 21:17:37 -08:00
Translate-O-Tron c6b110d339 fix: New German translations from Crowdin [ci skip] 2020-12-14 21:17:35 -08:00
Translate-O-Tron 0a43b50c66 fix: New Spanish translations from Crowdin [ci skip] 2020-12-14 21:17:33 -08:00
Translate-O-Tron 4a82cb0658 fix: New French translations from Crowdin [ci skip] 2020-12-14 21:17:31 -08:00
Tom Moor 2f7fca6106 chore: Move formatting out of translation strings 2020-12-14 21:16:02 -08:00
Translate-O-Tron 8f83cfef25 fix: New Korean translations from Crowdin [ci skip] 2020-12-14 20:39:04 -08:00
Tom Moor e2e66954b5 fix: Attachments should not always be deleted with their original document (#1715)
* fix: Attachments should not be deleted when their original document is deleted when referenced elsewhere

* fix: Attachments deleted prematurely when docs are placed in trash

* mock

* restore hook, cascading delete was the issue
2020-12-14 19:55:22 -08:00
Tom Moor 3dbe54ac1e fix: Bump RME, closes #1719 2020-12-14 19:18:46 -08:00
Translate-O-Tron 50577f6f2f fix: New Korean translations from Crowdin [ci skip] 2020-12-14 09:40:18 -08:00
Translate-O-Tron 16d504703d fix: New Korean translations from Crowdin [ci skip] 2020-12-14 08:42:26 -08:00
Translate-O-Tron 173febcaa1 fix: New Korean translations from Crowdin [ci skip] 2020-12-14 07:40:07 -08:00
Translate-O-Tron f92f4cde7a fix: New Korean translations from Crowdin [ci skip] 2020-12-14 06:45:01 -08:00
Translate-O-Tron 23bec75bd0 fix: New Korean translations from Crowdin [ci skip] 2020-12-14 05:49:49 -08:00
Translate-O-Tron 4dd667f68b fix: New Korean translations from Crowdin [ci skip] 2020-12-14 04:47:25 -08:00
Translate-O-Tron 4b3cb77cc7 fix: New Korean translations from Crowdin [ci skip] 2020-12-14 03:48:01 -08:00
Translate-O-Tron 0e83d54f93 fix: New German translations from Crowdin [ci skip] 2020-12-13 23:30:53 -08:00
Translate-O-Tron d867d9fea5 fix: New Spanish translations from Crowdin [ci skip] 2020-12-13 23:30:51 -08:00
Translate-O-Tron 28c8b8acfe fix: New French translations from Crowdin [ci skip] 2020-12-13 23:30:49 -08:00
Translate-O-Tron 51efffe2ce fix: New Korean translations from Crowdin [ci skip] 2020-12-13 22:34:18 -08:00
Tom Moor 4e9ee7249f Update LICENSE 2020-12-13 17:48:15 -08:00
Tom Moor 574fcc4bb3 0.51.0 2020-12-13 17:43:58 -08:00
Tom Moor 5c3000d5cf Bump RME, fixes table after list and image captions in Safari 2020-12-13 17:20:38 -08:00
Translate-O-Tron c0216cbb8d fix: New Portuguese, Brazilian translations from Crowdin [ci skip] 2020-12-12 22:40:33 -08:00
Translate-O-Tron cf12301077 fix: New Chinese Simplified translations from Crowdin [ci skip] 2020-12-12 22:40:31 -08:00
Translate-O-Tron 1eb7da8742 fix: New Russian translations from Crowdin [ci skip] 2020-12-12 22:40:29 -08:00
Translate-O-Tron b3c548382f fix: New Portuguese translations from Crowdin [ci skip] 2020-12-12 22:40:27 -08:00
Translate-O-Tron 7ebac53b43 fix: New Japanese translations from Crowdin [ci skip] 2020-12-12 22:40:25 -08:00
Translate-O-Tron 64428a6894 fix: New German translations from Crowdin [ci skip] 2020-12-12 22:40:23 -08:00
Translate-O-Tron d536af5269 fix: New Spanish translations from Crowdin [ci skip] 2020-12-12 22:40:22 -08:00
Translate-O-Tron 1726a88a60 fix: New French translations from Crowdin [ci skip] 2020-12-12 22:40:20 -08:00
Tom Moor 3fe807a10a fix: Object printed in UI 2020-12-12 22:29:20 -08:00
Tom Moor 72189e041b feat: attachments.delete (#1714)
* feat: Add endpoint for manually deleting attachments

* mock
2020-12-10 21:40:03 -08:00
Translate-O-Tron bc156f4cc8 New Crowdin updates (#1707)
* fix: New German translations from Crowdin [ci skip]

* fix: New German translations from Crowdin [ci skip]

* fix: New Chinese Simplified translations from Crowdin [ci skip]

* fix: New Chinese Simplified translations from Crowdin [ci skip]

* fix: New Portuguese, Brazilian translations from Crowdin [ci skip]

* fix: New French translations from Crowdin [ci skip]

* fix: New Portuguese, Brazilian translations from Crowdin [ci skip]
2020-12-10 19:03:01 -08:00
dependabot[bot] 26693c60df chore(deps): bump ini from 1.3.5 to 1.3.7 (#1713)
Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.7.
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.7)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-10 18:43:50 -08:00
Nan Yu 9e1f31e14c fix: dropzone error on image upload component (#1711) 2020-12-08 19:32:41 -08:00
Nan Yu 63d926e196 slightly nicer color definitions (#1705) 2020-12-07 08:56:07 -08:00
Reid Beels 3f9f1f0bed docs: Add note to .env.sample about Google OAuth URI (#1706) 2020-12-07 08:55:37 -08:00
Tom Moor b2bdc7f1d4 chore: Add user and auth context to server side error reports (#1693) 2020-12-06 17:59:44 -08:00
Translate-O-Tron 2e798c698d chore: New Crowdin updates (#1691) 2020-12-06 17:54:16 -08:00
Nan Yu aa59f5fe09 chore: React-Dropzone version bump (#1699)
* update dropzone to new version

* remove global styles import

* change bg on active item on drag as well

* add back background
2020-12-06 17:50:59 -08:00
Tom Moor ac2060b166 fix: Migrate attachment columns to incease available length (#1704)
closes #1703
2020-12-06 16:51:25 -08:00
Tom Moor 424c29536d chore: Bump RME (SQL language support) 2020-12-04 10:18:30 -08:00
Tom Moor 6c1ecde4e7 fix: Server error when attempting to update team with identical details to previous 2020-12-04 10:18:30 -08:00
Tom Moor aa6fc45097 Add localization status to README 2020-12-04 08:22:43 -08:00
Tom Moor 9478944718 fix: Account for non-recorded views, closes #1700 2020-12-02 20:50:54 -08:00
Tom Moor 9e1c5d1db3 fix: JS error in UserProfile introduced in refactoring to functional component 2020-12-02 20:48:24 -08:00
Nan Yu 474fbf07e6 chore: Flatten left nav in preparation to refactor drag to reorder (#1689)
* flatten hierarchy
* fix drop to import positioning on collections
2020-12-01 21:59:18 -08:00
Tom Moor fe62048890 fix: One source of transaction deadlock when invites > pg pool (#1696)
* fix: One source of transaction deadlock when invites > pg pool

* lint
2020-12-01 19:20:20 -08:00
Tom Moor 1851477290 fix: Disabling public sharing should disable all existing share links
Issue came through customer support
2020-11-30 23:39:23 -08:00
Tom Moor bde6f4b3c4 fix: Don't make request to record view for deleted document 2020-11-30 23:17:39 -08:00
Saumya Pandey 283b479689 chore: Change response of shares.info response for unshared document (#1666)
* Update server/api/share.js to send 204 status for unshared documents.

* Update shares.info endpoint to expect 204 in a few test.

* Update SharesStore and ApiClient to handle 204 status code
2020-11-30 22:49:15 -08:00
Tom Moor 183f06c2d1 fix: Policies not added to store from all fetch requests
closes #1688
2020-11-30 22:38:11 -08:00
Tom Moor 21fff8d172 fix: ui store spread onto DropToImport 2020-11-30 21:46:55 -08:00
Tom Moor 18e56aff65 fix: Editable title in sidebar impossible to see in dark mode 2020-11-30 21:45:48 -08:00
Tom Moor a97523a652 chore: Upgrade pg (performance improvements) 2020-11-30 21:31:13 -08:00
Tom Moor 2316512a19 Update crowdin.yml 2020-11-30 19:09:41 -08:00
Tom Moor 1285efc49a feat: I18n (#1653)
* feat: i18n

* Changing language single source of truth from TEAM to USER

* Changes according to @tommoor comments on PR

* Changed package.json for build:i18n and translation label

* Finished 1st MVP of i18n for outline

* new translation labels & Portuguese from Portugal translation

* Fixes from PR request

* Described language dropdown as an experimental feature

* Set keySeparator to false in order to cowork with html keys

* Added useTranslation to Breadcrumb

* Repositioned <strong> element

* Removed extra space from TemplatesMenu

* Fortified the test suite for i18n

* Fixed trans component problematic

* Check if selected language is available

* Update yarn.lock

* Removed unused Trans

* Removing debug variable from i18n init

* Removed debug variable

* test: update snapshots

* flow: Remove decorator usage to get proper flow typing
It's a shame, but hopefully we'll move to Typescript in the next 6 months and we can forget this whole Flow mistake ever happened

* translate: Drafts

* More translatable strings

* Mo translation strings

* translation: Search

* async translations loading

* cache translations in client

* Revert "cache translations in client"

This reverts commit 08fb61ce36.

* Revert localStorage cache for cache headers

* Update Crowdin configuration file

* Moved translation files to locales folder and fixed english text

* Added CONTRIBUTING File for CrowdIn

* chore: Move translations again to please CrowdIn

* fix: loading paths
chore: Add strings for editor

* fix: Improve validation on documents.import endpoint

* test: mock bull

* fix: Unknown mimetype should fallback to Markdown parsing if markdown extension (#1678)

* closes #1675

* Update CONTRIBUTING

* chore: Add link to translation portal from app UI

* refactor: Centralize language config

* fix: Ensure creation of i18n directory in build

* feat: Add language prompt

* chore: Improve contributing guidelines, add link from README

* chore: Normalize tab header casing

* chore: More string externalization

* fix: Language prompt in dark mode

Co-authored-by: André Glatzl <andreglatzl@gmail.com>
2020-11-29 20:04:58 -08:00
Tom Moor 63c73c9a51 Create auto_assign.yml 2020-11-27 09:51:52 -08:00
Tom Moor 1b7fe0f7da flow 2020-11-27 09:48:10 -08:00
Tom Moor 6eda1cc0d3 fix: Unknown mimetype should fallback to Markdown parsing if markdown extension (#1678)
closes #1675
2020-11-26 21:16:56 -08:00
Tom Moor ac349b40f5 test: mock bull 2020-11-26 21:11:35 -08:00
Tom Moor 8bddc1b338 fix: Improve validation on documents.import endpoint 2020-11-26 20:29:45 -08:00
Tom Moor 56d5f048f9 fix: Group membership count off after suspending users in groups (#1670)
* fix: Clear group memberships when suspending a user
Refactor to command

* test

* test
2020-11-22 11:21:47 -08:00
Tom Moor 273d9c4680 fix: Correctly map CMD+Shift+P to publish in title
fixes #1655
2020-11-21 22:20:59 -08:00
Tom Moor 44ca447185 fix: Scrollbar styling
closes #1665
2020-11-21 16:16:38 -08:00
Tom Moor 6b511e4251 Bump rich-markdown-editor, 2 minor fixes 2020-11-21 15:34:03 -08:00
Saumya Pandey de6ee91d96 fix: Prevent API request for views data for deleted documents (#1663)
* Prevent API request for views data for deleted documents

Added a conditional statement to check if the document.deletedAt is falsy before making a request to views.list.

* Update app/components/Collaborators.js to use isDeleted attribute.

Co-authored-by: Tom Moor <tom.moor@gmail.com>
2020-11-18 19:09:08 -08:00
Tom Moor 18fac781a9 Update LICENSE 2020-11-14 23:10:16 -08:00
431 changed files with 9862 additions and 43859 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"presets": [
"@babel/preset-react",
"@babel/preset-flow",
"@babel/preset-typescript",
[
"@babel/preset-env",
{
+8 -1
View File
@@ -18,12 +18,17 @@ PORT=3000
FORCE_HTTPS=true
ENABLE_UPDATES=true
DEBUG=cache,presenters,events
DEBUG=cache,presenters,events,emails,mailer,utils,multiplayer,server,services
# Third party signin credentials (at least one is required)
SLACK_KEY=get_a_key_from_slack
SLACK_SECRET=get_the_secret_of_above_key
# To configure Google auth, you'll need to create an OAuth Client ID at
# => https://console.cloud.google.com/apis/credentials
#
# When configuring the Client ID, add an Authorized redirect URI:
# https://<your Outline URL>/auth/google.callback
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
@@ -60,3 +65,5 @@ SMTP_REPLY_EMAIL=
# Custom logo that displays on the authentication screen, scaled to height: 60px
# TEAM_LOGO=https://example.com/images/logo.png
DEFAULT_LANGUAGE=en_US
+10
View File
@@ -0,0 +1,10 @@
# Set to true to add reviewers to pull requests
addReviewers: true
# A list of reviewers to be added to pull requests (GitHub user name)
reviewers:
- tommoor
# A list of keywords to be skipped the process that add reviewers if pull requests include it
skipKeywords:
- wip
+2 -2
View File
@@ -3,7 +3,7 @@ Business Source License 1.1
Parameters
Licensor: General Outline, Inc.
Licensed Work: Outline 0.49.0
Licensed Work: Outline 0.51.0
The Licensed Work is (c) 2020 General Outline, Inc.
Additional Use Grant: You may make use of the Licensed Work, provided that
you may not use the Licensed Work for a Document
@@ -15,7 +15,7 @@ Additional Use Grant: You may make use of the Licensed Work, provided that
Licensed Work by creating teams and documents
controlled by such third parties.
Change Date: 2023-10-26
Change Date: 2023-12-13
Change License: Apache License, Version 2.0
+16
View File
@@ -12,6 +12,7 @@
<a href="https://circleci.com/gh/outline/outline" rel="nofollow"><img src="https://circleci.com/gh/outline/outline.svg?style=shield&amp;circle-token=c0c4c2f39990e277385d5c1ae96169c409eb887a"></a>
<a href="https://github.com/prettier/prettier"><img src="https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat"></a>
<a href="https://github.com/styled-components/styled-components"><img src="https://img.shields.io/badge/style-%F0%9F%92%85%20styled--components-orange.svg"></a>
<a href="https://translate.getoutline.com/project/outline"><img src="https://badges.crowdin.net/outline/localized.svg"></a>
</p>
This is the source code that runs [**Outline**](https://www.getoutline.com) and all the associated services. If you want to use Outline then you don't need to run this code, we offer a hosted version of the app at [getoutline.com](https://www.getoutline.com).
@@ -74,6 +75,20 @@ In development you can quickly get an environment running using Docker by follow
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
### Upgrade
#### Docker
If you're running Outline with Docker you'll need to run migrations within the docker container after updating the image. The command will be something like:
```
docker run --rm outlinewiki/outline:latest yarn sequelize:migrate
```
#### Yarn
If you're running Outline by cloning this repository, run the following command to upgrade:
```
yarn upgrade
```
## Development
@@ -164,6 +179,7 @@ However, before working on a pull request please let the core team know by creat
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
+34
View File
@@ -0,0 +1,34 @@
# Translation
Outline is localized through community contributions. The text in Outline's user interface is in American English by default, we're very thankful for all help that the community provides bringing the app to different languages.
## Externalizing strings
Before a string can be translated, it must be externalized. This is the process where English strings in the source code are wrapped in a function that retrieves the translated string for the users language.
For externalization we use [react-i18next](https://react.i18next.com/), this provides the hooks [useTranslation](https://react.i18next.com/latest/usetranslation-hook) and the [Trans](https://react.i18next.com/latest/trans-component) component for wrapping English text.
PR's are accepted for wrapping English strings in the codebase that were not previously externalized.
## Translating strings
To manage the translation process we use [CrowdIn](https://translate.getoutline.com/), it keeps track of which strings in which languages still need translating, synchronizes with the codebase automatically, and provides a great editor interface.
You'll need to create a free account to use CrowdIn. Once you have joined, you can provide translations by following these steps:
1. Select the language for which you want to contribute (or vote for) a translation (below the language you can see the progress of the translation)
![CrowdIn UI](https://i.imgur.com/AkbDY60.png)
2. Please choose the translation.json file from your desired language
3. Once a file is selected, all the strings associated with the version are displayed on the left side. To display the untranslated strings first, select the filter icon next to the search bar and select “All, Untranslated First”.The red square next to an English string shows that a string has not been translated yet. To provide a translation, select a string on the left side, provide a translation in the target language in the text box in the right side (singular and plural) and press the save button. As soon as a translation has been provided by another user (green square next to string), you can also vote on a translation provided by another user. The translation with the most votes is used unless a different translation has been approved by a proof reader. ![Editor UI](https://i.imgur.com/pldZCRs.png)
## Proofreading
Once a translation has been provided, a proof reader can approve the translation and mark it for use in Outline.
If you are interested in becoming a proof reader, please contact one of the project managers in the Outline CrowdIn project or contact [@tommoor](https://github.com/tommoor). Similarly, if your language is not listed in the list of CrowdIn languages, please contact our project managers or [send us an email](https://www.getoutline.com/contact) so we can add your language.
## Release
Updated translations are automatically PR'd against the codebase by a bot and will be merged regularly so that new translations appear in the next release of Outline.
+18
View File
@@ -0,0 +1,18 @@
/* eslint-disable flowtype/require-valid-file-annotation */
export default class Queue {
name;
constructor(name) {
this.name = name;
}
process = (fn) => {
console.log(`Registered function ${this.name}`);
this.processFn = fn;
};
add = (data) => {
console.log(`Running ${this.name}`);
return this.processFn({ data });
};
}
+19 -7
View File
@@ -1,18 +1,30 @@
// @flow
import { observer, inject } from "mobx-react";
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { Redirect } from "react-router-dom";
import { isCustomSubdomain } from "shared/utils/domains";
import AuthStore from "stores/AuthStore";
import LoadingIndicator from "components/LoadingIndicator";
import useStores from "../hooks/useStores";
import env from "env";
type Props = {
auth: AuthStore,
children?: React.Node,
children: React.Node,
};
const Authenticated = observer(({ auth, children }: Props) => {
const Authenticated = ({ children }: Props) => {
const { auth } = useStores();
const { i18n } = useTranslation();
const language = auth.user && auth.user.language;
// Watching for language changes here as this is the earliest point we have
// the user available and means we can start loading translations faster
React.useEffect(() => {
if (i18n.language !== language) {
i18n.changeLanguage(language);
}
}, [i18n, language]);
if (auth.authenticated) {
const { user, team } = auth;
const { hostname } = window.location;
@@ -43,6 +55,6 @@ const Authenticated = observer(({ auth, children }: Props) => {
auth.logout(true);
return <Redirect to="/" />;
});
};
export default inject("auth")(Authenticated);
export default observer(Authenticated);
+14 -7
View File
@@ -4,6 +4,7 @@ import { observable } from "mobx";
import { observer } from "mobx-react";
import { EditIcon } from "outline-icons";
import * as React from "react";
import { withTranslation, type TFunction } from "react-i18next";
import styled from "styled-components";
import User from "models/User";
import UserProfile from "scenes/UserProfile";
@@ -16,6 +17,7 @@ type Props = {
isEditing: boolean,
isCurrentUser: boolean,
lastViewedAt: string,
t: TFunction,
};
@observer
@@ -37,20 +39,25 @@ class AvatarWithPresence extends React.Component<Props> {
isPresent,
isEditing,
isCurrentUser,
t,
} = this.props;
const action = isPresent
? isEditing
? t("currently editing")
: t("currently viewing")
: t("viewed {{ timeAgo }} ago", {
timeAgo: distanceInWordsToNow(new Date(lastViewedAt)),
});
return (
<>
<Tooltip
tooltip={
<Centered>
<strong>{user.name}</strong> {isCurrentUser && "(You)"}
<strong>{user.name}</strong> {isCurrentUser && `(${t("You")})`}
<br />
{isPresent
? isEditing
? "currently editing"
: "currently viewing"
: `viewed ${distanceInWordsToNow(new Date(lastViewedAt))} ago`}
{action}
</Centered>
}
placement="bottom"
@@ -83,4 +90,4 @@ const AvatarWrapper = styled.div`
transition: opacity 250ms ease-in-out;
`;
export default AvatarWithPresence;
export default withTranslation()<AvatarWithPresence>(AvatarWithPresence);
+17 -10
View File
@@ -1,5 +1,5 @@
// @flow
import { observer, inject } from "mobx-react";
import { observer } from "mobx-react";
import {
ArchiveIcon,
EditIcon,
@@ -10,6 +10,7 @@ import {
TrashIcon,
} from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
@@ -19,6 +20,7 @@ import Document from "models/Document";
import CollectionIcon from "components/CollectionIcon";
import Flex from "components/Flex";
import BreadcrumbMenu from "./BreadcrumbMenu";
import useStores from "hooks/useStores";
import { collectionUrl } from "utils/routeHelpers";
type Props = {
@@ -28,13 +30,15 @@ type Props = {
};
function Icon({ document }) {
const { t } = useTranslation();
if (document.isDeleted) {
return (
<>
<CollectionName to="/trash">
<TrashIcon color="currentColor" />
&nbsp;
<span>Trash</span>
<span>{t("Trash")}</span>
</CollectionName>
<Slash />
</>
@@ -46,7 +50,7 @@ function Icon({ document }) {
<CollectionName to="/archive">
<ArchiveIcon color="currentColor" />
&nbsp;
<span>Archive</span>
<span>{t("Archive")}</span>
</CollectionName>
<Slash />
</>
@@ -58,7 +62,7 @@ function Icon({ document }) {
<CollectionName to="/drafts">
<EditIcon color="currentColor" />
&nbsp;
<span>Drafts</span>
<span>{t("Drafts")}</span>
</CollectionName>
<Slash />
</>
@@ -70,7 +74,7 @@ function Icon({ document }) {
<CollectionName to="/templates">
<ShapesIcon color="currentColor" />
&nbsp;
<span>Templates</span>
<span>{t("Templates")}</span>
</CollectionName>
<Slash />
</>
@@ -79,20 +83,23 @@ function Icon({ document }) {
return null;
}
const Breadcrumb = observer(({ document, collections, onlyText }: Props) => {
const Breadcrumb = ({ document, onlyText }: Props) => {
const { collections } = useStores();
const { t } = useTranslation();
let collection = collections.get(document.collectionId);
if (!collection) {
if (!document.deletedAt) return <div />;
collection = {
id: document.collectionId,
name: "Deleted Collection",
name: t("Deleted Collection"),
color: "currentColor",
};
}
const path = collection.pathToDocument
? collection.pathToDocument(document).slice(0, -1)
? collection.pathToDocument(document.id).slice(0, -1)
: [];
if (onlyText === true) {
@@ -141,7 +148,7 @@ const Breadcrumb = observer(({ document, collections, onlyText }: Props) => {
)}
</Wrapper>
);
});
};
const Wrapper = styled(Flex)`
display: none;
@@ -202,4 +209,4 @@ const CollectionName = styled(Link)`
overflow: hidden;
`;
export default inject("collections")(Breadcrumb);
export default observer(Breadcrumb);
+3 -1
View File
@@ -20,7 +20,9 @@ type Props = {
@observer
class Collaborators extends React.Component<Props> {
componentDidMount() {
this.props.views.fetchPage({ documentId: this.props.document.id });
if (!this.props.document.isDeleted) {
this.props.views.fetchPage({ documentId: this.props.document.id });
}
}
render() {
+16 -18
View File
@@ -1,14 +1,14 @@
// @flow
import { inject, observer } from "mobx-react";
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import styled from "styled-components";
import AuthStore from "stores/AuthStore";
import CollectionsStore from "stores/CollectionsStore";
import Document from "models/Document";
import Breadcrumb from "components/Breadcrumb";
import Flex from "components/Flex";
import Time from "components/Time";
import useStores from "hooks/useStores";
const Container = styled(Flex)`
color: ${(props) => props.theme.textTertiary};
@@ -23,8 +23,6 @@ const Modified = styled.span`
`;
type Props = {
collections: CollectionsStore,
auth: AuthStore,
showCollection?: boolean,
showPublished?: boolean,
showLastViewed?: boolean,
@@ -34,8 +32,6 @@ type Props = {
};
function DocumentMeta({
auth,
collections,
showPublished,
showCollection,
showLastViewed,
@@ -44,6 +40,8 @@ function DocumentMeta({
to,
...rest
}: Props) {
const { t } = useTranslation();
const { collections, auth } = useStores();
const {
modifiedSinceViewed,
updatedAt,
@@ -67,37 +65,37 @@ function DocumentMeta({
if (deletedAt) {
content = (
<span>
deleted <Time dateTime={deletedAt} addSuffix />
{t("deleted")} <Time dateTime={deletedAt} addSuffix />
</span>
);
} else if (archivedAt) {
content = (
<span>
archived <Time dateTime={archivedAt} addSuffix />
{t("archived")} <Time dateTime={archivedAt} addSuffix />
</span>
);
} else if (createdAt === updatedAt) {
content = (
<span>
created <Time dateTime={updatedAt} addSuffix />
{t("created")} <Time dateTime={updatedAt} addSuffix />
</span>
);
} else if (publishedAt && (publishedAt === updatedAt || showPublished)) {
content = (
<span>
published <Time dateTime={publishedAt} addSuffix />
{t("published")} <Time dateTime={publishedAt} addSuffix />
</span>
);
} else if (isDraft) {
content = (
<span>
saved <Time dateTime={updatedAt} addSuffix />
{t("saved")} <Time dateTime={updatedAt} addSuffix />
</span>
);
} else {
content = (
<Modified highlight={modifiedSinceViewed}>
updated <Time dateTime={updatedAt} addSuffix />
{t("updated")} <Time dateTime={updatedAt} addSuffix />
</Modified>
);
}
@@ -112,25 +110,25 @@ function DocumentMeta({
if (!lastViewedAt) {
return (
<>
&nbsp;<Modified highlight>Never viewed</Modified>
&nbsp;<Modified highlight>{t("Never viewed")}</Modified>
</>
);
}
return (
<span>
&nbsp;Viewed <Time dateTime={lastViewedAt} addSuffix shorten />
&nbsp;{t("Viewed")} <Time dateTime={lastViewedAt} addSuffix shorten />
</span>
);
};
return (
<Container align="center" {...rest}>
{updatedByMe ? "You" : updatedBy.name}&nbsp;
{updatedByMe ? t("You") : updatedBy.name}&nbsp;
{to ? <Link to={to}>{content}</Link> : content}
{showCollection && collection && (
<span>
&nbsp;in&nbsp;
&nbsp;{t("in")}&nbsp;
<strong>
<Breadcrumb document={document} onlyText />
</strong>
@@ -142,4 +140,4 @@ function DocumentMeta({
);
}
export default inject("collections", "auth")(observer(DocumentMeta));
export default observer(DocumentMeta);
@@ -3,6 +3,7 @@ import { observable } from "mobx";
import { observer } from "mobx-react";
import { StarredIcon, PlusIcon } from "outline-icons";
import * as React from "react";
import { withTranslation, type TFunction } from "react-i18next";
import { Link, Redirect } from "react-router-dom";
import styled, { withTheme } from "styled-components";
import Document from "models/Document";
@@ -25,6 +26,7 @@ type Props = {
showPin?: boolean,
showDraft?: boolean,
showTemplate?: boolean,
t: TFunction,
};
const SEARCH_RESULT_REGEX = /<b\b[^>]*>(.*?)<\/b>/gi;
@@ -72,6 +74,7 @@ class DocumentPreview extends React.Component<Props> {
showTemplate,
highlight,
context,
t,
} = this.props;
if (this.redirectTo) {
@@ -91,7 +94,7 @@ class DocumentPreview extends React.Component<Props> {
>
<Heading>
<Title text={document.titleWithDefault} highlight={highlight} />
{document.isNew && <Badge yellow>New</Badge>}
{document.isNew && <Badge yellow>{t("New")}</Badge>}
{!document.isDraft &&
!document.isArchived &&
!document.isTemplate && (
@@ -104,12 +107,16 @@ class DocumentPreview extends React.Component<Props> {
</Actions>
)}
{document.isDraft && showDraft && (
<Tooltip tooltip="Only visible to you" delay={500} placement="top">
<Badge>Draft</Badge>
<Tooltip
tooltip={t("Only visible to you")}
delay={500}
placement="top"
>
<Badge>{t("Draft")}</Badge>
</Tooltip>
)}
{document.isTemplate && showTemplate && (
<Badge primary>Template</Badge>
<Badge primary>{t("Template")}</Badge>
)}
<SecondaryActions>
{document.isTemplate &&
@@ -120,7 +127,7 @@ class DocumentPreview extends React.Component<Props> {
icon={<PlusIcon />}
neutral
>
New doc
{t("New doc")}
</Button>
)}
&nbsp;
@@ -237,4 +244,4 @@ const ResultContext = styled(Highlight)`
margin-bottom: 0.25em;
`;
export default DocumentPreview;
export default withTranslation()<DocumentPreview>(DocumentPreview);
+32 -31
View File
@@ -5,7 +5,7 @@ import { observer, inject } from "mobx-react";
import * as React from "react";
import Dropzone from "react-dropzone";
import { withRouter, type RouterHistory, type Match } from "react-router-dom";
import { createGlobalStyle } from "styled-components";
import styled, { css } from "styled-components";
import DocumentsStore from "stores/DocumentsStore";
import UiStore from "stores/UiStore";
import LoadingIndicator from "components/LoadingIndicator";
@@ -17,8 +17,6 @@ type Props = {
children: React.Node,
collectionId: string,
documentId?: string,
activeClassName?: string,
rejectClassName?: string,
ui: UiStore,
documents: DocumentsStore,
disabled: boolean,
@@ -28,18 +26,6 @@ type Props = {
staticContext: Object,
};
export const GlobalStyles = createGlobalStyle`
.activeDropZone {
border-radius: 4px;
background: ${(props) => props.theme.slateDark};
svg { fill: ${(props) => props.theme.white}; }
}
.activeDropZone a {
color: ${(props) => props.theme.white} !important;
}
`;
@observer
class DropToImport extends React.Component<Props> {
@observable isImporting: boolean = false;
@@ -82,17 +68,7 @@ class DropToImport extends React.Component<Props> {
};
render() {
const {
documentId,
collectionId,
documents,
disabled,
location,
match,
history,
staticContext,
...rest
} = this.props;
const { documents } = this.props;
if (this.props.disabled) return this.props.children;
@@ -101,16 +77,41 @@ class DropToImport extends React.Component<Props> {
accept={documents.importFileTypes.join(", ")}
onDropAccepted={this.onDropAccepted}
style={EMPTY_OBJECT}
disableClick
disablePreview
noClick
multiple
{...rest}
>
{this.isImporting && <LoadingIndicator />}
{this.props.children}
{({
getRootProps,
getInputProps,
isDragActive,
isDragAccept,
isDragReject,
}) => (
<DropzoneContainer {...getRootProps()} {...{ isDragActive }}>
<input {...getInputProps()} />
{this.isImporting && <LoadingIndicator />}
{this.props.children}
</DropzoneContainer>
)}
</Dropzone>
);
}
}
const DropzoneContainer = styled("div")`
border-radius: 4px;
${({ isDragActive, theme }) =>
isDragActive &&
css`
background: ${theme.slateDark};
a {
color: ${theme.white} !important;
}
svg {
fill: ${theme.white};
}
`}
`;
export default inject("documents", "ui")(withRouter(DropToImport));
+5 -3
View File
@@ -5,6 +5,7 @@ import { observer } from "mobx-react";
import { MoreIcon } from "outline-icons";
import { rgba } from "polished";
import * as React from "react";
import { withTranslation, type TFunction } from "react-i18next";
import { PortalWithState } from "react-portal";
import styled from "styled-components";
import { fadeAndScaleIn } from "shared/styles/animations";
@@ -27,6 +28,7 @@ type Props = {|
hover?: boolean,
style?: Object,
position?: "left" | "right" | "center",
t: TFunction,
|};
@observer
@@ -150,7 +152,7 @@ class DropdownMenu extends React.Component<Props> {
};
render() {
const { className, hover, label, children } = this.props;
const { className, hover, label, children, t } = this.props;
return (
<div className={className}>
@@ -177,7 +179,7 @@ class DropdownMenu extends React.Component<Props> {
{label || (
<NudeButton
id={`${this.id}button`}
aria-label="More options"
aria-label={t("More options")}
aria-haspopup="true"
aria-expanded={isOpen ? "true" : "false"}
aria-controls={this.id}
@@ -284,4 +286,4 @@ export const Header = styled.h3`
margin: 1em 12px 0.5em;
`;
export default DropdownMenu;
export default withTranslation()<DropdownMenu>(DropdownMenu);
+122 -48
View File
@@ -1,6 +1,7 @@
// @flow
import { lighten } from "polished";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { withRouter, type RouterHistory } from "react-router-dom";
import styled, { withTheme } from "styled-components";
import UiStore from "stores/UiStore";
@@ -28,60 +29,129 @@ type PropsWithRef = Props & {
history: RouterHistory,
};
class Editor extends React.Component<PropsWithRef> {
onUploadImage = async (file: File) => {
const result = await uploadFile(file, { documentId: this.props.id });
return result.url;
};
function Editor(props: PropsWithRef) {
const { id, ui, history } = props;
const { t } = useTranslation();
onClickLink = (href: string, event: MouseEvent) => {
// on page hash
if (href[0] === "#") {
window.location.href = href;
return;
}
const onUploadImage = React.useCallback(
async (file: File) => {
const result = await uploadFile(file, { documentId: id });
return result.url;
},
[id]
);
if (isInternalUrl(href) && !event.metaKey && !event.shiftKey) {
// relative
let navigateTo = href;
// probably absolute
if (href[0] !== "/") {
try {
const url = new URL(href);
navigateTo = url.pathname + url.hash;
} catch (err) {
navigateTo = href;
}
const onClickLink = React.useCallback(
(href: string, event: MouseEvent) => {
// on page hash
if (href[0] === "#") {
window.location.href = href;
return;
}
this.props.history.push(navigateTo);
} else if (href) {
window.open(href, "_blank");
}
};
if (isInternalUrl(href) && !event.metaKey && !event.shiftKey) {
// relative
let navigateTo = href;
onShowToast = (message: string) => {
if (this.props.ui) {
this.props.ui.showToast(message);
}
};
// probably absolute
if (href[0] !== "/") {
try {
const url = new URL(href);
navigateTo = url.pathname + url.hash;
} catch (err) {
navigateTo = href;
}
}
render() {
return (
<ErrorBoundary reloadOnChunkMissing>
<StyledEditor
ref={this.props.forwardedRef}
uploadImage={this.onUploadImage}
onClickLink={this.onClickLink}
onShowToast={this.onShowToast}
embeds={this.props.disableEmbeds ? EMPTY_ARRAY : embeds}
tooltip={EditorTooltip}
{...this.props}
/>
</ErrorBoundary>
);
}
history.push(navigateTo);
} else if (href) {
window.open(href, "_blank");
}
},
[history]
);
const onShowToast = React.useCallback(
(message: string) => {
if (ui) {
ui.showToast(message);
}
},
[ui]
);
const dictionary = React.useMemo(() => {
return {
addColumnAfter: t("Insert column after"),
addColumnBefore: t("Insert column before"),
addRowAfter: t("Insert row after"),
addRowBefore: t("Insert row before"),
alignCenter: t("Align center"),
alignLeft: t("Align left"),
alignRight: t("Align right"),
bulletList: t("Bulleted list"),
checkboxList: t("Todo list"),
codeBlock: t("Code block"),
codeCopied: t("Copied to clipboard"),
codeInline: t("Code"),
createLink: t("Create link"),
createLinkError: t("Sorry, an error occurred creating the link"),
createNewDoc: t("Create a new doc"),
deleteColumn: t("Delete column"),
deleteRow: t("Delete row"),
deleteTable: t("Delete table"),
em: t("Italic"),
embedInvalidLink: t("Sorry, that link wont work for this embed type"),
findOrCreateDoc: `${t("Find or create a doc")}`,
h1: t("Big heading"),
h2: t("Medium heading"),
h3: t("Small heading"),
heading: t("Heading"),
hr: t("Divider"),
image: t("Image"),
imageUploadError: t("Sorry, an error occurred uploading the image"),
info: t("Info"),
infoNotice: t("Info notice"),
link: t("Link"),
linkCopied: t("Link copied to clipboard"),
mark: t("Highlight"),
newLineEmpty: `${t("Type '/' to insert")}`,
newLineWithSlash: `${t("Keep typing to filter")}`,
noResults: t("No results"),
openLink: t("Open link"),
orderedList: t("Ordered list"),
pasteLink: `${t("Paste a link")}`,
pasteLinkWithTitle: (service: string) =>
t("Paste a {{service}} link…", { service }),
placeholder: t("Placeholder"),
quote: t("Quote"),
removeLink: t("Remove link"),
searchOrPasteLink: `${t("Search or paste a link")}`,
strikethrough: t("Strikethrough"),
strong: t("Bold"),
subheading: t("Subheading"),
table: t("Table"),
tip: t("Tip"),
tipNotice: t("Tip notice"),
warning: t("Warning"),
warningNotice: t("Warning notice"),
};
}, [t]);
return (
<ErrorBoundary reloadOnChunkMissing>
<StyledEditor
ref={props.forwardedRef}
uploadImage={onUploadImage}
onClickLink={onClickLink}
onShowToast={onShowToast}
embeds={props.disableEmbeds ? EMPTY_ARRAY : embeds}
tooltip={EditorTooltip}
dictionary={dictionary}
{...props}
/>
</ErrorBoundary>
);
}
const StyledEditor = styled(RichMarkdownEditor)`
@@ -92,6 +162,10 @@ const StyledEditor = styled(RichMarkdownEditor)`
transition: ${(props) => props.theme.backgroundTransition};
}
& * {
box-sizing: content-box;
}
.notice-block.tip,
.notice-block.warning {
font-weight: 500;
+5 -5
View File
@@ -1,20 +1,20 @@
// @flow
import { inject, observer } from "mobx-react";
import { observer } from "mobx-react";
import * as React from "react";
import { Link } from "react-router-dom";
import styled from "styled-components";
import parseDocumentSlug from "shared/utils/parseDocumentSlug";
import DocumentsStore from "stores/DocumentsStore";
import DocumentMetaWithViews from "components/DocumentMetaWithViews";
import Editor from "components/Editor";
import useStores from "hooks/useStores";
type Props = {
url: string,
documents: DocumentsStore,
children: (React.Node) => React.Node,
};
function HoverPreviewDocument({ url, documents, children }: Props) {
function HoverPreviewDocument({ url, children }: Props) {
const { documents } = useStores();
const slug = parseDocumentSlug(url);
documents.prefetchDocument(slug, {
@@ -50,4 +50,4 @@ const Heading = styled.h2`
color: ${(props) => props.theme.text};
`;
export default inject("documents")(observer(HoverPreviewDocument));
export default observer(HoverPreviewDocument);
+6 -3
View File
@@ -22,6 +22,7 @@ import {
VehicleIcon,
} from "outline-icons";
import * as React from "react";
import { withTranslation, type TFunction } from "react-i18next";
import styled from "styled-components";
import { DropdownMenu } from "components/DropdownMenu";
import Flex from "components/Flex";
@@ -126,6 +127,7 @@ type Props = {
onChange: (color: string, icon: string) => void,
icon: string,
color: string,
t: TFunction,
};
function preventEventBubble(event) {
@@ -167,12 +169,13 @@ class IconPicker extends React.Component<Props> {
};
render() {
const { t } = this.props;
const Component = icons[this.props.icon || "collection"].component;
return (
<Wrapper ref={(ref) => (this.node = ref)}>
<label>
<LabelText>Icon</LabelText>
<LabelText>{t("Icon")}</LabelText>
</label>
<DropdownMenu
onOpen={this.handleOpen}
@@ -197,7 +200,7 @@ class IconPicker extends React.Component<Props> {
})}
</Icons>
<Flex onClick={preventEventBubble}>
<React.Suspense fallback={<Loading>Loading</Loading>}>
<React.Suspense fallback={<Loading>{t("Loading")}</Loading>}>
<ColorPicker
color={this.props.color}
onChange={(color) =>
@@ -246,4 +249,4 @@ const Wrapper = styled("div")`
position: relative;
`;
export default IconPicker;
export default withTranslation()<IconPicker>(IconPicker);
+9 -4
View File
@@ -3,6 +3,7 @@ import { observable } from "mobx";
import { observer } from "mobx-react";
import { SearchIcon } from "outline-icons";
import * as React from "react";
import { withTranslation, type TFunction } from "react-i18next";
import keydown from "react-keydown";
import { withRouter, type RouterHistory } from "react-router-dom";
import styled, { withTheme } from "styled-components";
@@ -16,6 +17,7 @@ type Props = {
source: string,
placeholder?: string,
collectionId?: string,
t: TFunction,
};
@observer
@@ -24,7 +26,7 @@ class InputSearch extends React.Component<Props> {
@observable focused: boolean = false;
@keydown("meta+f")
focus(ev) {
focus(ev: SyntheticEvent<>) {
ev.preventDefault();
if (this.input) {
@@ -32,7 +34,7 @@ class InputSearch extends React.Component<Props> {
}
}
handleSearchInput = (ev) => {
handleSearchInput = (ev: SyntheticInputEvent<>) => {
ev.preventDefault();
this.props.history.push(
searchUrl(ev.target.value, {
@@ -51,7 +53,8 @@ class InputSearch extends React.Component<Props> {
};
render() {
const { theme, placeholder = "Search…" } = this.props;
const { t } = this.props;
const { theme, placeholder = `${t("Search")}` } = this.props;
return (
<InputMaxWidth
@@ -76,4 +79,6 @@ const InputMaxWidth = styled(Input)`
max-width: 30vw;
`;
export default withTheme(withRouter(InputSearch));
export default withTranslation()<InputSearch>(
withTheme(withRouter(InputSearch))
);
+16 -3
View File
@@ -20,11 +20,17 @@ const Select = styled.select`
}
`;
const Wrapper = styled.label`
display: block;
max-width: ${(props) => (props.short ? "350px" : "100%")};
`;
type Option = { label: string, value: string };
export type Props = {
value?: string,
label?: string,
short?: boolean,
className?: string,
labelHidden?: boolean,
options: Option[],
@@ -43,12 +49,19 @@ class InputSelect extends React.Component<Props> {
};
render() {
const { label, className, labelHidden, options, ...rest } = this.props;
const {
label,
className,
labelHidden,
options,
short,
...rest
} = this.props;
const wrappedLabel = <LabelText>{label}</LabelText>;
return (
<label>
<Wrapper short={short}>
{label &&
(labelHidden ? (
<VisuallyHidden>{wrappedLabel}</VisuallyHidden>
@@ -64,7 +77,7 @@ class InputSelect extends React.Component<Props> {
))}
</Select>
</Outline>
</label>
</Wrapper>
);
}
}
+90
View File
@@ -0,0 +1,90 @@
// @flow
import { find } from "lodash";
import * as React from "react";
import { Trans, useTranslation } from "react-i18next";
import styled from "styled-components";
import { languages, languageOptions } from "shared/i18n";
import Flex from "components/Flex";
import NoticeTip from "components/NoticeTip";
import useCurrentUser from "hooks/useCurrentUser";
import useStores from "hooks/useStores";
import { detectLanguage } from "utils/language";
function Icon(props) {
return (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M21 18H16L14 16V6C14 4.89543 14.8954 4 16 4H28C29.1046 4 30 4.89543 30 6V16C30 17.1046 29.1046 18 28 18H27L25.4142 19.5858C24.6332 20.3668 23.3668 20.3668 22.5858 19.5858L21 18ZM16 15.1716V6H28V16H27H26.1716L25.5858 16.5858L24 18.1716L22.4142 16.5858L21.8284 16H21H16.8284L16 15.1716Z"
fill="#2B2F35"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M16 13H4C2.89543 13 2 13.8954 2 15V25C2 26.1046 2.89543 27 4 27H5L6.58579 28.5858C7.36684 29.3668 8.63316 29.3668 9.41421 28.5858L11 27H16C17.1046 27 18 26.1046 18 25V15C18 13.8954 17.1046 13 16 13ZM9 17L6 16.9681C6 16.9681 5 17.016 5 18C5 18.984 6 19 6 19H8.5H10C10 19 9.57627 20.1885 8.38983 21.0831C7.20339 21.9777 5.7197 23 5.7197 23C5.7197 23 4.99153 23.6054 5.5 24.5C6.00847 25.3946 7 24.8403 7 24.8403L9.74576 22.8722L11.9492 24.6614C11.9492 24.6614 12.6271 25.3771 13.3051 24.4825C13.9831 23.5879 13.3051 23.0512 13.3051 23.0512L11.1017 21.262C11.1017 21.262 11.5 21 12 20L12.5 19H14C14 19 15 19.0319 15 18C15 16.9681 14 16.9681 14 16.9681L11 17V16C11 16 11.0169 15 10 15C8.98305 15 9 16 9 16V17Z"
fill="#2B2F35"
/>
<path
d="M23.6672 12.5221L23.5526 12.1816H23.1934H20.8818H20.5215L20.4075 12.5235L20.082 13.5H19.2196L21.2292 8.10156H21.8774L21.5587 9.06116L20.7633 11.4562L20.5449 12.1138H21.2378H22.8374H23.5327L23.3114 11.4546L22.5072 9.05959L22.1855 8.10156H22.768L24.7887 13.5H23.9964L23.6672 12.5221Z"
fill="#2B2F35"
stroke="#2B2F35"
/>
</svg>
);
}
export default function LanguagePrompt() {
const { auth, ui } = useStores();
const { t } = useTranslation();
const user = useCurrentUser();
const language = detectLanguage();
if (language === "en_US" || language === user.language) {
return null;
}
if (!languages.includes(language)) {
return null;
}
const option = find(languageOptions, (o) => o.value === language);
const optionLabel = option ? option.label : "";
return (
<NoticeTip>
<Flex align="center">
<LanguageIcon />
<span>
<Trans>
Outline is available in your language {{ optionLabel }}, would you
like to change?
</Trans>
<br />
<a
onClick={() => {
auth.updateUser({
language,
});
ui.setLanguagePromptDismissed();
}}
>
{t("Change Language")}
</a>{" "}
&middot; <a onClick={ui.setLanguagePromptDismissed}>{t("Dismiss")}</a>
</span>
</Flex>
</NoticeTip>
);
}
const LanguageIcon = styled(Icon)`
margin-right: 12px;
`;
+25 -10
View File
@@ -3,6 +3,7 @@ import { observable } from "mobx";
import { observer, inject } from "mobx-react";
import * as React from "react";
import { Helmet } from "react-helmet";
import { withTranslation, type TFunction } from "react-i18next";
import keydown from "react-keydown";
import { Switch, Route, Redirect } from "react-router-dom";
import styled, { withTheme } from "styled-components";
@@ -14,7 +15,6 @@ import ErrorSuspended from "scenes/ErrorSuspended";
import KeyboardShortcuts from "scenes/KeyboardShortcuts";
import Analytics from "components/Analytics";
import DocumentHistory from "components/DocumentHistory";
import { GlobalStyles } from "components/DropToImport";
import Flex from "components/Flex";
import { LoadingIndicatorBar } from "components/LoadingIndicator";
@@ -37,6 +37,8 @@ type Props = {
ui: UiStore,
notifications?: React.Node,
theme: Theme,
i18n: Object,
t: TFunction,
};
@observer
@@ -45,7 +47,7 @@ class Layout extends React.Component<Props> {
@observable redirectTo: ?string;
@observable keyboardShortcutsOpen: boolean = false;
constructor(props) {
constructor(props: Props) {
super();
this.updateBackground(props);
}
@@ -58,11 +60,16 @@ class Layout extends React.Component<Props> {
}
}
updateBackground(props) {
updateBackground(props: Props) {
// ensure the wider page color always matches the theme
window.document.body.style.background = props.theme.background;
}
@keydown("meta+.")
handleToggleSidebar() {
this.props.ui.toggleCollapsedSidebar();
}
@keydown("shift+/")
handleOpenKeyboardShortcuts() {
if (this.props.ui.editMode) return;
@@ -74,7 +81,7 @@ class Layout extends React.Component<Props> {
};
@keydown(["t", "/", "meta+k"])
goToSearch(ev) {
goToSearch(ev: SyntheticEvent<>) {
if (this.props.ui.editMode) return;
ev.preventDefault();
ev.stopPropagation();
@@ -88,7 +95,7 @@ class Layout extends React.Component<Props> {
}
render() {
const { auth, ui } = this.props;
const { auth, t, ui } = this.props;
const { user, team } = auth;
const showSidebar = auth.authenticated && user && team;
@@ -117,7 +124,11 @@ class Layout extends React.Component<Props> {
</Switch>
)}
<Content auto justify="center" editMode={ui.editMode}>
<Content
auto
justify="center"
sidebarCollapsed={ui.editMode || ui.sidebarCollapsed}
>
{this.props.children}
</Content>
@@ -131,11 +142,10 @@ class Layout extends React.Component<Props> {
<Modal
isOpen={this.keyboardShortcutsOpen}
onRequestClose={this.handleCloseKeyboardShortcuts}
title="Keyboard shortcuts"
title={t("Keyboard shortcuts")}
>
<KeyboardShortcuts />
</Modal>
<GlobalStyles />
</Container>
);
}
@@ -158,8 +168,13 @@ const Content = styled(Flex)`
}
${breakpoint("tablet")`
margin-left: ${(props) => (props.editMode ? 0 : props.theme.sidebarWidth)};
margin-left: ${(props) =>
props.sidebarCollapsed
? props.theme.sidebarCollapsedWidth
: props.theme.sidebarWidth};
`};
`;
export default inject("auth", "ui", "documents")(withTheme(Layout));
export default withTranslation()<Layout>(
inject("auth", "ui", "documents")(withTheme(Layout))
);
+22
View File
@@ -0,0 +1,22 @@
// @flow
import styled from "styled-components";
const Notice = styled.p`
background: ${(props) => props.theme.brand.marine};
color: ${(props) => props.theme.almostBlack};
padding: 10px 12px;
margin-top: 24px;
border-radius: 4px;
position: relative;
a {
color: ${(props) => props.theme.almostBlack};
font-weight: 500;
}
a:hover {
text-decoration: underline;
}
`;
export default Notice;
+16 -12
View File
@@ -12,6 +12,7 @@ import {
PlusIcon,
} from "outline-icons";
import * as React from "react";
import { withTranslation, type TFunction } from "react-i18next";
import styled from "styled-components";
import AuthStore from "stores/AuthStore";
@@ -34,6 +35,7 @@ type Props = {
auth: AuthStore,
documents: DocumentsStore,
policies: PoliciesStore,
t: TFunction,
};
@observer
@@ -65,7 +67,7 @@ class MainSidebar extends React.Component<Props> {
};
render() {
const { auth, documents, policies } = this.props;
const { auth, documents, policies, t } = this.props;
const { user, team } = auth;
if (!user || !team) return null;
@@ -90,7 +92,7 @@ class MainSidebar extends React.Component<Props> {
to="/home"
icon={<HomeIcon color="currentColor" />}
exact={false}
label="Home"
label={t("Home")}
/>
<SidebarLink
to={{
@@ -98,20 +100,20 @@ class MainSidebar extends React.Component<Props> {
state: { fromMenu: true },
}}
icon={<SearchIcon color="currentColor" />}
label="Search"
label={t("Search")}
exact={false}
/>
<SidebarLink
to="/starred"
icon={<StarredIcon color="currentColor" />}
exact={false}
label="Starred"
label={t("Starred")}
/>
<SidebarLink
to="/templates"
icon={<ShapesIcon color="currentColor" />}
exact={false}
label="Templates"
label={t("Templates")}
active={
documents.active ? documents.active.template : undefined
}
@@ -121,7 +123,7 @@ class MainSidebar extends React.Component<Props> {
icon={<EditIcon color="currentColor" />}
label={
<Drafts align="center">
Drafts
{t("Drafts")}
{documents.totalDrafts > 0 && (
<Bubble count={documents.totalDrafts} />
)}
@@ -146,7 +148,7 @@ class MainSidebar extends React.Component<Props> {
to="/archive"
icon={<ArchiveIcon color="currentColor" />}
exact={false}
label="Archive"
label={t("Archive")}
active={
documents.active
? documents.active.isArchived && !documents.active.isDeleted
@@ -157,7 +159,7 @@ class MainSidebar extends React.Component<Props> {
to="/trash"
icon={<TrashIcon color="currentColor" />}
exact={false}
label="Trash"
label={t("Trash")}
active={
documents.active ? documents.active.isDeleted : undefined
}
@@ -167,21 +169,21 @@ class MainSidebar extends React.Component<Props> {
to="/settings/people"
onClick={this.handleInviteModalOpen}
icon={<PlusIcon color="currentColor" />}
label="Invite people…"
label={t("Invite people…")}
/>
)}
</Section>
</Scrollable>
</Flex>
<Modal
title="Invite people"
title={t("Invite people")}
onRequestClose={this.handleInviteModalClose}
isOpen={this.inviteModalOpen}
>
<Invite onSubmit={this.handleInviteModalClose} />
</Modal>
<Modal
title="Create a collection"
title={t("Create a collection")}
onRequestClose={this.handleCreateCollectionModalClose}
isOpen={this.createCollectionModalOpen}
>
@@ -196,4 +198,6 @@ const Drafts = styled(Flex)`
height: 24px;
`;
export default inject("documents", "policies", "auth")(MainSidebar);
export default withTranslation()<MainSidebar>(
inject("documents", "policies", "auth")(MainSidebar)
);
+18 -14
View File
@@ -13,6 +13,7 @@ import {
ExpandedIcon,
} from "outline-icons";
import * as React from "react";
import { withTranslation, type TFunction } from "react-i18next";
import type { RouterHistory } from "react-router-dom";
import styled from "styled-components";
import AuthStore from "stores/AuthStore";
@@ -36,6 +37,7 @@ type Props = {
history: RouterHistory,
policies: PoliciesStore,
auth: AuthStore,
t: TFunction,
};
@observer
@@ -45,7 +47,7 @@ class SettingsSidebar extends React.Component<Props> {
};
render() {
const { policies, auth } = this.props;
const { policies, t, auth } = this.props;
const { team } = auth;
if (!team) return null;
@@ -56,7 +58,7 @@ class SettingsSidebar extends React.Component<Props> {
<HeaderBlock
subheading={
<ReturnToApp align="center">
<BackIcon color="currentColor" /> Return to App
<BackIcon color="currentColor" /> {t("Return to App")}
</ReturnToApp>
}
teamName={team.name}
@@ -71,17 +73,17 @@ class SettingsSidebar extends React.Component<Props> {
<SidebarLink
to="/settings"
icon={<ProfileIcon color="currentColor" />}
label="Profile"
label={t("Profile")}
/>
<SidebarLink
to="/settings/notifications"
icon={<EmailIcon color="currentColor" />}
label="Notifications"
label={t("Notifications")}
/>
<SidebarLink
to="/settings/tokens"
icon={<CodeIcon color="currentColor" />}
label="API Tokens"
label={t("API Tokens")}
/>
</Section>
<Section>
@@ -90,44 +92,44 @@ class SettingsSidebar extends React.Component<Props> {
<SidebarLink
to="/settings/details"
icon={<TeamIcon color="currentColor" />}
label="Details"
label={t("Details")}
/>
)}
{can.update && (
<SidebarLink
to="/settings/security"
icon={<PadlockIcon color="currentColor" />}
label="Security"
label={t("Security")}
/>
)}
<SidebarLink
to="/settings/people"
icon={<UserIcon color="currentColor" />}
exact={false}
label="People"
label={t("People")}
/>
<SidebarLink
to="/settings/groups"
icon={<GroupIcon color="currentColor" />}
exact={false}
label="Groups"
label={t("Groups")}
/>
<SidebarLink
to="/settings/shares"
icon={<LinkIcon color="currentColor" />}
label="Share Links"
label={t("Share Links")}
/>
{can.export && (
<SidebarLink
to="/settings/export"
icon={<DocumentIcon color="currentColor" />}
label="Export Data"
label={t("Export Data")}
/>
)}
</Section>
{can.update && (
<Section>
<Header>Integrations</Header>
<Header>{t("Integrations")}</Header>
<SidebarLink
to="/settings/integrations/slack"
icon={<SlackIcon color="currentColor" />}
@@ -144,7 +146,7 @@ class SettingsSidebar extends React.Component<Props> {
)}
{can.update && !isHosted && (
<Section>
<Header>Installation</Header>
<Header>{t("Installation")}</Header>
<Version />
</Section>
)}
@@ -164,4 +166,6 @@ const ReturnToApp = styled(Flex)`
height: 16px;
`;
export default inject("auth", "policies")(SettingsSidebar);
export default withTranslation()<SettingsSidebar>(
inject("auth", "policies")(SettingsSidebar)
);
+32 -4
View File
@@ -8,6 +8,7 @@ import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import Fade from "components/Fade";
import Flex from "components/Flex";
import CollapseToggle, { Button } from "./components/CollapseToggle";
import usePrevious from "hooks/usePrevious";
import useStores from "hooks/useStores";
@@ -26,14 +27,18 @@ function Sidebar({ location, children }: Props) {
if (location !== previousLocation) {
ui.hideMobileSidebar();
}
}, [ui, location]);
}, [ui, location, previousLocation]);
const content = (
<Container
editMode={ui.editMode}
mobileSidebarVisible={ui.mobileSidebarVisible}
collapsed={ui.editMode || ui.sidebarCollapsed}
column
>
<CollapseToggle
collapsed={ui.sidebarCollapsed}
onClick={ui.toggleCollapsedSidebar}
/>
<Toggle
onClick={ui.toggleMobileSidebar}
mobileSidebarVisible={ui.mobileSidebarVisible}
@@ -63,7 +68,7 @@ const Container = styled(Flex)`
bottom: 0;
width: 100%;
background: ${(props) => props.theme.sidebarBackground};
transition: left 100ms ease-out,
transition: box-shadow, 100ms, ease-in-out, left 100ms ease-out,
${(props) => props.theme.backgroundTransition};
margin-left: ${(props) => (props.mobileSidebarVisible ? 0 : "-100%")};
z-index: ${(props) => props.theme.depths.sidebar};
@@ -90,10 +95,33 @@ const Container = styled(Flex)`
}
${breakpoint("tablet")`
left: ${(props) => (props.editMode ? `-${props.theme.sidebarWidth}` : 0)};
left: ${(props) =>
props.collapsed
? `calc(-${props.theme.sidebarWidth} + ${props.theme.sidebarCollapsedWidth})`
: 0};
width: ${(props) => props.theme.sidebarWidth};
margin: 0;
z-index: 3;
&:hover,
&:focus-within {
left: 0;
box-shadow: ${(props) =>
props.collapsed ? "rgba(0, 0, 0, 0.2) 1px 0 4px" : "none"};
& ${Button} {
opacity: .75;
}
& ${Button}:hover {
opacity: 1;
}
}
&:not(:hover):not(:focus-within) > div {
opacity: ${(props) => (props.collapsed ? "0" : "1")};
transition: opacity 100ms ease-in-out;
}
`};
`;
@@ -0,0 +1,59 @@
// @flow
import { NextIcon, BackIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import Tooltip from "components/Tooltip";
import { meta } from "utils/keyboard";
type Props = {|
collapsed: boolean,
onClick?: () => void,
|};
function CollapseToggle({ collapsed, ...rest }: Props) {
const { t } = useTranslation();
return (
<Tooltip
tooltip={collapsed ? t("Expand") : t("Collapse")}
shortcut={`${meta}+.`}
delay={500}
placement="bottom"
>
<Button {...rest} aria-hidden>
{collapsed ? (
<NextIcon color="currentColor" />
) : (
<BackIcon color="currentColor" />
)}
</Button>
</Tooltip>
);
}
export const Button = styled.button`
display: block;
position: absolute;
top: 28px;
right: 8px;
border: 0;
width: 24px;
height: 24px;
z-index: 1;
font-weight: 600;
color: ${(props) => props.theme.sidebarText};
background: ${(props) => props.theme.sidebarItemBackground};
transition: opacity 100ms ease-in-out;
border-radius: 4px;
opacity: 0;
cursor: pointer;
padding: 0;
&:hover {
color: ${(props) => props.theme.white};
background: ${(props) => props.theme.primary};
}
`;
export default CollapseToggle;
@@ -1,96 +1,109 @@
// @flow
import { observable } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import DocumentsStore from "stores/DocumentsStore";
import { useDrop } from "react-dnd";
import UiStore from "stores/UiStore";
import Collection from "models/Collection";
import Document from "models/Document";
import CollectionIcon from "components/CollectionIcon";
import DropToImport from "components/DropToImport";
import Flex from "components/Flex";
import DocumentLink from "./DocumentLink";
import EditableTitle from "./EditableTitle";
import SidebarLink from "./SidebarLink";
import useStores from "hooks/useStores";
import CollectionMenu from "menus/CollectionMenu";
type Props = {|
collection: Collection,
ui: UiStore,
canUpdate: boolean,
documents: DocumentsStore,
activeDocument: ?Document,
prefetchDocument: (id: string) => Promise<void>,
|};
@observer
class CollectionLink extends React.Component<Props> {
@observable menuOpen = false;
function CollectionLink({
collection,
activeDocument,
prefetchDocument,
canUpdate,
ui,
}: Props) {
const [menuOpen, setMenuOpen] = React.useState(false);
handleTitleChange = async (name: string) => {
await this.props.collection.save({ name });
};
const handleTitleChange = React.useCallback(
async (name: string) => {
await collection.save({ name });
},
[collection]
);
render() {
const {
collection,
documents,
activeDocument,
prefetchDocument,
canUpdate,
ui,
} = this.props;
const expanded = collection.id === ui.activeCollectionId;
const { documents, policies } = useStores();
const expanded = collection.id === ui.activeCollectionId;
return (
<DropToImport
key={collection.id}
collectionId={collection.id}
activeClassName="activeDropZone"
>
<SidebarLink
key={collection.id}
to={collection.url}
icon={<CollectionIcon collection={collection} expanded={expanded} />}
iconColor={collection.color}
expanded={expanded}
hideDisclosure
menuOpen={this.menuOpen}
label={
<EditableTitle
title={collection.name}
onSubmit={this.handleTitleChange}
canUpdate={canUpdate}
/>
}
exact={false}
menu={
<CollectionMenu
position="right"
collection={collection}
onOpen={() => (this.menuOpen = true)}
onClose={() => (this.menuOpen = false)}
/>
}
>
<Flex column>
{collection.documents.map((node) => (
<DocumentLink
key={node.id}
node={node}
documents={documents}
collection={collection}
activeDocument={activeDocument}
prefetchDocument={prefetchDocument}
// Droppable
const [{ isOver, canDrop }, drop] = useDrop({
accept: "document",
drop: (item, monitor) => {
if (!collection) return;
documents.move(item.id, collection.id);
},
canDrop: (item, monitor) => {
return policies.abilities(collection.id).update;
},
collect: (monitor) => ({
isOver: !!monitor.isOver(),
canDrop: monitor.canDrop(),
}),
});
return (
<>
<div ref={drop}>
<DropToImport key={collection.id} collectionId={collection.id}>
<SidebarLink
key={collection.id}
to={collection.url}
icon={
<CollectionIcon collection={collection} expanded={expanded} />
}
iconColor={collection.color}
expanded={expanded}
menuOpen={menuOpen}
isActiveDrop={isOver && canDrop}
label={
<EditableTitle
title={collection.name}
onSubmit={handleTitleChange}
canUpdate={canUpdate}
depth={1.5}
/>
))}
</Flex>
</SidebarLink>
</DropToImport>
);
}
}
exact={false}
menu={
<CollectionMenu
position="right"
collection={collection}
onOpen={() => setMenuOpen(true)}
onClose={() => setMenuOpen(false)}
/>
}
></SidebarLink>
</DropToImport>
</div>
{expanded &&
collection.documents.map((node) => (
<DocumentLink
key={node.id}
node={node}
collection={collection}
activeDocument={activeDocument}
prefetchDocument={prefetchDocument}
canUpdate={canUpdate}
depth={1.5}
/>
))}
</>
);
}
export default CollectionLink;
export default observer(CollectionLink);
@@ -2,6 +2,7 @@
import { observer, inject } from "mobx-react";
import { PlusIcon } from "outline-icons";
import * as React from "react";
import { withTranslation, type TFunction } from "react-i18next";
import keydown from "react-keydown";
import { withRouter, type RouterHistory } from "react-router-dom";
@@ -24,6 +25,7 @@ type Props = {
documents: DocumentsStore,
onCreateCollection: () => void,
ui: UiStore,
t: TFunction,
};
@observer
@@ -52,14 +54,13 @@ class Collections extends React.Component<Props> {
}
render() {
const { collections, ui, policies, documents } = this.props;
const { collections, ui, policies, documents, t } = this.props;
const content = (
<>
{collections.orderedData.map((collection) => (
<CollectionLink
key={collection.id}
documents={documents}
collection={collection}
activeDocument={documents.active}
prefetchDocument={documents.prefetchDocument}
@@ -71,7 +72,7 @@ class Collections extends React.Component<Props> {
to="/collections"
onClick={this.props.onCreateCollection}
icon={<PlusIcon color="currentColor" />}
label="New collection"
label={`${t("New collection")}`}
exact
/>
</>
@@ -79,7 +80,7 @@ class Collections extends React.Component<Props> {
return (
<Flex column>
<Header>Collections</Header>
<Header>{t("Collections")}</Header>
{collections.isLoaded ? (
this.isPreloaded ? (
content
@@ -94,9 +95,6 @@ class Collections extends React.Component<Props> {
}
}
export default inject(
"collections",
"ui",
"documents",
"policies"
)(withRouter(Collections));
export default withTranslation()<Collections>(
inject("collections", "ui", "documents", "policies")(withRouter(Collections))
);
+182 -122
View File
@@ -1,22 +1,22 @@
// @flow
import { observable } from "mobx";
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";
import styled from "styled-components";
import DocumentsStore from "stores/DocumentsStore";
import Collection from "models/Collection";
import Document from "models/Document";
import DropToImport from "components/DropToImport";
import Fade from "components/Fade";
import Flex from "components/Flex";
import EditableTitle from "./EditableTitle";
import SidebarLink from "./SidebarLink";
import useStores from "hooks/useStores";
import DocumentMenu from "menus/DocumentMenu";
import { type NavigationNode } from "types";
type Props = {|
node: NavigationNode,
documents: DocumentsStore,
canUpdate: boolean,
collection?: Collection,
activeDocument: ?Document,
@@ -25,139 +25,199 @@ type Props = {|
depth: number,
|};
@observer
class DocumentLink extends React.Component<Props> {
@observable menuOpen = false;
function DocumentLink({
node,
collection,
activeDocument,
activeDocumentRef,
prefetchDocument,
depth,
canUpdate,
}: Props) {
const { documents, policies } = useStores();
const { t } = useTranslation();
componentDidMount() {
if (this.isActiveDocument() && this.hasChildDocuments()) {
this.props.documents.fetchChildDocuments(this.props.node.id);
const isActiveDocument = activeDocument && activeDocument.id === node.id;
const hasChildDocuments = !!node.children.length;
const document = documents.get(node.id);
const { fetchChildDocuments } = documents;
React.useEffect(() => {
if (isActiveDocument && hasChildDocuments) {
fetchChildDocuments(node.id);
}
}
}, [fetchChildDocuments, node, hasChildDocuments, isActiveDocument]);
componentDidUpdate(prevProps: Props) {
if (prevProps.activeDocument !== this.props.activeDocument) {
if (this.isActiveDocument() && this.hasChildDocuments()) {
this.props.documents.fetchChildDocuments(this.props.node.id);
}
}
}
const pathToNode = React.useMemo(
() =>
collection && collection.pathToDocument(node.id).map((entry) => entry.id),
[collection, node]
);
handleMouseEnter = (ev: SyntheticEvent<>) => {
const { node, prefetchDocument } = this.props;
ev.stopPropagation();
ev.preventDefault();
prefetchDocument(node.id);
};
handleTitleChange = async (title: string) => {
const document = this.props.documents.get(this.props.node.id);
if (!document) return;
await this.props.documents.update({
id: document.id,
lastRevision: document.revision,
text: document.text,
title,
});
};
isActiveDocument = () => {
return (
this.props.activeDocument &&
this.props.activeDocument.id === this.props.node.id
);
};
hasChildDocuments = () => {
return !!this.props.node.children.length;
};
render() {
const {
node,
documents,
collection,
activeDocument,
activeDocumentRef,
prefetchDocument,
depth,
canUpdate,
} = this.props;
const showChildren = !!(
const showChildren = React.useMemo(() => {
return !!(
hasChildDocuments &&
activeDocument &&
collection &&
(collection
.pathToDocument(activeDocument)
.pathToDocument(activeDocument.id)
.map((entry) => entry.id)
.includes(node.id) ||
this.isActiveDocument())
isActiveDocument)
);
const document = documents.get(node.id);
const title = node.title || "Untitled";
}, [hasChildDocuments, activeDocument, isActiveDocument, node, collection]);
return (
<Flex
column
const [expanded, setExpanded] = React.useState(showChildren);
React.useEffect(() => {
if (showChildren) {
setExpanded(showChildren);
}
}, [showChildren]);
const handleDisclosureClick = React.useCallback(
(ev: SyntheticEvent<>) => {
ev.preventDefault();
ev.stopPropagation();
setExpanded(!expanded);
},
[expanded]
);
const handleMouseEnter = React.useCallback(
(ev: SyntheticEvent<>) => {
prefetchDocument(node.id);
},
[prefetchDocument, node]
);
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]
);
const [menuOpen, setMenuOpen] = React.useState(false);
const isMoving = documents.movingDocumentId === node.id;
// Draggable
const [{ isDragging }, drag] = useDrag({
item: { type: "document", ...node, depth, active: isActiveDocument },
collect: (monitor) => ({
isDragging: !!monitor.isDragging(),
}),
canDrag: (monitor) => {
return policies.abilities(node.id).move;
},
});
// Droppable
const [{ isOver, canDrop }, drop] = useDrop({
accept: "document",
drop: async (item, monitor) => {
if (!collection) return;
documents.move(item.id, collection.id, node.id);
},
canDrop: (item, monitor) =>
pathToNode && !pathToNode.includes(monitor.getItem().id),
collect: (monitor) => ({
isOver: !!monitor.isOver(),
canDrop: monitor.canDrop(),
}),
});
return (
<>
<Draggable
key={node.id}
ref={this.isActiveDocument() ? activeDocumentRef : undefined}
onMouseEnter={this.handleMouseEnter}
ref={drag}
$isDragging={isDragging}
$isMoving={isMoving}
>
<DropToImport documentId={node.id} activeClassName="activeDropZone">
<SidebarLink
to={{
pathname: node.url,
state: { title: node.title },
}}
expanded={showChildren ? true : undefined}
label={
<EditableTitle
title={title}
onSubmit={this.handleTitleChange}
canUpdate={canUpdate}
/>
}
depth={depth}
exact={false}
menuOpen={this.menuOpen}
menu={
document ? (
<Fade>
<DocumentMenu
position="right"
document={document}
onOpen={() => (this.menuOpen = true)}
onClose={() => (this.menuOpen = false)}
/>
</Fade>
) : undefined
}
>
{this.hasChildDocuments() && (
<DocumentChildren column>
{node.children.map((childNode) => (
<DocumentLink
key={childNode.id}
collection={collection}
node={childNode}
documents={documents}
activeDocument={activeDocument}
prefetchDocument={prefetchDocument}
depth={depth + 1}
<div ref={drop}>
<DropToImport documentId={node.id} activeClassName="activeDropZone">
<SidebarLink
innerRef={isActiveDocument ? activeDocumentRef : undefined}
onMouseEnter={handleMouseEnter}
to={{
pathname: node.url,
state: { title: node.title },
}}
label={
<>
{hasChildDocuments && (
<Disclosure
expanded={expanded && !isDragging}
onClick={handleDisclosureClick}
/>
)}
<EditableTitle
title={node.title || t("Untitled")}
onSubmit={handleTitleChange}
canUpdate={canUpdate}
/>
))}
</DocumentChildren>
)}
</SidebarLink>
</DropToImport>
</Flex>
);
}
</>
}
isActiveDrop={isOver && canDrop}
depth={depth}
exact={false}
menuOpen={menuOpen}
menu={
document && !isMoving ? (
<Fade>
<DocumentMenu
position="right"
document={document}
onOpen={() => setMenuOpen(true)}
onClose={() => setMenuOpen(false)}
/>
</Fade>
) : undefined
}
/>
</DropToImport>
</div>
</Draggable>
{expanded && !isDragging && (
<>
{node.children.map((childNode) => (
<ObservedDocumentLink
key={childNode.id}
collection={collection}
node={childNode}
activeDocument={activeDocument}
prefetchDocument={prefetchDocument}
depth={depth + 1}
canUpdate={canUpdate}
/>
))}
</>
)}
</>
);
}
const DocumentChildren = styled(Flex)``;
const Draggable = styled("div")`
opacity: ${(props) => (props.$isDragging || props.$isMoving ? 0.5 : 1)};
pointer-events: ${(props) => (props.$isMoving ? "none" : "all")};
`;
export default DocumentLink;
const Disclosure = styled(CollapsedIcon)`
position: absolute;
left: -24px;
${({ expanded }) => !expanded && "transform: rotate(-90deg);"};
`;
const ObservedDocumentLink = observer(DocumentLink);
export default ObservedDocumentLink;
@@ -82,6 +82,7 @@ function EditableTitle({ title, onSubmit, canUpdate }: Props) {
const Input = styled.input`
margin-left: -4px;
color: ${(props) => props.theme.sidebarText};
background: ${(props) => props.theme.background};
width: calc(100% - 10px);
border-radius: 3px;
@@ -61,7 +61,7 @@ const Header = styled.button`
display: flex;
align-items: center;
flex-shrink: 0;
padding: 16px 24px;
padding: 20px 24px;
position: relative;
background: none;
line-height: inherit;
@@ -1,25 +1,23 @@
// @flow
import { observer } from "mobx-react";
import { CollapsedIcon } from "outline-icons";
import * as React from "react";
import { withRouter, NavLink } from "react-router-dom";
import styled, { withTheme } from "styled-components";
import Flex from "components/Flex";
import { type Theme } from "types";
type Props = {
to?: string | Object,
href?: string | Object,
innerRef?: (?HTMLElement) => void,
onClick?: (SyntheticEvent<>) => void,
onMouseEnter?: (SyntheticEvent<>) => void,
children?: React.Node,
icon?: React.Node,
expanded?: boolean,
label?: React.Node,
menu?: React.Node,
menuOpen?: boolean,
hideDisclosure?: boolean,
iconColor?: string,
active?: boolean,
isActiveDrop?: boolean,
theme: Theme,
exact?: boolean,
depth?: number,
@@ -29,75 +27,50 @@ function SidebarLink({
icon,
children,
onClick,
onMouseEnter,
to,
label,
active,
isActiveDrop,
menu,
menuOpen,
hideDisclosure,
theme,
exact,
href,
innerRef,
depth,
...rest
}: Props) {
const [expanded, setExpanded] = React.useState(rest.expanded);
const style = React.useMemo(() => {
return {
paddingLeft: `${(depth || 0) * 16 + 16}px`,
};
}, [depth]);
React.useEffect(() => {
if (rest.expanded !== undefined) {
setExpanded(rest.expanded);
}
}, [rest.expanded]);
const handleClick = React.useCallback(
(ev: SyntheticEvent<>) => {
ev.preventDefault();
ev.stopPropagation();
setExpanded(!expanded);
},
[expanded]
);
const handleExpand = React.useCallback(() => {
setExpanded(true);
}, []);
const showDisclosure = !!children && !hideDisclosure;
const activeStyle = {
color: theme.text,
background: theme.sidebarItemBackground,
fontWeight: 600,
background: theme.sidebarItemBackground,
...style,
};
return (
<Wrapper column>
<StyledNavLink
activeStyle={activeStyle}
style={active ? activeStyle : style}
onClick={onClick}
exact={exact !== false}
to={to}
as={to ? undefined : href ? "a" : "div"}
href={href}
>
{icon && <IconWrapper>{icon}</IconWrapper>}
<Label onClick={handleExpand}>
{showDisclosure && (
<Disclosure expanded={expanded} onClick={handleClick} />
)}
{label}
</Label>
{menu && <Action menuOpen={menuOpen}>{menu}</Action>}
</StyledNavLink>
{expanded && children}
</Wrapper>
<StyledNavLink
$isActiveDrop={isActiveDrop}
activeStyle={isActiveDrop ? undefined : activeStyle}
style={active ? activeStyle : style}
onClick={onClick}
onMouseEnter={onMouseEnter}
exact={exact !== false}
to={to}
as={to ? undefined : href ? "a" : "div"}
href={href}
ref={innerRef}
>
{icon && <IconWrapper>{icon}</IconWrapper>}
<Label>{label}</Label>
{menu && <Action menuOpen={menuOpen}>{menu}</Action>}
</StyledNavLink>
);
}
@@ -133,12 +106,20 @@ const StyledNavLink = styled(NavLink)`
text-overflow: ellipsis;
padding: 4px 16px;
border-radius: 4px;
color: ${(props) => props.theme.sidebarText};
background: ${(props) =>
props.$isActiveDrop ? props.theme.slateDark : "inherit"};
color: ${(props) =>
props.$isActiveDrop ? props.theme.white : props.theme.sidebarText};
font-size: 15px;
cursor: pointer;
svg {
${(props) => (props.$isActiveDrop ? `fill: ${props.theme.white};` : "")}
}
&:hover {
color: ${(props) => props.theme.text};
color: ${(props) =>
props.$isActiveDrop ? props.theme.white : props.theme.text};
}
&:focus {
@@ -153,10 +134,6 @@ const StyledNavLink = styled(NavLink)`
}
`;
const Wrapper = styled(Flex)`
position: relative;
`;
const Label = styled.div`
position: relative;
width: 100%;
@@ -164,11 +141,4 @@ const Label = styled.div`
line-height: 1.6;
`;
const Disclosure = styled(CollapsedIcon)`
position: absolute;
left: -24px;
${({ expanded }) => !expanded && "transform: rotate(-90deg);"};
`;
export default withRouter(withTheme(observer(SidebarLink)));
export default withRouter(withTheme(SidebarLink));
+9
View File
@@ -0,0 +1,9 @@
// @flow
import invariant from "invariant";
import useStores from "./useStores";
export default function useCurrentUser() {
const { auth } = useStores();
invariant(auth.user, "user required");
return auth.user;
}
+15 -9
View File
@@ -3,9 +3,11 @@ import "mobx-react-lite/batchingForReactDom";
import "focus-visible";
import { Provider } from "mobx-react";
import * as React from "react";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { render } from "react-dom";
import { BrowserRouter as Router } from "react-router-dom";
import { initI18n } from "shared/i18n";
import stores from "stores";
import ErrorBoundary from "components/ErrorBoundary";
import ScrollToTop from "components/ScrollToTop";
@@ -14,6 +16,8 @@ import Toasts from "components/Toasts";
import Routes from "./routes";
import env from "env";
initI18n();
const element = document.getElementById("root");
if (element) {
@@ -21,14 +25,16 @@ if (element) {
<ErrorBoundary>
<Provider {...stores}>
<Theme>
<Router>
<>
<ScrollToTop>
<Routes />
</ScrollToTop>
<Toasts />
</>
</Router>
<DndProvider backend={HTML5Backend}>
<Router>
<>
<ScrollToTop>
<Routes />
</ScrollToTop>
<Toasts />
</>
</Router>
</DndProvider>
</Theme>
</Provider>
</ErrorBoundary>,
+18 -14
View File
@@ -3,6 +3,7 @@ import { observable } from "mobx";
import { inject, observer } from "mobx-react";
import { SunIcon, MoonIcon } from "outline-icons";
import * as React from "react";
import { withTranslation, type TFunction } from "react-i18next";
import { Link } from "react-router-dom";
import styled from "styled-components";
import AuthStore from "stores/AuthStore";
@@ -23,6 +24,7 @@ type Props = {
label: React.Node,
ui: UiStore,
auth: AuthStore,
t: TFunction,
};
@observer
@@ -42,14 +44,14 @@ class AccountMenu extends React.Component<Props> {
};
render() {
const { ui } = this.props;
const { ui, t } = this.props;
return (
<>
<Modal
isOpen={this.keyboardShortcutsOpen}
onRequestClose={this.handleCloseKeyboardShortcuts}
title="Keyboard shortcuts"
title={t("Keyboard shortcuts")}
>
<KeyboardShortcuts />
</Modal>
@@ -58,23 +60,23 @@ class AccountMenu extends React.Component<Props> {
label={this.props.label}
>
<DropdownMenuItem as={Link} to={settings()}>
Settings
{t("Settings")}
</DropdownMenuItem>
<DropdownMenuItem onClick={this.handleOpenKeyboardShortcuts}>
Keyboard shortcuts
{t("Keyboard shortcuts")}
</DropdownMenuItem>
<DropdownMenuItem href={developers()} target="_blank">
API documentation
{t("API documentation")}
</DropdownMenuItem>
<hr />
<DropdownMenuItem href={changelog()} target="_blank">
Changelog
{t("Changelog")}
</DropdownMenuItem>
<DropdownMenuItem href={mailToUrl()} target="_blank">
Send us feedback
{t("Send us feedback")}
</DropdownMenuItem>
<DropdownMenuItem href={githubIssuesUrl()} target="_blank">
Report a bug
{t("Report a bug")}
</DropdownMenuItem>
<hr />
<DropdownMenu
@@ -87,7 +89,7 @@ class AccountMenu extends React.Component<Props> {
label={
<DropdownMenuItem>
<ChangeTheme justify="space-between">
Appearance
{t("Appearance")}
{ui.resolvedTheme === "light" ? <SunIcon /> : <MoonIcon />}
</ChangeTheme>
</DropdownMenuItem>
@@ -98,24 +100,24 @@ class AccountMenu extends React.Component<Props> {
onClick={() => ui.setTheme("system")}
selected={ui.theme === "system"}
>
System
{t("System")}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => ui.setTheme("light")}
selected={ui.theme === "light"}
>
Light
{t("Light")}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => ui.setTheme("dark")}
selected={ui.theme === "dark"}
>
Dark
{t("Dark")}
</DropdownMenuItem>
</DropdownMenu>
<hr />
<DropdownMenuItem onClick={this.handleLogout}>
Log out
{t("Log out")}
</DropdownMenuItem>
</DropdownMenu>
</>
@@ -127,4 +129,6 @@ const ChangeTheme = styled(Flex)`
width: 100%;
`;
export default inject("ui", "auth")(AccountMenu);
export default withTranslation()<AccountMenu>(
inject("ui", "auth")(AccountMenu)
);
+16 -15
View File
@@ -2,6 +2,7 @@
import { observable } from "mobx";
import { inject, observer } from "mobx-react";
import * as React from "react";
import { withTranslation, type TFunction } from "react-i18next";
import { withRouter, type RouterHistory } from "react-router-dom";
import DocumentsStore from "stores/DocumentsStore";
import PoliciesStore from "stores/PoliciesStore";
@@ -27,6 +28,7 @@ type Props = {
history: RouterHistory,
onOpen?: () => void,
onClose?: () => void,
t: TFunction,
};
@observer
@@ -112,6 +114,7 @@ class CollectionMenu extends React.Component<Props> {
position,
onOpen,
onClose,
t,
} = this.props;
const can = policies.abilities(collection.id);
@@ -128,7 +131,7 @@ class CollectionMenu extends React.Component<Props> {
</VisuallyHidden>
<Modal
title="Collection permissions"
title={t("Collection permissions")}
onRequestClose={this.handleMembersModalClose}
isOpen={this.showCollectionMembers}
>
@@ -143,12 +146,12 @@ class CollectionMenu extends React.Component<Props> {
<DropdownMenuItems
items={[
{
title: "New document",
title: t("New document"),
visible: !!(collection && can.update),
onClick: this.onNewDocument,
},
{
title: "Import document",
title: t("Import document"),
visible: !!(collection && can.update),
onClick: this.onImportDocument,
},
@@ -156,22 +159,22 @@ class CollectionMenu extends React.Component<Props> {
type: "separator",
},
{
title: "Edit",
title: `${t("Edit")}`,
visible: !!(collection && can.update),
onClick: this.handleEditCollectionOpen,
},
{
title: "Permissions",
title: `${t("Permissions")}`,
visible: !!(collection && can.update),
onClick: this.handleMembersModalOpen,
},
{
title: "Export",
title: `${t("Export")}`,
visible: !!(collection && can.export),
onClick: this.handleExportCollectionOpen,
},
{
title: "Delete",
title: `${t("Delete")}`,
visible: !!(collection && can.delete),
onClick: this.handleDeleteCollectionOpen,
},
@@ -179,7 +182,7 @@ class CollectionMenu extends React.Component<Props> {
/>
</DropdownMenu>
<Modal
title="Edit collection"
title={t("Edit collection")}
isOpen={this.showCollectionEdit}
onRequestClose={this.handleEditCollectionClose}
>
@@ -189,7 +192,7 @@ class CollectionMenu extends React.Component<Props> {
/>
</Modal>
<Modal
title="Delete collection"
title={t("Delete collection")}
isOpen={this.showCollectionDelete}
onRequestClose={this.handleDeleteCollectionClose}
>
@@ -199,7 +202,7 @@ class CollectionMenu extends React.Component<Props> {
/>
</Modal>
<Modal
title="Export collection"
title={t("Export collection")}
isOpen={this.showCollectionExport}
onRequestClose={this.handleExportCollectionClose}
>
@@ -213,8 +216,6 @@ class CollectionMenu extends React.Component<Props> {
}
}
export default inject(
"ui",
"documents",
"policies"
)(withRouter(CollectionMenu));
export default withTranslation()<CollectionMenu>(
inject("ui", "documents", "policies")(withRouter(CollectionMenu))
);
+41 -30
View File
@@ -2,6 +2,7 @@
import { observable } from "mobx";
import { inject, observer } from "mobx-react";
import * as React from "react";
import { withTranslation, type TFunction } from "react-i18next";
import { Redirect } from "react-router-dom";
import AuthStore from "stores/AuthStore";
import CollectionStore from "stores/CollectionsStore";
@@ -38,6 +39,7 @@ type Props = {
label?: React.Node,
onOpen?: () => void,
onClose?: () => void,
t: TFunction,
};
@observer
@@ -83,7 +85,8 @@ class DocumentMenu extends React.Component<Props> {
// when duplicating, go straight to the duplicated document content
this.redirectTo = duped.url;
this.props.ui.showToast("Document duplicated");
const { t } = this.props;
this.props.ui.showToast(t("Document duplicated"));
};
handleOpenTemplateModal = () => {
@@ -100,7 +103,8 @@ class DocumentMenu extends React.Component<Props> {
handleArchive = async (ev: SyntheticEvent<>) => {
await this.props.document.archive();
this.props.ui.showToast("Document archived");
const { t } = this.props;
this.props.ui.showToast(t("Document archived"));
};
handleRestore = async (
@@ -108,12 +112,14 @@ class DocumentMenu extends React.Component<Props> {
options?: { collectionId: string }
) => {
await this.props.document.restore(options);
this.props.ui.showToast("Document restored");
const { t } = this.props;
this.props.ui.showToast(t("Document restored"));
};
handleUnpublish = async (ev: SyntheticEvent<>) => {
await this.props.document.unpublish();
this.props.ui.showToast("Document unpublished");
const { t } = this.props;
this.props.ui.showToast(t("Document unpublished"));
};
handlePin = (ev: SyntheticEvent<>) => {
@@ -164,6 +170,7 @@ class DocumentMenu extends React.Component<Props> {
label,
onOpen,
onClose,
t,
} = this.props;
const can = policies.abilities(document.id);
@@ -183,17 +190,17 @@ class DocumentMenu extends React.Component<Props> {
<DropdownMenuItems
items={[
{
title: "Restore",
title: t("Restore"),
visible: !!can.unarchive,
onClick: this.handleRestore,
},
{
title: "Restore",
title: t("Restore"),
visible: !!(collection && can.restore),
onClick: this.handleRestore,
},
{
title: "Restore",
title: `${t("Restore")}`,
visible: !collection && !!can.restore,
style: {
left: -170,
@@ -204,7 +211,7 @@ class DocumentMenu extends React.Component<Props> {
items: [
{
type: "heading",
title: "Choose a collection",
title: t("Choose a collection"),
},
...collections.orderedData.map((collection) => {
const can = policies.abilities(collection.id);
@@ -224,37 +231,37 @@ class DocumentMenu extends React.Component<Props> {
],
},
{
title: "Unpin",
title: t("Unpin"),
onClick: this.handleUnpin,
visible: !!(showPin && document.pinned && can.unpin),
},
{
title: "Pin to collection",
title: t("Pin to collection"),
onClick: this.handlePin,
visible: !!(showPin && !document.pinned && can.pin),
},
{
title: "Unstar",
title: t("Unstar"),
onClick: this.handleUnstar,
visible: document.isStarred && !!can.unstar,
},
{
title: "Star",
title: t("Star"),
onClick: this.handleStar,
visible: !document.isStarred && !!can.star,
},
{
title: "Share link",
title: `${t("Share link")}`,
onClick: this.handleShareLink,
visible: canShareDocuments,
},
{
title: "Enable embeds",
title: t("Enable embeds"),
onClick: document.enableEmbeds,
visible: !!showToggleEmbeds && document.embedsDisabled,
},
{
title: "Disable embeds",
title: t("Disable embeds"),
onClick: document.disableEmbeds,
visible: !!showToggleEmbeds && !document.embedsDisabled,
},
@@ -262,42 +269,42 @@ class DocumentMenu extends React.Component<Props> {
type: "separator",
},
{
title: "New nested document",
title: t("New nested document"),
onClick: this.handleNewChild,
visible: !!can.createChildDocument,
},
{
title: "Create template",
title: `${t("Create template")}`,
onClick: this.handleOpenTemplateModal,
visible: !!can.update && !document.isTemplate,
},
{
title: "Edit",
title: t("Edit"),
onClick: this.handleEdit,
visible: !!can.update,
},
{
title: "Duplicate",
title: t("Duplicate"),
onClick: this.handleDuplicate,
visible: !!can.update,
},
{
title: "Unpublish",
title: t("Unpublish"),
onClick: this.handleUnpublish,
visible: !!can.unpublish,
},
{
title: "Archive",
title: t("Archive"),
onClick: this.handleArchive,
visible: !!can.archive,
},
{
title: "Delete",
title: `${t("Delete")}`,
onClick: this.handleDelete,
visible: !!can.delete,
},
{
title: "Move",
title: `${t("Move")}`,
onClick: this.handleMove,
visible: !!can.move,
},
@@ -305,17 +312,17 @@ class DocumentMenu extends React.Component<Props> {
type: "separator",
},
{
title: "History",
title: t("History"),
onClick: this.handleDocumentHistory,
visible: canViewHistory,
},
{
title: "Download",
title: t("Download"),
onClick: this.handleExport,
visible: !!can.download,
},
{
title: "Print",
title: t("Print"),
onClick: window.print,
visible: !!showPrint,
},
@@ -323,7 +330,9 @@ class DocumentMenu extends React.Component<Props> {
/>
</DropdownMenu>
<Modal
title={`Delete ${this.props.document.noun}`}
title={t("Delete {{ documentName }}", {
documentName: this.props.document.noun,
})}
onRequestClose={this.handleCloseDeleteModal}
isOpen={this.showDeleteModal}
>
@@ -333,7 +342,7 @@ class DocumentMenu extends React.Component<Props> {
/>
</Modal>
<Modal
title="Create template"
title={t("Create template")}
onRequestClose={this.handleCloseTemplateModal}
isOpen={this.showTemplateModal}
>
@@ -343,7 +352,7 @@ class DocumentMenu extends React.Component<Props> {
/>
</Modal>
<Modal
title="Share document"
title={t("Share document")}
onRequestClose={this.handleCloseShareModal}
isOpen={this.showShareModal}
>
@@ -357,4 +366,6 @@ class DocumentMenu extends React.Component<Props> {
}
}
export default inject("ui", "auth", "collections", "policies")(DocumentMenu);
export default withTranslation()<DocumentMenu>(
inject("ui", "auth", "collections", "policies")(DocumentMenu)
);
+11 -7
View File
@@ -2,6 +2,7 @@
import { observable } from "mobx";
import { inject, observer } from "mobx-react";
import * as React from "react";
import { withTranslation, type TFunction } from "react-i18next";
import { withRouter, type RouterHistory } from "react-router-dom";
import PoliciesStore from "stores/PoliciesStore";
import UiStore from "stores/UiStore";
@@ -20,6 +21,7 @@ type Props = {
onMembers: () => void,
onOpen?: () => void,
onClose?: () => void,
t: TFunction,
};
@observer
@@ -46,13 +48,13 @@ class GroupMenu extends React.Component<Props> {
};
render() {
const { policies, group, onOpen, onClose } = this.props;
const { policies, group, onOpen, onClose, t } = this.props;
const can = policies.abilities(group.id);
return (
<>
<Modal
title="Edit group"
title={t("Edit group")}
onRequestClose={this.handleEditModalClose}
isOpen={this.editModalOpen}
>
@@ -63,7 +65,7 @@ class GroupMenu extends React.Component<Props> {
</Modal>
<Modal
title="Delete group"
title={t("Delete group")}
onRequestClose={this.handleDeleteModalClose}
isOpen={this.deleteModalOpen}
>
@@ -76,7 +78,7 @@ class GroupMenu extends React.Component<Props> {
<DropdownMenuItems
items={[
{
title: "Members",
title: `${t("Members")}`,
onClick: this.props.onMembers,
visible: !!(group && can.read),
},
@@ -84,12 +86,12 @@ class GroupMenu extends React.Component<Props> {
type: "separator",
},
{
title: "Edit",
title: `${t("Edit")}`,
onClick: this.onEdit,
visible: !!(group && can.update),
},
{
title: "Delete",
title: `${t("Delete")}`,
onClick: this.onDelete,
visible: !!(group && can.delete),
},
@@ -101,4 +103,6 @@ class GroupMenu extends React.Component<Props> {
}
}
export default inject("policies")(withRouter(GroupMenu));
export default withTranslation()<GroupMenu>(
inject("policies")(withRouter(GroupMenu))
);
+11 -5
View File
@@ -2,6 +2,7 @@
import { observable } from "mobx";
import { observer, inject } from "mobx-react";
import * as React from "react";
import { Trans, withTranslation, type TFunction } from "react-i18next";
import { Redirect } from "react-router-dom";
import CollectionsStore from "stores/CollectionsStore";
@@ -14,6 +15,7 @@ type Props = {
label?: React.Node,
document: Document,
collections: CollectionsStore,
t: TFunction,
};
@observer
@@ -39,8 +41,9 @@ class NewChildDocumentMenu extends React.Component<Props> {
render() {
if (this.redirectTo) return <Redirect to={this.redirectTo} push />;
const { label, document, collections } = this.props;
const { label, document, collections, t } = this.props;
const collection = collections.get(document.collectionId);
const collectionName = collection ? collection.name : t("collection");
return (
<DropdownMenu label={label}>
@@ -49,14 +52,15 @@ class NewChildDocumentMenu extends React.Component<Props> {
{
title: (
<span>
New document in{" "}
<strong>{collection ? collection.name : "collection"}</strong>
<Trans>
New document in <strong>{{ collectionName }}</strong>
</Trans>
</span>
),
onClick: this.handleNewDocument,
},
{
title: "New nested document",
title: t("New nested document"),
onClick: this.handleNewChild,
},
]}
@@ -66,4 +70,6 @@ class NewChildDocumentMenu extends React.Component<Props> {
}
}
export default inject("collections")(NewChildDocumentMenu);
export default withTranslation()<NewChildDocumentMenu>(
inject("collections")(NewChildDocumentMenu)
);
+17 -5
View File
@@ -3,6 +3,7 @@ import { observable } from "mobx";
import { inject, observer } from "mobx-react";
import { PlusIcon } from "outline-icons";
import * as React from "react";
import { withTranslation, type TFunction } from "react-i18next";
import { Redirect } from "react-router-dom";
import CollectionsStore from "stores/CollectionsStore";
@@ -19,6 +20,7 @@ type Props = {
documents: DocumentsStore,
collections: CollectionsStore,
policies: PoliciesStore,
t: TFunction,
};
@observer
@@ -29,7 +31,14 @@ class NewDocumentMenu extends React.Component<Props> {
this.redirectTo = undefined;
}
handleNewDocument = (collectionId: string, options) => {
handleNewDocument = (
collectionId: string,
options?: {
parentDocumentId?: string,
template?: boolean,
templateId?: string,
}
) => {
this.redirectTo = newDocumentUrl(collectionId, options);
};
@@ -44,7 +53,7 @@ class NewDocumentMenu extends React.Component<Props> {
render() {
if (this.redirectTo) return <Redirect to={this.redirectTo} push />;
const { collections, documents, policies, label, ...rest } = this.props;
const { collections, documents, policies, label, t, ...rest } = this.props;
const singleCollection = collections.orderedData.length === 1;
return (
@@ -52,14 +61,15 @@ class NewDocumentMenu extends React.Component<Props> {
label={
label || (
<Button icon={<PlusIcon />} small>
New doc{singleCollection ? "" : "…"}
{t("New doc")}
{singleCollection ? "" : "…"}
</Button>
)
}
onOpen={this.onOpen}
{...rest}
>
<Header>Choose a collection</Header>
<Header>{t("Choose a collection")}</Header>
<DropdownMenuItems
items={collections.orderedData.map((collection) => ({
onClick: () => this.handleNewDocument(collection.id),
@@ -77,4 +87,6 @@ class NewDocumentMenu extends React.Component<Props> {
}
}
export default inject("collections", "documents", "policies")(NewDocumentMenu);
export default withTranslation()<NewDocumentMenu>(
inject("collections", "documents", "policies")(NewDocumentMenu)
);
+8 -4
View File
@@ -3,6 +3,7 @@ import { observable } from "mobx";
import { inject, observer } from "mobx-react";
import { PlusIcon } from "outline-icons";
import * as React from "react";
import { withTranslation, type TFunction } from "react-i18next";
import { Redirect } from "react-router-dom";
import CollectionsStore from "stores/CollectionsStore";
@@ -17,6 +18,7 @@ type Props = {
label?: React.Node,
collections: CollectionsStore,
policies: PoliciesStore,
t: TFunction,
};
@observer
@@ -36,20 +38,20 @@ class NewTemplateMenu extends React.Component<Props> {
render() {
if (this.redirectTo) return <Redirect to={this.redirectTo} push />;
const { collections, policies, label, ...rest } = this.props;
const { collections, policies, label, t, ...rest } = this.props;
return (
<DropdownMenu
label={
label || (
<Button icon={<PlusIcon />} small>
New template
{t("New template")}
</Button>
)
}
{...rest}
>
<Header>Choose a collection</Header>
<Header>{t("Choose a collection")}</Header>
<DropdownMenuItems
items={collections.orderedData.map((collection) => ({
onClick: () => this.handleNewDocument(collection.id),
@@ -67,4 +69,6 @@ class NewTemplateMenu extends React.Component<Props> {
}
}
export default inject("collections", "policies")(NewTemplateMenu);
export default withTranslation()<NewTemplateMenu>(
inject("collections", "policies")(NewTemplateMenu)
);
+12 -6
View File
@@ -1,6 +1,7 @@
// @flow
import { inject } from "mobx-react";
import * as React from "react";
import { withTranslation, type TFunction } from "react-i18next";
import { withRouter, type RouterHistory } from "react-router-dom";
import UiStore from "stores/UiStore";
@@ -19,22 +20,25 @@ type Props = {
className?: string,
label: React.Node,
ui: UiStore,
t: TFunction,
};
class RevisionMenu extends React.Component<Props> {
handleRestore = async (ev: SyntheticEvent<>) => {
ev.preventDefault();
await this.props.document.restore({ revisionId: this.props.revision.id });
this.props.ui.showToast("Document restored");
const { t } = this.props;
this.props.ui.showToast(t("Document restored"));
this.props.history.push(this.props.document.url);
};
handleCopy = () => {
this.props.ui.showToast("Link copied");
const { t } = this.props;
this.props.ui.showToast(t("Link copied"));
};
render() {
const { className, label, onOpen, onClose } = this.props;
const { className, label, onOpen, onClose, t } = this.props;
const url = `${window.location.origin}${documentHistoryUrl(
this.props.document,
this.props.revision.id
@@ -48,15 +52,17 @@ class RevisionMenu extends React.Component<Props> {
label={label}
>
<DropdownMenuItem onClick={this.handleRestore}>
Restore version
{t("Restore version")}
</DropdownMenuItem>
<hr />
<CopyToClipboard text={url} onCopy={this.handleCopy}>
<DropdownMenuItem>Copy link</DropdownMenuItem>
<DropdownMenuItem>{t("Copy link")}</DropdownMenuItem>
</CopyToClipboard>
</DropdownMenu>
);
}
}
export default withRouter(inject("ui")(RevisionMenu));
export default withTranslation()<RevisionMenu>(
withRouter(inject("ui")(RevisionMenu))
);
+11 -7
View File
@@ -2,6 +2,7 @@
import { observable } from "mobx";
import { inject, observer } from "mobx-react";
import * as React from "react";
import { withTranslation, type TFunction } from "react-i18next";
import { Redirect } from "react-router-dom";
import SharesStore from "stores/SharesStore";
@@ -16,6 +17,7 @@ type Props = {
shares: SharesStore,
ui: UiStore,
share: Share,
t: TFunction,
};
@observer
@@ -36,36 +38,38 @@ class ShareMenu extends React.Component<Props> {
try {
await this.props.shares.revoke(this.props.share);
this.props.ui.showToast("Share link revoked");
const { t } = this.props;
this.props.ui.showToast(t("Share link revoked"));
} catch (err) {
this.props.ui.showToast(err.message);
}
};
handleCopy = () => {
this.props.ui.showToast("Share link copied");
const { t } = this.props;
this.props.ui.showToast(t("Share link copied"));
};
render() {
if (this.redirectTo) return <Redirect to={this.redirectTo} push />;
const { share, onOpen, onClose } = this.props;
const { share, onOpen, onClose, t } = this.props;
return (
<DropdownMenu onOpen={onOpen} onClose={onClose}>
<CopyToClipboard text={share.url} onCopy={this.handleCopy}>
<DropdownMenuItem>Copy link</DropdownMenuItem>
<DropdownMenuItem>{t("Copy link")}</DropdownMenuItem>
</CopyToClipboard>
<DropdownMenuItem onClick={this.handleGoToDocument}>
Go to document
{t("Go to document")}
</DropdownMenuItem>
<hr />
<DropdownMenuItem onClick={this.handleRevoke}>
Revoke link
{t("Revoke link")}
</DropdownMenuItem>
</DropdownMenu>
);
}
}
export default inject("shares", "ui")(ShareMenu);
export default withTranslation()<ShareMenu>(inject("shares", "ui")(ShareMenu));
+10 -4
View File
@@ -2,6 +2,7 @@
import { observer, inject } from "mobx-react";
import { DocumentIcon } from "outline-icons";
import * as React from "react";
import { withTranslation, type TFunction } from "react-i18next";
import styled from "styled-components";
import DocumentsStore from "stores/DocumentsStore";
import Document from "models/Document";
@@ -11,12 +12,13 @@ import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
type Props = {
document: Document,
documents: DocumentsStore,
t: TFunction,
};
@observer
class TemplatesMenu extends React.Component<Props> {
render() {
const { documents, document, ...rest } = this.props;
const { documents, document, t, ...rest } = this.props;
const templates = documents.templatesInCollection(document.collectionId);
if (!templates.length) {
@@ -28,7 +30,7 @@ class TemplatesMenu extends React.Component<Props> {
position="left"
label={
<Button disclosure neutral>
Templates
{t("Templates")}
</Button>
}
{...rest}
@@ -42,7 +44,9 @@ class TemplatesMenu extends React.Component<Props> {
<div>
<strong>{template.titleWithDefault}</strong>
<br />
<Author>By {template.createdBy.name}</Author>
<Author>
{t("By {{ author }}", { author: template.createdBy.name })}
</Author>
</div>
</DropdownMenuItem>
))}
@@ -55,4 +59,6 @@ const Author = styled.div`
font-size: 13px;
`;
export default inject("documents")(TemplatesMenu);
export default withTranslation()<TemplatesMenu>(
inject("documents")(TemplatesMenu)
);
+30 -13
View File
@@ -2,6 +2,7 @@
import { inject, observer } from "mobx-react";
import * as React from "react";
import { withTranslation, type TFunction } from "react-i18next";
import UsersStore from "stores/UsersStore";
import User from "models/User";
import { DropdownMenu } from "components/DropdownMenu";
@@ -10,16 +11,20 @@ import DropdownMenuItems from "components/DropdownMenu/DropdownMenuItems";
type Props = {
user: User,
users: UsersStore,
t: TFunction,
};
@observer
class UserMenu extends React.Component<Props> {
handlePromote = (ev: SyntheticEvent<>) => {
ev.preventDefault();
const { user, users } = this.props;
const { user, users, t } = this.props;
if (
!window.confirm(
`Are you want to make ${user.name} an admin? Admins can modify team and billing information.`
t(
"Are you sure you want to make {{ userName }} an admin? Admins can modify team and billing information.",
{ userName: user.name }
)
)
) {
return;
@@ -29,8 +34,14 @@ class UserMenu extends React.Component<Props> {
handleDemote = (ev: SyntheticEvent<>) => {
ev.preventDefault();
const { user, users } = this.props;
if (!window.confirm(`Are you want to make ${user.name} a member?`)) {
const { user, users, t } = this.props;
if (
!window.confirm(
t("Are you sure you want to make {{ userName }} a member?", {
userName: user.name,
})
)
) {
return;
}
users.demote(user);
@@ -38,10 +49,12 @@ class UserMenu extends React.Component<Props> {
handleSuspend = (ev: SyntheticEvent<>) => {
ev.preventDefault();
const { user, users } = this.props;
const { user, users, t } = this.props;
if (
!window.confirm(
"Are you want to suspend this account? Suspended users will be prevented from logging in."
t(
"Are you sure you want to suspend this account? Suspended users will be prevented from logging in."
)
)
) {
return;
@@ -62,19 +75,23 @@ class UserMenu extends React.Component<Props> {
};
render() {
const { user } = this.props;
const { user, t } = this.props;
return (
<DropdownMenu>
<DropdownMenuItems
items={[
{
title: `Make ${user.name} a member…`,
title: t("Make {{ userName }} a member…", {
userName: user.name,
}),
onClick: this.handleDemote,
visible: user.isAdmin,
},
{
title: `Make ${user.name} an admin…`,
title: t("Make {{ userName }} an admin…", {
userName: user.name,
}),
onClick: this.handlePromote,
visible: !user.isAdmin && !user.isSuspended,
},
@@ -82,17 +99,17 @@ class UserMenu extends React.Component<Props> {
type: "separator",
},
{
title: "Revoke invite",
title: `${t("Revoke invite")}`,
onClick: this.handleRevoke,
visible: user.isInvited,
},
{
title: "Reactivate account",
title: t("Activate account"),
onClick: this.handleActivate,
visible: !user.isInvited && user.isSuspended,
},
{
title: "Suspend account",
title: `${t("Suspend account")}`,
onClick: this.handleSuspend,
visible: !user.isInvited && !user.isSuspended,
},
@@ -103,4 +120,4 @@ class UserMenu extends React.Component<Props> {
}
}
export default inject("users")(UserMenu);
export default withTranslation()<UserMenu>(inject("users")(UserMenu));
+2 -2
View File
@@ -79,12 +79,12 @@ export default class Collection extends BaseModel {
return result;
}
pathToDocument(document: Document) {
pathToDocument(documentId: string) {
let path;
const traveler = (nodes, previousPath) => {
nodes.forEach((childNode) => {
const newPath = [...previousPath, childNode];
if (childNode.id === document.id) {
if (childNode.id === documentId) {
path = newPath;
return;
}
+6 -1
View File
@@ -206,6 +206,11 @@ export default class Document extends BaseModel {
@action
view = () => {
// we don't record views for documents in the trash
if (this.isDeleted || !this.publishedAt) {
return;
}
return this.store.rootStore.views.create({ documentId: this.id });
};
@@ -263,7 +268,7 @@ export default class Document extends BaseModel {
};
move = (collectionId: string, parentDocumentId: ?string) => {
return this.store.move(this, collectionId, parentDocumentId);
return this.store.move(this.id, collectionId, parentDocumentId);
};
duplicate = () => {
+1
View File
@@ -11,6 +11,7 @@ class User extends BaseModel {
lastActiveAt: string;
isSuspended: boolean;
createdAt: string;
language: string;
@computed
get isInvited(): boolean {
+21 -20
View File
@@ -1,6 +1,7 @@
// @flow
import { observer, inject } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import DocumentsStore from "stores/DocumentsStore";
import CenteredContent from "components/CenteredContent";
@@ -14,26 +15,26 @@ type Props = {
documents: DocumentsStore,
};
@observer
class Archive extends React.Component<Props> {
render() {
const { documents } = this.props;
function Archive(props: Props) {
const { t } = useTranslation();
const { documents } = props;
return (
<CenteredContent column auto>
<PageTitle title="Archive" />
<Heading>Archive</Heading>
<PaginatedDocumentList
documents={documents.archived}
fetch={documents.fetchArchived}
heading={<Subheading>Documents</Subheading>}
empty={<Empty>The document archive is empty at the moment.</Empty>}
showCollection
showTemplate
/>
</CenteredContent>
);
}
return (
<CenteredContent column auto>
<PageTitle title={t("Archive")} />
<Heading>{t("Archive")}</Heading>
<PaginatedDocumentList
documents={documents.archived}
fetch={documents.fetchArchived}
heading={<Subheading>{t("Documents")}</Subheading>}
empty={
<Empty>{t("The document archive is empty at the moment.")}</Empty>
}
showCollection
showTemplate
/>
</CenteredContent>
);
}
export default inject("documents")(Archive);
export default inject("documents")(observer(Archive));
+31 -24
View File
@@ -4,6 +4,7 @@ import { observer, inject } from "mobx-react";
import { NewDocumentIcon, PlusIcon, PinIcon } from "outline-icons";
import * as React from "react";
import { withTranslation, Trans, type TFunction } from "react-i18next";
import { Redirect, Link, Switch, Route, type Match } from "react-router-dom";
import styled, { withTheme } from "styled-components";
@@ -47,6 +48,7 @@ type Props = {
policies: PoliciesStore,
match: Match,
theme: Theme,
t: TFunction,
};
@observer
@@ -64,7 +66,7 @@ class CollectionScene extends React.Component<Props> {
}
}
componentDidUpdate(prevProps) {
componentDidUpdate(prevProps: Props) {
const { id } = this.props.match.params;
if (this.collection) {
@@ -132,7 +134,7 @@ class CollectionScene extends React.Component<Props> {
};
renderActions() {
const { match, policies } = this.props;
const { match, policies, t } = this.props;
const can = policies.abilities(match.params.id || "");
return (
@@ -142,19 +144,19 @@ class CollectionScene extends React.Component<Props> {
<Action>
<InputSearch
source="collection"
placeholder="Search in collection"
placeholder={`${t("Search in collection")}`}
collectionId={match.params.id}
/>
</Action>
<Action>
<Tooltip
tooltip="New document"
tooltip={t("New document")}
shortcut="n"
delay={500}
placement="bottom"
>
<Button onClick={this.onNewDocument} icon={<PlusIcon />}>
New doc
{t("New doc")}
</Button>
</Tooltip>
</Action>
@@ -169,7 +171,7 @@ class CollectionScene extends React.Component<Props> {
}
render() {
const { documents, theme } = this.props;
const { documents, theme, t } = this.props;
if (this.redirectTo) return <Redirect to={this.redirectTo} push />;
if (!this.isFetching && !this.collection) return <Search notFound />;
@@ -179,6 +181,7 @@ class CollectionScene extends React.Component<Props> {
: [];
const hasPinnedDocuments = !!pinnedDocuments.length;
const collection = this.collection;
const collectionName = collection ? collection.name : "";
return (
<CenteredContent>
@@ -188,26 +191,28 @@ class CollectionScene extends React.Component<Props> {
{collection.isEmpty ? (
<Centered column>
<HelpText>
<strong>{collection.name}</strong> doesnt contain any
documents yet.
<Trans>
<strong>{{ collectionName }}</strong> doesnt contain any
documents yet.
</Trans>
<br />
Get started by creating a new one!
<Trans>Get started by creating a new one!</Trans>
</HelpText>
<Wrapper>
<Link to={newDocumentUrl(collection.id)}>
<Button icon={<NewDocumentIcon color={theme.buttonText} />}>
Create a document
{t("Create a document")}
</Button>
</Link>
&nbsp;&nbsp;
{collection.private && (
<Button onClick={this.onPermissions} neutral>
Manage members
{t("Manage members")}
</Button>
)}
</Wrapper>
<Modal
title="Collection permissions"
title={t("Collection permissions")}
onRequestClose={this.handlePermissionsModalClose}
isOpen={this.permissionsModalOpen}
>
@@ -218,7 +223,7 @@ class CollectionScene extends React.Component<Props> {
/>
</Modal>
<Modal
title="Edit collection"
title={t("Edit collection")}
onRequestClose={this.handleEditModalClose}
isOpen={this.editModalOpen}
>
@@ -249,7 +254,7 @@ class CollectionScene extends React.Component<Props> {
{hasPinnedDocuments && (
<>
<Subheading>
<TinyPinIcon size={18} /> Pinned
<TinyPinIcon size={18} /> {t("Pinned")}
</Subheading>
<DocumentList documents={pinnedDocuments} showPin />
</>
@@ -257,16 +262,16 @@ class CollectionScene extends React.Component<Props> {
<Tabs>
<Tab to={collectionUrl(collection.id)} exact>
Recently updated
{t("Recently updated")}
</Tab>
<Tab to={collectionUrl(collection.id, "recent")} exact>
Recently published
{t("Recently published")}
</Tab>
<Tab to={collectionUrl(collection.id, "old")} exact>
Least recently updated
{t("Least recently updated")}
</Tab>
<Tab to={collectionUrl(collection.id, "alphabetical")} exact>
AZ
{t("AZ")}
</Tab>
</Tabs>
<Switch>
@@ -351,9 +356,11 @@ const Wrapper = styled(Flex)`
margin: 10px 0;
`;
export default inject(
"collections",
"policies",
"documents",
"ui"
)(withTheme(CollectionScene));
export default withTranslation()<CollectionScene>(
inject(
"collections",
"policies",
"documents",
"ui"
)(withTheme(CollectionScene))
);
+19 -11
View File
@@ -2,6 +2,7 @@
import { observable } from "mobx";
import { inject, observer } from "mobx-react";
import * as React from "react";
import { withTranslation, type TFunction } from "react-i18next";
import UiStore from "stores/UiStore";
import Collection from "models/Collection";
import Button from "components/Button";
@@ -16,6 +17,7 @@ type Props = {
collection: Collection,
ui: UiStore,
onSubmit: () => void,
t: TFunction,
};
@observer
@@ -30,6 +32,7 @@ class CollectionEdit extends React.Component<Props> {
handleSubmit = async (ev: SyntheticEvent<*>) => {
ev.preventDefault();
this.isSaving = true;
const { t } = this.props;
try {
await this.props.collection.save({
@@ -40,7 +43,7 @@ class CollectionEdit extends React.Component<Props> {
private: this.private,
});
this.props.onSubmit();
this.props.ui.showToast("The collection was updated");
this.props.ui.showToast(t("The collection was updated"));
} catch (err) {
this.props.ui.showToast(err.message);
} finally {
@@ -48,7 +51,7 @@ class CollectionEdit extends React.Component<Props> {
}
};
handleDescriptionChange = (getValue) => {
handleDescriptionChange = (getValue: () => string) => {
this.description = getValue();
};
@@ -66,17 +69,20 @@ class CollectionEdit extends React.Component<Props> {
};
render() {
const { t } = this.props;
return (
<Flex column>
<form onSubmit={this.handleSubmit}>
<HelpText>
You can edit the name and other details at any time, however doing
so often might confuse your team mates.
{t(
"You can edit the name and other details at any time, however doing so often might confuse your team mates."
)}
</HelpText>
<Flex>
<Input
type="text"
label="Name"
label={t("Name")}
onChange={this.handleNameChange}
value={this.name}
required
@@ -92,27 +98,29 @@ class CollectionEdit extends React.Component<Props> {
</Flex>
<InputRich
id={this.props.collection.id}
label="Description"
label={t("Description")}
onChange={this.handleDescriptionChange}
defaultValue={this.description || ""}
placeholder="More details about this collection…"
placeholder={t("More details about this collection…")}
minHeight={68}
maxHeight={200}
/>
<Switch
id="private"
label="Private collection"
label={t("Private collection")}
onChange={this.handlePrivateChange}
checked={this.private}
/>
<HelpText>
A private collection will only be visible to invited team members.
{t(
"A private collection will only be visible to invited team members."
)}
</HelpText>
<Button
type="submit"
disabled={this.isSaving || !this.props.collection.name}
>
{this.isSaving ? "Saving" : "Save"}
{this.isSaving ? `${t("Saving")}` : t("Save")}
</Button>
</form>
</Flex>
@@ -120,4 +128,4 @@ class CollectionEdit extends React.Component<Props> {
}
}
export default inject("ui")(CollectionEdit);
export default withTranslation()<CollectionEdit>(inject("ui")(CollectionEdit));
@@ -3,12 +3,14 @@ import { debounce } from "lodash";
import { observable } from "mobx";
import { inject, observer } from "mobx-react";
import * as React from "react";
import { withTranslation, type TFunction } from "react-i18next";
import styled from "styled-components";
import AuthStore from "stores/AuthStore";
import CollectionGroupMembershipsStore from "stores/CollectionGroupMembershipsStore";
import GroupsStore from "stores/GroupsStore";
import UiStore from "stores/UiStore";
import Collection from "models/Collection";
import Group from "models/Group";
import GroupNew from "scenes/GroupNew";
import Button from "components/Button";
import Empty from "components/Empty";
@@ -26,6 +28,7 @@ type Props = {
collectionGroupMemberships: CollectionGroupMembershipsStore,
groups: GroupsStore,
onSubmit: () => void,
t: TFunction,
};
@observer
@@ -52,50 +55,56 @@ class AddGroupsToCollection extends React.Component<Props> {
});
}, 250);
handleAddGroup = (group) => {
handleAddGroup = (group: Group) => {
const { t } = this.props;
try {
this.props.collectionGroupMemberships.create({
collectionId: this.props.collection.id,
groupId: group.id,
permission: "read_write",
});
this.props.ui.showToast(`${group.name} was added to the collection`);
this.props.ui.showToast(
t("{{ groupName }} was added to the collection", {
groupName: group.name,
})
);
} catch (err) {
this.props.ui.showToast("Could not add user");
this.props.ui.showToast(t("Could not add user"));
console.error(err);
}
};
render() {
const { groups, collection, auth } = this.props;
const { groups, collection, auth, t } = this.props;
const { user, team } = auth;
if (!user || !team) return null;
return (
<Flex column>
<HelpText>
Cant find the group youre looking for?{" "}
{t("Cant find the group youre looking for?")}{" "}
<a role="button" onClick={this.handleNewGroupModalOpen}>
Create a group
{t("Create a group")}
</a>
.
</HelpText>
<Input
type="search"
placeholder="Search by group name"
placeholder={`${t("Search by group name")}`}
value={this.query}
onChange={this.handleFilter}
label="Search groups"
label={t("Search groups")}
labelHidden
flex
/>
<PaginatedList
empty={
this.query ? (
<Empty>No groups matching your search</Empty>
<Empty>{t("No groups matching your search")}</Empty>
) : (
<Empty>No groups left to add</Empty>
<Empty>{t("No groups left to add")}</Empty>
)
}
items={groups.notInCollection(collection.id, this.query)}
@@ -108,7 +117,7 @@ class AddGroupsToCollection extends React.Component<Props> {
renderActions={() => (
<ButtonWrap>
<Button onClick={() => this.handleAddGroup(item)} neutral>
Add
{t("Add")}
</Button>
</ButtonWrap>
)}
@@ -116,7 +125,7 @@ class AddGroupsToCollection extends React.Component<Props> {
)}
/>
<Modal
title="Create a group"
title={t("Create a group")}
onRequestClose={this.handleNewGroupModalClose}
isOpen={this.newGroupModalOpen}
>
@@ -131,9 +140,11 @@ const ButtonWrap = styled.div`
margin-left: 6px;
`;
export default inject(
"auth",
"groups",
"collectionGroupMemberships",
"ui"
)(AddGroupsToCollection);
export default withTranslation()<AddGroupsToCollection>(
inject(
"auth",
"groups",
"collectionGroupMemberships",
"ui"
)(AddGroupsToCollection)
);
@@ -3,11 +3,13 @@ import { debounce } from "lodash";
import { observable } from "mobx";
import { inject, observer } from "mobx-react";
import * as React from "react";
import { withTranslation, type TFunction } from "react-i18next";
import AuthStore from "stores/AuthStore";
import MembershipsStore from "stores/MembershipsStore";
import UiStore from "stores/UiStore";
import UsersStore from "stores/UsersStore";
import Collection from "models/Collection";
import User from "models/User";
import Invite from "scenes/Invite";
import Empty from "components/Empty";
import Flex from "components/Flex";
@@ -24,6 +26,7 @@ type Props = {
memberships: MembershipsStore,
users: UsersStore,
onSubmit: () => void,
t: TFunction,
};
@observer
@@ -50,40 +53,43 @@ class AddPeopleToCollection extends React.Component<Props> {
});
}, 250);
handleAddUser = (user) => {
handleAddUser = (user: User) => {
const { t } = this.props;
try {
this.props.memberships.create({
collectionId: this.props.collection.id,
userId: user.id,
permission: "read_write",
});
this.props.ui.showToast(`${user.name} was added to the collection`);
this.props.ui.showToast(
t("{{ userName }} was added to the collection", { userName: user.name })
);
} catch (err) {
this.props.ui.showToast("Could not add user");
this.props.ui.showToast(t("Could not add user"));
}
};
render() {
const { users, collection, auth } = this.props;
const { users, collection, auth, t } = this.props;
const { user, team } = auth;
if (!user || !team) return null;
return (
<Flex column>
<HelpText>
Need to add someone whos not yet on the team yet?{" "}
{t("Need to add someone whos not yet on the team yet?")}{" "}
<a role="button" onClick={this.handleInviteModalOpen}>
Invite people to {team.name}
{t("Invite people to {{ teamName }}", { teamName: team.name })}
</a>
.
</HelpText>
<Input
type="search"
placeholder="Search by name"
placeholder={`${t("Search by name")}`}
value={this.query}
onChange={this.handleFilter}
label="Search people"
label={t("Search people")}
autoFocus
labelHidden
flex
@@ -91,9 +97,9 @@ class AddPeopleToCollection extends React.Component<Props> {
<PaginatedList
empty={
this.query ? (
<Empty>No people matching your search</Empty>
<Empty>{t("No people matching your search")}</Empty>
) : (
<Empty>No people left to add</Empty>
<Empty>{t("No people left to add")}</Empty>
)
}
items={users.notInCollection(collection.id, this.query)}
@@ -108,7 +114,7 @@ class AddPeopleToCollection extends React.Component<Props> {
)}
/>
<Modal
title="Invite people"
title={t("Invite people")}
onRequestClose={this.handleInviteModalClose}
isOpen={this.inviteModalOpen}
>
@@ -119,9 +125,6 @@ class AddPeopleToCollection extends React.Component<Props> {
}
}
export default inject(
"auth",
"users",
"memberships",
"ui"
)(AddPeopleToCollection);
export default withTranslation()<AddPeopleToCollection>(
inject("auth", "users", "memberships", "ui")(AddPeopleToCollection)
);
@@ -1,5 +1,6 @@
// @flow
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import CollectionGroupMembership from "models/CollectionGroupMembership";
import Group from "models/Group";
@@ -7,10 +8,6 @@ import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
import GroupListItem from "components/GroupListItem";
import InputSelect from "components/InputSelect";
const PERMISSIONS = [
{ label: "Read only", value: "read" },
{ label: "Read & Edit", value: "read_write" },
];
type Props = {
group: Group,
collectionGroupMembership: ?CollectionGroupMembership,
@@ -24,6 +21,16 @@ const MemberListItem = ({
onUpdate,
onRemove,
}: Props) => {
const { t } = useTranslation();
const PERMISSIONS = React.useMemo(
() => [
{ label: t("Read only"), value: "read" },
{ label: t("Read & Edit"), value: "read_write" },
],
[t]
);
return (
<GroupListItem
group={group}
@@ -32,7 +39,7 @@ const MemberListItem = ({
renderActions={({ openMembersModal }) => (
<>
<Select
label="Permissions"
label={t("Permissions")}
options={PERMISSIONS}
value={
collectionGroupMembership
@@ -45,10 +52,12 @@ const MemberListItem = ({
<ButtonWrap>
<DropdownMenu>
<DropdownMenuItem onClick={openMembersModal}>
Members
{t("Members")}
</DropdownMenuItem>
<hr />
<DropdownMenuItem onClick={onRemove}>Remove</DropdownMenuItem>
<DropdownMenuItem onClick={onRemove}>
{t("Remove")}
</DropdownMenuItem>
</DropdownMenu>
</ButtonWrap>
</>
@@ -1,5 +1,6 @@
// @flow
import * as React from "react";
import { Trans, useTranslation } from "react-i18next";
import styled from "styled-components";
import Membership from "models/Membership";
import User from "models/User";
@@ -12,10 +13,6 @@ import InputSelect from "components/InputSelect";
import ListItem from "components/List/Item";
import Time from "components/Time";
const PERMISSIONS = [
{ label: "Read only", value: "read" },
{ label: "Read & Edit", value: "read_write" },
];
type Props = {
user: User,
membership?: ?Membership,
@@ -33,20 +30,30 @@ const MemberListItem = ({
onAdd,
canEdit,
}: Props) => {
const { t } = useTranslation();
const PERMISSIONS = React.useMemo(
() => [
{ label: t("Read only"), value: "read" },
{ label: t("Read & Edit"), value: "read_write" },
],
[t]
);
return (
<ListItem
title={user.name}
subtitle={
<>
{user.lastActiveAt ? (
<>
<Trans>
Active <Time dateTime={user.lastActiveAt} /> ago
</>
</Trans>
) : (
"Never signed in"
t("Never signed in")
)}
{user.isInvited && <Badge>Invited</Badge>}
{user.isAdmin && <Badge primary={user.isAdmin}>Admin</Badge>}
{user.isInvited && <Badge>{t("Invited")}</Badge>}
{user.isAdmin && <Badge primary={user.isAdmin}>{t("Admin")}</Badge>}
</>
}
image={<Avatar src={user.avatarUrl} size={40} />}
@@ -54,7 +61,7 @@ const MemberListItem = ({
<Flex align="center">
{canEdit && onUpdate && (
<Select
label="Permissions"
label={t("Permissions")}
options={PERMISSIONS}
value={membership ? membership.permission : undefined}
onChange={(ev) => onUpdate(ev.target.value)}
@@ -64,12 +71,14 @@ const MemberListItem = ({
&nbsp;&nbsp;
{canEdit && onRemove && (
<DropdownMenu>
<DropdownMenuItem onClick={onRemove}>Remove</DropdownMenuItem>
<DropdownMenuItem onClick={onRemove}>
{t("Remove")}
</DropdownMenuItem>
</DropdownMenu>
)}
{canEdit && onAdd && (
<Button onClick={onAdd} neutral>
Add
{t("Add")}
</Button>
)}
</Flex>
@@ -1,6 +1,7 @@
// @flow
import { PlusIcon } from "outline-icons";
import * as React from "react";
import { Trans, useTranslation } from "react-i18next";
import User from "models/User";
import Avatar from "components/Avatar";
import Badge from "components/Badge";
@@ -15,6 +16,8 @@ type Props = {
};
const UserListItem = ({ user, onAdd, canEdit }: Props) => {
const { t } = useTranslation();
return (
<ListItem
title={user.name}
@@ -22,20 +25,20 @@ const UserListItem = ({ user, onAdd, canEdit }: Props) => {
subtitle={
<>
{user.lastActiveAt ? (
<>
<Trans>
Active <Time dateTime={user.lastActiveAt} /> ago
</>
</Trans>
) : (
"Never signed in"
t("Never signed in")
)}
{user.isInvited && <Badge>Invited</Badge>}
{user.isAdmin && <Badge primary={user.isAdmin}>Admin</Badge>}
{user.isInvited && <Badge>{t("Invited")}</Badge>}
{user.isAdmin && <Badge primary={user.isAdmin}>{t("Admin")}</Badge>}
</>
}
actions={
canEdit ? (
<Button type="button" onClick={onAdd} icon={<PlusIcon />} neutral>
Add
{t("Add")}
</Button>
) : undefined
}
+19 -11
View File
@@ -3,6 +3,7 @@ import { intersection } from "lodash";
import { observable } from "mobx";
import { inject, observer } from "mobx-react";
import * as React from "react";
import { withTranslation, type TFunction } from "react-i18next";
import { withRouter, type RouterHistory } from "react-router-dom";
import CollectionsStore from "stores/CollectionsStore";
import UiStore from "stores/UiStore";
@@ -20,6 +21,7 @@ type Props = {
ui: UiStore,
collections: CollectionsStore,
onSubmit: () => void,
t: TFunction,
};
@observer
@@ -84,7 +86,7 @@ class CollectionNew extends React.Component<Props> {
this.hasOpenedIconPicker = true;
};
handleDescriptionChange = (getValue) => {
handleDescriptionChange = (getValue: () => string) => {
this.description = getValue();
};
@@ -98,17 +100,19 @@ class CollectionNew extends React.Component<Props> {
};
render() {
const { t } = this.props;
return (
<form onSubmit={this.handleSubmit}>
<HelpText>
Collections are for grouping your knowledge base. They work best when
organized around a topic or internal team Product or Engineering for
example.
{t(
"Collections are for grouping your knowledge base. They work best when organized around a topic or internal team — Product or Engineering for example."
)}
</HelpText>
<Flex>
<Input
type="text"
label="Name"
label={t("Name")}
onChange={this.handleNameChange}
value={this.name}
required
@@ -124,29 +128,33 @@ class CollectionNew extends React.Component<Props> {
/>
</Flex>
<InputRich
label="Description"
label={t("Description")}
onChange={this.handleDescriptionChange}
defaultValue={this.description || ""}
placeholder="More details about this collection…"
placeholder={t("More details about this collection…")}
minHeight={68}
maxHeight={200}
/>
<Switch
id="private"
label="Private collection"
label={t("Private collection")}
onChange={this.handlePrivateChange}
checked={this.private}
/>
<HelpText>
A private collection will only be visible to invited team members.
{t(
"A private collection will only be visible to invited team members."
)}
</HelpText>
<Button type="submit" disabled={this.isSaving || !this.name}>
{this.isSaving ? "Creating" : "Create"}
{this.isSaving ? `${t("Creating")}` : t("Create")}
</Button>
</form>
);
}
}
export default inject("collections", "ui")(withRouter(CollectionNew));
export default withTranslation()<CollectionNew>(
inject("collections", "ui")(withRouter(CollectionNew))
);
+60 -64
View File
@@ -1,81 +1,77 @@
// @flow
import { observer, inject } from "mobx-react";
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { Switch, Route } from "react-router-dom";
import AuthStore from "stores/AuthStore";
import DocumentsStore from "stores/DocumentsStore";
import Actions, { Action } from "components/Actions";
import CenteredContent from "components/CenteredContent";
import InputSearch from "components/InputSearch";
import LanguagePrompt from "components/LanguagePrompt";
import PageTitle from "components/PageTitle";
import Tab from "components/Tab";
import Tabs from "components/Tabs";
import PaginatedDocumentList from "../components/PaginatedDocumentList";
import useStores from "../hooks/useStores";
import NewDocumentMenu from "menus/NewDocumentMenu";
type Props = {
documents: DocumentsStore,
auth: AuthStore,
};
function Dashboard() {
const { documents, ui, auth } = useStores();
const { t } = useTranslation();
@observer
class Dashboard extends React.Component<Props> {
render() {
const { documents, auth } = this.props;
if (!auth.user || !auth.team) return null;
const user = auth.user.id;
if (!auth.user || !auth.team) return null;
const user = auth.user.id;
return (
<CenteredContent>
<PageTitle title="Home" />
<h1>Home</h1>
<Tabs>
<Tab to="/home" exact>
Recently updated
</Tab>
<Tab to="/home/recent" exact>
Recently viewed
</Tab>
<Tab to="/home/created">Created by me</Tab>
</Tabs>
<Switch>
<Route path="/home/recent">
<PaginatedDocumentList
key="recent"
documents={documents.recentlyViewed}
fetch={documents.fetchRecentlyViewed}
showCollection
/>
</Route>
<Route path="/home/created">
<PaginatedDocumentList
key="created"
documents={documents.createdByUser(user)}
fetch={documents.fetchOwned}
options={{ user }}
showCollection
/>
</Route>
<Route path="/home">
<PaginatedDocumentList
documents={documents.recentlyUpdated}
fetch={documents.fetchRecentlyUpdated}
showCollection
/>
</Route>
</Switch>
<Actions align="center" justify="flex-end">
<Action>
<InputSearch source="dashboard" />
</Action>
<Action>
<NewDocumentMenu />
</Action>
</Actions>
</CenteredContent>
);
}
return (
<CenteredContent>
<PageTitle title={t("Home")} />
{!ui.languagePromptDismissed && <LanguagePrompt />}
<h1>{t("Home")}</h1>
<Tabs>
<Tab to="/home" exact>
{t("Recently updated")}
</Tab>
<Tab to="/home/recent" exact>
{t("Recently viewed")}
</Tab>
<Tab to="/home/created">{t("Created by me")}</Tab>
</Tabs>
<Switch>
<Route path="/home/recent">
<PaginatedDocumentList
key="recent"
documents={documents.recentlyViewed}
fetch={documents.fetchRecentlyViewed}
showCollection
/>
</Route>
<Route path="/home/created">
<PaginatedDocumentList
key="created"
documents={documents.createdByUser(user)}
fetch={documents.fetchOwned}
options={{ user }}
showCollection
/>
</Route>
<Route path="/home">
<PaginatedDocumentList
documents={documents.recentlyUpdated}
fetch={documents.fetchRecentlyUpdated}
showCollection
/>
</Route>
</Switch>
<Actions align="center" justify="flex-end">
<Action>
<InputSearch source="dashboard" />
</Action>
<Action>
<NewDocumentMenu />
</Action>
</Actions>
</CenteredContent>
);
}
export default inject("documents", "auth")(Dashboard);
export default observer(Dashboard);
+6 -1
View File
@@ -54,7 +54,7 @@ class DocumentEditor extends React.Component<Props> {
if (event.key === "Enter") {
event.preventDefault();
if (event.metaKey) {
this.props.onSave({ publish: true, done: true });
this.props.onSave({ done: true });
return;
}
@@ -67,6 +67,11 @@ class DocumentEditor extends React.Component<Props> {
this.focusAtStart();
return;
}
if (event.key === "p" && event.metaKey && event.shiftKey) {
event.preventDefault();
this.props.onSave({ publish: true, done: true });
return;
}
if (event.key === "s" && event.metaKey) {
event.preventDefault();
this.props.onSave({});
+26 -18
View File
@@ -11,6 +11,7 @@ import {
} from "outline-icons";
import { transparentize, darken } from "polished";
import * as React from "react";
import { withTranslation, Trans, type TFunction } from "react-i18next";
import { Redirect } from "react-router-dom";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
@@ -55,6 +56,7 @@ type Props = {
publish?: boolean,
autosave?: boolean,
}) => void,
t: TFunction,
};
@observer
@@ -131,6 +133,7 @@ class Header extends React.Component<Props> {
publishingIsDisabled,
ui,
auth,
t,
} = this.props;
const share = shares.getByDocumentId(document.id);
@@ -153,7 +156,7 @@ class Header extends React.Component<Props> {
<Modal
isOpen={this.showShareModal}
onRequestClose={this.handleCloseShareModal}
title="Share document"
title={t("Share document")}
>
<DocumentShare
document={document}
@@ -166,7 +169,9 @@ class Header extends React.Component<Props> {
<>
<Slash />
<Tooltip
tooltip={ui.tocVisible ? "Hide contents" : "Show contents"}
tooltip={
ui.tocVisible ? t("Hide contents") : t("Show contents")
}
shortcut={`ctrl+${meta}+h`}
delay={250}
placement="bottom"
@@ -190,14 +195,15 @@ class Header extends React.Component<Props> {
{this.isScrolled && (
<Title onClick={this.handleClickTitle}>
<Fade>
{document.title} {document.isArchived && <Badge>Archived</Badge>}
{document.title}{" "}
{document.isArchived && <Badge>{t("Archived")}</Badge>}
</Fade>
</Title>
)}
<Wrapper align="center" justify="flex-end">
{isSaving && !isPublishing && (
<Action>
<Status>Saving</Status>
<Status>{t("Saving")}</Status>
</Action>
)}
&nbsp;
@@ -217,10 +223,10 @@ class Header extends React.Component<Props> {
<Tooltip
tooltip={
isPubliclyShared ? (
<>
<Trans>
Anyone with the link <br />
can view this document
</>
</Trans>
) : (
""
)
@@ -234,7 +240,7 @@ class Header extends React.Component<Props> {
neutral
small
>
Share
{t("Share")}
</Button>
</Tooltip>
</Action>
@@ -243,7 +249,7 @@ class Header extends React.Component<Props> {
<>
<Action>
<Tooltip
tooltip="Save"
tooltip={t("Save")}
shortcut={`${meta}+enter`}
delay={500}
placement="bottom"
@@ -255,7 +261,7 @@ class Header extends React.Component<Props> {
neutral={isDraft}
small
>
{isDraft ? "Save Draft" : "Done Editing"}
{isDraft ? t("Save Draft") : t("Done Editing")}
</Button>
</Tooltip>
</Action>
@@ -264,7 +270,7 @@ class Header extends React.Component<Props> {
{canEdit && (
<Action>
<Tooltip
tooltip={`Edit ${document.noun}`}
tooltip={t("Edit {{noun}}", { noun: document.noun })}
shortcut="e"
delay={500}
placement="bottom"
@@ -275,7 +281,7 @@ class Header extends React.Component<Props> {
neutral
small
>
Edit
{t("Edit")}
</Button>
</Tooltip>
</Action>
@@ -286,13 +292,13 @@ class Header extends React.Component<Props> {
document={document}
label={
<Tooltip
tooltip="New document"
tooltip={t("New document")}
shortcut="n"
delay={500}
placement="bottom"
>
<Button icon={<PlusIcon />} neutral>
New doc
{t("New doc")}
</Button>
</Tooltip>
}
@@ -307,25 +313,25 @@ class Header extends React.Component<Props> {
primary
small
>
New from template
{t("New from template")}
</Button>
</Action>
)}
{can.update && isDraft && !isRevision && (
<Action>
<Tooltip
tooltip="Publish"
tooltip={t("Publish")}
shortcut={`${meta}+shift+p`}
delay={500}
placement="bottom"
>
<Button
onClick={this.handlePublish}
title="Publish document"
title={t("Publish document")}
disabled={publishingIsDisabled}
small
>
{isPublishing ? "Publishing" : "Publish"}
{isPublishing ? `${t("Publishing")}` : t("Publish")}
</Button>
</Tooltip>
</Action>
@@ -425,4 +431,6 @@ const Title = styled.div`
`};
`;
export default inject("auth", "ui", "policies", "shares")(Header);
export default withTranslation()<Header>(
inject("auth", "ui", "policies", "shares")(Header)
);
+6 -1
View File
@@ -1,5 +1,6 @@
// @flow
import * as React from "react";
import { useTranslation } from "react-i18next";
import CenteredContent from "components/CenteredContent";
import LoadingPlaceholder from "components/LoadingPlaceholder";
import PageTitle from "components/PageTitle";
@@ -11,9 +12,13 @@ type Props = {|
|};
export default function Loading({ location }: Props) {
const { t } = useTranslation();
return (
<Container column auto>
<PageTitle title={location.state ? location.state.title : "Untitled"} />
<PageTitle
title={location.state ? location.state.title : t("Untitled")}
/>
<CenteredContent>
<LoadingPlaceholder />
</CenteredContent>
@@ -16,8 +16,9 @@ class MarkAsViewed extends React.Component<Props> {
const { document } = this.props;
this.viewTimeout = setTimeout(async () => {
if (document.publishedAt) {
const view = await document.view();
const view = await document.view();
if (view) {
document.updateLastViewed(view);
}
}, MARK_AS_VIEWED_AFTER);
+14 -8
View File
@@ -3,6 +3,7 @@ import { observable } from "mobx";
import { inject, observer } from "mobx-react";
import queryString from "query-string";
import * as React from "react";
import { withTranslation, type TFunction } from "react-i18next";
import { type RouterHistory } from "react-router-dom";
import styled from "styled-components";
import DocumentsStore from "stores/DocumentsStore";
@@ -25,6 +26,7 @@ type Props = {|
documents: DocumentsStore,
history: RouterHistory,
location: LocationWithState,
t: TFunction,
|};
@observer
@@ -33,7 +35,7 @@ class Drafts extends React.Component<Props> {
this.props.location.search
);
componentDidUpdate(prevProps) {
componentDidUpdate(prevProps: Props) {
if (prevProps.location.search !== this.props.location.search) {
this.handleQueryChange();
}
@@ -43,7 +45,10 @@ class Drafts extends React.Component<Props> {
this.params = new URLSearchParams(this.props.location.search);
};
handleFilterChange = (search) => {
handleFilterChange = (search: {
dateFilter?: ?string,
collectionId?: ?string,
}) => {
this.props.history.replace({
pathname: this.props.location.pathname,
search: queryString.stringify({
@@ -64,6 +69,7 @@ class Drafts extends React.Component<Props> {
}
render() {
const { t } = this.props;
const { drafts, fetchDrafts } = this.props.documents;
const isFiltered = this.collectionId || this.dateFilter;
const options = {
@@ -73,10 +79,10 @@ class Drafts extends React.Component<Props> {
return (
<CenteredContent column auto>
<PageTitle title="Drafts" />
<Heading>Drafts</Heading>
<PageTitle title={t("Drafts")} />
<Heading>{t("Drafts")}</Heading>
<Subheading>
Documents
{t("Documents")}
<Filters>
<CollectionFilter
collectionId={this.collectionId}
@@ -95,8 +101,8 @@ class Drafts extends React.Component<Props> {
empty={
<Empty>
{isFiltered
? "No documents found for your filters."
: "Youve not got any drafts at the moment."}
? t("No documents found for your filters.")
: t("Youve not got any drafts at the moment.")}
</Empty>
}
fetch={fetchDrafts}
@@ -131,4 +137,4 @@ const Filters = styled(Flex)`
}
`;
export default inject("documents")(Drafts);
export default withTranslation()<Drafts>(inject("documents")(Drafts));
+9 -4
View File
@@ -1,18 +1,23 @@
// @flow
import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
import { Link } from "react-router-dom";
import CenteredContent from "components/CenteredContent";
import Empty from "components/Empty";
import PageTitle from "components/PageTitle";
const Error404 = () => {
const { t } = useTranslation();
return (
<CenteredContent>
<PageTitle title="Not Found" />
<h1>Not found</h1>
<PageTitle title={t("Not found")} />
<h1>{t("Not found")}</h1>
<Empty>
We were unable to find the page youre looking for. Go to the{" "}
<Link to="/home">homepage</Link>?
<Trans>
We were unable to find the page youre looking for. Go to the{" "}
<Link to="/home">homepage</Link>?
</Trans>
</Empty>
</CenteredContent>
);
+6 -3
View File
@@ -1,15 +1,18 @@
// @flow
import * as React from "react";
import { useTranslation } from "react-i18next";
import CenteredContent from "components/CenteredContent";
import Empty from "components/Empty";
import PageTitle from "components/PageTitle";
const ErrorOffline = () => {
const { t } = useTranslation();
return (
<CenteredContent>
<PageTitle title="Offline" />
<h1>Offline</h1>
<Empty>We were unable to load the document while offline.</Empty>
<PageTitle title={t("Offline")} />
<h1>{t("Offline")}</h1>
<Empty>{t("We were unable to load the document while offline.")}</Empty>
</CenteredContent>
);
};
+16 -8
View File
@@ -1,29 +1,37 @@
// @flow
import { inject, observer } from "mobx-react";
import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
import AuthStore from "stores/AuthStore";
import CenteredContent from "components/CenteredContent";
import PageTitle from "components/PageTitle";
const ErrorSuspended = observer(({ auth }: { auth: AuthStore }) => {
const ErrorSuspended = ({ auth }: { auth: AuthStore }) => {
const { t } = useTranslation();
return (
<CenteredContent>
<PageTitle title="Your account has been suspended" />
<PageTitle title={t("Your account has been suspended")} />
<h1>
<span role="img" aria-label="Warning sign">
</span>{" "}
Your account has been suspended
{t("Your account has been suspended")}
</h1>
<p>
A team admin (<strong>{auth.suspendedContactEmail}</strong>) has
suspended your account. To re-activate your account, please reach out to
them directly.
<Trans>
A team admin (
<strong>
{{ suspendedContactEmail: auth.suspendedContactEmail }}
</strong>
) has suspended your account. To re-activate your account, please
reach out to them directly.
</Trans>
</p>
</CenteredContent>
);
});
};
export default inject("auth")(ErrorSuspended);
export default inject("auth")(observer(ErrorSuspended));
+23 -18
View File
@@ -3,11 +3,13 @@ import { debounce } from "lodash";
import { observable } from "mobx";
import { inject, observer } from "mobx-react";
import * as React from "react";
import { withTranslation, type TFunction } from "react-i18next";
import AuthStore from "stores/AuthStore";
import GroupMembershipsStore from "stores/GroupMembershipsStore";
import UiStore from "stores/UiStore";
import UsersStore from "stores/UsersStore";
import Group from "models/Group";
import User from "models/User";
import Invite from "scenes/Invite";
import Empty from "components/Empty";
import Flex from "components/Flex";
@@ -24,6 +26,7 @@ type Props = {
groupMemberships: GroupMembershipsStore,
users: UsersStore,
onSubmit: () => void,
t: TFunction,
};
@observer
@@ -50,40 +53,45 @@ class AddPeopleToGroup extends React.Component<Props> {
});
}, 250);
handleAddUser = async (user) => {
handleAddUser = async (user: User) => {
const { t } = this.props;
try {
await this.props.groupMemberships.create({
groupId: this.props.group.id,
userId: user.id,
});
this.props.ui.showToast(`${user.name} was added to the group`);
this.props.ui.showToast(
t(`{{userName}} was added to the group`, { userName: user.name })
);
} catch (err) {
this.props.ui.showToast("Could not add user");
this.props.ui.showToast(t("Could not add user"));
}
};
render() {
const { users, group, auth } = this.props;
const { users, group, auth, t } = this.props;
const { user, team } = auth;
if (!user || !team) return null;
return (
<Flex column>
<HelpText>
Add team members below to give them access to the group. Need to add
someone whos not yet on the team yet?{" "}
{t(
"Add team members below to give them access to the group. Need to add someone whos not yet on the team yet?"
)}{" "}
<a role="button" onClick={this.handleInviteModalOpen}>
Invite them to {team.name}
{t("Invite them to {{teamName}}", { teamName: team.name })}
</a>
.
</HelpText>
<Input
type="search"
placeholder="Search by name"
placeholder={`${t("Search by name")}`}
value={this.query}
onChange={this.handleFilter}
label="Search people"
label={t("Search people")}
labelHidden
autoFocus
flex
@@ -91,9 +99,9 @@ class AddPeopleToGroup extends React.Component<Props> {
<PaginatedList
empty={
this.query ? (
<Empty>No people matching your search</Empty>
<Empty>{t("No people matching your search")}</Empty>
) : (
<Empty>No people left to add</Empty>
<Empty>{t("No people left to add")}</Empty>
)
}
items={users.notInGroup(group.id, this.query)}
@@ -108,7 +116,7 @@ class AddPeopleToGroup extends React.Component<Props> {
)}
/>
<Modal
title="Invite people"
title={t("Invite people")}
onRequestClose={this.handleInviteModalClose}
isOpen={this.inviteModalOpen}
>
@@ -119,9 +127,6 @@ class AddPeopleToGroup extends React.Component<Props> {
}
}
export default inject(
"auth",
"users",
"groupMemberships",
"ui"
)(AddPeopleToGroup);
export default withTranslation()<AddPeopleToGroup>(
inject("auth", "users", "groupMemberships", "ui")(AddPeopleToGroup)
);
+16 -13
View File
@@ -3,12 +3,14 @@ import { observable } from "mobx";
import { inject, observer } from "mobx-react";
import { PlusIcon } from "outline-icons";
import * as React from "react";
import { withTranslation, type TFunction } from "react-i18next";
import AuthStore from "stores/AuthStore";
import GroupMembershipsStore from "stores/GroupMembershipsStore";
import PoliciesStore from "stores/PoliciesStore";
import UiStore from "stores/UiStore";
import UsersStore from "stores/UsersStore";
import Group from "models/Group";
import User from "models/User";
import Button from "components/Button";
import Empty from "components/Empty";
import Flex from "components/Flex";
@@ -26,6 +28,7 @@ type Props = {
users: UsersStore,
policies: PoliciesStore,
groupMemberships: GroupMembershipsStore,
t: TFunction,
};
@observer
@@ -40,20 +43,24 @@ class GroupMembers extends React.Component<Props> {
this.addModalOpen = false;
};
handleRemoveUser = async (user) => {
handleRemoveUser = async (user: User) => {
const { t } = this.props;
try {
await this.props.groupMemberships.delete({
groupId: this.props.group.id,
userId: user.id,
});
this.props.ui.showToast(`${user.name} was removed from the group`);
this.props.ui.showToast(
t(`{{userName}} was removed from the group`, { userName: user.name })
);
} catch (err) {
this.props.ui.showToast("Could not remove user");
this.props.ui.showToast(t("Could not remove user"));
}
};
render() {
const { group, users, groupMemberships, policies, auth } = this.props;
const { group, users, groupMemberships, policies, t, auth } = this.props;
const { user } = auth;
if (!user) return null;
@@ -75,7 +82,7 @@ class GroupMembers extends React.Component<Props> {
icon={<PlusIcon />}
neutral
>
Add people
{t("Add people")}
</Button>
</span>
</>
@@ -90,7 +97,7 @@ class GroupMembers extends React.Component<Props> {
items={users.inGroup(group.id)}
fetch={groupMemberships.fetchPage}
options={{ id: group.id }}
empty={<Empty>This group has no members.</Empty>}
empty={<Empty>{t("This group has no members.")}</Empty>}
renderItem={(item) => (
<GroupMemberListItem
key={item.id}
@@ -119,10 +126,6 @@ class GroupMembers extends React.Component<Props> {
}
}
export default inject(
"auth",
"users",
"policies",
"groupMemberships",
"ui"
)(GroupMembers);
export default withTranslation()<GroupMembers>(
inject("auth", "users", "policies", "groupMemberships", "ui")(GroupMembers)
);
+42 -44
View File
@@ -1,5 +1,6 @@
// @flow
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import Flex from "components/Flex";
import HelpText from "components/HelpText";
@@ -7,153 +8,150 @@ import Key from "components/Key";
import { meta } from "utils/keyboard";
function KeyboardShortcuts() {
const { t } = useTranslation();
return (
<Flex column>
<HelpText>
Outline is designed to be fast and easy to use. All of your usual
keyboard shortcuts work here, and theres Markdown too.
{t(
"Outline is designed to be fast and easy to use. All of your usual keyboard shortcuts work here, and theres Markdown too."
)}
</HelpText>
<h2>Navigation</h2>
<h2>{t("Navigation")}</h2>
<List>
<Keys>
<Key>n</Key>
</Keys>
<Label>New document in current collection</Label>
<Label>{t("New document in current collection")}</Label>
<Keys>
<Key>e</Key>
</Keys>
<Label>Edit current document</Label>
<Label>{t("Edit current document")}</Label>
<Keys>
<Key>m</Key>
</Keys>
<Label>Move current document</Label>
<Label>{t("Move current document")}</Label>
<Keys>
<Key>/</Key> or <Key>t</Key>
</Keys>
<Label>Jump to search</Label>
<Label>{t("Jump to search")}</Label>
<Keys>
<Key>d</Key>
</Keys>
<Label>Jump to dashboard</Label>
<Label>{t("Jump to dashboard")}</Label>
<Keys>
<Key>{meta}</Key> + <Key>Ctrl</Key> + <Key>h</Key>
</Keys>
<Label>Table of contents</Label>
<Label>{t("Table of contents")}</Label>
<Keys>
<Key>?</Key>
</Keys>
<Label>Open this guide</Label>
<Label>{t("Open this guide")}</Label>
</List>
<h2>Editor</h2>
<h2>{t("Editor")}</h2>
<List>
<Keys>
<Key>{meta}</Key> + <Key>Enter</Key>
</Keys>
<Label>Save and exit document edit mode</Label>
<Label>{t("Save and exit document edit mode")}</Label>
<Keys>
<Key>{meta}</Key> + <Key>Shift</Key> + <Key>p</Key>
</Keys>
<Label>Publish and exit document edit mode</Label>
<Label>{t("Publish and exit document edit mode")}</Label>
<Keys>
<Key>{meta}</Key> + <Key>s</Key>
</Keys>
<Label>Save document and continue editing</Label>
<Label>{t("Save document and continue editing")}</Label>
<Keys>
<Key>{meta}</Key> + <Key>Esc</Key>
</Keys>
<Label>Cancel editing</Label>
<Label>{t("Cancel editing")}</Label>
<Keys>
<Key>{meta}</Key> + <Key>b</Key>
</Keys>
<Label>Bold</Label>
<Label>{t("Bold")}</Label>
<Keys>
<Key>{meta}</Key> + <Key>i</Key>
</Keys>
<Label>Italic</Label>
<Label>{t("Italic")}</Label>
<Keys>
<Key>{meta}</Key> + <Key>u</Key>
</Keys>
<Label>Underline</Label>
<Label>{t("Underline")}</Label>
<Keys>
<Key>{meta}</Key> + <Key>d</Key>
</Keys>
<Label>Strikethrough</Label>
<Label>{t("Strikethrough")}</Label>
<Keys>
<Key>{meta}</Key> + <Key>k</Key>
</Keys>
<Label>Link</Label>
<Label>{t("Link")}</Label>
<Keys>
<Key>{meta}</Key> + <Key>z</Key>
</Keys>
<Label>Undo</Label>
<Label>{t("Undo")}</Label>
<Keys>
<Key>{meta}</Key> + <Key>Shift</Key> + <Key>z</Key>
</Keys>
<Label>Redo</Label>
<Label>{t("Redo")}</Label>
</List>
<h2>Markdown</h2>
<h2>{t("Markdown")}</h2>
<List>
<Keys>
<Key>#</Key> <Key>Space</Key>
</Keys>
<Label>Large header</Label>
<Label>{t("Large header")}</Label>
<Keys>
<Key>##</Key> <Key>Space</Key>
</Keys>
<Label>Medium header</Label>
<Label>{t("Medium header")}</Label>
<Keys>
<Key>###</Key> <Key>Space</Key>
</Keys>
<Label>Small header</Label>
<Label>{t("Small header")}</Label>
<Keys>
<Key>1.</Key> <Key>Space</Key>
</Keys>
<Label>Numbered list</Label>
<Label>{t("Numbered list")}</Label>
<Keys>
<Key>-</Key> <Key>Space</Key>
</Keys>
<Label>Bulleted list</Label>
<Label>{t("Bulleted list")}</Label>
<Keys>
<Key>[ ]</Key> <Key>Space</Key>
</Keys>
<Label>Todo list</Label>
<Label>{t("Todo list")}</Label>
<Keys>
<Key>&gt;</Key> <Key>Space</Key>
</Keys>
<Label>Blockquote</Label>
<Label>{t("Blockquote")}</Label>
<Keys>
<Key>---</Key>
</Keys>
<Label>Horizontal divider</Label>
<Label>{t("Horizontal divider")}</Label>
<Keys>
<Key>{"```"}</Key>
</Keys>
<Label>Code block</Label>
<Label>{t("Code block")}</Label>
<Keys>
<Key>{":::"}</Key>
</Keys>
<Label>Info notice</Label>
<Label>{t("Info notice")}</Label>
<Keys>_italic_</Keys>
<Label>Italic</Label>
<Label>{t("Italic")}</Label>
<Keys>**bold**</Keys>
<Label>Bold</Label>
<Label>{t("Bold")}</Label>
<Keys>~~strikethrough~~</Keys>
<Label>Strikethrough</Label>
<Label>{t("Strikethrough")}</Label>
<Keys>{"`code`"}</Keys>
<Label>Inline code</Label>
<Label>{t("Inline code")}</Label>
<Keys>==highlight==</Keys>
<Label>highlight</Label>
<Label>{t("Highlight")}</Label>
</List>
</Flex>
);
+33 -18
View File
@@ -7,6 +7,7 @@ import { PlusIcon } from "outline-icons";
import queryString from "query-string";
import * as React from "react";
import ReactDOM from "react-dom";
import { withTranslation, Trans, type TFunction } from "react-i18next";
import keydown from "react-keydown";
import { withRouter, Link } from "react-router-dom";
import type { RouterHistory, Match } from "react-router-dom";
@@ -44,11 +45,12 @@ type Props = {
documents: DocumentsStore,
users: UsersStore,
notFound: ?boolean,
t: TFunction,
};
@observer
class Search extends React.Component<Props> {
firstDocument: ?React.Component<typeof DocumentPreview>;
firstDocument: ?React.Component<any>;
lastQuery: string = "";
@observable
@@ -67,7 +69,7 @@ class Search extends React.Component<Props> {
}
}
componentDidUpdate(prevProps) {
componentDidUpdate(prevProps: Props) {
if (prevProps.location.search !== this.props.location.search) {
this.handleQueryChange();
}
@@ -81,7 +83,7 @@ class Search extends React.Component<Props> {
this.props.history.goBack();
}
handleKeyDown = (ev) => {
handleKeyDown = (ev: SyntheticKeyboardEvent<>) => {
if (ev.key === "Enter") {
this.fetchResults();
return;
@@ -124,7 +126,12 @@ class Search extends React.Component<Props> {
this.fetchResultsDebounced();
};
handleFilterChange = (search) => {
handleFilterChange = (search: {
collectionId?: ?string,
userId?: ?string,
dateFilter?: ?string,
includeArchived?: ?string,
}) => {
this.props.history.replace({
pathname: this.props.location.pathname,
search: queryString.stringify({
@@ -170,7 +177,7 @@ class Search extends React.Component<Props> {
get title() {
const query = this.query;
const title = "Search";
const title = this.props.t("Search");
if (query) return `${query} ${title}`;
return title;
}
@@ -231,20 +238,19 @@ class Search extends React.Component<Props> {
trailing: true,
});
updateLocation = (query) => {
updateLocation = (query: string) => {
this.props.history.replace({
pathname: searchUrl(query),
search: this.props.location.search,
});
};
setFirstDocumentRef = (ref) => {
// $FlowFixMe
setFirstDocumentRef = (ref: any) => {
this.firstDocument = ref;
};
render() {
const { documents, notFound, location } = this.props;
const { documents, notFound, location, t } = this.props;
const results = documents.searchResults(this.query);
const showEmpty = !this.isLoading && this.query && results.length === 0;
const showShortcutTip =
@@ -256,12 +262,15 @@ class Search extends React.Component<Props> {
{this.isLoading && <LoadingIndicator />}
{notFound && (
<div>
<h1>Not Found</h1>
<Empty>We were unable to find the page youre looking for.</Empty>
<h1>{t("Not Found")}</h1>
<Empty>
{t("We were unable to find the page youre looking for.")}
</Empty>
</div>
)}
<ResultsWrapper pinToTop={this.pinToTop} column auto>
<SearchField
placeholder={`${t("Search")}`}
onKeyDown={this.handleKeyDown}
onChange={this.updateLocation}
defaultValue={this.query}
@@ -269,8 +278,10 @@ class Search extends React.Component<Props> {
{showShortcutTip && (
<Fade>
<HelpText small>
Use the <strong>{meta}+K</strong> shortcut to search from
anywhere in Outline
<Trans>
Use the <strong>{{ meta }}+K</strong> shortcut to search from
anywhere in your knowledge base
</Trans>
</HelpText>
</Fade>
)}
@@ -304,8 +315,10 @@ class Search extends React.Component<Props> {
<Fade>
<Centered column>
<HelpText>
No documents found for your search filters. <br />
Create a new document?
<Trans>
No documents found for your search filters. <br />
Create a new document?
</Trans>
</HelpText>
<Wrapper>
{this.collectionId ? (
@@ -314,14 +327,14 @@ class Search extends React.Component<Props> {
icon={<PlusIcon />}
primary
>
New doc
{t("New doc")}
</Button>
) : (
<NewDocumentMenu />
)}
&nbsp;&nbsp;
<Button as={Link} to="/search" neutral>
Clear filters
{t("Clear filters")}
</Button>
</Wrapper>
</Centered>
@@ -414,4 +427,6 @@ const Filters = styled(Flex)`
}
`;
export default withRouter(inject("documents")(Search));
export default withTranslation()<Search>(
withRouter(inject("documents")(Search))
);
+2 -1
View File
@@ -8,6 +8,7 @@ import { type Theme } from "types";
type Props = {
onChange: (string) => void,
defaultValue?: string,
placeholder?: string,
theme: Theme,
};
@@ -44,7 +45,7 @@ class SearchField extends React.Component<Props> {
ref={(ref) => (this.input = ref)}
onChange={this.handleChange}
spellCheck="false"
placeholder="Search…"
placeholder={this.props.placeholder}
type="search"
autoFocus
/>
+55 -14
View File
@@ -2,7 +2,9 @@
import { observable } from "mobx";
import { observer, inject } from "mobx-react";
import * as React from "react";
import { Trans, withTranslation, type TFunction } from "react-i18next";
import styled from "styled-components";
import { languageOptions } from "shared/i18n";
import AuthStore from "stores/AuthStore";
import UiStore from "stores/UiStore";
@@ -10,13 +12,16 @@ import UserDelete from "scenes/UserDelete";
import Button from "components/Button";
import CenteredContent from "components/CenteredContent";
import Flex from "components/Flex";
import HelpText from "components/HelpText";
import Input, { LabelText } from "components/Input";
import InputSelect from "components/InputSelect";
import PageTitle from "components/PageTitle";
import ImageUpload from "./components/ImageUpload";
type Props = {
auth: AuthStore,
ui: UiStore,
t: TFunction,
};
@observer
@@ -27,10 +32,12 @@ class Profile extends React.Component<Props> {
@observable name: string;
@observable avatarUrl: ?string;
@observable showDeleteModal: boolean = false;
@observable language: string;
componentDidMount() {
if (this.props.auth.user) {
this.name = this.props.auth.user.name;
this.language = this.props.auth.user.language;
}
}
@@ -39,13 +46,16 @@ class Profile extends React.Component<Props> {
}
handleSubmit = async (ev: SyntheticEvent<>) => {
const { t } = this.props;
ev.preventDefault();
await this.props.auth.updateUser({
name: this.name,
avatarUrl: this.avatarUrl,
language: this.language,
});
this.props.ui.showToast("Profile saved");
this.props.ui.showToast(t("Profile saved"));
};
handleNameChange = (ev: SyntheticInputEvent<*>) => {
@@ -53,16 +63,22 @@ class Profile extends React.Component<Props> {
};
handleAvatarUpload = async (avatarUrl: string) => {
const { t } = this.props;
this.avatarUrl = avatarUrl;
await this.props.auth.updateUser({
avatarUrl: this.avatarUrl,
});
this.props.ui.showToast("Profile picture updated");
this.props.ui.showToast(t("Profile picture updated"));
};
handleAvatarError = (error: ?string) => {
this.props.ui.showToast(error || "Unable to upload new avatar");
const { t } = this.props;
this.props.ui.showToast(error || t("Unable to upload new profile picture"));
};
handleLanguageChange = (ev: SyntheticInputEvent<*>) => {
this.language = ev.target.value;
};
toggleDeleteAccount = () => {
@@ -74,16 +90,17 @@ class Profile extends React.Component<Props> {
}
render() {
const { t } = this.props;
const { user, isSaving } = this.props.auth;
if (!user) return null;
const avatarUrl = this.avatarUrl || user.avatarUrl;
return (
<CenteredContent>
<PageTitle title="Profile" />
<h1>Profile</h1>
<PageTitle title={t("Profile")} />
<h1>{t("Profile")}</h1>
<ProfilePicture column>
<LabelText>Photo</LabelText>
<LabelText>{t("Photo")}</LabelText>
<AvatarContainer>
<ImageUpload
onSuccess={this.handleAvatarUpload}
@@ -91,31 +108,55 @@ class Profile extends React.Component<Props> {
>
<Avatar src={avatarUrl} />
<Flex auto align="center" justify="center">
Upload
{t("Upload")}
</Flex>
</ImageUpload>
</AvatarContainer>
</ProfilePicture>
<form onSubmit={this.handleSubmit} ref={(ref) => (this.form = ref)}>
<Input
label="Full name"
label={t("Full name")}
autoComplete="name"
value={this.name}
onChange={this.handleNameChange}
required
short
/>
<br />
<InputSelect
label={t("Language")}
options={languageOptions}
value={this.language}
onChange={this.handleLanguageChange}
short
/>
<HelpText small>
<Trans>
Please note that translations are currently in early access.
<br />
Community contributions are accepted though our{" "}
<a
href="https://translate.getoutline.com"
target="_blank"
rel="noreferrer"
>
translation portal
</a>
</Trans>
.
</HelpText>
<Button type="submit" disabled={isSaving || !this.isValid}>
{isSaving ? "Saving" : "Save"}
{isSaving ? `${t("Saving")}` : t("Save")}
</Button>
</form>
<DangerZone>
<LabelText>Delete Account</LabelText>
<LabelText>{t("Delete Account")}</LabelText>
<p>
You may delete your account at any time, note that this is
unrecoverable.{" "}
<a onClick={this.toggleDeleteAccount}>Delete account</a>.
{t(
"You may delete your account at any time, note that this is unrecoverable"
)}
. <a onClick={this.toggleDeleteAccount}>{t("Delete account")}</a>.
</p>
</DangerZone>
{this.showDeleteModal && (
@@ -170,4 +211,4 @@ const Avatar = styled.img`
${avatarStyles};
`;
export default inject("auth", "ui")(Profile);
export default withTranslation()<Profile>(inject("auth", "ui")(Profile));
+13 -3
View File
@@ -10,6 +10,7 @@ import Button from "components/Button";
import Flex from "components/Flex";
import LoadingIndicator from "components/LoadingIndicator";
import Modal from "components/Modal";
import { compressImage } from "utils/compressImage";
import { uploadFile, dataUrlToBlob } from "utils/uploadFile";
const EMPTY_OBJECT = {};
@@ -28,7 +29,7 @@ class ImageUpload extends React.Component<Props> {
@observable isUploading: boolean = false;
@observable isCropping: boolean = false;
@observable zoom: number = 1;
file: File;
@observable file: File;
avatarEditorRef: AvatarEditor;
static defaultProps = {
@@ -53,7 +54,11 @@ class ImageUpload extends React.Component<Props> {
const canvas = this.avatarEditorRef.getImage();
const imageBlob = dataUrlToBlob(canvas.toDataURL());
try {
const attachment = await uploadFile(imageBlob, {
const compressed = await compressImage(imageBlob, {
maxHeight: 512,
maxWidth: 512,
});
const attachment = await uploadFile(compressed, {
name: this.file.name,
public: true,
});
@@ -128,7 +133,12 @@ class ImageUpload extends React.Component<Props> {
style={EMPTY_OBJECT}
disablePreview
>
{this.props.children}
{({ getRootProps, getInputProps, isDragActive }) => (
<div {...getRootProps()} {...{ isDragActive }}>
<input {...getInputProps()} />
{this.props.children}
</div>
)}
</Dropzone>
);
}
+39 -41
View File
@@ -1,9 +1,8 @@
// @flow
import { observer, inject } from "mobx-react";
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { type Match } from "react-router-dom";
import DocumentsStore from "stores/DocumentsStore";
import Actions, { Action } from "components/Actions";
import CenteredContent from "components/CenteredContent";
import Empty from "components/Empty";
@@ -13,51 +12,50 @@ import PageTitle from "components/PageTitle";
import PaginatedDocumentList from "components/PaginatedDocumentList";
import Tab from "components/Tab";
import Tabs from "components/Tabs";
import useStores from "hooks/useStores";
import NewDocumentMenu from "menus/NewDocumentMenu";
type Props = {
documents: DocumentsStore,
match: Match,
};
@observer
class Starred extends React.Component<Props> {
render() {
const { fetchStarred, starred, starredAlphabetical } = this.props.documents;
const { sort } = this.props.match.params;
function Starred(props: Props) {
const { documents } = useStores();
const { t } = useTranslation();
const { fetchStarred, starred, starredAlphabetical } = documents;
const { sort } = props.match.params;
return (
<CenteredContent column auto>
<PageTitle title="Starred" />
<Heading>Starred</Heading>
<PaginatedDocumentList
heading={
<Tabs>
<Tab to="/starred" exact>
Recently Updated
</Tab>
<Tab to="/starred/alphabetical" exact>
Alphabetical
</Tab>
</Tabs>
}
empty={<Empty>Youve not starred any documents yet.</Empty>}
fetch={fetchStarred}
documents={sort === "alphabetical" ? starredAlphabetical : starred}
showCollection
/>
return (
<CenteredContent column auto>
<PageTitle title={t("Starred")} />
<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
/>
<Actions align="center" justify="flex-end">
<Action>
<InputSearch source="starred" />
</Action>
<Action>
<NewDocumentMenu />
</Action>
</Actions>
</CenteredContent>
);
}
<Actions align="center" justify="flex-end">
<Action>
<InputSearch source="starred" />
</Action>
<Action>
<NewDocumentMenu />
</Action>
</Actions>
</CenteredContent>
);
}
export default inject("documents")(Starred);
export default observer(Starred);
+43 -49
View File
@@ -1,9 +1,9 @@
// @flow
import { observer, inject } from "mobx-react";
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { type Match } from "react-router-dom";
import DocumentsStore from "stores/DocumentsStore";
import Actions, { Action } from "components/Actions";
import CenteredContent from "components/CenteredContent";
import Empty from "components/Empty";
@@ -12,60 +12,54 @@ import PageTitle from "components/PageTitle";
import PaginatedDocumentList from "components/PaginatedDocumentList";
import Tab from "components/Tab";
import Tabs from "components/Tabs";
import useStores from "hooks/useStores";
import NewTemplateMenu from "menus/NewTemplateMenu";
type Props = {
documents: DocumentsStore,
match: Match,
};
@observer
class Templates extends React.Component<Props> {
render() {
const {
fetchTemplates,
templates,
templatesAlphabetical,
} = this.props.documents;
const { sort } = this.props.match.params;
function Templates(props: Props) {
const { documents } = useStores();
const { t } = useTranslation();
const { fetchTemplates, templates, templatesAlphabetical } = documents;
const { sort } = props.match.params;
return (
<CenteredContent column auto>
<PageTitle title="Templates" />
<Heading>Templates</Heading>
<PaginatedDocumentList
heading={
<Tabs>
<Tab to="/templates" exact>
Recently Updated
</Tab>
<Tab to="/templates/alphabetical" exact>
Alphabetical
</Tab>
</Tabs>
}
empty={
<Empty>
There are no templates just yet. You can create templates to help
your team create consistent and accurate documentation.
</Empty>
}
fetch={fetchTemplates}
documents={
sort === "alphabetical" ? templatesAlphabetical : templates
}
showCollection
showDraft
/>
return (
<CenteredContent column auto>
<PageTitle title={t("Templates")} />
<Heading>{t("Templates")}</Heading>
<PaginatedDocumentList
heading={
<Tabs>
<Tab to="/templates" exact>
{t("Recently updated")}
</Tab>
<Tab to="/templates/alphabetical" exact>
{t("Alphabetical")}
</Tab>
</Tabs>
}
empty={
<Empty>
{t(
"There are no templates just yet. You can create templates to help your team create consistent and accurate documentation."
)}
</Empty>
}
fetch={fetchTemplates}
documents={sort === "alphabetical" ? templatesAlphabetical : templates}
showCollection
showDraft
/>
<Actions align="center" justify="flex-end">
<Action>
<NewTemplateMenu />
</Action>
</Actions>
</CenteredContent>
);
}
<Actions align="center" justify="flex-end">
<Action>
<NewTemplateMenu />
</Action>
</Actions>
</CenteredContent>
);
}
export default inject("documents")(Templates);
export default observer(Templates);
+21 -26
View File
@@ -1,39 +1,34 @@
// @flow
import { observer, inject } from "mobx-react";
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import DocumentsStore from "stores/DocumentsStore";
import CenteredContent from "components/CenteredContent";
import Empty from "components/Empty";
import Heading from "components/Heading";
import PageTitle from "components/PageTitle";
import PaginatedDocumentList from "components/PaginatedDocumentList";
import Subheading from "components/Subheading";
import useStores from "hooks/useStores";
type Props = {
documents: DocumentsStore,
};
function Trash() {
const { t } = useTranslation();
const { documents } = useStores();
@observer
class Trash extends React.Component<Props> {
render() {
const { documents } = this.props;
return (
<CenteredContent column auto>
<PageTitle title="Trash" />
<Heading>Trash</Heading>
<PaginatedDocumentList
documents={documents.deleted}
fetch={documents.fetchDeleted}
heading={<Subheading>Documents</Subheading>}
empty={<Empty>Trash is empty at the moment.</Empty>}
showCollection
showTemplate
/>
</CenteredContent>
);
}
return (
<CenteredContent column auto>
<PageTitle title={t("Trash")} />
<Heading>{t("Trash")}</Heading>
<PaginatedDocumentList
documents={documents.deleted}
fetch={documents.fetchDeleted}
heading={<Subheading>{t("Documents")}</Subheading>}
empty={<Empty>{t("Trash is empty at the moment.")}</Empty>}
showCollection
showTemplate
/>
</CenteredContent>
);
}
export default inject("documents")(Trash);
export default observer(Trash);
-1
View File
@@ -28,7 +28,6 @@ class UserDelete extends React.Component<Props> {
this.props.auth.logout();
} catch (error) {
this.props.ui.showToast(error.message);
throw error;
} finally {
this.isDeleting = false;
}
+65 -59
View File
@@ -1,13 +1,12 @@
// @flow
import distanceInWordsToNow from "date-fns/distance_in_words_to_now";
import { inject, observer } from "mobx-react";
import { observer } from "mobx-react";
import { EditIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { withRouter, type RouterHistory } from "react-router-dom";
import styled from "styled-components";
import { settings } from "shared/utils/routeHelpers";
import AuthStore from "stores/AuthStore";
import DocumentsStore from "stores/DocumentsStore";
import User from "models/User";
import Avatar from "components/Avatar";
import Badge from "components/Badge";
@@ -17,70 +16,77 @@ import HelpText from "components/HelpText";
import Modal from "components/Modal";
import PaginatedDocumentList from "components/PaginatedDocumentList";
import Subheading from "components/Subheading";
import useCurrentUser from "hooks/useCurrentUser";
import useStores from "hooks/useStores";
type Props = {
user: User,
auth: AuthStore,
documents: DocumentsStore,
history: RouterHistory,
onRequestClose: () => void,
};
@observer
class UserProfile extends React.Component<Props> {
render() {
const { user, auth, documents, ...rest } = this.props;
if (!user) return null;
const isCurrentUser = auth.user && auth.user.id === user.id;
function UserProfile(props: Props) {
const { t } = useTranslation();
const { documents } = useStores();
const currentUser = useCurrentUser();
const { user, ...rest } = props;
return (
<Modal
title={
<Flex align="center">
<Avatar src={user.avatarUrl} size={38} />
<span>&nbsp;{user.name}</span>
</Flex>
}
{...rest}
>
<Flex column>
<Meta>
{isCurrentUser
? "You joined"
: user.lastActiveAt
? "Joined"
: "Invited"}{" "}
{distanceInWordsToNow(new Date(user.createdAt))} ago.
{user.isAdmin && (
<StyledBadge admin={user.isAdmin}>Admin</StyledBadge>
)}
{user.isSuspended && <Badge>Suspended</Badge>}
{isCurrentUser && (
<Edit>
<Button
onClick={() => this.props.history.push(settings())}
icon={<EditIcon />}
neutral
>
Edit Profile
</Button>
</Edit>
)}
</Meta>
<PaginatedDocumentList
documents={documents.createdByUser(user.id)}
fetch={documents.fetchOwned}
options={{ user: user.id }}
heading={<Subheading>Recently updated</Subheading>}
empty={
<HelpText>{user.name} hasnt updated any documents yet.</HelpText>
}
showCollection
/>
if (!user) return null;
const isCurrentUser = currentUser.id === user.id;
return (
<Modal
title={
<Flex align="center">
<Avatar src={user.avatarUrl} size={38} />
<span>&nbsp;{user.name}</span>
</Flex>
</Modal>
);
}
}
{...rest}
>
<Flex column>
<Meta>
{isCurrentUser
? t("You joined")
: user.lastActiveAt
? t("Joined")
: t("Invited")}{" "}
{t("{{ time }} ago.", {
time: distanceInWordsToNow(new Date(user.createdAt)),
})}
{user.isAdmin && (
<StyledBadge admin={user.isAdmin}>{t("Admin")}</StyledBadge>
)}
{user.isSuspended && <Badge>{t("Suspended")}</Badge>}
{isCurrentUser && (
<Edit>
<Button
onClick={() => props.history.push(settings())}
icon={<EditIcon />}
neutral
>
{t("Edit Profile")}
</Button>
</Edit>
)}
</Meta>
<PaginatedDocumentList
documents={documents.createdByUser(user.id)}
fetch={documents.fetchOwned}
options={{ user: user.id }}
heading={<Subheading>{t("Recently updated")}</Subheading>}
empty={
<HelpText>
{t("{{ userName }} hasnt updated any documents yet.", {
userName: user.name,
})}
</HelpText>
}
showCollection
/>
</Flex>
</Modal>
);
}
const Edit = styled.span`
@@ -98,4 +104,4 @@ const Meta = styled(HelpText)`
margin-top: -12px;
`;
export default inject("documents", "auth")(withRouter(UserProfile));
export default withRouter(observer(UserProfile));
+19 -10
View File
@@ -19,6 +19,7 @@ export default class DocumentsStore extends BaseStore<Document> {
@observable searchCache: Map<string, SearchResult[]> = new Map();
@observable starredIds: Map<string, boolean> = new Map();
@observable backlinks: Map<string, string[]> = new Map();
@observable movingDocumentId: ?string;
importFileTypes: string[] = [
"text/markdown",
@@ -211,6 +212,7 @@ export default class DocumentsStore extends BaseStore<Document> {
const { data } = res;
runInAction("DocumentsStore#fetchBacklinks", () => {
data.forEach(this.add);
this.addPolicies(res.policies);
this.backlinks.set(
documentId,
data.map((doc) => doc.id)
@@ -236,6 +238,7 @@ export default class DocumentsStore extends BaseStore<Document> {
const { data } = res;
runInAction("DocumentsStore#fetchChildDocuments", () => {
data.forEach(this.add);
this.addPolicies(res.policies);
});
};
@@ -448,20 +451,26 @@ export default class DocumentsStore extends BaseStore<Document> {
@action
move = async (
document: Document,
documentId: string,
collectionId: string,
parentDocumentId: ?string
) => {
const res = await client.post("/documents.move", {
id: document.id,
collectionId,
parentDocumentId,
});
invariant(res && res.data, "Data not available");
this.movingDocumentId = documentId;
res.data.documents.forEach(this.add);
res.data.collections.forEach(this.rootStore.collections.add);
this.addPolicies(res.policies);
try {
const res = await client.post("/documents.move", {
id: documentId,
collectionId,
parentDocumentId,
});
invariant(res && res.data, "Data not available");
res.data.documents.forEach(this.add);
res.data.collections.forEach(this.rootStore.collections.add);
this.addPolicies(res.policies);
} finally {
this.movingDocumentId = undefined;
}
};
@action
+3 -1
View File
@@ -1,6 +1,6 @@
// @flow
import invariant from "invariant";
import { sortBy, filter, find } from "lodash";
import { sortBy, filter, find, isUndefined } from "lodash";
import { action, computed } from "mobx";
import Share from "models/Share";
import BaseStore from "./BaseStore";
@@ -47,6 +47,8 @@ export default class SharesStore extends BaseStore<Share> {
try {
const res = await client.post(`/${this.modelName}s.info`, { documentId });
if (isUndefined(res)) return;
invariant(res && res.data, "Data should be available");
this.addPolicies(res.policies);
+28
View File
@@ -9,6 +9,9 @@ import type { Toast } from "types";
const UI_STORE = "UI_STORE";
class UiStore {
// has the user seen the prompt to change the UI language and actioned it
@observable languagePromptDismissed: boolean;
// theme represents the users UI preference (defaults to system)
@observable theme: "light" | "dark" | "system";
@@ -20,6 +23,7 @@ class UiStore {
@observable editMode: boolean = false;
@observable tocVisible: boolean = false;
@observable mobileSidebarVisible: boolean = false;
@observable sidebarCollapsed: boolean = false;
@observable toasts: Map<string, Toast> = new Map();
constructor() {
@@ -47,6 +51,8 @@ class UiStore {
}
// persisted keys
this.languagePromptDismissed = data.languagePromptDismissed;
this.sidebarCollapsed = data.sidebarCollapsed;
this.tocVisible = data.tocVisible;
this.theme = data.theme || "system";
@@ -68,6 +74,11 @@ class UiStore {
}
};
@action
setLanguagePromptDismissed = () => {
this.languagePromptDismissed = true;
};
@action
setActiveDocument = (document: Document): void => {
this.activeDocumentId = document.id;
@@ -98,6 +109,21 @@ class UiStore {
this.activeCollectionId = undefined;
};
@action
collapseSidebar = () => {
this.sidebarCollapsed = true;
};
@action
expandSidebar = () => {
this.sidebarCollapsed = false;
};
@action
toggleCollapsedSidebar = () => {
this.sidebarCollapsed = !this.sidebarCollapsed;
};
@action
showTableOfContents = () => {
this.tocVisible = true;
@@ -181,6 +207,8 @@ class UiStore {
get asJson(): string {
return JSON.stringify({
tocVisible: this.tocVisible,
sidebarCollapsed: this.sidebarCollapsed,
languagePromptDismissed: this.languagePromptDismissed,
theme: this.theme,
});
}
+12
View File
@@ -5,10 +5,12 @@ import stores from "stores";
import download from "./download";
import {
AuthorizationError,
BadRequestError,
NetworkError,
NotFoundError,
OfflineError,
RequestError,
ServiceUnavailableError,
UpdateRequiredError,
} from "./errors";
@@ -110,6 +112,8 @@ class ApiClient {
download(blob, trim(fileName, '"'));
return;
} else if (success && response.status === 204) {
return;
} else if (success) {
return response.json();
}
@@ -139,6 +143,10 @@ class ApiClient {
throw new UpdateRequiredError(error.message);
}
if (response.status === 400) {
throw new BadRequestError(error.message);
}
if (response.status === 403) {
throw new AuthorizationError(error.message);
}
@@ -147,6 +155,10 @@ class ApiClient {
throw new NotFoundError(error.message);
}
if (response.status === 503) {
throw new ServiceUnavailableError(error.message);
}
throw new RequestError(error.message);
};
+17
View File
@@ -0,0 +1,17 @@
// @flow
import Compressor from "compressorjs";
type Options = Omit<Compressor.Options, "success" | "error">;
export const compressImage = async (
file: File | Blob,
options?: Options
): Promise<Blob> => {
return new Promise((resolve, reject) => {
new Compressor(file, {
...options,
success: resolve,
error: reject,
});
});
};
+2
View File
@@ -2,8 +2,10 @@
import ExtendableError from "es6-error";
export class AuthorizationError extends ExtendableError {}
export class BadRequestError extends ExtendableError {}
export class NetworkError extends ExtendableError {}
export class NotFoundError extends ExtendableError {}
export class OfflineError extends ExtendableError {}
export class ServiceUnavailableError extends ExtendableError {}
export class RequestError extends ExtendableError {}
export class UpdateRequiredError extends ExtendableError {}
+7
View File
@@ -0,0 +1,7 @@
// @flow
export function detectLanguage() {
const [ln, r] = navigator.language.split("-");
const region = (r || ln).toUpperCase();
return `${ln}_${region}`;
}
+5
View File
@@ -0,0 +1,5 @@
commit_message: "fix: New %language% translations from Crowdin [ci skip]"
append_commit_message: false
files:
- source: /shared/i18n/locales/en_US/translation.json
translation: /shared/i18n/locales/%locale_with_underscore%/translation.json
-8
View File
@@ -1,8 +0,0 @@
// @flow
declare var process: {
env: {
[string]: string,
},
};
declare var EDITOR_VERSION: string;
-309
View File
@@ -1,309 +0,0 @@
// flow-typed signature: 9c614e9038bfd47492a561085bc95586
// flow-typed version: <<STUB>>/@sentry/node_v^5.12.2/flow_v0.104.0
/**
* This is an autogenerated libdef stub for:
*
* '@sentry/node'
*
* 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 '@sentry/node' {
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 '@sentry/node/dist/backend' {
declare module.exports: any;
}
declare module '@sentry/node/dist/client' {
declare module.exports: any;
}
declare module '@sentry/node/dist/handlers' {
declare module.exports: any;
}
declare module '@sentry/node/dist' {
declare module.exports: any;
}
declare module '@sentry/node/dist/integrations/console' {
declare module.exports: any;
}
declare module '@sentry/node/dist/integrations/http' {
declare module.exports: any;
}
declare module '@sentry/node/dist/integrations' {
declare module.exports: any;
}
declare module '@sentry/node/dist/integrations/linkederrors' {
declare module.exports: any;
}
declare module '@sentry/node/dist/integrations/modules' {
declare module.exports: any;
}
declare module '@sentry/node/dist/integrations/onuncaughtexception' {
declare module.exports: any;
}
declare module '@sentry/node/dist/integrations/onunhandledrejection' {
declare module.exports: any;
}
declare module '@sentry/node/dist/parsers' {
declare module.exports: any;
}
declare module '@sentry/node/dist/sdk' {
declare module.exports: any;
}
declare module '@sentry/node/dist/stacktrace' {
declare module.exports: any;
}
declare module '@sentry/node/dist/transports/base' {
declare module.exports: any;
}
declare module '@sentry/node/dist/transports/http' {
declare module.exports: any;
}
declare module '@sentry/node/dist/transports/https' {
declare module.exports: any;
}
declare module '@sentry/node/dist/transports' {
declare module.exports: any;
}
declare module '@sentry/node/dist/version' {
declare module.exports: any;
}
declare module '@sentry/node/esm/backend' {
declare module.exports: any;
}
declare module '@sentry/node/esm/client' {
declare module.exports: any;
}
declare module '@sentry/node/esm/handlers' {
declare module.exports: any;
}
declare module '@sentry/node/esm' {
declare module.exports: any;
}
declare module '@sentry/node/esm/integrations/console' {
declare module.exports: any;
}
declare module '@sentry/node/esm/integrations/http' {
declare module.exports: any;
}
declare module '@sentry/node/esm/integrations' {
declare module.exports: any;
}
declare module '@sentry/node/esm/integrations/linkederrors' {
declare module.exports: any;
}
declare module '@sentry/node/esm/integrations/modules' {
declare module.exports: any;
}
declare module '@sentry/node/esm/integrations/onuncaughtexception' {
declare module.exports: any;
}
declare module '@sentry/node/esm/integrations/onunhandledrejection' {
declare module.exports: any;
}
declare module '@sentry/node/esm/parsers' {
declare module.exports: any;
}
declare module '@sentry/node/esm/sdk' {
declare module.exports: any;
}
declare module '@sentry/node/esm/stacktrace' {
declare module.exports: any;
}
declare module '@sentry/node/esm/transports/base' {
declare module.exports: any;
}
declare module '@sentry/node/esm/transports/http' {
declare module.exports: any;
}
declare module '@sentry/node/esm/transports/https' {
declare module.exports: any;
}
declare module '@sentry/node/esm/transports' {
declare module.exports: any;
}
declare module '@sentry/node/esm/version' {
declare module.exports: any;
}
// Filename aliases
declare module '@sentry/node/dist/backend.js' {
declare module.exports: $Exports<'@sentry/node/dist/backend'>;
}
declare module '@sentry/node/dist/client.js' {
declare module.exports: $Exports<'@sentry/node/dist/client'>;
}
declare module '@sentry/node/dist/handlers.js' {
declare module.exports: $Exports<'@sentry/node/dist/handlers'>;
}
declare module '@sentry/node/dist/index' {
declare module.exports: $Exports<'@sentry/node/dist'>;
}
declare module '@sentry/node/dist/index.js' {
declare module.exports: $Exports<'@sentry/node/dist'>;
}
declare module '@sentry/node/dist/integrations/console.js' {
declare module.exports: $Exports<'@sentry/node/dist/integrations/console'>;
}
declare module '@sentry/node/dist/integrations/http.js' {
declare module.exports: $Exports<'@sentry/node/dist/integrations/http'>;
}
declare module '@sentry/node/dist/integrations/index' {
declare module.exports: $Exports<'@sentry/node/dist/integrations'>;
}
declare module '@sentry/node/dist/integrations/index.js' {
declare module.exports: $Exports<'@sentry/node/dist/integrations'>;
}
declare module '@sentry/node/dist/integrations/linkederrors.js' {
declare module.exports: $Exports<'@sentry/node/dist/integrations/linkederrors'>;
}
declare module '@sentry/node/dist/integrations/modules.js' {
declare module.exports: $Exports<'@sentry/node/dist/integrations/modules'>;
}
declare module '@sentry/node/dist/integrations/onuncaughtexception.js' {
declare module.exports: $Exports<'@sentry/node/dist/integrations/onuncaughtexception'>;
}
declare module '@sentry/node/dist/integrations/onunhandledrejection.js' {
declare module.exports: $Exports<'@sentry/node/dist/integrations/onunhandledrejection'>;
}
declare module '@sentry/node/dist/parsers.js' {
declare module.exports: $Exports<'@sentry/node/dist/parsers'>;
}
declare module '@sentry/node/dist/sdk.js' {
declare module.exports: $Exports<'@sentry/node/dist/sdk'>;
}
declare module '@sentry/node/dist/stacktrace.js' {
declare module.exports: $Exports<'@sentry/node/dist/stacktrace'>;
}
declare module '@sentry/node/dist/transports/base.js' {
declare module.exports: $Exports<'@sentry/node/dist/transports/base'>;
}
declare module '@sentry/node/dist/transports/http.js' {
declare module.exports: $Exports<'@sentry/node/dist/transports/http'>;
}
declare module '@sentry/node/dist/transports/https.js' {
declare module.exports: $Exports<'@sentry/node/dist/transports/https'>;
}
declare module '@sentry/node/dist/transports/index' {
declare module.exports: $Exports<'@sentry/node/dist/transports'>;
}
declare module '@sentry/node/dist/transports/index.js' {
declare module.exports: $Exports<'@sentry/node/dist/transports'>;
}
declare module '@sentry/node/dist/version.js' {
declare module.exports: $Exports<'@sentry/node/dist/version'>;
}
declare module '@sentry/node/esm/backend.js' {
declare module.exports: $Exports<'@sentry/node/esm/backend'>;
}
declare module '@sentry/node/esm/client.js' {
declare module.exports: $Exports<'@sentry/node/esm/client'>;
}
declare module '@sentry/node/esm/handlers.js' {
declare module.exports: $Exports<'@sentry/node/esm/handlers'>;
}
declare module '@sentry/node/esm/index' {
declare module.exports: $Exports<'@sentry/node/esm'>;
}
declare module '@sentry/node/esm/index.js' {
declare module.exports: $Exports<'@sentry/node/esm'>;
}
declare module '@sentry/node/esm/integrations/console.js' {
declare module.exports: $Exports<'@sentry/node/esm/integrations/console'>;
}
declare module '@sentry/node/esm/integrations/http.js' {
declare module.exports: $Exports<'@sentry/node/esm/integrations/http'>;
}
declare module '@sentry/node/esm/integrations/index' {
declare module.exports: $Exports<'@sentry/node/esm/integrations'>;
}
declare module '@sentry/node/esm/integrations/index.js' {
declare module.exports: $Exports<'@sentry/node/esm/integrations'>;
}
declare module '@sentry/node/esm/integrations/linkederrors.js' {
declare module.exports: $Exports<'@sentry/node/esm/integrations/linkederrors'>;
}
declare module '@sentry/node/esm/integrations/modules.js' {
declare module.exports: $Exports<'@sentry/node/esm/integrations/modules'>;
}
declare module '@sentry/node/esm/integrations/onuncaughtexception.js' {
declare module.exports: $Exports<'@sentry/node/esm/integrations/onuncaughtexception'>;
}
declare module '@sentry/node/esm/integrations/onunhandledrejection.js' {
declare module.exports: $Exports<'@sentry/node/esm/integrations/onunhandledrejection'>;
}
declare module '@sentry/node/esm/parsers.js' {
declare module.exports: $Exports<'@sentry/node/esm/parsers'>;
}
declare module '@sentry/node/esm/sdk.js' {
declare module.exports: $Exports<'@sentry/node/esm/sdk'>;
}
declare module '@sentry/node/esm/stacktrace.js' {
declare module.exports: $Exports<'@sentry/node/esm/stacktrace'>;
}
declare module '@sentry/node/esm/transports/base.js' {
declare module.exports: $Exports<'@sentry/node/esm/transports/base'>;
}
declare module '@sentry/node/esm/transports/http.js' {
declare module.exports: $Exports<'@sentry/node/esm/transports/http'>;
}
declare module '@sentry/node/esm/transports/https.js' {
declare module.exports: $Exports<'@sentry/node/esm/transports/https'>;
}
declare module '@sentry/node/esm/transports/index' {
declare module.exports: $Exports<'@sentry/node/esm/transports'>;
}
declare module '@sentry/node/esm/transports/index.js' {
declare module.exports: $Exports<'@sentry/node/esm/transports'>;
}
declare module '@sentry/node/esm/version.js' {
declare module.exports: $Exports<'@sentry/node/esm/version'>;
}
-59
View File
@@ -1,59 +0,0 @@
// flow-typed signature: 3a7f9d7c7339a68225914ed21f313279
// flow-typed version: <<STUB>>/@tippy.js/react_v^2.2.2/flow_v0.104.0
/**
* This is an autogenerated libdef stub for:
*
* '@tippy.js/react'
*
* 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 '@tippy.js/react' {
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 '@tippy.js/react/esm' {
declare module.exports: any;
}
declare module '@tippy.js/react/esm/index.min' {
declare module.exports: any;
}
declare module '@tippy.js/react/umd' {
declare module.exports: any;
}
declare module '@tippy.js/react/umd/index.min' {
declare module.exports: any;
}
// Filename aliases
declare module '@tippy.js/react/esm/index' {
declare module.exports: $Exports<'@tippy.js/react/esm'>;
}
declare module '@tippy.js/react/esm/index.js' {
declare module.exports: $Exports<'@tippy.js/react/esm'>;
}
declare module '@tippy.js/react/esm/index.min.js' {
declare module.exports: $Exports<'@tippy.js/react/esm/index.min'>;
}
declare module '@tippy.js/react/umd/index' {
declare module.exports: $Exports<'@tippy.js/react/umd'>;
}
declare module '@tippy.js/react/umd/index.js' {
declare module.exports: $Exports<'@tippy.js/react/umd'>;
}
declare module '@tippy.js/react/umd/index.min.js' {
declare module.exports: $Exports<'@tippy.js/react/umd/index.min'>;
}
-38
View File
@@ -1,38 +0,0 @@
// flow-typed signature: 300612c60dee7fa8f0828284182b3ef6
// flow-typed version: <<STUB>>/@tommoor/remove-markdown_v0.3.1/flow_v0.104.0
/**
* This is an autogenerated libdef stub for:
*
* '@tommoor/remove-markdown'
*
* 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 '@tommoor/remove-markdown' {
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 '@tommoor/remove-markdown/test/remove-markdown' {
declare module.exports: any;
}
// Filename aliases
declare module '@tommoor/remove-markdown/index' {
declare module.exports: $Exports<'@tommoor/remove-markdown'>;
}
declare module '@tommoor/remove-markdown/index.js' {
declare module.exports: $Exports<'@tommoor/remove-markdown'>;
}
declare module '@tommoor/remove-markdown/test/remove-markdown.js' {
declare module.exports: $Exports<'@tommoor/remove-markdown/test/remove-markdown'>;
}
-434
View File
@@ -1,434 +0,0 @@
// flow-typed signature: f43805453dffac09f37f36e338814526
// flow-typed version: <<STUB>>/autotrack_v^2.4.1/flow_v0.104.0
/**
* This is an autogenerated libdef stub for:
*
* 'autotrack'
*
* 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 'autotrack' {
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 'autotrack/autotrack' {
declare module.exports: any;
}
declare module 'autotrack/bin/build' {
declare module.exports: any;
}
declare module 'autotrack/bin/errors' {
declare module.exports: any;
}
declare module 'autotrack/gulpfile' {
declare module.exports: any;
}
declare module 'autotrack/lib/constants' {
declare module.exports: any;
}
declare module 'autotrack/lib/event-emitter' {
declare module.exports: any;
}
declare module 'autotrack/lib/externs/analytics' {
declare module.exports: any;
}
declare module 'autotrack/lib/externs/clean-url-tracker' {
declare module.exports: any;
}
declare module 'autotrack/lib/externs/event-tracker' {
declare module.exports: any;
}
declare module 'autotrack/lib/externs/impression-tracker' {
declare module.exports: any;
}
declare module 'autotrack/lib/externs/max-scroll-tracker' {
declare module.exports: any;
}
declare module 'autotrack/lib/externs/media-query-tracker' {
declare module.exports: any;
}
declare module 'autotrack/lib/externs/outbound-form-tracker' {
declare module.exports: any;
}
declare module 'autotrack/lib/externs/outbound-link-tracker' {
declare module.exports: any;
}
declare module 'autotrack/lib/externs/page-visibility-tracker' {
declare module.exports: any;
}
declare module 'autotrack/lib/externs/session' {
declare module.exports: any;
}
declare module 'autotrack/lib/externs/social-widget-tracker' {
declare module.exports: any;
}
declare module 'autotrack/lib/externs/url-change-tracker' {
declare module.exports: any;
}
declare module 'autotrack/lib' {
declare module.exports: any;
}
declare module 'autotrack/lib/method-chain' {
declare module.exports: any;
}
declare module 'autotrack/lib/plugins/clean-url-tracker' {
declare module.exports: any;
}
declare module 'autotrack/lib/plugins/event-tracker' {
declare module.exports: any;
}
declare module 'autotrack/lib/plugins/impression-tracker' {
declare module.exports: any;
}
declare module 'autotrack/lib/plugins/max-scroll-tracker' {
declare module.exports: any;
}
declare module 'autotrack/lib/plugins/media-query-tracker' {
declare module.exports: any;
}
declare module 'autotrack/lib/plugins/outbound-form-tracker' {
declare module.exports: any;
}
declare module 'autotrack/lib/plugins/outbound-link-tracker' {
declare module.exports: any;
}
declare module 'autotrack/lib/plugins/page-visibility-tracker' {
declare module.exports: any;
}
declare module 'autotrack/lib/plugins/social-widget-tracker' {
declare module.exports: any;
}
declare module 'autotrack/lib/plugins/url-change-tracker' {
declare module.exports: any;
}
declare module 'autotrack/lib/provide' {
declare module.exports: any;
}
declare module 'autotrack/lib/session' {
declare module.exports: any;
}
declare module 'autotrack/lib/store' {
declare module.exports: any;
}
declare module 'autotrack/lib/usage' {
declare module.exports: any;
}
declare module 'autotrack/lib/utilities' {
declare module.exports: any;
}
declare module 'autotrack/test/analytics_debug' {
declare module.exports: any;
}
declare module 'autotrack/test/analytics' {
declare module.exports: any;
}
declare module 'autotrack/test/e2e/clean-url-tracker-test' {
declare module.exports: any;
}
declare module 'autotrack/test/e2e/event-tracker-test' {
declare module.exports: any;
}
declare module 'autotrack/test/e2e/ga' {
declare module.exports: any;
}
declare module 'autotrack/test/e2e/impression-tracker-test' {
declare module.exports: any;
}
declare module 'autotrack/test/e2e/index-test' {
declare module.exports: any;
}
declare module 'autotrack/test/e2e/max-scroll-tracker-test' {
declare module.exports: any;
}
declare module 'autotrack/test/e2e/media-query-tracker-test' {
declare module.exports: any;
}
declare module 'autotrack/test/e2e/outbound-form-tracker-test' {
declare module.exports: any;
}
declare module 'autotrack/test/e2e/outbound-link-tracker-test' {
declare module.exports: any;
}
declare module 'autotrack/test/e2e/page-visibility-tracker-test' {
declare module.exports: any;
}
declare module 'autotrack/test/e2e/server' {
declare module.exports: any;
}
declare module 'autotrack/test/e2e/social-widget-tracker-test' {
declare module.exports: any;
}
declare module 'autotrack/test/e2e/url-change-tracker-test' {
declare module.exports: any;
}
declare module 'autotrack/test/e2e/wdio.conf' {
declare module.exports: any;
}
declare module 'autotrack/test/unit/event-emitter-test' {
declare module.exports: any;
}
declare module 'autotrack/test/unit/method-chain-test' {
declare module.exports: any;
}
declare module 'autotrack/test/unit/plugins/clean-url-tracker-test' {
declare module.exports: any;
}
declare module 'autotrack/test/unit/plugins/page-visibility-tracker-test' {
declare module.exports: any;
}
declare module 'autotrack/test/unit/session-test' {
declare module.exports: any;
}
declare module 'autotrack/test/unit/store-test' {
declare module.exports: any;
}
declare module 'autotrack/test/unit/utilities-test' {
declare module.exports: any;
}
// Filename aliases
declare module 'autotrack/autotrack.js' {
declare module.exports: $Exports<'autotrack/autotrack'>;
}
declare module 'autotrack/bin/build.js' {
declare module.exports: $Exports<'autotrack/bin/build'>;
}
declare module 'autotrack/bin/errors.js' {
declare module.exports: $Exports<'autotrack/bin/errors'>;
}
declare module 'autotrack/gulpfile.js' {
declare module.exports: $Exports<'autotrack/gulpfile'>;
}
declare module 'autotrack/lib/constants.js' {
declare module.exports: $Exports<'autotrack/lib/constants'>;
}
declare module 'autotrack/lib/event-emitter.js' {
declare module.exports: $Exports<'autotrack/lib/event-emitter'>;
}
declare module 'autotrack/lib/externs/analytics.js' {
declare module.exports: $Exports<'autotrack/lib/externs/analytics'>;
}
declare module 'autotrack/lib/externs/clean-url-tracker.js' {
declare module.exports: $Exports<'autotrack/lib/externs/clean-url-tracker'>;
}
declare module 'autotrack/lib/externs/event-tracker.js' {
declare module.exports: $Exports<'autotrack/lib/externs/event-tracker'>;
}
declare module 'autotrack/lib/externs/impression-tracker.js' {
declare module.exports: $Exports<'autotrack/lib/externs/impression-tracker'>;
}
declare module 'autotrack/lib/externs/max-scroll-tracker.js' {
declare module.exports: $Exports<'autotrack/lib/externs/max-scroll-tracker'>;
}
declare module 'autotrack/lib/externs/media-query-tracker.js' {
declare module.exports: $Exports<'autotrack/lib/externs/media-query-tracker'>;
}
declare module 'autotrack/lib/externs/outbound-form-tracker.js' {
declare module.exports: $Exports<'autotrack/lib/externs/outbound-form-tracker'>;
}
declare module 'autotrack/lib/externs/outbound-link-tracker.js' {
declare module.exports: $Exports<'autotrack/lib/externs/outbound-link-tracker'>;
}
declare module 'autotrack/lib/externs/page-visibility-tracker.js' {
declare module.exports: $Exports<'autotrack/lib/externs/page-visibility-tracker'>;
}
declare module 'autotrack/lib/externs/session.js' {
declare module.exports: $Exports<'autotrack/lib/externs/session'>;
}
declare module 'autotrack/lib/externs/social-widget-tracker.js' {
declare module.exports: $Exports<'autotrack/lib/externs/social-widget-tracker'>;
}
declare module 'autotrack/lib/externs/url-change-tracker.js' {
declare module.exports: $Exports<'autotrack/lib/externs/url-change-tracker'>;
}
declare module 'autotrack/lib/index' {
declare module.exports: $Exports<'autotrack/lib'>;
}
declare module 'autotrack/lib/index.js' {
declare module.exports: $Exports<'autotrack/lib'>;
}
declare module 'autotrack/lib/method-chain.js' {
declare module.exports: $Exports<'autotrack/lib/method-chain'>;
}
declare module 'autotrack/lib/plugins/clean-url-tracker.js' {
declare module.exports: $Exports<'autotrack/lib/plugins/clean-url-tracker'>;
}
declare module 'autotrack/lib/plugins/event-tracker.js' {
declare module.exports: $Exports<'autotrack/lib/plugins/event-tracker'>;
}
declare module 'autotrack/lib/plugins/impression-tracker.js' {
declare module.exports: $Exports<'autotrack/lib/plugins/impression-tracker'>;
}
declare module 'autotrack/lib/plugins/max-scroll-tracker.js' {
declare module.exports: $Exports<'autotrack/lib/plugins/max-scroll-tracker'>;
}
declare module 'autotrack/lib/plugins/media-query-tracker.js' {
declare module.exports: $Exports<'autotrack/lib/plugins/media-query-tracker'>;
}
declare module 'autotrack/lib/plugins/outbound-form-tracker.js' {
declare module.exports: $Exports<'autotrack/lib/plugins/outbound-form-tracker'>;
}
declare module 'autotrack/lib/plugins/outbound-link-tracker.js' {
declare module.exports: $Exports<'autotrack/lib/plugins/outbound-link-tracker'>;
}
declare module 'autotrack/lib/plugins/page-visibility-tracker.js' {
declare module.exports: $Exports<'autotrack/lib/plugins/page-visibility-tracker'>;
}
declare module 'autotrack/lib/plugins/social-widget-tracker.js' {
declare module.exports: $Exports<'autotrack/lib/plugins/social-widget-tracker'>;
}
declare module 'autotrack/lib/plugins/url-change-tracker.js' {
declare module.exports: $Exports<'autotrack/lib/plugins/url-change-tracker'>;
}
declare module 'autotrack/lib/provide.js' {
declare module.exports: $Exports<'autotrack/lib/provide'>;
}
declare module 'autotrack/lib/session.js' {
declare module.exports: $Exports<'autotrack/lib/session'>;
}
declare module 'autotrack/lib/store.js' {
declare module.exports: $Exports<'autotrack/lib/store'>;
}
declare module 'autotrack/lib/usage.js' {
declare module.exports: $Exports<'autotrack/lib/usage'>;
}
declare module 'autotrack/lib/utilities.js' {
declare module.exports: $Exports<'autotrack/lib/utilities'>;
}
declare module 'autotrack/test/analytics_debug.js' {
declare module.exports: $Exports<'autotrack/test/analytics_debug'>;
}
declare module 'autotrack/test/analytics.js' {
declare module.exports: $Exports<'autotrack/test/analytics'>;
}
declare module 'autotrack/test/e2e/clean-url-tracker-test.js' {
declare module.exports: $Exports<'autotrack/test/e2e/clean-url-tracker-test'>;
}
declare module 'autotrack/test/e2e/event-tracker-test.js' {
declare module.exports: $Exports<'autotrack/test/e2e/event-tracker-test'>;
}
declare module 'autotrack/test/e2e/ga.js' {
declare module.exports: $Exports<'autotrack/test/e2e/ga'>;
}
declare module 'autotrack/test/e2e/impression-tracker-test.js' {
declare module.exports: $Exports<'autotrack/test/e2e/impression-tracker-test'>;
}
declare module 'autotrack/test/e2e/index-test.js' {
declare module.exports: $Exports<'autotrack/test/e2e/index-test'>;
}
declare module 'autotrack/test/e2e/max-scroll-tracker-test.js' {
declare module.exports: $Exports<'autotrack/test/e2e/max-scroll-tracker-test'>;
}
declare module 'autotrack/test/e2e/media-query-tracker-test.js' {
declare module.exports: $Exports<'autotrack/test/e2e/media-query-tracker-test'>;
}
declare module 'autotrack/test/e2e/outbound-form-tracker-test.js' {
declare module.exports: $Exports<'autotrack/test/e2e/outbound-form-tracker-test'>;
}
declare module 'autotrack/test/e2e/outbound-link-tracker-test.js' {
declare module.exports: $Exports<'autotrack/test/e2e/outbound-link-tracker-test'>;
}
declare module 'autotrack/test/e2e/page-visibility-tracker-test.js' {
declare module.exports: $Exports<'autotrack/test/e2e/page-visibility-tracker-test'>;
}
declare module 'autotrack/test/e2e/server.js' {
declare module.exports: $Exports<'autotrack/test/e2e/server'>;
}
declare module 'autotrack/test/e2e/social-widget-tracker-test.js' {
declare module.exports: $Exports<'autotrack/test/e2e/social-widget-tracker-test'>;
}
declare module 'autotrack/test/e2e/url-change-tracker-test.js' {
declare module.exports: $Exports<'autotrack/test/e2e/url-change-tracker-test'>;
}
declare module 'autotrack/test/e2e/wdio.conf.js' {
declare module.exports: $Exports<'autotrack/test/e2e/wdio.conf'>;
}
declare module 'autotrack/test/unit/event-emitter-test.js' {
declare module.exports: $Exports<'autotrack/test/unit/event-emitter-test'>;
}
declare module 'autotrack/test/unit/method-chain-test.js' {
declare module.exports: $Exports<'autotrack/test/unit/method-chain-test'>;
}
declare module 'autotrack/test/unit/plugins/clean-url-tracker-test.js' {
declare module.exports: $Exports<'autotrack/test/unit/plugins/clean-url-tracker-test'>;
}
declare module 'autotrack/test/unit/plugins/page-visibility-tracker-test.js' {
declare module.exports: $Exports<'autotrack/test/unit/plugins/page-visibility-tracker-test'>;
}
declare module 'autotrack/test/unit/session-test.js' {
declare module.exports: $Exports<'autotrack/test/unit/session-test'>;
}
declare module 'autotrack/test/unit/store-test.js' {
declare module.exports: $Exports<'autotrack/test/unit/store-test'>;
}
declare module 'autotrack/test/unit/utilities-test.js' {
declare module.exports: $Exports<'autotrack/test/unit/utilities-test'>;
}
-2682
View File
File diff suppressed because it is too large Load Diff
-58
View File
@@ -1,58 +0,0 @@
// flow-typed signature: 124ea3a8df633e16e9901a6e10ec7777
// flow-typed version: <<STUB>>/boundless-arrow-key-navigation_v^1.0.4/flow_v0.104.0
/**
* This is an autogenerated libdef stub for:
*
* 'boundless-arrow-key-navigation'
*
* 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 'boundless-arrow-key-navigation' {
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 'boundless-arrow-key-navigation/build' {
declare module.exports: any;
}
declare module 'boundless-arrow-key-navigation/demo' {
declare module.exports: any;
}
declare module 'boundless-arrow-key-navigation/index.spec' {
declare module.exports: any;
}
// Filename aliases
declare module 'boundless-arrow-key-navigation/build/index' {
declare module.exports: $Exports<'boundless-arrow-key-navigation/build'>;
}
declare module 'boundless-arrow-key-navigation/build/index.js' {
declare module.exports: $Exports<'boundless-arrow-key-navigation/build'>;
}
declare module 'boundless-arrow-key-navigation/demo/index' {
declare module.exports: $Exports<'boundless-arrow-key-navigation/demo'>;
}
declare module 'boundless-arrow-key-navigation/demo/index.js' {
declare module.exports: $Exports<'boundless-arrow-key-navigation/demo'>;
}
declare module 'boundless-arrow-key-navigation/index' {
declare module.exports: $Exports<'boundless-arrow-key-navigation'>;
}
declare module 'boundless-arrow-key-navigation/index.js' {
declare module.exports: $Exports<'boundless-arrow-key-navigation'>;
}
declare module 'boundless-arrow-key-navigation/index.spec.js' {
declare module.exports: $Exports<'boundless-arrow-key-navigation/index.spec'>;
}
-58
View File
@@ -1,58 +0,0 @@
// flow-typed signature: 81720de1e8cfea1529815ce45326fdff
// flow-typed version: <<STUB>>/boundless-popover_v^1.0.4/flow_v0.104.0
/**
* This is an autogenerated libdef stub for:
*
* 'boundless-popover'
*
* 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 'boundless-popover' {
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 'boundless-popover/build' {
declare module.exports: any;
}
declare module 'boundless-popover/demo' {
declare module.exports: any;
}
declare module 'boundless-popover/index.spec' {
declare module.exports: any;
}
// Filename aliases
declare module 'boundless-popover/build/index' {
declare module.exports: $Exports<'boundless-popover/build'>;
}
declare module 'boundless-popover/build/index.js' {
declare module.exports: $Exports<'boundless-popover/build'>;
}
declare module 'boundless-popover/demo/index' {
declare module.exports: $Exports<'boundless-popover/demo'>;
}
declare module 'boundless-popover/demo/index.js' {
declare module.exports: $Exports<'boundless-popover/demo'>;
}
declare module 'boundless-popover/index' {
declare module.exports: $Exports<'boundless-popover'>;
}
declare module 'boundless-popover/index.js' {
declare module.exports: $Exports<'boundless-popover'>;
}
declare module 'boundless-popover/index.spec.js' {
declare module.exports: $Exports<'boundless-popover/index.spec'>;
}
-132
View File
@@ -1,132 +0,0 @@
// flow-typed signature: 7ef49570a788f5303976a16379421a35
// flow-typed version: <<STUB>>/bull_v^3.5.2/flow_v0.104.0
/**
* This is an autogenerated libdef stub for:
*
* 'bull'
*
* 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 'bull' {
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 'bull/lib/backoffs' {
declare module.exports: any;
}
declare module 'bull/lib/commands' {
declare module.exports: any;
}
declare module 'bull/lib/errors' {
declare module.exports: any;
}
declare module 'bull/lib/getters' {
declare module.exports: any;
}
declare module 'bull/lib/job' {
declare module.exports: any;
}
declare module 'bull/lib/process/child-pool' {
declare module.exports: any;
}
declare module 'bull/lib/process/master' {
declare module.exports: any;
}
declare module 'bull/lib/process/sandbox' {
declare module.exports: any;
}
declare module 'bull/lib/queue' {
declare module.exports: any;
}
declare module 'bull/lib/repeatable' {
declare module.exports: any;
}
declare module 'bull/lib/scripts' {
declare module.exports: any;
}
declare module 'bull/lib/timer-manager' {
declare module.exports: any;
}
declare module 'bull/lib/utils' {
declare module.exports: any;
}
declare module 'bull/lib/worker' {
declare module.exports: any;
}
// Filename aliases
declare module 'bull/index' {
declare module.exports: $Exports<'bull'>;
}
declare module 'bull/index.js' {
declare module.exports: $Exports<'bull'>;
}
declare module 'bull/lib/backoffs.js' {
declare module.exports: $Exports<'bull/lib/backoffs'>;
}
declare module 'bull/lib/commands/index' {
declare module.exports: $Exports<'bull/lib/commands'>;
}
declare module 'bull/lib/commands/index.js' {
declare module.exports: $Exports<'bull/lib/commands'>;
}
declare module 'bull/lib/errors.js' {
declare module.exports: $Exports<'bull/lib/errors'>;
}
declare module 'bull/lib/getters.js' {
declare module.exports: $Exports<'bull/lib/getters'>;
}
declare module 'bull/lib/job.js' {
declare module.exports: $Exports<'bull/lib/job'>;
}
declare module 'bull/lib/process/child-pool.js' {
declare module.exports: $Exports<'bull/lib/process/child-pool'>;
}
declare module 'bull/lib/process/master.js' {
declare module.exports: $Exports<'bull/lib/process/master'>;
}
declare module 'bull/lib/process/sandbox.js' {
declare module.exports: $Exports<'bull/lib/process/sandbox'>;
}
declare module 'bull/lib/queue.js' {
declare module.exports: $Exports<'bull/lib/queue'>;
}
declare module 'bull/lib/repeatable.js' {
declare module.exports: $Exports<'bull/lib/repeatable'>;
}
declare module 'bull/lib/scripts.js' {
declare module.exports: $Exports<'bull/lib/scripts'>;
}
declare module 'bull/lib/timer-manager.js' {
declare module.exports: $Exports<'bull/lib/timer-manager'>;
}
declare module 'bull/lib/utils.js' {
declare module.exports: $Exports<'bull/lib/utils'>;
}
declare module 'bull/lib/worker.js' {
declare module.exports: $Exports<'bull/lib/worker'>;
}
-33
View File
@@ -1,33 +0,0 @@
// flow-typed signature: 5902d2130f75742810972de490b1bc44
// flow-typed version: <<STUB>>/cancan_v3.1.0/flow_v0.104.0
/**
* This is an autogenerated libdef stub for:
*
* 'cancan'
*
* 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 'cancan' {
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.
*/
// Filename aliases
declare module 'cancan/index' {
declare module.exports: $Exports<'cancan'>;
}
declare module 'cancan/index.js' {
declare module.exports: $Exports<'cancan'>;
}
-18
View File
@@ -1,18 +0,0 @@
// flow-typed signature: 04e310e8c98cdb5de377193da621970b
// flow-typed version: 7fd0a6404e/classnames_v2.x.x/flow_>=v0.25.x
type $npm$classnames$Classes =
| string
| { [className: string]: * }
| Array<string>
| false
| void
| null;
declare module 'classnames' {
declare function exports(...classes: Array<$npm$classnames$Classes>): string;
}
declare module 'classnames/bind' {
declare module.exports: $Exports<'classnames'>;
}
-11
View File
@@ -1,11 +0,0 @@
// flow-typed signature: 350413ab85bd03f3d1450c0ae307d106
// flow-typed version: c6154227d1/copy-to-clipboard_v3.x.x/flow_>=v0.104.x
declare module 'copy-to-clipboard' {
declare export type Options = {|
debug?: boolean,
message?: string,
|};
declare module.exports: (text: string, options?: Options) => boolean;
}
-12113
View File
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More