Compare commits

...

227 Commits

Author SHA1 Message Date
Tom Moor 2aaad03270 refactor 2020-08-20 19:05:48 -07:00
Tom Moor 9252683260 fix: SocketPresence account for socket changing 2020-08-20 00:06:51 -07:00
Tom Moor f5748eb5e7 check connection on page visibility change 2020-08-19 22:51:18 -07:00
Tom Moor 0555fd2caa pref: JS bundling improvements (#1461)
* perf: Split only initial vendors
2020-08-17 22:09:12 -07:00
Tom Moor d885252fb0 fix: Mobile style fixes and improvements (#1459)
* fixes #1457 – check for matchMedia function before using it

* fixes: Depth issues
closes #1458

* fixes: Long breadcrumbs cause horizontal overflow

* fix: Improve tabs and overflow on mobile
2020-08-17 00:08:22 -07:00
Tom Moor df9b0bcf91 fix: Websocket reconnect when navigating from settings -> home 2020-08-14 17:47:12 -07:00
Tom Moor 31910f1628 Remove auto reconnect, increase reconnectionDelayMax 2020-08-14 17:25:55 -07:00
Tom Moor 14cb3a36c1 perf: Reduce initial bundle size / async bundle loading (#1456)
* feat: Move to React.lazy

* perf: Remove duplicate babel/runtime

* fix: Run yarn-deduplicate

* Further attempts to remove rich-markdown-editor from initial chunk

* perf: Lazy loading of authenticated routes

* perf: Move color picker to async loading
fix: Display placeholder when loading rich editor

* fix: Cache bust on auto reload
2020-08-14 17:23:58 -07:00
Tom Moor d3350c20b6 perf: Attempt websocket connection before polling 2020-08-14 13:37:11 -07:00
Tom Moor 174acfac32 fix: Unnecessary shares.info request when loading public share (#1453)
closes #1450
2020-08-13 16:48:03 -07:00
Tom Moor 9ef4e2b437 Update LICENSE 2020-08-12 20:04:48 -07:00
Tom Moor 8088da8cf3 0.46.0 2020-08-12 20:03:57 -07:00
Tom Moor 221ee48429 fix: Don't mangle class names in production 2020-08-12 19:28:15 -07:00
Tom Moor ffe8c046ef fix: Bump RME – Improves floating toolbar behavior 2020-08-12 17:01:27 -07:00
Tom Moor dbe8a10702 fix: Login to X should be centered when team name wraps to newline 2020-08-12 14:05:32 -07:00
Tom Moor 11f7e3a060 chore: Bundle Stats / Webpack v4 (#1448)
* chore: Experiment with bundle size monitoring service

* chore: Ensure build runs on CI, move lint and flow before test

* chore: Upgrade Webpack v3 -> v4

* chore: Add webpack-cli
Remove unused dep
Move deps to dev

* Move babel deps to production

* Move babel deps to production
2020-08-12 13:16:10 -07:00
Tom Moor 0f41a04e49 refactor: Remove centralized Modal management (#1444)
* refactor: Finally remove centralized Modals component

* chore: Cleanup related unused methods in UiStore
2020-08-12 10:49:15 -07:00
Tom Moor d055021ad4 chore: Remove all usage of collection.type (#1445)
* chore: Remove all usage of collection.type

* migration: Remove type column
2020-08-12 10:49:02 -07:00
Tom Moor 810dc5a061 feat: Clicking the last updated time should open document history sidebar
Ref #1285
2020-08-11 21:01:03 -07:00
ktfth 7abe375b3e refactor: Removed unusued index on the onSearchLink (#1420)
Co-authored-by: Tom Moor <tom.moor@gmail.com>
2020-08-11 19:59:11 -07:00
Tom Moor 63371d8f5b flow 2020-08-11 18:59:57 -07:00
Tom Moor 6e61df0729 fix: Improved loading jank fix, new DelayedMount component 2020-08-10 21:30:12 -07:00
Tom Moor 5ddc4000d0 fixes: Strange scroll behavior on long collection descriptions
closes #1391
2020-08-10 16:23:55 -07:00
Tom Moor 48b61559cc fixes: JS error when attempting to show toast messages from collection description editor 2020-08-10 16:04:23 -07:00
Tom Moor 0cac5cfe51 fix: Prevent reload loop with error on editor load 2020-08-10 15:52:45 -07:00
Tom Moor e9ce80a3aa fixes: Case where websocket will not reconnect
closes #1384
2020-08-09 23:25:27 -07:00
Tom Moor 07d488c826 fix: GitHub Gist embed reliability, closes #1400 2020-08-09 21:53:57 -07:00
Tom Moor e2bd03494d chore: Update syntax, improve more typing (#1439)
* chore: <React.Fragment> to <>

* flow types
2020-08-09 09:48:04 -07:00
Tom Moor ead55442e0 flow: Restore lesser flowtype for styled-components
The current flow-typed def requires an insane amount of manual typing that just doesnt
make any sense. Restoring the old definition for now:
https://github.com/flow-typed/flow-typed/issues/3766
2020-08-08 23:41:02 -07:00
Tom Moor 449dc55aaa chore: Upgrade Babel, Jest, Eslint (#1437)
* chore: Upgrade Prettier 1.8 -> 2.0

* chore: Upgrade Babel 6 -> 7

* chore: Upgrade eslint plugins

* chore: Add eslint import/order rules

* chore: Update flow-typed deps
2020-08-08 22:53:59 -07:00
Tom Moor e312b264a6 chore: Upgrade Prettier 1.8 -> 2.0 (#1436) 2020-08-08 18:53:11 -07:00
Tom Moor 68dcb4de5f fix: Catch expected error when shares.info returns 404 2020-08-08 17:55:21 -07:00
Tom Moor d2b9a5c03f fix: Various React errors in console 2020-08-08 17:51:40 -07:00
Tom Moor 1b023fb6d7 fix: Remove flash of loading state for document lists 2020-08-08 17:39:30 -07:00
Tom Moor afe4553a7e chore: Resolve 2 open security alerts 2020-08-08 17:35:42 -07:00
Tom Moor 139e2e29d7 flow 2020-08-08 17:12:36 -07:00
Tom Moor 638418432a test 2020-08-08 16:32:12 -07:00
Tom Moor c6d2467fae chore: Upgrade Flow to v0.104.0 2020-08-08 16:26:20 -07:00
Tom Moor e9387db895 chore: Remove unused flow-typed 2020-08-08 16:05:32 -07:00
Tom Moor 065d04ec98 chore: Missing flow types 2020-08-08 15:58:34 -07:00
Tom Moor 869fc086d6 feat: Templates (#1399)
* Migrations
* New from template
* fix: Don't allow public share of template
* chore: Template badges
* fix: Collection active
* feat: New doc button on template list item
* feat: New template menu
* fix: Sorting
* feat: Templates onboarding notice
* fix: New doc button showing on archived/deleted templates
2020-08-08 15:18:37 -07:00
dependabot[bot] 59c24aba7c chore(deps): bump elliptic from 6.5.2 to 6.5.3 (#1406)
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.2 to 6.5.3.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.2...v6.5.3)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-07-30 19:23:44 -07:00
Tom Moor bc4806ac30 feat: Allow checkboxes to be toggled without going into 'edit' mode (#1349) 2020-07-28 20:43:34 -07:00
Tom Moor 169ad5b025 feat: Sharing improvements (#1388)
* add migrations

* first pass at API

* feat: Updated share dialog UI

* tests

* test

* styling tweaks

* feat: Show share state on document

* fix: Allow publishing share links for draft docs

* test: shares.info
2020-07-28 19:14:32 -07:00
Tom Moor 0b33b5bc05 company -> team 2020-07-25 11:33:04 -07:00
Tom Moor 109efcaa27 chore: Remove WEBSOCKETS_ENABLED flag (#1383)
* chore: Remove WEBSOCKETS_ENALBED flag

* lint
2020-07-22 22:44:24 -07:00
Nan Yu 2cc6d7add8 fix: added missing call to onOpen (#1378) 2020-07-21 21:35:27 -07:00
Joona Heikkilä 003d82fe8a refactor: Fix updater's use of UPDATES_KEY (#1376) 2020-07-21 15:05:09 -07:00
Tom Moor f75a07cb0d fix: Remove ugly blue cross 2020-07-20 19:53:13 -07:00
Tom Moor 0d6720e499 fix: Heading style regressions 2020-07-20 19:43:30 -07:00
Tom Moor a97a1df5f1 fix: Extra active outline around editor toolbar buttons 2020-07-20 19:37:50 -07:00
Tom Moor 822395c265 chore: Remove emojis from welcome docs 2020-07-20 19:14:15 -07:00
Tom Moor 96d6e9b85e fix: Breadcrumb spacing 2020-07-20 19:09:51 -07:00
Tom Moor 70e1194f90 feat: Notice blocks available as new editor options (#1371)
* feat: Notice blocks available as new editor options

* fix: styling tweak
feat: Add shortcut to keyboard modal

* add notices to welcome docs

* styling tweaks

* styling tweaks

* styling tweaks
2020-07-20 19:03:14 -07:00
Tom Moor 710fcc697c feat: Add login link to /create page, closes OLN-63 2020-07-19 10:58:35 -07:00
Nan Yu 58f9e95d2f feat: nicer gradient mask for hover previews (#1367)
* feat: nicer gradient mask for hover previews

* tweak the stops on gradient mask
2020-07-18 18:25:54 -07:00
Tom Moor bc128359ab chore: Remove Spectrum references (#1366)
* fix: knowledgebase -> knowledge base

* chore: Remove links and mentions to Spectrum community
2020-07-18 17:19:13 -07:00
Tom Moor af09713c8c fix: knowledgebase -> knowledge base 2020-07-18 13:17:10 -07:00
Tom Moor 35052ef38f 0.45.0 2020-07-18 11:52:32 -07:00
Tom Moor ec3adc6d1c fix: 2 missed process.env spots on frontend 2020-07-18 11:33:34 -07:00
Tom Moor 67981a351e chore: Remove env variables in webpack bundle (#1353)
* chore: Remove env variables in webpack bundle

* remove unused globals

* refactor: consolidate window.env calls to single file

* fix: Slack client side integration auth

* fix: developers url
2020-07-18 11:02:40 -07:00
Necmettin Karakaya 24448c7504 fix: common misspelling errors
https://github.com/outline/outline/blob/master/app/scenes/UserDelete.js#L40: corrected "destory" to "destroy"
https://github.com/outline/outline/blob/master/app/components/ScrollToTop.js#L16: corrected "postion" to "position"
https://github.com/outline/outline/blob/master/server/policies/document.js#L11: corrected "existance" to "existence"
https://github.com/outline/outline/blob/master/server/policies/document.js#L23: corrected "existance" to "existence"
https://github.com/outline/outline/blob/master/server/models/Document.js#L493: corrected "permanantly" to "permanently"
https://github.com/outline/outline/blob/master/shared/utils/domains.js#L12: corrected "unneccessarily" to "unnecessarily"
https://github.com/outline/outline/blob/master/server/api/documents.js#L34: corrected "compatablity" to "compatibility"
2020-07-18 09:33:27 -07:00
Tom Moor d5b5d4fc27 feat: Document hover cards (#1346)
* stash

* refactor

* refactor, styling

* tweaks

* pointer

* styling

* fi: Hide when printing

* fix: No hover cards on shared links

* remove suppressions no longer needed

* fix: Don't show hover cards when editing, they get in the way

* fix: Prevent hover card from going off rhs edge of screen

* fix: Remount hover card when changing between links

* fix: allow one part domains in links (#1350)

* allow one part domains in links

* no TLD when only one part domain

* return null for parseDomain of empty string

* fix fiddly hover preview behavior

* WIP

* refactor hover preview

* fix: Non-rounded bottom corners

* fix: Fixes an edgecase where mounting the nested editor in hovercard causesdocument to scroll if there is a hash in the url

* fix: Incorrect document preview rendering

* lint

Co-authored-by: Nan Yu <thenanyu@gmail.com>
Co-authored-by: Nan Yu <nan@getoutline.com>
2020-07-16 21:26:23 -07:00
dependabot[bot] d8603cc961 chore(deps): bump lodash from 4.17.15 to 4.17.19 (#1361)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-07-16 17:30:56 -07:00
Tom Moor 943a290b83 chore: Update flow just far enough to get hooks libdefs (#1348) 2020-07-13 20:23:15 -07:00
Tom Moor f4a4f034cf Merge branch 'MatheusRV-feat/change-title-based-on-teamName' into develop 2020-07-13 19:42:57 -07:00
Tom Moor a53934a5c9 fix 2020-07-13 19:41:42 -07:00
Tom Moor e859e3b9e0 Merge branch 'feat/change-title-based-on-teamName' of https://github.com/MatheusRV/outline into MatheusRV-feat/change-title-based-on-teamName 2020-07-13 19:13:59 -07:00
Tom Moor b51d818db3 feat: Adds documents.export endpoint to return cleaned up Markdown (#1343) 2020-07-13 18:23:15 -07:00
Tom Moor bfea742650 fix: Makefile test runner 2020-07-11 12:41:48 -07:00
Tom Moor 9951cbf194 fix: Unneccessary redirect to /home 2020-07-11 09:51:10 -07:00
Tom Moor 5cb04d7ac1 New login screen (#1331)
* wip

* feat: first draft of auth.config

* chore: auth methodS

* chore: styling

* styling, styling, styling

* feat: Auth notices

* chore: Remove server-rendered pages, move shared/components -> components

* lint

* cleanup

* cleanup

* fix: Remove unused component

* fix: Ensure env variables in prod too

* style tweaks

* fix: Entering SSO email into login form fails
fix: Tweak language around guest signin
2020-07-09 22:33:07 -07:00
Tom Moor 75561079eb closes: Lucidchart embed does not work for app subdomain
closes #1340
2020-07-09 21:07:35 -07:00
Matheus Rocha Vieira d2c7f3f166 Update app/components/PageTitle.js
Co-authored-by: Tom Moor <tom.moor@gmail.com>
2020-07-08 14:48:14 -03:00
Matheus Rocha Vieira a077766bff Update app/components/Layout.js
Co-authored-by: Tom Moor <tom.moor@gmail.com>
2020-07-08 14:48:05 -03:00
Matheus Breguêz a25e03d7cd Fix Name Mistake & Lint 2020-07-07 09:11:19 -03:00
Tom Moor 2953d09ee1 fix: Bump RME 2020-07-05 20:44:37 -07:00
Tom Moor 28d15b1573 Update LICENSE 2020-07-03 21:14:21 -07:00
Tom Moor 18108cb359 0.44.0 2020-07-03 13:11:01 -07:00
Tom Moor 5dfa6a71a4 chore: Remove Lato
closes #1295
2020-07-01 21:40:34 -07:00
Matheus Breguêz 35270cd104 add mobx observer and inject 2020-07-01 09:14:21 -03:00
Matheus Breguêz 19d01ed575 Add Outline in Title 2020-07-01 09:09:21 -03:00
Matheus Breguêz 2b7639c903 Improve Title when TeamName is empty 2020-07-01 08:56:49 -03:00
Matheus Breguêz c8269ca134 Remove Polices 2020-06-29 17:06:47 -03:00
Matheus Breguêz da9fc5bfdf New title based on TeamName 2020-06-29 17:01:34 -03:00
Tom Moor efcfda8398 fix: Port from hosted 2020-06-22 22:11:39 -07:00
Tom Moor ce2f69342c Fix: Port fix from hosted 2020-06-22 22:00:40 -07:00
Tom Moor 9be5597f4b chore: Lint shared directory 2020-06-22 21:29:21 -07:00
Tom Moor 26f6961c82 chore: Include new brand colors in icon color suggestions 2020-06-22 21:24:42 -07:00
Tom Moor bf5f83e97d Subtle styling adjustments to dropdown menus 2020-06-22 21:18:43 -07:00
Tom Moor 8db0260a1a Bump RME 2020-06-22 21:10:24 -07:00
Tom Moor edbae275ae Bump RME 2020-06-22 20:55:29 -07:00
Tom Moor f43deb7940 chore: Move to prettier standard double quotes (#1309) 2020-06-20 13:59:15 -07:00
Tom Moor 2a3b9e2104 chore: Make editor version comparison more lenient to reduce forced reloads 2020-06-20 12:35:32 -07:00
Tom Moor 64c3ff8d6b chore: Remove 'DEPLOYMENT' env option
Add 'Installation' section
2020-06-19 19:11:02 -07:00
Tom Moor 4f7e7ec853 Update package.json to match reality
This will be kept up to date from now on
2020-06-19 17:34:59 -07:00
Tom Moor d864e228e7 feat: Collection Icons (#1281)
* wip: Working for creation, and display

* feat: IconPicker

* fix

* feat: Invert collection icon color when dark in dark mode

* Improve readability of dropdown menus in dark mode
Suggest icon based on collection name

* Add additional icons
Tweaks and final polish

* fix: Write default icon as empty icon column

* feat: Improve icon selection logic
add more keywords
Improve icon coloring when selected and in dark mode

* lint

* lint
2020-06-19 17:18:03 -07:00
Tom Moor f3ea02fdd0 Bump RME, closes #1305 2020-06-18 23:54:02 -07:00
Tom Moor ed096013e4 fix: Cleanup print display 2020-06-18 22:10:20 -07:00
Tom Moor 2e376b32ec fix: sp mistake 2020-06-18 08:49:10 -07:00
Tom Moor f352b35e13 Bump RME 2020-06-16 21:11:01 -07:00
Tom Moor 0f8d503df8 chore: API Consistency (#1304)
* chore: Addressing API inconsistencies

* lint

* add: Missing sort to groups.list
fix: Documention issues

* test: fix

* feat: Add missing shares.info endpoint

* feat: Add sorting to users.list endpoint

* fix: Incorrect pagination parameters listed on user endpoints

* users.s3Upload -> attachments.create

* chore: exportAll -> export_all
2020-06-16 20:56:17 -07:00
Tom Moor 5010b08e83 Bump RME 2020-06-12 22:15:48 -07:00
Tom Moor 933bbdfb84 feat: Add ability to create docs from link editor (#1303)
* feat: Add ability to create docs from link editor

* fix: Handling of paste and click events

* fix: Filter untitled documents from search results

* refactor: Move onCreateLink to DataLoader

* bump rme
2020-06-12 00:19:03 -07:00
Tom Moor d25a9d56dc fix: updatedBy incorrect in documents.update response 2020-06-12 00:18:38 -07:00
Tom Moor 1d8c3f3faf fix: Suspended users showing in group and collection member management (#1302)
* fix: Suspended users showing in group and collection member management

* test
2020-06-11 08:48:47 -07:00
Tom Moor d7766280a9 fix: Possible fix for intermittent CI failures
https://github.com/facebook/flow/issues/8058
2020-06-09 21:50:25 -07:00
moekhalil ae5eff2914 Make MenuItem Links work (#1299)
Menu items pointed to anchors links that were non-existent. 
Adding id's to proper sections for anchor links in menu to work.
2020-06-09 20:39:22 -07:00
Tom Moor b444874944 fix: Shared documents with system in dark mode display partially on light background
closes #1300
2020-06-09 20:38:34 -07:00
Tom Moor 20efa82ad9 Add additional restricted subdomains 2020-06-05 23:46:32 -07:00
Tom Moor bd9d4b3d0d fix: backslash in search query not escaped 2020-06-03 23:59:59 -07:00
Tom Moor 4b4f4fd188 flow 2020-06-02 23:17:54 -07:00
Tom Moor 33815639f2 fix: Improved handling of simultaneous edits 2020-06-02 23:16:15 -07:00
Tom Moor 05e24df226 fix: Update value when saved elsewhere and viewing doc
closes #1103
2020-05-31 22:46:55 -07:00
Tom Moor d2d9164fa1 chore: Remove unused component 2020-05-31 21:19:44 -07:00
Tom Moor f2eb395e8d fix: regex -> startsWith 2020-05-31 16:43:51 -07:00
Tom Moor 133cb56cca fix: Use real icon on settings back arrow 2020-05-30 18:26:40 -07:00
Tom Moor e752dba566 fix: User profile should say 'invited' instead of 'joined' when user has yet to signin 2020-05-30 18:26:40 -07:00
Tom Moor c1a141d99f fix: Make it possible to downgrade permissions of suspended user
closes #1291
2020-05-30 18:26:40 -07:00
Tom Moor ccedea55d6 chore: Reduce loading jank in sidebar (#1294) 2020-05-30 12:50:08 -07:00
Tom Moor c929f83813 feat: Improved error filtering and reporting (#1293) 2020-05-29 07:22:09 -07:00
Tom Moor 1b25d12e2e fix: Regression with upgrade to styled-components@5 – new ServerStyleSheet needed per render 2020-05-28 20:49:07 -07:00
Tom Moor 0c254285a1 fix: Improved readability of input placeholders in dark mode 2020-05-28 20:08:02 -07:00
Tom Moor 8b274c3713 fix: Keyboard shortcuts dialog shortcut should not be active when editing a document
closes #1292
2020-05-28 19:44:52 -07:00
Tom Moor 5a20a35c8b Bump RME 2020-05-28 08:25:25 -07:00
Tom Moor 72635e98e6 Remove precommit hook 2020-05-28 08:25:25 -07:00
Tom Moor 7d55b7f69b fix: Server error when listing memberships for group with deleted user (#1288)
* fix: Server error when listing memberships for group with deleted user

* PR feedback: Filter before slice
2020-05-28 08:21:22 -07:00
Tom Moor ca1ba277ad fix: Gist embed error on load 2020-05-27 23:04:34 -07:00
Tom Moor 32562886aa fix: Bump RME, closes #1286 2020-05-26 20:18:07 -07:00
Tom Moor 7f07cb57a2 chore: Capture event data to error tracker when background jobs fail 2020-05-25 13:48:50 -07:00
Tom Moor 62dd1e41f9 fix: Invariant violation reported to error tracker when client is reloading due to editor update 2020-05-25 13:10:18 -07:00
Tom Moor 446a9ade8c fix: Imported documents should get a best-guess title 2020-05-24 23:10:55 -07:00
Tom Moor a281b1e5be fix: Documents with empty titles (hey, it can happen) are invisible in the sidebar) 2020-05-24 22:43:54 -07:00
Tom Moor 1b5600b025 chore: Cleaner Markdown output when exporting docs 2020-05-24 22:22:06 -07:00
Tom Moor f0e513542d fix: auto focus search input when adding users to a collection 2020-05-24 22:17:11 -07:00
Tom Moor 4797a5fc77 fix: Invalid regex error on Safari when filtering users with certain characters 2020-05-24 22:14:09 -07:00
Tom Moor f05035c0b7 fix: Incorrect overlay background when zooming images 2020-05-23 22:22:19 -07:00
Tom Moor 75cfbd8970 fix: More migration tweaks 2020-05-23 19:23:31 -07:00
Tom Moor c4837f1943 fix: Potential corruption migrating v1 docs to v2 when a mark is adjacent the same mark. This was a quirk of the old editor 2020-05-23 12:26:26 -07:00
Tom Moor 0b3166dab5 Bump yarn.lock 2020-05-23 11:06:37 -07:00
Tom Moor 603f7962a2 fix: An unknown language annotation in code fences will no longer throw an error. It was only possible to hit this if you imported markdown from another source.
closes #1284
2020-05-23 11:01:07 -07:00
Tom Moor 69d16dd1d4 fix: Tables with empty cells are corrupted when converted to v2 documents
fix: Marks with trailing or leading empty spaces are corrupted when converted to v2 documents
2020-05-23 10:58:46 -07:00
Tom Moor e2ffe06221 fix: Document title not centered once scrolled 2020-05-22 14:19:26 -07:00
Tom Moor 6e2ea3ac4b fix: overflow menu on history revision is incorrect color when selected
closes #1140
2020-05-21 22:22:04 -07:00
Tom Moor f4c4a11277 fix: Only transfer accessToken if matches root token 2020-05-21 21:42:46 -07:00
Tom Moor c28dc08f6a fix: Alignment of bullet list
closes #1277
2020-05-21 21:22:33 -07:00
Tom Moor 4ef82d5476 test 2020-05-20 23:48:37 -07:00
Tom Moor 3487eb7857 fix: Submenu auto close 2020-05-20 23:40:54 -07:00
Tom Moor 87020348ce lint 2020-05-20 23:20:05 -07:00
Tom Moor 672ffacc5b feat: Have theme follow system pref 2020-05-20 23:19:07 -07:00
Tom Moor 218b0ea76a fix: Unable to edit starred documents
closes #1275
2020-05-20 21:42:26 -07:00
Tom Moor 47ff6feaee fix: JS error on server 2020-05-20 21:03:53 -07:00
Tom Moor 8ac5a574f3 Merge branch 'master' of github.com:outline/outline 2020-05-20 21:01:10 -07:00
Ante Primorac aed76c7bcb fix revisions backup data type (#1274) 2020-05-20 10:50:52 -07:00
Tom Moor 7f9cd51f37 Update bug template to account for self hosted 2020-05-20 08:30:05 -07:00
Tom Moor 092b29ee63 fix: Breadcrumb separator color in dark mode 2020-05-19 22:34:04 -07:00
Tom Moor ee7e1959c9 fix: Toast color in dark mode 2020-05-19 21:55:26 -07:00
Tom Moor 092d9dce18 fix: Don't set cookie domain when not using multiple subdomains (#1145)
* fix: Don't set cookie domain when not using multiple subdomains

* wip logging domain

* wip logging domain

* wip logging domain

* wip logging domain

* Revert "wip logging domain"

This reverts commit 325907e749.

* Revert "wip logging domain"

This reverts commit 6ee095a49e.

* Revert "wip logging domain"

This reverts commit 813d8eb960.

* Revert "wip logging domain"

This reverts commit f1ca819276.

* Remove SUBDOMAINS_ENABLED from documented env variables, no-one self hosting should need this – it just adds confusion to those looking to host on a single subdomain
fix: Account for server/client process.env parsing

Co-authored-by: Nan Yu <nanyu@Nans-MBP-2.lan>
Co-authored-by: Nan Yu <nan@getoutline.com>
2020-05-19 21:05:57 -07:00
Tom Moor 9274005cbb feat: Upgrade editor (#1227)
* WIP

* document migration

* fix: Handle clashing keyboard events

* fix: convert getSummary

* fix: parseDocumentIds

* lint

* fix: Remove unused plugin

* Move editor version to header
Add editor version check for API endpoints

* fix: Editor update auto-reload
Bump RME

* test

* bump rme

* Remove slate flow types, improve themeing, bump rme

* bump rme

* fix: parseDocumentIds returning duplicate ID's, improved regression tests

* test

* fix: Missing code styles

* lint

* chore: Upgrade v2 migration to use AST

* Bump RME

* Update welcome doc

* add highlight to keyboard shortcuts ref

* theming improvements

* fix: Code comments show as headings, closes #1255

* loop

* fix: TOC highlighting

* lint

* add: Automated backup of docs before migration

* Update embeds to new format

* fix: React warning

* bump to final editor version 10.0.0

* test
2020-05-19 20:39:34 -07:00
Tom Moor 400a1c87bb lint 2020-05-18 20:22:58 -07:00
Tom Moor 93abbab44a feat: Show different icon in theme menu to switch to light mode 2020-05-17 19:15:55 -07:00
Tom Moor 18cf148bd1 chore: Improve performance in dev by running Node/Yarn outside of docker (#1271)
* Improve performance in dev by running Node/Yarn outside of docker

* Transpose exposed port numbers by 100, so less likely conflict with host processes
2020-05-17 18:12:48 -07:00
Tom Moor e0b33ee576 chore: Auto reload frontend of client is out of date (#1270)
* Move editor version to header
Add editor version check for API endpoints

* fix: Editor update auto-reload
Bump RME

* fix: Only redirect if editor header exists

* lint
2020-05-16 14:05:51 -07:00
Tom Moor 82749ffbd8 Remove Spectrum badge 2020-05-12 08:27:17 -07:00
Tom Moor 231aab6ef8 lint 2020-05-12 08:26:31 -07:00
Tom Moor f8077e2125 Add github template chooser 2020-05-12 08:25:11 -07:00
Tom Moor 9d9435cce5 fix: Server error in hooks.slack endpoint if team has no public collections 2020-05-10 18:49:57 -07:00
Tom Moor 5deec264bc chore: Add request-id to error tracking 2020-05-10 16:24:34 -07:00
Tom Moor 48c87a1902 fix: Long titles should wrap
closes #1249
2020-05-07 21:21:58 -07:00
Tom Moor 06a3258b99 fix: Allow empty document body to be saved
closes #1258
2020-05-07 20:52:02 -07:00
Tom Moor bd2837250b fix: Guard missing attachment, closes #1262 2020-05-07 20:37:36 -07:00
Tom Moor e1adb16c43 fix: Incompatible with node 14 2020-05-07 20:33:33 -07:00
Tom Moor d914ecb603 chore: Remove max listener warning in console 2020-04-26 14:08:39 -07:00
Tom Moor 187be4737e fix: Log errors to console when Sentry not installed 2020-04-25 19:53:24 -07:00
Tom Moor 870b91f17a fix: Various extra scrollbars when not using mac-style overlaying scrollbars (#1242)
* fix: Various extra scrollbars when not using mac-style overlaying scrollbars

* Sidebar z-index
2020-04-24 18:44:21 -07:00
Tom Moor 6db92c9f49 remove unused images 2020-04-24 05:33:13 -07:00
Tom Moor c41e6e0423 fix: Minor fixes in backlinks service (#1240) 2020-04-24 05:29:48 -07:00
Tom Moor 9f8e7be755 fix: Restore ability to disable embeds on a document (#1238)
closes #1237
2020-04-21 21:43:01 -07:00
Tom Moor cead37051e fix: test prevents server loading, add logs 2020-04-19 22:14:31 -07:00
Tom Moor fd99da96af chore: upgrade deps 2020-04-19 22:07:17 -07:00
Tom Moor c526adf292 feat: Auto update titles in linked documents (#1233)
* feat: Auto update titles in linked documents

* Add spec
2020-04-19 21:58:42 -07:00
Tom Moor ee5ae140c3 fix: Improve neutral button styling in dark mode 2020-04-11 09:47:52 -07:00
Tom Moor 083ac0d840 fix: Image cropper in dark mode
closes #1229
2020-04-11 09:34:33 -07:00
Tom Moor fbaaa08ec7 closes #1230 2020-04-11 09:27:14 -07:00
Tom Moor b536c682a2 fix: Escape characters visible in TOC
closes #1226
2020-04-06 09:01:03 -07:00
Tom Moor c94823dd59 fix: Failed editor chunk load should refresh page 2020-04-06 08:50:43 -07:00
Tom Moor 1a60f51460 fix: Attempt to focus readonly editor
fix: Non-grow clickable padding beneath editor regression
2020-04-05 22:48:48 -07:00
Tom Moor abf91a3a51 fix: Share link rendering 2020-04-05 22:42:55 -07:00
Tom Moor 383806d155 fix: Document shrinks if only content is embed 2020-04-05 18:44:05 -07:00
Tom Moor 7413e8bf7a Update LICENSE 2020-04-05 18:18:01 -07:00
Tom Moor 7c1aa7622a Update README screenshot 2020-04-05 17:01:54 -07:00
Tom Moor f94efaada8 Update README screenshot 2020-04-05 17:00:30 -07:00
Tom Moor 283a762a9c fix: Title index 2020-04-05 16:46:03 -07:00
Tom Moor cef687464a chore: Improved onboarding docs 2020-04-05 16:32:12 -07:00
Tom Moor 8287355261 CI 2020-04-05 16:11:16 -07:00
Tom Moor 02d33267cc fix: Document updated email does include team subdomain in url
fix: Send document updated emails to any collaborators
fix: Correct quotation marks in email subjects
2020-04-05 16:04:46 -07:00
Tom Moor b964bdbe90 lint 2020-04-05 15:53:27 -07:00
Tom Moor c832265e8a fix: Account for emoji-offset title 2020-04-05 15:50:37 -07:00
Tom Moor 8819a0836e fix: Initial welcome docs account for new title field 2020-04-05 15:41:29 -07:00
Tom Moor 9338a54fe0 feat: Separate title from body (#1216)
* first pass at updating all Time components each second

* fix a couple date variable typos

* use class style state management instead of hooks

* wip: Separate title from body

* address feedback

* test: Remove unused test

* feat: You in publishing info language
fix: Removal of secondary headings

* After much deliberation… a migration is needed for this to be reliable

* fix: Export to work with new title structure

* fix: Untitled

* fix: Consistent spacing of first editor node

* fix: Emoji in title handling

* fix: Time component not updating for new props

* chore: Add createdAt case

* fix: Conflict after merging new TOC

* PR feedback

* lint

* fix: Heading level adjustment

Co-authored-by: Taylor Lapeyre <taylorlapeyre@gmail.com>
2020-04-05 15:07:34 -07:00
Tom Moor a0e73bf4c2 fix: Add shortcut for toggling document contents to help 2020-04-05 13:33:50 -07:00
Tom Moor 8a0263093b fix: Back button on modals should not scroll off screen 2020-04-05 13:33:21 -07:00
Tom Moor 9d8e99400f fix: Various React errors in console 2020-04-05 13:27:11 -07:00
Tom Moor 98b5350c65 lint 2020-04-05 13:21:03 -07:00
Tom Moor 3bd1c1f047 fix: Doc history does not load when linked to directly (race condition) 2020-04-05 13:20:47 -07:00
Tom Moor 9712b8d205 fix: Document history should scroll separate to page content
closes #1225
2020-04-05 13:07:16 -07:00
Tom Moor 597c09d2bc fix: Non-toc horizontal heading alignment 2020-04-05 12:50:38 -07:00
Tom Moor d0606a72c3 feat: Improved table of contents (#1223)
* feat: New table of contents

* fix: Hide TOC in edit mode

* feat: Highlight follows scroll position

* scroll tracking

* UI

* fix: Unrelated css fix with long doc titles

* Improve responsiveness

* feat: Add keyboard shortcut access to TOC

* fix: Headings should reflect content correctly when viewing old document revision

* flow

* fix: Persist TOC choice between sessions
2020-04-05 12:22:26 -07:00
Nan Yu 0deecfac44 make the team logo a little friendlier for first timers (#1222) 2020-04-04 15:34:11 -07:00
Nan Yu f534203cbd Merge pull request #1218 from outline/thenanyu-patch-1
docs: minor ergonomic updates to readme
2020-03-30 16:43:27 -07:00
Nan Yu f521543b0a Update README.md 2020-03-30 08:57:21 -07:00
Nan Yu 7da0d7589e minor ergonomic updates to readme 2020-03-30 08:26:37 -07:00
Tom Moor 09dea295a2 fix: Cleanup S3 Attachments (#1217)
* fix: Server error if attempting to load an unknown attachment

* fix: Migration should cascade delete to document attachments

* fix: Delete S3 attachments along with documents
2020-03-28 15:56:01 -07:00
Taylor Lapeyre d3773dc943 fix: Update all Time components each second (#1214)
* first pass at updating all Time components each second

* fix a couple date variable typos

* use class style state management instead of hooks

* address feedback

* lint

Co-authored-by: Tom Moor <tom.moor@gmail.com>
2020-03-23 22:31:17 -07:00
Tom Moor 5db43f2607 lint 2020-03-23 22:26:17 -07:00
Tom Moor 4b3ddf8769 fix: Account for no text param passed to hooks.slack
This will never happen in production, Slack always provides the param but prevents a possible 500 server error when messing with the API manually.
2020-03-22 16:41:15 -07:00
Tom Moor 7a6ed86c95 Update LICENSE 2020-03-16 23:19:16 -07:00
Tom Moor f0be9beeb4 feat: Ensure that editorVersion is saved with document/revisions (#1212) 2020-03-16 08:30:23 -07:00
Tom Moor 4851f51d8b docs: Add documentation for groups API endpoints (#1211)
* docs: Add documentation for groups API endpoints
fix: Remove useless permission query param

* restore permission filter on collections.group_memberships

* tweak language
2020-03-16 08:30:15 -07:00
David Miranda e611979a8d Absolute routes won't work when users have subdomains (#1208) 2020-03-15 21:04:46 -07:00
Bryan Joseph 05af318a1d feat: Split up check for Slack environment variables (#1207)
* Split up check for Slack environment variables

This allows for the Slack slash command to be used without
needing Slack sign in enabled.

* Remove conditional checking for slack app id

Co-authored-by: Bryan Joseph <bryanjos@users.noreply.github.com>
2020-03-15 20:53:15 -07:00
Nan Yu 142303b3de feat: Add groups and group permissions (#1204)
* WIP - got one API test to pass yay

* adds group update endpoint

* added group policies

* adds groups.list API

* adds groups.info

* remove comment

* WIP

* tests for delete

* adds group membership list

* adds tests for groups list

* add and remove user endpoints for group

* ask some questions

* fix up some issues around primary keys

* remove export from group permissions

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* remove random file

* only create events on actual updates, add tests to ensure

* adds uniqueness validation to group name

* throw validation errors on model and let it pass through the controller

* fix linting

* WIP

* WIP

* WIP

* WIP

* WIP basic edit and delete

* basic CRUD for groups and memberships in place

* got member counts working

* add member count and limit the number of users sent over teh wire to 6

* factor avatar with AvatarWithPresence into its own class

* wip

* WIP avatars in group lists

* WIP collection groups

* add and remove group endpoints

* wip add collection groups

* wip get group adding to collections to work

* wip get updating collection group memberships to work

* wip get new group modal working

* add tests for collection index

* include collection groups in the withmemberships scope

* tie permissions to group memberships

* remove unused import

* Update app/components/GroupListItem.js

update title copy

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update server/migrations/20191211044318-create-groups.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update server/api/groups.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update server/api/groups.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update app/menus/CollectionMenu.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update server/models/Group.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* minor fixes

* Update app/scenes/CollectionMembers/AddGroupsToCollection.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update app/menus/GroupMenu.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update app/menus/GroupMenu.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update app/menus/GroupMenu.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update app/scenes/Collection.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update app/scenes/CollectionMembers/CollectionMembers.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update app/scenes/GroupNew.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update app/scenes/GroupNew.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update app/scenes/Settings/Groups.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update server/api/documents.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* Update app/scenes/CollectionMembers/components/CollectionGroupMemberListItem.js

Co-Authored-By: Tom Moor <tom.moor@gmail.com>

* address comments

* WIP - getting websocket stuff up and running

* socket event for group deletion

* wrapped up cascading deletes

* lint

* flow

* fix: UI feedback

* fix: Facepile size

* fix: Lots of missing await's

* Allow clicking facepile on group list item to open members

* remove unused route push, grammar

* fix: Remove bad analytics events
feat: Add group events to audit log

* collection. -> collections.

* Add groups to entity websocket events (sync create/update/delete) between clients

* fix: Users should not be able to see groups they are not a member of

* fix: Not caching errors in UI when changing group memberships

* fix: Hide unusable UI

* test

* fix: Tweak language

* feat: Automatically open 'add member' modal after creating group

Co-authored-by: Tom Moor <tom.moor@gmail.com>
2020-03-14 20:48:32 -07:00
Tom Moor 6c451a34d4 Update README.md 2020-03-13 20:15:57 -07:00
Tom Moor fa4f1846ec Update LICENSE (#1197) 2020-03-07 10:10:42 -08:00
Tom Moor 4baf5ce99a test: Google embeds (#1202)
Update slides to only embed pub links
2020-03-07 10:10:26 -08:00
Tom Moor 533ec3bd9c add: Support for published google docs / sheets 2020-03-06 22:45:32 -08:00
Tom Moor 572127b830 fix: Point changelog link to public site
Related to #1195
2020-02-28 19:07:25 -08:00
705 changed files with 41994 additions and 27428 deletions
+25 -14
View File
@@ -1,18 +1,29 @@
{
"presets": ["react", "env"],
"presets": [
"@babel/preset-react",
"@babel/preset-flow",
[
"@babel/preset-env",
{
"corejs": {
"version": "2",
"proposals": true
},
"useBuiltIns": "usage"
}
]
],
"plugins": [
"lodash",
"styled-components",
"transform-decorators-legacy",
"transform-es2015-destructuring",
"transform-object-rest-spread",
"transform-regenerator",
"transform-class-properties",
"syntax-dynamic-import"
],
"env": {
"development": {
"presets": ["react-hmre"]
}
}
}
[
"@babel/plugin-proposal-decorators",
{
"legacy": true
}
],
"@babel/plugin-transform-destructuring",
"@babel/plugin-transform-regenerator",
"transform-class-properties"
]
}
+7 -4
View File
@@ -29,12 +29,15 @@ jobs:
- run:
name: migrate
command: ./node_modules/.bin/sequelize db:migrate --url $DATABASE_URL_TEST
- run:
name: test
command: yarn test
- run:
name: lint
command: yarn lint
- run:
name: flow
command: yarn flow
command: yarn flow check --max-workers 4
- run:
name: test
command: yarn test
- run:
name: build
command: yarn build
+5 -7
View File
@@ -6,9 +6,9 @@
SECRET_KEY=generate_a_new_key
UTILS_SECRET=generate_a_new_key
DATABASE_URL=postgres://user:pass@postgres:5432/outline
DATABASE_URL_TEST=postgres://user:pass@postgres:5432/outline-test
REDIS_URL=redis://redis:6379
DATABASE_URL=postgres://user:pass@localhost:5532/outline
DATABASE_URL_TEST=postgres://user:pass@localhost:5532/outline-test
REDIS_URL=redis://localhost:6479
URL=http://localhost:3000
PORT=3000
@@ -17,10 +17,7 @@ PORT=3000
# set to false if your SSL is terminated at a loadbalancer, for example
FORCE_HTTPS=true
DEPLOYMENT=self
ENABLE_UPDATES=true
SUBDOMAINS_ENABLED=false
WEBSOCKETS_ENABLED=true
DEBUG=cache,presenters,events
# Third party signin credentials (at least one is required)
@@ -60,4 +57,5 @@ SMTP_PASSWORD=
SMTP_FROM_EMAIL=
SMTP_REPLY_EMAIL=
TEAM_LOGO=https://example.com/images/logo.png
# Custom logo that displays on the authentication screen, scaled to height: 60px
# TEAM_LOGO=https://example.com/images/logo.png
+60 -16
View File
@@ -6,11 +6,54 @@
"plugin:import/warnings",
"plugin:flowtype/recommended"
],
"plugins": ["prettier", "flowtype"],
"plugins": [
"prettier",
"flowtype"
],
"rules": {
"eqeqeq": 2,
"no-unused-vars": 2,
"no-mixed-operators": "off",
"import/order": [
"error",
{
"alphabetize": {
"order": "asc"
},
"pathGroups": [
{
"pattern": "shared/**",
"group": "external",
"position": "after"
},
{
"pattern": "stores",
"group": "external",
"position": "after"
},
{
"pattern": "stores/**",
"group": "external",
"position": "after"
},
{
"pattern": "models/**",
"group": "external",
"position": "after"
},
{
"pattern": "scenes/**",
"group": "external",
"position": "after"
},
{
"pattern": "components/**",
"group": "external",
"position": "after"
}
]
}
],
"flowtype/require-valid-file-annotation": [
2,
"always",
@@ -18,14 +61,19 @@
"annotationStyle": "line"
}
],
"flowtype/space-after-type-colon": [2, "always"],
"flowtype/space-before-type-colon": [2, "never"],
"flowtype/space-after-type-colon": [
2,
"always"
],
"flowtype/space-before-type-colon": [
2,
"never"
],
"prettier/prettier": [
"error",
{
"printWidth": 80,
"trailingComma": "es5",
"singleQuote": true
"trailingComma": "es5"
}
]
},
@@ -33,12 +81,14 @@
"react": {
"createClass": "createReactClass",
"pragma": "React",
"version": "detect",
"flowVersion": "0.86"
"version": "detect"
},
"import/resolver": {
"node": {
"paths": ["app", "."]
"paths": [
"app",
"."
]
}
},
"flowtype": {
@@ -49,12 +99,6 @@
"jest": true
},
"globals": {
"__DEV__": true,
"SLACK_KEY": true,
"DEPLOYMENT": true,
"BASE_URL": true,
"SENTRY_DSN": true,
"afterAll": true,
"Sentry": true
"EDITOR_VERSION": true
}
}
}
+1 -4
View File
@@ -7,12 +7,9 @@
.*/node_modules/tiny-cookie/flow/.*
.*/node_modules/styled-components/.*
.*/node_modules/polished/.*
.*/node_modules/mobx/.*.flow
.*/node_modules/react-side-effect/.*
.*/node_modules/fbjs/.*
.*/node_modules/slate-edit-code/example/.*
.*/node_modules/slate-edit-code/lib/.*
.*/node_modules/slate-edit-list/.*
.*/node_modules/slate-prism/.*
.*/node_modules/config-chain/.*
.*/server/scripts/.*
*.test.js
-1
View File
@@ -1 +0,0 @@
yarn flow
+37
View File
@@ -0,0 +1,37 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots or videos to help explain your problem.
**Outline (please complete the following information):**
- Install: [getoutline.com or self hosted]
- Version: [commit sha if self hosted]
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Mobile (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
+8
View File
@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Feature request
url: https://github.com/outline/outline/discussions/new
about: Request a feature to be added to the project
- name: Self hosting questions
url: https://github.com/outline/outline/discussions/new
about: Ask questions and discuss running Outline with community members
+4 -1
View File
@@ -1,3 +1,6 @@
{
"javascript.validate.enable": false
"javascript.validate.enable": false,
"typescript.validate.enable": false,
"editor.formatOnSave": true,
"typescript.format.enable": false
}
+2 -1
View File
@@ -9,8 +9,9 @@ WORKDIR $APP_PATH
COPY . $APP_PATH
RUN yarn install --pure-lockfile
RUN yarn build
RUN cp -r /opt/outline/node_modules /opt/node_modules
CMD yarn build && yarn start
CMD yarn start
EXPOSE 3000
+98 -14
View File
@@ -1,19 +1,103 @@
Copyright (c) 2020 General Outline, Inc (https://www.getoutline.com/) and individual contributors.
All rights reserved.
Business Source License 1.1
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
Parameters
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
Licensor: General Outline, Inc.
Licensed Work: Outline 0.46.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
Service.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
A “Document Service” is a commercial offering that
allows third parties (other than your employees and
contractors) to access the functionality of the
Licensed Work by creating teams and documents
controlled by such third parties.
3. Neither the name of the Outline nor the names of its contributors may be used to endorse or promote products derived from this software
without specific prior written permission.
Change Date: 2023-08-12
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Change License: Apache License, Version 2.0
For information about alternative licensing arrangements for the Software,
please visit: https://www.getoutline.com
Notice
The Business Source License (this document, or the “License”) is not an Open
Source license. However, the Licensed Work will eventually be made available
under an Open Source License, as stated in this License.
License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
“Business Source License” is a trademark of MariaDB Corporation Ab.
-----------------------------------------------------------------------------
Business Source License 1.1
Terms
The Licensor hereby grants you the right to copy, modify, create derivative
works, redistribute, and make non-production use of the Licensed Work. The
Licensor may make an Additional Use Grant, above, permitting limited
production use.
Effective on the Change Date, or the fourth anniversary of the first publicly
available distribution of a specific version of the Licensed Work under this
License, whichever comes first, the Licensor hereby grants you rights under
the terms of the Change License, and the rights granted in the paragraph
above terminate.
If your use of the Licensed Work does not comply with the requirements
currently in effect as described in this License, you must purchase a
commercial license from the Licensor, its affiliated entities, or authorized
resellers, or you must refrain from using the Licensed Work.
All copies of the original and modified Licensed Work, and derivative works
of the Licensed Work, are subject to this License. This License applies
separately for each version of the Licensed Work and the Change Date may vary
for each version of the Licensed Work released by Licensor.
You must conspicuously display this License on each original or modified copy
of the Licensed Work. If you receive the Licensed Work in original or
modified form from a third party, the terms and conditions set forth in this
License apply to your use of that work.
Any use of the Licensed Work in violation of this License will automatically
terminate your rights under this License for the current and all other
versions of the Licensed Work.
This License does not grant you any right in any trademark or logo of
Licensor or its affiliates (provided that you may use a trademark or logo of
Licensor as expressly required by this License).
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
TITLE.
MariaDB hereby grants you permission to use this Licenses text to license
your works, and to refer to it using the trademark “Business Source License”,
as long as you comply with the Covenants of Licensor below.
Covenants of Licensor
In consideration of the right to use this Licenses text and the “Business
Source License” name and trademark, Licensor covenants to MariaDB, and to all
other recipients of the licensed work to be provided by Licensor:
1. To specify as the Change License the GPL Version 2.0 or any later version,
or a license that is compatible with GPL Version 2.0 or a later version,
where “compatible” means that software provided under the Change License can
be included in a program with software provided under GPL Version 2.0 or a
later version. Licensor may specify additional Change Licenses without
limitation.
2. To either: (a) specify an additional grant of rights to use that does not
impose any additional restriction on the right granted in this License, as
the Additional Use Grant; or (b) insert the text “None”.
3. To specify a Change Date.
4. Not to modify this License in any other way.
+7 -4
View File
@@ -1,16 +1,19 @@
up:
docker-compose up -d redis postgres s3
docker-compose run --rm outline /bin/sh -c "yarn && yarn sequelize db:migrate"
docker-compose up outline
yarn install --pure-lockfile
yarn sequelize db:migrate
yarn dev
build:
docker-compose build --pull outline
test:
docker-compose run --rm outline yarn test
docker-compose up -d redis postgres s3
yarn test
watch:
docker-compose run --rm outline yarn test:watch
docker-compose up -d redis postgres s3
yarn test:watch
destroy:
docker-compose stop
+18 -14
View File
@@ -1,14 +1,15 @@
<p align="center">
<img src="https://user-images.githubusercontent.com/31465/34380645-bd67f474-eb0b-11e7-8d03-0151c1730654.png" height="29" />
</p>
<p align="center">
<i>An open, extensible, wiki for your team built using React and Node.js.<br/>Try out Outline using our hosted version at <a href="https://www.getoutline.com">www.getoutline.com</a>.</i>
<br/>
<img src="https://user-images.githubusercontent.com/31465/34456332-51e41eb0-ed9c-11e7-9fa9-20e7fa946494.jpg" alt="Outline" width="800" />
<img src="https://user-images.githubusercontent.com/380914/78513257-153ae080-775f-11ea-9b49-1e1939451a3e.png" alt="Outline" width="800" />
</p>
<p align="center">
<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://spectrum.chat/outline" rel="nofollow"><img src="https://withspectrum.github.io/badge/badge.svg" alt="Join the community on Spectrum"/></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>
</p>
@@ -32,15 +33,20 @@ Outline requires the following dependencies:
In development you can quickly get an environment running using Docker by following these steps:
1. Install these dependencies if you don't already have them
1. [Docker for Desktop](https://www.docker.com)
1. [Node.js](https://nodejs.org/) (v12 LTS preferred)
1. [Yarn](https://yarnpkg.com)
1. Clone this repo
1. Install [Docker for Desktop](https://www.docker.com) if you don't already have it
1. Register a Slack app at https://api.slack.com/apps
1. Copy the file `.env.sample` to `.env`
1. Fill out the following fields:
1. `SECRET_KEY` (follow instructions in the comments of `.env`)
1. `SECRET_KEY` (follow instructions in the comments at the top of `.env`)
1. `SLACK_KEY` (this is called "Client ID" in Slack admin)
1. `SLACK_SECRET`
1. Add `http://localhost:3000/auth/slack.callback` as an Oauth callback URL in Slack App settings
1. `SLACK_SECRET` (this is called "Client Secret" in Slack admin)
1. Configure your Slack app's Oauth & Permissions settings
1. Add `http://localhost:3000/auth/slack.callback` as an Oauth redirect URL
1. Ensure that the bot token scope contains at least `users:read`
1. Run `make up`. This will download dependencies, build and launch a development version of Outline
@@ -50,13 +56,13 @@ For a self-hosted production installation there is more flexibility, but these a
1. Clone this repo and install dependencies with `yarn` or `npm install`
> Requires [Node.js, npm](https://nodejs.org/) and [yarn](https://yarnpkg.com) installed
> Requires [Node.js](https://nodejs.org/) and [yarn](https://yarnpkg.com) installed
1. Build the web app with `yarn build:webpack` or `npm run build:webpack`
1. Using the `.env.sample` as a reference, set the required variables in your production environment. The following are required as a minimum:
1. `SECRET_KEY` (follow instructions in the comments of `.env`)
1. `SECRET_KEY` (follow instructions in the comments at the top of `.env`)
1. `SLACK_KEY` (this is called "Client ID" in Slack admin)
1. `SLACK_SECRET`
1. `SLACK_SECRET` (this is called "Client Secret" in Slack admin)
1. `DATABASE_URL` (run your own local copy of Postgres, or use a cloud service)
1. `REDIS_URL` (run your own local copy of Redis, or use a cloud service)
1. `URL` (the public facing URL of your installation)
@@ -103,7 +109,7 @@ Outline is composed of separate backend and frontend application which are both
Outline's frontend is a React application compiled with [Webpack](https://webpack.js.org/). It uses [Mobx](https://mobx.js.org/) for state management and [Styled Components](https://www.styled-components.com/) for component styles. Unless global, state logic and styles are always co-located with React components together with their subcomponents to make the component tree easier to manage.
The editor itself is built ontop of [Slate](https://github.com/ianstormtaylor/slate) and hosted in a separate repository to encourage reuse: [rich-markdown-editor](https://github.com/outline/rich-markdown-editor)
The editor itself is built on [Prosemirror](https://github.com/prosemirror) and hosted in a separate repository to encourage reuse: [rich-markdown-editor](https://github.com/outline/rich-markdown-editor)
- `app/` - Frontend React application
- `app/scenes` - Full page views
@@ -120,7 +126,6 @@ Backend is driven by [Koa](http://koajs.com/) (API, web server), [Sequelize](htt
- `server/commands` - Domain logic, currently being refactored from /models
- `server/emails` - React rendered email templates
- `server/models` - Database models
- `server/pages` - Server-side rendered public pages
- `server/policies` - Authorization logic
- `server/presenters` - API responses for database models
- `server/test` - Test helps and support
@@ -148,7 +153,7 @@ yarn test:app
Outline is built and maintained by a small team we'd love your help to fix bugs and add features!
However, before working on a pull request please let the core team know by creating or commenting in an issue on [GitHub](https://www.github.com/outline/outline/issues), and we'd also love to hear from you in our [Spectrum community](https://spectrum.chat/outline). This way we can ensure that an approach is agreed on before code is written and will hopefully help to get your contributions integrated faster!
However, before working on a pull request please let the core team know by creating or commenting in an issue on [GitHub](https://www.github.com/outline/outline/issues), and we'd also love to hear from you in the [Discussions](https://www.github.com/outline/outline/discussions). This way we can ensure that an approach is agreed on before code is written and will hopefully help to get your contributions integrated faster!
If youre looking for ways to get started, here's a list of ways to help us improve Outline:
@@ -156,8 +161,7 @@ If youre looking for ways to get started, here's a list of ways to help us im
* Performance improvements, both on server and frontend
* Developer happiness and documentation
* Bugs and other issues listed on GitHub
* Helping others on Spectrum
## License
Outline is [BSD licensed](https://github.com/outline/outline/blob/master/LICENSE).
Outline is [BSL 1.1 licensed](https://github.com/outline/outline/blob/master/LICENSE).
+1 -16
View File
@@ -35,25 +35,10 @@
"generator": "secret",
"required": true
},
"DEPLOYMENT": {
"description": "Should be 'self' for self hosted installations, turns off things like pricing pages",
"value": "self",
"required": true
},
"ENABLE_UPDATES": {
"value": "true",
"required": true
},
"SUBDOMAINS_ENABLED": {
"value": "false",
"required": true,
"description": "Allows each team to have a different subdomain. Not recommend when self hosting"
},
"WEBSOCKETS_ENABLED": {
"value": "true",
"required": true,
"description": "Allow realtime data to be pushed to clients over websockets"
},
"URL": {
"description": "https://{your app name}.herokuapp.com",
"required": true
@@ -154,4 +139,4 @@
"required": false
}
}
}
}
+12 -8
View File
@@ -1,7 +1,7 @@
// @flow
import styled from 'styled-components';
import breakpoint from 'styled-components-breakpoint';
import Flex from 'shared/components/Flex';
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import Flex from "components/Flex";
export const Action = styled(Flex)`
justify-content: center;
@@ -12,9 +12,13 @@ export const Action = styled(Flex)`
flex-shrink: 0;
a {
color: ${props => props.theme.text};
color: ${(props) => props.theme.text};
height: 24px;
}
&:empty {
display: none;
}
`;
export const Separator = styled.div`
@@ -22,7 +26,7 @@ export const Separator = styled.div`
margin-left: 12px;
width: 1px;
height: 28px;
background: ${props => props.theme.divider};
background: ${(props) => props.theme.divider};
`;
const Actions = styled(Flex)`
@@ -31,8 +35,8 @@ const Actions = styled(Flex)`
right: 0;
left: 0;
border-radius: 3px;
background: ${props => props.theme.background};
transition: ${props => props.theme.backgroundTransition};
background: ${(props) => props.theme.background};
transition: ${(props) => props.theme.backgroundTransition};
padding: 12px;
-webkit-backdrop-filter: blur(20px);
@@ -40,7 +44,7 @@ const Actions = styled(Flex)`
display: none;
}
${breakpoint('tablet')`
${breakpoint("tablet")`
left: auto;
padding: 24px;
`};
-36
View File
@@ -1,36 +0,0 @@
// @flow
import * as React from 'react';
import { observer } from 'mobx-react';
import Flex from 'shared/components/Flex';
import styled from 'styled-components';
type Props = {
children: React.Node,
type?: 'info' | 'success' | 'warning' | 'danger' | 'offline',
};
@observer
class Alert extends React.Component<Props> {
defaultProps = {
type: 'info',
};
render() {
return (
<Container align="center" justify="center" type={this.props.type}>
{this.props.children}
</Container>
);
}
}
const Container = styled(Flex)`
height: $headerHeight;
color: ${props => props.theme.white};
font-size: 14px;
line-height: 1;
background-color: ${({ theme, type }) => theme.color[type]};
`;
export default Alert;
+9 -8
View File
@@ -1,6 +1,7 @@
// @flow
/* global ga */
import * as React from 'react';
import * as React from "react";
import env from "env";
type Props = {
children?: React.Node,
@@ -8,24 +9,24 @@ type Props = {
export default class Analytics extends React.Component<Props> {
componentDidMount() {
if (!process.env.GOOGLE_ANALYTICS_ID) return;
if (!env.GOOGLE_ANALYTICS_ID) return;
// standard Google Analytics script
window.ga =
window.ga ||
function() {
function () {
// $FlowIssue
(ga.q = ga.q || []).push(arguments);
};
// $FlowIssue
ga.l = +new Date();
ga('create', process.env.GOOGLE_ANALYTICS_ID, 'auto');
ga('set', { dimension1: 'true' });
ga('send', 'pageview');
ga("create", env.GOOGLE_ANALYTICS_ID, "auto");
ga("set", { dimension1: "true" });
ga("send", "pageview");
const script = document.createElement('script');
script.src = 'https://www.google-analytics.com/analytics.js';
const script = document.createElement("script");
script.src = "https://www.google-analytics.com/analytics.js";
script.async = true;
if (document.body) {
+10 -8
View File
@@ -1,9 +1,11 @@
// @flow
import * as React from 'react';
import { observer, inject } from 'mobx-react';
import AuthStore from 'stores/AuthStore';
import LoadingIndicator from 'components/LoadingIndicator';
import { isCustomSubdomain } from 'shared/utils/domains';
import { observer, inject } from "mobx-react";
import * as React from "react";
import { Redirect } from "react-router-dom";
import { isCustomSubdomain } from "shared/utils/domains";
import AuthStore from "stores/AuthStore";
import LoadingIndicator from "components/LoadingIndicator";
import env from "env";
type Props = {
auth: AuthStore,
@@ -22,7 +24,7 @@ const Authenticated = observer(({ auth, children }: Props) => {
// If we're authenticated but viewing a subdomain that doesn't match the
// currently authenticated team then kick the user to the teams subdomain.
if (
process.env.SUBDOMAINS_ENABLED &&
env.SUBDOMAINS_ENABLED &&
team.subdomain &&
isCustomSubdomain(hostname) &&
!hostname.startsWith(`${team.subdomain}.`)
@@ -35,7 +37,7 @@ const Authenticated = observer(({ auth, children }: Props) => {
}
auth.logout(true);
return null;
return <Redirect to="/" />;
});
export default inject('auth')(Authenticated);
export default inject("auth")(Authenticated);
+13 -12
View File
@@ -1,9 +1,9 @@
// @flow
import * as React from 'react';
import styled from 'styled-components';
import { observable } from 'mobx';
import { observer } from 'mobx-react';
import placeholder from './placeholder.png';
import { observable } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import styled from "styled-components";
import placeholder from "./placeholder.png";
type Props = {
src: string,
@@ -39,27 +39,28 @@ class Avatar extends React.Component<Props> {
}
}
const AvatarWrapper = styled.span`
const AvatarWrapper = styled.div`
position: relative;
`;
const IconWrapper = styled.span`
const IconWrapper = styled.div`
display: flex;
position: absolute;
bottom: -2px;
right: -2px;
background: ${props => props.theme.primary};
border: 2px solid ${props => props.theme.background};
background: ${(props) => props.theme.primary};
border: 2px solid ${(props) => props.theme.background};
border-radius: 100%;
width: 20px;
height: 20px;
`;
const CircleImg = styled.img`
width: ${props => props.size}px;
height: ${props => props.size}px;
display: block;
width: ${(props) => props.size}px;
height: ${(props) => props.size}px;
border-radius: 50%;
border: 2px solid ${props => props.theme.background};
border: 2px solid ${(props) => props.theme.background};
flex-shrink: 0;
`;
@@ -0,0 +1,86 @@
// @flow
import distanceInWordsToNow from "date-fns/distance_in_words_to_now";
import { observable } from "mobx";
import { observer } from "mobx-react";
import { EditIcon } from "outline-icons";
import * as React from "react";
import styled from "styled-components";
import User from "models/User";
import UserProfile from "scenes/UserProfile";
import Avatar from "components/Avatar";
import Tooltip from "components/Tooltip";
type Props = {
user: User,
isPresent: boolean,
isEditing: boolean,
isCurrentUser: boolean,
lastViewedAt: string,
};
@observer
class AvatarWithPresence extends React.Component<Props> {
@observable isOpen: boolean = false;
handleOpenProfile = () => {
this.isOpen = true;
};
handleCloseProfile = () => {
this.isOpen = false;
};
render() {
const {
user,
lastViewedAt,
isPresent,
isEditing,
isCurrentUser,
} = this.props;
return (
<>
<Tooltip
tooltip={
<Centered>
<strong>{user.name}</strong> {isCurrentUser && "(You)"}
<br />
{isPresent
? isEditing
? "currently editing"
: "currently viewing"
: `viewed ${distanceInWordsToNow(new Date(lastViewedAt))} ago`}
</Centered>
}
placement="bottom"
>
<AvatarWrapper isPresent={isPresent}>
<Avatar
src={user.avatarUrl}
onClick={this.handleOpenProfile}
size={32}
icon={isEditing ? <EditIcon size={16} color="#FFF" /> : undefined}
/>
</AvatarWrapper>
</Tooltip>
<UserProfile
user={user}
isOpen={this.isOpen}
onRequestClose={this.handleCloseProfile}
/>
</>
);
}
}
const Centered = styled.div`
text-align: center;
`;
const AvatarWrapper = styled.div`
opacity: ${(props) => (props.isPresent ? 1 : 0.5)};
transition: opacity 250ms ease-in-out;
`;
export default AvatarWithPresence;
+4 -1
View File
@@ -1,3 +1,6 @@
// @flow
import Avatar from './Avatar';
import Avatar from "./Avatar";
import AvatarWithPresence from "./AvatarWithPresence";
export { AvatarWithPresence };
export default Avatar;
+4 -4
View File
@@ -1,12 +1,12 @@
// @flow
import styled from 'styled-components';
import styled from "styled-components";
const Badge = styled.span`
margin-left: 10px;
padding: 2px 6px 3px;
background-color: ${({ admin, theme }) =>
admin ? theme.primary : theme.textTertiary};
color: ${({ admin, theme }) => (admin ? theme.white : theme.background)};
background-color: ${({ primary, theme }) =>
primary ? theme.primary : theme.textTertiary};
color: ${({ primary, theme }) => (primary ? theme.white : theme.background)};
border-radius: 4px;
font-size: 11px;
font-weight: 500;
+43
View File
@@ -0,0 +1,43 @@
// @flow
import * as React from "react";
import styled from "styled-components";
import OutlineLogo from "./OutlineLogo";
import env from "env";
type Props = {
href?: string,
};
function Branding({ href = env.URL }: Props) {
return (
<Link href={href}>
<OutlineLogo size={16} />
&nbsp;Outline
</Link>
);
}
const Link = styled.a`
position: fixed;
bottom: 0;
left: 0;
font-weight: 600;
font-size: 14px;
text-decoration: none;
border-top-right-radius: 2px;
color: ${(props) => props.theme.text};
display: flex;
align-items: center;
padding: 16px;
svg {
fill: ${(props) => props.theme.text};
}
&:hover {
background: ${(props) => props.theme.sidebarBackground};
}
`;
export default Branding;
@@ -1,22 +1,23 @@
// @flow
import * as React from 'react';
import { observer, inject } from 'mobx-react';
import breakpoint from 'styled-components-breakpoint';
import styled from 'styled-components';
import { Link } from 'react-router-dom';
import { observer, inject } from "mobx-react";
import {
CollectionIcon,
PrivateCollectionIcon,
PadlockIcon,
GoToIcon,
MoreIcon,
} from 'outline-icons';
ShapesIcon,
EditIcon,
} from "outline-icons";
import * as React from "react";
import { Link } from "react-router-dom";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import Document from 'models/Document';
import CollectionsStore from 'stores/CollectionsStore';
import { collectionUrl } from 'utils/routeHelpers';
import Flex from 'shared/components/Flex';
import BreadcrumbMenu from './BreadcrumbMenu';
import CollectionsStore from "stores/CollectionsStore";
import Document from "models/Document";
import CollectionIcon from "components/CollectionIcon";
import Flex from "components/Flex";
import BreadcrumbMenu from "./BreadcrumbMenu";
import { collectionUrl } from "utils/routeHelpers";
type Props = {
document: Document,
@@ -32,59 +33,77 @@ const Breadcrumb = observer(({ document, collections, onlyText }: Props) => {
if (onlyText === true) {
return (
<React.Fragment>
<>
{collection.private && (
<React.Fragment>
<SmallPadlockIcon color="currentColor" size={16} />{' '}
</React.Fragment>
<>
<SmallPadlockIcon color="currentColor" size={16} />{" "}
</>
)}
{collection.name}
{path.map(n => (
{path.map((n) => (
<React.Fragment key={n.id}>
<SmallSlash />
{n.title}
</React.Fragment>
))}
</React.Fragment>
</>
);
}
const isTemplate = document.isTemplate;
const isDraft = !document.publishedAt && !isTemplate;
const isNestedDocument = path.length > 1;
const lastPath = path.length ? path[path.length - 1] : undefined;
const menuPath = isNestedDocument ? path.slice(0, -1) : [];
return (
<Wrapper justify="flex-start" align="center">
{isTemplate && (
<>
<CollectionName to="/templates">
<ShapesIcon color="currentColor" />
&nbsp;
<span>Templates</span>
</CollectionName>
<Slash />
</>
)}
{isDraft && (
<>
<CollectionName to="/drafts">
<EditIcon color="currentColor" />
&nbsp;
<span>Drafts</span>
</CollectionName>
<Slash />
</>
)}
<CollectionName to={collectionUrl(collection.id)}>
{collection.private ? (
<PrivateCollectionIcon color={collection.color} expanded />
) : (
<CollectionIcon color={collection.color} expanded />
)}{' '}
<CollectionIcon collection={collection} expanded />
&nbsp;
<span>{collection.name}</span>
</CollectionName>
{isNestedDocument && (
<React.Fragment>
<>
<Slash /> <BreadcrumbMenu label={<Overflow />} path={menuPath} />
</React.Fragment>
</>
)}
{lastPath && (
<React.Fragment>
<Slash />{' '}
<>
<Slash />{" "}
<Crumb to={lastPath.url} title={lastPath.title}>
{lastPath.title}
</Crumb>
</React.Fragment>
</>
)}
</Wrapper>
);
});
const Wrapper = styled(Flex)`
width: 33.3%;
display: none;
${breakpoint('tablet')`
${breakpoint("tablet")`
display: flex;
`};
`;
@@ -101,9 +120,9 @@ const SmallSlash = styled(GoToIcon)`
opacity: 0.25;
`;
const Slash = styled(GoToIcon)`
export const Slash = styled(GoToIcon)`
flex-shrink: 0;
opacity: 0.25;
fill: ${(props) => props.theme.divider};
`;
const Overflow = styled(MoreIcon)`
@@ -118,7 +137,7 @@ const Overflow = styled(MoreIcon)`
`;
const Crumb = styled(Link)`
color: ${props => props.theme.text};
color: ${(props) => props.theme.text};
font-size: 15px;
height: 24px;
text-overflow: ellipsis;
@@ -133,11 +152,11 @@ const Crumb = styled(Link)`
const CollectionName = styled(Link)`
display: flex;
flex-shrink: 0;
color: ${props => props.theme.text};
color: ${(props) => props.theme.text};
font-size: 15px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
`;
export default inject('collections')(Breadcrumb);
export default inject("collections")(Breadcrumb);
@@ -1,7 +1,7 @@
// @flow
import * as React from 'react';
import { Link } from 'react-router-dom';
import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu';
import * as React from "react";
import { Link } from "react-router-dom";
import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
type Props = {
label: React.Node,
@@ -14,7 +14,7 @@ export default class BreadcrumbMenu extends React.Component<Props> {
return (
<DropdownMenu label={this.props.label} position="center">
{path.map(item => (
{path.map((item) => (
<DropdownMenuItem as={Link} to={item.url} key={item.id}>
{item.title}
</DropdownMenuItem>
+36 -27
View File
@@ -1,16 +1,17 @@
// @flow
import * as React from 'react';
import styled from 'styled-components';
import { darken, lighten } from 'polished';
import { ExpandedIcon } from 'outline-icons';
import { ExpandedIcon } from "outline-icons";
import { darken, lighten } from "polished";
import * as React from "react";
import styled from "styled-components";
const RealButton = styled.button`
display: inline-block;
display: ${(props) => (props.fullwidth ? "block" : "inline-block")};
width: ${(props) => (props.fullwidth ? "100%" : "auto")};
margin: 0;
padding: 0;
border: 0;
background: ${props => props.theme.buttonBackground};
color: ${props => props.theme.buttonText};
background: ${(props) => props.theme.buttonBackground};
color: ${(props) => props.theme.buttonText};
box-shadow: rgba(0, 0, 0, 0.2) 0px 1px 2px;
border-radius: 4px;
font-size: 14px;
@@ -23,7 +24,7 @@ const RealButton = styled.button`
user-select: none;
svg {
fill: ${props => props.theme.buttonText};
fill: ${(props) => props.iconColor || props.theme.buttonText};
}
&::-moz-focus-inner {
@@ -32,12 +33,12 @@ const RealButton = styled.button`
}
&:hover {
background: ${props => darken(0.05, props.theme.buttonBackground)};
background: ${(props) => darken(0.05, props.theme.buttonBackground)};
}
&:focus {
transition-duration: 0.05s;
box-shadow: ${props => lighten(0.4, props.theme.buttonBackground)} 0px 0px
box-shadow: ${(props) => lighten(0.4, props.theme.buttonBackground)} 0px 0px
0px 3px;
outline: none;
}
@@ -45,24 +46,28 @@ const RealButton = styled.button`
&:disabled {
cursor: default;
pointer-events: none;
color: ${props => props.theme.white50};
color: ${(props) => props.theme.white50};
}
${props =>
${(props) =>
props.neutral &&
`
background: ${props.theme.buttonNeutralBackground};
color: ${props.theme.buttonNeutralText};
box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 2px;
border: 1px solid ${darken(0.1, props.theme.buttonNeutralBackground)};
box-shadow: ${
props.borderOnHover ? "none" : "rgba(0, 0, 0, 0.07) 0px 1px 2px"
};
border: 1px solid ${
props.borderOnHover ? "transparent" : props.theme.buttonNeutralBorder
};
svg {
fill: ${props.theme.buttonNeutralText};
fill: ${props.iconColor || props.theme.buttonNeutralText};
}
&:hover {
background: ${darken(0.05, props.theme.buttonNeutralBackground)};
border: 1px solid ${darken(0.15, props.theme.buttonNeutralBackground)};
border: 1px solid ${props.theme.buttonNeutralBorder};
}
&:focus {
@@ -75,9 +80,9 @@ const RealButton = styled.button`
&:disabled {
color: ${props.theme.textTertiary};
}
`} ${props =>
props.danger &&
`
`} ${(props) =>
props.danger &&
`
background: ${props.theme.danger};
color: ${props.theme.white};
@@ -98,32 +103,37 @@ const Label = styled.span`
white-space: nowrap;
text-overflow: ellipsis;
${props => props.hasIcon && 'padding-left: 4px;'};
${(props) => props.hasIcon && "padding-left: 4px;"};
`;
export const Inner = styled.span`
display: flex;
padding: 0 8px;
padding-right: ${props => (props.disclosure ? 2 : 8)}px;
line-height: ${props => (props.hasIcon ? 24 : 32)}px;
padding-right: ${(props) => (props.disclosure ? 2 : 8)}px;
line-height: ${(props) => (props.hasIcon ? 24 : 32)}px;
justify-content: center;
align-items: center;
min-height: 30px;
${props => props.hasIcon && 'padding-left: 4px;'};
${(props) => props.hasIcon && props.hasText && "padding-left: 4px;"};
${(props) => props.hasIcon && !props.hasText && "padding: 0 4px;"};
`;
export type Props = {
type?: string,
value?: string,
icon?: React.Node,
iconColor?: string,
className?: string,
children?: React.Node,
innerRef?: React.ElementRef<any>,
disclosure?: boolean,
fullwidth?: boolean,
borderOnHover?: boolean,
};
function Button({
type = 'text',
type = "text",
icon,
children,
value,
@@ -136,7 +146,7 @@ function Button({
return (
<RealButton type={type} ref={innerRef} {...rest}>
<Inner hasIcon={hasIcon} disclosure={disclosure}>
<Inner hasIcon={hasIcon} hasText={hasText} disclosure={disclosure}>
{hasIcon && icon}
{hasText && <Label hasIcon={hasIcon}>{children || value}</Label>}
{disclosure && <ExpandedIcon />}
@@ -145,7 +155,6 @@ function Button({
);
}
// $FlowFixMe - need to upgrade to get forwardRef
export default React.forwardRef((props, ref) => (
export default React.forwardRef<Props, typeof Button>((props, ref) => (
<Button {...props} innerRef={ref} />
));
+13
View File
@@ -0,0 +1,13 @@
// @flow
import styled from "styled-components";
import Button, { Inner } from "./Button";
const ButtonLarge = styled(Button)`
height: 40px;
${Inner} {
padding: 4px 16px;
}
`;
export default ButtonLarge;
+5 -4
View File
@@ -1,7 +1,7 @@
// @flow
import * as React from 'react';
import styled from 'styled-components';
import breakpoint from 'styled-components-breakpoint';
import * as React from "react";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
type Props = {
children?: React.Node,
@@ -9,9 +9,10 @@ type Props = {
const Container = styled.div`
width: 100%;
max-width: 100vw;
padding: 60px 20px;
${breakpoint('tablet')`
${breakpoint("tablet")`
padding: 60px;
`};
`;
+10 -9
View File
@@ -1,8 +1,8 @@
// @flow
import * as React from 'react';
import styled from 'styled-components';
import HelpText from 'components/HelpText';
import VisuallyHidden from 'components/VisuallyHidden';
import * as React from "react";
import styled from "styled-components";
import HelpText from "components/HelpText";
import VisuallyHidden from "components/VisuallyHidden";
export type Props = {
checked?: boolean,
@@ -10,18 +10,19 @@ export type Props = {
labelHidden?: boolean,
className?: string,
note?: string,
short?: boolean,
small?: boolean,
};
const LabelText = styled.span`
font-weight: 500;
margin-left: ${props => (props.small ? '6px' : '10px')};
${props => (props.small ? `color: ${props.theme.textSecondary}` : '')};
margin-left: ${(props) => (props.small ? "6px" : "10px")};
${(props) => (props.small ? `color: ${props.theme.textSecondary}` : "")};
`;
const Wrapper = styled.div`
padding-bottom: 8px;
${props => (props.small ? 'font-size: 14px' : '')};
${(props) => (props.small ? "font-size: 14px" : "")};
`;
const Label = styled.label`
@@ -42,7 +43,7 @@ export default function Checkbox({
const wrappedLabel = <LabelText small={small}>{label}</LabelText>;
return (
<React.Fragment>
<>
<Wrapper small={small}>
<Label>
<input type="checkbox" {...rest} />
@@ -55,6 +56,6 @@ export default function Checkbox({
</Label>
{note && <HelpText small>{note}</HelpText>}
</Wrapper>
</React.Fragment>
</>
);
}
+2 -2
View File
@@ -1,9 +1,9 @@
// @flow
import styled from 'styled-components';
import styled from "styled-components";
const ClickablePadding = styled.div`
min-height: 10em;
cursor: ${({ onClick }) => (onClick ? 'text' : 'default')};
cursor: ${({ onClick }) => (onClick ? "text" : "default")};
${({ grow }) => grow && `flex-grow: 100;`};
`;
+29 -128
View File
@@ -1,22 +1,14 @@
// @flow
import * as React from 'react';
import { observable } from 'mobx';
import { observer, inject } from 'mobx-react';
import { sortBy } from 'lodash';
import styled, { withTheme } from 'styled-components';
import distanceInWordsToNow from 'date-fns/distance_in_words_to_now';
import { sortBy, keyBy } from "lodash";
import { observer, inject } from "mobx-react";
import * as React from "react";
import { MAX_AVATAR_DISPLAY } from "shared/constants";
import Flex from 'shared/components/Flex';
import Avatar from 'components/Avatar';
import Tooltip from 'components/Tooltip';
import Document from 'models/Document';
import User from 'models/User';
import UserProfile from 'scenes/UserProfile';
import ViewsStore from 'stores/ViewsStore';
import DocumentPresenceStore from 'stores/DocumentPresenceStore';
import { EditIcon } from 'outline-icons';
const MAX_DISPLAY = 6;
import DocumentPresenceStore from "stores/DocumentPresenceStore";
import ViewsStore from "stores/ViewsStore";
import Document from "models/Document";
import { AvatarWithPresence } from "components/Avatar";
import Facepile from "components/Facepile";
type Props = {
views: ViewsStore,
@@ -25,66 +17,6 @@ type Props = {
currentUserId: string,
};
@observer
class AvatarWithPresence extends React.Component<{
user: User,
isPresent: boolean,
isEditing: boolean,
isCurrentUser: boolean,
lastViewedAt: string,
}> {
@observable isOpen: boolean = false;
handleOpenProfile = () => {
this.isOpen = true;
};
handleCloseProfile = () => {
this.isOpen = false;
};
render() {
const {
user,
lastViewedAt,
isPresent,
isEditing,
isCurrentUser,
} = this.props;
return (
<React.Fragment>
<Tooltip
tooltip={
<Centered>
<strong>{user.name}</strong> {isCurrentUser && '(You)'}
<br />
{isPresent
? isEditing ? 'currently editing' : 'currently viewing'
: `viewed ${distanceInWordsToNow(new Date(lastViewedAt))} ago`}
</Centered>
}
placement="bottom"
>
<AvatarWrapper isPresent={isPresent}>
<Avatar
src={user.avatarUrl}
onClick={this.handleOpenProfile}
size={32}
icon={isEditing ? <EditIcon size={16} color="#FFF" /> : undefined}
/>
</AvatarWrapper>
</Tooltip>
<UserProfile
user={user}
isOpen={this.isOpen}
onRequestClose={this.handleCloseProfile}
/>
</React.Fragment>
);
}
}
@observer
class Collaborators extends React.Component<Props> {
componentDidMount() {
@@ -93,33 +25,37 @@ class Collaborators extends React.Component<Props> {
render() {
const { document, presence, views, currentUserId } = this.props;
const documentViews = views.inDocument(document.id);
let documentPresence = presence.get(document.id);
documentPresence = documentPresence
? Array.from(documentPresence.values())
: [];
const presentIds = documentPresence.map(p => p.userId);
const editingIds = documentPresence
.filter(p => p.isEditing)
.map(p => p.userId);
// only show the most recent viewers, the rest can overflow
let mostRecentViewers = documentViews.slice(0, MAX_DISPLAY);
const documentViews = views.inDocument(document.id);
const presentIds = documentPresence.map((p) => p.userId);
const editingIds = documentPresence
.filter((p) => p.isEditing)
.map((p) => p.userId);
// ensure currently present via websocket are always ordered first
mostRecentViewers = sortBy(mostRecentViewers, view =>
presentIds.includes(view.user.id)
const mostRecentViewers = sortBy(
documentViews.slice(0, MAX_AVATAR_DISPLAY),
(view) => {
return presentIds.includes(view.user.id);
}
);
// if there are too many to display then add a (+X) to the UI
const viewersKeyedByUserId = keyBy(mostRecentViewers, (v) => v.user.id);
const overflow = documentViews.length - mostRecentViewers.length;
return (
<Avatars>
{overflow > 0 && <More>+{overflow}</More>}
{mostRecentViewers.map(({ lastViewedAt, user }) => {
<Facepile
users={mostRecentViewers.map((v) => v.user)}
overflow={overflow}
renderAvatar={(user) => {
const isPresent = presentIds.includes(user.id);
const isEditing = editingIds.includes(user.id);
const { lastViewedAt } = viewersKeyedByUserId[user.id];
return (
<AvatarWithPresence
@@ -131,45 +67,10 @@ class Collaborators extends React.Component<Props> {
isCurrentUser={currentUserId === user.id}
/>
);
})}
</Avatars>
}}
/>
);
}
}
const Centered = styled.div`
text-align: center;
`;
const AvatarWrapper = styled.div`
width: 32px;
height: 32px;
margin-right: -8px;
opacity: ${props => (props.isPresent ? 1 : 0.5)};
transition: opacity 250ms ease-in-out;
&:first-child {
margin-right: 0;
}
`;
const More = styled.div`
min-width: 30px;
height: 24px;
border-radius: 12px;
background: ${props => props.theme.slate};
color: ${props => props.theme.text};
border: 2px solid ${props => props.theme.background};
text-align: center;
line-height: 20px;
font-size: 11px;
font-weight: 600;
`;
const Avatars = styled(Flex)`
align-items: center;
flex-direction: row-reverse;
cursor: pointer;
`;
export default inject('views', 'presence')(withTheme(Collaborators));
export default inject("views", "presence")(Collaborators);
+45
View File
@@ -0,0 +1,45 @@
// @flow
import { inject, observer } from "mobx-react";
import { PrivateCollectionIcon, CollectionIcon } from "outline-icons";
import { getLuminance } from "polished";
import * as React from "react";
import UiStore from "stores/UiStore";
import Collection from "models/Collection";
import { icons } from "components/IconPicker";
type Props = {
collection: Collection,
expanded?: boolean,
size?: number,
ui: UiStore,
};
function ResolvedCollectionIcon({ collection, expanded, size, ui }: Props) {
// If the chosen icon color is very dark then we invert it in dark mode
// otherwise it will be impossible to see against the dark background.
const color =
ui.resolvedTheme === "dark"
? getLuminance(collection.color) > 0.12
? collection.color
: "currentColor"
: collection.color;
if (collection.icon && collection.icon !== "collection") {
try {
const Component = icons[collection.icon].component;
return <Component color={color} size={size} />;
} catch (error) {
console.warn("Failed to render custom icon " + collection.icon);
}
}
if (collection.private) {
return (
<PrivateCollectionIcon color={color} expanded={expanded} size={size} />
);
}
return <CollectionIcon color={color} expanded={expanded} size={size} />;
}
export default inject("ui")(observer(ResolvedCollectionIcon));
-106
View File
@@ -1,106 +0,0 @@
// @flow
import * as React from 'react';
import { observable } from 'mobx';
import { observer } from 'mobx-react';
import { TwitterPicker } from 'react-color';
import styled from 'styled-components';
import Fade from 'components/Fade';
import { LabelText } from 'components/Input';
const colors = [
'#4E5C6E',
'#19B7FF',
'#7F6BFF',
'#FC7419',
'#FC2D2D',
'#FFE100',
'#14CF9F',
'#00D084',
'#EE84F0',
'#2F362F',
];
type Props = {
onChange: (color: string) => void,
value?: string,
};
@observer
class ColorPicker extends React.Component<Props> {
@observable isOpen: boolean = false;
node: ?HTMLElement;
componentDidMount() {
window.addEventListener('click', this.handleClickOutside);
}
componentWillUnmount() {
window.removeEventListener('click', this.handleClickOutside);
}
handleClose = () => {
this.isOpen = false;
};
handleOpen = () => {
this.isOpen = true;
};
handleClickOutside = (ev: SyntheticMouseEvent<>) => {
// $FlowFixMe
if (ev.target && this.node && this.node.contains(ev.target)) {
return;
}
this.handleClose();
};
render() {
return (
<Wrapper ref={ref => (this.node = ref)}>
<label>
<LabelText>Color</LabelText>
</label>
<Swatch
role="button"
onClick={this.isOpen ? this.handleClose : this.handleOpen}
color={this.props.value}
/>
<Floating>
{this.isOpen && (
<Fade>
<TwitterPicker
colors={colors}
color={this.props.value}
onChange={color => this.props.onChange(color.hex)}
triangle="top-right"
/>
</Fade>
)}
</Floating>
</Wrapper>
);
}
}
const Wrapper = styled('div')`
display: inline-block;
position: relative;
`;
const Floating = styled('div')`
position: absolute;
top: 60px;
right: 0;
z-index: 1;
`;
const Swatch = styled('div')`
display: inline-block;
width: 48px;
height: 32px;
border: 1px solid ${({ active, color }) => (active ? 'white' : 'transparent')};
border-radius: 4px;
background: ${({ color }) => color};
`;
export default ColorPicker;
+4 -4
View File
@@ -1,6 +1,6 @@
// @flow
import * as React from 'react';
import copy from 'copy-to-clipboard';
import copy from "copy-to-clipboard";
import * as React from "react";
type Props = {
text: string,
@@ -14,12 +14,12 @@ class CopyToClipboard extends React.PureComponent<Props> {
const { text, onCopy, children } = this.props;
const elem = React.Children.only(children);
copy(text, {
debug: !!__DEV__,
debug: process.env.NODE_ENV !== "production",
});
if (onCopy) onCopy();
if (elem && elem.props && typeof elem.props.onClick === 'function') {
if (elem && elem.props && typeof elem.props.onClick === "function") {
elem.props.onClick(ev);
}
};
+24
View File
@@ -0,0 +1,24 @@
// @flow
import * as React from "react";
type Props = {
delay?: number,
children: React.Node,
};
export default function DelayedMount({ delay = 250, children }: Props) {
const [isShowing, setShowing] = React.useState(false);
React.useEffect(() => {
const timeout = setTimeout(() => setShowing(true), delay);
return () => {
clearTimeout(timeout);
};
}, []);
if (!isShowing) {
return null;
}
return children;
}
@@ -1,24 +1,23 @@
// @flow
import * as React from 'react';
import { observable, action } from 'mobx';
import { observer, inject } from 'mobx-react';
import type { RouterHistory } from 'react-router-dom';
import styled from 'styled-components';
import { Waypoint } from 'react-waypoint';
import ArrowKeyNavigation from 'boundless-arrow-key-navigation';
import ArrowKeyNavigation from "boundless-arrow-key-navigation";
import { observable, action } from "mobx";
import { observer, inject } from "mobx-react";
import * as React from "react";
import { type RouterHistory, type Match } from "react-router-dom";
import { Waypoint } from "react-waypoint";
import styled from "styled-components";
import { DEFAULT_PAGINATION_LIMIT } from 'stores/BaseStore';
import DocumentsStore from 'stores/DocumentsStore';
import RevisionsStore from 'stores/RevisionsStore';
import Document from 'models/Document';
import { DEFAULT_PAGINATION_LIMIT } from "stores/BaseStore";
import DocumentsStore from "stores/DocumentsStore";
import RevisionsStore from "stores/RevisionsStore";
import Flex from 'shared/components/Flex';
import { ListPlaceholder } from 'components/LoadingPlaceholder';
import Revision from './components/Revision';
import { documentHistoryUrl } from 'utils/routeHelpers';
import Flex from "components/Flex";
import { ListPlaceholder } from "components/LoadingPlaceholder";
import Revision from "./components/Revision";
import { documentHistoryUrl } from "utils/routeHelpers";
type Props = {
match: Object,
match: Match,
documents: DocumentsStore,
revisions: RevisionsStore,
history: RouterHistory,
@@ -30,29 +29,12 @@ class DocumentHistory extends React.Component<Props> {
@observable isFetching: boolean = false;
@observable offset: number = 0;
@observable allowLoadMore: boolean = true;
@observable document: Document;
constructor(props) {
super();
this.document = props.documents.getByUrl(props.match.params.documentSlug);
}
async componentDidMount() {
await this.loadMoreResults();
this.selectFirstRevision();
}
async componentWillReceiveProps(nextProps) {
const document = nextProps.documents.getByUrl(
nextProps.match.params.documentSlug
);
if (!this.document && document) {
this.document = document;
await this.loadMoreResults();
this.selectFirstRevision();
}
}
fetchResults = async () => {
this.isFetching = true;
@@ -60,7 +42,7 @@ class DocumentHistory extends React.Component<Props> {
const results = await this.props.revisions.fetchPage({
limit,
offset: this.offset,
id: this.document.id,
documentId: this.props.match.params.documentSlug,
});
if (
@@ -78,8 +60,13 @@ class DocumentHistory extends React.Component<Props> {
selectFirstRevision = () => {
if (this.revisions.length) {
const document = this.props.documents.getByUrl(
this.props.match.params.documentSlug
);
if (!document) return;
this.props.history.replace(
documentHistoryUrl(this.document, this.revisions[0].id)
documentHistoryUrl(document, this.revisions[0].id)
);
}
};
@@ -87,43 +74,52 @@ class DocumentHistory extends React.Component<Props> {
@action
loadMoreResults = async () => {
// Don't paginate if there aren't more results or were in the middle of fetching
if (!this.allowLoadMore || this.isFetching || !this.document) return;
if (!this.allowLoadMore || this.isFetching) return;
await this.fetchResults();
};
get revisions() {
if (!this.document) return [];
return this.props.revisions.getDocumentRevisions(this.document.id);
const document = this.props.documents.getByUrl(
this.props.match.params.documentSlug
);
if (!document) return [];
return this.props.revisions.getDocumentRevisions(document.id);
}
render() {
const showLoading = !this.isLoaded && this.isFetching;
const document = this.props.documents.getByUrl(
this.props.match.params.documentSlug
);
const showLoading = (!this.isLoaded && this.isFetching) || !document;
return (
<Wrapper column>
{showLoading ? (
<Loading>
<ListPlaceholder count={5} />
</Loading>
) : (
<ArrowKeyNavigation
mode={ArrowKeyNavigation.mode.VERTICAL}
defaultActiveChildIndex={0}
>
{this.revisions.map((revision, index) => (
<Revision
key={revision.id}
revision={revision}
document={this.document}
showMenu={index !== 0}
/>
))}
</ArrowKeyNavigation>
)}
{this.allowLoadMore && (
<Waypoint key={this.offset} onEnter={this.loadMoreResults} />
)}
</Wrapper>
<Sidebar>
<Wrapper column>
{showLoading ? (
<Loading>
<ListPlaceholder count={5} />
</Loading>
) : (
<ArrowKeyNavigation
mode={ArrowKeyNavigation.mode.VERTICAL}
defaultActiveChildIndex={0}
>
{this.revisions.map((revision, index) => (
<Revision
key={revision.id}
revision={revision}
document={document}
showMenu={index !== 0}
selected={this.props.match.params.revisionId === revision.id}
/>
))}
</ArrowKeyNavigation>
)}
{this.allowLoadMore && (
<Waypoint key={this.offset} onEnter={this.loadMoreResults} />
)}
</Wrapper>
</Sidebar>
);
}
}
@@ -133,12 +129,21 @@ const Loading = styled.div`
`;
const Wrapper = styled(Flex)`
background: ${props => props.theme.background};
min-width: ${props => props.theme.sidebarWidth};
border-left: 1px solid ${props => props.theme.divider};
overflow: scroll;
position: fixed;
top: 0;
right: 0;
z-index: 1;
min-width: ${(props) => props.theme.sidebarWidth};
height: 100%;
overflow-y: auto;
overscroll-behavior: none;
`;
const Sidebar = styled(Flex)`
background: ${(props) => props.theme.background};
min-width: ${(props) => props.theme.sidebarWidth};
border-left: 1px solid ${(props) => props.theme.divider};
z-index: 1;
`;
export default inject('documents', 'revisions')(DocumentHistory);
export default inject("documents", "revisions")(DocumentHistory);
@@ -1,29 +1,30 @@
// @flow
import * as React from 'react';
import { NavLink } from 'react-router-dom';
import styled, { withTheme } from 'styled-components';
import format from 'date-fns/format';
import { MoreIcon } from 'outline-icons';
import format from "date-fns/format";
import { MoreIcon } from "outline-icons";
import * as React from "react";
import { NavLink } from "react-router-dom";
import styled, { withTheme } from "styled-components";
import Flex from 'shared/components/Flex';
import Time from 'shared/components/Time';
import Avatar from 'components/Avatar';
import RevisionMenu from 'menus/RevisionMenu';
import Document from 'models/Document';
import Revision from 'models/Revision';
import Document from "models/Document";
import Revision from "models/Revision";
import Avatar from "components/Avatar";
import Flex from "components/Flex";
import Time from "components/Time";
import RevisionMenu from "menus/RevisionMenu";
import { documentHistoryUrl } from 'utils/routeHelpers';
import { documentHistoryUrl } from "utils/routeHelpers";
type Props = {
theme: Object,
showMenu: () => void,
showMenu: boolean,
selected: boolean,
document: Document,
revision: Revision,
};
class RevisionListItem extends React.Component<Props> {
render() {
const { revision, document, showMenu, theme } = this.props;
const { revision, document, showMenu, selected, theme } = this.props;
return (
<StyledNavLink
@@ -31,19 +32,21 @@ class RevisionListItem extends React.Component<Props> {
activeStyle={{ background: theme.primary, color: theme.white }}
>
<Author>
<StyledAvatar src={revision.createdBy.avatarUrl} />{' '}
<StyledAvatar src={revision.createdBy.avatarUrl} />{" "}
{revision.createdBy.name}
</Author>
<Meta>
<Time dateTime={revision.createdAt}>
{format(revision.createdAt, 'MMMM Do, YYYY h:mm a')}
{format(revision.createdAt, "MMMM Do, YYYY h:mm a")}
</Time>
</Meta>
{showMenu && (
<StyledRevisionMenu
document={document}
revision={revision}
label={<MoreIcon color={theme.white} />}
label={
<MoreIcon color={selected ? theme.white : theme.textTertiary} />
}
/>
)}
</StyledNavLink>
@@ -59,11 +62,11 @@ const StyledAvatar = styled(Avatar)`
const StyledRevisionMenu = styled(RevisionMenu)`
position: absolute;
right: 16px;
top: 16px;
top: 20px;
`;
const StyledNavLink = styled(NavLink)`
color: ${props => props.theme.text};
color: ${(props) => props.theme.text};
display: block;
padding: 8px 16px;
font-size: 15px;
+1 -1
View File
@@ -1,3 +1,3 @@
// @flow
import DocumentHistory from './DocumentHistory';
import DocumentHistory from "./DocumentHistory";
export default DocumentHistory;
+5 -5
View File
@@ -1,8 +1,8 @@
// @flow
import * as React from 'react';
import Document from 'models/Document';
import DocumentPreview from 'components/DocumentPreview';
import ArrowKeyNavigation from 'boundless-arrow-key-navigation';
import ArrowKeyNavigation from "boundless-arrow-key-navigation";
import * as React from "react";
import Document from "models/Document";
import DocumentPreview from "components/DocumentPreview";
type Props = {
documents: Document[],
@@ -17,7 +17,7 @@ export default function DocumentList({ limit, documents, ...rest }: Props) {
mode={ArrowKeyNavigation.mode.VERTICAL}
defaultActiveChildIndex={0}
>
{items.map(document => (
{items.map((document) => (
<DocumentPreview key={document.id} document={document} {...rest} />
))}
</ArrowKeyNavigation>
+123
View File
@@ -0,0 +1,123 @@
// @flow
import { inject, observer } from "mobx-react";
import * as React from "react";
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";
const Container = styled(Flex)`
color: ${(props) => props.theme.textTertiary};
font-size: 13px;
white-space: nowrap;
overflow: hidden;
`;
const Modified = styled.span`
color: ${(props) =>
props.highlight ? props.theme.text : props.theme.textTertiary};
font-weight: ${(props) => (props.highlight ? "600" : "400")};
`;
type Props = {
collections: CollectionsStore,
auth: AuthStore,
showCollection?: boolean,
showPublished?: boolean,
document: Document,
children: React.Node,
to?: string,
};
function DocumentMeta({
auth,
collections,
showPublished,
showCollection,
document,
children,
to,
...rest
}: Props) {
const {
modifiedSinceViewed,
updatedAt,
updatedBy,
createdAt,
publishedAt,
archivedAt,
deletedAt,
isDraft,
} = document;
// Prevent meta information from displaying if updatedBy is not available.
// Currently the situation where this is true is rendering share links.
if (!updatedBy) {
return null;
}
let content;
if (deletedAt) {
content = (
<span>
deleted <Time dateTime={deletedAt} /> ago
</span>
);
} else if (archivedAt) {
content = (
<span>
archived <Time dateTime={archivedAt} /> ago
</span>
);
} else if (createdAt === updatedAt) {
content = (
<span>
created <Time dateTime={updatedAt} /> ago
</span>
);
} else if (publishedAt && (publishedAt === updatedAt || showPublished)) {
content = (
<span>
published <Time dateTime={publishedAt} /> ago
</span>
);
} else if (isDraft) {
content = (
<span>
saved <Time dateTime={updatedAt} /> ago
</span>
);
} else {
content = (
<Modified highlight={modifiedSinceViewed}>
updated <Time dateTime={updatedAt} /> ago
</Modified>
);
}
const collection = collections.get(document.collectionId);
const updatedByMe = auth.user && auth.user.id === updatedBy.id;
return (
<Container align="center" {...rest}>
{updatedByMe ? "You" : updatedBy.name}&nbsp;
{to ? <Link to={to}>{content}</Link> : content}
{showCollection && collection && (
<span>
&nbsp;in&nbsp;
<strong>
<Breadcrumb document={document} onlyText />
</strong>
</span>
)}
{children}
</Container>
);
}
export default inject("collections", "auth")(observer(DocumentMeta));
+48
View File
@@ -0,0 +1,48 @@
// @flow
import { inject } from "mobx-react";
import * as React from "react";
import styled from "styled-components";
import ViewsStore from "stores/ViewsStore";
import Document from "models/Document";
import DocumentMeta from "components/DocumentMeta";
type Props = {|
views: ViewsStore,
document: Document,
isDraft: boolean,
to?: string,
|};
function DocumentMetaWithViews({ views, to, isDraft, document }: Props) {
const totalViews = views.countForDocument(document.id);
return (
<Meta document={document} to={to}>
{totalViews && !isDraft ? (
<>
&nbsp;&middot; Viewed{" "}
{totalViews === 1 ? "once" : `${totalViews} times`}
</>
) : null}
</Meta>
);
}
const Meta = styled(DocumentMeta)`
margin: -12px 0 2em 0;
font-size: 14px;
a {
color: inherit;
&:hover {
text-decoration: underline;
}
}
@media print {
display: none;
}
`;
export default inject("views")(DocumentMetaWithViews);
+145 -108
View File
@@ -1,18 +1,21 @@
// @flow
import * as React from 'react';
import { observer } from 'mobx-react';
import { Link } from 'react-router-dom';
import { StarredIcon } from 'outline-icons';
import styled, { withTheme } from 'styled-components';
import Flex from 'shared/components/Flex';
import Badge from 'components/Badge';
import Tooltip from 'components/Tooltip';
import Highlight from 'components/Highlight';
import PublishingInfo from 'components/PublishingInfo';
import DocumentMenu from 'menus/DocumentMenu';
import Document from 'models/Document';
import { observer } from "mobx-react";
import { StarredIcon, PlusIcon } from "outline-icons";
import * as React from "react";
import { Link, withRouter, type RouterHistory } from "react-router-dom";
import styled, { withTheme } from "styled-components";
import Document from "models/Document";
import Badge from "components/Badge";
import Button from "components/Button";
import DocumentMeta from "components/DocumentMeta";
import Flex from "components/Flex";
import Highlight from "components/Highlight";
import Tooltip from "components/Tooltip";
import DocumentMenu from "menus/DocumentMenu";
import { newDocumentUrl } from "utils/routeHelpers";
type Props = {
history: RouterHistory,
document: Document,
highlight?: ?string,
context?: ?string,
@@ -20,13 +23,127 @@ type Props = {
showPublished?: boolean,
showPin?: boolean,
showDraft?: boolean,
showTemplate?: boolean,
};
const SEARCH_RESULT_REGEX = /<b\b[^>]*>(.*?)<\/b>/gi;
@observer
class DocumentPreview extends React.Component<Props> {
handleStar = (ev: SyntheticEvent<>) => {
ev.preventDefault();
ev.stopPropagation();
this.props.document.star();
};
handleUnstar = (ev: SyntheticEvent<>) => {
ev.preventDefault();
ev.stopPropagation();
this.props.document.unstar();
};
replaceResultMarks = (tag: string) => {
// don't use SEARCH_RESULT_REGEX here as it causes
// an infinite loop to trigger a regex inside it's own callback
return tag.replace(/<b\b[^>]*>(.*?)<\/b>/gi, "$1");
};
handleNewFromTemplate = (event) => {
event.preventDefault();
event.stopPropagation();
const { document } = this.props;
this.props.history.push(
newDocumentUrl(document.collectionId, {
templateId: document.id,
})
);
};
render() {
const {
document,
showCollection,
showPublished,
showPin,
showDraft = true,
showTemplate,
highlight,
context,
} = this.props;
const queryIsInTitle =
!!highlight &&
!!document.title.toLowerCase().includes(highlight.toLowerCase());
return (
<DocumentLink
to={{
pathname: document.url,
state: { title: document.titleWithDefault },
}}
>
<Heading>
<Title text={document.titleWithDefault} highlight={highlight} />
{!document.isDraft &&
!document.isArchived &&
!document.isTemplate && (
<Actions>
{document.isStarred ? (
<StyledStar onClick={this.handleUnstar} solid />
) : (
<StyledStar onClick={this.handleStar} />
)}
</Actions>
)}
{document.isDraft && showDraft && (
<Tooltip tooltip="Only visible to you" delay={500} placement="top">
<Badge>Draft</Badge>
</Tooltip>
)}
{document.isTemplate && showTemplate && (
<Badge primary>Template</Badge>
)}
<SecondaryActions>
{document.isTemplate &&
!document.isArchived &&
!document.isDeleted && (
<Button
onClick={this.handleNewFromTemplate}
icon={<PlusIcon />}
neutral
>
New doc
</Button>
)}
&nbsp;
<DocumentMenu document={document} showPin={showPin} />
</SecondaryActions>
</Heading>
{!queryIsInTitle && (
<ResultContext
text={context}
highlight={highlight ? SEARCH_RESULT_REGEX : undefined}
processResult={this.replaceResultMarks}
/>
)}
<DocumentMeta
document={document}
showCollection={showCollection}
showPublished={showPublished}
/>
</DocumentLink>
);
}
}
const StyledStar = withTheme(styled(({ solid, theme, ...props }) => (
<StarredIcon color={theme.text} {...props} />
))`
flex-shrink: 0;
opacity: ${props => (props.solid ? '1 !important' : 0)};
opacity: ${(props) => (props.solid ? "1 !important" : 0)};
transition: all 100ms ease-in-out;
&:hover {
@@ -37,7 +154,8 @@ const StyledStar = withTheme(styled(({ solid, theme, ...props }) => (
}
`);
const StyledDocumentMenu = styled(DocumentMenu)`
const SecondaryActions = styled(Flex)`
align-items: center;
position: absolute;
right: 16px;
top: 50%;
@@ -51,20 +169,25 @@ const DocumentLink = styled(Link)`
border-radius: 8px;
max-height: 50vh;
min-width: 100%;
max-width: calc(100vw - 40px);
overflow: hidden;
position: relative;
${StyledDocumentMenu} {
${SecondaryActions} {
opacity: 0;
}
&:hover,
&:active,
&:focus {
background: ${props => props.theme.listItemHoverBackground};
background: ${(props) => props.theme.listItemHoverBackground};
outline: none;
${StyledStar}, ${StyledDocumentMenu} {
${SecondaryActions} {
opacity: 1;
}
${StyledStar} {
opacity: 0.5;
&:hover {
@@ -82,8 +205,9 @@ const Heading = styled.h3`
margin-bottom: 0.25em;
overflow: hidden;
white-space: nowrap;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
color: ${(props) => props.theme.text};
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
`;
const Actions = styled(Flex)`
@@ -99,97 +223,10 @@ const Title = styled(Highlight)`
const ResultContext = styled(Highlight)`
display: block;
color: ${props => props.theme.textTertiary};
color: ${(props) => props.theme.textTertiary};
font-size: 14px;
margin-top: -0.25em;
margin-bottom: 0.25em;
`;
const SEARCH_RESULT_REGEX = /<b\b[^>]*>(.*?)<\/b>/gi;
@observer
class DocumentPreview extends React.Component<Props> {
star = (ev: SyntheticEvent<>) => {
ev.preventDefault();
ev.stopPropagation();
this.props.document.star();
};
unstar = (ev: SyntheticEvent<>) => {
ev.preventDefault();
ev.stopPropagation();
this.props.document.unstar();
};
replaceResultMarks = (tag: string) => {
// don't use SEARCH_RESULT_REGEX here as it causes
// an infinite loop to trigger a regex inside it's own callback
return tag.replace(/<b\b[^>]*>(.*?)<\/b>/gi, '$1');
};
render() {
const {
document,
showCollection,
showPublished,
showPin,
showDraft = true,
highlight,
context,
...rest
} = this.props;
const queryIsInTitle =
!!highlight &&
!!document.title.toLowerCase().includes(highlight.toLowerCase());
return (
<DocumentLink
to={{
pathname: document.url,
state: { title: document.title },
}}
{...rest}
>
<Heading>
<Title text={document.title} highlight={highlight} />
{!document.isDraft &&
!document.isArchived && (
<Actions>
{document.isStarred ? (
<StyledStar onClick={this.unstar} solid />
) : (
<StyledStar onClick={this.star} />
)}
</Actions>
)}
{document.isDraft &&
showDraft && (
<Tooltip
tooltip="Only visible to you"
delay={500}
placement="top"
>
<Badge>Draft</Badge>
</Tooltip>
)}
<StyledDocumentMenu document={document} showPin={showPin} />
</Heading>
{!queryIsInTitle && (
<ResultContext
text={context}
highlight={highlight ? SEARCH_RESULT_REGEX : undefined}
processResult={this.replaceResultMarks}
/>
)}
<PublishingInfo
document={document}
showCollection={showCollection}
showPublished={showPublished}
/>
</DocumentLink>
);
}
}
export default DocumentPreview;
export default withRouter(DocumentPreview);
+1 -1
View File
@@ -1,3 +1,3 @@
// @flow
import DocumentPreview from './DocumentPreview';
import DocumentPreview from "./DocumentPreview";
export default DocumentPreview;
+16 -16
View File
@@ -1,14 +1,14 @@
// @flow
import * as React from 'react';
import { observable } from 'mobx';
import { observer, inject } from 'mobx-react';
import { withRouter, type RouterHistory } from 'react-router-dom';
import { createGlobalStyle } from 'styled-components';
import invariant from 'invariant';
import importFile from 'utils/importFile';
import Dropzone from 'react-dropzone';
import DocumentsStore from 'stores/DocumentsStore';
import LoadingIndicator from 'components/LoadingIndicator';
import invariant from "invariant";
import { observable } from "mobx";
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 DocumentsStore from "stores/DocumentsStore";
import LoadingIndicator from "components/LoadingIndicator";
import importFile from "utils/importFile";
const EMPTY_OBJECT = {};
let importingLock = false;
@@ -22,7 +22,7 @@ type Props = {
documents: DocumentsStore,
disabled: boolean,
location: Object,
match: Object,
match: Match,
history: RouterHistory,
staticContext: Object,
};
@@ -30,12 +30,12 @@ type Props = {
export const GlobalStyles = createGlobalStyle`
.activeDropZone {
border-radius: 4px;
background: ${props => props.theme.slateDark};
svg { fill: ${props => props.theme.white}; }
background: ${(props) => props.theme.slateDark};
svg { fill: ${(props) => props.theme.white}; }
}
.activeDropZone a {
color: ${props => props.theme.white} !important;
color: ${(props) => props.theme.white} !important;
}
`;
@@ -56,7 +56,7 @@ class DropToImport extends React.Component<Props> {
if (documentId && !collectionId) {
const document = await this.props.documents.fetch(documentId);
invariant(document, 'Document not available');
invariant(document, "Document not available");
collectionId = document.collectionId;
}
@@ -110,4 +110,4 @@ class DropToImport extends React.Component<Props> {
}
}
export default inject('documents')(withRouter(DropToImport));
export default inject("documents")(withRouter(DropToImport));
+96 -47
View File
@@ -1,14 +1,15 @@
// @flow
import * as React from 'react';
import invariant from 'invariant';
import { observable } from 'mobx';
import { observer } from 'mobx-react';
import { PortalWithState } from 'react-portal';
import { MoreIcon } from 'outline-icons';
import styled from 'styled-components';
import Flex from 'shared/components/Flex';
import { fadeAndScaleIn } from 'shared/styles/animations';
import NudeButton from 'components/NudeButton';
import invariant from "invariant";
import { observable } from "mobx";
import { observer } from "mobx-react";
import { MoreIcon } from "outline-icons";
import { rgba } from "polished";
import * as React from "react";
import { PortalWithState } from "react-portal";
import styled from "styled-components";
import { fadeAndScaleIn } from "shared/styles/animations";
import Flex from "components/Flex";
import NudeButton from "components/NudeButton";
let previousClosePortal;
let counter = 0;
@@ -23,19 +24,21 @@ type Props = {
onClose?: () => void,
children?: Children,
className?: string,
hover?: boolean,
style?: Object,
position?: 'left' | 'right' | 'center',
position?: "left" | "right" | "center",
};
@observer
class DropdownMenu extends React.Component<Props> {
id: string = `menu${counter++}`;
closeTimeout: TimeoutID;
@observable top: ?number;
@observable bottom: ?number;
@observable right: ?number;
@observable left: ?number;
@observable position: 'left' | 'right' | 'center';
@observable position: "left" | "right" | "center";
@observable fixed: ?boolean;
@observable bodyRect: ClientRect;
@observable labelRect: ClientRect;
@@ -49,21 +52,21 @@ class DropdownMenu extends React.Component<Props> {
return (ev: SyntheticMouseEvent<HTMLElement>) => {
ev.preventDefault();
const currentTarget = ev.currentTarget;
invariant(document.body, 'why you not here');
invariant(document.body, "why you not here");
if (currentTarget instanceof HTMLDivElement) {
this.bodyRect = document.body.getBoundingClientRect();
this.labelRect = currentTarget.getBoundingClientRect();
this.top = this.labelRect.bottom - this.bodyRect.top;
this.bottom = undefined;
this.position = this.props.position || 'left';
this.position = this.props.position || "left";
if (currentTarget.parentElement) {
const triggerParentStyle = getComputedStyle(
currentTarget.parentElement
);
if (triggerParentStyle.position === 'static') {
if (triggerParentStyle.position === "static") {
this.fixed = true;
this.top = this.labelRect.bottom;
}
@@ -72,7 +75,7 @@ class DropdownMenu extends React.Component<Props> {
this.initPosition();
// attempt to keep only one flyout menu open at once
if (previousClosePortal) {
if (previousClosePortal && !this.props.hover) {
previousClosePortal();
}
previousClosePortal = closePortal;
@@ -82,10 +85,10 @@ class DropdownMenu extends React.Component<Props> {
};
initPosition() {
if (this.position === 'left') {
if (this.position === "left") {
this.right =
this.bodyRect.width - this.labelRect.left - this.labelRect.width;
} else if (this.position === 'center') {
} else if (this.position === "center") {
this.left = this.labelRect.left + this.labelRect.width / 2;
} else {
this.left = this.labelRect.left;
@@ -93,7 +96,7 @@ class DropdownMenu extends React.Component<Props> {
}
onOpen = () => {
if (typeof this.props.onOpen === 'function') {
if (typeof this.props.onOpen === "function") {
this.props.onOpen();
}
this.fitOnTheScreen();
@@ -112,18 +115,18 @@ class DropdownMenu extends React.Component<Props> {
this.bottom = undefined;
}
if (this.position === 'left' || this.position === 'right') {
if (this.position === "left" || this.position === "right") {
const totalWidth =
Math.sign(this.position === 'left' ? -1 : 1) * el.offsetLeft +
Math.sign(this.position === "left" ? -1 : 1) * el.offsetLeft +
el.scrollWidth;
const isVisible = totalWidth < window.innerWidth;
if (!isVisible) {
if (this.position === 'right') {
this.position = 'left';
if (this.position === "right") {
this.position = "left";
this.left = undefined;
} else if (this.position === 'left') {
this.position = 'right';
} else if (this.position === "left") {
this.position = "right";
this.right = undefined;
}
}
@@ -133,8 +136,21 @@ class DropdownMenu extends React.Component<Props> {
this.forceUpdate();
}
closeAfterTimeout = (closePortal: () => void) => () => {
if (this.closeTimeout) {
clearTimeout(this.closeTimeout);
}
this.closeTimeout = setTimeout(closePortal, 500);
};
clearCloseTimeout = () => {
if (this.closeTimeout) {
clearTimeout(this.closeTimeout);
}
};
render() {
const { className, label, children } = this.props;
const { className, hover, label, children } = this.props;
return (
<div className={className}>
@@ -145,13 +161,24 @@ class DropdownMenu extends React.Component<Props> {
closeOnEsc
>
{({ closePortal, openPortal, isOpen, portal }) => (
<React.Fragment>
<Label onClick={this.handleOpen(openPortal, closePortal)}>
<>
<Label
onMouseMove={hover ? this.clearCloseTimeout : undefined}
onMouseOut={
hover ? this.closeAfterTimeout(closePortal) : undefined
}
onMouseEnter={
hover ? this.handleOpen(openPortal, closePortal) : undefined
}
onClick={
hover ? undefined : this.handleOpen(openPortal, closePortal)
}
>
{label || (
<NudeButton
id={`${this.id}button`}
aria-haspopup="true"
aria-expanded={isOpen ? 'true' : 'false'}
aria-expanded={isOpen ? "true" : "false"}
aria-controls={this.id}
>
<MoreIcon />
@@ -170,10 +197,14 @@ class DropdownMenu extends React.Component<Props> {
>
<Menu
ref={this.menuRef}
onMouseMove={hover ? this.clearCloseTimeout : undefined}
onMouseOut={
hover ? this.closeAfterTimeout(closePortal) : undefined
}
onClick={
typeof children === 'function'
typeof children === "function"
? undefined
: ev => {
: (ev) => {
ev.stopPropagation();
closePortal();
}
@@ -183,13 +214,13 @@ class DropdownMenu extends React.Component<Props> {
aria-labelledby={`${this.id}button`}
role="menu"
>
{typeof children === 'function'
{typeof children === "function"
? children({ closePortal })
: children}
</Menu>
</Position>
)}
</React.Fragment>
</>
)}
</PortalWithState>
</div>
@@ -198,40 +229,58 @@ class DropdownMenu extends React.Component<Props> {
}
const Label = styled(Flex).attrs({
justify: 'center',
align: 'center',
justify: "center",
align: "center",
})`
z-index: 1000;
z-index: ${(props) => props.theme.depths.menu};
cursor: pointer;
`;
const Position = styled.div`
position: ${({ fixed }) => (fixed ? 'fixed' : 'absolute')};
position: ${({ fixed }) => (fixed ? "fixed" : "absolute")};
display: flex;
${({ left }) => (left !== undefined ? `left: ${left}px` : '')};
${({ right }) => (right !== undefined ? `right: ${right}px` : '')};
${({ top }) => (top !== undefined ? `top: ${top}px` : '')};
${({ bottom }) => (bottom !== undefined ? `bottom: ${bottom}px` : '')};
${({ left }) => (left !== undefined ? `left: ${left}px` : "")};
${({ right }) => (right !== undefined ? `right: ${right}px` : "")};
${({ top }) => (top !== undefined ? `top: ${top}px` : "")};
${({ bottom }) => (bottom !== undefined ? `bottom: ${bottom}px` : "")};
max-height: 75%;
z-index: 1000;
transform: ${props =>
props.position === 'center' ? 'translateX(-50%)' : 'initial'};
z-index: ${(props) => props.theme.depths.menu};
transform: ${(props) =>
props.position === "center" ? "translateX(-50%)" : "initial"};
pointer-events: none;
`;
const Menu = styled.div`
animation: ${fadeAndScaleIn} 200ms ease;
transform-origin: ${props => (props.left !== undefined ? '25%' : '75%')} 0;
background: ${props => props.theme.menuBackground};
transform-origin: ${(props) => (props.left !== undefined ? "25%" : "75%")} 0;
backdrop-filter: blur(10px);
background: ${(props) => rgba(props.theme.menuBackground, 0.8)};
border: ${(props) =>
props.theme.menuBorder ? `1px solid ${props.theme.menuBorder}` : "none"};
border-radius: 2px;
padding: 0.5em 0;
min-width: 180px;
overflow: hidden;
overflow-y: auto;
box-shadow: ${props => props.theme.menuShadow};
box-shadow: ${(props) => props.theme.menuShadow};
pointer-events: all;
hr {
margin: 0.5em 12px;
}
@media print {
display: none;
}
`;
export const Header = styled.h3`
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
color: ${(props) => props.theme.sidebarText};
letter-spacing: 0.04em;
margin: 1em 12px 0.5em;
`;
export default DropdownMenu;
@@ -1,14 +1,22 @@
// @flow
import * as React from 'react';
import styled from 'styled-components';
import { CheckmarkIcon } from "outline-icons";
import * as React from "react";
import styled from "styled-components";
type Props = {
onClick?: (SyntheticEvent<>) => void | Promise<void>,
children?: React.Node,
selected?: boolean,
disabled?: boolean,
};
const DropdownMenuItem = ({ onClick, children, disabled, ...rest }: Props) => {
const DropdownMenuItem = ({
onClick,
children,
selected,
disabled,
...rest
}: Props) => {
return (
<MenuItem
onClick={disabled ? undefined : onClick}
@@ -17,6 +25,14 @@ const DropdownMenuItem = ({ onClick, children, disabled, ...rest }: Props) => {
tabIndex="-1"
{...rest}
>
{selected !== undefined && (
<>
<CheckmarkIcon
color={selected === false ? "transparent" : undefined}
/>
&nbsp;
</>
)}
{children}
</MenuItem>
);
@@ -26,9 +42,10 @@ const MenuItem = styled.a`
display: flex;
margin: 0;
padding: 6px 12px;
height: 32px;
width: 100%;
min-height: 32px;
color: ${props =>
color: ${(props) =>
props.disabled ? props.theme.textTertiary : props.theme.textSecondary};
justify-content: left;
align-items: center;
@@ -41,12 +58,12 @@ const MenuItem = styled.a`
}
svg {
opacity: ${props => (props.disabled ? '.5' : 1)};
opacity: ${(props) => (props.disabled ? ".5" : 1)};
}
${props =>
${(props) =>
props.disabled
? 'pointer-events: none;'
? "pointer-events: none;"
: `
&:hover {
@@ -59,6 +76,12 @@ const MenuItem = styled.a`
fill: ${props.theme.white};
}
}
&:focus {
color: ${props.theme.white};
background: ${props.theme.primary};
outline: none;
}
`};
`;
+2 -2
View File
@@ -1,3 +1,3 @@
// @flow
export { default as DropdownMenu } from './DropdownMenu';
export { default as DropdownMenuItem } from './DropdownMenuItem';
export { default as DropdownMenu, Header } from "./DropdownMenu";
export { default as DropdownMenuItem } from "./DropdownMenuItem";
+129
View File
@@ -0,0 +1,129 @@
// @flow
import { lighten } from "polished";
import * as React from "react";
import { withRouter, type RouterHistory } from "react-router-dom";
import styled, { withTheme } from "styled-components";
import UiStore from "stores/UiStore";
import ErrorBoundary from "components/ErrorBoundary";
import Tooltip from "components/Tooltip";
import embeds from "../embeds";
import isInternalUrl from "utils/isInternalUrl";
import { uploadFile } from "utils/uploadFile";
const RichMarkdownEditor = React.lazy(() => import("rich-markdown-editor"));
const EMPTY_ARRAY = [];
type Props = {
id?: string,
defaultValue?: string,
readOnly?: boolean,
grow?: boolean,
disableEmbeds?: boolean,
ui?: UiStore,
};
type PropsWithRef = Props & {
forwardedRef: React.Ref<any>,
history: RouterHistory,
};
class Editor extends React.Component<PropsWithRef> {
onUploadImage = async (file: File) => {
const result = await uploadFile(file, { documentId: this.props.id });
return result.url;
};
onClickLink = (href: string) => {
// on page hash
if (href[0] === "#") {
window.location.href = href;
return;
}
if (isInternalUrl(href)) {
// relative
let navigateTo = href;
// probably absolute
if (href[0] !== "/") {
try {
const url = new URL(href);
navigateTo = url.pathname + url.hash;
} catch (err) {
navigateTo = href;
}
}
this.props.history.push(navigateTo);
} else {
window.open(href, "_blank");
}
};
onShowToast = (message: string) => {
if (this.props.ui) {
this.props.ui.showToast(message);
}
};
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>
);
}
}
const StyledEditor = styled(RichMarkdownEditor)`
flex-grow: ${(props) => (props.grow ? 1 : 0)};
justify-content: start;
> div {
transition: ${(props) => props.theme.backgroundTransition};
}
.notice-block.tip,
.notice-block.warning {
font-weight: 500;
}
p {
a {
color: ${(props) => props.theme.text};
border-bottom: 1px solid ${(props) => lighten(0.5, props.theme.text)};
text-decoration: none !important;
font-weight: 500;
&:hover {
border-bottom: 1px solid ${(props) => props.theme.text};
text-decoration: none;
}
}
}
`;
const EditorTooltip = ({ children, ...props }) => (
<Tooltip offset="0, 16" delay={150} {...props}>
<Span>{children}</Span>
</Tooltip>
);
const Span = styled.span`
outline: none;
`;
const EditorWithRouterAndTheme = withRouter(withTheme(Editor));
export default React.forwardRef<Props, typeof Editor>((props, ref) => (
<EditorWithRouterAndTheme {...props} forwardedRef={ref} />
));
-288
View File
@@ -1,288 +0,0 @@
// @flow
import * as React from 'react';
import { withRouter, type RouterHistory } from 'react-router-dom';
import { observable } from 'mobx';
import { observer } from 'mobx-react';
import { lighten } from 'polished';
import styled, { withTheme, createGlobalStyle } from 'styled-components';
import RichMarkdownEditor from 'rich-markdown-editor';
import Placeholder from 'rich-markdown-editor/lib/components/Placeholder';
import { uploadFile } from 'utils/uploadFile';
import isInternalUrl from 'utils/isInternalUrl';
import Tooltip from 'components/Tooltip';
import UiStore from 'stores/UiStore';
import Embed from './Embed';
import embeds from '../../embeds';
type Props = {
id: string,
defaultValue?: string,
readOnly?: boolean,
grow?: boolean,
disableEmbeds?: boolean,
history: RouterHistory,
forwardedRef: React.Ref<RichMarkdownEditor>,
ui: UiStore,
};
@observer
class Editor extends React.Component<Props> {
@observable redirectTo: ?string;
onUploadImage = async (file: File) => {
const result = await uploadFile(file, { documentId: this.props.id });
return result.url;
};
onClickLink = (href: string) => {
// on page hash
if (href[0] === '#') {
window.location.href = href;
return;
}
if (isInternalUrl(href)) {
// relative
let navigateTo = href;
// probably absolute
if (href[0] !== '/') {
try {
const url = new URL(href);
navigateTo = url.pathname + url.hash;
} catch (err) {
navigateTo = href;
}
}
this.props.history.push(navigateTo);
} else {
window.open(href, '_blank');
}
};
onShowToast = (message: string) => {
this.props.ui.showToast(message);
};
getLinkComponent = node => {
if (this.props.disableEmbeds) return;
const url = node.data.get('href');
const keys = Object.keys(embeds);
for (const key of keys) {
const component = embeds[key];
for (const host of component.ENABLED) {
const matches = url.match(host);
if (matches) return Embed;
}
}
};
render() {
return (
<React.Fragment>
<PrismStyles />
<StyledEditor
ref={this.props.forwardedRef}
uploadImage={this.onUploadImage}
onClickLink={this.onClickLink}
onShowToast={this.onShowToast}
getLinkComponent={this.getLinkComponent}
tooltip={EditorTooltip}
{...this.props}
/>
</React.Fragment>
);
}
}
const StyledEditor = styled(RichMarkdownEditor)`
flex-grow: ${props => (props.grow ? 1 : 0)};
justify-content: start;
> div {
transition: ${props => props.theme.backgroundTransition};
}
p {
${Placeholder} {
visibility: hidden;
}
}
p:nth-child(2):last-child {
${Placeholder} {
visibility: visible;
}
}
p {
a {
color: ${props => props.theme.link};
border-bottom: 1px solid ${props => lighten(0.5, props.theme.link)};
font-weight: 500;
&:hover {
border-bottom: 1px solid ${props => props.theme.link};
text-decoration: none;
}
}
}
`;
/*
Based on Prism template by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/prism/)
Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16)
*/
const PrismStyles = createGlobalStyle`
code[class*="language-"],
pre[class*="language-"] {
-webkit-font-smoothing: initial;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
font-size: 13px;
line-height: 1.375;
direction: ltr;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
color: #24292e;
}
/* Code blocks */
pre[class*="language-"] {
padding: 1em;
margin: .5em 0;
overflow: auto;
}
/* Inline code */
:not(pre) > code[class*="language-"] {
padding: .1em;
border-radius: .3em;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #6a737d;
}
.token.punctuation {
color: #5e6687;
}
.token.namespace {
opacity: .7;
}
.token.operator,
.token.boolean,
.token.number {
color: #d73a49;
}
.token.property {
color: #c08b30;
}
.token.tag {
color: #3d8fd1;
}
.token.string {
color: #032f62;
}
.token.selector {
color: #6679cc;
}
.token.attr-name {
color: #c76b29;
}
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
color: #22a2c9;
}
.token.attr-value,
.token.keyword,
.token.control,
.token.directive,
.token.unit {
color: #d73a49;
}
.token.function {
color: #6f42c1;
}
.token.statement,
.token.regex,
.token.atrule {
color: #22a2c9;
}
.token.placeholder,
.token.variable {
color: #3d8fd1;
}
.token.deleted {
text-decoration: line-through;
}
.token.inserted {
border-bottom: 1px dotted #202746;
text-decoration: none;
}
.token.italic {
font-style: italic;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.important {
color: #c94922;
}
.token.entity {
cursor: help;
}
pre > code.highlight {
outline: 0.4em solid #c94922;
outline-offset: .4em;
}
`;
const EditorTooltip = ({ children, ...props }) => (
<Tooltip offset="0, 16" delay={150} {...props}>
<span>{children}</span>
</Tooltip>
);
const EditorWithRouterAndTheme = withRouter(withTheme(Editor));
// $FlowIssue - https://github.com/facebook/flow/issues/6103
export default React.forwardRef((props, ref) => (
<EditorWithRouterAndTheme {...props} forwardedRef={ref} />
));
-54
View File
@@ -1,54 +0,0 @@
// @flow
import * as React from 'react';
import styled from 'styled-components';
import { fadeIn } from 'shared/styles/animations';
import embeds from '../../embeds';
export default class Embed extends React.Component<*> {
get url(): string {
return this.props.node.data.get('href');
}
getMatchResults(): ?{ component: *, matches: string[] } {
const keys = Object.keys(embeds);
for (const key of keys) {
const component = embeds[key];
for (const host of component.ENABLED) {
const matches = this.url.match(host);
if (matches) return { component, matches };
}
}
}
render() {
const result = this.getMatchResults();
if (!result) return null;
const { attributes, isSelected, children } = this.props;
const { component, matches } = result;
const EmbedComponent = component;
return (
<Container
contentEditable={false}
isSelected={isSelected}
{...attributes}
>
<EmbedComponent matches={matches} url={this.url} />
{children}
</Container>
);
}
}
const Container = styled.div`
text-align: center;
animation: ${fadeIn} 500ms ease-in-out;
line-height: 0;
border-radius: 3px;
box-shadow: ${props =>
props.isSelected ? `0 0 0 2px ${props.theme.selected}` : 'none'};
`;
-3
View File
@@ -1,3 +0,0 @@
// @flow
import Editor from './Editor';
export default Editor;
+3 -15
View File
@@ -1,20 +1,8 @@
// @flow
import * as React from 'react';
import styled from 'styled-components';
import styled from "styled-components";
type Props = {
children: React.Node,
};
const Empty = (props: Props) => {
const { children, ...rest } = props;
return <Container {...rest}>{children}</Container>;
};
const Container = styled.div`
display: flex;
color: ${props => props.theme.slate};
text-align: center;
const Empty = styled.p`
color: ${(props) => props.theme.textTertiary};
`;
export default Empty;
+32 -17
View File
@@ -1,16 +1,18 @@
// @flow
import * as React from 'react';
import styled from 'styled-components';
import { observer } from 'mobx-react';
import { observable } from 'mobx';
import HelpText from 'components/HelpText';
import Button from 'components/Button';
import CenteredContent from 'components/CenteredContent';
import PageTitle from 'components/PageTitle';
import { githubIssuesUrl } from '../../shared/utils/routeHelpers';
import { observable } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import styled from "styled-components";
import Button from "components/Button";
import CenteredContent from "components/CenteredContent";
import HelpText from "components/HelpText";
import PageTitle from "components/PageTitle";
import { githubIssuesUrl } from "../../shared/utils/routeHelpers";
import env from "env";
type Props = {
children: React.Node,
reloadOnChunkMissing?: boolean,
};
@observer
@@ -20,14 +22,27 @@ class ErrorBoundary extends React.Component<Props> {
componentDidCatch(error: Error, info: Object) {
this.error = error;
console.error(error);
if (
this.props.reloadOnChunkMissing &&
error.message &&
error.message.match(/chunk/)
) {
// If the editor bundle fails to load then reload the entire window. This
// can happen if a deploy happens between the user loading the initial JS
// bundle and the async-loaded editor JS bundle as the hash will change.
window.location.reload(true);
return;
}
if (window.Sentry) {
Sentry.captureException(error);
window.Sentry.captureException(error);
}
}
handleReload = () => {
window.location.reload();
window.location.reload(true);
};
handleShowDetails = () => {
@@ -40,20 +55,20 @@ class ErrorBoundary extends React.Component<Props> {
render() {
if (this.error) {
const isReported = !!window.Sentry;
const isReported = !!window.Sentry && env.DEPLOYMENT === "hosted";
return (
<CenteredContent>
<PageTitle title="Something Unexpected Happened" />
<h1>Something Unexpected Happened</h1>
<HelpText>
Sorry, an unrecoverable error occurred{isReported &&
' our engineers have been notified'}. Please try reloading the
page, it may have been a temporary glitch.
Sorry, an unrecoverable error occurred
{isReported && " our engineers have been notified"}. Please try
reloading the page, it may have been a temporary glitch.
</HelpText>
{this.showDetails && <Pre>{this.error.toString()}</Pre>}
<p>
<Button onClick={this.handleReload}>Reload</Button>{' '}
<Button onClick={this.handleReload}>Reload</Button>{" "}
{this.showDetails ? (
<Button onClick={this.handleReportBug} neutral>
Report a Bug
@@ -72,7 +87,7 @@ class ErrorBoundary extends React.Component<Props> {
}
const Pre = styled.pre`
background: ${props => props.theme.smoke};
background: ${(props) => props.theme.smoke};
padding: 16px;
border-radius: 4px;
font-size: 12px;
+76
View File
@@ -0,0 +1,76 @@
// @flow
import { observer, inject } from "mobx-react";
import * as React from "react";
import styled, { withTheme } from "styled-components";
import User from "models/User";
import Avatar from "components/Avatar";
import Flex from "components/Flex";
type Props = {
users: User[],
size?: number,
overflow: number,
renderAvatar: (user: User) => React.Node,
};
@observer
class Facepile extends React.Component<Props> {
render() {
const {
users,
overflow,
size = 32,
renderAvatar = renderDefaultAvatar,
...rest
} = this.props;
return (
<Avatars {...rest}>
{overflow > 0 && (
<More size={size}>
<span>+{overflow}</span>
</More>
)}
{users.map((user) => (
<AvatarWrapper key={user.id}>{renderAvatar(user)}</AvatarWrapper>
))}
</Avatars>
);
}
}
function renderDefaultAvatar(user: User) {
return <Avatar user={user} src={user.avatarUrl} size={32} />;
}
const AvatarWrapper = styled.div`
margin-right: -8px;
&:first-child {
margin-right: 0;
}
`;
const More = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-width: ${(props) => props.size}px;
height: ${(props) => props.size}px;
border-radius: 100%;
background: ${(props) => props.theme.slate};
color: ${(props) => props.theme.text};
border: 2px solid ${(props) => props.theme.background};
text-align: center;
font-size: 11px;
font-weight: 600;
`;
const Avatars = styled(Flex)`
align-items: center;
flex-direction: row-reverse;
cursor: pointer;
`;
export default inject("views", "presence")(withTheme(Facepile));
+3 -3
View File
@@ -1,9 +1,9 @@
// @flow
import styled from 'styled-components';
import { fadeIn } from 'shared/styles/animations';
import styled from "styled-components";
import { fadeIn } from "shared/styles/animations";
const Fade = styled.span`
animation: ${fadeIn} ${props => props.timing || '250ms'} ease-in-out;
animation: ${fadeIn} ${(props) => props.timing || "250ms"} ease-in-out;
`;
export default Fade;
@@ -1,20 +1,20 @@
// @flow
import * as React from 'react';
import styled from 'styled-components';
import * as React from "react";
import styled from "styled-components";
type JustifyValues =
| 'center'
| 'space-around'
| 'space-between'
| 'flex-start'
| 'flex-end';
| "center"
| "space-around"
| "space-between"
| "flex-start"
| "flex-end";
type AlignValues =
| 'stretch'
| 'center'
| 'baseline'
| 'flex-start'
| 'flex-end';
| "stretch"
| "center"
| "baseline"
| "flex-start"
| "flex-end";
type Props = {
column?: ?boolean,
@@ -33,11 +33,11 @@ const Flex = (props: Props) => {
const Container = styled.div`
display: flex;
flex: ${({ auto }) => (auto ? '1 1 auto' : 'initial')};
flex-direction: ${({ column }) => (column ? 'column' : 'row')};
flex: ${({ auto }) => (auto ? "1 1 auto" : "initial")};
flex-direction: ${({ column }) => (column ? "column" : "row")};
align-items: ${({ align }) => align};
justify-content: ${({ justify }) => justify};
flex-shrink: ${({ shrink }) => (shrink ? 1 : 'initial')};
flex-shrink: ${({ shrink }) => (shrink ? 1 : "initial")};
min-height: 0;
min-width: 0;
`;
+24
View File
@@ -0,0 +1,24 @@
// @flow
import * as React from "react";
import styled from "styled-components";
import Empty from "components/Empty";
import Fade from "components/Fade";
import Flex from "components/Flex";
export default function FullscreenLoading() {
return (
<Fade timing={500}>
<Centered>
<Empty>Loading</Empty>
</Centered>
</Fade>
);
}
const Centered = styled(Flex)`
text-align: center;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
`;
@@ -1,5 +1,5 @@
// @flow
import * as React from 'react';
import * as React from "react";
type Props = {
size?: number,
@@ -7,7 +7,7 @@ type Props = {
className?: string,
};
function GithubLogo({ size = 34, fill = '#FFF', className }: Props) {
function GithubLogo({ size = 34, fill = "#FFF", className }: Props) {
return (
<svg
fill={fill}
@@ -1,5 +1,5 @@
// @flow
import * as React from 'react';
import * as React from "react";
type Props = {
size?: number,
@@ -7,7 +7,7 @@ type Props = {
className?: string,
};
function GoogleLogo({ size = 34, fill = '#FFF', className }: Props) {
function GoogleLogo({ size = 34, fill = "#FFF", className }: Props) {
return (
<svg
fill={fill}
+94
View File
@@ -0,0 +1,94 @@
// @flow
import { observable } from "mobx";
import { observer, inject } from "mobx-react";
import * as React from "react";
import styled from "styled-components";
import { MAX_AVATAR_DISPLAY } from "shared/constants";
import GroupMembershipsStore from "stores/GroupMembershipsStore";
import CollectionGroupMembership from "models/CollectionGroupMembership";
import Group from "models/Group";
import GroupMembers from "scenes/GroupMembers";
import Facepile from "components/Facepile";
import Flex from "components/Flex";
import ListItem from "components/List/Item";
import Modal from "components/Modal";
type Props = {
group: Group,
groupMemberships: GroupMembershipsStore,
membership?: CollectionGroupMembership,
showFacepile: boolean,
renderActions: ({ openMembersModal: () => void }) => React.Node,
};
@observer
class GroupListItem extends React.Component<Props> {
@observable membersModalOpen: boolean = false;
handleMembersModalOpen = () => {
this.membersModalOpen = true;
};
handleMembersModalClose = () => {
this.membersModalOpen = false;
};
render() {
const { group, groupMemberships, showFacepile, renderActions } = this.props;
const memberCount = group.memberCount;
const membershipsInGroup = groupMemberships.inGroup(group.id);
const users = membershipsInGroup
.slice(0, MAX_AVATAR_DISPLAY)
.map((gm) => gm.user);
const overflow = memberCount - users.length;
return (
<>
<ListItem
title={
<Title onClick={this.handleMembersModalOpen}>{group.name}</Title>
}
subtitle={
<>
{memberCount} member{memberCount === 1 ? "" : "s"}
</>
}
actions={
<Flex align="center">
{showFacepile && (
<Facepile
onClick={this.handleMembersModalOpen}
users={users}
overflow={overflow}
/>
)}
&nbsp;
{renderActions({
openMembersModal: this.handleMembersModalOpen,
})}
</Flex>
}
/>
<Modal
title="Group members"
onRequestClose={this.handleMembersModalClose}
isOpen={this.membersModalOpen}
>
<GroupMembers group={group} onSubmit={this.handleMembersModalClose} />
</Modal>
</>
);
}
}
const Title = styled.span`
&:hover {
text-decoration: underline;
cursor: pointer;
}
`;
export default inject("groupMemberships")(GroupListItem);
+2 -1
View File
@@ -1,9 +1,10 @@
// @flow
import styled from 'styled-components';
import styled from "styled-components";
const Heading = styled.h1`
display: flex;
align-items: center;
${(props) => (props.centered ? "text-align: center;" : "")}
svg {
margin-left: -6px;
+3 -3
View File
@@ -1,10 +1,10 @@
// @flow
import styled from 'styled-components';
import styled from "styled-components";
const HelpText = styled.p`
margin-top: 0;
color: ${props => props.theme.textSecondary};
font-size: ${props => (props.small ? '13px' : 'inherit')};
color: ${(props) => props.theme.textSecondary};
font-size: ${(props) => (props.small ? "13px" : "inherit")};
`;
export default HelpText;
+6 -6
View File
@@ -1,7 +1,7 @@
// @flow
import * as React from 'react';
import replace from 'string-replace-to-array';
import styled from 'styled-components';
import * as React from "react";
import replace from "string-replace-to-array";
import styled from "styled-components";
type Props = {
highlight: ?string | RegExp,
@@ -22,8 +22,8 @@ function Highlight({
regex = highlight;
} else {
regex = new RegExp(
(highlight || '').replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&'),
caseSensitive ? 'g' : 'gi'
(highlight || "").replace(/[-\\^$*+?.()|[\]{}]/g, "\\$&"),
caseSensitive ? "g" : "gi"
);
}
return (
@@ -38,7 +38,7 @@ function Highlight({
}
const Mark = styled.mark`
background: ${props => props.theme.yellow};
background: ${(props) => props.theme.yellow};
border-radius: 2px;
padding: 0 4px;
`;
+237
View File
@@ -0,0 +1,237 @@
// @flow
import { inject } from "mobx-react";
import { transparentize } from "polished";
import * as React from "react";
import { Portal } from "react-portal";
import styled from "styled-components";
import { fadeAndSlideIn } from "shared/styles/animations";
import { parseDocumentSlugFromUrl } from "shared/utils/parseDocumentSlug";
import DocumentsStore from "stores/DocumentsStore";
import HoverPreviewDocument from "components/HoverPreviewDocument";
import isInternalUrl from "utils/isInternalUrl";
const DELAY_OPEN = 300;
const DELAY_CLOSE = 300;
type Props = {
node: HTMLAnchorElement,
event: MouseEvent,
documents: DocumentsStore,
onClose: () => void,
};
function HoverPreview({ node, documents, onClose, event }: Props) {
// previews only work for internal doc links for now
if (!isInternalUrl(node.href)) {
return null;
}
const slug = parseDocumentSlugFromUrl(node.href);
const [isVisible, setVisible] = React.useState(false);
const timerClose = React.useRef();
const timerOpen = React.useRef();
const cardRef = React.useRef<?HTMLDivElement>();
const startCloseTimer = () => {
stopOpenTimer();
timerClose.current = setTimeout(() => {
if (isVisible) setVisible(false);
onClose();
}, DELAY_CLOSE);
};
const stopCloseTimer = () => {
if (timerClose.current) {
clearTimeout(timerClose.current);
}
};
const startOpenTimer = () => {
timerOpen.current = setTimeout(() => setVisible(true), DELAY_OPEN);
};
const stopOpenTimer = () => {
if (timerOpen.current) {
clearTimeout(timerOpen.current);
}
};
React.useEffect(() => {
if (slug) {
documents.prefetchDocument(slug, {
prefetch: true,
});
}
startOpenTimer();
if (cardRef.current) {
cardRef.current.addEventListener("mouseenter", stopCloseTimer);
}
if (cardRef.current) {
cardRef.current.addEventListener("mouseleave", startCloseTimer);
}
node.addEventListener("mouseout", startCloseTimer);
node.addEventListener("mouseover", stopCloseTimer);
node.addEventListener("mouseover", startOpenTimer);
return () => {
node.removeEventListener("mouseout", startCloseTimer);
node.removeEventListener("mouseover", stopCloseTimer);
node.removeEventListener("mouseover", startOpenTimer);
if (cardRef.current) {
cardRef.current.removeEventListener("mouseenter", stopCloseTimer);
}
if (cardRef.current) {
cardRef.current.removeEventListener("mouseleave", startCloseTimer);
}
if (timerClose.current) {
clearTimeout(timerClose.current);
}
};
}, [node]);
const anchorBounds = node.getBoundingClientRect();
const cardBounds = cardRef.current
? cardRef.current.getBoundingClientRect()
: undefined;
const left = cardBounds
? Math.min(anchorBounds.left, window.innerWidth - 16 - 350)
: anchorBounds.left;
const leftOffset = anchorBounds.left - left;
return (
<Portal>
<Position
top={anchorBounds.bottom + window.scrollY}
left={left}
aria-hidden
>
<div ref={cardRef}>
<HoverPreviewDocument url={node.href}>
{(content) =>
isVisible ? (
<Animate>
<Card>
<Margin />
<CardContent>{content}</CardContent>
</Card>
<Pointer offset={leftOffset + anchorBounds.width / 2} />
</Animate>
) : null
}
</HoverPreviewDocument>
</div>
</Position>
</Portal>
);
}
const Animate = styled.div`
animation: ${fadeAndSlideIn} 150ms ease;
@media print {
display: none;
}
`;
// fills the gap between the card and pointer to avoid a dead zone
const Margin = styled.div`
position: absolute;
top: -11px;
left: 0;
right: 0;
height: 11px;
`;
const CardContent = styled.div`
overflow: hidden;
max-height: 350px;
user-select: none;
`;
// &:after — gradient mask for overflow text
const Card = styled.div`
backdrop-filter: blur(10px);
background: ${(props) => props.theme.background};
border: ${(props) =>
props.theme.menuBorder ? `1px solid ${props.theme.menuBorder}` : "none"};
border-radius: 4px;
box-shadow: 0 30px 90px -20px rgba(0, 0, 0, 0.3),
0 0 1px 1px rgba(0, 0, 0, 0.05);
padding: 16px;
width: 350px;
font-size: 0.9em;
position: relative;
.placeholder,
.heading-anchor {
display: none;
}
&:after {
content: "";
display: block;
position: absolute;
pointer-events: none;
background: linear-gradient(
90deg,
${(props) => transparentize(1, props.theme.background)} 0%,
${(props) => transparentize(1, props.theme.background)} 75%,
${(props) => props.theme.background} 90%
);
bottom: 0;
left: 0;
right: 0;
height: 1.7em;
border-bottom: 16px solid ${(props) => props.theme.background};
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
`;
const Position = styled.div`
margin-top: 10px;
position: ${({ fixed }) => (fixed ? "fixed" : "absolute")};
display: flex;
max-height: 75%;
${({ top }) => (top !== undefined ? `top: ${top}px` : "")};
${({ left }) => (left !== undefined ? `left: ${left}px` : "")};
`;
const Pointer = styled.div`
top: -22px;
left: ${(props) => props.offset}px;
width: 22px;
height: 22px;
position: absolute;
transform: translateX(-50%);
&:before,
&:after {
content: "";
display: inline-block;
position: absolute;
bottom: 0;
right: 0;
}
&:before {
border: 8px solid transparent;
border-bottom-color: ${(props) =>
props.theme.menuBorder || "rgba(0, 0, 0, 0.1)"};
right: -1px;
}
&:after {
border: 7px solid transparent;
border-bottom-color: ${(props) => props.theme.background};
}
`;
export default inject("documents")(HoverPreview);
+53
View File
@@ -0,0 +1,53 @@
// @flow
import { inject, observer } from "mobx-react";
import * as React from "react";
import { Link } from "react-router-dom";
import styled from "styled-components";
import { parseDocumentSlugFromUrl } from "shared/utils/parseDocumentSlug";
import DocumentsStore from "stores/DocumentsStore";
import DocumentMetaWithViews from "components/DocumentMetaWithViews";
import Editor from "components/Editor";
type Props = {
url: string,
documents: DocumentsStore,
children: (React.Node) => React.Node,
};
function HoverPreviewDocument({ url, documents, children }: Props) {
const slug = parseDocumentSlugFromUrl(url);
documents.prefetchDocument(slug, {
prefetch: true,
});
const document = slug ? documents.getByUrl(slug) : undefined;
if (!document) return null;
return children(
<Content to={document.url}>
<Heading>{document.titleWithDefault}</Heading>
<DocumentMetaWithViews isDraft={document.isDraft} document={document} />
<React.Suspense fallback={<div />}>
<Editor
key={document.id}
defaultValue={document.getSummary()}
disableEmbeds
readOnly
/>
</React.Suspense>
</Content>
);
}
const Content = styled(Link)`
cursor: pointer;
`;
const Heading = styled.h2`
margin: 0 0 0.75em;
color: ${(props) => props.theme.text};
`;
export default inject("documents")(observer(HoverPreviewDocument));
+249
View File
@@ -0,0 +1,249 @@
// @flow
import { observable } from "mobx";
import { observer } from "mobx-react";
import {
CollectionIcon,
CoinsIcon,
AcademicCapIcon,
BeakerIcon,
BuildingBlocksIcon,
CloudIcon,
CodeIcon,
EditIcon,
EyeIcon,
LeafIcon,
LightBulbIcon,
MoonIcon,
NotepadIcon,
PadlockIcon,
PaletteIcon,
QuestionMarkIcon,
SunIcon,
VehicleIcon,
} from "outline-icons";
import * as React from "react";
import styled from "styled-components";
import { DropdownMenu } from "components/DropdownMenu";
import Flex from "components/Flex";
import HelpText from "components/HelpText";
import { LabelText } from "components/Input";
import NudeButton from "components/NudeButton";
const TwitterPicker = React.lazy(() =>
import("react-color/lib/components/twitter/Twitter")
);
export const icons = {
collection: {
component: CollectionIcon,
keywords: "collection",
},
coins: {
component: CoinsIcon,
keywords: "coins money finance sales income revenue cash",
},
academicCap: {
component: AcademicCapIcon,
keywords: "learn teach lesson guide tutorial onboarding training",
},
beaker: {
component: BeakerIcon,
keywords: "lab research experiment test",
},
buildingBlocks: {
component: BuildingBlocksIcon,
keywords: "app blocks product prototype",
},
cloud: {
component: CloudIcon,
keywords: "cloud service aws infrastructure",
},
code: {
component: CodeIcon,
keywords: "developer api code development engineering programming",
},
eye: {
component: EyeIcon,
keywords: "eye view",
},
leaf: {
component: LeafIcon,
keywords: "leaf plant outdoors nature ecosystem climate",
},
lightbulb: {
component: LightBulbIcon,
keywords: "lightbulb idea",
},
moon: {
component: MoonIcon,
keywords: "night moon dark",
},
notepad: {
component: NotepadIcon,
keywords: "journal notepad write notes",
},
padlock: {
component: PadlockIcon,
keywords: "padlock private security authentication authorization auth",
},
palette: {
component: PaletteIcon,
keywords: "design palette art brand",
},
pencil: {
component: EditIcon,
keywords: "copy writing post blog",
},
question: {
component: QuestionMarkIcon,
keywords: "question help support faq",
},
sun: {
component: SunIcon,
keywords: "day sun weather",
},
vehicle: {
component: VehicleIcon,
keywords: "truck car travel transport",
},
};
const colors = [
"#4E5C6E",
"#0366d6",
"#9E5CF7",
"#FF825C",
"#FF5C80",
"#FFBE0B",
"#42DED1",
"#00D084",
"#FF4DFA",
"#2F362F",
];
type Props = {
onOpen?: () => void,
onChange: (color: string, icon: string) => void,
icon: string,
color: string,
};
function preventEventBubble(event) {
event.stopPropagation();
}
@observer
class IconPicker extends React.Component<Props> {
@observable isOpen: boolean = false;
node: ?HTMLElement;
componentDidMount() {
window.addEventListener("click", this.handleClickOutside);
}
componentWillUnmount() {
window.removeEventListener("click", this.handleClickOutside);
}
handleClose = () => {
this.isOpen = false;
};
handleOpen = () => {
this.isOpen = true;
if (this.props.onOpen) {
this.props.onOpen();
}
};
handleClickOutside = (ev: SyntheticMouseEvent<>) => {
// $FlowFixMe
if (ev.target && this.node && this.node.contains(ev.target)) {
return;
}
this.handleClose();
};
render() {
const Component = icons[this.props.icon || "collection"].component;
return (
<Wrapper ref={(ref) => (this.node = ref)}>
<label>
<LabelText>Icon</LabelText>
</label>
<DropdownMenu
onOpen={this.handleOpen}
label={
<LabelButton>
<Component role="button" color={this.props.color} size={30} />
</LabelButton>
}
>
<Icons onClick={preventEventBubble}>
{Object.keys(icons).map((name) => {
const Component = icons[name].component;
return (
<IconButton
key={name}
onClick={() => this.props.onChange(this.props.color, name)}
style={{ width: 30, height: 30 }}
>
<Component color={this.props.color} size={30} />
</IconButton>
);
})}
</Icons>
<Flex onClick={preventEventBubble}>
<React.Suspense fallback={<Loading>Loading</Loading>}>
<ColorPicker
color={this.props.color}
onChange={(color) =>
this.props.onChange(color.hex, this.props.icon)
}
colors={colors}
triangle="hide"
/>
</React.Suspense>
</Flex>
</DropdownMenu>
</Wrapper>
);
}
}
const Icons = styled.div`
padding: 15px 9px 9px 15px;
width: 276px;
`;
const LabelButton = styled(NudeButton)`
border: 1px solid ${(props) => props.theme.inputBorder};
width: 32px;
height: 32px;
`;
const IconButton = styled(NudeButton)`
border-radius: 4px;
margin: 0px 6px 6px 0px;
width: 30px;
height: 30px;
`;
const Loading = styled(HelpText)`
padding: 16px;
`;
const ColorPicker = styled(TwitterPicker)`
box-shadow: none !important;
background: transparent !important;
`;
const Wrapper = styled("div")`
display: inline-block;
position: relative;
`;
export default IconPicker;
+27 -25
View File
@@ -1,37 +1,37 @@
// @flow
import * as React from 'react';
import { observer } from 'mobx-react';
import { observable } from 'mobx';
import styled from 'styled-components';
import VisuallyHidden from 'components/VisuallyHidden';
import Flex from 'shared/components/Flex';
import { observable } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import styled from "styled-components";
import Flex from "components/Flex";
import VisuallyHidden from "components/VisuallyHidden";
const RealTextarea = styled.textarea`
border: 0;
flex: 1;
padding: 8px 12px 8px ${props => (props.hasIcon ? '8px' : '12px')};
padding: 8px 12px 8px ${(props) => (props.hasIcon ? "8px" : "12px")};
outline: none;
background: none;
color: ${props => props.theme.text};
color: ${(props) => props.theme.text};
&:disabled,
&::placeholder {
color: ${props => props.theme.placeholder};
color: ${(props) => props.theme.placeholder};
}
`;
const RealInput = styled.input`
border: 0;
flex: 1;
padding: 8px 12px 8px ${props => (props.hasIcon ? '8px' : '12px')};
padding: 8px 12px 8px ${(props) => (props.hasIcon ? "8px" : "12px")};
outline: none;
background: none;
color: ${props => props.theme.text};
color: ${(props) => props.theme.text};
height: 30px;
&:disabled,
&::placeholder {
color: ${props => props.theme.placeholder};
color: ${(props) => props.theme.placeholder};
}
&::-webkit-search-cancel-button {
@@ -40,10 +40,10 @@ const RealInput = styled.input`
`;
const Wrapper = styled.div`
flex: ${props => (props.flex ? '1' : '0')};
max-width: ${props => (props.short ? '350px' : '100%')};
min-height: ${({ minHeight }) => (minHeight ? `${minHeight}px` : '0')};
max-height: ${({ maxHeight }) => (maxHeight ? `${maxHeight}px` : 'initial')};
flex: ${(props) => (props.flex ? "1" : "0")};
max-width: ${(props) => (props.short ? "350px" : "100%")};
min-height: ${({ minHeight }) => (minHeight ? `${minHeight}px` : "0")};
max-height: ${({ maxHeight }) => (maxHeight ? `${maxHeight}px` : "initial")};
`;
const IconWrapper = styled.span`
@@ -56,16 +56,17 @@ const IconWrapper = styled.span`
export const Outline = styled(Flex)`
display: flex;
flex: 1;
margin: ${props => (props.margin !== undefined ? props.margin : '0 0 16px')};
margin: ${(props) =>
props.margin !== undefined ? props.margin : "0 0 16px"};
color: inherit;
border-width: 1px;
border-style: solid;
border-color: ${props =>
border-color: ${(props) =>
props.hasError
? 'red'
? "red"
: props.focused
? props.theme.inputBorderFocused
: props.theme.inputBorder};
? props.theme.inputBorderFocused
: props.theme.inputBorder};
border-radius: 4px;
font-weight: normal;
align-items: center;
@@ -75,6 +76,7 @@ export const Outline = styled(Flex)`
export const LabelText = styled.div`
font-weight: 500;
padding-bottom: 4px;
display: inline-block;
`;
export type Props = {
@@ -118,7 +120,7 @@ class Input extends React.Component<Props> {
render() {
const {
type = 'text',
type = "text",
icon,
label,
margin,
@@ -131,7 +133,7 @@ class Input extends React.Component<Props> {
...rest
} = this.props;
const InputComponent = type === 'textarea' ? RealTextarea : RealInput;
const InputComponent = type === "textarea" ? RealTextarea : RealInput;
const wrappedLabel = <LabelText>{label}</LabelText>;
return (
@@ -146,10 +148,10 @@ class Input extends React.Component<Props> {
<Outline focused={this.focused} margin={margin}>
{icon && <IconWrapper>{icon}</IconWrapper>}
<InputComponent
ref={ref => (this.input = ref)}
ref={(ref) => (this.input = ref)}
onBlur={this.handleBlur}
onFocus={this.handleFocus}
type={type === 'textarea' ? undefined : type}
type={type === "textarea" ? undefined : type}
hasIcon={!!icon}
{...rest}
/>
+13
View File
@@ -0,0 +1,13 @@
// @flow
import styled from "styled-components";
import Input from "./Input";
const InputLarge = styled(Input)`
height: 40px;
input {
height: 40px;
}
`;
export default InputLarge;
+20 -36
View File
@@ -1,15 +1,19 @@
// @flow
import * as React from 'react';
import { observable } from 'mobx';
import { observer } from 'mobx-react';
import styled, { withTheme } from 'styled-components';
import { LabelText, Outline } from 'components/Input';
import { observable } from "mobx";
import { observer, inject } from "mobx-react";
import * as React from "react";
import styled, { withTheme } from "styled-components";
import UiStore from "stores/UiStore";
import Editor from "components/Editor";
import HelpText from "components/HelpText";
import { LabelText, Outline } from "components/Input";
type Props = {
label: string,
minHeight?: number,
maxHeight?: number,
readOnly?: boolean,
ui: UiStore,
};
@observer
@@ -17,10 +21,6 @@ class InputRich extends React.Component<Props> {
@observable editorComponent: React.ComponentType<any>;
@observable focused: boolean = false;
componentDidMount() {
this.loadEditor();
}
handleBlur = () => {
this.focused = false;
};
@@ -29,58 +29,42 @@ class InputRich extends React.Component<Props> {
this.focused = true;
};
loadEditor = async () => {
try {
const EditorImport = await import('./Editor');
this.editorComponent = EditorImport.default;
} catch (err) {
console.error(err);
// If the editor bundle fails to load then reload the entire window. This
// can happen if a deploy happens between the user loading the initial JS
// bundle and the async-loaded editor JS bundle as the hash will change.
window.location.reload();
}
};
render() {
const { label, minHeight, maxHeight, ...rest } = this.props;
const Editor = this.editorComponent;
const { label, minHeight, maxHeight, ui, ...rest } = this.props;
return (
<React.Fragment>
<>
<LabelText>{label}</LabelText>
<StyledOutline
maxHeight={maxHeight}
minHeight={minHeight}
focused={this.focused}
>
{Editor ? (
<React.Suspense fallback={<HelpText>Loading editor</HelpText>}>
<Editor
onBlur={this.handleBlur}
onFocus={this.handleFocus}
ui={ui}
grow
{...rest}
/>
) : (
'Loading…'
)}
</React.Suspense>
</StyledOutline>
</React.Fragment>
</>
);
}
}
const StyledOutline = styled(Outline)`
display: block;
padding: 8px 12px;
min-height: ${({ minHeight }) => (minHeight ? `${minHeight}px` : '0')};
max-height: ${({ maxHeight }) => (maxHeight ? `${maxHeight}px` : 'auto')};
overflow: scroll;
min-height: ${({ minHeight }) => (minHeight ? `${minHeight}px` : "0")};
max-height: ${({ maxHeight }) => (maxHeight ? `${maxHeight}px` : "auto")};
overflow-y: auto;
> * {
display: block;
}
`;
export default withTheme(InputRich);
export default inject("ui")(withTheme(InputRich));
+13 -13
View File
@@ -1,13 +1,13 @@
// @flow
import * as React from 'react';
import keydown from 'react-keydown';
import { observer } from 'mobx-react';
import { observable } from 'mobx';
import { withRouter, type RouterHistory } from 'react-router-dom';
import styled, { withTheme } from 'styled-components';
import { SearchIcon } from 'outline-icons';
import { searchUrl } from 'utils/routeHelpers';
import Input from './Input';
import { observable } from "mobx";
import { observer } from "mobx-react";
import { SearchIcon } from "outline-icons";
import * as React from "react";
import keydown from "react-keydown";
import { withRouter, type RouterHistory } from "react-router-dom";
import styled, { withTheme } from "styled-components";
import Input from "./Input";
import { searchUrl } from "utils/routeHelpers";
type Props = {
history: RouterHistory,
@@ -21,7 +21,7 @@ class InputSearch extends React.Component<Props> {
input: ?Input;
@observable focused: boolean = false;
@keydown('meta+f')
@keydown("meta+f")
focus(ev) {
ev.preventDefault();
@@ -30,7 +30,7 @@ class InputSearch extends React.Component<Props> {
}
}
handleSearchInput = ev => {
handleSearchInput = (ev) => {
ev.preventDefault();
this.props.history.push(
searchUrl(ev.target.value, this.props.collectionId)
@@ -46,11 +46,11 @@ class InputSearch extends React.Component<Props> {
};
render() {
const { theme, placeholder = 'Search…' } = this.props;
const { theme, placeholder = "Search…" } = this.props;
return (
<InputMaxWidth
ref={ref => (this.input = ref)}
ref={(ref) => (this.input = ref)}
type="search"
placeholder={placeholder}
onInput={this.handleSearchInput}
+9 -9
View File
@@ -1,10 +1,10 @@
// @flow
import * as React from 'react';
import { observer } from 'mobx-react';
import { observable } from 'mobx';
import styled from 'styled-components';
import VisuallyHidden from 'components/VisuallyHidden';
import { Outline, LabelText } from './Input';
import { observable } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import styled from "styled-components";
import VisuallyHidden from "components/VisuallyHidden";
import { Outline, LabelText } from "./Input";
const Select = styled.select`
border: 0;
@@ -12,11 +12,11 @@ const Select = styled.select`
padding: 8px 12px;
outline: none;
background: none;
color: ${props => props.theme.text};
color: ${(props) => props.theme.text};
&:disabled,
&::placeholder {
color: ${props => props.theme.placeholder};
color: ${(props) => props.theme.placeholder};
}
`;
@@ -57,7 +57,7 @@ class InputSelect extends React.Component<Props> {
))}
<Outline focused={this.focused} className={className}>
<Select onBlur={this.handleBlur} onFocus={this.handleFocus} {...rest}>
{options.map(option => (
{options.map((option) => (
<option value={option.value} key={option.value}>
{option.label}
</option>
+7 -7
View File
@@ -1,19 +1,19 @@
// @flow
import styled from 'styled-components';
import styled from "styled-components";
const Key = styled.kbd`
display: inline-block;
padding: 4px 6px;
font: 11px 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier,
font: 11px "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier,
monospace;
line-height: 10px;
color: ${props => props.theme.almostBlack};
color: ${(props) => props.theme.almostBlack};
vertical-align: middle;
background-color: ${props => props.theme.smokeLight};
border: solid 1px ${props => props.theme.slateLight};
border-bottom-color: ${props => props.theme.slate};
background-color: ${(props) => props.theme.smokeLight};
border: solid 1px ${(props) => props.theme.slateLight};
border-bottom-color: ${(props) => props.theme.slate};
border-radius: 3px;
box-shadow: inset 0 -1px 0 ${props => props.theme.slate};
box-shadow: inset 0 -1px 0 ${(props) => props.theme.slate};
`;
export default Key;
+5 -5
View File
@@ -1,8 +1,8 @@
// @flow
import * as React from 'react';
import { observer } from 'mobx-react';
import Flex from 'shared/components/Flex';
import styled from 'styled-components';
import { observer } from "mobx-react";
import * as React from "react";
import styled from "styled-components";
import Flex from "components/Flex";
type Props = {
label: React.Node | string,
@@ -21,7 +21,7 @@ export const Label = styled(Flex)`
font-size: 13px;
font-weight: 500;
text-transform: uppercase;
color: ${props => props.theme.textTertiary};
color: ${(props) => props.theme.textTertiary};
letter-spacing: 0.04em;
`;
+41 -40
View File
@@ -1,32 +1,31 @@
// @flow
import * as React from 'react';
import { Switch, Route, Redirect } from 'react-router-dom';
import { Helmet } from 'react-helmet';
import styled, { withTheme } from 'styled-components';
import breakpoint from 'styled-components-breakpoint';
import { observable } from 'mobx';
import { observer, inject } from 'mobx-react';
import keydown from 'react-keydown';
import Analytics from 'components/Analytics';
import Flex from 'shared/components/Flex';
import { observable } from "mobx";
import { observer, inject } from "mobx-react";
import * as React from "react";
import { Helmet } from "react-helmet";
import keydown from "react-keydown";
import { Switch, Route, Redirect } from "react-router-dom";
import styled, { withTheme } from "styled-components";
import breakpoint from "styled-components-breakpoint";
import AuthStore from "stores/AuthStore";
import DocumentsStore from "stores/DocumentsStore";
import UiStore from "stores/UiStore";
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";
import Modal from "components/Modal";
import Sidebar from "components/Sidebar";
import SettingsSidebar from "components/Sidebar/Settings";
import {
homeUrl,
searchUrl,
matchDocumentSlug as slug,
} from 'utils/routeHelpers';
import { LoadingIndicatorBar } from 'components/LoadingIndicator';
import { GlobalStyles } from 'components/DropToImport';
import Sidebar from 'components/Sidebar';
import SettingsSidebar from 'components/Sidebar/Settings';
import Modals from 'components/Modals';
import DocumentHistory from 'components/DocumentHistory';
import Modal from 'components/Modal';
import KeyboardShortcuts from 'scenes/KeyboardShortcuts';
import ErrorSuspended from 'scenes/ErrorSuspended';
import AuthStore from 'stores/AuthStore';
import UiStore from 'stores/UiStore';
import DocumentsStore from 'stores/DocumentsStore';
} from "utils/routeHelpers";
type Props = {
documents: DocumentsStore,
@@ -57,8 +56,14 @@ class Layout extends React.Component<Props> {
}
}
@keydown('shift+/')
updateBackground() {
// ensure the wider page color always matches the theme
window.document.body.style.background = this.props.theme.background;
}
@keydown("shift+/")
handleOpenKeyboardShortcuts() {
if (this.props.ui.editMode) return;
this.keyboardShortcutsOpen = true;
}
@@ -66,20 +71,17 @@ class Layout extends React.Component<Props> {
this.keyboardShortcutsOpen = false;
};
updateBackground() {
// ensure the wider page color always matches the theme
window.document.body.style.background = this.props.theme.background;
}
@keydown(['/', 't', 'meta+k'])
@keydown(["t", "/", "meta+k"])
goToSearch(ev) {
if (this.props.ui.editMode) return;
ev.preventDefault();
ev.stopPropagation();
this.redirectTo = searchUrl();
}
@keydown('d')
@keydown("d")
goToDashboard() {
if (this.props.ui.editMode) return;
this.redirectTo = homeUrl();
}
@@ -94,7 +96,7 @@ class Layout extends React.Component<Props> {
return (
<Container column auto>
<Helmet>
<title>Outline</title>
<title>{team && team.name ? team.name : "Outline"}</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
@@ -124,7 +126,6 @@ class Layout extends React.Component<Props> {
/>
</Switch>
</Container>
<Modals ui={ui} />
<Modal
isOpen={this.keyboardShortcutsOpen}
onRequestClose={this.handleCloseKeyboardShortcuts}
@@ -139,10 +140,10 @@ class Layout extends React.Component<Props> {
}
const Container = styled(Flex)`
background: ${props => props.theme.background};
transition: ${props => props.theme.backgroundTransition};
background: ${(props) => props.theme.background};
transition: ${(props) => props.theme.backgroundTransition};
position: relative;
width: 100vw;
width: 100%;
min-height: 100%;
`;
@@ -154,9 +155,9 @@ const Content = styled(Flex)`
margin: 0;
}
${breakpoint('tablet')`
margin-left: ${props => (props.editMode ? 0 : props.theme.sidebarWidth)};
${breakpoint("tablet")`
margin-left: ${(props) => (props.editMode ? 0 : props.theme.sidebarWidth)};
`};
`;
export default inject('auth', 'ui', 'documents')(withTheme(Layout));
export default inject("auth", "ui", "documents")(withTheme(Layout));
+8 -7
View File
@@ -1,7 +1,7 @@
// @flow
import * as React from 'react';
import styled from 'styled-components';
import Flex from 'shared/components/Flex';
import * as React from "react";
import styled from "styled-components";
import Flex from "components/Flex";
type Props = {
image?: React.Node,
@@ -16,7 +16,7 @@ const ListItem = ({ image, title, subtitle, actions }: Props) => {
return (
<Wrapper compact={compact}>
{image && <Image>{image}</Image>}
<Content align={compact ? 'center' : undefined} column={!compact}>
<Content align={compact ? "center" : undefined} column={!compact}>
<Heading>{title}</Heading>
{subtitle && <Subtitle>{subtitle}</Subtitle>}
</Content>
@@ -27,9 +27,9 @@ const ListItem = ({ image, title, subtitle, actions }: Props) => {
const Wrapper = styled.li`
display: flex;
padding: ${props => (props.compact ? '8px' : '12px')} 0;
padding: ${(props) => (props.compact ? "8px" : "12px")} 0;
margin: 0;
border-bottom: 1px solid ${props => props.theme.divider};
border-bottom: 1px solid ${(props) => props.theme.divider};
&:last-child {
border-bottom: 0;
@@ -42,6 +42,7 @@ const Image = styled(Flex)`
align-items: center;
user-select: none;
flex-shrink: 0;
align-self: flex-start;
`;
const Heading = styled.p`
@@ -58,7 +59,7 @@ const Content = styled(Flex)`
const Subtitle = styled.p`
margin: 0;
font-size: 14px;
color: ${props => props.theme.slate};
color: ${(props) => props.theme.slate};
`;
const Actions = styled.div`
+1 -1
View File
@@ -1,5 +1,5 @@
// @flow
import styled from 'styled-components';
import styled from "styled-components";
const List = styled.ol`
margin: 0;
+7 -7
View File
@@ -1,10 +1,10 @@
// @flow
import * as React from 'react';
import { times } from 'lodash';
import styled from 'styled-components';
import Mask from 'components/Mask';
import Fade from 'components/Fade';
import Flex from 'shared/components/Flex';
import { times } from "lodash";
import * as React from "react";
import styled from "styled-components";
import Fade from "components/Fade";
import Flex from "components/Flex";
import Mask from "components/Mask";
type Props = {
count?: number,
@@ -13,7 +13,7 @@ type Props = {
const Placeholder = ({ count }: Props) => {
return (
<Fade>
{times(count || 2, index => (
{times(count || 2, (index) => (
<Item key={index} column auto>
<Mask />
</Item>
+1 -1
View File
@@ -1,3 +1,3 @@
// @flow
import List from './List';
import List from "./List";
export default List;
@@ -1,7 +1,7 @@
// @flow
import * as React from 'react';
import { inject, observer } from 'mobx-react';
import UiStore from 'stores/UiStore';
import { inject, observer } from "mobx-react";
import * as React from "react";
import UiStore from "stores/UiStore";
type Props = {
ui: UiStore,
@@ -22,4 +22,4 @@ class LoadingIndicator extends React.Component<Props> {
}
}
export default inject('ui')(LoadingIndicator);
export default inject("ui")(LoadingIndicator);
@@ -1,6 +1,6 @@
// @flow
import * as React from 'react';
import styled, { keyframes } from 'styled-components';
import * as React from "react";
import styled, { keyframes } from "styled-components";
const LoadingIndicatorBar = () => {
return (
@@ -18,7 +18,7 @@ const loadingFrame = keyframes`
const Container = styled.div`
position: fixed;
top: 0;
z-index: 9999;
z-index: ${(props) => props.theme.depths.loadingIndicatorBar};
background-color: #03a9f4;
width: 100%;
+2 -2
View File
@@ -1,5 +1,5 @@
// @flow
import LoadingIndicator from './LoadingIndicator';
import LoadingIndicatorBar from './LoadingIndicatorBar';
import LoadingIndicator from "./LoadingIndicator";
import LoadingIndicatorBar from "./LoadingIndicatorBar";
export default LoadingIndicator;
export { LoadingIndicatorBar };
@@ -1,10 +1,10 @@
// @flow
import * as React from 'react';
import { times } from 'lodash';
import styled from 'styled-components';
import Mask from 'components/Mask';
import Fade from 'components/Fade';
import Flex from 'shared/components/Flex';
import { times } from "lodash";
import * as React from "react";
import styled from "styled-components";
import Fade from "components/Fade";
import Flex from "components/Flex";
import Mask from "components/Mask";
type Props = {
count?: number,
@@ -13,7 +13,7 @@ type Props = {
const ListPlaceHolder = ({ count }: Props) => {
return (
<Fade>
{times(count || 2, index => (
{times(count || 2, (index) => (
<Item key={index} column auto>
<Mask header />
<Mask />
@@ -1,21 +1,24 @@
// @flow
import * as React from 'react';
import styled from 'styled-components';
import Mask from 'components/Mask';
import Fade from 'components/Fade';
import Flex from 'shared/components/Flex';
import * as React from "react";
import styled from "styled-components";
import DelayedMount from "components/DelayedMount";
import Fade from "components/Fade";
import Flex from "components/Flex";
import Mask from "components/Mask";
export default function LoadingPlaceholder(props: Object) {
return (
<Wrapper>
<Flex column auto {...props}>
<Mask height={34} />
<br />
<Mask />
<Mask />
<Mask />
</Flex>
</Wrapper>
<DelayedMount>
<Wrapper>
<Flex column auto {...props}>
<Mask height={34} />
<br />
<Mask />
<Mask />
<Mask />
</Flex>
</Wrapper>
</DelayedMount>
);
}
+2 -2
View File
@@ -1,6 +1,6 @@
// @flow
import LoadingPlaceholder from './LoadingPlaceholder';
import ListPlaceholder from './ListPlaceholder';
import ListPlaceholder from "./ListPlaceholder";
import LoadingPlaceholder from "./LoadingPlaceholder";
export default LoadingPlaceholder;
export { ListPlaceholder };
+9 -8
View File
@@ -1,9 +1,9 @@
// @flow
import * as React from 'react';
import styled from 'styled-components';
import { pulsate } from 'shared/styles/animations';
import { randomInteger } from 'shared/random';
import Flex from 'shared/components/Flex';
import * as React from "react";
import styled from "styled-components";
import { randomInteger } from "shared/random";
import { pulsate } from "shared/styles/animations";
import Flex from "components/Flex";
type Props = {
header?: boolean,
@@ -27,10 +27,11 @@ class Mask extends React.Component<Props> {
}
const Redacted = styled(Flex)`
width: ${props => (props.header ? props.width / 2 : props.width)}%;
height: ${props => (props.height ? props.height : props.header ? 24 : 18)}px;
width: ${(props) => (props.header ? props.width / 2 : props.width)}%;
height: ${(props) =>
props.height ? props.height : props.header ? 24 : 18}px;
margin-bottom: 6px;
background-color: ${props => props.theme.divider};
background-color: ${(props) => props.theme.divider};
animation: ${pulsate} 1.3s infinite;
&:last-child {
+34 -28
View File
@@ -1,16 +1,16 @@
// @flow
import * as React from 'react';
import { observer } from 'mobx-react';
import styled, { createGlobalStyle } from 'styled-components';
import breakpoint from 'styled-components-breakpoint';
import ReactModal from 'react-modal';
import { transparentize } from 'polished';
import { CloseIcon, BackIcon } from 'outline-icons';
import NudeButton from 'components/NudeButton';
import { fadeAndScaleIn } from 'shared/styles/animations';
import Flex from 'shared/components/Flex';
import { observer } from "mobx-react";
import { CloseIcon, BackIcon } from "outline-icons";
import { transparentize } from "polished";
import * as React from "react";
import ReactModal from "react-modal";
import styled, { createGlobalStyle } from "styled-components";
import breakpoint from "styled-components-breakpoint";
import { fadeAndScaleIn } from "shared/styles/animations";
import Flex from "components/Flex";
import NudeButton from "components/NudeButton";
ReactModal.setAppElement('#root');
ReactModal.setAppElement("#root");
type Props = {
children?: React.Node,
@@ -21,16 +21,16 @@ type Props = {
const GlobalStyles = createGlobalStyle`
.ReactModal__Overlay {
background-color: ${props =>
background-color: ${(props) =>
transparentize(0.25, props.theme.background)} !important;
z-index: 100;
z-index: ${(props) => props.theme.depths.modalOverlay};
}
${breakpoint('tablet')`
${breakpoint("tablet")`
.ReactModalPortal + .ReactModalPortal {
.ReactModal__Overlay {
margin-left: 12px;
box-shadow: 0 -2px 10px ${props => props.theme.shadow};
box-shadow: 0 -2px 10px ${(props) => props.theme.shadow};
border-radius: 8px 0 0 8px;
overflow: hidden;
}
@@ -41,6 +41,12 @@ const GlobalStyles = createGlobalStyle`
margin-left: 24px;
}
}
.ReactModalPortal + .ReactModalPortal + .ReactModalPortal + .ReactModalPortal {
.ReactModal__Overlay {
margin-left: 36px;
}
}
`};
.ReactModal__Body--open {
@@ -51,14 +57,14 @@ const GlobalStyles = createGlobalStyle`
const Modal = ({
children,
isOpen,
title = 'Untitled',
title = "Untitled",
onRequestClose,
...rest
}: Props) => {
if (!isOpen) return null;
return (
<React.Fragment>
<>
<GlobalStyles />
<StyledModal
contentLabel={title}
@@ -66,7 +72,7 @@ const Modal = ({
isOpen={isOpen}
{...rest}
>
<Content onClick={ev => ev.stopPropagation()} column>
<Content onClick={(ev) => ev.stopPropagation()} column>
{title && <h1>{title}</h1>}
{children}
@@ -79,7 +85,7 @@ const Modal = ({
<CloseIcon size={32} color="currentColor" />
</Close>
</StyledModal>
</React.Fragment>
</>
);
};
@@ -97,18 +103,18 @@ const StyledModal = styled(ReactModal)`
left: 0;
bottom: 0;
right: 0;
z-index: 100;
z-index: ${(props) => props.theme.depths.modal};
display: flex;
justify-content: center;
align-items: flex-start;
overflow-x: hidden;
overflow-y: auto;
background: ${props => props.theme.background};
transition: ${props => props.theme.backgroundTransition};
background: ${(props) => props.theme.background};
transition: ${(props) => props.theme.backgroundTransition};
padding: 8vh 2rem 2rem;
outline: none;
${breakpoint('tablet')`
${breakpoint("tablet")`
padding-top: 13vh;
`};
`;
@@ -127,7 +133,7 @@ const Close = styled(NudeButton)`
right: 0;
margin: 12px;
opacity: 0.75;
color: ${props => props.theme.text};
color: ${(props) => props.theme.text};
width: auto;
height: auto;
@@ -135,19 +141,19 @@ const Close = styled(NudeButton)`
opacity: 1;
}
${breakpoint('tablet')`
${breakpoint("tablet")`
display: none;
`};
`;
const Back = styled(NudeButton)`
position: absolute;
position: fixed;
display: none;
align-items: center;
top: 2rem;
left: 2rem;
opacity: 0.75;
color: ${props => props.theme.text};
color: ${(props) => props.theme.text};
width: auto;
height: auto;
@@ -155,7 +161,7 @@ const Back = styled(NudeButton)`
opacity: 1;
}
${breakpoint('tablet')`
${breakpoint("tablet")`
display: flex;
`};
`;
-62
View File
@@ -1,62 +0,0 @@
// @flow
import * as React from 'react';
import { observer } from 'mobx-react';
import BaseModal from 'components/Modal';
import UiStore from 'stores/UiStore';
import CollectionNew from 'scenes/CollectionNew';
import CollectionEdit from 'scenes/CollectionEdit';
import CollectionDelete from 'scenes/CollectionDelete';
import CollectionExport from 'scenes/CollectionExport';
import DocumentDelete from 'scenes/DocumentDelete';
import DocumentShare from 'scenes/DocumentShare';
type Props = {
ui: UiStore,
};
@observer
class Modals extends React.Component<Props> {
handleClose = () => {
this.props.ui.clearActiveModal();
};
render() {
const { activeModalName, activeModalProps } = this.props.ui;
const Modal = ({ name, children, ...rest }) => {
return (
<BaseModal
isOpen={activeModalName === name}
onRequestClose={this.handleClose}
{...rest}
>
{React.cloneElement(children, activeModalProps)}
</BaseModal>
);
};
return (
<span>
<Modal name="collection-new" title="Create a collection">
<CollectionNew onSubmit={this.handleClose} />
</Modal>
<Modal name="collection-edit" title="Edit collection">
<CollectionEdit onSubmit={this.handleClose} />
</Modal>
<Modal name="collection-delete" title="Delete collection">
<CollectionDelete onSubmit={this.handleClose} />
</Modal>
<Modal name="collection-export" title="Export collection">
<CollectionExport onSubmit={this.handleClose} />
</Modal>
<Modal name="document-share" title="Share document">
<DocumentShare onSubmit={this.handleClose} />
</Modal>
<Modal name="document-delete" title="Delete document">
<DocumentDelete onSubmit={this.handleClose} />
</Modal>
</span>
);
}
}
export default Modals;
+12
View File
@@ -0,0 +1,12 @@
// @flow
import styled from "styled-components";
const Notice = styled.p`
background: ${(props) => props.theme.sidebarBackground};
color: ${(props) => props.theme.sidebarText};
padding: 10px 12px;
border-radius: 4px;
position: relative;
`;
export default Notice;
+24
View File
@@ -0,0 +1,24 @@
// @flow
import * as React from "react";
import Notice from "components/Notice";
export default function AlertNotice({ children }: { children: React.Node }) {
return (
<Notice muted>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
style={{ position: "relative", top: "2px" }}
>
<path
d="M15.6676 11.5372L10.0155 1.14735C9.10744 -0.381434 6.89378 -0.383465 5.98447 1.14735L0.332715 11.5372C-0.595598 13.0994 0.528309 15.0776 2.34778 15.0776H13.652C15.47 15.0776 16.5959 13.101 15.6676 11.5372ZM8 13.2026C7.48319 13.2026 7.0625 12.7819 7.0625 12.2651C7.0625 11.7483 7.48319 11.3276 8 11.3276C8.51681 11.3276 8.9375 11.7483 8.9375 12.2651C8.9375 12.7819 8.51681 13.2026 8 13.2026ZM8.9375 9.45257C8.9375 9.96938 8.51681 10.3901 8 10.3901C7.48319 10.3901 7.0625 9.96938 7.0625 9.45257V4.76507C7.0625 4.24826 7.48319 3.82757 8 3.82757C8.51681 3.82757 8.9375 4.24826 8.9375 4.76507V9.45257Z"
fill="currentColor"
/>
</svg>{" "}
{children}
</Notice>
);
}
+5 -6
View File
@@ -1,7 +1,7 @@
// @flow
import * as React from 'react';
import styled from 'styled-components';
import { lighten } from 'polished';
import { lighten } from "polished";
import * as React from "react";
import styled from "styled-components";
const Button = styled.button`
width: 24px;
@@ -14,13 +14,12 @@ const Button = styled.button`
&:focus {
transition-duration: 0.05s;
box-shadow: ${props => lighten(0.4, props.theme.buttonBackground)} 0px 0px
box-shadow: ${(props) => lighten(0.4, props.theme.buttonBackground)} 0px 0px
0px 3px;
outline: none;
}
`;
// $FlowFixMe - need to upgrade to get forwardRef
export default React.forwardRef((props, ref) => (
export default React.forwardRef<any, typeof Button>((props, ref) => (
<Button {...props} ref={ref} />
));
@@ -1,5 +1,5 @@
// @flow
import * as React from 'react';
import * as React from "react";
type Props = {
size?: number,
@@ -7,7 +7,7 @@ type Props = {
className?: string,
};
function OutlineLogo({ size = 32, fill = '#333', className }: Props) {
function OutlineLogo({ size = 32, fill = "#333", className }: Props) {
return (
<svg
fill={fill}
+24 -15
View File
@@ -1,23 +1,32 @@
// @flow
import * as React from 'react';
import { Helmet } from 'react-helmet';
import { observer, inject } from "mobx-react";
import * as React from "react";
import { Helmet } from "react-helmet";
import AuthStore from "stores/AuthStore";
type Props = {
title: string,
favicon?: string,
auth: AuthStore,
};
const PageTitle = ({ title, favicon }: Props) => (
<Helmet>
<title>{`${title} - Outline`}</title>
<link
rel="shortcut icon"
type="image/png"
href={favicon || '/favicon-32.png'}
sizes="32x32"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</Helmet>
);
const PageTitle = observer(({ auth, title, favicon }: Props) => {
const { team } = auth;
export default PageTitle;
return (
<Helmet>
<title>
{team && team.name ? `${title} - ${team.name}` : `${title} - Outline`}
</title>
<link
rel="shortcut icon"
type="image/png"
href={favicon || "/favicon-32.png"}
sizes="32x32"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</Helmet>
);
});
export default inject("auth")(PageTitle);
+6 -6
View File
@@ -1,9 +1,9 @@
// @flow
import * as React from 'react';
import { observer } from 'mobx-react';
import Document from 'models/Document';
import DocumentPreview from 'components/DocumentPreview';
import PaginatedList from 'components/PaginatedList';
import { observer } from "mobx-react";
import * as React from "react";
import Document from "models/Document";
import DocumentPreview from "components/DocumentPreview";
import PaginatedList from "components/PaginatedList";
type Props = {
documents: Document[],
@@ -25,7 +25,7 @@ class PaginatedDocumentList extends React.Component<Props> {
heading={heading}
fetch={fetch}
options={options}
renderItem={item => (
renderItem={(item) => (
<DocumentPreview key={item.id} document={item} {...rest} />
)}
/>
+23 -15
View File
@@ -1,12 +1,12 @@
// @flow
import * as React from 'react';
import { observable, action } from 'mobx';
import { observer } from 'mobx-react';
import { Waypoint } from 'react-waypoint';
import ArrowKeyNavigation from 'boundless-arrow-key-navigation';
import { DEFAULT_PAGINATION_LIMIT } from 'stores/BaseStore';
import { ListPlaceholder } from 'components/LoadingPlaceholder';
import ArrowKeyNavigation from "boundless-arrow-key-navigation";
import { observable, action } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import { Waypoint } from "react-waypoint";
import { DEFAULT_PAGINATION_LIMIT } from "stores/BaseStore";
import DelayedMount from "components/DelayedMount";
import { ListPlaceholder } from "components/LoadingPlaceholder";
type Props = {
fetch?: (options: ?Object) => Promise<void>,
@@ -14,7 +14,7 @@ type Props = {
heading?: React.Node,
empty?: React.Node,
items: any[],
renderItem: any => React.Node,
renderItem: (any) => React.Node,
};
@observer
@@ -27,8 +27,12 @@ class PaginatedList extends React.Component<Props> {
@observable offset: number = 0;
@observable allowLoadMore: boolean = true;
constructor(props: Props) {
super(props);
this.isInitiallyLoaded = this.props.items.length > 0;
}
componentDidMount() {
this.isInitiallyLoaded = !!this.props.items.length;
this.fetchResults();
}
@@ -92,10 +96,10 @@ class PaginatedList extends React.Component<Props> {
(this.isLoaded || this.isInitiallyLoaded) && !showLoading && !showEmpty;
return (
<React.Fragment>
<>
{showEmpty && empty}
{showList && (
<React.Fragment>
<>
{heading}
<ArrowKeyNavigation
mode={ArrowKeyNavigation.mode.VERTICAL}
@@ -106,10 +110,14 @@ class PaginatedList extends React.Component<Props> {
{this.allowLoadMore && (
<Waypoint key={this.renderCount} onEnter={this.loadMoreResults} />
)}
</React.Fragment>
</>
)}
{showLoading && <ListPlaceholder count={5} />}
</React.Fragment>
{showLoading && (
<DelayedMount>
<ListPlaceholder count={5} />
</DelayedMount>
)}
</>
);
}
}
+17 -22
View File
@@ -1,20 +1,20 @@
// @flow
import * as React from 'react';
import { observer } from 'mobx-react';
import styled from 'styled-components';
import { GoToIcon, CollectionIcon, PrivateCollectionIcon } from 'outline-icons';
import Flex from 'shared/components/Flex';
import Document from 'models/Document';
import Collection from 'models/Collection';
import type { DocumentPath } from 'stores/CollectionsStore';
import { observer } from "mobx-react";
import { GoToIcon } from "outline-icons";
import * as React from "react";
import styled from "styled-components";
import type { DocumentPath } from "stores/CollectionsStore";
import Collection from "models/Collection";
import Document from "models/Document";
import CollectionIcon from "components/CollectionIcon";
import Flex from "components/Flex";
type Props = {
result: DocumentPath,
document?: ?Document,
collection: ?Collection,
onSuccess?: () => void,
ref?: (?React.ElementRef<'div'>) => void,
ref?: (?React.ElementRef<"div">) => void,
};
@observer
@@ -24,7 +24,7 @@ class PathToDocument extends React.Component<Props> {
const { document, result, onSuccess } = this.props;
if (!document) return;
if (result.type === 'document') {
if (result.type === "document") {
await document.move(result.collectionId, result.id);
} else {
await document.move(result.collectionId, null);
@@ -41,18 +41,13 @@ class PathToDocument extends React.Component<Props> {
return (
<Component ref={ref} onClick={this.handleClick} href="" selectable>
{collection &&
(collection.private ? (
<PrivateCollectionIcon color={collection.color} />
) : (
<CollectionIcon color={collection.color} />
))}
{collection && <CollectionIcon collection={collection} />}
{result.path
.map(doc => <Title key={doc.id}>{doc.title}</Title>)
.map((doc) => <Title key={doc.id}>{doc.title}</Title>)
.reduce((prev, curr) => [prev, <StyledGoToIcon />, curr])}
{document && (
<Flex>
{' '}
{" "}
<StyledGoToIcon /> <Title>{document.title}</Title>
</Flex>
)}
@@ -77,11 +72,11 @@ const ResultWrapper = styled.div`
margin-left: -4px;
user-select: none;
color: ${props => props.theme.text};
color: ${(props) => props.theme.text};
cursor: default;
`;
const ResultWrapperLink = styled(ResultWrapper.withComponent('a'))`
const ResultWrapperLink = styled(ResultWrapper.withComponent("a"))`
margin: 0 -8px;
padding: 8px 4px;
border-radius: 8px;
@@ -89,7 +84,7 @@ const ResultWrapperLink = styled(ResultWrapper.withComponent('a'))`
&:hover,
&:active,
&:focus {
background: ${props => props.theme.listItemHoverBackground};
background: ${(props) => props.theme.listItemHoverBackground};
outline: none;
}
`;
+4 -4
View File
@@ -1,7 +1,7 @@
// @flow
import * as React from 'react';
import BoundlessPopover from 'boundless-popover';
import styled, { keyframes } from 'styled-components';
import BoundlessPopover from "boundless-popover";
import * as React from "react";
import styled, { keyframes } from "styled-components";
const fadeIn = keyframes`
from {
@@ -22,7 +22,7 @@ const StyledPopover = styled(BoundlessPopover)`
position: absolute;
top: 0;
left: 0;
z-index: 9999;
z-index: ${(props) => props.theme.depths.popover};
svg {
height: 16px;
-102
View File
@@ -1,102 +0,0 @@
// @flow
import * as React from 'react';
import { inject } from 'mobx-react';
import styled from 'styled-components';
import Document from 'models/Document';
import Flex from 'shared/components/Flex';
import Time from 'shared/components/Time';
import Breadcrumb from 'shared/components/Breadcrumb';
import CollectionsStore from 'stores/CollectionsStore';
const Container = styled(Flex)`
color: ${props => props.theme.textTertiary};
font-size: 13px;
white-space: nowrap;
overflow: hidden;
`;
const Modified = styled.span`
color: ${props =>
props.highlight ? props.theme.text : props.theme.textTertiary};
font-weight: ${props => (props.highlight ? '600' : '400')};
`;
type Props = {
collections: CollectionsStore,
showCollection?: boolean,
showPublished?: boolean,
document: Document,
views?: number,
};
function PublishingInfo({
collections,
showPublished,
showCollection,
document,
}: Props) {
const {
modifiedSinceViewed,
updatedAt,
updatedBy,
publishedAt,
archivedAt,
deletedAt,
isDraft,
} = document;
const neverUpdated = publishedAt === updatedAt;
let content;
if (deletedAt) {
content = (
<span>
deleted <Time dateTime={deletedAt} /> ago
</span>
);
} else if (archivedAt) {
content = (
<span>
archived <Time dateTime={archivedAt} /> ago
</span>
);
} else if (publishedAt && (neverUpdated || showPublished)) {
content = (
<span>
published <Time dateTime={publishedAt} /> ago
</span>
);
} else if (isDraft) {
content = (
<span>
saved <Time dateTime={updatedAt} /> ago
</span>
);
} else {
content = (
<Modified highlight={modifiedSinceViewed}>
updated <Time dateTime={updatedAt} /> ago
</Modified>
);
}
const collection = collections.get(document.collectionId);
return (
<Container align="center">
{updatedBy.name}&nbsp;
{content}
{showCollection &&
collection && (
<span>
&nbsp;in&nbsp;
<strong>
<Breadcrumb document={document} onlyText />
</strong>
</span>
)}
</Container>
);
}
export default inject('collections')(PublishingInfo);
+4 -4
View File
@@ -1,8 +1,8 @@
// @flow
// based on: https://reacttraining.com/react-router/web/guides/scroll-restoration
import * as React from 'react';
import { withRouter } from 'react-router-dom';
import type { Location } from 'react-router-dom';
import * as React from "react";
import { withRouter } from "react-router-dom";
import type { Location } from "react-router-dom";
type Props = {
location: Location,
@@ -13,7 +13,7 @@ class ScrollToTop extends React.Component<Props> {
componentDidUpdate(prevProps) {
if (this.props.location.pathname === prevProps.location.pathname) return;
// exception for when entering or exiting document edit, scroll postion should not reset
// exception for when entering or exiting document edit, scroll position should not reset
if (
this.props.location.pathname.match(/\/edit\/?$/) ||
prevProps.location.pathname.match(/\/edit\/?$/)
+6 -6
View File
@@ -1,8 +1,8 @@
// @flow
import * as React from 'react';
import { observable } from 'mobx';
import { observer } from 'mobx-react';
import styled from 'styled-components';
import { observable } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import styled from "styled-components";
type Props = {
shadow?: boolean,
@@ -31,8 +31,8 @@ const Wrapper = styled.div`
overflow-x: hidden;
overscroll-behavior: none;
-webkit-overflow-scrolling: touch;
box-shadow: ${props =>
props.shadow ? '0 1px inset rgba(0,0,0,.1)' : 'none'};
box-shadow: ${(props) =>
props.shadow ? "0 1px inset rgba(0,0,0,.1)" : "none"};
transition: all 250ms ease-in-out;
`;
+66 -38
View File
@@ -1,55 +1,62 @@
// @flow
import * as React from 'react';
import { observer, inject } from 'mobx-react';
import styled from 'styled-components';
import { observable } from "mobx";
import { observer, inject } from "mobx-react";
import {
ArchiveIcon,
HomeIcon,
EditIcon,
SearchIcon,
StarredIcon,
ShapesIcon,
TrashIcon,
PlusIcon,
} from 'outline-icons';
} from "outline-icons";
import * as React from "react";
import styled from "styled-components";
import Flex from 'shared/components/Flex';
import Modal from 'components/Modal';
import Invite from 'scenes/Invite';
import AccountMenu from 'menus/AccountMenu';
import Sidebar from './Sidebar';
import Scrollable from 'components/Scrollable';
import Section from './components/Section';
import Collections from './components/Collections';
import SidebarLink from './components/SidebarLink';
import HeaderBlock from './components/HeaderBlock';
import Bubble from './components/Bubble';
import AuthStore from 'stores/AuthStore';
import DocumentsStore from 'stores/DocumentsStore';
import PoliciesStore from 'stores/PoliciesStore';
import UiStore from 'stores/UiStore';
import { observable } from 'mobx';
import AuthStore from "stores/AuthStore";
import DocumentsStore from "stores/DocumentsStore";
import PoliciesStore from "stores/PoliciesStore";
import CollectionNew from "scenes/CollectionNew";
import Invite from "scenes/Invite";
import Flex from "components/Flex";
import Modal from "components/Modal";
import Scrollable from "components/Scrollable";
import Sidebar from "./Sidebar";
import Bubble from "./components/Bubble";
import Collections from "./components/Collections";
import HeaderBlock from "./components/HeaderBlock";
import Section from "./components/Section";
import SidebarLink from "./components/SidebarLink";
import AccountMenu from "menus/AccountMenu";
type Props = {
auth: AuthStore,
documents: DocumentsStore,
policies: PoliciesStore,
ui: UiStore,
};
@observer
class MainSidebar extends React.Component<Props> {
@observable inviteModalOpen: boolean = false;
@observable inviteModalOpen = false;
@observable createCollectionModalOpen = false;
componentDidMount() {
this.props.documents.fetchDrafts();
this.props.documents.fetchTemplates();
}
handleCreateCollection = () => {
this.props.ui.setActiveModal('collection-new');
handleCreateCollectionModalOpen = (ev: SyntheticEvent<>) => {
ev.preventDefault();
this.createCollectionModalOpen = true;
};
handleInviteModalOpen = () => {
handleCreateCollectionModalClose = (ev: SyntheticEvent<>) => {
this.createCollectionModalOpen = false;
};
handleInviteModalOpen = (ev: SyntheticEvent<>) => {
ev.preventDefault();
this.inviteModalOpen = true;
};
@@ -82,31 +89,41 @@ class MainSidebar extends React.Component<Props> {
<Section>
<SidebarLink
to="/home"
icon={<HomeIcon />}
icon={<HomeIcon color="currentColor" />}
exact={false}
label="Home"
/>
<SidebarLink
to={{
pathname: '/search',
pathname: "/search",
state: { fromMenu: true },
}}
icon={<SearchIcon />}
icon={<SearchIcon color="currentColor" />}
label="Search"
exact={false}
/>
<SidebarLink
to="/starred"
icon={<StarredIcon />}
icon={<StarredIcon color="currentColor" />}
exact={false}
label="Starred"
/>
<SidebarLink
to="/templates"
icon={<ShapesIcon color="currentColor" />}
exact={false}
label="Templates"
active={
documents.active ? documents.active.template : undefined
}
/>
<SidebarLink
to="/drafts"
icon={<EditIcon />}
icon={<EditIcon color="currentColor" />}
label={
<Drafts align="center">
Drafts{draftDocumentsCount > 0 && (
Drafts
{draftDocumentsCount > 0 && (
<Bubble count={draftDocumentsCount} />
)}
</Drafts>
@@ -114,18 +131,21 @@ class MainSidebar extends React.Component<Props> {
active={
documents.active
? !documents.active.publishedAt &&
!documents.active.isDeleted
!documents.active.isDeleted &&
!documents.active.isTemplate
: undefined
}
/>
</Section>
<Section>
<Collections onCreateCollection={this.handleCreateCollection} />
<Collections
onCreateCollection={this.handleCreateCollectionModalOpen}
/>
</Section>
<Section>
<SidebarLink
to="/archive"
icon={<ArchiveIcon />}
icon={<ArchiveIcon color="currentColor" />}
exact={false}
label="Archive"
active={
@@ -136,7 +156,7 @@ class MainSidebar extends React.Component<Props> {
/>
<SidebarLink
to="/trash"
icon={<TrashIcon />}
icon={<TrashIcon color="currentColor" />}
exact={false}
label="Trash"
active={
@@ -145,8 +165,9 @@ class MainSidebar extends React.Component<Props> {
/>
{can.invite && (
<SidebarLink
to="/settings/people"
onClick={this.handleInviteModalOpen}
icon={<PlusIcon />}
icon={<PlusIcon color="currentColor" />}
label="Invite people…"
/>
)}
@@ -160,6 +181,13 @@ class MainSidebar extends React.Component<Props> {
>
<Invite onSubmit={this.handleInviteModalClose} />
</Modal>
<Modal
title="Create a collection"
onRequestClose={this.handleCreateCollectionModalClose}
isOpen={this.createCollectionModalOpen}
>
<CollectionNew onSubmit={this.handleCreateCollectionModalClose} />
</Modal>
</Sidebar>
);
}
@@ -169,4 +197,4 @@ const Drafts = styled(Flex)`
height: 24px;
`;
export default inject('documents', 'policies', 'auth', 'ui')(MainSidebar);
export default inject("documents", "policies", "auth")(MainSidebar);

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