Compare commits

...

429 Commits

Author SHA1 Message Date
Tom Moor 6e07aa877f clear secret 2024-10-01 22:17:45 -04:00
Tom Moor 19d5ef5694 Fix code scanning alert no. 67: Shell command built from environment values
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2024-10-01 19:05:20 -07:00
Tom Moor b37074304a fix 2024-10-01 22:04:50 -04:00
Tom Moor 35c7cc2086 suppress on cloud 2024-10-01 21:40:09 -04:00
Tom Moor 82f9600d9e fix: last4 not written 2024-10-01 21:38:31 -04:00
Tom Moor 686f9aeb5c Merge main 2024-10-01 21:02:36 -04:00
Tom Moor a41e17f875 feat: Add 'Copy public link' option to document menu 2024-10-01 09:55:33 -04:00
Hemachandar db114fd966 perf: store document state in context (#7658)
* perf: store document state in context

* provide doc-context for shared routes
2024-10-01 05:16:32 -07:00
dependabot[bot] ce987d23ed chore(deps): bump socket.io-client from 4.7.5 to 4.8.0 (#7701)
Bumps [socket.io-client](https://github.com/socketio/socket.io) from 4.7.5 to 4.8.0.
- [Release notes](https://github.com/socketio/socket.io/releases)
- [Changelog](https://github.com/socketio/socket.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io/compare/socket.io-client@4.7.5...socket.io-client@4.8.0)

---
updated-dependencies:
- dependency-name: socket.io-client
  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>
2024-09-30 17:16:53 -07:00
Tom Moor 5e61fcd336 Add hashed column for API keys (#7699)
* Add hashed column for API keys

* test

* Add obfuscatedValue getter
2024-09-30 17:16:35 -07:00
dependabot[bot] 4f84daf558 chore(deps-dev): bump @types/body-scroll-lock from 3.1.0 to 3.1.2 (#7703)
Bumps [@types/body-scroll-lock](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/body-scroll-lock) from 3.1.0 to 3.1.2.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/body-scroll-lock)

---
updated-dependencies:
- dependency-name: "@types/body-scroll-lock"
  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>
2024-09-30 16:30:17 -07:00
dependabot[bot] f80842ca20 chore(deps-dev): bump @types/jest from 29.5.12 to 29.5.13 (#7702)
Bumps [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest) from 29.5.12 to 29.5.13.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jest)

---
updated-dependencies:
- dependency-name: "@types/jest"
  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>
2024-09-30 16:30:07 -07:00
Tom Moor 53758b69fb fix: Another race condition with policy removal on startup 2024-09-30 18:57:39 -04:00
Tom Moor cd86877cb0 fix: Race condition with policies removed on app load 2024-09-30 17:51:01 -04:00
Tom Moor a2d5598b96 wip 2024-09-30 17:46:30 -04:00
清纯的小黄瓜 ffae5d2f20 fix: sidebar cannot display correctly on mobile, when collapsed is true (#7694) 2024-09-30 05:21:44 -07:00
Tom Moor 53272c8c3d test 2024-09-30 07:16:03 -04:00
Tom Moor 65ff9bde3e Add hashed column for API keys 2024-09-30 07:07:38 -04:00
Hemachandar 21adfdd1bf perf: use findAll for querying document collaborators (#7697)
* perf: use findAll for querying document collaborators

* remove unnecessary compact
2024-09-29 09:43:07 -07:00
Hemachandar 91c2f60827 fix: align document and collection title (#7696) 2024-09-29 09:37:09 -07:00
Tom Moor a253d2921a Add reverse alphabetical sort for collections, closes #7692 2024-09-29 09:53:50 -04:00
Tom Moor b83d218fbe fix: Add icons to copy functionality in command menu 2024-09-29 09:37:15 -04:00
Tom Moor dce96955a1 Tweak command bar heading style 2024-09-29 09:33:09 -04:00
Tom Moor ba7c446f59 Add confirmation dialog when dragging 2024-09-29 09:31:00 -04:00
Tom Moor 7b9ec4c43a Show targeted collection/document in command bar, closes #7678 2024-09-28 22:46:41 -04:00
Tom Moor faaf0a6733 Add recently viewed documents to top of command menu (#7685)
* Add recent documents to command menu
Add priority key to actions and sections

* refactor

* Rename section

* refactor, remove more unused code
2024-09-28 17:30:40 -07:00
Hemachandar c58aafeb32 fix: check doc access before sending mention email (#7664)
* fix: check doc access before sending mention email

* refactor

---------

Co-authored-by: Tom Moor <tom.moor@gmail.com>
2024-09-28 14:29:34 -07:00
bLue 3f73c9d2bf fix: Adjust math delimiter to fix markdown import/export compatibility issue (#6727)
#6650)
2024-09-28 12:45:03 -07:00
Tom Moor b6e43e1990 Add Heading 4 to block menu 2024-09-28 13:13:25 -04:00
Tom Moor 0a2c066253 fix: Improve display of Mermaid pie charts 2024-09-28 12:31:46 -04:00
Tom Moor 840db4692e fix: Correctly decorate urlId as Unique (#7671) 2024-09-28 06:23:27 -07:00
Tom Moor fa961d7464 fix: Drop to reorder cursor appearing where move location is invalid 2024-09-28 09:21:26 -04:00
Tom Moor 3e75b24f7a fix: Table sorting does not take into account tables without a header 2024-09-27 22:12:01 -04:00
Tom Moor ce91071995 fix: post login redirect path not correctly spent, closes #7662 2024-09-26 17:35:13 -04:00
Tom Moor 9b807f7a9e v0.80.2 2024-09-25 22:50:22 -04:00
Tom Moor 17493ca0cf fix: Account for multiple existing OIDC authentication providers, closes #7638 2024-09-25 22:49:48 -04:00
Tom Moor 1d4b05c9f6 fix: Add check for paths in useLastVisitedPath, closes #7655 2024-09-25 22:00:52 -04:00
Tom Moor 8a5e42071f fix: Remove deprecated meta tag 2024-09-25 08:58:17 -04:00
Tom Moor 6b53755f5a fix: Maximum call stack size exceeded, closes #7642 2024-09-24 23:11:58 -04:00
Tom Moor 709e4f44fd fix: Allow team creation without SSO methods 2024-09-24 22:33:53 -04:00
dependabot[bot] c37646b5ad chore(deps): bump bull from 4.12.2 to 4.16.3 (#7649)
Bumps [bull](https://github.com/OptimalBits/bull) from 4.12.2 to 4.16.3.
- [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.12.2...v4.16.3)

---
updated-dependencies:
- dependency-name: bull
  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>
2024-09-23 20:37:56 -07:00
dependabot[bot] 36ca667c50 chore(deps-dev): bump discord-api-types from 0.37.87 to 0.37.101 (#7646)
Bumps [discord-api-types](https://github.com/discordjs/discord-api-types) from 0.37.87 to 0.37.101.
- [Release notes](https://github.com/discordjs/discord-api-types/releases)
- [Changelog](https://github.com/discordjs/discord-api-types/blob/main/CHANGELOG.md)
- [Commits](https://github.com/discordjs/discord-api-types/compare/0.37.87...0.37.101)

---
updated-dependencies:
- dependency-name: discord-api-types
  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>
2024-09-23 19:13:38 -07:00
uy/sun 009e66a466 fix: disable bilibili autoplay (#7639) 2024-09-23 04:50:52 -07:00
Tom Moor 7adda26c6d v0.80.1 2024-09-22 13:00:56 -04:00
Tom Moor 62860c593b fix: Version number not updated from previous release, #7635 2024-09-22 13:00:48 -04:00
Tom Moor bdc2357984 Merge branch 'main' of github.com:outline/outline 2024-09-22 12:59:16 -04:00
Tom Moor 4fc1ed0d7e Add release script (#7637) 2024-09-22 09:59:10 -07:00
Tom Moor 5d068361cc fix: Protect against dangerous characters in env 2024-09-22 12:58:49 -04:00
Tom Moor 176cfff7f8 fix: Document editors are sometimes not included in insights/collaboratorIds, closes #7613 2024-09-21 15:30:56 -04:00
Tom Moor 2fd18f7fdb fix: Add resilience for absolute attachment paths in Markdown importer, closes #7512 2024-09-21 14:34:28 -04:00
Tom Moor 34f951c511 fix: Pragmatic fix to drafts count in sidebar, closes #7219 2024-09-21 13:51:46 -04:00
Tom Moor f0c26cf8c8 refactor: Move hooks to correct folder 2024-09-21 13:27:10 -04:00
Translate-O-Tron d77ddbd7de New Crowdin updates (#7595) 2024-09-21 06:31:07 -07:00
Tom Moor 4e1038837b chore: Remove old feature flag
fix: Loading jank when creating new collection
Add italic prop to Text component
2024-09-21 09:03:20 -04:00
Tom Moor c54fcc3536 fix: Improve scroll to comment logic, closes #7435 2024-09-20 23:31:46 -04:00
Tom Moor c4fa63df3d Sticky public access toggle (#7617)
* Sticky public access toggle

* tweak
2024-09-20 18:42:50 -07:00
Tom Moor 2b42ce0c0f fix: document.content column not updated when sending text attribute through API (#7630) 2024-09-20 07:37:42 -07:00
Apoorv Mishra 3208156591 In documents.archived, allow sort: "index" in request (#7628)
* fix: allow sorting by index and filtering by collectionId

* fix: cleanup
2024-09-20 11:29:20 +05:30
Tom Moor e8577ef2a8 chore: Bump prosemirror-view 2024-09-19 20:01:22 -04:00
Tom Moor ca66dec22b Merge branch 'main' of github.com:outline/outline 2024-09-19 20:00:36 -04:00
Tom Moor 41ccad7cce Adds natural behavior of Tab, Shift-Tab in code blocks (#7622)
* Adds proper handling of Tab, Shift-Tab in code blocks

* refactor
2024-09-19 17:00:05 -07:00
Tom Moor bd52b364dd Increase collection rate limit to 50/hour 2024-09-19 09:01:05 -04:00
Tom Moor 5c56714bc8 test: Remove table truncation as it causes sporadic test failures 2024-09-18 20:13:52 -04:00
Tom Moor 895a88f934 fix: Incorrect filter input background in dark mode 2024-09-18 09:25:46 -04:00
Tom Moor f32db08ef3 Add 'Toggle task list item' to shortcut guide 2024-09-18 09:22:38 -04:00
Tom Moor 05a513b10c test 2024-09-18 08:59:35 -04:00
Tom Moor bf3c6333b0 Allow drag and drop into "Shared with me" as parent (#7619)
* wip

* Remove collections from documents.move response

* Remove parsing of collections in documents.move response
2024-09-18 05:41:36 -07:00
Tom Moor 544554f106 fix: Prevent mismatch of parentDocumentId and collectionId in documents.move request 2024-09-17 20:48:13 -04:00
Tom Moor 37c90e1592 fix: Add guard for empty actorId, closes #7614 2024-09-16 22:18:50 -04:00
Nam Vu 815abc8423 feat: group/groupuser commands (#7548) 2024-09-16 17:50:57 -07:00
Tom Moor b9ed7ddf58 feat: Add confirmation for document archive, closes #7605 2024-09-16 20:36:51 -04:00
Tom Moor bc0b73e7a7 fix: OIDC signin to prevent duplicate auth providers (#7598)
* Refactor OIDC signin to prevent duplicate auth providers

* refactor
2024-09-16 17:21:41 -07:00
dependabot[bot] 1218bc1f3c chore(deps): bump @babel/plugin-transform-destructuring from 7.24.7 to 7.24.8 (#7608)
Bumps [@babel/plugin-transform-destructuring](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-destructuring) from 7.24.7 to 7.24.8.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.24.8/packages/babel-plugin-transform-destructuring)

---
updated-dependencies:
- dependency-name: "@babel/plugin-transform-destructuring"
  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>
2024-09-16 16:45:40 -07:00
dependabot[bot] ae3b05fdba chore(deps-dev): bump @types/validator from 13.12.0 to 13.12.1 (#7609)
Bumps [@types/validator](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/validator) from 13.12.0 to 13.12.1.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/validator)

---
updated-dependencies:
- dependency-name: "@types/validator"
  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>
2024-09-16 16:45:25 -07:00
Tom Moor 549c8d9ed8 feat: Add ability to tag users out of document (#7601)
* feat: Add ability to tag users out of document

* cleanup fetchDocumentUsers
2024-09-16 16:45:01 -07:00
Tom Moor 6bb798220b feat: Add userId filter to documents.users endpoint 2024-09-15 21:41:06 -04:00
Tom Moor e032bb5ab8 Check document access before creating @mention notification 2024-09-15 21:27:07 -04:00
Tom Moor 23b3b8aa54 fix: Archived documents can appear in shared with me sidebar 2024-09-14 22:04:13 -04:00
Tom Moor 738d943bd4 fix: CMD+K with link editor open should focus editor 2024-09-14 16:31:06 -04:00
Tom Moor ae5c737ed2 perf: Do not trigger list hover transactions in read-only editor 2024-09-14 11:57:09 -04:00
Tom Moor 5116147ace fix: Internal URL detection on client, closes #7245 2024-09-14 11:46:42 -04:00
Tom Moor e6ba84e434 fix: Disconnected icon is misaligned 2024-09-14 11:38:26 -04:00
Tom Moor 3b546a7935 fix: Negative days in deletion banner, closes #7485 2024-09-14 11:20:48 -04:00
Tom Moor 9373da0da6 Add filtering and async loading to search filters (#7597)
* Add input within search filters

* Query users on demand

* Enforce input on collection and user filters

* Improve filter matching, reduce flickering
2024-09-14 08:12:01 -07:00
Tom Moor 494ef2a6cd fix: Include results from users with accents in users.list query 2024-09-12 20:46:17 -04:00
Tom Moor c60703cc5a Merge branch 'main' of github.com:outline/outline 2024-09-12 19:44:10 -04:00
Translate-O-Tron f5b6d10a73 New Crowdin updates (#7551) 2024-09-12 16:42:47 -07:00
Tom Moor 3b17926023 fix: Include results from users with accents in @mention search (#7590)
* fix: Include results from users with accents in @mention search

* test
2024-09-12 16:42:31 -07:00
Tom Moor 0c080038d7 fix: Filter self from user suggestions, closes #7559 2024-09-12 19:26:23 -04:00
Tom Moor ae0bd5f59d fix: Add guards around sessionStorage use, closes #7583 2024-09-12 18:41:21 -04:00
Tom Moor 7c9a2bbcf6 fix: Team is required for user queries from slack hooks 2024-09-12 12:39:24 -04:00
Tom Moor b55a8ab54f fix: Restore individual tabs to correct page post-login, closes #7467 2024-09-10 22:40:02 -04:00
Tom Moor 1bc41b4d62 fix: Syncronized login state of tabs, related #7467 2024-09-10 22:02:55 -04:00
Tom Moor 43b9eb0ad7 Home and Search sticky in sidebar 2024-09-10 21:39:24 -04:00
Tom Moor 3f87912656 perf: Remove subscriptions.list request on document load, ref #7442 2024-09-10 21:11:16 -04:00
Tom Moor c960804bb8 fix: Warning of active prop passed to dom 2024-09-10 21:04:48 -04:00
Tom Moor 26fa70cbbd fix: JS error when user removed from membership relation, closes #7582 2024-09-10 20:58:37 -04:00
Tom Moor ba749cac71 Add missing keyboard shortcuts in guide 2024-09-10 20:45:23 -04:00
Tom Moor df08a0063c fix: Missing highlight color in floating toolbar 2024-09-10 20:41:38 -04:00
Tom Moor 6591bbebc9 feat: Add Cmd-Shift-c as shortcut for inline code 2024-09-10 20:29:26 -04:00
Tom Moor cb56941a17 fix: Issue in latest Chrome with Korean IME, closes #7574 2024-09-10 20:23:53 -04:00
dependabot[bot] 209e5e20d5 chore(deps-dev): bump terser from 5.31.6 to 5.32.0 (#7567)
Bumps [terser](https://github.com/terser/terser) from 5.31.6 to 5.32.0.
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/compare/v5.31.6...v5.32.0)

---
updated-dependencies:
- dependency-name: terser
  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>
2024-09-09 20:39:47 -07:00
dependabot[bot] 2d0612a9d0 chore(deps): bump @octokit/auth-app from 6.1.1 to 6.1.2 (#7569)
Bumps [@octokit/auth-app](https://github.com/octokit/auth-app.js) from 6.1.1 to 6.1.2.
- [Release notes](https://github.com/octokit/auth-app.js/releases)
- [Commits](https://github.com/octokit/auth-app.js/compare/v6.1.1...v6.1.2)

---
updated-dependencies:
- dependency-name: "@octokit/auth-app"
  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>
2024-09-09 20:39:30 -07:00
Apoorv Mishra fca4467bda Delay loading of document policies until document menu is opened (#7442)
* fix: loading of doc policies should be delayed until menu is opened

* fix: fetch upon hover and display menu only when data is loaded

* fix: remove documents.isFetching

* fix: wait for policies to load

* fix: bifurcate

* fix: className is never used at any callsite of

* fix: MenuContext

* fix: collection is derived from document

* fix: mount MenuContent only when button is clicked

* fix: jsdoc

* fix: aria-label

* fix: review
2024-09-09 22:46:37 +05:30
Tom Moor b77af9bda3 fix: Remove manual revision deletion in document delete endpoint 2024-09-08 11:06:43 -04:00
Tom Moor f984ee0fcc fix: Cannot unset collection description, closes #7553 2024-09-07 18:42:39 -04:00
Meng Sen f3fe73057a feat: add Umami (#7366)
Co-authored-by: Tom Moor <tom.moor@gmail.com>
2024-09-07 15:15:35 -07:00
Tom Moor 4a009ed35b feat: Adds team preference to disable user account removal (#7556)
* feat: Adds team preference to disable user account removal

* Switch to using policy
2024-09-07 10:36:41 -07:00
Tom Moor cd419190ef chore: Remove from presenter payloads 2024-09-07 11:53:38 -04:00
Tom Moor 7c309c7986 fix: Use padlock icon for document permissions in menu, small refactor 2024-09-07 11:52:11 -04:00
Tom Moor 4a2707c74c Update LICENSE 2024-09-05 19:50:09 -07:00
Tom Moor a6b9672779 0.79.1 2024-09-05 20:03:45 -04:00
Tom Moor 3bce4853c3 fix: group_user primary key migration needs to cleanup duplicate rows before adding constraint.
closes #7546
2024-09-05 19:57:15 -04:00
Tom Moor 6859b0cf62 Add 'Permissions' option to document menu for those with, erm.. permission 2024-09-05 19:30:13 -04:00
Tom Moor d10668de54 feat: Add 'nginx' language, closes #7542 2024-09-05 19:22:03 -04:00
Tom Moor f8535ff047 fix: Error in non-Outline Electron browser, improved typings 2024-09-05 19:19:39 -04:00
Tom Moor e2355d63a2 fix: Usage of env.URL before environment validation 2024-09-05 18:59:41 -04:00
Tom Moor ed22891a69 feat: Adds 'Dockerfile' as language (#7549) 2024-09-05 15:59:30 -07:00
Tom Moor 363c416873 fix: Docker HEALTHCHECK does not work if PORT is not explicity defined in env
closes #7547
2024-09-05 18:46:34 -04:00
dependabot[bot] 967594686e chore(deps): bump vite-plugin-pwa from 0.17.4 to 0.20.3 (#7522)
* chore(deps): bump vite-plugin-pwa from 0.17.4 to 0.20.3

Bumps [vite-plugin-pwa](https://github.com/vite-pwa/vite-plugin-pwa) from 0.17.4 to 0.20.3.
- [Release notes](https://github.com/vite-pwa/vite-plugin-pwa/releases)
- [Commits](https://github.com/vite-pwa/vite-plugin-pwa/compare/v0.17.4...v0.20.3)

---
updated-dependencies:
- dependency-name: vite-plugin-pwa
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

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

* fix: build error

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tom Moor <tom.moor@gmail.com>
2024-09-05 05:28:52 -07:00
Tom Moor ce85b8f94d 0.79.0 2024-09-04 23:18:47 -04:00
Tom Moor 81b7ac5776 0.79.0-0 2024-09-04 22:49:05 -04:00
Translate-O-Tron fe5d8b7158 New Crowdin updates (#7505) 2024-09-04 19:37:05 -07:00
Tom Moor 7013a87c6e fix: Move wget installation and HEALTHCHECK to correct stage.
closes #7279
2024-09-04 22:30:59 -04:00
Hemachandar 4ef7e95863 feat: Add background task for empty_trash action (#7531)
* feat: Add background task for empty_trash action

* review

* enqueue task only if docs are available
2024-09-04 19:04:15 -07:00
Tom Moor f81a836549 fix: Improve scrolling behavior
closes #7533
2024-09-04 22:03:03 -04:00
Tom Moor 97674471db fix: Publish child documents created through editor CMD+K 2024-09-04 21:34:08 -04:00
Tom Moor 5a3e97d6c5 fix: CTRL+E shortcut code block scroll behavior
closes #7535
2024-09-04 21:21:34 -04:00
Tom Moor 273d6550ca fix: In templates insert template string for times 2024-09-04 21:09:26 -04:00
Tom Moor 75a78cd1c7 chore: fix Vite chunk warning 2024-09-03 22:39:22 -04:00
Tom Moor ff11a3c667 fix: Include plugin tests on CI (#7528)
* fix: Include plugin tests on CI
2024-09-03 19:13:42 -07:00
Tom Moor 1236cc9c16 fix: Emoji in heading does not appear in TOC, closes #7525 2024-09-03 21:01:42 -04:00
Tom Moor 8da5afc394 fix: Consider port in isInternalUrl util (#7529) 2024-09-03 17:22:43 -07:00
Hemachandar 7f17a51e11 feat: Add email headers to enhance threading (#7477)
* feat: Add email headers to enhance threading

* tsc

* review

* change comment mentioned email subject

* paginated load

* rename util method

* add tests

* group events

* test for unsupported notification

* typo

* review
2024-09-03 17:20:56 -07:00
Tom Moor bf1580a459 fix: Missing error toast if file size is too large on import document
closes #7524
2024-09-03 19:37:49 -04:00
Tom Moor 3e991c71c4 fix: Markdown table with column alignment not detected as markdown 2024-09-03 19:31:33 -04:00
Tom Moor e90b4d8871 Improve error log when SECRET_KEY is incorrectly defined.
Related #7523
2024-09-03 19:23:38 -04:00
dependabot[bot] ac6b4fcb4f chore(deps-dev): bump @types/readable-stream from 4.0.14 to 4.0.15 (#7513)
Bumps [@types/readable-stream](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/readable-stream) from 4.0.14 to 4.0.15.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/readable-stream)

---
updated-dependencies:
- dependency-name: "@types/readable-stream"
  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>
2024-09-03 06:17:39 -07:00
dependabot[bot] 0ac5139730 chore(deps): bump @sentry/react from 7.118.0 to 7.119.0 (#7514)
Bumps [@sentry/react](https://github.com/getsentry/sentry-javascript) from 7.118.0 to 7.119.0.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/7.119.0/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.118.0...7.119.0)

---
updated-dependencies:
- dependency-name: "@sentry/react"
  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>
2024-09-03 06:17:24 -07:00
dependabot[bot] 53d2d68a21 chore(deps-dev): bump eslint-import-resolver-typescript (#7515)
Bumps [eslint-import-resolver-typescript](https://github.com/import-js/eslint-import-resolver-typescript) from 3.6.1 to 3.6.3.
- [Release notes](https://github.com/import-js/eslint-import-resolver-typescript/releases)
- [Changelog](https://github.com/import-js/eslint-import-resolver-typescript/blob/master/CHANGELOG.md)
- [Commits](https://github.com/import-js/eslint-import-resolver-typescript/compare/v3.6.1...v3.6.3)

---
updated-dependencies:
- dependency-name: eslint-import-resolver-typescript
  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>
2024-09-03 06:17:15 -07:00
Tom Moor 783122186a fix: Protect against pasting invalid @mentions 2024-09-03 08:37:33 -04:00
Tom Moor 9b24482c46 fix: Prevent ISE when searched query contains colon, closes #7481 2024-09-03 08:24:33 -04:00
Tom Moor 29fdd7e566 fix: Migration script check, closes #7479 2024-09-02 22:39:05 -04:00
Tom Moor bb074fc8cf feat: Add 'R' language highlighting, closes #7473 2024-09-02 22:18:42 -04:00
Tom Moor 19b6ee832b Suppress 'Added to document' notifications when user has existing document membership and added through group 2024-09-02 22:07:26 -04:00
Tom Moor 63e667d6d3 fix: Uncaught URI malformed error 2024-09-02 21:42:27 -04:00
Tom Moor 2afec241a0 test 2024-09-02 09:40:42 -04:00
Tom Moor d65037d4e7 perf: Increase default concurrency on worker 2024-09-02 09:31:18 -04:00
Tom Moor 58eb55efb3 fix: limit parallel image downloads in document importer 2024-09-02 09:22:29 -04:00
Hemachandar d930824b27 fix: Set DB batch limit (#7511)
* fix: Set DB batch limit

* better assertion
2024-09-02 05:08:58 -07:00
Tom Moor 167cc1adbf fix: Store group name against group events 2024-09-01 17:19:54 -04:00
Tom Moor 1491fc2eb4 fix: Allow dragging shared documents to starred section (#7506)
* fix: Allow dragging shared documents to starred section

* fix: Allow read-only collection drag and drop
fix: Full screen delete modal from drag and drop
2024-09-01 14:19:40 -07:00
Tom Moor b95eb114f1 fix: Do not send notification to actor in group addition 2024-09-01 12:44:33 -04:00
Boris Bliznioukov 20799c941e logout for OIDC #6909 (#7446)
* logout for OIDC

* logout fix

* add some time
2024-09-01 07:11:45 -07:00
Translate-O-Tron 2290dff1f2 New Crowdin updates (#7489) 2024-09-01 06:52:52 -07:00
Tom Moor f61689abdc feat: Invite groups to documents (#7275) 2024-09-01 06:51:52 -07:00
Tom Moor fefb9200f1 fix: Assorted fixes from group memberships branch (tom/document-group-permissions) 2024-08-31 10:11:11 -04:00
Tom Moor 0a4d67d96f perf: Reduce rendering when editing title 2024-08-30 13:04:43 -04:00
Apoorv Mishra 5374784df6 New Replace type utility (#7476)
* fix: Replace type

* trigger ci
2024-08-30 10:02:38 +05:30
Translate-O-Tron d090316065 New Crowdin updates (#7438) 2024-08-29 13:32:55 -07:00
Tom Moor 0fc3099f75 Standardize websocket collection channel logic 2024-08-29 12:09:52 -04:00
Tom Moor 264dda25a5 fix: Filter trashed documents from 'Shared with me'
closes #7478
2024-08-29 11:27:08 -04:00
Tom Moor 8031b2906d fix: Assorted fixes from group memberships branch (tom/document-group-permissions) 2024-08-29 11:26:12 -04:00
Tom Moor 0642396264 perf: Policies refactor (#7460) 2024-08-28 06:10:38 -07:00
dependabot[bot] 88f405375c chore(deps): bump react-hook-form from 7.52.2 to 7.53.0 (#7462)
Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 7.52.2 to 7.53.0.
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.52.2...v7.53.0)

---
updated-dependencies:
- dependency-name: react-hook-form
  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>
2024-08-27 04:28:40 -07:00
Tom Moor 7bd8738ecd Client lifecycle hooks (#7407)
* Add example client lifecycle hooks

* AfterCreate, AfterDelete hooks
2024-08-27 04:28:21 -07:00
Hemachandar 043a8623b9 chore: Script to invalidate unfurls cache (#7459) 2024-08-26 19:14:25 -07:00
Apoorv Mishra 5d85a3a093 Specify time conversion unit (#7458)
* fix: specificity in time units

* fix: milliseconds -> ms
2024-08-25 18:57:45 +05:30
Tom Moor 2578a1f75f fix: Random icon color chosen when none set 2024-08-24 17:56:49 -04:00
Tom Moor 9578611d8f fix: Floating formatting toolbar on Android mobile, closes #7052 2024-08-24 15:13:29 -04:00
Tom Moor 65b1fd9a1f fix: Date/time commands operate differently when hitting space than other menu items 2024-08-24 11:10:25 -04:00
Tom Moor 282b0f486b Merge branch 'main' of github.com:outline/outline 2024-08-24 08:36:44 -04:00
Hemachandar e3cd9af6df fix: Set redis expiry in seconds (#7456)
* fix: Set redis expiry in seconds

* change expiry for github and iframely
2024-08-24 05:36:12 -07:00
Tom Moor a1373f8078 fix: MobX warning - attempted to use array index out of bounds 2024-08-23 11:25:01 -04:00
Tom Moor 6a85d7444d Add reserved space for pinned documents 2024-08-23 11:13:05 -04:00
Tom Moor 24222ddbb1 fix: Add small amount of empty space to the bottom of shared documents 2024-08-23 09:02:52 -04:00
Tom Moor a59215d27c Add video and audio as safe inline content-disposition 2024-08-23 08:53:18 -04:00
github-actions[bot] 23ad780672 chore: Auto Compress Images (#7455)
Co-authored-by: tommoor <tommoor@users.noreply.github.com>
2024-08-23 05:18:23 -07:00
Blendman974 ac26fd2be7 Feature: Add generic Iframe support (#7319)
* Added generic Iframes support

* Rename Iframe to Embed and more explicit matchOnPaste boolean

* Update icon, text

---------

Co-authored-by: Tom Moor <tom.moor@gmail.com>
2024-08-23 05:17:27 -07:00
Tom Moor 6a09af16a4 fix: Blue focus outline on browser edge when zooming images 2024-08-22 21:51:55 -04:00
Tom Moor ce2fc94289 fix: Do not replace smart quotes in code (#7450) 2024-08-22 18:40:36 -07:00
Tom Moor 3b01551e1a fix: Allow shift+enter soft break in comments, closes #7451 2024-08-22 21:06:24 -04:00
Tom Moor d2b3e50a48 Vendorize cancan with performance improvements (#7448)
* Vendorize CanCan with performance improvements

* docs
2024-08-22 17:10:58 -07:00
Tom Moor 5549676185 fix: Escape key with suggestion menu open should not unfocus editor 2024-08-22 09:21:47 -04:00
Tom Moor 976db13ea0 Allow space when searching for @mention 2024-08-22 09:21:23 -04:00
Tom Moor 510a756378 perf: Cache textSerializers on server 2024-08-21 23:15:06 -04:00
Tom Moor d399ffa9f8 Add clear option to remove highlight in formatting menu
closes #7436
2024-08-21 22:55:57 -04:00
Tom Moor 932e5bf121 fix: Error on search shows previous results instead of error state, closes #7437 2024-08-21 22:32:45 -04:00
Tom Moor 330ee819c5 fix: Error in documentDuplicate when content is not yet written, closes #7432 2024-08-21 22:15:32 -04:00
Tom Moor 61e29d91bf fix: Enable the 'e' shortcut to focus editor 2024-08-21 22:07:23 -04:00
Tom Moor 5e0c773826 fix: Incorrect response value from fetchAll (not used up until now)
Added warning when fetchAll is used with an endpoint without pagination info
2024-08-21 21:54:07 -04:00
Tom Moor 1f7e8c158d perf: Avoid multiple renders when adding search results to store 2024-08-21 16:59:25 -04:00
Translate-O-Tron eb6cc62630 New Crowdin updates (#7406) 2024-08-21 05:23:38 -07:00
Tom Moor 5380d8c7ac fix: Refactor postLoginRedirect logic for reliability (#7428) 2024-08-21 05:23:25 -07:00
Tom Moor fd379dddba perf: Remove old usage of dynamic require statement in policy serialization (#7427) 2024-08-21 05:22:59 -07:00
Tom Moor 5e825cef10 fix: Include actor as sender for document edited emails, related #7424 2024-08-21 08:18:56 -04:00
Tom Moor ad07f4b5f7 fix: 'Restore' option appears on documents when policy is not loaded 2024-08-21 00:03:03 -04:00
Tom Moor 2a502e43ef perf: Avoid count query in search where possible (#7426)
* Remove unused replacements

* perf: Avoid count query where possible

* logic
2024-08-20 18:59:32 -07:00
Tom Moor 18d4eaee07 perf: Add missing indexes (#7425)
* perf: Add missing indexes to notifications table

* Add two more missing indexes

* tests
2024-08-20 17:44:06 -07:00
Tom Moor 058e413a6e perf: Avoid unneccessary toMarkdown call in document presenter 2024-08-20 20:15:52 -04:00
Tom Moor 1c527c97a7 fix: Partial documents.update payload wipes existing document icon 2024-08-19 19:23:17 -04:00
Tom Moor 8ac3c17310 fix: Reduce unneccessary client fetches 2024-08-19 19:10:30 -04:00
Tom Moor 5687514fd5 fix: Set deletedAt rather than remove ParanoidModels
Related #7413
2024-08-19 16:55:09 -04:00
dependabot[bot] 27ea4332eb chore(deps): bump katex from 0.16.10 to 0.16.11 (#7416)
Bumps [katex](https://github.com/KaTeX/KaTeX) from 0.16.10 to 0.16.11.
- [Release notes](https://github.com/KaTeX/KaTeX/releases)
- [Changelog](https://github.com/KaTeX/KaTeX/blob/main/CHANGELOG.md)
- [Commits](https://github.com/KaTeX/KaTeX/compare/v0.16.10...v0.16.11)

---
updated-dependencies:
- dependency-name: katex
  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>
2024-08-19 11:48:52 -07:00
dependabot[bot] 382a0155a2 chore(deps-dev): bump @types/crypto-js from 4.2.1 to 4.2.2 (#7415)
Bumps [@types/crypto-js](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/crypto-js) from 4.2.1 to 4.2.2.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/crypto-js)

---
updated-dependencies:
- dependency-name: "@types/crypto-js"
  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>
2024-08-19 11:48:38 -07:00
Tom Moor 24b07698a7 Sidebar order 2024-08-19 09:16:56 -04:00
Tom Moor e9380f270e fix: Non-clickable items have cursor 2024-08-19 09:16:30 -04:00
Tom Moor 16badaea5d fix: Counter cache not correctly recalculated for missing keys 2024-08-19 08:45:19 -04:00
DiamondYuan b82a379c0e refactor: Improve search functionality in SuggestionsMenu (#7411) 2024-08-19 04:47:21 -07:00
Tom Moor 300d0c56ac chore: Restore createDatabaseInstance flexibility 2024-08-18 11:00:12 -04:00
Tom Moor f887a5b4f1 fix: Select inputs unactionable 2024-08-18 10:44:15 -04:00
Tom Moor 0ab8b52582 feat: Cache count of group members (#7377) 2024-08-17 14:34:12 -07:00
Translate-O-Tron c9d4f5038b New Crowdin updates (#7336) 2024-08-17 07:42:07 -07:00
Tom Moor 53cc2d8154 fix: Flash of highlighted menu item state as context menu opens 2024-08-17 10:02:04 -04:00
Tom Moor 82651737b8 Tweak initial avatar styling 2024-08-17 09:22:45 -04:00
Tom Moor 7a06d94548 fix: Visible horizontal scrollbars on EmojiPicker 2024-08-17 09:18:07 -04:00
Tom Moor 516c5082c8 fix: Reduce size of button to match visual area on Collaborators component 2024-08-17 09:11:18 -04:00
Tom Moor 7269201bca fix: 'Image failed to load' state not centered on large images 2024-08-17 09:09:59 -04:00
Tom Moor 86be197049 fix: Websocket event not required for each document in imported collection
fix: Newly imported collections do not appear in sidebar until reload
2024-08-17 09:03:05 -04:00
Tom Moor a65d126ccf fix: Empty IFRAMELY_URL environment variable triggers validation
Related #7401
2024-08-16 22:40:04 -04:00
Tom Moor eee14d98a7 fix: Flashing of document header with many live collaborators
closes #7400
2024-08-16 22:33:00 -04:00
Tom Moor d738081880 fix: Menu on document revisions not actionable 2024-08-16 22:15:58 -04:00
Tom Moor c3cefc40e5 fix: Improve sidebar loading state 2024-08-15 21:38:21 -04:00
Tom Moor 695476a038 fix: Hide 'duplicate child documents' option when there are none 2024-08-15 21:08:46 -04:00
Tom Moor 1a31cf562e fix: Comment marks should be removed from duplicated documents
closes #7390
2024-08-15 21:01:12 -04:00
Tom Moor d7c13bc8b0 chore: Ensure failed build step exits build 2024-08-15 20:49:25 -04:00
dependabot[bot] 36e8c4796d chore(deps): bump axios from 1.6.8 to 1.7.4 (#7395)
Bumps [axios](https://github.com/axios/axios) from 1.6.8 to 1.7.4.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.6.8...v1.7.4)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-15 17:07:09 -07:00
Tom Moor d3ddb25c76 fix: Issues viewing document comments past default pagination (#7397)
* fix: Default pagination always used for fetchAll

* fix: comments.list does not return total in pagination
2024-08-15 16:56:39 -07:00
Tom Moor 51839dd780 fix: Unable to scroll elements in latest chrome (#7394) 2024-08-15 07:10:51 -07:00
Tom Moor fc2967d080 chore: Bump prosemirror deps for fixes 2024-08-14 20:49:43 -04:00
Tom Moor aba0297bd5 chore: Add PK to group_users, remove deletedAt column (#7369) 2024-08-14 04:03:56 -07:00
Tom Moor dd1df68e74 chore: Refactor @Encrypted decorator (#7381)
* chore: Simplify encrypted decorator

* fix: Correctly handle and type nullable encrypted fields

* docs
2024-08-14 03:54:37 -07:00
Tom Moor d2f5ac3d53 fix: Canva links can contain an underscore 2024-08-14 06:48:10 -04:00
Tom Moor 5eae8734c1 fix: Unresponsive collection permissions, closes #7380 2024-08-13 22:28:23 -04:00
Tom Moor 542f01e36f fix: Disable unused markdown-it parser rules when associated node is not in schema
closes #7383
2024-08-13 21:33:23 -04:00
dependabot[bot] e0dfda6f7e chore(deps): bump prosemirror-tables from 1.3.7 to 1.4.0 (#7374)
Bumps [prosemirror-tables](https://github.com/prosemirror/prosemirror-tables) from 1.3.7 to 1.4.0.
- [Release notes](https://github.com/prosemirror/prosemirror-tables/releases)
- [Commits](https://github.com/prosemirror/prosemirror-tables/compare/v1.3.7...v1.4.0)

---
updated-dependencies:
- dependency-name: prosemirror-tables
  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>
2024-08-12 14:31:16 -07:00
dependabot[bot] 6b7837a8d6 chore(deps): bump react-hook-form from 7.52.1 to 7.52.2 (#7375)
Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 7.52.1 to 7.52.2.
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.52.1...v7.52.2)

---
updated-dependencies:
- dependency-name: react-hook-form
  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>
2024-08-12 14:29:54 -07:00
dependabot[bot] b6c7a61243 chore(deps-dev): bump @types/sequelize from 4.28.19 to 4.28.20 (#7373)
Bumps [@types/sequelize](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/sequelize) from 4.28.19 to 4.28.20.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/sequelize)

---
updated-dependencies:
- dependency-name: "@types/sequelize"
  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>
2024-08-12 14:29:41 -07:00
Tom Moor a54218d9cc fix: Collections not fetched for viewers 2024-08-12 07:55:49 -04:00
Tom Moor a432a7caa3 fix: Show partial data in share popovers when loading 2024-08-11 09:39:35 -04:00
Tom Moor bb8aa3616a fix: Special case resizing image/video to editor width 2024-08-11 09:19:16 -04:00
Tom Moor 949d93bbfd fix: Do not touch updatedAt timestamp on Attachment access 2024-08-11 08:51:30 -04:00
Tom Moor d28e23dd8e fix: Public shared docs not correctly using cache 2024-08-10 20:59:33 -04:00
Tom Moor d8145ac370 fix: Image and video resize calculation 2024-08-10 19:38:00 -04:00
Tom Moor fbc4a7fcbd fix: Prevent attachment key modification 2024-08-10 18:27:30 -04:00
Tom Moor 04ecf14cc8 fix: Flash of scrolled document on nested shared links 2024-08-10 16:57:21 -04:00
Tom Moor 4eae1f1db3 fix: Copied internal links in shared documents are incorrect (#7368)
* Add internalUrlBase option for toJSON

* Return correct internal urls for shared data

* test
2024-08-10 09:36:03 -07:00
Tom Moor fd4ab0077d fix: Protect against 'position out of range' 2024-08-10 08:43:31 -04:00
Tom Moor d6c074102b Remove paranoid deletion from GroupUser (#7356) 2024-08-10 05:16:31 -07:00
Tom Moor 2beab0c274 fix: Mobile toolbar can display on printed docs depending on browser size
closes #7367
2024-08-10 08:02:40 -04:00
Tom Moor 4f35b8ea0d chore: 411 -> 387 lint warnings 2024-08-09 16:11:35 +01:00
Tom Moor e4cbf0a34a fix: Pasting into placeholder should replace placeholder
fix: Improved click behavior for placeholders

closes #7346
2024-08-08 21:56:30 +01:00
Tom Moor d79ce99629 fix: Enter in code block in list exits code block
closes #7363
2024-08-08 21:31:03 +01:00
Apoorv Mishra 8bf488de0b Fetch collections upon initial load (#7358)
* fix: don't hide sidebar if collections are not loaded, loading drafts should be enough

* fix: preload collections

* fix: remove duplicate API call
2024-08-08 12:52:20 -07:00
Tom Moor d420319b28 fix: readTemplate permission false if team unmutable 2024-08-06 22:05:22 +01:00
Tom Moor 413bcfa7de chore: Port zodIconType 2024-08-05 21:33:25 +01:00
dependabot[bot] 363f1fffca chore(deps): bump pg from 8.11.5 to 8.12.0 (#7351)
Bumps [pg](https://github.com/brianc/node-postgres/tree/HEAD/packages/pg) from 8.11.5 to 8.12.0.
- [Changelog](https://github.com/brianc/node-postgres/blob/master/CHANGELOG.md)
- [Commits](https://github.com/brianc/node-postgres/commits/pg@8.12.0/packages/pg)

---
updated-dependencies:
- dependency-name: pg
  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>
2024-08-05 09:53:17 -07:00
dependabot[bot] 3e7b61c9d7 chore(deps-dev): bump eslint-plugin-react from 7.34.3 to 7.35.0 (#7349)
Bumps [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) from 7.34.3 to 7.35.0.
- [Release notes](https://github.com/jsx-eslint/eslint-plugin-react/releases)
- [Changelog](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jsx-eslint/eslint-plugin-react/compare/v7.34.3...v7.35.0)

---
updated-dependencies:
- dependency-name: eslint-plugin-react
  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>
2024-08-05 09:47:22 -07:00
dependabot[bot] ac0488a4d6 chore(deps): bump prosemirror-view from 1.33.8 to 1.33.9 (#7347)
Bumps [prosemirror-view](https://github.com/prosemirror/prosemirror-view) from 1.33.8 to 1.33.9.
- [Changelog](https://github.com/ProseMirror/prosemirror-view/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prosemirror/prosemirror-view/compare/1.33.8...1.33.9)

---
updated-dependencies:
- dependency-name: prosemirror-view
  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>
2024-08-05 09:46:14 -07:00
Hemachandar 41af3a107e chore: Remove emoji column from documents and revisions (#7144)
* chore: Remove emoji column from documents and revisions

* fix: Incorrect icon color on collections in share menu

* Update types.ts

---------

Co-authored-by: Tom Moor <tom.moor@gmail.com>
Co-authored-by: Tom Moor <tom@getoutline.com>
2024-08-05 01:08:20 -07:00
Tom Moor 964ba78d75 fix: Incorrect icon color on collections in share menu 2024-08-04 22:31:47 +01:00
Apoorv Mishra 340109d9a3 Make share dialog scrollable (#7344)
* fix: make share dialog scrollable

* fix: rename

* fix: mobile

* fix: useMaxHeight margin calculation

* cleanup

---------

Co-authored-by: Tom Moor <tom.moor@gmail.com>
2024-08-04 08:59:54 -07:00
Tom Moor 6c430dc747 fix: Do not parse response stream twice on 502 error 2024-08-02 20:03:33 +01:00
Tom Moor 93f12d8846 Shift-Ctrl-c added as editor shortcut for toggling code blocks 2024-08-02 17:39:37 +01:00
dependabot[bot] a93655bf6e chore(deps-dev): bump rollup-plugin-webpack-stats from 0.2.4 to 0.4.1 (#7322)
Bumps [rollup-plugin-webpack-stats](https://github.com/relative-ci/rollup-plugin-webpack-stats) from 0.2.4 to 0.4.1.
- [Release notes](https://github.com/relative-ci/rollup-plugin-webpack-stats/releases)
- [Commits](https://github.com/relative-ci/rollup-plugin-webpack-stats/compare/v0.2.4...v0.4.1)

---
updated-dependencies:
- dependency-name: rollup-plugin-webpack-stats
  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>
2024-08-02 01:32:37 -07:00
Tom Moor e2b4fa456b chore: Improve debugging for BadGatewayError
closes #7307
2024-08-01 22:19:08 +01:00
Tom Moor cd04c4a8bf Improve buildAttachment construction in tests 2024-08-01 22:01:49 +01:00
Tom Moor bf7fb8aa68 fix: User avatar not correct cleaned up, closes #7337 2024-08-01 22:01:22 +01:00
Tom Moor 08a6376947 fix: Improve sanitization on file keys 2024-08-01 20:24:46 +01:00
Tom Moor a120427943 fix: Image download timeouts importing document should not exceed overall request timeout 2024-08-01 09:58:44 +01:00
Translate-O-Tron 59e97eba2b New Crowdin updates (#7284)
* fix: New Spanish translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: New Norwegian Bokmal translations from Crowdin [ci skip]

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

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

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

* fix: New Norwegian Bokmal translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: New Norwegian Bokmal translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

* fix: New French translations from Crowdin [ci skip]
2024-08-01 01:52:23 -07:00
dependabot[bot] 80b59b1174 chore(deps): bump prosemirror-schema-list from 1.3.0 to 1.4.1 (#7324)
Bumps [prosemirror-schema-list](https://github.com/prosemirror/prosemirror-schema-list) from 1.3.0 to 1.4.1.
- [Changelog](https://github.com/ProseMirror/prosemirror-schema-list/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prosemirror/prosemirror-schema-list/compare/1.3.0...1.4.1)

---
updated-dependencies:
- dependency-name: prosemirror-schema-list
  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>
2024-07-31 06:49:17 -07:00
dependabot[bot] 6a17e8deec chore(deps): bump datadog-metrics from 0.11.1 to 0.11.2 (#7323)
Bumps [datadog-metrics](https://github.com/dbader/node-datadog-metrics) from 0.11.1 to 0.11.2.
- [Release notes](https://github.com/dbader/node-datadog-metrics/releases)
- [Commits](https://github.com/dbader/node-datadog-metrics/compare/v0.11.1...v0.11.2)

---
updated-dependencies:
- dependency-name: datadog-metrics
  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>
2024-07-31 03:22:57 -07:00
dependabot[bot] cd0aba119b chore(deps): bump prosemirror-commands from 1.5.2 to 1.6.0 (#7325)
Bumps [prosemirror-commands](https://github.com/prosemirror/prosemirror-commands) from 1.5.2 to 1.6.0.
- [Changelog](https://github.com/ProseMirror/prosemirror-commands/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prosemirror/prosemirror-commands/compare/1.5.2...1.6.0)

---
updated-dependencies:
- dependency-name: prosemirror-commands
  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>
2024-07-31 03:22:45 -07:00
dependabot[bot] eca17ec63d chore(deps-dev): bump @types/react-color from 3.0.10 to 3.0.12 (#7326)
Bumps [@types/react-color](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-color) from 3.0.10 to 3.0.12.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-color)

---
updated-dependencies:
- dependency-name: "@types/react-color"
  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>
2024-07-31 03:22:23 -07:00
Brian Krausz e164c4e7ca Fix icon naming (#7327) 2024-07-30 02:38:15 -04:00
Tom Moor bead9ae79a fix: replaceImagesWithAttachments ran twice during documents.import 2024-07-29 23:50:37 +01:00
Tom Moor 336e424b8b docs 2024-07-27 16:10:09 -04:00
Tom Moor 0bb993634a fix: Allow starring drafts from document lists 2024-07-27 15:48:32 -04:00
Tom Moor 2f26e76b1e chore: Add transactions to stars mutations 2024-07-27 15:47:23 -04:00
Tom Moor 93a89eeef3 chore: Add transaction to integrations.update 2024-07-27 15:41:55 -04:00
Tom Moor 6e6a5014af chore: Add transactions to groups mutations 2024-07-27 15:37:45 -04:00
Tom Moor 3da1945bea perf: Optimize common path in presentDocument to not include JSON parsing 2024-07-27 15:12:11 -04:00
Hemachandar c2fbb31e77 Workspace templates (#7150)
* feat: Workspace templates

Co-authored-by: Tom Moor <tom.moor@gmail.com>
2024-07-27 08:38:16 -07:00
Hemachandar 4c999d00d2 fix: use all properties from zip when importing a collection (#7318)
* fix: use all properties from zip when importing a collection

* use shared defaults
2024-07-27 08:31:00 -07:00
Tom Moor 738449a7d0 fix: Catch Iframely non-json response correctly in lib.
closes #7306
2024-07-27 09:49:56 -04:00
Tom Moor ae80128396 chore: aws-sdk upgrade 2024-07-27 09:47:59 -04:00
Tom Moor 1da5ac0bfe chore: Remove no longer required resolutions 2024-07-27 09:47:59 -04:00
Apoorv Mishra f56f240d9b Remove trailing spaces from search query (#7314)
* fix: tsquery err

* fix: test
2024-07-26 20:27:56 -07:00
github-actions[bot] 7de0ffb7f7 chore: Auto Compress Images (#7310)
Co-authored-by: tommoor <tommoor@users.noreply.github.com>
2024-07-26 05:49:18 -07:00
Baboon 0e667c5d3d add Dropbox embeddings support (#7299)
* add Dropbox embedder support

* Update embeds.ts

---------

Co-authored-by: Tom Moor <tom@getoutline.com>
2024-07-26 05:47:35 -07:00
Tom Moor 465c935879 fix: Remove .at usage, closes #7305 2024-07-26 08:47:09 -04:00
Tom Moor 9c628dfc54 fix: Ensure document changes are saved on unmount (#7293) 2024-07-24 17:16:43 -07:00
Apoorv Mishra 38b11b3f1e Ignore documents.empty_trash for DeliverWebhookTask (#7304) 2024-07-24 16:46:11 -07:00
Apoorv Mishra d7f53acfa2 Suppress document publish notification upon document creation (#7301)
* fix: do not trigger document publish event upon document creation
2024-07-24 06:44:07 -07:00
Tom Moor e033b20d6f fix: Query not taken into account when filtering groups, closes #7291 2024-07-23 22:49:57 -04:00
Tom Moor 87aacae479 Add back and forward shortcuts to shortcut guide 2024-07-23 22:28:25 -04:00
Tom Moor bbe6df19ea Add locking to collection.move mutation
Add removeIndexCollision tests
2024-07-23 22:13:56 -04:00
dependabot[bot] fd851dfbd1 chore(deps): bump react-hook-form from 7.41.5 to 7.52.1 (#7287)
Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 7.41.5 to 7.52.1.
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.41.5...v7.52.1)

---
updated-dependencies:
- dependency-name: react-hook-form
  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>
2024-07-23 04:20:44 -07:00
Tom Moor 1426c4e6ab fix: Index calculation does not take into account user scope (#7294)
* fix: Index calculation does not take into account user scope

* Standardize sort index maxlength to 256
2024-07-23 04:20:30 -07:00
dependabot[bot] 10b33ff91f chore(deps): bump outline-icons from 3.5.0 to 3.6.0 (#7288)
Bumps outline-icons from 3.5.0 to 3.6.0.

---
updated-dependencies:
- dependency-name: outline-icons
  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>
2024-07-22 17:59:24 -07:00
dependabot[bot] 15f58d0e15 chore(deps): bump prosemirror-history from 1.4.0 to 1.4.1 (#7286)
Bumps [prosemirror-history](https://github.com/prosemirror/prosemirror-history) from 1.4.0 to 1.4.1.
- [Changelog](https://github.com/ProseMirror/prosemirror-history/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prosemirror/prosemirror-history/compare/1.4.0...1.4.1)

---
updated-dependencies:
- dependency-name: prosemirror-history
  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>
2024-07-22 17:59:01 -07:00
dependabot[bot] 6f5859a175 chore(deps): bump react-merge-refs from 2.0.2 to 2.1.1 (#7289)
Bumps [react-merge-refs](https://github.com/gregberge/react-merge-refs) from 2.0.2 to 2.1.1.
- [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.2...v2.1.1)

---
updated-dependencies:
- dependency-name: react-merge-refs
  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>
2024-07-22 17:58:45 -07:00
Tom Moor 52776d5add fix: Empty emoji placeholder color 2024-07-22 20:13:42 -04:00
Tom Moor 474baf7b42 fix: Suppress notifications for changes that are not visible 2024-07-22 20:09:03 -04:00
Tom Moor 516ccce51d fix: Add support for new dbdiagram share links, closes #7281 2024-07-21 22:50:02 -04:00
Tom Moor 16c51c10fe docs 2024-07-21 21:24:55 -04:00
Tom Moor d312d00bca Allow function definition for onDelete behavior 2024-07-21 21:24:55 -04:00
Hemachandar e4c1281bf7 fix: replace document links when importing markdown (#7259) 2024-07-21 17:42:36 -07:00
Hemachandar bbbc00baaa fix: Set doc slug on importing collections (#7270)
* fix: Set doc slug on importing collections

* review
2024-07-21 17:42:22 -07:00
Translate-O-Tron 78d5992ad4 New Crowdin updates (#7272) 2024-07-21 17:42:09 -07:00
Brian Krausz f58ce49e3d fix: Error when passed too many user invites (#7282) 2024-07-21 17:41:46 -07:00
Tom Moor f0ba98e936 fix: Upon adding user to a group only the first collection in the group proactively joined 2024-07-21 08:14:51 -04:00
Tom Moor ff34a46361 0.78.0 2024-07-21 08:14:51 -04:00
Tom Moor 7849af6887 Update no-response.yml 2024-07-19 07:38:16 -07:00
Tom Moor 95a87878c3 fix: Groups added to collection are always added with read-only permission 2024-07-19 04:05:44 -07:00
Tom Moor 011ffc450c tsc 2024-07-18 20:18:55 -04:00
Tom Moor 51c5512902 fix: Templates menu not visible after creating document in collection
Regressed in e2213dbfa2
2024-07-18 19:30:50 -04:00
Alexis Lefebvre 76d64f90c1 chore: fix typo on “intitial” (#7273) 2024-07-18 08:00:39 -07:00
Tom Moor f48c05bef3 chore: CollectionGroupMembership -> GroupMembership (#7269)
* chore: CollectionGroupMembership -> GroupMembership

* Backwards compat

* docs
2024-07-17 18:39:13 -07:00
Tom Moor f675a04735 chore: Rename GroupPermission -> GroupMembership (#7214)
* GroupPermission -> GroupMembership

* Add group membership source

* wip
2024-07-17 16:31:20 -07:00
Tom Moor e52719c38e chore: Rename GroupMembership -> GroupUser (#7268)
* Rename GroupMembership -> GroupUserMembership

* GroupUser
2024-07-17 16:14:59 -07:00
Tom Moor cd854d4adb fix: Admins unable to see private shared docs, closes #7264 2024-07-17 07:42:47 -04:00
Tom Moor 7bb7f96008 Add optional logging of all server side requests for self-hosted debugging
related #7221
2024-07-15 21:44:08 -04:00
Translate-O-Tron 6d70b4b9c7 New Crowdin updates (#7240) 2024-07-15 17:53:54 -07:00
Hemachandar 78fd39a0fb fix: unset active collection id when navigating away from a document (#7241) 2024-07-15 17:53:33 -07:00
Tom Moor 8186e23d45 fix: Incorrect policies returned after unpublishing document
Related https://github.com/outline/outline/discussions/7258
2024-07-15 20:28:44 -04:00
Tom Moor e4492a32d2 fix: Table insert controls remain accessible in read-only mode
closes #7249
2024-07-15 20:20:16 -04:00
feingold-ant cb4f610bb4 doc editors can create child docs (#7257) 2024-07-15 16:57:06 -07:00
Ali Afsharzadeh 14a96e7262 Resolve buildx warnings during Docker image creation (#7246) 2024-07-15 16:56:56 -07:00
feingold-ant e2213dbfa2 If a document is created within a collection, publish it automatically. (#7243)
* If a document is created within a collection, publish it automatically.

* automatically publish from app, not server

* remove auto publish from server

* New draft -> New doc
2024-07-15 16:56:43 -07:00
dependabot[bot] e3d7fba239 chore(deps): bump validator from 13.11.0 to 13.12.0 (#7250)
Bumps [validator](https://github.com/validatorjs/validator.js) from 13.11.0 to 13.12.0.
- [Release notes](https://github.com/validatorjs/validator.js/releases)
- [Changelog](https://github.com/validatorjs/validator.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/validatorjs/validator.js/compare/13.11.0...13.12.0)

---
updated-dependencies:
- dependency-name: validator
  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>
2024-07-15 14:42:51 -07:00
dependabot[bot] 33a43077d5 chore(deps-dev): bump @relative-ci/agent from 4.1.10 to 4.2.9 (#7252)
Bumps [@relative-ci/agent](https://github.com/relative-ci/agent) from 4.1.10 to 4.2.9.
- [Release notes](https://github.com/relative-ci/agent/releases)
- [Commits](https://github.com/relative-ci/agent/compare/v4.1.10...v4.2.9)

---
updated-dependencies:
- dependency-name: "@relative-ci/agent"
  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>
2024-07-15 14:42:09 -07:00
dependabot[bot] 4ced6c3b7f chore(deps): bump mailparser from 3.6.7 to 3.7.1 (#7251)
Bumps [mailparser](https://github.com/nodemailer/mailparser) from 3.6.7 to 3.7.1.
- [Release notes](https://github.com/nodemailer/mailparser/releases)
- [Changelog](https://github.com/nodemailer/mailparser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/mailparser/compare/v3.6.7...v3.7.1)

---
updated-dependencies:
- dependency-name: mailparser
  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>
2024-07-15 14:41:09 -07:00
dependabot[bot] 06a40e8b7c chore(deps): bump prosemirror-view from 1.33.7 to 1.33.8 (#7254)
Bumps [prosemirror-view](https://github.com/prosemirror/prosemirror-view) from 1.33.7 to 1.33.8.
- [Changelog](https://github.com/ProseMirror/prosemirror-view/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prosemirror/prosemirror-view/compare/1.33.7...1.33.8)

---
updated-dependencies:
- dependency-name: prosemirror-view
  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>
2024-07-15 14:40:12 -07:00
Tom Moor 6a6c069e4e chore: Bump prosemirror-model 2024-07-14 15:50:13 -04:00
Tom Moor cf3ef2a839 fix: parseAttachmentIds error when node data contains empty href/src
closes #7230
2024-07-14 11:00:20 -04:00
Tom Moor a49f89bda6 Remove 'You saved' language 2024-07-14 09:58:12 -04:00
Tom Moor 82b2defac1 Cast node attrs to string 2024-07-14 09:33:29 -04:00
Vivian Erbenich c05a8cf73a Add AZURE_TENANT_ID enviroment variable for specific azure tenant. (#7028)
* Adds support for specific azure tenant via .env variables.

* remove AZURE_DISPLAY_NAME again

* lint

---------

Co-authored-by: Tom Moor <tom.moor@gmail.com>
2024-07-12 22:02:40 -04:00
Hemachandar 7f5e2cacd8 feat: show frequent languages in code block menu (#7222) 2024-07-12 18:51:48 -07:00
Tom Moor d8e97e0c1f Add POST handling for oidc callback (#7232) 2024-07-12 18:36:07 -07:00
Tom Moor 74bcf152f0 fix: Incorrect icon color on documents pinned to home cards 2024-07-12 08:53:23 -04:00
Tom Moor 57431720b2 Add document icons to editor search results 2024-07-12 08:48:45 -04:00
Tom Moor b8c6cc603b tsc 2024-07-11 08:09:26 -04:00
Tom Moor 71c48bcd47 More possible fixes for #7161 2024-07-11 08:01:40 -04:00
Tom Moor 4ad1148aa7 Make secondary login options non-primary. https://x.com/brian_lovin/status/1810895651217232135 2024-07-10 22:54:13 -04:00
Tom Moor 67c0335099 fix: Icon missing in ghost when dragging collection 2024-07-10 22:45:10 -04:00
Tom Moor c600148f4b fix: Inconsistencies with title icon positioning (#7224) 2024-07-10 19:05:05 -07:00
Tom Moor 299e49625b fix: Icon missing in ghost when dragging sidebar item 2024-07-10 22:04:37 -04:00
Translate-O-Tron 82d84e4859 New Crowdin updates (#7154) 2024-07-10 17:41:47 -07:00
Tom Moor e7d3dac36c fix: Outdated updatedBy returned in document mutations (#7223)
* fix: Outdated updatedBy information returned in document mutations

* tsc

* Update delete mutations to match
2024-07-10 17:41:30 -07:00
Tom Moor 0d0932a6f6 chore: Remove usage of deprecated parameter on client 2024-07-10 19:46:50 -04:00
Tom Moor 37e68413f0 fix: Groups do not appear in collection share UI unless previously loaded 2024-07-10 15:10:11 -04:00
Tom Moor 01bbc48c01 fix: Incorrect dialog padding 2024-07-10 08:27:43 -04:00
Tom Moor f02bbc3942 fix: Missing icons in document explorer, incorrect active styling 2024-07-10 07:57:39 -04:00
Tom Moor 9419e65837 Add OS icons to library, closes #6469 2024-07-08 22:20:14 -04:00
Tom Moor 28cfeba99a fix: Positioning of archived / trashed notices 2024-07-08 21:49:32 -04:00
Tom Moor 2c138687c7 fix: Increase contrast of search highlights in dark mode 2024-07-08 21:28:49 -04:00
Tom Moor f38c948573 Enable TOC by default on publicly shared docs 2024-07-08 21:26:45 -04:00
Tom Moor 1f5ccc3055 Add error state for broken images (#7200) 2024-07-08 17:55:17 -07:00
Max Mykhailenko 58008d84c4 Add support for TLDR viewer links (#7205) 2024-07-08 17:55:06 -07:00
dependabot[bot] ea678e40cc chore(deps): bump react-medium-image-zoom from 5.2.5 to 5.2.7 (#7209)
Bumps [react-medium-image-zoom](https://github.com/rpearce/react-medium-image-zoom) from 5.2.5 to 5.2.7.
- [Release notes](https://github.com/rpearce/react-medium-image-zoom/releases)
- [Changelog](https://github.com/rpearce/react-medium-image-zoom/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rpearce/react-medium-image-zoom/compare/v5.2.5...v5.2.7)

---
updated-dependencies:
- dependency-name: react-medium-image-zoom
  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>
2024-07-08 17:54:43 -07:00
dependabot[bot] 53caddd930 chore(deps): bump prosemirror-inputrules from 1.3.0 to 1.4.0 (#7207)
Bumps [prosemirror-inputrules](https://github.com/prosemirror/prosemirror-inputrules) from 1.3.0 to 1.4.0.
- [Changelog](https://github.com/ProseMirror/prosemirror-inputrules/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prosemirror/prosemirror-inputrules/compare/1.3.0...1.4.0)

---
updated-dependencies:
- dependency-name: prosemirror-inputrules
  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>
2024-07-08 17:54:10 -07:00
dependabot[bot] 6ab182433b chore(deps): bump @fortawesome/react-fontawesome from 0.2.0 to 0.2.2 (#7206)
Bumps [@fortawesome/react-fontawesome](https://github.com/FortAwesome/react-fontawesome) from 0.2.0 to 0.2.2.
- [Release notes](https://github.com/FortAwesome/react-fontawesome/releases)
- [Changelog](https://github.com/FortAwesome/react-fontawesome/blob/0.2.x/CHANGELOG.md)
- [Commits](https://github.com/FortAwesome/react-fontawesome/compare/0.2.0...0.2.2)

---
updated-dependencies:
- dependency-name: "@fortawesome/react-fontawesome"
  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>
2024-07-08 17:53:46 -07:00
dependabot[bot] f01a45a80a chore(deps): bump string-replace-to-array from 2.1.0 to 2.1.1 (#7208)
Bumps [string-replace-to-array](https://github.com/appfigures/string-replace-to-array) from 2.1.0 to 2.1.1.
- [Changelog](https://github.com/appfigures/string-replace-to-array/blob/master/changelog.md)
- [Commits](https://github.com/appfigures/string-replace-to-array/compare/v2.1.0...v2.1.1)

---
updated-dependencies:
- dependency-name: string-replace-to-array
  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>
2024-07-08 17:53:29 -07:00
dependabot[bot] 44366e1462 chore(deps): bump mammoth from 1.7.2 to 1.8.0 (#7210)
Bumps [mammoth](https://github.com/mwilliamson/mammoth.js) from 1.7.2 to 1.8.0.
- [Release notes](https://github.com/mwilliamson/mammoth.js/releases)
- [Changelog](https://github.com/mwilliamson/mammoth.js/blob/master/NEWS)
- [Commits](https://github.com/mwilliamson/mammoth.js/compare/1.7.2...1.8.0)

---
updated-dependencies:
- dependency-name: mammoth
  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>
2024-07-08 17:53:11 -07:00
Hemachandar b3a24e4917 fix: TOC active indicator for last heading (#7204) 2024-07-08 17:51:26 -07:00
Tom Moor 43cf33fc0a fix: Light icons are not responsive to dark theme 2024-07-07 21:06:04 -04:00
Tom Moor 07b6441655 fix: Retain space for hidden TOC to prevent horizontal movement 2024-07-07 20:21:51 -04:00
Tom Moor 49198aafe9 Hide document TOC when empty 2024-07-07 20:15:26 -04:00
Tom Moor ddd103542a fix: Focusing shared document title on mobile causes it to move 2024-07-07 19:56:31 -04:00
Tom Moor 4654dfb658 test: Fix incorrect userProvisioner test 2024-07-07 11:00:10 -04:00
Tom Moor bdcde1aa53 centralize email parsing logic 2024-07-07 10:54:19 -04:00
Tom Moor c484d1defe fix: Error when accessing search from share logged in, 0067d1a58d 2024-07-06 11:49:51 -04:00
Tom Moor 1efd3b6f96 fix: Do not prompt for app install on public shares, closes #7198 2024-07-06 11:10:58 -04:00
Tom Moor e07be1ee5e chore: Remove long deprecated filter options 2024-07-06 10:53:00 -04:00
Tom Moor 0067d1a58d fix: Document data from documents.search with shareId does not include signed asset urls
closes #7196
2024-07-06 10:51:27 -04:00
Tom Moor 17451c180a fix: Header search input is on the wrong side on Drafts page 2024-07-03 21:14:56 -04:00
Tom Moor 4e989e5c44 fix: Resizing sidebars can also select text 2024-07-03 20:37:56 -04:00
Tom Moor 335957d914 fix: Alignment of keyboard help button 2024-07-03 20:31:51 -04:00
Tom Moor 1711d17e25 fix: newMentionIds no longer always in event data 2024-07-03 17:58:15 -04:00
Apoorv Mishra de90f879f1 Prevent modal top from shrinking (#7167)
* fix: prevent modal top from shrinking

* fix: scroll and max height for share modal and popover

* fix: review
2024-07-03 09:11:00 +05:30
Tom Moor 303125b682 chore: Upgrade nodemon 2024-07-02 21:50:32 -04:00
Tom Moor f33026f7b3 chore: Upgrade socket.io deps 2024-07-02 21:47:01 -04:00
Tom Moor 06b5efd18a chore: Update babel, aws-sdk deps 2024-07-02 21:38:21 -04:00
Tom Moor c8e67b969d fix: Gap between icon picker and title 2024-07-02 20:28:37 -04:00
Tom Moor b84851a4c3 tsc 2024-07-02 19:41:48 -04:00
Tom Moor c6408f7b3f fix: CJK content results in long context strings in search results
closes #7183
2024-07-02 19:34:49 -04:00
Tom Moor 18f729b970 Make selected icon in color picker 2px weight to match others 2024-07-02 18:31:41 -04:00
Tom Moor 2f9a7f9a21 Remove random color on document icons, closes #7181 2024-07-02 18:30:34 -04:00
Tom Moor c5b94e50df fix: RangeError when resolving or removing comment across marks, closes #7182 2024-07-02 18:24:23 -04:00
Tom Moor 8a8dad15ef fix: newMentionIds no longer always in event data, closes #7186 2024-07-02 18:03:22 -04:00
dependabot[bot] a8d4a5b587 chore(deps): bump koa from 2.15.0 to 2.15.3 (#7134)
Bumps [koa](https://github.com/koajs/koa) from 2.15.0 to 2.15.3.
- [Changelog](https://github.com/koajs/koa/blob/2.15.3/History.md)
- [Commits](https://github.com/koajs/koa/compare/2.15.0...2.15.3)

---
updated-dependencies:
- dependency-name: koa
  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>
2024-07-02 14:58:03 -07:00
Liam Robinson a67c35257e fixed wrong discord callback url (#7184) 2024-07-02 13:35:55 -07:00
Tom Moor f9dadf5548 fix: Comment resolution control visible to non-editors 2024-07-02 07:52:21 -04:00
Tom Moor 117c4f5009 feat: Comment resolving (#7115) 2024-07-02 03:55:16 -07:00
dependabot[bot] f34557337d chore(deps): bump @octokit/auth-app from 6.0.4 to 6.1.1 (#7173)
Bumps [@octokit/auth-app](https://github.com/octokit/auth-app.js) from 6.0.4 to 6.1.1.
- [Release notes](https://github.com/octokit/auth-app.js/releases)
- [Commits](https://github.com/octokit/auth-app.js/compare/v6.0.4...v6.1.1)

---
updated-dependencies:
- dependency-name: "@octokit/auth-app"
  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>
2024-07-01 12:22:27 -07:00
dependabot[bot] 9b25e623b4 chore(deps-dev): bump @types/randomstring from 1.1.11 to 1.3.0 (#7172)
Bumps [@types/randomstring](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/randomstring) from 1.1.11 to 1.3.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/randomstring)

---
updated-dependencies:
- dependency-name: "@types/randomstring"
  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>
2024-07-01 12:21:28 -07:00
dependabot[bot] fe41824ef2 chore(deps): bump sequelize-cli from 6.6.1 to 6.6.2 (#7174)
Bumps [sequelize-cli](https://github.com/sequelize/cli) from 6.6.1 to 6.6.2.
- [Release notes](https://github.com/sequelize/cli/releases)
- [Changelog](https://github.com/sequelize/cli/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sequelize/cli/compare/v6.6.1...v6.6.2)

---
updated-dependencies:
- dependency-name: sequelize-cli
  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>
2024-07-01 12:21:19 -07:00
dependabot[bot] e9755faf9a chore(deps): bump @sentry/react from 7.99.0 to 7.118.0 (#7175)
Bumps [@sentry/react](https://github.com/getsentry/sentry-javascript) from 7.99.0 to 7.118.0.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/7.118.0/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.99.0...7.118.0)

---
updated-dependencies:
- dependency-name: "@sentry/react"
  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>
2024-07-01 12:21:07 -07:00
Tom Moor 9dcb04b58a fix: Empty space where logo should be on shared docs with no branding
fix: Not using emoji fallback on shared links
2024-07-01 11:04:00 -04:00
Tom Moor d8e571d82d fix: Flickering of resize cursor on sidebar when dragging 2024-07-01 10:49:00 -04:00
Tom Moor 3f4027c6fa fix: Event listener memory leak 2024-07-01 10:42:15 -04:00
Apoorv Mishra e507f09ff9 fix: collection permission check (#7171) 2024-07-01 19:40:35 +05:30
Apoorv Mishra 63ddc31710 Disallow empty comments and comments with only whitespaces (#7156)
* fix: disallow empty comments

* fix: avoid full traversal to validate comment

* fix: text

* fix:review

* fix: review
2024-06-30 11:38:43 +05:30
Tom Moor 5aa5ba0aa1 fix: Possible fix for #7161 2024-06-29 14:47:26 -07:00
Tom Moor ed496bdf60 fix: Flash of mispositioned document loading placeholder 2024-06-29 10:12:06 -07:00
Tom Moor 7201bdb9d8 fix: Flickering suggestion menu selection in Safari 2024-06-28 13:56:37 -07:00
Tom Moor 53e3245b15 fix: Fallback to emoji attribute in document structure 2024-06-26 11:44:17 -04:00
Tom Moor 2c666ddde2 fix: Duplicated documents use text rather than content 2024-06-26 09:20:16 -04:00
Tom Moor 6bb2953e8d 0.78.0-0 2024-06-25 22:21:32 -04:00
Tom Moor 6a1a3eee91 fix: Crash rendering some document hover previews 2024-06-25 22:20:33 -04:00
Tom Moor d03c7b33d3 Cast values in error response, related outline/openapi#5 2024-06-25 21:33:51 -04:00
Translate-O-Tron 355bc33f7c New Crowdin updates (#7128) 2024-06-25 18:19:03 -07:00
Tom Moor bf2378ec81 fix: Iframely is not correctly disabled with no API_KEY in env
closes #7147
2024-06-25 21:10:21 -04:00
Tom Moor 5c999f5327 Add Swedish translations (#7146) 2024-06-25 05:28:42 -07:00
Tom Moor 29a653aaeb fix: Admins cannot query permissions on private collections (#7145)
* fix: Admins have permission to see existence of all collections (in settings)

* fix: Current user filtered from suggestions. As an admin managing other collections this is limiting

* test
2024-06-25 05:28:32 -07:00
Jack Woodgate beabd32e6a fix: Improve SmartText fraction regex pattern (#7141)
Modify fraction regex statements to not match if directly preceded by a character
2024-06-25 05:11:13 -07:00
Tom Moor 77d6797d85 fix: Error choosing 'No access' as bulk import permission 2024-06-24 21:24:43 -04:00
Tom Moor fbd8f5981b fix: Consistently default letter icon content to ? 2024-06-24 21:20:45 -04:00
Tom Moor 8336207c23 fix: Document title steals focus on mount 2024-06-24 21:20:02 -04:00
Tom Moor 3054f34a90 fix: 'Search in collection' appearing in collection menu user does not have access to read documents within 2024-06-24 18:13:01 -04:00
Tom Moor 07a805696d fix: Templatize spuriously appearing in collection menu 2024-06-24 18:08:30 -04:00
dependabot[bot] 142493ddcc chore(deps): bump vite from 5.2.11 to 5.3.1 (#7132)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.2.11 to 5.3.1.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.3.1/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  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>
2024-06-24 14:32:24 -07:00
dependabot[bot] bd18b33b9d chore(deps): bump class-validator from 0.14.0 to 0.14.1 (#7131)
Bumps [class-validator](https://github.com/typestack/class-validator) from 0.14.0 to 0.14.1.
- [Release notes](https://github.com/typestack/class-validator/releases)
- [Changelog](https://github.com/typestack/class-validator/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/typestack/class-validator/compare/v0.14.0...v0.14.1)

---
updated-dependencies:
- dependency-name: class-validator
  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>
2024-06-24 14:32:05 -07:00
dependabot[bot] 03373804fa chore(deps-dev): bump eslint-plugin-react from 7.34.1 to 7.34.3 (#7133)
Bumps [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) from 7.34.1 to 7.34.3.
- [Release notes](https://github.com/jsx-eslint/eslint-plugin-react/releases)
- [Changelog](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jsx-eslint/eslint-plugin-react/compare/v7.34.1...v7.34.3)

---
updated-dependencies:
- dependency-name: eslint-plugin-react
  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>
2024-06-24 14:31:48 -07:00
dependabot[bot] daba308440 chore(deps): bump nodemailer and @types/nodemailer (#7135)
Bumps [nodemailer](https://github.com/nodemailer/nodemailer) and [@types/nodemailer](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/nodemailer). These dependencies needed to be updated together.

Updates `nodemailer` from 6.9.9 to 6.9.14
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v6.9.9...v6.9.14)

Updates `@types/nodemailer` from 6.4.14 to 6.4.15
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/nodemailer)

---
updated-dependencies:
- dependency-name: nodemailer
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: "@types/nodemailer"
  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>
2024-06-24 14:31:36 -07:00
Tom Moor 1451f70b9e Changes plugin interface from 'roles' to 'enabled' method for more flexibility 2024-06-24 08:33:48 -04:00
580 changed files with 20831 additions and 12557 deletions
+2 -2
View File
@@ -12,7 +12,7 @@
"legacy": true
}
],
"@babel/plugin-proposal-class-properties",
"@babel/plugin-transform-class-properties",
[
"transform-inline-environment-variables",
{
@@ -60,4 +60,4 @@
]
}
}
}
}
+1 -1
View File
@@ -88,7 +88,7 @@ jobs:
- run:
name: test
command: |
TESTFILES=$(circleci tests glob "server/**/*.test.ts" | circleci tests split)
TESTFILES=$(circleci tests glob "**/server/**/*.test.ts" | circleci tests split)
yarn test --maxWorkers=2 $TESTFILES
bundle-size:
<<: *defaults
+5 -1
View File
@@ -131,7 +131,7 @@ GITHUB_APP_PRIVATE_KEY=
# => https://discord.com/developers/applications/
#
# When configuring the Client ID, add a redirect URL under "OAuth2":
# https://<URL>/api/discord.callback
# https://<URL>/auth/discord.callback
DISCORD_CLIENT_ID=
DISCORD_CLIENT_SECRET=
@@ -189,6 +189,10 @@ SLACK_VERIFICATION_TOKEN=your_token
SLACK_APP_ID=A0XXXXXXX
SLACK_MESSAGE_ACTIONS=true
# For Dropbox integration, follow these instructions to get the key https://www.dropbox.com/developers/embedder#setup
# and do not forget to whitelist your domain name in the app settings
DROPBOX_APP_KEY=
# Optionally enable Sentry (sentry.io) to track errors and performance,
# and optionally add a Sentry proxy tunnel for bypassing ad blockers in the UI:
# https://docs.sentry.io/platforms/javascript/troubleshooting/#using-the-tunnel-option)
+2 -1
View File
@@ -41,6 +41,7 @@
"@typescript-eslint/no-shadow": [
"warn",
{
"allow": ["transaction"],
"hoist": "all",
"ignoreTypeValueShadow": true
}
@@ -139,4 +140,4 @@
"typescript": {}
}
}
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
# Configuration for probot-no-response - https://github.com/probot/no-response
# Number of days of inactivity before an Issue is closed for lack of response
daysUntilClose: 14
daysUntilClose: 7
# Label requiring a response
responseRequiredLabel: more information needed
+10 -3
View File
@@ -1,5 +1,5 @@
ARG APP_PATH=/opt/outline
FROM outlinewiki/outline-base as base
FROM outlinewiki/outline-base AS base
ARG APP_PATH
WORKDIR $APP_PATH
@@ -11,7 +11,7 @@ LABEL org.opencontainers.image.source="https://github.com/outline/outline"
ARG APP_PATH
WORKDIR $APP_PATH
ENV NODE_ENV production
ENV NODE_ENV=production
COPY --from=base $APP_PATH/build ./build
COPY --from=base $APP_PATH/server ./server
@@ -20,6 +20,11 @@ COPY --from=base $APP_PATH/.sequelizerc ./.sequelizerc
COPY --from=base $APP_PATH/node_modules ./node_modules
COPY --from=base $APP_PATH/package.json ./package.json
# Install wget to healthcheck the server
RUN apt-get update \
&& apt-get install -y wget \
&& rm -rf /var/lib/apt/lists/*
# Create a non-root user compatible with Debian and BusyBox based images
RUN addgroup --gid 1001 nodejs && \
adduser --uid 1001 --ingroup nodejs nodejs && \
@@ -27,7 +32,7 @@ RUN addgroup --gid 1001 nodejs && \
mkdir -p /var/lib/outline && \
chown -R nodejs:nodejs /var/lib/outline
ENV FILE_STORAGE_LOCAL_ROOT_DIR /var/lib/outline/data
ENV FILE_STORAGE_LOCAL_ROOT_DIR=/var/lib/outline/data
RUN mkdir -p "$FILE_STORAGE_LOCAL_ROOT_DIR" && \
chown -R nodejs:nodejs "$FILE_STORAGE_LOCAL_ROOT_DIR" && \
chmod 1777 "$FILE_STORAGE_LOCAL_ROOT_DIR"
@@ -36,5 +41,7 @@ VOLUME /var/lib/outline/data
USER nodejs
HEALTHCHECK --interval=1m CMD wget -qO- "http://localhost:${PORT:-3000}/_health" | grep -q "OK" || exit 1
EXPOSE 3000
CMD ["yarn", "start"]
+1 -6
View File
@@ -6,10 +6,6 @@ WORKDIR $APP_PATH
COPY ./package.json ./yarn.lock ./
COPY ./patches ./patches
RUN apt-get update \
&& apt-get install -y wget \
&& rm -rf /var/lib/apt/lists/*
RUN yarn install --no-optional --frozen-lockfile --network-timeout 1000000 && \
yarn cache clean
@@ -22,5 +18,4 @@ RUN rm -rf node_modules
RUN yarn install --production=true --frozen-lockfile --network-timeout 1000000 && \
yarn cache clean
ENV PORT 3000
HEALTHCHECK CMD wget -qO- http://localhost:${PORT}/_health | grep -q "OK" || exit 1
ENV PORT=3000
+3 -3
View File
@@ -3,8 +3,8 @@ Business Source License 1.1
Parameters
Licensor: General Outline, Inc.
Licensed Work: Outline 0.71.0
The Licensed Work is (c) 2020 General Outline, Inc.
Licensed Work: Outline 0.80.2
The Licensed Work is (c) 2024 General Outline, Inc.
Additional Use Grant: You may make use of the Licensed Work, provided that
you may not use the Licensed Work for a Document
Service.
@@ -15,7 +15,7 @@ Additional Use Grant: You may make use of the Licensed Work, provided that
Licensed Work by creating teams and documents
controlled by such third parties.
Change Date: 2027-08-18
Change Date: 2028-09-26
Change License: Apache License, Version 2.0
+8 -2
View File
@@ -3,7 +3,13 @@
"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": {
@@ -212,4 +218,4 @@
"required": false
}
}
}
}
+43 -18
View File
@@ -4,6 +4,7 @@ import {
PadlockIcon,
PlusIcon,
SearchIcon,
ShapesIcon,
StarredIcon,
TrashIcon,
UnstarredIcon,
@@ -18,10 +19,10 @@ import DynamicCollectionIcon from "~/components/Icons/CollectionIcon";
import SharePopover from "~/components/Sharing/Collection/SharePopover";
import { getHeaderExpandedKey } from "~/components/Sidebar/components/Header";
import { createAction } from "~/actions";
import { CollectionSection } from "~/actions/sections";
import { ActiveCollectionSection, CollectionSection } from "~/actions/sections";
import { setPersistedState } from "~/hooks/usePersistedState";
import history from "~/utils/history";
import { searchPath } from "~/utils/routeHelpers";
import { newTemplatePath, searchPath } from "~/utils/routeHelpers";
const ColorCollectionIcon = ({ collection }: { collection: Collection }) => (
<DynamicCollectionIcon collection={collection} />
@@ -69,9 +70,9 @@ export const editCollection = createAction({
name: ({ t, isContextMenu }) =>
isContextMenu ? `${t("Edit")}` : t("Edit collection"),
analyticsName: "Edit collection",
section: CollectionSection,
section: ActiveCollectionSection,
icon: <EditIcon />,
visible: ({ stores, activeCollectionId }) =>
visible: ({ activeCollectionId }) =>
!!activeCollectionId &&
stores.policies.abilities(activeCollectionId).update,
perform: ({ t, activeCollectionId }) => {
@@ -95,12 +96,12 @@ export const editCollectionPermissions = createAction({
name: ({ t, isContextMenu }) =>
isContextMenu ? `${t("Permissions")}` : t("Collection permissions"),
analyticsName: "Collection permissions",
section: CollectionSection,
section: ActiveCollectionSection,
icon: <PadlockIcon />,
visible: ({ stores, activeCollectionId }) =>
visible: ({ activeCollectionId }) =>
!!activeCollectionId &&
stores.policies.abilities(activeCollectionId).update,
perform: ({ t, stores, activeCollectionId }) => {
perform: ({ t, activeCollectionId }) => {
if (!activeCollectionId) {
return;
}
@@ -111,6 +112,7 @@ export const editCollectionPermissions = createAction({
stores.dialogs.openModal({
title: t("Share this collection"),
style: { marginBottom: -12 },
content: (
<SharePopover
collection={collection}
@@ -125,9 +127,11 @@ export const editCollectionPermissions = createAction({
export const searchInCollection = createAction({
name: ({ t }) => t("Search in collection"),
analyticsName: "Search collection",
section: CollectionSection,
section: ActiveCollectionSection,
icon: <SearchIcon />,
visible: ({ activeCollectionId }) => !!activeCollectionId,
visible: ({ activeCollectionId }) =>
!!activeCollectionId &&
stores.policies.abilities(activeCollectionId).readDocument,
perform: ({ activeCollectionId }) => {
history.push(searchPath(undefined, { collectionId: activeCollectionId }));
},
@@ -136,10 +140,10 @@ export const searchInCollection = createAction({
export const starCollection = createAction({
name: ({ t }) => t("Star"),
analyticsName: "Star collection",
section: CollectionSection,
section: ActiveCollectionSection,
icon: <StarredIcon />,
keywords: "favorite bookmark",
visible: ({ activeCollectionId, stores }) => {
visible: ({ activeCollectionId }) => {
if (!activeCollectionId) {
return false;
}
@@ -149,7 +153,7 @@ export const starCollection = createAction({
stores.policies.abilities(activeCollectionId).star
);
},
perform: async ({ activeCollectionId, stores }) => {
perform: async ({ activeCollectionId }) => {
if (!activeCollectionId) {
return;
}
@@ -163,10 +167,10 @@ export const starCollection = createAction({
export const unstarCollection = createAction({
name: ({ t }) => t("Unstar"),
analyticsName: "Unstar collection",
section: CollectionSection,
section: ActiveCollectionSection,
icon: <UnstarredIcon />,
keywords: "unfavorite unbookmark",
visible: ({ activeCollectionId, stores }) => {
visible: ({ activeCollectionId }) => {
if (!activeCollectionId) {
return false;
}
@@ -176,7 +180,7 @@ export const unstarCollection = createAction({
stores.policies.abilities(activeCollectionId).unstar
);
},
perform: async ({ activeCollectionId, stores }) => {
perform: async ({ activeCollectionId }) => {
if (!activeCollectionId) {
return;
}
@@ -189,16 +193,16 @@ export const unstarCollection = createAction({
export const deleteCollection = createAction({
name: ({ t }) => `${t("Delete")}`,
analyticsName: "Delete collection",
section: CollectionSection,
section: ActiveCollectionSection,
dangerous: true,
icon: <TrashIcon />,
visible: ({ activeCollectionId, stores }) => {
visible: ({ activeCollectionId }) => {
if (!activeCollectionId) {
return false;
}
return stores.policies.abilities(activeCollectionId).delete;
},
perform: ({ activeCollectionId, stores, t }) => {
perform: ({ activeCollectionId, t }) => {
if (!activeCollectionId) {
return;
}
@@ -220,6 +224,27 @@ export const deleteCollection = createAction({
},
});
export const createTemplate = createAction({
name: ({ t }) => t("New template"),
analyticsName: "New template",
section: ActiveCollectionSection,
icon: <ShapesIcon />,
keywords: "new create template",
visible: ({ activeCollectionId }) =>
!!(
!!activeCollectionId &&
stores.policies.abilities(activeCollectionId).createDocument
),
perform: ({ activeCollectionId, event }) => {
if (!activeCollectionId) {
return;
}
event?.preventDefault();
event?.stopPropagation();
history.push(newTemplatePath(activeCollectionId));
},
});
export const rootCollectionActions = [
openCollection,
createCollection,
+90
View File
@@ -0,0 +1,90 @@
import { DoneIcon, TrashIcon } from "outline-icons";
import * as React from "react";
import { toast } from "sonner";
import stores from "~/stores";
import Comment from "~/models/Comment";
import CommentDeleteDialog from "~/components/CommentDeleteDialog";
import history from "~/utils/history";
import { createAction } from "..";
import { DocumentSection } from "../sections";
export const deleteCommentFactory = ({
comment,
onDelete,
}: {
comment: Comment;
onDelete: () => void;
}) =>
createAction({
name: ({ t }) => `${t("Delete")}`,
analyticsName: "Delete comment",
section: DocumentSection,
icon: <TrashIcon />,
keywords: "trash",
dangerous: true,
visible: () => stores.policies.abilities(comment.id).delete,
perform: ({ t, event }) => {
event?.preventDefault();
event?.stopPropagation();
stores.dialogs.openModal({
title: t("Delete comment"),
content: <CommentDeleteDialog comment={comment} onSubmit={onDelete} />,
});
},
});
export const resolveCommentFactory = ({
comment,
onResolve,
}: {
comment: Comment;
onResolve: () => void;
}) =>
createAction({
name: ({ t }) => t("Mark as resolved"),
analyticsName: "Resolve thread",
section: DocumentSection,
icon: <DoneIcon outline />,
visible: () =>
stores.policies.abilities(comment.id).resolve &&
stores.policies.abilities(comment.documentId).update,
perform: async ({ t }) => {
await comment.resolve();
history.replace({
...history.location,
state: null,
});
onResolve();
toast.success(t("Thread resolved"));
},
});
export const unresolveCommentFactory = ({
comment,
onUnresolve,
}: {
comment: Comment;
onUnresolve: () => void;
}) =>
createAction({
name: ({ t }) => t("Mark as unresolved"),
analyticsName: "Unresolve thread",
section: DocumentSection,
icon: <DoneIcon outline />,
visible: () =>
stores.policies.abilities(comment.id).unresolve &&
stores.policies.abilities(comment.documentId).update,
perform: async () => {
await comment.unresolve();
history.replace({
...history.location,
state: null,
});
onUnresolve();
},
});
+246 -104
View File
@@ -24,25 +24,37 @@ import {
UnpublishIcon,
PublishIcon,
CommentIcon,
GlobeIcon,
CopyIcon,
EyeIcon,
PadlockIcon,
GlobeIcon,
} from "outline-icons";
import * as React from "react";
import { toast } from "sonner";
import { ExportContentType, TeamPreference } from "@shared/types";
import {
ExportContentType,
TeamPreference,
NavigationNode,
} from "@shared/types";
import { getEventFiles } from "@shared/utils/files";
import DocumentDelete from "~/scenes/DocumentDelete";
import DocumentMove from "~/scenes/DocumentMove";
import DocumentPermanentDelete from "~/scenes/DocumentPermanentDelete";
import DocumentPublish from "~/scenes/DocumentPublish";
import DeleteDocumentsInTrash from "~/scenes/Trash/components/DeleteDocumentsInTrash";
import DocumentTemplatizeDialog from "~/components/DocumentTemplatizeDialog";
import ConfirmationDialog from "~/components/ConfirmationDialog";
import DuplicateDialog from "~/components/DuplicateDialog";
import Icon from "~/components/Icon";
import MarkdownIcon from "~/components/Icons/MarkdownIcon";
import SharePopover from "~/components/Sharing/Document";
import { getHeaderExpandedKey } from "~/components/Sidebar/components/Header";
import DocumentTemplatizeDialog from "~/components/TemplatizeDialog";
import { createAction } from "~/actions";
import { DocumentSection, TrashSection } from "~/actions/sections";
import {
ActiveDocumentSection,
DocumentSection,
TrashSection,
} from "~/actions/sections";
import env from "~/env";
import { setPersistedState } from "~/hooks/usePersistedState";
import history from "~/utils/history";
@@ -56,7 +68,6 @@ import {
documentPath,
urlify,
trashPath,
newTemplatePath,
} from "~/utils/routeHelpers";
export const openDocument = createAction({
@@ -67,23 +78,24 @@ export const openDocument = createAction({
keywords: "go to",
icon: <DocumentIcon />,
children: ({ stores }) => {
const paths = stores.collections.pathsToDocuments;
const nodes = stores.collections.navigationNodes.reduce(
(acc, node) => [...acc, ...node.children],
[] as NavigationNode[]
);
return paths
.filter((path) => path.type === "document")
.map((path) => ({
// Note: using url which includes the slug rather than id here to bust
// cache if the document is renamed
id: path.url,
name: path.title,
icon: function _Icon() {
return stores.documents.get(path.id)?.isStarred ? (
<StarredIcon />
) : null;
},
section: DocumentSection,
perform: () => history.push(path.url),
}));
return nodes.map((item) => ({
// Note: using url which includes the slug rather than id here to bust
// cache if the document is renamed
id: item.url,
name: item.title,
icon: item.icon ? (
<Icon value={item.icon} color={item.color ?? undefined} />
) : (
<DocumentIcon />
),
section: DocumentSection,
perform: () => history.push(item.url),
}));
},
});
@@ -105,9 +117,9 @@ export const createDocument = createAction({
!!currentTeamId && stores.policies.abilities(currentTeamId).createDocument
);
},
perform: ({ activeCollectionId, inStarredSection }) =>
perform: ({ activeCollectionId, sidebarContext }) =>
history.push(newDocumentPath(activeCollectionId), {
starred: inStarredSection,
sidebarContext,
}),
});
@@ -122,11 +134,11 @@ export const createDocumentFromTemplate = createAction({
!!activeDocumentId &&
!!stores.documents.get(activeDocumentId)?.template &&
stores.policies.abilities(currentTeamId).createDocument,
perform: ({ activeCollectionId, activeDocumentId, inStarredSection }) =>
perform: ({ activeCollectionId, activeDocumentId, sidebarContext }) =>
history.push(
newDocumentPath(activeCollectionId, { templateId: activeDocumentId }),
{
starred: inStarredSection,
sidebarContext,
}
),
});
@@ -134,7 +146,7 @@ export const createDocumentFromTemplate = createAction({
export const createNestedDocument = createAction({
name: ({ t }) => t("New nested document"),
analyticsName: "New document",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <NewDocumentIcon />,
keywords: "create",
visible: ({ currentTeamId, activeDocumentId, stores }) =>
@@ -142,16 +154,16 @@ export const createNestedDocument = createAction({
!!activeDocumentId &&
stores.policies.abilities(currentTeamId).createDocument &&
stores.policies.abilities(activeDocumentId).createChildDocument,
perform: ({ activeDocumentId, inStarredSection }) =>
perform: ({ activeDocumentId, sidebarContext }) =>
history.push(newNestedDocumentPath(activeDocumentId), {
starred: inStarredSection,
sidebarContext,
}),
});
export const starDocument = createAction({
name: ({ t }) => t("Star"),
analyticsName: "Star document",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <StarredIcon />,
keywords: "favorite bookmark",
visible: ({ activeDocumentId, stores }) => {
@@ -177,7 +189,7 @@ export const starDocument = createAction({
export const unstarDocument = createAction({
name: ({ t }) => t("Unstar"),
analyticsName: "Unstar document",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <UnstarredIcon />,
keywords: "unfavorite unbookmark",
visible: ({ activeDocumentId, stores }) => {
@@ -203,7 +215,7 @@ export const unstarDocument = createAction({
export const publishDocument = createAction({
name: ({ t }) => t("Publish"),
analyticsName: "Publish document",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <PublishIcon />,
visible: ({ activeDocumentId, stores }) => {
if (!activeDocumentId) {
@@ -224,7 +236,7 @@ export const publishDocument = createAction({
return;
}
if (document?.collectionId) {
if (document?.collectionId || document?.template) {
await document.save(undefined, {
publish: true,
});
@@ -245,7 +257,7 @@ export const publishDocument = createAction({
export const unpublishDocument = createAction({
name: ({ t }) => t("Unpublish"),
analyticsName: "Unpublish document",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <UnpublishIcon />,
visible: ({ activeDocumentId, stores }) => {
if (!activeDocumentId) {
@@ -276,7 +288,7 @@ export const unpublishDocument = createAction({
export const subscribeDocument = createAction({
name: ({ t }) => t("Subscribe"),
analyticsName: "Subscribe to document",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <SubscribeIcon />,
visible: ({ activeDocumentId, stores }) => {
if (!activeDocumentId) {
@@ -304,7 +316,7 @@ export const subscribeDocument = createAction({
export const unsubscribeDocument = createAction({
name: ({ t }) => t("Unsubscribe"),
analyticsName: "Unsubscribe from document",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <UnsubscribeIcon />,
visible: ({ activeDocumentId, stores }) => {
if (!activeDocumentId) {
@@ -332,10 +344,14 @@ export const unsubscribeDocument = createAction({
});
export const shareDocument = createAction({
name: ({ t }) => t("Share"),
name: ({ t }) => `${t("Permissions")}`,
analyticsName: "Share document",
section: DocumentSection,
icon: <GlobeIcon />,
section: ActiveDocumentSection,
icon: <PadlockIcon />,
visible: ({ stores, activeDocumentId }) => {
const can = stores.policies.abilities(activeDocumentId!);
return can.manageUsers || can.share;
},
perform: async ({ activeDocumentId, stores, currentUserId, t }) => {
if (!activeDocumentId || !currentUserId) {
return;
@@ -349,6 +365,7 @@ export const shareDocument = createAction({
}
stores.dialogs.openModal({
style: { marginBottom: -12 },
title: t("Share this document"),
content: (
<SharePopover
@@ -366,7 +383,7 @@ export const shareDocument = createAction({
export const downloadDocumentAsHTML = createAction({
name: ({ t }) => t("HTML"),
analyticsName: "Download document as HTML",
section: DocumentSection,
section: ActiveDocumentSection,
keywords: "html export",
icon: <DownloadIcon />,
iconInContextMenu: false,
@@ -385,7 +402,7 @@ export const downloadDocumentAsHTML = createAction({
export const downloadDocumentAsPDF = createAction({
name: ({ t }) => t("PDF"),
analyticsName: "Download document as PDF",
section: DocumentSection,
section: ActiveDocumentSection,
keywords: "export",
icon: <DownloadIcon />,
iconInContextMenu: false,
@@ -409,7 +426,7 @@ export const downloadDocumentAsPDF = createAction({
export const downloadDocumentAsMarkdown = createAction({
name: ({ t }) => t("Markdown"),
analyticsName: "Download document as Markdown",
section: DocumentSection,
section: ActiveDocumentSection,
keywords: "md markdown export",
icon: <DownloadIcon />,
iconInContextMenu: false,
@@ -429,9 +446,11 @@ export const downloadDocument = createAction({
name: ({ t, isContextMenu }) =>
isContextMenu ? t("Download") : t("Download document"),
analyticsName: "Download document",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <DownloadIcon />,
keywords: "export",
visible: ({ activeDocumentId, stores }) =>
!!activeDocumentId && stores.policies.abilities(activeDocumentId).download,
children: [
downloadDocumentAsHTML,
downloadDocumentAsPDF,
@@ -441,8 +460,10 @@ export const downloadDocument = createAction({
export const copyDocumentAsMarkdown = createAction({
name: ({ t }) => t("Copy as Markdown"),
section: DocumentSection,
section: ActiveDocumentSection,
keywords: "clipboard",
icon: <MarkdownIcon />,
iconInContextMenu: false,
visible: ({ activeDocumentId, stores }) =>
!!activeDocumentId && stores.policies.abilities(activeDocumentId).download,
perform: ({ stores, activeDocumentId, t }) => {
@@ -456,10 +477,33 @@ export const copyDocumentAsMarkdown = createAction({
},
});
export const copyDocumentShareLink = createAction({
name: ({ t }) => t("Copy public link"),
section: ActiveDocumentSection,
keywords: "clipboard share",
icon: <GlobeIcon />,
iconInContextMenu: false,
visible: ({ activeDocumentId, stores }) =>
!!activeDocumentId &&
!!stores.shares.getByDocumentId(activeDocumentId)?.published,
perform: ({ stores, activeDocumentId, t }) => {
if (!activeDocumentId) {
return;
}
const share = stores.shares.getByDocumentId(activeDocumentId);
if (share) {
copy(share.url);
toast.success(t("Link copied to clipboard"));
}
},
});
export const copyDocumentLink = createAction({
name: ({ t }) => t("Copy link"),
section: DocumentSection,
section: ActiveDocumentSection,
keywords: "clipboard",
icon: <CopyIcon />,
iconInContextMenu: false,
visible: ({ activeDocumentId }) => !!activeDocumentId,
perform: ({ stores, activeDocumentId, t }) => {
const document = activeDocumentId
@@ -475,17 +519,17 @@ export const copyDocumentLink = createAction({
export const copyDocument = createAction({
name: ({ t }) => t("Copy"),
analyticsName: "Copy document",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <CopyIcon />,
keywords: "clipboard",
children: [copyDocumentLink, copyDocumentAsMarkdown],
children: [copyDocumentLink, copyDocumentShareLink, copyDocumentAsMarkdown],
});
export const duplicateDocument = createAction({
name: ({ t, isContextMenu }) =>
isContextMenu ? t("Duplicate") : t("Duplicate document"),
analyticsName: "Duplicate document",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <DuplicateIcon />,
keywords: "copy",
visible: ({ activeDocumentId, stores }) =>
@@ -529,7 +573,7 @@ export const pinDocumentToCollection = createAction({
});
},
analyticsName: "Pin document to collection",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <PinIcon />,
iconInContextMenu: false,
visible: ({ activeCollectionId, activeDocumentId, stores }) => {
@@ -565,7 +609,7 @@ export const pinDocumentToCollection = createAction({
export const pinDocumentToHome = createAction({
name: ({ t }) => t("Pin to home"),
analyticsName: "Pin document to home",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <PinIcon />,
iconInContextMenu: false,
visible: ({ activeDocumentId, currentTeamId, stores }) => {
@@ -597,7 +641,7 @@ export const pinDocumentToHome = createAction({
export const pinDocument = createAction({
name: ({ t }) => t("Pin"),
analyticsName: "Pin document",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <PinIcon />,
children: [pinDocumentToCollection, pinDocumentToHome],
});
@@ -605,7 +649,7 @@ export const pinDocument = createAction({
export const searchInDocument = createAction({
name: ({ t }) => t("Search in document"),
analyticsName: "Search document",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <SearchIcon />,
visible: ({ stores, activeDocumentId }) => {
if (!activeDocumentId) {
@@ -623,7 +667,7 @@ export const printDocument = createAction({
name: ({ t, isContextMenu }) =>
isContextMenu ? t("Print") : t("Print document"),
analyticsName: "Print document",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <PrintIcon />,
visible: ({ activeDocumentId }) => !!(activeDocumentId && window.print),
perform: () => {
@@ -658,52 +702,55 @@ export const importDocument = createAction({
const files = getEventFiles(ev);
const file = files[0];
const document = await documents.import(
file,
activeDocumentId,
activeCollectionId,
{
publish: true,
}
);
history.push(document.url);
try {
const document = await documents.import(
file,
activeDocumentId,
activeCollectionId,
{
publish: true,
}
);
history.push(document.url);
} catch (err) {
toast.error(err.message);
throw err;
}
};
input.click();
},
});
export const createTemplate = createAction({
name: ({ t, activeDocumentId }) =>
activeDocumentId ? t("Templatize") : t("New template"),
export const createTemplateFromDocument = createAction({
name: ({ t }) => t("Templatize"),
analyticsName: "Templatize document",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <ShapesIcon />,
keywords: "new create template",
visible: ({ activeCollectionId, activeDocumentId, stores }) => {
if (activeDocumentId) {
const document = stores.documents.get(activeDocumentId);
if (document?.isTemplate || !document?.isActive) {
return false;
}
const document = activeDocumentId
? stores.documents.get(activeDocumentId)
: undefined;
if (document?.isTemplate || !document?.isActive) {
return false;
}
return !!(
!!activeCollectionId &&
stores.policies.abilities(activeCollectionId).update
stores.policies.abilities(activeCollectionId).updateDocument
);
},
perform: ({ activeCollectionId, activeDocumentId, stores, t, event }) => {
perform: ({ activeDocumentId, stores, t, event }) => {
if (!activeDocumentId) {
return;
}
event?.preventDefault();
event?.stopPropagation();
if (activeDocumentId) {
stores.dialogs.openModal({
title: t("Create template"),
content: <DocumentTemplatizeDialog documentId={activeDocumentId} />,
});
} else if (activeCollectionId) {
history.push(newTemplatePath(activeCollectionId));
}
stores.dialogs.openModal({
title: t("Create template"),
content: <DocumentTemplatizeDialog documentId={activeDocumentId} />,
});
},
});
@@ -714,14 +761,14 @@ export const openRandomDocument = createAction({
section: DocumentSection,
icon: <ShuffleIcon />,
perform: ({ stores, activeDocumentId }) => {
const documentPaths = stores.collections.pathsToDocuments.filter(
(path) => path.type === "document" && path.id !== activeDocumentId
);
const documentPath =
documentPaths[Math.round(Math.random() * documentPaths.length)];
const nodes = stores.collections.navigationNodes
.reduce((acc, node) => [...acc, ...node.children], [] as NavigationNode[])
.filter((node) => node.id !== activeDocumentId);
if (documentPath) {
history.push(documentPath.url);
const random = nodes[Math.round(Math.random() * nodes.length)];
if (random) {
history.push(random.url);
}
},
});
@@ -738,11 +785,50 @@ export const searchDocumentsForQuery = (searchQuery: string) =>
visible: ({ location }) => location.pathname !== searchPath(),
});
export const moveDocument = createAction({
name: ({ t }) => t("Move"),
analyticsName: "Move document",
export const moveTemplateToWorkspace = createAction({
name: ({ t }) => t("Move to workspace"),
analyticsName: "Move template to workspace",
section: DocumentSection,
icon: <MoveIcon />,
iconInContextMenu: false,
visible: ({ activeDocumentId, stores }) => {
if (!activeDocumentId) {
return false;
}
const document = stores.documents.get(activeDocumentId);
if (!document || !document.template || document.isWorkspaceTemplate) {
return false;
}
return !!stores.policies.abilities(activeDocumentId).move;
},
perform: async ({ activeDocumentId, stores }) => {
if (activeDocumentId) {
const document = stores.documents.get(activeDocumentId);
if (!document) {
return;
}
await document.move({
collectionId: null,
});
}
},
});
export const moveDocumentToCollection = createAction({
name: ({ activeDocumentId, stores, t }) => {
if (!activeDocumentId) {
return t("Move");
}
const document = stores.documents.get(activeDocumentId);
return document?.template && document?.collectionId
? t("Move to collection")
: t("Move");
},
analyticsName: "Move document",
section: ActiveDocumentSection,
icon: <MoveIcon />,
iconInContextMenu: false,
visible: ({ activeDocumentId, stores }) => {
if (!activeDocumentId) {
return false;
@@ -766,10 +852,48 @@ export const moveDocument = createAction({
},
});
export const moveDocument = createAction({
name: ({ t }) => t("Move"),
analyticsName: "Move document",
section: ActiveDocumentSection,
icon: <MoveIcon />,
visible: ({ activeDocumentId, stores }) => {
if (!activeDocumentId) {
return false;
}
const document = stores.documents.get(activeDocumentId);
// Don't show the button if this is a non-workspace template.
if (!document || (document.template && !document.isWorkspaceTemplate)) {
return false;
}
return !!stores.policies.abilities(activeDocumentId).move;
},
perform: moveDocumentToCollection.perform,
});
export const moveTemplate = createAction({
name: ({ t }) => t("Move"),
analyticsName: "Move document",
section: ActiveDocumentSection,
icon: <MoveIcon />,
visible: ({ activeDocumentId, stores }) => {
if (!activeDocumentId) {
return false;
}
const document = stores.documents.get(activeDocumentId);
// Don't show the menu if this is not a template (or) a workspace template.
if (!document || !document.template || document.isWorkspaceTemplate) {
return false;
}
return !!stores.policies.abilities(activeDocumentId).move;
},
children: [moveTemplateToWorkspace, moveDocumentToCollection],
});
export const archiveDocument = createAction({
name: ({ t }) => t("Archive"),
name: ({ t }) => `${t("Archive")}`,
analyticsName: "Archive document",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <ArchiveIcon />,
visible: ({ activeDocumentId, stores }) => {
if (!activeDocumentId) {
@@ -778,14 +902,30 @@ export const archiveDocument = createAction({
return !!stores.policies.abilities(activeDocumentId).archive;
},
perform: async ({ activeDocumentId, stores, t }) => {
const { dialogs, documents } = stores;
if (activeDocumentId) {
const document = stores.documents.get(activeDocumentId);
const document = documents.get(activeDocumentId);
if (!document) {
return;
}
await document.archive();
toast.success(t("Document archived"));
dialogs.openModal({
title: t("Are you sure you want to archive this document?"),
content: (
<ConfirmationDialog
onSubmit={async () => {
await document.archive();
toast.success(t("Document archived"));
}}
savingText={`${t("Archiving")}`}
>
{t(
"Archiving this document will remove it from the collection and search results."
)}
</ConfirmationDialog>
),
});
}
},
});
@@ -793,7 +933,7 @@ export const archiveDocument = createAction({
export const deleteDocument = createAction({
name: ({ t }) => `${t("Delete")}`,
analyticsName: "Delete document",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <TrashIcon />,
dangerous: true,
visible: ({ activeDocumentId, stores }) => {
@@ -827,7 +967,7 @@ export const deleteDocument = createAction({
export const permanentlyDeleteDocument = createAction({
name: ({ t }) => t("Permanently delete"),
analyticsName: "Permanently delete document",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <CrossIcon />,
dangerous: true,
visible: ({ activeDocumentId, stores }) => {
@@ -882,7 +1022,7 @@ export const permanentlyDeleteDocumentsInTrash = createAction({
export const openDocumentComments = createAction({
name: ({ t }) => t("Comments"),
analyticsName: "Open comments",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <CommentIcon />,
visible: ({ activeDocumentId, stores }) => {
const can = stores.policies.abilities(activeDocumentId ?? "");
@@ -904,7 +1044,7 @@ export const openDocumentComments = createAction({
export const openDocumentHistory = createAction({
name: ({ t }) => t("History"),
analyticsName: "Open document history",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <HistoryIcon />,
visible: ({ activeDocumentId, stores }) => {
const can = stores.policies.abilities(activeDocumentId ?? "");
@@ -925,7 +1065,7 @@ export const openDocumentHistory = createAction({
export const openDocumentInsights = createAction({
name: ({ t }) => t("Insights"),
analyticsName: "Open document insights",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <GraphIcon />,
visible: ({ activeDocumentId, stores }) => {
const can = stores.policies.abilities(activeDocumentId ?? "");
@@ -962,7 +1102,7 @@ export const toggleViewerInsights = createAction({
: t("Enable viewer insights");
},
analyticsName: "Toggle viewer insights",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <EyeIcon />,
visible: ({ activeDocumentId, stores }) => {
const can = stores.policies.abilities(activeDocumentId ?? "");
@@ -987,11 +1127,12 @@ export const rootDocumentActions = [
openDocument,
archiveDocument,
createDocument,
createTemplate,
createTemplateFromDocument,
deleteDocument,
importDocument,
downloadDocument,
copyDocumentLink,
copyDocumentShareLink,
copyDocumentAsMarkdown,
starDocument,
unstarDocument,
@@ -1000,7 +1141,8 @@ export const rootDocumentActions = [
subscribeDocument,
unsubscribeDocument,
duplicateDocument,
moveDocument,
moveTemplateToWorkspace,
moveDocumentToCollection,
openRandomDocument,
permanentlyDeleteDocument,
permanentlyDeleteDocumentsInTrash,
+3 -1
View File
@@ -216,7 +216,9 @@ export const logout = createAction({
perform: async () => {
await stores.auth.logout();
if (env.OIDC_LOGOUT_URI) {
window.location.replace(env.OIDC_LOGOUT_URI);
setTimeout(() => {
window.location.replace(env.OIDC_LOGOUT_URI);
}, 200);
}
},
});
+2 -2
View File
@@ -17,7 +17,7 @@ export const restoreRevision = createAction({
analyticsName: "Restore revision",
icon: <RestoreIcon />,
section: RevisionSection,
visible: ({ activeDocumentId, stores }) =>
visible: ({ activeDocumentId }) =>
!!activeDocumentId && stores.policies.abilities(activeDocumentId).update,
perform: async ({ event, location, activeDocumentId }) => {
event?.preventDefault();
@@ -47,7 +47,7 @@ export const copyLinkToRevision = createAction({
analyticsName: "Copy link to revision",
icon: <LinkIcon />,
section: RevisionSection,
perform: async ({ activeDocumentId, stores, t }) => {
perform: async ({ activeDocumentId, t }) => {
if (!activeDocumentId) {
return;
}
+3 -3
View File
@@ -18,7 +18,7 @@ export const inviteUser = createAction({
icon: <PlusIcon />,
keywords: "team member workspace user",
section: UserSection,
visible: ({ stores }) =>
visible: () =>
stores.policies.abilities(stores.auth.team?.id || "").inviteUser,
perform: ({ t }) => {
stores.dialogs.openModal({
@@ -40,7 +40,7 @@ export const updateUserRoleActionFactory = (user: User, role: UserRole) =>
})}…`,
analyticsName: "Update user role",
section: UserSection,
visible: ({ stores }) => {
visible: () => {
const can = stores.policies.abilities(user.id);
return UserRoleHelper.isRoleHigher(role, user.role)
@@ -70,7 +70,7 @@ export const deleteUserActionFactory = (userId: string) =>
keywords: "leave",
dangerous: true,
section: UserSection,
visible: ({ stores }) => stores.policies.abilities(userId).delete,
visible: () => stores.policies.abilities(userId).delete,
perform: ({ t }) => {
const user = stores.users.get(userId);
if (!user) {
+6
View File
@@ -98,6 +98,11 @@ export function actionToKBar(
)
: [];
const sectionPriority =
typeof action.section !== "string" && "priority" in action.section
? (action.section.priority as number) ?? 0
: 0;
return [
{
id: action.id,
@@ -108,6 +113,7 @@ export function actionToKBar(
keywords: action.keywords ?? "",
shortcut: action.shortcut || [],
icon: resolvedIcon,
priority: (1 + (action.priority ?? 0)) * (1 + (sectionPriority ?? 0)),
perform: action.perform
? () => performAction(action, context)
: undefined,
+20
View File
@@ -2,10 +2,28 @@ import { ActionContext } from "~/types";
export const CollectionSection = ({ t }: ActionContext) => t("Collection");
export const ActiveCollectionSection = ({ t, stores }: ActionContext) => {
const activeCollection = stores.collections.active;
return `${t("Collection")} · ${activeCollection?.name}`;
};
ActiveCollectionSection.priority = 0.8;
export const DeveloperSection = ({ t }: ActionContext) => t("Debug");
export const DocumentSection = ({ t }: ActionContext) => t("Document");
export const ActiveDocumentSection = ({ t, stores }: ActionContext) => {
const activeDocument = stores.documents.active;
return `${t("Document")} · ${activeDocument?.titleWithDefault}`;
};
ActiveDocumentSection.priority = 0.9;
export const RecentSection = ({ t }: ActionContext) => t("Recently viewed");
RecentSection.priority = 1;
export const RevisionSection = ({ t }: ActionContext) => t("Revision");
export const SettingsSection = ({ t }: ActionContext) => t("Settings");
@@ -21,4 +39,6 @@ export const TeamSection = ({ t }: ActionContext) => t("Workspace");
export const RecentSearchesSection = ({ t }: ActionContext) =>
t("Recent searches");
RecentSearchesSection.priority = -0.1;
export const TrashSection = ({ t }: ActionContext) => t("Trash");
+18
View File
@@ -106,6 +106,24 @@ const Analytics: React.FC = ({ children }: Props) => {
});
}, []);
// Umami
React.useEffect(() => {
(env.analytics as PublicEnv["analytics"]).forEach((integration) => {
if (integration.service !== IntegrationService.Umami) {
return;
}
const script = document.createElement("script");
script.defer = true;
script.src = `${integration.settings?.instanceUrl}${integration.settings?.scriptName}`;
script.setAttribute(
"data-website-id",
integration.settings?.measurementId
);
document.getElementsByTagName("head")[0]?.appendChild(script);
});
}, []);
return <>{children}</>;
};
+1 -1
View File
@@ -19,7 +19,7 @@ function ArrowKeyNavigation(
return;
}
if (ev.key === "Escape") {
if (ev.key === "Escape" || ev.key === "Backspace") {
ev.preventDefault();
onEscape(ev);
}
+4 -12
View File
@@ -1,17 +1,14 @@
import { AnimatePresence } from "framer-motion";
import { observer, useLocalStore } from "mobx-react";
import { observer } from "mobx-react";
import * as React from "react";
import { Switch, Route, useLocation, matchPath } from "react-router-dom";
import { TeamPreference } from "@shared/types";
import ErrorSuspended from "~/scenes/ErrorSuspended";
import DocumentContext from "~/components/DocumentContext";
import type { DocumentContextValue } from "~/components/DocumentContext";
import Layout from "~/components/Layout";
import RegisterKeyDown from "~/components/RegisterKeyDown";
import Sidebar from "~/components/Sidebar";
import SidebarRight from "~/components/Sidebar/Right";
import SettingsSidebar from "~/components/Sidebar/Settings";
import type { Editor as TEditor } from "~/editor";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
@@ -25,6 +22,7 @@ import {
matchDocumentSlug as slug,
matchDocumentInsights,
} from "~/utils/routeHelpers";
import { DocumentContextProvider } from "./DocumentContext";
import Fade from "./Fade";
import { PortalContext } from "./Portal";
@@ -50,12 +48,6 @@ const AuthenticatedLayout: React.FC = ({ children }: Props) => {
const can = usePolicy(ui.activeDocumentId);
const canCollection = usePolicy(ui.activeCollectionId);
const team = useCurrentTeam();
const documentContext = useLocalStore<DocumentContextValue>(() => ({
editor: null,
setEditor: (editor: TEditor) => {
documentContext.editor = editor;
},
}));
const goToSearch = (ev: KeyboardEvent) => {
if (!ev.metaKey && !ev.ctrlKey) {
@@ -125,7 +117,7 @@ const AuthenticatedLayout: React.FC = ({ children }: Props) => {
);
return (
<DocumentContext.Provider value={documentContext}>
<DocumentContextProvider>
<PortalContext.Provider value={layoutRef.current}>
<Layout
title={team.name}
@@ -142,7 +134,7 @@ const AuthenticatedLayout: React.FC = ({ children }: Props) => {
</React.Suspense>
</Layout>
</PortalContext.Provider>
</DocumentContext.Provider>
</DocumentContextProvider>
);
};
+1 -1
View File
@@ -4,8 +4,8 @@ import { useTranslation } from "react-i18next";
import styled, { css } from "styled-components";
import { s } from "@shared/styles";
import User from "~/models/User";
import Avatar from "~/components/Avatar";
import Tooltip from "~/components/Tooltip";
import Avatar from "./Avatar";
type Props = {
user: User;
+35
View File
@@ -0,0 +1,35 @@
import { GroupIcon } from "outline-icons";
import * as React from "react";
import { useTheme } from "styled-components";
import Squircle from "@shared/components/Squircle";
import Group from "~/models/Group";
import { AvatarSize } from "../Avatar/Avatar";
type Props = {
/** The group to show an avatar for */
group: Group;
/** The size of the icon, 24px is default to match standard avatars */
size?: number;
/** The color of the avatar */
color?: string;
/** The background color of the avatar */
backgroundColor?: string;
className?: string;
};
export function GroupAvatar({
color,
backgroundColor,
size = AvatarSize.Medium,
className,
}: Props) {
const theme = useTheme();
return (
<Squircle color={color ?? theme.text} size={size} className={className}>
<GroupIcon
color={backgroundColor ?? theme.background}
size={size * 0.75}
/>
</Squircle>
);
}
+2 -1
View File
@@ -1,4 +1,5 @@
import styled from "styled-components";
import { s } from "@shared/styles";
import Flex from "~/components/Flex";
const Initials = styled(Flex)<{
@@ -11,7 +12,7 @@ const Initials = styled(Flex)<{
border-radius: 50%;
width: 100%;
height: 100%;
color: #fff;
color: ${s("white75")};
background-color: ${(props) => props.color};
width: ${(props) => props.size}px;
height: ${(props) => props.size}px;
+4 -3
View File
@@ -1,6 +1,7 @@
import Avatar from "./Avatar";
import Avatar, { IAvatar, AvatarSize } from "./Avatar";
import AvatarWithPresence from "./AvatarWithPresence";
import { GroupAvatar } from "./GroupAvatar";
export { AvatarWithPresence };
export { Avatar, GroupAvatar, AvatarSize, AvatarWithPresence };
export default Avatar;
export type { IAvatar };
+2 -2
View File
@@ -25,7 +25,7 @@ const RealButton = styled(ActionButton)<RealProps>`
background: ${s("accent")};
color: ${s("accentText")};
box-shadow: rgba(0, 0, 0, 0.2) 0px 1px 2px;
border-radius: 4px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
height: 32px;
@@ -105,7 +105,7 @@ const RealButton = styled(ActionButton)<RealProps>`
background: ${lighten(0.05, props.theme.danger)};
}
&.focus-visible {
&:focus-visible {
outline-color: ${darken(0.2, props.theme.danger)} !important;
}
`};
+23 -10
View File
@@ -1,13 +1,13 @@
import filter from "lodash/filter";
import isEqual from "lodash/isEqual";
import sortBy from "lodash/sortBy";
import orderBy from "lodash/orderBy";
import uniq from "lodash/uniq";
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { usePopoverState, PopoverDisclosure } from "reakit/Popover";
import Document from "~/models/Document";
import AvatarWithPresence from "~/components/Avatar/AvatarWithPresence";
import { AvatarWithPresence } from "~/components/Avatar";
import DocumentViews from "~/components/DocumentViews";
import Facepile from "~/components/Facepile";
import NudeButton from "~/components/NudeButton";
@@ -16,9 +16,14 @@ import useCurrentUser from "~/hooks/useCurrentUser";
import useStores from "~/hooks/useStores";
type Props = {
/** The document to display live collaborators for */
document: Document;
};
/**
* Displays a list of live collaborators for a document, including their avatars
* and presence status.
*/
function Collaborators(props: Props) {
const { t } = useTranslation();
const user = useCurrentUser();
@@ -39,15 +44,16 @@ function Collaborators(props: Props) {
// ensure currently present via websocket are always ordered first
const collaborators = React.useMemo(
() =>
sortBy(
orderBy(
filter(
users.orderedData,
(user) =>
(presentIds.includes(user.id) ||
document.collaboratorIds.includes(user.id)) &&
!user.isSuspended
(u) =>
(presentIds.includes(u.id) ||
document.collaboratorIds.includes(u.id)) &&
!u.isSuspended
),
(user) => presentIds.includes(user.id)
[(u) => presentIds.includes(u.id), "id"],
["asc", "asc"]
),
[document.collaboratorIds, users.orderedData, presentIds]
);
@@ -69,12 +75,19 @@ function Collaborators(props: Props) {
placement: "bottom-end",
});
const limit = 8;
return (
<>
<PopoverDisclosure {...popover}>
{(props) => (
<NudeButton width={collaborators.length * 32} height={32} {...props}>
{(popoverProps) => (
<NudeButton
width={Math.min(collaborators.length, limit) * 32}
height={32}
{...popoverProps}
>
<Facepile
limit={limit}
users={collaborators}
renderAvatar={(collaborator) => {
const isPresent = presentIds.includes(collaborator.id);
+15 -15
View File
@@ -18,7 +18,7 @@ import Switch from "~/components/Switch";
import Text from "~/components/Text";
import useBoolean from "~/hooks/useBoolean";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import { Feature, FeatureFlags } from "~/utils/FeatureFlags";
import { EmptySelectValue } from "~/types";
const IconPicker = React.lazy(() => import("~/components/IconPicker"));
@@ -142,8 +142,10 @@ export const CollectionForm = observer(function CollectionForm_({
<InputSelectPermission
ref={field.ref}
value={field.value}
onChange={(value: CollectionPermission) => {
field.onChange(value);
onChange={(
value: CollectionPermission | typeof EmptySelectValue
) => {
field.onChange(value === EmptySelectValue ? null : value);
}}
note={t(
"The default access for workspace members, you can share with more users or groups later."
@@ -153,18 +155,16 @@ export const CollectionForm = observer(function CollectionForm_({
/>
)}
{team.sharing &&
(!collection ||
FeatureFlags.isEnabled(Feature.newCollectionSharing)) && (
<Switch
id="sharing"
label={t("Public document sharing")}
note={t(
"Allow documents within this collection to be shared publicly on the internet."
)}
{...register("sharing")}
/>
)}
{team.sharing && (
<Switch
id="sharing"
label={t("Public document sharing")}
note={t(
"Allow documents within this collection to be shared publicly on the internet."
)}
{...register("sharing")}
/>
)}
<Flex justify="flex-end">
<Button
+6 -3
View File
@@ -1,7 +1,7 @@
import { runInAction } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import { toast } from "sonner";
import Collection from "~/models/Collection";
import useStores from "~/hooks/useStores";
import history from "~/utils/history";
import { CollectionForm, FormData } from "./CollectionForm";
@@ -17,8 +17,11 @@ export const CollectionNew = observer(function CollectionNew_({
const handleSubmit = React.useCallback(
async (data: FormData) => {
try {
const collection = new Collection(data, collections);
await collection.save();
const collection = await collections.save(data);
// Avoid flash of loading state for the new collection, we know it's empty.
runInAction(() => {
collection.documents = [];
});
onSubmit?.();
history.push(collection.path);
} catch (error) {
@@ -6,20 +6,27 @@ import { Portal } from "react-portal";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import { depths, s } from "@shared/styles";
import CommandBarResults from "~/components/CommandBarResults";
import SearchActions from "~/components/SearchActions";
import rootActions from "~/actions/root";
import useCommandBarActions from "~/hooks/useCommandBarActions";
import useSettingsActions from "~/hooks/useSettingsActions";
import useTemplateActions from "~/hooks/useTemplateActions";
import CommandBarResults from "./CommandBarResults";
import useRecentDocumentActions from "./useRecentDocumentActions";
import useSettingsAction from "./useSettingsAction";
import useTemplatesAction from "./useTemplatesAction";
function CommandBar() {
const { t } = useTranslation();
const settingsActions = useSettingsActions();
const templateActions = useTemplateActions();
const recentDocumentActions = useRecentDocumentActions();
const settingsAction = useSettingsAction();
const templatesAction = useTemplatesAction();
const commandBarActions = React.useMemo(
() => [...rootActions, templateActions, settingsActions],
[settingsActions, templateActions]
() => [
...recentDocumentActions,
...rootActions,
templatesAction,
settingsAction,
],
[recentDocumentActions, settingsAction, templatesAction]
);
useCommandBarActions(commandBarActions);
@@ -30,7 +37,9 @@ function CommandBar() {
<Positioner>
<Animator>
<SearchActions />
<SearchInput defaultPlaceholder={t("Type a command or search")} />
<SearchInput
defaultPlaceholder={`${t("Type a command or search")}`}
/>
<CommandBarResults />
</Animator>
</Positioner>
@@ -60,12 +69,15 @@ const Positioner = styled(KBarPositioner)`
`;
const SearchInput = styled(KBarSearch)`
padding: 16px 20px;
width: 100%;
position: relative;
padding: 16px 12px;
margin: 0 8px;
width: calc(100% - 16px);
outline: none;
border: none;
background: ${s("menuBackground")};
color: ${s("text")};
border-bottom: 1px solid ${s("inputBorder")};
&:disabled,
&::placeholder {
@@ -5,7 +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";
import Text from "~/components/Text";
type Props = {
action: ActionImpl;
@@ -69,8 +69,8 @@ function CommandBarItem(
) : (
""
)}
{sc.split("+").map((s) => (
<Key key={s}>{s}</Key>
{sc.split("+").map((key) => (
<Key key={key}>{key}</Key>
))}
</React.Fragment>
))}
@@ -1,8 +1,8 @@
import { useMatches, KBarResults } from "kbar";
import * as React from "react";
import styled from "styled-components";
import { s } from "@shared/styles";
import CommandBarItem from "~/components/CommandBarItem";
import Text from "~/components/Text";
import CommandBarItem from "./CommandBarItem";
export default function CommandBarResults() {
const { results, rootActionId } = useMatches();
@@ -14,7 +14,9 @@ export default function CommandBarResults() {
maxHeight={400}
onRender={({ item, active }) =>
typeof item === "string" ? (
<Header>{item}</Header>
<Header type="tertiary" size="xsmall" ellipsis>
{item}
</Header>
) : (
<CommandBarItem
action={item}
@@ -35,11 +37,10 @@ const Container = styled.div`
}
`;
const Header = styled.h3`
font-size: 13px;
letter-spacing: 0.04em;
const Header = styled(Text).attrs({ as: "h3" })`
letter-spacing: 0.03em;
margin: 0;
padding: 16px 0 4px 20px;
color: ${s("textTertiary")};
height: 36px;
cursor: default;
`;
+3
View File
@@ -0,0 +1,3 @@
import CommandBar from "./CommandBar";
export default CommandBar;
@@ -0,0 +1,35 @@
import { DocumentIcon } from "outline-icons";
import * as React from "react";
import Icon from "~/components/Icon";
import { createAction } from "~/actions";
import { RecentSection } from "~/actions/sections";
import useStores from "~/hooks/useStores";
import history from "~/utils/history";
import { documentPath } from "~/utils/routeHelpers";
const useRecentDocumentActions = (count = 6) => {
const { documents, ui } = useStores();
return React.useMemo(
() =>
documents.recentlyViewed
.filter((document) => document.id !== ui.activeDocumentId)
.slice(0, count)
.map((item) =>
createAction({
name: item.titleWithDefault,
analyticsName: "Recently viewed document",
section: RecentSection,
icon: item.icon ? (
<Icon value={item.icon} color={item.color ?? undefined} />
) : (
<DocumentIcon />
),
perform: () => history.push(documentPath(item)),
})
),
[count, ui.activeDocumentId, documents.recentlyViewed]
);
};
export default useRecentDocumentActions;
@@ -2,10 +2,10 @@ import { SettingsIcon } from "outline-icons";
import * as React from "react";
import { createAction } from "~/actions";
import { NavigationSection } from "~/actions/sections";
import useSettingsConfig from "~/hooks/useSettingsConfig";
import history from "~/utils/history";
import useSettingsConfig from "./useSettingsConfig";
const useSettingsActions = () => {
const useSettingsAction = () => {
const config = useSettingsConfig();
const actions = React.useMemo(
() =>
@@ -38,4 +38,4 @@ const useSettingsActions = () => {
return navigateToSettings;
};
export default useSettingsActions;
export default useSettingsAction;
@@ -3,11 +3,11 @@ import * as React from "react";
import Icon from "~/components/Icon";
import { createAction } from "~/actions";
import { DocumentSection } from "~/actions/sections";
import useStores from "~/hooks/useStores";
import history from "~/utils/history";
import { newDocumentPath } from "~/utils/routeHelpers";
import useStores from "./useStores";
const useTemplatesActions = () => {
const useTemplatesAction = () => {
const { documents } = useStores();
React.useEffect(() => {
@@ -27,13 +27,13 @@ const useTemplatesActions = () => {
<NewDocumentIcon />
),
keywords: "create",
perform: ({ activeCollectionId, inStarredSection }) =>
perform: ({ activeCollectionId, sidebarContext }) =>
history.push(
newDocumentPath(item.collectionId ?? activeCollectionId, {
templateId: item.id,
}),
{
starred: inStarredSection,
sidebarContext,
}
),
})
@@ -60,4 +60,4 @@ const useTemplatesActions = () => {
return newFromTemplate;
};
export default useTemplatesActions;
export default useTemplatesAction;
+62
View File
@@ -0,0 +1,62 @@
import { observer } from "mobx-react";
import * as React from "react";
import { Trans, useTranslation } from "react-i18next";
import { CollectionPermission, NavigationNode } from "@shared/types";
import type Collection from "~/models/Collection";
import ConfirmationDialog from "~/components/ConfirmationDialog";
import useStores from "~/hooks/useStores";
type Props = {
/** The navigation node to move, must represent a document. */
item: NavigationNode;
/** The collection to move the document to. */
collection: Collection;
/** The parent document to move the document under. */
parentDocumentId?: string | null;
/** The index to move the document to. */
index?: number | null;
};
function ConfirmMoveDialog({ collection, item, ...rest }: Props) {
const { documents, dialogs, collections } = useStores();
const { t } = useTranslation();
const prevCollection = collections.get(item.collectionId!);
const accessMapping = {
[CollectionPermission.ReadWrite]: t("view and edit access"),
[CollectionPermission.Read]: t("view only access"),
null: t("no access"),
};
const handleSubmit = async () => {
await documents.move({
documentId: item.id,
collectionId: collection.id,
...rest,
});
dialogs.closeAllModals();
};
return (
<ConfirmationDialog
onSubmit={handleSubmit}
submitText={t("Move document")}
savingText={`${t("Moving")}`}
>
<Trans
defaults="Moving the document <em>{{ title }}</em> to the {{ newCollectionName }} collection will change permission for all workspace members from <em>{{ prevPermission }}</em> to <em>{{ newPermission }}</em>."
values={{
title: item.title,
prevCollectionName: prevCollection?.name,
newCollectionName: collection.name,
prevPermission: accessMapping[prevCollection?.permission || "null"],
newPermission: accessMapping[collection.permission || "null"],
}}
components={{
em: <strong />,
}}
/>
</ConfirmationDialog>
);
}
export default observer(ConfirmMoveDialog);
+3 -4
View File
@@ -2,7 +2,7 @@ import { observer } from "mobx-react";
import { DisconnectedIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled, { useTheme } from "styled-components";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import Fade from "~/components/Fade";
import NudeButton from "~/components/NudeButton";
@@ -11,7 +11,6 @@ import useStores from "~/hooks/useStores";
function ConnectionStatus() {
const { ui } = useStores();
const theme = useTheme();
const { t } = useTranslation();
const codeToMessage = {
@@ -61,7 +60,7 @@ function ConnectionStatus() {
>
<Button>
<Fade>
<DisconnectedIcon color={theme.sidebarText} />
<DisconnectedIcon />
</Fade>
</Button>
</Tooltip>
@@ -72,7 +71,7 @@ const Button = styled(NudeButton)`
display: none;
position: fixed;
bottom: 0;
margin: 24px;
margin: 20px;
transform: translateX(-32px);
${breakpoint("tablet")`
+25 -14
View File
@@ -6,12 +6,13 @@ import { mergeRefs } from "react-merge-refs";
import { MenuItem as BaseMenuItem } from "reakit/Menu";
import styled, { css } from "styled-components";
import breakpoint from "styled-components-breakpoint";
import { s } from "@shared/styles";
import Text from "../Text";
import MenuIconWrapper from "./MenuIconWrapper";
type Props = {
id?: string;
onClick?: (event: React.SyntheticEvent) => void | Promise<void>;
onClick?: (event: React.MouseEvent) => void | Promise<void>;
active?: boolean;
selected?: boolean;
disabled?: boolean;
@@ -43,39 +44,40 @@ const MenuItem = (
) => {
const content = React.useCallback(
(props) => {
// 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 preventDefault = (ev: React.MouseEvent) => {
ev.preventDefault();
ev.stopPropagation();
};
const handleClick = async (ev: React.MouseEvent) => {
hide?.();
if (onClick) {
ev.preventDefault();
preventDefault(ev);
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();
ev.stopPropagation();
};
return (
<MenuAnchor
{...props}
$active={active}
as={onClick ? "button" : as}
onClick={handleClick}
onMouseDown={handleMouseDown}
onPointerDown={preventDefault}
onMouseDown={preventDefault}
ref={mergeRefs([
ref,
props.ref as React.RefObject<HTMLAnchorElement>,
])}
>
{selected !== undefined && (
<MenuIconWrapper aria-hidden>
<SelectedWrapper aria-hidden>
{selected ? <CheckmarkIcon /> : <Spacer />}
</MenuIconWrapper>
</SelectedWrapper>
)}
{icon && <MenuIconWrapper aria-hidden>{icon}</MenuIconWrapper>}
<Title>{children}</Title>
@@ -151,7 +153,7 @@ export const MenuAnchorCSS = css<MenuAnchorProps>`
@media (hover: hover) {
&:hover,
&:focus,
&.focus-visible {
&:focus-visible {
color: ${props.theme.accentText};
background: ${props.dangerous ? props.theme.danger : props.theme.accent};
box-shadow: none;
@@ -195,4 +197,13 @@ export const MenuAnchor = styled.a`
${MenuAnchorCSS}
`;
const SelectedWrapper = styled.span`
width: 24px;
height: 24px;
margin-right: 4px;
margin-left: -8px;
flex-shrink: 0;
color: ${s("textSecondary")};
`;
export default React.forwardRef<HTMLAnchorElement, Props>(MenuItem);
+13 -6
View File
@@ -30,6 +30,7 @@ type Props = Omit<MenuStateReturn, "items"> & {
actions?: (Action | MenuSeparator | MenuHeading)[];
context?: Partial<ActionContext>;
items?: TMenuItem[];
showIcons?: boolean;
};
const Disclosure = styled(ExpandedIcon)`
@@ -98,7 +99,7 @@ export function filterTemplateItems(items: TMenuItem[]): TMenuItem[] {
});
}
function Template({ items, actions, context, ...menu }: Props) {
function Template({ items, actions, context, showIcons, ...menu }: Props) {
const ctx = useActionContext({
isContextMenu: true,
});
@@ -124,7 +125,8 @@ function Template({ items, actions, context, ...menu }: Props) {
if (
iconIsPresentInAnyMenuItem &&
item.type !== "separator" &&
item.type !== "heading"
item.type !== "heading" &&
showIcons !== false
) {
item.icon = item.icon || <MenuIconWrapper aria-hidden />;
}
@@ -138,7 +140,7 @@ function Template({ items, actions, context, ...menu }: Props) {
key={index}
disabled={item.disabled}
selected={item.selected}
icon={item.icon}
icon={showIcons !== false ? item.icon : undefined}
{...menu}
>
{item.title}
@@ -156,7 +158,7 @@ function Template({ items, actions, context, ...menu }: Props) {
selected={item.selected}
level={item.level}
target={item.href.startsWith("#") ? undefined : "_blank"}
icon={item.icon}
icon={showIcons !== false ? item.icon : undefined}
{...menu}
>
{item.title}
@@ -174,7 +176,7 @@ function Template({ items, actions, context, ...menu }: Props) {
selected={item.selected}
dangerous={item.dangerous}
key={index}
icon={item.icon}
icon={showIcons !== false ? item.icon : undefined}
{...menu}
>
{item.title}
@@ -190,7 +192,12 @@ function Template({ items, actions, context, ...menu }: Props) {
id={`${item.title}-${index}`}
templateItems={item.items}
parentMenuState={menu}
title={<Title title={item.title} icon={item.icon} />}
title={
<Title
title={item.title}
icon={showIcons !== false ? item.icon : undefined}
/>
}
{...menu}
/>
);
+54 -3
View File
@@ -6,6 +6,7 @@ import styled, { DefaultTheme } from "styled-components";
import breakpoint from "styled-components-breakpoint";
import { depths, s } from "@shared/styles";
import Scrollable from "~/components/Scrollable";
import useEventListener from "~/hooks/useEventListener";
import useMenuContext from "~/hooks/useMenuContext";
import useMenuHeight from "~/hooks/useMenuHeight";
import useMobile from "~/hooks/useMobile";
@@ -50,6 +51,8 @@ type Props = MenuStateReturn & {
onClick?: (ev: React.MouseEvent) => void;
/** The maximum width of the context menu. */
maxWidth?: number;
/** The minimum height of the context menu. */
minHeight?: number;
children?: React.ReactNode;
};
@@ -134,6 +137,7 @@ type InnerContextMenuProps = MenuStateReturn & {
menuProps: { style?: React.CSSProperties; placement: string };
children: React.ReactNode;
maxWidth?: number;
minHeight?: number;
};
/**
@@ -171,6 +175,32 @@ const InnerContextMenu = (props: InnerContextMenuProps) => {
};
}, [props.isSubMenu, props.visible]);
useEventListener(
"animationstart",
(event) => {
if (event.target instanceof HTMLElement) {
const parent = event.target.parentElement;
if (parent) {
parent.style.pointerEvents = "none";
}
}
},
backgroundRef.current
);
useEventListener(
"animationend",
(event) => {
if (event.target instanceof HTMLElement) {
const parent = event.target.parentElement;
if (parent) {
parent.style.pointerEvents = "auto";
}
}
},
backgroundRef.current
);
const style =
topAnchor && !isMobile
? {
@@ -193,6 +223,7 @@ const InnerContextMenu = (props: InnerContextMenuProps) => {
<Background
dir="auto"
maxWidth={props.maxWidth}
minHeight={props.minHeight}
topAnchor={topAnchor}
rightAnchor={rightAnchor}
ref={backgroundRef}
@@ -223,10 +254,30 @@ export const Position = styled.div`
position: absolute;
z-index: ${depths.menu};
&.focus-visible {
// Note: pointer events are re-enabled after the animation ends, see event listeners above
pointer-events: none;
&:focus-visible {
transition-delay: 250ms;
transition-property: outline-width;
transition-duration: 0;
outline: none;
&:after {
content: "";
position: absolute;
top: 1px;
left: 1px;
right: 1px;
bottom: 1px;
pointer-events: none;
border-radius: 4px;
outline-color: ${s("accent")};
outline-width: initial;
outline-offset: -1px;
outline-style: solid;
}
}
/*
@@ -247,6 +298,7 @@ type BackgroundProps = {
topAnchor?: boolean;
rightAnchor?: boolean;
maxWidth?: number;
minHeight?: number;
theme: DefaultTheme;
};
@@ -258,9 +310,8 @@ export const Background = styled(Scrollable)<BackgroundProps>`
border-radius: 6px;
padding: 6px;
min-width: 180px;
min-height: 44px;
min-height: ${(props) => props.minHeight || 44}px;
max-height: 75vh;
pointer-events: all;
font-weight: normal;
@media print {
@@ -49,7 +49,7 @@ const DefaultCollectionInputSelect = ({
const options = React.useMemo(
() =>
collections.publicCollections.reduce(
collections.nonPrivate.reduce(
(acc, collection) => [
...acc,
{
@@ -78,7 +78,7 @@ const DefaultCollectionInputSelect = ({
},
]
),
[collections.publicCollections, t]
[collections.nonPrivate, t]
);
if (fetching) {
+1
View File
@@ -25,6 +25,7 @@ function Dialogs() {
fullscreen={modal.fullscreen ?? false}
onRequestClose={() => dialogs.closeModal(id)}
title={modal.title}
style={modal.style}
>
{modal.content}
</Modal>
+4 -2
View File
@@ -8,6 +8,7 @@ import Document from "~/models/Document";
import Breadcrumb from "~/components/Breadcrumb";
import Icon from "~/components/Icon";
import CollectionIcon from "~/components/Icons/CollectionIcon";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
import { MenuInternalLink } from "~/types";
import {
@@ -67,14 +68,15 @@ const DocumentBreadcrumb: React.FC<Props> = ({
const collection = document.collectionId
? collections.get(document.collectionId)
: undefined;
const can = usePolicy(collection);
React.useEffect(() => {
void document.loadRelations();
void document.loadRelations({ withoutPolicies: true });
}, [document]);
let collectionNode: MenuInternalLink | undefined;
if (collection) {
if (collection && can.readDocument) {
collectionNode = {
type: "route",
title: collection.name,
+2 -3
View File
@@ -178,12 +178,11 @@ const DocumentSquircle = ({
}) => {
const theme = useTheme();
const iconType = determineIconType(icon)!;
const squircleColor =
iconType === IconType.Outline ? color : theme.slateLight;
const squircleColor = iconType === IconType.SVG ? color : theme.slateLight;
return (
<Squircle color={squircleColor}>
<Icon value={icon} color={theme.white} />
<Icon value={icon} color={theme.white} forceColor />
</Squircle>
);
};
-37
View File
@@ -1,37 +0,0 @@
import * as React from "react";
import { Editor } from "~/editor";
import useIdle from "~/hooks/useIdle";
export type DocumentContextValue = {
/** The current editor instance for this document. */
editor: Editor | null;
/** Set the current editor instance for this document. */
setEditor: (editor: Editor) => void;
};
const DocumentContext = React.createContext<DocumentContextValue>({
editor: null,
// eslint-disable-next-line @typescript-eslint/no-empty-function
setEditor() {},
});
export const useDocumentContext = () => React.useContext(DocumentContext);
const activityEvents = [
"click",
"mousemove",
"DOMMouseScroll",
"mousewheel",
"mousedown",
"touchstart",
"touchmove",
"focus",
];
export const useEditingFocus = () => {
const { editor } = useDocumentContext();
const isIdle = useIdle(3000, activityEvents);
return isIdle && !!editor?.view.hasFocus();
};
export default DocumentContext;
+76
View File
@@ -0,0 +1,76 @@
import { action, computed, observable } from "mobx";
import React, { PropsWithChildren } from "react";
import { Heading } from "@shared/utils/ProsemirrorHelper";
import Document from "~/models/Document";
import { Editor } from "~/editor";
class DocumentContext {
/** The current document */
document?: Document;
/** The editor instance for this document */
editor?: Editor;
@observable
headings: Heading[] = [];
@computed
get hasHeadings() {
return this.headings.length > 0;
}
@action
setDocument = (document: Document) => {
this.document = document;
this.updateState();
};
@action
setEditor = (editor: Editor) => {
this.editor = editor;
this.updateState();
};
@action
updateState = () => {
this.updateHeadings();
this.updateTasks();
};
private updateHeadings() {
const currHeadings = this.editor?.getHeadings() ?? [];
const hasChanged =
currHeadings.map((h) => h.level + h.title).join("") !==
this.headings.map((h) => h.level + h.title).join("");
if (hasChanged) {
this.headings = currHeadings;
}
}
private updateTasks() {
const tasks = this.editor?.getTasks() ?? [];
const total = tasks.length ?? 0;
const completed = tasks.filter((t) => t.completed).length ?? 0;
this.document?.updateTasks(total, completed);
}
}
const Context = React.createContext<DocumentContext | null>(null);
export const useDocumentContext = () => {
const ctx = React.useContext(Context);
if (!ctx) {
throw new Error(
"useDocumentContext must be used within DocumentContextProvider"
);
}
return ctx;
};
export const DocumentContextProvider = ({
children,
}: PropsWithChildren<unknown>) => {
const context = React.useMemo(() => new DocumentContext(), []);
return <Context.Provider value={context}>{children}</Context.Provider>;
};
+2 -2
View File
@@ -11,7 +11,7 @@ import * as React from "react";
import { useTranslation } from "react-i18next";
import AutoSizer from "react-virtualized-auto-sizer";
import { FixedSizeList as List } from "react-window";
import scrollIntoView from "smooth-scroll-into-view-if-needed";
import scrollIntoView from "scroll-into-view-if-needed";
import styled, { useTheme } from "styled-components";
import breakpoint from "styled-components-breakpoint";
import { NavigationNode } from "@shared/types";
@@ -230,7 +230,7 @@ function DocumentExplorer({ onSubmit, onSelect, items }: Props) {
title = node.title;
} else {
const doc = documents.get(node.id);
icon = doc?.icon ?? node.icon;
icon = doc?.icon ?? node.icon ?? node.emoji;
color = doc?.color ?? node.color;
title = doc?.title ?? node.title;
+1
View File
@@ -120,6 +120,7 @@ export const Node = styled.span<{
color: ${props.theme.white};
svg {
color: ${props.theme.white};
fill: ${props.theme.white};
}
`}
@@ -1,7 +1,7 @@
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import scrollIntoView from "smooth-scroll-into-view-if-needed";
import scrollIntoView from "scroll-into-view-if-needed";
import styled from "styled-components";
import { ellipsis } from "@shared/styles";
import { Node as SearchResult } from "~/components/DocumentExplorerNode";
+8 -7
View File
@@ -76,8 +76,7 @@ function DocumentListItem(
const queryIsInTitle =
!!highlight &&
!!document.title.toLowerCase().includes(highlight.toLowerCase());
const canStar =
!document.isDraft && !document.isArchived && !document.isTemplate;
const canStar = !document.isArchived && !document.isTemplate;
return (
<DocumentLink
@@ -111,11 +110,6 @@ function DocumentListItem(
{document.isBadgedNew && document.createdBy?.id !== user.id && (
<Badge yellow>{t("New")}</Badge>
)}
{canStar && (
<StarPositioner>
<StarButton document={document} />
</StarPositioner>
)}
{document.isDraft && showDraft && (
<Tooltip
content={t("Only visible to you")}
@@ -125,6 +119,11 @@ function DocumentListItem(
<Badge>{t("Draft")}</Badge>
</Tooltip>
)}
{canStar && (
<StarPositioner>
<StarButton document={document} />
</StarPositioner>
)}
{document.isTemplate && showTemplate && (
<Badge primary>{t("Template")}</Badge>
)}
@@ -275,6 +274,8 @@ const ResultContext = styled(Highlight)`
font-size: 15px;
margin-top: -0.25em;
margin-bottom: 0.25em;
max-height: 90px;
overflow: hidden;
`;
export default observer(React.forwardRef(DocumentListItem));
+1 -10
View File
@@ -128,15 +128,6 @@ const DocumentMeta: React.FC<Props> = ({
<Time dateTime={publishedAt} addSuffix />
</span>
);
} else if (isDraft) {
content = (
<span>
{lastUpdatedByCurrentUser
? t("You saved")
: t("{{ userName }} saved", { userName })}{" "}
<Time dateTime={updatedAt} addSuffix />
</span>
);
} else {
content = (
<Modified highlight={modifiedSinceViewed && !lastUpdatedByCurrentUser}>
@@ -149,7 +140,7 @@ const DocumentMeta: React.FC<Props> = ({
}
const nestedDocumentsCount = collection
? collection.getDocumentChildren(document.id).length
? collection.getChildrenForDocument(document.id).length
: 0;
const canShowProgressBar = isTasks && !isTemplate;
@@ -1,49 +0,0 @@
import invariant from "invariant";
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
import { useHistory } from "react-router-dom";
import { toast } from "sonner";
import ConfirmationDialog from "~/components/ConfirmationDialog";
import useStores from "~/hooks/useStores";
import { documentPath } from "~/utils/routeHelpers";
type Props = {
documentId: string;
};
function DocumentTemplatizeDialog({ documentId }: Props) {
const history = useHistory();
const { t } = useTranslation();
const { documents } = useStores();
const document = documents.get(documentId);
invariant(document, "Document must exist");
const handleSubmit = React.useCallback(async () => {
const template = await document?.templatize();
if (template) {
history.push(documentPath(template));
toast.success(t("Template created, go ahead and customize it"));
}
}, [document, history, t]);
return (
<ConfirmationDialog
onSubmit={handleSubmit}
submitText={t("Create template")}
savingText={`${t("Creating")}`}
>
<Trans
defaults="Creating a template from <em>{{titleWithDefault}}</em> is a non-destructive action we'll make a copy of the document and turn it into a template that can be used as a starting point for new documents."
values={{
titleWithDefault: document.titleWithDefault,
}}
components={{
em: <strong />,
}}
/>
</ConfirmationDialog>
);
}
export default observer(DocumentTemplatizeDialog);
+1 -1
View File
@@ -6,7 +6,7 @@ import { useTranslation } from "react-i18next";
import { dateLocale, dateToRelative } from "@shared/utils/date";
import Document from "~/models/Document";
import User from "~/models/User";
import Avatar from "~/components/Avatar";
import { Avatar } from "~/components/Avatar";
import ListItem from "~/components/List/Item";
import PaginatedList from "~/components/PaginatedList";
import useCurrentUser from "~/hooks/useCurrentUser";
+2 -2
View File
@@ -70,14 +70,14 @@ function DuplicateDialog({ document, onSubmit }: Props) {
<Text size="small">
<Switch
name="publish"
label={t("Published")}
label={t("Publish")}
labelPosition="right"
checked={publish}
onChange={handlePublishChange}
/>
</Text>
)}
{document.publishedAt && (
{document.publishedAt && document.childDocuments.length > 0 && (
<Text size="small">
<Switch
name="recursive"
+14 -30
View File
@@ -9,7 +9,6 @@ import { mergeRefs } from "react-merge-refs";
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";
@@ -28,6 +27,7 @@ import { NotFoundError } from "~/utils/errors";
import { uploadFile } from "~/utils/files";
import lazyWithRetry from "~/utils/lazyWithRetry";
import DocumentBreadcrumb from "./DocumentBreadcrumb";
import Icon from "./Icon";
const LazyLoadedEditor = lazyWithRetry(() => import("~/editor"));
@@ -42,21 +42,14 @@ export type Props = Optional<
> & {
shareId?: string | undefined;
embedsDisabled?: boolean;
onHeadingsChange?: (headings: Heading[]) => void;
onSynced?: () => Promise<void>;
onPublish?: (event: React.MouseEvent) => void;
editorStyle?: React.CSSProperties;
};
function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
const {
id,
shareId,
onChange,
onHeadingsChange,
onCreateCommentMark,
onDeleteCommentMark,
} = props;
const { id, shareId, onChange, onCreateCommentMark, onDeleteCommentMark } =
props;
const userLocale = useUserLocale();
const locale = dateLocale(userLocale);
const { comments, documents } = useStores();
@@ -64,7 +57,6 @@ function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
const embeds = useEmbeds(!shareId);
const localRef = React.useRef<SharedEditor>();
const preferences = useCurrentUser({ rejectOnEmpty: false })?.preferences;
const previousHeadings = React.useRef<Heading[] | null>(null);
const previousCommentIds = React.useRef<string[]>();
const handleSearchLink = React.useCallback(
@@ -89,6 +81,12 @@ function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
title: document.title,
subtitle: `Updated ${time}`,
url: document.url,
icon: document.icon ? (
<Icon
value={document.icon}
color={document.color ?? undefined}
/>
) : undefined,
},
];
} catch (error) {
@@ -107,6 +105,9 @@ function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
title: document.title,
subtitle: <DocumentBreadcrumb document={document} onlyText />,
url: document.url,
icon: document.icon ? (
<Icon value={document.icon} color={document.color ?? undefined} />
) : undefined,
})),
(document) =>
deburr(document.title)
@@ -202,21 +203,6 @@ function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
[]
);
// Calculate if headings have changed and trigger callback if so
const updateHeadings = React.useCallback(() => {
if (onHeadingsChange) {
const headings = localRef?.current?.getHeadings();
if (
headings &&
headings.map((h) => h.level + h.title).join("") !==
previousHeadings.current?.map((h) => h.level + h.title).join("")
) {
previousHeadings.current = headings;
onHeadingsChange(headings);
}
}
}, [localRef, onHeadingsChange]);
const updateComments = React.useCallback(() => {
if (onCreateCommentMark && onDeleteCommentMark && localRef.current) {
const commentMarks = localRef.current.getComments();
@@ -251,20 +237,18 @@ function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
const handleChange = React.useCallback(
(event) => {
onChange?.(event);
updateHeadings();
updateComments();
},
[onChange, updateComments, updateHeadings]
[onChange, updateComments]
);
const handleRefChanged = React.useCallback(
(node: SharedEditor | null) => {
if (node) {
updateHeadings();
updateComments();
}
},
[updateComments, updateHeadings]
[updateComments]
);
return (
+9 -2
View File
@@ -12,10 +12,11 @@ import * as React from "react";
import { useTranslation } from "react-i18next";
import { useLocation } from "react-router-dom";
import styled, { css } from "styled-components";
import EventBoundary from "@shared/components/EventBoundary";
import { s } from "@shared/styles";
import Document from "~/models/Document";
import Event from "~/models/Event";
import Avatar from "~/components/Avatar";
import { Avatar } from "~/components/Avatar";
import Item, { Actions, Props as ItemProps } from "~/components/List/Item";
import Time from "~/components/Time";
import useStores from "~/hooks/useStores";
@@ -158,7 +159,9 @@ const EventListItem = ({ event, latest, document, ...rest }: Props) => {
}
actions={
isRevision && isActive && event.modelId && !latest ? (
<RevisionMenu document={document} revisionId={event.modelId} />
<StyledEventBoundary>
<RevisionMenu document={document} revisionId={event.modelId} />
</StyledEventBoundary>
) : undefined
}
onMouseEnter={prefetchRevision}
@@ -175,6 +178,10 @@ const BaseItem = React.forwardRef(function _BaseItem(
return <ListItem to={to} ref={ref} {...rest} />;
});
const StyledEventBoundary = styled(EventBoundary)`
height: 24px;
`;
const Subtitle = styled.span`
svg {
margin: -3px;
+1 -2
View File
@@ -3,9 +3,8 @@ import * as React from "react";
import styled from "styled-components";
import { s } from "@shared/styles";
import User from "~/models/User";
import Avatar from "~/components/Avatar";
import { Avatar, AvatarSize } from "~/components/Avatar";
import Flex from "~/components/Flex";
import { AvatarSize } from "./Avatar/Avatar";
type Props = {
users: User[];
+182 -24
View File
@@ -1,18 +1,23 @@
import deburr from "lodash/deburr";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { useMenuState, MenuButton } from "reakit/Menu";
import styled from "styled-components";
import { s } from "@shared/styles";
import type { FetchPageParams } from "~/stores/base/Store";
import Button, { Inner } from "~/components/Button";
import ContextMenu from "~/components/ContextMenu";
import MenuItem from "~/components/ContextMenu/MenuItem";
import Text from "~/components/Text";
import Input, { NativeInput, Outline } from "./Input";
import PaginatedList, { PaginatedItem } from "./PaginatedList";
type TFilterOption = {
interface TFilterOption extends PaginatedItem {
key: string;
label: string;
note?: string;
icon?: React.ReactNode;
};
}
type Props = {
options: TFilterOption[];
@@ -21,6 +26,9 @@ type Props = {
selectedPrefix?: string;
className?: string;
onSelect: (key: string | null | undefined) => void;
showFilter?: boolean;
fetchQuery?: (options: FetchPageParams) => Promise<PaginatedItem[]>;
fetchQueryOptions?: Record<string, string>;
};
const FilterOptions = ({
@@ -30,13 +38,20 @@ const FilterOptions = ({
selectedPrefix = "",
className,
onSelect,
showFilter,
fetchQuery,
fetchQueryOptions,
}: Props) => {
const { t } = useTranslation();
const searchInputRef = React.useRef<HTMLInputElement>(null);
const listRef = React.useRef<HTMLDivElement | null>(null);
const menu = useMenuState({
modal: true,
});
const selectedItems = options.filter((option) =>
selectedKeys.includes(option.key)
);
const [query, setQuery] = React.useState("");
const selectedLabel = selectedItems.length
? selectedItems
@@ -44,6 +59,109 @@ const FilterOptions = ({
.join(", ")
: "";
const renderItem = React.useCallback(
(option: TFilterOption) => (
<MenuItem
key={option.key}
onClick={() => {
onSelect(option.key);
menu.hide();
}}
selected={selectedKeys.includes(option.key)}
{...menu}
>
{option.icon && <Icon>{option.icon}</Icon>}
{option.note ? (
<LabelWithNote>
{option.label}
<Note>{option.note}</Note>
</LabelWithNote>
) : (
option.label
)}
</MenuItem>
),
[menu, onSelect, selectedKeys]
);
const handleFilter = (ev: React.ChangeEvent<HTMLInputElement>) => {
setQuery(ev.target.value);
};
const filteredOptions = React.useMemo(() => {
const normalizedQuery = deburr(query.toLowerCase());
return query
? options
.filter((option) =>
deburr(option.label).toLowerCase().includes(normalizedQuery)
)
// sort options starting with query first
.sort((a, b) => {
const aStartsWith = deburr(a.label)
.toLowerCase()
.startsWith(normalizedQuery);
const bStartsWith = deburr(b.label)
.toLowerCase()
.startsWith(normalizedQuery);
if (aStartsWith && !bStartsWith) {
return -1;
}
if (!aStartsWith && bStartsWith) {
return 1;
}
return 0;
})
: options;
}, [options, query]);
const handleKeyDown = React.useCallback(
(ev: React.KeyboardEvent) => {
if (ev.nativeEvent.isComposing || ev.shiftKey) {
return;
}
switch (ev.key) {
case "Escape":
menu.hide();
break;
case "Enter":
if (filteredOptions.length === 1) {
ev.preventDefault();
onSelect(filteredOptions[0].key);
menu.hide();
}
break;
case "ArrowDown":
ev.preventDefault();
(listRef.current?.firstElementChild as HTMLElement)?.focus();
break;
default:
break;
}
},
[filteredOptions, menu, onSelect]
);
const handleEscapeFromList = React.useCallback((ev: React.KeyboardEvent) => {
searchInputRef.current?.focus();
if (ev.key === "Backspace") {
setQuery((prev) => prev.slice(0, -1));
}
}, []);
React.useEffect(() => {
if (menu.visible) {
searchInputRef.current?.focus();
} else {
setQuery("");
}
}, [menu.visible]);
const showFilterInput = showFilter || options.length > 10;
return (
<div>
<MenuButton {...menu}>
@@ -53,33 +171,73 @@ const FilterOptions = ({
</StyledButton>
)}
</MenuButton>
<ContextMenu aria-label={defaultLabel} {...menu}>
{options.map((option) => (
<MenuItem
key={option.key}
onClick={() => {
onSelect(option.key);
menu.hide();
}}
selected={selectedKeys.includes(option.key)}
{...menu}
>
{option.icon && <Icon>{option.icon}</Icon>}
{option.note ? (
<LabelWithNote>
{option.label}
<Note>{option.note}</Note>
</LabelWithNote>
) : (
option.label
)}
</MenuItem>
))}
<ContextMenu aria-label={defaultLabel} minHeight={66} {...menu}>
<PaginatedList
listRef={listRef}
options={{ query, ...fetchQueryOptions }}
items={filteredOptions}
fetch={fetchQuery}
renderItem={renderItem}
onEscape={handleEscapeFromList}
heading={showFilterInput ? <Spacer /> : undefined}
empty={<Empty />}
/>
{showFilterInput && (
<SearchInput
ref={searchInputRef}
value={query}
onChange={handleFilter}
onKeyDown={handleKeyDown}
placeholder={`${t("Filter")}`}
autoFocus
/>
)}
</ContextMenu>
</div>
);
};
const Empty = () => {
const { t } = useTranslation();
return (
<>
<Spacer />
<Text size="small" type="tertiary" style={{ marginLeft: 6 }}>
{t("No results")}
</Text>
</>
);
};
const Spacer = styled.div`
height: 30px;
`;
const SearchInput = styled(Input)`
position: absolute;
width: 100%;
border: none;
border-top-left-radius: 6px;
border-top-right-radius: 6px;
overflow: hidden;
margin: 0;
top: 0;
left: 0;
right: 0;
${Outline} {
border: none;
border-radius: 0;
border-bottom: 1px solid ${s("inputBorder")};
background: ${s("menuBackground")};
}
${NativeInput} {
font-size: 14px;
}
`;
const Note = styled(Text)`
display: block;
margin: 2px 0;
+3 -8
View File
@@ -5,36 +5,31 @@ import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { MAX_AVATAR_DISPLAY } from "@shared/constants";
import { s } from "@shared/styles";
import CollectionGroupMembership from "~/models/CollectionGroupMembership";
import Group from "~/models/Group";
import GroupMembership from "~/models/GroupMembership";
import GroupMembers from "~/scenes/GroupMembers";
import Facepile from "~/components/Facepile";
import Flex from "~/components/Flex";
import ListItem from "~/components/List/Item";
import Modal from "~/components/Modal";
import useBoolean from "~/hooks/useBoolean";
import useStores from "~/hooks/useStores";
import { hover } from "~/styles";
import NudeButton from "./NudeButton";
type Props = {
group: Group;
membership?: CollectionGroupMembership;
membership?: GroupMembership;
showFacepile?: boolean;
showAvatar?: boolean;
renderActions: (params: { openMembersModal: () => void }) => React.ReactNode;
};
function GroupListItem({ group, showFacepile, renderActions }: Props) {
const { groupMemberships } = useStores();
const { t } = useTranslation();
const [membersModalOpen, setMembersModalOpen, setMembersModalClosed] =
useBoolean();
const memberCount = group.memberCount;
const membershipsInGroup = groupMemberships.inGroup(group.id);
const users = membershipsInGroup
.slice(0, MAX_AVATAR_DISPLAY)
.map((gm) => gm.user);
const users = group.users.slice(0, MAX_AVATAR_DISPLAY);
const overflow = memberCount - users.length;
return (
+2 -1
View File
@@ -2,6 +2,7 @@ import escapeRegExp from "lodash/escapeRegExp";
import * as React from "react";
import replace from "string-replace-to-array";
import styled from "styled-components";
import { s } from "@shared/styles";
type Props = React.HTMLAttributes<HTMLSpanElement> & {
highlight: (string | null | undefined) | RegExp;
@@ -43,7 +44,7 @@ function Highlight({
}
export const Mark = styled.mark`
color: inherit;
color: ${s("text")};
background: transparent;
font-weight: 600;
`;
@@ -1,7 +1,9 @@
import * as React from "react";
import { richExtensions } from "@shared/editor/nodes";
import { UnfurlResourceType, UnfurlResponse } from "@shared/types";
import Editor from "~/components/Editor";
import Flex from "~/components/Flex";
import ErrorBoundary from "../ErrorBoundary";
import {
Preview,
Title,
@@ -21,20 +23,23 @@ const HoverPreviewDocument = React.forwardRef(function _HoverPreviewDocument(
<Preview to={url}>
<Card ref={ref}>
<CardContent>
<Flex column gap={2}>
<Title>{title}</Title>
<Info>{lastActivityByViewer}</Info>
<Description as="div">
<React.Suspense fallback={<div />}>
<Editor
key={id}
defaultValue={summary}
embedsDisabled
readOnly
/>
</React.Suspense>
</Description>
</Flex>
<ErrorBoundary showTitle={false} reloadOnChunkMissing={false}>
<Flex column gap={2}>
<Title>{title}</Title>
<Info>{lastActivityByViewer}</Info>
<Description as="div">
<React.Suspense fallback={<div />}>
<Editor
key={id}
extensions={richExtensions}
defaultValue={summary}
embedsDisabled
readOnly
/>
</React.Suspense>
</Description>
</Flex>
</ErrorBoundary>
</CardContent>
</Card>
</Preview>
@@ -1,8 +1,8 @@
import * as React from "react";
import { Trans } from "react-i18next";
import { UnfurlResourceType, UnfurlResponse } from "@shared/types";
import { Avatar } from "~/components/Avatar";
import Flex from "~/components/Flex";
import Avatar from "../Avatar";
import { IssueStatusIcon } from "../Icons/IssueStatusIcon";
import Text from "../Text";
import Time from "../Time";
@@ -1,7 +1,6 @@
import * as React from "react";
import { UnfurlResourceType, UnfurlResponse } from "@shared/types";
import Avatar from "~/components/Avatar";
import { AvatarSize } from "~/components/Avatar/Avatar";
import { Avatar, AvatarSize } from "~/components/Avatar";
import Flex from "~/components/Flex";
import { Preview, Title, Info, Card, CardContent } from "./Components";
@@ -1,8 +1,8 @@
import * as React from "react";
import { Trans } from "react-i18next";
import { UnfurlResourceType, UnfurlResponse } from "@shared/types";
import { Avatar } from "~/components/Avatar";
import Flex from "~/components/Flex";
import Avatar from "../Avatar";
import { PullRequestIcon } from "../Icons/PullRequestIcon";
import Text from "../Text";
import Time from "../Time";
+62 -32
View File
@@ -1,6 +1,7 @@
import { observer } from "mobx-react";
import { getLuminance } from "polished";
import * as React from "react";
import { randomElement } from "@shared/random";
import styled from "styled-components";
import { IconType } from "@shared/types";
import { IconLibrary } from "@shared/utils/IconLibrary";
import { colorPalette } from "@shared/utils/collections";
@@ -8,13 +9,24 @@ import { determineIconType } from "@shared/utils/icon";
import EmojiIcon from "~/components/Icons/EmojiIcon";
import useStores from "~/hooks/useStores";
import Logger from "~/utils/Logger";
import Flex from "./Flex";
type IconProps = {
export type Props = {
/** The icon to render */
value: string;
/** The color of the icon */
color?: string;
/** The size of the icon */
size?: number;
/** The initial to display if the icon is a letter icon */
initial?: string;
/** Optional additional class name */
className?: string;
/**
* Ensure the color does not change in response to theme and contrast. Should only be
* used in color picker UI.
*/
forceColor?: boolean;
};
const Icon = ({
@@ -22,8 +34,9 @@ const Icon = ({
color,
size = 24,
initial,
forceColor,
className,
}: IconProps) => {
}: Props) => {
const iconType = determineIconType(icon);
if (!iconType) {
@@ -34,14 +47,15 @@ const Icon = ({
}
try {
if (iconType === IconType.Outline) {
if (iconType === IconType.SVG) {
return (
<OutlineIcon
<SVGIcon
value={icon}
color={color}
size={size}
initial={initial}
className={className}
forceColor={forceColor}
/>
);
}
@@ -56,38 +70,54 @@ const Icon = ({
return null;
};
type OutlineIconProps = {
value: string;
color?: string;
size?: number;
initial?: string;
className?: string;
};
const SVGIcon = observer(
({
value: icon,
color: inputColor,
initial,
size,
className,
forceColor,
}: Props) => {
const { ui } = useStores();
const OutlineIcon = ({
value: icon,
color: inputColor,
initial,
size,
className,
}: OutlineIconProps) => {
const { ui } = useStores();
let color = inputColor ?? colorPalette[0];
let color = inputColor ?? randomElement(colorPalette);
// If the chosen icon color is very dark then we invert it in dark mode
if (!forceColor) {
if (ui.resolvedTheme === "dark" && color !== "currentColor") {
color = getLuminance(color) > 0.09 ? color : "currentColor";
}
// If the chosen icon color is very dark then we invert it in dark mode
// otherwise it will be impossible to see against the dark background.
if (!inputColor && ui.resolvedTheme === "dark" && color !== "currentColor") {
color = getLuminance(color) > 0.09 ? color : "currentColor";
// If the chosen icon color is very light then we invert it in light mode
if (ui.resolvedTheme === "light" && color !== "currentColor") {
color = getLuminance(color) < 0.9 ? color : "currentColor";
}
}
const Component = IconLibrary.getComponent(icon);
return (
<Component color={color} size={size} className={className}>
{initial}
</Component>
);
}
);
const Component = IconLibrary.getComponent(icon);
export const IconTitleWrapper = styled(Flex)<{ dir?: string }>`
align-items: center;
justify-content: center;
position: absolute;
top: 3px;
height: 40px;
width: 40px;
return (
<Component color={color} size={size} className={className}>
{initial}
</Component>
);
};
// Always move above TOC
z-index: 1;
${(props: { dir?: string }) =>
props.dir === "rtl" ? "right: -44px" : "left: -44px"};
`;
export default Icon;
@@ -80,8 +80,8 @@ const BuiltinColors = ({
{colorPalette.map((color) => (
<ColorButton
key={color}
color={color}
active={color === activeColor}
$color={color}
$active={color === activeColor}
onClick={() => onClick(color)}
>
<Selected />
@@ -149,29 +149,29 @@ const Container = styled(Flex)`
`;
const Selected = styled.span`
width: 8px;
height: 4px;
border-left: 1px solid white;
border-bottom: 1px solid white;
width: 10px;
height: 5px;
border-left: 2px solid white;
border-bottom: 2px solid white;
transform: translateY(-25%) rotate(-45deg);
`;
const ColorButton = styled(NudeButton)<{ color: string; active: boolean }>`
const ColorButton = styled(NudeButton)<{ $color: string; $active: boolean }>`
display: inline-flex;
justify-content: center;
align-items: center;
width: 24px;
height: 24px;
border-radius: 50%;
background-color: ${({ color }) => color};
background-color: ${({ $color }) => $color};
&: ${hover} {
outline: 2px solid ${s("menuBackground")} !important;
box-shadow: ${({ color }) => `0px 0px 3px 3px ${color}`};
box-shadow: ${({ $color }) => `0px 0px 3px 3px ${$color}`};
}
& ${Selected} {
display: ${({ active }) => (active ? "block" : "none")};
display: ${({ $active }) => ($active ? "block" : "none")};
}
`;
@@ -44,6 +44,7 @@ const Row = ({ index, style, data }: ListChildComponentProps<RowProps>) => {
const Container = styled(FixedSizeList<RowProps>)`
padding: 0px 12px;
overflow-x: hidden !important;
// Needed for the absolutely positioned children
// to respect the VirtualList's padding
@@ -16,7 +16,7 @@ import { IconButton } from "./IconButton";
const BUTTON_SIZE = 32;
type OutlineNode = {
type: IconType.Outline;
type: IconType.SVG;
name: string;
color: string;
initial: string;
@@ -66,7 +66,7 @@ const GridTemplate = (
);
const items = node.icons.map((item) => {
if (item.type === IconType.Outline) {
if (item.type === IconType.SVG) {
return (
<IconButton
key={item.name}
@@ -129,7 +129,7 @@ const IconPanel = ({
const baseIcons: DataNode = {
category,
icons: filteredIcons.map((name, index) => ({
type: IconType.Outline,
type: IconType.SVG,
name,
color,
initial,
@@ -144,7 +144,7 @@ const IconPanel = ({
{
category: DisplayCategory.Frequent,
icons: freqIcons.map((name, index) => ({
type: IconType.Outline,
type: IconType.SVG,
name,
color,
initial,
+11 -14
View File
@@ -20,6 +20,7 @@ import NudeButton from "~/components/NudeButton";
import Popover from "~/components/Popover";
import useMobile from "~/hooks/useMobile";
import useOnClickOutside from "~/hooks/useOnClickOutside";
import usePrevious from "~/hooks/usePrevious";
import useWindowSize from "~/hooks/useWindowSize";
import { hover } from "~/styles";
import EmojiPanel from "./components/EmojiPanel";
@@ -82,6 +83,7 @@ const IconPicker = ({
unstable_offset: [0, 0],
});
const tab = useTabState({ selectedId: defaultTab });
const previouslyVisible = usePrevious(popover.visible);
const popoverWidth = isMobile ? windowWidth : POPOVER_WIDTH;
// In mobile, popover is absolutely positioned to leave 8px on both sides.
@@ -96,7 +98,7 @@ const IconPicker = ({
(ic: string) => {
popover.hide();
const icType = determineIconType(ic);
const finalColor = icType === IconType.Outline ? chosenColor : null;
const finalColor = icType === IconType.SVG ? chosenColor : null;
onChange(ic, finalColor);
},
[popover, onChange, chosenColor]
@@ -108,7 +110,7 @@ const IconPicker = ({
const icType = determineIconType(icon);
// Outline icon set; propagate color change
if (icType === IconType.Outline) {
if (icType === IconType.SVG) {
onChange(icon, c);
}
},
@@ -120,11 +122,6 @@ const IconPicker = ({
onChange(null, null);
}, [popover, onChange]);
const handleQueryChange = React.useCallback(
(q: string) => setQuery(q),
[setQuery]
);
const handlePopoverButtonClick = React.useCallback(
(ev: React.MouseEvent) => {
ev.stopPropagation();
@@ -139,14 +136,14 @@ const IconPicker = ({
// Popover open effect
React.useEffect(() => {
if (popover.visible) {
if (popover.visible && !previouslyVisible) {
onOpen?.();
} else {
} else if (!popover.visible && previouslyVisible) {
onClose?.();
setQuery("");
resetDefaultTab();
}
}, [popover.visible, onOpen, onClose, setQuery, resetDefaultTab]);
}, [popover.visible, previouslyVisible, onOpen, onClose, resetDefaultTab]);
// Custom click outside handling rather than using `hideOnClickOutside` from reakit so that we can
// prevent event bubbling.
@@ -180,7 +177,7 @@ const IconPicker = ({
{iconType && icon ? (
<Icon value={icon} color={color} size={size} initial={initial} />
) : (
<StyledSmileyIcon color={theme.textTertiary} size={size} />
<StyledSmileyIcon color={theme.placeholder} size={size} />
)}
</PopoverButton>
)}
@@ -231,7 +228,7 @@ const IconPicker = ({
}
onIconChange={handleIconChange}
onColorChange={handleIconColorChange}
onQueryChange={handleQueryChange}
onQueryChange={setQuery}
/>
</StyledTabPanel>
<StyledTabPanel {...tab}>
@@ -242,7 +239,7 @@ const IconPicker = ({
popover.visible && tab.selectedId === TAB_NAMES["Emoji"]
}
onEmojiChange={handleIconChange}
onQueryChange={handleQueryChange}
onQueryChange={setQuery}
/>
</StyledTabPanel>
</>
@@ -312,4 +309,4 @@ const StyledTabPanel = styled(TabPanel)`
overflow-y: auto;
`;
export default IconPicker;
export default React.memo(IconPicker);
+6 -2
View File
@@ -1,5 +1,5 @@
import { observer } from "mobx-react";
import { CollectionIcon } from "outline-icons";
import { CollectionIcon, PrivateCollectionIcon } from "outline-icons";
import { getLuminance } from "polished";
import * as React from "react";
import { colorPalette } from "@shared/utils/collections";
@@ -40,8 +40,11 @@ function ResolvedCollectionIcon({
: "currentColor"
: collectionColor);
const Component = collection.isPrivate
? PrivateCollectionIcon
: CollectionIcon;
return (
<CollectionIcon
<Component
color={color}
expanded={expanded}
size={size}
@@ -57,6 +60,7 @@ function ResolvedCollectionIcon({
size={size}
initial={collection.initial}
className={className}
forceColor={inputColor ? true : false}
/>
);
}
+8
View File
@@ -11,13 +11,21 @@ import { searchPath } from "~/utils/routeHelpers";
import Input, { Outline } from "./Input";
type Props = {
/** A string representing where the search started, for tracking. */
source: string;
/** Placeholder text for the input. */
placeholder?: string;
/** Label for the input. */
label?: string;
/** Whether the label should be hidden. */
labelHidden?: boolean;
/** An optional ID of a collection to search within. */
collectionId?: string;
/** The current value of the input. */
value?: string;
/** Event handler for when the input value changes. */
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => unknown;
/** Event handler for when a key is pressed. */
onKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => unknown;
};
+3 -1
View File
@@ -352,7 +352,9 @@ const Wrapper = styled.label<{ short?: boolean }>`
`;
export const Positioner = styled(Position)`
&.focus-visible {
pointer-events: all;
&:focus-visible {
${StyledSelectOption} {
&[aria-selected="true"] {
color: ${(props) => props.theme.white};
-1
View File
@@ -41,7 +41,6 @@ const Layout = React.forwardRef(function Layout_(
<Container column auto ref={ref}>
<Helmet>
<title>{title ? title : env.APP_NAME}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</Helmet>
<SkipNavLink />
+32 -8
View File
@@ -4,6 +4,7 @@ import {
} from "@getoutline/react-roving-tabindex";
import { LocationDescriptor } from "history";
import * as React from "react";
import scrollIntoView from "scroll-into-view-if-needed";
import styled, { useTheme } from "styled-components";
import { s, ellipsis } from "@shared/styles";
import Flex from "~/components/Flex";
@@ -47,22 +48,33 @@ const ListItem = (
keyboardNavigation,
...rest
}: Props,
ref?: React.Ref<HTMLAnchorElement>
ref: React.RefObject<HTMLAnchorElement>
) => {
const theme = useTheme();
const compact = !subtitle;
let itemRef: React.Ref<HTMLAnchorElement> =
let itemRef: React.RefObject<HTMLAnchorElement> =
React.useRef<HTMLAnchorElement>(null);
if (ref) {
itemRef = ref;
}
const { focused, ...rovingTabIndex } = useRovingTabIndex(
itemRef as React.RefObject<HTMLAnchorElement>,
itemRef,
keyboardNavigation || to ? false : true
);
useFocusEffect(focused, itemRef as React.RefObject<HTMLAnchorElement>);
useFocusEffect(focused, itemRef);
const handleFocus = React.useCallback(() => {
if (itemRef.current) {
scrollIntoView(itemRef.current, {
scrollMode: "if-needed",
behavior: "auto",
block: "center",
boundary: window.document.body,
});
}
}, [itemRef]);
const content = (selected: boolean) => (
<>
@@ -110,6 +122,10 @@ const ListItem = (
}
rovingTabIndex.onKeyDown(ev);
}}
onFocus={(ev) => {
rovingTabIndex.onFocus(ev);
handleFocus();
}}
as={NavLink}
to={to}
>
@@ -126,14 +142,22 @@ const ListItem = (
$hover={!!rest.onClick}
{...rest}
{...rovingTabIndex}
onClick={(ev) => {
rest.onClick?.(ev);
rovingTabIndex.onClick(ev);
}}
onClick={
rest.onClick
? (ev) => {
rest.onClick?.(ev);
rovingTabIndex.onClick(ev);
}
: undefined
}
onKeyDown={(ev) => {
rest.onKeyDown?.(ev);
rovingTabIndex.onKeyDown(ev);
}}
onFocus={(ev) => {
rovingTabIndex.onFocus(ev);
handleFocus();
}}
>
{content(false)}
</Wrapper>
+4 -2
View File
@@ -25,6 +25,7 @@ type Props = {
isOpen: boolean;
fullscreen?: boolean;
title?: React.ReactNode;
style?: React.CSSProperties;
onRequestClose: () => void;
};
@@ -33,6 +34,7 @@ const Modal: React.FC<Props> = ({
isOpen,
fullscreen = true,
title = "Untitled",
style,
onRequestClose,
}: Props) => {
const dialog = useDialogState({
@@ -115,7 +117,7 @@ const Modal: React.FC<Props> = ({
column
reverse
>
<SmallContent shadow>
<SmallContent style={style} shadow>
<ErrorBoundary component="div">{children}</ErrorBoundary>
</SmallContent>
<Header>
@@ -254,7 +256,7 @@ const Header = styled(Flex)`
const Small = styled.div`
animation: ${fadeAndScaleIn} 250ms ease;
margin: auto auto;
margin: 25vh auto auto auto;
width: 75vw;
min-width: 350px;
max-width: 450px;
+1 -1
View File
@@ -1,6 +1,6 @@
import { LocationDescriptor, LocationDescriptorObject } from "history";
import * as React from "react";
import { match, NavLink, Route } from "react-router-dom";
import { type match, NavLink, Route } from "react-router-dom";
type Props = React.ComponentProps<typeof NavLink> & {
children?: (
@@ -9,8 +9,7 @@ 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 { Avatar, AvatarSize } from "../Avatar";
import Flex from "../Flex";
import Text from "../Text";
import Time from "../Time";
-1
View File
@@ -29,7 +29,6 @@ const PageTitle = ({ title, favicon }: Props) => {
href={favicon ?? originalShortcutHref}
key={favicon ?? originalShortcutHref}
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</Helmet>
);
};
+12 -4
View File
@@ -13,9 +13,9 @@ import withStores from "~/components/withStores";
import { dateToHeading } from "~/utils/date";
export interface PaginatedItem {
id: string;
createdAt?: string;
id?: string;
updatedAt?: string;
createdAt?: string;
}
type Props<T> = WithTranslation &
@@ -36,6 +36,7 @@ type Props<T> = WithTranslation &
}) => React.ReactNode;
renderHeading?: (name: React.ReactElement<any> | string) => React.ReactNode;
onEscape?: (ev: React.KeyboardEvent<HTMLDivElement>) => void;
listRef?: React.RefObject<HTMLDivElement>;
};
@observer
@@ -196,6 +197,7 @@ class PaginatedList<T extends PaginatedItem> extends React.PureComponent<
onEscape={onEscape}
className={this.props.className}
items={this.itemsToRender}
ref={this.props.listRef}
>
{() => {
let previousHeading = "";
@@ -211,7 +213,11 @@ class PaginatedList<T extends PaginatedItem> extends React.PureComponent<
// Our models have standard date fields, updatedAt > createdAt.
// Get what a heading would look like for this item
const currentDate =
item.updatedAt || item.createdAt || previousHeading;
"updatedAt" in item && item.updatedAt
? item.updatedAt
: "createdAt" in item && item.createdAt
? item.createdAt
: previousHeading;
const currentHeading = dateToHeading(
currentDate,
this.props.t,
@@ -227,7 +233,9 @@ class PaginatedList<T extends PaginatedItem> extends React.PureComponent<
) {
previousHeading = currentHeading;
return (
<React.Fragment key={item.id}>
<React.Fragment
key={"id" in item && item.id ? item.id : index}
>
{renderHeading(currentHeading)}
{children}
</React.Fragment>
+45 -27
View File
@@ -30,13 +30,24 @@ type Props = {
pins: Pin[];
/** Maximum number of pins to display */
limit?: number;
/** Number of placeholder pins to display */
placeholderCount?: number;
/** Whether the user has permission to update pins */
canUpdate?: boolean;
};
function PinnedDocuments({ limit, pins, canUpdate, ...rest }: Props) {
const { documents, collections } = useStores();
function PinnedDocuments({
limit,
pins,
placeholderCount,
canUpdate,
...rest
}: Props) {
const { documents } = useStores();
const [items, setItems] = React.useState(pins.map((pin) => pin.documentId));
const showPlaceholderRef = React.useRef(true);
const showPlaceholder =
placeholderCount && !items.length && showPlaceholderRef.current;
React.useEffect(() => {
setItems(pins.map((pin) => pin.documentId));
@@ -59,9 +70,9 @@ function PinnedDocuments({ limit, pins, canUpdate, ...rest }: Props) {
const { active, over } = event;
if (over && active.id !== over.id) {
setItems((items) => {
const activePos = items.indexOf(active.id as string);
const overPos = items.indexOf(over.id as string);
setItems((existing) => {
const activePos = existing.indexOf(active.id as string);
const overPos = existing.indexOf(over.id as string);
const overIndex = pins[overPos]?.index || null;
const nextIndex = pins[overPos + 1]?.index || null;
@@ -78,20 +89,16 @@ function PinnedDocuments({ limit, pins, canUpdate, ...rest }: Props) {
? fractionalIndex(prevIndex, overIndex)
: fractionalIndex(overIndex, nextIndex),
})
.catch(() => setItems(items));
.catch(() => setItems(existing));
// Update the order in state immediately
return arrayMove(items, activePos, overPos);
return arrayMove(existing, activePos, overPos);
});
}
},
[pins]
);
if (collections.orderedData.length === 0) {
return null;
}
return (
<DndContext
sensors={sensors}
@@ -109,23 +116,34 @@ function PinnedDocuments({ limit, pins, canUpdate, ...rest }: Props) {
>
<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);
{showPlaceholder ? (
Array(placeholderCount)
.fill(undefined)
.map((_, index) => (
<div key={index} style={{ width: 170, height: 180 }} />
))
) : (
<AnimatePresence initial={false}>
{items.map((documentId) => {
const document = documents.get(documentId);
const pin = pins.find((p) => p.documentId === documentId);
return document ? (
<DocumentCard
key={documentId}
document={document}
canUpdatePin={canUpdate}
isDraggable={items.length > 1}
pin={pin}
{...rest}
/>
) : null;
})}
</AnimatePresence>
// Once any document is loaded, never render the placeholder again
showPlaceholderRef.current = false;
return document ? (
<DocumentCard
key={documentId}
document={document}
canUpdatePin={canUpdate}
isDraggable={items.length > 1}
pin={pin}
{...rest}
/>
) : null;
})}
</AnimatePresence>
)}
</List>
</SortableContext>
</ResizingHeightContainer>
+11 -4
View File
@@ -5,8 +5,9 @@ import { s } from "@shared/styles";
import Flex from "~/components/Flex";
import { pulsate } from "~/styles/animations";
export type Props = {
export type Props = React.ComponentProps<typeof Flex> & {
header?: boolean;
width?: number;
height?: number;
minWidth?: number;
maxWidth?: number;
@@ -17,16 +18,22 @@ function PlaceholderText({ minWidth, maxWidth, ...restProps }: Props) {
// We only want to compute the width once so we are storing it inside ref
const widthRef = React.useRef(randomInteger(minWidth || 75, maxWidth || 100));
return <Mask width={widthRef.current} {...restProps} />;
return (
<Mask
width={`${widthRef.current / (restProps.header ? 2 : 1)}%`}
{...restProps}
/>
);
}
const Mask = styled(Flex)<{
width: number;
width: number | string;
height?: number;
delay?: number;
header?: boolean;
}>`
width: ${(props) => (props.header ? props.width / 2 : props.width)}%;
width: ${(props) =>
typeof props.width === "number" ? `${props.width}px` : props.width};
height: ${(props) =>
props.height ? props.height : props.header ? 24 : 18}px;
margin-bottom: 6px;
+32 -29
View File
@@ -1,5 +1,6 @@
import { m, TargetAndTransition } from "framer-motion";
import * as React from "react";
import { mergeRefs } from "react-merge-refs";
import useComponentSize from "~/hooks/useComponentSize";
type Props = {
@@ -18,35 +19,37 @@ type Props = {
/**
* Automatically animates the height of a container based on it's contents.
*/
export function ResizingHeightContainer(props: Props) {
const {
hideOverflow,
children,
config = {
transition: {
duration: 0.1,
ease: "easeInOut",
export const ResizingHeightContainer = React.forwardRef<HTMLDivElement, Props>(
function ResizingHeightContainer_(props, forwardedRef) {
const {
hideOverflow,
children,
config = {
transition: {
duration: 0.1,
ease: "easeInOut",
},
},
},
style,
} = props;
style,
} = props;
const ref = React.useRef<HTMLDivElement>(null);
const { height } = useComponentSize(ref);
const ref = React.useRef<HTMLDivElement>(null);
const { height } = useComponentSize(ref);
return (
<m.div
animate={{
...config,
height: Math.round(height),
}}
style={{
...style,
overflow: hideOverflow ? "hidden" : "inherit",
position: "relative",
}}
>
<div ref={ref}>{children}</div>
</m.div>
);
}
return (
<m.div
animate={{
...config,
height: Math.round(height),
}}
style={{
...style,
overflow: hideOverflow ? "hidden" : "inherit",
position: "relative",
}}
>
<div ref={mergeRefs([ref, forwardedRef])}>{children}</div>
</m.div>
);
}
);
+1 -1
View File
@@ -10,7 +10,7 @@ export default function SearchActions() {
const { searches } = useStores();
React.useEffect(() => {
if (!searches.isLoaded) {
if (!searches.isLoaded && !searches.isFetching) {
void searches.fetchPage({
source: "app",
});
+4 -4
View File
@@ -9,7 +9,7 @@ import Empty from "~/components/Empty";
import { Outline } from "~/components/Input";
import InputSearch from "~/components/InputSearch";
import Placeholder from "~/components/List/Placeholder";
import PaginatedList, { PaginatedItem } from "~/components/PaginatedList";
import PaginatedList from "~/components/PaginatedList";
import Popover from "~/components/Popover";
import { id as bodyContentId } from "~/components/SkipNavContent";
import useKeyDown from "~/hooks/useKeyDown";
@@ -36,11 +36,11 @@ function SearchPopover({ shareId }: Props) {
const { show, hide } = popover;
const [searchResults, setSearchResults] = React.useState<
PaginatedItem[] | undefined
SearchResult[] | undefined
>();
const [cachedQuery, setCachedQuery] = React.useState(query);
const [cachedSearchResults, setCachedSearchResults] = React.useState<
PaginatedItem[] | undefined
SearchResult[] | undefined
>(searchResults);
React.useEffect(() => {
@@ -54,7 +54,7 @@ function SearchPopover({ shareId }: Props) {
const performSearch = React.useCallback(
async ({ query, ...options }) => {
if (query?.length > 0) {
const response: PaginatedItem[] = await documents.search(query, {
const response = await documents.search(query, {
shareId,
...options,
});
@@ -0,0 +1,254 @@
import { observer } from "mobx-react";
import { UserIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled, { useTheme } from "styled-components";
import Squircle from "@shared/components/Squircle";
import { CollectionPermission } from "@shared/types";
import Collection from "~/models/Collection";
import { Avatar, GroupAvatar, AvatarSize } from "~/components/Avatar";
import InputMemberPermissionSelect from "~/components/InputMemberPermissionSelect";
import InputSelectPermission from "~/components/InputSelectPermission";
import Scrollable from "~/components/Scrollable";
import useMaxHeight from "~/hooks/useMaxHeight";
import usePolicy from "~/hooks/usePolicy";
import useRequest from "~/hooks/useRequest";
import useStores from "~/hooks/useStores";
import { EmptySelectValue, Permission } from "~/types";
import { ListItem } from "../components/ListItem";
import { Placeholder } from "../components/Placeholder";
type Props = {
/** Collection to which team members are supposed to be invited */
collection: Collection;
/** Children to be rendered before the list of members */
children?: React.ReactNode;
/** List of users and groups that have been invited during the current editing session */
invitedInSession: string[];
};
export const AccessControlList = observer(
({ collection, invitedInSession }: Props) => {
const { memberships, groupMemberships } = useStores();
const can = usePolicy(collection);
const { t } = useTranslation();
const theme = useTheme();
const collectionId = collection.id;
const { request: fetchMemberships, loading: membershipLoading } =
useRequest(
React.useCallback(
() => memberships.fetchAll({ id: collectionId }),
[memberships, collectionId]
)
);
const { request: fetchGroupMemberships, loading: groupMembershipLoading } =
useRequest(
React.useCallback(
() => groupMemberships.fetchAll({ collectionId }),
[groupMemberships, collectionId]
)
);
const groupMembershipsInCollection =
groupMemberships.inCollection(collectionId);
const membershipsInCollection = memberships.inCollection(collectionId);
const hasMemberships =
groupMembershipsInCollection.length > 0 ||
membershipsInCollection.length > 0;
const showLoading =
!hasMemberships && (membershipLoading || groupMembershipLoading);
React.useEffect(() => {
void fetchMemberships();
void fetchGroupMemberships();
}, [fetchMemberships, fetchGroupMemberships]);
const containerRef = React.useRef<HTMLDivElement | null>(null);
const { maxHeight, calcMaxHeight } = useMaxHeight({
elementRef: containerRef,
maxViewportPercentage: 70,
});
React.useEffect(() => {
calcMaxHeight();
});
const permissions = React.useMemo(
() =>
[
{
label: t("View only"),
value: CollectionPermission.Read,
},
{
label: t("Can edit"),
value: CollectionPermission.ReadWrite,
},
{
label: t("Manage"),
value: CollectionPermission.Admin,
},
{
divider: true,
label: t("Remove"),
value: EmptySelectValue,
},
] as Permission[],
[t]
);
return (
<ScrollableContainer
ref={containerRef}
hiddenScrollbars
style={{ maxHeight }}
>
{showLoading ? (
<Placeholder count={2} />
) : (
<>
<ListItem
image={
<Squircle color={theme.accent} size={AvatarSize.Medium}>
<UserIcon color={theme.accentText} size={16} />
</Squircle>
}
title={t("All members")}
subtitle={t("Everyone in the workspace")}
actions={
<div style={{ marginRight: -8 }}>
<InputSelectPermission
style={{ margin: 0 }}
onChange={(
value: CollectionPermission | typeof EmptySelectValue
) => {
void collection.save({
permission: value === EmptySelectValue ? null : value,
});
}}
disabled={!can.update}
value={collection?.permission}
labelHidden
nude
/>
</div>
}
/>
{groupMembershipsInCollection
.filter((membership) => membership.group)
.sort((a, b) =>
(
(invitedInSession.includes(a.group.id) ? "_" : "") +
a.group.name
).localeCompare(b.group.name)
)
.map((membership) => (
<ListItem
key={membership.id}
image={
<GroupAvatar
group={membership.group}
backgroundColor={theme.modalBackground}
/>
}
title={membership.group.name}
subtitle={t("{{ count }} member", {
count: membership.group.memberCount,
})}
actions={
<div style={{ marginRight: -8 }}>
<InputMemberPermissionSelect
style={{ margin: 0 }}
permissions={permissions}
onChange={async (
permission:
| CollectionPermission
| typeof EmptySelectValue
) => {
if (permission === EmptySelectValue) {
await groupMemberships.delete({
collectionId: collection.id,
groupId: membership.groupId,
});
} else {
await groupMemberships.create({
collectionId: collection.id,
groupId: membership.groupId,
permission,
});
}
}}
disabled={!can.update}
value={membership.permission}
labelHidden
nude
/>
</div>
}
/>
))}
{membershipsInCollection
.filter((membership) => membership.user)
.sort((a, b) =>
(
(invitedInSession.includes(a.user.id) ? "_" : "") +
a.user.name
).localeCompare(b.user.name)
)
.map((membership) => (
<ListItem
key={membership.id}
image={
<Avatar
model={membership.user}
size={AvatarSize.Medium}
showBorder={false}
/>
}
title={membership.user.name}
subtitle={membership.user.email}
actions={
<div style={{ marginRight: -8 }}>
<InputMemberPermissionSelect
style={{ margin: 0 }}
permissions={permissions}
onChange={async (
permission:
| CollectionPermission
| typeof EmptySelectValue
) => {
if (permission === EmptySelectValue) {
await memberships.delete({
collectionId: collection.id,
userId: membership.userId,
});
} else {
await memberships.create({
collectionId: collection.id,
userId: membership.userId,
permission,
});
}
}}
disabled={!can.update}
value={membership.permission}
labelHidden
nude
/>
</div>
}
/>
))}
</>
)}
</ScrollableContainer>
);
}
);
const ScrollableContainer = styled(Scrollable)`
padding: 12px 24px;
margin: -12px -24px;
`;
@@ -1,180 +0,0 @@
import { observer } from "mobx-react";
import { GroupIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { useTheme } from "styled-components";
import Squircle from "@shared/components/Squircle";
import { CollectionPermission } from "@shared/types";
import Collection from "~/models/Collection";
import Avatar, { AvatarSize } from "~/components/Avatar/Avatar";
import InputMemberPermissionSelect from "~/components/InputMemberPermissionSelect";
import usePolicy from "~/hooks/usePolicy";
import useRequest from "~/hooks/useRequest";
import useStores from "~/hooks/useStores";
import { EmptySelectValue, Permission } from "~/types";
import { ListItem } from "../components/ListItem";
type Props = {
/** Collection to which team members are supposed to be invited */
collection: Collection;
/** Children to be rendered before the list of members */
children?: React.ReactNode;
/** List of users and groups that have been invited during the current editing session */
invitedInSession: string[];
};
function CollectionMemberList({ collection, invitedInSession }: Props) {
const { memberships, collectionGroupMemberships } = useStores();
const can = usePolicy(collection);
const { t } = useTranslation();
const theme = useTheme();
const collectionId = collection.id;
const { request: fetchMemberships } = useRequest(
React.useCallback(
() => memberships.fetchAll({ id: collectionId }),
[memberships, collectionId]
)
);
const { request: fetchGroupMemberships } = useRequest(
React.useCallback(
() => collectionGroupMemberships.fetchAll({ id: collectionId }),
[collectionGroupMemberships, collectionId]
)
);
React.useEffect(() => {
void fetchMemberships();
void fetchGroupMemberships();
}, [fetchMemberships, fetchGroupMemberships]);
const permissions = React.useMemo(
() =>
[
{
label: t("View only"),
value: CollectionPermission.Read,
},
{
label: t("Can edit"),
value: CollectionPermission.ReadWrite,
},
{
label: t("Manage"),
value: CollectionPermission.Admin,
},
{
divider: true,
label: t("Remove"),
value: EmptySelectValue,
},
] as Permission[],
[t]
);
return (
<>
{collectionGroupMemberships
.inCollection(collection.id)
.sort((a, b) =>
(
(invitedInSession.includes(a.group.id) ? "_" : "") + a.group.name
).localeCompare(b.group.name)
)
.map((membership) => (
<ListItem
key={membership.id}
image={
<Squircle color={theme.text} size={AvatarSize.Medium}>
<GroupIcon color={theme.background} size={16} />
</Squircle>
}
title={membership.group.name}
subtitle={t("{{ count }} member", {
count: membership.group.memberCount,
})}
actions={
<div style={{ marginRight: -8 }}>
<InputMemberPermissionSelect
style={{ margin: 0 }}
permissions={permissions}
onChange={async (
permission: CollectionPermission | typeof EmptySelectValue
) => {
if (permission === EmptySelectValue) {
await collectionGroupMemberships.delete({
collectionId: collection.id,
groupId: membership.groupId,
});
} else {
await collectionGroupMemberships.create({
collectionId: collection.id,
groupId: membership.groupId,
permission,
});
}
}}
disabled={!can.update}
value={membership.permission}
labelHidden
nude
/>
</div>
}
/>
))}
{memberships
.inCollection(collection.id)
.sort((a, b) =>
(
(invitedInSession.includes(a.user.id) ? "_" : "") + a.user.name
).localeCompare(b.user.name)
)
.map((membership) => (
<ListItem
key={membership.id}
image={
<Avatar
model={membership.user}
size={AvatarSize.Medium}
showBorder={false}
/>
}
title={membership.user.name}
subtitle={membership.user.email}
actions={
<div style={{ marginRight: -8 }}>
<InputMemberPermissionSelect
style={{ margin: 0 }}
permissions={permissions}
onChange={async (
permission: CollectionPermission | typeof EmptySelectValue
) => {
if (permission === EmptySelectValue) {
await memberships.delete({
collectionId: collection.id,
userId: membership.userId,
});
} else {
await memberships.create({
collectionId: collection.id,
userId: membership.userId,
permission,
});
}
}}
disabled={!can.update}
value={membership.permission}
labelHidden
nude
/>
</div>
}
/>
))}
</>
);
}
export default observer(CollectionMemberList);
@@ -1,18 +1,15 @@
import { isEmail } from "class-validator";
import { m } from "framer-motion";
import { observer } from "mobx-react";
import { BackIcon, UserIcon } from "outline-icons";
import { BackIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { useTheme } from "styled-components";
import Squircle from "@shared/components/Squircle";
import { CollectionPermission } from "@shared/types";
import Collection from "~/models/Collection";
import Group from "~/models/Group";
import User from "~/models/User";
import Avatar, { AvatarSize } from "~/components/Avatar/Avatar";
import InputSelectPermission from "~/components/InputSelectPermission";
import { Avatar, AvatarSize } from "~/components/Avatar";
import NudeButton from "~/components/NudeButton";
import { createAction } from "~/actions";
import { UserSection } from "~/actions/sections";
@@ -22,15 +19,14 @@ import useKeyDown from "~/hooks/useKeyDown";
import usePolicy from "~/hooks/usePolicy";
import usePrevious from "~/hooks/usePrevious";
import useStores from "~/hooks/useStores";
import { EmptySelectValue, Permission } from "~/types";
import { Permission } from "~/types";
import { collectionPath, urlify } from "~/utils/routeHelpers";
import { Wrapper, presence } from "../components";
import { CopyLinkButton } from "../components/CopyLinkButton";
import { ListItem } from "../components/ListItem";
import { PermissionAction } from "../components/PermissionAction";
import { SearchInput } from "../components/SearchInput";
import { Suggestions } from "../components/Suggestions";
import CollectionMemberList from "./CollectionMemberList";
import { AccessControlList } from "./AccessControlList";
type Props = {
/** The collection to share. */
@@ -42,10 +38,8 @@ type Props = {
};
function SharePopover({ collection, visible, onRequestClose }: Props) {
const theme = useTheme();
const team = useCurrentTeam();
const { collectionGroupMemberships, users, groups, memberships } =
useStores();
const { groupMemberships, users, groups, memberships } = useStores();
const { t } = useTranslation();
const can = usePolicy(collection);
const [query, setQuery] = React.useState("");
@@ -206,10 +200,10 @@ function SharePopover({ collection, visible, onRequestClose }: Props) {
}
if (group) {
await collectionGroupMemberships.create({
await groupMemberships.create({
collectionId: collection.id,
groupId: group.id,
permission: CollectionPermission.Read,
permission,
});
return group;
}
@@ -268,7 +262,7 @@ function SharePopover({ collection, visible, onRequestClose }: Props) {
}),
[
collection.id,
collectionGroupMemberships,
groupMemberships,
groups,
hidePicker,
memberships,
@@ -355,49 +349,19 @@ function SharePopover({ collection, visible, onRequestClose }: Props) {
)}
{picker && (
<div>
<Suggestions
ref={suggestionsRef}
query={query}
collection={collection}
pendingIds={pendingIds}
addPendingId={handleAddPendingId}
removePendingId={handleRemovePendingId}
onEscape={handleEscape}
/>
</div>
<Suggestions
ref={suggestionsRef}
query={query}
collection={collection}
pendingIds={pendingIds}
addPendingId={handleAddPendingId}
removePendingId={handleRemovePendingId}
onEscape={handleEscape}
/>
)}
<div style={{ display: picker ? "none" : "block" }}>
<ListItem
image={
<Squircle color={theme.accent} size={AvatarSize.Medium}>
<UserIcon color={theme.accentText} size={16} />
</Squircle>
}
title={t("All members")}
subtitle={t("Everyone in the workspace")}
actions={
<div style={{ marginRight: -8 }}>
<InputSelectPermission
style={{ margin: 0 }}
onChange={(
value: CollectionPermission | typeof EmptySelectValue
) => {
void collection.save({
permission: value === EmptySelectValue ? null : value,
});
}}
disabled={!can.update}
value={collection?.permission}
labelHidden
nude
/>
</div>
}
/>
<CollectionMemberList
<AccessControlList
collection={collection}
invitedInSession={invitedInSession}
/>
@@ -0,0 +1,287 @@
import { observer } from "mobx-react";
import { MoreIcon, QuestionMarkIcon, UserIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled, { useTheme } from "styled-components";
import Squircle from "@shared/components/Squircle";
import { Pagination } from "@shared/constants";
import { s } from "@shared/styles";
import { CollectionPermission, IconType } from "@shared/types";
import { determineIconType } from "@shared/utils/icon";
import type Collection from "~/models/Collection";
import type Document from "~/models/Document";
import Share from "~/models/Share";
import Flex from "~/components/Flex";
import Scrollable from "~/components/Scrollable";
import Text from "~/components/Text";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import useCurrentUser from "~/hooks/useCurrentUser";
import useMaxHeight from "~/hooks/useMaxHeight";
import usePolicy from "~/hooks/usePolicy";
import useRequest from "~/hooks/useRequest";
import useStores from "~/hooks/useStores";
import { Avatar, AvatarSize } from "../../Avatar";
import CollectionIcon from "../../Icons/CollectionIcon";
import Tooltip from "../../Tooltip";
import { Separator } from "../components";
import { ListItem } from "../components/ListItem";
import { Placeholder } from "../components/Placeholder";
import DocumentMemberList from "./DocumentMemberList";
import PublicAccess from "./PublicAccess";
type Props = {
/** The document being shared. */
document: Document;
/** List of users that have been invited during the current editing session */
invitedInSession: string[];
/** The existing share model, if any. */
share: Share | null | undefined;
/** The existing share parent model, if any. */
sharedParent: Share | null | undefined;
/** Callback fired when the popover requests to be closed. */
onRequestClose: () => void;
/** Whether the popover is visible. */
visible: boolean;
};
export const AccessControlList = observer(
({
document,
invitedInSession,
share,
sharedParent,
onRequestClose,
visible,
}: Props) => {
const { t } = useTranslation();
const theme = useTheme();
const collection = document.collection;
const usersInCollection = useUsersInCollection(collection);
const user = useCurrentUser();
const { userMemberships, groupMemberships } = useStores();
const collectionSharingDisabled = document.collection?.sharing === false;
const team = useCurrentTeam();
const can = usePolicy(document);
const canCollection = usePolicy(collection);
const documentId = document.id;
const containerRef = React.useRef<HTMLDivElement | null>(null);
const { maxHeight, calcMaxHeight } = useMaxHeight({
elementRef: containerRef,
maxViewportPercentage: 65,
margin: 24,
});
const { loading: userMembershipLoading, request: fetchUserMemberships } =
useRequest(
React.useCallback(
() =>
userMemberships.fetchDocumentMemberships({
id: documentId,
limit: Pagination.defaultLimit,
}),
[userMemberships, documentId]
)
);
const { loading: groupMembershipLoading, request: fetchGroupMemberships } =
useRequest(
React.useCallback(
() => groupMemberships.fetchAll({ documentId }),
[groupMemberships, documentId]
)
);
const hasMemberships =
groupMemberships.inDocument(documentId)?.length > 0 ||
document.members.length > 0;
const showLoading =
!hasMemberships && (groupMembershipLoading || userMembershipLoading);
React.useEffect(() => {
void fetchUserMemberships();
void fetchGroupMemberships();
}, [fetchUserMemberships, fetchGroupMemberships]);
React.useEffect(() => {
calcMaxHeight();
});
return (
<ScrollableContainer
ref={containerRef}
hiddenScrollbars
style={{ maxHeight }}
>
{showLoading ? (
<Placeholder />
) : (
<>
{collection && canCollection.readDocument ? (
<>
{collection.permission ? (
<ListItem
image={
<Squircle color={theme.accent} size={AvatarSize.Medium}>
<UserIcon color={theme.accentText} size={16} />
</Squircle>
}
title={t("All members")}
subtitle={t("Everyone in the workspace")}
actions={
<AccessTooltip>
{collection?.permission ===
CollectionPermission.ReadWrite
? t("Can edit")
: t("Can view")}
</AccessTooltip>
}
/>
) : usersInCollection ? (
<ListItem
image={<CollectionSquircle collection={collection} />}
title={collection.name}
subtitle={t("Everyone in the collection")}
actions={<AccessTooltip>{t("Can view")}</AccessTooltip>}
/>
) : (
<ListItem
image={<Avatar model={user} showBorder={false} />}
title={user.name}
subtitle={t("You have full access")}
actions={<AccessTooltip>{t("Can edit")}</AccessTooltip>}
/>
)}
<DocumentMemberList
document={document}
invitedInSession={invitedInSession}
/>
</>
) : document.isDraft ? (
<>
<ListItem
image={
<Avatar model={document.createdBy} showBorder={false} />
}
title={document.createdBy?.name}
actions={
<AccessTooltip content={t("Created the document")}>
{t("Can edit")}
</AccessTooltip>
}
/>
<DocumentMemberList
document={document}
invitedInSession={invitedInSession}
/>
</>
) : (
<>
<DocumentMemberList
document={document}
invitedInSession={invitedInSession}
/>
<ListItem
image={
<Squircle color={theme.accent} size={AvatarSize.Medium}>
<MoreIcon color={theme.accentText} size={16} />
</Squircle>
}
title={t("Other people")}
subtitle={t("Other workspace members may have access")}
actions={
<AccessTooltip
content={t(
"This document may be shared with more workspace members through a parent document or collection you do not have access to"
)}
/>
}
/>
</>
)}
</>
)}
{team.sharing && can.share && !collectionSharingDisabled && visible && (
<Sticky>
{document.members.length ? <Separator /> : null}
<PublicAccess
document={document}
share={share}
sharedParent={sharedParent}
onRequestClose={onRequestClose}
/>
</Sticky>
)}
</ScrollableContainer>
);
}
);
const AccessTooltip = ({
children,
content,
}: {
children?: React.ReactNode;
content?: string;
}) => {
const { t } = useTranslation();
return (
<Flex align="center" gap={2}>
<Text type="secondary" size="small">
{children}
</Text>
<Tooltip content={content ?? t("Access inherited from collection")}>
<QuestionMarkIcon size={18} />
</Tooltip>
</Flex>
);
};
const CollectionSquircle = ({ collection }: { collection: Collection }) => {
const theme = useTheme();
const iconType = determineIconType(collection.icon)!;
const squircleColor =
iconType === IconType.SVG ? collection.color! : theme.slateLight;
const iconSize = iconType === IconType.SVG ? 16 : 22;
return (
<Squircle color={squircleColor} size={AvatarSize.Medium}>
<CollectionIcon
collection={collection}
color={theme.white}
size={iconSize}
/>
</Squircle>
);
};
function useUsersInCollection(collection?: Collection) {
const { users, memberships } = useStores();
const { request } = useRequest(() =>
memberships.fetchPage({ limit: 1, id: collection!.id })
);
React.useEffect(() => {
if (collection && !collection.permission) {
void request();
}
}, [collection]);
return collection
? collection.permission
? true
: users.inCollection(collection.id).length > 1
: false;
}
const Sticky = styled.div`
background: ${s("menuBackground")};
position: sticky;
bottom: -12px;
`;
const ScrollableContainer = styled(Scrollable)`
padding: 12px 24px;
margin: -12px -24px;
`;
@@ -1,19 +1,23 @@
import orderBy from "lodash/orderBy";
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { useHistory } from "react-router-dom";
import { useTranslation, Trans } from "react-i18next";
import { Link, useHistory } from "react-router-dom";
import { toast } from "sonner";
import { Pagination } from "@shared/constants";
import styled, { useTheme } from "styled-components";
import { s } from "@shared/styles";
import { DocumentPermission } from "@shared/types";
import Document from "~/models/Document";
import UserMembership from "~/models/UserMembership";
import LoadingIndicator from "~/components/LoadingIndicator";
import { GroupAvatar } from "~/components/Avatar";
import InputMemberPermissionSelect from "~/components/InputMemberPermissionSelect";
import useCurrentUser from "~/hooks/useCurrentUser";
import usePolicy from "~/hooks/usePolicy";
import useRequest from "~/hooks/useRequest";
import useStores from "~/hooks/useStores";
import { EmptySelectValue, Permission } from "~/types";
import { homePath } from "~/utils/routeHelpers";
import MemberListItem from "./DocumentMemberListItem";
import { ListItem } from "../components/ListItem";
import DocumentMemberListItem from "./DocumentMemberListItem";
type Props = {
/** Document to which team members are supposed to be invited */
@@ -25,27 +29,13 @@ type Props = {
};
function DocumentMembersList({ document, invitedInSession }: Props) {
const { userMemberships } = useStores();
const { userMemberships, groupMemberships } = useStores();
const user = useCurrentUser();
const history = useHistory();
const can = usePolicy(document);
const { t } = useTranslation();
const { loading: loadingDocumentMembers, request: fetchDocumentMembers } =
useRequest(
React.useCallback(
() =>
userMemberships.fetchDocumentMemberships({
id: document.id,
limit: Pagination.defaultLimit,
}),
[userMemberships, document.id]
)
);
React.useEffect(() => {
void fetchDocumentMembers();
}, [fetchDocumentMembers]);
const theme = useTheme();
const handleRemoveUser = React.useCallback(
async (item) => {
@@ -68,7 +58,7 @@ function DocumentMembersList({ document, invitedInSession }: Props) {
toast.error(t("Could not remove user"));
}
},
[history, userMemberships, user, document]
[t, history, userMemberships, user, document]
);
const handleUpdateUser = React.useCallback(
@@ -88,7 +78,7 @@ function DocumentMembersList({ document, invitedInSession }: Props) {
toast.error(t("Could not update user"));
}
},
[userMemberships, document]
[t, userMemberships, document]
);
// Order newly added users first during the current editing session, on reload members are
@@ -105,14 +95,101 @@ function DocumentMembersList({ document, invitedInSession }: Props) {
[document.members, invitedInSession]
);
if (loadingDocumentMembers) {
return <LoadingIndicator />;
}
const permissions = React.useMemo(
() =>
[
{
label: t("View only"),
value: DocumentPermission.Read,
},
{
label: t("Can edit"),
value: DocumentPermission.ReadWrite,
},
{
label: t("Manage"),
value: DocumentPermission.Admin,
},
{
divider: true,
label: t("Remove"),
value: EmptySelectValue,
},
] as Permission[],
[t]
);
return (
<>
{groupMemberships
.inDocument(document.id)
.sort((a, b) =>
(
(invitedInSession.includes(a.group.id) ? "_" : "") + a.group.name
).localeCompare(b.group.name)
)
.map((membership) => {
const MaybeLink = membership?.source ? StyledLink : React.Fragment;
return (
<ListItem
key={membership.id}
image={
<GroupAvatar
group={membership.group}
backgroundColor={theme.modalBackground}
/>
}
title={membership.group.name}
subtitle={
membership.sourceId ? (
<Trans>
Has access through{" "}
<MaybeLink
// @ts-expect-error to prop does not exist on React.Fragment
to={membership.source?.document?.path ?? ""}
>
parent
</MaybeLink>
</Trans>
) : (
t("{{ count }} member", {
count: membership.group.memberCount,
})
)
}
actions={
<div style={{ marginRight: -8 }}>
<InputMemberPermissionSelect
style={{ margin: 0 }}
permissions={permissions}
onChange={async (
permission: DocumentPermission | typeof EmptySelectValue
) => {
if (permission === EmptySelectValue) {
await groupMemberships.delete({
documentId: document.id,
groupId: membership.groupId,
});
} else {
await groupMemberships.create({
documentId: document.id,
groupId: membership.groupId,
permission,
});
}
}}
disabled={!can.manageUsers}
value={membership.permission}
labelHidden
nude
/>
</div>
}
/>
);
})}
{members.map((item) => (
<MemberListItem
<DocumentMemberListItem
key={item.id}
user={item}
membership={item.getMembership(document)}
@@ -131,4 +208,9 @@ function DocumentMembersList({ document, invitedInSession }: Props) {
);
}
const StyledLink = styled(Link)`
color: ${s("textTertiary")};
text-decoration: underline;
`;
export default observer(DocumentMembersList);
@@ -7,9 +7,9 @@ import { s } from "@shared/styles";
import { DocumentPermission } from "@shared/types";
import User from "~/models/User";
import UserMembership from "~/models/UserMembership";
import Avatar from "~/components/Avatar";
import { AvatarSize } from "~/components/Avatar/Avatar";
import { Avatar, AvatarSize } from "~/components/Avatar";
import InputMemberPermissionSelect from "~/components/InputMemberPermissionSelect";
import Time from "~/components/Time";
import { EmptySelectValue, Permission } from "~/types";
import { ListItem } from "../components/ListItem";
@@ -68,7 +68,6 @@ const DocumentMemberListItem = ({
if (!currentPermission) {
return null;
}
const disabled = !onUpdate && !onLeave;
const MaybeLink = membership?.source ? StyledLink : React.Fragment;
return (
@@ -90,36 +89,35 @@ const DocumentMemberListItem = ({
</Trans>
) : user.isSuspended ? (
t("Suspended")
) : user.email ? (
user.email
) : user.isInvited ? (
t("Invited")
) : user.isViewer ? (
t("Viewer")
) : user.lastActiveAt ? (
<Trans>
Active <Time dateTime={user.lastActiveAt} /> ago
</Trans>
) : (
t("Editor")
t("Never signed in")
)
}
actions={
disabled ? null : (
<div style={{ marginRight: -8 }}>
<InputMemberPermissionSelect
permissions={
onLeave
? [
currentPermission,
{
label: `${t("Leave")}`,
value: EmptySelectValue,
},
]
: permissions
}
value={membership?.permission}
onChange={handleChange}
/>
</div>
)
<div style={{ marginRight: -8 }}>
<InputMemberPermissionSelect
permissions={
onLeave
? [
currentPermission,
{
label: `${t("Leave")}`,
value: EmptySelectValue,
},
]
: permissions
}
value={membership?.permission}
onChange={handleChange}
disabled={!onUpdate && !onLeave}
/>
</div>
}
/>
);
@@ -1,167 +0,0 @@
import { observer } from "mobx-react";
import { MoreIcon, QuestionMarkIcon, UserIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { useTheme } from "styled-components";
import Squircle from "@shared/components/Squircle";
import { CollectionPermission, IconType } from "@shared/types";
import { determineIconType } from "@shared/utils/icon";
import type Collection from "~/models/Collection";
import type Document from "~/models/Document";
import Flex from "~/components/Flex";
import Text from "~/components/Text";
import useCurrentUser from "~/hooks/useCurrentUser";
import useRequest from "~/hooks/useRequest";
import useStores from "~/hooks/useStores";
import Avatar from "../../Avatar";
import { AvatarSize } from "../../Avatar/Avatar";
import CollectionIcon from "../../Icons/CollectionIcon";
import Tooltip from "../../Tooltip";
import { ListItem } from "../components/ListItem";
type Props = {
/** The document being shared. */
document: Document;
children: React.ReactNode;
};
export const OtherAccess = observer(({ document, children }: Props) => {
const { t } = useTranslation();
const theme = useTheme();
const collection = document.collection;
const usersInCollection = useUsersInCollection(collection);
const user = useCurrentUser();
return (
<>
{collection ? (
<>
{collection.permission ? (
<ListItem
image={
<Squircle color={theme.accent} size={AvatarSize.Medium}>
<UserIcon color={theme.accentText} size={16} />
</Squircle>
}
title={t("All members")}
subtitle={t("Everyone in the workspace")}
actions={
<AccessTooltip>
{collection?.permission === CollectionPermission.ReadWrite
? t("Can edit")
: t("Can view")}
</AccessTooltip>
}
/>
) : usersInCollection ? (
<ListItem
image={<CollectionSquircle collection={collection} />}
title={collection.name}
subtitle={t("Everyone in the collection")}
actions={<AccessTooltip>{t("Can view")}</AccessTooltip>}
/>
) : (
<ListItem
image={<Avatar model={user} showBorder={false} />}
title={user.name}
subtitle={t("You have full access")}
actions={<AccessTooltip>{t("Can edit")}</AccessTooltip>}
/>
)}
{children}
</>
) : document.isDraft ? (
<>
<ListItem
image={<Avatar model={document.createdBy} showBorder={false} />}
title={document.createdBy?.name}
actions={
<AccessTooltip content={t("Created the document")}>
{t("Can edit")}
</AccessTooltip>
}
/>
{children}
</>
) : (
<>
{children}
<ListItem
image={
<Squircle color={theme.accent} size={AvatarSize.Medium}>
<MoreIcon color={theme.accentText} size={16} />
</Squircle>
}
title={t("Other people")}
subtitle={t("Other workspace members may have access")}
actions={
<AccessTooltip
content={t(
"This document may be shared with more workspace members through a parent document or collection you do not have access to"
)}
/>
}
/>
</>
)}
</>
);
});
const AccessTooltip = ({
children,
content,
}: {
children?: React.ReactNode;
content?: string;
}) => {
const { t } = useTranslation();
return (
<Flex align="center" gap={2}>
<Text type="secondary" size="small">
{children}
</Text>
<Tooltip content={content ?? t("Access inherited from collection")}>
<QuestionMarkIcon size={18} />
</Tooltip>
</Flex>
);
};
const CollectionSquircle = ({ collection }: { collection: Collection }) => {
const theme = useTheme();
const iconType = determineIconType(collection.icon)!;
const squircleColor =
iconType === IconType.Outline ? collection.color! : theme.slateLight;
const iconSize = iconType === IconType.Outline ? 16 : 22;
return (
<Squircle color={squircleColor} size={AvatarSize.Medium}>
<CollectionIcon
collection={collection}
color={theme.white}
size={iconSize}
/>
</Squircle>
);
};
function useUsersInCollection(collection?: Collection) {
const { users, memberships } = useStores();
const { request } = useRequest(() =>
memberships.fetchPage({ limit: 1, id: collection!.id })
);
React.useEffect(() => {
if (collection && !collection.permission) {
void request();
}
}, [collection]);
return collection
? collection.permission
? true
: users.inCollection(collection.id).length > 1
: false;
}
@@ -18,7 +18,7 @@ import Switch from "~/components/Switch";
import env from "~/env";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
import { AvatarSize } from "../../Avatar/Avatar";
import { AvatarSize } from "../../Avatar";
import CopyToClipboard from "../../CopyToClipboard";
import NudeButton from "../../NudeButton";
import { ResizingHeightContainer } from "../../ResizingHeightContainer";
@@ -203,7 +203,7 @@ const StyledInfoIcon = styled(InfoIcon)`
`;
const Wrapper = styled.div`
margin-bottom: 8px;
padding-bottom: 8px;
`;
const DomainPrefix = styled.span`
@@ -7,10 +7,10 @@ import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { DocumentPermission } from "@shared/types";
import Document from "~/models/Document";
import Group from "~/models/Group";
import Share from "~/models/Share";
import User from "~/models/User";
import Avatar from "~/components/Avatar";
import { AvatarSize } from "~/components/Avatar/Avatar";
import { Avatar, GroupAvatar, AvatarSize } from "~/components/Avatar";
import NudeButton from "~/components/NudeButton";
import { createAction } from "~/actions";
import { UserSection } from "~/actions/sections";
@@ -22,14 +22,12 @@ import usePrevious from "~/hooks/usePrevious";
import useStores from "~/hooks/useStores";
import { Permission } from "~/types";
import { documentPath, urlify } from "~/utils/routeHelpers";
import { Separator, Wrapper, presence } from "../components";
import { Wrapper, presence } from "../components";
import { CopyLinkButton } from "../components/CopyLinkButton";
import { PermissionAction } from "../components/PermissionAction";
import { SearchInput } from "../components/SearchInput";
import { Suggestions } from "../components/Suggestions";
import DocumentMembersList from "./DocumentMemberList";
import { OtherAccess } from "./OtherAccess";
import PublicAccess from "./PublicAccess";
import { AccessControlList } from "./AccessControlList";
type Props = {
/** The document to share. */
@@ -55,12 +53,11 @@ function SharePopover({
const { t } = useTranslation();
const can = usePolicy(document);
const [hasRendered, setHasRendered] = React.useState(visible);
const { users, userMemberships } = useStores();
const { users, userMemberships, groups, groupMemberships } = useStores();
const [query, setQuery] = React.useState("");
const [picker, showPicker, hidePicker] = useBoolean();
const [invitedInSession, setInvitedInSession] = React.useState<string[]>([]);
const [pendingIds, setPendingIds] = React.useState<string[]>([]);
const collectionSharingDisabled = document.collection?.sharing === false;
const [permission, setPermission] = React.useState<DocumentPermission>(
DocumentPermission.Read
);
@@ -132,9 +129,9 @@ function SharePopover({
name: t("Invite"),
section: UserSection,
perform: async () => {
const usersInvited = await Promise.all(
const invited = await Promise.all(
pendingIds.map(async (idOrEmail) => {
let user;
let user, group;
// convert email to user
if (isEmail(idOrEmail)) {
@@ -148,38 +145,77 @@ function SharePopover({
user = response[0];
} else {
user = users.get(idOrEmail);
group = groups.get(idOrEmail);
}
if (!user) {
return;
if (user) {
await userMemberships.create({
documentId: document.id,
userId: user.id,
permission,
});
return user;
}
await userMemberships.create({
documentId: document.id,
userId: user.id,
permission,
});
if (group) {
await groupMemberships.create({
documentId: document.id,
groupId: group.id,
permission,
});
return group;
}
return user;
return;
})
);
if (usersInvited.length === 1) {
const user = usersInvited[0] as User;
toast.message(
t("{{ userName }} was invited to the document", {
userName: user.name,
}),
{
icon: <Avatar model={user} size={AvatarSize.Toast} />,
}
);
} else {
toast.success(
t("{{ count }} people invited to the document", {
count: pendingIds.length,
})
);
const invitedUsers = invited.filter(
(item) => item instanceof User
) as User[];
const invitedGroups = invited.filter(
(item) => item instanceof Group
) as Group[];
if (invitedUsers.length > 0) {
// Special case for the common action of adding a single user.
if (invitedUsers.length === 1) {
const user = invitedUsers[0];
toast.message(
t("{{ userName }} was added to the document", {
userName: user.name,
}),
{
icon: <Avatar model={user} size={AvatarSize.Toast} />,
}
);
} else {
toast.message(
t("{{ count }} people added to the document", {
count: invitedUsers.length,
})
);
}
}
if (invitedGroups.length > 0) {
// Special case for the common action of adding a single group.
if (invitedGroups.length === 1) {
const group = invitedGroups[0];
toast.message(
t("{{ userName }} was added to the document", {
userName: group.name,
}),
{
icon: <GroupAvatar group={group} size={AvatarSize.Toast} />,
}
);
} else {
toast.message(
t("{{ count }} groups added to the document", {
count: invitedGroups.length,
})
);
}
}
setInvitedInSession((prev) => [...prev, ...pendingIds]);
@@ -188,14 +224,16 @@ function SharePopover({
},
}),
[
t,
pendingIds,
document.id,
groupMemberships,
groups,
hidePicker,
userMemberships,
document.id,
pendingIds,
permission,
users,
t,
team.defaultUserRole,
users,
]
);
@@ -341,24 +379,14 @@ function SharePopover({
)}
<div style={{ display: picker ? "none" : "block" }}>
<OtherAccess document={document}>
<DocumentMembersList
document={document}
invitedInSession={invitedInSession}
/>
</OtherAccess>
{team.sharing && can.share && !collectionSharingDisabled && visible && (
<>
{document.members.length ? <Separator /> : null}
<PublicAccess
document={document}
share={share}
sharedParent={sharedParent}
onRequestClose={onRequestClose}
/>
</>
)}
<AccessControlList
document={document}
invitedInSession={invitedInSession}
share={share}
sharedParent={sharedParent}
visible={visible}
onRequestClose={onRequestClose}
/>
</div>
</Wrapper>
);
@@ -0,0 +1,47 @@
import times from "lodash/times";
import * as React from "react";
import { AvatarSize } from "~/components/Avatar";
import Fade from "~/components/Fade";
import PlaceholderText from "~/components/PlaceholderText";
import { ListItem } from "../components/ListItem";
type Props = {
count?: number;
};
/**
* Placeholder for a list item in the share popover.
*/
export function Placeholder({ count = 1 }: Props) {
return (
<Fade>
{times(count, (index) => (
<ListItem
key={index}
image={
<PlaceholderText
width={AvatarSize.Medium}
height={AvatarSize.Medium}
/>
}
title={
<PlaceholderText
maxWidth={50}
minWidth={30}
height={14}
style={{ marginTop: 4, marginBottom: 4 }}
/>
}
subtitle={
<PlaceholderText
maxWidth={75}
minWidth={50}
height={12}
style={{ marginBottom: 4 }}
/>
}
/>
))}
</Fade>
);
}
@@ -1,11 +1,10 @@
import { isEmail } from "class-validator";
import concat from "lodash/concat";
import { observer } from "mobx-react";
import { CheckmarkIcon, CloseIcon, GroupIcon } from "outline-icons";
import { CheckmarkIcon, CloseIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled, { useTheme } from "styled-components";
import Squircle from "@shared/components/Squircle";
import styled from "styled-components";
import { s } from "@shared/styles";
import { stringToColor } from "@shared/utils/color";
import Collection from "~/models/Collection";
@@ -13,11 +12,12 @@ import Document from "~/models/Document";
import Group from "~/models/Group";
import User from "~/models/User";
import ArrowKeyNavigation from "~/components/ArrowKeyNavigation";
import Avatar from "~/components/Avatar";
import { AvatarSize, IAvatar } from "~/components/Avatar/Avatar";
import { Avatar, GroupAvatar, AvatarSize, IAvatar } from "~/components/Avatar";
import Empty from "~/components/Empty";
import Placeholder from "~/components/List/Placeholder";
import Scrollable from "~/components/Scrollable";
import useCurrentUser from "~/hooks/useCurrentUser";
import useMaxHeight from "~/hooks/useMaxHeight";
import useStores from "~/hooks/useStores";
import useThrottledCallback from "~/hooks/useThrottledCallback";
import { hover } from "~/styles";
@@ -40,8 +40,6 @@ type Props = {
addPendingId: (id: string) => void;
/** Callback to remove a user from the pending list. */
removePendingId: (id: string) => void;
/** Show group suggestions. */
showGroups?: boolean;
/** Handles escape from suggestions list */
onEscape?: (ev: React.KeyboardEvent<HTMLDivElement>) => void;
};
@@ -55,7 +53,6 @@ export const Suggestions = observer(
pendingIds,
addPendingId,
removePendingId,
showGroups,
onEscape,
}: Props,
ref: React.Ref<HTMLDivElement>
@@ -64,15 +61,16 @@ export const Suggestions = observer(
const { users, groups } = useStores();
const { t } = useTranslation();
const user = useCurrentUser();
const theme = useTheme();
const containerRef = React.useRef<HTMLDivElement | null>(null);
const { maxHeight } = useMaxHeight({
elementRef: containerRef,
maxViewportPercentage: 70,
});
const fetchUsersByQuery = useThrottledCallback(
(query: string) => {
void users.fetchPage({ query });
if (showGroups) {
void groups.fetchPage({ query });
}
void groups.fetchPage({ query });
},
250,
undefined,
@@ -100,21 +98,26 @@ export const Suggestions = observer(
: collection
? users.notInCollection(collection.id, query)
: users.orderedData
).filter((u) => u.id !== user.id && !u.isSuspended);
).filter((u) => !u.isSuspended && u.id !== user.id);
if (isEmail(query)) {
filtered.push(getSuggestionForEmail(query));
}
if (collection?.id) {
return [...groups.notInCollection(collection.id, query), ...filtered];
}
return filtered;
return [
...(document
? groups.notInDocument(document.id, query)
: collection
? groups.notInCollection(collection.id, query)
: []),
...filtered,
];
}, [
getSuggestionForEmail,
users,
users.orderedData,
groups,
groups.orderedData,
document?.id,
document?.members,
collection?.id,
@@ -132,7 +135,7 @@ export const Suggestions = observer(
: users.get(id) ?? groups.get(id)
)
.filter(Boolean) as User[],
[users, getSuggestionForEmail, pendingIds]
[users, groups, getSuggestionForEmail, pendingIds]
);
React.useEffect(() => {
@@ -146,11 +149,7 @@ export const Suggestions = observer(
subtitle: t("{{ count }} member", {
count: suggestion.memberCount,
}),
image: (
<Squircle color={theme.text} size={AvatarSize.Medium}>
<GroupIcon color={theme.background} size={16} />
</Squircle>
),
image: <GroupAvatar group={suggestion} />,
};
}
return {
@@ -182,55 +181,63 @@ export const Suggestions = observer(
neverRenderedList.current = false;
return (
<ArrowKeyNavigation
ref={ref}
onEscape={onEscape}
aria-label={t("Suggestions for invitation")}
items={concat(pending, suggestionsWithPending)}
<ScrollableContainer
ref={containerRef}
hiddenScrollbars
style={{ maxHeight }}
>
{() => [
...pending.map((suggestion) => (
<PendingListItem
keyboardNavigation
{...getListItemProps(suggestion)}
key={suggestion.id}
onClick={() => removePendingId(suggestion.id)}
onKeyDown={(ev) => {
if (ev.key === "Enter") {
ev.preventDefault();
ev.stopPropagation();
removePendingId(suggestion.id);
<ArrowKeyNavigation
ref={ref}
onEscape={onEscape}
aria-label={t("Suggestions for invitation")}
items={concat(pending, suggestionsWithPending)}
>
{() => [
...pending.map((suggestion) => (
<PendingListItem
keyboardNavigation
{...getListItemProps(suggestion)}
key={suggestion.id}
onClick={() => removePendingId(suggestion.id)}
onKeyDown={(ev) => {
if (ev.key === "Enter") {
ev.preventDefault();
ev.stopPropagation();
removePendingId(suggestion.id);
}
}}
actions={
<>
<InvitedIcon />
<RemoveIcon />
</>
}
}}
actions={
<>
<InvitedIcon />
<RemoveIcon />
</>
}
/>
)),
pending.length > 0 &&
(suggestionsWithPending.length > 0 || isEmpty) && <Separator />,
...suggestionsWithPending.map((suggestion) => (
<ListItem
keyboardNavigation
{...getListItemProps(suggestion as User)}
key={suggestion.id}
onClick={() => addPendingId(suggestion.id)}
onKeyDown={(ev) => {
if (ev.key === "Enter") {
ev.preventDefault();
ev.stopPropagation();
addPendingId(suggestion.id);
}
}}
actions={<InviteIcon />}
/>
)),
isEmpty && <Empty style={{ marginTop: 22 }}>{t("No matches")}</Empty>,
]}
</ArrowKeyNavigation>
/>
)),
pending.length > 0 &&
(suggestionsWithPending.length > 0 || isEmpty) && <Separator />,
...suggestionsWithPending.map((suggestion) => (
<ListItem
keyboardNavigation
{...getListItemProps(suggestion as User)}
key={suggestion.id}
onClick={() => addPendingId(suggestion.id)}
onKeyDown={(ev) => {
if (ev.key === "Enter") {
ev.preventDefault();
ev.stopPropagation();
addPendingId(suggestion.id);
}
}}
actions={<InviteIcon />}
/>
)),
isEmpty && (
<Empty style={{ marginTop: 22 }}>{t("No matches")}</Empty>
),
]}
</ArrowKeyNavigation>
</ScrollableContainer>
);
})
);
@@ -259,3 +266,8 @@ const Separator = styled.div`
border-top: 1px dashed ${s("divider")};
margin: 12px 0;
`;
const ScrollableContainer = styled(Scrollable)`
padding: 12px 24px;
margin: -12px -24px;
`;
+1 -1
View File
@@ -15,7 +15,7 @@ export const Wrapper = styled.div`
export const Separator = styled.div`
border-top: 1px dashed ${s("divider")};
margin: 12px 0;
margin: 8px 0;
`;
export const HeaderInput = styled(Flex)`
+37 -33
View File
@@ -34,16 +34,18 @@ import TrashLink from "./components/TrashLink";
function AppSidebar() {
const { t } = useTranslation();
const { documents, ui } = useStores();
const { documents, ui, collections } = useStores();
const team = useCurrentTeam();
const user = useCurrentUser();
const can = usePolicy(team);
React.useEffect(() => {
void collections.fetchAll();
if (!user.isViewer) {
void documents.fetchDrafts();
}
}, [documents, user.isViewer]);
}, [documents, collections, user.isViewer]);
const [dndArea, setDndArea] = React.useState();
const handleSidebarRef = React.useCallback((node) => setDndArea(node), []);
@@ -92,43 +94,45 @@ function AppSidebar() {
</SidebarButton>
)}
</OrganizationMenu>
<Section>
<SidebarLink
to={homePath()}
icon={<HomeIcon />}
exact={false}
label={t("Home")}
/>
<SidebarLink
to={searchPath()}
icon={<SearchIcon />}
label={t("Search")}
exact={false}
/>
{can.createDocument && (
<SidebarLink
to={draftsPath()}
icon={<DraftsIcon />}
label={
<Flex align="center" justify="space-between">
{t("Drafts")}
{documents.totalDrafts > 0 ? (
<Drafts size="xsmall" type="tertiary">
{documents.totalDrafts > 25
? "25+"
: documents.totalDrafts}
</Drafts>
) : null}
</Flex>
}
/>
)}
</Section>
<Scrollable flex shadow>
<Section>
<SidebarLink
to={homePath()}
icon={<HomeIcon />}
exact={false}
label={t("Home")}
/>
<SidebarLink
to={searchPath()}
icon={<SearchIcon />}
label={t("Search")}
exact={false}
/>
{can.createDocument && (
<SidebarLink
to={draftsPath()}
icon={<DraftsIcon />}
label={
<Flex align="center" justify="space-between">
{t("Drafts")}
{documents.totalDrafts > 0 ? (
<Drafts size="xsmall" type="tertiary">
{documents.totalDrafts}
</Drafts>
) : null}
</Flex>
}
/>
)}
<Starred />
</Section>
<Section>
<SharedWithMe />
</Section>
<Section>
<Starred />
</Section>
<Section auto>
<Collections />
</Section>
+2 -1
View File
@@ -50,7 +50,8 @@ function Right({ children, border, className }: Props) {
}
}, []);
const handleMouseDown = React.useCallback(() => {
const handleMouseDown = React.useCallback((event) => {
event.preventDefault();
setResizing(true);
}, []);
+1 -1
View File
@@ -29,7 +29,7 @@ function SharedSidebar({ rootNode, shareId }: Props) {
return (
<Sidebar>
{team && (
{team?.name && (
<SidebarButton
title={team.name}
image={<TeamLogo model={team} size={32} alt={t("Logo")} />}

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