Compare commits

...

328 Commits

Author SHA1 Message Date
Tom Moor 8e9beac59f test 2023-08-08 23:12:41 -04:00
Tom Moor a0f7c76405 Add support for SSL in development 2023-08-08 22:46:31 -04:00
dependabot[bot] 454a4e9a8d chore(deps): bump y-indexeddb from 9.0.9 to 9.0.11 (#5656)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tom Moor <tom.moor@gmail.com>
2023-08-07 15:08:18 -07:00
dependabot[bot] ef9c410d97 chore(deps-dev): bump terser from 5.18.2 to 5.19.2 (#5658)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-07 14:56:21 -07:00
dependabot[bot] 7c2f779f68 chore(deps): bump fs-extra from 11.1.0 to 11.1.1 (#5657)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-07 14:56:04 -07:00
Tom Moor c45de6904b chore: Upgrade dd-trace 2023-08-07 17:54:54 -04:00
Tom Moor 4758778fc7 test 2023-08-06 13:10:50 -04:00
Apoorv Mishra 401ae73a04 Request validation for /api/collections.* (#5619) 2023-08-06 09:54:13 -07:00
Apoorv Mishra 0ddbd9c608 Calculate HoverPreview position inside useLayoutEffect (#5636) 2023-08-06 09:00:05 -07:00
Tom Moor 6c4e2a9d11 perf: Narrow scopes of Slack hook queries 2023-08-06 11:54:48 -04:00
Tom Moor d8f1f55a80 fix: type is optional input for integrations.list endpoint 2023-08-06 11:09:22 -04:00
Tom Moor 9b811c999d fix: Cannot exit code block with mod-enter shortcut with edit mode enabled 2023-08-05 19:45:54 -04:00
Tom Moor 5a60329021 fix: Unable to access document without reload after 24h+ session 2023-08-05 08:24:37 -04:00
Tom Moor 042ea7b61f Misc fixes from qa pass (#5650) 2023-08-04 20:40:36 -07:00
Tom Moor 80acc16791 fix: Badge's do not correctly use accent text color 2023-08-04 08:46:05 -04:00
Tom Moor 3c25b2b047 Merge branch 'main' of github.com:outline/outline 2023-08-04 08:45:15 -04:00
Adrien Ballet 16f1328a83 Added syntax highlighting for the Verilog and VHDL languages (#5641) 2023-08-03 20:26:41 -07:00
Tom Moor d1a7a30c00 fix: Closing find and replace on long document jumps to end 2023-08-03 22:41:49 -04:00
Tom Moor 5b67273d8f fix: Account for older desktop versions 2023-08-03 21:10:36 -04:00
Tom Moor fdd8ecc79d Add find and replace hooks for desktop app 2023-08-03 20:46:03 -04:00
Tom Moor 7c15d03b50 fix: Crash with some find characters
fix: Warning on close of find dialog
2023-08-03 19:32:09 -04:00
Tom Moor b691311f88 feat: Add find and replace interface (#5642) 2023-08-03 15:47:44 -07:00
Tom Moor eda023c908 Restore code blocks in notices 2023-08-01 21:42:19 -04:00
Apoorv Mishra 2331bbbd36 Request validation for /api/integrations.* (#5638) 2023-08-01 18:17:01 -07:00
Tom Moor 228d1faa9f feat: Add Czech translations, remove Russian translations 2023-08-01 19:43:33 -04:00
Tom Moor ff6d30581a New Crowdin updates (#5637) 2023-08-01 16:29:14 -07:00
Apoorv Mishra 027545a768 Close hover preview upon scroll (#5629) 2023-07-31 15:08:14 -07:00
Tom Moor 91585ee09d feat: Add tracking pixel to notifications for mark-as-read functionality (#5626) 2023-07-31 15:01:50 -07:00
Tom Moor a13f2c7311 New Crowdin updates (#5593) 2023-07-31 15:01:40 -07:00
dependabot[bot] d4a51b420f chore(deps-dev): bump vite-plugin-static-copy from 0.13.0 to 0.17.0 (#5631)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-31 15:01:20 -07:00
dependabot[bot] faa02623b3 chore(deps-dev): bump concurrently from 7.4.0 to 7.6.0 (#5632)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-31 14:59:31 -07:00
dependabot[bot] 2baf4d7d8b chore(deps): bump patch-package from 7.0.0 to 7.0.2 (#5630)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-31 14:59:23 -07:00
dependabot[bot] 2b21ac1b97 chore(deps-dev): bump @types/markdown-it-container from 2.0.5 to 2.0.6 (#5634)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-31 14:58:59 -07:00
Tom Moor d2fcd1dee6 fix: Skip unsupported node types when uploading
closes #5544
2023-07-30 15:52:08 -04:00
Tom Moor 3b43460a0a fix: Restrict content in notices, closes #5624 2023-07-29 23:42:58 -04:00
Tom Moor 1864ed605f fix: Allow copy code block to clipboard in read-only
closes #5614
2023-07-29 23:24:50 -04:00
Tom Moor 20932a08d0 fix: Selection jumps when dragging and selection ends outside editor bounds 2023-07-29 23:04:21 -04:00
Tom Moor 7e1ea69939 fix: Transparent thumbnails show document behind in hover previews 2023-07-29 22:51:14 -04:00
Tom Moor a3983c36c9 fix: Do not use CDN image component for hover card previews 2023-07-29 22:33:23 -04:00
Tom Moor ccdcda372f chore: Move last usage of sequelize.transaction to middleware 2023-07-29 22:30:26 -04:00
Tom Moor 07ad5032b4 Protect usage of navigator 2023-07-29 21:56:31 -04:00
Tom Moor 286aea2701 fix: Improve empty state for math blocks 2023-07-29 21:22:52 -04:00
Tom Moor 30e63e022c fix: Improve empty state for mermaid diagrams 2023-07-29 21:12:55 -04:00
Tom Moor b88670b58d fix: Improve emoji trigger for french language
closes #5611
2023-07-29 20:58:33 -04:00
Apoorv Mishra ddc883bfcd Preview arbitrary urls within a document (#5598) 2023-07-29 16:51:49 -07:00
Tom Moor 67691477a9 fix: Hover card timer should reset on url change 2023-07-29 19:51:22 -04:00
Apoorv Mishra e3807a1c75 fix: tests 2023-07-26 21:40:34 +05:30
Apoorv Mishra f95ce018e1 perf: cache response 2023-07-26 18:26:39 +05:30
Apoorv Mishra 2201fd7bd6 fix: description chopping and some cleanup 2023-07-26 13:08:43 +05:30
Apoorv Mishra fbb793ab8e fix: styles 2023-07-25 23:16:53 +05:30
Apoorv Mishra 31f8a3fb44 fix: hover card styling 2023-07-25 19:58:35 +05:30
Apoorv Mishra 03ebca2f0c fix: no overloading 2023-07-25 19:35:31 +05:30
Apoorv Mishra 2a17e0cbf6 fix: send user context for authorize calls 2023-07-25 19:35:31 +05:30
Apoorv Mishra 9ac1e13227 fix: just return 204 2023-07-25 19:35:31 +05:30
Apoorv Mishra bd0240b7a5 fix: handle errors from Iframely 2023-07-25 19:35:31 +05:30
Apoorv Mishra 81bd68380e feat: preview arbitrary url 2023-07-25 19:35:31 +05:30
Apoorv Mishra b3d8bd1cc8 cleanup: separate info and description 2023-07-25 19:35:31 +05:30
Apoorv Mishra a30487c2d7 fix: presentUnfurl 2023-07-25 19:35:31 +05:30
Apoorv Mishra 8b3c58a162 fix: coalesce to empty str 2023-07-25 19:35:30 +05:30
Apoorv Mishra 43a91626b2 feat: pipe external urls through iframely 2023-07-25 19:35:30 +05:30
Tom Moor 15c8a4867f fix: Text caret not placed in new math block after creation
fix: Excessive padding on inline math node
2023-07-25 00:04:14 -04:00
Tom Moor d94caf2783 fix: Missing translation for Slack hook 2023-07-24 23:41:34 -04:00
Tom Moor aaeb6f7dc6 fix: Flash of incorrect cursor when hover preview opens 2023-07-24 23:39:27 -04:00
Tom Moor e0289aed40 chore: Enable recommended React eslint rules (#5600) 2023-07-24 18:23:54 -07:00
dependabot[bot] 8865d394c6 chore(deps): bump @joplin/turndown-plugin-gfm from 1.0.47 to 1.0.49 (#5602)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-24 18:23:30 -07:00
dependabot[bot] 1d893a06f9 chore(deps): bump i18next from 22.5.0 to 22.5.1 (#5604)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-24 17:54:23 -07:00
dependabot[bot] 0c291ee806 chore(deps): bump styled-components and @types/styled-components (#5601)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-24 17:53:55 -07:00
dependabot[bot] 8732155dbb chore(deps-dev): bump @typescript-eslint/parser from 5.60.1 to 5.62.0 (#5605)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-24 17:51:13 -07:00
dependabot[bot] 56e01b784d chore(deps): bump sequelize from 6.29.0 to 6.32.1 (#5603)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-24 17:50:52 -07:00
Tom Moor e47d493d13 fix: Remove absolute positiioning of insights management, closes #5599 2023-07-24 08:28:35 -04:00
Tom Moor 2677c964a5 chore: Improve constraints on file_operations table 2023-07-23 19:51:42 -04:00
Tom Moor f8927ff819 tsc 2023-07-23 17:50:33 -04:00
j0ok34n 72adcd10ef Comment fix
- Workspace administrators will not be able to delete or edit comments within private collections for which they do not have permissions.
- Users will not be able to delete or modify their comments if they have been removed from a private collection.
2023-07-23 15:57:20 -04:00
Tom Moor 7bc37cb700 tsc 2023-07-23 13:11:02 -04:00
Tom Moor 217e53d8b6 fix: Enable toggling of insights while document is draft 2023-07-23 13:06:34 -04:00
Tom Moor 404f5ff871 Merge branch 'main' of github.com:outline/outline 2023-07-23 12:01:54 -04:00
Apoorv Mishra 0db6f39f43 Display correct info in hover preview (#5597) 2023-07-23 09:01:46 -07:00
Tom Moor 479b805613 Add per-document control over who can see viewer insights (#5594) 2023-07-23 09:01:36 -07:00
Tom Moor 48f1047016 chore: improve collections router 2023-07-22 16:39:47 -04:00
Tom Moor caf7333682 fix: Pass user context to document loader in urls unfurl 2023-07-22 16:07:21 -04:00
Tom Moor cd59af4a9b Allow service worker to serve cached unfurl responses 2023-07-22 13:32:01 -04:00
Tom Moor 8d549abaa9 Add rate limiting to unfurl endpoint 2023-07-22 13:27:58 -04:00
Tom Moor 5e705f3dc7 fix: Tweaks to hover card behavior 2023-07-22 12:47:01 -04:00
Apoorv Mishra 5d71398ea6 Preview mentions (#5571)
Co-authored-by: Tom Moor <tom.moor@gmail.com>
2023-07-22 09:13:09 -07:00
Tom Moor dbd85d62cb fix: Duplicate mentions results in duplicate notifications (#5585) 2023-07-21 05:49:14 -07:00
Tom Moor d180ecbe96 fix: Cropping of text on document lists on non-Mac platforms 2023-07-20 22:14:39 -04:00
Tom Moor 9046abb682 Hide 'Self hosted' settings on cloud 2023-07-20 22:01:48 -04:00
Tom Moor 5810ddb589 New Crowdin updates (#5525) 2023-07-20 18:41:11 -07:00
dependabot[bot] 0d30220017 chore(deps): bump @sentry/node from 7.51.2 to 7.59.2 (#5580)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-19 16:29:49 -07:00
Tom Moor b60e46a961 Restore empty table selection, closes #5581 2023-07-19 17:46:14 -04:00
Tom Moor 3c6e2aaac6 fix: Opening contextual menus sometimes change scroll position 2023-07-18 21:31:43 -04:00
Tom Moor eae6204d55 fix: Code toolbar in read-only 2023-07-18 19:39:23 -04:00
Tom Moor 1e78079ade Add SCSS and Sass code highlighting 2023-07-18 19:20:40 -04:00
dependabot[bot] d3fc6fc0fd chore(deps): bump winston from 3.8.2 to 3.10.0 (#5573)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-18 15:39:03 -07:00
dependabot[bot] 0f10fe4052 chore(deps): bump word-wrap from 1.2.3 to 1.2.4 (#5579)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-18 15:38:46 -07:00
Tom Moor d928d456de fix: Use correct error type when token is missing 2023-07-17 22:38:44 -04:00
Tom Moor 5206beaf19 fix: 'Plain text' language toolbar showing on code block in position 0 2023-07-17 22:33:47 -04:00
Tom Moor 70113be9af chore: Bump kbar 2023-07-17 22:18:08 -04:00
dependabot[bot] 04ea3431e7 chore(deps-dev): bump jest-cli from 29.5.0 to 29.6.1 (#5574)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-17 18:37:03 -07:00
dependabot[bot] d3ce70016e chore(deps-dev): bump eslint from 8.44.0 to 8.45.0 (#5572)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-17 18:25:51 -07:00
dependabot[bot] 46d6664307 chore(deps): bump validator and @types/validator (#5575)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-17 18:25:36 -07:00
Tom Moor 2427f4747a Rebuilding code block menus (#5569) 2023-07-17 18:25:22 -07:00
Tom Moor 60b456f35a closes #5570 2023-07-17 20:10:27 -04:00
Tom Moor 64b2718673 fix: Race condition on login 2023-07-17 19:06:31 -04:00
Tom Moor 4b14fa5dd7 Inherit 'full width' setting creating new child document
towards #5562
2023-07-15 23:21:59 -04:00
Tom Moor abb38ea447 fix: Server error with invalid Prosemirror JSON should be validation error 2023-07-15 23:04:30 -04:00
Tom Moor e81f97b2de Allow override of theme on shared documents 2023-07-15 21:36:04 -04:00
Tom Moor e653b185a4 fix: Regression loading shares in #5552
fix: Double auth.info load with multiple tabs open
fix: Request loop when suspended with multiple tabs open
2023-07-15 21:10:22 -04:00
Tom Moor 39e12cef65 chore: Use httpOnly authentication cookie (#5552) 2023-07-15 13:56:32 -07:00
Tom Moor b1230d0c81 fix: Improve code highlighting in dark mode
closes #5021
2023-07-15 16:54:55 -04:00
dependabot[bot] 6e9e1c15a5 chore(deps-dev): bump babel-jest from 29.5.0 to 29.6.1 (#5550)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-15 11:54:46 -07:00
Tom Moor 66331d3d4f Add csp nonce to all inline script tags (#5566) 2023-07-15 07:15:14 -07:00
Tom Moor ea07b72c7a fix: Show max 3 lines of content on notification items 2023-07-14 21:51:15 -04:00
Tom Moor 5dcd7a74ca fix: Remove no longer required unescaping, closes #5555 2023-07-14 21:46:31 -04:00
Tom Moor 5c83070941 fix: Pasting rich text into image caption inherits styling 2023-07-11 21:28:38 -04:00
Tom Moor a9ab196a18 fix: Guard against empty attachment size
I don't see how this can happen based on default props, but it does
2023-07-11 20:40:48 -04:00
Tom Moor b9fc301589 0.70.2 2023-07-11 19:00:36 -04:00
Tom Moor c56add74c6 fix: Azure single-tenant SSO tokens are unable to refresh (#5551) 2023-07-11 15:59:28 -07:00
dependabot[bot] 5ae4834333 chore(deps): bump pg-tsquery from 8.4.0 to 8.4.1 (#5548)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-10 20:00:15 -07:00
dependabot[bot] 437865e7aa chore(deps-dev): bump terser from 5.16.6 to 5.18.2 (#5549)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-10 19:59:31 -07:00
dependabot[bot] 042f2ff737 chore(deps-dev): bump @typescript-eslint/eslint-plugin from 5.60.1 to 5.61.0 (#5546)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-10 19:59:00 -07:00
Tom Moor 5656384cc4 fix: Error logging second parameter used as interpolation parameters 2023-07-08 15:35:06 -04:00
Tom Moor 098d91808b fix: Selection passed to setSelection must point at the current document, triple clicking caption 2023-07-08 15:02:44 -04:00
Tom Moor 21d446881e perf: Preconnect to CDN 2023-07-08 14:19:51 -04:00
Tom Moor cf32d227e6 fix: Error loading image: [object Event] 2023-07-08 13:57:25 -04:00
Tom Moor e59e121179 fix: Do not log errors for failed webhooks in hosted environment 2023-07-08 13:33:16 -04:00
Tom Moor 98a182c892 Improve reliability of embed regex (missing start char) 2023-07-08 12:04:03 -04:00
Tom Moor 6bc1b789ee fix: State of user preferences UI does not reflect defaults 2023-07-08 11:02:58 -04:00
Tom Moor a8674c7dda fix: Adding guard against double reload
closes #5384
2023-07-08 10:29:42 -04:00
Tom Moor c952dfa065 fix: Cannot unpin archived documents 2023-07-08 10:20:39 -04:00
Tom Moor 9a95fa47a0 fix: Error details are not output in development 2023-07-08 10:20:39 -04:00
dependabot[bot] 5bfb2c89c8 chore(deps): bump smooth-scroll-into-view-if-needed from 1.1.32 to 1.1.33 (#5517)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-08 06:49:11 -07:00
dependabot[bot] 9b6a645928 chore(deps): bump tough-cookie from 4.1.2 to 4.1.3 (#5543)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-08 05:49:23 -07:00
dependabot[bot] d550fb79d3 chore(deps): bump protobufjs from 7.1.2 to 7.2.4 (#5542)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-08 05:44:51 -07:00
Tom Moor ed9cf4cee3 fix: No visible error message when maximm pinned documents is reached 2023-07-07 08:34:50 -04:00
Tom Moor 8cc2853102 fix: Email for document update can include empty diff block 2023-07-07 08:23:42 -04:00
Tom Moor 814bacbead chore: Update node-fetch 2023-07-07 08:05:44 -04:00
Tom Moor 9431df45c2 0.70.1 2023-07-06 22:00:39 -04:00
Tom Moor a75d6b298e fix: Sanitize url missing in editor embeds and widgets 2023-07-06 21:38:02 -04:00
Tom Moor ff1bc5db2a fix: HTML export 2023-07-05 08:47:41 -04:00
Tom Moor e6e9512979 fix: Keyboard handlers should not be considered while composing 2023-07-05 08:47:41 -04:00
Tom Moor 29db1ef1bf Update Crowdin configuration file 2023-07-04 12:15:05 -04:00
Tom Moor b54a370e01 fix: Enter/Esc keys in content editable should not be considered while composing
closes #5523
2023-07-04 08:31:22 -04:00
Tom Moor cce22bcdee fix: Embed with underscore in url fails when hydrating from Markdown 2023-07-04 08:26:47 -04:00
Tom Moor da62c2c044 fix: Extra content in copying notice, closes #5522 2023-07-04 07:07:23 -04:00
dependabot[bot] 201690e342 chore(deps): bump @babel/preset-react from 7.18.6 to 7.22.5 (#5516)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-03 17:43:43 -07:00
dependabot[bot] 4fddc0fd87 chore(deps-dev): bump lint-staged from 13.1.0 to 13.2.3 (#5515)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-03 17:42:22 -07:00
Tom Moor 72c7b0373b fix: Small rendering flash in sidebar when document slug changes 2023-07-03 20:42:10 -04:00
dependabot[bot] 7c5d834f39 chore(deps): bump pg from 8.8.0 to 8.11.1 (#5518)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-03 16:42:28 -07:00
dependabot[bot] f1f3159b12 chore(deps-dev): bump yarn-deduplicate from 6.0.1 to 6.0.2 (#5519)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-03 16:42:12 -07:00
Tom Moor a62739be8f chore: Add allowUrls Sentry configuration to reduce spurious extension errors 2023-07-03 19:24:22 -04:00
Tom Moor 3124423eeb fix: Vertical movement in sidebar when entering title edit 2023-07-03 19:24:22 -04:00
Tom Moor 40dbbe10c5 fix: Small rendering flash in sidebar when document slug changes 2023-07-03 19:24:22 -04:00
Tom Moor a4ba1f18bc fix: Do not send document text when modifying title 2023-07-03 19:24:22 -04:00
Tom Moor 0aabcb8d22 Remove unused isOnlyTitle 2023-07-03 19:24:22 -04:00
Apoorv Mishra 3c2e7b5b63 Request validation for /api/subscriptions.* (#5476)
* chore: req validation for subscriptions.list

* chore: req validation for subscriptions.info

* chore: req validation for subscriptions.create

* chore: req validation for subscriptions.delete

* fix: reuse validations
2023-07-03 08:43:45 +05:30
Tom Moor 9d4a1965b0 0.70.0 2023-07-02 10:24:01 -04:00
Tom Moor ea527bf147 fix 2023-07-01 22:57:59 -04:00
Tom Moor 02744411f3 fix: Do not allow copy/pasting comment marks between documents (#5507) 2023-07-01 12:45:22 -07:00
Tom Moor f843a20a54 chore: Improves linting rule to catch mishandled promises (#5506) 2023-07-01 10:25:51 -07:00
Apoorv Mishra 7aec0e24ef fix: validation err (#5505) 2023-07-01 20:30:34 +05:30
Tom Moor 843092e5f0 test 2023-07-01 10:14:55 -04:00
Tom Moor 1fd7f75929 tsc 2023-07-01 10:11:38 -04:00
Apoorv Mishra 768fcbf6c4 Request validation for /api/stars.* (#5475)
* chore: req validation for stars.create

* chore: req validation for stars.list

* chore: req validation for stars.update

* chore: req validation for stars.delete

* fix: DRY

* fix: group validation attributes and message
2023-07-01 19:25:57 +05:30
Tom Moor f214db0ab7 fix: Scroll to document header on page load 2023-07-01 09:17:37 -04:00
Tom Moor aec190245b fix: No scroll to anchor if already exists 2023-06-30 19:07:36 -04:00
Tom Moor 88775cd287 fix: Database transaction used after committed 2023-06-30 17:15:40 -04:00
Tom Moor 64b2bfe704 fix: Unneccessary animation of sidebar on app load 2023-06-29 22:51:30 -04:00
Tom Moor 8957e86c12 fix: Add server-side validation of comment length 2023-06-29 22:41:57 -04:00
Tom Moor 4bec08ee05 tsc 2023-06-29 21:31:26 -04:00
Tom Moor a3d70622af Improve notifications empty state 2023-06-29 21:10:33 -04:00
Tom Moor 92c8eff61d fix: No need to validate SSO access immediately after sign-in 2023-06-29 21:08:49 -04:00
Tom Moor 73c2a67fa5 fix: Allow any SSO validation rather than _all_ 2023-06-29 21:08:49 -04:00
Tom Moor c2af5db0f8 fix: console error while building plugins without server folder 2023-06-29 21:08:49 -04:00
dependabot[bot] 1e4b59ac6c chore(deps): bump babel-plugin-styled-components from 2.0.7 to 2.1.4 (#5487)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-29 07:05:15 -07:00
dependabot[bot] 9933f30c25 chore(deps): bump react-table and @types/react-table (#5484)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-29 07:04:58 -07:00
Tom Moor bbee4b5791 Add control to enable debug logging in production 2023-06-28 20:26:15 -04:00
Tom Moor 89d5527d39 Handle promise linting (#5488) 2023-06-28 17:18:18 -07:00
Tom Moor f3d8129a13 fix: Await logout 2023-06-27 22:20:41 -04:00
Tom Moor 87a675d02b fix: Add /logout route to those that cannot be redirected after login 2023-06-27 19:51:37 -04:00
Tom Moor a2fae1f1eb fix: Keyboard navigation around inline code marks (#5477) 2023-06-26 15:28:42 -07:00
dependabot[bot] e001b8d161 chore(deps): bump sequelize-cli from 6.6.0 to 6.6.1 (#5483)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-26 10:50:29 -07:00
dependabot[bot] 5000500f96 chore(deps-dev): bump jest-environment-jsdom from 29.4.1 to 29.5.0 (#5485)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-26 10:20:33 -07:00
dependabot[bot] 8a33bdcb8a chore(deps): bump react-portal from 4.2.1 to 4.2.2 (#5486)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-26 10:20:23 -07:00
Apoorv Mishra 9987c15daf chore: req validation for api/developer (#5482) 2023-06-26 19:20:22 +05:30
Tom Moor 06f2d7a993 Reduce view writing 2023-06-26 09:23:47 -04:00
Apoorv Mishra a234a92f80 chore: schema for api/auth (#5481) 2023-06-26 18:37:18 +05:30
Apoorv Mishra 7314a71bf1 Resolve OIDC_DISPLAY_NAME mismatch (#5479)
* fix: oidc name mismatch

* trigger ci
2023-06-26 18:06:56 +05:30
Tom Moor edb9099c9d Cleanup intervals on destroyed views extension 2023-06-25 22:52:28 -04:00
Tom Moor 94882d4d3a Add connection rate limiting to collaboration server 2023-06-25 22:46:43 -04:00
Tom Moor 453bbb3b25 Bump hocuspocus 2023-06-25 18:59:01 -04:00
Tom Moor e58163ef5f Add more logging detail when _health endpoint fails 2023-06-25 16:13:58 -04:00
Tom Moor 7940cef517 Improve document revision creation (#5474) 2023-06-25 05:29:24 -07:00
Apoorv Mishra 86d6117a31 Request validation for /api/shares.* (#5467)
* chore: req validation for shares.info

* chore: req validation for shares.list

* chore: req validation for shares.update

* chore: req validation for shares.create

* chore: req validation for shares.revoke

* fix: review
2023-06-25 17:50:23 +05:30
Tom Moor 4e69ae1ffe fix: Descenders chopped on document list items on Windows 2023-06-24 17:04:40 -04:00
Tom Moor 06033ac781 Add HCL (terraform) to code languages 2023-06-24 16:48:45 -04:00
Tom Moor 25e8c32b84 fix: allow-storage-access-by-user-activation on embeds
closes #5471
2023-06-24 13:56:37 -04:00
Tom Moor 08601a9f84 fix: Error when importing collections with longer descriptions than 1000 chars
related #5472
2023-06-24 13:48:17 -04:00
Tom Moor 831318d941 fix: Invalid LOG_LEVEL in environment results in server crash with no displayed error message
Related: https://github.com/outline/outline/discussions/5466
2023-06-22 09:17:25 -04:00
Apoorv Mishra d96bf5106d chore: request validation for pins (#5465) 2023-06-22 15:57:00 +05:30
Tobi Kremer a094087342 Remove temporary files after processing (#5456)
Co-authored-by: Tom Moor <tom.moor@gmail.com>
2023-06-21 03:47:34 -07:00
Apoorv Mishra 8d69de1be0 chore: request validation for searches (#5460) 2023-06-21 10:38:38 +05:30
Tom Moor 6d556c7a55 Include collectionId in revisions.create webhook payload 2023-06-20 23:12:00 -04:00
Tom Moor eb62b961a4 feat: Add option to not include attachments in exported data (#5463) 2023-06-20 18:17:39 -07:00
Tom Moor 0e5a576439 fix: Clear document policies when public sharing option is updated
closes #5461
2023-06-20 21:17:13 -04:00
Tom Moor e7861d0bb9 fix: New checkbox items should not be checked by default
closes #3663
2023-06-19 20:50:58 -04:00
Tom Moor 25ae923130 fix: Cannot drag attachment without selecting first
closes #5040
2023-06-19 20:20:32 -04:00
Tom Moor d7ae829d92 Add sidebar toggle to command menu 2023-06-19 18:42:29 -04:00
Tom Moor cef048572a fix: Unable to click block action button on full width editor
closes #5444
2023-06-19 17:27:34 -04:00
dependabot[bot] 51ed458ab2 chore(deps): bump compressorjs from 1.1.1 to 1.2.1 (#5455)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-19 11:35:11 -07:00
dependabot[bot] 38e8a649ef chore(deps): bump @babel/core from 7.21.0 to 7.22.5 (#5453)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-19 10:24:07 -07:00
dependabot[bot] 127728db29 chore(deps): bump prosemirror-transform from 1.7.2 to 1.7.3 (#5454)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-19 10:23:55 -07:00
dependabot[bot] 8b87ab0fd7 chore(deps): bump @babel/plugin-transform-destructuring from 7.20.7 to 7.22.5 (#5451)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-19 10:13:33 -07:00
dependabot[bot] ba65c52e97 chore(deps): bump semver and @types/semver (#5452)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-19 10:08:21 -07:00
Tom Moor f76f2e61d8 Increase maximum index size, closes #5426 2023-06-19 12:54:04 -04:00
Tom Moor b3c08fdb48 fix: Add guard and log against corrupt zip 2023-06-18 07:43:14 -04:00
Tom Moor 9cfe5da70b fix: Error processing task in DetachDraftsFromCollectionTask 2023-06-18 07:38:57 -04:00
Tom Moor 5fa8ebc587 fix: Unneccessary retry of ValidateSSO task 2023-06-18 07:36:45 -04:00
Tom Moor 9ef375d83c fix: Import max length not correctly communicated on import (#5434) 2023-06-17 00:52:57 -07:00
Tom Moor 9d04d5ebd9 fix: Cursor jump on long title edit on Firefox (#5449) 2023-06-17 00:52:41 -07:00
Christian Rendl 4494a30441 Init app language with DEFAULT_LANGUAGE (#5445) 2023-06-17 00:52:12 -07:00
Tom Moor 7bce4c807d fix #5441 2023-06-14 09:24:20 +03:00
dependabot[bot] 80a9bae761 chore(deps): bump dottie from 2.0.2 to 2.0.4 (#5440)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-12 12:23:13 -07:00
dependabot[bot] f40e1f09fd chore(deps): bump react-merge-refs from 2.0.1 to 2.0.2 (#5436)
Bumps [react-merge-refs](https://github.com/gregberge/react-merge-refs) from 2.0.1 to 2.0.2.
- [Release notes](https://github.com/gregberge/react-merge-refs/releases)
- [Changelog](https://github.com/gregberge/react-merge-refs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/gregberge/react-merge-refs/compare/v2.0.1...v2.0.2)

---
updated-dependencies:
- dependency-name: react-merge-refs
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-12 20:56:32 +03:00
dependabot[bot] 2c3e842b3c chore(deps): bump prosemirror-model from 1.19.1 to 1.19.2 (#5438)
Bumps [prosemirror-model](https://github.com/prosemirror/prosemirror-model) from 1.19.1 to 1.19.2.
- [Changelog](https://github.com/ProseMirror/prosemirror-model/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prosemirror/prosemirror-model/compare/1.19.1...1.19.2)

---
updated-dependencies:
- dependency-name: prosemirror-model
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-12 20:55:52 +03:00
Tom Moor f1d1035550 Increase padding on lists 2023-06-11 18:57:55 +03:00
Tom Moor c23e415237 Update README.md 2023-06-11 09:18:26 +03:00
Tom Moor 137c33c323 fix: Grammar in error message 2023-06-10 22:14:59 +03:00
Tom Moor e00c30bd1f fix: Empty state text align 2023-06-10 22:11:30 +03:00
Tom Moor 1ddb9aba3b feat: Add Canva embed support 2023-06-10 17:03:12 +03:00
Tom Moor 746c27e718 fix: Another fix for heading scrolling 2023-06-10 16:00:51 +03:00
Tom Moor d319bb7d9a Allows commenting outside edit mode when seamless editing is disabled. (#5422) 2023-06-10 05:56:00 -07:00
Tom Moor 3f7e66980b fix: Remove requirement to have SLACK_CLIENT_ID to use SLACK_VERIFICATION_TOKEN 2023-06-10 15:31:27 +03:00
Tom Moor d5c1336580 fix: Error pasting code block into comment, closes #5418 2023-06-10 15:29:39 +03:00
Tom Moor a810d9176f Remove empty comment and mark on cancel 2023-06-07 23:34:50 +03:00
Tom Moor ffbf138748 closes #5403 2023-06-07 22:40:01 +03:00
dependabot[bot] b9844472b9 chore(deps): bump bull from 4.10.2 to 4.10.4 (#5414)
Bumps [bull](https://github.com/OptimalBits/bull) from 4.10.2 to 4.10.4.
- [Release notes](https://github.com/OptimalBits/bull/releases)
- [Changelog](https://github.com/OptimalBits/bull/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/OptimalBits/bull/compare/v4.10.2...v4.10.4)

---
updated-dependencies:
- dependency-name: bull
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-06 07:52:21 +03:00
dependabot[bot] 4363d4e1e5 chore(deps): bump query-string from 7.1.1 to 7.1.3 (#5415)
Bumps [query-string](https://github.com/sindresorhus/query-string) from 7.1.1 to 7.1.3.
- [Release notes](https://github.com/sindresorhus/query-string/releases)
- [Commits](https://github.com/sindresorhus/query-string/compare/v7.1.1...v7.1.3)

---
updated-dependencies:
- dependency-name: query-string
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-06 07:51:46 +03:00
dependabot[bot] 46c4185078 chore(deps-dev): bump @types/jsonwebtoken from 8.5.8 to 8.5.9 (#5416)
Bumps [@types/jsonwebtoken](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jsonwebtoken) from 8.5.8 to 8.5.9.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jsonwebtoken)

---
updated-dependencies:
- dependency-name: "@types/jsonwebtoken"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-06 07:51:26 +03:00
dependabot[bot] 424dea8197 chore(deps-dev): bump @types/tmp from 0.2.2 to 0.2.3 (#5417)
Bumps [@types/tmp](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/tmp) from 0.2.2 to 0.2.3.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/tmp)

---
updated-dependencies:
- dependency-name: "@types/tmp"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-06 07:51:06 +03:00
dependabot[bot] 0d0b53f292 chore(deps): bump vite from 4.1.3 to 4.1.5 (#5421)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.1.3 to 4.1.5.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v4.1.5/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.1.5/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-06 07:50:47 +03:00
Tom Moor d18994b14e Improve reliability by retrying failed imports (#5408) 2023-06-03 11:17:36 -07:00
Tom Moor a8a506af3e Merge branch 'tom/fix-html-paste' 2023-06-02 09:03:45 +01:00
Tom Moor ba3a47d138 Cleanup observer 2023-06-01 11:03:45 +01:00
Tom Moor c70bef0fdd fix: Pasting of HTML elements in new PM 2023-06-01 10:41:37 +01:00
Tom Moor 5f4e942d31 stash 2023-06-01 10:25:44 +01:00
Tom Moor 874bdb1e11 New Crowdin updates (#5331) 2023-05-30 17:12:54 -07:00
Apoorv Mishra 7f8a177b01 Use umzug to autorun migrations (#5281) 2023-05-30 17:12:38 -07:00
Tom Moor 5e76d72ae6 fix: Document search command menu action went missing, closes #5400 2023-05-30 19:49:05 -04:00
Tom Moor 45641103ba Allow viewing diff before revision is written (#5399) 2023-05-29 19:49:13 -07:00
dependabot[bot] 555691c79b chore(deps): bump @bull-board/koa from 4.12.1 to 4.12.2 (#5398)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-29 19:02:02 -07:00
dependabot[bot] 20ea422f77 chore(deps-dev): bump babel-plugin-tsconfig-paths-module-resolver from 1.0.3 to 1.0.4 (#5396)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-29 10:21:39 -07:00
dependabot[bot] 045dbb932e chore(deps): bump i18next from 22.4.15 to 22.5.0 (#5394)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-29 10:19:47 -07:00
dependabot[bot] 582c4ba99d chore(deps): bump core-js from 3.30.0 to 3.30.2 (#5395)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-29 10:18:42 -07:00
dependabot[bot] d519e93300 chore(deps-dev): bump @babel/cli from 7.21.0 to 7.21.5 (#5397)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-29 10:18:27 -07:00
Tom Moor 5dab811c5f fix: Improved guard on floating toolbar position calculation 2023-05-27 09:59:55 -04:00
Tom Moor ef7ae719a6 fix: Incorrect method binding on link serialization 2023-05-27 09:56:36 -04:00
Tom Moor aa80f5667f fix: Suggestion menus should close after typing space beyond trigger
closes #5387
2023-05-25 22:21:26 -04:00
Tom Moor d57ec39113 fix: Attachments in imported JSON reference files on previous instance
closes #5361
2023-05-25 22:09:08 -04:00
Tom Moor be3bcebf6b fix: Remove empty top-level list items in imported HTML content 2023-05-25 21:34:26 -04:00
Tom Moor e9ec31e5b8 fix: Spotify embed shows white background in dark mode 2023-05-25 19:58:01 -04:00
Tom Moor 33b0354cfe fix: Incorrect method binding on link serialization 2023-05-25 18:02:04 -04:00
Tom Moor d5341a486c chore: Upgrade all of prosemirror (#5366)
Co-authored-by: Apoorv Mishra <apoorvmishra101092@gmail.com>
2023-05-24 19:24:05 -07:00
dependabot[bot] e340e568e2 chore(deps): bump socket.io-parser from 4.2.1 to 4.2.3 (#5382)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-23 18:16:22 -07:00
Tom Moor e8b9a1b650 fix: AwarenessChangeEvent type 2023-05-22 23:10:05 -04:00
Tom Moor bb0cd891bd fix: Remove old policies from frontend when collection sharing permission changes 2023-05-22 22:30:53 -04:00
dependabot[bot] fbd16d4b9a chore(deps-dev): bump prettier from 2.1.2 to 2.8.8 (#5372)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tom Moor <tom.moor@gmail.com>
2023-05-22 19:14:56 -07:00
Tom Moor 3317bf2396 fix: User presence is not updated when leaving a document 2023-05-22 21:05:40 -04:00
dependabot[bot] 4e75b4029a chore(deps): bump react-virtualized-auto-sizer from 1.0.5 to 1.0.17 (#5373)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-22 14:56:48 -07:00
dependabot[bot] 73fe70817f chore(deps-dev): bump @types/throng from 5.0.3 to 5.0.4 (#5374)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-22 14:56:38 -07:00
Tom Moor 3c5dc10446 fix: Notification from unknown actor causes app crash 2023-05-22 09:31:18 -04:00
Tom Moor d587360f4b fix: Show desktop sign-in errors within the app 2023-05-21 11:54:38 -04:00
Tom Moor 458f24185a fix: Flashing sidebar on load (suspense boundary) 2023-05-20 21:04:33 -04:00
Tom Moor 502d8b9e8d fix: Tweak back button on desktop app 2023-05-20 18:51:28 -04:00
Tom Moor 7e6831c803 Add notification count to dock icon on desktop 2023-05-20 18:01:55 -04:00
Tom Moor 965d85fb6f fix: Delayed mount around all sidebar loading indicators 2023-05-20 17:20:47 -04:00
Tom Moor 31b9c2b8a4 Improve data prefetching, less false positives 2023-05-20 17:11:40 -04:00
Tom Moor dbad4a9b84 fix: Missing association 2023-05-20 11:47:32 -04:00
Tom Moor f3caaed7fe fix: Scroll notifications to top on open 2023-05-20 11:43:25 -04:00
Tom Moor ac6047bbb7 fix: Mobile hover states make notifications unscrollable 2023-05-20 11:32:05 -04:00
Tom Moor ea885133ac Notifications interface (#5354)
Co-authored-by: Apoorv Mishra <apoorvmishra101092@gmail.com>
2023-05-20 07:47:32 -07:00
Tom Moor b1e2ff0713 fix: Various command bar fixes 2023-05-18 18:36:12 -04:00
DandrewsDev dca64fe84b Update providerId to fallback to id in the absence of a sub field. (#5343) 2023-05-18 06:09:08 -07:00
Tom Moor 1e50facd5d chore: collection actions 2023-05-17 23:20:05 -04:00
Tom Moor ce87624289 fix: n.languages is undefined (type seems incorrect here?) 2023-05-17 22:50:47 -04:00
Tom Moor a19c19985e fix: Potential missmatch between selected language preference and browser language preference 2023-05-17 20:23:39 -04:00
Tom Moor 9b257e9593 Animate appearance of pinned documents 2023-05-17 20:13:09 -04:00
Tom Moor aff9413b0f chore: Bump socket.io 2023-05-15 23:38:55 -04:00
Tom Moor de46178871 chore: Bump patch-package 2023-05-15 23:07:00 -04:00
Tom Moor fd700e6fd6 Install curl and update-ca-certificates in container
closes #5277
2023-05-15 23:04:38 -04:00
dependabot[bot] b2ab3b010d chore(deps): bump dd-trace from 3.14.1 to 3.21.0 (#5348)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-15 15:13:27 -07:00
dependabot[bot] ff72d381fb chore(deps): bump fetch-retry from 5.0.3 to 5.0.5 (#5349)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-15 15:13:02 -07:00
dependabot[bot] a24f09afe3 chore(deps-dev): bump @types/formidable from 2.0.5 to 2.0.6 (#5351)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-15 15:12:42 -07:00
dependabot[bot] a6844c7c8a chore(deps): bump vm2 from 3.9.17 to 3.9.18 (#5353)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-15 15:12:33 -07:00
dependabot[bot] fadf51907d chore(deps): bump tiny-cookie from 2.4.0 to 2.4.1 (#5350)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-15 15:12:19 -07:00
Tom Moor 42dea7859c chore: Dependency bumps (#5342) 2023-05-13 12:12:02 -07:00
dependabot[bot] 164263f13a chore(deps): bump yjs from 13.5.39 to 13.6.1 (#5320)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-13 11:24:08 -07:00
Mario Noll 35dbeae80b Add OCI image source label (#5338) 2023-05-13 11:23:39 -07:00
Tom Moor e2bc2f2067 Transfer changes from enterprise codebase 2023-05-13 12:30:24 -04:00
Tom Moor 7ce97f4d50 feat: Enable admins to list all collections in workspace 2023-05-11 22:25:12 -04:00
Tom Moor 0ce9931910 fix: Line numbers overlay code when block is horizontally scrollable 2023-05-11 21:47:26 -04:00
Tom Moor 693dfecab7 fix: Reorder code block language choice 2023-05-11 21:35:51 -04:00
Tom Moor 86a7d1c548 New Crowdin updates (#5321) 2023-05-10 17:55:06 -07:00
Conner 44ed374636 feat: add syntax highlighting for jsx and tsx (#5330) 2023-05-10 17:37:31 -07:00
dependabot[bot] 8c5c445f0d chore(deps): bump emoji-regex from 10.0.0 to 10.2.1 (#5317)Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [emoji-regex](https://github.com/mathiasbynens/emoji-regex) from 10.0.0 to 10.2.1.
- [Commits](https://github.com/mathiasbynens/emoji-regex/compare/v10.0.0...v10.2.1)

---
updated-dependencies:
- dependency-name: emoji-regex
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-09 06:25:32 -07:00
dependabot[bot] 2a0780cfff chore(deps-dev): bump i18next-parser from 7.7.0 to 7.9.0 (#5319)Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [i18next-parser](https://github.com/i18next/i18next-parser) from 7.7.0 to 7.9.0.
- [Release notes](https://github.com/i18next/i18next-parser/releases)
- [Changelog](https://github.com/i18next/i18next-parser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next-parser/compare/7.7.0...7.9.0)

---
updated-dependencies:
- dependency-name: i18next-parser
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-09 06:25:16 -07:00
Tom Moor 8d157ff962 fix: Re-focus comment input after comment 2023-05-08 22:52:34 -04:00
Tom Moor 26a8c5e4ab fix: Revert #5278 2023-05-08 22:37:24 -04:00
Tom Moor d9d15bffa5 Merge branch 'feat/ga4-support' 2023-05-08 22:16:48 -04:00
Tom Moor bb2ee9dd40 single 2023-05-08 22:16:21 -04:00
Tom Moor 90b13d5f27 Move 'public branding' option to Settings -> Details 2023-05-08 21:01:08 -04:00
Tom Moor 679a86fe6f Merge branch 'feat/custom-branding' 2023-05-08 19:04:07 -04:00
Tom Moor ab30bfcf24 fix: Cannot sign-in with Gmail on desktop app 2023-05-08 19:03:56 -04:00
Tom Moor 9dfdafa116 fix: Increase collection pagination limit to max, closes #5311 2023-05-08 17:00:13 -04:00
Tom Moor 06be19090c feat: Add support for parsing Confluence notices 2023-05-08 16:09:49 -04:00
Tom Moor 07ae67924f Use team name and favicon (when public branding enabled) on shared links 2023-05-08 14:46:25 -04:00
Tom Moor 1cf597aca7 feat: Add support for GA4 measurement IDs in GOOGLE_ANALYTICS_ID 2023-05-08 12:01:35 -04:00
Tom Moor a0df79ea5a feat: Allow embeds to be used inside tables (#5315) 2023-05-07 18:05:54 -07:00
Newton 738fa55e12 fix: use real boolean instead of aws's bool (#5313) 2023-05-07 14:13:16 -07:00
Apoorv Mishra c8ee501377 Request validation for cron (#5307)
* chore: add validations for /api/cron.*

* fix: coerce limit to number

* fix: review
2023-05-07 10:41:20 +05:30
Rohan Sharma 3421f24896 fix: package.json & yarn.lock to reduce vulnerabilities (#5288)
Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2023-05-06 15:27:05 -07:00
dependabot[bot] 33e67a11ed chore(deps): bump socket.io-client from 4.5.4 to 4.6.1 (#5176)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-04 20:59:07 -07:00
Tom Moor cfe7bebd95 New Crowdin updates (#5243) 2023-05-04 20:06:14 -07:00
Tom Moor ac8946f0c5 fix: Admins should be able to add and remove themselves from collections 2023-05-04 21:52:59 -04:00
Tom Moor 0504e91aa6 fix: Restore edit permission for workspace admins in non-private collections, closes #5300 2023-05-04 21:04:43 -04:00
Tom Moor aebd626954 fix: Error receiving document update for non-preloaded collection 2023-05-04 20:30:10 -04:00
Tom Moor 9942bbee3e fix: Refactor attachment downloads during export to use promises (#5294
* Refactor attachment downloads during export to use promises instead of streams
Date attachments in zip file correctly

* tsc
2023-05-04 17:20:33 -07:00
Chris Aumann d096ba486f Remove "millisecond" suffix from getSignedUrl() function (#5302) 2023-05-04 17:20:02 -07:00
594 changed files with 18794 additions and 11001 deletions
+6 -2
View File
@@ -30,7 +30,7 @@ REDIS_URL=redis://localhost:6379
# 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
URL=https://app.outline.dev:3000
PORT=3000
# See [documentation](docs/SERVICES.md) on running a separate collaboration
@@ -99,7 +99,7 @@ OIDC_USERINFO_URI=
OIDC_USERNAME_CLAIM=preferred_username
# Display name for OIDC authentication
OIDC_DISPLAY_NAME=OpenID
OIDC_DISPLAY_NAME=OpenID Connect
# Space separated auth scopes.
OIDC_SCOPES=openid profile email
@@ -181,3 +181,7 @@ RATE_LIMITER_ENABLED=true
# Configure default throttling parameters for rate limiter
RATE_LIMITER_REQUESTS=1000
RATE_LIMITER_DURATION_WINDOW=60
# Iframely API config
IFRAMELY_URL=
IFRAMELY_API_KEY=
+9
View File
@@ -3,6 +3,7 @@
"parserOptions": {
"sourceType": "module",
"extraFileExtensions": [".json"],
"project": "./tsconfig.json",
"ecmaFeatures": {
"jsx": true
}
@@ -36,6 +37,14 @@
"component": true,
"html": true
}],
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/await-thenable": "error",
"@typescript-eslint/no-misused-promises": [
"error",
{
"checksVoidReturn": false
}
],
"@typescript-eslint/no-unused-vars": [
"error",
{
+4
View File
@@ -7,6 +7,10 @@ WORKDIR $APP_PATH
# ---
FROM node:18-alpine AS runner
RUN apk update && apk add --no-cache curl && apk add --no-cache ca-certificates
LABEL org.opencontainers.image.source="https://github.com/outline/outline"
ARG APP_PATH
WORKDIR $APP_PATH
ENV NODE_ENV production
-2
View File
@@ -1,8 +1,6 @@
ARG APP_PATH=/opt/outline
FROM node:18-alpine AS deps
RUN apk --no-cache add curl
ARG APP_PATH
WORKDIR $APP_PATH
COPY ./package.json ./yarn.lock ./
+1
View File
@@ -1,5 +1,6 @@
up:
docker-compose up -d redis postgres s3
yarn install-local-ssl
yarn install --pure-lockfile
yarn dev:watch
+1 -1
View File
@@ -7,7 +7,7 @@
<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://circleci.com/gh/outline/outline" rel="nofollow"><img src="https://circleci.com/gh/outline/outline.svg?style=shield"></a>
<a href="http://www.typescriptlang.org" rel="nofollow"><img src="https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg" alt="TypeScript"></a>
<a href="https://github.com/prettier/prettier"><img src="https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat" alt="Prettier"></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" alt="Styled Components"></a>
+3 -9
View File
@@ -3,13 +3,7 @@
"description": "Open source wiki and knowledge base for growing teams",
"website": "https://www.getoutline.com/",
"repository": "https://github.com/outline/outline",
"keywords": [
"wiki",
"team",
"node",
"markdown",
"slack"
],
"keywords": ["wiki", "team", "node", "markdown", "slack"],
"success_url": "/",
"formation": {
"web": {
@@ -94,7 +88,7 @@
},
"OIDC_DISPLAY_NAME": {
"description": "Display name for OIDC authentication",
"value": "OpenID",
"value": "OpenID Connect",
"required": false
},
"OIDC_SCOPES": {
@@ -188,7 +182,7 @@
"required": false
},
"GOOGLE_ANALYTICS_ID": {
"description": "UA-xxxx (optional)",
"description": "G-xxxx (optional)",
"required": false
},
"SENTRY_DSN": {
+1
View File
@@ -1,6 +1,7 @@
{
"extends": [
"../.eslintrc",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
],
"plugins": [
+41 -4
View File
@@ -4,6 +4,7 @@ import {
PadlockIcon,
PlusIcon,
StarredIcon,
TrashIcon,
UnstarredIcon,
} from "outline-icons";
import * as React from "react";
@@ -12,6 +13,7 @@ import Collection from "~/models/Collection";
import CollectionEdit from "~/scenes/CollectionEdit";
import CollectionNew from "~/scenes/CollectionNew";
import CollectionPermissions from "~/scenes/CollectionPermissions";
import CollectionDeleteDialog from "~/components/CollectionDeleteDialog";
import DynamicCollectionIcon from "~/components/Icons/CollectionIcon";
import { createAction } from "~/actions";
import { CollectionSection } from "~/actions/sections";
@@ -122,13 +124,13 @@ export const starCollection = createAction({
stores.policies.abilities(activeCollectionId).star
);
},
perform: ({ activeCollectionId, stores }) => {
perform: async ({ activeCollectionId, stores }) => {
if (!activeCollectionId) {
return;
}
const collection = stores.collections.get(activeCollectionId);
collection?.star();
await collection?.star();
},
});
@@ -148,13 +150,47 @@ export const unstarCollection = createAction({
stores.policies.abilities(activeCollectionId).unstar
);
},
perform: ({ activeCollectionId, stores }) => {
perform: async ({ activeCollectionId, stores }) => {
if (!activeCollectionId) {
return;
}
const collection = stores.collections.get(activeCollectionId);
collection?.unstar();
await collection?.unstar();
},
});
export const deleteCollection = createAction({
name: ({ t }) => t("Delete"),
analyticsName: "Delete collection",
section: CollectionSection,
icon: <TrashIcon />,
visible: ({ activeCollectionId, stores }) => {
if (!activeCollectionId) {
return false;
}
return stores.policies.abilities(activeCollectionId).delete;
},
perform: ({ activeCollectionId, stores, t }) => {
if (!activeCollectionId) {
return;
}
const collection = stores.collections.get(activeCollectionId);
if (!collection) {
return;
}
stores.dialogs.openModal({
isCentered: true,
title: t("Delete collection"),
content: (
<CollectionDeleteDialog
collection={collection}
onSubmit={stores.dialogs.closeAllModals}
/>
),
});
},
});
@@ -163,4 +199,5 @@ export const rootCollectionActions = [
createCollection,
starCollection,
unstarCollection,
deleteCollection,
];
+17 -5
View File
@@ -5,6 +5,7 @@ import { createAction } from "~/actions";
import { DeveloperSection } from "~/actions/sections";
import env from "~/env";
import { client } from "~/utils/ApiClient";
import Logger from "~/utils/Logger";
import { deleteAllDatabases } from "~/utils/developer";
export const clearIndexedDB = createAction({
@@ -35,16 +36,27 @@ export const createTestUsers = createAction({
},
});
export const toggleDebugLogging = createAction({
name: ({ t }) => t("Toggle debug logging"),
icon: <ToolsIcon />,
section: DeveloperSection,
perform: async ({ t }) => {
Logger.debugLoggingEnabled = !Logger.debugLoggingEnabled;
stores.toasts.showToast(
Logger.debugLoggingEnabled
? t("Debug logging enabled")
: t("Debug logging disabled")
);
},
});
export const developer = createAction({
name: ({ t }) => t("Developer"),
name: ({ t }) => t("Development"),
keywords: "debug",
icon: <ToolsIcon />,
iconInContextMenu: false,
section: DeveloperSection,
visible: ({ event }) =>
env.ENVIRONMENT === "development" ||
(event instanceof KeyboardEvent && event.altKey),
children: [clearIndexedDB, createTestUsers],
children: [clearIndexedDB, toggleDebugLogging, createTestUsers],
});
export const rootDeveloperActions = [developer];
+40 -25
View File
@@ -61,8 +61,11 @@ export const openDocument = createAction({
// cache if the document is renamed
id: path.url,
name: path.title,
icon: () =>
stores.documents.get(path.id)?.isStarred ? <StarredIcon /> : null,
icon: function _Icon() {
return stores.documents.get(path.id)?.isStarred ? (
<StarredIcon />
) : null;
},
section: DocumentSection,
perform: () => history.push(path.url),
}));
@@ -98,13 +101,13 @@ export const starDocument = createAction({
!document?.isStarred && stores.policies.abilities(activeDocumentId).star
);
},
perform: ({ activeDocumentId, stores }) => {
perform: async ({ activeDocumentId, stores }) => {
if (!activeDocumentId) {
return;
}
const document = stores.documents.get(activeDocumentId);
document?.star();
await document?.star();
},
});
@@ -124,13 +127,13 @@ export const unstarDocument = createAction({
stores.policies.abilities(activeDocumentId).unstar
);
},
perform: ({ activeDocumentId, stores }) => {
perform: async ({ activeDocumentId, stores }) => {
if (!activeDocumentId) {
return;
}
const document = stores.documents.get(activeDocumentId);
document?.unstar();
await document?.unstar();
},
});
@@ -159,7 +162,7 @@ export const publishDocument = createAction({
}
if (document?.collectionId) {
await document.save({
await document.save(undefined, {
publish: true,
});
stores.toasts.showToast(t("Document published"), {
@@ -186,14 +189,14 @@ export const unpublishDocument = createAction({
}
return stores.policies.abilities(activeDocumentId).unpublish;
},
perform: ({ activeDocumentId, stores, t }) => {
perform: async ({ activeDocumentId, stores, t }) => {
if (!activeDocumentId) {
return;
}
const document = stores.documents.get(activeDocumentId);
document?.unpublish();
await document?.unpublish();
stores.toasts.showToast(t("Document unpublished"), {
type: "success",
@@ -218,14 +221,14 @@ export const subscribeDocument = createAction({
stores.policies.abilities(activeDocumentId).subscribe
);
},
perform: ({ activeDocumentId, stores, t }) => {
perform: async ({ activeDocumentId, stores, t }) => {
if (!activeDocumentId) {
return;
}
const document = stores.documents.get(activeDocumentId);
document?.subscribe();
await document?.subscribe();
stores.toasts.showToast(t("Subscribed to document notifications"), {
type: "success",
@@ -250,14 +253,14 @@ export const unsubscribeDocument = createAction({
stores.policies.abilities(activeDocumentId).unsubscribe
);
},
perform: ({ activeDocumentId, stores, currentUserId, t }) => {
perform: async ({ activeDocumentId, stores, currentUserId, t }) => {
if (!activeDocumentId || !currentUserId) {
return;
}
const document = stores.documents.get(activeDocumentId);
document?.unsubscribe(currentUserId);
await document?.unsubscribe(currentUserId);
stores.toasts.showToast(t("Unsubscribed from document notifications"), {
type: "success",
@@ -274,13 +277,13 @@ export const downloadDocumentAsHTML = createAction({
iconInContextMenu: false,
visible: ({ activeDocumentId, stores }) =>
!!activeDocumentId && stores.policies.abilities(activeDocumentId).download,
perform: ({ activeDocumentId, stores }) => {
perform: async ({ activeDocumentId, stores }) => {
if (!activeDocumentId) {
return;
}
const document = stores.documents.get(activeDocumentId);
document?.download(ExportContentType.Html);
await document?.download(ExportContentType.Html);
},
});
@@ -321,13 +324,13 @@ export const downloadDocumentAsMarkdown = createAction({
iconInContextMenu: false,
visible: ({ activeDocumentId, stores }) =>
!!activeDocumentId && stores.policies.abilities(activeDocumentId).download,
perform: ({ activeDocumentId, stores }) => {
perform: async ({ activeDocumentId, stores }) => {
if (!activeDocumentId) {
return;
}
const document = stores.documents.get(activeDocumentId);
document?.download(ExportContentType.Markdown);
await document?.download(ExportContentType.Markdown);
},
});
@@ -404,13 +407,19 @@ export const pinDocumentToCollection = createAction({
return;
}
const document = stores.documents.get(activeDocumentId);
await document?.pin(document.collectionId);
try {
const document = stores.documents.get(activeDocumentId);
await document?.pin(document.collectionId);
const collection = stores.collections.get(activeCollectionId);
const collection = stores.collections.get(activeCollectionId);
if (!collection || !location.pathname.startsWith(collection?.url)) {
stores.toasts.showToast(t("Pinned to collection"));
if (!collection || !location.pathname.startsWith(collection?.url)) {
stores.toasts.showToast(t("Pinned to collection"));
}
} catch (err) {
stores.toasts.showToast(err.message, {
type: "error",
});
}
},
});
@@ -443,10 +452,16 @@ export const pinDocumentToHome = createAction({
}
const document = stores.documents.get(activeDocumentId);
await document?.pin();
try {
await document?.pin();
if (location.pathname !== homePath()) {
stores.toasts.showToast(t("Pinned to team home"));
if (location.pathname !== homePath()) {
stores.toasts.showToast(t("Pinned to team home"));
}
} catch (err) {
stores.toasts.showToast(err.message, {
type: "error",
});
}
},
});
+22 -6
View File
@@ -30,15 +30,13 @@ import { isMac } from "~/utils/browser";
import history from "~/utils/history";
import isCloudHosted from "~/utils/isCloudHosted";
import {
organizationSettingsPath,
profileSettingsPath,
accountPreferencesPath,
homePath,
searchPath,
draftsPath,
templatesPath,
archivePath,
trashPath,
settingsPath,
} from "~/utils/routeHelpers";
export const navigateToHome = createAction({
@@ -105,7 +103,7 @@ export const navigateToSettings = createAction({
icon: <SettingsIcon />,
visible: ({ stores }) =>
stores.policies.abilities(stores.auth.team?.id || "").update,
perform: () => history.push(organizationSettingsPath()),
perform: () => history.push(settingsPath("details")),
});
export const navigateToProfileSettings = createAction({
@@ -114,7 +112,16 @@ export const navigateToProfileSettings = createAction({
section: NavigationSection,
iconInContextMenu: false,
icon: <ProfileIcon />,
perform: () => history.push(profileSettingsPath()),
perform: () => history.push(settingsPath()),
});
export const navigateToNotificationSettings = createAction({
name: ({ t }) => t("Notifications"),
analyticsName: "Navigate to notification settings",
section: NavigationSection,
iconInContextMenu: false,
icon: <EmailIcon />,
perform: () => history.push(settingsPath("notifications")),
});
export const navigateToAccountPreferences = createAction({
@@ -123,7 +130,7 @@ export const navigateToAccountPreferences = createAction({
section: NavigationSection,
iconInContextMenu: false,
icon: <SettingsIcon />,
perform: () => history.push(accountPreferencesPath()),
perform: () => history.push(settingsPath("preferences")),
});
export const openAPIDocumentation = createAction({
@@ -135,6 +142,14 @@ export const openAPIDocumentation = createAction({
perform: () => window.open(developersUrl()),
});
export const toggleSidebar = createAction({
name: ({ t }) => t("Toggle sidebar"),
analyticsName: "Toggle sidebar",
keywords: "hide show navigation",
section: NavigationSection,
perform: ({ stores }) => stores.ui.toggleCollapsedSidebar(),
});
export const openFeedbackUrl = createAction({
name: ({ t }) => t("Send us feedback"),
analyticsName: "Open feedback",
@@ -210,5 +225,6 @@ export const rootNavigationActions = [
openBugReportUrl,
openChangelog,
openKeyboardShortcuts,
toggleSidebar,
logout,
];
+16
View File
@@ -0,0 +1,16 @@
import { MarkAsReadIcon } from "outline-icons";
import * as React from "react";
import { createAction } from "..";
import { NotificationSection } from "../sections";
export const markNotificationsAsRead = createAction({
name: ({ t }) => t("Mark notifications as read"),
analyticsName: "Mark notifications as read",
section: NotificationSection,
icon: <MarkAsReadIcon />,
shortcut: ["Shift+Escape"],
perform: ({ stores }) => stores.notifications.markAllAsRead(),
visible: ({ stores }) => stores.notifications.approximateUnreadCount > 0,
});
export const rootNotificationActions = [markNotificationsAsRead];
+3 -2
View File
@@ -43,8 +43,9 @@ export const changeTheme = createAction({
isContextMenu ? t("Appearance") : t("Change theme"),
analyticsName: "Change theme",
placeholder: ({ t }) => t("Change theme to"),
icon: () =>
stores.ui.resolvedTheme === "light" ? <SunIcon /> : <MoonIcon />,
icon: function _Icon() {
return stores.ui.resolvedTheme === "light" ? <SunIcon /> : <MoonIcon />;
},
keywords: "appearance display",
section: SettingsSection,
children: [changeToLightTheme, changeToDarkTheme, changeToSystemTheme],
+14 -12
View File
@@ -16,18 +16,20 @@ export const createTeamsList = ({ stores }: { stores: RootStore }) =>
analyticsName: "Switch workspace",
section: TeamSection,
keywords: "change switch workspace organization team",
icon: () => (
<StyledTeamLogo
alt={session.name}
model={{
initial: session.name[0],
avatarUrl: session.avatarUrl,
id: session.id,
color: stringToColor(session.id),
}}
size={24}
/>
),
icon: function _Icon() {
return (
<StyledTeamLogo
alt={session.name}
model={{
initial: session.name[0],
avatarUrl: session.avatarUrl,
id: session.id,
color: stringToColor(session.id),
}}
size={24}
/>
);
},
visible: ({ currentTeamId }: ActionContext) => currentTeamId !== session.id,
perform: () => (window.location.href = session.url),
})) ?? [];
+1 -1
View File
@@ -35,7 +35,7 @@ export function createAction(definition: Optional<Action, "id">): Action {
return definition.perform?.(context);
}
: undefined,
id: uuidv4(),
id: definition.id ?? uuidv4(),
};
}
+2
View File
@@ -2,6 +2,7 @@ import { rootCollectionActions } from "./definitions/collections";
import { rootDeveloperActions } from "./definitions/developer";
import { rootDocumentActions } from "./definitions/documents";
import { rootNavigationActions } from "./definitions/navigation";
import { rootNotificationActions } from "./definitions/notifications";
import { rootRevisionActions } from "./definitions/revisions";
import { rootSettingsActions } from "./definitions/settings";
import { rootTeamActions } from "./definitions/teams";
@@ -12,6 +13,7 @@ export default [
...rootDocumentActions,
...rootUserActions,
...rootNavigationActions,
...rootNotificationActions,
...rootRevisionActions,
...rootSettingsActions,
...rootDeveloperActions,
+2
View File
@@ -12,6 +12,8 @@ export const SettingsSection = ({ t }: ActionContext) => t("Settings");
export const NavigationSection = ({ t }: ActionContext) => t("Navigation");
export const NotificationSection = ({ t }: ActionContext) => t("Notification");
export const UserSection = ({ t }: ActionContext) => t("People");
export const TeamSection = ({ t }: ActionContext) => t("Workspace");
+8 -4
View File
@@ -1,8 +1,9 @@
/* eslint-disable react/prop-types */
import * as React from "react";
import Tooltip, { Props as TooltipProps } from "~/components/Tooltip";
import { Action, ActionContext } from "~/types";
export type Props = React.ComponentPropsWithoutRef<"button"> & {
export type Props = React.HTMLAttributes<HTMLButtonElement> & {
/** Show the button in a disabled state */
disabled?: boolean;
/** Hide the button entirely if action is not applicable */
@@ -18,14 +19,17 @@ export type Props = React.ComponentPropsWithoutRef<"button"> & {
/**
* Button that can be used to trigger an action definition.
*/
const ActionButton = React.forwardRef(
(
const ActionButton = React.forwardRef<HTMLButtonElement, Props>(
function _ActionButton(
{ action, context, tooltip, hideOnActionDisabled, ...rest }: Props,
ref: React.Ref<HTMLButtonElement>
) => {
) {
const [executing, setExecuting] = React.useState(false);
const disabled = rest.disabled;
if (action && !context) {
throw new Error("Context must be provided with action");
}
if (!context || !action) {
return <button {...rest} ref={ref} />;
}
+24 -9
View File
@@ -5,10 +5,14 @@ import * as React from "react";
import { IntegrationService } from "@shared/types";
import env from "~/env";
const Analytics: React.FC = ({ children }) => {
type Props = {
children?: React.ReactNode;
};
const Analytics: React.FC = ({ children }: Props) => {
// Google Analytics 3
React.useEffect(() => {
if (!env.GOOGLE_ANALYTICS_ID) {
if (!env.GOOGLE_ANALYTICS_ID?.startsWith("UA-")) {
return;
}
@@ -37,25 +41,36 @@ const Analytics: React.FC = ({ children }) => {
// Google Analytics 4
React.useEffect(() => {
if (env.analytics.service !== IntegrationService.GoogleAnalytics) {
const measurementIds = [];
if (env.analytics.service === IntegrationService.GoogleAnalytics) {
measurementIds.push(escape(env.analytics.settings?.measurementId));
}
if (env.GOOGLE_ANALYTICS_ID?.startsWith("G-")) {
measurementIds.push(env.GOOGLE_ANALYTICS_ID);
}
if (measurementIds.length === 0) {
return;
}
const measurementId = escape(env.analytics.settings?.measurementId);
const params = {
allow_google_signals: false,
restricted_data_processing: true,
};
window.dataLayer = window.dataLayer || [];
window.gtag = function () {
window.dataLayer.push(arguments);
};
window.gtag("js", new Date());
window.gtag("config", measurementId, {
allow_google_signals: false,
restricted_data_processing: true,
});
for (const measurementId of measurementIds) {
window.gtag("config", measurementId, params);
}
const script = document.createElement("script");
script.type = "text/javascript";
script.src = `https://www.googletagmanager.com/gtag/js?id=${measurementId}`;
script.src = `https://www.googletagmanager.com/gtag/js?id=${measurementIds[0]}`;
script.async = true;
document.getElementsByTagName("head")[0]?.appendChild(script);
}, []);
+4
View File
@@ -20,6 +20,10 @@ function ArrowKeyNavigation(
const handleKeyDown = React.useCallback(
(ev) => {
if (onEscape) {
if (ev.nativeEvent.isComposing) {
return;
}
if (ev.key === "Escape") {
onEscape(ev);
}
+7 -9
View File
@@ -2,9 +2,9 @@ import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { Redirect } from "react-router-dom";
import LoadingIndicator from "~/components/LoadingIndicator";
import useStores from "~/hooks/useStores";
import { changeLanguage } from "~/utils/language";
import LoadingIndicator from "./LoadingIndicator";
type Props = {
children: JSX.Element;
@@ -18,20 +18,18 @@ const Authenticated = ({ children }: Props) => {
// 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);
void changeLanguage(language, i18n);
}, [i18n, language]);
if (auth.authenticated) {
const { user, team } = auth;
if (!team || !user) {
return <LoadingIndicator />;
}
return children;
}
auth.logout(true);
if (auth.isFetching) {
return <LoadingIndicator />;
}
void auth.logout(true);
return <Redirect to="/" />;
};
+17 -7
View File
@@ -15,6 +15,7 @@ import type { Editor as TEditor } from "~/editor";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
import history from "~/utils/history";
import lazyWithRetry from "~/utils/lazyWithRetry";
import {
searchPath,
newDocumentPath,
@@ -25,18 +26,22 @@ import {
} from "~/utils/routeHelpers";
import Fade from "./Fade";
const DocumentComments = React.lazy(
const DocumentComments = lazyWithRetry(
() => import("~/scenes/Document/components/Comments")
);
const DocumentHistory = React.lazy(
const DocumentHistory = lazyWithRetry(
() => import("~/scenes/Document/components/History")
);
const DocumentInsights = React.lazy(
const DocumentInsights = lazyWithRetry(
() => import("~/scenes/Document/components/Insights")
);
const CommandBar = React.lazy(() => import("~/components/CommandBar"));
const CommandBar = lazyWithRetry(() => import("~/components/CommandBar"));
const AuthenticatedLayout: React.FC = ({ children }) => {
type Props = {
children?: React.ReactNode;
};
const AuthenticatedLayout: React.FC = ({ children }: Props) => {
const { ui, auth } = useStores();
const location = useLocation();
const can = usePolicy(ui.activeCollectionId);
@@ -96,7 +101,10 @@ const AuthenticatedLayout: React.FC = ({ children }) => {
team?.getPreference(TeamPreference.Commenting);
const sidebarRight = (
<AnimatePresence>
<AnimatePresence
initial={false}
key={ui.activeDocumentId ? "active" : "inactive"}
>
{(showHistory || showInsights || showComments) && (
<Route path={`/doc/${slug}`}>
<SidebarRight>
@@ -118,7 +126,9 @@ const AuthenticatedLayout: React.FC = ({ children }) => {
<RegisterKeyDown trigger="t" handler={goToSearch} />
<RegisterKeyDown trigger="/" handler={goToSearch} />
{children}
<CommandBar />
<React.Suspense fallback={null}>
<CommandBar />
</React.Suspense>
</Layout>
</DocumentContext.Provider>
);
+11 -18
View File
@@ -1,9 +1,16 @@
import * as React from "react";
import styled from "styled-components";
import { s } from "@shared/styles";
import useBoolean from "~/hooks/useBoolean";
import Initials from "./Initials";
export enum AvatarSize {
Small = 16,
Medium = 24,
Large = 32,
XLarge = 48,
XXLarge = 64,
}
export interface IAvatar {
avatarUrl: string | null;
color?: string;
@@ -12,9 +19,8 @@ export interface IAvatar {
}
type Props = {
size: number;
size: AvatarSize;
src?: string;
icon?: React.ReactNode;
model?: IAvatar;
alt?: string;
showBorder?: boolean;
@@ -24,7 +30,7 @@ type Props = {
};
function Avatar(props: Props) {
const { icon, showBorder, model, style, ...rest } = props;
const { showBorder, model, style, ...rest } = props;
const src = props.src || model?.avatarUrl;
const [error, handleError] = useBoolean(false);
@@ -44,13 +50,12 @@ function Avatar(props: Props) {
) : (
<Initials $showBorder={showBorder} {...rest} />
)}
{icon && <IconWrapper>{icon}</IconWrapper>}
</Relative>
);
}
Avatar.defaultProps = {
size: 24,
size: AvatarSize.Medium,
};
const Relative = styled.div`
@@ -59,18 +64,6 @@ const Relative = styled.div`
flex-shrink: 0;
`;
const IconWrapper = styled.div`
display: flex;
position: absolute;
bottom: -2px;
right: -2px;
background: ${s("accent")};
border: 2px solid ${s("background")};
border-radius: 100%;
width: 20px;
height: 20px;
`;
const CircleImg = styled.img<{ size: number; $showBorder?: boolean }>`
display: block;
width: ${(props) => props.size}px;
+5 -1
View File
@@ -7,7 +7,11 @@ const Badge = styled.span<{ yellow?: boolean; primary?: boolean }>`
background-color: ${({ yellow, primary, theme }) =>
yellow ? theme.yellow : primary ? theme.accent : "transparent"};
color: ${({ primary, yellow, theme }) =>
primary ? theme.white : yellow ? theme.almostBlack : theme.textTertiary};
primary
? theme.accentText
: yellow
? theme.almostBlack
: theme.textTertiary};
border: 1px solid
${({ primary, yellow, theme }) =>
primary || yellow
+1 -1
View File
@@ -60,7 +60,7 @@ const RealButton = styled(ActionButton)<RealProps>`
${(props) =>
props.$neutral &&
`
background: ${props.theme.buttonNeutralBackground};
background: inherit;
color: ${props.theme.buttonNeutralText};
box-shadow: ${
props.$borderOnHover
+2 -1
View File
@@ -3,6 +3,7 @@ import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
type Props = {
children?: React.ReactNode;
withStickyHeader?: boolean;
};
@@ -26,7 +27,7 @@ const Content = styled.div`
`};
`;
const CenteredContent: React.FC<Props> = ({ children, ...rest }) => (
const CenteredContent: React.FC<Props> = ({ children, ...rest }: Props) => (
<Container {...rest}>
<Content>{children}</Content>
</Container>
+1 -1
View File
@@ -57,7 +57,7 @@ function Collaborators(props: Props) {
if (!isEqual(requestedUserIds, ids) && ids.length > 0) {
setRequestedUserIds(ids);
users.fetchPage({ ids, limit: 100 });
void users.fetchPage({ ids, limit: 100 });
}
}, [document, users, presentIds, document.collaboratorIds, requestedUserIds]);
+3 -3
View File
@@ -71,9 +71,9 @@ function CollectionDescription({ collection }: Props) {
);
const handleChange = React.useCallback(
(getValue) => {
async (getValue) => {
setDirty(true);
handleSave(getValue);
await handleSave(getValue);
},
[handleSave]
);
@@ -111,7 +111,7 @@ function CollectionDescription({ collection }: Props) {
onBlur={handleStopEditing}
maxLength={1000}
embedsDisabled
readOnlyWriteCheckboxes
canUpdate
/>
</React.Suspense>
) : (
+7 -3
View File
@@ -25,9 +25,9 @@ function CommandBar() {
const { rootAction } = useKBar((state) => ({
rootAction: state.currentRootActionId
? ((state.actions[
? (state.actions[
state.currentRootActionId
] as unknown) as CommandBarAction)
] as unknown as CommandBarAction)
: undefined,
}));
@@ -52,7 +52,11 @@ function CommandBar() {
);
}
const KBarPortal: React.FC = ({ children }) => {
type Props = {
children?: React.ReactNode;
};
const KBarPortal: React.FC = ({ children }: Props) => {
const { showing } = useKBar((state) => ({
showing: state.visualState !== "hidden",
}));
+25 -10
View File
@@ -5,6 +5,7 @@ import styled, { css, useTheme } from "styled-components";
import { s, ellipsis } from "@shared/styles";
import Flex from "~/components/Flex";
import Key from "~/components/Key";
import Text from "./Text";
type Props = {
action: ActionImpl;
@@ -55,22 +56,36 @@ function CommandBarItem(
{action.children?.length ? "…" : ""}
</Content>
{action.shortcut?.length ? (
<div
style={{
display: "grid",
gridAutoFlow: "column",
gap: "4px",
}}
>
{action.shortcut.map((sc: string) => (
<Key key={sc}>{sc}</Key>
<Shortcut>
{action.shortcut.map((sc: string, index) => (
<React.Fragment key={sc}>
{index > 0 ? (
<>
{" "}
<Text size="xsmall" as="span" type="secondary">
then
</Text>{" "}
</>
) : (
""
)}
{sc.split("+").map((s) => (
<Key key={s}>{s}</Key>
))}
</React.Fragment>
))}
</div>
</Shortcut>
) : null}
</Item>
);
}
const Shortcut = styled.div`
display: grid;
grid-auto-flow: column;
gap: 4px;
`;
const Icon = styled(Flex)`
align-items: center;
justify-content: center;
+24 -15
View File
@@ -8,24 +8,33 @@ export default function CommandBarResults() {
const { results, rootActionId } = useMatches();
return (
<KBarResults
items={results}
maxHeight={400}
onRender={({ item, active }) =>
typeof item === "string" ? (
<Header>{item}</Header>
) : (
<CommandBarItem
action={item}
active={active}
currentRootActionId={rootActionId}
/>
)
}
/>
<Container>
<KBarResults
items={results}
maxHeight={400}
onRender={({ item, active }) =>
typeof item === "string" ? (
<Header>{item}</Header>
) : (
<CommandBarItem
action={item}
active={active}
currentRootActionId={rootActionId}
/>
)
}
/>
</Container>
);
}
// Cannot style KBarResults unfortunately, so we must wrap and target the inner
const Container = styled.div`
> div {
padding-bottom: 8px;
}
`;
const Header = styled.h3`
font-size: 13px;
letter-spacing: 0.04em;
+2 -1
View File
@@ -17,6 +17,7 @@ type Props = {
danger?: boolean;
/** Keep the submit button disabled */
disabled?: boolean;
children?: React.ReactNode;
};
const ConfirmationDialog: React.FC<Props> = ({
@@ -26,7 +27,7 @@ const ConfirmationDialog: React.FC<Props> = ({
savingText,
danger,
disabled = false,
}) => {
}: Props) => {
const [isSaving, setIsSaving] = React.useState(false);
const { dialogs } = useStores();
const { showToast } = useToasts();
+107 -101
View File
@@ -30,69 +30,74 @@ export type RefHandle = {
* Defines a content editable component with the same interface as a native
* HTMLInputElement (or, as close as we can get).
*/
const ContentEditable = React.forwardRef(
(
{
disabled,
onChange,
onInput,
onBlur,
onKeyDown,
value,
children,
className,
maxLength,
autoFocus,
placeholder,
readOnly,
dir,
onClick,
...rest
}: Props,
ref: React.RefObject<RefHandle>
) => {
const contentRef = React.useRef<HTMLSpanElement>(null);
const [innerValue, setInnerValue] = React.useState<string>(value);
const lastValue = React.useRef("");
const ContentEditable = React.forwardRef(function _ContentEditable(
{
disabled,
onChange,
onInput,
onBlur,
onKeyDown,
value,
children,
className,
maxLength,
autoFocus,
placeholder,
readOnly,
dir,
onClick,
...rest
}: Props,
ref: React.RefObject<RefHandle>
) {
const contentRef = React.useRef<HTMLSpanElement>(null);
const [innerValue, setInnerValue] = React.useState<string>(value);
const lastValue = React.useRef(value);
React.useImperativeHandle(ref, () => ({
focus: () => {
if (contentRef.current) {
contentRef.current.focus();
// looks unnecessary but required because of https://github.com/outline/outline/issues/5198
if (!contentRef.current.innerText) {
placeCaret(contentRef.current, true);
}
}
},
focusAtStart: () => {
if (contentRef.current) {
contentRef.current.focus();
React.useImperativeHandle(ref, () => ({
focus: () => {
if (contentRef.current) {
contentRef.current.focus();
// looks unnecessary but required because of https://github.com/outline/outline/issues/5198
if (!contentRef.current.innerText) {
placeCaret(contentRef.current, true);
}
},
focusAtEnd: () => {
if (contentRef.current) {
contentRef.current.focus();
placeCaret(contentRef.current, false);
}
},
getComputedDirection: () => {
if (contentRef.current) {
return window.getComputedStyle(contentRef.current).direction;
}
return "ltr";
},
}));
}
},
focusAtStart: () => {
if (contentRef.current) {
contentRef.current.focus();
placeCaret(contentRef.current, true);
}
},
focusAtEnd: () => {
if (contentRef.current) {
contentRef.current.focus();
placeCaret(contentRef.current, false);
}
},
getComputedDirection: () => {
if (contentRef.current) {
return window.getComputedStyle(contentRef.current).direction;
}
return "ltr";
},
}));
const wrappedEvent = (
const wrappedEvent =
(
callback:
| React.FocusEventHandler<HTMLSpanElement>
| React.FormEventHandler<HTMLSpanElement>
| React.KeyboardEventHandler<HTMLSpanElement>
| undefined
) => (event: any) => {
const text = contentRef.current?.innerText || "";
) =>
(event: any) => {
if (readOnly) {
return;
}
const text = event.currentTarget.textContent || "";
if (maxLength && isPrintableKeyEvent(event) && text.length >= maxLength) {
event?.preventDefault();
@@ -101,62 +106,62 @@ const ContentEditable = React.forwardRef(
if (text !== lastValue.current) {
lastValue.current = text;
onChange && onChange(text);
onChange?.(text);
}
callback?.(event);
};
// This is to account for being within a React.Suspense boundary, in this
// case the component may be rendered with display: none. React 18 may solve
// this in the future by delaying useEffect hooks:
// https://github.com/facebook/react/issues/14536#issuecomment-861980492
const isVisible = useOnScreen(contentRef);
// This is to account for being within a React.Suspense boundary, in this
// case the component may be rendered with display: none. React 18 may solve
// this in the future by delaying useEffect hooks:
// https://github.com/facebook/react/issues/14536#issuecomment-861980492
const isVisible = useOnScreen(contentRef);
React.useEffect(() => {
if (autoFocus && isVisible && !disabled && !readOnly) {
contentRef.current?.focus();
}
}, [autoFocus, disabled, isVisible, readOnly, contentRef]);
React.useEffect(() => {
if (autoFocus && isVisible && !disabled && !readOnly) {
contentRef.current?.focus();
}
}, [autoFocus, disabled, isVisible, readOnly, contentRef]);
React.useEffect(() => {
if (value !== contentRef.current?.innerText) {
setInnerValue(value);
}
}, [value, contentRef]);
React.useEffect(() => {
if (contentRef.current && value !== contentRef.current.textContent) {
setInnerValue(value);
}
}, [value, contentRef]);
// Ensure only plain text can be pasted into input when pasting from another
// rich text source
const handlePaste = React.useCallback(
(event: React.ClipboardEvent<HTMLSpanElement>) => {
event.preventDefault();
const text = event.clipboardData.getData("text/plain");
window.document.execCommand("insertText", false, text);
},
[]
);
// Ensure only plain text can be pasted into input when pasting from another
// rich text source. Note: If `onPaste` prop is passed then it takes
// priority over this behavior.
const handlePaste = React.useCallback(
(event: React.ClipboardEvent<HTMLSpanElement>) => {
event.preventDefault();
const text = event.clipboardData.getData("text/plain");
window.document.execCommand("insertText", false, text);
},
[]
);
return (
<div className={className} dir={dir} onClick={onClick}>
<Content
ref={contentRef}
contentEditable={!disabled && !readOnly}
onInput={wrappedEvent(onInput)}
onBlur={wrappedEvent(onBlur)}
onKeyDown={wrappedEvent(onKeyDown)}
onPaste={handlePaste}
data-placeholder={placeholder}
suppressContentEditableWarning
role="textbox"
{...rest}
>
{innerValue}
</Content>
{children}
</div>
);
}
);
return (
<div className={className} dir={dir} onClick={onClick}>
<Content
ref={contentRef}
contentEditable={!disabled && !readOnly}
onInput={wrappedEvent(onInput)}
onBlur={wrappedEvent(onBlur)}
onKeyDown={wrappedEvent(onKeyDown)}
onPaste={handlePaste}
data-placeholder={placeholder}
suppressContentEditableWarning
role="textbox"
{...rest}
>
{innerValue}
</Content>
{children}
</div>
);
});
function placeCaret(element: HTMLElement, atStart: boolean) {
if (
@@ -180,6 +185,7 @@ const Content = styled.span`
outline: none;
resize: none;
cursor: text;
word-break: anywhere;
&:empty {
display: inline-block;
+33 -27
View File
@@ -8,6 +8,7 @@ import breakpoint from "styled-components-breakpoint";
import MenuIconWrapper from "../MenuIconWrapper";
type Props = {
id?: string;
onClick?: (event: React.SyntheticEvent) => void | Promise<void>;
active?: boolean;
selected?: boolean;
@@ -21,6 +22,7 @@ type Props = {
level?: number;
icon?: React.ReactElement;
children?: React.ReactNode;
ref?: React.LegacyRef<HTMLButtonElement> | undefined;
};
const MenuItem = (
@@ -37,34 +39,26 @@ const MenuItem = (
}: Props,
ref: React.Ref<HTMLAnchorElement>
) => {
const handleClick = React.useCallback(
(ev) => {
if (onClick) {
const content = React.useCallback(
(props) => {
const handleClick = async (ev: React.MouseEvent) => {
hide?.();
if (onClick) {
ev.preventDefault();
await onClick(ev);
}
};
// 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 = (ev: React.MouseEvent) => {
ev.preventDefault();
onClick(ev);
}
ev.stopPropagation();
};
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) => (
return (
<MenuAnchor
{...props}
$active={active}
@@ -85,7 +79,19 @@ const MenuItem = (
{icon && <MenuIconWrapper>{icon}</MenuIconWrapper>}
{children}
</MenuAnchor>
)}
);
},
[active, as, hide, icon, onClick, ref, children, selected]
);
return (
<BaseMenuItem
onClick={disabled ? undefined : onClick}
disabled={disabled}
hide={hide}
{...rest}
>
{content}
</BaseMenuItem>
);
};
+6 -2
View File
@@ -24,8 +24,12 @@ type Positions = {
export default function MouseSafeArea(props: {
parentRef: React.RefObject<HTMLElement | null>;
}) {
const { x = 0, y = 0, height: h = 0, width: w = 0 } =
props.parentRef.current?.getBoundingClientRect() || {};
const {
x = 0,
y = 0,
height: h = 0,
width: w = 0,
} = props.parentRef.current?.getBoundingClientRect() || {};
const [mouseX, mouseY] = useMousePosition();
const positions = { x, y, h, w, mouseX, mouseY };
+32 -30
View File
@@ -44,37 +44,35 @@ type SubMenuProps = MenuStateReturn & {
title: React.ReactNode;
};
const SubMenu = React.forwardRef(
(
{ templateItems, title, parentMenuState, ...rest }: SubMenuProps,
ref: React.LegacyRef<HTMLButtonElement>
) => {
const { t } = useTranslation();
const theme = useTheme();
const menu = useMenuState();
const SubMenu = React.forwardRef(function _Template(
{ templateItems, title, parentMenuState, ...rest }: SubMenuProps,
ref: React.LegacyRef<HTMLButtonElement>
) {
const { t } = useTranslation();
const theme = useTheme();
const menu = useMenuState();
return (
<>
<MenuButton ref={ref} {...menu} {...rest}>
{(props) => (
<MenuAnchor disclosure {...props}>
{title} <Disclosure color={theme.textTertiary} />
</MenuAnchor>
)}
</MenuButton>
<ContextMenu
{...menu}
aria-label={t("Submenu")}
onClick={parentMenuState.hide}
parentMenuState={parentMenuState}
>
<MouseSafeArea parentRef={menu.unstable_popoverRef} />
<Template {...menu} items={templateItems} />
</ContextMenu>
</>
);
}
);
return (
<>
<MenuButton ref={ref} {...menu} {...rest}>
{(props) => (
<MenuAnchor disclosure {...props}>
{title} <Disclosure color={theme.textTertiary} />
</MenuAnchor>
)}
</MenuButton>
<ContextMenu
{...menu}
aria-label={t("Submenu")}
onClick={parentMenuState.hide}
parentMenuState={parentMenuState}
>
<MouseSafeArea parentRef={menu.unstable_popoverRef} />
<Template {...menu} items={templateItems} />
</ContextMenu>
</>
);
});
export function filterTemplateItems(items: TMenuItem[]): TMenuItem[] {
return items
@@ -135,6 +133,7 @@ function Template({ items, actions, context, ...menu }: Props) {
return (
<MenuItem
as={Link}
id={`${item.title}-${index}`}
to={item.to}
key={index}
disabled={item.disabled}
@@ -150,6 +149,7 @@ function Template({ items, actions, context, ...menu }: Props) {
if (item.type === "link") {
return (
<MenuItem
id={`${item.title}-${index}`}
href={item.href}
key={index}
disabled={item.disabled}
@@ -168,6 +168,7 @@ function Template({ items, actions, context, ...menu }: Props) {
return (
<MenuItem
as="button"
id={`${item.title}-${index}`}
onClick={item.onClick}
disabled={item.disabled}
selected={item.selected}
@@ -186,6 +187,7 @@ function Template({ items, actions, context, ...menu }: Props) {
<BaseMenuItem
key={index}
as={SubMenu}
id={`${item.title}-${index}`}
templateItems={item.items}
parentMenuState={menu}
title={<Title title={item.title} icon={item.icon} />}
+7 -3
View File
@@ -46,6 +46,7 @@ type Props = MenuStateReturn & {
onClose?: () => void;
/** Called when the context menu is clicked. */
onClick?: (ev: React.MouseEvent) => void;
children?: React.ReactNode;
};
const ContextMenu: React.FC<Props> = ({
@@ -54,9 +55,12 @@ const ContextMenu: React.FC<Props> = ({
onClose,
parentMenuState,
...rest
}) => {
}: Props) => {
const previousVisible = usePrevious(rest.visible);
const maxHeight = useMenuHeight(rest.visible, rest.unstable_disclosureRef);
const maxHeight = useMenuHeight({
visible: rest.visible,
elementRef: rest.unstable_disclosureRef,
});
const backgroundRef = React.useRef<HTMLDivElement>(null);
const { ui } = useStores();
const { t } = useTranslation();
@@ -147,7 +151,7 @@ const ContextMenu: React.FC<Props> = ({
ref={backgroundRef}
hiddenScrollbars
style={
maxHeight && topAnchor
topAnchor
? {
maxHeight,
}
@@ -28,7 +28,7 @@ const DefaultCollectionInputSelect = ({
const { showToast } = useToasts();
React.useEffect(() => {
async function load() {
async function fetchData() {
if (!collections.isLoaded && !fetching && !fetchError) {
try {
setFetching(true);
@@ -48,7 +48,7 @@ const DefaultCollectionInputSelect = ({
}
}
}
load();
void fetchData();
}, [showToast, fetchError, t, fetching, collections]);
const options = React.useMemo(
+1 -1
View File
@@ -30,7 +30,7 @@ export default function DesktopEventHandler() {
action: {
text: "Install now",
onClick: () => {
Desktop.bridge?.restartAndInstall();
void Desktop.bridge?.restartAndInstall();
},
},
});
+7 -2
View File
@@ -17,6 +17,7 @@ import {
} from "~/utils/routeHelpers";
type Props = {
children?: React.ReactNode;
document: Document;
onlyText?: boolean;
};
@@ -58,7 +59,7 @@ const DocumentBreadcrumb: React.FC<Props> = ({
document,
children,
onlyText,
}) => {
}: Props) => {
const { collections } = useStores();
const { t } = useTranslation();
const category = useCategory(document);
@@ -129,7 +130,11 @@ const DocumentBreadcrumb: React.FC<Props> = ({
);
}
return <Breadcrumb items={items} children={children} highlightFirstItem />;
return (
<Breadcrumb items={items} highlightFirstItem>
{children}
</Breadcrumb>
);
};
const SmallSlash = styled(GoToIcon)`
+6 -5
View File
@@ -14,6 +14,7 @@ import Flex from "~/components/Flex";
import NudeButton from "~/components/NudeButton";
import Time from "~/components/Time";
import useStores from "~/hooks/useStores";
import { hover } from "~/styles";
import CollectionIcon from "./Icons/CollectionIcon";
import EmojiIcon from "./Icons/EmojiIcon";
import Squircle from "./Squircle";
@@ -57,10 +58,10 @@ function DocumentCard(props: Props) {
};
const handleUnpin = React.useCallback(
(ev) => {
async (ev) => {
ev.preventDefault();
ev.stopPropagation();
pin?.delete();
await pin?.delete();
},
[pin]
);
@@ -179,7 +180,7 @@ const Fold = styled.svg`
const PinButton = styled(NudeButton)`
color: ${s("textTertiary")};
&:hover,
&:${hover},
&:active {
color: ${s("text")};
}
@@ -209,7 +210,7 @@ const Reorderable = styled.div<{ $isDragging: boolean }>`
z-index: ${(props) => (props.$isDragging ? 1 : "inherit")};
pointer-events: ${(props) => (props.$isDragging ? "none" : "inherit")};
&:hover ${Actions} {
&: ${hover} ${Actions} {
opacity: 1;
}
`;
@@ -248,7 +249,7 @@ const DocumentLink = styled(Link)<{
opacity: 0;
}
&:hover,
&:${hover},
&:active,
&:focus,
&:focus-within {
+18 -14
View File
@@ -45,9 +45,8 @@ function DocumentExplorer({ onSubmit, onSelect, items }: Props) {
const [selectedNode, selectNode] = React.useState<NavigationNode | null>(
null
);
const [initialScrollOffset, setInitialScrollOffset] = React.useState<number>(
0
);
const [initialScrollOffset, setInitialScrollOffset] =
React.useState<number>(0);
const [activeNode, setActiveNode] = React.useState<number>(0);
const [expandedNodes, setExpandedNodes] = React.useState<string[]>([]);
const [itemRefs, setItemRefs] = React.useState<
@@ -165,7 +164,7 @@ function DocumentExplorer({ onSubmit, onSelect, items }: Props) {
(collection) => expandedNodes.includes(collection.id) || searchTerm
)
.forEach((collection) => {
collection.fetchDocuments();
void collection.fetchDocuments();
});
}, [collections, expandedNodes, searchTerm]);
@@ -336,16 +335,21 @@ function DocumentExplorer({ onSubmit, onSelect, items }: Props) {
const innerElementType = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ style, ...rest }, ref) => (
<div
ref={ref}
style={{
...style,
height: `${parseFloat(style?.height + "") + VERTICAL_PADDING * 2}px`,
}}
{...rest}
/>
));
>(function innerElementType(
{ style, ...rest }: React.HTMLAttributes<HTMLDivElement>,
ref
) {
return (
<div
ref={ref}
style={{
...style,
height: `${parseFloat(style?.height + "") + VERTICAL_PADDING * 2}px`,
}}
{...rest}
/>
);
});
return (
<Container tabIndex={-1} onKeyDown={handleKeyDown}>
+2 -5
View File
@@ -181,8 +181,7 @@ const Actions = styled(EventBoundary)`
color: ${s("textSecondary")};
${NudeButton} {
&:hover,
&[aria-expanded="true"] {
&: ${hover}, &[aria-expanded= "true"] {
background: ${s("sidebarControlHoverBackground")};
}
}
@@ -234,7 +233,7 @@ const DocumentLink = styled(Link)<{
${AnimatedStar} {
opacity: 0.5;
&:hover {
&:${hover} {
opacity: 1;
}
}
@@ -259,10 +258,8 @@ const Heading = styled.h3<{ rtl?: boolean }>`
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: ${s("text")};
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
+15 -2
View File
@@ -6,6 +6,7 @@ import { Link } from "react-router-dom";
import styled from "styled-components";
import { s, ellipsis } from "@shared/styles";
import Document from "~/models/Document";
import Revision from "~/models/Revision";
import DocumentBreadcrumb from "~/components/DocumentBreadcrumb";
import DocumentTasks from "~/components/DocumentTasks";
import Flex from "~/components/Flex";
@@ -14,11 +15,13 @@ import useCurrentUser from "~/hooks/useCurrentUser";
import useStores from "~/hooks/useStores";
type Props = {
children?: React.ReactNode;
showCollection?: boolean;
showPublished?: boolean;
showLastViewed?: boolean;
showParentDocuments?: boolean;
document: Document;
revision?: Revision;
replace?: boolean;
to?: LocationDescriptor;
};
@@ -29,11 +32,12 @@ const DocumentMeta: React.FC<Props> = ({
showLastViewed,
showParentDocuments,
document,
revision,
children,
replace,
to,
...rest
}) => {
}: Props) => {
const { t } = useTranslation();
const { collections } = useStores();
const user = useCurrentUser();
@@ -64,7 +68,16 @@ const DocumentMeta: React.FC<Props> = ({
const userName = updatedBy.name;
let content;
if (deletedAt) {
if (revision) {
content = (
<span>
{revision.createdBy?.id === user.id
? t("You updated")
: t("{{ userName }} updated", { userName })}{" "}
<Time dateTime={revision.createdAt} addSuffix />
</span>
);
} else if (deletedAt) {
content = (
<span>
{lastUpdatedByCurrentUser
+6 -5
View File
@@ -1,8 +1,8 @@
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 { dateToRelative } from "@shared/utils/date";
import Document from "~/models/Document";
import User from "~/models/User";
import Avatar from "~/components/Avatar";
@@ -33,9 +33,10 @@ function DocumentViews({ document, isOpen }: Props) {
documentViews,
(view) => !presentIds.includes(view.user.id)
);
const users = React.useMemo(() => sortedViews.map((v) => v.user), [
sortedViews,
]);
const users = React.useMemo(
() => sortedViews.map((v) => v.user),
[sortedViews]
);
return (
<>
@@ -52,7 +53,7 @@ function DocumentViews({ document, isOpen }: Props) {
? t("Currently editing")
: t("Currently viewing")
: t("Viewed {{ timeAgo }} ago", {
timeAgo: formatDistanceToNow(
timeAgo: dateToRelative(
view ? Date.parse(view.lastViewedAt) : new Date()
),
});
+11 -8
View File
@@ -1,4 +1,3 @@
import { formatDistanceToNow } from "date-fns";
import { deburr, difference, sortBy } from "lodash";
import { observer } from "mobx-react";
import { DOMParser as ProsemirrorDOMParser } from "prosemirror-model";
@@ -10,6 +9,7 @@ import { Optional } from "utility-types";
import insertFiles from "@shared/editor/commands/insertFiles";
import { AttachmentPreset } from "@shared/types";
import { Heading } from "@shared/utils/ProsemirrorHelper";
import { dateLocale, dateToRelative } from "@shared/utils/date";
import { getDataTransferFiles } from "@shared/utils/files";
import parseDocumentSlug from "@shared/utils/parseDocumentSlug";
import { isInternalUrl } from "@shared/utils/urls";
@@ -23,14 +23,16 @@ import useDictionary from "~/hooks/useDictionary";
import useEmbeds from "~/hooks/useEmbeds";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
import useUserLocale from "~/hooks/useUserLocale";
import { NotFoundError } from "~/utils/errors";
import { uploadFile } from "~/utils/files";
import { isModKey } from "~/utils/keyboard";
import lazyWithRetry from "~/utils/lazyWithRetry";
import { sharedDocumentPath } from "~/utils/routeHelpers";
import { isHash } from "~/utils/urls";
import DocumentBreadcrumb from "./DocumentBreadcrumb";
const LazyLoadedEditor = React.lazy(() => import("~/editor"));
const LazyLoadedEditor = lazyWithRetry(() => import("~/editor"));
export type Props = Optional<
EditorProps,
@@ -59,6 +61,8 @@ function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
onCreateCommentMark,
onDeleteCommentMark,
} = props;
const userLocale = useUserLocale();
const locale = dateLocale(userLocale);
const { auth, comments, documents } = useStores();
const { showToast } = useToasts();
const dictionary = useDictionary();
@@ -67,10 +71,8 @@ function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
const localRef = React.useRef<SharedEditor>();
const preferences = auth.user?.preferences;
const previousHeadings = React.useRef<Heading[] | null>(null);
const [
activeLinkElement,
setActiveLink,
] = React.useState<HTMLAnchorElement | null>(null);
const [activeLinkElement, setActiveLink] =
React.useState<HTMLAnchorElement | null>(null);
const previousCommentIds = React.useRef<string[]>();
const handleLinkActive = React.useCallback((element: HTMLAnchorElement) => {
@@ -93,8 +95,10 @@ function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
try {
const document = await documents.fetch(slug);
const time = formatDistanceToNow(Date.parse(document.updatedAt), {
const time = dateToRelative(Date.parse(document.updatedAt), {
addSuffix: true,
shorten: true,
locale,
});
return [
@@ -350,7 +354,6 @@ function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
)}
{activeLinkElement && !shareId && (
<HoverPreview
id={props.id}
element={activeLinkElement}
onClose={handleLinkInactive}
/>
+2 -1
View File
@@ -1,10 +1,11 @@
import * as React from "react";
type Props = {
children?: React.ReactNode;
className?: string;
};
const EventBoundary: React.FC<Props> = ({ children, className }) => {
const EventBoundary: React.FC<Props> = ({ children, className }: Props) => {
const handleClick = React.useCallback((event: React.SyntheticEvent) => {
event.preventDefault();
event.stopPropagation();
+22 -24
View File
@@ -7,7 +7,6 @@ import {
PublishIcon,
MoveIcon,
UnpublishIcon,
LightningIcon,
} from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
@@ -25,6 +24,7 @@ import Item, { Actions } from "~/components/List/Item";
import Time from "~/components/Time";
import useStores from "~/hooks/useStores";
import RevisionMenu from "~/menus/RevisionMenu";
import { hover } from "~/styles";
import Logger from "~/utils/Logger";
import { documentHistoryPath } from "~/utils/routeHelpers";
@@ -51,27 +51,24 @@ const EventListItem = ({ event, latest, document, ...rest }: Props) => {
ref.current?.focus();
};
const prefetchRevision = () => {
const prefetchRevision = async () => {
if (event.name === "revisions.create" && event.modelId) {
revisions.fetch(event.modelId);
await revisions.fetch(event.modelId);
}
};
switch (event.name) {
case "revisions.create":
icon = <EditIcon size={16} />;
meta = t("{{userName}} edited", opts);
meta = latest ? (
<>
{t("Current version")} &middot; {event.actor.name}
</>
) : (
t("{{userName}} edited", opts)
);
to = {
pathname: documentHistoryPath(document, event.modelId || ""),
state: { retainScrollPosition: true },
};
break;
case "documents.live_editing":
icon = <LightningIcon size={16} />;
meta = t("Latest");
to = {
pathname: documentHistoryPath(document),
pathname: documentHistoryPath(document, event.modelId || "latest"),
state: { retainScrollPosition: true },
};
break;
@@ -152,7 +149,7 @@ const EventListItem = ({ event, latest, document, ...rest }: Props) => {
</Subtitle>
}
actions={
isRevision && isActive && event.modelId ? (
isRevision && isActive && event.modelId && !latest ? (
<RevisionMenu document={document} revisionId={event.modelId} />
) : undefined
}
@@ -163,15 +160,16 @@ const EventListItem = ({ event, latest, document, ...rest }: Props) => {
);
};
const BaseItem = React.forwardRef(
({ to, ...rest }: ItemProps, ref?: React.Ref<HTMLAnchorElement>) => {
if (to) {
return <CompositeListItem to={to} ref={ref} {...rest} />;
}
return <ListItem ref={ref} {...rest} />;
const BaseItem = React.forwardRef(function _BaseItem(
{ to, ...rest }: ItemProps,
ref?: React.Ref<HTMLAnchorElement>
) {
if (to) {
return <CompositeListItem to={to} ref={ref} {...rest} />;
}
);
return <ListItem ref={ref} {...rest} />;
});
const Subtitle = styled.span`
svg {
@@ -220,7 +218,7 @@ const ItemStyle = css`
${Actions} {
opacity: 0.5;
&:hover {
&: ${hover} {
opacity: 1;
}
}
+29 -3
View File
@@ -21,6 +21,8 @@ function ExportDialog({ collection, onSubmit }: Props) {
const [format, setFormat] = React.useState<FileOperationFormat>(
FileOperationFormat.MarkdownZip
);
const [includeAttachments, setIncludeAttachments] =
React.useState<boolean>(true);
const user = useCurrentUser();
const { showToast } = useToasts();
const { collections } = useStores();
@@ -34,11 +36,18 @@ function ExportDialog({ collection, onSubmit }: Props) {
[]
);
const handleIncludeAttachmentsChange = React.useCallback(
(ev: React.ChangeEvent<HTMLInputElement>) => {
setIncludeAttachments(ev.target.checked);
},
[]
);
const handleSubmit = async () => {
if (collection) {
await collection.export(format);
await collection.export(format, includeAttachments);
} else {
await collections.export(format);
await collections.export(format, includeAttachments);
}
onSubmit();
showToast(t("Export started"), { type: "success" });
@@ -90,7 +99,7 @@ function ExportDialog({ collection, onSubmit }: Props) {
)}
<Flex gap={12} column>
{items.map((item) => (
<Option>
<Option key={item.value}>
<input
type="radio"
name="format"
@@ -107,6 +116,23 @@ function ExportDialog({ collection, onSubmit }: Props) {
</Option>
))}
</Flex>
<hr />
<Option>
<input
type="checkbox"
name="includeAttachments"
checked={includeAttachments}
onChange={handleIncludeAttachmentsChange}
/>
<div>
<Text size="small" weight="bold">
{t("Include attachments")}
</Text>
<Text size="small">
{t("Including uploaded images and files in the exported data")}.
</Text>{" "}
</div>
</Option>
</ConfirmationDialog>
);
}
+2 -1
View File
@@ -28,7 +28,8 @@ const Flex = styled.div<{
align-items: ${({ align }) => align};
justify-content: ${({ justify }) => justify};
flex-wrap: ${({ wrap }) => (wrap ? "wrap" : "initial")};
flex-shrink: ${({ shrink }) => (shrink ? 1 : "initial")};
flex-shrink: ${({ shrink }) =>
shrink === true ? 1 : shrink === false ? 0 : "initial"};
gap: ${({ gap }) => (gap ? `${gap}px` : "initial")};
min-height: 0;
min-width: 0;
+4 -6
View File
@@ -14,6 +14,7 @@ import ListItem from "~/components/List/Item";
import Modal from "~/components/Modal";
import useBoolean from "~/hooks/useBoolean";
import useStores from "~/hooks/useStores";
import { hover } from "~/styles";
import NudeButton from "./NudeButton";
type Props = {
@@ -27,11 +28,8 @@ type Props = {
function GroupListItem({ group, showFacepile, renderActions }: Props) {
const { groupMemberships } = useStores();
const { t } = useTranslation();
const [
membersModalOpen,
setMembersModalOpen,
setMembersModalClosed,
] = useBoolean();
const [membersModalOpen, setMembersModalOpen, setMembersModalClosed] =
useBoolean();
const memberCount = group.memberCount;
const membershipsInGroup = groupMemberships.inGroup(group.id);
const users = membershipsInGroup
@@ -87,7 +85,7 @@ const Image = styled(Flex)`
`;
const Title = styled.span`
&:hover {
&: ${hover} {
text-decoration: underline;
cursor: var(--pointer);
}
+2 -1
View File
@@ -6,6 +6,7 @@ import Scrollable from "~/components/Scrollable";
import usePrevious from "~/hooks/usePrevious";
type Props = {
children?: React.ReactNode;
isOpen: boolean;
title?: string;
onRequestClose: () => void;
@@ -17,7 +18,7 @@ const Guide: React.FC<Props> = ({
title = "Untitled",
onRequestClose,
...rest
}) => {
}: Props) => {
const dialog = useDialogState({
animated: 250,
});
-247
View File
@@ -1,247 +0,0 @@
import { transparentize } from "polished";
import * as React from "react";
import { Portal } from "react-portal";
import styled from "styled-components";
import { depths, s } from "@shared/styles";
import parseDocumentSlug from "@shared/utils/parseDocumentSlug";
import { isExternalUrl } from "@shared/utils/urls";
import HoverPreviewDocument from "~/components/HoverPreviewDocument";
import useMobile from "~/hooks/useMobile";
import useStores from "~/hooks/useStores";
import { fadeAndSlideDown } from "~/styles/animations";
const DELAY_OPEN = 300;
const DELAY_CLOSE = 300;
type Props = {
/* The document associated with the editor, if any */
id?: string;
/* The HTML element that is being hovered over */
element: HTMLAnchorElement;
/* A callback on close of the hover preview */
onClose: () => void;
};
function HoverPreviewInternal({ element, id, onClose }: Props) {
const { documents } = useStores();
const slug = parseDocumentSlug(element.href);
const [isVisible, setVisible] = React.useState(false);
const timerClose = React.useRef<ReturnType<typeof setTimeout>>();
const timerOpen = React.useRef<ReturnType<typeof setTimeout>>();
const cardRef = React.useRef<HTMLDivElement>(null);
const startCloseTimer = () => {
stopOpenTimer();
timerClose.current = setTimeout(() => {
if (isVisible) {
setVisible(false);
}
onClose();
}, DELAY_CLOSE);
};
const stopCloseTimer = () => {
if (timerClose.current) {
clearTimeout(timerClose.current);
}
};
const startOpenTimer = () => {
timerOpen.current = setTimeout(() => setVisible(true), DELAY_OPEN);
};
const stopOpenTimer = () => {
if (timerOpen.current) {
clearTimeout(timerOpen.current);
}
};
React.useEffect(() => {
if (slug) {
documents.prefetchDocument(slug);
}
startOpenTimer();
if (cardRef.current) {
cardRef.current.addEventListener("mouseenter", stopCloseTimer);
}
if (cardRef.current) {
cardRef.current.addEventListener("mouseleave", startCloseTimer);
}
element.addEventListener("mouseout", startCloseTimer);
element.addEventListener("mouseover", stopCloseTimer);
element.addEventListener("mouseover", startOpenTimer);
return () => {
element.removeEventListener("mouseout", startCloseTimer);
element.removeEventListener("mouseover", stopCloseTimer);
element.removeEventListener("mouseover", startOpenTimer);
if (cardRef.current) {
cardRef.current.removeEventListener("mouseenter", stopCloseTimer);
}
if (cardRef.current) {
cardRef.current.removeEventListener("mouseleave", startCloseTimer);
}
if (timerClose.current) {
clearTimeout(timerClose.current);
}
};
}, [element, slug]);
const anchorBounds = element.getBoundingClientRect();
const cardBounds = cardRef.current?.getBoundingClientRect();
const left = cardBounds
? Math.min(anchorBounds.left, window.innerWidth - 16 - 350)
: anchorBounds.left;
const leftOffset = anchorBounds.left - left;
return (
<Portal>
<Position
top={anchorBounds.bottom + window.scrollY}
left={left}
aria-hidden
>
<div ref={cardRef}>
<HoverPreviewDocument url={element.href} id={id}>
{(content: React.ReactNode) =>
isVisible ? (
<Animate>
<Card>
<Margin />
<CardContent>{content}</CardContent>
</Card>
<Pointer offset={leftOffset + anchorBounds.width / 2} />
</Animate>
) : null
}
</HoverPreviewDocument>
</div>
</Position>
</Portal>
);
}
function HoverPreview({ element, ...rest }: Props) {
const isMobile = useMobile();
if (isMobile) {
return null;
}
// previews only work for internal doc links for now
if (isExternalUrl(element.href)) {
return null;
}
return <HoverPreviewInternal {...rest} element={element} />;
}
const Animate = styled.div`
animation: ${fadeAndSlideDown} 150ms ease;
@media print {
display: none;
}
`;
// fills the gap between the card and pointer to avoid a dead zone
const Margin = styled.div`
position: absolute;
top: -11px;
left: 0;
right: 0;
height: 11px;
`;
const CardContent = styled.div`
overflow: hidden;
max-height: 20em;
user-select: none;
`;
// &:after — gradient mask for overflow text
const Card = styled.div`
backdrop-filter: blur(10px);
background: ${s("background")};
border-radius: 4px;
box-shadow: 0 30px 90px -20px rgba(0, 0, 0, 0.3),
0 0 1px 1px rgba(0, 0, 0, 0.05);
padding: 16px;
width: 350px;
font-size: 0.9em;
position: relative;
.placeholder,
.heading-anchor {
display: none;
}
&:after {
content: "";
display: block;
position: absolute;
pointer-events: none;
background: linear-gradient(
90deg,
${(props) => transparentize(1, props.theme.background)} 0%,
${(props) => transparentize(1, props.theme.background)} 75%,
${s("background")} 90%
);
bottom: 0;
left: 0;
right: 0;
height: 1.7em;
border-bottom: 16px solid ${s("background")};
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
`;
const Position = styled.div<{ fixed?: boolean; top?: number; left?: number }>`
margin-top: 10px;
position: ${({ fixed }) => (fixed ? "fixed" : "absolute")};
z-index: ${depths.hoverPreview};
display: flex;
max-height: 75%;
${({ top }) => (top !== undefined ? `top: ${top}px` : "")};
${({ left }) => (left !== undefined ? `left: ${left}px` : "")};
`;
const Pointer = styled.div<{ offset: number }>`
top: -22px;
left: ${(props) => props.offset}px;
width: 22px;
height: 22px;
position: absolute;
transform: translateX(-50%);
pointer-events: none;
&:before,
&:after {
content: "";
display: inline-block;
position: absolute;
bottom: 0;
right: 0;
}
&:before {
border: 8px solid transparent;
border-bottom-color: ${(props) =>
props.theme.menuBorder || "rgba(0, 0, 0, 0.1)"};
right: -1px;
}
&:after {
border: 7px solid transparent;
border-bottom-color: ${s("background")};
}
`;
export default HoverPreview;
+108
View File
@@ -0,0 +1,108 @@
import { transparentize } from "polished";
import { Link } from "react-router-dom";
import styled, { css } from "styled-components";
import { s } from "@shared/styles";
import Text from "~/components/Text";
export const CARD_MARGIN = 16;
const NUMBER_OF_LINES = 10;
const sharedVars = css`
--line-height: 1.6em;
`;
const StyledText = styled(Text)`
margin-bottom: 0;
`;
export const Preview = styled(Link)`
cursor: ${(props: any) =>
props.as === "div" ? "default" : "var(--pointer)"};
border-radius: 4px;
box-shadow: 0 30px 90px -20px rgba(0, 0, 0, 0.3),
0 0 1px 1px rgba(0, 0, 0, 0.05);
overflow: hidden;
position: absolute;
min-width: 350px;
max-width: 375px;
`;
export const Title = styled.h2`
font-size: 1.25em;
margin: 0;
color: ${s("text")};
`;
export const Info = styled(StyledText).attrs(() => ({
type: "tertiary",
size: "xsmall",
}))`
white-space: nowrap;
`;
export const Description = styled(StyledText)`
${sharedVars}
margin-top: 0.5em;
line-height: var(--line-height);
max-height: calc(var(--line-height) * ${NUMBER_OF_LINES});
`;
export const Thumbnail = styled.img`
object-fit: cover;
height: 200px;
background: ${s("menuBackground")};
`;
export const CardContent = styled.div`
overflow: hidden;
user-select: none;
`;
// &:after — gradient mask for overflow text
export const Card = styled.div<{ fadeOut?: boolean; $borderRadius?: string }>`
backdrop-filter: blur(10px);
background: ${s("menuBackground")};
padding: 16px;
font-size: 0.9em;
position: relative;
.placeholder,
.heading-anchor {
display: none;
}
// fills the gap between the card and pointer to avoid a dead zone
&::before {
content: "";
position: absolute;
top: -10px;
left: 0;
right: 0;
height: 10px;
}
${(props) =>
props.fadeOut !== false
? `&:after {
${sharedVars}
content: "";
display: block;
position: absolute;
pointer-events: none;
background: linear-gradient(
90deg,
${transparentize(1, props.theme.menuBackground)} 0%,
${transparentize(1, props.theme.menuBackground)} 75%,
${props.theme.menuBackground} 90%
);
bottom: 0;
left: 0;
right: 0;
height: var(--line-height);
border-bottom: 16px solid ${props.theme.menuBackground};
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}`
: ""}
`;
@@ -0,0 +1,261 @@
import { m } from "framer-motion";
import * as React from "react";
import { Portal } from "react-portal";
import styled from "styled-components";
import { depths, s } from "@shared/styles";
import { UnfurlType } from "@shared/types";
import LoadingIndicator from "~/components/LoadingIndicator";
import useEventListener from "~/hooks/useEventListener";
import useKeyDown from "~/hooks/useKeyDown";
import useMobile from "~/hooks/useMobile";
import useOnClickOutside from "~/hooks/useOnClickOutside";
import useRequest from "~/hooks/useRequest";
import useStores from "~/hooks/useStores";
import { client } from "~/utils/ApiClient";
import { CARD_MARGIN } from "./Components";
import HoverPreviewDocument from "./HoverPreviewDocument";
import HoverPreviewLink from "./HoverPreviewLink";
import HoverPreviewMention from "./HoverPreviewMention";
const DELAY_OPEN = 300;
const DELAY_CLOSE = 600;
type Props = {
/* The HTML element that is being hovered over */
element: HTMLAnchorElement;
/* A callback on close of the hover preview */
onClose: () => void;
};
function HoverPreviewInternal({ element, onClose }: Props) {
const url = element.href || element.dataset.url;
const [isVisible, setVisible] = React.useState(false);
const timerClose = React.useRef<ReturnType<typeof setTimeout>>();
const timerOpen = React.useRef<ReturnType<typeof setTimeout>>();
const cardRef = React.useRef<HTMLDivElement>(null);
const stores = useStores();
const [cardLeft, setCardLeft] = React.useState(0);
const [cardTop, setCardTop] = React.useState(0);
const [pointerOffset, setPointerOffset] = React.useState(0);
React.useLayoutEffect(() => {
if (isVisible && cardRef.current) {
const elem = element.getBoundingClientRect();
const card = cardRef.current.getBoundingClientRect();
const top = elem.bottom + window.scrollY;
setCardTop(top);
let left = elem.left;
let pointerOffset = elem.width / 2;
if (left + card.width > window.innerWidth) {
// shift card leftwards by the amount it went out of screen
let shiftBy = left + card.width - window.innerWidth;
// shift a littler further to leave some margin between card and window boundary
shiftBy += CARD_MARGIN;
left -= shiftBy;
// shift pointer rightwards by same amount so as to position it back correctly
pointerOffset += shiftBy;
}
setCardLeft(left);
setPointerOffset(pointerOffset);
}
}, [isVisible, element]);
const { data, request, loading } = useRequest(
React.useCallback(
() =>
client.post("/urls.unfurl", {
url,
documentId: stores.ui.activeDocumentId,
}),
[url, stores.ui.activeDocumentId]
)
);
React.useEffect(() => {
if (url) {
stopOpenTimer();
setVisible(false);
void request();
}
}, [url, request]);
const stopOpenTimer = () => {
if (timerOpen.current) {
clearTimeout(timerOpen.current);
timerOpen.current = undefined;
}
};
const closePreview = React.useCallback(() => {
if (isVisible) {
stopOpenTimer();
setVisible(false);
onClose();
}
}, [isVisible, onClose]);
useOnClickOutside(cardRef, closePreview);
useKeyDown("Escape", closePreview);
useEventListener("scroll", closePreview, window, { capture: true });
const stopCloseTimer = () => {
if (timerClose.current) {
clearTimeout(timerClose.current);
timerClose.current = undefined;
}
};
const startOpenTimer = () => {
if (!timerOpen.current) {
timerOpen.current = setTimeout(() => setVisible(true), DELAY_OPEN);
}
};
const startCloseTimer = React.useCallback(() => {
stopOpenTimer();
timerClose.current = setTimeout(closePreview, DELAY_CLOSE);
}, [closePreview]);
React.useEffect(() => {
const card = cardRef.current;
if (data) {
startOpenTimer();
if (card) {
card.addEventListener("mouseenter", stopCloseTimer);
card.addEventListener("mouseleave", startCloseTimer);
}
element.addEventListener("mouseout", startCloseTimer);
element.addEventListener("mouseover", stopCloseTimer);
element.addEventListener("mouseover", startOpenTimer);
}
return () => {
element.removeEventListener("mouseout", startCloseTimer);
element.removeEventListener("mouseover", stopCloseTimer);
element.removeEventListener("mouseover", startOpenTimer);
if (card) {
card.removeEventListener("mouseenter", stopCloseTimer);
card.removeEventListener("mouseleave", startCloseTimer);
}
stopCloseTimer();
};
}, [element, startCloseTimer, data]);
if (loading) {
return <LoadingIndicator />;
}
if (!data) {
return null;
}
return (
<Portal>
<Position top={cardTop} left={cardLeft} aria-hidden>
{isVisible ? (
<Animate
initial={{ opacity: 0, y: -20, pointerEvents: "none" }}
animate={{ opacity: 1, y: 0, pointerEvents: "auto" }}
>
{data.type === UnfurlType.Mention ? (
<HoverPreviewMention
ref={cardRef}
url={data.thumbnailUrl}
title={data.title}
info={data.meta.info}
color={data.meta.color}
/>
) : data.type === UnfurlType.Document ? (
<HoverPreviewDocument
ref={cardRef}
id={data.meta.id}
url={data.url}
title={data.title}
description={data.description}
info={data.meta.info}
/>
) : (
<HoverPreviewLink
ref={cardRef}
url={data.url}
thumbnailUrl={data.thumbnailUrl}
title={data.title}
description={data.description}
/>
)}
<Pointer offset={pointerOffset} />
</Animate>
) : null}
</Position>
</Portal>
);
}
function HoverPreview({ element, ...rest }: Props) {
const isMobile = useMobile();
if (isMobile) {
return null;
}
return <HoverPreviewInternal {...rest} element={element} />;
}
const Animate = styled(m.div)`
@media print {
display: none;
}
`;
const Position = styled.div<{ fixed?: boolean; top?: number; left?: number }>`
margin-top: 10px;
position: ${({ fixed }) => (fixed ? "fixed" : "absolute")};
z-index: ${depths.hoverPreview};
display: flex;
max-height: 75%;
${({ top }) => (top !== undefined ? `top: ${top}px` : "")};
${({ left }) => (left !== undefined ? `left: ${left}px` : "")};
`;
const Pointer = styled.div<{ offset: number }>`
top: -22px;
left: ${(props) => props.offset}px;
width: 22px;
height: 22px;
position: absolute;
transform: translateX(-50%);
pointer-events: none;
&:before,
&:after {
content: "";
display: inline-block;
position: absolute;
bottom: 0;
right: 0;
}
&:before {
border: 8px solid transparent;
border-bottom-color: ${(props) =>
props.theme.menuBorder || "rgba(0, 0, 0, 0.1)"};
right: -1px;
}
&:after {
border: 7px solid transparent;
border-bottom-color: ${s("menuBackground")};
}
`;
export default HoverPreview;
@@ -0,0 +1,54 @@
import * as React from "react";
import Editor from "~/components/Editor";
import Flex from "~/components/Flex";
import {
Preview,
Title,
Info,
Card,
CardContent,
Description,
} from "./Components";
type Props = {
/** Document id associated with the editor, if any */
id?: string;
/** Document url */
url: string;
/** Title for the preview card */
title: string;
/** Info about last activity on the document */
info: string;
/** Text preview of document content */
description: string;
};
const HoverPreviewDocument = React.forwardRef(function _HoverPreviewDocument(
{ id, url, title, info, description }: Props,
ref: React.Ref<HTMLDivElement>
) {
return (
<Preview to={url}>
<Card ref={ref}>
<CardContent>
<Flex column gap={2}>
<Title>{title}</Title>
<Info>{info}</Info>
<Description as="div">
<React.Suspense fallback={<div />}>
<Editor
key={id}
defaultValue={description}
embedsDisabled
readOnly
/>
</React.Suspense>
</Description>
</Flex>
</CardContent>
</Card>
</Preview>
);
});
export default HoverPreviewDocument;
@@ -0,0 +1,44 @@
import * as React from "react";
import Flex from "~/components/Flex";
import {
Preview,
Title,
Description,
Card,
CardContent,
Thumbnail,
} from "./Components";
type Props = {
/** Link url */
url: string;
/** Title for the preview card */
title: string;
/** Url for thumbnail served by the link provider */
thumbnailUrl: string;
/** Some description about the link provider */
description: string;
};
const HoverPreviewLink = React.forwardRef(function _HoverPreviewLink(
{ url, thumbnailUrl, title, description }: Props,
ref: React.Ref<HTMLDivElement>
) {
return (
<Preview as="a" href={url} target="_blank" rel="noopener noreferrer">
<Flex column>
{thumbnailUrl ? <Thumbnail src={thumbnailUrl} alt={""} /> : null}
<Card ref={ref}>
<CardContent>
<Flex column>
<Title>{title}</Title>
<Description>{description}</Description>
</Flex>
</CardContent>
</Card>
</Flex>
</Preview>
);
});
export default HoverPreviewLink;
@@ -0,0 +1,46 @@
import * as React from "react";
import Avatar from "~/components/Avatar";
import { AvatarSize } from "~/components/Avatar/Avatar";
import Flex from "~/components/Flex";
import { Preview, Title, Info, Card, CardContent } from "./Components";
type Props = {
/** Resource url, avatar url in case of user mention */
url: string;
/** Title for the preview card*/
title: string;
/** Info about mentioned user's recent activity */
info: string;
/** Used for avatar's background color in absence of avatar url */
color: string;
};
const HoverPreviewMention = React.forwardRef(function _HoverPreviewMention(
{ url, title, info, color }: Props,
ref: React.Ref<HTMLDivElement>
) {
return (
<Preview as="div">
<Card fadeOut={false} ref={ref}>
<CardContent>
<Flex gap={12}>
<Avatar
model={{
avatarUrl: url,
initial: title ? title[0] : "?",
color,
}}
size={AvatarSize.XLarge}
/>
<Flex column gap={2} justify="center">
<Title>{title}</Title>
<Info>{info}</Info>
</Flex>
</Flex>
</CardContent>
</Card>
</Preview>
);
});
export default HoverPreviewMention;
+3
View File
@@ -0,0 +1,3 @@
import HoverPreview from "./HoverPreview";
export default HoverPreview;
-62
View File
@@ -1,62 +0,0 @@
import { observer } from "mobx-react";
import * as React from "react";
import { Link } from "react-router-dom";
import styled from "styled-components";
import { s } from "@shared/styles";
import parseDocumentSlug from "@shared/utils/parseDocumentSlug";
import DocumentMeta from "~/components/DocumentMeta";
import Editor from "~/components/Editor";
import useStores from "~/hooks/useStores";
type Props = {
/* The document associated with the editor, if any */
id?: string;
/* The URL we want a preview for */
url: string;
children: (content: React.ReactNode) => React.ReactNode;
};
function HoverPreviewDocument({ url, id, children }: Props) {
const { documents } = useStores();
const slug = parseDocumentSlug(url);
if (slug) {
documents.prefetchDocument(slug);
}
const document = slug ? documents.getByUrl(slug) : undefined;
if (!document || document.id === id) {
return null;
}
return (
<>
{children(
<Content to={document.url}>
<Heading>{document.titleWithDefault}</Heading>
<DocumentMeta document={document} />
<React.Suspense fallback={<div />}>
<Editor
key={document.id}
defaultValue={document.getSummary()}
embedsDisabled
readOnly
/>
</React.Suspense>
</Content>
)}
</>
);
}
const Content = styled(Link)`
cursor: var(--pointer);
`;
const Heading = styled.h2`
margin: 0 0 0.75em;
color: ${s("text")};
`;
export default observer(HoverPreviewDocument);
+2 -1
View File
@@ -47,6 +47,7 @@ import Flex from "~/components/Flex";
import { LabelText } from "~/components/Input";
import NudeButton from "~/components/NudeButton";
import Text from "~/components/Text";
import lazyWithRetry from "~/utils/lazyWithRetry";
import DelayedMount from "./DelayedMount";
const style = {
@@ -54,7 +55,7 @@ const style = {
height: 30,
};
const TwitterPicker = React.lazy(
const TwitterPicker = lazyWithRetry(
() => import("react-color/lib/components/twitter/Twitter")
);
+1 -1
View File
@@ -24,7 +24,7 @@ export default function MarkdownIcon({
<path
d="M19.2692 7H3.86538C3.38745 7 3 7.38476 3 7.85938V16.2812C3 16.7559 3.38745 17.1406 3.86538 17.1406H19.2692C19.7472 17.1406 20.1346 16.7559 20.1346 16.2812V7.85938C20.1346 7.38476 19.7472 7 19.2692 7Z"
stroke={color}
stroke-width="2"
strokeWidth="2"
/>
<path
d="M5.16345 14.9922V9.14844H6.89422L8.62499 11.2969L10.3558 9.14844H12.0865V14.9922H10.3558V11.6406L8.62499 13.7891L6.89422 11.6406V14.9922H5.16345ZM15.9808 14.9922L13.3846 12.1562H15.1154V9.14844H16.8461V12.1562H18.5769L15.9808 14.9922Z"
+5
View File
@@ -30,6 +30,8 @@ const RealInput = styled.input<{ hasIcon?: boolean }>`
color: ${s("text")};
height: 30px;
min-width: 0;
font-size: 15px;
${ellipsis()}
${undraggableOnDesktop()}
@@ -175,6 +177,8 @@ function Input(
labelHidden,
onFocus,
onBlur,
onRequestSubmit,
children,
...rest
} = props;
@@ -211,6 +215,7 @@ function Input(
{...rest}
/>
)}
{children}
</Outline>
</label>
{error && (
+4 -3
View File
@@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next";
import { MenuButton, useMenuState } from "reakit/Menu";
import styled from "styled-components";
import { s } from "@shared/styles";
import lazyWithRetry from "~/utils/lazyWithRetry";
import ContextMenu from "./ContextMenu";
import DelayedMount from "./DelayedMount";
import Input, { Props as InputProps } from "./Input";
@@ -15,7 +16,7 @@ type Props = Omit<InputProps, "onChange"> & {
onChange: (value: string) => void;
};
const InputColor: React.FC<Props> = ({ value, onChange, ...rest }) => {
const InputColor: React.FC<Props> = ({ value, onChange, ...rest }: Props) => {
const { t } = useTranslation();
const menu = useMenuState({
modal: true,
@@ -26,7 +27,7 @@ const InputColor: React.FC<Props> = ({ value, onChange, ...rest }) => {
<Relative>
<Input
value={value}
onChange={(event) => onChange(event.target.value)}
onChange={(event) => onChange(event.target.value.replace(/^#?/, "#"))}
placeholder="#"
maxLength={7}
{...rest}
@@ -68,7 +69,7 @@ const SwatchButton = styled(NudeButton)<{ $background: string | undefined }>`
right: 6px;
`;
const ColorPicker = React.lazy(
const ColorPicker = lazyWithRetry(
() => import("react-color/lib/components/chrome/Chrome")
);
+4
View File
@@ -45,6 +45,10 @@ function InputSearchPage({
const handleKeyDown = React.useCallback(
(ev: React.KeyboardEvent<HTMLInputElement>) => {
if (ev.nativeEvent.isComposing) {
return;
}
if (ev.key === "Enter") {
ev.preventDefault();
history.push(
+6 -10
View File
@@ -85,11 +85,11 @@ const InputSelect = (props: Props) => {
const contentRef = React.useRef<HTMLDivElement>(null);
const minWidth = buttonRef.current?.offsetWidth || 0;
const margin = 8;
const menuMaxHeight = useMenuHeight(
select.visible,
select.unstable_disclosureRef,
margin
);
const menuMaxHeight = useMenuHeight({
visible: select.visible,
elementRef: select.unstable_disclosureRef,
margin,
});
const maxHeight = Math.min(
menuMaxHeight ?? 0,
window.innerHeight -
@@ -108,11 +108,7 @@ const InputSelect = (props: Props) => {
}
previousValue.current = select.selectedValue;
async function load() {
await onChange?.(select.selectedValue);
}
load();
onChange?.(select.selectedValue);
}, [onChange, select.selectedValue]);
React.useLayoutEffect(() => {
+2 -1
View File
@@ -5,10 +5,11 @@ import { s } from "@shared/styles";
import Flex from "~/components/Flex";
type Props = {
children?: React.ReactNode;
label: React.ReactNode | string;
};
const Labeled: React.FC<Props> = ({ label, children, ...props }) => (
const Labeled: React.FC<Props> = ({ label, children, ...props }: Props) => (
<Flex column {...props}>
<Label>{label}</Label>
{children}
+7 -7
View File
@@ -21,14 +21,14 @@ function Icon({ className }: { className?: string }) {
className={className}
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
fillRule="evenodd"
clipRule="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"
fillRule="evenodd"
clipRule="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"
/>
@@ -73,11 +73,11 @@ export default function LanguagePrompt() {
</Trans>
<br />
<Link
onClick={() => {
auth.updateUser({
onClick={async () => {
ui.setLanguagePromptDismissed();
await auth.updateUser({
language,
});
ui.setLanguagePromptDismissed();
}}
>
{t("Change Language")}
+2 -1
View File
@@ -16,6 +16,7 @@ import useStores from "~/hooks/useStores";
import { isModKey } from "~/utils/keyboard";
type Props = {
children?: React.ReactNode;
title?: string;
sidebar?: React.ReactNode;
sidebarRight?: React.ReactNode;
@@ -26,7 +27,7 @@ const Layout: React.FC<Props> = ({
children,
sidebar,
sidebarRight,
}) => {
}: Props) => {
const { ui } = useStores();
const sidebarCollapsed = !sidebar || ui.sidebarIsClosed;
+13 -4
View File
@@ -1,16 +1,25 @@
import * as React from "react";
import Logger from "~/utils/Logger";
import { loadPolyfills } from "~/utils/polyfills";
type Props = {
children?: React.ReactNode;
};
/**
* Asyncronously load required polyfills. Should wrap the React tree.
*/
export const LazyPolyfill: React.FC = ({ children }) => {
export const LazyPolyfill: React.FC = ({ children }: Props) => {
const [isLoaded, setIsLoaded] = React.useState(false);
React.useEffect(() => {
loadPolyfills().then(() => {
setIsLoaded(true);
});
loadPolyfills()
.then(() => {
setIsLoaded(true);
})
.catch((error) => {
Logger.error("Polyfills failed to load", error);
});
}, []);
if (!isLoaded) {
+9 -13
View File
@@ -1,8 +1,8 @@
import { format as formatDate, formatDistanceToNow } from "date-fns";
import { format as formatDate } from "date-fns";
import * as React from "react";
import { dateLocale, dateToRelative, locales } from "@shared/utils/date";
import Tooltip from "~/components/Tooltip";
import useUserLocale from "~/hooks/useUserLocale";
import { dateLocale, locales } from "~/utils/i18n";
let callbacks: (() => void)[] = [];
@@ -21,6 +21,7 @@ function eachMinute(fn: () => void) {
}
type Props = {
children?: React.ReactNode;
dateTime: string;
tooltipDelay?: number;
addSuffix?: boolean;
@@ -37,7 +38,7 @@ const LocaleTime: React.FC<Props> = ({
format,
relative,
tooltipDelay,
}) => {
}: Props) => {
const userLocale: string = useUserLocale() || "";
const dateFormatLong = {
en_US: "MMMM do, yyyy h:mm a",
@@ -59,26 +60,21 @@ const LocaleTime: React.FC<Props> = ({
};
}, []);
const date = new Date(Date.parse(dateTime));
const locale = dateLocale(userLocale);
let relativeContent = formatDistanceToNow(Date.parse(dateTime), {
const relativeContent = dateToRelative(date, {
addSuffix,
locale,
shorten,
});
if (shorten) {
relativeContent = relativeContent
.replace("about", "")
.replace("less than a minute ago", "just now")
.replace("minute", "min");
}
const tooltipContent = formatDate(Date.parse(dateTime), formatLocaleLong, {
const tooltipContent = formatDate(date, formatLocaleLong, {
locale,
});
const content =
relative !== false
? relativeContent
: formatDate(Date.parse(dateTime), formatLocale, {
: formatDate(date, formatLocale, {
locale,
});
+3 -1
View File
@@ -19,7 +19,9 @@ import Desktop from "~/utils/Desktop";
import ErrorBoundary from "./ErrorBoundary";
let openModals = 0;
type Props = {
children?: React.ReactNode;
isOpen: boolean;
isCentered?: boolean;
title?: React.ReactNode;
@@ -32,7 +34,7 @@ const Modal: React.FC<Props> = ({
isCentered,
title = "Untitled",
onRequestClose,
}) => {
}: Props) => {
const dialog = useDialogState({
animated: 250,
});
+2 -1
View File
@@ -5,11 +5,12 @@ import Flex from "./Flex";
import Text from "./Text";
type Props = {
children?: React.ReactNode;
icon?: JSX.Element;
description?: JSX.Element;
};
const Notice: React.FC<Props> = ({ children, icon, description }) => (
const Notice: React.FC<Props> = ({ children, icon, description }: Props) => (
<Container>
<Flex as="span" gap={8}>
{icon}
@@ -0,0 +1,32 @@
import { observer } from "mobx-react";
import { SubscribeIcon } from "outline-icons";
import * as React from "react";
import styled, { useTheme } from "styled-components";
import { s } from "@shared/styles";
import useStores from "~/hooks/useStores";
import Relative from "../Sidebar/components/Relative";
const NotificationIcon = () => {
const { notifications } = useStores();
const theme = useTheme();
const count = notifications.approximateUnreadCount;
return (
<Relative style={{ height: 24 }}>
<SubscribeIcon color={theme.textTertiary} />
{count > 0 && <Badge />}
</Relative>
);
};
const Badge = styled.div`
position: absolute;
width: 8px;
height: 8px;
border-radius: 50%;
background: ${s("accent")};
top: 0;
right: 0;
`;
export default observer(NotificationIcon);
@@ -0,0 +1,110 @@
import { toJS } from "mobx";
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 { s } from "@shared/styles";
import Notification from "~/models/Notification";
import CommentEditor from "~/scenes/Document/components/CommentEditor";
import useStores from "~/hooks/useStores";
import { hover, truncateMultiline } from "~/styles";
import Avatar from "../Avatar";
import { AvatarSize } from "../Avatar/Avatar";
import Flex from "../Flex";
import Text from "../Text";
import Time from "../Time";
type Props = {
notification: Notification;
onNavigate: () => void;
};
function NotificationListItem({ notification, onNavigate }: Props) {
const { t } = useTranslation();
const { collections } = useStores();
const collectionId = notification.document?.collectionId;
const collection = collectionId ? collections.get(collectionId) : undefined;
const handleClick: React.MouseEventHandler<HTMLAnchorElement> = (event) => {
if (event.altKey) {
event.preventDefault();
event.stopPropagation();
void notification.toggleRead();
return;
}
void notification.markAsRead();
onNavigate();
};
return (
<Link to={notification.path} onClick={handleClick}>
<Container gap={8} $unread={!notification.viewedAt}>
<StyledAvatar model={notification.actor} size={AvatarSize.Large} />
<Flex column>
<Text as="div" size="small">
<Text as="span" weight="bold">
{notification.actor?.name ?? t("Unknown")}
</Text>{" "}
{notification.eventText(t)}{" "}
<Text as="span" weight="bold">
{notification.subject}
</Text>
</Text>
<Text as="span" type="tertiary" size="xsmall">
<Time
dateTime={notification.createdAt}
tooltipDelay={1000}
addSuffix
/>{" "}
{collection && <>&middot; {collection.name}</>}
</Text>
{notification.comment && (
<StyledCommentEditor
defaultValue={toJS(notification.comment.data)}
/>
)}
</Flex>
{notification.viewedAt ? null : <Unread />}
</Container>
</Link>
);
}
const StyledCommentEditor = styled(CommentEditor)`
font-size: 0.9em;
margin-top: 4px;
${truncateMultiline(3)}
`;
const StyledAvatar = styled(Avatar)`
margin-top: 4px;
`;
const Container = styled(Flex)<{ $unread: boolean }>`
position: relative;
padding: 8px 12px;
margin: 0 8px;
border-radius: 4px;
&:${hover},
&:active {
background: ${s("listItemHoverBackground")};
cursor: var(--pointer);
}
`;
const Unread = styled.div`
width: 8px;
height: 8px;
background: ${s("accent")};
border-radius: 8px;
align-self: center;
position: absolute;
right: 20px;
`;
export default observer(NotificationListItem);
@@ -0,0 +1,131 @@
import { observer } from "mobx-react";
import { MarkAsReadIcon, SettingsIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { s } from "@shared/styles";
import Notification from "~/models/Notification";
import { navigateToNotificationSettings } from "~/actions/definitions/navigation";
import { markNotificationsAsRead } from "~/actions/definitions/notifications";
import useActionContext from "~/hooks/useActionContext";
import useStores from "~/hooks/useStores";
import { hover } from "~/styles";
import Desktop from "~/utils/Desktop";
import Empty from "../Empty";
import ErrorBoundary from "../ErrorBoundary";
import Flex from "../Flex";
import NudeButton from "../NudeButton";
import PaginatedList from "../PaginatedList";
import Scrollable from "../Scrollable";
import Text from "../Text";
import Tooltip from "../Tooltip";
import NotificationListItem from "./NotificationListItem";
type Props = {
/* Callback when the notification panel wants to close. */
onRequestClose: () => void;
};
/**
* A panel containing a list of notifications and controls to manage them.
*/
function Notifications(
{ onRequestClose }: Props,
ref: React.RefObject<HTMLDivElement>
) {
const context = useActionContext();
const { notifications } = useStores();
const { t } = useTranslation();
const isEmpty = notifications.orderedData.length === 0;
// Update the notification count in the dock icon, if possible.
React.useEffect(() => {
// Account for old versions of the desktop app that don't have the
// setNotificationCount method on the bridge.
if (Desktop.bridge && "setNotificationCount" in Desktop.bridge) {
void Desktop.bridge.setNotificationCount(
notifications.approximateUnreadCount
);
}
}, [notifications.approximateUnreadCount]);
return (
<ErrorBoundary>
<Flex style={{ width: "100%" }} column>
<Header justify="space-between">
<Text weight="bold" as="span">
{t("Notifications")}
</Text>
<Text color="textSecondary" as={Flex} gap={8}>
{notifications.approximateUnreadCount > 0 && (
<Tooltip delay={500} tooltip={t("Mark all as read")}>
<Button action={markNotificationsAsRead} context={context}>
<MarkAsReadIcon />
</Button>
</Tooltip>
)}
<Tooltip delay={500} tooltip={t("Settings")}>
<Button action={navigateToNotificationSettings} context={context}>
<SettingsIcon />
</Button>
</Tooltip>
</Text>
</Header>
<React.Suspense fallback={null}>
<Scrollable ref={ref} flex topShadow>
<PaginatedList
fetch={notifications.fetchPage}
items={notifications.orderedData}
renderItem={(item: Notification) => (
<NotificationListItem
key={item.id}
notification={item}
onNavigate={onRequestClose}
/>
)}
/>
</Scrollable>
</React.Suspense>
{isEmpty && (
<EmptyNotifications>{t("You're all caught up")}.</EmptyNotifications>
)}
</Flex>
</ErrorBoundary>
);
}
const EmptyNotifications = styled(Empty)`
display: flex;
align-items: center;
justify-content: center;
min-height: 200px;
`;
const Button = styled(NudeButton)`
color: ${s("textSecondary")};
&:${hover},
&:active {
color: ${s("text")};
background: ${s("sidebarControlHoverBackground")};
}
`;
const Header = styled(Flex)`
padding: 8px 12px 12px;
height: 44px;
${Button} {
opacity: 0.75;
transition: opacity 250ms ease-in-out;
}
&:${hover},
&:focus-within {
${Button} {
opacity: 1;
}
}
`;
export default observer(React.forwardRef(Notifications));
@@ -0,0 +1,53 @@
import { observer } 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 { depths } from "@shared/styles";
import Popover from "~/components/Popover";
import Notifications from "./Notifications";
type Props = {
children?: React.ReactNode;
};
const NotificationsPopover: React.FC = ({ children }: Props) => {
const { t } = useTranslation();
const scrollableRef = React.useRef<HTMLDivElement>(null);
const popover = usePopoverState({
gutter: 0,
placement: "top-start",
unstable_fixed: true,
});
// Reset scroll position to the top when popover is opened
React.useEffect(() => {
if (popover.visible && scrollableRef.current) {
scrollableRef.current.scrollTop = 0;
}
}, [popover.visible]);
return (
<>
<PopoverDisclosure {...popover}>{children}</PopoverDisclosure>
<StyledPopover
{...popover}
scrollable={false}
mobilePosition="bottom"
aria-label={t("Notifications")}
unstable_initialFocusRef={scrollableRef}
shrink
flex
>
<Notifications onRequestClose={popover.hide} ref={scrollableRef} />
</StyledPopover>
</>
);
};
const StyledPopover = styled(Popover)`
z-index: ${depths.menu};
`;
export default observer(NotificationsPopover);
+12 -13
View File
@@ -1,35 +1,34 @@
import { observer } from "mobx-react";
import * as React from "react";
import { Helmet } from "react-helmet-async";
import { cdnPath } from "@shared/utils/urls";
import env from "~/env";
import useStores from "~/hooks/useStores";
import { useTeamContext } from "./TeamContext";
type Props = {
title: React.ReactNode;
favicon?: string;
};
const originalShortcutHref = document
.querySelector('link[rel="shortcut icon"]')
?.getAttribute("href") as string;
const PageTitle = ({ title, favicon }: Props) => {
const { auth } = useStores();
const { team } = auth;
const team = useTeamContext() ?? auth.team;
return (
<Helmet>
<title>
{team?.name ? `${title} - ${team.name}` : `${title} - ${env.APP_NAME}`}
</title>
{favicon ? (
<link rel="shortcut icon" href={favicon} key={favicon} />
) : (
<link
rel="shortcut icon"
type="image/png"
key="favicon"
href={cdnPath("/images/favicon-32.png")}
sizes="32x32"
/>
)}
<link
rel="shortcut icon"
type="image/png"
href={favicon ?? originalShortcutHref}
key={favicon ?? originalShortcutHref}
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</Helmet>
);
+2 -2
View File
@@ -70,7 +70,7 @@ class PaginatedList<T extends PaginatedItem> extends React.Component<Props<T>> {
allowLoadMore = true;
componentDidMount() {
this.fetchResults();
void this.fetchResults();
}
componentDidUpdate(prevProps: Props<T>) {
@@ -79,7 +79,7 @@ class PaginatedList<T extends PaginatedItem> extends React.Component<Props<T>> {
!isEqual(prevProps.options, this.props.options)
) {
this.reset();
this.fetchResults();
void this.fetchResults();
}
}
+30 -20
View File
@@ -23,6 +23,7 @@ import breakpoint from "styled-components-breakpoint";
import Pin from "~/models/Pin";
import DocumentCard from "~/components/DocumentCard";
import useStores from "~/hooks/useStores";
import { ResizingHeightContainer } from "./ResizingHeightContainer";
type Props = {
/** Pins to display */
@@ -98,27 +99,36 @@ function PinnedDocuments({ limit, pins, canUpdate, ...rest }: Props) {
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext items={items} strategy={rectSortingStrategy}>
<List>
<AnimatePresence initial={false}>
{items.map((documentId) => {
const document = documents.get(documentId);
const pin = pins.find((pin) => pin.documentId === documentId);
<ResizingHeightContainer
config={{
transition: {
duration: 0.2,
ease: "easeInOut",
},
}}
>
<SortableContext items={items} strategy={rectSortingStrategy}>
<List>
<AnimatePresence initial={false}>
{items.map((documentId) => {
const document = documents.get(documentId);
const pin = pins.find((pin) => pin.documentId === documentId);
return document ? (
<DocumentCard
key={documentId}
document={document}
canUpdatePin={canUpdate}
isDraggable={items.length > 1}
pin={pin}
{...rest}
/>
) : null;
})}
</AnimatePresence>
</List>
</SortableContext>
return document ? (
<DocumentCard
key={documentId}
document={document}
canUpdatePin={canUpdate}
isDraggable={items.length > 1}
pin={pin}
{...rest}
/>
) : null;
})}
</AnimatePresence>
</List>
</SortableContext>
</ResizingHeightContainer>
</DndContext>
);
}
+43 -8
View File
@@ -1,7 +1,7 @@
import * as React from "react";
import { Dialog } from "reakit/Dialog";
import { Popover as ReakitPopover, PopoverProps } from "reakit/Popover";
import styled from "styled-components";
import styled, { css } from "styled-components";
import breakpoint from "styled-components-breakpoint";
import { depths, s } from "@shared/styles";
import useMobile from "~/hooks/useMobile";
@@ -11,52 +11,87 @@ type Props = PopoverProps & {
children: React.ReactNode;
width?: number;
shrink?: boolean;
flex?: boolean;
tabIndex?: number;
scrollable?: boolean;
mobilePosition?: "top" | "bottom";
};
const Popover: React.FC<Props> = ({
children,
shrink,
width = 380,
scrollable = true,
flex,
mobilePosition,
...rest
}) => {
}: Props) => {
const isMobile = useMobile();
if (isMobile) {
return (
<Dialog {...rest} modal>
<Contents $shrink={shrink}>{children}</Contents>
<Contents
$shrink={shrink}
$scrollable={scrollable}
$flex={flex}
$mobilePosition={mobilePosition}
>
{children}
</Contents>
</Dialog>
);
}
return (
<ReakitPopover {...rest}>
<Contents $shrink={shrink} $width={width}>
<Contents
$shrink={shrink}
$width={width}
$scrollable={scrollable}
$flex={flex}
>
{children}
</Contents>
</ReakitPopover>
);
};
const Contents = styled.div<{ $shrink?: boolean; $width?: number }>`
type ContentsProps = {
$shrink?: boolean;
$width?: number;
$flex?: boolean;
$scrollable: boolean;
$mobilePosition?: "top" | "bottom";
};
const Contents = styled.div<ContentsProps>`
display: ${(props) => (props.$flex ? "flex" : "block")};
animation: ${fadeAndScaleIn} 200ms ease;
transform-origin: 75% 0;
background: ${s("menuBackground")};
border-radius: 6px;
padding: ${(props) => (props.$shrink ? "6px 0" : "12px 24px")};
max-height: 75vh;
overflow-x: hidden;
overflow-y: auto;
box-shadow: ${s("menuShadow")};
width: ${(props) => props.$width}px;
${(props) =>
props.$scrollable &&
css`
overflow-x: hidden;
overflow-y: auto;
`}
${breakpoint("mobile", "tablet")`
position: fixed;
z-index: ${depths.menu};
// 50 is a magic number that positions us nicely under the top bar
top: 50px;
top: ${(props: ContentsProps) =>
props.$mobilePosition === "bottom" ? "auto" : "50px"};
bottom: ${(props: ContentsProps) =>
props.$mobilePosition === "bottom" ? "0" : "auto"};
left: 8px;
right: 8px;
width: auto;
+2 -1
View File
@@ -11,6 +11,7 @@ type Props = {
left?: React.ReactNode;
actions?: React.ReactNode;
centered?: boolean;
children?: React.ReactNode;
};
const Scene: React.FC<Props> = ({
@@ -21,7 +22,7 @@ const Scene: React.FC<Props> = ({
left,
children,
centered,
}) => (
}: Props) => (
<FillWidth>
<PageTitle title={textTitle || title} />
<Header
+3 -2
View File
@@ -11,7 +11,7 @@ export default function SearchActions() {
React.useEffect(() => {
if (!searches.isLoaded) {
searches.fetchPage({});
void searches.fetchPage({});
}
}, [searches]);
@@ -20,7 +20,8 @@ export default function SearchActions() {
}));
useCommandBarActions(
searchQuery ? [searchDocumentsForQuery(searchQuery)] : []
searchQuery ? [searchDocumentsForQuery(searchQuery)] : [],
[searchQuery]
);
useCommandBarActions(searches.recent.map(navigateToRecentSearchQuery));
+13 -9
View File
@@ -17,7 +17,7 @@ import useStores from "~/hooks/useStores";
import { SearchResult } from "~/types";
import SearchListItem from "./SearchListItem";
type Props = { shareId: string };
type Props = React.HTMLAttributes<HTMLInputElement> & { shareId: string };
function SearchPopover({ shareId }: Props) {
const { t } = useTranslation();
@@ -32,6 +32,7 @@ function SearchPopover({ shareId }: Props) {
const [query, setQuery] = React.useState("");
const searchResults = documents.searchResults(query);
const { show, hide } = popover;
const [cachedQuery, setCachedQuery] = React.useState(query);
const [cachedSearchResults, setCachedSearchResults] = React.useState<
@@ -42,9 +43,9 @@ function SearchPopover({ shareId }: Props) {
if (searchResults) {
setCachedQuery(query);
setCachedSearchResults(searchResults);
popover.show();
show();
}
}, [searchResults, query, popover.show]);
}, [searchResults, query, show]);
const performSearch = React.useCallback(
async ({ query, ...options }) => {
@@ -76,9 +77,8 @@ function SearchPopover({ shareId }: Props) {
[popover, cachedQuery]
);
const searchInputRef = popover.unstable_referenceRef as React.RefObject<
HTMLInputElement
>;
const searchInputRef =
popover.unstable_referenceRef as React.RefObject<HTMLInputElement>;
const firstSearchItem = React.useRef<HTMLAnchorElement>(null);
@@ -89,10 +89,14 @@ function SearchPopover({ shareId }: Props) {
const handleSearchInputFocus = React.useCallback(() => {
focusRef.current = searchInputRef.current;
}, []);
}, [searchInputRef]);
const handleKeyDown = React.useCallback(
(ev: React.KeyboardEvent<HTMLInputElement>) => {
if (ev.nativeEvent.isComposing) {
return;
}
if (ev.key === "Enter") {
if (searchResults) {
popover.show();
@@ -138,12 +142,12 @@ function SearchPopover({ shareId }: Props) {
);
const handleSearchItemClick = React.useCallback(() => {
popover.hide();
hide();
if (searchInputRef.current) {
searchInputRef.current.value = "";
focusRef.current = document.getElementById(bodyContentId);
}
}, [popover.hide]);
}, [searchInputRef, hide]);
useKeyDown("/", (ev) => {
if (
+2 -2
View File
@@ -43,8 +43,8 @@ function AppSidebar() {
React.useEffect(() => {
if (!user.isViewer) {
documents.fetchDrafts();
documents.fetchTemplates();
void documents.fetchDrafts();
void documents.fetchTemplates();
}
}, [documents, user.isViewer]);
+3 -3
View File
@@ -8,7 +8,7 @@ import SearchPopover from "~/components/SearchPopover";
import useStores from "~/hooks/useStores";
import history from "~/utils/history";
import { homePath, sharedDocumentPath } from "~/utils/routeHelpers";
import { IAvatar } from "../Avatar/Avatar";
import { useTeamContext } from "../TeamContext";
import TeamLogo from "../TeamLogo";
import Sidebar from "./Sidebar";
import HeaderButton from "./components/HeaderButton";
@@ -16,12 +16,12 @@ import Section from "./components/Section";
import DocumentLink from "./components/SharedDocumentLink";
type Props = {
team?: IAvatar & { name: string };
rootNode: NavigationNode;
shareId: string;
};
function SharedSidebar({ rootNode, team, shareId }: Props) {
function SharedSidebar({ rootNode, shareId }: Props) {
const team = useTeamContext();
const { ui, documents, auth } = useStores();
const { t } = useTranslation();
+174 -165
View File
@@ -15,6 +15,8 @@ import { draggableOnDesktop, fadeOnDesktopBackgrounded } from "~/styles";
import { fadeIn } from "~/styles/animations";
import Desktop from "~/utils/Desktop";
import Avatar from "../Avatar";
import NotificationIcon from "../Notifications/NotificationIcon";
import NotificationsPopover from "../Notifications/NotificationsPopover";
import HeaderButton, { HeaderButtonProps } from "./components/HeaderButton";
import ResizeBorder from "./components/ResizeBorder";
import Toggle, { ToggleButton, Positioner } from "./components/Toggle";
@@ -25,191 +27,198 @@ type Props = {
children: React.ReactNode;
};
const Sidebar = React.forwardRef<HTMLDivElement, Props>(
({ children }: Props, ref: React.RefObject<HTMLDivElement>) => {
const [isCollapsing, setCollapsing] = React.useState(false);
const theme = useTheme();
const { t } = useTranslation();
const { ui, auth } = useStores();
const location = useLocation();
const previousLocation = usePrevious(location);
const { isMenuOpen } = useMenuContext();
const { user } = auth;
const width = ui.sidebarWidth;
const collapsed = ui.sidebarIsClosed && !isMenuOpen;
const maxWidth = theme.sidebarMaxWidth;
const minWidth = theme.sidebarMinWidth + 16; // padding
const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
{ children }: Props,
ref: React.RefObject<HTMLDivElement>
) {
const [isCollapsing, setCollapsing] = React.useState(false);
const theme = useTheme();
const { t } = useTranslation();
const { ui, auth } = useStores();
const location = useLocation();
const previousLocation = usePrevious(location);
const { isMenuOpen } = useMenuContext();
const { user } = auth;
const width = ui.sidebarWidth;
const collapsed = ui.sidebarIsClosed && !isMenuOpen;
const maxWidth = theme.sidebarMaxWidth;
const minWidth = theme.sidebarMinWidth + 16; // padding
const setWidth = ui.setSidebarWidth;
const [offset, setOffset] = React.useState(0);
const [isAnimating, setAnimating] = React.useState(false);
const [isResizing, setResizing] = React.useState(false);
const isSmallerThanMinimum = width < minWidth;
const setWidth = ui.setSidebarWidth;
const [offset, setOffset] = React.useState(0);
const [isAnimating, setAnimating] = React.useState(false);
const [isResizing, setResizing] = React.useState(false);
const isSmallerThanMinimum = width < minWidth;
const handleDrag = React.useCallback(
(event: MouseEvent) => {
// suppresses text selection
event.preventDefault();
// this is simple because the sidebar is always against the left edge
const width = Math.min(event.pageX - offset, maxWidth);
const isSmallerThanCollapsePoint = width < minWidth / 2;
const handleDrag = React.useCallback(
(event: MouseEvent) => {
// suppresses text selection
event.preventDefault();
// this is simple because the sidebar is always against the left edge
const width = Math.min(event.pageX - offset, maxWidth);
const isSmallerThanCollapsePoint = width < minWidth / 2;
if (isSmallerThanCollapsePoint) {
setWidth(theme.sidebarCollapsedWidth);
} else {
setWidth(width);
}
},
[theme, offset, minWidth, maxWidth, setWidth]
);
const handleStopDrag = React.useCallback(() => {
setResizing(false);
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
if (isSmallerThanMinimum) {
const isSmallerThanCollapsePoint = width < minWidth / 2;
if (isSmallerThanCollapsePoint) {
setAnimating(false);
setCollapsing(true);
ui.collapseSidebar();
} else {
setWidth(minWidth);
setAnimating(true);
}
if (isSmallerThanCollapsePoint) {
setWidth(theme.sidebarCollapsedWidth);
} else {
setWidth(width);
}
}, [ui, isSmallerThanMinimum, minWidth, width, setWidth]);
},
[theme, offset, minWidth, maxWidth, setWidth]
);
const handleMouseDown = React.useCallback(
(event) => {
setOffset(event.pageX - width);
setResizing(true);
const handleStopDrag = React.useCallback(() => {
setResizing(false);
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
if (isSmallerThanMinimum) {
const isSmallerThanCollapsePoint = width < minWidth / 2;
if (isSmallerThanCollapsePoint) {
setAnimating(false);
},
[width]
);
React.useEffect(() => {
if (isAnimating) {
setTimeout(() => setAnimating(false), ANIMATION_MS);
setCollapsing(true);
ui.collapseSidebar();
} else {
setWidth(minWidth);
setAnimating(true);
}
}, [isAnimating]);
} else {
setWidth(width);
}
}, [ui, isSmallerThanMinimum, minWidth, width, setWidth]);
React.useEffect(() => {
if (isCollapsing) {
setTimeout(() => {
setWidth(minWidth);
setCollapsing(false);
}, ANIMATION_MS);
}
}, [setWidth, minWidth, isCollapsing]);
const handleMouseDown = React.useCallback(
(event) => {
setOffset(event.pageX - width);
setResizing(true);
setAnimating(false);
},
[width]
);
React.useEffect(() => {
if (isResizing) {
document.addEventListener("mousemove", handleDrag);
document.addEventListener("mouseup", handleStopDrag);
}
React.useEffect(() => {
if (isAnimating) {
setTimeout(() => setAnimating(false), ANIMATION_MS);
}
}, [isAnimating]);
return () => {
document.removeEventListener("mousemove", handleDrag);
document.removeEventListener("mouseup", handleStopDrag);
};
}, [isResizing, handleDrag, handleStopDrag]);
React.useEffect(() => {
if (isCollapsing) {
setTimeout(() => {
setWidth(minWidth);
setCollapsing(false);
}, ANIMATION_MS);
}
}, [setWidth, minWidth, isCollapsing]);
const handleReset = React.useCallback(() => {
ui.setSidebarWidth(theme.sidebarWidth);
}, [ui, theme.sidebarWidth]);
React.useEffect(() => {
if (isResizing) {
document.addEventListener("mousemove", handleDrag);
document.addEventListener("mouseup", handleStopDrag);
}
React.useEffect(() => {
ui.setSidebarResizing(isResizing);
}, [ui, isResizing]);
return () => {
document.removeEventListener("mousemove", handleDrag);
document.removeEventListener("mouseup", handleStopDrag);
};
}, [isResizing, handleDrag, handleStopDrag]);
React.useEffect(() => {
if (location !== previousLocation) {
ui.hideMobileSidebar();
}
}, [ui, location, previousLocation]);
const handleReset = React.useCallback(() => {
ui.setSidebarWidth(theme.sidebarWidth);
}, [ui, theme.sidebarWidth]);
const style = React.useMemo(
() => ({
width: `${width}px`,
}),
[width]
);
React.useEffect(() => {
ui.setSidebarResizing(isResizing);
}, [ui, isResizing]);
const toggleStyle = React.useMemo(
() => ({
right: "auto",
marginLeft: `${collapsed ? theme.sidebarCollapsedWidth : width}px`,
}),
[width, theme.sidebarCollapsedWidth, collapsed]
);
React.useEffect(() => {
if (location !== previousLocation) {
ui.hideMobileSidebar();
}
}, [ui, location, previousLocation]);
return (
<>
<Container
ref={ref}
style={style}
$isAnimating={isAnimating}
$isSmallerThanMinimum={isSmallerThanMinimum}
$mobileSidebarVisible={ui.mobileSidebarVisible}
$collapsed={collapsed}
column
>
{ui.mobileSidebarVisible && (
<Portal>
<Backdrop onClick={ui.toggleMobileSidebar} />
</Portal>
)}
{children}
const style = React.useMemo(
() => ({
width: `${width}px`,
}),
[width]
);
{user && (
<AccountMenu>
{(props: HeaderButtonProps) => (
<HeaderButton
{...props}
showMoreMenu
title={user.name}
image={
<StyledAvatar
alt={user.name}
model={user}
size={24}
showBorder={false}
/>
}
/>
)}
</AccountMenu>
)}
<ResizeBorder
onMouseDown={handleMouseDown}
onDoubleClick={ui.sidebarIsClosed ? undefined : handleReset}
/>
{ui.sidebarIsClosed && (
<Toggle
onClick={ui.toggleCollapsedSidebar}
direction={"right"}
aria-label={t("Expand")}
/>
)}
</Container>
<Toggle
style={toggleStyle}
onClick={ui.toggleCollapsedSidebar}
direction={ui.sidebarIsClosed ? "right" : "left"}
aria-label={ui.sidebarIsClosed ? t("Expand") : t("Collapse")}
const toggleStyle = React.useMemo(
() => ({
right: "auto",
marginLeft: `${collapsed ? theme.sidebarCollapsedWidth : width}px`,
}),
[width, theme.sidebarCollapsedWidth, collapsed]
);
return (
<>
<Container
ref={ref}
style={style}
$isAnimating={isAnimating}
$isSmallerThanMinimum={isSmallerThanMinimum}
$mobileSidebarVisible={ui.mobileSidebarVisible}
$collapsed={collapsed}
column
>
{ui.mobileSidebarVisible && (
<Portal>
<Backdrop onClick={ui.toggleMobileSidebar} />
</Portal>
)}
{children}
{user && (
<AccountMenu>
{(props: HeaderButtonProps) => (
<HeaderButton
{...props}
showMoreMenu
title={user.name}
image={
<StyledAvatar
alt={user.name}
model={user}
size={24}
showBorder={false}
/>
}
>
<NotificationsPopover>
{(rest: HeaderButtonProps) => (
<HeaderButton {...rest} image={<NotificationIcon />} />
)}
</NotificationsPopover>
</HeaderButton>
)}
</AccountMenu>
)}
<ResizeBorder
onMouseDown={handleMouseDown}
onDoubleClick={ui.sidebarIsClosed ? undefined : handleReset}
/>
</>
);
}
);
{ui.sidebarIsClosed && (
<Toggle
onClick={ui.toggleCollapsedSidebar}
direction={"right"}
aria-label={t("Expand")}
/>
)}
</Container>
<Toggle
style={toggleStyle}
onClick={ui.toggleCollapsedSidebar}
direction={ui.sidebarIsClosed ? "right" : "left"}
aria-label={ui.sidebarIsClosed ? t("Expand") : t("Collapse")}
/>
</>
);
});
const StyledAvatar = styled(Avatar)`
margin-left: 4px;
@@ -37,7 +37,7 @@ const CollectionLink: React.FC<Props> = ({
expanded,
onDisclosureClick,
isDraggingAnyCollection,
}) => {
}: Props) => {
const itemRef = React.useRef<
NavigationNode & { depth: number; active: boolean; collectionId: string }
>();
@@ -118,7 +118,7 @@ const CollectionLink: React.FC<Props> = ({
setIsEditing(isEditing);
}, []);
const handleMouseEnter = React.useCallback(() => {
const handlePrefetch = React.useCallback(() => {
void collection.fetchDocuments();
}, [collection]);
@@ -138,7 +138,7 @@ const CollectionLink: React.FC<Props> = ({
}}
expanded={expanded}
onDisclosureClick={onDisclosureClick}
onMouseEnter={handleMouseEnter}
onClickIntent={handlePrefetch}
icon={
<CollectionIcon collection={collection} expanded={expanded} />
}
@@ -5,7 +5,6 @@ import { useTranslation } from "react-i18next";
import styled from "styled-components";
import Collection from "~/models/Collection";
import Document from "~/models/Document";
import DelayedMount from "~/components/DelayedMount";
import DocumentsLoader from "~/components/DocumentsLoader";
import { ResizingHeightContainer } from "~/components/ResizingHeightContainer";
import usePolicy from "~/hooks/usePolicy";
@@ -58,7 +57,7 @@ function CollectionLinkChildren({
if (!collection) {
return;
}
documents.move(item.id, collection.id, undefined, 0);
void documents.move(item.id, collection.id, undefined, 0);
},
collect: (monitor) => ({
isOverReorder: !!monitor.isOver(),
@@ -78,9 +77,7 @@ function CollectionLinkChildren({
<DocumentsLoader collection={collection} enabled={expanded}>
{!childDocuments && (
<ResizingHeightContainer hideOverflow>
<DelayedMount>
<Loading />
</DelayedMount>
<Loading />
</ResizingHeightContainer>
)}
{childDocuments?.map((node, index) => (
@@ -23,13 +23,20 @@ function Collections() {
const { t } = useTranslation();
const orderedCollections = collections.orderedData;
const params = React.useMemo(
() => ({
limit: 100,
}),
[]
);
const [
{ isCollectionDropping, isDraggingAnyCollection },
dropToReorderCollection,
] = useDrop({
accept: "collection",
drop: async (item: DragObject) => {
collections.move(
void collections.move(
item.id,
fractionalIndex(null, orderedCollections[0].index)
);
@@ -47,7 +54,7 @@ function Collections() {
<Relative>
<PaginatedList
fetch={collections.fetchPage}
options={{ limit: 25 }}
options={params}
aria-label={t("Collections")}
items={collections.orderedData}
loading={<PlaceholderCollections />}
@@ -67,9 +67,9 @@ function InnerDocumentLink(
React.useEffect(() => {
if (isActiveDocument && hasChildDocuments) {
fetchChildDocuments(node.id);
void fetchChildDocuments(node.id);
}
}, [fetchChildDocuments, node, hasChildDocuments, isActiveDocument]);
}, [fetchChildDocuments, node.id, hasChildDocuments, isActiveDocument]);
const pathToNode = React.useMemo(
() => collection?.pathToDocument(node.id).map((entry) => entry.id),
@@ -114,8 +114,8 @@ function InnerDocumentLink(
[expanded]
);
const handleMouseEnter = React.useCallback(() => {
prefetchDocument?.(node.id);
const handlePrefetch = React.useCallback(() => {
void prefetchDocument?.(node.id);
}, [prefetchDocument, node]);
const handleTitleChange = React.useCallback(
@@ -125,7 +125,6 @@ function InnerDocumentLink(
}
await documents.update({
id: document.id,
text: document.text,
title,
});
},
@@ -242,11 +241,11 @@ function InnerDocumentLink(
}
if (expanded) {
documents.move(item.id, collection.id, node.id, 0);
void documents.move(item.id, collection.id, node.id, 0);
return;
}
documents.move(item.id, collection.id, parentId, index + 1);
void documents.move(item.id, collection.id, parentId, index + 1);
},
collect: (monitor) => ({
isOverReorder: monitor.isOver(),
@@ -317,7 +316,7 @@ function InnerDocumentLink(
<SidebarLink
expanded={hasChildren ? isExpanded : undefined}
onDisclosureClick={handleDisclosureClick}
onMouseEnter={handleMouseEnter}
onClickIntent={handlePrefetch}
to={{
pathname: node.url,
state: {
@@ -335,7 +334,9 @@ function InnerDocumentLink(
/>
}
isActive={(match, location: Location<{ starred?: boolean }>) =>
!!match && location.state?.starred === inStarredSection
((document && location.pathname.endsWith(document.urlId)) ||
!!match) &&
location.state?.starred === inStarredSection
}
isActiveDrop={isOverReparent && canDropToReparent}
depth={depth}
@@ -51,7 +51,7 @@ function DraggableCollectionLink({
] = useDrop({
accept: "collection",
drop: (item: DragObject) => {
collections.move(
void collections.move(
item.id,
fractionalIndex(collection.index, belowCollectionIndex)
);
@@ -121,7 +121,7 @@ const Input = styled.input`
border: 0;
padding: 5px 6px;
margin: -4px;
height: 32px;
height: 30px;
&:focus {
outline-color: ${s("accent")};
+2 -1
View File
@@ -3,9 +3,10 @@ import styled from "styled-components";
type Props = {
expanded: boolean;
children?: React.ReactNode;
};
const Folder: React.FC<Props> = ({ expanded, children }) => {
const Folder: React.FC<Props> = ({ expanded, children }: Props) => {
const [openedOnce, setOpenedOnce] = React.useState(expanded);
// allows us to avoid rendering all children when the folder hasn't been opened
+2 -1
View File
@@ -9,12 +9,13 @@ type Props = {
/** Unique header id if passed the header will become toggleable */
id?: string;
title: React.ReactNode;
children?: React.ReactNode;
};
/**
* Toggleable sidebar header
*/
export const Header: React.FC<Props> = ({ id, title, children }) => {
export const Header: React.FC<Props> = ({ id, title, children }: Props) => {
const [firstRender, setFirstRender] = React.useState(true);
const [expanded, setExpanded] = usePersistedState<boolean>(
`sidebar-header-${id}`,
@@ -5,7 +5,7 @@ import { s } from "@shared/styles";
import Flex from "~/components/Flex";
import { undraggableOnDesktop } from "~/styles";
export type HeaderButtonProps = React.ComponentProps<typeof Wrapper> & {
export type HeaderButtonProps = React.ComponentProps<typeof Button> & {
title: React.ReactNode;
image: React.ReactNode;
minHeight?: number;
@@ -13,54 +13,62 @@ export type HeaderButtonProps = React.ComponentProps<typeof Wrapper> & {
showDisclosure?: boolean;
showMoreMenu?: boolean;
onClick: React.MouseEventHandler<HTMLButtonElement>;
children?: React.ReactNode;
};
const HeaderButton = React.forwardRef<HTMLButtonElement, HeaderButtonProps>(
(
function _HeaderButton(
{
showDisclosure,
showMoreMenu,
image,
title,
minHeight = 0,
children,
...rest
}: HeaderButtonProps,
ref
) => (
<Wrapper
role="button"
justify="space-between"
align="center"
as="button"
minHeight={minHeight}
{...rest}
ref={ref}
>
<Title gap={6} align="center">
{image}
{title}
</Title>
{showDisclosure && <ExpandedIcon />}
{showMoreMenu && <MoreIcon />}
</Wrapper>
)
) {
return (
<Flex justify="space-between" align="center" shrink={false}>
<Button
{...rest}
minHeight={minHeight}
as="button"
ref={ref}
role="button"
>
<Title gap={8} align="center">
{image}
{title}
</Title>
{showDisclosure && <ExpandedIcon />}
{showMoreMenu && <MoreIcon />}
</Button>
{children}
</Flex>
);
}
);
const Title = styled(Flex)`
color: ${s("text")};
flex-shrink: 1;
flex-grow: 1;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
`;
const Wrapper = styled(Flex)<{ minHeight: number }>`
const Button = styled(Flex)<{ minHeight: number }>`
flex: 1;
color: ${s("textTertiary")};
align-items: center;
padding: 8px 4px;
font-size: 15px;
font-weight: 500;
border-radius: 4px;
margin: 8px;
color: ${s("textTertiary")};
margin: 8px 0;
border: 0;
background: none;
flex-shrink: 0;
@@ -81,6 +89,14 @@ const Wrapper = styled(Flex)<{ minHeight: number }>`
transition: background 100ms ease-in-out;
background: ${s("sidebarActiveBackground")};
}
&:last-child {
margin-right: 8px;
}
&:first-child {
margin-left: 8px;
}
`;
export default HeaderButton;
+10 -5
View File
@@ -62,7 +62,7 @@ const NavLink = ({
to,
...rest
}: Props) => {
const linkRef = React.useRef(null);
const linkRef = React.useRef<HTMLAnchorElement>(null);
const context = React.useContext(RouterContext);
const [preActive, setPreActive] = React.useState<boolean | undefined>(
undefined
@@ -93,10 +93,15 @@ const NavLink = ({
React.useLayoutEffect(() => {
if (isActive && linkRef.current && scrollIntoViewIfNeeded !== false) {
scrollIntoView(linkRef.current, {
scrollMode: "if-needed",
behavior: "auto",
});
// If the page has an anchor hash then this means we're linking to an
// anchor in the document smooth scrolling the sidebar may the scrolling
// to the anchor of the document so we must avoid it.
if (!window.location.hash) {
scrollIntoView(linkRef.current, {
scrollMode: "if-needed",
behavior: "auto",
});
}
}
}, [linkRef, scrollIntoViewIfNeeded, isActive]);
@@ -42,9 +42,10 @@ function DocumentLink(
!!node.children.length || activeDocument?.parentDocumentId === node.id;
const document = documents.get(node.id);
const showChildren = React.useMemo(() => !!hasChildDocuments, [
hasChildDocuments,
]);
const showChildren = React.useMemo(
() => !!hasChildDocuments,
[hasChildDocuments]
);
const [expanded, setExpanded] = React.useState(showChildren);
@@ -6,6 +6,7 @@ import { s } from "@shared/styles";
import { NavigationNode } from "@shared/types";
import EventBoundary from "~/components/EventBoundary";
import NudeButton from "~/components/NudeButton";
import useUnmount from "~/hooks/useUnmount";
import { undraggableOnDesktop } from "~/styles";
import Disclosure from "./Disclosure";
import NavLink, { Props as NavLinkProps } from "./NavLink";
@@ -20,7 +21,8 @@ type Props = Omit<NavLinkProps, "to"> & {
to?: LocationDescriptor;
innerRef?: (ref: HTMLElement | null | undefined) => void;
onClick?: React.MouseEventHandler<HTMLAnchorElement>;
onMouseEnter?: React.MouseEventHandler<HTMLAnchorElement>;
/* Callback when we expect the user to click on the link. Used for prefetching data. */
onClickIntent?: () => void;
onDisclosureClick?: React.MouseEventHandler<HTMLButtonElement>;
icon?: React.ReactNode;
label?: React.ReactNode;
@@ -44,7 +46,7 @@ function SidebarLink(
{
icon,
onClick,
onMouseEnter,
onClickIntent,
to,
label,
active,
@@ -63,6 +65,7 @@ function SidebarLink(
}: Props,
ref: React.RefObject<HTMLAnchorElement>
) {
const timer = React.useRef<number>();
const theme = useTheme();
const style = React.useMemo(
() => ({
@@ -81,6 +84,28 @@ function SidebarLink(
[theme.text, theme.sidebarActiveBackground, style]
);
const handleMouseEnter = React.useCallback(() => {
if (timer.current) {
clearTimeout(timer.current);
}
if (onClickIntent) {
timer.current = window.setTimeout(onClickIntent, 100);
}
}, [onClickIntent]);
const handleMouseLeave = React.useCallback(() => {
if (timer.current) {
clearTimeout(timer.current);
}
}, []);
useUnmount(() => {
if (timer.current) {
clearTimeout(timer.current);
}
});
return (
<>
<Link
@@ -90,7 +115,8 @@ function SidebarLink(
activeStyle={isActiveDrop ? activeDropStyle : activeStyle}
style={active ? activeStyle : style}
onClick={onClick}
onMouseEnter={onMouseEnter}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
// @ts-expect-error exact does not exist on div
exact={exact !== false}
to={to}
@@ -174,6 +200,7 @@ const Link = styled(NavLink)<{
text-overflow: ellipsis;
padding: 6px 16px;
border-radius: 4px;
min-height: 32px;
transition: background 50ms, color 50ms;
user-select: none;
background: ${(props) =>

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