Compare commits

..

242 Commits

Author SHA1 Message Date
Tom Moor 9bcf5b0292 fix: race, avoid update event if no text changed 2020-11-16 21:25:40 -08:00
Tom Moor 3b4aa02c67 fix: various connection issues 2020-11-16 21:25:16 -08:00
Tom Moor 375d658231 skip backlink title rewriting for now 2020-11-16 21:25:07 -08:00
Tom Moor 70ea77ce01 hook up awareness to UI
fix header disappearing
2020-11-15 20:06:41 -08:00
Tom Moor cea1d808d1 Bump deps 2020-11-15 12:24:01 -08:00
Tom Moor bea8b85cf9 Merge develop 2020-11-15 11:23:04 -08:00
Tom Moor 1ce01fa936 0.50.0 2020-11-14 23:07:41 -08:00
Tom Moor 12a2e1c387 chore: Menu templates (#1644)
* chore: Menu template system

* NewTemplateMenu

* UserMenu

* MenuItemsTemplate -> DropdownMenuItems

* support nested menus

* DocumentMenu

* BreadcrumbMenu

* isInvited
2020-11-14 20:44:31 -08:00
Tom Moor 19ab32f551 chore: Add additional missing events (#1639)
* chore: Add additional missing events
signed in
profile updated
team setting updated

* Minor refactor to DRY existing code

* Add events

* lint

* flow: Add missing ip to event types
2020-11-14 20:44:18 -08:00
Tom Moor 7bdcba46b8 fix: Virtualize move document listbox (#1650)
* fix: Virtualize move document listbox
closes #1645
2020-11-12 22:02:56 -08:00
Tom Moor 32d3053002 fix: Heading anchor position changed _again_ 2020-11-12 22:02:19 -08:00
Tom Moor 85dd54db3d fix: Consistent spacing when first item in document is a header 2020-11-12 21:51:20 -08:00
Tom Moor 5d0dd9b734 chore: Bump RME (various editor fixes) 2020-11-12 21:47:44 -08:00
Tom Moor 735d5cbf26 fix: Ignore safely ignored loop error
closes #1646
2020-11-11 17:05:05 -08:00
Tom Moor a94012a03a chore: Drop and create test database automatically between runs
docs: Update README to specify using make to run tests
2020-11-11 16:58:45 -08:00
Tom Moor 80d74c9334 fix: Clicking the share link from a context menu propagates the click onto the container (#1643)
closes #1642
2020-11-11 09:02:02 -08:00
Tom Moor 26e6db1afd fix: Update document policy to disable sharing for read-only users (#1638)
* fix: Update document policy to disable sharing for read-only users

* test: Update test for new permission logic
2020-11-10 20:35:41 -08:00
Tom Moor 5e7bbdc111 feat: Sync sessions across browser tabs (#1641)
* feat: Sync sessions across browser tabs

If the user signs in or out in one tab, then make sure their session on the same subdomain is kept in sync on other tabs
2020-11-10 19:43:14 -08:00
Tom Moor 71e9860f88 flow: Correctly type Theme 2020-11-09 00:01:50 -08:00
Tom Moor 26f4901547 fix: 'Not found' page should obey theme
closes #1626
2020-11-08 23:52:00 -08:00
Tom Moor 4c56ed40f1 feat: Hide Outline branding on custom domain share links
closes #1629
2020-11-08 23:01:57 -08:00
Tom Moor 4e7a1cd121 feat: add filters to drafts (#1631)
* feat: add filters to drafts

Co-authored-by: Guilherme Diniz <guilhermedassumpcao@gmail.com>
2020-11-08 22:33:52 -08:00
Tom Moor 14e0ed8108 fix: Various React warnings 2020-11-08 20:28:27 -08:00
Tom Moor 0372ff2727 fix: Unneccessary scrollbar 2020-11-08 20:04:46 -08:00
Tom Moor 02e7e75cb9 fix: Remove non-standardized css (console warning) 2020-11-08 20:01:47 -08:00
Tom Moor af73de4128 feat: Show table of contents when useful on publicly shared docs
closes #1625
2020-11-08 19:50:00 -08:00
Tom Moor abeccb8a4c stash 2020-11-08 15:58:00 -08:00
Tom Moor c8d3d26044 Merge branch 'develop' of github.com:outline/outline into yjs 2020-11-05 23:05:16 -08:00
Tom Moor 0125a5361d fix: Document published notification potentially sent to users without permission to view document 2020-11-05 19:49:05 -08:00
Tom Moor ec5a7d79f5 flow 2020-11-05 18:41:22 -08:00
Nonpawit Teerachetmongkol fdaa36c9fd fix: gist not support username contains numbers (#1622) 2020-11-05 09:03:16 -08:00
Tom Moor 1b6a986986 chore: Refactor authentication pass between subdomains (#1619)
* fix: Use get request instead of cookie to transfer token between domains

* Add domain to database
Add redirects to team domain when present

* 30s -> 1m

* fix: Avoid redirect loop if subdomain and domain set

* fix: Create a transfer specific token to prevent replay requests

* refactor: Move isCustomDomain out of shared as it won't work on the client
2020-11-04 19:54:04 -08:00
Tom Moor 30d31b35ac fix: Issue with inflating clientIds in pud 2020-11-04 19:12:55 -08:00
Tom Moor 8abf2436dd wip 2020-11-01 19:52:34 -08:00
Tom Moor 4df75bda7b Remove unneeded applyUpdate 2020-11-01 15:52:11 -08:00
Tom Moor 220546c40a fix: Multiplayer cursors in headings 2020-11-01 13:06:13 -08:00
Tom Moor b96ffe59db Merge develop 2020-11-01 13:02:58 -08:00
Tom Moor 3d09c8f655 chore: Refactor backlinks and revisions (#1611)
* Update backlinks service to not rely on revisions

* fix: Add missing index for finding backlinks

* Debounce revision creation (#1616)

* refactor debounce logic to service

* Debounce slack notification

* Revisions created by service

* fix: Revision sidebar latest

* test: Add tests for notifications
2020-11-01 10:26:39 -08:00
Tom Moor 7735aa12d7 GSuite -> Google Workspace 2020-10-27 22:14:51 -07:00
Tom Moor 7ac724909d fix: Cannot duplicate document with title close to max title length
closes #1610
2020-10-27 19:15:35 -07:00
Tom Moor abd3a1ee12 Update LICENSE 2020-10-26 23:26:43 -07:00
Tom Moor 9bfc0cacae 0.49.0 2020-10-26 23:25:50 -07:00
Tom Moor 2676a7e8cf events 2020-10-26 23:25:19 -07:00
Tom Moor 5e9e4fb028 Merge branch 'develop' into yjs 2020-10-26 21:39:57 -07:00
Tom Moor bd80e8384a chore: Remove events log from settings 2020-10-26 19:32:48 -07:00
Tom Moor 551b1620e0 fix: Flag without realtime editing 2020-10-26 19:27:49 -07:00
Tom Moor b8569ed8de remove flag sidebar item for now 2020-10-25 19:37:13 -07:00
Tom Moor 8d1a707dd0 basic offline messaging 2020-10-25 19:36:10 -07:00
Tom Moor 9877cf1f4e install 2020-10-25 19:13:29 -07:00
Tom Moor 50fbcd8d85 Merge develop 2020-10-25 19:11:21 -07:00
Tom Moor 359d228771 lint 2020-10-25 19:10:31 -07:00
Tom Moor bfdfa3ee4b feat: Debounce notification emails by 5 minutes to avoid duplicate notifications where possible (#1598) 2020-10-25 15:06:07 -07:00
Tom Moor 0347620c75 fix: Update collaboratorIds 2020-10-25 14:42:28 -07:00
Tom Moor cb362511a5 Load from db 2020-10-25 10:40:39 -07:00
Tom Moor 700db463fc fix: Awareness state not available if server dies and restarts 2020-10-24 19:46:08 -07:00
Tom Moor a28dfa77ee fix: Race condition when setting up socket listeners 2020-10-24 19:45:51 -07:00
Tom Moor d8bc6515dd race 2020-10-23 10:06:44 -07:00
Tom Moor dba5dd14e7 fix: Put public and private uploads in separate folders to allow for restrictive AWS policies
closes #1581
2020-10-21 21:00:40 -07:00
Tom Moor a9c05adc3c fix: Account for multiple images on the same line of text when converting images for public share
closes #1602
2020-10-21 20:22:23 -07:00
Tom Moor 34bdc88003 fix: Offset heading styles 2020-10-21 09:04:21 -07:00
Tom Moor df7b9f3e88 feat: Add support for "word" files exported from Confluence (#1600)
* Display error message to end user

* fix: Improve conversion of tables

* fix: Characters at ends of lines in tables lost
2020-10-21 08:53:59 -07:00
Tom Moor b78e2f1e05 fix: Match search requests from Slack using Integration for non-Slack teams (#1599)
* Match slack hook requests to integration
2020-10-21 08:53:38 -07:00
Tom Moor 15337b5bdf fix: Improve error handling when redis connection is lost 2020-10-20 07:41:17 -07:00
Tom Moor 4103f53f2a fix: Offset headers when scrolling from TOC to account for fixed header
closes #1578
2020-10-19 23:07:37 -07:00
Tom Moor 758fcc1759 feat: Show unique views rather than total views in document meta. (#1559)
* unique-views

* fix: 'only you' displays briefly when visiting a document previously only viewed by one other
2020-10-19 22:44:28 -07:00
Debajyoti Halder b3549637fe fix: Dark mode inline code styling fixed #1593 (#1597)
background changed to props.theme.codeBackground
2020-10-19 22:14:14 -07:00
Tom Moor 7bee60a337 fix: Clicking link with blank href should not open new tab 2020-10-19 08:00:52 -07:00
Tom Moor 38a005ed8a lint 2020-10-19 07:48:51 -07:00
Tom Moor 71b7ef1186 fix: Websockets cannot connect in Safari 2020-10-19 07:43:03 -07:00
Tom Moor 3af1a80615 fix: Incorrect cursor on headings 2020-10-19 07:40:12 -07:00
Tom Moor 0776b78e25 wip 2020-10-19 07:33:43 -07:00
Tom Moor 4256e7ec87 flow 2020-10-18 22:32:04 -07:00
Tom Moor e723124f8f lint 2020-10-18 22:26:36 -07:00
Tom Moor c2fbd78622 flow-types 2020-10-18 22:16:42 -07:00
Tom Moor 17cbeab409 refactor 2020-10-18 21:36:24 -07:00
Tom Moor 37d456a0fb refactor 2020-10-18 15:37:50 -07:00
Tom Moor 48a0ba0dec refactor, resolve memory leak 2020-10-17 17:47:04 -07:00
Tom Moor f454467bf1 event filtering 2020-10-17 13:21:48 -07:00
Tom Moor f21f660543 stash 2020-10-16 16:19:29 -07:00
Tom Moor 50637bc7ce local cache, more brand-like colors 2020-10-15 23:03:05 -07:00
Tom Moor acb61d5e0c Match editing color to avatar and brand 2020-10-15 22:43:42 -07:00
Tom Moor 7dcbaa9c5c cursors 2020-10-15 22:10:30 -07:00
Tom Moor ae1761e517 wip 2020-10-15 20:24:44 -07:00
Tom Moor 4044818daa fix: Archived documents should not be returned from documents.list
closes #1575
2020-10-13 20:38:28 -07:00
Tom Moor 428171a1ec closes #1587 – remove global queue events when completed 2020-10-13 20:30:37 -07:00
Tom Moor 9c3195ef25 chore(deps): Bump RME
Fixes unable to upload the same image multiple times
2020-10-13 20:13:51 -07:00
Tom Moor 7166378c32 Restore old functionality, put new functionality behind flag 2020-10-13 08:43:53 -07:00
Tom Moor 2719321430 refactoring 2020-10-11 23:06:15 -07:00
Tom Moor a7f2c7edb3 rough, but working 2020-10-11 20:54:31 -07:00
Yohann Leon a6dc708fc0 fix: spotify widget wrong size (#1579) 2020-10-09 19:02:09 -07:00
Tom Moor 9c8f125668 chore: No need to build:server on CI 2020-09-30 20:29:54 -07:00
Tom Moor f348db048e chore: Specify babelrc for server specifically (#1585) 2020-09-30 20:02:33 -07:00
Tom Moor 0b8eb326ab chore(deps): Bump RME
closes #1566
2020-09-30 19:40:00 -07:00
Tom Moor 1da1f3d6e8 chore: Update Zapier integration (no longer invite-only)
fix: Remove link to Zapier integration from self-hosted as it doesnt make sense
2020-09-28 21:08:58 -07:00
Tom Moor 7def0dfab1 chore(deps): Upgrade socket.io-redis dependency for fix related to recent downtime
https://github.com/socketio/socket.io-redis/issues/210
2020-09-28 18:51:49 -07:00
Tom Moor 96987d2091 0.48.1 2020-09-27 10:32:15 -07:00
Tom Moor 6bb32c253b fix: Unable to run migrations in latest image. Added option to run non-SSL migration in production 2020-09-27 10:32:08 -07:00
Tom Moor 1d5f735032 Update LICENSE 2020-09-26 17:19:03 -07:00
Tom Moor f3e3651222 0.48.0 2020-09-26 17:18:34 -07:00
Tom Moor b3d9478486 Merge pull request #1563 from outline/release-0.48.0
release: v48.0.0
2020-09-26 17:09:40 -07:00
Tom Moor f26aeca46a chore(migrations): Add missing indexes 2020-09-26 14:01:04 -07:00
Tom Moor f1a95e5e79 feat: Improved search results when finding links in document editor (#1573)
* feat: Improved search results when finding links in document editor

* chore(deps): Bump RME for smoother link search
2020-09-26 11:07:49 -07:00
Tom Moor 6f1f855083 feat: Add tracking of search source in UI 2020-09-24 21:56:37 -07:00
Tom Moor 40d52e9a78 fix: Cannot press down arrow to navigate via keyboard to search results
(due to withRouter converting DocumentPreview to a functional component)
2020-09-24 21:36:31 -07:00
Tom Moor a2f2971fec fix: Reduce liklihood of false search queries
fix: Reduce possibility of dupe search queries
feat: Allow 'Enter' to trigger search before debounce
2020-09-24 21:29:00 -07:00
Tom Moor d89808ce9d fix: Home link on 'Not Found' page 2020-09-24 19:38:19 -07:00
Tom Moor a43cc9c5a9 chore(deps): Upgrade RME for improved doc search results 2020-09-24 19:35:28 -07:00
Tom Moor bb7fcd1b67 feat: Allow embedding /share/ routes in iframes 2020-09-23 19:26:18 -07:00
Tom Moor c1957025ec fix: Dont dupe record search queries when paging results
feat: Record queries via api tokens separately
2020-09-21 23:31:10 -07:00
Renan Filipe 98626ebbaf feat: Record search queries (#1554)
* Record search queries

* feat: add totalCount to the search response

* feat: add comments to explain why we use setTimeout
2020-09-21 23:05:42 -07:00
Tom Moor 0fa8a6ed2e feat: Add ctx.state.authType for tracking (#1567) 2020-09-21 22:02:37 -07:00
Tom Moor fa96891c8e fix(editor): Upgrade RME, improved link pasting 2020-09-21 20:33:58 -07:00
Tom Moor 9aa81dcf82 fix: Error deleting account as only admin not displayed to user 2020-09-21 20:31:06 -07:00
Tom Moor c04d5bdfb0 flow 2020-09-21 19:47:17 -07:00
Tom Moor ea69d09562 fix: Guard usage of localStorage 2020-09-21 19:43:51 -07:00
Tom Moor c8ff5cf221 fix: Styling of 'New' badge in dark mode
fix: less than a min ago -> just now
2020-09-21 19:35:51 -07:00
Tom Moor 5638f7a687 Merge develop 2020-09-21 19:18:34 -07:00
Tom Moor 1293f52552 lint 2020-09-21 19:18:09 -07:00
Tom Moor 86812cfe76 fix(editor): Upgrade RME, fixes cursor navigation around headings in FF 2020-09-21 19:11:54 -07:00
Tom Moor d9b7384853 fix: Improve handling of invalid file type passed to documents.import API endpoint 2020-09-21 00:34:13 -07:00
Tom Moor 292afd774d fix: CMD+Enter in title should leave editing mode 2020-09-21 00:21:35 -07:00
Tom Moor d3d286b1be tweak search highlight 2020-09-21 00:07:32 -07:00
Tom Moor 26b9566b96 fix: Various fixes for unread tracking 2020-09-20 23:37:09 -07:00
Guilherme DIniz d487da8f15 feat: Visually differentiate unread documents (#1507)
* feat: Visually differentiate unread documents

* feat: add document treatment in document preview

* fix requested changes

Co-authored-by: Tom Moor <tom.moor@gmail.com>
2020-09-20 22:32:28 -07:00
Tom Moor 4ffc04bc5d fix: Allow selection of embeds (#1562)
* feat: Support importing .docx or .html files as new documents (#1551)

* Support importing .docx as new documents

* Add html file support, build types and interface for easily adding file types to importer

* fix: Upload embedded images in docx to storage

* refactor: Bulk of logic to command

* refactor: Do all importing on server, so we're not splitting logic for import into two places

* test: Add documentImporter tests


Co-authored-by: Lance Whatley <whatl3y@gmail.com>

* fix: Accessibility audit

* fix: Quick fix, non editable title
closes #1560

* fix: Embed selection

Co-authored-by: Lance Whatley <whatl3y@gmail.com>
2020-09-20 22:27:11 -07:00
Tom Moor 68148bd4d8 fix: Quick fix, non editable title
closes #1560
2020-09-18 08:36:57 -07:00
Tom Moor 881105992e fix: Accessibility audit 2020-09-16 22:31:14 -07:00
Tom Moor 2c1a111dee feat: Support importing .docx or .html files as new documents (#1551)
* Support importing .docx as new documents

* Add html file support, build types and interface for easily adding file types to importer

* fix: Upload embedded images in docx to storage

* refactor: Bulk of logic to command

* refactor: Do all importing on server, so we're not splitting logic for import into two places

* test: Add documentImporter tests


Co-authored-by: Lance Whatley <whatl3y@gmail.com>
2020-09-16 21:54:33 -07:00
Tom Moor e67d319e2b fix: Update DocumentMetaWithViews to hooks, correctly observe store changes
closes #1555
2020-09-16 21:15:21 -07:00
Tom Moor 85f7e03921 test: No hotreload in test env 2020-09-16 08:22:50 -07:00
Tom Moor e30adbaac2 fix: Flip production/development NODE_ENV logic
closes #1548
2020-09-16 00:13:12 -07:00
Tom Moor ac8f0ebaac feat: Allow Google Embeds from regular (non publish-to-web) links (#1533)
Improve styling to allow getting back to source document
2020-09-15 18:38:42 -07:00
Tom Moor b3b71d2dc7 feat: Editable titles in sidebar (#1544)
* feat/editable-titles

* feat: Double click to edit titles in the sidebar

* Take into account policies

* fix: Title update on another client

* Improved styling
2020-09-15 18:01:40 -07:00
Tom Moor ab3613af48 fix: Path to onboarding markdown files (changed with updated build process) 2020-09-15 12:20:27 -07:00
Tom Moor 3940f1a108 Update README.md 2020-09-14 19:29:55 -07:00
Tom Moor 142e7da6a5 fix: Placeholder style 2020-09-13 20:26:38 -07:00
Tom Moor 021de66f7a fix: Document titles look faded in Safari 2020-09-13 20:18:50 -07:00
Nan Yu 55858d5d7d fix: put guard around sentry init (#1547) 2020-09-13 19:54:08 -07:00
Tom Moor 93d3582ac7 fix: Dead pointer zone over links when hover card is showing 2020-09-13 19:49:25 -07:00
Tom Moor 0b2107c1ee Merge develop 2020-09-13 10:50:13 -07:00
Tom Moor f8a167fd4b Merge branch 'develop' of github.com:outline/outline into develop 2020-09-13 10:47:31 -07:00
Tom Moor 608be3deef refactor: Remove babel/register for instant production server startup (#1481)
* refactor: Remove babel/register for instant production server startup

* fix: Update procfile location

* fix: package.json must be copied, not linked for production build

* fix: Production file paths

* fix: Public assets path

* Remove unused scripts
2020-09-13 10:46:33 -07:00
Tom Moor 56551d1ab3 fix: Error in backlinks service when updating an old document (pre v1) 2020-09-13 01:03:39 -07:00
Tom Moor 450d6b7e42 fix: Focus accessibility (#1536)
* fix: Focus ring appearing on click
fix: Focus ring not appearing on sidebar links
add: focus-visible polyfill

* fix: More visible outlines on keyboard focus
fix: Header block should be a button semantically
2020-09-12 23:27:23 -07:00
Matheus Rocha Vieira fc98cf78e6 chore: Upgrade Sentry to Latest Version (#1541)
* Upgrade Sentry to Latest Version
2020-09-12 23:24:32 -07:00
Nan Yu d9aa53a094 feat: allow searching for urls of internal documents (#1529)
* core search logic

* bump version of rich markdown editor

* let shift and meta modifiers do their thing when clicking on a link in a doc

* version bump editor

* test: Add parseDocumentSlug test

Co-authored-by: Tom Moor <tom.moor@gmail.com>
2020-09-12 23:23:40 -07:00
Nan Yu ffab4fbf76 remove -1 bottom from navigation links 2020-09-12 11:24:58 -07:00
Tom Moor 2161fba1dd Update LICENSE 2020-09-11 12:28:10 -07:00
Tom Moor cd2cdd025c 0.47.1 2020-09-11 12:26:13 -07:00
Tom Moor d5f5319f80 fix: History sidebar header shrinking when lots of history 2020-09-11 12:10:40 -07:00
Tom Moor c298c73240 fix: Revision skipped after identical previous autosave 2020-09-11 12:06:57 -07:00
Tom Moor 38d1831259 fix: Send events for draft documents down user channel always 2020-09-11 11:19:30 -07:00
Tom Moor 9c9b95741c fix: Unable to edit collection description
closes #1523
2020-09-09 23:23:01 -07:00
Tom Moor cc8db7e991 fix: Save team logo automatically
closes #1521
2020-09-08 21:12:37 -07:00
Tom Moor c5b7d9be13 fix: Flash of sidebar when navigating between documents 2020-09-08 20:48:52 -07:00
Tom Moor be2e46b5d2 References -> Referenced by
Less ambiguous as to the backlink direction
2020-09-08 18:44:19 -07:00
Tom Moor 4b2a766357 fix: Missing toast message when successfully copying code 2020-09-08 07:31:03 -07:00
Tom Moor f264d67862 fix: Two API endpoints where requesting a permenantly deleted document results in server error 2020-09-08 07:29:06 -07:00
Tom Moor b1648ac2aa Update LICENSE 2020-09-07 20:25:16 -07:00
Tom Moor 25423d8c85 0.47.0 2020-09-07 20:24:40 -07:00
Nan Yu e7ab2939d4 fix: Improved handling of delete events from collection and document sockets (#1517)
* handle delete events fron collection and document sockets

* handle collection deletes to documents

* rework semantics to always reload after a delete

Co-authored-by: Tom Moor <tom.moor@gmail.com>
2020-09-07 19:05:10 -07:00
Tom Moor ceeac9b982 fix: Deeply nested document titles not updated in collection documentStructure cache
closes #1508
2020-09-07 13:41:17 -07:00
Tom Moor 5de2f969e3 fix: Preload membership 2020-09-07 12:17:04 -07:00
Tom Moor b54901d50c fix: CMD+Eneter in title should still publish 2020-09-07 12:06:03 -07:00
Tom Moor 4de3f69474 fix: Documents in deleted collection should appear in trash (#1362)
* fix: Documents from deleted collection should show in trash

* improve messaging

* test: Add documents.move tests
feat: Add ability to restore trashed documents from deleted collection

* update store

* fix

* ui

* lint

* fix: Improve breadcrumb
2020-09-07 11:51:09 -07:00
Tom Moor c5de2da115 Merge branch 'develop' of github.com:outline/outline into develop 2020-09-07 10:43:36 -07:00
Tom Moor 709c3e78bd fix: Occasional render loop in editor toolbar (#1518)
* fix: CMD+S should save when editor title is focused

* fix: Bump RME, fixes various small editor issues
2020-09-07 10:42:51 -07:00
Tom Moor acb04fdf1a fix: CMD+S should save when editor title is focused 2020-09-06 10:33:39 -07:00
Tom Moor f13696dd2a Update yarn.lock 2020-09-05 19:58:17 -07:00
Tom Moor d6f245d67e Bump RME: Fixes inline styles in table cells with newlines 2020-09-05 19:51:12 -07:00
Tom Moor f2abf38fe4 perf: Remove source once compiled 2020-09-05 12:57:27 -07:00
Tom Moor f0712e22d8 perf: Improving dockerfile 2020-09-05 12:44:40 -07:00
Tom Moor e7e289d9fa end 2020-09-04 23:28:29 -07:00
Tom Moor 713187cfb4 fix 2020-09-04 15:39:36 -07:00
Tom Moor 11d3a5c9b9 fix: Enter in document title should create a newline at the top of editor and focus
closes #1511
2020-09-04 13:23:33 -07:00
Matheus Rocha Vieira cf1e506009 feat: Add ClickUp Embed Service (#1465)
* Add Clickup Embed Service

* Transparency Icon


Co-authored-by: Tom Moor <tom.moor@gmail.com>
2020-09-04 13:21:27 -07:00
Tom Moor 6b6d67beb6 fix: Allow disabling SSL for Postgres connection with standard PGSSLMODE env
closes #1501
2020-09-04 12:51:48 -07:00
Tom Moor a98e8ad8df fix: Breadcrumb overflow color is inaccessible
closes #1505
2020-09-04 12:49:20 -07:00
Tom Moor 9049785d98 fix: e shortcut to edit doesn't work when title is selected
closes #1510
2020-09-04 12:42:41 -07:00
Tom Moor e8648d4611 fix: Error in shares.info endpoint when requesting share record for deleted document 2020-09-03 23:22:41 -07:00
Tom Moor dd7436f78c fix: async error in backlinks service when dealing with a deleted document 2020-09-03 22:44:42 -07:00
Tom Moor b93a397ab3 feat: Bump RME 2020-09-03 22:27:30 -07:00
Tom Moor 206160582e chore: Tweak ordering of unpublish menu item 2020-09-02 20:19:36 -07:00
Tom Moor 4bf5926ee3 Bump RME 2020-08-31 21:57:26 -07:00
Tom Moor 82433e02a0 feat: Add custom error state for chunk loading failed 2020-08-31 21:09:23 -07:00
Tom Moor 637a9b5cf9 refactor: Remove unused event handlers 2020-08-31 20:44:19 -07:00
Tom Moor 95b91c466a fix: Disallow creating nested document within document in trash 2020-08-31 20:32:08 -07:00
Tom Moor 4edf90a184 fix: Development warning, missing key on event list items 2020-08-31 20:29:55 -07:00
Tom Moor 31522b0d6f fix: Local shares state not cleared when document is deleted 2020-08-31 20:28:41 -07:00
Tom Moor 759d4a5ac2 fix: Handle error revoking share link on frontend 2020-08-31 20:24:05 -07:00
Tom Moor dd0d51dd9d test 2020-08-31 20:23:51 -07:00
Tom Moor 8550116c6b fix: shares.list should not return shares for deleted documents
fix: shares.info should not return info for revoked shares
closes #1492
2020-08-31 20:15:10 -07:00
Tom Moor 3c7dc93982 Merge branch 'develop' of github.com:outline/outline into develop 2020-08-31 20:06:44 -07:00
Guilherme DIniz 0aa338cccc feat: Allow unpublishing documents (#1467)
* Allow unpublishing documents

* add block unpublish files that has children

* add api tests to new route
2020-08-31 20:03:05 -07:00
Tom Moor 8f41895e66 Merge develop 2020-08-31 19:40:41 -07:00
Tom Moor de8ac4acf5 fix: Configure mobx-react-lite observer batching
Removes development warning
2020-08-31 18:42:12 -07:00
Tom Moor de59147418 chore: Upgrade Sentry to 5.22.3
closes #1498
2020-08-31 18:36:30 -07:00
Tom Moor cf522cc85f fix: Regression with TOC not showing when navigating directly to document (#1500)
fix: Editing document too wide when TOC visible in read only
2020-08-31 18:31:13 -07:00
Tom Moor 8c7200fa87 chore: yarn deduplicate 2020-08-30 19:44:30 -07:00
Tom Moor f2310be173 Updated Yarn lockfile 2020-08-29 12:11:12 -07:00
Tom Moor 29f4dc9331 Bump RME
Fixes #1107 - It's now possible to use line breaks in table cells with Shift+Enter
Fixes #1253 - Selected content can now be dragged to reorder
2020-08-29 12:00:55 -07:00
Tom Moor 03b6dd62a8 fix: Missing click action to change permissions on a collection
fix: Modals no longer stacking correctly since upgrading react-portal
2020-08-25 21:00:50 -07:00
Tom Moor 7f0c608dbb Merge branch 'guilherme-diniz-feature/document-history-header' into develop 2020-08-25 20:04:02 -07:00
Tom Moor c52fbb944e Styling tidy up 2020-08-25 20:03:52 -07:00
Tom Moor e22e952606 Merge branch 'feature/document-history-header' of git://github.com/guilherme-diniz/outline into guilherme-diniz-feature/document-history-header 2020-08-25 19:44:56 -07:00
Guilherme Diniz 197cdff6c3 fix layout issues 2020-08-25 17:22:13 -03:00
Tom Moor 85d09b2351 fix: Deleting a document should correctly show who deleted (#1488) 2020-08-25 08:51:12 -07:00
Tom Moor 69611638b9 fix: Redirect to parent document when deleting a child document if possible (#1489) 2020-08-25 08:45:04 -07:00
Tom Moor e117d5f103 fix: Unable to view all possible locations when moving document (#1490)
* fix: Remove limit of displayed results on Move dialog

* fix: Filter templates from results

* Show final document location on hover/active, reduces visual noise
2020-08-25 08:44:46 -07:00
Tom Moor 03db975217 Merge branch 'feature/document-history-header' of git://github.com/guilherme-diniz/outline into guilherme-diniz-feature/document-history-header 2020-08-24 23:46:16 -07:00
Tom Moor 76279902f9 chore: Introduce AWS_S3_FORCE_PATH_STYLE option to maintain compatability with Minio et al (#1443)
- Make AWS_S3_UPLOAD_BUCKET_NAME optional
2020-08-24 23:27:10 -07:00
Tom Moor a304e91ffc Merge branch 'develop' into perf/issue-1464 2020-08-24 20:58:56 -07:00
Tom Moor 9b5573c5e2 0.46.1 2020-08-24 20:22:08 -07:00
Tom Moor ec61efa12b Remove unused scripts 2020-08-23 21:10:32 -07:00
Tom Moor b01778a39f fix: Public assets path 2020-08-23 20:44:44 -07:00
Tom Moor 5aa092853b fix: Production file paths 2020-08-23 20:35:59 -07:00
Tom Moor 1fa3db4bdc fix: package.json must be copied, not linked for production build 2020-08-23 20:29:17 -07:00
Tom Moor 6a9f74e6cc fix: Update procfile location 2020-08-23 19:21:43 -07:00
Tom Moor e8719340d1 refactor: Remove babel/register for instant production server startup 2020-08-23 19:10:16 -07:00
Tom Moor 70838918c3 fix: Collections not collapsing 2020-08-23 12:51:35 -07:00
Tom Moor ec38f5d79c refactor: Remove old react lifecycle methods (#1480)
* refactor: Remove deprecated APIs

* bump mobx-react for hooks support

* inject -> useStores
https://mobx-react.js.org/recipes-migration\#hooks-to-the-rescue

* chore: React rules of hooks lint
2020-08-23 11:51:56 -07:00
Jonathan Killian 179176c312 fix: Update package.json build script to use yarn instead of npm. (#1476)
* fix: Update package.json build script to yarn.

Update package.json build script to use yarn instead of npm to maintain consistency with the rest of scripts. I was running into an issue with the Dockerfile when using nvm with yarn and this fixed the issue.
2020-08-22 19:56:52 -07:00
Tom Moor c446a91f7d fix: Restore Postgres SSL support on Heroku
https://github.com/brianc/node-postgres/issues/2009
2020-08-22 08:27:42 -07:00
Guilherme Diniz 05f48f054b feat: Add Header to Document History Sidebar 2020-08-21 20:58:57 -03:00
Tom Moor ec55299c8b fix: Improve websocket reliability (#1470)
* check connection on page visibility change

* fix: SocketPresence account for socket changing
2020-08-20 20:37:54 -07:00
Tom Moor 26c574ab58 chore: Upgrade pg and sequelize to support node 14+ (#1462)
* Upgrade pg and sequelize to support node 14+

When Node 14 came out the app was incompatible, we should always have a maximum version defined here until the server code has been tested to prove compatibility

Co-authored-by: Lance Whatley <whatl3y@gmail.com>
2020-08-20 20:19:44 -07:00
Tom Moor 6dd6768f07 feat: Allow moving templates between collections (#1454)
- Allow template move in document policy
- fix: Ensure that document is not added to collection structure in documentMover command
- fix: Moving a template should now show nested documents as options
- fix: Hitting 'm' should not allow moving a draft
- fix: Styling of seperators on move screen
2020-08-20 19:46:29 -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
288 changed files with 10255 additions and 4271 deletions
+1 -1
View File
@@ -9,7 +9,7 @@
"version": "2",
"proposals": true
},
"useBuiltIns": "usage",
"useBuiltIns": "usage"
}
]
],
+8 -5
View File
@@ -3,7 +3,7 @@ jobs:
build:
working_directory: ~/outline
docker:
- image: circleci/node:12
- image: circleci/node:14
- image: circleci/redis:latest
- image: circleci/postgres:9.6.5-alpine-ram
environment:
@@ -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 check --max-workers 4
command: yarn flow check --max-workers 4
- run:
name: test
command: yarn test
- run:
name: build-webpack
command: yarn build:webpack
+19
View File
@@ -0,0 +1,19 @@
__mocks__
.git
.vscode
.github
.circleci
.DS_Store
.env*
.eslint*
.flowconfig
.log
Makefile
Procfile
app.json
build
docker-compose.yml
fakes3
flow-typed
node_modules
setupJest.js
+2 -1
View File
@@ -14,7 +14,7 @@ URL=http://localhost:3000
PORT=3000
# enforce (auto redirect to) https in production, (optional) default is true.
# set to false if your SSL is terminated at a loadbalancer, for example
# set to false if your SSL is terminated at a loadbalancer, for example
FORCE_HTTPS=true
ENABLE_UPDATES=true
@@ -45,6 +45,7 @@ AWS_REGION=xx-xxxx-x
AWS_S3_UPLOAD_BUCKET_URL=http://s3:4569
AWS_S3_UPLOAD_BUCKET_NAME=bucket_name_here
AWS_S3_UPLOAD_MAX_SIZE=26214400
AWS_S3_FORCE_PATH_STYLE=true
# uploaded s3 objects permission level, default is private
# set to "public-read" to allow public access
AWS_S3_ACL=private
+5 -1
View File
@@ -4,7 +4,8 @@
"react-app",
"plugin:import/errors",
"plugin:import/warnings",
"plugin:flowtype/recommended"
"plugin:flowtype/recommended",
"plugin:react-hooks/recommended"
],
"plugins": [
"prettier",
@@ -97,5 +98,8 @@
},
"env": {
"jest": true
},
"globals": {
"EDITOR_VERSION": true
}
}
+4
View File
@@ -11,6 +11,10 @@
.*/node_modules/react-side-effect/.*
.*/node_modules/fbjs/.*
.*/node_modules/config-chain/.*
.*/node_modules/yjs/.*
.*/node_modules/@tommoor/y-prosemirror/.*
.*/node_modules/y-protocols/.*
.*/node_modules/lib0/.*
.*/server/scripts/.*
*.test.js
+2
View File
@@ -1,4 +1,5 @@
dist
build
node_modules/*
server/scripts
.env
@@ -7,3 +8,4 @@ npm-debug.log
stats.json
.DS_Store
fakes3/*
.idea
+13 -7
View File
@@ -1,17 +1,23 @@
FROM node:12-alpine
FROM node:14-alpine
ENV PATH /opt/outline/node_modules/.bin:/opt/node_modules/.bin:$PATH
ENV NODE_PATH /opt/outline/node_modules:/opt/node_modules
ENV APP_PATH /opt/outline
RUN mkdir -p $APP_PATH
WORKDIR $APP_PATH
COPY . $APP_PATH
RUN yarn install --pure-lockfile
RUN yarn build
RUN cp -r /opt/outline/node_modules /opt/node_modules
COPY package.json ./
COPY yarn.lock ./
RUN yarn --pure-lockfile
COPY . .
RUN yarn build && \
yarn --production --ignore-scripts --prefer-offline && \
rm -rf shared && \
rm -rf app
ENV NODE_ENV production
CMD yarn start
EXPOSE 3000
+2 -2
View File
@@ -3,7 +3,7 @@ Business Source License 1.1
Parameters
Licensor: General Outline, Inc.
Licensed Work: Outline 0.44.0
Licensed Work: Outline 0.49.0
The Licensed Work is (c) 2020 General Outline, Inc.
Additional Use Grant: You may make use of the Licensed Work, provided that
you may not use the Licensed Work for a Document
@@ -15,7 +15,7 @@ Additional Use Grant: You may make use of the Licensed Work, provided that
Licensed Work by creating teams and documents
controlled by such third parties.
Change Date: 2023-07-03
Change Date: 2023-10-26
Change License: Apache License, Version 2.0
+6
View File
@@ -9,10 +9,16 @@ build:
test:
docker-compose up -d redis postgres s3
yarn sequelize db:drop --env=test
yarn sequelize db:create --env=test
yarn sequelize db:migrate --env=test
yarn test
watch:
docker-compose up -d redis postgres s3
yarn sequelize db:drop --env=test
yarn sequelize db:create --env=test
yarn sequelize db:migrate --env=test
yarn test:watch
destroy:
+1 -1
View File
@@ -1 +1 @@
web: node index.js
web: node ./build/server/index.js
+38 -31
View File
@@ -22,13 +22,38 @@ If you'd like to run your own copy of Outline or contribute to development then
Outline requires the following dependencies:
- Node.js >= 12
- Postgres >=9.5
- Redis >= 4
- AWS S3 storage bucket for media and other attachments
- [Node.js](https://nodejs.org/) >= 12
- [Yarn](https://yarnpkg.com)
- [Postgres](https://www.postgresql.org/download/) >=9.5
- [Redis](https://redis.io/) >= 4
- AWS S3 bucket or compatible API for file storage
- Slack or Google developer application for authentication
### Production
For a manual self-hosted production installation these are the suggested steps:
1. Clone this repo and install dependencies with `yarn install`
1. Build the source code with `yarn build`
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 at the top of `.env`)
1. `SLACK_KEY` (this is called "Client ID" in Slack admin)
1. `SLACK_SECRET` (this is called "Client Secret" in Slack admin)
1. `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)
1. `AWS_` (all of the keys beginning with AWS)
1. Migrate database schema with `yarn sequelize:migrate`. Production assumes an SSL connection, if
Postgres is on the same machine and is not SSL you can migrate with `yarn sequelize:migrate --env=production-ssl-disabled`.
1. Start the service with any daemon tools you prefer. Take PM2 for example, `NODE_ENV=production pm2 start ./build/server/index.js --name outline `
1. Visit http://you_server_ip:3000 and you should be able to see Outline page
> Port number can be changed using the `PORT` environment variable
1. (Optional) You can add an `nginx` reverse proxy to serve your instance of Outline for a clean URL without the port number, support SSL, etc.
### Development
In development you can quickly get an environment running using Docker by following these steps:
@@ -50,32 +75,6 @@ In development you can quickly get an environment running using Docker by follow
1. Run `make up`. This will download dependencies, build and launch a development version of Outline
### Production
For a self-hosted production installation there is more flexibility, but these are the suggested steps:
1. Clone this repo and install dependencies with `yarn` or `npm install`
> 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 at the top of `.env`)
1. `SLACK_KEY` (this is called "Client ID" in Slack admin)
1. `SLACK_SECRET` (this is called "Client Secret" in Slack admin)
1. `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)
1. `AWS_` (all of the keys beginning with AWS)
1. Migrate database schema with `yarn sequelize:migrate` or `npm run sequelize:migrate `
1. Start the service with any daemon tools you prefer. Take PM2 for example, `NODE_ENV=production pm2 start index.js --name outline `
1. Visit http://you_server_ip:3000 and you should be able to see Outline page
> Port number can be changed in the `.env` file
1. (Optional) You can add an `nginx` reverse proxy to serve your instance of Outline for a clean URL without the port number, support SSL, etc.
## Development
### Server
@@ -140,8 +139,16 @@ To add new tests, write your tests with [Jest](https://facebook.github.io/jest/)
```shell
# To run all tests
yarn test
make test
# To run backend tests in watch mode
make watch
```
Once the test database is created with `make test` you may individually run
frontend and backend tests directly.
```shell
# To run backend tests
yarn test:server
+5
View File
@@ -92,6 +92,11 @@
"value": "26214400",
"required": false
},
"AWS_S3_FORCE_PATH_STYLE": {
"description": "Use path-style URL's for connecting to S3 instead of subdomain. This is useful for S3-compatible storage.",
"value": "true",
"required": false
},
"AWS_REGION": {
"value": "us-east-1",
"description": "Region in which the above S3 bucket exists",
+8 -3
View File
@@ -21,9 +21,14 @@ const Authenticated = observer(({ auth, children }: Props) => {
return <LoadingIndicator />;
}
// 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 (
// If we're authenticated but viewing a domain that doesn't match the
// current team then kick the user to the teams correct domain.
if (team.domain) {
if (team.domain !== hostname) {
window.location.href = `${team.url}${window.location.pathname}`;
return <LoadingIndicator />;
}
} else if (
env.SUBDOMAINS_ENABLED &&
team.subdomain &&
isCustomSubdomain(hostname) &&
+4 -3
View File
@@ -4,9 +4,10 @@ import styled from "styled-components";
const Badge = styled.span`
margin-left: 10px;
padding: 2px 6px 3px;
background-color: ${({ primary, theme }) =>
primary ? theme.primary : theme.textTertiary};
color: ${({ primary, theme }) => (primary ? theme.white : theme.background)};
background-color: ${({ yellow, primary, theme }) =>
yellow ? theme.yellow : primary ? theme.primary : theme.textTertiary};
color: ${({ primary, yellow, theme }) =>
primary ? theme.white : yellow ? theme.almostBlack : theme.background};
border-radius: 4px;
font-size: 11px;
font-weight: 500;
+75 -32
View File
@@ -1,11 +1,13 @@
// @flow
import { observer, inject } from "mobx-react";
import {
PadlockIcon,
ArchiveIcon,
EditIcon,
GoToIcon,
MoreIcon,
PadlockIcon,
ShapesIcon,
EditIcon,
TrashIcon,
} from "outline-icons";
import * as React from "react";
import { Link } from "react-router-dom";
@@ -25,11 +27,73 @@ type Props = {
onlyText: boolean,
};
const Breadcrumb = observer(({ document, collections, onlyText }: Props) => {
const collection = collections.get(document.collectionId);
if (!collection) return <div />;
function Icon({ document }) {
if (document.isDeleted) {
return (
<>
<CollectionName to="/trash">
<TrashIcon color="currentColor" />
&nbsp;
<span>Trash</span>
</CollectionName>
<Slash />
</>
);
}
if (document.isArchived) {
return (
<>
<CollectionName to="/archive">
<ArchiveIcon color="currentColor" />
&nbsp;
<span>Archive</span>
</CollectionName>
<Slash />
</>
);
}
if (document.isDraft) {
return (
<>
<CollectionName to="/drafts">
<EditIcon color="currentColor" />
&nbsp;
<span>Drafts</span>
</CollectionName>
<Slash />
</>
);
}
if (document.isTemplate) {
return (
<>
<CollectionName to="/templates">
<ShapesIcon color="currentColor" />
&nbsp;
<span>Templates</span>
</CollectionName>
<Slash />
</>
);
}
return null;
}
const path = collection.pathToDocument(document).slice(0, -1);
const Breadcrumb = observer(({ document, collections, onlyText }: Props) => {
let collection = collections.get(document.collectionId);
if (!collection) {
if (!document.deletedAt) return <div />;
collection = {
id: document.collectionId,
name: "Deleted Collection",
color: "currentColor",
};
}
const path = collection.pathToDocument
? collection.pathToDocument(document).slice(0, -1)
: [];
if (onlyText === true) {
return (
@@ -50,34 +114,13 @@ const Breadcrumb = observer(({ document, collections, onlyText }: Props) => {
);
}
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 />
</>
)}
<Icon document={document} />
<CollectionName to={collectionUrl(collection.id)}>
<CollectionIcon collection={collection} expanded />
&nbsp;
@@ -127,12 +170,12 @@ export const Slash = styled(GoToIcon)`
const Overflow = styled(MoreIcon)`
flex-shrink: 0;
opacity: 0.25;
transition: opacity 100ms ease-in-out;
fill: ${(props) => props.theme.divider};
&:hover,
&:active {
opacity: 1;
&:active,
&:hover {
fill: ${(props) => props.theme.text};
}
`;
+13 -16
View File
@@ -1,25 +1,22 @@
// @flow
import * as React from "react";
import { Link } from "react-router-dom";
import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
import { DropdownMenu } from "components/DropdownMenu";
import DropdownMenuItems from "components/DropdownMenu/DropdownMenuItems";
type Props = {
label: React.Node,
path: Array<any>,
};
export default class BreadcrumbMenu extends React.Component<Props> {
render() {
const { path } = this.props;
return (
<DropdownMenu label={this.props.label} position="center">
{path.map((item) => (
<DropdownMenuItem as={Link} to={item.url} key={item.id}>
{item.title}
</DropdownMenuItem>
))}
</DropdownMenu>
);
}
export default function BreadcrumbMenu({ label, path }: Props) {
return (
<DropdownMenu label={label} position="center">
<DropdownMenuItems
items={path.map((item) => ({
title: item.title,
to: item.url,
}))}
/>
</DropdownMenu>
);
}
+1 -22
View File
@@ -1,6 +1,6 @@
// @flow
import { ExpandedIcon } from "outline-icons";
import { darken, lighten } from "polished";
import { darken } from "polished";
import * as React from "react";
import styled from "styled-components";
@@ -19,7 +19,6 @@ const RealButton = styled.button`
height: 32px;
text-decoration: none;
flex-shrink: 0;
outline: none;
cursor: pointer;
user-select: none;
@@ -36,13 +35,6 @@ const RealButton = styled.button`
background: ${(props) => darken(0.05, props.theme.buttonBackground)};
}
&:focus {
transition-duration: 0.05s;
box-shadow: ${(props) => lighten(0.4, props.theme.buttonBackground)} 0px 0px
0px 3px;
outline: none;
}
&:disabled {
cursor: default;
pointer-events: none;
@@ -70,13 +62,6 @@ const RealButton = styled.button`
border: 1px solid ${props.theme.buttonNeutralBorder};
}
&:focus {
transition-duration: 0.05s;
border: 1px solid ${lighten(0.4, props.theme.buttonBackground)};
box-shadow: ${lighten(0.4, props.theme.buttonBackground)} 0px 0px
0px 2px;
}
&:disabled {
color: ${props.theme.textTertiary};
}
@@ -89,12 +74,6 @@ const RealButton = styled.button`
&:hover {
background: ${darken(0.05, props.theme.danger)};
}
&:focus {
transition-duration: 0.05s;
box-shadow: ${lighten(0.4, props.theme.danger)} 0px 0px
0px 3px;
}
`};
`;
+1
View File
@@ -9,6 +9,7 @@ type Props = {
const Container = styled.div`
width: 100%;
max-width: 100vw;
padding: 60px 20px;
${breakpoint("tablet")`
+1 -1
View File
@@ -18,7 +18,7 @@ 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"
ui.resolvedTheme === "dark" && collection.color !== "currentColor"
? getLuminance(collection.color) > 0.12
? collection.color
: "currentColor"
+2 -2
View File
@@ -6,7 +6,7 @@ type Props = {
children: React.Node,
};
export default function DelayedMount({ delay = 150, children }: Props) {
export default function DelayedMount({ delay = 250, children }: Props) {
const [isShowing, setShowing] = React.useState(false);
React.useEffect(() => {
@@ -14,7 +14,7 @@ export default function DelayedMount({ delay = 150, children }: Props) {
return () => {
clearTimeout(timeout);
};
}, []);
}, [delay]);
if (!isShowing) {
return null;
@@ -1,20 +1,23 @@
// @flow
import ArrowKeyNavigation from "boundless-arrow-key-navigation";
import { observable, action } from "mobx";
import { observer, inject } from "mobx-react";
import { action, observable } from "mobx";
import { inject, observer } from "mobx-react";
import { CloseIcon } from "outline-icons";
import * as React from "react";
import { type RouterHistory, type Match } from "react-router-dom";
import { type Match, Redirect, type RouterHistory } from "react-router-dom";
import { Waypoint } from "react-waypoint";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import { DEFAULT_PAGINATION_LIMIT } from "stores/BaseStore";
import DocumentsStore from "stores/DocumentsStore";
import RevisionsStore from "stores/RevisionsStore";
import Button from "components/Button";
import Flex from "components/Flex";
import { ListPlaceholder } from "components/LoadingPlaceholder";
import Revision from "./components/Revision";
import { documentHistoryUrl } from "utils/routeHelpers";
import { documentHistoryUrl, documentUrl } from "utils/routeHelpers";
type Props = {
match: Match,
@@ -29,6 +32,7 @@ class DocumentHistory extends React.Component<Props> {
@observable isFetching: boolean = false;
@observable offset: number = 0;
@observable allowLoadMore: boolean = true;
@observable redirectTo: ?string;
async componentDidMount() {
await this.loadMoreResults();
@@ -86,15 +90,34 @@ class DocumentHistory extends React.Component<Props> {
return this.props.revisions.getDocumentRevisions(document.id);
}
onCloseHistory = () => {
const document = this.props.documents.getByUrl(
this.props.match.params.documentSlug
);
this.redirectTo = documentUrl(document);
};
render() {
const document = this.props.documents.getByUrl(
this.props.match.params.documentSlug
);
const showLoading = (!this.isLoaded && this.isFetching) || !document;
if (this.redirectTo) return <Redirect to={this.redirectTo} push />;
return (
<Sidebar>
<Wrapper column>
<Header>
<Title>History</Title>
<Button
icon={<CloseIcon />}
onClick={this.onCloseHistory}
borderOnHover
neutral
/>
</Header>
{showLoading ? (
<Loading>
<ListPlaceholder count={5} />
@@ -140,10 +163,37 @@ const Wrapper = styled(Flex)`
`;
const Sidebar = styled(Flex)`
display: none;
background: ${(props) => props.theme.background};
min-width: ${(props) => props.theme.sidebarWidth};
border-left: 1px solid ${(props) => props.theme.divider};
z-index: 1;
${breakpoint("tablet")`
display: flex;
`};
`;
const Title = styled(Flex)`
font-size: 16px;
font-weight: 600;
text-align: center;
align-items: center;
justify-content: flex-start;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
width: 0;
flex-grow: 1;
`;
const Header = styled(Flex)`
align-items: center;
position: relative;
padding: 12px;
border-bottom: 1px solid ${(props) => props.theme.divider};
color: ${(props) => props.theme.text};
flex-shrink: 0;
`;
export default inject("documents", "revisions")(DocumentHistory);
@@ -11,11 +11,12 @@ import Avatar from "components/Avatar";
import Flex from "components/Flex";
import Time from "components/Time";
import RevisionMenu from "menus/RevisionMenu";
import { type Theme } from "types";
import { documentHistoryUrl } from "utils/routeHelpers";
type Props = {
theme: Object,
theme: Theme,
showMenu: boolean,
selected: boolean,
document: Document,
@@ -36,7 +37,7 @@ class RevisionListItem extends React.Component<Props> {
{revision.createdBy.name}
</Author>
<Meta>
<Time dateTime={revision.createdAt}>
<Time dateTime={revision.createdAt} tooltipDelay={250}>
{format(revision.createdAt, "MMMM Do, YYYY h:mm a")}
</Time>
</Meta>
+30 -8
View File
@@ -18,8 +18,7 @@ const Container = styled(Flex)`
`;
const Modified = styled.span`
color: ${(props) =>
props.highlight ? props.theme.text : props.theme.textTertiary};
color: ${(props) => props.theme.textTertiary};
font-weight: ${(props) => (props.highlight ? "600" : "400")};
`;
@@ -28,6 +27,7 @@ type Props = {
auth: AuthStore,
showCollection?: boolean,
showPublished?: boolean,
showLastViewed?: boolean,
document: Document,
children: React.Node,
to?: string,
@@ -38,6 +38,7 @@ function DocumentMeta({
collections,
showPublished,
showCollection,
showLastViewed,
document,
children,
to,
@@ -52,6 +53,7 @@ function DocumentMeta({
archivedAt,
deletedAt,
isDraft,
lastViewedAt,
} = document;
// Prevent meta information from displaying if updatedBy is not available.
@@ -65,37 +67,37 @@ function DocumentMeta({
if (deletedAt) {
content = (
<span>
deleted <Time dateTime={deletedAt} /> ago
deleted <Time dateTime={deletedAt} addSuffix />
</span>
);
} else if (archivedAt) {
content = (
<span>
archived <Time dateTime={archivedAt} /> ago
archived <Time dateTime={archivedAt} addSuffix />
</span>
);
} else if (createdAt === updatedAt) {
content = (
<span>
created <Time dateTime={updatedAt} /> ago
created <Time dateTime={updatedAt} addSuffix />
</span>
);
} else if (publishedAt && (publishedAt === updatedAt || showPublished)) {
content = (
<span>
published <Time dateTime={publishedAt} /> ago
published <Time dateTime={publishedAt} addSuffix />
</span>
);
} else if (isDraft) {
content = (
<span>
saved <Time dateTime={updatedAt} /> ago
saved <Time dateTime={updatedAt} addSuffix />
</span>
);
} else {
content = (
<Modified highlight={modifiedSinceViewed}>
updated <Time dateTime={updatedAt} /> ago
updated <Time dateTime={updatedAt} addSuffix />
</Modified>
);
}
@@ -103,6 +105,25 @@ function DocumentMeta({
const collection = collections.get(document.collectionId);
const updatedByMe = auth.user && auth.user.id === updatedBy.id;
const timeSinceNow = () => {
if (isDraft || !showLastViewed) {
return null;
}
if (!lastViewedAt) {
return (
<>
&nbsp;<Modified highlight>Never viewed</Modified>
</>
);
}
return (
<span>
&nbsp;Viewed <Time dateTime={lastViewedAt} addSuffix shorten />
</span>
);
};
return (
<Container align="center" {...rest}>
{updatedByMe ? "You" : updatedBy.name}&nbsp;
@@ -115,6 +136,7 @@ function DocumentMeta({
</strong>
</span>
)}
&nbsp;{timeSinceNow()}
{children}
</Container>
);
+13 -9
View File
@@ -1,27 +1,31 @@
// @flow
import { inject } from "mobx-react";
import { useObserver } 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";
import useStores from "../hooks/useStores";
type Props = {|
views: ViewsStore,
document: Document,
isDraft: boolean,
to?: string,
|};
function DocumentMetaWithViews({ views, to, isDraft, document }: Props) {
const totalViews = views.countForDocument(document.id);
function DocumentMetaWithViews({ to, isDraft, document }: Props) {
const { views } = useStores();
const documentViews = useObserver(() => views.inDocument(document.id));
const totalViewers = documentViews.length;
const onlyYou = totalViewers === 1 && documentViews[0].user.id;
return (
<Meta document={document} to={to}>
{totalViews && !isDraft ? (
{totalViewers && !isDraft ? (
<>
&nbsp;&middot; Viewed{" "}
{totalViews === 1 ? "once" : `${totalViews} times`}
&nbsp;&middot; Viewed by{" "}
{onlyYou
? "only you"
: `${totalViewers} ${totalViewers === 1 ? "person" : "people"}`}
</>
) : null}
</Meta>
@@ -45,4 +49,4 @@ const Meta = styled(DocumentMeta)`
}
`;
export default inject("views")(DocumentMetaWithViews);
export default DocumentMetaWithViews;
@@ -1,13 +1,15 @@
// @flow
import { observable } from "mobx";
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 { Link, Redirect } 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 EventBoundary from "components/EventBoundary";
import Flex from "components/Flex";
import Highlight from "components/Highlight";
import Tooltip from "components/Tooltip";
@@ -15,7 +17,6 @@ import DocumentMenu from "menus/DocumentMenu";
import { newDocumentUrl } from "utils/routeHelpers";
type Props = {
history: RouterHistory,
document: Document,
highlight?: ?string,
context?: ?string,
@@ -30,6 +31,8 @@ const SEARCH_RESULT_REGEX = /<b\b[^>]*>(.*?)<\/b>/gi;
@observer
class DocumentPreview extends React.Component<Props> {
@observable redirectTo: ?string;
handleStar = (ev: SyntheticEvent<>) => {
ev.preventDefault();
ev.stopPropagation();
@@ -48,17 +51,15 @@ class DocumentPreview extends React.Component<Props> {
return tag.replace(/<b\b[^>]*>(.*?)<\/b>/gi, "$1");
};
handleNewFromTemplate = (event) => {
handleNewFromTemplate = (event: SyntheticEvent<>) => {
event.preventDefault();
event.stopPropagation();
const { document } = this.props;
this.props.history.push(
newDocumentUrl(document.collectionId, {
templateId: document.id,
})
);
this.redirectTo = newDocumentUrl(document.collectionId, {
templateId: document.id,
});
};
render() {
@@ -73,6 +74,10 @@ class DocumentPreview extends React.Component<Props> {
context,
} = this.props;
if (this.redirectTo) {
return <Redirect to={this.redirectTo} push />;
}
const queryIsInTitle =
!!highlight &&
!!document.title.toLowerCase().includes(highlight.toLowerCase());
@@ -86,6 +91,7 @@ class DocumentPreview extends React.Component<Props> {
>
<Heading>
<Title text={document.titleWithDefault} highlight={highlight} />
{document.isNew && <Badge yellow>New</Badge>}
{!document.isDraft &&
!document.isArchived &&
!document.isTemplate && (
@@ -118,7 +124,9 @@ class DocumentPreview extends React.Component<Props> {
</Button>
)}
&nbsp;
<DocumentMenu document={document} showPin={showPin} />
<EventBoundary>
<DocumentMenu document={document} showPin={showPin} />
</EventBoundary>
</SecondaryActions>
</Heading>
@@ -133,6 +141,7 @@ class DocumentPreview extends React.Component<Props> {
document={document}
showCollection={showCollection}
showPublished={showPublished}
showLastViewed
/>
</DocumentLink>
);
@@ -169,6 +178,7 @@ const DocumentLink = styled(Link)`
border-radius: 8px;
max-height: 50vh;
min-width: 100%;
max-width: calc(100vw - 40px);
overflow: hidden;
position: relative;
@@ -180,7 +190,6 @@ const DocumentLink = styled(Link)`
&:active,
&:focus {
background: ${(props) => props.theme.listItemHoverBackground};
outline: none;
${SecondaryActions} {
opacity: 1;
@@ -228,4 +237,4 @@ const ResultContext = styled(Highlight)`
margin-bottom: 0.25em;
`;
export default withRouter(DocumentPreview);
export default DocumentPreview;
+9 -6
View File
@@ -7,8 +7,8 @@ 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 UiStore from "stores/UiStore";
import LoadingIndicator from "components/LoadingIndicator";
import importFile from "utils/importFile";
const EMPTY_OBJECT = {};
let importingLock = false;
@@ -19,6 +19,7 @@ type Props = {
documentId?: string,
activeClassName?: string,
rejectClassName?: string,
ui: UiStore,
documents: DocumentsStore,
disabled: boolean,
location: Object,
@@ -61,17 +62,19 @@ class DropToImport extends React.Component<Props> {
}
for (const file of files) {
const doc = await importFile({
documents: this.props.documents,
const doc = await this.props.documents.import(
file,
documentId,
collectionId,
});
{ publish: true }
);
if (redirect) {
this.props.history.push(doc.url);
}
}
} catch (err) {
this.props.ui.showToast(`Could not import file. ${err.message}`);
} finally {
this.isImporting = false;
importingLock = false;
@@ -95,7 +98,7 @@ class DropToImport extends React.Component<Props> {
return (
<Dropzone
accept="text/markdown, text/plain"
accept={documents.importFileTypes.join(", ")}
onDropAccepted={this.onDropAccepted}
style={EMPTY_OBJECT}
disableClick
@@ -110,4 +113,4 @@ class DropToImport extends React.Component<Props> {
}
}
export default inject("documents")(withRouter(DropToImport));
export default inject("documents", "ui")(withRouter(DropToImport));
+5 -4
View File
@@ -18,7 +18,7 @@ type Children =
| React.Node
| ((options: { closePortal: () => void }) => React.Node);
type Props = {
type Props = {|
label?: React.Node,
onOpen?: () => void,
onClose?: () => void,
@@ -27,7 +27,7 @@ type Props = {
hover?: boolean,
style?: Object,
position?: "left" | "right" | "center",
};
|};
@observer
class DropdownMenu extends React.Component<Props> {
@@ -177,6 +177,7 @@ class DropdownMenu extends React.Component<Props> {
{label || (
<NudeButton
id={`${this.id}button`}
aria-label="More options"
aria-haspopup="true"
aria-expanded={isOpen ? "true" : "false"}
aria-controls={this.id}
@@ -232,7 +233,7 @@ const Label = styled(Flex).attrs({
justify: "center",
align: "center",
})`
z-index: 1000;
z-index: ${(props) => props.theme.depths.menu};
cursor: pointer;
`;
@@ -244,7 +245,7 @@ const Position = styled.div`
${({ top }) => (top !== undefined ? `top: ${top}px` : "")};
${({ bottom }) => (bottom !== undefined ? `bottom: ${bottom}px` : "")};
max-height: 75%;
z-index: 1000;
z-index: ${(props) => props.theme.depths.menu};
transform: ${(props) =>
props.position === "center" ? "translateX(-50%)" : "initial"};
pointer-events: none;
@@ -80,7 +80,6 @@ const MenuItem = styled.a`
&:focus {
color: ${props.theme.white};
background: ${props.theme.primary};
outline: none;
}
`};
`;
@@ -0,0 +1,128 @@
// @flow
import * as React from "react";
import { Link } from "react-router-dom";
import DropdownMenu from "./DropdownMenu";
import DropdownMenuItem from "./DropdownMenuItem";
type MenuItem =
| {|
title: React.Node,
to: string,
visible?: boolean,
disabled?: boolean,
|}
| {|
title: React.Node,
onClick: (event: SyntheticEvent<>) => void | Promise<void>,
visible?: boolean,
disabled?: boolean,
|}
| {|
title: React.Node,
href: string,
visible?: boolean,
disabled?: boolean,
|}
| {|
title: React.Node,
visible?: boolean,
disabled?: boolean,
style?: Object,
hover?: boolean,
items: MenuItem[],
|}
| {|
type: "separator",
visible?: boolean,
|}
| {|
type: "heading",
visible?: boolean,
title: React.Node,
|};
type Props = {|
items: MenuItem[],
|};
export default function DropdownMenuItems({ items }: Props): React.Node {
let filtered = items.filter((item) => item.visible !== false);
// this block literally just trims unneccessary separators
filtered = filtered.reduce((acc, item, index) => {
// trim separators from start / end
if (item.type === "separator" && index === 0) return acc;
if (item.type === "separator" && index === filtered.length - 1) return acc;
// trim double separators looking ahead / behind
const prev = filtered[index - 1];
if (prev && prev.type === "separator" && item.type === "separator")
return acc;
// otherwise, continue
return [...acc, item];
}, []);
return filtered.map((item, index) => {
if (item.to) {
return (
<DropdownMenuItem
as={Link}
to={item.to}
key={index}
disabled={item.disabled}
>
{item.title}
</DropdownMenuItem>
);
}
if (item.href) {
return (
<DropdownMenuItem
href={item.href}
key={index}
disabled={item.disabled}
target="_blank"
>
{item.title}
</DropdownMenuItem>
);
}
if (item.onClick) {
return (
<DropdownMenuItem
onClick={item.onClick}
disabled={item.disabled}
key={index}
>
{item.title}
</DropdownMenuItem>
);
}
if (item.items) {
return (
<DropdownMenu
style={item.style}
label={
<DropdownMenuItem disabled={item.disabled}>
{item.title}
</DropdownMenuItem>
}
hover={item.hover}
key={index}
>
<DropdownMenuItems items={item.items} />
</DropdownMenu>
);
}
if (item.type === "separator") {
return <hr key={index} />;
}
return null;
});
}
+92 -14
View File
@@ -2,14 +2,16 @@
import { lighten } from "polished";
import * as React from "react";
import { withRouter, type RouterHistory } from "react-router-dom";
import RichMarkdownEditor from "rich-markdown-editor";
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 = {
@@ -22,7 +24,7 @@ type Props = {
};
type PropsWithRef = Props & {
forwardedRef: React.Ref<RichMarkdownEditor>,
forwardedRef: React.Ref<any>,
history: RouterHistory,
};
@@ -32,14 +34,14 @@ class Editor extends React.Component<PropsWithRef> {
return result.url;
};
onClickLink = (href: string) => {
onClickLink = (href: string, event: MouseEvent) => {
// on page hash
if (href[0] === "#") {
window.location.href = href;
return;
}
if (isInternalUrl(href)) {
if (isInternalUrl(href) && !event.metaKey && !event.shiftKey) {
// relative
let navigateTo = href;
@@ -54,7 +56,7 @@ class Editor extends React.Component<PropsWithRef> {
}
this.props.history.push(navigateTo);
} else {
} else if (href) {
window.open(href, "_blank");
}
};
@@ -67,15 +69,17 @@ class Editor extends React.Component<PropsWithRef> {
render() {
return (
<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 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>
);
}
}
@@ -93,6 +97,66 @@ const StyledEditor = styled(RichMarkdownEditor)`
font-weight: 500;
}
.ProseMirror {
.ProseMirror-yjs-cursor {
position: relative;
margin-left: -1px;
margin-right: -1px;
border-left: 1px solid black;
border-right: 1px solid black;
height: 1em;
word-break: normal;
&:after {
content: "";
display: block;
position: absolute;
left: -8px;
right: -8px;
top: 0;
bottom: 0;
}
> div {
opacity: 0;
position: absolute;
top: -1.8em;
font-size: 13px;
background-color: rgb(250, 129, 0);
font-style: normal;
line-height: normal;
user-select: none;
white-space: nowrap;
color: white;
padding: 2px 6px;
font-weight: 500;
border-radius: 4px;
pointer-events: none;
left: -1px;
}
&:hover {
> div {
opacity: 1;
transition: opacity 100ms ease-in-out;
}
}
}
}
.heading-name {
pointer-events: none;
}
.heading-name:first-child {
& + h1,
& + h2,
& + h3,
& + h4 {
margin-top: 0;
}
}
p {
a {
color: ${(props) => props.theme.text};
@@ -123,3 +187,17 @@ const EditorWithRouterAndTheme = withRouter(withTheme(Editor));
export default React.forwardRef<Props, typeof Editor>((props, ref) => (
<EditorWithRouterAndTheme {...props} forwardedRef={ref} />
));
// > .ProseMirror-yjs-cursor:first-child {
// margin-top: 16px;
// }
// p:first-child,
// h1:first-child,
// h2:first-child,
// h3:first-child,
// h4:first-child,
// h5:first-child,
// h6:first-child {
// margin-top: 16px;
// }
+34 -2
View File
@@ -12,6 +12,7 @@ import env from "env";
type Props = {
children: React.Node,
reloadOnChunkMissing?: boolean,
};
@observer
@@ -23,13 +24,25 @@ class ErrorBoundary extends React.Component<Props> {
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) {
window.Sentry.captureException(error);
}
}
handleReload = () => {
window.location.reload();
window.location.reload(true);
};
handleShowDetails = () => {
@@ -42,7 +55,26 @@ class ErrorBoundary extends React.Component<Props> {
render() {
if (this.error) {
const error = this.error;
const isReported = !!window.Sentry && env.DEPLOYMENT === "hosted";
const isChunkError = this.error.message.match(/chunk/);
if (isChunkError) {
return (
<CenteredContent>
<PageTitle title="Module failed to load" />
<h1>Loading Failed</h1>
<HelpText>
Sorry, part of the application failed to load. This may be because
it was updated since you opened the tab or because of a failed
network request. Please try reloading.
</HelpText>
<p>
<Button onClick={this.handleReload}>Reload</Button>
</p>
</CenteredContent>
);
}
return (
<CenteredContent>
@@ -53,7 +85,7 @@ class ErrorBoundary extends React.Component<Props> {
{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>}
{this.showDetails && <Pre>{error.toString()}</Pre>}
<p>
<Button onClick={this.handleReload}>Reload</Button>{" "}
{this.showDetails ? (
+16
View File
@@ -0,0 +1,16 @@
// @flow
import * as React from "react";
type Props = {
children: React.Node,
};
export default function EventBoundary({ children }: Props) {
const handleClick = React.useCallback((event: SyntheticEvent<>) => {
event.preventDefault();
event.stopPropagation();
}, []);
return <span onClick={handleClick}>{children}</span>;
}
+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
View File
@@ -4,6 +4,7 @@ import styled from "styled-components";
const Heading = styled.h1`
display: flex;
align-items: center;
${(props) => (props.centered ? "text-align: center;" : "")}
svg {
margin-left: -6px;
+6 -3
View File
@@ -18,6 +18,7 @@ function Highlight({
...rest
}: Props) {
let regex;
let index = 0;
if (highlight instanceof RegExp) {
regex = highlight;
} else {
@@ -29,8 +30,10 @@ function Highlight({
return (
<span {...rest}>
{highlight
? replace(text, regex, (tag, index) => (
<Mark key={index}>{processResult ? processResult(tag) : tag}</Mark>
? replace(text, regex, (tag) => (
<Mark key={index++}>
{processResult ? processResult(tag) : tag}
</Mark>
))
: text}
</span>
@@ -38,7 +41,7 @@ function Highlight({
}
const Mark = styled.mark`
background: ${(props) => props.theme.yellow};
background: ${(props) => props.theme.searchHighlight};
border-radius: 2px;
padding: 0 4px;
`;
+13 -8
View File
@@ -5,7 +5,7 @@ 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/parseDocumentIds";
import parseDocumentSlug from "shared/utils/parseDocumentSlug";
import DocumentsStore from "stores/DocumentsStore";
import HoverPreviewDocument from "components/HoverPreviewDocument";
import isInternalUrl from "utils/isInternalUrl";
@@ -20,13 +20,8 @@ type Props = {
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);
function HoverPreviewInternal({ node, documents, onClose, event }: Props) {
const slug = parseDocumentSlug(node.href);
const [isVisible, setVisible] = React.useState(false);
const timerClose = React.useRef();
@@ -131,6 +126,15 @@ function HoverPreview({ node, documents, onClose, event }: Props) {
);
}
function HoverPreview({ node, ...rest }: Props) {
// previews only work for internal doc links for now
if (!isInternalUrl(node.href)) {
return null;
}
return <HoverPreviewInternal {...rest} node={node} />;
}
const Animate = styled.div`
animation: ${fadeAndSlideIn} 150ms ease;
@@ -211,6 +215,7 @@ const Pointer = styled.div`
height: 22px;
position: absolute;
transform: translateX(-50%);
pointer-events: none;
&:before,
&:after {
+10 -8
View File
@@ -3,7 +3,7 @@ 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/parseDocumentIds";
import parseDocumentSlug from "shared/utils/parseDocumentSlug";
import DocumentsStore from "stores/DocumentsStore";
import DocumentMetaWithViews from "components/DocumentMetaWithViews";
import Editor from "components/Editor";
@@ -15,7 +15,7 @@ type Props = {
};
function HoverPreviewDocument({ url, documents, children }: Props) {
const slug = parseDocumentSlugFromUrl(url);
const slug = parseDocumentSlug(url);
documents.prefetchDocument(slug, {
prefetch: true,
@@ -29,12 +29,14 @@ function HoverPreviewDocument({ url, documents, children }: Props) {
<Heading>{document.titleWithDefault}</Heading>
<DocumentMetaWithViews isDraft={document.isDraft} document={document} />
<Editor
key={document.id}
defaultValue={document.getSummary()}
disableEmbeds
readOnly
/>
<React.Suspense fallback={<div />}>
<Editor
key={document.id}
defaultValue={document.getSummary()}
disableEmbeds
readOnly
/>
</React.Suspense>
</Content>
);
}
+19 -9
View File
@@ -22,13 +22,17 @@ import {
VehicleIcon,
} from "outline-icons";
import * as React from "react";
import { TwitterPicker } from "react-color";
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,
@@ -193,14 +197,16 @@ class IconPicker extends React.Component<Props> {
})}
</Icons>
<Flex onClick={preventEventBubble}>
<ColorPicker
color={this.props.color}
onChange={(color) =>
this.props.onChange(color.hex, this.props.icon)
}
colors={colors}
triangle="hide"
/>
<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>
@@ -226,6 +232,10 @@ const IconButton = styled(NudeButton)`
height: 30px;
`;
const Loading = styled(HelpText)`
padding: 16px;
`;
const ColorPicker = styled(TwitterPicker)`
box-shadow: none !important;
background: transparent !important;
-4
View File
@@ -33,10 +33,6 @@ const RealInput = styled.input`
&::placeholder {
color: ${(props) => props.theme.placeholder};
}
&::-webkit-search-cancel-button {
-webkit-appearance: searchfield-cancel-button;
}
`;
const Wrapper = styled.div`
+4 -26
View File
@@ -4,6 +4,8 @@ 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 = {
@@ -19,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;
};
@@ -31,36 +29,18 @@ class InputRich extends React.Component<Props> {
this.focused = true;
};
loadEditor = async () => {
try {
const EditorImport = await import("./Editor");
this.editorComponent = EditorImport.default;
} catch (err) {
if (err.message && err.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();
return;
}
throw err;
}
};
render() {
const { label, minHeight, maxHeight, ui, ...rest } = this.props;
const Editor = this.editorComponent;
return (
<>
<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}
@@ -68,9 +48,7 @@ class InputRich extends React.Component<Props> {
grow
{...rest}
/>
) : (
"Loading…"
)}
</React.Suspense>
</StyledOutline>
</>
);
+7 -2
View File
@@ -7,11 +7,13 @@ import keydown from "react-keydown";
import { withRouter, type RouterHistory } from "react-router-dom";
import styled, { withTheme } from "styled-components";
import Input from "./Input";
import { type Theme } from "types";
import { searchUrl } from "utils/routeHelpers";
type Props = {
history: RouterHistory,
theme: Object,
theme: Theme,
source: string,
placeholder?: string,
collectionId?: string,
};
@@ -33,7 +35,10 @@ class InputSearch extends React.Component<Props> {
handleSearchInput = (ev) => {
ev.preventDefault();
this.props.history.push(
searchUrl(ev.target.value, this.props.collectionId)
searchUrl(ev.target.value, {
collectionId: this.props.collectionId,
ref: this.props.source,
})
);
};
+8 -6
View File
@@ -21,6 +21,7 @@ import { LoadingIndicatorBar } from "components/LoadingIndicator";
import Modal from "components/Modal";
import Sidebar from "components/Sidebar";
import SettingsSidebar from "components/Sidebar/Settings";
import { type Theme } from "types";
import {
homeUrl,
searchUrl,
@@ -35,7 +36,7 @@ type Props = {
auth: AuthStore,
ui: UiStore,
notifications?: React.Node,
theme: Object,
theme: Theme,
};
@observer
@@ -44,21 +45,22 @@ class Layout extends React.Component<Props> {
@observable redirectTo: ?string;
@observable keyboardShortcutsOpen: boolean = false;
componentWillMount() {
this.updateBackground();
constructor(props) {
super();
this.updateBackground(props);
}
componentDidUpdate() {
this.updateBackground();
this.updateBackground(this.props);
if (this.redirectTo) {
this.redirectTo = undefined;
}
}
updateBackground() {
updateBackground(props) {
// ensure the wider page color always matches the theme
window.document.body.style.background = this.props.theme.background;
window.document.body.style.background = props.theme.background;
}
@keydown("shift+/")
@@ -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 -1
View File
@@ -17,7 +17,8 @@ class Mask extends React.Component<Props> {
return false;
}
componentWillMount() {
constructor() {
super();
this.width = randomInteger(75, 100);
}
+26 -18
View File
@@ -9,6 +9,7 @@ import breakpoint from "styled-components-breakpoint";
import { fadeAndScaleIn } from "shared/styles/animations";
import Flex from "components/Flex";
import NudeButton from "components/NudeButton";
import Scrollable from "components/Scrollable";
ReactModal.setAppElement("#root");
@@ -23,11 +24,12 @@ const GlobalStyles = createGlobalStyle`
.ReactModal__Overlay {
background-color: ${(props) =>
transparentize(0.25, props.theme.background)} !important;
z-index: 100;
z-index: ${(props) => props.theme.depths.modalOverlay};
}
${breakpoint("tablet")`
.ReactModalPortal + .ReactModalPortal {
.ReactModalPortal + .ReactModalPortal,
.ReactModalPortal + [data-react-modal-body-trap] + .ReactModalPortal {
.ReactModal__Overlay {
margin-left: 12px;
box-shadow: 0 -2px 10px ${(props) => props.theme.shadow};
@@ -36,13 +38,15 @@ const GlobalStyles = createGlobalStyle`
}
}
.ReactModalPortal + .ReactModalPortal + .ReactModalPortal {
.ReactModalPortal + .ReactModalPortal + .ReactModalPortal,
.ReactModalPortal + .ReactModalPortal + [data-react-modal-body-trap] + .ReactModalPortal {
.ReactModal__Overlay {
margin-left: 24px;
}
}
.ReactModalPortal + .ReactModalPortal + .ReactModalPortal + .ReactModalPortal {
.ReactModalPortal + .ReactModalPortal + .ReactModalPortal + .ReactModalPortal,
.ReactModalPortal + .ReactModalPortal + .ReactModalPortal + [data-react-modal-body-trap] + .ReactModalPortal {
.ReactModal__Overlay {
margin-left: 36px;
}
@@ -72,10 +76,11 @@ const Modal = ({
isOpen={isOpen}
{...rest}
>
<Content onClick={(ev) => ev.stopPropagation()} column>
{title && <h1>{title}</h1>}
{children}
<Content>
<Centered onClick={(ev) => ev.stopPropagation()} column>
{title && <h1>{title}</h1>}
{children}
</Centered>
</Content>
<Back onClick={onRequestClose}>
<BackIcon size={32} color="currentColor" />
@@ -89,10 +94,20 @@ const Modal = ({
);
};
const Content = styled(Flex)`
const Content = styled(Scrollable)`
width: 100%;
padding: 8vh 2rem 2rem;
${breakpoint("tablet")`
padding-top: 13vh;
`};
`;
const Centered = styled(Flex)`
width: 640px;
max-width: 100%;
position: relative;
margin: 0 auto;
`;
const StyledModal = styled(ReactModal)`
@@ -103,20 +118,13 @@ 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};
padding: 8vh 2rem 2rem;
outline: none;
${breakpoint("tablet")`
padding-top: 13vh;
`};
`;
const Text = styled.span`
@@ -147,7 +155,7 @@ const Close = styled(NudeButton)`
`;
const Back = styled(NudeButton)`
position: fixed;
position: absolute;
display: none;
align-items: center;
top: 2rem;
-8
View File
@@ -1,5 +1,4 @@
// @flow
import { lighten } from "polished";
import * as React from "react";
import styled from "styled-components";
@@ -11,13 +10,6 @@ const Button = styled.button`
line-height: 0;
border: 0;
padding: 0;
&:focus {
transition-duration: 0.05s;
box-shadow: ${(props) => lighten(0.4, props.theme.buttonBackground)} 0px 0px
0px 3px;
outline: none;
}
`;
export default React.forwardRef<any, typeof Button>((props, ref) => (
+4
View File
@@ -1,5 +1,6 @@
// @flow
import ArrowKeyNavigation from "boundless-arrow-key-navigation";
import { isEqual } from "lodash";
import { observable, action } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
@@ -40,6 +41,9 @@ class PaginatedList extends React.Component<Props> {
if (prevProps.fetch !== this.props.fetch) {
this.fetchResults();
}
if (!isEqual(prevProps.options, this.props.options)) {
this.fetchResults();
}
}
fetchResults = async () => {
+32 -8
View File
@@ -14,6 +14,7 @@ type Props = {
document?: ?Document,
collection: ?Collection,
onSuccess?: () => void,
style?: Object,
ref?: (?React.ElementRef<"div">) => void,
};
@@ -34,28 +35,38 @@ class PathToDocument extends React.Component<Props> {
};
render() {
const { result, collection, document, ref } = this.props;
const { result, collection, document, ref, style } = this.props;
const Component = document ? ResultWrapperLink : ResultWrapper;
if (!result) return <div />;
return (
<Component ref={ref} onClick={this.handleClick} href="" selectable>
<Component
ref={ref}
onClick={this.handleClick}
href=""
style={style}
role="option"
selectable
>
{collection && <CollectionIcon collection={collection} />}
&nbsp;
{result.path
.map((doc) => <Title key={doc.id}>{doc.title}</Title>)
.reduce((prev, curr) => [prev, <StyledGoToIcon />, curr])}
{document && (
<Flex>
<DocumentTitle>
{" "}
<StyledGoToIcon /> <Title>{document.title}</Title>
</Flex>
</DocumentTitle>
)}
</Component>
);
}
}
const DocumentTitle = styled(Flex)``;
const Title = styled.span`
white-space: nowrap;
overflow: hidden;
@@ -63,29 +74,42 @@ const Title = styled.span`
`;
const StyledGoToIcon = styled(GoToIcon)`
opacity: 0.25;
fill: ${(props) => props.theme.divider};
`;
const ResultWrapper = styled.div`
display: flex;
margin-bottom: 10px;
margin-left: -4px;
user-select: none;
color: ${(props) => props.theme.text};
cursor: default;
svg {
flex-shrink: 0;
}
`;
const ResultWrapperLink = styled(ResultWrapper.withComponent("a"))`
margin: 0 -8px;
padding: 8px 4px;
border-radius: 8px;
${DocumentTitle} {
display: none;
}
svg {
flex-shrink: 0;
}
&:hover,
&:active,
&:focus {
background: ${(props) => props.theme.listItemHoverBackground};
outline: none;
${DocumentTitle} {
display: flex;
}
}
`;
+1 -1
View File
@@ -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;
+2 -3
View File
@@ -69,7 +69,6 @@ class MainSidebar extends React.Component<Props> {
const { user, team } = auth;
if (!user || !team) return null;
const draftDocumentsCount = documents.drafts.length;
const can = policies.abilities(team.id);
return (
@@ -123,8 +122,8 @@ class MainSidebar extends React.Component<Props> {
label={
<Drafts align="center">
Drafts
{draftDocumentsCount > 0 && (
<Bubble count={draftDocumentsCount} />
{documents.totalDrafts > 0 && (
<Bubble count={documents.totalDrafts} />
)}
</Drafts>
}
+11 -15
View File
@@ -10,7 +10,6 @@ import {
GroupIcon,
LinkIcon,
TeamIcon,
BulletedListIcon,
ExpandedIcon,
} from "outline-icons";
import * as React from "react";
@@ -31,6 +30,8 @@ import SlackIcon from "./icons/Slack";
import ZapierIcon from "./icons/Zapier";
import env from "env";
const isHosted = env.DEPLOYMENT === "hosted";
type Props = {
history: RouterHistory,
policies: PoliciesStore,
@@ -40,7 +41,7 @@ type Props = {
@observer
class SettingsSidebar extends React.Component<Props> {
returnToDashboard = () => {
this.props.history.push("/");
this.props.history.push("/home");
};
render() {
@@ -116,13 +117,6 @@ class SettingsSidebar extends React.Component<Props> {
icon={<LinkIcon color="currentColor" />}
label="Share Links"
/>
{can.auditLog && (
<SidebarLink
to="/settings/events"
icon={<BulletedListIcon color="currentColor" />}
label="Audit Log"
/>
)}
{can.export && (
<SidebarLink
to="/settings/export"
@@ -139,14 +133,16 @@ class SettingsSidebar extends React.Component<Props> {
icon={<SlackIcon color="currentColor" />}
label="Slack"
/>
<SidebarLink
to="/settings/integrations/zapier"
icon={<ZapierIcon color="currentColor" />}
label="Zapier"
/>
{isHosted && (
<SidebarLink
to="/settings/integrations/zapier"
icon={<ZapierIcon color="currentColor" />}
label="Zapier"
/>
)}
</Section>
)}
{can.update && env.DEPLOYMENT !== "hosted" && (
{can.update && !isHosted && (
<Section>
<Header>Installation</Header>
<Version />
+36 -41
View File
@@ -1,65 +1,60 @@
// @flow
import { observer, inject } from "mobx-react";
import { observer } from "mobx-react";
import { CloseIcon, MenuIcon } from "outline-icons";
import * as React from "react";
import { withRouter } from "react-router-dom";
import type { Location } from "react-router-dom";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import UiStore from "stores/UiStore";
import Fade from "components/Fade";
import Flex from "components/Flex";
import usePrevious from "hooks/usePrevious";
import useStores from "hooks/useStores";
let firstRender = true;
type Props = {
children: React.Node,
location: Location,
ui: UiStore,
};
@observer
class Sidebar extends React.Component<Props> {
componentWillReceiveProps = (nextProps: Props) => {
if (this.props.location !== nextProps.location) {
this.props.ui.hideMobileSidebar();
function Sidebar({ location, children }: Props) {
const { ui } = useStores();
const previousLocation = usePrevious(location);
React.useEffect(() => {
if (location !== previousLocation) {
ui.hideMobileSidebar();
}
};
}, [ui, location]);
toggleSidebar = () => {
this.props.ui.toggleMobileSidebar();
};
render() {
const { children, ui } = this.props;
const content = (
<Container
editMode={ui.editMode}
const content = (
<Container
editMode={ui.editMode}
mobileSidebarVisible={ui.mobileSidebarVisible}
column
>
<Toggle
onClick={ui.toggleMobileSidebar}
mobileSidebarVisible={ui.mobileSidebarVisible}
column
>
<Toggle
onClick={this.toggleSidebar}
mobileSidebarVisible={ui.mobileSidebarVisible}
>
{ui.mobileSidebarVisible ? (
<CloseIcon size={32} />
) : (
<MenuIcon size={32} />
)}
</Toggle>
{children}
</Container>
);
{ui.mobileSidebarVisible ? (
<CloseIcon size={32} />
) : (
<MenuIcon size={32} />
)}
</Toggle>
{children}
</Container>
);
// Fade in the sidebar on first render after page load
if (firstRender) {
firstRender = false;
return <Fade>{content}</Fade>;
}
return content;
// Fade in the sidebar on first render after page load
if (firstRender) {
firstRender = false;
return <Fade>{content}</Fade>;
}
return content;
}
const Container = styled(Flex)`
@@ -71,7 +66,7 @@ const Container = styled(Flex)`
transition: left 100ms ease-out,
${(props) => props.theme.backgroundTransition};
margin-left: ${(props) => (props.mobileSidebarVisible ? 0 : "-100%")};
z-index: 1000;
z-index: ${(props) => props.theme.depths.sidebar};
@media print {
display: none;
@@ -117,4 +112,4 @@ const Toggle = styled.a`
`};
`;
export default withRouter(inject("ui")(Sidebar));
export default withRouter(observer(Sidebar));
@@ -10,27 +10,34 @@ import CollectionIcon from "components/CollectionIcon";
import DropToImport from "components/DropToImport";
import Flex from "components/Flex";
import DocumentLink from "./DocumentLink";
import EditableTitle from "./EditableTitle";
import SidebarLink from "./SidebarLink";
import CollectionMenu from "menus/CollectionMenu";
type Props = {
type Props = {|
collection: Collection,
ui: UiStore,
canUpdate: boolean,
documents: DocumentsStore,
activeDocument: ?Document,
prefetchDocument: (id: string) => Promise<void>,
};
|};
@observer
class CollectionLink extends React.Component<Props> {
@observable menuOpen = false;
handleTitleChange = async (name: string) => {
await this.props.collection.save({ name });
};
render() {
const {
collection,
documents,
activeDocument,
prefetchDocument,
canUpdate,
ui,
} = this.props;
const expanded = collection.id === ui.activeCollectionId;
@@ -49,7 +56,13 @@ class CollectionLink extends React.Component<Props> {
expanded={expanded}
hideDisclosure
menuOpen={this.menuOpen}
label={collection.name}
label={
<EditableTitle
title={collection.name}
onSubmit={this.handleTitleChange}
canUpdate={canUpdate}
/>
}
exact={false}
menu={
<CollectionMenu
@@ -69,6 +82,7 @@ class CollectionLink extends React.Component<Props> {
collection={collection}
activeDocument={activeDocument}
prefetchDocument={prefetchDocument}
canUpdate={canUpdate}
depth={1.5}
/>
))}
@@ -52,7 +52,7 @@ class Collections extends React.Component<Props> {
}
render() {
const { collections, ui, documents } = this.props;
const { collections, ui, policies, documents } = this.props;
const content = (
<>
@@ -63,6 +63,7 @@ class Collections extends React.Component<Props> {
collection={collection}
activeDocument={documents.active}
prefetchDocument={documents.prefetchDocument}
canUpdate={policies.abilities(collection.id).update}
ui={ui}
/>
))}
@@ -9,19 +9,21 @@ import Document from "models/Document";
import DropToImport from "components/DropToImport";
import Fade from "components/Fade";
import Flex from "components/Flex";
import EditableTitle from "./EditableTitle";
import SidebarLink from "./SidebarLink";
import DocumentMenu from "menus/DocumentMenu";
import { type NavigationNode } from "types";
type Props = {
type Props = {|
node: NavigationNode,
documents: DocumentsStore,
canUpdate: boolean,
collection?: Collection,
activeDocument: ?Document,
activeDocumentRef?: (?HTMLElement) => void,
prefetchDocument: (documentId: string) => Promise<void>,
depth: number,
};
|};
@observer
class DocumentLink extends React.Component<Props> {
@@ -49,6 +51,18 @@ class DocumentLink extends React.Component<Props> {
prefetchDocument(node.id);
};
handleTitleChange = async (title: string) => {
const document = this.props.documents.get(this.props.node.id);
if (!document) return;
await this.props.documents.update({
id: document.id,
lastRevision: document.revision,
text: document.text,
title,
});
};
isActiveDocument = () => {
return (
this.props.activeDocument &&
@@ -69,6 +83,7 @@ class DocumentLink extends React.Component<Props> {
activeDocumentRef,
prefetchDocument,
depth,
canUpdate,
} = this.props;
const showChildren = !!(
@@ -81,6 +96,7 @@ class DocumentLink extends React.Component<Props> {
this.isActiveDocument())
);
const document = documents.get(node.id);
const title = node.title || "Untitled";
return (
<Flex
@@ -96,7 +112,13 @@ class DocumentLink extends React.Component<Props> {
state: { title: node.title },
}}
expanded={showChildren ? true : undefined}
label={node.title || "Untitled"}
label={
<EditableTitle
title={title}
onSubmit={this.handleTitleChange}
canUpdate={canUpdate}
/>
}
depth={depth}
exact={false}
menuOpen={this.menuOpen}
@@ -124,6 +146,7 @@ class DocumentLink extends React.Component<Props> {
activeDocument={activeDocument}
prefetchDocument={prefetchDocument}
depth={depth + 1}
canUpdate={canUpdate}
/>
))}
</DocumentChildren>
@@ -0,0 +1,98 @@
// @flow
import * as React from "react";
import styled from "styled-components";
import useStores from "hooks/useStores";
type Props = {|
onSubmit: (title: string) => Promise<void>,
title: string,
canUpdate: boolean,
|};
function EditableTitle({ title, onSubmit, canUpdate }: Props) {
const [isEditing, setIsEditing] = React.useState(false);
const [originalValue, setOriginalValue] = React.useState(title);
const [value, setValue] = React.useState(title);
const { ui } = useStores();
React.useEffect(() => {
setValue(title);
}, [title]);
const handleChange = React.useCallback((event) => {
setValue(event.target.value);
}, []);
const handleDoubleClick = React.useCallback((event) => {
event.preventDefault();
event.stopPropagation();
setIsEditing(true);
}, []);
const handleKeyDown = React.useCallback(
(event) => {
if (event.key === "Escape") {
setIsEditing(false);
setValue(originalValue);
}
},
[originalValue]
);
const handleSave = React.useCallback(async () => {
setIsEditing(false);
if (value === originalValue) {
return;
}
if (document) {
try {
await onSubmit(value);
setOriginalValue(value);
} catch (error) {
setValue(originalValue);
ui.showToast(error.message);
throw error;
}
}
}, [ui, originalValue, value, onSubmit]);
return (
<>
{isEditing ? (
<form onSubmit={handleSave}>
<Input
type="text"
value={value}
onKeyDown={handleKeyDown}
onChange={handleChange}
onBlur={handleSave}
autoFocus
/>
</form>
) : (
<span onDoubleClick={canUpdate ? handleDoubleClick : undefined}>
{value}
</span>
)}
</>
);
}
const Input = styled.input`
margin-left: -4px;
background: ${(props) => props.theme.background};
width: calc(100% - 10px);
border-radius: 3px;
border: 1px solid ${(props) => props.theme.inputBorderFocused};
padding: 5px 6px;
margin: -4px;
height: 32px;
&:focus {
outline-color: ${(props) => props.theme.primary};
}
`;
export default EditableTitle;
@@ -1,16 +1,15 @@
// @flow
import { ExpandedIcon } from "outline-icons";
import * as React from "react";
import styled, { withTheme } from "styled-components";
import styled from "styled-components";
import Flex from "components/Flex";
import TeamLogo from "components/TeamLogo";
type Props = {
teamName: string,
subheading: string,
subheading: React.Node,
showDisclosure?: boolean,
logoUrl: string,
theme: Object,
};
function HeaderBlock({
@@ -18,16 +17,15 @@ function HeaderBlock({
teamName,
subheading,
logoUrl,
theme,
...rest
}: Props) {
return (
<Header justify="flex-start" align="center" {...rest}>
<TeamLogo alt={`${teamName} logo`} src={logoUrl} />
<TeamLogo alt={`${teamName} logo`} src={logoUrl} size="38px" />
<Flex align="flex-start" column>
<TeamName showDisclosure>
{teamName}{" "}
{showDisclosure && <StyledExpandedIcon color={theme.text} />}
{showDisclosure && <StyledExpandedIcon color="currentColor" />}
</TeamName>
<Subheading>{subheading}</Subheading>
</Flex>
@@ -59,10 +57,16 @@ const TeamName = styled.div`
font-size: 16px;
`;
const Header = styled(Flex)`
const Header = styled.button`
display: flex;
align-items: center;
flex-shrink: 0;
padding: 16px 24px;
position: relative;
background: none;
line-height: inherit;
border: 0;
margin: 0;
cursor: pointer;
width: 100%;
@@ -73,4 +77,4 @@ const Header = styled(Flex)`
}
`;
export default withTheme(HeaderBlock);
export default HeaderBlock;
@@ -1,11 +1,11 @@
// @flow
import { observable, action } from "mobx";
import { observer } from "mobx-react";
import { CollapsedIcon } from "outline-icons";
import * as React from "react";
import { withRouter, NavLink } from "react-router-dom";
import styled, { withTheme } from "styled-components";
import Flex from "components/Flex";
import { type Theme } from "types";
type Props = {
to?: string | Object,
@@ -20,84 +20,85 @@ type Props = {
hideDisclosure?: boolean,
iconColor?: string,
active?: boolean,
theme: Object,
theme: Theme,
exact?: boolean,
depth?: number,
};
@observer
class SidebarLink extends React.Component<Props> {
@observable expanded: ?boolean = this.props.expanded;
function SidebarLink({
icon,
children,
onClick,
to,
label,
active,
menu,
menuOpen,
hideDisclosure,
theme,
exact,
href,
depth,
...rest
}: Props) {
const [expanded, setExpanded] = React.useState(rest.expanded);
style = {
paddingLeft: `${(this.props.depth || 0) * 16 + 16}px`,
};
componentWillReceiveProps(nextProps: Props) {
if (nextProps.expanded !== undefined) {
this.expanded = nextProps.expanded;
}
}
@action
handleClick = (ev: SyntheticEvent<>) => {
ev.preventDefault();
ev.stopPropagation();
this.expanded = !this.expanded;
};
@action
handleExpand = () => {
this.expanded = true;
};
render() {
const {
icon,
children,
onClick,
to,
label,
active,
menu,
menuOpen,
hideDisclosure,
exact,
href,
} = this.props;
const showDisclosure = !!children && !hideDisclosure;
const activeStyle = {
color: this.props.theme.text,
background: this.props.theme.sidebarItemBackground,
fontWeight: 600,
...this.style,
const style = React.useMemo(() => {
return {
paddingLeft: `${(depth || 0) * 16 + 16}px`,
};
}, [depth]);
return (
<Wrapper column>
<StyledNavLink
activeStyle={activeStyle}
style={active ? activeStyle : this.style}
onClick={onClick}
exact={exact !== false}
to={to}
as={to ? undefined : href ? "a" : "div"}
href={href}
>
{icon && <IconWrapper>{icon}</IconWrapper>}
<Label onClick={this.handleExpand}>
{showDisclosure && (
<Disclosure expanded={this.expanded} onClick={this.handleClick} />
)}
{label}
</Label>
{menu && <Action menuOpen={menuOpen}>{menu}</Action>}
</StyledNavLink>
{this.expanded && children}
</Wrapper>
);
}
React.useEffect(() => {
if (rest.expanded !== undefined) {
setExpanded(rest.expanded);
}
}, [rest.expanded]);
const handleClick = React.useCallback(
(ev: SyntheticEvent<>) => {
ev.preventDefault();
ev.stopPropagation();
setExpanded(!expanded);
},
[expanded]
);
const handleExpand = React.useCallback(() => {
setExpanded(true);
}, []);
const showDisclosure = !!children && !hideDisclosure;
const activeStyle = {
color: theme.text,
background: theme.sidebarItemBackground,
fontWeight: 600,
...style,
};
return (
<Wrapper column>
<StyledNavLink
activeStyle={activeStyle}
style={active ? activeStyle : style}
onClick={onClick}
exact={exact !== false}
to={to}
as={to ? undefined : href ? "a" : "div"}
href={href}
>
{icon && <IconWrapper>{icon}</IconWrapper>}
<Label onClick={handleExpand}>
{showDisclosure && (
<Disclosure expanded={expanded} onClick={handleClick} />
)}
{label}
</Label>
{menu && <Action menuOpen={menuOpen}>{menu}</Action>}
</StyledNavLink>
{expanded && children}
</Wrapper>
);
}
// accounts for whitespace around icon
@@ -143,7 +144,6 @@ const StyledNavLink = styled(NavLink)`
&:focus {
color: ${(props) => props.theme.text};
background: ${(props) => props.theme.black05};
outline: none;
}
&:hover {
@@ -171,4 +171,4 @@ const Disclosure = styled(CollapsedIcon)`
${({ expanded }) => !expanded && "transform: rotate(-90deg);"};
`;
export default withRouter(withTheme(SidebarLink));
export default withRouter(withTheme(observer(SidebarLink)));
+57 -16
View File
@@ -3,7 +3,7 @@ import { find } from "lodash";
import { observable } from "mobx";
import { inject, observer } from "mobx-react";
import * as React from "react";
import io from "socket.io-client";
import io, { Socket } from "socket.io-client";
import AuthStore from "stores/AuthStore";
import CollectionsStore from "stores/CollectionsStore";
import DocumentPresenceStore from "stores/DocumentPresenceStore";
@@ -13,6 +13,7 @@ import MembershipsStore from "stores/MembershipsStore";
import PoliciesStore from "stores/PoliciesStore";
import UiStore from "stores/UiStore";
import ViewsStore from "stores/ViewsStore";
import { getVisibilityListener, getPageVisible } from "utils/pageVisibility";
export const SocketContext: any = React.createContext();
@@ -31,12 +32,42 @@ type Props = {
@observer
class SocketProvider extends React.Component<Props> {
@observable socket;
@observable socket: Socket;
componentDidMount() {
this.createConnection();
document.addEventListener(getVisibilityListener(), this.checkConnection);
}
componentWillUnmount() {
if (this.socket) {
this.socket.authenticated = false;
this.socket.disconnect();
}
document.removeEventListener(getVisibilityListener(), this.checkConnection);
}
checkConnection = () => {
if (this.socket && this.socket.disconnected && getPageVisible()) {
// null-ifying this reference is important, do not remove. Without it
// references to old sockets are potentially held in context
this.socket.close();
this.socket = null;
this.createConnection();
}
};
createConnection = () => {
this.socket = io(window.location.origin, {
path: "/realtime",
transports: ["websocket"],
reconnectionDelay: 1000,
reconnectionDelayMax: 30000,
});
this.socket.authenticated = false;
const {
@@ -65,12 +96,12 @@ class SocketProvider extends React.Component<Props> {
// when the socket is disconnected we need to clear all presence state as
// it's no longer reliable.
presence.clear();
});
if (reason === "io server disconnect") {
// the disconnection was initiated by the server, need to reconnect
// manually, else the socket will automatically try to reconnect
this.socket.connect();
}
// on reconnection, reset the transports option, as the Websocket
// connection may have failed (caused by proxy, firewall, browser, ...)
this.socket.on("reconnect_attempt", () => {
this.socket.io.opts.transports = ["polling", "websocket"];
});
this.socket.on("authenticated", () => {
@@ -94,6 +125,8 @@ class SocketProvider extends React.Component<Props> {
if (document) {
document.deletedAt = documentDescriptor.updatedAt;
}
policies.remove(documentId);
continue;
}
@@ -141,7 +174,21 @@ class SocketProvider extends React.Component<Props> {
const collection = collections.get(collectionId) || {};
if (event.event === "collections.delete") {
const collection = collections.get(collectionId);
if (collection) {
collection.deletedAt = collectionDescriptor.updatedAt;
}
const deletedDocuments = documents.inCollection(collectionId);
deletedDocuments.forEach((doc) => {
doc.deletedAt = collectionDescriptor.updatedAt;
policies.remove(doc.id);
});
documents.removeCollectionDocuments(collectionId);
memberships.removeCollectionMemberships(collectionId);
collections.remove(collectionId);
policies.remove(collectionId);
continue;
}
@@ -156,9 +203,10 @@ class SocketProvider extends React.Component<Props> {
await collections.fetch(collectionId, { force: true });
} catch (err) {
if (err.statusCode === 404 || err.statusCode === 403) {
collections.remove(collectionId);
documents.removeCollectionDocuments(collectionId);
memberships.removeCollectionMemberships(collectionId);
collections.remove(collectionId);
policies.remove(collectionId);
return;
}
}
@@ -260,14 +308,7 @@ class SocketProvider extends React.Component<Props> {
this.socket.on("user.presence", (event) => {
presence.touch(event.documentId, event.userId, event.isEditing);
});
}
componentWillUnmount() {
if (this.socket) {
this.socket.disconnect();
this.socket.authenticated = false;
}
}
};
render() {
return (
+2 -3
View File
@@ -11,12 +11,11 @@ const H3 = styled.h3`
margin-top: 22px;
margin-bottom: 12px;
line-height: 1;
position: relative;
`;
const Underline = styled("span")`
position: relative;
top: 1px;
margin-top: -1px;
display: inline-block;
font-weight: 500;
font-size: 14px;
+2 -10
View File
@@ -1,16 +1,15 @@
// @flow
import { lighten } from "polished";
import * as React from "react";
import { NavLink } from "react-router-dom";
import styled, { withTheme } from "styled-components";
import { type Theme } from "types";
type Props = {
theme: Object,
theme: Theme,
};
const StyledNavLink = styled(NavLink)`
position: relative;
bottom: -1px;
display: inline-block;
font-weight: 500;
@@ -24,13 +23,6 @@ const StyledNavLink = styled(NavLink)`
border-bottom: 3px solid ${(props) => props.theme.divider};
padding-bottom: 5px;
}
&:focus {
outline: none;
border-bottom: 3px solid
${(props) => lighten(0.4, props.theme.buttonBackground)};
padding-bottom: 5px;
}
`;
function Tab({ theme, ...rest }: Props) {
+2
View File
@@ -6,6 +6,8 @@ const Tabs = styled.nav`
border-bottom: 1px solid ${(props) => props.theme.divider};
margin-top: 22px;
margin-bottom: 12px;
overflow-y: auto;
white-space: nowrap;
`;
export const Separator = styled.span`
+3 -2
View File
@@ -2,11 +2,12 @@
import styled from "styled-components";
const TeamLogo = styled.img`
width: auto;
height: 38px;
width: ${(props) => props.size || "auto"};
height: ${(props) => props.size || "38px"};
border-radius: 4px;
background: ${(props) => props.theme.background};
border: 1px solid ${(props) => props.theme.divider};
overflow: hidden;
`;
export default TeamLogo;
+17 -1
View File
@@ -23,6 +23,9 @@ function eachMinute(fn) {
type Props = {
dateTime: string,
children?: React.Node,
tooltipDelay?: number,
addSuffix?: boolean,
shorten?: boolean,
};
class Time extends React.Component<Props> {
@@ -39,13 +42,26 @@ class Time extends React.Component<Props> {
}
render() {
const { shorten, addSuffix } = this.props;
let content = distanceInWordsToNow(this.props.dateTime, {
addSuffix,
});
if (shorten) {
content = content
.replace("about", "")
.replace("less than a minute ago", "just now")
.replace("minute", "min");
}
return (
<Tooltip
tooltip={format(this.props.dateTime, "MMMM Do, YYYY h:mm a")}
delay={this.props.tooltipDelay}
placement="bottom"
>
<time dateTime={this.props.dateTime}>
{this.props.children || distanceInWordsToNow(this.props.dateTime)}
{this.props.children || content}
</time>
</Tooltip>
);
+1 -1
View File
@@ -34,7 +34,7 @@ const List = styled.ol`
list-style: none;
margin: 0;
padding: 0;
z-index: 1000;
z-index: ${(props) => props.theme.depths.toasts};
`;
export default inject("ui")(Toasts);
+1
View File
@@ -21,6 +21,7 @@ export default class Abstract extends React.Component<Props> {
return (
<Frame
{...this.props}
src={`https://app.goabstract.com/embed/${shareId}`}
title={`Abstract (${shareId})`}
/>
+1
View File
@@ -20,6 +20,7 @@ export default class Airtable extends React.Component<Props> {
return (
<Frame
{...this.props}
src={`https://airtable.com/embed/${shareId}`}
title={`Airtable (${shareId})`}
border
+28
View File
@@ -0,0 +1,28 @@
// @flow
import * as React from "react";
import Frame from "./components/Frame";
const URL_REGEX = new RegExp(
"^https?://share.clickup.com/[a-z]/[a-z]/(.*)/(.*)$"
);
type Props = {|
attrs: {|
href: string,
matches: string[],
|},
|};
export default class ClickUp extends React.Component<Props> {
static ENABLED = [URL_REGEX];
render() {
return (
<Frame
{...this.props}
src={this.props.attrs.href}
title="ClickUp Embed"
/>
);
}
}
+17
View File
@@ -0,0 +1,17 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import ClickUp from "./ClickUp";
describe("ClickUp", () => {
const match = ClickUp.ENABLED[0];
test("to be enabled on share link", () => {
expect(
"https://share.clickup.com/b/h/6-9310960-2/c9d837d74182317".match(match)
).toBeTruthy();
});
test("to not be enabled elsewhere", () => {
expect("https://share.clickup.com".match(match)).toBe(null);
expect("https://clickup.com/".match(match)).toBe(null);
expect("https://clickup.com/features".match(match)).toBe(null);
});
});
+1 -1
View File
@@ -17,6 +17,6 @@ export default class Codepen extends React.Component<Props> {
render() {
const normalizedUrl = this.props.attrs.href.replace(/\/pen\//, "/embed/");
return <Frame src={normalizedUrl} title="Codepen Embed" />;
return <Frame {...this.props} src={normalizedUrl} title="Codepen Embed" />;
}
}
+1
View File
@@ -19,6 +19,7 @@ export default class Figma extends React.Component<Props> {
render() {
return (
<Frame
{...this.props}
src={`https://www.figma.com/embed?embed_host=outline&url=${this.props.attrs.href}`}
title="Figma Embed"
border
+8 -1
View File
@@ -15,6 +15,13 @@ export default class Framer extends React.Component<Props> {
static ENABLED = [URL_REGEX];
render() {
return <Frame src={this.props.attrs.href} title="Framer Embed" border />;
return (
<Frame
{...this.props}
src={this.props.attrs.href}
title="Framer Embed"
border
/>
);
}
}
+3 -1
View File
@@ -2,10 +2,11 @@
import * as React from "react";
const URL_REGEX = new RegExp(
"^https://gist.github.com/([a-zd](?:[a-zd]|-(?=[a-zd])){0,38})/(.*)$"
"^https://gist.github.com/([a-z\\d](?:[a-z\\d]|-(?=[a-z\\d])){0,38})/(.*)$"
);
type Props = {|
isSelected: boolean,
attrs: {|
href: string,
matches: string[],
@@ -48,6 +49,7 @@ class Gist extends React.Component<Props> {
return (
<iframe
className={this.props.isSelected ? "ProseMirror-selectednode" : ""}
ref={this.updateIframeContent}
type="text/html"
frameBorder="0"
+6
View File
@@ -9,6 +9,12 @@ describe("Gist", () => {
match
)
).toBeTruthy();
expect(
"https://gist.github.com/n3n/eb51ada6308b539d388c8ff97711adfa".match(
match
)
).toBeTruthy();
});
test("to not be enabled elsewhere", () => {
+16 -4
View File
@@ -2,9 +2,7 @@
import * as React from "react";
import Frame from "./components/Frame";
const URL_REGEX = new RegExp(
"^https?://docs.google.com/document/d/(.*)/pub(.*)$"
);
const URL_REGEX = new RegExp("^https?://docs.google.com/document/(.*)$");
type Props = {|
attrs: {|
@@ -18,7 +16,21 @@ export default class GoogleDocs extends React.Component<Props> {
render() {
return (
<Frame src={this.props.attrs.href} title="Google Docs Embed" border />
<Frame
{...this.props}
src={this.props.attrs.href.replace("/edit", "/preview")}
icon={
<img
src="/images/google-docs.png"
alt="Google Docs Icon"
width={16}
height={16}
/>
}
canonicalUrl={this.props.attrs.href}
title="Google Docs"
border
/>
);
}
}
+10 -5
View File
@@ -14,14 +14,19 @@ describe("GoogleDocs", () => {
match
)
).toBeTruthy();
expect(
"https://docs.google.com/document/d/1SsDfWzFFTjZM2LanvpyUzjKhqVQpwpTMeiPeYxhVqOg/edit".match(
match
)
).toBeTruthy();
expect(
"https://docs.google.com/document/d/1SsDfWzFFTjZM2LanvpyUzjKhqVQpwpTMeiPeYxhVqOg/preview".match(
match
)
).toBeTruthy();
});
test("to not be enabled elsewhere", () => {
expect(
"https://docs.google.com/document/d/e/2PACX-1vTdddHPoZ5M_47wmSHCoigR/edit".match(
match
)
).toBe(null);
expect("https://docs.google.com/document".match(match)).toBe(null);
expect("https://docs.google.com".match(match)).toBe(null);
expect("https://www.google.com".match(match)).toBe(null);
+16 -4
View File
@@ -2,9 +2,7 @@
import * as React from "react";
import Frame from "./components/Frame";
const URL_REGEX = new RegExp(
"^https?://docs.google.com/spreadsheets/d/(.*)/pub(.*)$"
);
const URL_REGEX = new RegExp("^https?://docs.google.com/spreadsheets/d/(.*)$");
type Props = {|
attrs: {|
@@ -18,7 +16,21 @@ export default class GoogleSlides extends React.Component<Props> {
render() {
return (
<Frame src={this.props.attrs.href} title="Google Sheets Embed" border />
<Frame
{...this.props}
src={this.props.attrs.href.replace("/edit", "/preview")}
icon={
<img
src="/images/google-sheets.png"
alt="Google Sheets Icon"
width={16}
height={16}
/>
}
canonicalUrl={this.props.attrs.href}
title="Google Sheets"
border
/>
);
}
}
+4 -4
View File
@@ -9,14 +9,14 @@ describe("GoogleSheets", () => {
match
)
).toBeTruthy();
});
test("to not be enabled elsewhere", () => {
expect(
"https://docs.google.com/spreadsheets/d/e/2PACX-1vTdddHPoZ5M_47wmSHCoigR/edit".match(
match
)
).toBe(null);
).toBeTruthy();
});
test("to not be enabled elsewhere", () => {
expect("https://docs.google.com/spreadsheets".match(match)).toBe(null);
expect("https://docs.google.com".match(match)).toBe(null);
expect("https://www.google.com".match(match)).toBe(null);
+15 -5
View File
@@ -2,9 +2,7 @@
import * as React from "react";
import Frame from "./components/Frame";
const URL_REGEX = new RegExp(
"^https?://docs.google.com/presentation/d/(.*)/pub(.*)$"
);
const URL_REGEX = new RegExp("^https?://docs.google.com/presentation/d/(.*)$");
type Props = {|
attrs: {|
@@ -19,8 +17,20 @@ export default class GoogleSlides extends React.Component<Props> {
render() {
return (
<Frame
src={this.props.attrs.href.replace("/pub", "/embed")}
title="Google Slides Embed"
{...this.props}
src={this.props.attrs.href
.replace("/edit", "/preview")
.replace("/pub", "/embed")}
icon={
<img
src="/images/google-slides.png"
alt="Google Slides Icon"
width={16}
height={16}
/>
}
canonicalUrl={this.props.attrs.href}
title="Google Slides"
border
/>
);
+4 -4
View File
@@ -14,14 +14,14 @@ describe("GoogleSlides", () => {
match
)
).toBeTruthy();
});
test("to not be enabled elsewhere", () => {
expect(
"https://docs.google.com/presentation/d/e/2PACX-1vTdddHPoZ5M_47wmSHCoigR/edit".match(
match
)
).toBe(null);
).toBeTruthy();
});
test("to not be enabled elsewhere", () => {
expect("https://docs.google.com/presentation".match(match)).toBe(null);
expect("https://docs.google.com".match(match)).toBe(null);
expect("https://www.google.com".match(match)).toBe(null);
+9 -1
View File
@@ -12,6 +12,7 @@ const IMAGE_REGEX = new RegExp(
);
type Props = {|
isSelected: boolean,
attrs: {|
href: string,
matches: string[],
@@ -25,6 +26,7 @@ export default class InVision extends React.Component<Props> {
if (IMAGE_REGEX.test(this.props.attrs.href)) {
return (
<ImageZoom
className={this.props.isSelected ? "ProseMirror-selectednode" : ""}
image={{
src: this.props.attrs.href,
alt: "InVision Embed",
@@ -37,6 +39,12 @@ export default class InVision extends React.Component<Props> {
/>
);
}
return <Frame src={this.props.attrs.href} title="InVision Embed" />;
return (
<Frame
{...this.props}
src={this.props.attrs.href}
title="InVision Embed"
/>
);
}
}
+1 -1
View File
@@ -17,6 +17,6 @@ export default class Loom extends React.Component<Props> {
render() {
const normalizedUrl = this.props.attrs.href.replace("share", "embed");
return <Frame src={normalizedUrl} title="Loom Embed" />;
return <Frame {...this.props} src={normalizedUrl} title="Loom Embed" />;
}
}
+1
View File
@@ -20,6 +20,7 @@ export default class Lucidchart extends React.Component<Props> {
return (
<Frame
{...this.props}
src={`https://lucidchart.com/documents/embeddedchart/${chartId}`}
title="Lucidchart Embed"
/>
+8 -1
View File
@@ -15,6 +15,13 @@ export default class Marvel extends React.Component<Props> {
static ENABLED = [URL_REGEX];
render() {
return <Frame src={this.props.attrs.href} title="Marvel Embed" border />;
return (
<Frame
{...this.props}
src={this.props.attrs.href}
title="Marvel Embed"
border
/>
);
}
}
+1
View File
@@ -21,6 +21,7 @@ export default class Mindmeister extends React.Component<Props> {
return (
<Frame
{...this.props}
src={`https://www.mindmeister.com/maps/public_map_shell/${chartId}`}
title="Mindmeister Embed"
border
+1
View File
@@ -20,6 +20,7 @@ export default class RealtimeBoard extends React.Component<Props> {
return (
<Frame
{...this.props}
src={`https://realtimeboard.com/app/embed/${boardId}`}
title={`RealtimeBoard (${boardId})`}
/>
+5 -1
View File
@@ -21,7 +21,11 @@ export default class ModeAnalytics extends React.Component<Props> {
const normalizedUrl = this.props.attrs.href.replace(/\/embed$/, "");
return (
<Frame src={`${normalizedUrl}/embed`} title="Mode Analytics Embed" />
<Frame
{...this.props}
src={`${normalizedUrl}/embed`}
title="Mode Analytics Embed"
/>
);
}
}
+3 -1
View File
@@ -17,6 +17,8 @@ export default class Prezi extends React.Component<Props> {
render() {
const url = this.props.attrs.href.replace(/\/embed$/, "");
return <Frame src={`${url}/embed`} title="Prezi Embed" border />;
return (
<Frame {...this.props} src={`${url}/embed`} title="Prezi Embed" border />
);
}
}
+13 -2
View File
@@ -25,10 +25,21 @@ export default class Spotify extends React.Component<Props> {
render() {
const normalizedPath = this.pathname.replace(/^\/embed/, "/");
var height;
if (normalizedPath.includes("episode") || normalizedPath.includes("show")) {
height = 232;
} else if (normalizedPath.includes("track")) {
height = 80;
} else {
height = 380;
}
return (
<Frame
width="300px"
height="380px"
{...this.props}
width="100%"
height={`${height}px`}
src={`https://open.spotify.com/embed${normalizedPath}`}
title="Spotify Embed"
allow="encrypted-media"
+1
View File
@@ -31,6 +31,7 @@ export default class Trello extends React.Component<Props> {
return (
<Frame
{...this.props}
width="248px"
height="185px"
src={`https://trello.com/embed/board?id=${objectId}`}
+7 -1
View File
@@ -17,6 +17,12 @@ export default class Typeform extends React.Component<Props> {
static ENABLED = [URL_REGEX];
render() {
return <Frame src={this.props.attrs.href} title="Typeform Embed" />;
return (
<Frame
{...this.props}
src={this.props.attrs.href}
title="Typeform Embed"
/>
);
}
}
+1
View File
@@ -20,6 +20,7 @@ export default class Vimeo extends React.Component<Props> {
return (
<Frame
{...this.props}
src={`https://player.vimeo.com/video/${videoId}?byline=0`}
title={`Vimeo Embed (${videoId})`}
/>
+2
View File
@@ -5,6 +5,7 @@ import Frame from "./components/Frame";
const URL_REGEX = /(?:https?:\/\/)?(?:www\.)?youtu\.?be(?:\.com)?\/?.*(?:watch|embed)?(?:.*v=|v\/|\/)([a-zA-Z0-9_-]{11})$/i;
type Props = {|
isSelected: boolean,
attrs: {|
href: string,
matches: string[],
@@ -20,6 +21,7 @@ export default class YouTube extends React.Component<Props> {
return (
<Frame
{...this.props}
src={`https://www.youtube.com/embed/${videoId}?modestbranding=1`}
title={`YouTube (${videoId})`}
/>
+64 -6
View File
@@ -1,12 +1,18 @@
// @flow
import { observable } from "mobx";
import { observer } from "mobx-react";
import { OpenIcon } from "outline-icons";
import * as React from "react";
import styled from "styled-components";
import Flex from "components/Flex";
type Props = {
src?: string,
border?: boolean,
title?: string,
icon?: React.Node,
canonicalUrl?: string,
isSelected?: boolean,
width?: string,
height?: string,
};
@@ -40,15 +46,26 @@ class Frame extends React.Component<PropsWithRef> {
width = "100%",
height = "400px",
forwardedRef,
...rest
icon,
title,
canonicalUrl,
isSelected,
src,
} = this.props;
const Component = border ? StyledIframe : "iframe";
const withBar = !!(icon || canonicalUrl);
return (
<Rounded width={width} height={height}>
<Rounded
width={width}
height={height}
withBar={withBar}
className={isSelected ? "ProseMirror-selectednode" : ""}
>
{this.isLoaded && (
<Component
ref={forwardedRef}
withBar={withBar}
sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
width={width}
height={height}
@@ -56,20 +73,60 @@ class Frame extends React.Component<PropsWithRef> {
frameBorder="0"
title="embed"
loading="lazy"
src={src}
allowFullScreen
{...rest}
/>
)}
{withBar && (
<Bar align="center">
{icon} <Title>{title}</Title>
{canonicalUrl && (
<Open
href={canonicalUrl}
target="_blank"
rel="noopener noreferrer"
>
<OpenIcon color="currentColor" size={18} /> Open
</Open>
)}
</Bar>
)}
</Rounded>
);
}
}
const Rounded = styled.div`
border-radius: 3px;
border-radius: ${(props) => (props.withBar ? "3px 3px 0 0" : "3px")};
overflow: hidden;
width: ${(props) => props.width};
height: ${(props) => props.height};
height: ${(props) => (props.withBar ? props.height + 28 : props.height)};
`;
const Open = styled.a`
color: ${(props) => props.theme.textSecondary} !important;
font-size: 13px;
font-weight: 500;
align-items: center;
display: flex;
position: absolute;
right: 0;
padding: 0 8px;
`;
const Title = styled.span`
font-size: 13px;
font-weight: 500;
padding-left: 4px;
`;
const Bar = styled(Flex)`
background: ${(props) => props.theme.secondaryBackground};
color: ${(props) => props.theme.textSecondary};
padding: 0 8px;
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
user-select: none;
`;
// This wrapper allows us to pass non-standard HTML attributes through to the DOM element
@@ -79,7 +136,8 @@ const Iframe = (props) => <iframe {...props} />;
const StyledIframe = styled(Iframe)`
border: 1px solid;
border-color: ${(props) => props.theme.embedBorder};
border-radius: 3px;
border-radius: ${(props) => (props.withBar ? "3px 3px 0 0" : "3px")};
display: block;
`;
export default React.forwardRef<Props, typeof Frame>((props, ref) => (
+8
View File
@@ -3,6 +3,7 @@ import * as React from "react";
import styled from "styled-components";
import Abstract from "./Abstract";
import Airtable from "./Airtable";
import ClickUp from "./ClickUp";
import Codepen from "./Codepen";
import Figma from "./Figma";
import Framer from "./Framer";
@@ -57,6 +58,13 @@ export default [
component: Airtable,
matcher: matcher(Airtable),
},
{
title: "ClickUp",
keywords: "project",
icon: () => <Img src="/images/clickup.png" />,
component: ClickUp,
matcher: matcher(ClickUp),
},
{
title: "Codepen",
keywords: "code editor",
+10
View File
@@ -0,0 +1,10 @@
// @flow
import * as React from "react";
export default function usePrevious(value: any) {
const ref = React.useRef();
React.useEffect(() => {
ref.current = value;
});
return ref.current;
}
+8
View File
@@ -0,0 +1,8 @@
// @flow
import { MobXProviderContext } from "mobx-react";
import * as React from "react";
import RootStore from "stores";
export default function useStores(): typeof RootStore {
return React.useContext(MobXProviderContext);
}
+16 -22
View File
@@ -1,4 +1,6 @@
// @flow
import "mobx-react-lite/batchingForReactDom";
import "focus-visible";
import { Provider } from "mobx-react";
import * as React from "react";
import { render } from "react-dom";
@@ -12,32 +14,24 @@ import Toasts from "components/Toasts";
import Routes from "./routes";
import env from "env";
let DevTools;
if (process.env.NODE_ENV !== "production") {
DevTools = require("mobx-react-devtools").default; // eslint-disable-line global-require
}
const element = document.getElementById("root");
if (element) {
render(
<>
<ErrorBoundary>
<Provider {...stores}>
<Theme>
<Router>
<>
<ScrollToTop>
<Routes />
</ScrollToTop>
<Toasts />
</>
</Router>
</Theme>
</Provider>
</ErrorBoundary>
{DevTools && <DevTools position={{ bottom: 0, right: 0 }} />}
</>,
<ErrorBoundary>
<Provider {...stores}>
<Theme>
<Router>
<>
<ScrollToTop>
<Routes />
</ScrollToTop>
<Toasts />
</>
</Router>
</Theme>
</Provider>
</ErrorBoundary>,
element
);
}
+56 -44
View File
@@ -11,11 +11,11 @@ import CollectionDelete from "scenes/CollectionDelete";
import CollectionEdit from "scenes/CollectionEdit";
import CollectionExport from "scenes/CollectionExport";
import CollectionMembers from "scenes/CollectionMembers";
import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
import { DropdownMenu } from "components/DropdownMenu";
import DropdownMenuItems from "components/DropdownMenu/DropdownMenuItems";
import Modal from "components/Modal";
import VisuallyHidden from "components/VisuallyHidden";
import getDataTransferFiles from "utils/getDataTransferFiles";
import importFile from "utils/importFile";
import { newDocumentUrl } from "utils/routeHelpers";
type Props = {
@@ -55,11 +55,13 @@ class CollectionMenu extends React.Component<Props> {
const files = getDataTransferFiles(ev);
try {
const document = await importFile({
file: files[0],
documents: this.props.documents,
collectionId: this.props.collection.id,
});
const file = files[0];
const document = await this.props.documents.import(
file,
null,
this.props.collection.id,
{ publish: true }
);
this.props.history.push(document.url);
} catch (err) {
this.props.ui.showToast(err.message);
@@ -103,7 +105,14 @@ class CollectionMenu extends React.Component<Props> {
};
render() {
const { policies, collection, position, onOpen, onClose } = this.props;
const {
policies,
documents,
collection,
position,
onOpen,
onClose,
} = this.props;
const can = policies.abilities(collection.id);
return (
@@ -114,7 +123,7 @@ class CollectionMenu extends React.Component<Props> {
ref={(ref) => (this.file = ref)}
onChange={this.onFilePicked}
onClick={(ev) => ev.stopPropagation()}
accept="text/markdown, text/plain"
accept={documents.importFileTypes.join(", ")}
/>
</VisuallyHidden>
@@ -127,44 +136,47 @@ class CollectionMenu extends React.Component<Props> {
collection={collection}
onSubmit={this.handleMembersModalClose}
handleEditCollectionOpen={this.handleEditCollectionOpen}
onEdit={this.handleEditCollectionOpen}
/>
</Modal>
<DropdownMenu onOpen={onOpen} onClose={onClose} position={position}>
{collection && (
<>
{can.update && (
<DropdownMenuItem onClick={this.onNewDocument}>
New document
</DropdownMenuItem>
)}
{can.update && (
<DropdownMenuItem onClick={this.onImportDocument}>
Import document
</DropdownMenuItem>
)}
{can.update && <hr />}
{can.update && (
<DropdownMenuItem onClick={this.handleEditCollectionOpen}>
Edit
</DropdownMenuItem>
)}
{can.update && (
<DropdownMenuItem onClick={this.handleMembersModalOpen}>
Permissions
</DropdownMenuItem>
)}
{can.export && (
<DropdownMenuItem onClick={this.handleExportCollectionOpen}>
Export
</DropdownMenuItem>
)}
</>
)}
{can.delete && (
<DropdownMenuItem onClick={this.handleDeleteCollectionOpen}>
Delete
</DropdownMenuItem>
)}
<DropdownMenuItems
items={[
{
title: "New document",
visible: !!(collection && can.update),
onClick: this.onNewDocument,
},
{
title: "Import document",
visible: !!(collection && can.update),
onClick: this.onImportDocument,
},
{
type: "separator",
},
{
title: "Edit…",
visible: !!(collection && can.update),
onClick: this.handleEditCollectionOpen,
},
{
title: "Permissions…",
visible: !!(collection && can.update),
onClick: this.handleMembersModalOpen,
},
{
title: "Export…",
visible: !!(collection && can.export),
onClick: this.handleExportCollectionOpen,
},
{
title: "Delete…",
visible: !!(collection && can.delete),
onClick: this.handleDeleteCollectionOpen,
},
]}
/>
</DropdownMenu>
<Modal
title="Edit collection"

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