Compare commits

..

846 Commits

Author SHA1 Message Date
Tom Moor ce65f27a47 wip: Pull work across from previous branch 2021-08-06 16:39:17 -07:00
Tom Moor a800fe4fd1 authorization 2021-08-06 13:15:48 -07:00
Tom Moor 13d5bc281b Merge branch 'main' into feat/multiplayer 2021-08-06 11:35:55 -07:00
Saumya Pandey 09b73401de fix: Sidebar links highlighting issue when a template is deleted or archived. (#2420) 2021-08-06 23:01:25 +05:30
Saumya Pandey 42b384688d fix: Options to create a document is available when the policies of collection in the context doesn't permits the user (#2424) 2021-08-06 22:58:26 +05:30
Tom Moor 5bdee1204e fix: Copying header results in '#' copied
fix: urls in text become linked when reloading doc
fix: Allow creation of links to anchors from link toolbar
2021-08-06 09:39:03 -07:00
Tom Moor 9db72217af feat: Include more events in document history sidebar (#2334)
closes #2230
2021-08-05 15:03:55 -07:00
Tom Moor 57a2524fbd fix: /public directory missing in new docker releases (#2417)
closes #2416
2021-08-04 09:21:25 -07:00
Tom Moor bd148f4790 fix: Paste handler should default to HTML when paste source is Outline editor
related #2416
2021-08-04 09:20:51 -07:00
Tom Moor 28d32af613 perf: Remove unused database indexes according to a month of data in production (#2395) 2021-08-03 20:51:12 -07:00
Tom Moor f2f550e1d2 fix: Policies missing on documents.viewed endpoint 2021-08-03 20:02:11 -07:00
Translate-O-Tron dad21b2186 New Crowdin updates (#2400) 2021-08-03 19:32:51 -07:00
Tom Moor 5fb5f1e8b5 perf: Remove backup column migration (#2397)
* perf: Remove no-longer-used 'backup' columns

These were added as part of the move to the v2 editor over a year ago incase any text was not correctly converted. After a year of use no cases of failed conversion have occurred that required the use of this column

* Remove migration, will do in 2-step release

* perf: Remove no-longer-used 'backup' columns

These were added as part of the move to the v2 editor over a year ago incase any text was not correctly converted. After a year of use no cases of failed conversion have occurred that required the use of this column
2021-08-03 18:55:52 -07:00
Tom Moor 7bbbfa6bbf Merge main 2021-08-03 15:28:23 -07:00
Tom Moor 2d0690697c 0.58.0 2021-08-03 15:17:06 -07:00
Tom Moor 6b551749d4 chore: Remove version- prefix from docker tags 2021-08-03 14:23:14 -07:00
Jack Baron 52fc861bcf feat: Optimize Dockerfile (#2337)
* feat: optimize dockerfile
use new dockerfile syntaxes
leverage multi-stage builds
strip yarn cache from image
use stricter yarn install command
run as a non-root user

* fix: mark yarn-deduplicate as a required dep
`yarn --production` will fail on a clean install otherwise

* fix: add sequelize required files for migrations

* fix: use correct ARG syntax for multistage builds

* revert: mark yarn-deduplicate as a required dep
no longer required as of 0b3adad751
2021-08-03 13:22:41 -07:00
Tom Moor c81c9a9d2d chore: CI Automated Builds (#2409)
closes #2408
2021-08-02 23:35:13 -07:00
Tom Moor 29c742a673 fix: Settings on 'Security' tab not persisting correctly after refactor (#2407)
* fix: Settings on 'Security' tab not persisting correctly after refactor
closes #2406
2021-08-02 13:37:53 -07:00
Tom Moor dd249021e7 fix: GoogleDrive embeds stopped working with new share urls
closes #2405
2021-08-02 11:09:16 -07:00
Tom Moor 21d3b9c7e0 fix: Formatting of welcome docs :rolleyes: 2021-08-01 13:03:21 -07:00
Tom Moor 6665dfff28 Merge branch 'main' of github.com:outline/outline 2021-08-01 12:55:03 -07:00
Tom Moor cdfe3a7fc3 chore: Add new 'getting started' onboarding document (#2391)
Remove support document
Remove confusing images
Added onboarding checklist
2021-08-01 12:54:41 -07:00
Tom Moor 401c91f90b perf: Correctly parallelize count query in users.list 2021-07-30 12:20:19 -04:00
Tom Moor ed5320507d perf: Separate slow joins (#2394) 2021-07-30 08:50:02 -07:00
Translate-O-Tron e34581d25f New Crowdin updates (#2372) 2021-07-30 07:45:58 -07:00
Tom Moor 65a1e2630c perf: Remove no-longer-used 'backup' columns (#2396)
* perf: Remove no-longer-used 'backup' columns

These were added as part of the move to the v2 editor over a year ago incase any text was not correctly converted. After a year of use no cases of failed conversion have occurred that required the use of this column

* Remove migration, will do in 2-step release
2021-07-30 07:22:17 -07:00
Tom Moor 59de4a7db0 feat: Default to "recently viewed" (#2390)
* feat: Default user to first collection on first app open

* Default home tab to 'recently viewed'

* fix: Styling of inactive tab
2021-07-30 07:16:03 -07:00
Tom Moor 63eb8aadaf fix: Flow, remove misused withTranslation on functional component 2021-07-30 00:52:42 -04:00
Saumya Pandey 37fd7ec97a fix: Enable offline access to google accounts (#2392)
* Enable google offline access

* Prevent overriding prompt parameter
2021-07-29 20:04:57 -07:00
Tom Moor 928106067f chore: Tone down notices (#2393) 2021-07-29 20:04:45 -07:00
Tom Moor f1f8badc8f Merge branch 'main' into feat/multiplayer 2021-07-29 14:49:12 -04:00
Tom Moor cb7c27690f fix: Slow tooltips on timestamps 2021-07-28 20:26:04 -04:00
Tom Moor 26da8c4165 feat: Add 'done' icon when all tasks are complete 2021-07-28 19:55:46 -04:00
Tom Moor 36b8ae859e fix: Bump Editor
fix: Sticky formatting toolbar behavior on iOS
fix: Image caption localized
2021-07-28 18:01:01 -04:00
Tom Moor ad1eaa5210 fix: Jank at beginning of loading indicator bar 2021-07-28 17:56:44 -04:00
Saumya Pandey 98024f6be1 fix: "1 tasks done" incorrectly pluralized (#2382) 2021-07-29 01:39:55 +05:30
falleng0d 37c02a572b feat: Auto detect language on login page access (#2338)
* feat: Auto detect language on login page access

* fix: Apply tommoor suggested changes

* fix: QOL improvements for translators

* fix: consistency fix provider -> authProviderName

Co-authored-by: Tom Moor <tom.moor@gmail.com>
2021-07-28 12:00:02 -07:00
Tom Moor e53bb8bfbc fix: Error uploading fallback avatar when name contains characters that need to be escaped (#2387)
* Todo -> Task to match new langauge elsewhere

* fix: Correctly escape characters in Tiley url

* Move encoding to avatars logic, add test
2021-07-28 11:45:47 -07:00
Tom Moor 2a473bf7b4 Todo -> Task to match new langauge elsewhere 2021-07-28 13:15:30 -04:00
Tom Moor f3b09ab56a test 2021-07-27 21:30:00 -04:00
Tom Moor 6eb51a9cb9 chore: Allow passing of page to revisions backfill script 2021-07-27 18:53:39 -04:00
Tom Moor d01c40badb fix: Minor positioning fix of Account menu 2021-07-27 18:16:23 -04:00
Tom Moor fc551c91bd Bump editor
- Fixes enter with horizontal gap cursor
- Improves pasting behavior
- Fixes heading uncollapse when value changes
- Fixes notice blocks not hidden with other collapsed content
closes #2371
2021-07-27 17:50:16 -04:00
Tom Moor fdc1955b91 fix: Mixture of middots with different weights in document meta 2021-07-27 10:33:26 -04:00
Tom Moor b6703671e2 fix: Task progress svg shrinks width in some circumstances 2021-07-27 10:33:11 -04:00
Tom Moor 84f647674a Merge branch 'main' of github.com:outline/outline 2021-07-27 10:24:36 -04:00
Saumya Pandey a81fbd8608 fix: Show tasks completion on document list items (#2342)
Co-authored-by: Tom Moor <tom.moor@gmail.com>
2021-07-27 11:31:27 +05:30
Tom Moor 8ee018a759 feat: Web concurrency (#2347)
* feat: Fork multiple processes

* Remove boxen

* comment

* chore: Add support for Heroku DATABASE_CONNECTION_POOL_URL
closes #2306
2021-07-26 15:51:50 -07:00
Tom Moor 6815c940b2 fix: Failure case during account provision that can result in no welcome collection 2021-07-26 13:46:55 -04:00
Saumya Pandey c9bd3bbf45 fix: Editing title in sidebar allows removal of title (#2364) 2021-07-26 00:17:39 +05:30
Translate-O-Tron f61f9703f3 New Crowdin updates (#2368) 2021-07-25 08:23:53 -07:00
Tom Moor 48d538b424 fix: Server error when rendering share for deleted document
closes #2352
2021-07-23 11:25:11 -04:00
Tom Moor 84ad7c482c fix: Various editor header and metadata fixes (#2361)
* fix: Publish button disabled on drafts in read-only mode
fix: Template selector appears on edited documents

* fix: Save button does not immediately come available when selecting a template

* fix: Template menu item alignment
closes #2204

* fixes: Use policy for display of star in document title
closes #2354

* fix: Modified time is sometimes bold when last edited user is current user
closes #2355

* fix: Allow starring of drafts
2021-07-22 15:17:18 -07:00
Tom Moor d35b5d2613 tidy for blog post ;) 2021-07-22 13:43:29 -04:00
Tom Moor 3090c2cfa3 chore: Improve perf of new tab loading by caching team policy in localStorage (#2351) 2021-07-21 15:53:57 -07:00
Translate-O-Tron 140b04c126 New Crowdin updates (#2340) 2021-07-21 15:24:45 -07:00
Tom Moor 2aedf4440b feat: Enable Persian language translations (#2341) 2021-07-21 10:41:45 -07:00
Tom Moor 6e07ee3f3e chore: Move animations and globals from shared directory (#2344) 2021-07-21 10:34:55 -07:00
Saumya Pandey bba8cd183b fix: Archive and trash a document by dropping in the sidebar (#2318) 2021-07-21 00:49:41 +05:30
Tom Moor a6b43abed1 wip 2021-07-20 14:50:59 -04:00
Saumya Pandey 0bc609634c fix: Allow searching of previous document titles (#2326)
* Add migrations

* Handle previousTitles when titles is updated

* Add necessary test cases

* Use previous title while searching

* Rewrite logic to update previousTitles in beforeSave hook

* Update weights

* Update test to match new rank order

* Add tooltip to inform user on document

* Add code comment

* Remove previous title tooltip

* fix: Remove unused string, add model tests

Co-authored-by: Tom Moor <tom.moor@gmail.com>
2021-07-20 10:35:29 -07:00
Tom Moor b3b8cb3d9c missing translation string 2021-07-20 12:02:46 -04:00
Saumya Pandey fdb85ec195 fix: Separate toasts storage to own MobX store (#2339)
Co-authored-by: Tom Moor <tom.moor@gmail.com>
2021-07-20 14:36:10 +05:30
Tom Moor f64ab37d3c fix: Interpolation on archive/delete translationsg 2021-07-19 17:26:48 -04:00
Tom Moor 0b3adad751 chore: Move yarn-deduplicate postinstall -> prepare
should not run in production
2021-07-19 17:12:24 -04:00
Tom Moor 83477de300 fix: Account for revisions.create event being debounced 2021-07-19 17:02:33 -04:00
Tom Moor 1726006858 chore: Pass problematic url to error tracking
towards #2319
2021-07-19 16:57:06 -04:00
Tom Moor 3d9eaeeeeb chore: Add revisions.create backfill script (#2330)
* chore: Add revisions.create backfill script

* fix: Correct timestamp on revisions.create events
2021-07-19 13:32:03 -07:00
falleng0d 2e955353ae feat: translations (#2275)
Co-authored-by: Tom Moor <tom.moor@gmail.com>
2021-07-19 11:12:53 -07:00
Tom Moor 05aba68457 feat: Add support for collapsible headings (#2327) 2021-07-19 09:19:36 -07:00
Tom Moor 8f6e956bc5 chore: Add documentId index to events table (#2331) 2021-07-19 09:19:26 -07:00
Tom Moor 0cad99c343 chore: Move 'templates' to bottom of sidebar (#2328)
chore: Hide trash and archive for read-only users
2021-07-19 09:18:33 -07:00
Translate-O-Tron 04746f6a2c New Crowdin updates (#2304) 2021-07-16 06:46:32 -07:00
Tom Moor 25907f5c72 chore: Reduce idle CPU usage in development 2021-07-16 09:30:43 -04:00
Jack Baron d7a21db72f fix: Remove duplicate translation key (#2325) 2021-07-16 14:36:34 +05:30
Saumya Pandey 9596979993 fix: Add translation hooks on document and collection pages (#2307) 2021-07-16 01:49:09 +05:30
Tom Moor 31714efb0b feat: useBoolean hook (#2314)
* feat: Add useBoolean hook and example usage

* More example usage

* chore: More useBoolean conversion
2021-07-15 12:27:03 -07:00
Tom Moor 8884da8a4b feat: Add revisionCreator command (#2321)
add revisions.create event
2021-07-15 12:26:43 -07:00
Tom Moor 30cf244610 chore: Loading placeholders (#2322)
* Improve visual of loading mask

* Normalize placeholder naming

* Remove unused file
2021-07-15 12:26:34 -07:00
Saumya Pandey 3f030540b3 fix: Add translation hooks on settings screen (#2298)
Co-authored-by: Tom Moor <tom.moor@gmail.com>
2021-07-15 14:50:36 +05:30
Saumya Pandey 7ae3addea0 fix: Add space to the valid index characters list (#2316) 2021-07-15 00:35:47 +05:30
Saumya Pandey a9d758bb0c fix: Add translation hooks on remaining files (#2311) 2021-07-15 00:30:08 +05:30
Matheus Breguêz 06e16eef12 feat: Add Google DataStudio embed (#2293)
Co-authored-by: Tom Moor <tom.moor@gmail.com>
2021-07-14 11:57:12 -07:00
Tom Moor 8e5a2b85c2 feat: Improved UI motion design (#2310)
* feat: Improved UI motion design

* fix: Animation direction when screen placement causes context menu to be flipped
2021-07-12 11:57:17 -07:00
Saumya Pandey 5689d96cc4 fix: Add translation hooks on groups screen (#2303)
* Refactor groups page to functional component and translate strings

* Update app/scenes/GroupNew.js

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

* Update app/scenes/GroupEdit.js

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

* Update app/scenes/GroupDelete.js

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

* Update app/scenes/GroupMembers/GroupMembers.js

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

* Format GroupMember.js

* Change Trans usage

* Format GroupDelete

* Revert "Format GroupDelete"

This reverts commit 880128f94d.

* Update app/scenes/GroupNew.js

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

* Update app/scenes/GroupNew.js

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

* Update GroupNew

* Remove newlines

Co-authored-by: Tom Moor <tom.moor@gmail.com>
2021-07-12 11:54:55 -07:00
Tom Moor 5cd4dbd9d7 fix: Mispositioned TOC control on mobile due to merge conflict
fix: Show message in mobile TOC when no headings in document
fix: MenuItem with level should still have background edge-to-edge
fix: Show developer warning when creating incorrect menu item type
2021-07-11 13:09:10 -04:00
Tom Moor 587a0e0517 chore: Update html import related deps 2021-07-11 10:02:35 -04:00
Tom Moor 686ecdfa92 fix: CSS syntax error 2021-07-09 14:09:52 -04:00
Translate-O-Tron bb019b081f New Crowdin updates (#2281) 2021-07-09 05:55:06 -07:00
Saumya Pandey 7d5fbeb7b0 fix: Add access to document TOC on mobile (#2279)
* Add TOC button for mobile

* Undo NewDocumentMenu changes

* Place the toc button in the correct position.

* Pass menu props to menuitem

* Update app/menus/TableOfContentsMenu.js

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

* Update app/menus/TableOfContentsMenu.js

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

* Use the existing prop type

* Write menu inside actions prop

* Prevent blank webpage behaviour for toc

* Use href instead of level to determine target

* Update app/scenes/Document/components/Header.js

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

* Add heading to menu items

* Use existing Heading component

Co-authored-by: Tom Moor <tom.moor@gmail.com>
2021-07-09 04:50:27 -07:00
Tom Moor 056f89fcfd fix: Allow TOC to scroll when larger than browser height (#2296) 2021-07-09 04:07:28 -07:00
Tom Moor 0e7d352781 chore: Add fetch-retry, remove isomorphic-fetch (#2297)
* chore: Add fetch-retry, remove isomorphic-fetch

closes #2270

* test: Mock fetch
2021-07-09 04:07:18 -07:00
Tom Moor b5e4e4fe82 fix: Various mobile fixes (#2295)
* fix: Input placeholder ellipsis

* fix: Hide scrollbar on nav tabs on mobile

* fix: Header actions should be fixed on mobile

* fix: Add fade when content in tabs does not fit in available horizontal width
2021-07-08 18:32:14 -07:00
Tom Moor e41f17c701 feat: Enable Japanese translations (#2282) 2021-07-08 18:32:05 -07:00
Tom Moor 9a1c8f07d1 feat: Add documentId filter to events.list (#2287) 2021-07-08 10:12:06 -07:00
Tom Moor 241cb11493 chore: Automate running yarn-deduplicate, see comment:
https://github.com/outline/outline/pull/2283\#discussion_r665301770
2021-07-07 22:26:56 -04:00
Saumya Pandey 8195791bb2 fix: Make search query string user friendly (#2283)
* Upgrade query-string package and skip empty string

* Run yarn-deduplicate command
2021-07-07 18:45:40 -07:00
Saumya Pandey b037ae5dc1 fix: Improve isChildDocument performance (#2284) 2021-07-07 04:53:40 -07:00
Tom Moor aeba8ce4eb fix: Empty context menu when user does not have permission to update collection 2021-07-06 22:02:31 -04:00
Tom Moor 429c5fba85 0.57.0 2021-07-06 09:12:54 -04:00
Tom Moor 9495ddba25 fix: Restore previous WSS CORS behavior 2021-07-05 23:01:25 -04:00
dependabot[bot] 486a60e97c chore(deps): bump socket.io from 2.3.0 to 2.4.0 (#1831)
Bumps [socket.io](https://github.com/socketio/socket.io) from 2.3.0 to 2.4.0.
- [Release notes](https://github.com/socketio/socket.io/releases)
- [Changelog](https://github.com/socketio/socket.io/blob/2.4.0/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io/compare/2.3.0...2.4.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-07-05 17:11:40 -07:00
Tom Moor c687745263 fix: E-mail signin on incorrect subdomain should allow the process to continue instead of error
closes #2276
2021-07-05 19:25:21 -04:00
Translate-O-Tron 1b92993b90 fix: New Portuguese, Brazilian translations from Crowdin (#2271) 2021-07-05 13:37:09 -07:00
Tom Moor 181a20a268 fix: More context menu fixes 2021-07-05 16:35:46 -04:00
Tom Moor f8ffa4e25a tweak sidebar item background 2021-07-04 18:44:09 -04:00
Farzad 7e139ca8f7 fix: nested link positions for RTL titles in sidebar (#2272) 2021-07-04 12:08:05 -07:00
Tom Moor bb58db507d fix: ew-resize -> col-resize cursor 2021-07-04 11:54:29 -04:00
Translate-O-Tron 49bf86d6d9 New Crowdin updates (#2268) 2021-07-04 06:54:52 -07:00
Tom Moor 286a15cf10 fix: Clicking dropdown menu items in FF (#2269)
* fix: Clicking dropdown menu items in FF
closes #2264

* fix: Anchor items, add comment

* fix: CI test memory issues
2021-07-04 06:54:40 -07:00
Tom Moor f65469b777 lockfile 2021-07-03 21:22:52 -04:00
Translate-O-Tron fe65a79d66 New Crowdin updates (#2267) 2021-07-03 07:02:01 -07:00
Tom Moor a1d5ac0907 RTL document support (#2263)
* Basic RTL support in documents

* fix: DocumentListItem and ReferenceListItem for RTL content
2021-07-03 07:00:10 -07:00
Tom Moor 04eabe68a7 feat: Enable traditional Chinese translations (#2266) 2021-07-02 12:08:08 -07:00
Tom Moor 1c0c694c22 fix: Email auth should allow same guest user on multiple subdomains (#2252)
* test: Add email auth tests to establish current state of system

* fix: Update logic to account for dupe emails used between subdomains

* test

* test
2021-07-02 12:07:43 -07:00
Translate-O-Tron 2ae74f2834 New Crowdin updates (#2262) 2021-07-02 11:23:02 -07:00
Tom Moor 0f01fc5faa test: Reduce memory usage by not requiring stores into all (#2265) 2021-07-02 11:16:07 -07:00
Tom Moor 7f1322b7ba fix: Down arrow in search input should move focus to results (#2257)
closes #2253
2021-07-01 15:01:30 -07:00
dependabot[bot] 3c98133e24 chore(deps): bump socket.io-parser from 3.3.1 to 3.3.2 (#2258)
Bumps [socket.io-parser](https://github.com/socketio/socket.io-parser) from 3.3.1 to 3.3.2.
- [Release notes](https://github.com/socketio/socket.io-parser/releases)
- [Changelog](https://github.com/socketio/socket.io-parser/blob/3.3.2/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io-parser/compare/3.3.1...3.3.2)

---
updated-dependencies:
- dependency-name: socket.io-parser
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-07-01 12:08:57 -07:00
Tom Moor 088353d61f fix: Data loading state not reset when props change to PaginatedList (#2254)
* fix: Data loading state not reset when significant props change to PaginatedList

closes #2251

* test: Add enzyme and component test
2021-06-26 21:49:25 -07:00
Translate-O-Tron 31180619e1 New Crowdin updates (#2182) 2021-06-26 13:43:54 -07:00
Saumya Pandey 9fccc280d7 fix: Add ability to permanently delete documents in trash (#2192)
* Align false conditions before true

* Update documents.delete endpoint for permanent delete

* Add permanent delete to events table and integrate with socket.io

* Add permanent delete to document menu

* Update parentDocumentId of direct child to null

* Add translation

* Add test for permanent delete

* Add space

* Update app/scenes/DocumentPermanentDelete.js

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

* Update app/stores/DocumentsStore.js

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

* Update server/commands/documentPermanentDeleter.js

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

* Update app/scenes/DocumentPermanentDelete.js

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

* Change socket room from team to collection

* Add translation

* Create log func for commands

* Move tests from utils to permanentDeleter command

* Add additional tests

* Set redirect to trash

* Return promise from beforeEach

* Add undeleted documents validation

* Include deleteAt attribute in db query

* Update server/commands/documentPermanentDeleter.js

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

* tweak language

Co-authored-by: Tom Moor <tom.moor@gmail.com>
2021-06-25 16:14:40 -07:00
Tom Moor c69b4efc34 fix: Aligned images do not load in publicly shared documents (#2248) 2021-06-25 10:09:44 -07:00
Tom Moor 3cec6b4903 fix: Allow for offline development 2021-06-21 21:40:28 -07:00
Tom Moor ede7f2e3e6 fix: Bump RME (table and image fixes) 2021-06-21 17:39:14 -07:00
Tom Moor cf8fa5ffa3 fix: Bump RME (checkbox list fixes) 2021-06-18 16:28:27 -07:00
Tom Moor 1a2a0f4264 fix: Long search term causes server error writing query to db (#2237)
closes #2234
2021-06-17 23:23:35 -07:00
Tom Moor 5f3a38bf87 fix: todo list checkbox consistency issue
closes #2179
2021-06-17 22:57:55 -07:00
Tom Moor afff3a6f25 fix: Server error when user cancels OAuth process with Azure (#2231) 2021-06-16 21:45:20 -07:00
Tom Moor b5824879a3 Merge branch 'fix/concat-tags' 2021-06-16 18:36:28 -07:00
Tom Moor 1c82e292e0 fix: Allow embed of private mindmeister embeds
fix: Missing right and bottom border of some embeds
2021-06-16 18:36:21 -07:00
Tom Moor 317289ac2a fix: Error in Datadog tracking, if only we had TS :( 2021-06-16 08:52:54 -07:00
Tom Moor 8331026cb3 fix: No search results from link editor search due to error parsing date (date-fns upgrade) 2021-06-16 07:54:56 -07:00
Tom Moor de285f2b63 feat: Add TLS ciphers option (#2217)
closes #2175
2021-06-15 21:37:41 -07:00
G. Santos d205c48296 docs: Fix SECRET_KEY variable description (#2229)
Updated the description of the SECRET_KEY variable in the .env.sample
file to clarify that the key needs to be 32 bytes long and hex-encoded.
The previous description of "32 character hexadecimal" was confusing
as it left open the possibility of a hex-encoded 16-byte key.
2021-06-15 21:37:19 -07:00
Tom Moor 277c37dae6 fix: Metrics lib to account for multiple server instances 2021-06-15 20:34:46 -07:00
Tom Moor 2c39cd6496 chore: Normalize "new" actions in settings (#2226)
* fix: Unauthorized request to views.list from shared documents

* Bump dep styled-components

* chore: Normalize 'new' actions in settings area to top right
chore: Add translation hooks to API tokens screen
chore: Move API tokens loading to paginated list
2021-06-15 19:10:50 -07:00
Tom Moor d85592b5f3 feat: DataDog metrics (#2228)
* wip

* chore: Change event names, add additional events

* fix: Not counting connect events
2021-06-15 19:10:38 -07:00
Tom Moor cdf0df0faa Bump dep styled-components 2021-06-13 18:26:25 -07:00
Tom Moor 48f54b5aa2 fix: Unauthorized request to views.list from shared documents 2021-06-13 18:24:02 -07:00
Tom Moor 2ca57fc7cf fix: 3 locations with return undefined (not compatible with React 17) 2021-06-13 17:47:17 -07:00
Tom Moor 470920e2c3 feat: Allow templates from any collection to be used
fix: Hover state of context menu items with icons
2021-06-13 17:43:50 -07:00
Tom Moor beee8ebee7 fix: Sidebar flash when moving between collection/document due to mobx-react upgrade 2021-06-13 17:22:35 -07:00
Tom Moor 9f05c9bd43 chore: Upgrade React to v17 (#2045)
* chore: Upgrade React v17

* chore: Upgrade additional deps to reduce warnings

* fix: Restore react-table dep

* Bump react-avatar-editor, mobx-react

* Remove unmaintained @rehooks/window-scroll-position dep

* Bump react-waypoint dep for React 17 support

* fix: Syntax error in autotrack chunk name comment
2021-06-13 15:23:53 -07:00
Tom Moor 65be808556 fix: Cause of sporadic test failures in CI, promise not returned for flushdb 2021-06-13 14:52:24 -07:00
Tom Moor 89f8df619c fix: Remove export permission for read-only users (#2220) 2021-06-13 14:41:29 -07:00
Tom Moor 756ec92cdb fix: Link copied to clipboard takes dark mode styles (#2218)
Upgrade copy-to-clipboard
closes #2207
2021-06-12 15:44:58 -07:00
Dave a8e2e349e9 fix: change metaDisplay key to Alt for "Table of contents" (#2187)
* change `metaDisplay` key to Alt for "Table of contents"

Co-authored-by: Tom Moor <tom.moor@gmail.com>
2021-06-12 15:37:35 -07:00
Tom Moor 25f745e7e5 fix: Text alignment in templates menu
closes #2204
2021-06-12 13:31:22 -07:00
Nainterceptor 07b1811993 feat: Use SMTP_SECURE environment variable to force secure parameter of smtp configuration (#2214) 2021-06-12 11:01:48 -07:00
Saumya Pandey d71f0ae6bd fix: Two restore options when an archived document is deleted (#2194)
* Merge two menu items

* Add deletedAt guard condition in document unarchive policy

* Make the parentDocumentId null

* Update test
2021-06-10 22:52:32 -07:00
Tom Moor f58032d305 fix: Flash of sidebar when first loading Document chunk 2021-06-09 18:01:35 -07:00
Saumya Pandey 6beb6febc4 fix: Use friendly urls for collections (#2162)
Co-authored-by: Tom Moor <tom.moor@gmail.com>
2021-06-09 17:48:48 -07:00
Saumya Pandey a6d4d4ea36 fix: Add Portugese, Brazil to language options (#2164)
* Add Portugese, Brazil to language options

* Upgrade date-fns package

Co-authored-by: Tom Moor <tom.moor@gmail.com>
2021-06-09 17:42:14 -07:00
Tom Moor a99f6bed42 feat: Return publicly shared document title in SSR HTML (#2191)
* feat: Return publicly shared document title in SSR HTML
closes #2146

* tests
2021-06-09 17:41:39 -07:00
Tom Moor 4cd61db1ea fix: Move views loading to avoid duplicate request 2021-06-08 21:13:56 -07:00
Tom Moor 0db7bb7f3e Bump editor
closes #2156
closes #2067
2021-06-07 20:32:41 -07:00
Tom Moor d8ca9c6111 fix: Server error if non-array passed to users.invite 2021-06-07 20:28:28 -07:00
Dave 4a8d357084 style: add option background for InputSelect (#2188) 2021-06-07 18:34:01 -07:00
Yao Wang e0fb76cb63 documentation: Instructions for local development (#2180)
* Fix the instruction for local development

* update readme for Slack OAuth in local development

* Fix the callback URL setting instruction
2021-06-07 18:11:45 -07:00
Saumya Pandey ffed38bf71 fix: Prevent API request for views (#2193) 2021-06-07 18:10:54 -07:00
Tom Moor b4c08a027b fix: Remove hover state css on sidebar items on mobile
closes #2043
2021-06-06 19:56:31 -07:00
Tom Moor 74e0f4dfb3 fix: Parallelize loading attachments in document presenter (#2184)
closes #2157
2021-06-05 18:40:55 -07:00
Tom Moor 5c7f2cf164 feat: Add optional http logging in production (#2183)
* feat: Add optional http logging in production
closes #2174

* Update app.js
2021-06-05 15:19:54 -07:00
Tom Moor f517a2cecb chore: Add React.StrictMode
closes #2177
2021-06-05 14:59:14 -07:00
Saumya Pandey a19ac6aa5f fix: Failure loading collections on frontend results in loading loop (#2176)
Co-authored-by: Tom Moor <tom.moor@gmail.com>
2021-06-05 09:52:49 -07:00
Saumya Pandey ddbbb963b6 fix: Add guard condition for matchMedia usage (#2178) 2021-06-05 09:51:42 -07:00
Viorel Cojocaru ba24a3318e Fix chunks setup (#2181)
* build: Webpack config - use named chunk ids

prevent invalidation across builds by using a deterministic chunkId algorithm

* fix: Autotrack chunk name syntax
2021-06-05 09:50:38 -07:00
Viorel Cojocaru 7a6491cf0d build: Webpack config - allow package.json module field usage (#2173)
- revert resolve.alias to default
- revert bundless-* package aliases to commonjs folder to avoid transpiling
2021-06-04 18:18:17 -07:00
Tom Moor 0c8d4428fc Update stale.yml 2021-06-04 09:11:09 -07:00
Viorel Cojocaru b19fd799ef chore: Update @relative-ci/agent to v2 (#2171) 2021-06-03 22:02:38 -07:00
Viorel Cojocaru 082ced3072 build: Add async chunk names (#2170) 2021-06-03 22:01:23 -07:00
Tom Moor 1f49b35c89 documentation: Improve notes around SECRET_KEY generation 2021-06-03 08:30:53 -07:00
Tom Moor 9817e2f3bf 0.56.0 2021-06-02 12:52:19 -07:00
Translate-O-Tron 04d7c7ac0e New Crowdin updates (#2143) 2021-06-02 12:51:14 -07:00
Tom Moor e625e77a56 fix: Data loading loop on old browsers 2021-06-02 12:45:07 -07:00
Tom Moor 636023aceb fix: Bump RME, improved image download behavior in editor 2021-05-24 20:56:58 -07:00
dependabot[bot] f2dfed4c72 chore(deps): bump browserslist from 4.14.7 to 4.16.6 (#2149)
Bumps [browserslist](https://github.com/browserslist/browserslist) from 4.14.7 to 4.16.6.
- [Release notes](https://github.com/browserslist/browserslist/releases)
- [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md)
- [Commits](https://github.com/browserslist/browserslist/compare/4.14.7...4.16.6)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-24 18:35:53 -07:00
Tom Moor 8cfa724200 feat: Animate disclosure when toggling sidebar items 2021-05-22 21:22:28 -07:00
Tom Moor 6c011eb9b5 fix: Guard empty documentStructure 2021-05-22 21:12:47 -07:00
Tom Moor 7dc11e5b86 fix: Local cache for shared link share trees to reduce render flashing 2021-05-22 20:03:50 -07:00
Tom Moor 44920a25f4 feat: Nested document sharing (#2075)
* migration

* frontend routing, api permissioning

* feat: apiVersion=2

* feat: re-writing document links to point to share

* poc nested documents on share links

* fix: nested shareId permissions

* ui and language tweaks, comments

* breadcrumbs

* Add icons to reference list items

* refactor: Breadcrumb component

* tweaks

* Add shared parent note
2021-05-22 19:34:05 -07:00
Tom Moor dc4b5588b7 feat: Add 'Descript' embed (#2144) 2021-05-22 19:21:56 -07:00
Tom Moor 635910195b i18n 2021-05-22 17:18:10 -07:00
Tom Moor eaf2e50af8 feat: Add 'download image' button
feat: Enable Enter+Shift shortcut in blockquotes
fix: Improve behavior of caret around inline code marks
fix: Disallow pasting embeds in table cells
2021-05-22 17:17:46 -07:00
Tom Moor 505ed3403a fix: Bump RME, improves behavior typing words with underscores 2021-05-20 19:29:59 -07:00
Tom Moor b93d15e967 fix: PaginatedList loading loop 2021-05-20 19:21:30 -07:00
Tom Moor 028eb72f9c fix: Restore behavior of displaying document collaborators in facepile 2021-05-19 22:05:17 -07:00
Tom Moor b0196f0cf0 feat: Rebuilt member admin (#2139) 2021-05-19 21:36:10 -07:00
Translate-O-Tron 833bd51f4c New Crowdin updates (#2120) 2021-05-18 20:00:18 -07:00
Tom Moor 14d9adefe7 test 2021-05-15 18:22:42 -07:00
Tom Moor ec3ea09b2d fix: Return lastActiveAt 2021-05-15 18:14:44 -07:00
Tom Moor 2c0f14f07b fix: Explicit import of fetch-with-proxy 2021-05-13 17:20:24 -07:00
Tom Moor a93d034091 fix: Moving documents between collections does not update attachment permissions (#2136)
* fix: Copy attachments when neccessary and moving between collections

* test: regression
2021-05-12 22:38:24 -07:00
Tom Moor 447371f35a fix: Add server-side proxy support via fetch-with-proxy (#2044)
* fix: Add server-side proxy support via fetch-with-proxy

closes #1893

For some fun discussion on why this is required, see this issue: https://github.com/nodejs/node/issues/8381

* lint
2021-05-12 22:37:32 -07:00
Tom Moor 3bd56fff9e fix: Search query backslash replacement only touched first instance
closes #2111
2021-05-12 20:27:14 -07:00
Tom Moor 9d03c89c02 chore: Return new permissions-policy header on app pages
closes #2040
2021-05-12 20:16:55 -07:00
Tom Moor 9f226cf3b4 fix: Extra space on lhs when printing in Firefox, closes #2128 2021-05-12 20:06:58 -07:00
Tom Moor d01e3f7c72 fix: Print styles in dark mode when OS is light mode
closes #2124
2021-05-12 20:00:10 -07:00
Tom Moor 2cb0bab82a fix: Welcome emails should not be sent when inviting a user (#2132)
* chore: Bump nodemailer

* fix: Welcome email sent to invites

* test: Add regression test for emails from accountProvisioner
2021-05-11 18:59:31 -07:00
dependabot[bot] 456a7e497b chore(deps): bump nodemailer from 4.7.0 to 6.4.16 (#2131)
Bumps [nodemailer](https://github.com/nodemailer/nodemailer) from 4.7.0 to 6.4.16.
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v4.7.0...v6.4.16)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-11 18:11:36 -07:00
Saumya Pandey a39f7a4e55 fix: Remove application/octet-stream as valid frontend mimetype (#2126)
* Remove application/octet-stream and add explicit extensions

* Modify the condition to check for extensions too
2021-05-11 08:07:41 -07:00
Tom Moor fed3774cee chore: Bump RME 2021-05-09 22:36:20 -07:00
Saumya Pandey 985f0da674 fix: Move collection index validation logic to a context assert function (#2116)
* Abstract validation logic for readability

* Add index validation in collections.move

* Add tests
2021-05-09 22:30:37 -07:00
dependabot[bot] 721e7466e6 chore(deps): bump hosted-git-info from 2.8.8 to 2.8.9 (#2127)
Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.8.8 to 2.8.9.
- [Release notes](https://github.com/npm/hosted-git-info/releases)
- [Changelog](https://github.com/npm/hosted-git-info/blob/v2.8.9/CHANGELOG.md)
- [Commits](https://github.com/npm/hosted-git-info/compare/v2.8.8...v2.8.9)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-09 19:28:51 -07:00
Tom Moor 8e1d9f0a7d fix: Welcome collection should be visible to all by default 2021-05-05 21:12:49 -07:00
Tom Moor 71de0c7e5f fix: Currently viewing users should be ordered to top 2021-05-05 21:11:09 -07:00
Tom Moor 4f4067c449 fix: Upgrade RME, fixes image flicked post-upload in editor 2021-05-05 20:09:37 -07:00
Tom Moor b945b614f8 fix: Layout of Keyboard Shortcuts guide for languages where definition wraps onto two lines 2021-05-05 20:05:49 -07:00
Tom Moor 896ee5c20d feat: Improved viewers popover (#2106)
* refactoring popover

* feat: DocumentViews popover

* i18n

* fix: tab focus warnings

* test: Add tests around users.info changes

* snapshots
2021-05-05 19:35:23 -07:00
Translate-O-Tron e984a3dcdb New Crowdin updates (#2100)
* fix: New Polish translations from Crowdin [ci skip]

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

* fix: New German translations from Crowdin [ci skip]
2021-05-05 19:35:04 -07:00
Tom Moor 69802cc985 fix: Add application/octet-stream as a valid mimetype for docx uploads (#2105)
* fix: Add application/octet-stream as a valid mimetype for docx uploads

* fix: Include application/octet-stream in frontend filter
fix: Add file size and file type guards

* Validate .docx extension in files with application/octet-stream mimetype

* refactor: Move MAXIMUM_IMPORT_SIZE to an optional environment config
fix: Add file size check on server too

Co-authored-by: Saumya Pandey <sp160899@gmail.com>
2021-05-05 18:48:37 -07:00
Saumya Pandey 6ef8d9ddb3 fix: Handle null case (#2118) 2021-05-05 18:47:23 -07:00
John Viscel M. Sangkal d21594a6f4 fix: Add onClick event listener to show Appearance Menu options in mobile (#2119) 2021-05-05 18:46:57 -07:00
Tom Moor 974d6b2cbe fix: Submenu overflow broken 2021-05-05 09:13:44 -07:00
Tom Moor aa3cb22703 test: Fix tests around utils.gc 2021-05-03 21:39:01 -07:00
Tom Moor 49ffcda8e0 fix: 'Post to channel' functionality does not work unless Slack SSO used (#2099)
* fix: 'Post to channel' functionality does not work unless Slack SSO used

* test: And this is why we have tests
2021-05-01 16:35:00 -07:00
Tom Moor 77d6adb73b feat: Signup query params tracking (#2098)
* feat: Add tracking of signup query params

* fix: Headers already sent to client

* fix: OAuth error wipes previously written query params cookie
2021-05-01 13:46:08 -07:00
Tom Moor 4d68a34897 fix: ReDoS attack vulnerability when searching documents that contain many space characters
see: https://github.com/outline/outline/pull/2097
see: https://snyk.io/vuln/SNYK-JS-REMOVEMARKDOWN-73635
2021-04-28 22:44:05 -07:00
Tom Moor 61b2e63a44 Merge branch 'main' of github.com:outline/outline 2021-04-28 22:40:53 -07:00
Translate-O-Tron ae940dd255 New Crowdin updates (#2048) 2021-04-27 20:30:31 -07:00
Tom Moor b13626631c fix: Space for overflow menu on sidebar items 2021-04-27 18:58:37 -07:00
Tom Moor 7221e51b96 chore: Move settings screens to Scene component (#2092)
* chore: Convert groups and people settings screens to Scene/functional

* chore: ImportExport to Scene component

* Remaining settings scenes
2021-04-27 18:46:58 -07:00
Tom Moor b89f4c36f4 chore: Rename Authentication -> IntegrationAuthentication (#2091) 2021-04-27 18:42:45 -07:00
Tom Moor 829cc14d36 build:i18n 2021-04-27 18:07:11 -07:00
Tom Moor 8009e8f691 fix: Missing bg blur, closes #2082 2021-04-27 17:29:22 -07:00
Tom Moor ab2aaf7b7b feat: Upgrade RME – includes new page break functionality 2021-04-27 17:21:45 -07:00
dependabot[bot] 65b4480e93 chore(deps): bump redis from 3.0.2 to 3.1.2 (#2090)
Bumps [redis](https://github.com/NodeRedis/node-redis) from 3.0.2 to 3.1.2.
- [Release notes](https://github.com/NodeRedis/node-redis/releases)
- [Changelog](https://github.com/NodeRedis/node-redis/blob/master/CHANGELOG.md)
- [Commits](https://github.com/NodeRedis/node-redis/compare/v3.0.2...v3.1.2)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-27 17:05:32 -07:00
Tom Moor 6de793e94e fix: uuid import broken by dep bump 🤦‍♂️ 2021-04-25 12:54:06 -07:00
Tom Moor 2d22399bbc fix: Correctly guard against last admin deleting their account (#2069)
* fix: Correctly guard against last admin deleting their account

* test
2021-04-24 20:52:46 -07:00
Tom Moor 3fbb3a2403 fix: Continued undefined error in serverWorker registration 2021-04-24 12:50:30 -07:00
Tom Moor d45178cb44 chore: Remove dependency on twemoji 2021-04-23 23:24:54 -07:00
Tom Moor 5786a03f33 chore: Update uuid package, removes dupe dependency 2021-04-23 18:48:47 -07:00
Tom Moor 011a1383ec chore: Upgrade tmp dependency 2021-04-23 18:44:24 -07:00
Tom Moor 72d7b5734d chore: Upgrade slug dependency 2021-04-23 18:40:25 -07:00
Tom Moor b6fe3cb556 chore: Upgrade mammoth for html import fixes 2021-04-23 18:35:54 -07:00
Tom Moor 1e2224cb0d chore: Upgrade dd-trace 2021-04-23 18:32:49 -07:00
Tom Moor 0477060b35 chore: Upgrade relative-ci/agent 2021-04-23 18:31:43 -07:00
Tom Moor a261abcdef Merge branch 'main' of github.com:outline/outline 2021-04-23 18:25:43 -07:00
Tom Moor f64d0ce660 feat: Share flyover (#2065)
* feat: Implement share as flyover instead of modal

* refactor

* i18n
2021-04-23 17:31:27 -07:00
Tom Moor f27072d06e feat: More space for content on larger screens 2021-04-23 16:41:40 -07:00
Tom Moor c8055e40bb fix: Content appearing behind status bar in iOS PWA on some models of phone 🤷 2021-04-23 14:57:38 -07:00
Tom Moor cfae180093 chore: Upgrade Sentry 6.1.0 -> 6.3.1 2021-04-23 12:50:40 -07:00
Tom Moor 094c6418c9 Update LICENSE 2021-04-23 12:50:19 -07:00
Tom Moor 99b1bf0ecb fix: Avoid rare 'undefined is not a function' when attempting to register a server worker on Windows Chrome 2021-04-23 12:31:27 -07:00
Tom Moor 3b696cfa9a fix: Page reloads in Firefox when clicking some menu items (#2060)
* fix: Some context menu items result in page reload in Firefox

closes #1877

* fix: Display of sidebar link actions on hover
2021-04-23 12:25:15 -07:00
Tom Moor eb6acdae20 fix: CMD+F not working on screens with keyboard shortcut guide (#2066) 2021-04-23 12:10:02 -07:00
Tom Moor a818c7a924 fix: Hover card behind subheadings, previously it relied on being a portal without any explicit depth
closes #2062
2021-04-23 12:09:30 -07:00
Tom Moor d157e9bfcd 0.55.0 2021-04-22 20:23:24 -07:00
Tom Moor f2052c2a05 fix: Escape key in keyboard shortcut guide should clear search input if search term 2021-04-22 19:37:40 -07:00
Tom Moor 40b4270e35 chore: Faster source map in dev 2021-04-22 18:59:59 -07:00
Tom Moor 16c60a0d59 fix: URLSearchParams polyfill via core-js upgrade (#2059)
* fix: URLSearchParams polyfill via core-js upgrade

* deduplicate

* testing, remove manual imports

* chore: bump rme
2021-04-22 18:21:27 -07:00
Mark Steve Samson 1a183ba0fc Document and include PGSSLMODE in sample env file (#2052) 2021-04-21 18:15:23 -07:00
Tom Moor 2ffc0ae81c feat: New keyboard shortcuts guide (#2051)
* feat: Add search

* feat: New design for keyboard shortcuts guide
feat: Include quick search
fix: Add missing shortcuts

* tweaks

* fix: Two other spots that should trigger guide-style instead of modal

* sink,lift -> indent,outdent

* fix: Animation should slide out as well as in
2021-04-21 18:15:07 -07:00
Tom Moor 50fdd73610 fix: Remove HMR in test env (#2054) 2021-04-21 17:53:53 -07:00
dependabot[bot] a134773d4e chore(deps): bump ssri from 6.0.1 to 6.0.2 (#2050)
Bumps [ssri](https://github.com/npm/ssri) from 6.0.1 to 6.0.2.
- [Release notes](https://github.com/npm/ssri/releases)
- [Changelog](https://github.com/npm/ssri/blob/v6.0.2/CHANGELOG.md)
- [Commits](https://github.com/npm/ssri/compare/v6.0.1...v6.0.2)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-20 17:37:39 -07:00
Tom Moor 317c52df62 fix: Improve error handling for Azure-specific errors not captured in OAuth2 strategy 2021-04-18 22:41:27 -07:00
Tom Moor 04b8d7ae7b Normalize sidebar style 2021-04-18 21:52:54 -07:00
Tom Moor 3569d2fee7 chore: Update caniuse 2021-04-18 19:52:47 -07:00
Tom Moor ab267ce38d fix: Disable polling on custom domain, closes #2041 2021-04-18 18:58:26 -07:00
Tom Moor fa52bc5afd chore: Slack integration screen improvements (#2049)
* feat: Add collection iconography and colors to Slack settings page
fix: Use standardized list components
fix: Slack icon size
chore: Convert to translation strings

* fix: Missing translation, convert to Scene
2021-04-18 18:34:49 -07:00
Tom Moor bf668d6347 fix: Double documents.info request loading public share links 2021-04-18 11:34:11 -07:00
Tom Moor 7f9cba9819 feat: Record share link last accessed time (#2047)
* chore: Migrations

* chore: Add recording of share link views

* feat: Add display of share link accessed date in admin

* translations

* test

* translations, admin pagination
2021-04-18 09:38:13 -07:00
Tom Moor e9f083feb8 fix: Document title with slashes produces folders in exported zip file
closes #2036
2021-04-17 19:30:31 -07:00
Tom Moor 03d90b3f15 fix: Hide secondary actions in document header on mobile
closes #2042
2021-04-17 18:14:24 -07:00
Tom Moor 2432b4dcbd fix: Editor lightbox stacked below sidebar 2021-04-17 18:08:09 -07:00
Translate-O-Tron 2c2c1341f7 fix: New Spanish translations from Crowdin [ci skip] (#2035) 2021-04-17 13:24:17 -07:00
Tom Moor 7a8ccdb229 feat: Microsoft authentication (#1953)
closes #755
2021-04-17 13:22:18 -07:00
Tom Moor b2d703bee4 fix: Improved mobile styling
fix: Severla context menus miss-positioned
fix: Search filters not large enough on mobile
fix: Deep black background on mobile to match native apps
fix: Sticky document header allowing horizontal scrolling on mobile
2021-04-17 10:40:39 -07:00
Tom Moor c46a032f0b fix: CSS stacking context issue with behind menu backdrops on mobile
Moving the animation to the same element that has position: fixed resolves
2021-04-16 19:02:43 -07:00
Tom Moor 940ad8479e perf: Remove collaborators from documents.list response (#2039)
* fix: Remove unused, unperformant query

* lint

* collaborators -> collaboratorIds
2021-04-15 22:49:16 -07:00
Tom Moor c5401a467d fix: Overlapping header, closes #2038 2021-04-15 20:30:05 -07:00
Tom Moor 1dd97c1ddd feat: Show mobile-style (slide from bottom) menus on mobile (#2025)
* feat: Show mobile-style (slide from bottom) menus at responsive viewport sizes

* More mobile improvements

* fix: Safari compatability
2021-04-13 21:43:24 -07:00
Translate-O-Tron f37371c16e New Crowdin updates (#2027) 2021-04-13 21:31:35 -07:00
Tom Moor 62f9262b2c fix: Improved handling of authentication edge-cases (#2023)
* fix: authentication records not cleaned up for deleted user
closes #2022

* fix: Improve debugging for duplicate providerId sign-in requests
2021-04-11 19:39:31 -07:00
Saumya Pandey bc4fe05147 feat: Read-only users (#1955)
* Introduce isViewer field

* Update policies

* Make users read-only feature

* Remove not demoting current user validation

* Update tests

* Catch the unhandled promise rejection

* Hide unnecessary ui elements for read-only user

* Update app/scenes/Settings/People.js

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

* Remove redundant logic for admin only policies

* Use can logic

* Update snapshot

* Remove lint error

* Update snapshot

* Minor fix

* Update app/menus/UserMenu.js

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

* Update server/api/users.js

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

* Update app/components/DocumentListItem.js

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

* Update app/stores/UsersStore.js

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

* Use useCurrentTeam hook in functional component

* Update translation

* Update ternary

* Remove punctuation

* Move the functions to User model

* Update share policy and shareMenu

* Rename makeAdmin to promote

* Create updateCounts function and Rank enum

* Update tests

* Remove enum

* Use async await, remove enum and create computed accessor

* Remove unused variable

* Fix lint issues

* Hide templates

* Create shared/types and use rank type from it

* Delete shared/utils/rank type file

Co-authored-by: Tom Moor <tom.moor@gmail.com>
2021-04-11 19:39:17 -07:00
Tom Moor cdc7f61fa1 chore: Enable HMR for frontend code (#2024)
* chore: Enable HMR for frontend code
closes #2021

* revert
2021-04-11 15:09:00 -07:00
Tom Moor 2a6dfdea5d fix: Highlight states and dropzones when user does not have permission to import 2021-04-10 13:54:05 -07:00
Translate-O-Tron de25ea0ed9 fix: New Chinese Simplified translations from Crowdin [ci skip] (#2020) 2021-04-09 08:43:23 -07:00
Tom Moor d2227a2488 Update stale.yml 2021-04-08 22:56:22 -07:00
Translate-O-Tron 3e050727cb fix: New Chinese Simplified translations from Crowdin [ci skip] (#2019) 2021-04-08 21:20:10 -07:00
Tom Moor 326518873e fix: Logout suspended users immediately 2021-04-08 21:04:34 -07:00
Translate-O-Tron ed779a250f New Crowdin updates (#2011) 2021-04-08 20:40:34 -07:00
Tom Moor 190f0b6dc5 fix: Improve handling of suspended users signing in with email (#2012)
* chore: Separate signin/auth middleware
fix: Email signin token parsed by JWT middleware
fix: Email signin marked as active when logging in as suspended
fix: Suspended email signin correctly redirected to login screen
closes #1740

* refactor middleware -> lib

* lint
2021-04-08 20:40:04 -07:00
Tom Moor 1a889e9913 fix: Add embed support for lucid.app domain
closes #2017
2021-04-07 21:48:45 -07:00
Tom Moor b3203857d7 Create FUNDING.yml 2021-04-06 19:32:07 -07:00
Tom Moor 5762fb33d9 chore: Improve display of configuration errors (#2014)
* chore: Show all configuration errors at once
fix: Remove requirement for deprecated Slack key
fix: Add requirement for UTILS_SECRET

* chore: Add funding/sponsorship message
2021-04-06 19:29:59 -07:00
Tom Moor 1101ea428b feat: Drop to import onto collection scene (#2005)
* Refactor to functional component

* feat: Basic drag and drop into collection
2021-04-05 19:05:27 -07:00
Translate-O-Tron b4213e498c New Crowdin updates (#1951) 2021-04-05 18:28:27 -07:00
Saumya Pandey f9f76d4438 Format local development instructions (#2007) 2021-04-05 17:19:44 -07:00
Tom Moor 4a9571a174 fix: Alignment of backlinks and references (#2006)
closes #1998
2021-04-05 17:19:31 -07:00
Tom Moor bf856dbafa Merge branch 'main' of github.com:outline/outline 2021-04-04 11:04:46 -07:00
Nilay Sharma 0e54302d75 stretch email login input (#2004)
* stretch email login input

* Update InputLarge.js

Co-authored-by: Tom Moor <tom.moor@gmail.com>
2021-04-04 09:57:44 -07:00
Tom Moor 4777176d84 fix: User afterCreate hook using deprecated column 2021-04-03 18:37:39 -07:00
Tom Moor 3ffa21b07f fix: Unneeded object keys in API response 2021-04-03 17:18:26 -07:00
Tom Moor 8cbc873451 chore: Clarify flow on /create page 2021-04-03 14:29:08 -07:00
Tom Moor d2e8311b39 chore: Add tracking ref to branding on share links 2021-03-31 21:52:49 -07:00
Tom Moor 810257bcf5 fix: Improve dark mode styling
fix: Improve user and group list styling
fix: Member list reload when changing permissions, closes #1999
2021-03-31 20:57:30 -07:00
Tom Moor 2ef0caba88 fix: Server error when invalid 'sort' field is passed from an API client (#2000) 2021-03-31 18:54:02 -07:00
Tom Moor 2e64972574 fix: Error in shares.info endpoint when user originally creating share has been deleted 2021-03-31 18:04:40 -07:00
Tom Moor 7e1b07ef98 feat: Add read-only collections (#1991)
closes #1017
2021-03-30 21:02:08 -07:00
Tom Moor d7acf616cf chore: Bump rich-markdown-editor 2021-03-30 20:39:41 -07:00
Tom Moor c5569bd365 feat: Add /logout route for SLO support 2021-03-30 20:10:52 -07:00
Tom Moor 25023fb086 chore: Fix modal nesting, remove react-modal (#1996)
* chore: Fix modal nesting, remove react-modal

* tweak

* fix: Janky route jump when accessing Document -> Move from non-document scene
2021-03-30 18:46:14 -07:00
Tom Moor 07425f4243 Minor context menu visual updates 2021-03-29 22:28:40 -07:00
Tom Moor b5dcb1b3fe fix: JS error from #1962 refactor when Google or Slack auth credentials are missing 2021-03-29 22:03:40 -07:00
Tom Moor ae57cdea2a fix: GA should load on public share pages, closes #1992 2021-03-29 22:02:38 -07:00
Tom Moor 8599b60a6c fix: group -> createGroup 2021-03-26 20:17:46 -07:00
Tom Moor e00a437f2f feat: authenticationProviders API endpoints (#1962) 2021-03-26 11:31:07 -07:00
Tom Moor 626c94ecea fix: Bump react-dropzone for eroneous 'green plus' cursor fix
https://github.com/react-dropzone/react-dropzone/issues/1042
2021-03-26 11:17:50 -07:00
Tom Moor 889186e510 fix: Close appearance menu when selecting a theme
fix: Position disclosure correctly when menu has submenu
fix: More reliably close context menus
2021-03-26 11:15:58 -07:00
Tom Moor 4166257283 chore: Improved context menu behavior 2021-03-24 19:23:16 -07:00
Tom Moor 6a7d7af767 fix: Sticky archive header 2021-03-24 18:42:12 -07:00
Tom Moor 46912f8ddb fix: Single share record per document (#1984) 2021-03-24 18:28:38 -07:00
Tom Moor 877c01f723 feat: Show 'Edit' button when visiting share link as signed in user with permission (#1980) 2021-03-23 19:22:15 -07:00
Tom Moor 97158b1337 fix: Invisible list bullets in Firefox 2021-03-23 19:02:19 -07:00
Tom Moor 8d8bde4b8b closes #1784 2021-03-22 22:23:03 -07:00
Tom Moor 059fca27b3 buildUser -> buildAdmin 2021-03-22 20:59:11 -07:00
Tom Moor 9f6ba798c8 Merge branch 'main' of github.com:outline/outline 2021-03-22 20:51:45 -07:00
Tom Moor 349e971a8a chore: Serialize domain policies on team (#1970)
* domain policies exposed on team, consistency

* fix: Remove usage of isAdmin in frontend

* test
2021-03-22 20:50:12 -07:00
Tom Moor 9af9d3a008 chore: Bump ioredis for fixes
chore: Remove sequelize language from user-facing db migrations
2021-03-22 18:41:54 -07:00
Tom Moor bb5443452b flow 2021-03-21 18:52:42 -07:00
Tom Moor b3353f20d5 chore: Move error logging in passport 2021-03-21 18:36:10 -07:00
Tom Moor 200f25c4b2 0.54.0 2021-03-21 10:27:27 -07:00
Saumya Pandey f1296cc8e3 Add import document menuitem (#1966) 2021-03-20 23:20:49 -07:00
Tom Moor ad8c08497c fix: Moving document in private collection returns incorrect policies (#1969)
* fix: Return no policies when collection doesn't change
fix: Return correct policies when it does change

* test
2021-03-19 08:01:51 -07:00
Tom Moor 7891a8ee8b fix: Cannot create collection if all collections deleted 2021-03-19 08:01:13 -07:00
Tom Moor 56c4acc18f fix: Background on error details not correct in dark theme 2021-03-18 23:56:05 -07:00
Tom Moor 1b972070d7 feat: Enforce single team when self-hosted (#1954)
* fix: Enforce single team when self hosting

* test: positive case

* refactor

* fix: Visible error message on login screen for max teams scenario

* Update Notices.js

* lint
2021-03-18 21:56:24 -07:00
Nan Yu 138336639d fix: touch up UI for compact breadcrumbs (#1961) 2021-03-18 19:44:28 -07:00
Tom Moor 8ea746dbe8 Bump RME
fixes: Code does not highlight until editor is focused
fixes: Improved pasting behavior
fixes: Error when pasting iframes from elsewhere
2021-03-18 18:01:07 -07:00
Saumya Pandey 46bcc2e2ae feat: Allow sorting collections in sidebar (#1870)
closes #1759

Co-authored-by: Tom Moor <tom.moor@gmail.com>
2021-03-18 17:27:33 -07:00
Tom Moor b93002ad93 fix: JS error when pasting gist link, closes #1963 2021-03-16 22:53:32 -07:00
Tom Moor a427d77076 fix #1964 – error when selecting no files to import after previously selecting files 2021-03-16 22:09:19 -07:00
Nan Yu eff56b758c feat: use flexbox to center the floating title (#1959)
* use flexbox to center the floating title

* simplify breadcumb alignment

* small adjustments

* fix alignment on actions
2021-03-15 19:24:14 -07:00
Tom Moor ffc270b567 fix: Incorrect calculation of subdomain when previously used more than once 2021-03-15 18:04:41 -07:00
Saleh Yusefnejad d86b7babb9 fix: correct GoogleSheets component class name (#1960) 2021-03-15 15:36:10 -07:00
Tom Moor ec57951087 feat: Upgrade editor - move list items with keyboard 2021-03-14 15:58:50 -07:00
Tom Moor 2385f41a98 chore: Show 'private' badge on private collections (#1952)
* chore: Show 'private' badge on private collections

* lint

* fix: Missing translation mapping
2021-03-12 16:41:26 -08:00
Translate-O-Tron bdb684a4be New Crowdin updates (#1950)
* fix: New Chinese Simplified translations from Crowdin [ci skip]

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

* fix: New Chinese Simplified translations from Crowdin [ci skip]
2021-03-11 16:54:24 -08:00
Tom Moor 5d6f68d399 feat: Move to passport for authentication (#1934)
- Added `accountProvisioner`
- Move authentication to use passport strategies
- Make authentication more pluggable
- Change language of services -> providers

closes #1120
2021-03-11 10:02:22 -08:00
Tom Moor dc967be4fc chore: Syncs changes that were erroneously made in enterprise repo (#1949) 2021-03-10 14:56:34 -08:00
Saumya Pandey d530edcc2f fix: Remove double separator in collection menu (#1948)
* Hide separator

* Remove visible logic
2021-03-10 14:56:16 -08:00
Tom Moor 1393d1950e chore: Test performance and warnings (#1946)
* test: Do not request mailer account in test environment

* test: Dupe migrations
2021-03-10 12:04:42 -08:00
Tom Moor 0aa72036d7 fix: Templates should not be visible in collection structure 2021-03-10 09:39:42 -08:00
Tom Moor f50b88716b fix: Handle document Id in document structure does not exist in db
closes #1938
2021-03-09 22:45:40 -08:00
Tom Moor e90c02bec7 feat: Hide less popular services by default in block menu 2021-03-09 22:32:28 -08:00
Tom Moor 504b11576a bump 2021-03-09 22:04:47 -08:00
Tom Moor bac7a364d0 fix: Drag and drop images in editor, conflict with sidebar react-dnd (#1918)
* fix: Drag and drop images in editor, conflict with sidebar react-dnd
see: https://github.com/react-dnd/react-dnd/pull/3052

* Bump to non-canary

* Upgrade react-dnd

* react-dnd api changes

* lint

* fix: dnd doesn't work on first render

* remove unneccessary async

* chore: Update react-dnd (API changed again)

* restore fade
2021-03-09 18:41:30 -08:00
Tom Moor ed2a42ac27 chore: Migrate authentication to new tables (#1929)
This work provides a foundation for a more pluggable authentication system such as the one outlined in #1317.

closes #1317
2021-03-09 12:22:08 -08:00
Saumya Pandey ab7b16bbb9 fix: Centered document title moves when saving (#1943)
closes #1939
2021-03-09 12:21:29 -08:00
Translate-O-Tron d8eefc1972 New Crowdin updates (#1920) 2021-03-08 17:17:13 -08:00
dependabot[bot] b188a8ff30 chore(deps): bump elliptic from 6.5.3 to 6.5.4 (#1941)
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.3 to 6.5.4.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.3...v6.5.4)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-08 17:16:33 -08:00
Tom Moor e1c7b07af9 fix: Image in README was removed on Github's servers 2021-03-08 09:19:42 -08:00
Tom Moor eaadeb26e5 fix: Upgrade reakit for better context menu behavior 2021-03-01 19:17:24 -08:00
Tom Moor 0c301fcf0c fix: Enlarge scrollable area in sidebar for improved UX on small screens 2021-02-26 10:51:14 -08:00
Tom Moor a3e95023dc fix: Temporary fix for outline-icons build issue 2021-02-23 22:53:55 -08:00
Tom Moor e08b17561e fix: Capitalize Pусский 2021-02-23 10:35:50 -08:00
Tom Moor ac79a4c4cc fix: Update PWA macOS icon 2021-02-23 08:36:59 -08:00
Tom Moor e085553306 feat: Add tracking of PWA installs 2021-02-22 22:30:47 -08:00
Tom Moor 38bd1d5585 translate: Search filter dropdowns, modal 2021-02-22 22:18:00 -08:00
Tom Moor cd7cbab5ac fixes #1879 – race condition in websockets service 2021-02-22 21:53:20 -08:00
Tom Moor 2195787e7d logistics -> exporter, remove cutesy naming of lib 2021-02-22 21:51:01 -08:00
Tom Moor 04f942141f fix: Tone-down separator for PWA titlebar in dark mode 2021-02-21 22:42:22 -08:00
Tom Moor d0f1fd533a feat: Add Russian language support
closes #1916
2021-02-21 20:42:01 -08:00
Translate-O-Tron a1e885f057 New Crowdin updates (#1913) 2021-02-21 20:35:39 -08:00
Tom Moor 2ad9f69f7f fix: Scrollbars should match theme, closes #1917 2021-02-21 19:59:57 -08:00
Tom Moor 65bca35bbf Merge branch 'main' of github.com:outline/outline 2021-02-21 13:33:17 -08:00
Tom Moor a96993fda9 feat: Add PWA support to subdomains (#1915)
* fix: Remove overscroll
* Remove title from fixed header in PWA as it's displayed immediately above in application title
2021-02-21 13:32:49 -08:00
Tom Moor 9fc03b6ece fix: Cannot upload images into collection description (authentication failure) 2021-02-20 22:38:28 -08:00
Tom Moor 100360adb3 fix: Drop cursor not visible in dark theme 2021-02-20 22:35:07 -08:00
Tom Moor d277d80323 Merge pull request #1914 from outline/fix/issue-1896
fix: Documents in trash should still load their attachments
2021-02-20 13:35:41 -08:00
Tom Moor c79cfbd30d fix: Documents in trash should still load their attachments
closes #1896
2021-02-20 13:22:02 -08:00
Tom Moor e66611e771 fix: Error with search term including %, closes #1891 2021-02-20 13:03:41 -08:00
Tom Moor 903e83a618 feat: Batch Import (#1747)
closes #1846
closes #914
2021-02-20 12:36:05 -08:00
Tom Moor 4ef4ef963a i18n 2021-02-20 12:31:54 -08:00
Translate-O-Tron 51c6a19dc3 New Crowdin updates (#1906) 2021-02-19 09:28:47 -08:00
Tom Moor bbf434e2f4 fix: Disable 'Invite people…' control for non-admins (#1903)
closes #1902
2021-02-18 23:35:55 -08:00
Tom Moor 5b7018058d i18n 2021-02-18 23:30:48 -08:00
Tom Moor fae54c7957 fix: Mispositioned sticky headers in modals 2021-02-18 23:20:12 -08:00
Tom Moor fabfa6a491 Tweak language, remove original attachment once complete 2021-02-18 23:08:48 -08:00
Tom Moor c5f9412ac0 fix: Collection creator not written (bad merge from refactor while this branch has been open)
refactor: Move processing to async queue now that file can be loaded from external storage
2021-02-18 22:55:29 -08:00
Tom Moor f4c871bb62 i18n 2021-02-18 22:36:18 -08:00
Tom Moor df233c95a9 refactor: Upload file to storage, and then pass attachmentId to collections.import
This avoids having large file uploads going directly to the server and allows us to fetch it async into a worker process
2021-02-18 22:36:07 -08:00
Tom Moor 568e271738 lint 2021-02-18 21:05:21 -08:00
Tom Moor 9efed11a3e Merge branch 'main' of github.com:outline/outline into feat/mass-import 2021-02-18 20:57:01 -08:00
PedroSeda c30132e558 feat: Embed Cawemo (#1890) 2021-02-18 18:48:40 -08:00
Tom Moor b152a5595e Merge branch 'main' into feat/mass-import 2021-02-17 23:57:45 -08:00
Tom Moor 887e341e48 Add NODE_ENV to app.json 2021-02-17 19:38:44 -08:00
Tom Moor ae2f1b47e7 0.53.1 2021-02-17 00:08:04 -08:00
Tom Moor 86d9a14c5c fix: is virtual host 2021-02-16 23:52:25 -08:00
Tom Moor 6a8a83610f fix: Input data is not a String when clicking on new collection description 2021-02-16 23:42:31 -08:00
Tom Moor 54bf7a9dea fix: Restore specifying AWS endpoint for non-S3 support 2021-02-16 23:41:39 -08:00
Tom Moor 43ed7d0343 0.53.0 2021-02-16 21:18:06 -08:00
Tom Moor a81a18b173 fix: Remove hard-coded ServerSideEncryption on AWS, configure on AWS or storage provider 2021-02-16 00:16:23 -08:00
Tom Moor f18a2a048d fix: Sticky heading stacking 2021-02-16 00:15:04 -08:00
Tom Moor 7e922d8716 feat: Installable PWA (#1882) 2021-02-15 15:19:51 -08:00
Tom Moor 4b603460cb chore: Standardized headers (#1883)
* feat: Collection to standard header
feat: Sticky tabs

* chore: Document to standard header

* chore: Dashboard -> Home
chore: Scene component

* chore: Trash, Templates, Drafts

* fix: Mobile improvements

* fix: Content showing at sides and occassionally ontop of sticky headers
2021-02-14 13:18:33 -08:00
Tom Moor 32a298054d Bump RME – Editor fixes and improvements 2021-02-13 12:47:44 -08:00
Tom Moor ca2459361e chore: de-dupe lockfile 2021-02-13 09:25:04 -08:00
Tom Moor e49f3ab9fb ResizeObserver loop completed with undelivered notifications 2021-02-13 08:43:22 -08:00
Tom Moor e9338df057 Update README screenshot 2021-02-12 20:29:43 -08:00
Tom Moor 2629d6db23 fix: 'Suspended' badge misaligned on user profiles
closes #1880
2021-02-12 17:34:40 -08:00
Tom Moor b017590033 fix: 'bake' release env variables at build time 2021-02-12 17:18:55 -08:00
Tom Moor 7d244dfa1f 'bake' release env variables at build time 2021-02-12 16:53:16 -08:00
Tom Moor 2a225d81d2 chore: Update Sentry to avoid duplicate packages
chore: Pass current release version to Sentry and Datadog
2021-02-12 16:39:02 -08:00
Tom Moor 41df5c74be Add description -> Add a description 2021-02-12 16:24:31 -08:00
Tom Moor ef026b34fa Return to App -> Back to App 2021-02-12 16:21:32 -08:00
Tom Moor 1dbcc12648 feat: Inline collection editing (#1865) 2021-02-12 16:20:49 -08:00
Tom Moor 2611376b21 chore: Add optional DD tracer 2021-02-11 18:58:56 -08:00
Tom Moor a1b3cfc7de Yarn.lock 2021-02-10 20:25:26 -08:00
Tom Moor 5a478ec127 fix: Incorrect policy returned after document create/import 2021-02-09 21:29:24 -08:00
Tom Moor c0325fcaf3 Merge branch 'main' into feat/mass-import 2021-02-09 20:46:57 -08:00
Tom Moor df472ac391 feat: add total users to people management screen (#1878)
* feat: add total users to pagination

* move this.total in runInAction callback

* add total counts + counts to people tabs

* progress: use raw pg query

* progress: add test

* fix: SQL interpolation

* Styling and translation of People page

Co-authored-by: Tim <timothychang94@gmail.com>
2021-02-09 20:13:09 -08:00
Tom Moor 097359bf7c feat: Added ability to disable sharing at collection (#1875)
* feat: Added ability to disable sharing at collection

* fix: Disable all previous share links when disabling collection share
Language

* fix: Disable document sharing for read-only collection members

* wip

* test

* fix: Clear policies after updating sharing settings

* chore: Less ambiguous language

* feat: Allow setting sharing choice on collection creation
2021-02-09 19:04:03 -08:00
Tom Moor 3739bb7c55 Update translation.json 2021-02-08 21:50:34 -08:00
Tom Moor cc90c8de1c feat: Sidebar Improvements (#1862)
* wip

* refactor behaviorg

* stash

* simplify
2021-02-07 21:51:56 -08:00
Tom Moor ac6c48817c fix: Unable to select .md files by default on some machines depending on installed software 2021-02-07 16:14:03 -08:00
Tom Moor 8e3534dcbc fix: File import via collection menu regression 2021-02-07 16:13:44 -08:00
Tom Moor cada91a135 Merge main 2021-02-07 12:58:17 -08:00
Yaroslav Zhavoronkov e2d7d34f30 fix: Pass credentials with API requests when required to work with Cloudflare Access (#1867) 2021-02-06 22:49:49 -08:00
Tom Moor 0d88a1dfda Update README.md 2021-02-06 21:49:07 -08:00
Tom Moor df5a2e45c5 chore: Improved deployment documentation (#1868) 2021-02-06 21:33:56 -08:00
Tom Moor 1a7a48674b fix: link in README, add ARCHITECTURE document 2021-02-06 17:46:54 -08:00
Tom Moor e23474fa1c feat: Add parameters for filtering events (#1863)
* feat: Add parameters for filtering events

* test
2021-02-04 20:20:56 -08:00
Tom Moor 37fa13d841 fix: flash of 'Deleted Collection' when loading app on doc page 2021-02-02 22:03:02 -08:00
Tom Moor 6d88c02869 chore: Remove unused Popover component 2021-02-02 21:17:17 -08:00
Tom Moor a2fb3bb9f8 fix: Favicon should load from domain root, not current path 2021-02-02 21:13:11 -08:00
Tom Moor fb0b38fb71 fix: Mobile menu toggle button appearing in print media, closes #377 2021-02-02 20:57:08 -08:00
Tom Moor 8ff2f41068 Merge branch 'develop' of github.com:outline/outline into develop 2021-02-01 21:13:51 -08:00
Tom Moor 334dce7984 chore: Add Timing-Allow-Origin header (#1860) 2021-02-01 21:13:44 -08:00
Tom Moor 61b303831f fix: Document history sidebar layout issue 2021-02-01 21:13:31 -08:00
Tom Moor a9d60d288e feat: Automatically scroll to active item in sidebar (#1858) 2021-02-01 19:29:54 -08:00
Translate-O-Tron 3f267d7745 New Crowdin updates (#1848) 2021-01-31 23:24:10 -08:00
Tom Moor e845652cb8 flow: Convert to different <Trans> component syntax for flow compatability 2021-01-31 21:14:14 -08:00
Tom Moor 7066a45323 feat: Improve star/unstarred iconography 2021-01-31 20:53:27 -08:00
Tom Moor 654fdf1c7e fix: Guard unset language 2021-01-31 15:41:33 -08:00
Tom Moor 2a5fd0b332 test 2021-01-31 14:47:28 -08:00
Tom Moor 9ba63c6054 feat: Show nested document count on document list items on collection home 2021-01-31 14:41:18 -08:00
Tom Moor 785e208c6c lint 2021-01-31 14:41:00 -08:00
Tom Moor 9d84652dff fix: Frontend translation library expects dash separated, not underscore separated languages – this fix is required to enable working pluralization 2021-01-31 14:40:50 -08:00
Tom Moor ef6ce72cf5 fix: Recently published redirect 2021-01-31 13:01:56 -08:00
Tom Moor 7777cccf3b fix: Save regression from flow refactor 2021-01-31 12:53:52 -08:00
Tom Moor 620e4942d8 feat: Update default collection tab (#1821)
* feat: Allow listing root level documents only via documents.list

* feat: New tab on collection home

* update tab layout

* fix: Correctly sort index sorted documents.list

* revert: Tab layout changes

* fix: Missing route for recently published
fix: Redirect unknown tabs
2021-01-31 12:37:27 -08:00
Tom Moor 91ee3e62f2 fix: Reassign user on unpublish (#1857)
* findOne -> findByPk
2021-01-30 18:31:08 -08:00
Tom Moor eeb7650941 fix: New documents should sort to the top of manually organized collection 2021-01-30 00:18:56 -08:00
Tom Moor ee57f1ccf5 fix 2021-01-29 23:59:48 -08:00
Tom Moor 32f0589190 chore: Upgrade flow (#1854)
* wip: upgrade flow

* chore: More sealed props improvements

* Final fixes
2021-01-29 21:36:09 -08:00
Tom Moor ce2b246e60 fix: auth.config request should only be made on Login screen (#1852) 2021-01-29 17:54:28 -08:00
Tom Moor ae13347d55 chore: Add flow support for M1 macs 2021-01-28 23:25:37 -08:00
Tom Moor 13205171d7 chore: Improve dev efficient on M1 Mac 2021-01-28 21:01:53 -08:00
Tom Moor a912ea24f6 chore: Remove references to window.Sentry 2021-01-27 22:56:40 -08:00
Tom Moor 6fa760688b fix: Adds support for VirtualHost style AWS S3 buckets (#1847)
* Bump aws-sdk

* support virtual host buckets

* fix

* fix: VirtualHost bucket without explicit AWS_S3_FORCE_PATH_STYLE=false
2021-01-27 07:46:43 -08:00
Tom Moor 2825df29de Merge branch 'develop' of github.com:outline/outline into develop 2021-01-25 20:48:42 -08:00
Translate-O-Tron 604e97a6ce New Crowdin updates (#1835)
* fix: New French translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: New Chinese Simplified translations from Crowdin [ci skip]
2021-01-25 20:48:18 -08:00
milesstoetzner dc1bc44c8f feat: Google Drawings Integration (#1814)
* add google drawings integration

* add google drawings image

* update google drawings image and regex

* allow query parameter in google drawings regex

* support CDN for google drawings image
2021-01-25 20:47:23 -08:00
Tom Moor 2a55e78722 fix: Improve some editor alignment 2021-01-25 20:36:20 -08:00
Tom Moor eaf8dc5a3c fix: Text highlight of link in dark mode is impossible to read
closes #1838
2021-01-24 22:47:27 -08:00
Tom Moor f89d5adc37 fix: Ellipisis left in translation string 2021-01-24 12:09:44 -08:00
Tom Moor 978a123122 fix: 16 linting warnings 2021-01-23 10:19:08 -08:00
Tom Moor 96e65f495e chore: Remove custom VisuallyHidden component 2021-01-23 09:47:02 -08:00
Tom Moor 4106f15450 fix: Content jump when leaving edit mode 2021-01-22 23:58:34 -08:00
Tom Moor b3cd78c833 chore: Enable parameterized route profiling 2021-01-22 23:02:12 -08:00
Tom Moor 7b87fea4f4 Merge branch 'develop' of github.com:outline/outline into develop 2021-01-22 21:16:37 -08:00
Tom Moor 7e9bcb0c37 fix: More missing a11y labels 2021-01-22 21:12:25 -08:00
Tom Moor f6370ccf6d chore: Sentry performance monitoring (#1841)
* Hook up performance monitoring

* lint
2021-01-22 20:42:45 -08:00
Tom Moor 11e1108f4a fix: Unneccessary ev.preventDefault 2021-01-22 20:40:26 -08:00
Tom Moor c9fdf48c33 chore: Add missing labels to buttons without text and search inputs 2021-01-22 19:31:30 -08:00
Tom Moor 6a206de6cd chore: Add meta description 2021-01-22 19:12:39 -08:00
Tom Moor c69b393776 fix: JS error when submitting invites from sidebar-triggered modal 2021-01-22 08:57:52 -08:00
Tom Moor 6e9c456147 isMetaKey -> isModKey 2021-01-21 07:28:10 -08:00
Tom Moor 70626ffff0 feat: Organize sidebar (#1834)
* chore: Flip chinese label in language select

* feat: Add settings to sidebar, organize secondary items to bottom
2021-01-21 07:22:20 -08:00
Translate-O-Tron 993aad004e fix: New Korean translations from Crowdin [ci skip] (#1833) 2021-01-21 07:21:23 -08:00
Tom Moor 6fa9e700c8 chore: Flip chinese label in language select 2021-01-20 23:20:06 -08:00
Tom Moor 836b2e310a chore: Missing translation hooks in settings sidebar 2021-01-20 23:13:51 -08:00
Tom Moor 24ecaa8ce4 chore: Reduce default menu width 2021-01-20 23:07:48 -08:00
Tom Moor 40491fafe9 fix: Document star/unstar from list item navigates to document (regression) 2021-01-20 23:07:39 -08:00
Tom Moor 111212b038 feat: Resizable sidebar (#1827)
* wip: First round on sidebar resizing

* feat: Saving setting, animation

* all requirements, refactoring needed

* lint

* refactor useResize

* some mobile improvements

* fix

* refactor
2021-01-20 23:00:14 -08:00
Translate-O-Tron 774c3534d8 fix: New Chinese Simplified translations from Crowdin [ci skip] (#1832) 2021-01-20 22:23:30 -08:00
Malek Hijazi 9759227d73 fix: upgrade command (#1830)
I tested this on the server. Running yarn upgrade will result in yarn self updating. To solve this issue we need to run yarn run upgrade.
2021-01-20 22:19:44 -08:00
Tom Moor f608872c11 chore: Add Chinese and Italian translations 2021-01-20 22:09:36 -08:00
Translate-O-Tron eff9544ef9 New Crowdin updates (#1810)
* fix: New Chinese Simplified translations from Crowdin [ci skip]

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

* fix: New Italian translations from Crowdin [ci skip]
2021-01-20 21:36:03 -08:00
Tom Moor 22fb464b87 lint 2021-01-18 16:11:48 -08:00
Tom Moor 3bace8c9e4 fix: Restore DNS prefetching for static resources (#1820)
* fix: Restore DNS prefetching for static resources

* fix: CDN paths
feat: preload instead of prefetch for key bundles

* csp

* fix: Turns out prefetch-src is still behind a flag in Chrome, not publicly available yet
2021-01-18 15:48:46 -08:00
Tom Moor 27fca28450 fix: Account for rehydrated old users before language
closes #1819
2021-01-17 22:19:54 -08:00
Tom Moor afcce7a0ef fix: Add missing width/height tags to img 2021-01-17 21:49:51 -08:00
Tom Moor f33495dddc fix: Editor mod shortcuts not working on Windows
closes #1745
2021-01-17 18:32:32 -08:00
Tom Moor 51b75fa09d 0.52.0 2021-01-16 14:12:35 -08:00
Tom Moor 522df125aa feat: Add CDN support (#1817)
* chore: CSP

* chore: Optionally use CDN for serving images
2021-01-16 11:12:10 -08:00
Tom Moor 1fd2ec31fd fix: Heading positioning changing between edit/read-only
fix: List items beyond #9 chopped
2021-01-15 08:50:19 -08:00
Tom Moor 1af00a0b3d test 2021-01-14 20:15:46 -08:00
Tom Moor ab40545a01 lint 2021-01-14 20:11:04 -08:00
Tom Moor c8d305aeca fix: Unintended scroll reset when switching between view / edit (#1807)
* fix: Don't remount document when switching between edit/read-only
fix: Button vertical alignment when using as=Link

* fix: Bump RME, fixes issue with image behavior changing between read-only/edit without editor remount

* fix: Heading anchor positioning
2021-01-14 19:50:10 -08:00
Maximilian Zinke 68a65be135 feat: Embed Google Drive (#1804)
* implement google drive extension

* add current logo of google drive

* fix issue when posting gdrive links which are already a preview

* always only show the preview

* Add bottom bar to get to original url for Google Drive embeds

Co-authored-by: Tom Moor <tom.moor@gmail.com>
2021-01-14 19:49:56 -08:00
Tom Moor b4d307b3b4 fix: Confusing breadcrumb collapsing 2021-01-14 19:36:31 -08:00
Tom Moor 03cb6d66e7 fix: Alignment of collection icon in header when collection name is very long 2021-01-14 18:59:43 -08:00
Tom Moor 7b8cbc50d5 fix: Document meta unclickable when first item in a document is a heading 2021-01-14 18:51:58 -08:00
Tom Moor f501da9c0f flow 2021-01-14 18:38:02 -08:00
Translate-O-Tron 74a762a7c7 New Crowdin updates (#1790) 2021-01-14 09:08:38 -08:00
Rubén Moya 93ac9892d5 fix: take into account user lang in Time component (#1793)
This PR takes into account the user selected language to format the time in the Time component.

Co-authored-by: tommoor <tom.moor@gmail.com>
2021-01-14 09:08:14 -08:00
Tom Moor e8b7782f5e fix: Keyboard accessible context menus (#1768)
- Makes menus fully accessible and keyboard driven
- Currently adds 2.8% to initial bundle size due to the inclusion of Reakit and its dependency, popperjs.
- Converts all menus to functional components
- Remove old custom menu system
- Various layout and flow improvements around the menus

closes #1766
2021-01-13 22:00:25 -08:00
Rubén Moya 47369dd968 chore: rename collection creatorId to createdById (#1794) 2021-01-11 23:17:31 -08:00
Tom Moor d258082c5f lint 2021-01-11 18:25:21 -08:00
Tom Moor c0bbae50c4 fix: Search results not updated when changing filters 2021-01-11 00:50:44 -08:00
Tom Moor ac082e4a5f Merge branch 'develop' of github.com:outline/outline into develop 2021-01-11 00:47:48 -08:00
Tom Moor 7504d43452 fix: Add indicator of starred status when viewing a document (#1785)
* fix: Add indicator of starred status when viewing a document
closes #461

* fix: Account for shared document
2021-01-10 23:13:58 -08:00
Tom Moor 5dba68dfd3 fix: Incorrect border color on table of contents in dark mode 2021-01-07 23:50:28 -08:00
Tom Moor 4b85603f30 chore: Text highlight blue -> yellow 2021-01-07 23:25:14 -08:00
Tom Moor 34598b317d fix: Deleting a collection should not deleted archived documents within it automatially (#1776)
closes #1775
2021-01-07 19:46:12 -08:00
Translate-O-Tron cbfa25fa2f New Crowdin updates (#1736)
* fix: New Japanese translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: New Chinese Simplified translations from Crowdin [ci skip]
2021-01-07 08:09:44 -08:00
Tom Moor 67a2246e1a fix: Attempting to restore document in deleted collection without a collectionId override results in server error (#1777)
closes #1767
2021-01-07 08:09:19 -08:00
Tom Moor de7bf8c133 fix: Fixes padding on sidebar collection links
closes #1770
2021-01-06 20:35:02 -08:00
Tom Moor 4fda50f4ad feat: Add 'archive' option in delete confirmation modal (#1764)
* feat: Add 'archive' option in delete confirmation modal
chore: Add translation tags to delete confirmation

* i18n

* language
2021-01-03 11:04:09 -08:00
Tom Moor f4c5cc054e chore: Update sorting icons 2021-01-03 09:38:29 -08:00
Tom Moor f799758a6f feat: Allow Google sign-in users to choose account
Alternative to https://github.com/outline/outline/pull/1763
2021-01-03 08:54:47 -08:00
Tom Moor 9df02d6fd4 chore: Improve toasts 2021-01-02 21:47:02 -08:00
Tom Moor bb81aa0065 fix: Improve toast messages to not show multiple of the same 2021-01-02 21:09:43 -08:00
Tom Moor 68bbd9fa34 fix: Hold hover state on DocumentListItem while DocumentMenu is open 2021-01-02 20:02:57 -08:00
Tom Moor 308d4bd797 i18n 2021-01-02 19:19:45 -08:00
Tom Moor 5329474c85 fix: Developer warning batchingForReactDom 2021-01-02 19:13:11 -08:00
Tom Moor d90af48741 fix: Outer error boundary generates more errors as it doesnt have access to store and theme providers 2021-01-02 19:12:51 -08:00
Tom Moor 611e9b97b3 chore: Move collection sort control (#1762)
* feat: Move collection sort menu

* fix: Improve accessibility

* fix: Dedupe outline-icons (temporary until rme is next merged)
2021-01-02 19:11:13 -08:00
Nan Yu eda5adca2c feat: adds hover to expand functionality on sidebar (#1761)
* feat: adds hover to expand functionality on sidebar

* clear hover intent timeout on drag leave
2021-01-02 17:20:13 -08:00
Tom Moor f0b361158e flow 2021-01-02 09:09:06 -08:00
Tom Moor f8ab793053 fix: 'New' badge should never show to document creator, regardless of whether a view has been logged (#1758) 2020-12-31 16:46:33 -08:00
Nan Yu 2cc45187e6 feat: reordering documents in collection (#1722)
* tweaking effect details

* wrap work on this feature

* adds correct color to drop cursor

* simplify logic for early return

* much better comment so Tom doesn't fire me

* feat: Allow changing sort order of collections

* refactor: Move validation to model
feat: Make custom order the default (in prep for dnd)

* feat: Add sort choice to edit collection modal
fix: Improved styling of generic InputSelect

* fix: Vertical space left after removing previous collection description

* chore: Tweak language, menu contents, add auto-disclosure on sub menus

* only show drop-to-reorder cursor when sort is set to manual

Co-authored-by: Tom Moor <tom.moor@gmail.com>
2020-12-31 12:51:12 -08:00
Tom Moor ba61091c4c fix: Allow soft deletion of teams (#1754)
* fix: Allow soft deletion of teams

* test: regression specs
2020-12-30 09:40:23 -08:00
Tom Moor 8dba32b5e0 fix: Meta key shortcuts not bound correctly in Windows browsers (#1753) 2020-12-30 09:35:33 -08:00
Clifton Cunningham 40bd9aed0a fix: miro - use the incoming domain to ensure access to logged in boards works (#1756) 2020-12-30 09:35:18 -08:00
Tom Moor d4bb04e921 fix: Handle linked documents destroyed when document is published
closes #1739
2020-12-29 10:32:09 -08:00
Nan Yu 8a3a279c0e Merge branch 'develop' of github.com:outline/outline into develop 2020-12-28 21:35:37 -08:00
Nan Yu 37f2cc8d55 closes #1752 2020-12-28 21:35:13 -08:00
Gustavo Maronato 89903b4bbe feat: Compress avatar images before upload (#1751)
* compress avatar images before upload

* move compressImage to dedicated file

* Update ImageUpload.js
2020-12-28 21:08:10 -08:00
Tom Moor 41be18e938 test 2020-12-28 20:43:27 -08:00
Tom Moor caee7afde2 refactor: documents.batchImport -> collections.import 2020-12-28 18:51:12 -08:00
Tom Moor d79933887d fix: Don't trigger email and slack notifications when mass importing
feat: Show success message after import
2020-12-28 18:02:58 -08:00
Tom Moor 2787e56de3 test: Add additional tests and input validation 2020-12-28 15:30:01 -08:00
Tom Moor b932457fd3 fix: Improve single collection export compatability 2020-12-28 10:07:38 -08:00
Tom Moor ea5d2ea9e0 refactor, add preview 2020-12-27 23:00:26 -08:00
Tom Moor 6e9b4e8363 lint 2020-12-27 12:54:58 -08:00
Tom Moor 012e6b320e feat: Allow document metadata to be stored in zip comment 2020-12-27 12:36:06 -08:00
Tom Moor c8cd7fcf4a fix: API response 2020-12-26 23:12:22 -08:00
Tom Moor 7021c2a9e5 Hook up API 2020-12-26 17:53:56 -08:00
Tom Moor 799e639439 Merge branch 'feat/import-export' into feat/mass-import 2020-12-25 18:05:02 -08:00
Tom Moor ba2552f69f fix 2020-12-25 18:04:38 -08:00
Malek Hijazi b6ab816bb3 feat: command to upgrade outline (#1727)
* Add upgrade script to package.json

* Update the docs to include docker and yarn guides
2020-12-25 15:23:55 -08:00
Tom Moor ac1120914a fix: Unable to delete archived and templated documents (#1749)
closes #1746
2020-12-24 13:28:08 -08:00
Tom Moor a51af98d43 refactor 2020-12-24 10:18:53 -08:00
Tom Moor ad7400a4f5 Merge branch 'develop' of github.com:outline/outline into feat/mass-import 2020-12-22 20:43:58 -08:00
Tom Moor ea57cef89c fix: Reduce double reporting of errors 2020-12-21 21:10:25 -08:00
Tom Moor 087ccdd825 stash 2020-12-21 21:03:11 -08:00
Translate-O-Tron 7d44e1aeeb New Crowdin updates (#1725)
* fix: New Japanese translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* styling, add keyboard shortcut

* tweak styling
2020-12-17 22:26:04 -08:00
Tom Moor b98e4bb1ff stash 2020-12-17 21:19:31 -08:00
Tom Moor 5012104a10 refactor 2020-12-16 21:39:37 -08:00
Nan Yu 051ecab0fc feat: Moving documents via drag and drop in sidebar (#1717)
* wip: added some basic drag and drop UI for combining items

* refactor: pathToDocument to accept only id

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

* Improving display while moving doc

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

* add move guard to drag

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* mock

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

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

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

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

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

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

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

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

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

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

* remove global styles import

* change bg on active item on drag as well

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

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

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

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

* Changing language single source of truth from TEAM to USER

* Changes according to @tommoor comments on PR

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

* Finished 1st MVP of i18n for outline

* new translation labels & Portuguese from Portugal translation

* Fixes from PR request

* Described language dropdown as an experimental feature

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

* Added useTranslation to Breadcrumb

* Repositioned <strong> element

* Removed extra space from TemplatesMenu

* Fortified the test suite for i18n

* Fixed trans component problematic

* Check if selected language is available

* Update yarn.lock

* Removed unused Trans

* Removing debug variable from i18n init

* Removed debug variable

* test: update snapshots

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

* translate: Drafts

* More translatable strings

* Mo translation strings

* translation: Search

* async translations loading

* cache translations in client

* Revert "cache translations in client"

This reverts commit 08fb61ce36.

* Revert localStorage cache for cache headers

* Update Crowdin configuration file

* Moved translation files to locales folder and fixed english text

* Added CONTRIBUTING File for CrowdIn

* chore: Move translations again to please CrowdIn

* fix: loading paths
chore: Add strings for editor

* fix: Improve validation on documents.import endpoint

* test: mock bull

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

* closes #1675

* Update CONTRIBUTING

* chore: Add link to translation portal from app UI

* refactor: Centralize language config

* fix: Ensure creation of i18n directory in build

* feat: Add language prompt

* chore: Improve contributing guidelines, add link from README

* chore: Normalize tab header casing

* chore: More string externalization

* fix: Language prompt in dark mode

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

* test

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

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

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

Co-authored-by: Tom Moor <tom.moor@gmail.com>
2020-11-18 19:09:08 -08:00
Tom Moor 18fac781a9 Update LICENSE 2020-11-14 23:10:16 -08:00
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 0125a5361d fix: Document published notification potentially sent to users without permission to view document 2020-11-05 19:49:05 -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 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 bd80e8384a chore: Remove events log from settings 2020-10-26 19:32:48 -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 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 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
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
669 changed files with 47702 additions and 16631 deletions
+2 -2
View File
@@ -6,10 +6,10 @@
"@babel/preset-env",
{
"corejs": {
"version": "2",
"version": "3",
"proposals": true
},
"useBuiltIns": "usage",
"useBuiltIns": "usage"
}
]
],
+101 -6
View File
@@ -1,9 +1,17 @@
version: 2
version: 2.1
executors:
docker-publisher:
environment:
IMAGE_NAME: outlinewiki/outline
docker:
- image: circleci/buildpack-deps:stretch
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 +37,99 @@ 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
build-image:
executor: docker-publisher
steps:
- checkout
- setup_remote_docker:
version: 20.10.6
- run:
name: Build Docker image
command: docker build -t $IMAGE_NAME:latest .
- run:
name: Archive Docker image
command: docker save -o image.tar $IMAGE_NAME
- persist_to_workspace:
root: .
paths:
- ./image.tar
publish-latest:
executor: docker-publisher
steps:
- attach_workspace:
at: /tmp/workspace
- setup_remote_docker:
version: 20.10.6
- run:
name: Load archived Docker image
command: docker load -i /tmp/workspace/image.tar
- run:
name: Publish Docker Image to Docker Hub
command: |
echo "$DOCKERHUB_PASS" | docker login -u "$DOCKERHUB_USERNAME" --password-stdin
IMAGE_TAG=${CIRCLE_TAG/v/''}
docker tag $IMAGE_NAME:latest $IMAGE_NAME:$IMAGE_TAG
docker push $IMAGE_NAME:latest
docker push $IMAGE_NAME:$IMAGE_TAG
publish-tag:
executor: docker-publisher
steps:
- attach_workspace:
at: /tmp/workspace
- setup_remote_docker:
version: 20.10.6
- run:
name: Load archived Docker image
command: docker load -i /tmp/workspace/image.tar
- run:
name: Publish Docker Image to Docker Hub
command: |
echo "$DOCKERHUB_PASS" | docker login -u "$DOCKERHUB_USERNAME" --password-stdin
IMAGE_TAG=${CIRCLE_TAG/v/''}
docker tag $IMAGE_NAME:latest $IMAGE_NAME:$IMAGE_TAG
docker push $IMAGE_NAME:$IMAGE_TAG
workflows:
version: 2
build-and-test:
jobs:
- build:
filters:
tags:
ignore: /^v.*/
build-docker:
jobs:
- build-image:
filters:
tags:
only: /^v.*/
branches:
ignore: /.*/
- publish-latest:
requires:
- build-image
filters:
tags:
only: /^v\d+\.\d+\.\d+$/
branches:
ignore: /.*/
- publish-tag:
requires:
- build-image
filters:
tags:
only: /^v\d+\.\d+\.\d+-.*$/
branches:
ignore: /.*/
+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
+115 -33
View File
@@ -1,61 +1,143 @@
# Copy this file to .env, remove this comment and change the keys. For development
# with docker this should mostly work out of the box other than setting the Slack
# keys (for auth) and the SECRET_KEY.
#
# Please use `openssl rand -hex 32` to create SECRET_KEY
# 👋 Welcome, we're glad you're setting up an installation of Outline. Copy this
# file to .env or set the variables in your local environment manually. For
# development with docker this should mostly work out of the box other than
# setting the Slack keys and the SECRET_KEY.
# –––––––––––––––– REQUIRED ––––––––––––––––
# Generate a hex-encoded 32-byte random key. You should use `openssl rand -hex 32`
# in your terminal to generate a random value.
SECRET_KEY=generate_a_new_key
# Generate a unique random key. The format is not important but you could still use
# `openssl rand -hex 32` in your terminal to produce this.
UTILS_SECRET=generate_a_new_key
# For production point these at your databases, in development the default
# should work out of the box.
DATABASE_URL=postgres://user:pass@localhost:5532/outline
DATABASE_URL_TEST=postgres://user:pass@localhost:5532/outline-test
# Uncomment this to disable SSL for connecting to Postgres
# PGSSLMODE=disable
REDIS_URL=redis://localhost:6479
# URL should point to the fully qualified, publicly accessible URL. If using a
# proxy the port in URL and PORT may be different.
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
FORCE_HTTPS=true
# To support uploading of images for avatars and document attachments an
# s3-compatible storage must be provided. AWS S3 is recommended for redundency
# however if you want to keep all file storage local an alternative such as
# minio (https://github.com/minio/minio) can be used.
ENABLE_UPDATES=true
DEBUG=cache,presenters,events
# Third party signin credentials (at least one is required)
SLACK_KEY=get_a_key_from_slack
SLACK_SECRET=get_the_secret_of_above_key
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# Comma separated list of domains to be allowed (optional)
# If not set, all Google apps domains are allowed by default
GOOGLE_ALLOWED_DOMAINS=
# Third party credentials (optional)
SLACK_VERIFICATION_TOKEN=PLxk6OlXXXXXVj3YYYY
SLACK_APP_ID=A0XXXXXXX
SLACK_MESSAGE_ACTIONS=true
GOOGLE_ANALYTICS_ID=
SENTRY_DSN=
# AWS credentials (optional in development)
# A more detailed guide on setting up S3 is available here:
# => https://wiki.generaloutline.com/share/125de1cc-9ff6-424b-8415-0d58c809a40f
#
AWS_ACCESS_KEY_ID=get_a_key_from_aws
AWS_SECRET_ACCESS_KEY=get_the_secret_of_above_key
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
# uploaded s3 objects permission level, default is private
# set to "public-read" to allow public access
AWS_S3_FORCE_PATH_STYLE=true
AWS_S3_ACL=private
# Emails configuration (optional)
# –––––––––––––– AUTHENTICATION ––––––––––––––
# Third party signin credentials, at least ONE OF EITHER Google, Slack,
# or Microsoft is required for a working installation or you'll have no sign-in
# options.
# To configure Slack auth, you'll need to create an Application at
# => https://api.slack.com/apps
#
# When configuring the Client ID, add a redirect URL under "OAuth & Permissions":
# https://<URL>/auth/slack.callback
SLACK_KEY=get_a_key_from_slack
SLACK_SECRET=get_the_secret_of_above_key
# To configure Google auth, you'll need to create an OAuth Client ID at
# => https://console.cloud.google.com/apis/credentials
#
# When configuring the Client ID, add an Authorized redirect URI:
# https://<URL>/auth/google.callback
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# To configure Microsoft/Azure auth, you'll need to create an OAuth Client. See
# the guide for details on setting up your Azure App:
# => https://wiki.generaloutline.com/share/dfa77e56-d4d2-4b51-8ff8-84ea6608faa4
AZURE_CLIENT_ID=
AZURE_CLIENT_SECRET=
AZURE_RESOURCE_APP_ID=
# –––––––––––––––– OPTIONAL ––––––––––––––––
# If using a Cloudfront/Cloudflare distribution or similar it can be set below.
# This will cause paths to javascript, stylesheets, and images to be updated to
# the hostname defined in CDN_URL. In your CDN configuration the origin server
# should be set to the same as URL.
CDN_URL=
# Auto-redirect to https in production. The default is true but you may set to
# false if you can be sure that SSL is terminated at an external loadbalancer.
FORCE_HTTPS=true
# Have the installation check for updates by sending anonymized statistics to
# the maintainers
ENABLE_UPDATES=true
# How many processes should be spawned. As a reasonable rule divide your servers
# available memory by 512 for a rough estimate
WEB_CONCURRENCY=1
# Override the maxium size of document imports, could be required if you have
# especially large Word documents with embedded imagery
MAXIMUM_IMPORT_SIZE=5120000
# You may enable or disable debugging categories to increase the noisiness of
# logs. The default is a good balance
DEBUG=cache,presenters,events,emails,mailer,utils,http,server,services
# Comma separated list of domains to be allowed to signin to the wiki. If not
# set, all domains are allowed by default when using Google OAuth to signin
ALLOWED_DOMAINS=
# For a complete Slack integration with search and posting to channels the
# following configs are also needed, some more details
# => https://wiki.generaloutline.com/share/be25efd1-b3ef-4450-b8e5-c4a4fc11e02a
#
SLACK_VERIFICATION_TOKEN=your_token
SLACK_APP_ID=A0XXXXXXX
SLACK_MESSAGE_ACTIONS=true
# Optionally enable google analytics to track pageviews in the knowledge base
GOOGLE_ANALYTICS_ID=
# Optionally enable Sentry (sentry.io) to track errors and performance
SENTRY_DSN=
# To support sending outgoing transactional emails such as "document updated" or
# "you've been invited" you'll need to provide authentication for an SMTP server
SMTP_HOST=
SMTP_PORT=
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_FROM_EMAIL=
SMTP_REPLY_EMAIL=
SMTP_TLS_CIPHERS=
SMTP_SECURE=true
# Custom logo that displays on the authentication screen, scaled to height: 60px
# TEAM_LOGO=https://example.com/images/logo.png
# The default interface language. See translate.getoutline.com for a list of
# available language codes and their rough percentage translated.
DEFAULT_LANGUAGE=en_US
+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
}
}
+6
View File
@@ -11,6 +11,10 @@
.*/node_modules/react-side-effect/.*
.*/node_modules/fbjs/.*
.*/node_modules/config-chain/.*
.*/node_modules/yjs/.*
.*/node_modules/y-prosemirror/.*
.*/node_modules/y-protocols/.*
.*/node_modules/lib0/.*
.*/server/scripts/.*
*.test.js
@@ -18,6 +22,7 @@
[options]
emoji=true
sharedmemory.heap_size=3221225472
module.system.node.resolve_dirname=node_modules
module.system.node.resolve_dirname=app
@@ -32,6 +37,7 @@ module.file_ext=.json
esproposal.decorators=ignore
esproposal.class_static_fields=enable
esproposal.class_instance_fields=enable
esproposal.optional_chaining=enable
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue
+3
View File
@@ -0,0 +1,3 @@
# These are supported funding model platforms
github: [outline]
+10
View File
@@ -0,0 +1,10 @@
# Set to true to add reviewers to pull requests
addReviewers: true
# A list of reviewers to be added to pull requests (GitHub user name)
reviewers:
- tommoor
# A list of keywords to be skipped the process that add reviewers if pull requests include it
skipKeywords:
- wip
+3 -2
View File
@@ -1,12 +1,13 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 60
daysUntilStale: 90
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
daysUntilClose: 14
# Issues with these labels will never be considered stale
exemptLabels:
- security
- pinned
# Label to use when marking an issue as stale
staleLabel: stale
+2 -1
View File
@@ -1,9 +1,10 @@
dist
build
node_modules/*
server/scripts
.env
.log
npm-debug.log
stats.json
.DS_Store
fakes3/*
.idea
+2 -1
View File
@@ -1,6 +1,7 @@
{
"javascript.validate.enable": false,
"javascript.format.enable": false,
"typescript.validate.enable": false,
"typescript.format.enable": false,
"editor.formatOnSave": true,
"typescript.format.enable": false
}
+67
View File
@@ -0,0 +1,67 @@
# Architecture
Outline is composed of a backend and frontend codebase in this monorepo. As both are written in Javascript, they share some code where possible. We utilize the latest ES6 language features, including `async`/`await`, and [Flow](https://flow.org/) typing. Prettier formatting and ESLint are enforced by CI.
## Frontend
Outline's frontend is a React application compiled with [Webpack](https://webpack.js.org/). It uses [MobX](https://mobx.js.org/) for state management and [Styled Components](https://www.styled-components.com/) for component styles. Unless global, state logic and styles are always co-located with React components together with their subcomponents to make the component tree easier to manage.
> Important Note: The Outline editor is built on [Prosemirror](https://github.com/prosemirror) and managed in a separate open source repository to encourage re-use: [rich-markdown-editor](https://github.com/outline/rich-markdown-editor).
```
app
├── components - React components reusable across scenes
├── embeds - Embed definitions that represent rich interactive embeds in the editor
├── hooks - Reusable React hooks
├── menus - Context menus, often appear in multiple places in the UI
├── models - State models using MobX observables
├── routes - Route definitions, note that chunks are async loaded with suspense
├── scenes - A scene represents a full-page view that contains several components
├── stores - Collections of models and associated fetch logic
├── types - Flow types
└── utils - Utility methods specific to the frontend
```
## Backend
The API server is driven by [Koa](http://koajs.com/), it uses [Sequelize](http://docs.sequelizejs.com/) as the ORM and Redis with [Bull](https://github.com/OptimalBits/bull) for queues and async event management. Authorization logic
is contained in [cancan](https://www.npmjs.com/package/cancan) policies under the "policies" directory.
Interested in more documentation on the API routes? Check out the [API documentation](https://getoutline.com/developers).
```
server
├── api - All API routes are contained within here
│ └── middlewares - Koa middlewares specific to the API
├── auth - Authentication logic
│ └── providers - Authentication providers export passport.js strategies and config
├── commands - We are gradually moving to the command pattern for new write logic
├── config - Database configuration
├── emails - Transactional email templates
│ └── components - Shared React components for email templates
├── middlewares - Koa middlewares
├── migrations - Database migrations
├── models - Sequelize models
├── onboarding - Markdown templates for onboarding documents
├── policies - Authorization logic based on cancan
├── presenters - JSON presenters for database models, the interface between backend -> frontend
├── services - Service definitions are triggered for events and perform async jobs
├── static - Static assets
├── test - Test helpers and fixtures, tests themselves are colocated
└── utils - Utility methods specific to the backend
```
## Shared
Where logic is shared between the client and server it is placed in this directory. This is generally
small utilities.
```
shared
├── i18n - Internationalization confiuration
│ └── locales - Language specific translation files
├── styles - Styles, colors and other global aesthetics
├── utils - Shared utility methods
└── constants - Shared constants
```
+43 -10
View File
@@ -1,17 +1,50 @@
FROM node:12-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
# syntax=docker/dockerfile:1.2
ARG APP_PATH=/opt/outline
FROM node:14-alpine AS deps-common
ARG APP_PATH
WORKDIR $APP_PATH
COPY . $APP_PATH
COPY ./package.json ./yarn.lock ./
RUN yarn install --pure-lockfile
# ---
FROM deps-common AS deps-dev
RUN yarn install --no-optional --frozen-lockfile && \
yarn cache clean
# ---
FROM deps-common AS deps-prod
RUN yarn install --production=true --frozen-lockfile && \
yarn cache clean
# ---
FROM node:14-alpine AS builder
ARG APP_PATH
WORKDIR $APP_PATH
COPY . .
COPY --from=deps-dev $APP_PATH/node_modules ./node_modules
RUN yarn build
RUN cp -r /opt/outline/node_modules /opt/node_modules
CMD yarn start
# ---
FROM node:14-alpine AS runner
ARG APP_PATH
WORKDIR $APP_PATH
ENV NODE_ENV production
COPY --from=builder $APP_PATH/build ./build
COPY --from=builder $APP_PATH/server ./server
COPY --from=builder $APP_PATH/public ./public
COPY --from=builder $APP_PATH/.sequelizerc ./.sequelizerc
COPY --from=deps-prod $APP_PATH/node_modules ./node_modules
COPY --from=builder $APP_PATH/package.json ./package.json
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001 && \
chown -R nodejs:nodejs $APP_PATH/build
USER nodejs
EXPOSE 3000
CMD ["yarn", "start"]
+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.55.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: 2024-04-22
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
+106 -97
View File
@@ -6,37 +6,88 @@
<p align="center">
<i>An open, extensible, wiki for your team built using React and Node.js.<br/>Try out Outline using our hosted version at <a href="https://www.getoutline.com">www.getoutline.com</a>.</i>
<br/>
<img src="https://user-images.githubusercontent.com/380914/78513257-153ae080-775f-11ea-9b49-1e1939451a3e.png" alt="Outline" width="800" />
<img width="1640" alt="screenshot" src="https://user-images.githubusercontent.com/380914/110356468-26374600-7fef-11eb-9f6a-f2cc2c8c6590.png">
</p>
<p align="center">
<a href="https://circleci.com/gh/outline/outline" rel="nofollow"><img src="https://circleci.com/gh/outline/outline.svg?style=shield&amp;circle-token=c0c4c2f39990e277385d5c1ae96169c409eb887a"></a>
<a href="https://github.com/prettier/prettier"><img src="https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat"></a>
<a href="https://github.com/styled-components/styled-components"><img src="https://img.shields.io/badge/style-%F0%9F%92%85%20styled--components-orange.svg"></a>
<a href="https://translate.getoutline.com/project/outline"><img src="https://badges.crowdin.net/outline/localized.svg"></a>
</p>
This is the source code that runs [**Outline**](https://www.getoutline.com) and all the associated services. If you want to use Outline then you don't need to run this code, we offer a hosted version of the app at [getoutline.com](https://www.getoutline.com).
If you'd like to run your own copy of Outline or contribute to development then this is the place for you.
## Installation
# Installation
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
### Development
## Self-Hosted Production
In development you can quickly get an environment running using Docker by following these steps:
### Docker
For a manual self-hosted production installation these are the recommended steps:
1. First setup Redis and Postgres servers, this is outside the scope of the guide.
1. Download the latest official Docker image, new releases are available around the middle of every month:
`docker pull outlinewiki/outline`
1. Using the [.env.sample](.env.sample) as a reference, set the required variables in your production environment. You can export the environment variables directly, or create a `.env` file and pass it to the docker image like so:
`docker run --env-file=.env outlinewiki/outline`
1. Setup the database with `yarn db:migrate`. Production assumes an SSL connection to the database by default, if
Postgres is on the same machine and is not SSL you can migrate with `yarn db:migrate --env=production-ssl-disabled`, for example:
`docker run --rm outlinewiki/outline yarn db:migrate`
1. Start the container:
`docker run outlinewiki/outline`
1. Visit http://you_server_ip:3000 and you should be able to see Outline page
> Port number can be changed using the `PORT` environment variable
1. (Optional) You can add an `nginx` or other reverse proxy to serve your instance of Outline for a clean URL without the port number, support SSL, etc.
### Terraform
Alternatively a community member maintains a script to deploy Outline on Google Cloud Platform with [Terraform & Ansible](https://github.com/rjsgn/outline-terraform-ansible).
### Upgrading
#### Docker
If you're running Outline with Docker you'll need to run migrations within the docker container after updating the image. The command will be something like:
```shell
docker run --rm outlinewiki/outline:latest yarn db:migrate
```
#### Git
If you're running Outline by cloning this repository, run the following command to upgrade:
```shell
yarn run upgrade
```
## Local Development
For contributing features and fixes you can quickly get an environment running using Docker by following these steps:
1. Install these dependencies if you don't already have them
1. [Docker for Desktop](https://www.docker.com)
1. [Node.js](https://nodejs.org/) (v12 LTS preferred)
1. [Yarn](https://yarnpkg.com)
1. [Docker for Desktop](https://www.docker.com)
1. [Node.js](https://nodejs.org/) (v12 LTS preferred)
1. [Yarn](https://yarnpkg.com)
1. Clone this repo
1. Register a Slack app at https://api.slack.com/apps
1. Copy the file `.env.sample` to `.env`
@@ -45,45 +96,64 @@ In development you can quickly get an environment running using Docker by follow
1. `SLACK_KEY` (this is called "Client ID" in Slack admin)
1. `SLACK_SECRET` (this is called "Client Secret" in Slack admin)
1. Configure your Slack app's Oauth & Permissions settings
1. Add `http://localhost:3000/auth/slack.callback` as an Oauth redirect URL
1. Slack recently prevented the use of `http` protocol for localhost. For local development, you can use a tool like [ngrok](https://ngrok.com) or a package like `mkcert`. ([How to use HTTPS for local development](https://web.dev/how-to-use-local-https/))
1. Add `https://my_ngrok_address/auth/slack.callback` as an Oauth redirect URL
1. Ensure that the bot token scope contains at least `users:read`
1. Run `make up`. This will download dependencies, build and launch a development version of Outline
### Production
# Contributing
For a self-hosted production installation there is more flexibility, but these are the suggested steps:
Outline is built and maintained by a small team we'd love your help to fix bugs and add features!
1. Clone this repo and install dependencies with `yarn` or `npm install`
Before submitting a pull request please let the core team know by creating or commenting in an issue on [GitHub](https://www.github.com/outline/outline/issues), and we'd also love to hear from you in the [Discussions](https://www.github.com/outline/outline/discussions). This way we can ensure that an approach is agreed on before code is written. This will result in a much higher liklihood of code being accepted.
> Requires [Node.js](https://nodejs.org/) and [yarn](https://yarnpkg.com) installed
If youre looking for ways to get started, here's a list of ways to help us improve Outline:
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.
* [Translation](TRANSLATION.md) into other languages
* Issues with [`good first issue`](https://github.com/outline/outline/labels/good%20first%20issue) label
* Performance improvements, both on server and frontend
* Developer happiness and documentation
* Bugs and other issues listed on GitHub
## Development
## Architecture
### Server
If you're interested in contributing or learning more about the Outline codebase
please refer to the [architecture document](ARCHITECTURE.md) first for a high level overview of how the application is put together.
## Debugging
Outline uses [debug](https://www.npmjs.com/package/debug). To enable debugging output, the following categories are available:
```
DEBUG=sql,cache,presenters,events,logistics,emails,mailer
DEBUG=sql,cache,presenters,events,importer,exporter,emails,mailer
```
## Tests
We aim to have sufficient test coverage for critical parts of the application and aren't aiming for 100% unit test coverage. All API endpoints and anything authentication related should be thoroughly tested.
To add new tests, write your tests with [Jest](https://facebook.github.io/jest/) and add a file with `.test.js` extension next to the tested code.
```shell
# To run all tests
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
# To run frontend tests
yarn test:app
```
## Migrations
@@ -101,67 +171,6 @@ Or to run migrations on test database:
yarn sequelize db:migrate --env test
```
## Structure
Outline is composed of separate backend and frontend application which are both driven by the same Node process. As both are written in Javascript, they share some code but are mostly separate. We utilize the latest language features, including `async`/`await`, and [Flow](https://flow.org/) typing. Prettier and ESLint are enforced by CI.
### Frontend
Outline's frontend is a React application compiled with [Webpack](https://webpack.js.org/). It uses [Mobx](https://mobx.js.org/) for state management and [Styled Components](https://www.styled-components.com/) for component styles. Unless global, state logic and styles are always co-located with React components together with their subcomponents to make the component tree easier to manage.
The editor itself is built on [Prosemirror](https://github.com/prosemirror) and hosted in a separate repository to encourage reuse: [rich-markdown-editor](https://github.com/outline/rich-markdown-editor)
- `app/` - Frontend React application
- `app/scenes` - Full page views
- `app/components` - Reusable React components
- `app/stores` - Global state stores
- `app/models` - State models
- `app/types` - Flow types for non-models
### Backend
Backend is driven by [Koa](http://koajs.com/) (API, web server), [Sequelize](http://docs.sequelizejs.com/) (database) and React for public pages and emails.
- `server/api` - API endpoints
- `server/commands` - Domain logic, currently being refactored from /models
- `server/emails` - React rendered email templates
- `server/models` - Database models
- `server/policies` - Authorization logic
- `server/presenters` - API responses for database models
- `server/test` - Test helps and support
- `server/utils` - Utility methods
- `shared` - Code shared between frontend and backend applications
## Tests
We aim to have sufficient test coverage for critical parts of the application and aren't aiming for 100% unit test coverage. All API endpoints and anything authentication related should be thoroughly tested.
To add new tests, write your tests with [Jest](https://facebook.github.io/jest/) and add a file with `.test.js` extension next to the tested code.
```shell
# To run all tests
yarn test
# To run backend tests
yarn test:server
# To run frontend tests
yarn test:app
```
## Contributing
Outline is built and maintained by a small team we'd love your help to fix bugs and add features!
However, before working on a pull request please let the core team know by creating or commenting in an issue on [GitHub](https://www.github.com/outline/outline/issues), and we'd also love to hear from you in the [Discussions](https://www.github.com/outline/outline/discussions). This way we can ensure that an approach is agreed on before code is written and will hopefully help to get your contributions integrated faster!
If youre looking for ways to get started, here's a list of ways to help us improve Outline:
* Issues with [`good first issue`](https://github.com/outline/outline/labels/good%20first%20issue) label
* Performance improvements, both on server and frontend
* Developer happiness and documentation
* Bugs and other issues listed on GitHub
## License
Outline is [BSL 1.1 licensed](https://github.com/outline/outline/blob/master/LICENSE).
Outline is [BSL 1.1 licensed](LICENSE).
+34
View File
@@ -0,0 +1,34 @@
# Translation
Outline is localized through community contributions. The text in Outline's user interface is in American English by default, we're very thankful for all help that the community provides bringing the app to different languages.
## Externalizing strings
Before a string can be translated, it must be externalized. This is the process where English strings in the source code are wrapped in a function that retrieves the translated string for the users language.
For externalization we use [react-i18next](https://react.i18next.com/), this provides the hooks [useTranslation](https://react.i18next.com/latest/usetranslation-hook) and the [Trans](https://react.i18next.com/latest/trans-component) component for wrapping English text.
PR's are accepted for wrapping English strings in the codebase that were not previously externalized.
## Translating strings
To manage the translation process we use [CrowdIn](https://translate.getoutline.com/), it keeps track of which strings in which languages still need translating, synchronizes with the codebase automatically, and provides a great editor interface.
You'll need to create a free account to use CrowdIn. Once you have joined, you can provide translations by following these steps:
1. Select the language for which you want to contribute (or vote for) a translation (below the language you can see the progress of the translation)
![CrowdIn UI](https://i.imgur.com/AkbDY60.png)
2. Please choose the translation.json file from your desired language
3. Once a file is selected, all the strings associated with the version are displayed on the left side. To display the untranslated strings first, select the filter icon next to the search bar and select “All, Untranslated First”.The red square next to an English string shows that a string has not been translated yet. To provide a translation, select a string on the left side, provide a translation in the target language in the text box in the right side (singular and plural) and press the save button. As soon as a translation has been provided by another user (green square next to string), you can also vote on a translation provided by another user. The translation with the most votes is used unless a different translation has been approved by a proof reader. ![Editor UI](https://i.imgur.com/pldZCRs.png)
## Proofreading
Once a translation has been provided, a proof reader can approve the translation and mark it for use in Outline.
If you are interested in becoming a proof reader, please contact one of the project managers in the Outline CrowdIn project or contact [@tommoor](https://github.com/tommoor). Similarly, if your language is not listed in the list of CrowdIn languages, please contact our project managers or [send us an email](https://www.getoutline.com/contact) so we can add your language.
## Release
Updated translations are automatically PR'd against the codebase by a bot and will be merged regularly so that new translations appear in the next release of Outline.
+18
View File
@@ -0,0 +1,18 @@
/* eslint-disable flowtype/require-valid-file-annotation */
export default class Queue {
name;
constructor(name) {
this.name = name;
}
process = (fn) => {
console.log(`Registered function ${this.name}`);
this.processFn = fn;
};
add = (data) => {
console.log(`Running ${this.name}`);
return this.processFn({ data });
};
}
+19 -1
View File
@@ -30,6 +30,10 @@
"postdeploy": "yarn sequelize db:migrate"
},
"env": {
"NODE_ENV": {
"value": "production",
"required": true
},
"SECRET_KEY": {
"description": "A secret key",
"generator": "secret",
@@ -51,7 +55,7 @@
"description": "",
"required": false
},
"GOOGLE_ALLOWED_DOMAINS": {
"ALLOWED_DOMAINS": {
"description": "Comma separated list of domains to be allowed (optional). If not set, all Google apps domains are allowed by default",
"required": false
},
@@ -92,6 +96,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",
@@ -126,6 +135,15 @@
"description": "wikireply@example.com (optional)",
"required": false
},
"SMTP_SECURE": {
"value": "true",
"description": "Use a secure SMTP connection (optional)",
"required": false
},
"SMTP_TLS_CIPHERS": {
"description": "Override SMTP cipher configuration (optional)",
"required": false
},
"GOOGLE_ANALYTICS_ID": {
"description": "UA-xxxx (optional)",
"required": false
+30
View File
@@ -0,0 +1,30 @@
{
"testURL": "http://localhost",
"verbose": false,
"rootDir": "..",
"roots": [
"<rootDir>/app",
"<rootDir>/shared"
],
"moduleNameMapper": {
"^shared/(.*)$": "<rootDir>/shared/$1",
"^.*[.](gif|ttf|eot|svg)$": "<rootDir>/__test__/fileMock.js"
},
"moduleFileExtensions": [
"js",
"jsx",
"json"
],
"moduleDirectories": [
"node_modules"
],
"modulePaths": [
"<rootDir>/app"
],
"setupFiles": [
"<rootDir>/__mocks__/window.js"
],
"setupFilesAfterEnv": [
"./app/test/setup.js"
]
}
+1 -6
View File
@@ -11,11 +11,6 @@ export const Action = styled(Flex)`
font-size: 15px;
flex-shrink: 0;
a {
color: ${(props) => props.theme.text};
height: 24px;
}
&:empty {
display: none;
}
@@ -38,7 +33,7 @@ const Actions = styled(Flex)`
background: ${(props) => props.theme.background};
transition: ${(props) => props.theme.backgroundTransition};
padding: 12px;
-webkit-backdrop-filter: blur(20px);
backdrop-filter: blur(20px);
@media print {
display: none;
+8 -1
View File
@@ -9,7 +9,9 @@ type Props = {
export default class Analytics extends React.Component<Props> {
componentDidMount() {
if (!env.GOOGLE_ANALYTICS_ID) return;
if (!env.GOOGLE_ANALYTICS_ID) {
return null;
}
// standard Google Analytics script
window.ga =
@@ -29,6 +31,11 @@ export default class Analytics extends React.Component<Props> {
script.src = "https://www.google-analytics.com/analytics.js";
script.async = true;
// Track PWA install event
window.addEventListener("appinstalled", () => {
ga("send", "event", "pwa", "install");
});
if (document.body) {
document.body.appendChild(script);
}
+23
View File
@@ -0,0 +1,23 @@
// @flow
import * as React from "react";
export default function Arrow() {
return (
<svg
width="13"
height="30"
viewBox="0 0 13 30"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill="currentColor"
d="M7.40242 1.48635C8.23085 0.0650039 10.0656 -0.421985 11.5005 0.39863C12.9354 1.21924 13.427 3.03671 12.5986 4.45806L5.59858 16.4681C4.77015 17.8894 2.93538 18.3764 1.5005 17.5558C0.065623 16.7352 -0.426002 14.9177 0.402425 13.4964L7.40242 1.48635Z"
/>
<path
fill="currentColor"
d="M12.5986 25.5419C13.427 26.9633 12.9354 28.7808 11.5005 29.6014C10.0656 30.422 8.23087 29.935 7.40244 28.5136L0.402438 16.5036C-0.425989 15.0823 0.0656365 13.2648 1.50051 12.4442C2.93539 11.6236 4.77016 12.1106 5.59859 13.5319L12.5986 25.5419Z"
/>
</svg>
);
}
+44
View File
@@ -0,0 +1,44 @@
// @flow
import * as React from "react";
type Props = {
size?: number,
fill?: string,
className?: string,
};
function MicrosoftLogo({ size = 34, fill = "#FFF", className }: Props) {
return (
<svg
fill={fill}
width={size}
height={size}
viewBox="0 0 34 34"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M18.0002 1H33.9998C33.9998 5.8172 34.0007 10.6344 33.9988 15.4516C28.6666 15.4508 23.3334 15.4516 18.0012 15.4516C17.9993 10.6344 18.0002 5.8172 18.0002 1Z"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M18.0009 17.5173C23.3333 17.5155 28.6667 17.5164 34 17.5164V33H18C18.0009 27.8388 17.9991 22.6776 18.0009 17.5173V17.5173Z"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M0 1H16L15.9988 15.4516H0V1Z"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M0 17.5161C5.3332 17.5179 10.6664 17.5155 15.9996 17.5179C16.0005 22.6789 15.9996 27.839 15.9996 33H0V17.5161Z"
/>
</svg>
);
}
export default MicrosoftLogo;
+46
View File
@@ -0,0 +1,46 @@
// @flow
import * as React from "react";
import styled from "styled-components";
import GoogleLogo from "./GoogleLogo";
import MicrosoftLogo from "./MicrosoftLogo";
import SlackLogo from "./SlackLogo";
type Props = {|
providerName: string,
size?: number,
|};
function AuthLogo({ providerName, size = 16 }: Props) {
switch (providerName) {
case "slack":
return (
<Logo>
<SlackLogo size={size} />
</Logo>
);
case "google":
return (
<Logo>
<GoogleLogo size={size} />
</Logo>
);
case "azure":
return (
<Logo>
<MicrosoftLogo size={size} />
</Logo>
);
default:
return null;
}
}
const Logo = styled.div`
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
`;
export default AuthLogo;
+26 -10
View File
@@ -1,18 +1,29 @@
// @flow
import { observer, inject } from "mobx-react";
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { Redirect } from "react-router-dom";
import { isCustomSubdomain } from "shared/utils/domains";
import AuthStore from "stores/AuthStore";
import LoadingIndicator from "components/LoadingIndicator";
import useStores from "../hooks/useStores";
import { changeLanguage } from "../utils/language";
import env from "env";
type Props = {
auth: AuthStore,
children?: React.Node,
children: React.Node,
};
const Authenticated = observer(({ auth, children }: Props) => {
const Authenticated = ({ children }: Props) => {
const { auth } = useStores();
const { i18n } = useTranslation();
const language = auth.user && auth.user.language;
// Watching for language changes here as this is the earliest point we have
// the user available and means we can start loading translations faster
React.useEffect(() => {
changeLanguage(language, i18n);
}, [i18n, language]);
if (auth.authenticated) {
const { user, team } = auth;
const { hostname } = window.location;
@@ -21,9 +32,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) &&
@@ -38,6 +54,6 @@ const Authenticated = observer(({ auth, children }: Props) => {
auth.logout(true);
return <Redirect to="/" />;
});
};
export default inject("auth")(Authenticated);
export default observer(Authenticated);
+6 -2
View File
@@ -3,13 +3,17 @@ import { observable } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import styled from "styled-components";
import User from "models/User";
import placeholder from "./placeholder.png";
type Props = {
type Props = {|
src: string,
size: number,
icon?: React.Node,
};
user?: User,
onClick?: () => void,
className?: string,
|};
@observer
class Avatar extends React.Component<Props> {
+23 -18
View File
@@ -1,9 +1,9 @@
// @flow
import distanceInWordsToNow from "date-fns/distance_in_words_to_now";
import { observable } from "mobx";
import { observer } from "mobx-react";
import { EditIcon } from "outline-icons";
import * as React from "react";
import { withTranslation, type TFunction } from "react-i18next";
import styled from "styled-components";
import User from "models/User";
import UserProfile from "scenes/UserProfile";
@@ -15,7 +15,8 @@ type Props = {
isPresent: boolean,
isEditing: boolean,
isCurrentUser: boolean,
lastViewedAt: string,
profileOnClick: boolean,
t: TFunction,
};
@observer
@@ -31,26 +32,26 @@ class AvatarWithPresence extends React.Component<Props> {
};
render() {
const {
user,
lastViewedAt,
isPresent,
isEditing,
isCurrentUser,
} = this.props;
const { user, isPresent, isEditing, isCurrentUser, t } = this.props;
const action = isPresent
? isEditing
? t("currently editing")
: t("currently viewing")
: t("previously edited");
return (
<>
<Tooltip
tooltip={
<Centered>
<strong>{user.name}</strong> {isCurrentUser && "(You)"}
<br />
{isPresent
? isEditing
? "currently editing"
: "currently viewing"
: `viewed ${distanceInWordsToNow(new Date(lastViewedAt))} ago`}
<strong>{user.name}</strong> {isCurrentUser && `(${t("You")})`}
{action && (
<>
<br />
{action}
</>
)}
</Centered>
}
placement="bottom"
@@ -58,7 +59,11 @@ class AvatarWithPresence extends React.Component<Props> {
<AvatarWrapper isPresent={isPresent}>
<Avatar
src={user.avatarUrl}
onClick={this.handleOpenProfile}
onClick={
this.props.profileOnClick === false
? undefined
: this.handleOpenProfile
}
size={32}
icon={isEditing ? <EditIcon size={16} color="#FFF" /> : undefined}
/>
@@ -83,4 +88,4 @@ const AvatarWrapper = styled.div`
transition: opacity 250ms ease-in-out;
`;
export default AvatarWithPresence;
export default withTranslation()<AvatarWithPresence>(AvatarWithPresence);
+10 -7
View File
@@ -3,14 +3,17 @@ 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)};
border-radius: 4px;
font-size: 11px;
padding: 1px 5px 2px;
background-color: ${({ yellow, primary, theme }) =>
yellow ? theme.yellow : primary ? theme.primary : "transparent"};
color: ${({ primary, yellow, theme }) =>
primary ? theme.white : yellow ? theme.almostBlack : theme.textTertiary};
border: 1px solid
${({ primary, yellow, theme }) =>
primary || yellow ? "transparent" : theme.textTertiary};
border-radius: 8px;
font-size: 12px;
font-weight: 500;
text-transform: uppercase;
user-select: none;
`;
+57 -132
View File
@@ -1,162 +1,87 @@
// @flow
import { observer, inject } from "mobx-react";
import {
PadlockIcon,
GoToIcon,
MoreIcon,
ShapesIcon,
EditIcon,
} from "outline-icons";
import { GoToIcon } from "outline-icons";
import * as React from "react";
import { Link } from "react-router-dom";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import CollectionsStore from "stores/CollectionsStore";
import Document from "models/Document";
import CollectionIcon from "components/CollectionIcon";
import Flex from "components/Flex";
import BreadcrumbMenu from "./BreadcrumbMenu";
import { collectionUrl } from "utils/routeHelpers";
import BreadcrumbMenu from "menus/BreadcrumbMenu";
type Props = {
document: Document,
collections: CollectionsStore,
onlyText: boolean,
};
type MenuItem = {|
icon?: React.Node,
title: React.Node,
to?: string,
|};
const Breadcrumb = observer(({ document, collections, onlyText }: Props) => {
const collection = collections.get(document.collectionId);
if (!collection) return <div />;
type Props = {|
items: MenuItem[],
max?: number,
children?: React.Node,
highlightFirstItem?: boolean,
|};
const path = collection.pathToDocument(document).slice(0, -1);
function Breadcrumb({ items, highlightFirstItem, children, max = 2 }: Props) {
const totalItems = items.length;
let topLevelItems: MenuItem[] = [...items];
let overflowItems;
if (onlyText === true) {
return (
<>
{collection.private && (
<>
<SmallPadlockIcon color="currentColor" size={16} />{" "}
</>
)}
{collection.name}
{path.map((n) => (
<React.Fragment key={n.id}>
<SmallSlash />
{n.title}
</React.Fragment>
))}
</>
);
// chop middle breadcrumbs and present a "..." menu instead
if (totalItems > max) {
const halfMax = Math.floor(max / 2);
overflowItems = topLevelItems.splice(halfMax, totalItems - max);
topLevelItems.splice(halfMax, 0, {
title: <BreadcrumbMenu items={overflowItems} />,
});
}
const isTemplate = document.isTemplate;
const isDraft = !document.publishedAt && !isTemplate;
const isNestedDocument = path.length > 1;
const lastPath = path.length ? path[path.length - 1] : undefined;
const menuPath = isNestedDocument ? path.slice(0, -1) : [];
return (
<Wrapper justify="flex-start" align="center">
{isTemplate && (
<>
<CollectionName to="/templates">
<ShapesIcon color="currentColor" />
&nbsp;
<span>Templates</span>
</CollectionName>
<Slash />
</>
)}
{isDraft && (
<>
<CollectionName to="/drafts">
<EditIcon color="currentColor" />
&nbsp;
<span>Drafts</span>
</CollectionName>
<Slash />
</>
)}
<CollectionName to={collectionUrl(collection.id)}>
<CollectionIcon collection={collection} expanded />
&nbsp;
<span>{collection.name}</span>
</CollectionName>
{isNestedDocument && (
<>
<Slash /> <BreadcrumbMenu label={<Overflow />} path={menuPath} />
</>
)}
{lastPath && (
<>
<Slash />{" "}
<Crumb to={lastPath.url} title={lastPath.title}>
{lastPath.title}
</Crumb>
</>
)}
</Wrapper>
<Flex justify="flex-start" align="center">
{topLevelItems.map((item, index) => (
<React.Fragment key={item.to || index}>
{item.icon}
{item.to ? (
<Item
to={item.to}
$withIcon={!!item.icon}
$highlight={highlightFirstItem && index === 0}
>
{item.title}
</Item>
) : (
item.title
)}
{index !== topLevelItems.length - 1 || !!children ? <Slash /> : null}
</React.Fragment>
))}
{children}
</Flex>
);
});
}
const Wrapper = styled(Flex)`
display: none;
${breakpoint("tablet")`
display: flex;
`};
`;
const SmallPadlockIcon = styled(PadlockIcon)`
display: inline-block;
vertical-align: sub;
`;
const SmallSlash = styled(GoToIcon)`
width: 15px;
height: 10px;
flex-shrink: 0;
opacity: 0.25;
`;
export const Slash = styled(GoToIcon)`
const Slash = styled(GoToIcon)`
flex-shrink: 0;
fill: ${(props) => props.theme.divider};
`;
const Overflow = styled(MoreIcon)`
flex-shrink: 0;
opacity: 0.25;
transition: opacity 100ms ease-in-out;
&:hover,
&:active {
opacity: 1;
}
`;
const Crumb = styled(Link)`
const Item = styled(Link)`
display: flex;
flex-shrink: 1;
min-width: 0;
color: ${(props) => props.theme.text};
font-size: 15px;
height: 24px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
font-weight: ${(props) => (props.$highlight ? "500" : "inherit")};
margin-left: ${(props) => (props.$withIcon ? "4px" : "0")};
svg {
flex-shrink: 0;
}
&:hover {
text-decoration: underline;
}
`;
const CollectionName = styled(Link)`
display: flex;
flex-shrink: 0;
color: ${(props) => props.theme.text};
font-size: 15px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
`;
export default inject("collections")(Breadcrumb);
export default Breadcrumb;
-25
View File
@@ -1,25 +0,0 @@
// @flow
import * as React from "react";
import { Link } from "react-router-dom";
import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
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>
);
}
}
@@ -1,13 +1,17 @@
// @flow
import * as React from "react";
import styled from "styled-components";
import { bounceIn } from "shared/styles/animations";
import { bounceIn } from "styles/animations";
type Props = {
type Props = {|
count: number,
};
|};
const Bubble = ({ count }: Props) => {
if (!count) {
return null;
}
return <Count>{count}</Count>;
};
+73 -64
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,13 +19,16 @@ const RealButton = styled.button`
height: 32px;
text-decoration: none;
flex-shrink: 0;
outline: none;
cursor: pointer;
user-select: none;
svg {
fill: ${(props) => props.iconColor || props.theme.buttonText};
}
${(props) =>
!props.borderOnHover &&
`
svg {
fill: ${props.iconColor || props.theme.buttonText};
}
`}
&::-moz-focus-inner {
padding: 0;
@@ -36,13 +39,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;
@@ -50,31 +46,34 @@ const RealButton = styled.button`
}
${(props) =>
props.neutral &&
props.$neutral &&
`
background: ${props.theme.buttonNeutralBackground};
color: ${props.theme.buttonNeutralText};
box-shadow: ${
props.borderOnHover ? "none" : "rgba(0, 0, 0, 0.07) 0px 1px 2px"
};
border: 1px solid ${
props.borderOnHover ? "transparent" : props.theme.buttonNeutralBorder
props.borderOnHover
? "none"
: `rgba(0, 0, 0, 0.07) 0px 1px 2px, ${props.theme.buttonNeutralBorder} 0 0 0 1px inset`
};
svg {
${
props.borderOnHover
? ""
: `svg {
fill: ${props.iconColor || props.theme.buttonNeutralText};
}`
}
&:hover {
background: ${darken(0.05, props.theme.buttonNeutralBackground)};
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;
background: ${
props.borderOnHover
? props.theme.buttonNeutralBackground
: darken(0.05, props.theme.buttonNeutralBackground)
};
box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 2px, ${
props.theme.buttonNeutralBorder
} 0 0 0 1px inset;
}
&:disabled {
@@ -86,15 +85,9 @@ const RealButton = styled.button`
background: ${props.theme.danger};
color: ${props.theme.white};
&: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;
}
&:hover {
background: ${darken(0.05, props.theme.danger)};
}
`};
`;
@@ -113,14 +106,14 @@ export const Inner = styled.span`
line-height: ${(props) => (props.hasIcon ? 24 : 32)}px;
justify-content: center;
align-items: center;
min-height: 30px;
min-height: 32px;
${(props) => props.hasIcon && props.hasText && "padding-left: 4px;"};
${(props) => props.hasIcon && !props.hasText && "padding: 0 4px;"};
`;
export type Props = {
type?: string,
export type Props = {|
type?: "button" | "submit",
value?: string,
icon?: React.Node,
iconColor?: string,
@@ -128,33 +121,49 @@ export type Props = {
children?: React.Node,
innerRef?: React.ElementRef<any>,
disclosure?: boolean,
neutral?: boolean,
danger?: boolean,
primary?: boolean,
disabled?: boolean,
fullwidth?: boolean,
autoFocus?: boolean,
style?: Object,
as?: React.ComponentType<any>,
to?: string,
onClick?: (event: SyntheticEvent<>) => mixed,
borderOnHover?: boolean,
};
function Button({
type = "text",
icon,
children,
value,
disclosure,
innerRef,
...rest
}: Props) {
const hasText = children !== undefined || value !== undefined;
const hasIcon = icon !== undefined;
"data-on"?: string,
"data-event-category"?: string,
"data-event-action"?: string,
|};
return (
<RealButton type={type} ref={innerRef} {...rest}>
<Inner hasIcon={hasIcon} hasText={hasText} disclosure={disclosure}>
{hasIcon && icon}
{hasText && <Label hasIcon={hasIcon}>{children || value}</Label>}
{disclosure && <ExpandedIcon />}
</Inner>
</RealButton>
);
}
const Button = React.forwardRef<Props, HTMLButtonElement>(
(
{
type = "text",
icon,
children,
value,
disclosure,
neutral,
...rest
}: Props,
innerRef
) => {
const hasText = children !== undefined || value !== undefined;
const hasIcon = icon !== undefined;
export default React.forwardRef<Props, typeof Button>((props, ref) => (
<Button {...props} innerRef={ref} />
));
return (
<RealButton type={type} ref={innerRef} $neutral={neutral} {...rest}>
<Inner hasIcon={hasIcon} hasText={hasText} disclosure={disclosure}>
{hasIcon && icon}
{hasText && <Label hasIcon={hasIcon}>{children || value}</Label>}
{disclosure && <ExpandedIcon />}
</Inner>
</RealButton>
);
}
);
export default Button;
+23
View File
@@ -0,0 +1,23 @@
// @flow
import * as React from "react";
import styled from "styled-components";
type Props = {
onClick: (ev: SyntheticEvent<>) => void,
children: React.Node,
};
export default function ButtonLink(props: Props) {
return <Button {...props} />;
}
const Button = styled.button`
margin: 0;
padding: 0;
border: 0;
color: ${(props) => props.theme.link};
line-height: inherit;
background: none;
text-decoration: none;
cursor: pointer;
`;
+10 -4
View File
@@ -3,22 +3,28 @@ import * as React from "react";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
type Props = {
type Props = {|
children?: React.Node,
};
withStickyHeader?: boolean,
|};
const Container = styled.div`
width: 100%;
padding: 60px 20px;
max-width: 100vw;
padding: ${(props) => (props.withStickyHeader ? "4px 12px" : "60px 12px")};
${breakpoint("tablet")`
padding: 60px;
padding: ${(props) => (props.withStickyHeader ? "4px 60px" : "60px")};
`};
`;
const Content = styled.div`
max-width: 46em;
margin: 0 auto;
${breakpoint("desktopLarge")`
max-width: 52em;
`};
`;
const CenteredContent = ({ children, ...rest }: Props) => {
+8 -4
View File
@@ -1,18 +1,21 @@
// @flow
import * as React from "react";
import { VisuallyHidden } from "reakit/VisuallyHidden";
import styled from "styled-components";
import HelpText from "components/HelpText";
import VisuallyHidden from "components/VisuallyHidden";
export type Props = {
export type Props = {|
checked?: boolean,
label?: string,
label?: React.Node,
labelHidden?: boolean,
className?: string,
name?: string,
disabled?: boolean,
onChange: (event: SyntheticInputEvent<HTMLInputElement>) => mixed,
note?: string,
short?: boolean,
small?: boolean,
};
|};
const LabelText = styled.span`
font-weight: 500;
@@ -23,6 +26,7 @@ const LabelText = styled.span`
const Wrapper = styled.div`
padding-bottom: 8px;
${(props) => (props.small ? "font-size: 14px" : "")};
width: 100%;
`;
const Label = styled.label`
+78
View File
@@ -0,0 +1,78 @@
// @flow
import React from "react";
import styled, { useTheme } from "styled-components";
const cleanPercentage = (percentage) => {
const tooLow = !Number.isFinite(+percentage) || percentage < 0;
const tooHigh = percentage > 100;
return tooLow ? 0 : tooHigh ? 100 : +percentage;
};
const Circle = ({
color,
percentage,
offset,
}: {
color: string,
percentage?: number,
offset: number,
}) => {
const radius = offset * 0.7;
const circumference = 2 * Math.PI * radius;
let strokePercentage;
if (percentage) {
// because the circle is so small, anything greater than 85% appears like 100%
percentage = percentage > 85 && percentage < 100 ? 85 : percentage;
strokePercentage = percentage
? ((100 - percentage) * circumference) / 100
: 0;
}
return (
<circle
r={radius}
cx={offset}
cy={offset}
fill="none"
stroke={strokePercentage !== circumference ? color : ""}
strokeWidth={2.5}
strokeDasharray={circumference}
strokeDashoffset={percentage ? strokePercentage : 0}
strokeLinecap="round"
style={{ transition: "stroke-dashoffset 0.6s ease 0s" }}
></circle>
);
};
const CircularProgressBar = ({
percentage,
size = 16,
}: {
percentage: number,
size?: number,
}) => {
const theme = useTheme();
percentage = cleanPercentage(percentage);
const offset = Math.floor(size / 2);
return (
<SVG width={size} height={size}>
<g transform={`rotate(-90 ${offset} ${offset})`}>
<Circle color={theme.progressBarBackground} offset={offset} />
{percentage > 0 && (
<Circle
color={theme.primary}
percentage={percentage}
offset={offset}
/>
)}
</g>
</SVG>
);
};
const SVG = styled.svg`
flex-shrink: 0;
`;
export default CircularProgressBar;
+89 -56
View File
@@ -1,76 +1,109 @@
// @flow
import { sortBy, keyBy } from "lodash";
import { observer, inject } from "mobx-react";
import { sortBy, filter, uniq } from "lodash";
import { observer } from "mobx-react";
import * as React from "react";
import { MAX_AVATAR_DISPLAY } from "shared/constants";
import DocumentPresenceStore from "stores/DocumentPresenceStore";
import ViewsStore from "stores/ViewsStore";
import { useTranslation } from "react-i18next";
import { usePopoverState, PopoverDisclosure } from "reakit/Popover";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import Document from "models/Document";
import { AvatarWithPresence } from "components/Avatar";
import DocumentViews from "components/DocumentViews";
import Facepile from "components/Facepile";
import NudeButton from "components/NudeButton";
import Popover from "components/Popover";
import useStores from "hooks/useStores";
type Props = {
views: ViewsStore,
presence: DocumentPresenceStore,
type Props = {|
document: Document,
currentUserId: string,
};
|};
@observer
class Collaborators extends React.Component<Props> {
componentDidMount() {
this.props.views.fetchPage({ documentId: this.props.document.id });
}
function Collaborators(props: Props) {
const { t } = useTranslation();
const { users, presence } = useStores();
const { document, currentUserId } = props;
render() {
const { document, presence, views, currentUserId } = this.props;
let documentPresence = presence.get(document.id);
documentPresence = documentPresence
? Array.from(documentPresence.values())
: [];
let documentPresence = presence.get(document.id);
documentPresence = documentPresence
? Array.from(documentPresence.values())
: [];
const documentViews = views.inDocument(document.id);
const presentIds = documentPresence.map((p) => p.userId);
const editingIds = documentPresence
.filter((p) => p.isEditing)
.map((p) => p.userId);
const presentIds = documentPresence.map((p) => p.userId);
const editingIds = documentPresence
.filter((p) => p.isEditing)
.map((p) => p.userId);
// ensure currently present via websocket are always ordered first
const collaborators = React.useMemo(
() =>
sortBy(
filter(
users.orderedData,
(user) =>
presentIds.includes(user.id) ||
document.collaboratorIds.includes(user.id)
),
(user) => presentIds.includes(user.id)
),
[document.collaboratorIds, users.orderedData, presentIds]
);
// ensure currently present via websocket are always ordered first
const mostRecentViewers = sortBy(
documentViews.slice(0, MAX_AVATAR_DISPLAY),
(view) => {
return presentIds.includes(view.user.id);
// load any users we don't know about
React.useEffect(() => {
if (users.isFetching) {
return;
}
uniq([...document.collaboratorIds, ...presentIds]).forEach((userId) => {
if (!users.get(userId)) {
return users.fetch(userId);
}
);
});
}, [document, users, presentIds, document.collaboratorIds]);
const viewersKeyedByUserId = keyBy(mostRecentViewers, (v) => v.user.id);
const overflow = documentViews.length - mostRecentViewers.length;
const popover = usePopoverState({
gutter: 0,
placement: "bottom-end",
});
return (
<Facepile
users={mostRecentViewers.map((v) => v.user)}
overflow={overflow}
renderAvatar={(user) => {
const isPresent = presentIds.includes(user.id);
const isEditing = editingIds.includes(user.id);
const { lastViewedAt } = viewersKeyedByUserId[user.id];
return (
<>
<PopoverDisclosure {...popover}>
{(props) => (
<NudeButton width={collaborators.length * 32} height={32} {...props}>
<FacepileHiddenOnMobile
users={collaborators}
renderAvatar={(user) => {
const isPresent = presentIds.includes(user.id);
const isEditing = editingIds.includes(user.id);
return (
<AvatarWithPresence
key={user.id}
user={user}
lastViewedAt={lastViewedAt}
isPresent={isPresent}
isEditing={isEditing}
isCurrentUser={currentUserId === user.id}
return (
<AvatarWithPresence
key={user.id}
user={user}
isPresent={isPresent}
isEditing={isEditing}
isCurrentUser={currentUserId === user.id}
profileOnClick={false}
/>
);
}}
/>
);
}}
/>
);
}
</NudeButton>
)}
</PopoverDisclosure>
<Popover {...popover} width={300} aria-label={t("Viewers")} tabIndex={0}>
<DocumentViews document={document} isOpen={popover.visible} />
</Popover>
</>
);
}
export default inject("views", "presence")(Collaborators);
const FacepileHiddenOnMobile = styled(Facepile)`
${breakpoint("mobile", "tablet")`
display: none;
`};
`;
export default observer(Collaborators);
+213
View File
@@ -0,0 +1,213 @@
// @flow
import { observer } from "mobx-react";
import { transparentize } from "polished";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import Collection from "models/Collection";
import Arrow from "components/Arrow";
import ButtonLink from "components/ButtonLink";
import Editor from "components/Editor";
import LoadingIndicator from "components/LoadingIndicator";
import NudeButton from "components/NudeButton";
import useDebouncedCallback from "hooks/useDebouncedCallback";
import useStores from "hooks/useStores";
import useToasts from "hooks/useToasts";
type Props = {|
collection: Collection,
|};
function CollectionDescription({ collection }: Props) {
const { collections, policies } = useStores();
const { showToast } = useToasts();
const { t } = useTranslation();
const [isExpanded, setExpanded] = React.useState(false);
const [isEditing, setEditing] = React.useState(false);
const [isDirty, setDirty] = React.useState(false);
const can = policies.abilities(collection.id);
const handleStartEditing = React.useCallback(() => {
setEditing(true);
}, []);
const handleStopEditing = React.useCallback(() => {
setEditing(false);
}, []);
const handleClickDisclosure = React.useCallback(
(event) => {
event.preventDefault();
if (isExpanded && document.activeElement) {
document.activeElement.blur();
}
setExpanded(!isExpanded);
},
[isExpanded]
);
const handleSave = useDebouncedCallback(async (getValue) => {
try {
await collection.save({
description: getValue(),
});
setDirty(false);
} catch (err) {
showToast(
t("Sorry, an error occurred saving the collection", {
type: "error",
})
);
throw err;
}
}, 1000);
const handleChange = React.useCallback(
(getValue) => {
setDirty(true);
handleSave(getValue);
},
[handleSave]
);
React.useEffect(() => {
setEditing(false);
}, [collection.id]);
const placeholder = `${t("Add a description")}`;
const key = isEditing || isDirty ? "draft" : collection.updatedAt;
return (
<MaxHeight data-editing={isEditing} data-expanded={isExpanded}>
<Input
$isEditable={can.update}
data-editing={isEditing}
data-expanded={isExpanded}
>
<span onClick={can.update ? handleStartEditing : undefined}>
{collections.isSaving && <LoadingIndicator />}
{collection.hasDescription || isEditing || isDirty ? (
<React.Suspense fallback={<Placeholder>Loading</Placeholder>}>
<Editor
key={key}
defaultValue={collection.description || ""}
onChange={handleChange}
placeholder={placeholder}
readOnly={!isEditing}
autoFocus={isEditing}
onBlur={handleStopEditing}
maxLength={1000}
disableEmbeds
readOnlyWriteCheckboxes
grow
/>
</React.Suspense>
) : (
can.update && <Placeholder>{placeholder}</Placeholder>
)}
</span>
</Input>
{!isEditing && (
<Disclosure
onClick={handleClickDisclosure}
aria-label={isExpanded ? t("Collapse") : t("Expand")}
size={30}
>
<Arrow />
</Disclosure>
)}
</MaxHeight>
);
}
const Disclosure = styled(NudeButton)`
opacity: 0;
color: ${(props) => props.theme.divider};
position: absolute;
top: calc(25vh - 50px);
left: 50%;
z-index: 1;
transform: rotate(-90deg) translateX(-50%);
transition: opacity 100ms ease-in-out;
&:focus,
&:hover {
opacity: 1;
}
&:active {
color: ${(props) => props.theme.sidebarText};
}
`;
const Placeholder = styled(ButtonLink)`
color: ${(props) => props.theme.placeholder};
cursor: text;
min-height: 27px;
`;
const MaxHeight = styled.div`
position: relative;
max-height: 25vh;
overflow: hidden;
margin: -12px -8px -8px;
padding: 8px;
&[data-editing="true"],
&[data-expanded="true"] {
max-height: initial;
overflow: initial;
${Disclosure} {
top: initial;
bottom: 0;
transform: rotate(90deg) translateX(-50%);
}
}
&:hover ${Disclosure} {
opacity: 1;
}
`;
const Input = styled.div`
margin: -8px;
padding: 8px;
border-radius: 8px;
transition: ${(props) => props.theme.backgroundTransition};
&:after {
content: "";
position: absolute;
top: calc(25vh - 50px);
left: 0;
right: 0;
height: 50px;
pointer-events: none;
background: linear-gradient(
180deg,
${(props) => transparentize(1, props.theme.background)} 0%,
${(props) => props.theme.background} 100%
);
}
&[data-editing="true"],
&[data-expanded="true"] {
&:after {
background: transparent;
}
}
&[data-editing="true"] {
background: ${(props) => props.theme.secondaryBackground};
}
.block-menu-trigger,
.heading-anchor {
display: none !important;
}
`;
export default observer(CollectionDescription);
+8 -13
View File
@@ -1,24 +1,25 @@
// @flow
import { inject, observer } from "mobx-react";
import { PrivateCollectionIcon, CollectionIcon } from "outline-icons";
import { observer } from "mobx-react";
import { CollectionIcon } from "outline-icons";
import { getLuminance } from "polished";
import * as React from "react";
import UiStore from "stores/UiStore";
import Collection from "models/Collection";
import { icons } from "components/IconPicker";
import useStores from "hooks/useStores";
type Props = {
collection: Collection,
expanded?: boolean,
size?: number,
ui: UiStore,
};
function ResolvedCollectionIcon({ collection, expanded, size, ui }: Props) {
function ResolvedCollectionIcon({ collection, expanded, size }: Props) {
const { ui } = useStores();
// 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"
@@ -33,13 +34,7 @@ function ResolvedCollectionIcon({ collection, expanded, size, ui }: Props) {
}
}
if (collection.private) {
return (
<PrivateCollectionIcon color={color} expanded={expanded} size={size} />
);
}
return <CollectionIcon color={color} expanded={expanded} size={size} />;
}
export default inject("ui")(observer(ResolvedCollectionIcon));
export default observer(ResolvedCollectionIcon);
+13
View File
@@ -0,0 +1,13 @@
// @flow
import styled from "styled-components";
const Header = styled.h3`
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
color: ${(props) => props.theme.sidebarText};
letter-spacing: 0.04em;
margin: 1em 12px 0.5em;
`;
export default Header;
+138
View File
@@ -0,0 +1,138 @@
// @flow
import { CheckmarkIcon } from "outline-icons";
import * as React from "react";
import { MenuItem as BaseMenuItem } from "reakit/Menu";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
type Props = {|
onClick?: (SyntheticEvent<>) => void | Promise<void>,
children?: React.Node,
selected?: boolean,
disabled?: boolean,
to?: string,
href?: string,
target?: "_blank",
as?: string | React.ComponentType<*>,
hide?: () => void,
level?: number,
|};
const MenuItem = ({
onClick,
children,
selected,
disabled,
as,
hide,
...rest
}: Props) => {
const handleClick = React.useCallback(
(ev) => {
if (onClick) {
ev.preventDefault();
ev.stopPropagation();
onClick(ev);
}
if (hide) {
hide();
}
},
[onClick, hide]
);
// Preventing default mousedown otherwise menu items do not work in Firefox,
// which triggers the hideOnClickOutside handler first via mousedown hiding
// and un-rendering the menu contents.
const handleMouseDown = React.useCallback((ev) => {
ev.preventDefault();
ev.stopPropagation();
}, []);
return (
<BaseMenuItem
onClick={disabled ? undefined : onClick}
disabled={disabled}
hide={hide}
{...rest}
>
{(props) => (
<MenuAnchor
{...props}
$toggleable={selected !== undefined}
as={onClick ? "button" : as}
onClick={handleClick}
onMouseDown={handleMouseDown}
>
{selected !== undefined && (
<>
{selected ? <CheckmarkIcon color="currentColor" /> : <Spacer />}
&nbsp;
</>
)}
{children}
</MenuAnchor>
)}
</BaseMenuItem>
);
};
const Spacer = styled.svg`
width: 24px;
height: 24px;
flex-shrink: 0;
`;
export const MenuAnchor = styled.a`
display: flex;
margin: 0;
border: 0;
padding: 12px;
padding-left: ${(props) => 12 + props.level * 10}px;
width: 100%;
min-height: 32px;
background: none;
color: ${(props) =>
props.disabled ? props.theme.textTertiary : props.theme.textSecondary};
justify-content: left;
align-items: center;
font-size: 16px;
cursor: default;
user-select: none;
svg:not(:last-child) {
margin-right: 4px;
}
svg {
flex-shrink: 0;
opacity: ${(props) => (props.disabled ? ".5" : 1)};
}
${(props) =>
props.disabled
? "pointer-events: none;"
: `
&:hover,
&:focus,
&.focus-visible {
color: ${props.theme.white};
background: ${props.theme.primary};
box-shadow: none;
cursor: pointer;
svg {
fill: ${props.theme.white};
}
}
`};
${breakpoint("tablet")`
padding: ${(props) => (props.$toggleable ? "4px 12px" : "6px 12px")};
font-size: 15px;
`};
`;
export default MenuItem;
@@ -0,0 +1,21 @@
// @flow
import { MoreIcon } from "outline-icons";
import * as React from "react";
import { MenuButton } from "reakit/Menu";
import NudeButton from "components/NudeButton";
export default function OverflowMenuButton({
iconColor,
className,
...rest
}: any) {
return (
<MenuButton {...rest}>
{(props) => (
<NudeButton className={className} {...props}>
<MoreIcon color={iconColor} />
</NudeButton>
)}
</MenuButton>
);
}
+16
View File
@@ -0,0 +1,16 @@
// @flow
import * as React from "react";
import { MenuSeparator } from "reakit/Menu";
import styled from "styled-components";
export default function Separator(rest: {}) {
return (
<MenuSeparator {...rest}>
{(props) => <HorizontalRule {...props} />}
</MenuSeparator>
);
}
const HorizontalRule = styled.hr`
margin: 0.5em 12px;
`;
+142
View File
@@ -0,0 +1,142 @@
// @flow
import { ExpandedIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import {
useMenuState,
MenuButton,
MenuItem as BaseMenuItem,
} from "reakit/Menu";
import styled from "styled-components";
import Header from "./Header";
import MenuItem, { MenuAnchor } from "./MenuItem";
import Separator from "./Separator";
import ContextMenu from ".";
import { type MenuItem as TMenuItem } from "types";
type Props = {|
items: TMenuItem[],
|};
const Disclosure = styled(ExpandedIcon)`
transform: rotate(270deg);
position: absolute;
right: 8px;
`;
const Submenu = React.forwardRef(({ templateItems, title, ...rest }, ref) => {
const { t } = useTranslation();
const menu = useMenuState({ modal: true });
return (
<>
<MenuButton ref={ref} {...menu} {...rest}>
{(props) => (
<MenuAnchor {...props}>
{title} <Disclosure color="currentColor" />
</MenuAnchor>
)}
</MenuButton>
<ContextMenu {...menu} aria-label={t("Submenu")}>
<Template {...menu} items={templateItems} />
</ContextMenu>
</>
);
});
export function filterTemplateItems(items: TMenuItem[]): TMenuItem[] {
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;
}
function Template({ items, ...menu }: Props): React.Node {
return filterTemplateItems(items).map((item, index) => {
if (item.to) {
return (
<MenuItem
as={Link}
to={item.to}
key={index}
disabled={item.disabled}
selected={item.selected}
{...menu}
>
{item.title}
</MenuItem>
);
}
if (item.href) {
return (
<MenuItem
href={item.href}
key={index}
disabled={item.disabled}
selected={item.selected}
level={item.level}
target={item.href.startsWith("#") ? undefined : "_blank"}
{...menu}
>
{item.title}
</MenuItem>
);
}
if (item.onClick) {
return (
<MenuItem
as="button"
onClick={item.onClick}
disabled={item.disabled}
selected={item.selected}
key={index}
{...menu}
>
{item.title}
</MenuItem>
);
}
if (item.items) {
return (
<BaseMenuItem
key={index}
as={Submenu}
templateItems={item.items}
title={item.title}
{...menu}
/>
);
}
if (item.type === "separator") {
return <Separator key={index} />;
}
if (item.type === "heading") {
return <Header>{item.title}</Header>;
}
console.warn("Unrecognized menu item", item);
return null;
});
}
export default React.memo<Props>(Template);
+136
View File
@@ -0,0 +1,136 @@
// @flow
import * as React from "react";
import { Portal } from "react-portal";
import { Menu } from "reakit/Menu";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import usePrevious from "hooks/usePrevious";
import {
fadeIn,
fadeAndSlideUp,
fadeAndSlideDown,
mobileContextMenu,
} from "styles/animations";
type Props = {|
"aria-label": string,
visible?: boolean,
placement?: string,
animating?: boolean,
children: React.Node,
onOpen?: () => void,
onClose?: () => void,
|};
export default function ContextMenu({
children,
onOpen,
onClose,
...rest
}: Props) {
const previousVisible = usePrevious(rest.visible);
React.useEffect(() => {
if (rest.visible && !previousVisible) {
if (onOpen) {
onOpen();
}
}
if (!rest.visible && previousVisible) {
if (onClose) {
onClose();
}
}
}, [onOpen, onClose, previousVisible, rest.visible]);
return (
<>
<Menu hideOnClickOutside preventBodyScroll {...rest}>
{(props) => {
// kind of hacky, but this is an effective way of telling which way
// the menu will _actually_ be placed when taking into account screen
// positioning.
const topAnchor = props.style.top === "0";
const rightAnchor = props.placement === "bottom-end";
return (
<Position {...props}>
<Background
dir="auto"
topAnchor={topAnchor}
rightAnchor={rightAnchor}
>
{rest.visible || rest.animating ? children : null}
</Background>
</Position>
);
}}
</Menu>
{(rest.visible || rest.animating) && (
<Portal>
<Backdrop />
</Portal>
)}
</>
);
}
const Backdrop = styled.div`
animation: ${fadeIn} 200ms ease-in-out;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: ${(props) => props.theme.backdrop};
z-index: ${(props) => props.theme.depths.menu - 1};
${breakpoint("tablet")`
display: none;
`};
`;
const Position = styled.div`
position: absolute;
z-index: ${(props) => props.theme.depths.menu};
${breakpoint("mobile", "tablet")`
position: fixed !important;
transform: none !important;
top: auto !important;
right: 8px !important;
bottom: 16px !important;
left: 8px !important;
`};
`;
const Background = styled.div`
animation: ${mobileContextMenu} 200ms ease;
transform-origin: 50% 100%;
max-width: 100%;
background: ${(props) => props.theme.menuBackground};
border-radius: 6px;
padding: 6px 0;
min-width: 180px;
overflow: hidden;
overflow-y: auto;
max-height: 75vh;
pointer-events: all;
font-weight: normal;
@media print {
display: none;
}
${breakpoint("tablet")`
animation: ${(props) =>
props.topAnchor ? fadeAndSlideDown : fadeAndSlideUp} 200ms ease;
transform-origin: ${(props) =>
props.rightAnchor === "bottom-end" ? "75%" : "25%"} 0;
max-width: 276px;
background: ${(props) => props.theme.menuBackground};
box-shadow: ${(props) => props.theme.menuShadow};
border: ${(props) =>
props.theme.menuBorder ? `1px solid ${props.theme.menuBorder}` : "none"};
`};
`;
+1
View File
@@ -15,6 +15,7 @@ class CopyToClipboard extends React.PureComponent<Props> {
const elem = React.Children.only(children);
copy(text, {
debug: process.env.NODE_ENV !== "production",
format: "text/plain",
});
if (onCopy) onCopy();
+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;
+11
View File
@@ -0,0 +1,11 @@
// @flow
import styled from "styled-components";
const Divider = styled.hr`
border: 0;
border-bottom: 1px solid ${(props) => props.theme.divider};
margin: 0;
padding: 0;
`;
export default Divider;
+138
View File
@@ -0,0 +1,138 @@
// @flow
import { observer } from "mobx-react";
import {
ArchiveIcon,
EditIcon,
GoToIcon,
ShapesIcon,
TrashIcon,
} from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import Document from "models/Document";
import Breadcrumb from "components/Breadcrumb";
import CollectionIcon from "components/CollectionIcon";
import useStores from "hooks/useStores";
import { collectionUrl } from "utils/routeHelpers";
type Props = {|
document: Document,
children?: React.Node,
onlyText: boolean,
|};
function useCategory(document) {
const { t } = useTranslation();
if (document.isDeleted) {
return {
icon: <TrashIcon color="currentColor" />,
title: t("Trash"),
to: "/trash",
};
}
if (document.isArchived) {
return {
icon: <ArchiveIcon color="currentColor" />,
title: t("Archive"),
to: "/archive",
};
}
if (document.isDraft) {
return {
icon: <EditIcon color="currentColor" />,
title: t("Drafts"),
to: "/drafts",
};
}
if (document.isTemplate) {
return {
icon: <ShapesIcon color="currentColor" />,
title: t("Templates"),
to: "/templates",
};
}
return null;
}
const DocumentBreadcrumb = ({ document, children, onlyText }: Props) => {
const { collections } = useStores();
const { t } = useTranslation();
const category = useCategory(document);
let collection = collections.get(document.collectionId);
if (!collection) {
collection = {
id: document.collectionId,
name: t("Deleted Collection"),
color: "currentColor",
url: "deleted-collection",
};
}
const path = React.useMemo(
() =>
collection && collection.pathToDocument
? collection.pathToDocument(document.id).slice(0, -1)
: [],
[collection, document.id]
);
const items = React.useMemo(() => {
let output = [];
if (category) {
output.push(category);
}
if (collection) {
output.push({
icon: <CollectionIcon collection={collection} expanded />,
title: collection.name,
to: collectionUrl(collection.url),
});
}
path.forEach((p) => {
output.push({
title: p.title,
to: p.url,
});
});
return output;
}, [path, category, collection]);
if (!collections.isLoaded) {
return null;
}
if (onlyText === true) {
return (
<>
{collection.name}
{path.map((n) => (
<React.Fragment key={n.id}>
<SmallSlash />
{n.title}
</React.Fragment>
))}
</>
);
}
return <Breadcrumb items={items} children={children} highlightFirstItem />;
};
const SmallSlash = styled(GoToIcon)`
width: 12px;
height: 12px;
vertical-align: middle;
flex-shrink: 0;
fill: ${(props) => props.theme.slate};
opacity: 0.5;
`;
export default observer(DocumentBreadcrumb);
+124
View File
@@ -0,0 +1,124 @@
// @flow
import { observer } from "mobx-react";
import { CloseIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { useHistory, useRouteMatch } from "react-router-dom";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import Event from "models/Event";
import Button from "components/Button";
import Empty from "components/Empty";
import Flex from "components/Flex";
import PaginatedEventList from "components/PaginatedEventList";
import Scrollable from "components/Scrollable";
import useStores from "hooks/useStores";
import { documentUrl } from "utils/routeHelpers";
const EMPTY_ARRAY = [];
function DocumentHistory() {
const { events, documents } = useStores();
const { t } = useTranslation();
const match = useRouteMatch();
const history = useHistory();
const document = documents.getByUrl(match.params.documentSlug);
const eventsInDocument = document
? events.inDocument(document.id)
: EMPTY_ARRAY;
const onCloseHistory = () => {
history.push(documentUrl(document));
};
const items = React.useMemo(() => {
if (
eventsInDocument[0] &&
document &&
eventsInDocument[0].createdAt !== document.updatedAt
) {
eventsInDocument.unshift(
new Event({
name: "documents.latest_version",
documentId: document.id,
createdAt: document.updatedAt,
actor: document.updatedBy,
})
);
}
return eventsInDocument;
}, [eventsInDocument, document]);
return (
<Sidebar>
{document ? (
<Position column>
<Header>
<Title>{t("History")}</Title>
<Button
icon={<CloseIcon />}
onClick={onCloseHistory}
borderOnHover
neutral
/>
</Header>
<Scrollable topShadow>
<PaginatedEventList
fetch={events.fetchPage}
events={items}
options={{ documentId: document.id }}
document={document}
empty={<Empty>{t("Oh weird, there's nothing here")}</Empty>}
/>
</Scrollable>
</Position>
) : null}
</Sidebar>
);
}
const Position = styled(Flex)`
position: fixed;
top: 0;
bottom: 0;
width: ${(props) => props.theme.sidebarWidth}px;
`;
const Sidebar = styled(Flex)`
display: none;
position: relative;
flex-shrink: 0;
background: ${(props) => props.theme.background};
width: ${(props) => props.theme.sidebarWidth}px;
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;
color: ${(props) => props.theme.text};
flex-shrink: 0;
`;
export default observer(DocumentHistory);
@@ -1,149 +0,0 @@
// @flow
import ArrowKeyNavigation from "boundless-arrow-key-navigation";
import { observable, action } from "mobx";
import { observer, inject } from "mobx-react";
import * as React from "react";
import { type RouterHistory, type Match } from "react-router-dom";
import { Waypoint } from "react-waypoint";
import styled from "styled-components";
import { DEFAULT_PAGINATION_LIMIT } from "stores/BaseStore";
import DocumentsStore from "stores/DocumentsStore";
import RevisionsStore from "stores/RevisionsStore";
import Flex from "components/Flex";
import { ListPlaceholder } from "components/LoadingPlaceholder";
import Revision from "./components/Revision";
import { documentHistoryUrl } from "utils/routeHelpers";
type Props = {
match: Match,
documents: DocumentsStore,
revisions: RevisionsStore,
history: RouterHistory,
};
@observer
class DocumentHistory extends React.Component<Props> {
@observable isLoaded: boolean = false;
@observable isFetching: boolean = false;
@observable offset: number = 0;
@observable allowLoadMore: boolean = true;
async componentDidMount() {
await this.loadMoreResults();
this.selectFirstRevision();
}
fetchResults = async () => {
this.isFetching = true;
const limit = DEFAULT_PAGINATION_LIMIT;
const results = await this.props.revisions.fetchPage({
limit,
offset: this.offset,
documentId: this.props.match.params.documentSlug,
});
if (
results &&
(results.length === 0 || results.length < DEFAULT_PAGINATION_LIMIT)
) {
this.allowLoadMore = false;
} else {
this.offset += DEFAULT_PAGINATION_LIMIT;
}
this.isLoaded = true;
this.isFetching = false;
};
selectFirstRevision = () => {
if (this.revisions.length) {
const document = this.props.documents.getByUrl(
this.props.match.params.documentSlug
);
if (!document) return;
this.props.history.replace(
documentHistoryUrl(document, this.revisions[0].id)
);
}
};
@action
loadMoreResults = async () => {
// Don't paginate if there aren't more results or were in the middle of fetching
if (!this.allowLoadMore || this.isFetching) return;
await this.fetchResults();
};
get revisions() {
const document = this.props.documents.getByUrl(
this.props.match.params.documentSlug
);
if (!document) return [];
return this.props.revisions.getDocumentRevisions(document.id);
}
render() {
const document = this.props.documents.getByUrl(
this.props.match.params.documentSlug
);
const showLoading = (!this.isLoaded && this.isFetching) || !document;
return (
<Sidebar>
<Wrapper column>
{showLoading ? (
<Loading>
<ListPlaceholder count={5} />
</Loading>
) : (
<ArrowKeyNavigation
mode={ArrowKeyNavigation.mode.VERTICAL}
defaultActiveChildIndex={0}
>
{this.revisions.map((revision, index) => (
<Revision
key={revision.id}
revision={revision}
document={document}
showMenu={index !== 0}
selected={this.props.match.params.revisionId === revision.id}
/>
))}
</ArrowKeyNavigation>
)}
{this.allowLoadMore && (
<Waypoint key={this.offset} onEnter={this.loadMoreResults} />
)}
</Wrapper>
</Sidebar>
);
}
}
const Loading = styled.div`
margin: 0 16px;
`;
const Wrapper = styled(Flex)`
position: fixed;
top: 0;
right: 0;
z-index: 1;
min-width: ${(props) => props.theme.sidebarWidth};
height: 100%;
overflow-y: auto;
overscroll-behavior: none;
`;
const Sidebar = styled(Flex)`
background: ${(props) => props.theme.background};
min-width: ${(props) => props.theme.sidebarWidth};
border-left: 1px solid ${(props) => props.theme.divider};
z-index: 1;
`;
export default inject("documents", "revisions")(DocumentHistory);
@@ -1,89 +0,0 @@
// @flow
import format from "date-fns/format";
import { MoreIcon } from "outline-icons";
import * as React from "react";
import { NavLink } from "react-router-dom";
import styled, { withTheme } from "styled-components";
import Document from "models/Document";
import Revision from "models/Revision";
import Avatar from "components/Avatar";
import Flex from "components/Flex";
import Time from "components/Time";
import RevisionMenu from "menus/RevisionMenu";
import { documentHistoryUrl } from "utils/routeHelpers";
type Props = {
theme: Object,
showMenu: boolean,
selected: boolean,
document: Document,
revision: Revision,
};
class RevisionListItem extends React.Component<Props> {
render() {
const { revision, document, showMenu, selected, theme } = this.props;
return (
<StyledNavLink
to={documentHistoryUrl(document, revision.id)}
activeStyle={{ background: theme.primary, color: theme.white }}
>
<Author>
<StyledAvatar src={revision.createdBy.avatarUrl} />{" "}
{revision.createdBy.name}
</Author>
<Meta>
<Time dateTime={revision.createdAt}>
{format(revision.createdAt, "MMMM Do, YYYY h:mm a")}
</Time>
</Meta>
{showMenu && (
<StyledRevisionMenu
document={document}
revision={revision}
label={
<MoreIcon color={selected ? theme.white : theme.textTertiary} />
}
/>
)}
</StyledNavLink>
);
}
}
const StyledAvatar = styled(Avatar)`
border-color: transparent;
margin-right: 4px;
`;
const StyledRevisionMenu = styled(RevisionMenu)`
position: absolute;
right: 16px;
top: 20px;
`;
const StyledNavLink = styled(NavLink)`
color: ${(props) => props.theme.text};
display: block;
padding: 8px 16px;
font-size: 15px;
position: relative;
`;
const Author = styled(Flex)`
font-weight: 500;
padding: 0;
margin: 0;
`;
const Meta = styled.p`
font-size: 14px;
opacity: 0.75;
margin: 0 0 2px;
padding: 0;
`;
export default withTheme(RevisionListItem);
-3
View File
@@ -1,3 +0,0 @@
// @flow
import DocumentHistory from "./DocumentHistory";
export default DocumentHistory;
+9 -4
View File
@@ -2,12 +2,17 @@
import ArrowKeyNavigation from "boundless-arrow-key-navigation";
import * as React from "react";
import Document from "models/Document";
import DocumentPreview from "components/DocumentPreview";
import DocumentListItem from "components/DocumentListItem";
type Props = {
type Props = {|
documents: Document[],
limit?: number,
};
showCollection?: boolean,
showPublished?: boolean,
showPin?: boolean,
showDraft?: boolean,
showTemplate?: boolean,
|};
export default function DocumentList({ limit, documents, ...rest }: Props) {
const items = limit ? documents.splice(0, limit) : documents;
@@ -18,7 +23,7 @@ export default function DocumentList({ limit, documents, ...rest }: Props) {
defaultActiveChildIndex={0}
>
{items.map((document) => (
<DocumentPreview key={document.id} document={document} {...rest} />
<DocumentListItem key={document.id} document={document} {...rest} />
))}
</ArrowKeyNavigation>
);
+264
View File
@@ -0,0 +1,264 @@
// @flow
import { observer } from "mobx-react";
import { PlusIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import styled, { css } from "styled-components";
import breakpoint from "styled-components-breakpoint";
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 StarButton, { AnimatedStar } from "components/Star";
import Tooltip from "components/Tooltip";
import useBoolean from "hooks/useBoolean";
import useCurrentTeam from "hooks/useCurrentTeam";
import useCurrentUser from "hooks/useCurrentUser";
import useStores from "hooks/useStores";
import DocumentMenu from "menus/DocumentMenu";
import { newDocumentUrl } from "utils/routeHelpers";
type Props = {|
document: Document,
highlight?: ?string,
context?: ?string,
showNestedDocuments?: boolean,
showCollection?: boolean,
showPublished?: boolean,
showPin?: boolean,
showDraft?: boolean,
showTemplate?: boolean,
|};
const SEARCH_RESULT_REGEX = /<b\b[^>]*>(.*?)<\/b>/gi;
function replaceResultMarks(tag: string) {
// don't use SEARCH_RESULT_REGEX here as it causes
// an infinite loop to trigger a regex inside it's own callback
return tag.replace(/<b\b[^>]*>(.*?)<\/b>/gi, "$1");
}
function DocumentListItem(props: Props, ref) {
const { t } = useTranslation();
const { policies } = useStores();
const currentUser = useCurrentUser();
const currentTeam = useCurrentTeam();
const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean();
const {
document,
showNestedDocuments,
showCollection,
showPublished,
showPin,
showDraft = true,
showTemplate,
highlight,
context,
} = props;
const queryIsInTitle =
!!highlight &&
!!document.title.toLowerCase().includes(highlight.toLowerCase());
const canStar =
!document.isDraft && !document.isArchived && !document.isTemplate;
const can = policies.abilities(currentTeam.id);
const canCollection = policies.abilities(document.collectionId);
return (
<DocumentLink
ref={ref}
dir={document.dir}
$isStarred={document.isStarred}
$menuOpen={menuOpen}
to={{
pathname: document.url,
state: { title: document.titleWithDefault },
}}
>
<Content>
<Heading dir={document.dir}>
<Title
text={document.titleWithDefault}
highlight={highlight}
dir={document.dir}
/>
{document.isNew && document.createdBy.id !== currentUser.id && (
<Badge yellow>{t("New")}</Badge>
)}
{canStar && (
<StarPositioner>
<StarButton document={document} />
</StarPositioner>
)}
{document.isDraft && showDraft && (
<Tooltip
tooltip={t("Only visible to you")}
delay={500}
placement="top"
>
<Badge>{t("Draft")}</Badge>
</Tooltip>
)}
{document.isTemplate && showTemplate && (
<Badge primary>{t("Template")}</Badge>
)}
</Heading>
{!queryIsInTitle && (
<ResultContext
text={context}
highlight={highlight ? SEARCH_RESULT_REGEX : undefined}
processResult={replaceResultMarks}
/>
)}
<DocumentMeta
document={document}
showCollection={showCollection}
showPublished={showPublished}
showNestedDocuments={showNestedDocuments}
showLastViewed
/>
</Content>
<Actions>
{document.isTemplate &&
!document.isArchived &&
!document.isDeleted &&
can.createDocument &&
canCollection.update && (
<>
<Button
as={Link}
to={newDocumentUrl(document.collectionId, {
templateId: document.id,
})}
icon={<PlusIcon />}
neutral
>
{t("New doc")}
</Button>
&nbsp;
</>
)}
<DocumentMenu
document={document}
showPin={showPin}
onOpen={handleMenuOpen}
onClose={handleMenuClose}
modal={false}
/>
</Actions>
</DocumentLink>
);
}
const Content = styled.div`
flex-grow: 1;
flex-shrink: 1;
min-width: 0;
`;
const Actions = styled(EventBoundary)`
display: none;
align-items: center;
margin: 8px;
flex-shrink: 0;
flex-grow: 0;
${breakpoint("tablet")`
display: flex;
`};
`;
const DocumentLink = styled(Link)`
display: flex;
align-items: center;
margin: 10px -8px;
padding: 6px 8px;
border-radius: 8px;
max-height: 50vh;
width: calc(100vw - 8px);
${breakpoint("tablet")`
width: auto;
`};
${Actions} {
opacity: 0;
}
${AnimatedStar} {
opacity: ${(props) => (props.$isStarred ? "1 !important" : 0)};
}
&:hover,
&:active,
&:focus,
&:focus-within {
background: ${(props) => props.theme.listItemHoverBackground};
${Actions} {
opacity: 1;
}
${AnimatedStar} {
opacity: 0.5;
&:hover {
opacity: 1;
}
}
}
${(props) =>
props.$menuOpen &&
css`
background: ${(props) => props.theme.listItemHoverBackground};
${Actions} {
opacity: 1;
}
${AnimatedStar} {
opacity: 0.5;
}
`}
`;
const Heading = styled.h3`
display: flex;
justify-content: ${(props) => (props.rtl ? "flex-end" : "flex-start")};
align-items: center;
height: 24px;
margin-top: 0;
margin-bottom: 0.25em;
overflow: hidden;
white-space: nowrap;
color: ${(props) => props.theme.text};
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
`;
const StarPositioner = styled(Flex)`
margin-left: 4px;
align-items: center;
`;
const Title = styled(Highlight)`
max-width: 90%;
overflow: hidden;
text-overflow: ellipsis;
`;
const ResultContext = styled(Highlight)`
display: block;
color: ${(props) => props.theme.textTertiary};
font-size: 14px;
margin-top: -0.25em;
margin-bottom: 0.25em;
`;
export default observer(React.forwardRef(DocumentListItem));
+77 -26
View File
@@ -1,48 +1,60 @@
// @flow
import { inject, observer } from "mobx-react";
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import styled from "styled-components";
import AuthStore from "stores/AuthStore";
import CollectionsStore from "stores/CollectionsStore";
import Document from "models/Document";
import Breadcrumb from "components/Breadcrumb";
import DocumentBreadcrumb from "components/DocumentBreadcrumb";
import DocumentTasks from "components/DocumentTasks";
import Flex from "components/Flex";
import Time from "components/Time";
import useCurrentUser from "hooks/useCurrentUser";
import useStores from "hooks/useStores";
const Container = styled(Flex)`
justify-content: ${(props) => (props.rtl ? "flex-end" : "flex-start")};
color: ${(props) => props.theme.textTertiary};
font-size: 13px;
white-space: nowrap;
overflow: hidden;
min-width: 0;
`;
const Viewed = styled.span`
text-overflow: ellipsis;
overflow: hidden;
`;
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")};
`;
type Props = {
collections: CollectionsStore,
auth: AuthStore,
type Props = {|
showCollection?: boolean,
showPublished?: boolean,
showLastViewed?: boolean,
showNestedDocuments?: boolean,
document: Document,
children: React.Node,
to?: string,
};
|};
function DocumentMeta({
auth,
collections,
showPublished,
showCollection,
showLastViewed,
showNestedDocuments,
document,
children,
to,
...rest
}: Props) {
const { t } = useTranslation();
const { collections } = useStores();
const user = useCurrentUser();
const {
modifiedSinceViewed,
updatedAt,
@@ -52,6 +64,9 @@ function DocumentMeta({
archivedAt,
deletedAt,
isDraft,
lastViewedAt,
isTasks,
isTemplate,
} = document;
// Prevent meta information from displaying if updatedBy is not available.
@@ -60,64 +75,100 @@ function DocumentMeta({
return null;
}
const collection = collections.get(document.collectionId);
const lastUpdatedByCurrentUser = user.id === updatedBy.id;
let content;
if (deletedAt) {
content = (
<span>
deleted <Time dateTime={deletedAt} /> ago
{t("deleted")} <Time dateTime={deletedAt} addSuffix />
</span>
);
} else if (archivedAt) {
content = (
<span>
archived <Time dateTime={archivedAt} /> ago
{t("archived")} <Time dateTime={archivedAt} addSuffix />
</span>
);
} else if (createdAt === updatedAt) {
content = (
<span>
created <Time dateTime={updatedAt} /> ago
{t("created")} <Time dateTime={updatedAt} addSuffix />
</span>
);
} else if (publishedAt && (publishedAt === updatedAt || showPublished)) {
content = (
<span>
published <Time dateTime={publishedAt} /> ago
{t("published")} <Time dateTime={publishedAt} addSuffix />
</span>
);
} else if (isDraft) {
content = (
<span>
saved <Time dateTime={updatedAt} /> ago
{t("saved")} <Time dateTime={updatedAt} addSuffix />
</span>
);
} else {
content = (
<Modified highlight={modifiedSinceViewed}>
updated <Time dateTime={updatedAt} /> ago
<Modified highlight={modifiedSinceViewed && !lastUpdatedByCurrentUser}>
{t("updated")} <Time dateTime={updatedAt} addSuffix />
</Modified>
);
}
const collection = collections.get(document.collectionId);
const updatedByMe = auth.user && auth.user.id === updatedBy.id;
const nestedDocumentsCount = collection
? collection.getDocumentChildren(document.id).length
: 0;
const canShowProgressBar = isTasks && !isTemplate;
const timeSinceNow = () => {
if (isDraft || !showLastViewed) {
return null;
}
if (!lastViewedAt) {
return (
<Viewed>
&nbsp;<Modified highlight>{t("Never viewed")}</Modified>
</Viewed>
);
}
return (
<Viewed>
&nbsp;{t("Viewed")} <Time dateTime={lastViewedAt} addSuffix shorten />
</Viewed>
);
};
return (
<Container align="center" {...rest}>
{updatedByMe ? "You" : updatedBy.name}&nbsp;
<Container align="center" rtl={document.dir === "rtl"} {...rest} dir="ltr">
{lastUpdatedByCurrentUser ? t("You") : updatedBy.name}&nbsp;
{to ? <Link to={to}>{content}</Link> : content}
{showCollection && collection && (
<span>
&nbsp;in&nbsp;
&nbsp;{t("in")}&nbsp;
<strong>
<Breadcrumb document={document} onlyText />
<DocumentBreadcrumb document={document} onlyText />
</strong>
</span>
)}
{showNestedDocuments && nestedDocumentsCount > 0 && (
<span>
&nbsp; {nestedDocumentsCount}{" "}
{t("nested document", { count: nestedDocumentsCount })}
</span>
)}
&nbsp;{timeSinceNow()}
{canShowProgressBar && (
<>
&nbsp;&nbsp;
<DocumentTasks document={document} />
</>
)}
{children}
</Container>
);
}
export default inject("collections", "auth")(observer(DocumentMeta));
export default observer(DocumentMeta);
+49 -12
View File
@@ -1,36 +1,73 @@
// @flow
import { inject } from "mobx-react";
import { useObserver } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { usePopoverState, PopoverDisclosure } from "reakit/Popover";
import styled from "styled-components";
import ViewsStore from "stores/ViewsStore";
import Document from "models/Document";
import DocumentMeta from "components/DocumentMeta";
import DocumentViews from "components/DocumentViews";
import Popover from "components/Popover";
import useStores from "../hooks/useStores";
type Props = {|
views: ViewsStore,
document: Document,
isDraft: boolean,
to?: string,
rtl?: boolean,
|};
function DocumentMetaWithViews({ views, to, isDraft, document }: Props) {
const totalViews = views.countForDocument(document.id);
function DocumentMetaWithViews({ to, isDraft, document, ...rest }: Props) {
const { views } = useStores();
const { t } = useTranslation();
const documentViews = useObserver(() => views.inDocument(document.id));
const totalViewers = documentViews.length;
const onlyYou = totalViewers === 1 && documentViews[0].user.id;
React.useEffect(() => {
if (!document.isDeleted) {
views.fetchPage({ documentId: document.id });
}
}, [views, document.id, document.isDeleted]);
const popover = usePopoverState({
gutter: 8,
placement: "bottom",
modal: true,
});
return (
<Meta document={document} to={to}>
{totalViews && !isDraft ? (
<>
&nbsp;&middot; Viewed{" "}
{totalViews === 1 ? "once" : `${totalViews} times`}
</>
<Meta document={document} to={to} {...rest}>
{totalViewers && !isDraft ? (
<PopoverDisclosure {...popover}>
{(props) => (
<>
&nbsp;&nbsp;
<a {...props}>
{t("Viewed by")}{" "}
{onlyYou
? t("only you")
: `${totalViewers} ${
totalViewers === 1 ? t("person") : t("people")
}`}
</a>
</>
)}
</PopoverDisclosure>
) : null}
<Popover {...popover} width={300} aria-label={t("Viewers")} tabIndex={0}>
<DocumentViews document={document} isOpen={popover.visible} />
</Popover>
</Meta>
);
}
const Meta = styled(DocumentMeta)`
justify-content: ${(props) => (props.rtl ? "flex-end" : "flex-start")};
margin: -12px 0 2em 0;
font-size: 14px;
position: relative;
z-index: 1;
a {
color: inherit;
@@ -45,4 +82,4 @@ const Meta = styled(DocumentMeta)`
}
`;
export default inject("views")(DocumentMetaWithViews);
export default DocumentMetaWithViews;
@@ -1,231 +0,0 @@
// @flow
import { observer } from "mobx-react";
import { StarredIcon, PlusIcon } from "outline-icons";
import * as React from "react";
import { Link, withRouter, type RouterHistory } from "react-router-dom";
import styled, { withTheme } from "styled-components";
import Document from "models/Document";
import Badge from "components/Badge";
import Button from "components/Button";
import DocumentMeta from "components/DocumentMeta";
import Flex from "components/Flex";
import Highlight from "components/Highlight";
import Tooltip from "components/Tooltip";
import DocumentMenu from "menus/DocumentMenu";
import { newDocumentUrl } from "utils/routeHelpers";
type Props = {
history: RouterHistory,
document: Document,
highlight?: ?string,
context?: ?string,
showCollection?: boolean,
showPublished?: boolean,
showPin?: boolean,
showDraft?: boolean,
showTemplate?: boolean,
};
const SEARCH_RESULT_REGEX = /<b\b[^>]*>(.*?)<\/b>/gi;
@observer
class DocumentPreview extends React.Component<Props> {
handleStar = (ev: SyntheticEvent<>) => {
ev.preventDefault();
ev.stopPropagation();
this.props.document.star();
};
handleUnstar = (ev: SyntheticEvent<>) => {
ev.preventDefault();
ev.stopPropagation();
this.props.document.unstar();
};
replaceResultMarks = (tag: string) => {
// don't use SEARCH_RESULT_REGEX here as it causes
// an infinite loop to trigger a regex inside it's own callback
return tag.replace(/<b\b[^>]*>(.*?)<\/b>/gi, "$1");
};
handleNewFromTemplate = (event) => {
event.preventDefault();
event.stopPropagation();
const { document } = this.props;
this.props.history.push(
newDocumentUrl(document.collectionId, {
templateId: document.id,
})
);
};
render() {
const {
document,
showCollection,
showPublished,
showPin,
showDraft = true,
showTemplate,
highlight,
context,
} = this.props;
const queryIsInTitle =
!!highlight &&
!!document.title.toLowerCase().includes(highlight.toLowerCase());
return (
<DocumentLink
to={{
pathname: document.url,
state: { title: document.titleWithDefault },
}}
>
<Heading>
<Title text={document.titleWithDefault} highlight={highlight} />
{!document.isDraft &&
!document.isArchived &&
!document.isTemplate && (
<Actions>
{document.isStarred ? (
<StyledStar onClick={this.handleUnstar} solid />
) : (
<StyledStar onClick={this.handleStar} />
)}
</Actions>
)}
{document.isDraft && showDraft && (
<Tooltip tooltip="Only visible to you" delay={500} placement="top">
<Badge>Draft</Badge>
</Tooltip>
)}
{document.isTemplate && showTemplate && (
<Badge primary>Template</Badge>
)}
<SecondaryActions>
{document.isTemplate &&
!document.isArchived &&
!document.isDeleted && (
<Button
onClick={this.handleNewFromTemplate}
icon={<PlusIcon />}
neutral
>
New doc
</Button>
)}
&nbsp;
<DocumentMenu document={document} showPin={showPin} />
</SecondaryActions>
</Heading>
{!queryIsInTitle && (
<ResultContext
text={context}
highlight={highlight ? SEARCH_RESULT_REGEX : undefined}
processResult={this.replaceResultMarks}
/>
)}
<DocumentMeta
document={document}
showCollection={showCollection}
showPublished={showPublished}
/>
</DocumentLink>
);
}
}
const StyledStar = withTheme(styled(({ solid, theme, ...props }) => (
<StarredIcon color={theme.text} {...props} />
))`
flex-shrink: 0;
opacity: ${(props) => (props.solid ? "1 !important" : 0)};
transition: all 100ms ease-in-out;
&:hover {
transform: scale(1.1);
}
&:active {
transform: scale(0.95);
}
`);
const SecondaryActions = styled(Flex)`
align-items: center;
position: absolute;
right: 16px;
top: 50%;
transform: translateY(-50%);
`;
const DocumentLink = styled(Link)`
display: block;
margin: 10px -8px;
padding: 6px 8px;
border-radius: 8px;
max-height: 50vh;
min-width: 100%;
overflow: hidden;
position: relative;
${SecondaryActions} {
opacity: 0;
}
&:hover,
&:active,
&:focus {
background: ${(props) => props.theme.listItemHoverBackground};
outline: none;
${SecondaryActions} {
opacity: 1;
}
${StyledStar} {
opacity: 0.5;
&:hover {
opacity: 1;
}
}
}
`;
const Heading = styled.h3`
display: flex;
align-items: center;
height: 24px;
margin-top: 0;
margin-bottom: 0.25em;
overflow: hidden;
white-space: nowrap;
color: ${(props) => props.theme.text};
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
`;
const Actions = styled(Flex)`
margin-left: 4px;
align-items: center;
`;
const Title = styled(Highlight)`
max-width: 90%;
overflow: hidden;
text-overflow: ellipsis;
`;
const ResultContext = styled(Highlight)`
display: block;
color: ${(props) => props.theme.textTertiary};
font-size: 14px;
margin-top: -0.25em;
margin-bottom: 0.25em;
`;
export default withRouter(DocumentPreview);
-3
View File
@@ -1,3 +0,0 @@
// @flow
import DocumentPreview from "./DocumentPreview";
export default DocumentPreview;
+59
View File
@@ -0,0 +1,59 @@
// @flow
import { DoneIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled, { useTheme } from "styled-components";
import CircularProgressBar from "components/CircularProgressBar";
import usePrevious from "../hooks/usePrevious";
import Document from "../models/Document";
import { bounceIn } from "styles/animations";
type Props = {|
document: Document,
|};
function getMessage(t, total, completed) {
if (completed === 0) {
return t(`{{ total }} task`, { total, count: total });
} else if (completed === total) {
return t(`{{ completed }} task done`, { completed, count: completed });
} else {
return t(`{{ completed }} of {{ total }} tasks`, {
total,
completed,
});
}
}
function DocumentTasks({ document }: Props) {
const { tasks, tasksPercentage } = document;
const { t } = useTranslation();
const theme = useTheme();
const { completed, total } = tasks;
const done = completed === total;
const previousDone = usePrevious(done);
const message = getMessage(t, total, completed);
return (
<>
{completed === total ? (
<Done
color={theme.primary}
size={20}
$animated={done && previousDone === false}
/>
) : (
<CircularProgressBar percentage={tasksPercentage} />
)}
&nbsp;{message}
</>
);
}
const Done = styled(DoneIcon)`
margin: -1px;
animation: ${(props) => (props.$animated ? bounceIn : "none")} 600ms;
transform-origin: center center;
`;
export default DocumentTasks;
+80
View File
@@ -0,0 +1,80 @@
// @flow
import { formatDistanceToNow } from "date-fns";
import { sortBy } from "lodash";
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import Document from "models/Document";
import Avatar from "components/Avatar";
import ListItem from "components/List/Item";
import PaginatedList from "components/PaginatedList";
import useStores from "hooks/useStores";
type Props = {|
document: Document,
isOpen?: boolean,
|};
function DocumentViews({ document, isOpen }: Props) {
const { t } = useTranslation();
const { views, presence } = useStores();
let documentPresence = presence.get(document.id);
documentPresence = documentPresence
? Array.from(documentPresence.values())
: [];
const presentIds = documentPresence.map((p) => p.userId);
const editingIds = documentPresence
.filter((p) => p.isEditing)
.map((p) => p.userId);
// ensure currently present via websocket are always ordered first
const documentViews = views.inDocument(document.id);
const sortedViews = sortBy(
documentViews,
(view) => !presentIds.includes(view.user.id)
);
const users = React.useMemo(() => sortedViews.map((v) => v.user), [
sortedViews,
]);
return (
<>
{isOpen && (
<PaginatedList
items={users}
renderItem={(item) => {
const view = documentViews.find((v) => v.user.id === item.id);
const isPresent = presentIds.includes(item.id);
const isEditing = editingIds.includes(item.id);
const subtitle = isPresent
? isEditing
? t("Currently editing")
: t("Currently viewing")
: t("Viewed {{ timeAgo }} ago", {
timeAgo: formatDistanceToNow(
view ? Date.parse(view.lastViewedAt) : new Date()
),
});
return (
<ListItem
key={item.id}
title={item.name}
subtitle={subtitle}
image={<Avatar key={item.id} src={item.avatarUrl} size={32} />}
border={false}
small
/>
);
}}
/>
)}
</>
);
}
export default observer(DocumentViews);
-113
View File
@@ -1,113 +0,0 @@
// @flow
import invariant from "invariant";
import { observable } from "mobx";
import { observer, inject } from "mobx-react";
import * as React from "react";
import Dropzone from "react-dropzone";
import { withRouter, type RouterHistory, type Match } from "react-router-dom";
import { createGlobalStyle } from "styled-components";
import DocumentsStore from "stores/DocumentsStore";
import LoadingIndicator from "components/LoadingIndicator";
import importFile from "utils/importFile";
const EMPTY_OBJECT = {};
let importingLock = false;
type Props = {
children: React.Node,
collectionId: string,
documentId?: string,
activeClassName?: string,
rejectClassName?: string,
documents: DocumentsStore,
disabled: boolean,
location: Object,
match: Match,
history: RouterHistory,
staticContext: Object,
};
export const GlobalStyles = createGlobalStyle`
.activeDropZone {
border-radius: 4px;
background: ${(props) => props.theme.slateDark};
svg { fill: ${(props) => props.theme.white}; }
}
.activeDropZone a {
color: ${(props) => props.theme.white} !important;
}
`;
@observer
class DropToImport extends React.Component<Props> {
@observable isImporting: boolean = false;
onDropAccepted = async (files = []) => {
if (importingLock) return;
this.isImporting = true;
importingLock = true;
try {
let collectionId = this.props.collectionId;
const documentId = this.props.documentId;
const redirect = files.length === 1;
if (documentId && !collectionId) {
const document = await this.props.documents.fetch(documentId);
invariant(document, "Document not available");
collectionId = document.collectionId;
}
for (const file of files) {
const doc = await importFile({
documents: this.props.documents,
file,
documentId,
collectionId,
});
if (redirect) {
this.props.history.push(doc.url);
}
}
} finally {
this.isImporting = false;
importingLock = false;
}
};
render() {
const {
documentId,
collectionId,
documents,
disabled,
location,
match,
history,
staticContext,
...rest
} = this.props;
if (this.props.disabled) return this.props.children;
return (
<Dropzone
accept="text/markdown, text/plain"
onDropAccepted={this.onDropAccepted}
style={EMPTY_OBJECT}
disableClick
disablePreview
multiple
{...rest}
>
{this.isImporting && <LoadingIndicator />}
{this.props.children}
</Dropzone>
);
}
}
export default inject("documents")(withRouter(DropToImport));
-286
View File
@@ -1,286 +0,0 @@
// @flow
import invariant from "invariant";
import { observable } from "mobx";
import { observer } from "mobx-react";
import { MoreIcon } from "outline-icons";
import { rgba } from "polished";
import * as React from "react";
import { PortalWithState } from "react-portal";
import styled from "styled-components";
import { fadeAndScaleIn } from "shared/styles/animations";
import Flex from "components/Flex";
import NudeButton from "components/NudeButton";
let previousClosePortal;
let counter = 0;
type Children =
| React.Node
| ((options: { closePortal: () => void }) => React.Node);
type Props = {
label?: React.Node,
onOpen?: () => void,
onClose?: () => void,
children?: Children,
className?: string,
hover?: boolean,
style?: Object,
position?: "left" | "right" | "center",
};
@observer
class DropdownMenu extends React.Component<Props> {
id: string = `menu${counter++}`;
closeTimeout: TimeoutID;
@observable top: ?number;
@observable bottom: ?number;
@observable right: ?number;
@observable left: ?number;
@observable position: "left" | "right" | "center";
@observable fixed: ?boolean;
@observable bodyRect: ClientRect;
@observable labelRect: ClientRect;
@observable dropdownRef: { current: null | HTMLElement } = React.createRef();
@observable menuRef: { current: null | HTMLElement } = React.createRef();
handleOpen = (
openPortal: (SyntheticEvent<>) => void,
closePortal: () => void
) => {
return (ev: SyntheticMouseEvent<HTMLElement>) => {
ev.preventDefault();
const currentTarget = ev.currentTarget;
invariant(document.body, "why you not here");
if (currentTarget instanceof HTMLDivElement) {
this.bodyRect = document.body.getBoundingClientRect();
this.labelRect = currentTarget.getBoundingClientRect();
this.top = this.labelRect.bottom - this.bodyRect.top;
this.bottom = undefined;
this.position = this.props.position || "left";
if (currentTarget.parentElement) {
const triggerParentStyle = getComputedStyle(
currentTarget.parentElement
);
if (triggerParentStyle.position === "static") {
this.fixed = true;
this.top = this.labelRect.bottom;
}
}
this.initPosition();
// attempt to keep only one flyout menu open at once
if (previousClosePortal && !this.props.hover) {
previousClosePortal();
}
previousClosePortal = closePortal;
openPortal(ev);
}
};
};
initPosition() {
if (this.position === "left") {
this.right =
this.bodyRect.width - this.labelRect.left - this.labelRect.width;
} else if (this.position === "center") {
this.left = this.labelRect.left + this.labelRect.width / 2;
} else {
this.left = this.labelRect.left;
}
}
onOpen = () => {
if (typeof this.props.onOpen === "function") {
this.props.onOpen();
}
this.fitOnTheScreen();
};
fitOnTheScreen() {
if (!this.dropdownRef || !this.dropdownRef.current) return;
const el = this.dropdownRef.current;
const sticksOutPastBottomEdge =
el.clientHeight + this.top > window.innerHeight;
if (sticksOutPastBottomEdge) {
this.top = undefined;
this.bottom = this.fixed ? 0 : -1 * window.pageYOffset;
} else {
this.bottom = undefined;
}
if (this.position === "left" || this.position === "right") {
const totalWidth =
Math.sign(this.position === "left" ? -1 : 1) * el.offsetLeft +
el.scrollWidth;
const isVisible = totalWidth < window.innerWidth;
if (!isVisible) {
if (this.position === "right") {
this.position = "left";
this.left = undefined;
} else if (this.position === "left") {
this.position = "right";
this.right = undefined;
}
}
}
this.initPosition();
this.forceUpdate();
}
closeAfterTimeout = (closePortal: () => void) => () => {
if (this.closeTimeout) {
clearTimeout(this.closeTimeout);
}
this.closeTimeout = setTimeout(closePortal, 500);
};
clearCloseTimeout = () => {
if (this.closeTimeout) {
clearTimeout(this.closeTimeout);
}
};
render() {
const { className, hover, label, children } = this.props;
return (
<div className={className}>
<PortalWithState
onOpen={this.onOpen}
onClose={this.props.onClose}
closeOnOutsideClick
closeOnEsc
>
{({ closePortal, openPortal, isOpen, portal }) => (
<>
<Label
onMouseMove={hover ? this.clearCloseTimeout : undefined}
onMouseOut={
hover ? this.closeAfterTimeout(closePortal) : undefined
}
onMouseEnter={
hover ? this.handleOpen(openPortal, closePortal) : undefined
}
onClick={
hover ? undefined : this.handleOpen(openPortal, closePortal)
}
>
{label || (
<NudeButton
id={`${this.id}button`}
aria-haspopup="true"
aria-expanded={isOpen ? "true" : "false"}
aria-controls={this.id}
>
<MoreIcon />
</NudeButton>
)}
</Label>
{portal(
<Position
ref={this.dropdownRef}
position={this.position}
fixed={this.fixed}
top={this.top}
bottom={this.bottom}
left={this.left}
right={this.right}
>
<Menu
ref={this.menuRef}
onMouseMove={hover ? this.clearCloseTimeout : undefined}
onMouseOut={
hover ? this.closeAfterTimeout(closePortal) : undefined
}
onClick={
typeof children === "function"
? undefined
: (ev) => {
ev.stopPropagation();
closePortal();
}
}
style={this.props.style}
id={this.id}
aria-labelledby={`${this.id}button`}
role="menu"
>
{typeof children === "function"
? children({ closePortal })
: children}
</Menu>
</Position>
)}
</>
)}
</PortalWithState>
</div>
);
}
}
const Label = styled(Flex).attrs({
justify: "center",
align: "center",
})`
z-index: 1000;
cursor: pointer;
`;
const Position = styled.div`
position: ${({ fixed }) => (fixed ? "fixed" : "absolute")};
display: flex;
${({ left }) => (left !== undefined ? `left: ${left}px` : "")};
${({ right }) => (right !== undefined ? `right: ${right}px` : "")};
${({ top }) => (top !== undefined ? `top: ${top}px` : "")};
${({ bottom }) => (bottom !== undefined ? `bottom: ${bottom}px` : "")};
max-height: 75%;
z-index: 1000;
transform: ${(props) =>
props.position === "center" ? "translateX(-50%)" : "initial"};
pointer-events: none;
`;
const Menu = styled.div`
animation: ${fadeAndScaleIn} 200ms ease;
transform-origin: ${(props) => (props.left !== undefined ? "25%" : "75%")} 0;
backdrop-filter: blur(10px);
background: ${(props) => rgba(props.theme.menuBackground, 0.8)};
border: ${(props) =>
props.theme.menuBorder ? `1px solid ${props.theme.menuBorder}` : "none"};
border-radius: 2px;
padding: 0.5em 0;
min-width: 180px;
overflow: hidden;
overflow-y: auto;
box-shadow: ${(props) => props.theme.menuShadow};
pointer-events: all;
hr {
margin: 0.5em 12px;
}
@media print {
display: none;
}
`;
export const Header = styled.h3`
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
color: ${(props) => props.theme.sidebarText};
letter-spacing: 0.04em;
margin: 1em 12px 0.5em;
`;
export default DropdownMenu;
@@ -1,88 +0,0 @@
// @flow
import { CheckmarkIcon } from "outline-icons";
import * as React from "react";
import styled from "styled-components";
type Props = {
onClick?: (SyntheticEvent<>) => void | Promise<void>,
children?: React.Node,
selected?: boolean,
disabled?: boolean,
};
const DropdownMenuItem = ({
onClick,
children,
selected,
disabled,
...rest
}: Props) => {
return (
<MenuItem
onClick={disabled ? undefined : onClick}
disabled={disabled}
role="menuitem"
tabIndex="-1"
{...rest}
>
{selected !== undefined && (
<>
<CheckmarkIcon
color={selected === false ? "transparent" : undefined}
/>
&nbsp;
</>
)}
{children}
</MenuItem>
);
};
const MenuItem = styled.a`
display: flex;
margin: 0;
padding: 6px 12px;
width: 100%;
min-height: 32px;
color: ${(props) =>
props.disabled ? props.theme.textTertiary : props.theme.textSecondary};
justify-content: left;
align-items: center;
font-size: 15px;
cursor: default;
user-select: none;
svg:not(:last-child) {
margin-right: 8px;
}
svg {
opacity: ${(props) => (props.disabled ? ".5" : 1)};
}
${(props) =>
props.disabled
? "pointer-events: none;"
: `
&:hover {
color: ${props.theme.white};
background: ${props.theme.primary};
box-shadow: none;
cursor: pointer;
svg {
fill: ${props.theme.white};
}
}
&:focus {
color: ${props.theme.white};
background: ${props.theme.primary};
outline: none;
}
`};
`;
export default DropdownMenuItem;
-3
View File
@@ -1,3 +0,0 @@
// @flow
export { default as DropdownMenu, Header } from "./DropdownMenu";
export { default as DropdownMenuItem } from "./DropdownMenuItem";
+233 -49
View File
@@ -1,83 +1,198 @@
// @flow
import { lighten } from "polished";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { withRouter, type RouterHistory } from "react-router-dom";
import RichMarkdownEditor from "rich-markdown-editor";
import styled, { withTheme } from "styled-components";
import { light } from "shared/theme";
import UiStore from "stores/UiStore";
import ErrorBoundary from "components/ErrorBoundary";
import Tooltip from "components/Tooltip";
import embeds from "../embeds";
import isInternalUrl from "utils/isInternalUrl";
import useMediaQuery from "hooks/useMediaQuery";
import useToasts from "hooks/useToasts";
import { type Theme } from "types";
import { isModKey } from "utils/keyboard";
import { uploadFile } from "utils/uploadFile";
import { isInternalUrl } from "utils/urls";
const RichMarkdownEditor = React.lazy(() =>
import(/* webpackChunkName: "rich-markdown-editor" */ "rich-markdown-editor")
);
const EMPTY_ARRAY = [];
type Props = {
export type Props = {|
id?: string,
value?: string,
defaultValue?: string,
readOnly?: boolean,
grow?: boolean,
disableEmbeds?: boolean,
ui?: UiStore,
};
shareId?: ?string,
autoFocus?: boolean,
template?: boolean,
placeholder?: string,
maxLength?: number,
scrollTo?: string,
theme?: Theme,
handleDOMEvents?: Object,
readOnlyWriteCheckboxes?: boolean,
onBlur?: (event: SyntheticEvent<>) => any,
onFocus?: (event: SyntheticEvent<>) => any,
onPublish?: (event: SyntheticEvent<>) => any,
onSave?: ({ done?: boolean, autosave?: boolean, publish?: boolean }) => any,
onCancel?: () => any,
onDoubleClick?: () => any,
onChange?: (getValue: () => string) => any,
onSearchLink?: (title: string) => any,
onHoverLink?: (event: MouseEvent) => any,
onCreateLink?: (title: string) => Promise<string>,
onImageUploadStart?: () => any,
onImageUploadStop?: () => any,
|};
type PropsWithRef = Props & {
forwardedRef: React.Ref<RichMarkdownEditor>,
forwardedRef: React.Ref<any>,
history: RouterHistory,
};
class Editor extends React.Component<PropsWithRef> {
onUploadImage = async (file: File) => {
const result = await uploadFile(file, { documentId: this.props.id });
return result.url;
};
function Editor(props: PropsWithRef) {
const { id, shareId, history } = props;
const { t } = useTranslation();
const { showToast } = useToasts();
const isPrinting = useMediaQuery("print");
onClickLink = (href: string) => {
// on page hash
if (href[0] === "#") {
window.location.href = href;
return;
}
const onUploadImage = React.useCallback(
async (file: File) => {
const result = await uploadFile(file, { documentId: id });
return result.url;
},
[id]
);
if (isInternalUrl(href)) {
// relative
let navigateTo = href;
// probably absolute
if (href[0] !== "/") {
try {
const url = new URL(href);
navigateTo = url.pathname + url.hash;
} catch (err) {
navigateTo = href;
}
const onClickLink = React.useCallback(
(href: string, event: MouseEvent) => {
// on page hash
if (href[0] === "#") {
window.location.href = href;
return;
}
this.props.history.push(navigateTo);
} else {
window.open(href, "_blank");
}
};
if (isInternalUrl(href) && !isModKey(event) && !event.shiftKey) {
// relative
let navigateTo = href;
onShowToast = (message: string) => {
if (this.props.ui) {
this.props.ui.showToast(message);
}
};
// probably absolute
if (href[0] !== "/") {
try {
const url = new URL(href);
navigateTo = url.pathname + url.hash;
} catch (err) {
navigateTo = href;
}
}
render() {
return (
if (shareId) {
navigateTo = `/share/${shareId}${navigateTo}`;
}
history.push(navigateTo);
} else if (href) {
window.open(href, "_blank");
}
},
[history, shareId]
);
const onShowToast = React.useCallback(
(message: string) => {
showToast(message);
},
[showToast]
);
const dictionary = React.useMemo(() => {
return {
addColumnAfter: t("Insert column after"),
addColumnBefore: t("Insert column before"),
addRowAfter: t("Insert row after"),
addRowBefore: t("Insert row before"),
alignCenter: t("Align center"),
alignLeft: t("Align left"),
alignRight: t("Align right"),
bulletList: t("Bulleted list"),
checkboxList: t("Todo list"),
codeBlock: t("Code block"),
codeCopied: t("Copied to clipboard"),
codeInline: t("Code"),
createLink: t("Create link"),
createLinkError: t("Sorry, an error occurred creating the link"),
createNewDoc: t("Create a new doc"),
deleteColumn: t("Delete column"),
deleteRow: t("Delete row"),
deleteTable: t("Delete table"),
deleteImage: t("Delete image"),
downloadImage: t("Download image"),
alignImageLeft: t("Float left"),
alignImageRight: t("Float right"),
alignImageDefault: t("Center large"),
em: t("Italic"),
embedInvalidLink: t("Sorry, that link wont work for this embed type"),
findOrCreateDoc: `${t("Find or create a doc")}`,
h1: t("Big heading"),
h2: t("Medium heading"),
h3: t("Small heading"),
heading: t("Heading"),
hr: t("Divider"),
image: t("Image"),
imageUploadError: t("Sorry, an error occurred uploading the image"),
imageCaptionPlaceholder: t("Write a caption"),
info: t("Info"),
infoNotice: t("Info notice"),
link: t("Link"),
linkCopied: t("Link copied to clipboard"),
mark: t("Highlight"),
newLineEmpty: `${t("Type '/' to insert")}`,
newLineWithSlash: `${t("Keep typing to filter")}`,
noResults: t("No results"),
openLink: t("Open link"),
orderedList: t("Ordered list"),
pageBreak: t("Page break"),
pasteLink: `${t("Paste a link")}`,
pasteLinkWithTitle: (service: string) =>
t("Paste a {{service}} link…", { service }),
placeholder: t("Placeholder"),
quote: t("Quote"),
removeLink: t("Remove link"),
searchOrPasteLink: `${t("Search or paste a link")}`,
strikethrough: t("Strikethrough"),
strong: t("Bold"),
subheading: t("Subheading"),
table: t("Table"),
tip: t("Tip"),
tipNotice: t("Tip notice"),
warning: t("Warning"),
warningNotice: t("Warning notice"),
};
}, [t]);
return (
<ErrorBoundary reloadOnChunkMissing>
<StyledEditor
ref={this.props.forwardedRef}
uploadImage={this.onUploadImage}
onClickLink={this.onClickLink}
onShowToast={this.onShowToast}
embeds={this.props.disableEmbeds ? EMPTY_ARRAY : embeds}
ref={props.forwardedRef}
uploadImage={onUploadImage}
onClickLink={onClickLink}
onShowToast={onShowToast}
embeds={props.disableEmbeds ? EMPTY_ARRAY : embeds}
tooltip={EditorTooltip}
{...this.props}
dictionary={dictionary}
{...props}
theme={isPrinting ? light : props.theme}
/>
);
}
</ErrorBoundary>
);
}
const StyledEditor = styled(RichMarkdownEditor)`
@@ -85,7 +200,11 @@ const StyledEditor = styled(RichMarkdownEditor)`
justify-content: start;
> div {
transition: ${(props) => props.theme.backgroundTransition};
background: transparent;
}
& * {
box-sizing: content-box;
}
.notice-block.tip,
@@ -93,6 +212,27 @@ const StyledEditor = styled(RichMarkdownEditor)`
font-weight: 500;
}
.heading-anchor {
box-sizing: border-box;
}
.heading-name {
pointer-events: none;
display: block;
position: relative;
top: -60px;
visibility: hidden;
}
.heading-name:first-child {
& + h1,
& + h2,
& + h3,
& + h4 {
margin-top: 0;
}
}
p {
a {
color: ${(props) => props.theme.text};
@@ -106,6 +246,50 @@ const StyledEditor = styled(RichMarkdownEditor)`
}
}
}
.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;
}
}
}
}
`;
const EditorTooltip = ({ children, ...props }) => (
+69 -17
View File
@@ -1,7 +1,9 @@
// @flow
import * as Sentry from "@sentry/react";
import { observable } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import { withTranslation, type TFunction, Trans } from "react-i18next";
import styled from "styled-components";
import Button from "components/Button";
import CenteredContent from "components/CenteredContent";
@@ -10,9 +12,11 @@ import PageTitle from "components/PageTitle";
import { githubIssuesUrl } from "../../shared/utils/routeHelpers";
import env from "env";
type Props = {
type Props = {|
children: React.Node,
};
reloadOnChunkMissing?: boolean,
t: TFunction,
|};
@observer
class ErrorBoundary extends React.Component<Props> {
@@ -23,13 +27,25 @@ class ErrorBoundary extends React.Component<Props> {
this.error = error;
console.error(error);
if (window.Sentry) {
window.Sentry.captureException(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 (env.SENTRY_DSN) {
Sentry.captureException(error);
}
}
handleReload = () => {
window.location.reload();
window.location.reload(true);
};
handleShowDetails = () => {
@@ -41,28 +57,64 @@ class ErrorBoundary extends React.Component<Props> {
};
render() {
const { t } = this.props;
if (this.error) {
const isReported = !!window.Sentry && env.DEPLOYMENT === "hosted";
const error = this.error;
const isReported = !!env.SENTRY_DSN && env.DEPLOYMENT === "hosted";
const isChunkError = this.error.message.match(/chunk/);
if (isChunkError) {
return (
<CenteredContent>
<PageTitle title={t("Module failed to load")} />
<h1>
<Trans>Loading Failed</Trans>
</h1>
<HelpText>
<Trans>
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.
</Trans>
</HelpText>
<p>
<Button onClick={this.handleReload}>
<Trans>Reload</Trans>
</Button>
</p>
</CenteredContent>
);
}
return (
<CenteredContent>
<PageTitle title="Something Unexpected Happened" />
<h1>Something Unexpected Happened</h1>
<PageTitle title={t("Something Unexpected Happened")} />
<h1>
<Trans>Something Unexpected Happened</Trans>
</h1>
<HelpText>
Sorry, an unrecoverable error occurred
{isReported && " our engineers have been notified"}. Please try
reloading the page, it may have been a temporary glitch.
<Trans
defaults="Sorry, an unrecoverable error occurred{{notified}}. Please try reloading the page, it may have been a temporary glitch."
values={{
notified: isReported
? ` ${t("our engineers have been notified")}`
: undefined,
}}
/>
</HelpText>
{this.showDetails && <Pre>{this.error.toString()}</Pre>}
{this.showDetails && <Pre>{error.toString()}</Pre>}
<p>
<Button onClick={this.handleReload}>Reload</Button>{" "}
<Button onClick={this.handleReload}>
<Trans>Reload</Trans>
</Button>{" "}
{this.showDetails ? (
<Button onClick={this.handleReportBug} neutral>
Report a Bug
<Trans>Report a Bug</Trans>
</Button>
) : (
<Button onClick={this.handleShowDetails} neutral>
Show Details
<Trans>Show Detail</Trans>
</Button>
)}
</p>
@@ -74,11 +126,11 @@ class ErrorBoundary extends React.Component<Props> {
}
const Pre = styled.pre`
background: ${(props) => props.theme.smoke};
background: ${(props) => props.theme.secondaryBackground};
padding: 16px;
border-radius: 4px;
font-size: 12px;
white-space: pre-wrap;
`;
export default ErrorBoundary;
export default withTranslation()<ErrorBoundary>(ErrorBoundary);
+21
View File
@@ -0,0 +1,21 @@
// @flow
import * as React from "react";
type Props = {
children: React.Node,
className?: string,
};
export default function EventBoundary({ children, className }: Props) {
const handleClick = React.useCallback((event: SyntheticEvent<>) => {
event.preventDefault();
event.stopPropagation();
}, []);
return (
<span onClick={handleClick} className={className}>
{children}
</span>
);
}
+163
View File
@@ -0,0 +1,163 @@
// @flow
import {
TrashIcon,
ArchiveIcon,
EditIcon,
PublishIcon,
MoveIcon,
CheckboxIcon,
} from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import Document from "models/Document";
import Event from "models/Event";
import Avatar from "components/Avatar";
import Item, { Actions } from "components/List/Item";
import Time from "components/Time";
import RevisionMenu from "menus/RevisionMenu";
import { documentHistoryUrl } from "utils/routeHelpers";
type Props = {|
document: Document,
event: Event,
latest?: boolean,
|};
const EventListItem = ({ event, latest, document }: Props) => {
const { t } = useTranslation();
const opts = { userName: event.actor.name };
const isRevision = event.name === "revisions.create";
let meta, icon, to;
switch (event.name) {
case "revisions.create":
case "documents.latest_version": {
if (latest) {
icon = <CheckboxIcon color="currentColor" size={16} checked />;
meta = t("Latest version");
to = documentHistoryUrl(document);
break;
} else {
icon = <EditIcon color="currentColor" size={16} />;
meta = t("{{userName}} edited", opts);
to = documentHistoryUrl(document, event.modelId || "");
break;
}
}
case "documents.archive":
icon = <ArchiveIcon color="currentColor" size={16} />;
meta = t("{{userName}} archived", opts);
break;
case "documents.unarchive":
meta = t("{{userName}} restored", opts);
break;
case "documents.delete":
icon = <TrashIcon color="currentColor" size={16} />;
meta = t("{{userName}} deleted", opts);
break;
case "documents.restore":
meta = t("{{userName}} moved from trash", opts);
break;
case "documents.publish":
icon = <PublishIcon color="currentColor" size={16} />;
meta = t("{{userName}} published", opts);
break;
case "documents.move":
icon = <MoveIcon color="currentColor" size={16} />;
meta = t("{{userName}} moved", opts);
break;
default:
console.warn("Unhandled event: ", event.name);
}
if (!meta) {
return null;
}
return (
<ListItem
small
exact
to={to}
title={
<Time
dateTime={event.createdAt}
tooltipDelay={250}
format="MMMM do, h:mm a"
relative={false}
addSuffix
/>
}
image={<Avatar src={event.actor?.avatarUrl} size={32} />}
subtitle={
<Subtitle>
{icon}
{meta}
</Subtitle>
}
actions={
isRevision ? (
<RevisionMenu document={document} revisionId={event.modelId} />
) : undefined
}
/>
);
};
const Subtitle = styled.span`
svg {
margin: -3px;
margin-right: 2px;
}
`;
const ListItem = styled(Item)`
border: 0;
position: relative;
margin: 8px;
padding: 8px;
border-radius: 8px;
img {
border-color: transparent;
}
&::before {
content: "";
display: block;
position: absolute;
top: -4px;
left: 23px;
width: 2px;
height: calc(100% + 8px);
background: ${(props) => props.theme.textSecondary};
opacity: 0.25;
}
&:nth-child(2)::before {
height: 50%;
top: 50%;
}
&:last-child::before {
height: 50%;
}
&:first-child:last-child::before {
display: none;
}
${Actions} {
opacity: 0.25;
transition: opacity 100ms ease-in-out;
}
&:hover {
${Actions} {
opacity: 1;
}
}
`;
export default EventListItem;
+27 -31
View File
@@ -1,45 +1,41 @@
// @flow
import { observer, inject } from "mobx-react";
import { observer } from "mobx-react";
import * as React from "react";
import styled, { withTheme } from "styled-components";
import styled from "styled-components";
import User from "models/User";
import Avatar from "components/Avatar";
import Flex from "components/Flex";
type Props = {
type Props = {|
users: User[],
size?: number,
overflow: number,
renderAvatar: (user: User) => React.Node,
};
onClick?: (event: SyntheticEvent<>) => mixed,
renderAvatar?: (user: User) => React.Node,
|};
@observer
class Facepile extends React.Component<Props> {
render() {
const {
users,
overflow,
size = 32,
renderAvatar = renderDefaultAvatar,
...rest
} = this.props;
return (
<Avatars {...rest}>
{overflow > 0 && (
<More size={size}>
<span>+{overflow}</span>
</More>
)}
{users.map((user) => (
<AvatarWrapper key={user.id}>{renderAvatar(user)}</AvatarWrapper>
))}
</Avatars>
);
}
function Facepile({
users,
overflow,
size = 32,
renderAvatar = DefaultAvatar,
...rest
}: Props) {
return (
<Avatars {...rest}>
{overflow > 0 && (
<More size={size}>
<span>+{overflow}</span>
</More>
)}
{users.map((user) => (
<AvatarWrapper key={user.id}>{renderAvatar(user)}</AvatarWrapper>
))}
</Avatars>
);
}
function renderDefaultAvatar(user: User) {
function DefaultAvatar(user: User) {
return <Avatar user={user} src={user.avatarUrl} size={32} />;
}
@@ -73,4 +69,4 @@ const Avatars = styled(Flex)`
cursor: pointer;
`;
export default inject("views", "presence")(withTheme(Facepile));
export default observer(Facepile);
+1 -1
View File
@@ -1,6 +1,6 @@
// @flow
import styled from "styled-components";
import { fadeIn } from "shared/styles/animations";
import { fadeIn } from "styles/animations";
const Fade = styled.span`
animation: ${fadeIn} ${(props) => props.timing || "250ms"} ease-in-out;
+111
View File
@@ -0,0 +1,111 @@
// @flow
import { find } from "lodash";
import * as React from "react";
import { useMenuState, MenuButton } from "reakit/Menu";
import styled from "styled-components";
import Button, { Inner } from "components/Button";
import ContextMenu from "components/ContextMenu";
import MenuItem from "components/ContextMenu/MenuItem";
import HelpText from "components/HelpText";
type TFilterOption = {|
key: string,
label: string,
note?: string,
|};
type Props = {|
options: TFilterOption[],
activeKey: ?string,
defaultLabel?: string,
selectedPrefix?: string,
className?: string,
onSelect: (key: ?string) => void,
|};
const FilterOptions = ({
options,
activeKey = "",
defaultLabel,
selectedPrefix = "",
className,
onSelect,
}: Props) => {
const menu = useMenuState({ modal: true });
const selected = find(options, { key: activeKey }) || options[0];
const selectedLabel = selected ? `${selectedPrefix} ${selected.label}` : "";
return (
<Wrapper>
<MenuButton {...menu}>
{(props) => (
<StyledButton
{...props}
className={className}
neutral
disclosure
small
>
{activeKey ? selectedLabel : defaultLabel}
</StyledButton>
)}
</MenuButton>
<ContextMenu aria-label={defaultLabel} {...menu}>
{options.map((option) => (
<MenuItem
key={option.key}
onClick={() => {
onSelect(option.key);
menu.hide();
}}
selected={option.key === activeKey}
{...menu}
>
{option.note ? (
<LabelWithNote>
{option.label}
<Note>{option.note}</Note>
</LabelWithNote>
) : (
option.label
)}
</MenuItem>
))}
</ContextMenu>
</Wrapper>
);
};
const LabelWithNote = styled.div`
font-weight: 500;
text-align: left;
`;
const Note = styled(HelpText)`
margin-top: 2px;
margin-bottom: 0;
line-height: 1.2em;
font-size: 14px;
font-weight: 400;
color: ${(props) => props.theme.textTertiary};
`;
const StyledButton = styled(Button)`
box-shadow: none;
text-transform: none;
border-color: transparent;
&:hover {
background: transparent;
}
${Inner} {
line-height: 28px;
}
`;
const Wrapper = styled.div`
margin-right: 8px;
`;
export default FilterOptions;
+13 -5
View File
@@ -16,7 +16,7 @@ type AlignValues =
| "flex-start"
| "flex-end";
type Props = {
type Props = {|
column?: ?boolean,
shrink?: ?boolean,
align?: AlignValues,
@@ -24,12 +24,19 @@ type Props = {
auto?: ?boolean,
className?: string,
children?: React.Node,
};
role?: string,
gap?: number,
|};
const Flex = (props: Props) => {
const Flex = React.forwardRef<Props, HTMLDivElement>((props: Props, ref) => {
const { children, ...restProps } = props;
return <Container {...restProps}>{children}</Container>;
};
return (
<Container ref={ref} {...restProps}>
{children}
</Container>
);
});
const Container = styled.div`
display: flex;
@@ -38,6 +45,7 @@ const Container = styled.div`
align-items: ${({ align }) => align};
justify-content: ${({ justify }) => justify};
flex-shrink: ${({ shrink }) => (shrink ? 1 : "initial")};
gap: ${({ gap }) => (gap ? `${gap}px` : "initial")};
min-height: 0;
min-width: 0;
`;
+24
View File
@@ -0,0 +1,24 @@
// @flow
import * as React from "react";
import styled from "styled-components";
import Empty from "components/Empty";
import Fade from "components/Fade";
import Flex from "components/Flex";
export default function FullscreenLoading() {
return (
<Fade timing={500}>
<Centered>
<Empty>Loading</Empty>
</Centered>
</Fade>
);
}
const Centered = styled(Flex)`
text-align: center;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
`;
+17 -1
View File
@@ -1,6 +1,7 @@
// @flow
import { observable } from "mobx";
import { observer, inject } from "mobx-react";
import { GroupIcon } from "outline-icons";
import * as React from "react";
import styled from "styled-components";
import { MAX_AVATAR_DISPLAY } from "shared/constants";
@@ -17,7 +18,8 @@ type Props = {
group: Group,
groupMemberships: GroupMembershipsStore,
membership?: CollectionGroupMembership,
showFacepile: boolean,
showFacepile?: boolean,
showAvatar?: boolean,
renderActions: ({ openMembersModal: () => void }) => React.Node,
};
@@ -48,6 +50,11 @@ class GroupListItem extends React.Component<Props> {
return (
<>
<ListItem
image={
<Image>
<GroupIcon size={24} />
</Image>
}
title={
<Title onClick={this.handleMembersModalOpen}>{group.name}</Title>
}
@@ -84,6 +91,15 @@ class GroupListItem extends React.Component<Props> {
}
}
const Image = styled(Flex)`
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: ${(props) => props.theme.secondaryBackground};
border-radius: 32px;
`;
const Title = styled.span`
&:hover {
text-decoration: underline;
+112
View File
@@ -0,0 +1,112 @@
// @flow
import { observer } from "mobx-react";
import * as React from "react";
import { Dialog, DialogBackdrop, useDialogState } from "reakit/Dialog";
import styled from "styled-components";
import Scrollable from "components/Scrollable";
import usePrevious from "hooks/usePrevious";
type Props = {|
children?: React.Node,
isOpen: boolean,
title?: string,
onRequestClose: () => void,
|};
const Guide = ({
children,
isOpen,
title = "Untitled",
onRequestClose,
...rest
}: Props) => {
const dialog = useDialogState({ animated: 250 });
const wasOpen = usePrevious(isOpen);
React.useEffect(() => {
if (!wasOpen && isOpen) {
dialog.show();
}
if (wasOpen && !isOpen) {
dialog.hide();
}
}, [dialog, wasOpen, isOpen]);
return (
<DialogBackdrop {...dialog}>
{(props) => (
<Backdrop {...props}>
<Dialog
{...dialog}
aria-label={title}
preventBodyScrollhideOnEsc
hide={onRequestClose}
>
{(props) => (
<Scene {...props} {...rest}>
<Content>
{title && <Header>{title}</Header>}
{children}
</Content>
</Scene>
)}
</Dialog>
</Backdrop>
)}
</DialogBackdrop>
);
};
const Header = styled.h1`
font-size: 18px;
margin-top: 0;
margin-bottom: 1em;
`;
const Backdrop = styled.div`
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: ${(props) => props.theme.backdrop} !important;
z-index: ${(props) => props.theme.depths.modalOverlay};
transition: opacity 200ms ease-in-out;
opacity: 0;
&[data-enter] {
opacity: 1;
}
`;
const Scene = styled.div`
position: absolute;
top: 0;
right: 0;
bottom: 0;
margin: 12px;
z-index: ${(props) => props.theme.depths.modal};
display: flex;
justify-content: center;
align-items: flex-start;
width: 350px;
background: ${(props) => props.theme.background};
transition: ${(props) => props.theme.backgroundTransition};
border-radius: 8px;
outline: none;
opacity: 0;
transform: translateX(16px);
transition: transform 250ms ease, opacity 250ms ease;
&[data-enter] {
opacity: 1;
transform: translateX(0px);
}
`;
const Content = styled(Scrollable)`
width: 100%;
padding: 16px;
`;
export default observer(Guide);
+128
View File
@@ -0,0 +1,128 @@
// @flow
import { throttle } from "lodash";
import { observer } from "mobx-react";
import { transparentize } from "polished";
import * as React from "react";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import Fade from "components/Fade";
import Flex from "components/Flex";
type Props = {|
breadcrumb?: React.Node,
title: React.Node,
actions?: React.Node,
|};
function Header({ breadcrumb, title, actions }: Props) {
const [isScrolled, setScrolled] = React.useState(false);
const handleScroll = React.useCallback(
throttle(() => setScrolled(window.scrollY > 75), 50),
[]
);
React.useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, [handleScroll]);
const handleClickTitle = React.useCallback(() => {
window.scrollTo({
top: 0,
behavior: "smooth",
});
}, []);
return (
<Wrapper align="center" isCompact={isScrolled} shrink={false}>
{breadcrumb ? <Breadcrumbs>{breadcrumb}</Breadcrumbs> : null}
{isScrolled ? (
<Title align="center" justify="flex-start" onClick={handleClickTitle}>
<Fade>{title}</Fade>
</Title>
) : (
<div />
)}
{actions && (
<Actions align="center" justify="flex-end">
{actions}
</Actions>
)}
</Wrapper>
);
}
const Breadcrumbs = styled("div")`
flex-grow: 1;
flex-basis: 0;
align-items: center;
padding-right: 8px;
/* Don't show breadcrumbs on mobile */
display: none;
${breakpoint("tablet")`
display: flex;
`};
`;
const Actions = styled(Flex)`
flex-grow: 1;
flex-basis: 0;
min-width: auto;
padding-left: 8px;
${breakpoint("tablet")`
position: unset;
`};
`;
const Wrapper = styled(Flex)`
position: sticky;
top: 0;
z-index: ${(props) => props.theme.depths.header};
background: ${(props) => transparentize(0.2, props.theme.background)};
padding: 12px;
transition: all 100ms ease-out;
transform: translate3d(0, 0, 0);
backdrop-filter: blur(20px);
min-height: 56px;
justify-content: flex-start;
@media print {
display: none;
}
${breakpoint("tablet")`
padding: ${(props) => (props.isCompact ? "12px" : `24px 24px 0`)};
justify-content: "center";
`};
`;
const Title = styled("div")`
display: none;
font-size: 16px;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
cursor: pointer;
min-width: 0;
${breakpoint("tablet")`
padding-left: 0;
display: block;
`};
svg {
vertical-align: bottom;
}
@media (display-mode: standalone) {
overflow: hidden;
flex-grow: 0 !important;
}
`;
export default observer(Header);
+4
View File
@@ -4,10 +4,14 @@ import styled from "styled-components";
const Heading = styled.h1`
display: flex;
align-items: center;
${(props) => (props.centered ? "text-align: center;" : "")}
svg {
margin-top: 4px;
margin-left: -6px;
margin-right: 2px;
align-self: flex-start;
flex-shrink: 0;
}
`;
+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;
`;
+17 -11
View File
@@ -4,11 +4,11 @@ import { transparentize } from "polished";
import * as React from "react";
import { Portal } from "react-portal";
import styled from "styled-components";
import { fadeAndSlideIn } from "shared/styles/animations";
import { parseDocumentSlugFromUrl } from "shared/utils/parseDocumentIds";
import parseDocumentSlug from "shared/utils/parseDocumentSlug";
import DocumentsStore from "stores/DocumentsStore";
import HoverPreviewDocument from "components/HoverPreviewDocument";
import isInternalUrl from "utils/isInternalUrl";
import { fadeAndSlideDown } from "styles/animations";
import { isInternalUrl } from "utils/urls";
const DELAY_OPEN = 300;
const DELAY_CLOSE = 300;
@@ -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,8 +126,17 @@ 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;
animation: ${fadeAndSlideDown} 150ms ease;
@media print {
display: none;
@@ -197,6 +201,7 @@ const Card = styled.div`
const Position = styled.div`
margin-top: 10px;
position: ${({ fixed }) => (fixed ? "fixed" : "absolute")};
z-index: ${(props) => props.theme.depths.hoverPreview};
display: flex;
max-height: 75%;
@@ -211,6 +216,7 @@ const Pointer = styled.div`
height: 22px;
position: absolute;
transform: translateX(-50%);
pointer-events: none;
&:before,
&:after {
+15 -13
View File
@@ -1,21 +1,21 @@
// @flow
import { inject, observer } from "mobx-react";
import { observer } from "mobx-react";
import * as React from "react";
import { Link } from "react-router-dom";
import styled from "styled-components";
import { parseDocumentSlugFromUrl } from "shared/utils/parseDocumentIds";
import DocumentsStore from "stores/DocumentsStore";
import parseDocumentSlug from "shared/utils/parseDocumentSlug";
import DocumentMetaWithViews from "components/DocumentMetaWithViews";
import Editor from "components/Editor";
import useStores from "hooks/useStores";
type Props = {
url: string,
documents: DocumentsStore,
children: (React.Node) => React.Node,
};
function HoverPreviewDocument({ url, documents, children }: Props) {
const slug = parseDocumentSlugFromUrl(url);
function HoverPreviewDocument({ url, children }: Props) {
const { documents } = useStores();
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>
);
}
@@ -48,4 +50,4 @@ const Heading = styled.h2`
color: ${(props) => props.theme.text};
`;
export default inject("documents")(observer(HoverPreviewDocument));
export default observer(HoverPreviewDocument);
+71 -84
View File
@@ -1,6 +1,4 @@
// @flow
import { observable } from "mobx";
import { observer } from "mobx-react";
import {
CollectionIcon,
CoinsIcon,
@@ -22,13 +20,24 @@ import {
VehicleIcon,
} from "outline-icons";
import * as React from "react";
import { TwitterPicker } from "react-color";
import { useTranslation } from "react-i18next";
import { useMenuState, MenuButton, MenuItem } from "reakit/Menu";
import styled from "styled-components";
import { DropdownMenu } from "components/DropdownMenu";
import ContextMenu from "components/ContextMenu";
import Flex from "components/Flex";
import HelpText from "components/HelpText";
import { LabelText } from "components/Input";
import NudeButton from "components/NudeButton";
const style = { width: 30, height: 30 };
const TwitterPicker = React.lazy(() =>
import(
/* webpackChunkName: "twitter-picker" */
"react-color/lib/components/twitter/Twitter"
)
);
export const icons = {
collection: {
component: CollectionIcon,
@@ -117,103 +126,77 @@ const colors = [
"#2F362F",
];
type Props = {
type Props = {|
onOpen?: () => void,
onChange: (color: string, icon: string) => void,
icon: string,
color: string,
};
|};
function preventEventBubble(event) {
event.stopPropagation();
}
function IconPicker({ onOpen, icon, color, onChange }: Props) {
const { t } = useTranslation();
const menu = useMenuState({
modal: true,
placement: "bottom-end",
});
const Component = icons[icon || "collection"].component;
@observer
class IconPicker extends React.Component<Props> {
@observable isOpen: boolean = false;
node: ?HTMLElement;
componentDidMount() {
window.addEventListener("click", this.handleClickOutside);
}
componentWillUnmount() {
window.removeEventListener("click", this.handleClickOutside);
}
handleClose = () => {
this.isOpen = false;
};
handleOpen = () => {
this.isOpen = true;
if (this.props.onOpen) {
this.props.onOpen();
}
};
handleClickOutside = (ev: SyntheticMouseEvent<>) => {
// $FlowFixMe
if (ev.target && this.node && this.node.contains(ev.target)) {
return;
}
this.handleClose();
};
render() {
const Component = icons[this.props.icon || "collection"].component;
return (
<Wrapper ref={(ref) => (this.node = ref)}>
<label>
<LabelText>Icon</LabelText>
</label>
<DropdownMenu
onOpen={this.handleOpen}
label={
<LabelButton>
<Component role="button" color={this.props.color} size={30} />
</LabelButton>
}
>
<Icons onClick={preventEventBubble}>
{Object.keys(icons).map((name) => {
const Component = icons[name].component;
return (
<IconButton
key={name}
onClick={() => this.props.onChange(this.props.color, name)}
style={{ width: 30, height: 30 }}
>
<Component color={this.props.color} size={30} />
</IconButton>
);
})}
</Icons>
<Flex onClick={preventEventBubble}>
return (
<Wrapper>
<Label>
<LabelText>{t("Icon")}</LabelText>
</Label>
<MenuButton {...menu}>
{(props) => (
<Button aria-label={t("Show menu")} {...props}>
<Component color={color} size={30} />
</Button>
)}
</MenuButton>
<ContextMenu {...menu} onOpen={onOpen} aria-label={t("Choose icon")}>
<Icons>
{Object.keys(icons).map((name) => {
const Component = icons[name].component;
return (
<MenuItem
key={name}
onClick={() => onChange(color, name)}
{...menu}
>
{(props) => (
<IconButton style={style} {...props}>
<Component color={color} size={30} />
</IconButton>
)}
</MenuItem>
);
})}
</Icons>
<Flex>
<React.Suspense fallback={<Loading>{t("Loading")}</Loading>}>
<ColorPicker
color={this.props.color}
onChange={(color) =>
this.props.onChange(color.hex, this.props.icon)
}
color={color}
onChange={(color) => onChange(color.hex, icon)}
colors={colors}
triangle="hide"
/>
</Flex>
</DropdownMenu>
</Wrapper>
);
}
</React.Suspense>
</Flex>
</ContextMenu>
</Wrapper>
);
}
const Label = styled.label`
display: block;
`;
const Icons = styled.div`
padding: 15px 9px 9px 15px;
width: 276px;
`;
const LabelButton = styled(NudeButton)`
const Button = styled(NudeButton)`
border: 1px solid ${(props) => props.theme.inputBorder};
width: 32px;
height: 32px;
@@ -226,6 +209,10 @@ const IconButton = styled(NudeButton)`
height: 30px;
`;
const Loading = styled(HelpText)`
padding: 16px;
`;
const ColorPicker = styled(TwitterPicker)`
box-shadow: none !important;
background: transparent !important;
+15
View File
@@ -0,0 +1,15 @@
// @flow
import * as React from "react";
import { cdnPath } from "utils/urls";
type Props = {|
alt: string,
src: string,
title?: string,
width?: number,
height?: number,
|};
export default function Image({ src, alt, ...rest }: Props) {
return <img src={cdnPath(src)} alt={alt} {...rest} />;
}
+31 -9
View File
@@ -2,9 +2,10 @@
import { observable } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import { VisuallyHidden } from "reakit/VisuallyHidden";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import Flex from "components/Flex";
import VisuallyHidden from "components/VisuallyHidden";
const RealTextarea = styled.textarea`
border: 0;
@@ -28,6 +29,10 @@ const RealInput = styled.input`
background: none;
color: ${(props) => props.theme.text};
height: 30px;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&:disabled,
&::placeholder {
@@ -35,12 +40,17 @@ const RealInput = styled.input`
}
&::-webkit-search-cancel-button {
-webkit-appearance: searchfield-cancel-button;
-webkit-appearance: none;
}
${breakpoint("mobile", "tablet")`
font-size: 16px;
`};
`;
const Wrapper = styled.div`
flex: ${(props) => (props.flex ? "1" : "0")};
width: ${(props) => (props.short ? "49%" : "auto")};
max-width: ${(props) => (props.short ? "350px" : "100%")};
min-height: ${({ minHeight }) => (minHeight ? `${minHeight}px` : "0")};
max-height: ${({ maxHeight }) => (maxHeight ? `${maxHeight}px` : "initial")};
@@ -54,7 +64,6 @@ const IconWrapper = styled.span`
`;
export const Outline = styled(Flex)`
display: flex;
flex: 1;
margin: ${(props) =>
props.margin !== undefined ? props.margin : "0 0 16px"};
@@ -63,7 +72,7 @@ export const Outline = styled(Flex)`
border-style: solid;
border-color: ${(props) =>
props.hasError
? "red"
? props.theme.danger
: props.focused
? props.theme.inputBorderFocused
: props.theme.inputBorder};
@@ -79,8 +88,8 @@ export const LabelText = styled.div`
display: inline-block;
`;
export type Props = {
type?: string,
export type Props = {|
type?: "text" | "email" | "checkbox" | "search" | "textarea",
value?: string,
label?: string,
className?: string,
@@ -89,9 +98,22 @@ export type Props = {
short?: boolean,
margin?: string | number,
icon?: React.Node,
onFocus?: (ev: SyntheticEvent<>) => void,
onBlur?: (ev: SyntheticEvent<>) => void,
};
name?: string,
minLength?: number,
maxLength?: number,
autoFocus?: boolean,
autoComplete?: boolean | string,
readOnly?: boolean,
required?: boolean,
disabled?: boolean,
placeholder?: string,
onChange?: (
ev: SyntheticInputEvent<HTMLInputElement | HTMLTextAreaElement>
) => mixed,
onKeyDown?: (ev: SyntheticKeyboardEvent<HTMLInputElement>) => mixed,
onFocus?: (ev: SyntheticEvent<>) => mixed,
onBlur?: (ev: SyntheticEvent<>) => mixed,
|};
@observer
class Input extends React.Component<Props> {
+4 -2
View File
@@ -3,10 +3,12 @@ import styled from "styled-components";
import Input from "./Input";
const InputLarge = styled(Input)`
height: 40px;
height: 38px;
flex-grow: 1;
margin-right: 8px;
input {
height: 40px;
height: 38px;
}
`;
+42 -64
View File
@@ -1,80 +1,58 @@
// @flow
import { observable } from "mobx";
import { observer, inject } from "mobx-react";
import { observer } from "mobx-react";
import * as React from "react";
import { Trans } from "react-i18next";
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";
import useStores from "hooks/useStores";
type Props = {
type Props = {|
label: string,
minHeight?: number,
maxHeight?: number,
readOnly?: boolean,
ui: UiStore,
};
|};
@observer
class InputRich extends React.Component<Props> {
@observable editorComponent: React.ComponentType<any>;
@observable focused: boolean = false;
function InputRich({ label, minHeight, maxHeight, ...rest }: Props) {
const [focused, setFocused] = React.useState<boolean>(false);
const { ui } = useStores();
componentDidMount() {
this.loadEditor();
}
const handleBlur = React.useCallback(() => {
setFocused(false);
}, []);
handleBlur = () => {
this.focused = false;
};
const handleFocus = React.useCallback(() => {
setFocused(true);
}, []);
handleFocus = () => {
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}
return (
<>
<LabelText>{label}</LabelText>
<StyledOutline
maxHeight={maxHeight}
minHeight={minHeight}
focused={focused}
>
<React.Suspense
fallback={
<HelpText>
<Trans>Loading editor</Trans>
</HelpText>
}
>
{Editor ? (
<Editor
onBlur={this.handleBlur}
onFocus={this.handleFocus}
ui={ui}
grow
{...rest}
/>
) : (
"Loading…"
)}
</StyledOutline>
</>
);
}
<Editor
onBlur={handleBlur}
onFocus={handleFocus}
ui={ui}
grow
{...rest}
/>
</React.Suspense>
</StyledOutline>
</>
);
}
const StyledOutline = styled(Outline)`
@@ -89,4 +67,4 @@ const StyledOutline = styled(Outline)`
}
`;
export default inject("ui")(withTheme(InputRich));
export default observer(withTheme(InputRich));
+37 -63
View File
@@ -1,74 +1,48 @@
// @flow
import { observable } from "mobx";
import { observer } from "mobx-react";
import { SearchIcon } from "outline-icons";
import * as React from "react";
import keydown from "react-keydown";
import { withRouter, type RouterHistory } from "react-router-dom";
import styled, { withTheme } from "styled-components";
import Input from "./Input";
import { searchUrl } from "utils/routeHelpers";
import { useTranslation } from "react-i18next";
import { useTheme } from "styled-components";
import Input, { type Props as InputProps } from "./Input";
type Props = {
history: RouterHistory,
theme: Object,
type Props = {|
...InputProps,
placeholder?: string,
collectionId?: string,
};
value?: string,
onChange: (event: SyntheticInputEvent<>) => mixed,
onKeyDown?: (event: SyntheticKeyboardEvent<HTMLInputElement>) => mixed,
|};
@observer
class InputSearch extends React.Component<Props> {
input: ?Input;
@observable focused: boolean = false;
export default function InputSearch(props: Props) {
const { t } = useTranslation();
const theme = useTheme();
const [isFocused, setIsFocused] = React.useState(false);
@keydown("meta+f")
focus(ev) {
ev.preventDefault();
const handleFocus = React.useCallback(() => {
setIsFocused(true);
}, []);
if (this.input) {
this.input.focus();
}
}
const handleBlur = React.useCallback(() => {
setIsFocused(false);
}, []);
handleSearchInput = (ev) => {
ev.preventDefault();
this.props.history.push(
searchUrl(ev.target.value, this.props.collectionId)
);
};
const { placeholder = `${t("Search")}`, onKeyDown, ...rest } = props;
handleFocus = () => {
this.focused = true;
};
handleBlur = () => {
this.focused = false;
};
render() {
const { theme, placeholder = "Search…" } = this.props;
return (
<InputMaxWidth
ref={(ref) => (this.input = ref)}
type="search"
placeholder={placeholder}
onInput={this.handleSearchInput}
icon={
<SearchIcon
color={this.focused ? theme.inputBorderFocused : theme.inputBorder}
/>
}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
margin={0}
/>
);
}
return (
<Input
type="search"
placeholder={placeholder}
icon={
<SearchIcon
color={isFocused ? theme.inputBorderFocused : theme.inputBorder}
/>
}
onKeyDown={onKeyDown}
onFocus={handleFocus}
onBlur={handleBlur}
margin={0}
labelHidden
{...rest}
/>
);
}
const InputMaxWidth = styled(Input)`
max-width: 30vw;
`;
export default withTheme(withRouter(InputSearch));
+95
View File
@@ -0,0 +1,95 @@
// @flow
import { observable } from "mobx";
import { observer } from "mobx-react";
import { SearchIcon } from "outline-icons";
import * as React from "react";
import { withTranslation, type TFunction } from "react-i18next";
import keydown from "react-keydown";
import { withRouter, type RouterHistory } from "react-router-dom";
import styled, { withTheme } from "styled-components";
import Input from "./Input";
import { type Theme } from "types";
import { meta } from "utils/keyboard";
import { searchUrl } from "utils/routeHelpers";
type Props = {
history: RouterHistory,
theme: Theme,
source: string,
placeholder?: string,
label?: string,
labelHidden?: boolean,
collectionId?: string,
value: string,
onChange: (event: SyntheticInputEvent<>) => mixed,
onKeyDown: (event: SyntheticKeyboardEvent<HTMLInputElement>) => mixed,
t: TFunction,
};
@observer
class InputSearchPage extends React.Component<Props> {
input: ?Input;
@observable focused: boolean = false;
@keydown(`${meta}+f`)
focus(ev: SyntheticEvent<>) {
ev.preventDefault();
if (this.input) {
this.input.focus();
}
}
handleSearchInput = (ev: SyntheticInputEvent<>) => {
ev.preventDefault();
this.props.history.push(
searchUrl(ev.target.value, {
collectionId: this.props.collectionId,
ref: this.props.source,
})
);
};
handleFocus = () => {
this.focused = true;
};
handleBlur = () => {
this.focused = false;
};
render() {
const { t, value, onChange, onKeyDown } = this.props;
const { theme, placeholder = `${t("Search")}` } = this.props;
return (
<InputMaxWidth
ref={(ref) => (this.input = ref)}
type="search"
placeholder={placeholder}
onInput={this.handleSearchInput}
value={value}
onChange={onChange}
onKeyDown={onKeyDown}
icon={
<SearchIcon
color={this.focused ? theme.inputBorderFocused : theme.inputBorder}
/>
}
label={this.props.label}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
margin={0}
labelHidden
/>
);
}
}
const InputMaxWidth = styled(Input)`
max-width: 30vw;
`;
export default withTranslation()<InputSearchPage>(
withTheme(withRouter(InputSearchPage))
);
+27 -6
View File
@@ -2,17 +2,23 @@
import { observable } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import { VisuallyHidden } from "reakit/VisuallyHidden";
import styled from "styled-components";
import VisuallyHidden from "components/VisuallyHidden";
import { Outline, LabelText } from "./Input";
const Select = styled.select`
border: 0;
flex: 1;
padding: 8px 12px;
padding: 4px 0;
margin: 0 12px;
outline: none;
background: none;
color: ${(props) => props.theme.text};
height: 30px;
option {
background: ${(props) => props.theme.buttonNeutralBackground};
}
&:disabled,
&::placeholder {
@@ -20,14 +26,22 @@ const Select = styled.select`
}
`;
type Option = { label: string, value: string };
const Wrapper = styled.label`
display: block;
max-width: ${(props) => (props.short ? "350px" : "100%")};
`;
export type Option = { label: string, value: string };
export type Props = {
value?: string,
label?: string,
short?: boolean,
className?: string,
labelHidden?: boolean,
options: Option[],
onBlur?: () => void,
onFocus?: () => void,
};
@observer
@@ -43,12 +57,19 @@ class InputSelect extends React.Component<Props> {
};
render() {
const { label, className, labelHidden, options, ...rest } = this.props;
const {
label,
className,
labelHidden,
options,
short,
...rest
} = this.props;
const wrappedLabel = <LabelText>{label}</LabelText>;
return (
<label>
<Wrapper short={short}>
{label &&
(labelHidden ? (
<VisuallyHidden>{wrappedLabel}</VisuallyHidden>
@@ -64,7 +85,7 @@ class InputSelect extends React.Component<Props> {
))}
</Select>
</Outline>
</label>
</Wrapper>
);
}
}
+22
View File
@@ -0,0 +1,22 @@
// @flow
import * as React from "react";
import { useTranslation } from "react-i18next";
import InputSelect, { type Props, type Option } from "./InputSelect";
export default function InputSelectPermission(
props: $Rest<Props, { options: Array<Option> }>
) {
const { t } = useTranslation();
return (
<InputSelect
label={t("Default access")}
options={[
{ label: t("View and edit"), value: "read_write" },
{ label: t("View only"), value: "read" },
{ label: t("No access"), value: "" },
]}
{...props}
/>
);
}
+5 -7
View File
@@ -4,10 +4,10 @@ import * as React from "react";
import styled from "styled-components";
import Flex from "components/Flex";
type Props = {
type Props = {|
label: React.Node | string,
children: React.Node,
};
|};
const Labeled = ({ label, children, ...props }: Props) => (
<Flex column {...props}>
@@ -17,12 +17,10 @@ const Labeled = ({ label, children, ...props }: Props) => (
);
export const Label = styled(Flex)`
margin-bottom: 8px;
font-size: 13px;
font-weight: 500;
text-transform: uppercase;
color: ${(props) => props.theme.textTertiary};
letter-spacing: 0.04em;
padding-bottom: 4px;
display: inline-block;
color: ${(props) => props.theme.text};
`;
export default observer(Labeled);
+101
View File
@@ -0,0 +1,101 @@
// @flow
import { find } from "lodash";
import * as React from "react";
import { Trans, useTranslation } from "react-i18next";
import styled from "styled-components";
import { languages, languageOptions } from "shared/i18n";
import ButtonLink from "components/ButtonLink";
import Flex from "components/Flex";
import NoticeTip from "components/NoticeTip";
import useCurrentUser from "hooks/useCurrentUser";
import useStores from "hooks/useStores";
import { detectLanguage } from "utils/language";
function Icon(props) {
return (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M21 18H16L14 16V6C14 4.89543 14.8954 4 16 4H28C29.1046 4 30 4.89543 30 6V16C30 17.1046 29.1046 18 28 18H27L25.4142 19.5858C24.6332 20.3668 23.3668 20.3668 22.5858 19.5858L21 18ZM16 15.1716V6H28V16H27H26.1716L25.5858 16.5858L24 18.1716L22.4142 16.5858L21.8284 16H21H16.8284L16 15.1716Z"
fill="#2B2F35"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M16 13H4C2.89543 13 2 13.8954 2 15V25C2 26.1046 2.89543 27 4 27H5L6.58579 28.5858C7.36684 29.3668 8.63316 29.3668 9.41421 28.5858L11 27H16C17.1046 27 18 26.1046 18 25V15C18 13.8954 17.1046 13 16 13ZM9 17L6 16.9681C6 16.9681 5 17.016 5 18C5 18.984 6 19 6 19H8.5H10C10 19 9.57627 20.1885 8.38983 21.0831C7.20339 21.9777 5.7197 23 5.7197 23C5.7197 23 4.99153 23.6054 5.5 24.5C6.00847 25.3946 7 24.8403 7 24.8403L9.74576 22.8722L11.9492 24.6614C11.9492 24.6614 12.6271 25.3771 13.3051 24.4825C13.9831 23.5879 13.3051 23.0512 13.3051 23.0512L11.1017 21.262C11.1017 21.262 11.5 21 12 20L12.5 19H14C14 19 15 19.0319 15 18C15 16.9681 14 16.9681 14 16.9681L11 17V16C11 16 11.0169 15 10 15C8.98305 15 9 16 9 16V17Z"
fill="#2B2F35"
/>
<path
d="M23.6672 12.5221L23.5526 12.1816H23.1934H20.8818H20.5215L20.4075 12.5235L20.082 13.5H19.2196L21.2292 8.10156H21.8774L21.5587 9.06116L20.7633 11.4562L20.5449 12.1138H21.2378H22.8374H23.5327L23.3114 11.4546L22.5072 9.05959L22.1855 8.10156H22.768L24.7887 13.5H23.9964L23.6672 12.5221Z"
fill="#2B2F35"
stroke="#2B2F35"
/>
</svg>
);
}
export default function LanguagePrompt() {
const { auth, ui } = useStores();
const { t } = useTranslation();
const user = useCurrentUser();
const language = detectLanguage();
if (language === "en_US" || language === user.language) {
return null;
}
if (!languages.includes(language)) {
return null;
}
const option = find(languageOptions, (o) => o.value === language);
const optionLabel = option ? option.label : "";
return (
<NoticeTip>
<Flex align="center">
<LanguageIcon />
<span>
<Trans>
Outline is available in your language {{ optionLabel }}, would you
like to change?
</Trans>
<br />
<Link
onClick={() => {
auth.updateUser({
language,
});
ui.setLanguagePromptDismissed();
}}
>
{t("Change Language")}
</Link>{" "}
&middot;{" "}
<Link onClick={ui.setLanguagePromptDismissed}>{t("Dismiss")}</Link>
</span>
</Flex>
</NoticeTip>
);
}
const Link = styled(ButtonLink)`
color: ${(props) => props.theme.almostBlack};
font-weight: 500;
&:hover {
text-decoration: underline;
}
`;
const LanguageIcon = styled(Icon)`
margin-right: 12px;
`;
+95 -37
View File
@@ -1,30 +1,41 @@
// @flow
import { observable } from "mobx";
import { observer, inject } from "mobx-react";
import { MenuIcon } from "outline-icons";
import * as React from "react";
import { Helmet } from "react-helmet";
import { withTranslation, type TFunction } from "react-i18next";
import keydown from "react-keydown";
import { Switch, Route, Redirect } from "react-router-dom";
import styled, { withTheme } from "styled-components";
import {
Switch,
Route,
Redirect,
withRouter,
type RouterHistory,
} from "react-router-dom";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import AuthStore from "stores/AuthStore";
import DocumentsStore from "stores/DocumentsStore";
import PoliciesStore from "stores/PoliciesStore";
import UiStore from "stores/UiStore";
import ErrorSuspended from "scenes/ErrorSuspended";
import KeyboardShortcuts from "scenes/KeyboardShortcuts";
import Analytics from "components/Analytics";
import Button from "components/Button";
import DocumentHistory from "components/DocumentHistory";
import { GlobalStyles } from "components/DropToImport";
import Flex from "components/Flex";
import Guide from "components/Guide";
import { LoadingIndicatorBar } from "components/LoadingIndicator";
import Modal from "components/Modal";
import Sidebar from "components/Sidebar";
import SettingsSidebar from "components/Sidebar/Settings";
import SkipNavContent from "components/SkipNavContent";
import SkipNavLink from "components/SkipNavLink";
import { meta } from "utils/keyboard";
import {
homeUrl,
searchUrl,
matchDocumentSlug as slug,
newDocumentUrl,
} from "utils/routeHelpers";
type Props = {
@@ -34,8 +45,11 @@ type Props = {
title?: ?React.Node,
auth: AuthStore,
ui: UiStore,
history: RouterHistory,
policies: PoliciesStore,
notifications?: React.Node,
theme: Object,
i18n: Object,
t: TFunction,
};
@observer
@@ -44,26 +58,19 @@ class Layout extends React.Component<Props> {
@observable redirectTo: ?string;
@observable keyboardShortcutsOpen: boolean = false;
componentWillMount() {
this.updateBackground();
}
componentDidUpdate() {
this.updateBackground();
if (this.redirectTo) {
this.redirectTo = undefined;
}
}
updateBackground() {
// ensure the wider page color always matches the theme
window.document.body.style.background = this.props.theme.background;
@keydown(`${meta}+.`)
handleToggleSidebar() {
this.props.ui.toggleCollapsedSidebar();
}
@keydown("shift+/")
handleOpenKeyboardShortcuts() {
if (this.props.ui.editMode) return;
this.keyboardShortcutsOpen = true;
}
@@ -71,9 +78,8 @@ class Layout extends React.Component<Props> {
this.keyboardShortcutsOpen = false;
};
@keydown(["t", "/", "meta+k"])
goToSearch(ev) {
if (this.props.ui.editMode) return;
@keydown(["t", "/", `${meta}+k`])
goToSearch(ev: SyntheticEvent<>) {
ev.preventDefault();
ev.stopPropagation();
this.redirectTo = searchUrl();
@@ -81,14 +87,25 @@ class Layout extends React.Component<Props> {
@keydown("d")
goToDashboard() {
if (this.props.ui.editMode) return;
this.redirectTo = homeUrl();
}
@keydown("n")
goToNewDocument() {
const { activeCollectionId } = this.props.ui;
if (!activeCollectionId) return;
const can = this.props.policies.abilities(activeCollectionId);
if (!can.update) return;
this.props.history.push(newDocumentUrl(activeCollectionId));
}
render() {
const { auth, ui } = this.props;
const { auth, t, ui } = this.props;
const { user, team } = auth;
const showSidebar = auth.authenticated && user && team;
const sidebarCollapsed = ui.isEditing || ui.sidebarCollapsed;
if (auth.isSuspended) return <ErrorSuspended />;
if (this.redirectTo) return <Redirect to={this.redirectTo} push />;
@@ -102,11 +119,18 @@ class Layout extends React.Component<Props> {
content="width=device-width, initial-scale=1.0"
/>
</Helmet>
<Analytics />
<SkipNavLink />
{this.props.ui.progressBarVisible && <LoadingIndicatorBar />}
{this.props.notifications}
<MobileMenuButton
onClick={ui.toggleMobileSidebar}
icon={<MenuIcon />}
iconColor="currentColor"
neutral
/>
<Container auto>
{showSidebar && (
<Switch>
@@ -115,7 +139,18 @@ class Layout extends React.Component<Props> {
</Switch>
)}
<Content auto justify="center" editMode={ui.editMode}>
<SkipNavContent />
<Content
auto
justify="center"
$isResizing={ui.sidebarIsResizing}
$sidebarCollapsed={sidebarCollapsed}
style={
sidebarCollapsed
? undefined
: { marginLeft: `${ui.sidebarWidth}px` }
}
>
{this.props.children}
</Content>
@@ -126,14 +161,13 @@ class Layout extends React.Component<Props> {
/>
</Switch>
</Container>
<Modal
<Guide
isOpen={this.keyboardShortcutsOpen}
onRequestClose={this.handleCloseKeyboardShortcuts}
title="Keyboard shortcuts"
title={t("Keyboard shortcuts")}
>
<KeyboardShortcuts />
</Modal>
<GlobalStyles />
</Guide>
</Container>
);
}
@@ -147,17 +181,41 @@ const Container = styled(Flex)`
min-height: 100%;
`;
const Content = styled(Flex)`
margin: 0;
transition: margin-left 100ms ease-out;
@media print {
margin: 0;
}
const MobileMenuButton = styled(Button)`
position: fixed;
top: 12px;
left: 12px;
z-index: ${(props) => props.theme.depths.sidebar - 1};
${breakpoint("tablet")`
margin-left: ${(props) => (props.editMode ? 0 : props.theme.sidebarWidth)};
display: none;
`};
@media print {
display: none;
}
`;
const Content = styled(Flex)`
margin: 0;
transition: ${(props) =>
props.$isResizing ? "none" : `margin-left 100ms ease-out`};
@media print {
margin: 0 !important;
}
${breakpoint("mobile", "tablet")`
margin-left: 0 !important;
`}
${breakpoint("tablet")`
${(props) =>
props.$sidebarCollapsed &&
`margin-left: ${props.theme.sidebarCollapsedWidth}px;`}
`};
`;
export default inject("auth", "ui", "documents")(withTheme(Layout));
export default withTranslation()<Layout>(
inject("auth", "ui", "documents", "policies")(withRouter(Layout))
);
+67 -23
View File
@@ -1,35 +1,67 @@
// @flow
import * as React from "react";
import styled from "styled-components";
import styled, { useTheme } from "styled-components";
import Flex from "components/Flex";
import NavLink from "components/NavLink";
type Props = {
type Props = {|
image?: React.Node,
to?: string,
title: React.Node,
subtitle?: React.Node,
actions?: React.Node,
};
border?: boolean,
small?: boolean,
|};
const ListItem = ({ image, title, subtitle, actions }: Props) => {
const ListItem = (
{ image, title, subtitle, actions, small, border, to, ...rest }: Props,
ref
) => {
const theme = useTheme();
const compact = !subtitle;
return (
<Wrapper compact={compact}>
const content = (selected) => (
<>
{image && <Image>{image}</Image>}
<Content align={compact ? "center" : undefined} column={!compact}>
<Heading>{title}</Heading>
{subtitle && <Subtitle>{subtitle}</Subtitle>}
<Content
align={compact ? "center" : undefined}
column={!compact}
$selected={selected}
>
<Heading $small={small}>{title}</Heading>
{subtitle && (
<Subtitle $small={small} $selected={selected}>
{subtitle}
</Subtitle>
)}
</Content>
{actions && <Actions>{actions}</Actions>}
{actions && <Actions $selected={selected}>{actions}</Actions>}
</>
);
return (
<Wrapper
ref={ref}
$border={border}
activeStyle={{ background: theme.primary }}
{...rest}
as={to ? NavLink : undefined}
to={to}
>
{to ? content : content(false)}
</Wrapper>
);
};
const Wrapper = styled.li`
const Wrapper = styled.div`
display: flex;
padding: ${(props) => (props.compact ? "8px" : "12px")} 0;
margin: 0;
border-bottom: 1px solid ${(props) => props.theme.divider};
user-select: none;
padding: ${(props) => (props.$border === false ? 0 : "8px 0")};
margin: ${(props) => (props.$border === false ? "8px 0" : 0)};
border-bottom: 1px solid
${(props) =>
props.$border === false ? "transparent" : props.theme.divider};
&:last-child {
border-bottom: 0;
@@ -38,32 +70,44 @@ const Wrapper = styled.li`
const Image = styled(Flex)`
padding: 0 8px 0 0;
max-height: 40px;
max-height: 32px;
align-items: center;
user-select: none;
flex-shrink: 0;
align-self: flex-start;
align-self: center;
`;
const Heading = styled.p`
font-size: 16px;
font-size: ${(props) => (props.$small ? 14 : 16)}px;
font-weight: 500;
line-height: 1.2;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
line-height: ${(props) => (props.$small ? 1.3 : 1.2)};
margin: 0;
`;
const Content = styled(Flex)`
const Content = styled.div`
display: flex;
flex-direction: column;
flex-grow: 1;
color: ${(props) => (props.$selected ? props.theme.white : props.theme.text)};
`;
const Subtitle = styled.p`
margin: 0;
font-size: 14px;
color: ${(props) => props.theme.slate};
font-size: ${(props) => (props.$small ? 13 : 14)}px;
color: ${(props) =>
props.$selected ? props.theme.white50 : props.theme.textTertiary};
margin-top: -2px;
`;
const Actions = styled.div`
export const Actions = styled(Flex)`
align-self: center;
justify-content: center;
margin-right: 4px;
color: ${(props) =>
props.$selected ? props.theme.white : props.theme.textSecondary};
`;
export default ListItem;
export default React.forwardRef<Props, HTMLDivElement>(ListItem);
+6 -5
View File
@@ -4,18 +4,19 @@ import * as React from "react";
import styled from "styled-components";
import Fade from "components/Fade";
import Flex from "components/Flex";
import Mask from "components/Mask";
import PlaceholderText from "components/PlaceholderText";
type Props = {
count?: number,
};
const Placeholder = ({ count }: Props) => {
const ListPlaceHolder = ({ count }: Props) => {
return (
<Fade>
{times(count || 2, (index) => (
<Item key={index} column auto>
<Mask />
<PlaceholderText header delay={0.2 * index} />
<PlaceholderText delay={0.2 * index} />
</Item>
))}
</Fade>
@@ -23,7 +24,7 @@ const Placeholder = ({ count }: Props) => {
};
const Item = styled(Flex)`
padding: 15px 0 16px;
padding: 10px 0;
`;
export default Placeholder;
export default ListPlaceHolder;
@@ -11,16 +11,14 @@ const LoadingIndicatorBar = () => {
};
const loadingFrame = keyframes`
from {margin-left: -100%; z-index:100;}
to {margin-left: 100%; }
from { margin-left: -100%; }
to { margin-left: 100%; }
`;
const Container = styled.div`
position: fixed;
top: 0;
z-index: 9999;
background-color: #03a9f4;
z-index: ${(props) => props.theme.depths.loadingIndicatorBar};
width: 100%;
animation: ${loadingFrame} 4s ease-in-out infinite;
animation-delay: 250ms;
@@ -30,7 +28,7 @@ const Container = styled.div`
const Loader = styled.div`
width: 100%;
height: 2px;
background-color: #03a9f4;
background-color: ${(props) => props.theme.primary};
`;
export default LoadingIndicatorBar;

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