Compare commits

...

393 Commits

Author SHA1 Message Date
Tom Moor d6b2889f1d wip 2026-01-10 00:26:57 -05:00
Tom Moor 69a3346280 fix: Remove stale UserAuthentication records on sign-in
When signing in with an authentication method, remove any other
UserAuthentication records for the same provider to prevent expired
tokens from causing premature logout.

This fix applies to both:
- Existing users signing in again
- Invited users accepting their invitation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-10 00:13:33 -05:00
Tom Moor 68fd23580a Show upload progress for large/slow files (#11109)
* Show upload progress for large/slow files

* PR feedback
2026-01-08 10:15:43 -05:00
Copilot f6e25b0d32 Add "Email display" preference to control email address visibility (#11103)
* Initial plan

* Add emailDisplay TeamPreference to control email address visibility

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Add tests for emailDisplay TeamPreference policy logic

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* tweaks

* Add Everyone setting, tests

* PR feedback

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>
Co-authored-by: Tom Moor <tom@getoutline.com>
2026-01-08 10:15:36 -05:00
Tom Moor b06c18ecf6 Update Headers and Dividers to clear aligned images (#11108) 2026-01-07 22:41:10 -05:00
Tom Moor ca21b8a17d fix: Sync schema between frontend editor and API (#11101)
* fix: Sync schema between frontend editor and API
Allow lists in basic schema

* test

* snap
2026-01-07 22:10:41 -05:00
Tom Moor 2116d9972f fix: Event propagation from input in SidebarLink (#11105)
* fix: Event propagation from input in SidebarLink

closes #11093

* Include keyboard
2026-01-07 21:59:19 -05:00
Tom Moor 974c5f9f70 fix: Embedded PDF rendering in Safari (#11107) 2026-01-07 21:59:12 -05:00
Tom Moor eb0ac044a0 fix: Copy/paste diagrams.net diagram loses edit button (#11102) 2026-01-08 01:18:01 +00:00
Apoorv Mishra dd9c2a0cd8 fix: restore color picker (#11095) 2026-01-07 18:14:24 -05:00
Tom Moor ad975620b0 Remove HTML from revisions.info (#11088)
closes #11024
2026-01-07 08:31:48 -05:00
Tom Moor f5a7904cbd Improve user feedback when copying/moving documents (#11089)
* UI feedback when copying

* UI feedback when moving
2026-01-07 08:31:41 -05:00
Tom Moor c0f276e23f fix: Task summary in meta shrinks (#11087)
closes #11082
2026-01-07 03:36:32 +00:00
Tom Moor e00297a6c7 fix: Wrap overlapping filters on search page (#11086) 2026-01-07 03:20:55 +00:00
Tom Moor 4aa5cacc57 v1.2.0 2026-01-06 18:02:37 -05:00
Tom Moor 1466e261a7 fix: Remove flash on share popover (#11075)
* fix: Remove flash on share popover

* Loading flash within share dialog
2026-01-06 04:09:12 +00:00
Translate-O-Tron 8ed9753302 New Crowdin updates (#11007)
* fix: New Chinese Simplified translations from Crowdin [ci skip]

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

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

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

* fix: New Spanish 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 Korean translations from Crowdin [ci skip]

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

* fix: New Polish 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 Ukrainian translations from Crowdin [ci skip]

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

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

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

* fix: New Portuguese, Brazilian 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 English, United Kingdom translations from Crowdin [ci skip]

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

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

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

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

* fix: New Spanish 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 Korean translations from Crowdin [ci skip]

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

* fix: New Polish 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 Ukrainian translations from Crowdin [ci skip]

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

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

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

* fix: New Portuguese, Brazilian 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 English, United Kingdom translations from Crowdin [ci skip]

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

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

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

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

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

* fix: New Spanish 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 Korean translations from Crowdin [ci skip]

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

* fix: New Polish 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 Ukrainian translations from Crowdin [ci skip]

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

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

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

* fix: New Portuguese, Brazilian 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 English, United Kingdom translations from Crowdin [ci skip]

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

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

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

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

* fix: New Spanish 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 Korean translations from Crowdin [ci skip]

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

* fix: New Polish 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 Ukrainian translations from Crowdin [ci skip]

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

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

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

* fix: New Portuguese, Brazilian 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 English, United Kingdom translations from Crowdin [ci skip]

* fix: New Norwegian Bokmal translations from Crowdin [ci skip]
2026-01-05 21:53:54 -05:00
Tom Moor 8b6b8039c5 fix: Passkeys not supported in desktop (#11072) 2026-01-06 02:20:32 +00:00
Tom Moor 468f5dbf20 References margin (#11071) 2026-01-06 01:21:14 +00:00
Tom Moor 57b6e9aca4 feat: Passkey support (#11065)
closes #6930
2026-01-05 19:58:46 -05:00
Tom Moor 9f07607c05 fix: Exports linked to non-accessible location for non-admins (#11070)
* fix: Incorrect test for non-admins

* Update email language
2026-01-05 19:58:24 -05:00
Tom Moor 17a5c4a4a1 chore: Tidy ApiKeyListItem (#11069) 2026-01-05 19:58:17 -05:00
Tom Moor 09200cab46 chore: Upgrade tmp dep (#11062) 2026-01-04 14:09:23 +00:00
Tom Moor 69fa36c058 Add explicit edit toggle to Mermaid diagrams (#11060)
* Add explicit edit toggle to Mermaid diagrams

* click handling
2026-01-04 08:57:17 -05:00
Tom Moor e9be951694 chore: Update qs dep (#11061) 2026-01-04 08:54:55 -05:00
Tom Moor 9675f9bd87 Show provider ID on auth list (#11058) 2026-01-03 20:06:25 +00:00
Tom Moor b87df2d1a3 Normalize share styling, fix overflow (#11057)
closes #11049
2026-01-03 18:54:59 +00:00
Tom Moor 4f3c573b5d fix: Alignment of integration cards (#11056) 2026-01-03 17:56:01 +00:00
Tom Moor 8e43e044b6 fix: Hover state missing on document list item action (#11055) 2026-01-03 17:51:00 +00:00
Tom Moor 331f25ac17 fix: Hover state is lost on sidebar items when context menu is open (#11054)
* fix: Unread badge position

* fix: Hover state with active context menu
2026-01-03 17:41:50 +00:00
Tom Moor 8ee51a864a Add download options to revision menu (#11048) 2026-01-03 11:25:30 -05:00
Copilot 7e2d2542b0 Split group mention notifications into separate background task (#11040)
* Initial plan

* Split group notifications into separate background task

- Created GroupMentionedInCommentNotificationsTask for handling group mention notifications
- Updated CommentCreatedNotificationsTask to delegate group notifications
- Updated CommentUpdatedNotificationsTask to delegate group notifications
- Added comprehensive tests for the new task
- Improved scalability with batch processing (10 members per batch)
- Enhanced resilience by isolating group notification failures

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Optimize database queries in GroupMentionedInCommentNotificationsTask

- Batch fetch users instead of individual queries for better performance
- Add defensive comment explaining duplicate mentions check
- Create user map for O(1) lookups within batch

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Add comment explaining concurrency control in batch processing

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Remove unnecessary document loading - use event properties directly

- Removed Document import and document loading query
- Use event.documentId and event.teamId directly
- Reduces database queries and improves performance
- canUserAccessDocument loads document internally when needed

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Fix TypeScript errors and test failures

- Fixed test: buildComment requires documentId parameter
- Fixed type error: use nullish coalescing for group.actorId ?? event.actorId
- Fixed type definition: use Omit<CommentEvent, "data"> to avoid type conflicts
- All TypeScript errors resolved

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* test

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>
Co-authored-by: Tom Moor <tom@getoutline.com>
2026-01-03 10:14:56 -05:00
Tom Moor 211b747c0a fix: Update email diff rendering to use ChangesetHelper (#11017)
* stash

* toHTML changes property

* stash

* stash

* document access

* test

* fix cleanup

* cleanup, fixes

* Restore styles

* Remove magic number

* wip
2026-01-03 09:52:08 -05:00
Tom Moor 9c3e896385 Remove yarn usage in prod (#11045) 2026-01-02 21:41:39 -05:00
Tom Moor b1cb6e4a41 fix: Improve handling of object_not_found individual blocks (#11042) 2026-01-02 20:55:03 -05:00
Tom Moor e8019855dc fix: Extra gap in group mention chip (#11041) 2026-01-01 20:36:03 +00:00
Tom Moor 546929cdce fix: Mobile toolbar improvements (#11038) 2026-01-01 14:36:38 -05:00
Copilot e0812faec9 Add revisions.export endpoint for downloading document revisions (#11039)
* Initial plan

* Add revisions.export endpoint with tests

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Fix: Add rejectOnEmpty to Document.findByPk in revisions.export

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>
2026-01-01 14:36:24 -05:00
Tom Moor 06f2123629 fix: Table spacing for southeastern scripts (#11037) 2026-01-01 18:31:31 +00:00
Tom Moor c3b91a8441 fix: Docker build with Yarn 4 (#11031)
* Add missing corepack

* Serialize plugin gen to protect memory

* fix: Missing Yarn4 config

* Prune dev deps
2025-12-31 22:29:25 -05:00
Copilot 755833c64c Fix whitespace preservation after collection/document names in email templates (#11034)
* Initial plan

* Fix missing whitespace after collection/document names in shared emails

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>
2025-12-31 21:26:42 +00:00
Tom Moor 491102e865 fix: Minor layout issues (#11030)
* fix: History empty state

* fix: Layout issues in import dialog
2025-12-31 02:34:53 +00:00
Tom Moor 67397e9b97 chore: Update docker install (#11025) 2025-12-29 16:20:37 -05:00
Tom Moor 363eb0585d v1.2.0-0 2025-12-29 15:53:30 -05:00
dependabot[bot] 05d8f6c438 chore(deps-dev): bump babel-plugin-transform-typescript-metadata (#11021)
Bumps [babel-plugin-transform-typescript-metadata](https://github.com/leonardfactory/babel-plugin-transform-typescript-metadata) from 0.3.2 to 0.4.0.
- [Release notes](https://github.com/leonardfactory/babel-plugin-transform-typescript-metadata/releases)
- [Changelog](https://github.com/leonardfactory/babel-plugin-transform-typescript-metadata/blob/master/CHANGELOG.md)
- [Commits](https://github.com/leonardfactory/babel-plugin-transform-typescript-metadata/compare/v0.3.2...v0.4.0)

---
updated-dependencies:
- dependency-name: babel-plugin-transform-typescript-metadata
  dependency-version: 0.4.0
  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>
2025-12-29 14:27:59 -05:00
dependabot[bot] 8a3ae049b2 chore(deps): bump http-errors from 2.0.0 to 2.0.1 (#11022)
Bumps [http-errors](https://github.com/jshttp/http-errors) from 2.0.0 to 2.0.1.
- [Release notes](https://github.com/jshttp/http-errors/releases)
- [Changelog](https://github.com/jshttp/http-errors/blob/master/HISTORY.md)
- [Commits](https://github.com/jshttp/http-errors/compare/v2.0.0...v2.0.1)

---
updated-dependencies:
- dependency-name: http-errors
  dependency-version: 2.0.1
  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>
2025-12-29 11:13:20 -05:00
Tom Moor efb3178b82 fix: Catch-all route in development (#11016) 2025-12-28 15:58:28 +00:00
Tom Moor 2e1d3f0e65 chore: Add missing error logging on wss connection (#11015) 2025-12-28 10:40:13 -05:00
Tom Moor fd49602e40 feat: Improved revision viewer (#10824) 2025-12-28 08:56:32 -05:00
Tom Moor 222d86d084 fix: Flaky ErrorTimedOutFileOperationsTask test (#11011) 2025-12-27 14:34:38 -05:00
Copilot 7cbd06541c Upgrade Yarn to 4.11.0 (#10865)
* Initial plan

* Upgrade Yarn to 4.11.0 with node-modules linker and security settings

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Restore rolldown resolution to package.json resolutions

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Enable Corepack in CI workflow for Yarn 4.11.0 support

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* test

* module resolution

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>
Co-authored-by: Tom Moor <tom@getoutline.com>
2025-12-25 08:46:08 -05:00
Translate-O-Tron b5305e04e1 New Crowdin updates (#10969)
* fix: New Romanian translations from Crowdin [ci skip]

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

* fix: New Spanish 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 Korean translations from Crowdin [ci skip]

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

* fix: New Polish 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 Ukrainian translations from Crowdin [ci skip]

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

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

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

* fix: New Portuguese, Brazilian 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 English, United Kingdom translations from Crowdin [ci skip]

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

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

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

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

* fix: New Spanish 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 Korean translations from Crowdin [ci skip]

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

* fix: New Polish 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 Ukrainian translations from Crowdin [ci skip]

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

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

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

* fix: New Portuguese, Brazilian 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 English, United Kingdom translations from Crowdin [ci skip]

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

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

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

* fix: New Spanish 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 Korean translations from Crowdin [ci skip]

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

* fix: New Polish 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 Ukrainian translations from Crowdin [ci skip]

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

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

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

* fix: New Portuguese, Brazilian 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 English, United Kingdom translations from Crowdin [ci skip]

* fix: New Norwegian Bokmal translations from Crowdin [ci skip]
2025-12-24 20:41:01 -05:00
Tom Moor 771af42ed0 chore: Add VStack and HStack components (#10971)
* Add VStack and HStack

* VStack,HStack usage

* refactor
2025-12-24 20:40:37 -05:00
Tom Moor a3b2615edf chore: Remove future public bucket usage (#10977)
* No longer upload avatars to public bucket

* Public redirect

* tests

* test

* test
2025-12-24 20:24:58 -05:00
Tom Moor 4c3ed8c87c fix: Grips on merged table cells (#11003) 2025-12-24 19:16:32 -05:00
Tom Moor fbd4ded5b4 feat: Add authentication provider management (#10997)
* Gemini first-pass

* Prevent post-connect login

* stash

* stash

* Add OIDC logo

* Separate security page

* test

* Update icon

* test

* ui

* Add extra guards for disabling auth provider

* refactor

* test
2025-12-24 09:09:24 -05:00
Tom Moor 816b6508b0 fix: Image warp exiting lightbox (#10999)
closes #10731
2025-12-24 09:09:12 -05:00
Tom Moor 9febf90638 Revert "chore(deps): bump dd-trace from 5.76.0 to 5.80.0 (#10922)" (#11000)
This reverts commit 0fdf553ff1.
2025-12-24 09:09:03 -05:00
Tom Moor d67b881b93 fix: Appending content via API (#10998)
* fix: Appending content via API

closes #10936

* Append without newline
2025-12-23 21:48:25 -05:00
Tom Moor 15f2959574 fix: Large base64 images pasted as HTML cause doc not to sync (#10982)
* First pass

* Use placeholders

* tsc
2025-12-23 20:03:37 -05:00
Tom Moor bec1386253 fix: User with 'can edit' permission on sub-document cannot sort (#10990) 2025-12-23 17:37:08 -05:00
Tom Moor 2486eda1c9 design: List view tweaks (#10989)
* Add icons to list views

* Badge

* Badge styling
2025-12-23 11:53:41 -05:00
Tom Moor cf1cd8efd5 perf: Refactor of permission loading (#10988)
* perf: Refactor of permission loading

* Simplify condition
2025-12-23 11:53:31 -05:00
dependabot[bot] 0185dc5aca chore(deps): bump the aws group with 5 updates (#10991)
Bumps the aws group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) | `3.948.0` | `3.956.0` |
| [@aws-sdk/lib-storage](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/lib/lib-storage) | `3.948.0` | `3.956.0` |
| [@aws-sdk/s3-presigned-post](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-presigned-post) | `3.948.0` | `3.956.0` |
| [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-request-presigner) | `3.948.0` | `3.956.0` |
| [@aws-sdk/signature-v4-crt](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/signature-v4-crt) | `3.947.0` | `3.956.0` |


Updates `@aws-sdk/client-s3` from 3.948.0 to 3.956.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.956.0/clients/client-s3)

Updates `@aws-sdk/lib-storage` from 3.948.0 to 3.956.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/lib/lib-storage/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.956.0/lib/lib-storage)

Updates `@aws-sdk/s3-presigned-post` from 3.948.0 to 3.956.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages/s3-presigned-post/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.956.0/packages/s3-presigned-post)

Updates `@aws-sdk/s3-request-presigner` from 3.948.0 to 3.956.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages/s3-request-presigner/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.956.0/packages/s3-request-presigner)

Updates `@aws-sdk/signature-v4-crt` from 3.947.0 to 3.956.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages/signature-v4-crt/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.956.0/packages/signature-v4-crt)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.956.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/lib-storage"
  dependency-version: 3.956.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-presigned-post"
  dependency-version: 3.956.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-request-presigner"
  dependency-version: 3.956.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/signature-v4-crt"
  dependency-version: 3.956.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-22 11:41:49 -05:00
dependabot[bot] 944c1111ee chore(deps-dev): bump @types/turndown from 5.0.5 to 5.0.6 (#10992)
Bumps [@types/turndown](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/turndown) from 5.0.5 to 5.0.6.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/turndown)

---
updated-dependencies:
- dependency-name: "@types/turndown"
  dependency-version: 5.0.6
  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>
2025-12-22 11:41:37 -05:00
dependabot[bot] 51aecc506d chore(deps-dev): bump nodemon from 3.1.10 to 3.1.11 (#10993)
Bumps [nodemon](https://github.com/remy/nodemon) from 3.1.10 to 3.1.11.
- [Release notes](https://github.com/remy/nodemon/releases)
- [Commits](https://github.com/remy/nodemon/compare/v3.1.10...v3.1.11)

---
updated-dependencies:
- dependency-name: nodemon
  dependency-version: 3.1.11
  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>
2025-12-22 11:41:21 -05:00
dependabot[bot] 6a3397974a chore(deps): bump @dotenvx/dotenvx from 1.49.0 to 1.51.2 (#10994)
Bumps [@dotenvx/dotenvx](https://github.com/dotenvx/dotenvx) from 1.49.0 to 1.51.2.
- [Release notes](https://github.com/dotenvx/dotenvx/releases)
- [Changelog](https://github.com/dotenvx/dotenvx/blob/main/CHANGELOG.md)
- [Commits](https://github.com/dotenvx/dotenvx/compare/v1.49.0...v1.51.2)

---
updated-dependencies:
- dependency-name: "@dotenvx/dotenvx"
  dependency-version: 1.51.2
  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>
2025-12-22 11:41:11 -05:00
dependabot[bot] 64a9b3def9 chore(deps): bump class-validator from 0.14.2 to 0.14.3 (#10995)
Bumps [class-validator](https://github.com/typestack/class-validator) from 0.14.2 to 0.14.3.
- [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.2...v0.14.3)

---
updated-dependencies:
- dependency-name: class-validator
  dependency-version: 0.14.3
  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>
2025-12-22 11:40:59 -05:00
Tom Moor 041c4217b2 perf: Update documents popularity score (#10986) 2025-12-22 08:10:27 -05:00
Tom Moor ad513250e5 chore: Remove unneccessary redis warnings (#10985) 2025-12-21 20:52:57 -05:00
Tom Moor 607ca39684 fix: Restore 'Create a doc' item in mention menu (#10980) 2025-12-21 11:24:34 -05:00
Copilot 2959f2b300 Fix invisible email buttons in iOS Mail dark mode (#10976)
* Initial plan

* Fix invisible button in iOS Mail app dark mode using CSS media queries

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Add border

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>
Co-authored-by: Tom Moor <tom@getoutline.com>
2025-12-20 20:27:29 -05:00
Tom Moor 478453009e fix: Display of reactions dialog (#10973) 2025-12-20 14:05:48 -05:00
Tom Moor 7342715535 fix: SQL errors do not correctly bubble in Jest (#10975) 2025-12-20 14:05:39 -05:00
Tom Moor 5e7fa979a6 fix: Menu has context menu (#10974) 2025-12-20 14:05:30 -05:00
Tom Moor bf45e97641 chore: Enforce type import consistency (#10968)
* Update types

* fix circular dep

* type imports

* lint type imports and --fix
2025-12-19 23:07:02 -05:00
Salihu 419cf2a583 PDF embed (#10198)
* simple PDF embded

* send pdf url

* pdf resize

* resize pdf accordingly

* pdf alignment

* minor fixes

* use attachment node for PDF preview

* remove unnecessary comments

* fix pdf class

* minor fixes

* adjust upload pdf logo

* revert SelectionToolbar

* pass embed URL directly

* pass embed URL directly

* remove embedded pdf alignment

* improve resize UX

* improve resize UX

* fix: X-Frame-Options with local hosting
fix: Resize not persisted

* Add dimensions to attachment toolbar

* fix: Styling

* fix: Non-interactable in read-only editor

* Revert unneccessary changes

* Avoid setting width/height on all attachment nodes

* fix: Disable embeds should also disable PDF embeds

---------

Co-authored-by: Tom Moor <tom@getoutline.com>
2025-12-19 21:48:25 -05:00
Tom Moor b45a096aeb feat: Implement RFC 9700 hardening against refresh token reuse (#10960)
* feat: Implement RFC 9700 hardening against refresh token reuse

* tests

* Update tests with less mocking, hit actual endpoints
2025-12-19 17:52:23 -05:00
Copilot 65662ef402 Update request-filtering-agent to v3.2.0 for CIDR range support (#10923)
* Initial plan

* Update request-filtering-agent to v3.2.0 with CIDR support

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Remove unnecessary mock and use transformIgnorePatterns for request-filtering-agent

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Mock

* Revert unused change

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>
Co-authored-by: Tom Moor <tom@getoutline.com>
2025-12-19 17:47:50 -05:00
Translate-O-Tron 9518b3f969 New Crowdin updates (#10755)
* fix: New French translations from Crowdin [ci skip]

* fix: New Spanish 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 Korean translations from Crowdin [ci skip]

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

* fix: New Polish 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 Ukrainian translations from Crowdin [ci skip]

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

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

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

* fix: New Portuguese, Brazilian 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 English, United Kingdom 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 Romanian translations from Crowdin [ci skip]

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

* fix: New Spanish 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 Korean translations from Crowdin [ci skip]

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

* fix: New Polish 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 Ukrainian translations from Crowdin [ci skip]

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

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

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

* fix: New Portuguese, Brazilian 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 English, United Kingdom translations from Crowdin [ci skip]

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

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

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

* fix: New Spanish 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 Korean translations from Crowdin [ci skip]

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

* fix: New Polish 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 Ukrainian translations from Crowdin [ci skip]

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

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

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

* fix: New Portuguese, Brazilian 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 English, United Kingdom translations from Crowdin [ci skip]

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

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

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

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

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

* fix: New Spanish 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 Korean translations from Crowdin [ci skip]

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

* fix: New Polish 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 Ukrainian translations from Crowdin [ci skip]

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

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

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

* fix: New Portuguese, Brazilian 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 English, United Kingdom translations from Crowdin [ci skip]

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

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

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

* fix: New Spanish 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 Korean translations from Crowdin [ci skip]

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

* fix: New Polish 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 Ukrainian translations from Crowdin [ci skip]

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

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

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

* fix: New Portuguese, Brazilian 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 English, United Kingdom translations from Crowdin [ci skip]

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

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

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

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

* fix: New Spanish 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 Korean translations from Crowdin [ci skip]

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

* fix: New Polish 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 Ukrainian translations from Crowdin [ci skip]

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

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

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

* fix: New Portuguese, Brazilian 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 English, United Kingdom translations from Crowdin [ci skip]

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

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

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

* fix: New Spanish 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 Korean translations from Crowdin [ci skip]

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

* fix: New Polish 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 Ukrainian translations from Crowdin [ci skip]

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

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

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

* fix: New Portuguese, Brazilian 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 English, United Kingdom translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: New Spanish 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 Korean translations from Crowdin [ci skip]

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

* fix: New Polish 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 Ukrainian translations from Crowdin [ci skip]

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

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

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

* fix: New Portuguese, Brazilian 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 English, United Kingdom translations from Crowdin [ci skip]

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

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

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

* fix: New Spanish 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 Korean translations from Crowdin [ci skip]

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

* fix: New Polish 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 Ukrainian translations from Crowdin [ci skip]

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

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

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

* fix: New Portuguese, Brazilian 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 English, United Kingdom translations from Crowdin [ci skip]

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

* fix: New German translations from Crowdin [ci skip]
2025-12-19 17:33:07 -05:00
HJ Doo 605a488a40 fix(editor): prevent crash in checkbox parser (#10949)
When parsing a list item for a checkbox, the code did not validate that the first child token was a text token. This could lead to a runtime error when pasting content that resulted in a different token structure.

This change adds a check to ensure the first child is a text token before attempting to access its content, preventing the crash.
2025-12-19 08:43:16 -05:00
Tom Moor 2b84770b0d fix: path in sitemap.xml for root node on domain share (#10957) 2025-12-18 18:12:15 -05:00
Tom Moor 2f9b30c30c fix: Extra newlines in pasted code blocks (#10958)
* fix: Extra newlines in pasted code blocks

This code is super old, came across with the old markdown editor.

It seems to not have any real effect on well-formatted markdown

* Add tests for normalizePastedMarkdown internal logic (#10959)

* Initial plan

* Add comprehensive tests for normalizePastedMarkdown

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

---------

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>
2025-12-18 18:11:55 -05:00
Tom Moor 9a3821d806 fix: fetchDocuments still required on collection root (#10956) 2025-12-18 08:43:04 -05:00
Copilot 539b9a8114 Add ContextMenu to RevisionListItem (#10952)
* Initial plan

* Add ContextMenu to RevisionListItem with same menu items

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Fix: Use numeric opacity values for better type safety

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Inline menu action logic in RevisionListItem instead of separate hook

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* fix: Account for not current url

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>
Co-authored-by: Tom Moor <tom@getoutline.com>
2025-12-18 08:37:37 -05:00
Tom Moor 58ddcd1b1e wip (#10953) 2025-12-18 07:49:04 -05:00
Tom Moor 105edbde25 chore: Upgrade rolldown-vite (#10946) 2025-12-18 07:33:07 -05:00
Tom Moor 85cd2d0c2c perf: Remove default fetchDocuments call (#10951) 2025-12-18 07:32:47 -05:00
Tom Moor 93356472a1 fix: Comment.data should not be included in event log (#10947) 2025-12-17 23:37:21 -05:00
Tom Moor 2dba0cb4c0 Update AGENTS.md 2025-12-17 23:18:29 -05:00
Tom Moor a54e66e19a chore: Test improvements (#10945)
* Lazy queues, correctly closing Redis and server

* feedback

* fix: Tests not correctly split across matrix
2025-12-17 23:15:55 -05:00
Tom Moor b722a361ff chore: Update prosemirror packages (#10942) 2025-12-17 20:42:01 -05:00
Tom Moor fe5cc8e007 chore: Cleanup of CI logs and connections (#10944)
* chore: Remove info logs in CI

* Upgrade jest
2025-12-17 20:41:51 -05:00
Tom Moor 6499164187 chore: fix MaxEventListener warning in tests (#10943) 2025-12-17 18:57:56 -05:00
Tom Moor 02aee884a3 chore: Remove unused indexes (#10941) 2025-12-17 17:26:38 -05:00
Tom Moor 4a59faf1a5 chore: Add missing indexes for hooks.unfurl (#10940) 2025-12-17 17:26:29 -05:00
Tom Moor e0be5cf1d0 perf: Inline lastActiveAt update (#10931) 2025-12-16 08:33:18 -05:00
Tom Moor 6fb5ca0d7d fix: Focus reset on comment edit (#10932) 2025-12-16 08:33:10 -05:00
Tom Moor 206df6afd7 perf: Reducing database contention (#10926)
* perf: Remove findOrCreate in views.create

* findOrCreate for subscriptionCreator

* test

* Remove findOrCreate for document,collection,group memberships

* tsc
2025-12-16 04:58:21 -05:00
Tom Moor 4fb52f9776 fix: Comments draw on mobile is not scrollable (#10928) 2025-12-16 04:57:43 -05:00
dependabot[bot] 0c1a31e199 chore(deps): bump turndown from 7.2.1 to 7.2.2 (#10919)
Bumps [turndown](https://github.com/mixmark-io/turndown) from 7.2.1 to 7.2.2.
- [Release notes](https://github.com/mixmark-io/turndown/releases)
- [Commits](https://github.com/mixmark-io/turndown/compare/v7.2.1...v7.2.2)

---
updated-dependencies:
- dependency-name: turndown
  dependency-version: 7.2.2
  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>
2025-12-15 23:20:05 -05:00
Tom Moor 08eab03041 perf: Ensure collab persistence cannot hold lock for more than 15s (#10927) 2025-12-15 23:19:52 -05:00
Tom Moor e6d4ae0c87 chore: Remove flaky tests (#10929) 2025-12-15 23:19:43 -05:00
Tom Moor e0dd08998d fix: Restore EventBoundary in a better location (#10924) 2025-12-16 02:32:58 +00:00
dependabot[bot] 8e87291037 chore(deps): bump the aws group with 5 updates (#10918)
Bumps the aws group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) | `3.946.0` | `3.948.0` |
| [@aws-sdk/lib-storage](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/lib/lib-storage) | `3.946.0` | `3.948.0` |
| [@aws-sdk/s3-presigned-post](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-presigned-post) | `3.946.0` | `3.948.0` |
| [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-request-presigner) | `3.946.0` | `3.948.0` |
| [@aws-sdk/signature-v4-crt](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/signature-v4-crt) | `3.946.0` | `3.947.0` |


Updates `@aws-sdk/client-s3` from 3.946.0 to 3.948.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.948.0/clients/client-s3)

Updates `@aws-sdk/lib-storage` from 3.946.0 to 3.948.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/lib/lib-storage/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.948.0/lib/lib-storage)

Updates `@aws-sdk/s3-presigned-post` from 3.946.0 to 3.948.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages/s3-presigned-post/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.948.0/packages/s3-presigned-post)

Updates `@aws-sdk/s3-request-presigner` from 3.946.0 to 3.948.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages/s3-request-presigner/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.948.0/packages/s3-request-presigner)

Updates `@aws-sdk/signature-v4-crt` from 3.946.0 to 3.947.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages/signature-v4-crt/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.947.0/packages/signature-v4-crt)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.948.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/lib-storage"
  dependency-version: 3.948.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-presigned-post"
  dependency-version: 3.948.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-request-presigner"
  dependency-version: 3.948.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/signature-v4-crt"
  dependency-version: 3.947.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-15 21:28:47 -05:00
dependabot[bot] bb8ee65dc6 chore(deps): bump i18next-fs-backend from 2.6.0 to 2.6.1 (#10920)
Bumps [i18next-fs-backend](https://github.com/i18next/i18next-fs-backend) from 2.6.0 to 2.6.1.
- [Changelog](https://github.com/i18next/i18next-fs-backend/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next-fs-backend/compare/v2.6.0...v2.6.1)

---
updated-dependencies:
- dependency-name: i18next-fs-backend
  dependency-version: 2.6.1
  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>
2025-12-15 21:28:39 -05:00
dependabot[bot] 0fdf553ff1 chore(deps): bump dd-trace from 5.76.0 to 5.80.0 (#10922)
Bumps [dd-trace](https://github.com/DataDog/dd-trace-js) from 5.76.0 to 5.80.0.
- [Release notes](https://github.com/DataDog/dd-trace-js/releases)
- [Commits](https://github.com/DataDog/dd-trace-js/compare/v5.76.0...v5.80.0)

---
updated-dependencies:
- dependency-name: dd-trace
  dependency-version: 5.80.0
  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>
2025-12-15 21:28:20 -05:00
Tom Moor 937b224f63 feat: Add ability to filter notifications by type (#10916)
* stash

* refactor

* fix scrolling

* restore
2025-12-15 07:32:47 -05:00
Tom Moor 14cdfb6917 fix: Mismatched avatar sizes (#10915) 2025-12-14 19:16:31 -05:00
Tom Moor 1328fef603 feat: Add context menu to notifications (#10914) 2025-12-14 19:16:22 -05:00
Tom Moor 5d5e56251c fix: HealthMonitor check can fail if long running job (#10883) 2025-12-14 19:15:43 -05:00
Tom Moor f309f39b5e Move icon above header on mobile (#10912) 2025-12-14 22:40:12 +00:00
Tom Moor 7426ed785f fix: New comment auto-focus (#10911)
* refactor

* fix: autoFocus on comment editor
2025-12-14 17:10:18 -05:00
Tom Moor 52936f9d22 fix: SelectionToolbar missing on mobile (#10905)
* fix: Selection toolbar missing on mobile

* Mobile toolbar positioning
2025-12-14 02:08:01 +00:00
Tom Moor 92b24f9460 fix: Comment actions do not reliably appear in mobile drawer (#10904)
* fix: Comment actions do not reliably appear in mobile drawer

* fix: Reaction picker in mobile comments
2025-12-14 00:58:49 +00:00
Copilot 6ab63ecca1 Fix race condition in AuthenticationProvider.disable leaving teams locked out (#10902)
* Initial plan

* Fix race condition in AuthenticationProvider.disable with shared lock

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Address code review comments: add transaction validation and test assertion

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Revert changes to .env.test file

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>
2025-12-13 17:54:37 -05:00
Tom Moor d05b888bc9 fix: pins.list does not check collection policy (#10903)
This does not leak document info, by may leak ID's
2025-12-13 15:06:34 -05:00
Copilot 2772de2766 Fix security check in /auth/redirect comparing against undefined ctx.params.token (#10894)
* Initial plan

* Fix security check in /auth/redirect to use ctx.state.auth.token instead of ctx.params.token

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>
2025-12-13 15:02:39 -05:00
Copilot 44b754884f Fix double pagination in documents.list and documents.archived with sort=index (#10895)
* Initial plan

* Fix double pagination bug and add tests

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Add comments explaining pagination behavior

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Fix test expectations for reversed document order

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* fix

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>
Co-authored-by: Tom Moor <tom@getoutline.com>
2025-12-13 14:48:47 -05:00
Tom Moor 9704bc188f chore: Restart on .env changes (#10899) 2025-12-13 14:37:42 -05:00
Tom Moor 8fc44ca681 fix: Move Slack credentials to Authorization header in token exchange (#10898) 2025-12-13 12:59:38 -05:00
Tom Moor e2e8d23428 fix: Validation of SECRET_KEY environment variable is too loose (#10897) 2025-12-13 12:51:33 -05:00
Tom Moor 2e48ed8cd1 fix: Replace the strict higher-than check with a condition that includes Viewer as a valid previous role (#10877) 2025-12-13 12:42:06 -05:00
Tom Moor a33731dd23 fix: Base64 uploads are not correctly verified for size limits (#10878) 2025-12-13 12:41:58 -05:00
Tom Moor 5c37f0a91d fix: Details returned from OAuth client list endpoint (#10896) 2025-12-13 12:41:43 -05:00
Tom Moor 615cad5484 fix: Incorrect handling of missing refresh token (#10886) 2025-12-13 12:37:06 -05:00
Copilot 478781ae53 Fix custom rate limiters ignored due to mountPath mismatch (#10893)
* Initial plan

* Fix rate limiter path mismatch bug by using fullPath in defaultRateLimiter

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>
2025-12-13 12:14:22 -05:00
Tom Moor 07421a3cba fix: Return placeholder tasks object on public shares (#10888) 2025-12-13 07:16:18 -05:00
Tom Moor 774c973e0d fix: Replacement parameters in index collision query (#10880)
* fix: Replacement parameters in index collision query

* refactor
2025-12-12 23:03:59 -05:00
Tom Moor 4777a90fa9 fix: hooks.unfurl check (#10884) 2025-12-12 22:54:56 -05:00
Tom Moor a51188882b fix: isUrl requireHttps option is never hit (#10885) 2025-12-12 22:54:43 -05:00
Tom Moor bdeac4e44b fix: profileId extraction in OIDC does not fallback to token.sub (#10882) 2025-12-12 22:21:16 -05:00
Tom Moor f085a30406 fix: Shutdown during migrations does not release mutex lock (#10879)
* fix: Shutdown during migrations does not release mutex lock

* tsc
2025-12-12 22:20:53 -05:00
Tom Moor e3f23d7324 chore: Upgrade caniuse (#10881) 2025-12-12 22:20:00 -05:00
Copilot 682f9a1f88 Add index on source column for search_queries table (#10876)
* Initial plan

* Add migration to create index on source column for search_queries table

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>
2025-12-12 20:04:41 -05:00
Apoorv Mishra 948e557bdd Utilize GitHub integration to fetch information about public issues/PRs (#10827)
* fix: use github APIs to unfurl public gh issues/prs

* fix: revert

* fix: multiple gh accounts

* fix: use replacements
2025-12-12 19:05:14 -05:00
Copilot d5dbf286cc Add missing database indexes for hooks.unfurl endpoint (#10870)
* Initial plan

* Add database indexes to improve hooks.unfurl performance

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Verify migrations and query plans for new indexes

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Address code review feedback: improve migration rollback order and add comments

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Change index column order to teamId first as requested

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Update .env.test

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>
Co-authored-by: Tom Moor <tom@getoutline.com>
2025-12-12 19:02:20 -05:00
Tom Moor 27f4ba7062 perf: Reorder policy checks (#10874)
* Reorder document policy checks

* Reorder collection policy checks
2025-12-12 18:58:23 -05:00
Tom Moor c3ffcd8d38 Update documents.ts (#10873) 2025-12-12 18:46:08 -05:00
Tom Moor e19b23c22f perf: Remove serialization of tasks for public API responses (#10864) 2025-12-11 22:40:26 -05:00
Tom Moor 2e471f88be perf: Policy evaluation (#10863)
* perf: Several O(n) improvements in policy calculation

* perf: Simplify to single loop in can method

* perf: refactor ability lookups
2025-12-11 20:51:22 -05:00
Tom Moor 8cb07889ce perf: Further break up popularity batch querying (#10862) 2025-12-11 18:58:18 -05:00
Apoorv Mishra f8a79f9e79 Bring back notice menu (#10860)
* fix: notice menu regression

* fix: local var
2025-12-11 10:51:19 -05:00
Tom Moor 1e894aabdf fix: Query not forwarded on internal links (#10854)
closes #10853
2025-12-11 02:17:12 +00:00
Tom Moor 6cd2346d46 fix: Media editor crashes page (#10852)
closes #10851
2025-12-11 00:43:19 +00:00
Tom Moor 35510fb4be fix: Ignore missing .env in bootstrap.ts (#10848) 2025-12-10 17:14:39 -05:00
Tom Moor ac460318fd fix: Quick fix for selection behavior (#10845) 2025-12-10 14:24:55 +00:00
Tom Moor 6b3900cfc5 fix: Code language picker missing (#10844) 2025-12-10 08:35:35 -05:00
Tom Moor 2543d6d56c fix: Code words should wrap on mobile (#10842) 2025-12-09 22:05:47 -05:00
Apoorv Mishra 5140d2434e fix: apply react/rules-of-hooks (#10840) 2025-12-09 18:53:49 -05:00
dependabot[bot] 108e14338b chore(deps): bump the aws group with 5 updates (#10830)
Bumps the aws group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) | `3.927.0` | `3.946.0` |
| [@aws-sdk/lib-storage](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/lib/lib-storage) | `3.927.0` | `3.946.0` |
| [@aws-sdk/s3-presigned-post](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-presigned-post) | `3.927.0` | `3.946.0` |
| [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-request-presigner) | `3.927.0` | `3.946.0` |
| [@aws-sdk/signature-v4-crt](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/signature-v4-crt) | `3.927.0` | `3.946.0` |


Updates `@aws-sdk/client-s3` from 3.927.0 to 3.946.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.946.0/clients/client-s3)

Updates `@aws-sdk/lib-storage` from 3.927.0 to 3.946.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/lib/lib-storage/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.946.0/lib/lib-storage)

Updates `@aws-sdk/s3-presigned-post` from 3.927.0 to 3.946.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages/s3-presigned-post/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.946.0/packages/s3-presigned-post)

Updates `@aws-sdk/s3-request-presigner` from 3.927.0 to 3.946.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages/s3-request-presigner/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.946.0/packages/s3-request-presigner)

Updates `@aws-sdk/signature-v4-crt` from 3.927.0 to 3.946.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages/signature-v4-crt/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.946.0/packages/signature-v4-crt)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.946.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/lib-storage"
  dependency-version: 3.946.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-presigned-post"
  dependency-version: 3.946.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-request-presigner"
  dependency-version: 3.946.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/signature-v4-crt"
  dependency-version: 3.946.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-08 17:15:22 -05:00
dependabot[bot] df284756f1 chore(deps): bump prosemirror-inputrules from 1.5.0 to 1.5.1 (#10831)
Bumps [prosemirror-inputrules](https://github.com/prosemirror/prosemirror-inputrules) from 1.5.0 to 1.5.1.
- [Changelog](https://github.com/ProseMirror/prosemirror-inputrules/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prosemirror/prosemirror-inputrules/compare/1.5.0...1.5.1)

---
updated-dependencies:
- dependency-name: prosemirror-inputrules
  dependency-version: 1.5.1
  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>
2025-12-08 17:15:09 -05:00
dependabot[bot] 410c196943 chore(deps-dev): bump discord-api-types from 0.38.30 to 0.38.36 (#10832)
Bumps [discord-api-types](https://github.com/discordjs/discord-api-types) from 0.38.30 to 0.38.36.
- [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.38.30...0.38.36)

---
updated-dependencies:
- dependency-name: discord-api-types
  dependency-version: 0.38.36
  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>
2025-12-08 17:14:45 -05:00
dependabot[bot] 4893c61a1f chore(deps-dev): bump ioredis-mock from 8.9.0 to 8.13.1 (#10833)
Bumps [ioredis-mock](https://github.com/stipsan/ioredis-mock) from 8.9.0 to 8.13.1.
- [Release notes](https://github.com/stipsan/ioredis-mock/releases)
- [Changelog](https://github.com/stipsan/ioredis-mock/blob/main/CHANGELOG.md)
- [Commits](https://github.com/stipsan/ioredis-mock/compare/v8.9.0...v8.13.1)

---
updated-dependencies:
- dependency-name: ioredis-mock
  dependency-version: 8.13.1
  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>
2025-12-08 17:14:35 -05:00
dependabot[bot] 6e2793a751 chore(deps): bump form-data from 4.0.4 to 4.0.5 (#10834)
Bumps [form-data](https://github.com/form-data/form-data) from 4.0.4 to 4.0.5.
- [Release notes](https://github.com/form-data/form-data/releases)
- [Changelog](https://github.com/form-data/form-data/blob/master/CHANGELOG.md)
- [Commits](https://github.com/form-data/form-data/compare/v4.0.4...v4.0.5)

---
updated-dependencies:
- dependency-name: form-data
  dependency-version: 4.0.5
  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>
2025-12-08 17:14:26 -05:00
Salihu 5e5b37c418 only show "show more" when not loading (#10837) 2025-12-08 17:14:07 -05:00
Tom Moor e30c35acdb fix: Another spot rules of hooks were broken (#10836) 2025-12-08 19:27:40 +00:00
Tom Moor c3ad5bb7f6 fix: Rules of hooks error (#10820) 2025-12-07 11:29:02 +00:00
Tom Moor 971c542613 fix: "Edit" button for collection overview (#10816)
* refactor

* working

* collectionPath refactor

* fix: Flush debounced save

* Move to Actions component

* Keyboard shortcuts

* PR feedback
2025-12-06 18:02:58 -05:00
Tom Moor 3681d1c9b2 fix: Fetch emoji name if necessary (#10819)
* fix: Fetch emoji name if neccessary

* Avoid re-render

* fix: Minor layout issues in emoji dialog picker

* Add upload button to empty search results
2025-12-06 18:45:40 +00:00
Tom Moor 621409ae0b fix: Ensure shutdown with db migration lock correctly releases (#10817)
* fix: Ensure unsafe shutdown with db migration lock correctly releases

* Update server/utils/MutexLock.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-06 13:43:00 -05:00
Apoorv Mishra 8af6fdcc4f Backlink to toolbar menu (#10762)
* fix: port link related commands to work for image selection

* fix: selection

* fix: click img to open link

* fix: hover preview for image with link

* cleanup: hasLink not needed

* fix: we've img wrapped in an `a` tag now, so this is no more required

* fix: cover all edge cases

* fix: cleanup

* fix: zoom in action button in edit mode

* fix: separator div instead of gap

* fix: toolbar refactor

* fix: back button press

* fix: import

* fix: revert

* fix: enum

* fix: onClick on item

* fix: selection at end after link

* fix: show linkbar if link present

* fix: ReturnIcon

* fix: onClickBack

* fix: TOOLBAR -> Toolbar

* fix: show zoom in icon even when selected

* fix: isInlineMarkActive

* fix: jsdoc

* yarn.lock

* Revert "yarn.lock"

This reverts commit 5f44e5e017.

* fix: yarn.lock

* fix: link editor closes upon zoom in click action

* Update shared/editor/queries/isMarkActive.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update shared/editor/queries/getMarkRange.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update shared/editor/components/Image.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update app/editor/components/LinkEditor.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix: prevent toolbar state reset

* fix: tooltip

* fix: copilot

* fix: i18n, misuse of attrs

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Tom Moor <tom@getoutline.com>
2025-12-06 12:21:35 -05:00
Tom Moor ccbc7779ea feat: Add fade variant of Scrollable (#10814) 2025-12-06 09:47:10 -05:00
Tom Moor 2eeeae4a7c perf: Table decorations (#10812)
* perf: CodeFence decos

* perf: Caching for table decos

* PR feedback
2025-12-06 12:08:22 +00:00
Tom Moor 050499b8fc chore: Convert dashes to underscores instead of removing (#10811) 2025-12-06 03:54:26 +00:00
Tom Moor 7d88c97914 fix: Heading caret positioning (#10810)
* fix: Avoid decoration

* perf
2025-12-06 03:49:43 +00:00
Tom Moor e82c848051 feat: Add upload button in emoji picker (#10809) 2025-12-05 20:52:59 -05:00
Salihu 4b6c6f7b36 feat: Distribute table columns evenly (#10645)
* space columns evenly feature

* more accurate col width

* distribute table width more accurately

* minor fix

* code cleanup

* minor fixes

* minor fix

* adjust icon

* language, remove font awesome usage

---------

Co-authored-by: Tom Moor <tom@getoutline.com>
2025-12-06 01:25:04 +00:00
Tom Moor d795e78b79 fix: Shared document root node alignment (#10808)
closes #10800
2025-12-06 01:09:30 +00:00
Tom Moor 5b3d6c3535 fix: Use XMLSerializer to extract valid XML (#10807) 2025-12-06 01:02:07 +00:00
Tom Moor 6f3534c713 feat: Custom emoji reactions (#10805)
* Claude first pass

* Move custom emojis first in search results

* refactor

* fix: Remove extra load emoji call
2025-12-05 18:47:12 -05:00
Copilot 133ec073be Add CSV export for member list (#10803)
* Initial plan

* Add CSV export functionality to members page

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Align Export CSV button to the right

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Address code review feedback: improve type safety, error handling, and date formatting

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Improve CSV utility and date handling consistency

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Improve error messages and fix useCallback dependencies

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Add comprehensive tests for CSV utility

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Refactor: reduce limit to 100, replace lastActiveIp with role, extract ExportCSV component

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Improve type safety and extract pagination constant

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* refactor

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>
Co-authored-by: Tom Moor <tom@getoutline.com>
2025-12-05 09:42:36 -05:00
Tom Moor 305b81fbf4 fix: Badges should never wrap (#10804) 2025-12-05 13:43:22 +00:00
Tom Moor aeb777b2f5 fix: Shift paste inserts next to selection (#10799) 2025-12-04 23:11:33 -05:00
Tom Moor f307c678c2 feat: Support user initials in mention search (#10797)
* feat: Support user initials in mention search

* test
2025-12-05 03:32:29 +00:00
Tom Moor ac23277b7c fix: Find and replace positioning (#10795) 2025-12-05 03:19:48 +00:00
Copilot eee64e363f Skip compression for GIF emoji uploads to preserve animation (#10792)
* Initial plan

* Skip compression for GIF files to preserve animation

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Simplify type assertions in tests

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Remove unnecessary test file

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>
2025-12-05 02:45:39 +00:00
Copilot 55116b4761 Fix template insertion to use cursor position instead of document start (#10783)
* Initial plan

* Fix template insertion to use current cursor position

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Apply prettier formatting

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>
2025-12-04 21:45:18 -05:00
dependabot[bot] 9746df193c chore(deps): bump jws from 3.2.2 to 3.2.3 (#10793)
Bumps [jws](https://github.com/brianloveswords/node-jws) from 3.2.2 to 3.2.3.
- [Release notes](https://github.com/brianloveswords/node-jws/releases)
- [Changelog](https://github.com/auth0/node-jws/blob/master/CHANGELOG.md)
- [Commits](https://github.com/brianloveswords/node-jws/compare/v3.2.2...v3.2.3)

---
updated-dependencies:
- dependency-name: jws
  dependency-version: 3.2.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-04 13:18:05 -05:00
Copilot 9c3956f72d Auto-generate emoji name from uploaded filename (#10788)
* Initial plan

* Add auto-generate emoji name from filename functionality

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Fix useCallback dependency issue in handleFileSelection

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Remove numbers from generated emoji names

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>
2025-12-04 10:08:26 -05:00
Tom Moor 0d51b43ebe fix: Missing locations for initial prop (#10779) 2025-12-03 21:42:42 -05:00
Tom Moor 6976f01d7d chore: Reduce lock contention in collaboration server (#10778)
* chore: Reduce lock contention in collaboration server

* Review suggestions
2025-12-03 20:54:27 -05:00
Copilot d02f35b770 Fix collection filter returning documents from all collections when no search query (#10775)
* Initial plan

* Fix search filtering by collection to exclude other collections

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Add API-level test for collection filtering without search term

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* fix: Private collections

* refactor

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>
Co-authored-by: Tom Moor <tom@getoutline.com>
2025-12-03 20:49:57 -05:00
Tom Moor 55f21bfbb3 fix: Rapid retry behavior (#10776) 2025-12-03 20:25:12 -05:00
Nowon Lee 1d2f6e7c55 fix: unable to delete the last character before a inline image (#10759) 2025-12-03 23:20:10 +00:00
Hemachandar 321d0ee124 Rename ActionV2 to Action (#10770)
* chore: Remove `Action` v1 dependency

* cleanup

* rename
2025-12-03 18:16:36 -05:00
Tom Moor 94252672f8 feat: Allow PKCE clients to refresh tokens (#10769)
* Add clientType concept

* Add clientType mutations

* tsc

* i18n

* fix: Invalid input handling

* tsc
2025-12-03 18:09:43 -05:00
Tom Moor a8048962f6 fix: sitemap.xml base url (again) (#10764) 2025-12-02 21:13:16 -05:00
Tom Moor f009236144 feat: Custom emojis in editor (#10758)
* Working pass, needs refactor

* revert

* fix: Copy/paste behavior

* fix: Public share rendering

* fixes

* fix: Click around emoji atom behavior

* fix: Cannot position caret next to heading

* Update app/scenes/Settings/components/EmojisTable.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-02 20:17:17 -05:00
dependabot[bot] 977e01e96a chore(deps): bump validator from 13.15.20 to 13.15.22 (#10763)
Bumps [validator](https://github.com/validatorjs/validator.js) from 13.15.20 to 13.15.22.
- [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.15.20...13.15.22)

---
updated-dependencies:
- dependency-name: validator
  dependency-version: 13.15.22
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-02 17:00:02 -05:00
dependabot[bot] 66c31c2b99 chore(deps): bump nodemailer from 7.0.7 to 7.0.11 (#10761)
Bumps [nodemailer](https://github.com/nodemailer/nodemailer) from 7.0.7 to 7.0.11.
- [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/v7.0.7...v7.0.11)

---
updated-dependencies:
- dependency-name: nodemailer
  dependency-version: 7.0.11
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-02 07:00:25 -05:00
Tom Moor 61e06cfe86 fix: sitemap.xml contains incorrect hostname on custom domain share (#10760) 2025-12-02 06:59:20 -05:00
Copilot d75f8d64db Support PostgreSQL multi-host connection URIs in DATABASE_URL (#10754)
* Initial plan

* Add isDatabaseUrl validator for multi-host PostgreSQL URIs

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Update env.ts

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>
Co-authored-by: Tom Moor <tom@getoutline.com>
2025-12-01 08:12:39 -05:00
Translate-O-Tron 8546a2bada New Crowdin updates (#10714) 2025-12-01 02:49:14 +01:00
Salihu 430883f186 feat: Custom emoji (#10513)
Towards #9278
2025-12-01 02:31:50 +01:00
Copilot 25a1bf6889 Hide diagrams.net functionality in desktop app (#10753)
* Initial plan

* Hide diagrams.net functionality in desktop app

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Add unit tests for menu filtering logic

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Fix missing ImageSource import in test

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

* Remove test files as requested

Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>
2025-11-30 20:10:17 +01:00
Tom Moor 71d15a3f9b chore: Plugin naming consistency (#10751) 2025-11-29 20:29:50 +00:00
Tom Moor ac06a06a66 feat: Diagrams/Draw.io integration (#10707)
* wip

* wip

* tsc

* lint

* Detect imported Draw.io

* Add empty diagram placeholder

* fix: Do not close editor on save
fix: Account for nodes moving / multiplayer

* fix: Reduce image menu for diagrams

* Add custom server settings page

* refactor

* sp

* Move edit button
2025-11-29 21:02:08 +01:00
Tom Moor d9c50edd98 fix: MMB on internal link in Firefox opens multiple tabs (#10748) 2025-11-29 02:15:05 +01:00
Tom Moor b90da88341 fix: Magic link url incorrect for custom domains (#10746) 2025-11-28 23:06:38 +01:00
Tom Moor 1c6fd082a0 feat: Add /sitemap.xml route for root shares (#10745) 2025-11-28 23:06:24 +01:00
Tom Moor 19858845ff fix: No IP provided to insertEvent (#10743)
* fix: No IP provded to insertEvent

* fix
2025-11-28 15:05:13 +01:00
Tom Moor 65db323ce6 fix: Do not fail attachment model delete (#10744) 2025-11-28 15:05:02 +01:00
Tom Moor 7ce407910e fix: Improve validation of urls extracted from data transfer event (#10740) 2025-11-27 20:04:14 +01:00
Tom Moor e85fbf3299 Add AGENTS.md (#10739) 2025-11-27 18:46:38 +01:00
Tom Moor 42959d66db chore: Add cron task partitioning (#10736)
* wip

* Implementation complete

* tidying

* test

* Address feedback

* Remove duplicative retry logic from UpdateDocumentsPopularityScoreTask.
Now that we're split across many runs this is not neccessary

* Refactor to subclass, config to instance

* Refactor BaseTask to named export

* fix: Missing partition

* tsc

* Feedback
2025-11-27 16:57:52 +01:00
codegen-sh[bot] 4212e0e8d4 Fix flaky availableTeams test by sorting team IDs (#10737) 2025-11-27 14:28:23 +01:00
Tom Moor 8b205fdf09 fix: In-document find fails with multiple escaped characters (#10735)
closes #10732
2025-11-26 11:29:32 +00:00
Tom Moor df5fd8d0db fix: Add missing escape on head tags (#10734) 2025-11-26 12:18:47 +01:00
Tom Moor b6a8986235 chore: Update UpdateDocumentsPopularityScoreTask to run 4 times per day (#10729)
* chore: Update UpdateDocumentsPopularityScoreTask to run 4 times per day

* chore: Add index to popularityScore column
2025-11-26 01:39:16 +01:00
Tom Moor ac820e4e2a fix: Speed up popularity score calculation further (#10728)
* fix: Speed up popularity score calculation further

* Add READ_ONLY database connection

* UNNEST performs better

* Move config to env
2025-11-26 01:06:24 +01:00
Tom Moor e3c5be6e57 fix: Using keys instead of values meant cron task fallback (#10726) 2025-11-26 01:06:11 +01:00
Tom Moor 9dfdf9a1ec fix: Refactor to decrease lock contention (#10727) 2025-11-25 23:32:55 +01:00
Tom Moor 9c0d6fcc42 fix: Root shares do not include sitemap (#10725)
* fix: Sitemap not included on root shares

* fix: Remove apple-touch-icon on public shares
fix: Remove opensearch.xml on public shares
2025-11-25 12:59:32 +00:00
Tom Moor 8b2214fa5e fix: Popularity score should be calculated hourly, not daily (#10723) 2025-11-25 12:36:59 +00:00
Tom Moor 747fde1105 feat: Sync avatars automatically from iDP (#10718)
* feat: Sync avatars automatically from iDP unless user has manually uploaded

* Update test for new logic
2025-11-25 13:36:15 +01:00
Tom Moor b51692cdc5 feat: Add popularity scoring (#10721)
* Simple first pass

* Use findAllInBatches

* Add comments,views,revisions

* Add 'popular' tab to Home

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Add 'Popular' tab to collections

* Boost search results based on popularityScore

* Move to unlogged temp table

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-25 13:11:05 +01:00
Salihu 74f66af232 fix: click name to toggle expanded when on collection or document page (#10713) 2025-11-25 01:43:35 +01:00
Tom Moor 8ba1dfb708 fix: Public sitemaps (#10716)
* fix: sitemap meta tag links to 400

* fix: Do not generate sitemap when allow indexing is disabled
2025-11-25 00:42:36 +01:00
Tom Moor cae0c5c8fc fix: Search popover on public pages (#10717) 2025-11-25 00:42:29 +01:00
Tom Moor e767ec34db fix: Cannot delete first character in heading (#10706) 2025-11-24 01:50:18 +01:00
Tom Moor 1be502105c fix: Mermaid injecting errors into page (#10705) 2025-11-23 22:09:49 +00:00
Tom Moor 82021b685e fix: 'Empty trash' button should be hidden with no permissions (#10704) 2025-11-23 20:00:28 +00:00
Tom Moor 9925c692c1 fix: Add @SkipChangeset to document summary (#10703) 2025-11-23 19:50:54 +00:00
Hemachandar 142985c6d7 Move Document event writing to model layer (#9790)
* documents.restore, documents.unarchive

* documents.templatize

* documents.archive

* documents.unpublish

* documents.create, documents.update

* documents.title_change event

* documents.move

* documents.delete

* tsc, tests

* tsc

* Copilot feedback

---------

Co-authored-by: Tom Moor <tom@getoutline.com>
2025-11-23 20:40:45 +01:00
Tom Moor f4d9b6b257 chore: Remove chalk module (#10700) 2025-11-23 20:01:49 +01:00
Tom Moor 893e451f7f Improve liklihood of auto-reload (#10699) 2025-11-23 18:48:24 +00:00
Tom Moor bd9b54e8dc chore: Port changes from upstream (#10698) 2025-11-23 19:45:52 +01:00
Tom Moor 1e36b459dc feat: UI color picker available in icon picker (#10696)
* wip

* fix: Mobile treatment

* Debounce SwatchButton onSelect
2025-11-23 16:47:28 +01:00
Tom Moor 2abc1e97ae fix: 'undefined' changed document log (#10695) 2025-11-23 14:36:58 +01:00
Tom Moor 8619ef2bea chore: Extract SwatchButton from InputColor (#10693) 2025-11-23 14:28:05 +01:00
Tom Moor a9263afa2c fix: Mermaid diagrams occassionally fail to render (#10691) 2025-11-23 12:58:00 +01:00
Translate-O-Tron 9d60deae60 New Crowdin updates (#10679)
* fix: New Hungarian translations from Crowdin [ci skip]

* fix: New Turkish translations from Crowdin [ci skip]
2025-11-23 12:54:09 +01:00
Tom Moor 5cc7601972 fix: Click route shared link should not toggle (#10690) 2025-11-23 12:28:15 +01:00
Tom Moor 5bc2e7e62e Sidebar design tweaks (#10684)
* Single line sidebar items

* fix: Letter icon in breadcrumb missing letter
Alignment of sidebar items

* fix: Editing state

* fix: Shared sidebar

* fix: Drag over unloaded document in sidebar does not allow drop

* perf

* Sidebar hover background

* Allow click toggle closed

* fix: Disclosure toggle

* perf: Avoid rendering collapsed folders
2025-11-23 11:38:40 +01:00
Tom Moor 22317e550a fix: Add missing drag cursor in top position (#10689) 2025-11-23 11:38:30 +01:00
Tom Moor 64526538ec fix: Forward compatibility for upcoming custom emoji logic (#10688) 2025-11-23 01:33:03 +01:00
Tom Moor 8ab107571d fix: When TOC extends beyond window bounds ensure headings scroll into view (#10687) 2025-11-23 01:30:31 +01:00
Tom Moor c7d7c3a66f fix: Plain text serializer squashes blocks (#10683)
closes #10673
2025-11-21 10:32:32 +00:00
Tom Moor 0d1c8490b5 fix: Header selection in Safari (#10671)
* Move heading actions to decorations

* fix: Selection issues in Safari
2025-11-21 10:42:09 +01:00
Tom Moor 9bdf904b7e fix: Invalid access of firstChild for mermaid diagrams (#10668) 2025-11-21 10:41:58 +01:00
Tom Moor 1c0191f9ed fix: UI does not update when deleting API key (#10670)
closes #10663
2025-11-19 21:27:56 +00:00
Tom Moor c034255c96 fix: Restore ability to resize shared sidebar (#10669) 2025-11-19 21:06:00 +00:00
Tom Moor 66474c82a3 fix: Incompatibility between path and query search terms (#10667) 2025-11-19 19:54:04 +01:00
Salihu 1125581fd0 Fix/access contol list UI (#10662)
* space columns evenly feature

* access control list exceeds the size of parent container

* remove irrelevant changes

* Further tweaks

---------

Co-authored-by: Tom Moor <tom@getoutline.com>
2025-11-19 18:38:26 +00:00
Tom Moor 6cfc7da40b v1.1.0 2025-11-16 15:47:23 +01:00
Tom Moor 523526b236 fix: Pointer down handling on mobile devices (#10649) 2025-11-16 14:59:05 +01:00
Tom Moor d5e651436b chore: Add confirmation step to release script (#10648)
* chore: Add confirmation step to release script

* Accept y/n/enter input
2025-11-15 13:29:05 -05:00
bleatingsheep 31dee071dd fix: STARTTLS is not used (#10647) 2025-11-15 13:13:53 -05:00
Salihu 9a180e486d Fix: admin permissions bypass (#10542)
* admins should not be able to update documents when it has view only for everyone

* fix tests

* fix broken tests

* remove unworkable test blocks

* fix broken tests

* fix all broken tests

* minor fix

* remove public collection check

* fix broken tests
2025-11-14 20:49:23 -05:00
Tom Moor 5a8a8d3fb0 fix: Short circuit automatic reload if repeated error (#10640) 2025-11-14 04:29:48 +00:00
Translate-O-Tron d242eb1d5a New Crowdin updates (#10603)
* fix: New Romanian translations from Crowdin [ci skip]

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

* fix: New Spanish 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 Korean translations from Crowdin [ci skip]

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

* fix: New Polish 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 Ukrainian translations from Crowdin [ci skip]

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

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

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

* fix: New Portuguese, Brazilian 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 English, United Kingdom translations from Crowdin [ci skip]

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

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

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

* fix: New Spanish 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 Korean translations from Crowdin [ci skip]

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

* fix: New Polish 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 Ukrainian translations from Crowdin [ci skip]

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

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

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

* fix: New Portuguese, Brazilian 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 English, United Kingdom translations from Crowdin [ci skip]

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

* fix: New Polish translations from Crowdin [ci skip]
2025-11-13 22:39:03 -05:00
Tom Moor 7adb64ff39 fix: Missing list formatting (#10639) 2025-11-14 02:32:03 +00:00
Tom Moor 8e95f13793 fix: Multiplayer close retry (#10638) 2025-11-14 01:25:18 +00:00
Tom Moor c2762ce087 fix: Update to require manage users policy for group management (#10637) 2025-11-13 19:24:06 -05:00
Tom Moor 9ebcf6cc4c chore: Move user suspension check inside getUserForJWT (#10636) 2025-11-13 19:23:59 -05:00
Tom Moor 8b9c4962a6 fix: Increase amount of accepted services in SMTP_SERVICE (#10635) 2025-11-13 18:32:19 -05:00
Tom Moor b134aa8220 fix: Slight downward movement when document viewers loads (#10630) 2025-11-13 00:28:16 +00:00
Tom Moor 8033416053 fix: Collection children disappear when adding new doc (#10629) 2025-11-13 00:21:52 +00:00
Tom Moor 958c9e1e66 chore: Porting changes from private fork (#10628)
* chore: Porting changes from private fork

* tsc
2025-11-12 19:08:15 -05:00
Tom Moor 2e9847e8b9 fix: Multiplayer editor reconnect behavior with expired token (#10626) 2025-11-12 09:01:18 -05:00
Apoorv Mishra 6a564a575c Link bar refactor to use commands (#10556)
* fix: quick refactor

* fix: `state.selection.to` is equivalent to mark's end pos in this case

* fix: get rid of paste handler

* fix: inline actions

* fix: remove and inline enum
2025-11-12 11:07:57 +05:30
Tom Moor 4abd36195c fix: Revoking parent permission not correctly reflected on open children (#10625)
closes #10616
2025-11-12 02:05:18 +00:00
Tom Moor 4b6db34583 fix: Empty state of collection should be hidden with new inline doc creation (#10624) 2025-11-11 20:04:27 -05:00
dependabot[bot] 83bb628a90 chore(deps): bump prosemirror-model from 1.25.2 to 1.25.4 (#10614)
Bumps [prosemirror-model](https://github.com/prosemirror/prosemirror-model) from 1.25.2 to 1.25.4.
- [Changelog](https://github.com/ProseMirror/prosemirror-model/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prosemirror/prosemirror-model/compare/1.25.2...1.25.4)

---
updated-dependencies:
- dependency-name: prosemirror-model
  dependency-version: 1.25.4
  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>
2025-11-11 20:02:05 -05:00
Tom Moor 6dd4e846b7 fix: Avoid seeing lang unneccessarily (#10623) 2025-11-11 19:35:57 -05:00
Tom Moor 468620b208 fix: Webhook UI extends out of modal bounds (#10622)
* fix: Webhook UI overlap

* tweaks
2025-11-11 19:35:43 -05:00
dependabot[bot] f9b137e5f8 chore(deps): bump the aws group with 5 updates (#10611)
Bumps the aws group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) | `3.922.0` | `3.927.0` |
| [@aws-sdk/lib-storage](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/lib/lib-storage) | `3.922.0` | `3.927.0` |
| [@aws-sdk/s3-presigned-post](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-presigned-post) | `3.922.0` | `3.927.0` |
| [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-request-presigner) | `3.922.0` | `3.927.0` |
| [@aws-sdk/signature-v4-crt](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/signature-v4-crt) | `3.922.0` | `3.927.0` |


Updates `@aws-sdk/client-s3` from 3.922.0 to 3.927.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.927.0/clients/client-s3)

Updates `@aws-sdk/lib-storage` from 3.922.0 to 3.927.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/lib/lib-storage/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.927.0/lib/lib-storage)

Updates `@aws-sdk/s3-presigned-post` from 3.922.0 to 3.927.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages/s3-presigned-post/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.927.0/packages/s3-presigned-post)

Updates `@aws-sdk/s3-request-presigner` from 3.922.0 to 3.927.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages/s3-request-presigner/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.927.0/packages/s3-request-presigner)

Updates `@aws-sdk/signature-v4-crt` from 3.922.0 to 3.927.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages/signature-v4-crt/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.927.0/packages/signature-v4-crt)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.927.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/lib-storage"
  dependency-version: 3.927.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-presigned-post"
  dependency-version: 3.927.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-request-presigner"
  dependency-version: 3.927.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/signature-v4-crt"
  dependency-version: 3.927.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-10 23:23:27 -05:00
dependabot[bot] 105fbbf6e1 chore(deps): bump @radix-ui/react-visually-hidden in the radix-ui group (#10612)
Bumps the radix-ui group with 1 update: [@radix-ui/react-visually-hidden](https://github.com/radix-ui/primitives).


Updates `@radix-ui/react-visually-hidden` from 1.2.3 to 1.2.4
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-visually-hidden"
  dependency-version: 1.2.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-10 23:22:55 -05:00
dependabot[bot] 7f657f43a1 chore(deps-dev): bump terser from 5.44.0 to 5.44.1 (#10613)
Bumps [terser](https://github.com/terser/terser) from 5.44.0 to 5.44.1.
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/compare/v5.44.0...v5.44.1)

---
updated-dependencies:
- dependency-name: terser
  dependency-version: 5.44.1
  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>
2025-11-10 23:22:45 -05:00
dependabot[bot] e6100c4bc9 chore(deps): bump katex from 0.16.22 to 0.16.25 (#10615)
Bumps [katex](https://github.com/KaTeX/KaTeX) from 0.16.22 to 0.16.25.
- [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.22...v0.16.25)

---
updated-dependencies:
- dependency-name: katex
  dependency-version: 0.16.25
  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>
2025-11-10 23:22:34 -05:00
Tom Moor e85e31a481 chore: UI tweaks (#10606)
* chore: Add transition on button mouseout

* Change default API key expiration to 1 month

* fix: Image upload triggered twice on New Application form

* Improve clickarea of sidebar disclosures

ref #10482

* Interaction fade
2025-11-09 20:32:22 +00:00
codegen-sh[bot] 2c74a801a4 Add double-click resize functionality to ResizeHandle components (#10594)
* Add double-click resize functionality to image and video resize handles

- Add handleDoubleClick function to useDragResize hook that toggles between fit-width and original size
- Update Image and Video components to use double-click handlers on resize handles
- Maintain aspect ratio when resizing via double-click
- Fit-width size is calculated as minimum of container width and natural width
- Fix ESLint warnings for useEffect dependencies

* simplify

---------

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
Co-authored-by: Tom Moor <tom@getoutline.com>
2025-11-09 18:16:05 +00:00
Tom Moor 77e6f0c6a3 fix: Cannot export collection with manage permissions (#10602)
* fix: Cannot export collection with manage permissions

* test
2025-11-09 09:35:52 -05:00
Tom Moor 7a0e88cd3c fix: CORS preflight (#10604) 2025-11-09 09:12:08 -05:00
Tom Moor 959dccf119 fix: Account for reading time longer than an hour (#10601) 2025-11-08 21:53:40 -05:00
Tom Moor 40f8cbaa0f fix: Revision model is not observable (#10600) 2025-11-08 17:33:24 -05:00
Tom Moor e9daf9c292 fix: Restore templates page in settings for editors (#10598)
closes #10583
2025-11-08 16:50:39 -05:00
Tom Moor ef94f10fae fix: Account menu missing hover state (#10599) 2025-11-08 16:50:28 -05:00
Tom Moor 73c607896d fix: lang attribute not passed to simplified renderer (#10597) 2025-11-08 21:14:55 +00:00
codegen-sh[bot] 99167bbdd6 Serialize document and collection mentions as regular links (#10595)
* Serialize document and collection mentions as regular links

- Update toMarkdown method in Mention.tsx to use regular markdown links for documents and collections
- Documents now serialize as [label](URL/doc/modelId) instead of @[label](mention://id/document/modelId)
- Collections now serialize as [label](URL/collection/modelId) instead of @[label](mention://id/collection/modelId)
- User mentions and other types retain the existing mention:// format for backward compatibility
- Improves portability of exported markdown and aligns with standard markdown link format

Fixes #10544

* Use relative URLs for document and collection mentions

- Remove domain from mention links to use relative paths
- Documents now serialize as [label](/doc/modelId)
- Collections now serialize as [label](/collection/modelId)
- Makes exported markdown more portable across different domains

---------

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
2025-11-08 14:27:45 -05:00
Tom Moor f683822852 fix: Various fixes and tweaks to dropdown filters (#10596)
* fix: Icon spacing in filter menus
fix: Selected options should be sorted to the top always

* fix: Focus lost when typing in filter options

* Reduce size of selected icon
2025-11-08 14:14:56 -05:00
Tom Moor 9c065b229c fix: Menu highlight not always visible (#10591) 2025-11-08 13:05:38 -05:00
Tom Moor a544a01835 fix: Positioning of table row actions overflow (#10593) 2025-11-08 13:05:29 -05:00
Tom Moor 90e19e8097 fix: Ensure clipPath is hidden (#10592) 2025-11-08 13:05:11 -05:00
Tom Moor 3f3a70d996 feat: Adjust line-height depending on script (#10565)
* migration

* Auto detect language and adjust line-height accordingly

* Remove accidental commit

* Remove unneccessary adjustment

* test

* mock
2025-11-08 11:47:51 -05:00
Salihu 265f2721f8 fix: 'shared with me' optimistic updates (#10547)
* optimistic state updates when documents under 'shared with me' section are created

* optimistic updates for other 'shared with me' document actions

* update top level document

* use action decorator
2025-11-08 11:47:38 -05:00
Salihu 636153a56b Fix: nested document order when duplicating (#10543)
* duplicate nested documents in correct order

* include document structure

* use more accurate structure

* minor fix

* debug test on remote

* remove failing test

* fix test

* fix test

* fix test

* fix tests
2025-11-08 11:06:42 -05:00
Tom Moor c44a3c0f69 fix: Restore some conditions on copy/paste of Markdown (#10587)
closes #10476
2025-11-08 11:02:56 -05:00
Tom Moor c9076b0be0 fix: pointer-events: none left hanging on popovers sometimes (#10585)
closes #10579
2025-11-08 11:02:47 -05:00
Tom Moor a533d0c462 fix: Popover with many search results does not correctly paginate (#10584) 2025-11-08 11:02:40 -05:00
Tom Moor 72bf9b86f6 Add missing total to groupMemberships pagination (#10589)
* Add missing total to groupMembership pagination

closes #10499

* test
2025-11-08 11:01:54 -05:00
Translate-O-Tron b925854247 New Crowdin updates (#10478)
* fix: New Persian translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: New Danish 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 Ukrainian translations from Crowdin [ci skip]

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

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

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

* fix: New Portuguese, Brazilian 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 English, United Kingdom translations from Crowdin [ci skip]

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

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

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

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

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

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

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

* fix: New Vietnamese translations from Crowdin [ci skip]
2025-11-08 09:48:00 -05:00
unknown 3ca7c7369e docs: Fix small typo in TSDoc comment (#10570) 2025-11-05 02:41:46 -05:00
dependabot[bot] cb153ded8f chore(deps): bump @css-inline/css-inline-wasm from 0.17.0 to 0.18.0 (#10553)
Bumps [@css-inline/css-inline-wasm](https://github.com/Stranger6667/css-inline) from 0.17.0 to 0.18.0.
- [Release notes](https://github.com/Stranger6667/css-inline/releases)
- [Changelog](https://github.com/Stranger6667/css-inline/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stranger6667/css-inline/compare/v0.17.0...v0.18.0)

---
updated-dependencies:
- dependency-name: "@css-inline/css-inline-wasm"
  dependency-version: 0.18.0
  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>
2025-11-04 19:27:05 -05:00
Tom Moor 5761903407 fix: Indent/outdent not appearing in mobile toolbar (#10558)
closes #10557
2025-11-04 19:26:53 -05:00
Cellivar 248cc1ba8b Send cookies with S3 POST upload (#10562) 2025-11-04 19:26:45 -05:00
dependabot[bot] ad0f0b39b8 chore(deps): bump dd-trace from 5.67.0 to 5.76.0 (#10552)
Bumps [dd-trace](https://github.com/DataDog/dd-trace-js) from 5.67.0 to 5.76.0.
- [Release notes](https://github.com/DataDog/dd-trace-js/releases)
- [Commits](https://github.com/DataDog/dd-trace-js/compare/v5.67.0...v5.76.0)

---
updated-dependencies:
- dependency-name: dd-trace
  dependency-version: 5.76.0
  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>
2025-11-04 17:02:38 -05:00
dependabot[bot] cdcff3899d chore(deps): bump the aws group with 5 updates (#10551)
Bumps the aws group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) | `3.917.0` | `3.922.0` |
| [@aws-sdk/lib-storage](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/lib/lib-storage) | `3.917.0` | `3.922.0` |
| [@aws-sdk/s3-presigned-post](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-presigned-post) | `3.917.0` | `3.922.0` |
| [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-request-presigner) | `3.917.0` | `3.922.0` |
| [@aws-sdk/signature-v4-crt](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/signature-v4-crt) | `3.916.0` | `3.922.0` |


Updates `@aws-sdk/client-s3` from 3.917.0 to 3.922.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.922.0/clients/client-s3)

Updates `@aws-sdk/lib-storage` from 3.917.0 to 3.922.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/lib/lib-storage/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.922.0/lib/lib-storage)

Updates `@aws-sdk/s3-presigned-post` from 3.917.0 to 3.922.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages/s3-presigned-post/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.922.0/packages/s3-presigned-post)

Updates `@aws-sdk/s3-request-presigner` from 3.917.0 to 3.922.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages/s3-request-presigner/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.922.0/packages/s3-request-presigner)

Updates `@aws-sdk/signature-v4-crt` from 3.916.0 to 3.922.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages/signature-v4-crt/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.922.0/packages/signature-v4-crt)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.922.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/lib-storage"
  dependency-version: 3.922.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-presigned-post"
  dependency-version: 3.922.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-request-presigner"
  dependency-version: 3.922.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/signature-v4-crt"
  dependency-version: 3.922.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-04 17:02:23 -05:00
dependabot[bot] 7be11eb44e chore(deps-dev): bump terser from 5.43.1 to 5.44.0 (#10554)
Bumps [terser](https://github.com/terser/terser) from 5.43.1 to 5.44.0.
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/compare/v5.43.1...v5.44.0)

---
updated-dependencies:
- dependency-name: terser
  dependency-version: 5.44.0
  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>
2025-11-04 17:02:09 -05:00
Tom Moor 5a77217d25 fix: Layout of todo summary in metadata on mobile (#10559) 2025-11-04 17:01:58 -05:00
Tom Moor 89425ccdab chore: Add a mutex lock around migrations to ensure in multi-instance deployments multiple machines don't attempt to run migrations at once (#10560) 2025-11-04 17:01:44 -05:00
dependabot[bot] 1c64b6c93f chore(deps): bump prosemirror-state from 1.4.3 to 1.4.4 (#10555)
Bumps [prosemirror-state](https://github.com/prosemirror/prosemirror-state) from 1.4.3 to 1.4.4.
- [Changelog](https://github.com/ProseMirror/prosemirror-state/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prosemirror/prosemirror-state/compare/1.4.3...1.4.4)

---
updated-dependencies:
- dependency-name: prosemirror-state
  dependency-version: 1.4.4
  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>
2025-11-04 17:01:33 -05:00
codegen-sh[bot] c2d516c5f1 Upgrade mermaid to 11.12.1 (#10564)
- Updated mermaid dependency from 11.10.1 to 11.12.1
- Updated yarn.lock with new dependency resolution

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
2025-11-04 11:57:03 -05:00
Tom Moor e4268c9a1f chore: Public share cleanup (#10541)
* chore: More public share cleanup

* fix: Use correct amount of spaces for tab

* fix: Pointer on public shares

* fix: Tweak AAA contrast

* Show code language on public share
2025-11-01 10:25:38 -04:00
codegen-sh[bot] bf9065d6e6 Add description column to groups (#10511)
* Add description column to groups

- Add database migration to add description column to groups table
- Update server-side Group model with description field and validation
- Update group presenter to include description in API responses
- Update API schemas to validate description field in create/update operations
- Update client-side Group model with description field and search integration
- Update unfurl types and presenter to include description for hover cards
- Update HoverPreviewGroup component to display description in UI

The description field is optional with a 2000 character limit and is included
in group search functionality.

* Fix TypeScript error: Add missing description prop to HoverPreviewGroup

The HoverPreviewGroup component expects a description prop but it wasn't being passed from HoverPreview.tsx. This was causing the types check to fail with:

error TS2741: Property 'description' is missing in type '{ ref: MutableRefObject<HTMLDivElement | null>; name: any; memberCount: any; users: any; }' but required in type 'Props'.

Fixed by adding the description prop from data.description which is available in the UnfurlResponse[UnfurlResourceType.Group] type.

* Move 2000 char validation to shared constant

- Add GroupValidation.maxDescriptionLength constant to shared/validations.ts
- Update server Group model to use GroupValidation.maxDescriptionLength
- Update API schemas to use the shared constant instead of hardcoded value
- Ensures consistent validation across the entire application

* Add description field to CreateGroupDialog and EditGroupDialog

- Add description textarea input to both create and edit group dialogs
- Import GroupValidation constant for consistent character limit validation
- Set maxLength to GroupValidation.maxDescriptionLength (2000 chars)
- Include description in form submission for both create and update operations
- Add placeholder text for better UX
- Maintain backward compatibility with optional description field

* Add description column to GroupsTable

- Add description column between name and members columns
- Display group description with fallback to em dash (—) for empty descriptions
- Use secondary text styling for consistent visual hierarchy
- Set column width to 2fr for adequate space
- Maintain sortable functionality through accessor

* tweaks

* animation

---------

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
Co-authored-by: Tom Moor <tom@getoutline.com>
2025-10-31 11:36:26 -04:00
Apoorv Mishra 3e5ae49ad9 Link bar cleanup (#10522)
* fix: link bar bugs

* fix: restore click on search results

* fix: esc

* fix: comment
2025-10-31 17:57:11 +05:30
Tom Moor 9a4d754a39 Improved syntax highlighting (#10533)
* Improve syntax highlighting

* fixes

* fix
2025-10-31 07:30:10 -04:00
Tom Moor 0009a08278 Add group member count to mention menu (#10535)
* Add group member count to mention menu

* i18n
2025-10-31 07:30:01 -04:00
Tom Moor 84bc914940 Hide collection root if empty (#10534) 2025-10-31 07:29:50 -04:00
Tom Moor da6a449cf3 fix: Double 'selected' state on menus when hovering as it opens (#10532) 2025-10-31 01:50:08 +00:00
Tom Moor 4631b5ccaa chore: Remove ability to collapse sidebar on shared links (#10531)
* fix: Remove ability to collapse sidebar on shared links

* fix: Existing collapsed sidebars should be forced open
2025-10-31 01:17:03 +00:00
Tom Moor 4d5895d2a8 fix: Extra lines before template application (#10528) 2025-10-30 21:16:33 -04:00
Tom Moor 3543fafee3 fix: Input in embed toolbar grabs focus (#10530) 2025-10-31 01:11:29 +00:00
Tom Moor e77cdc2903 fix: emdash replacement conflicts with horizontal rule (#10515) 2025-10-30 01:59:47 +00:00
Tom Moor ecba11b786 v1.0.1 2025-10-28 23:17:50 -04:00
Tom Moor 6d13347806 fix: Cannot resize embed on collection overview (#10498)
fix: Toolbar too small on embed link editor
2025-10-28 21:12:55 -04:00
Tom Moor 36773febd2 fix: YouTube referrerpolicy requirements seem to have tightened (#10503) 2025-10-28 21:12:44 -04:00
Tom Moor fa8d82d82a fix: Restore uuid package on frontend (#10491)
* fix: Restore uuid package on frontend

* Remove legacy moduleNameMapper

* Add lint rule

* lint - getRandomValues can be used without SSL

* Update Comment.ts
2025-10-28 08:13:48 -04:00
Tom Moor cc6d2dc471 fix: Missizing of math (#10494) 2025-10-28 08:13:39 -04:00
Tom Moor 5035ad2027 fix: Pin to node 22.21.0 (#10496) 2025-10-28 08:13:28 -04:00
Apoorv Mishra 06ec6fdfbb Enable commenting on images (#10474)
* feat: enable commenting on image nodes

* chore: make anchorPlugin a top level plugin

* fix: className

* fix: review

* fix: tsc

* fix: checks

* Tweak menu order to match

---------

Co-authored-by: Tom Moor <tom@getoutline.com>
2025-10-28 11:34:40 +05:30
dependabot[bot] acc8d99ca0 chore(deps): bump the babel group with 5 updates (#10484)
Bumps the babel group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) | `7.28.4` | `7.28.5` |
| [@babel/plugin-transform-destructuring](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-destructuring) | `7.28.0` | `7.28.5` |
| [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) | `7.28.3` | `7.28.5` |
| [@babel/preset-react](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-react) | `7.27.1` | `7.28.5` |
| [@babel/preset-typescript](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-typescript) | `7.27.1` | `7.28.5` |


Updates `@babel/core` from 7.28.4 to 7.28.5
- [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.28.5/packages/babel-core)

Updates `@babel/plugin-transform-destructuring` from 7.28.0 to 7.28.5
- [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.28.5/packages/babel-plugin-transform-destructuring)

Updates `@babel/preset-env` from 7.28.3 to 7.28.5
- [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.28.5/packages/babel-preset-env)

Updates `@babel/preset-react` from 7.27.1 to 7.28.5
- [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.28.5/packages/babel-preset-react)

Updates `@babel/preset-typescript` from 7.27.1 to 7.28.5
- [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.28.5/packages/babel-preset-typescript)

---
updated-dependencies:
- dependency-name: "@babel/core"
  dependency-version: 7.28.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: babel
- dependency-name: "@babel/plugin-transform-destructuring"
  dependency-version: 7.28.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: babel
- dependency-name: "@babel/preset-env"
  dependency-version: 7.28.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: babel
- dependency-name: "@babel/preset-react"
  dependency-version: 7.28.5
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: babel
- dependency-name: "@babel/preset-typescript"
  dependency-version: 7.28.5
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: babel
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 23:12:28 -04:00
dependabot[bot] 7da3108412 chore(deps): bump validator from 13.15.15 to 13.15.20 (#10490)
Bumps [validator](https://github.com/validatorjs/validator.js) from 13.15.15 to 13.15.20.
- [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.15.15...13.15.20)

---
updated-dependencies:
- dependency-name: validator
  dependency-version: 13.15.20
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-28 03:03:06 +00:00
dependabot[bot] 7e56d04285 chore(deps): bump ioredis from 5.7.0 to 5.8.2 (#10483)
Bumps [ioredis](https://github.com/luin/ioredis) from 5.7.0 to 5.8.2.
- [Release notes](https://github.com/luin/ioredis/releases)
- [Changelog](https://github.com/redis/ioredis/blob/main/CHANGELOG.md)
- [Commits](https://github.com/luin/ioredis/compare/v5.7.0...v5.8.2)

---
updated-dependencies:
- dependency-name: ioredis
  dependency-version: 5.8.2
  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>
2025-10-27 22:55:16 -04:00
dependabot[bot] 3987b7de3d chore(deps): bump the aws group with 5 updates (#10485)
Bumps the aws group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) | `3.913.0` | `3.917.0` |
| [@aws-sdk/lib-storage](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/lib/lib-storage) | `3.913.0` | `3.917.0` |
| [@aws-sdk/s3-presigned-post](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-presigned-post) | `3.913.0` | `3.917.0` |
| [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-request-presigner) | `3.913.0` | `3.917.0` |
| [@aws-sdk/signature-v4-crt](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/signature-v4-crt) | `3.911.0` | `3.916.0` |


Updates `@aws-sdk/client-s3` from 3.913.0 to 3.917.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.917.0/clients/client-s3)

Updates `@aws-sdk/lib-storage` from 3.913.0 to 3.917.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/lib/lib-storage/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.917.0/lib/lib-storage)

Updates `@aws-sdk/s3-presigned-post` from 3.913.0 to 3.917.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages/s3-presigned-post/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.917.0/packages/s3-presigned-post)

Updates `@aws-sdk/s3-request-presigner` from 3.913.0 to 3.917.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages/s3-request-presigner/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.917.0/packages/s3-request-presigner)

Updates `@aws-sdk/signature-v4-crt` from 3.911.0 to 3.916.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages/signature-v4-crt/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.916.0/packages/signature-v4-crt)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.917.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/lib-storage"
  dependency-version: 3.917.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-presigned-post"
  dependency-version: 3.917.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-request-presigner"
  dependency-version: 3.917.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/signature-v4-crt"
  dependency-version: 3.916.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 22:55:01 -04:00
dependabot[bot] 6daed33b4a chore(deps-dev): bump @types/readable-stream from 4.0.21 to 4.0.22 (#10486)
Bumps [@types/readable-stream](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/readable-stream) from 4.0.21 to 4.0.22.
- [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-version: 4.0.22
  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>
2025-10-27 22:54:53 -04:00
Tom Moor 3551d16bd8 v1.0.0 2025-10-26 11:36:27 -04:00
Translate-O-Tron 641c0da603 New Crowdin updates (#10347)
* fix: New Hungarian translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

* fix: New Danish 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 Ukrainian translations from Crowdin [ci skip]

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

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

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

* fix: New Portuguese, Brazilian 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 English, United Kingdom translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

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

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

* fix: New Danish 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 Ukrainian translations from Crowdin [ci skip]

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

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

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

* fix: New Portuguese, Brazilian 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 English, United Kingdom translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: New Danish 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 Ukrainian translations from Crowdin [ci skip]

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

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

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

* fix: New Portuguese, Brazilian 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 English, United Kingdom translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

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

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

* fix: New Danish 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 Ukrainian translations from Crowdin [ci skip]

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

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

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

* fix: New Portuguese, Brazilian 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 English, United Kingdom translations from Crowdin [ci skip]

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

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

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

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

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

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

* fix: New Czech 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 Chinese Simplified translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: New Danish 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 Ukrainian translations from Crowdin [ci skip]

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

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

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

* fix: New Portuguese, Brazilian 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 English, United Kingdom translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

* fix: New Danish 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 Ukrainian translations from Crowdin [ci skip]

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

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

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

* fix: New Portuguese, Brazilian 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 English, United Kingdom translations from Crowdin [ci skip]

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

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

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

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

* fix: New Persian translations from Crowdin [ci skip]
2025-10-25 20:16:11 -04:00
Tom Moor 7768273255 fix: Replace base64 encoded images in documents.update (#10402)
* fix: Replace base64 encoded images in documents.update

* isInternalUrl

* b64 only
2025-10-26 00:11:02 +00:00
Tom Moor 9cadcc668c fix: Update email magic links (#10471)
* fix: Update email magic links to check IP within time limit rather than usage

* Add option to force OTP method
2025-10-25 12:23:45 -04:00
Tom Moor adc11aee9f chore: More sidebar performance fixes (#10470) 2025-10-25 15:05:19 +00:00
AnastasiyaHladina 7ab247f367 chore: update minimal Node.js version (#10403) 2025-10-24 21:44:27 -04:00
huiseo 9ec5c473f1 fix: prevent list conversion inside heading nodes (#10462)
* fix: prevent list conversion inside heading nodes

Fixes a bug where typing list syntax (e.g., "1. ", "* ", "[ ]")
inside heading nodes would incorrectly trigger list conversion.

Previously, when a user selected H1 from the "/" menu and typed
"1. " followed by a space, the OrderedList inputRule would attempt
to convert the heading into an ordered list, causing a conflict
since headings can only contain inline content.

Changes:
- Add isInHeading utility to detect if selection is inside a heading
- Create safeWrappingInputRule wrapper that prevents list conversion
  when inside heading nodes
- Apply the fix to OrderedList, BulletList, and CheckboxList nodes

This ensures that list markdown syntax is preserved as plain text
when typed within headings, matching expected editor behavior.

* refactor: extract listWrappingInputRule to shared helper

Refactored duplicated safeWrappingInputRule implementations across
BulletList, OrderedList, and CheckboxList into a single shared helper
function named listWrappingInputRule in shared/editor/lib/listInputRule.ts.

This reduces code duplication and follows the same pattern as other
input rule helpers like markInputRule.

Changes:
- Create shared/editor/lib/listInputRule.ts with listWrappingInputRule
- Update BulletList.ts to use shared helper
- Update OrderedList.ts to use shared helper
- Update CheckboxList.ts to use shared helper
- Restore .env.development file

Co-Authored-By: huiseo <hui.seo@gmail.com>
2025-10-23 20:23:47 -04:00
Tom Moor 02bdb2e464 fix: Render-per-model type, 4x improvement on perf (#10465)
* fix: Render-per-model type, 4x improvement on perf

* fix: Sidebar CollectionLinkChildren render when @mention changes
2025-10-23 20:23:38 -04:00
codegen-sh[bot] 77d50f8323 Add disableMentions option for individual groups (#10459)
* Add disableMentions option for groups

- Add database migration to add disableMentions column to groups table
- Update server-side Group model with new field
- Add disableMentions to group create/update API schemas and endpoints
- Update client-side Group model with new field
- Add checkbox to EditGroupDialog for disabling mentions
- Filter out groups with disableMentions=true from mention suggestions
- Prevent notifications for groups with disabled mentions

* Fix TypeScript error in GroupDialogs checkbox handler

- Add properly typed handleDisableMentionsChange callback
- Replace inline onChange handler with typed callback
- Fixes TS2339 error: Property 'checked' does not exist on type 'EventTarget'

* UI tweaks

* Add groups to suggestions endpoint

* Remove mentionableData

---------

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
Co-authored-by: Tom Moor <tom@getoutline.com>
2025-10-23 08:24:22 -04:00
Tom Moor 76691e8aaa fix: Add yet another guard against crawlers consuming magic links (#10457) 2025-10-23 08:24:10 -04:00
Tom Moor 633d41e67f fix: Fallback to any Linear integration (#10458)
* Fallback to any integration

* fix: Cannot unfurl Linear links without creator
2025-10-23 08:23:54 -04:00
Tom Moor 3db845b395 fix: Protect against empty content passed to Backticks component (#10456) 2025-10-22 21:05:10 -04:00
codegen-sh[bot] 3269eacf68 Add hover card for group mentions (#10432)
* Add hover card for group mentions

- Add Group type to UnfurlResourceType enum
- Create HoverPreviewGroup component following HoverUser pattern
- Add server-side support for group unfurling in URLs route
- Display group name, member count, and member avatars in hover card
- Implement presentGroup function in unfurl presenter

Fixes #10418

* Fix TypeScript errors in group hover card implementation

- Make presentGroup async to properly handle group.memberCount Promise
- Update presentUnfurl to await presentGroup result
- Fix Facepile users prop by creating User-like objects with required properties
- Add User import to HoverPreviewGroup component

Fixes TypeScript compilation errors:
- TS2322: Type mismatch in HoverPreviewGroup.tsx
- TS2362: Arithmetic operation type error in unfurl.ts
- TS2322: Promise<number> not assignable to number in unfurl.ts

* tweaks

* tweaks

---------

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
Co-authored-by: Tom Moor <tom@getoutline.com>
2025-10-22 21:04:58 -04:00
dependabot[bot] eef2ea4347 chore(deps): bump koa from 3.0.1 to 3.0.3 (#10444)
Bumps [koa](https://github.com/koajs/koa) from 3.0.1 to 3.0.3.
- [Release notes](https://github.com/koajs/koa/releases)
- [Changelog](https://github.com/koajs/koa/blob/master/History.md)
- [Commits](https://github.com/koajs/koa/compare/v3.0.1...v3.0.3)

---
updated-dependencies:
- dependency-name: koa
  dependency-version: 3.0.3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-22 19:32:57 -04:00
Tom Moor a2ce13a7dd chore: Improve email sign-in debugging (#10455) 2025-10-22 23:32:25 +00:00
Tom Moor ff13f1a452 Update API responses to 204 (#10441)
* shares.info

* subscriptions and pins to 204
2025-10-22 17:48:24 -04:00
Tom Moor a5d065e5ec chore: Annotate delayed notifs (#10447) 2025-10-22 17:48:12 -04:00
Tom Moor fc6152bd55 fix: Simplify logic for suppressing markdown copy (#10450) 2025-10-22 17:47:47 -04:00
Tom Moor 06d4d7e893 chore: Improve Redis retry behavior (#10440) 2025-10-20 23:54:13 -04:00
dependabot[bot] a85f36d896 chore(deps-dev): bump discord-api-types from 0.38.20 to 0.38.30 (#10435)
Bumps [discord-api-types](https://github.com/discordjs/discord-api-types) from 0.38.20 to 0.38.30.
- [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.38.20...0.38.30)

---
updated-dependencies:
- dependency-name: discord-api-types
  dependency-version: 0.38.30
  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>
2025-10-20 20:18:30 -04:00
dependabot[bot] 5231318e55 chore(deps): bump patch-package from 8.0.0 to 8.0.1 (#10434)
Bumps [patch-package](https://github.com/ds300/patch-package) from 8.0.0 to 8.0.1.
- [Release notes](https://github.com/ds300/patch-package/releases)
- [Changelog](https://github.com/ds300/patch-package/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ds300/patch-package/commits)

---
updated-dependencies:
- dependency-name: patch-package
  dependency-version: 8.0.1
  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>
2025-10-20 20:18:23 -04:00
dependabot[bot] 916032508c chore(deps-dev): bump react-refresh from 0.17.0 to 0.18.0 (#10436)
Bumps [react-refresh](https://github.com/facebook/react/tree/HEAD/packages/react) from 0.17.0 to 0.18.0.
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/HEAD/packages/react)

---
updated-dependencies:
- dependency-name: react-refresh
  dependency-version: 0.18.0
  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>
2025-10-20 20:18:15 -04:00
dependabot[bot] 1a3478a228 chore(deps): bump the aws group with 5 updates (#10438)
Bumps the aws group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) | `3.908.0` | `3.913.0` |
| [@aws-sdk/lib-storage](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/lib/lib-storage) | `3.908.0` | `3.913.0` |
| [@aws-sdk/s3-presigned-post](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-presigned-post) | `3.908.0` | `3.913.0` |
| [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-request-presigner) | `3.908.0` | `3.913.0` |
| [@aws-sdk/signature-v4-crt](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/signature-v4-crt) | `3.908.0` | `3.911.0` |


Updates `@aws-sdk/client-s3` from 3.908.0 to 3.913.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.913.0/clients/client-s3)

Updates `@aws-sdk/lib-storage` from 3.908.0 to 3.913.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/lib/lib-storage/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.913.0/lib/lib-storage)

Updates `@aws-sdk/s3-presigned-post` from 3.908.0 to 3.913.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages/s3-presigned-post/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.913.0/packages/s3-presigned-post)

Updates `@aws-sdk/s3-request-presigner` from 3.908.0 to 3.913.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages/s3-request-presigner/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.913.0/packages/s3-request-presigner)

Updates `@aws-sdk/signature-v4-crt` from 3.908.0 to 3.911.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages/signature-v4-crt/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.911.0/packages/signature-v4-crt)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.913.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/lib-storage"
  dependency-version: 3.913.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-presigned-post"
  dependency-version: 3.913.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-request-presigner"
  dependency-version: 3.913.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/signature-v4-crt"
  dependency-version: 3.911.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-20 20:18:06 -04:00
Tom Moor 1028edaa03 Rounded display of tables (#10421) 2025-10-20 07:58:25 -04:00
Translate-O-Tron 6a736072f0 Add translation changes from outline-cloud (#10427)
Co-authored-by: GitHub Action <action@github.com>
2025-10-19 22:34:40 -04:00
Tom Moor 94f302f712 Revert "Update en_US translations from upstream (#10425)" (#10426)
This reverts commit d2ef7e770d.
2025-10-19 22:02:11 -04:00
Translate-O-Tron d2ef7e770d Update en_US translations from upstream (#10425)
Co-authored-by: GitHub Action <action@github.com>
2025-10-19 21:50:48 -04:00
Tom Moor 323094ce57 fix: min-width applied to all floating toolbars (#10424) 2025-10-20 00:38:45 +00:00
Tom Moor 0e596f61c8 fix: Various React warnings (#10423) 2025-10-19 20:06:09 -04:00
Tom Moor a23888f5d6 fix: Query error in export task (#10422) 2025-10-19 23:33:39 +00:00
Salihu 515e160bdb feat: Allow editing image source URLs (#10258)
* allow users edit image links

* use menu dropdown for image replacement options

* copy

* keep editing state in selection toolbar

* avoid overly broad types

* use fixed toolbar width

* tweaks

---------

Co-authored-by: Tom Moor <tom@getoutline.com>
2025-10-19 16:56:51 -04:00
github-actions[bot] c853063d1f chore: Compressed inefficient images automatically (#10420)
Co-authored-by: tommoor <tommoor@users.noreply.github.com>
2025-10-19 16:50:39 -04:00
Salihu e86593f234 feat: add group mentions (#10331)
* add group mentions

* group mention functionality

* add notification test

* fix: Group icon in mention menu

* language

* toast message

* fix: Group icon in mention menu light mode color

---------

Co-authored-by: Tom Moor <tom@getoutline.com>
2025-10-19 15:40:10 -04:00
Tom Moor 285b770b3d chore: Convert SelectionToolbar to editor widget pattern (#10414)
* refactor

* fix: Restore toolbar arrow
fix: Delayed width calculation
fix: Unused menuBorder theme prop
2025-10-18 20:34:19 -04:00
Tom Moor 2c27ef9c2c chore: Restore menu safe-area (#10415)
* chore: Restore menu safe-area

Removed in #10219

* Remove unneccessary dev translations
2025-10-18 19:33:35 -04:00
Tom Moor 3704dc2a4d fix: Combination of <br> and inline nodes in table cell is not imported correctly (#10416) 2025-10-18 19:30:45 -04:00
Tom Moor d37422ab8a fix: Creating new doc offline in sidebar leaves corrupt state in UI (#10412) 2025-10-18 10:43:03 -04:00
Tom Moor a75af8759b fix: Horizontal rule menu appears in read-only editor (#10413) 2025-10-18 10:33:17 -04:00
github-actions[bot] 7c048ef168 chore: Compressed inefficient images automatically (#10407)
Co-authored-by: tommoor <tommoor@users.noreply.github.com>
2025-10-17 23:25:31 -04:00
Tom Moor b3b4ed1dc0 chore: Prevent calibre image actions repeatedly compressing the same images (#10408) 2025-10-17 23:25:15 -04:00
Tom Moor 1417a4b958 Delete .github/workflows/lint.yml 2025-10-17 23:24:47 -04:00
patroldo c33d9fd6ec Added plantuml embedding (#10379)
* Added plantuml embedding

* Added plantUML icon

* Updated alt of plantuml icon

* Removed edit button, fixed plantuml placeholder and replaced image url

* tweaks

---------

Co-authored-by: Tom Moor <tom@getoutline.com>
2025-10-17 23:13:42 -04:00
Tom Moor 84b874c1a3 fix: Small transform issue with lightbox zoom-out (#10406) 2025-10-17 22:43:36 +00:00
Tom Moor 2da2081b6f feat: Add includePrivate param to export_all endpoint (#10401) 2025-10-17 18:28:02 -04:00
Tom Moor 0c3c92aebf fix: Change behavior of SMTP_SECURE=false so that it will never upgrade to a secure connection (#10399) 2025-10-17 18:15:50 -04:00
Tom Moor 6ed666fb38 fix: Clicking around image should close lightbox (#10400)
* fix: Clicking around image should close lightbox

* PR feedback
2025-10-17 18:15:15 -04:00
AnastasiyaHladina 79ea6279d5 chore: update GitHub actions version (#10405)
* chore: update actions/stale version

* chore: update stefanzweifel/git-auto-commit-action version

* chore: update actions/stale action version
2025-10-17 18:09:48 -04:00
AnastasiyaHladina fd7f359489 chore: update actions versions (#10397) 2025-10-16 21:29:19 -04:00
Tom Moor 3d7f971d86 fix: Cascade of client-side paranoid deletion (#10393) 2025-10-15 22:13:17 -04:00
Tom Moor 9e8f206ebf fix: release script does not work with gpgSign=true (#10392) 2025-10-15 21:42:19 -04:00
Tom Moor 61d8c2bdb6 chore: Add clarity to error message when private IP address is banned (#10391) 2025-10-15 20:31:48 -04:00
Tom Moor e77d918871 chore: Allow setting width and height of modals (#10389)
Tweaks to invite modal
2025-10-15 20:31:40 -04:00
Tom Moor dddf28a834 fix: Image toolbar width (#10390)
Regressed in #10343
Closes #10387
2025-10-15 23:02:24 +00:00
Tom Moor b694250f51 chore: Return unsent invites from API response (#10383) 2025-10-15 07:49:19 -04:00
Tom Moor d7374730e3 chore: Tweak UX of Facepile (#10384)
* chore: Tweak UX of facepile

* tsc
2025-10-15 07:49:08 -04:00
Tom Moor 908d0408f5 fix: Display fallback instead of error if cannot unfurl URL (#10370)
* fix: Display fallback instead of error if cannot unfurl URL

* Optimised images with calibre/image-actions

* fix: Write loaded to props to attrs

* Optimised images with calibre/image-actions

* white background

* Optimised images with calibre/image-actions

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-14 20:37:44 -04:00
Tom Moor 269bd60b5a fix: Enable workspace creation from Discord without DISCORD_SERVER_ID (#10380)
ref #8471
2025-10-14 19:51:15 -04:00
dependabot[bot] 87c03fd088 chore(deps-dev): bump rollup-plugin-webpack-stats from 2.1.3 to 2.1.6 (#10369)
Bumps [rollup-plugin-webpack-stats](https://github.com/relative-ci/rollup-plugin-webpack-stats) from 2.1.3 to 2.1.6.
- [Release notes](https://github.com/relative-ci/rollup-plugin-webpack-stats/releases)
- [Commits](https://github.com/relative-ci/rollup-plugin-webpack-stats/compare/v2.1.3...v2.1.6)

---
updated-dependencies:
- dependency-name: rollup-plugin-webpack-stats
  dependency-version: 2.1.6
  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>
2025-10-13 13:02:36 -04:00
dependabot[bot] b9a8b0f6d6 chore(deps): bump fs-extra from 11.3.1 to 11.3.2 (#10365)
Bumps [fs-extra](https://github.com/jprichardson/node-fs-extra) from 11.3.1 to 11.3.2.
- [Changelog](https://github.com/jprichardson/node-fs-extra/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jprichardson/node-fs-extra/compare/11.3.1...11.3.2)

---
updated-dependencies:
- dependency-name: fs-extra
  dependency-version: 11.3.2
  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>
2025-10-13 13:02:27 -04:00
dependabot[bot] 34ee3b7ea7 chore(deps): bump the aws group with 5 updates (#10367)
Bumps the aws group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) | `3.901.0` | `3.908.0` |
| [@aws-sdk/lib-storage](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/lib/lib-storage) | `3.903.0` | `3.908.0` |
| [@aws-sdk/s3-presigned-post](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-presigned-post) | `3.901.0` | `3.908.0` |
| [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-request-presigner) | `3.901.0` | `3.908.0` |
| [@aws-sdk/signature-v4-crt](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/signature-v4-crt) | `3.901.0` | `3.908.0` |


Updates `@aws-sdk/client-s3` from 3.901.0 to 3.908.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.908.0/clients/client-s3)

Updates `@aws-sdk/lib-storage` from 3.903.0 to 3.908.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/lib/lib-storage/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.908.0/lib/lib-storage)

Updates `@aws-sdk/s3-presigned-post` from 3.901.0 to 3.908.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages/s3-presigned-post/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.908.0/packages/s3-presigned-post)

Updates `@aws-sdk/s3-request-presigner` from 3.901.0 to 3.908.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages/s3-request-presigner/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.908.0/packages/s3-request-presigner)

Updates `@aws-sdk/signature-v4-crt` from 3.901.0 to 3.908.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages/signature-v4-crt/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.908.0/packages/signature-v4-crt)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.908.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/lib-storage"
  dependency-version: 3.908.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-presigned-post"
  dependency-version: 3.908.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-request-presigner"
  dependency-version: 3.908.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/signature-v4-crt"
  dependency-version: 3.908.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-13 13:02:19 -04:00
dependabot[bot] 5ffe02bcc0 chore(deps): bump emoji-regex from 10.5.0 to 10.6.0 (#10364)
Bumps [emoji-regex](https://github.com/mathiasbynens/emoji-regex) from 10.5.0 to 10.6.0.
- [Commits](https://github.com/mathiasbynens/emoji-regex/compare/v10.5.0...v10.6.0)

---
updated-dependencies:
- dependency-name: emoji-regex
  dependency-version: 10.6.0
  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>
2025-10-13 12:40:09 -04:00
dependabot[bot] 670428d322 chore(deps-dev): bump @types/node from 20.17.30 to 20.19.21 (#10368)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.17.30 to 20.19.21.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 20.19.21
  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>
2025-10-13 12:39:55 -04:00
Tom Moor 3e58a6ca46 fix: Single frame blank flash when saving comments (#10362) 2025-10-13 14:34:44 +00:00
Tom Moor b21d548d06 fix: Template settings should not show to guests (#10361) 2025-10-13 14:04:52 +00:00
ZhuoYang Wu(阿离) cadbd0d698 fix: repeat submission (#10355) 2025-10-12 21:32:23 -04:00
Tom Moor 6fdba0ecba fix: Icon in editor suggestions missing spacing (#10354) 2025-10-13 01:23:20 +00:00
1277 changed files with 61455 additions and 28621 deletions
+1
View File
@@ -15,3 +15,4 @@ crowdin.yml
build
docker-compose.yml
node_modules
.yarn
+9
View File
@@ -12,6 +12,7 @@ GOOGLE_CLIENT_SECRET=123
SLACK_CLIENT_ID=123
SLACK_CLIENT_SECRET=123
SLACK_VERIFICATION_TOKEN=test-token-123
GITHUB_CLIENT_ID=123;
GITHUB_CLIENT_SECRET=123;
@@ -29,3 +30,11 @@ RATE_LIMITER_ENABLED=false
FILE_STORAGE=local
FILE_STORAGE_LOCAL_ROOT_DIR=/tmp
URL=http://localhost:3000
COLLABORATION_URL=
REDIS_URL=redis://localhost:6379
UTILS_SECRET=test-utils-secret
DEBUG=
LOG_LEVEL=error
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
steps:
- name: Close unsigned PRs
uses: actions/github-script@v6
uses: actions/github-script@v8
with:
script: |
const now = new Date();
@@ -40,7 +40,7 @@ jobs:
github.event.pull_request.head.repo.full_name == github.repository)
steps:
- name: Checkout Branch
uses: actions/checkout@v2
uses: actions/checkout@v5
- name: Compress Images
id: calibre
uses: calibreapp/image-actions@main
@@ -48,6 +48,7 @@ jobs:
githubToken: ${{ secrets.GITHUB_TOKEN }}
# For non-Pull Requests, run in compressOnly mode and we'll PR after.
compressOnly: ${{ github.event_name != 'pull_request' }}
minPctChange: "10"
- name: Create Pull Request
# If it's not a Pull Request then commit any changes as a new PR.
if: |
+74 -39
View File
@@ -18,44 +18,61 @@ env:
SMTP_USERNAME: localhost
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.x, 22.x]
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: "yarn"
- name: Install dependencies
run: yarn install --frozen-lockfile --prefer-offline
lint:
needs: build
setup:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v5
- name: Enable Corepack
run: corepack enable
- name: Use Node.js 22.x
uses: actions/setup-node@v5
with:
node-version: 22.x
cache: "yarn"
- run: yarn install --frozen-lockfile --prefer-offline
- name: Cache node_modules
id: cache-node-modules
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node-modules-${{ hashFiles('yarn.lock') }}
- name: Install dependencies
if: steps.cache-node-modules.outputs.cache-hit != 'true'
run: yarn install --immutable
lint:
needs: setup
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Enable Corepack
run: corepack enable
- uses: actions/setup-node@v5
with:
node-version: 22.x
cache: "yarn"
- name: Restore node_modules
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node-modules-${{ hashFiles('yarn.lock') }}
- run: yarn lint --quiet
types:
needs: build
needs: setup
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v5
- name: Enable Corepack
run: corepack enable
- uses: actions/setup-node@v5
with:
node-version: 22.x
cache: "yarn"
- run: yarn install --frozen-lockfile --prefer-offline
- name: Restore node_modules
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node-modules-${{ hashFiles('yarn.lock') }}
- run: yarn tsc
changes:
@@ -65,7 +82,7 @@ jobs:
server: ${{ steps.filter.outputs.server }}
app: ${{ steps.filter.outputs.app }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: dorny/paths-filter@v2
id: filter
with:
@@ -85,23 +102,29 @@ jobs:
- 'yarn.lock'
test:
needs: [build, changes]
needs: [setup, changes]
if: ${{ needs.changes.outputs.app == 'true' || needs.changes.outputs.config == 'true' }}
runs-on: ubuntu-latest
strategy:
matrix:
test-group: [app, shared]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v5
- name: Enable Corepack
run: corepack enable
- uses: actions/setup-node@v5
with:
node-version: 22.x
cache: "yarn"
- run: yarn install --frozen-lockfile --prefer-offline
- name: Restore node_modules
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node-modules-${{ hashFiles('yarn.lock') }}
- run: yarn test:${{ matrix.test-group }}
test-server:
needs: [build, changes]
needs: [setup, changes]
if: ${{ needs.changes.outputs.server == 'true' || needs.changes.outputs.config == 'true' }}
runs-on: ubuntu-latest
services:
@@ -124,29 +147,41 @@ jobs:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v5
- name: Enable Corepack
run: corepack enable
- uses: actions/setup-node@v5
with:
node-version: 22.x
cache: "yarn"
- run: yarn install --frozen-lockfile --prefer-offline
- name: Restore node_modules
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node-modules-${{ hashFiles('yarn.lock') }}
- run: yarn sequelize db:migrate
- name: Run server tests
run: |
TESTFILES=$(find . -name "*.test.ts" -path "*/server/*" | sort | split -n -d -l $(($(find . -name "*.test.ts" -path "*/server/*" | wc -l)/${{ matrix.shard }})) - | sed -n "${{ matrix.shard }}p")
TESTFILES=$(find . -name "*.test.ts" -path "*/server/*" | sort | awk "NR % 4 == (${{ matrix.shard }} - 1)")
yarn test --maxWorkers=2 $TESTFILES
bundle-size:
needs: [build, types, changes]
needs: [setup, types, changes]
if: ${{ needs.changes.outputs.app == 'true' && github.repository == 'outline/outline' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v5
- name: Enable Corepack
run: corepack enable
- uses: actions/setup-node@v5
with:
node-version: 22.x
cache: "yarn"
- run: yarn install --frozen-lockfile --prefer-offline
- name: Restore node_modules
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node-modules-${{ hashFiles('yarn.lock') }}
- name: Set environment to production
run: echo "NODE_ENV=production" >> $GITHUB_ENV
- run: yarn vite:build
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v5
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
+2 -2
View File
@@ -14,7 +14,7 @@ jobs:
runs-on: ubicloud-standard-8-arm
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
@@ -93,7 +93,7 @@ jobs:
runs-on: ubicloud-standard-8
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-30
View File
@@ -1,30 +0,0 @@
name: Lint
on:
pull_request:
branches: [main]
jobs:
run-linters:
if: startsWith(github.actor, 'codegen-sh')
name: Run linters
runs-on: ubuntu-latest
permissions:
# Give the default GITHUB_TOKEN write permission to commit and push the
# added or changed files to the repository.
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20.x
cache: "yarn"
- run: yarn install --frozen-lockfile --prefer-offline
- run: yarn lint --fix
- name: Commit changes
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "Applied automatic fixes"
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v5
- uses: actions/stale@v10
with:
stale-pr-message: "This PR is stale because it has been open 90 days with no activity. Remove stale label or comment or this will be closed in 5 days"
stale-issue-message: "This issue is stale because it has been open 90 days with no activity. Remove stale label or comment or this will be closed in 5 days"
+7
View File
@@ -14,3 +14,10 @@ data/*
*.pem
*.key
*.cert
# Yarn Berry
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
+7 -5
View File
@@ -1,6 +1,7 @@
{
"workerIdleMemoryLimit": "0.75",
"maxWorkers": "50%",
"transformIgnorePatterns": ["node_modules/(?!(franc|trigram-utils)/)"],
"projects": [
{
"displayName": "server",
@@ -9,7 +10,10 @@
"^@server/(.*)$": "<rootDir>/server/$1",
"^@shared/(.*)$": "<rootDir>/shared/$1"
},
"setupFiles": ["<rootDir>/__mocks__/console.js"],
"setupFiles": [
"<rootDir>/__mocks__/console.js",
"<rootDir>/server/test/setupMocks.js"
],
"setupFilesAfterEnv": ["<rootDir>/server/test/setup.ts"],
"globalTeardown": "<rootDir>/server/test/globalTeardown.js",
"testEnvironment": "node"
@@ -20,8 +24,7 @@
"moduleNameMapper": {
"^~/(.*)$": "<rootDir>/app/$1",
"^@shared/(.*)$": "<rootDir>/shared/$1",
"^.*[.](gif|ttf|eot|svg)$": "<rootDir>/__test__/fileMock.js",
"^uuid$": "<rootDir>/node_modules/uuid/dist/index.js"
"^.*[.](gif|ttf|eot|svg)$": "<rootDir>/__test__/fileMock.js"
},
"modulePaths": ["<rootDir>/app"],
"setupFiles": ["<rootDir>/__mocks__/window.js"],
@@ -48,8 +51,7 @@
"moduleNameMapper": {
"^~/(.*)$": "<rootDir>/app/$1",
"^@shared/(.*)$": "<rootDir>/shared/$1",
"^.*[.](gif|ttf|eot|svg)$": "<rootDir>/__test__/fileMock.js",
"^uuid$": "<rootDir>/node_modules/uuid/dist/index.js"
"^.*[.](gif|ttf|eot|svg)$": "<rootDir>/__test__/fileMock.js"
},
"setupFiles": ["<rootDir>/__mocks__/window.js"],
"testEnvironment": "jsdom",
+3 -1
View File
@@ -86,6 +86,7 @@
"@typescript-eslint/no-require-imports": "off",
"import/no-named-as-default": "off",
"import/no-named-as-default-member": "off",
"typescript/consistent-type-imports": "error",
"no-unused-vars": [
"error",
{
@@ -94,7 +95,8 @@
"args": "after-used",
"ignoreRestSiblings": true
}
]
],
"react/rules-of-hooks": "error"
},
"plugins": ["eslint", "oxc", "react", "typescript", "import"]
}
+3
View File
@@ -0,0 +1,3 @@
nodeLinker: node-modules
npmMinimalAgeGate: 86400
+195
View File
@@ -0,0 +1,195 @@
Outline is a fast, collaborative knowledge base built for teams. It's built with React and TypeScript in both frontend and backend, uses a real-time collaboration engine, and is designed for excellent performance and user experience. The backend is a Koa server with an RPC API and uses PostgreSQL and Redis. The application can be self-hosted or used as a cloud service.
There is a web client which is fully responsive and works on mobile devices.
**Monorepo Structure:**
- **`app/`** - React web application with MobX state management
- **`server/`** - Koa API server with Sequelize ORM and background workers
- **`shared/`** - Shared TypeScript types, utilities, and editor components
- **`plugins/`** - Plugin system for extending functionality
- **`public/`** - Static assets served directly
- **Various config files** - TypeScript, Vite, Jest, Prettier, Oxlint configurations
Refer to /docs/ARCHITECTURE.md for detailed architecture documentation.
## Instructions
You're an expert in the following areas:
- TypeScript
- React and React Router
- MobX and MobX-React
- Node.js and Koa
- Sequelize ORM
- PostgreSQL
- Redis
- HTML, CSS and Styled Components
- Prosemirror (rich text editor)
- WebSockets and real-time collaboration
## General Guidelines
- Critical Do not create new markdown (.md) files.
- Use early returns for readability.
- Emphasize type safety and static analysis.
- Follow consistent Prettier formatting.
- Do not replace smart quotes ("") or ('') with simple quotes ("").
- Do not add translation strings manually; they will be extracted automatically from the codebase.
## Dependencies and Upgrading
- Use yarn for all dependency management.
- After updating dependency versions, install to update lockfiles:
```bash
yarn install
```
## TypeScript Usage
- Use strict mode.
- Avoid "unknown" unless absolutely necessary.
- Never use "any".
- Prefer type definitions; avoid type assertions (as, !).
- Always use curly braces for if statements.
- Avoid # for private properties.
- Prefer interface over type for object shapes.
## Classes & Code Organization
### Class Member Order
1. Public static variables
2. Public static methods
3. Public variables
4. Public methods
5. Protected variables & methods
6. Private variables & methods
### Exports
- Exported members must appear at the top of the file.
- Prefer named exports for components & classes.
- Document ALL public/exported functions with JSDoc.
## React Usage
- Use functional components with hooks.
- Event handlers should be prefixed with "handle", like "handleClick" for onClick.
- Avoid unnecessary re-renders by using React.memo, useMemo, and useCallback appropriately.
- Use descriptive prop types with TypeScript interfaces.
- Do not import React unless it is used directly.
- Use styled-components for component styling.
- Ensure high accessibility (a11y) standards using ARIA roles and semantic HTML.
## MobX State Management
- Use MobX stores for global state management.
- Keep stores in `app/stores/`.
- Use `observable`, `action`, and `computed` decorators appropriately.
- Prefer computed values over manual calculations in render.
- Keep business logic in stores, not components.
## Database & ORM
- Use Sequelize models in `server/models/`.
- Generate migrations with Sequelize CLI:
```bash
yarn sequelize migration:create --name=add-field-to-table
```
- Run migrations with `yarn db:migrate`.
- Use transactions for multi-table operations.
- Add appropriate indexes for query performance.
- Always handle database errors gracefully.
## API Design
- RESTful endpoints under `/api/`.
- Authentication endpoints under `/auth/`.
- Use consistent error responses.
- Validate request data using the validation middleware and schemas
- Use presenters to format API responses.
- Keep API routes thin, use model methods for business logic, or commands if logic spans multiple models.
## Authentication & Authorization
- JWT tokens for authentication.
- Policies in `server/policies/` for authorization.
- Use cancan-style ability checks.
- Use authenticated middleware for protected routes.
- Always verify user permissions before data access.
## Real-time Collaboration
- WebSocket connections for real-time updates.
- Use Y.js for collaborative editing.
- Handle connection state changes gracefully.
## Documentation
- All public/exported functions & classes must have JSDoc.
- Include:
- Description
- @param and @return (start lowercase, end with period)
- @throws if applicable
- Add a newline between the description and the @ block.
- Use correct punctuation.
## Testing
- Run tests with Jest:
```bash
# Run a specific test file (preferred)
yarn test path/to/test.spec.ts
# Run every test (avoid)
yarn test
# Run test suites (avoid)
yarn test:app # All frontend tests
yarn test:server # All backend tests
yarn test:shared # All shared code tests
```
- Write unit tests for utilities and business logic in a collocated .test.ts file.
- Do not create new test directories
- Mock external dependencies appropriately in **mocks** folder.
- Aim for high code coverage but focus on critical paths.
## Code Quality
- Use Oxlint for linting: `yarn lint`
- Format code with Prettier: `yarn format`
- Check types with TypeScript: `yarn tsc`
- Pre-commit hooks run automatically via Husky.
- Fix linting issues before committing.
## Error Handling
- Use custom error classes in `server/errors.ts`.
- Always catch and handle errors appropriately.
- Log errors with appropriate context.
- Return user-friendly error messages.
- Never expose sensitive information in errors.
## Performance
- Use React.memo for expensive components.
- Implement pagination for large lists.
- Use database indexes effectively.
- Cache expensive computations.
- Monitor performance with appropriate tools.
- Lazy load routes and components where appropriate.
## Security
- Sanitize all user input.
- Use CSRF protection.
- Use rateLimiter middleware for sensitive endpoints.
- Follow OWASP guidelines.
- Never store sensitive data in plain text.
- Use environment variables for secrets.
Symlink
+1
View File
@@ -0,0 +1 @@
AGENTS.md
+15 -15
View File
@@ -6,7 +6,7 @@ ARG APP_PATH
WORKDIR $APP_PATH
# ---
FROM node:22-slim AS runner
FROM node:22.21.0-slim AS runner
LABEL org.opencontainers.image.source="https://github.com/outline/outline"
@@ -16,26 +16,26 @@ ENV NODE_ENV=production
# Create a non-root user compatible with Debian and BusyBox based images
RUN addgroup --gid 1001 nodejs && \
adduser --uid 1001 --ingroup nodejs nodejs && \
mkdir -p /var/lib/outline && \
chown -R nodejs:nodejs /var/lib/outline
adduser --uid 1001 --ingroup nodejs nodejs && \
mkdir -p /var/lib/outline && \
chown -R nodejs:nodejs /var/lib/outline && \
chown -R nodejs:nodejs $APP_PATH
COPY --from=base --chown=nodejs:nodejs $APP_PATH/build ./build
COPY --from=base $APP_PATH/server ./server
COPY --from=base $APP_PATH/public ./public
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
COPY --from=base --chown=nodejs:nodejs $APP_PATH/server ./server
COPY --from=base --chown=nodejs:nodejs $APP_PATH/public ./public
COPY --from=base --chown=nodejs:nodejs $APP_PATH/.sequelizerc ./.sequelizerc
COPY --from=base --chown=nodejs:nodejs $APP_PATH/node_modules ./node_modules
COPY --from=base --chown=nodejs:nodejs $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/*
&& apt-get install -y wget \
&& rm -rf /var/lib/apt/lists/*
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"
chown -R nodejs:nodejs "$FILE_STORAGE_LOCAL_ROOT_DIR" && \
chmod 1777 "$FILE_STORAGE_LOCAL_ROOT_DIR"
VOLUME /var/lib/outline/data
@@ -44,4 +44,4 @@ USER nodejs
HEALTHCHECK --interval=1m CMD wget -qO- "http://localhost:${PORT:-3000}/_health" | grep -q "OK" || exit 1
EXPOSE 3000
CMD ["yarn", "start"]
CMD ["node", "build/server/index.js"]
+5 -6
View File
@@ -1,24 +1,23 @@
ARG APP_PATH=/opt/outline
FROM node:22 AS deps
FROM node:22.21.0 AS deps
ARG APP_PATH
WORKDIR $APP_PATH
COPY ./package.json ./yarn.lock ./
COPY ./package.json ./yarn.lock ./.yarnrc.yml ./
COPY ./patches ./patches
RUN apt-get update && apt-get install -y cmake
ENV NODE_OPTIONS="--max-old-space-size=24000"
RUN yarn install --no-optional --frozen-lockfile --network-timeout 1000000 && \
RUN corepack enable
RUN yarn install --immutable --network-timeout 1000000 && \
yarn cache clean
COPY . .
ARG CDN_URL
RUN yarn build
RUN rm -rf node_modules
RUN yarn install --production=true --frozen-lockfile --network-timeout 1000000 && \
RUN yarn workspaces focus --production && \
yarn cache clean
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.87.4
The Licensed Work is (c) 2025 General Outline, Inc.
Licensed Work: Outline 1.2.0
The Licensed Work is (c) 2026 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: 2029-09-18
Change Date: 2030-01-06
Change License: Apache License, Version 2.0
+1 -1
View File
@@ -1,7 +1,7 @@
up:
docker compose up -d redis postgres
yarn install-local-ssl
yarn install --pure-lockfile
yarn install --immutable
yarn dev:watch
build:
+5
View File
@@ -194,6 +194,11 @@
"description": "Use a secure SMTP connection (optional)",
"required": false
},
"SMTP_DISABLE_STARTTLS": {
"value": "false",
"description": "Disable STARTTLS even if the server supports it (optional)",
"required": false
},
"SMTP_TLS_CIPHERS": {
"description": "Override SMTP cipher configuration (optional)",
"required": false
+7
View File
@@ -5,6 +5,13 @@
{
"files": ["**/*.{jsx,tsx}"],
"rules": {
"no-restricted-globals": [
"error",
{
"name": "crypto",
"message": "Do not use, does not work in environments without SSL."
}
],
"no-restricted-imports": [
"error",
{
+3 -3
View File
@@ -1,9 +1,9 @@
import { PlusIcon, TrashIcon } from "outline-icons";
import stores from "~/stores";
import ApiKey from "~/models/ApiKey";
import type ApiKey from "~/models/ApiKey";
import ApiKeyNew from "~/scenes/ApiKeyNew";
import ApiKeyRevokeDialog from "~/scenes/Settings/components/ApiKeyRevokeDialog";
import { createAction, createActionV2 } from "..";
import { createAction } from "..";
import { SettingsSection } from "../sections";
export const createApiKey = createAction({
@@ -26,7 +26,7 @@ export const createApiKey = createAction({
});
export const revokeApiKeyFactory = ({ apiKey }: { apiKey: ApiKey }) =>
createActionV2({
createAction({
name: ({ t, isMenu }) =>
isMenu
? apiKey.isExpired
+35 -38
View File
@@ -20,7 +20,7 @@ import {
UnsubscribeIcon,
} from "outline-icons";
import { toast } from "sonner";
import Collection from "~/models/Collection";
import type Collection from "~/models/Collection";
import { CollectionEdit } from "~/components/Collection/CollectionEdit";
import { CollectionNew } from "~/components/Collection/CollectionNew";
import CollectionDeleteDialog from "~/components/CollectionDeleteDialog";
@@ -29,9 +29,8 @@ import DynamicCollectionIcon from "~/components/Icons/CollectionIcon";
import { getHeaderExpandedKey } from "~/components/Sidebar/components/Header";
import {
createAction,
createActionV2,
createActionV2WithChildren,
createInternalLinkActionV2,
createActionWithChildren,
createInternalLinkAction,
} from "~/actions";
import { ActiveCollectionSection, CollectionSection } from "~/actions/sections";
import { setPersistedState } from "~/hooks/usePersistedState";
@@ -52,7 +51,7 @@ const SharePopover = lazyWithRetry(
() => import("~/components/Sharing/Collection/SharePopover")
);
export const openCollection = createAction({
export const openCollection = createActionWithChildren({
name: ({ t }) => t("Open collection"),
analyticsName: "Open collection",
section: CollectionSection,
@@ -60,15 +59,17 @@ export const openCollection = createAction({
icon: <CollectionIcon />,
children: ({ stores }) => {
const collections = stores.collections.orderedData;
return collections.map((collection) => ({
// Note: using url which includes the slug rather than id here to bust
// cache if the collection is renamed
id: collection.path,
name: collection.name,
icon: <ColorCollectionIcon collection={collection} />,
section: CollectionSection,
to: collection.path,
}));
return collections.map((collection) =>
createInternalLinkAction({
// Note: using url which includes the slug rather than id here to bust
// cache if the collection is renamed
id: collection.path,
name: collection.name,
icon: <ColorCollectionIcon collection={collection} />,
section: CollectionSection,
to: collection.path,
})
);
},
});
@@ -90,7 +91,7 @@ export const createCollection = createAction({
},
});
export const editCollection = createActionV2({
export const editCollection = createAction({
name: ({ t, isMenu }) => (isMenu ? `${t("Edit")}` : t("Edit collection")),
analyticsName: "Edit collection",
section: ActiveCollectionSection,
@@ -115,7 +116,7 @@ export const editCollection = createActionV2({
},
});
export const editCollectionPermissions = createActionV2({
export const editCollectionPermissions = createAction({
name: ({ t, isMenu }) =>
isMenu ? `${t("Permissions")}` : t("Collection permissions"),
analyticsName: "Collection permissions",
@@ -135,7 +136,6 @@ export const editCollectionPermissions = createActionV2({
stores.dialogs.openModal({
title: t("Share this collection"),
style: { marginBottom: -12 },
content: (
<SharePopover
collection={collection}
@@ -147,7 +147,7 @@ export const editCollectionPermissions = createActionV2({
},
});
export const importDocument = createActionV2({
export const importDocument = createAction({
name: ({ t }) => t("Import document"),
analyticsName: "Import document",
section: ActiveCollectionSection,
@@ -163,7 +163,7 @@ export const importDocument = createActionV2({
const { documents } = stores;
const input = document.createElement("input");
input.type = "file";
input.accept = documents.importFileTypes.join(", ");
input.accept = documents.importFileTypesString;
input.onchange = async (ev) => {
const files = getEventFiles(ev);
@@ -188,7 +188,7 @@ export const importDocument = createActionV2({
},
});
export const sortCollection = createActionV2WithChildren({
export const sortCollection = createActionWithChildren({
name: ({ t }) => t("Sort in sidebar"),
section: ActiveCollectionSection,
visible: ({ activeCollectionId, stores }) =>
@@ -210,7 +210,7 @@ export const sortCollection = createActionV2WithChildren({
);
},
children: [
createActionV2({
createAction({
name: ({ t }) => t("A-Z sort"),
section: ActiveCollectionSection,
selected: ({ activeCollectionId, stores }) => {
@@ -230,7 +230,7 @@ export const sortCollection = createActionV2WithChildren({
});
},
}),
createActionV2({
createAction({
name: ({ t }) => t("Z-A sort"),
section: ActiveCollectionSection,
selected: ({ activeCollectionId, stores }) => {
@@ -250,7 +250,7 @@ export const sortCollection = createActionV2WithChildren({
});
},
}),
createActionV2({
createAction({
name: ({ t }) => t("Manual sort"),
section: ActiveCollectionSection,
selected: ({ activeCollectionId, stores }) => {
@@ -270,7 +270,7 @@ export const sortCollection = createActionV2WithChildren({
],
});
export const searchInCollection = createInternalLinkActionV2({
export const searchInCollection = createInternalLinkAction({
name: ({ t }) => t("Search in collection"),
analyticsName: "Search collection",
section: ActiveCollectionSection,
@@ -301,7 +301,7 @@ export const searchInCollection = createInternalLinkActionV2({
},
});
export const starCollection = createActionV2({
export const starCollection = createAction({
name: ({ t }) => t("Star"),
analyticsName: "Star collection",
section: ActiveCollectionSection,
@@ -328,7 +328,7 @@ export const starCollection = createActionV2({
},
});
export const unstarCollection = createActionV2({
export const unstarCollection = createAction({
name: ({ t }) => t("Unstar"),
analyticsName: "Unstar collection",
section: ActiveCollectionSection,
@@ -354,7 +354,7 @@ export const unstarCollection = createActionV2({
},
});
export const subscribeCollection = createActionV2({
export const subscribeCollection = createAction({
name: ({ t }) => t("Subscribe"),
analyticsName: "Subscribe to collection",
section: ActiveCollectionSection,
@@ -385,7 +385,7 @@ export const subscribeCollection = createActionV2({
},
});
export const unsubscribeCollection = createActionV2({
export const unsubscribeCollection = createAction({
name: ({ t }) => t("Unsubscribe"),
analyticsName: "Unsubscribe from collection",
section: ActiveCollectionSection,
@@ -416,7 +416,7 @@ export const unsubscribeCollection = createActionV2({
},
});
export const archiveCollection = createActionV2({
export const archiveCollection = createAction({
name: ({ t }) => `${t("Archive")}`,
analyticsName: "Archive collection",
section: ActiveCollectionSection,
@@ -457,7 +457,7 @@ export const archiveCollection = createActionV2({
},
});
export const restoreCollection = createActionV2({
export const restoreCollection = createAction({
name: ({ t }) => t("Restore"),
analyticsName: "Restore collection",
section: CollectionSection,
@@ -482,7 +482,7 @@ export const restoreCollection = createActionV2({
},
});
export const deleteCollection = createActionV2({
export const deleteCollection = createAction({
name: ({ t }) => `${t("Delete")}`,
analyticsName: "Delete collection",
section: ActiveCollectionSection,
@@ -516,7 +516,7 @@ export const deleteCollection = createActionV2({
},
});
export const exportCollection = createActionV2({
export const exportCollection = createAction({
name: ({ t }) => `${t("Export")}`,
analyticsName: "Export collection",
section: ActiveCollectionSection,
@@ -526,10 +526,7 @@ export const exportCollection = createActionV2({
return false;
}
return (
!!stores.policies.abilities(currentTeamId).createExport &&
!!stores.policies.abilities(activeCollectionId).export
);
return !!stores.policies.abilities(activeCollectionId).export;
},
perform: async ({ activeCollectionId, stores, t }) => {
if (!activeCollectionId) {
@@ -552,7 +549,7 @@ export const exportCollection = createActionV2({
},
});
export const createDocument = createInternalLinkActionV2({
export const createDocument = createInternalLinkAction({
name: ({ t }) => t("New document"),
analyticsName: "New document",
section: ActiveCollectionSection,
@@ -574,7 +571,7 @@ export const createDocument = createInternalLinkActionV2({
},
});
export const createTemplate = createInternalLinkActionV2({
export const createTemplate = createInternalLinkAction({
name: ({ t }) => t("New template"),
analyticsName: "New template",
section: ActiveCollectionSection,
+6 -6
View File
@@ -1,9 +1,9 @@
import { DoneIcon, SmileyIcon, TrashIcon } from "outline-icons";
import { toast } from "sonner";
import Comment from "~/models/Comment";
import type Comment from "~/models/Comment";
import CommentDeleteDialog from "~/components/CommentDeleteDialog";
import ViewReactionsDialog from "~/components/Reactions/ViewReactionsDialog";
import { createActionV2 } from "..";
import { createAction } from "..";
import { ActiveDocumentSection } from "../sections";
export const deleteCommentFactory = ({
@@ -13,7 +13,7 @@ export const deleteCommentFactory = ({
comment: Comment;
onDelete: () => void;
}) =>
createActionV2({
createAction({
name: ({ t }) => `${t("Delete")}`,
analyticsName: "Delete comment",
section: ActiveDocumentSection,
@@ -39,7 +39,7 @@ export const resolveCommentFactory = ({
comment: Comment;
onResolve: () => void;
}) =>
createActionV2({
createAction({
name: ({ t }) => t("Mark as resolved"),
analyticsName: "Resolve thread",
section: ActiveDocumentSection,
@@ -61,7 +61,7 @@ export const unresolveCommentFactory = ({
comment: Comment;
onUnresolve: () => void;
}) =>
createActionV2({
createAction({
name: ({ t }) => t("Mark as unresolved"),
analyticsName: "Unresolve thread",
section: ActiveDocumentSection,
@@ -80,7 +80,7 @@ export const viewCommentReactionsFactory = ({
}: {
comment: Comment;
}) =>
createActionV2({
createAction({
name: ({ t }) => `${t("View reactions")}`,
analyticsName: "View comment reactions",
section: ActiveDocumentSection,
+32 -5
View File
@@ -9,7 +9,7 @@ import {
UserIcon,
} from "outline-icons";
import { toast } from "sonner";
import { createAction } from "~/actions";
import { createAction, createActionWithChildren } from "~/actions";
import { DeveloperSection } from "~/actions/sections";
import env from "~/env";
import { client } from "~/utils/ApiClient";
@@ -17,9 +17,19 @@ import { Feature, FeatureFlags } from "~/utils/FeatureFlags";
import Logger from "~/utils/Logger";
import { deleteAllDatabases } from "~/utils/developer";
import history from "~/utils/history";
import { homePath } from "~/utils/routeHelpers";
import { homePath, debugPath } from "~/utils/routeHelpers";
export const copyId = createAction({
export const goToDebug = createAction({
name: "Go to debug screen",
icon: <BeakerIcon />,
section: DeveloperSection,
visible: () => env.ENVIRONMENT === "development",
perform: () => {
history.push(debugPath());
},
});
export const copyId = createActionWithChildren({
name: ({ t }) => t("Copy ID"),
icon: <CopyIcon />,
keywords: "uuid",
@@ -176,7 +186,22 @@ export const toggleDebugLogging = createAction({
},
});
export const toggleFeatureFlag = createAction({
export const toggleDebugSafeArea = createAction({
name: () => "Toggle menu safe area debugging",
icon: <ToolsIcon />,
section: DeveloperSection,
visible: () => env.ENVIRONMENT === "development",
perform: ({ stores }) => {
stores.ui.toggleDebugSafeArea();
toast.message(
stores.ui.debugSafeArea
? "Menu safe area debugging enabled"
: "Menu safe area debugging disabled"
);
},
});
export const toggleFeatureFlag = createActionWithChildren({
name: "Toggle feature flag",
icon: <BeakerIcon />,
section: DeveloperSection,
@@ -200,15 +225,17 @@ export const toggleFeatureFlag = createAction({
),
});
export const developer = createAction({
export const developer = createActionWithChildren({
name: ({ t }) => t("Development"),
keywords: "debug",
icon: <ToolsIcon />,
iconInContextMenu: false,
section: DeveloperSection,
children: [
goToDebug,
copyId,
toggleDebugLogging,
toggleDebugSafeArea,
toggleFeatureFlag,
createToast,
createTestUsers,
+92 -88
View File
@@ -35,13 +35,10 @@ import {
} from "outline-icons";
import { toast } from "sonner";
import Icon from "@shared/components/Icon";
import {
ExportContentType,
TeamPreference,
NavigationNode,
} from "@shared/types";
import type { NavigationNode } from "@shared/types";
import { ExportContentType, TeamPreference } from "@shared/types";
import { getEventFiles } from "@shared/utils/files";
import UserMembership from "~/models/UserMembership";
import type UserMembership from "~/models/UserMembership";
import DocumentDelete from "~/scenes/DocumentDelete";
import DocumentMove from "~/scenes/DocumentMove";
import DocumentPermanentDelete from "~/scenes/DocumentPermanentDelete";
@@ -54,10 +51,9 @@ import { getHeaderExpandedKey } from "~/components/Sidebar/components/Header";
import DocumentTemplatizeDialog from "~/components/TemplatizeDialog";
import {
createAction,
createActionV2,
createActionV2Group,
createActionV2WithChildren,
createInternalLinkActionV2,
createActionGroup,
createActionWithChildren,
createInternalLinkAction,
} from "~/actions";
import {
ActiveDocumentSection,
@@ -80,7 +76,7 @@ import {
} from "~/utils/routeHelpers";
import capitalize from "lodash/capitalize";
import CollectionIcon from "~/components/Icons/CollectionIcon";
import { ActionV2, ActionV2Group, ActionV2Separator } from "~/types";
import type { Action, ActionGroup, ActionSeparator } from "~/types";
import lazyWithRetry from "~/utils/lazyWithRetry";
const Insights = lazyWithRetry(
@@ -90,7 +86,7 @@ const SharePopover = lazyWithRetry(
() => import("~/components/Sharing/Document/SharePopover")
);
export const openDocument = createAction({
export const openDocument = createActionWithChildren({
name: ({ t }) => t("Open document"),
analyticsName: "Open document",
section: DocumentSection,
@@ -104,23 +100,29 @@ export const openDocument = createAction({
);
const documents = stores.documents.orderedData;
return uniqBy([...documents, ...nodes], "id").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,
to: item.url,
}));
return uniqBy([...documents, ...nodes], "id").map((item) =>
createInternalLinkAction({
// 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}
initial={item.title}
color={item.color ?? undefined}
/>
) : (
<DocumentIcon />
),
section: DocumentSection,
to: item.url,
})
);
},
});
export const editDocument = createInternalLinkActionV2({
export const editDocument = createInternalLinkAction({
name: ({ t }) => t("Edit"),
analyticsName: "Edit document",
section: ActiveDocumentSection,
@@ -152,7 +154,7 @@ export const editDocument = createInternalLinkActionV2({
},
});
export const createDocument = createAction({
export const createDocument = createInternalLinkAction({
name: ({ t }) => t("New document"),
analyticsName: "New document",
section: DocumentSection,
@@ -170,13 +172,18 @@ export const createDocument = createAction({
!!currentTeamId && stores.policies.abilities(currentTeamId).createDocument
);
},
perform: ({ activeCollectionId, sidebarContext }) =>
history.push(newDocumentPath(activeCollectionId), {
sidebarContext,
}),
to: ({ activeCollectionId, sidebarContext }) => {
const [pathname, search] = newDocumentPath(activeCollectionId).split("?");
return {
pathname,
search,
state: { sidebarContext },
};
},
});
export const createDraftDocument = createAction({
export const createDraftDocument = createInternalLinkAction({
name: ({ t }) => t("New draft"),
analyticsName: "New document",
section: DocumentSection,
@@ -184,13 +191,13 @@ export const createDraftDocument = createAction({
keywords: "create document",
visible: ({ currentTeamId, stores }) =>
!!currentTeamId && stores.policies.abilities(currentTeamId).createDocument,
perform: ({ sidebarContext }) =>
history.push(newDocumentPath(), {
sidebarContext,
}),
to: ({ sidebarContext }) => ({
pathname: newDocumentPath(),
state: { sidebarContext },
}),
});
export const createDocumentFromTemplate = createInternalLinkActionV2({
export const createDocumentFromTemplate = createInternalLinkAction({
name: ({ t }) => t("New from template"),
analyticsName: "New document",
section: DocumentSection,
@@ -237,7 +244,7 @@ export const createDocumentFromTemplate = createInternalLinkActionV2({
},
});
export const createNestedDocument = createInternalLinkActionV2({
export const createNestedDocument = createInternalLinkAction({
name: ({ t }) => t("New nested document"),
analyticsName: "New document",
section: ActiveDocumentSection,
@@ -260,7 +267,7 @@ export const createNestedDocument = createInternalLinkActionV2({
},
});
export const starDocument = createActionV2({
export const starDocument = createAction({
name: ({ t }) => t("Star"),
analyticsName: "Star document",
section: ActiveDocumentSection,
@@ -286,7 +293,7 @@ export const starDocument = createActionV2({
},
});
export const unstarDocument = createActionV2({
export const unstarDocument = createAction({
name: ({ t }) => t("Unstar"),
analyticsName: "Unstar document",
section: ActiveDocumentSection,
@@ -312,7 +319,7 @@ export const unstarDocument = createActionV2({
},
});
export const publishDocument = createActionV2({
export const publishDocument = createAction({
name: ({ t }) => t("Publish"),
analyticsName: "Publish document",
section: ActiveDocumentSection,
@@ -354,7 +361,7 @@ export const publishDocument = createActionV2({
},
});
export const unpublishDocument = createActionV2({
export const unpublishDocument = createAction({
name: ({ t }) => t("Unpublish"),
analyticsName: "Unpublish document",
section: ActiveDocumentSection,
@@ -385,7 +392,7 @@ export const unpublishDocument = createActionV2({
},
});
export const subscribeDocument = createActionV2({
export const subscribeDocument = createAction({
name: ({ t }) => t("Subscribe"),
analyticsName: "Subscribe to document",
section: ActiveDocumentSection,
@@ -431,7 +438,7 @@ export const subscribeDocument = createActionV2({
},
});
export const unsubscribeDocument = createActionV2({
export const unsubscribeDocument = createAction({
name: ({ t }) => t("Unsubscribe"),
analyticsName: "Unsubscribe from document",
section: ActiveDocumentSection,
@@ -479,7 +486,7 @@ export const unsubscribeDocument = createActionV2({
},
});
export const shareDocument = createActionV2({
export const shareDocument = createAction({
name: ({ t }) => `${t("Permissions")}`,
analyticsName: "Share document",
section: ActiveDocumentSection,
@@ -499,7 +506,6 @@ export const shareDocument = createActionV2({
}
stores.dialogs.openModal({
style: { marginBottom: -12 },
title: t("Share this document"),
content: (
<SharePopover
@@ -512,7 +518,7 @@ export const shareDocument = createActionV2({
},
});
export const downloadDocumentAsHTML = createActionV2({
export const downloadDocumentAsHTML = createAction({
name: ({ t }) => t("HTML"),
analyticsName: "Download document as HTML",
section: ActiveDocumentSection,
@@ -531,7 +537,7 @@ export const downloadDocumentAsHTML = createActionV2({
},
});
export const downloadDocumentAsPDF = createActionV2({
export const downloadDocumentAsPDF = createAction({
name: ({ t }) => t("PDF"),
analyticsName: "Download document as PDF",
section: ActiveDocumentSection,
@@ -557,7 +563,7 @@ export const downloadDocumentAsPDF = createActionV2({
},
});
export const downloadDocumentAsMarkdown = createActionV2({
export const downloadDocumentAsMarkdown = createAction({
name: ({ t }) => t("Markdown"),
analyticsName: "Download document as Markdown",
section: ActiveDocumentSection,
@@ -576,7 +582,7 @@ export const downloadDocumentAsMarkdown = createActionV2({
},
});
export const downloadDocument = createActionV2WithChildren({
export const downloadDocument = createActionWithChildren({
name: ({ t, isMenu }) => (isMenu ? t("Download") : t("Download document")),
analyticsName: "Download document",
section: ActiveDocumentSection,
@@ -591,7 +597,7 @@ export const downloadDocument = createActionV2WithChildren({
],
});
export const copyDocumentAsMarkdown = createActionV2({
export const copyDocumentAsMarkdown = createAction({
name: ({ t }) => t("Copy as Markdown"),
section: ActiveDocumentSection,
keywords: "clipboard",
@@ -604,16 +610,15 @@ export const copyDocumentAsMarkdown = createActionV2({
? stores.documents.get(activeDocumentId)
: undefined;
if (document) {
const { ProsemirrorHelper } = await import(
"~/models/helpers/ProsemirrorHelper"
);
const { ProsemirrorHelper } =
await import("~/models/helpers/ProsemirrorHelper");
copy(ProsemirrorHelper.toMarkdown(document));
toast.success(t("Markdown copied to clipboard"));
}
},
});
export const copyDocumentAsPlainText = createActionV2({
export const copyDocumentAsPlainText = createAction({
name: ({ t }) => t("Copy as text"),
section: ActiveDocumentSection,
keywords: "clipboard",
@@ -626,16 +631,15 @@ export const copyDocumentAsPlainText = createActionV2({
? stores.documents.get(activeDocumentId)
: undefined;
if (document) {
const { ProsemirrorHelper } = await import(
"~/models/helpers/ProsemirrorHelper"
);
const { ProsemirrorHelper } =
await import("~/models/helpers/ProsemirrorHelper");
copy(ProsemirrorHelper.toPlainText(document));
toast.success(t("Text copied to clipboard"));
}
},
});
export const copyDocumentShareLink = createActionV2({
export const copyDocumentShareLink = createAction({
name: ({ t }) => t("Copy public link"),
section: ActiveDocumentSection,
keywords: "clipboard share",
@@ -656,7 +660,7 @@ export const copyDocumentShareLink = createActionV2({
},
});
export const copyDocumentLink = createActionV2({
export const copyDocumentLink = createAction({
name: ({ t }) => t("Copy link"),
section: ActiveDocumentSection,
keywords: "clipboard",
@@ -674,7 +678,7 @@ export const copyDocumentLink = createActionV2({
},
});
export const copyDocument = createActionV2WithChildren({
export const copyDocument = createActionWithChildren({
name: ({ t }) => t("Copy"),
analyticsName: "Copy document",
section: ActiveDocumentSection,
@@ -688,7 +692,7 @@ export const copyDocument = createActionV2WithChildren({
],
});
export const duplicateDocument = createActionV2({
export const duplicateDocument = createAction({
name: ({ t, isMenu }) => (isMenu ? t("Duplicate") : t("Duplicate document")),
analyticsName: "Duplicate document",
section: ActiveDocumentSection,
@@ -723,7 +727,7 @@ export const duplicateDocument = createActionV2({
* Pin a document to a collection. Pinned documents will be displayed at the top
* of the collection for all collection members to see.
*/
export const pinDocumentToCollection = createActionV2({
export const pinDocumentToCollection = createAction({
name: ({ activeDocumentId = "", t, stores }) => {
const selectedDocument = stores.documents.get(activeDocumentId);
const collectionName = selectedDocument
@@ -768,7 +772,7 @@ export const pinDocumentToCollection = createActionV2({
* Pin a document to team home. Pinned documents will be displayed at the top
* of the home screen for all team members to see.
*/
export const pinDocumentToHome = createActionV2({
export const pinDocumentToHome = createAction({
name: ({ t }) => t("Pin to home"),
analyticsName: "Pin document to home",
section: ActiveDocumentSection,
@@ -800,7 +804,7 @@ export const pinDocumentToHome = createActionV2({
},
});
export const pinDocument = createActionV2WithChildren({
export const pinDocument = createActionWithChildren({
name: ({ t }) => t("Pin"),
analyticsName: "Pin document",
section: ActiveDocumentSection,
@@ -808,7 +812,7 @@ export const pinDocument = createActionV2WithChildren({
children: [pinDocumentToCollection, pinDocumentToHome],
});
export const searchInDocument = createInternalLinkActionV2({
export const searchInDocument = createInternalLinkAction({
name: ({ t }) => t("Search in document"),
analyticsName: "Search document",
section: ActiveDocumentSection,
@@ -838,7 +842,7 @@ export const searchInDocument = createInternalLinkActionV2({
},
});
export const printDocument = createActionV2({
export const printDocument = createAction({
name: ({ t, isMenu }) => (isMenu ? t("Print") : t("Print document")),
analyticsName: "Print document",
section: ActiveDocumentSection,
@@ -849,7 +853,7 @@ export const printDocument = createActionV2({
},
});
export const importDocument = createActionV2({
export const importDocument = createAction({
name: ({ t }) => t("Import document"),
analyticsName: "Import document",
section: DocumentSection,
@@ -870,7 +874,7 @@ export const importDocument = createActionV2({
const { documents } = stores;
const input = document.createElement("input");
input.type = "file";
input.accept = documents.importFileTypes.join(", ");
input.accept = documents.importFileTypesString;
input.onchange = async (ev) => {
const files = getEventFiles(ev);
@@ -895,7 +899,7 @@ export const importDocument = createActionV2({
},
});
export const createTemplateFromDocument = createActionV2({
export const createTemplateFromDocument = createAction({
name: ({ t }) => t("Templatize"),
analyticsName: "Templatize document",
section: ActiveDocumentSection,
@@ -946,7 +950,7 @@ export const openRandomDocument = createAction({
});
export const searchDocumentsForQuery = (query: string) =>
createAction({
createInternalLinkAction({
id: "search",
name: ({ t }) =>
t(`Search documents for "{{searchQuery}}"`, { searchQuery: query }),
@@ -957,7 +961,7 @@ export const searchDocumentsForQuery = (query: string) =>
visible: ({ location }) => location.pathname !== searchPath(),
});
export const moveTemplateToWorkspace = createActionV2({
export const moveTemplateToWorkspace = createAction({
name: ({ t }) => t("Move to workspace"),
analyticsName: "Move template to workspace",
section: DocumentSection,
@@ -987,7 +991,7 @@ export const moveTemplateToWorkspace = createActionV2({
},
});
export const moveDocumentToCollection = createActionV2({
export const moveDocumentToCollection = createAction({
name: ({ activeDocumentId, stores, t }) => {
if (!activeDocumentId) {
return t("Move");
@@ -1024,7 +1028,7 @@ export const moveDocumentToCollection = createActionV2({
},
});
export const moveDocument = createActionV2({
export const moveDocument = createAction({
name: ({ t }) => t("Move"),
analyticsName: "Move document",
section: ActiveDocumentSection,
@@ -1043,7 +1047,7 @@ export const moveDocument = createActionV2({
perform: moveDocumentToCollection.perform,
});
export const moveTemplate = createActionV2WithChildren({
export const moveTemplate = createActionWithChildren({
name: ({ t }) => t("Move"),
analyticsName: "Move document",
section: ActiveDocumentSection,
@@ -1062,7 +1066,7 @@ export const moveTemplate = createActionV2WithChildren({
children: [moveTemplateToWorkspace, moveDocumentToCollection],
});
export const archiveDocument = createActionV2({
export const archiveDocument = createAction({
name: ({ t }) => `${t("Archive")}`,
analyticsName: "Archive document",
section: ActiveDocumentSection,
@@ -1102,7 +1106,7 @@ export const archiveDocument = createActionV2({
},
});
export const restoreDocument = createActionV2({
export const restoreDocument = createAction({
name: ({ t }) => `${t("Restore")}`,
analyticsName: "Restore document",
section: ActiveDocumentSection,
@@ -1142,7 +1146,7 @@ export const restoreDocument = createActionV2({
},
});
export const restoreDocumentToCollection = createActionV2WithChildren({
export const restoreDocumentToCollection = createActionWithChildren({
name: ({ t }) => `${t("Restore")}`,
analyticsName: "Restore document",
section: ActiveDocumentSection,
@@ -1177,7 +1181,7 @@ export const restoreDocumentToCollection = createActionV2WithChildren({
const actions = collections.orderedData.map((collection) => {
const can = policies.abilities(collection.id);
return createActionV2({
return createAction({
name: collection.name,
section: ActiveDocumentSection,
icon: <CollectionIcon collection={collection} />,
@@ -1193,11 +1197,11 @@ export const restoreDocumentToCollection = createActionV2WithChildren({
});
});
return [createActionV2Group({ name: t("Choose a collection"), actions })];
return [createActionGroup({ name: t("Choose a collection"), actions })];
},
});
export const deleteDocument = createActionV2({
export const deleteDocument = createAction({
name: ({ t }) => `${t("Delete")}`,
analyticsName: "Delete document",
section: ActiveDocumentSection,
@@ -1231,7 +1235,7 @@ export const deleteDocument = createActionV2({
},
});
export const permanentlyDeleteDocument = createActionV2({
export const permanentlyDeleteDocument = createAction({
name: ({ t }) => t("Permanently delete"),
analyticsName: "Permanently delete document",
section: ActiveDocumentSection,
@@ -1286,7 +1290,7 @@ export const permanentlyDeleteDocumentsInTrash = createAction({
},
});
export const openDocumentComments = createActionV2({
export const openDocumentComments = createAction({
name: ({ t }) => t("Comments"),
analyticsName: "Open comments",
section: ActiveDocumentSection,
@@ -1309,7 +1313,7 @@ export const openDocumentComments = createActionV2({
},
});
export const openDocumentHistory = createInternalLinkActionV2({
export const openDocumentHistory = createInternalLinkAction({
name: ({ t }) => t("History"),
analyticsName: "Open document history",
section: ActiveDocumentSection,
@@ -1336,7 +1340,7 @@ export const openDocumentHistory = createInternalLinkActionV2({
},
});
export const openDocumentInsights = createActionV2({
export const openDocumentInsights = createAction({
name: ({ t }) => t("Insights"),
analyticsName: "Open document insights",
section: ActiveDocumentSection,
@@ -1369,7 +1373,7 @@ export const openDocumentInsights = createActionV2({
},
});
export const leaveDocument = createActionV2({
export const leaveDocument = createAction({
name: ({ t }) => t("Leave document"),
analyticsName: "Leave document",
section: ActiveDocumentSection,
@@ -1408,9 +1412,9 @@ export const leaveDocument = createActionV2({
export const applyTemplateFactory = ({
actions,
}: {
actions: (ActionV2 | ActionV2Group | ActionV2Separator)[];
actions: (Action | ActionGroup | ActionSeparator)[];
}) =>
createActionV2WithChildren({
createActionWithChildren({
name: ({ t }) => t("Apply template"),
analyticsName: "Apply template",
section: ActiveDocumentSection,
+21
View File
@@ -0,0 +1,21 @@
import { PlusIcon } from "outline-icons";
import { createAction } from "~/actions";
import { TeamSection } from "../sections";
import stores from "~/stores";
import { EmojiCreateDialog } from "~/components/EmojiCreateDialog";
export const createEmoji = createAction({
name: ({ t }) => `${t("New emoji")}`,
analyticsName: "Create emoji",
icon: <PlusIcon />,
keywords: "emoji custom upload image",
section: TeamSection,
visible: () =>
stores.policies.abilities(stores.auth.team?.id || "").createEmoji,
perform: ({ t }) => {
stores.dialogs.openModal({
title: t("Upload emoji"),
content: <EmojiCreateDialog onSubmit={stores.dialogs.closeAllModals} />,
});
},
});
+22 -3
View File
@@ -2,9 +2,28 @@ import { TrashIcon } from "outline-icons";
import stores from "~/stores";
import { createAction } from "..";
import { SettingsSection } from "../sections";
import Integration from "~/models/Integration";
import { IntegrationType } from "@shared/types";
import { DisconnectAnalyticsDialog } from "~/components/DisconnectAnalyticsDialog";
import type Integration from "~/models/Integration";
import { DisconnectAnalyticsDialog } from "~/scenes/Settings/components/DisconnectAnalyticsDialog";
import type { IntegrationType } from "@shared/types";
import { settingsPath } from "@shared/utils/routeHelpers";
import history from "~/utils/history";
export const disconnectIntegrationFactory = (integration?: Integration) =>
createAction({
name: ({ t }) => t("Disconnect"),
analyticsName: "Disconnect integration",
section: SettingsSection,
icon: <TrashIcon />,
keywords: "disconnect",
visible: () => !!integration,
perform: async ({ event }) => {
event?.preventDefault();
event?.stopPropagation();
await integration?.delete();
history.push(settingsPath("integrations"));
},
});
export const disconnectAnalyticsIntegrationFactory = (
integration?: Integration<IntegrationType.Analytics>
+25 -28
View File
@@ -17,13 +17,12 @@ import {
import { UrlHelper } from "@shared/utils/UrlHelper";
import { isMac } from "@shared/utils/browser";
import stores from "~/stores";
import SearchQuery from "~/models/SearchQuery";
import type SearchQuery from "~/models/SearchQuery";
import KeyboardShortcuts from "~/scenes/KeyboardShortcuts";
import {
createAction,
createActionV2,
createExternalLinkActionV2,
createInternalLinkActionV2,
createExternalLinkAction,
createInternalLinkAction,
} from "~/actions";
import { NavigationSection, RecentSearchesSection } from "~/actions/sections";
import Desktop from "~/utils/Desktop";
@@ -37,7 +36,7 @@ import {
settingsPath,
} from "~/utils/routeHelpers";
export const navigateToHome = createAction({
export const navigateToHome = createInternalLinkAction({
name: ({ t }) => t("Home"),
analyticsName: "Navigate to home",
section: NavigationSection,
@@ -48,7 +47,7 @@ export const navigateToHome = createAction({
});
export const navigateToRecentSearchQuery = (searchQuery: SearchQuery) =>
createAction({
createInternalLinkAction({
section: RecentSearchesSection,
name: searchQuery.query,
analyticsName: "Navigate to recent search query",
@@ -56,7 +55,7 @@ export const navigateToRecentSearchQuery = (searchQuery: SearchQuery) =>
to: searchPath({ query: searchQuery.query }),
});
export const navigateToDrafts = createAction({
export const navigateToDrafts = createInternalLinkAction({
name: ({ t }) => t("Drafts"),
analyticsName: "Navigate to drafts",
section: NavigationSection,
@@ -65,7 +64,7 @@ export const navigateToDrafts = createAction({
visible: ({ location }) => location.pathname !== draftsPath(),
});
export const navigateToSearch = createAction({
export const navigateToSearch = createInternalLinkAction({
name: ({ t }) => t("Search"),
analyticsName: "Navigate to search",
section: NavigationSection,
@@ -74,7 +73,7 @@ export const navigateToSearch = createAction({
visible: ({ location }) => location.pathname !== searchPath(),
});
export const navigateToArchive = createAction({
export const navigateToArchive = createInternalLinkAction({
name: ({ t }) => t("Archive"),
analyticsName: "Navigate to archive",
section: NavigationSection,
@@ -84,7 +83,7 @@ export const navigateToArchive = createAction({
visible: ({ location }) => location.pathname !== archivePath(),
});
export const navigateToTrash = createAction({
export const navigateToTrash = createInternalLinkAction({
name: ({ t }) => t("Trash"),
analyticsName: "Navigate to trash",
section: NavigationSection,
@@ -93,7 +92,7 @@ export const navigateToTrash = createAction({
visible: ({ location }) => location.pathname !== trashPath(),
});
export const navigateToSettings = createAction({
export const navigateToSettings = createInternalLinkAction({
name: ({ t }) => t("Settings"),
analyticsName: "Navigate to settings",
section: NavigationSection,
@@ -103,7 +102,7 @@ export const navigateToSettings = createAction({
to: settingsPath(),
});
export const navigateToWorkspaceSettings = createInternalLinkActionV2({
export const navigateToWorkspaceSettings = createInternalLinkAction({
name: ({ t }) => t("Settings"),
analyticsName: "Navigate to workspace settings",
section: NavigationSection,
@@ -112,7 +111,7 @@ export const navigateToWorkspaceSettings = createInternalLinkActionV2({
to: settingsPath("details"),
});
export const navigateToProfileSettings = createInternalLinkActionV2({
export const navigateToProfileSettings = createInternalLinkAction({
name: ({ t }) => t("Profile"),
analyticsName: "Navigate to profile settings",
section: NavigationSection,
@@ -121,7 +120,7 @@ export const navigateToProfileSettings = createInternalLinkActionV2({
to: settingsPath(),
});
export const navigateToTemplateSettings = createAction({
export const navigateToTemplateSettings = createInternalLinkAction({
name: ({ t }) => t("Templates"),
analyticsName: "Navigate to template settings",
section: NavigationSection,
@@ -130,7 +129,7 @@ export const navigateToTemplateSettings = createAction({
to: settingsPath("templates"),
});
export const navigateToNotificationSettings = createInternalLinkActionV2({
export const navigateToNotificationSettings = createInternalLinkAction({
name: ({ t, isMenu }) =>
isMenu ? t("Notification settings") : t("Notifications"),
analyticsName: "Navigate to notification settings",
@@ -140,7 +139,7 @@ export const navigateToNotificationSettings = createInternalLinkActionV2({
to: settingsPath("notifications"),
});
export const navigateToAccountPreferences = createInternalLinkActionV2({
export const navigateToAccountPreferences = createInternalLinkAction({
name: ({ t }) => t("Preferences"),
analyticsName: "Navigate to account preferences",
section: NavigationSection,
@@ -149,7 +148,7 @@ export const navigateToAccountPreferences = createInternalLinkActionV2({
to: settingsPath("preferences"),
});
export const openDocumentation = createExternalLinkActionV2({
export const openDocumentation = createExternalLinkAction({
name: ({ t }) => t("Documentation"),
analyticsName: "Open documentation",
section: NavigationSection,
@@ -159,7 +158,7 @@ export const openDocumentation = createExternalLinkActionV2({
target: "_blank",
});
export const openAPIDocumentation = createExternalLinkActionV2({
export const openAPIDocumentation = createExternalLinkAction({
name: ({ t }) => t("API documentation"),
analyticsName: "Open API documentation",
section: NavigationSection,
@@ -177,7 +176,7 @@ export const toggleSidebar = createAction({
perform: () => stores.ui.toggleCollapsedSidebar(),
});
export const openFeedbackUrl = createExternalLinkActionV2({
export const openFeedbackUrl = createExternalLinkAction({
name: ({ t }) => t("Send us feedback"),
analyticsName: "Open feedback",
section: NavigationSection,
@@ -187,7 +186,7 @@ export const openFeedbackUrl = createExternalLinkActionV2({
target: "_blank",
});
export const openBugReportUrl = createExternalLinkActionV2({
export const openBugReportUrl = createExternalLinkAction({
name: ({ t }) => t("Report a bug"),
analyticsName: "Open bug report",
section: NavigationSection,
@@ -197,7 +196,7 @@ export const openBugReportUrl = createExternalLinkActionV2({
target: "_blank",
});
export const openChangelog = createExternalLinkActionV2({
export const openChangelog = createExternalLinkAction({
name: ({ t }) => t("Changelog"),
analyticsName: "Open changelog",
section: NavigationSection,
@@ -207,7 +206,7 @@ export const openChangelog = createExternalLinkActionV2({
target: "_blank",
});
export const openKeyboardShortcuts = createActionV2({
export const openKeyboardShortcuts = createAction({
name: ({ t }) => t("Keyboard shortcuts"),
analyticsName: "Open keyboard shortcuts",
section: NavigationSection,
@@ -222,7 +221,7 @@ export const openKeyboardShortcuts = createActionV2({
},
});
export const downloadApp = createAction({
export const downloadApp = createExternalLinkAction({
name: ({ t }) =>
t("Download {{ platform }} app", {
platform: isMac() ? "macOS" : "Windows",
@@ -232,13 +231,11 @@ export const downloadApp = createAction({
iconInContextMenu: false,
icon: <BrowserIcon />,
visible: () => !Desktop.isElectron() && isMac() && isCloudHosted,
to: {
url: "https://desktop.getoutline.com",
target: "_blank",
},
url: "https://desktop.getoutline.com",
target: "_blank",
});
export const logout = createActionV2({
export const logout = createAction({
name: ({ t }) => t("Log out"),
analyticsName: "Log out",
section: NavigationSection,
+34 -3
View File
@@ -1,6 +1,7 @@
import { ArchiveIcon, MarkAsReadIcon } from "outline-icons";
import { createAction, createActionV2 } from "..";
import { ArchiveIcon, CheckmarkIcon, MarkAsReadIcon } from "outline-icons";
import { createAction } from "..";
import { NotificationSection } from "../sections";
import type Notification from "~/models/Notification";
export const markNotificationsAsRead = createAction({
name: ({ t }) => t("Mark notifications as read"),
@@ -12,7 +13,7 @@ export const markNotificationsAsRead = createAction({
visible: ({ stores }) => stores.notifications.approximateUnreadCount > 0,
});
export const markNotificationsAsArchived = createActionV2({
export const markNotificationsAsArchived = createAction({
name: ({ t }) => t("Archive all notifications"),
analyticsName: "Mark notifications as archived",
section: NotificationSection,
@@ -22,6 +23,36 @@ export const markNotificationsAsArchived = createActionV2({
visible: ({ stores }) => stores.notifications.orderedData.length > 0,
});
export const notificationMarkRead = (notification: Notification) =>
createAction({
name: ({ t }) => t("Mark as read"),
analyticsName: "Mark notification read",
section: NotificationSection,
icon: <CheckmarkIcon />,
perform: () => notification.toggleRead(),
visible: () => !notification.viewedAt,
});
export const notificationMarkUnread = (notification: Notification) =>
createAction({
name: ({ t }) => t("Mark as unread"),
analyticsName: "Mark notification unread",
section: NotificationSection,
icon: <CheckmarkIcon />,
perform: () => notification.toggleRead(),
visible: () => !!notification.viewedAt,
});
export const notificationArchive = (notification: Notification) =>
createAction({
name: ({ t }) => t("Archive"),
analyticsName: "Mark notification as archived",
section: NotificationSection,
icon: <ArchiveIcon />,
perform: () => notification.archive(),
visible: () => !notification.archivedAt,
});
export const rootNotificationActions = [
markNotificationsAsRead,
markNotificationsAsArchived,
+103 -32
View File
@@ -1,17 +1,20 @@
import copy from "copy-to-clipboard";
import { LinkIcon, RestoreIcon, TrashIcon } from "outline-icons";
import { LinkIcon, RestoreIcon, TrashIcon, DownloadIcon } from "outline-icons";
import { matchPath } from "react-router-dom";
import { toast } from "sonner";
import { ExportContentType } from "@shared/types";
import stores from "~/stores";
import { createAction, createActionV2 } from "~/actions";
import { createAction, createActionWithChildren } from "~/actions";
import { RevisionSection } from "~/actions/sections";
import env from "~/env";
import history from "~/utils/history";
import {
documentHistoryPath,
matchDocumentHistory,
urlify,
} from "~/utils/routeHelpers";
export const restoreRevision = createActionV2({
export const restoreRevision = createAction({
name: ({ t }) => t("Restore"),
analyticsName: "Restore revision",
icon: <RestoreIcon />,
@@ -73,37 +76,105 @@ export const deleteRevision = createAction({
},
});
export const copyLinkToRevision = createActionV2({
name: ({ t }) => t("Copy link"),
analyticsName: "Copy link to revision",
icon: <LinkIcon />,
section: RevisionSection,
perform: async ({ activeDocumentId, t }) => {
if (!activeDocumentId) {
return;
}
export const copyLinkToRevision = (revisionId: string) =>
createAction({
name: ({ t }) => t("Copy link"),
analyticsName: "Copy link to revision",
icon: <LinkIcon />,
section: RevisionSection,
perform: async ({ activeDocumentId, t }) => {
if (!activeDocumentId) {
return;
}
const match = matchPath<{ revisionId: string }>(location.pathname, {
path: matchDocumentHistory,
});
const revisionId = match?.params.revisionId;
const document = stores.documents.get(activeDocumentId);
if (!document) {
return;
}
const document = stores.documents.get(activeDocumentId);
if (!document) {
return;
}
const url = `${window.location.origin}${documentHistoryPath(
document,
revisionId
)}`;
const url = urlify(documentHistoryPath(document, revisionId));
copy(url, {
format: "text/plain",
onCopy: () => {
toast.message(t("Link copied"));
},
});
},
});
copy(url, {
format: "text/plain",
onCopy: () => {
toast.message(t("Link copied"));
},
});
},
});
export const downloadRevisionAsHTML = (revisionId: string) =>
createAction({
name: ({ t }) => t("HTML"),
analyticsName: "Download revision as HTML",
section: RevisionSection,
keywords: "html export",
icon: <DownloadIcon />,
iconInContextMenu: false,
visible: ({ activeDocumentId }) =>
!!activeDocumentId &&
stores.policies.abilities(activeDocumentId).download,
perform: async () => {
const revision = stores.revisions.get(revisionId);
await revision?.download(ExportContentType.Html);
},
});
export const downloadRevisionAsPDF = (revisionId: string) =>
createAction({
name: ({ t }) => t("PDF"),
analyticsName: "Download revision as PDF",
section: RevisionSection,
keywords: "export",
icon: <DownloadIcon />,
iconInContextMenu: false,
visible: ({ activeDocumentId }) =>
!!(
activeDocumentId &&
stores.policies.abilities(activeDocumentId).download &&
env.PDF_EXPORT_ENABLED
),
perform: ({ t }) => {
const id = toast.loading(`${t("Exporting")}`);
const revision = stores.revisions.get(revisionId);
return revision
?.download(ExportContentType.Pdf)
.finally(() => id && toast.dismiss(id));
},
});
export const downloadRevisionAsMarkdown = (revisionId: string) =>
createAction({
name: ({ t }) => t("Markdown"),
analyticsName: "Download revision as Markdown",
section: RevisionSection,
keywords: "md markdown export",
icon: <DownloadIcon />,
iconInContextMenu: false,
visible: ({ activeDocumentId }) =>
!!activeDocumentId &&
stores.policies.abilities(activeDocumentId).download,
perform: async () => {
const revision = stores.revisions.get(revisionId);
await revision?.download(ExportContentType.Markdown);
},
});
export const downloadRevision = (revisionId: string) =>
createActionWithChildren({
name: ({ t, isMenu }) => (isMenu ? t("Download") : t("Download revision")),
analyticsName: "Download revision",
section: RevisionSection,
icon: <DownloadIcon />,
keywords: "export",
visible: ({ activeDocumentId }) =>
!!activeDocumentId &&
stores.policies.abilities(activeDocumentId).download,
children: [
downloadRevisionAsHTML(revisionId),
downloadRevisionAsPDF(revisionId),
downloadRevisionAsMarkdown(revisionId),
],
});
export const rootRevisionActions = [];
+5 -5
View File
@@ -1,9 +1,9 @@
import { SunIcon, MoonIcon, BrowserIcon } from "outline-icons";
import { Theme } from "~/stores/UiStore";
import { createActionV2, createActionV2WithChildren } from "~/actions";
import { createAction, createActionWithChildren } from "~/actions";
import { SettingsSection } from "~/actions/sections";
export const changeToDarkTheme = createActionV2({
export const changeToDarkTheme = createAction({
name: ({ t }) => t("Dark"),
analyticsName: "Change to dark theme",
icon: <MoonIcon />,
@@ -14,7 +14,7 @@ export const changeToDarkTheme = createActionV2({
perform: ({ stores }) => stores.ui.setTheme(Theme.Dark),
});
export const changeToLightTheme = createActionV2({
export const changeToLightTheme = createAction({
name: ({ t }) => t("Light"),
analyticsName: "Change to light theme",
icon: <SunIcon />,
@@ -25,7 +25,7 @@ export const changeToLightTheme = createActionV2({
perform: ({ stores }) => stores.ui.setTheme(Theme.Light),
});
export const changeToSystemTheme = createActionV2({
export const changeToSystemTheme = createAction({
name: ({ t }) => t("System"),
analyticsName: "Change to system theme",
icon: <BrowserIcon />,
@@ -36,7 +36,7 @@ export const changeToSystemTheme = createActionV2({
perform: ({ stores }) => stores.ui.setTheme(Theme.System),
});
export const changeTheme = createActionV2WithChildren({
export const changeTheme = createActionWithChildren({
name: ({ t, isMenu }) => (isMenu ? t("Appearance") : t("Change theme")),
analyticsName: "Change theme",
placeholder: ({ t }) => t("Change theme to"),
+5 -5
View File
@@ -1,13 +1,13 @@
import copy from "copy-to-clipboard";
import Share from "~/models/Share";
import { createActionV2, createInternalLinkActionV2 } from "..";
import type Share from "~/models/Share";
import { createAction, createInternalLinkAction } from "..";
import { ArrowIcon, CopyIcon, TrashIcon } from "outline-icons";
import { ShareSection } from "../sections";
import env from "~/env";
import { toast } from "sonner";
export const copyShareUrlFactory = ({ share }: { share: Share }) =>
createActionV2({
createAction({
name: ({ t }) => t("Copy link"),
analyticsName: "Copy share link",
section: ShareSection,
@@ -22,7 +22,7 @@ export const copyShareUrlFactory = ({ share }: { share: Share }) =>
});
export const goToShareSourceFactory = ({ share }: { share: Share }) =>
createInternalLinkActionV2({
createInternalLinkAction({
name: ({ t }) =>
share.collectionId ? t("Go to collection") : t("Go to document"),
analyticsName: "Go to share source",
@@ -41,7 +41,7 @@ export const revokeShareFactory = ({
share: Share;
can: Record<string, boolean>;
}) =>
createActionV2({
createAction({
name: ({ t }) => t("Revoke link"),
analyticsName: "Revoke share",
section: ShareSection,
+10 -10
View File
@@ -1,22 +1,22 @@
import { ArrowIcon, PlusIcon } from "outline-icons";
import styled from "styled-components";
import { stringToColor } from "@shared/utils/color";
import RootStore from "~/stores/RootStore";
import type RootStore from "~/stores/RootStore";
import { LoginDialog } from "~/scenes/Login/components/LoginDialog";
import TeamNew from "~/scenes/TeamNew";
import TeamLogo from "~/components/TeamLogo";
import {
createActionV2,
createActionV2WithChildren,
createExternalLinkActionV2,
createAction,
createActionWithChildren,
createExternalLinkAction,
} from "~/actions";
import { ActionContext, ExternalLinkActionV2 } from "~/types";
import type { ActionContext, ExternalLinkAction } from "~/types";
import Desktop from "~/utils/Desktop";
import { TeamSection } from "../sections";
export const switchTeamsList = ({ stores }: { stores: RootStore }) =>
stores.auth.availableTeams?.map<ExternalLinkActionV2>((session) =>
createExternalLinkActionV2({
stores.auth.availableTeams?.map<ExternalLinkAction>((session) =>
createExternalLinkAction({
id: `switch-${session.id}`,
name: session.name,
analyticsName: "Switch workspace",
@@ -41,7 +41,7 @@ export const switchTeamsList = ({ stores }: { stores: RootStore }) =>
})
) ?? [];
export const switchTeam = createActionV2WithChildren({
export const switchTeam = createActionWithChildren({
name: ({ t }) => t("Switch workspace"),
placeholder: ({ t }) => t("Select a workspace"),
analyticsName: "Switch workspace",
@@ -52,7 +52,7 @@ export const switchTeam = createActionV2WithChildren({
children: switchTeamsList,
});
export const createTeam = createActionV2({
export const createTeam = createAction({
name: ({ t }) => `${t("New workspace")}`,
analyticsName: "New workspace",
keywords: "create change switch workspace organization team",
@@ -74,7 +74,7 @@ export const createTeam = createActionV2({
},
});
export const desktopLoginTeam = createActionV2({
export const desktopLoginTeam = createAction({
name: ({ t }) => t("Login to workspace"),
analyticsName: "Login to workspace",
keywords: "change switch workspace organization team",
+6 -5
View File
@@ -1,14 +1,14 @@
import { PlusIcon } from "outline-icons";
import { UserRole } from "@shared/types";
import type { UserRole } from "@shared/types";
import { UserRoleHelper } from "@shared/utils/UserRoleHelper";
import stores from "~/stores";
import User from "~/models/User";
import type User from "~/models/User";
import Invite from "~/scenes/Invite";
import {
UserChangeRoleDialog,
UserDeleteDialog,
} from "~/components/UserDialogs";
import { createAction, createActionV2 } from "~/actions";
import { createAction } from "~/actions";
import { UserSection } from "~/actions/sections";
export const inviteUser = createAction({
@@ -22,13 +22,14 @@ export const inviteUser = createAction({
perform: ({ t }) => {
stores.dialogs.openModal({
title: t("Invite to workspace"),
width: "500px",
content: <Invite onSubmit={stores.dialogs.closeAllModals} />,
});
},
});
export const updateUserRoleActionFactory = (user: User, role: UserRole) =>
createActionV2({
createAction({
name: ({ t }) =>
UserRoleHelper.isRoleHigher(role, user!.role)
? `${t("Promote to {{ role }}", {
@@ -63,7 +64,7 @@ export const updateUserRoleActionFactory = (user: User, role: UserRole) =>
});
export const deleteUserActionFactory = (userId: string) =>
createActionV2({
createAction({
name: ({ t }) => `${t("Delete user")}`,
analyticsName: "Delete user",
keywords: "leave",
+48 -201
View File
@@ -1,186 +1,33 @@
import { LocationDescriptor } from "history";
import flattenDeep from "lodash/flattenDeep";
import type { LocationDescriptor } from "history";
import { toast } from "sonner";
import { Optional } from "utility-types";
import {
Action,
import type { Optional } from "utility-types";
import { v4 as uuidv4 } from "uuid";
import type {
ActionContext,
ActionV2,
ActionV2Group,
ActionV2Separator as TActionV2Separator,
ActionV2Variant,
ActionV2WithChildren,
ExternalLinkActionV2,
InternalLinkActionV2,
MenuExternalLink,
MenuInternalLink,
Action,
ActionGroup,
ActionSeparator as TActionSeparator,
ActionVariant,
ActionWithChildren,
ExternalLinkAction,
InternalLinkAction,
MenuItem,
MenuItemButton,
MenuItemWithChildren,
} from "~/types";
import Analytics from "~/utils/Analytics";
import history from "~/utils/history";
import { Action as KbarAction } from "kbar";
import type { Action as KbarAction } from "kbar";
export function resolve<T>(value: any, context: ActionContext): T {
return typeof value === "function" ? value(context) : value;
}
export function createAction(definition: Optional<Action, "id">): Action {
return {
...definition,
perform: definition.perform
? (context) => {
// We must use the specific analytics name here as the action name is
// translated and potentially contains user strings.
if (definition.analyticsName) {
Analytics.track("perform_action", definition.analyticsName, {
context: context.isButton
? "button"
: context.isCommandBar
? "commandbar"
: "contextmenu",
});
}
return definition.perform?.(context);
}
: undefined,
id: definition.id ?? crypto.randomUUID(),
};
}
export function actionToMenuItem(
action: Action,
context: ActionContext
): MenuItemButton | MenuExternalLink | MenuInternalLink | MenuItemWithChildren {
const resolvedIcon = resolve<React.ReactElement<any>>(action.icon, context);
const resolvedChildren = resolve<Action[]>(action.children, context);
const visible = action.visible ? action.visible(context) : true;
const title = resolve<string>(action.name, context);
const icon =
resolvedIcon && action.iconInContextMenu !== false
? resolvedIcon
: undefined;
if (resolvedChildren) {
const items = resolvedChildren
.map((a) => actionToMenuItem(a, context))
.filter(Boolean)
.filter((a) => a.visible);
return {
type: "submenu",
title,
icon,
items,
visible: visible && items.length > 0,
};
}
if (action.to) {
return typeof action.to === "string"
? {
type: "route",
title,
icon,
visible,
to: action.to,
selected: action.selected?.(context),
}
: {
type: "link",
title,
icon,
visible,
href: action.to,
selected: action.selected?.(context),
};
}
return {
type: "button",
title,
icon,
visible,
dangerous: action.dangerous,
onClick: () => performAction(action, context),
selected: action.selected?.(context),
};
}
export function actionToKBar(
action: Action,
context: ActionContext
): KbarAction[] {
if (typeof action.visible === "function" && !action.visible(context)) {
return [];
}
const resolvedIcon = resolve<React.ReactElement>(action.icon, context);
const resolvedChildren = resolve<Action[]>(action.children, context);
const resolvedSection = resolve<string>(action.section, context);
const resolvedName = resolve<string>(action.name, context);
const resolvedPlaceholder = resolve<string>(action.placeholder, context);
const children = resolvedChildren
? flattenDeep(resolvedChildren.map((a) => actionToKBar(a, context))).filter(
(a) => !!a
)
: [];
const sectionPriority =
typeof action.section !== "string" && "priority" in action.section
? ((action.section.priority as number) ?? 0)
: 0;
return [
{
id: action.id,
name: resolvedName,
analyticsName: action.analyticsName,
section: resolvedSection,
placeholder: resolvedPlaceholder,
keywords: action.keywords ?? "",
shortcut: action.shortcut || [],
icon: resolvedIcon,
priority: (1 + (action.priority ?? 0)) * (1 + (sectionPriority ?? 0)),
perform:
action.perform || action.to
? () => performAction(action, context)
: undefined,
},
].concat(
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
children.map((child) => ({ ...child, parent: child.parent ?? action.id }))
);
}
export async function performAction(action: Action, context: ActionContext) {
const result = action.perform
? action.perform(context)
: action.to
? typeof action.to === "string"
? history.push(action.to)
: window.open(action.to.url, action.to.target)
: undefined;
if (result instanceof Promise) {
return result.catch((err: Error) => {
toast.error(err.message);
});
}
return result;
}
/** Actions V2 */
export const ActionV2Separator: TActionV2Separator = {
export const ActionSeparator: TActionSeparator = {
type: "action_separator",
};
export function createActionV2(
definition: Optional<Omit<ActionV2, "type" | "variant">, "id">
): ActionV2 {
export function createAction(
definition: Optional<Omit<Action, "type" | "variant">, "id">
): Action {
return {
...definition,
type: "action",
@@ -201,46 +48,46 @@ export function createActionV2(
return definition.perform(context);
}
: () => {},
id: definition.id ?? crypto.randomUUID(),
id: definition.id ?? uuidv4(),
};
}
export function createInternalLinkActionV2(
definition: Optional<Omit<InternalLinkActionV2, "type" | "variant">, "id">
): InternalLinkActionV2 {
export function createInternalLinkAction(
definition: Optional<Omit<InternalLinkAction, "type" | "variant">, "id">
): InternalLinkAction {
return {
...definition,
type: "action",
variant: "internal_link",
id: definition.id ?? crypto.randomUUID(),
id: definition.id ?? uuidv4(),
};
}
export function createExternalLinkActionV2(
definition: Optional<Omit<ExternalLinkActionV2, "type" | "variant">, "id">
): ExternalLinkActionV2 {
export function createExternalLinkAction(
definition: Optional<Omit<ExternalLinkAction, "type" | "variant">, "id">
): ExternalLinkAction {
return {
...definition,
type: "action",
variant: "external_link",
id: definition.id ?? crypto.randomUUID(),
id: definition.id ?? uuidv4(),
};
}
export function createActionV2WithChildren(
definition: Optional<Omit<ActionV2WithChildren, "type" | "variant">, "id">
): ActionV2WithChildren {
export function createActionWithChildren(
definition: Optional<Omit<ActionWithChildren, "type" | "variant">, "id">
): ActionWithChildren {
return {
...definition,
type: "action",
variant: "action_with_children",
id: definition.id ?? crypto.randomUUID(),
id: definition.id ?? uuidv4(),
};
}
export function createActionV2Group(
definition: Omit<ActionV2Group, "type">
): ActionV2Group {
export function createActionGroup(
definition: Omit<ActionGroup, "type">
): ActionGroup {
return {
...definition,
type: "action_group",
@@ -248,10 +95,10 @@ export function createActionV2Group(
}
export function createRootMenuAction(
actions: (ActionV2Variant | ActionV2Group | TActionV2Separator)[]
): ActionV2WithChildren {
actions: (ActionVariant | ActionGroup | TActionSeparator)[]
): ActionWithChildren {
return {
id: crypto.randomUUID(),
id: uuidv4(),
type: "action",
variant: "action_with_children",
name: "root_action",
@@ -260,8 +107,8 @@ export function createRootMenuAction(
};
}
export function actionV2ToMenuItem(
action: ActionV2Variant | ActionV2Group | TActionV2Separator,
export function actionToMenuItem(
action: ActionVariant | ActionGroup | TActionSeparator,
context: ActionContext
): MenuItem {
switch (action.type) {
@@ -285,7 +132,7 @@ export function actionV2ToMenuItem(
tooltip: resolve<React.ReactChild>(action.tooltip, context),
selected: resolve<boolean>(action.selected, context),
dangerous: action.dangerous,
onClick: () => performActionV2(action, context),
onClick: () => performAction(action, context),
};
case "internal_link": {
@@ -314,10 +161,10 @@ export function actionV2ToMenuItem(
case "action_with_children": {
const children = resolve<
(ActionV2Variant | ActionV2Group | TActionV2Separator)[]
(ActionVariant | ActionGroup | TActionSeparator)[]
>(action.children, context);
const subMenuItems = children.map((a) =>
actionV2ToMenuItem(a, context)
actionToMenuItem(a, context)
);
return {
type: "submenu",
@@ -336,7 +183,7 @@ export function actionV2ToMenuItem(
case "action_group": {
const groupItems = action.actions.map((a) =>
actionV2ToMenuItem(a, context)
actionToMenuItem(a, context)
);
return {
type: "group",
@@ -351,8 +198,8 @@ export function actionV2ToMenuItem(
}
}
export function actionV2ToKBar(
action: ActionV2Variant,
export function actionToKBar(
action: ActionVariant,
context: ActionContext
): KbarAction[] {
const visible = resolve<boolean>(action.visible, context);
@@ -384,18 +231,18 @@ export function actionV2ToKBar(
shortcut: action.shortcut,
icon,
priority,
perform: () => performActionV2(action, context),
perform: () => performAction(action, context),
},
];
}
case "action_with_children": {
const resolvedChildren = resolve<ActionV2Variant[]>(
const resolvedChildren = resolve<ActionVariant[]>(
action.children,
context
);
const children = resolvedChildren
.map((a) => actionV2ToKBar(a, context))
.map((a) => actionToKBar(a, context))
.flat()
.filter(Boolean);
@@ -421,8 +268,8 @@ export function actionV2ToKBar(
}
}
export async function performActionV2(
action: Exclude<ActionV2Variant, ActionV2WithChildren>,
export async function performAction(
action: Exclude<ActionVariant, ActionWithChildren>,
context: ActionContext
) {
const perform =
+3 -1
View File
@@ -1,4 +1,4 @@
import { ActionContext } from "~/types";
import type { ActionContext } from "~/types";
export const CollectionSection = ({ t }: ActionContext) => t("Collection");
@@ -38,6 +38,8 @@ export const NotificationSection = ({ t }: ActionContext) => t("Notification");
export const GroupSection = ({ t }: ActionContext) => t("Groups");
export const EmojiSecion = ({ t }: ActionContext) => t("Emoji");
export const UserSection = ({ t }: ActionContext) => t("People");
UserSection.priority = 0.5;
+13 -15
View File
@@ -1,10 +1,11 @@
/* oxlint-disable react/prop-types */
import * as React from "react";
import Tooltip, { Props as TooltipProps } from "~/components/Tooltip";
import { performAction, performActionV2, resolve } from "~/actions";
import type { Props as TooltipProps } from "~/components/Tooltip";
import Tooltip from "~/components/Tooltip";
import { performAction, resolve } from "~/actions";
import useIsMounted from "~/hooks/useIsMounted";
import { Action, ActionV2Variant, ActionV2WithChildren } from "~/types";
import useActionContext from "~/hooks/useActionContext";
import type { ActionVariant, ActionWithChildren } from "~/types";
export type Props = React.HTMLAttributes<HTMLButtonElement> & {
/** Show the button in a disabled state */
@@ -12,7 +13,7 @@ export type Props = React.HTMLAttributes<HTMLButtonElement> & {
/** Hide the button entirely if action is not applicable */
hideOnActionDisabled?: boolean;
/** Action to use on button */
action?: Action | Exclude<ActionV2Variant, ActionV2WithChildren>;
action?: Exclude<ActionVariant, ActionWithChildren>;
/** If tooltip props are provided the button will be wrapped in a tooltip */
tooltip?: Omit<TooltipProps, "children">;
};
@@ -21,7 +22,7 @@ export type Props = React.HTMLAttributes<HTMLButtonElement> & {
* Button that can be used to trigger an action definition.
*/
const ActionButton = React.forwardRef<HTMLButtonElement, Props>(
function _ActionButton(
function ActionButton_(
{ action, tooltip, hideOnActionDisabled, ...rest }: Props,
ref: React.Ref<HTMLButtonElement>
) {
@@ -30,20 +31,20 @@ const ActionButton = React.forwardRef<HTMLButtonElement, Props>(
});
const isMounted = useIsMounted();
const [executing, setExecuting] = React.useState(false);
const disabled = rest.disabled;
if (!actionContext || !action) {
return <button {...rest} ref={ref} />;
}
if (
action.visible &&
!resolve<boolean>(action.visible, actionContext) &&
hideOnActionDisabled
) {
const actionIsDisabled =
action.visible && !resolve<boolean>(action.visible, actionContext);
if (actionIsDisabled && hideOnActionDisabled) {
return null;
}
const disabled = rest.disabled || actionIsDisabled;
const label =
rest["aria-label"] ??
(typeof action.name === "function"
@@ -61,10 +62,7 @@ const ActionButton = React.forwardRef<HTMLButtonElement, Props>(
? (ev) => {
ev.preventDefault();
ev.stopPropagation();
const response =
"variant" in action
? performActionV2(action, actionContext)
: performAction(action, actionContext);
const response = performAction(action, actionContext);
if (response?.finally) {
setExecuting(true);
void response.finally(
+2 -1
View File
@@ -2,7 +2,8 @@
/* global ga */
import escape from "lodash/escape";
import * as React from "react";
import { IntegrationService, PublicEnv } from "@shared/types";
import type { PublicEnv } from "@shared/types";
import { IntegrationService } from "@shared/types";
import env from "~/env";
type Props = {
+1 -1
View File
@@ -32,7 +32,7 @@ import { PortalContext } from "./Portal";
import CommandBar from "./CommandBar";
const DocumentComments = lazyWithRetry(
() => import("~/scenes/Document/components/Comments")
() => import("~/scenes/Document/components/Comments/Comments")
);
const DocumentHistory = lazyWithRetry(
() => import("~/scenes/Document/components/History")
+15 -2
View File
@@ -2,6 +2,7 @@ import * as React from "react";
import styled from "styled-components";
import useBoolean from "~/hooks/useBoolean";
import Initials from "./Initials";
import Tooltip from "../Tooltip";
export enum AvatarSize {
Small = 16,
@@ -22,6 +23,7 @@ export interface IAvatar {
avatarUrl: string | null;
color?: string;
initial?: string;
name?: string;
id?: string;
}
@@ -42,6 +44,8 @@ type Props = {
className?: string;
/** Optional style */
style?: React.CSSProperties;
/** Whether to show a tooltip */
showTooltip?: boolean;
};
function Avatar(props: Props) {
@@ -50,12 +54,15 @@ function Avatar(props: Props) {
style,
variant = AvatarVariant.Round,
className,
showTooltip,
...rest
} = props;
const src = props.src || model?.avatarUrl;
const [error, handleError] = useBoolean(false);
const initial =
model?.initial || (model?.name ? model.name[0] : "").toUpperCase();
return (
const content = (
<Relative
style={style}
$variant={variant}
@@ -66,13 +73,19 @@ function Avatar(props: Props) {
<Image onError={handleError} src={src} {...rest} />
) : model ? (
<Initials color={model.color} {...rest}>
{model.initial}
{initial}
</Initials>
) : (
<Initials {...rest} />
)}
</Relative>
);
return showTooltip ? (
<Tooltip content={props.alt || model?.name || ""}>{content}</Tooltip>
) : (
content
);
}
Avatar.defaultProps = {
+1 -1
View File
@@ -3,7 +3,7 @@ import * as React from "react";
import { useTranslation } from "react-i18next";
import styled, { css } from "styled-components";
import { s } from "@shared/styles";
import User from "~/models/User";
import type User from "~/models/User";
import Tooltip from "~/components/Tooltip";
import Avatar, { AvatarSize } from "./Avatar";
+2 -1
View File
@@ -1,7 +1,7 @@
import { GroupIcon } from "outline-icons";
import { useTheme } from "styled-components";
import Squircle from "@shared/components/Squircle";
import Group from "~/models/Group";
import type Group from "~/models/Group";
import { AvatarSize } from "../Avatar/Avatar";
type Props = {
@@ -26,6 +26,7 @@ export function GroupAvatar({
return (
<Squircle color={color ?? theme.text} size={size} className={className}>
<GroupIcon
data-fixed-color
color={backgroundColor ?? theme.background}
size={size * 0.75}
/>
+2 -1
View File
@@ -1,4 +1,5 @@
import Avatar, { IAvatar, AvatarSize, AvatarVariant } from "./Avatar";
import type { IAvatar } from "./Avatar";
import Avatar, { AvatarSize, AvatarVariant } from "./Avatar";
import AvatarWithPresence from "./AvatarWithPresence";
import { GroupAvatar } from "./GroupAvatar";
+4 -3
View File
@@ -2,8 +2,8 @@ import { transparentize } from "polished";
import styled from "styled-components";
const Badge = styled.span<{ yellow?: boolean; primary?: boolean }>`
margin-left: 10px;
padding: 1px 5px 2px;
padding: 1.5px 5.5px;
margin: 0 2px;
background-color: ${({ yellow, primary, theme }) =>
yellow ? theme.yellow : primary ? theme.accent : "transparent"};
color: ${({ primary, yellow, theme }) =>
@@ -17,10 +17,11 @@ const Badge = styled.span<{ yellow?: boolean; primary?: boolean }>`
primary || yellow
? "transparent"
: transparentize(0.4, theme.textTertiary)};
border-radius: 10px;
border-radius: 8px;
font-size: 12px;
font-weight: 500;
user-select: none;
white-space: nowrap;
`;
export default Badge;
+7 -10
View File
@@ -6,17 +6,17 @@ import { s, ellipsis } from "@shared/styles";
import Flex from "~/components/Flex";
import BreadcrumbMenu from "~/menus/BreadcrumbMenu";
import { undraggableOnDesktop } from "~/styles";
import { InternalLinkActionV2, MenuInternalLink } from "~/types";
import { actionV2ToMenuItem } from "~/actions";
import type { InternalLinkAction, MenuInternalLink } from "~/types";
import { actionToMenuItem } from "~/actions";
import useActionContext from "~/hooks/useActionContext";
import { useComputed } from "~/hooks/useComputed";
type TopLevelAction =
| InternalLinkActionV2
| { type: "menu"; actions: InternalLinkActionV2[] };
| InternalLinkAction
| { type: "menu"; actions: InternalLinkAction[] };
type Props = React.PropsWithChildren<{
actions: InternalLinkActionV2[];
actions: InternalLinkAction[];
max?: number;
highlightFirstItem?: boolean;
}>;
@@ -46,7 +46,7 @@ function Breadcrumb(
const menuActions = topLevelActions.splice(
halfMax,
totalVisibleActions - max
) as InternalLinkActionV2[];
) as InternalLinkAction[];
topLevelActions.splice(halfMax, 0, {
type: "menu",
@@ -60,10 +60,7 @@ function Breadcrumb(
return <BreadcrumbMenu key="menu" actions={action.actions} />;
}
const item = actionV2ToMenuItem(
action,
actionContext
) as MenuInternalLink;
const item = actionToMenuItem(action, actionContext) as MenuInternalLink;
return (
<>
+7 -4
View File
@@ -1,12 +1,11 @@
import { LocationDescriptor } from "history";
import type { LocationDescriptor } from "history";
import { DisclosureIcon } from "outline-icons";
import { darken, lighten, transparentize } from "polished";
import * as React from "react";
import styled from "styled-components";
import { s } from "@shared/styles";
import ActionButton, {
Props as ActionButtonProps,
} from "~/components/ActionButton";
import type { Props as ActionButtonProps } from "~/components/ActionButton";
import ActionButton from "~/components/ActionButton";
import { undraggableOnDesktop } from "~/styles";
type RealProps = {
@@ -34,6 +33,7 @@ const RealButton = styled(ActionButton)<RealProps>`
cursor: var(--pointer);
user-select: none;
appearance: none !important;
transition: background 200ms ease-out;
${undraggableOnDesktop()}
&::-moz-focus-inner {
@@ -44,6 +44,7 @@ const RealButton = styled(ActionButton)<RealProps>`
&:hover:not(:disabled),
&[aria-expanded="true"] {
background: ${(props) => darken(0.05, props.theme.accent)};
transition: background 0s;
}
&:disabled {
@@ -78,6 +79,7 @@ const RealButton = styled(ActionButton)<RealProps>`
box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 2px, ${
props.theme.buttonNeutralBorder
} 0 0 0 1px inset;
transition: background 0s;
}
&:focus-visible {
@@ -103,6 +105,7 @@ const RealButton = styled(ActionButton)<RealProps>`
&:hover:not(:disabled),
&[aria-expanded="true"] {
background: ${darken(0.05, props.theme.danger)};
transition: background 0s;
}
&:disabled {
+2 -1
View File
@@ -1,6 +1,7 @@
import * as React from "react";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import { EditorStyleHelper } from "@shared/editor/styles/EditorStyleHelper";
type Props = {
children?: React.ReactNode;
@@ -26,7 +27,7 @@ const Content = styled.div<ContentProps>`
margin: 0 auto;
${breakpoint("desktopLarge")`
max-width: ${(props: ContentProps) => props.$maxWidth ?? "52em"};
max-width: ${(props: ContentProps) => props.$maxWidth ?? EditorStyleHelper.documentWidth};
`};
`;
+1 -1
View File
@@ -5,7 +5,7 @@ import uniq from "lodash/uniq";
import { observer } from "mobx-react";
import { useState, useMemo, useEffect, useCallback } from "react";
import { useTranslation } from "react-i18next";
import Document from "~/models/Document";
import type Document from "~/models/Document";
import { AvatarSize, AvatarWithPresence } from "~/components/Avatar";
import DocumentViews from "~/components/DocumentViews";
import Facepile from "~/components/Facepile";
+2 -1
View File
@@ -2,7 +2,8 @@ import { observer } from "mobx-react";
import { useCallback } from "react";
import { toast } from "sonner";
import useStores from "~/hooks/useStores";
import { CollectionForm, FormData } from "./CollectionForm";
import type { FormData } from "./CollectionForm";
import { CollectionForm } from "./CollectionForm";
type Props = {
collectionId: string;
+15 -8
View File
@@ -6,13 +6,13 @@ import { Trans, useTranslation } from "react-i18next";
import styled from "styled-components";
import Icon from "@shared/components/Icon";
import { randomElement } from "@shared/random";
import { CollectionPermission, TeamPreference } from "@shared/types";
import type { CollectionPermission } from "@shared/types";
import { TeamPreference } from "@shared/types";
import { IconLibrary } from "@shared/utils/IconLibrary";
import { colorPalette } from "@shared/utils/collections";
import { CollectionValidation } from "@shared/validations";
import Collection from "~/models/Collection";
import type Collection from "~/models/Collection";
import Button from "~/components/Button";
import Flex from "~/components/Flex";
import Input from "~/components/Input";
import { InputSelectPermission } from "~/components/InputSelectPermission";
import { createLazyComponent } from "~/components/LazyLoad";
@@ -22,6 +22,7 @@ import useBoolean from "~/hooks/useBoolean";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import useStores from "~/hooks/useStores";
import { EmptySelectValue } from "~/types";
import { HStack } from "../primitives/HStack";
const IconPicker = createLazyComponent(() => import("~/components/IconPicker"));
@@ -67,7 +68,13 @@ export const CollectionForm = observer(function CollectionForm_({
const [hasOpenedIconPicker, setHasOpenedIconPicker] = useBoolean(false);
const iconColor = useIconColor(collection);
const fallbackIcon = <Icon value="collection" color={iconColor} />;
const fallbackIcon = (
<Icon
value="collection"
initial={collection?.initial ?? "?"}
color={iconColor}
/>
);
const {
register,
@@ -134,7 +141,7 @@ export const CollectionForm = observer(function CollectionForm_({
Collections are used to group documents and choose permissions
</Trans>
</Text>
<Flex gap={8}>
<HStack>
<Input
type="text"
placeholder={t("Name")}
@@ -158,7 +165,7 @@ export const CollectionForm = observer(function CollectionForm_({
autoFocus
flex
/>
</Flex>
</HStack>
{/* Following controls are available in create flow, but moved elsewhere for edit */}
{!collection && (
@@ -216,7 +223,7 @@ export const CollectionForm = observer(function CollectionForm_({
/>
)}
<Flex justify="flex-end">
<HStack justify="flex-end">
<Button
type="submit"
disabled={formState.isSubmitting || !formState.isValid}
@@ -229,7 +236,7 @@ export const CollectionForm = observer(function CollectionForm_({
? `${t("Creating")}`
: t("Create")}
</Button>
</Flex>
</HStack>
</form>
);
});
+2 -1
View File
@@ -4,7 +4,8 @@ import { useCallback } from "react";
import { toast } from "sonner";
import useStores from "~/hooks/useStores";
import history from "~/utils/history";
import { CollectionForm, FormData } from "./CollectionForm";
import type { FormData } from "./CollectionForm";
import { CollectionForm } from "./CollectionForm";
type Props = {
onSubmit: () => void;
+5 -5
View File
@@ -1,11 +1,11 @@
import { ArchiveIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import Collection from "~/models/Collection";
import type Collection from "~/models/Collection";
import CollectionIcon from "~/components/Icons/CollectionIcon";
import { archivePath, collectionPath } from "~/utils/routeHelpers";
import Breadcrumb from "./Breadcrumb";
import { createInternalLinkActionV2 } from "~/actions";
import { createInternalLinkAction } from "~/actions";
import { ActiveCollectionSection } from "~/actions/sections";
type Props = {
@@ -17,18 +17,18 @@ export const CollectionBreadcrumb: React.FC<Props> = ({ collection }) => {
const actions = React.useMemo(
() => [
createInternalLinkActionV2({
createInternalLinkAction({
name: t("Archive"),
section: ActiveCollectionSection,
icon: <ArchiveIcon />,
visible: collection.isArchived,
to: archivePath(),
}),
createInternalLinkActionV2({
createInternalLinkAction({
name: collection.name,
section: ActiveCollectionSection,
icon: <CollectionIcon collection={collection} expanded />,
to: collectionPath(collection.path),
to: collectionPath(collection),
}),
],
[collection, t]
+1 -1
View File
@@ -2,7 +2,7 @@ import { observer } from "mobx-react";
import { useTranslation, Trans } from "react-i18next";
import { useHistory } from "react-router-dom";
import { toast } from "sonner";
import Collection from "~/models/Collection";
import type Collection from "~/models/Collection";
import ConfirmationDialog from "~/components/ConfirmationDialog";
import Text from "~/components/Text";
import useCurrentTeam from "~/hooks/useCurrentTeam";
+63
View File
@@ -0,0 +1,63 @@
import * as React from "react";
import styled from "styled-components";
import NudeButton from "./NudeButton";
import { hover, s } from "@shared/styles";
type Props = React.HTMLAttributes<HTMLButtonElement> & {
/** The current color value in hex format. If no color is passed a radial gradient will be shown */
color?: string;
/** Whether the button is currently active/selected */
active?: boolean;
/** The size of the button in pixels */
size?: number;
};
export const ColorButton = React.forwardRef(
(
{ color, active = false, size = 24, ...rest }: Props,
ref: React.Ref<HTMLButtonElement>
) => (
<ColorButtonInternal
$active={active}
$size={size}
{...rest}
style={{ "--color": color, ...rest.style } as React.CSSProperties}
ref={ref}
>
<Selected />
</ColorButtonInternal>
)
);
const Selected = styled.span`
width: 10px;
height: 5px;
border-left: 2px solid white;
border-bottom: 2px solid white;
transform: translateY(-25%) rotate(-45deg);
`;
const ColorButtonInternal = styled(NudeButton)<{
$active: boolean;
$size: number;
}>`
display: inline-flex;
justify-content: center;
align-items: center;
width: ${({ $size }) => $size}px;
height: ${({ $size }) => $size}px;
border-radius: 50%;
background: var(
--color,
linear-gradient(135deg, #ff5858 0%, #fbcc34 50%, #00c6ff 100%)
);
&: ${hover} {
outline: 2px solid ${s("menuBackground")} !important;
box-shadow: 0px 0px 3px 3px var(--color);
}
& ${Selected} {
display: ${({ $active }) => ($active ? "block" : "none")};
}
`;
+4 -3
View File
@@ -1,4 +1,4 @@
import { ActionImpl } from "kbar";
import type { ActionImpl } from "kbar";
import { ArrowIcon, BackIcon } from "outline-icons";
import * as React from "react";
import styled, { css, useTheme } from "styled-components";
@@ -7,6 +7,7 @@ import { normalizeKeyDisplay } from "@shared/utils/keyboard";
import Flex from "~/components/Flex";
import Key from "~/components/Key";
import Text from "~/components/Text";
import { HStack } from "../primitives/HStack";
type Props = {
action: ActionImpl;
@@ -35,7 +36,7 @@ function CommandBarItem(
return (
<Item active={active} ref={ref}>
<Content align="center" gap={8}>
<Content>
<Icon>
{action.icon ? (
// @ts-expect-error no icon on ActionImpl
@@ -100,7 +101,7 @@ const Ancestor = styled.span`
color: ${s("textSecondary")};
`;
const Content = styled(Flex)`
const Content = styled(HStack)`
${ellipsis()}
flex-shrink: 1;
`;
@@ -1,7 +1,7 @@
import { DocumentIcon } from "outline-icons";
import { useMemo } from "react";
import Icon from "@shared/components/Icon";
import { createAction } from "~/actions";
import { createInternalLinkAction } from "~/actions";
import { RecentSection } from "~/actions/sections";
import useStores from "~/hooks/useStores";
import { documentPath } from "~/utils/routeHelpers";
@@ -15,12 +15,16 @@ const useRecentDocumentActions = (count = 6) => {
.filter((document) => document.id !== ui.activeDocumentId)
.slice(0, count)
.map((item) =>
createAction({
createInternalLinkAction({
name: item.titleWithDefault,
analyticsName: "Recently viewed document",
section: RecentSection,
icon: item.icon ? (
<Icon value={item.icon} color={item.color ?? undefined} />
<Icon
value={item.icon}
initial={item.initial}
color={item.color ?? undefined}
/>
) : (
<DocumentIcon />
),
@@ -1,6 +1,6 @@
import { SettingsIcon } from "outline-icons";
import { useMemo } from "react";
import { createAction } from "~/actions";
import { createActionWithChildren, createInternalLinkAction } from "~/actions";
import { NavigationSection } from "~/actions/sections";
import useSettingsConfig from "~/hooks/useSettingsConfig";
@@ -10,20 +10,20 @@ const useSettingsAction = () => {
() =>
config.map((item) => {
const Icon = item.icon;
return {
return createInternalLinkAction({
id: item.path,
name: item.name,
icon: <Icon />,
section: NavigationSection,
to: item.path,
};
});
}),
[config]
);
const navigateToSettings = useMemo(
() =>
createAction({
createActionWithChildren({
id: "settings",
name: ({ t }) => t("Settings"),
section: NavigationSection,
@@ -1,14 +1,13 @@
import { NewDocumentIcon, ShapesIcon } from "outline-icons";
import { useEffect, useMemo } from "react";
import Icon from "@shared/components/Icon";
import { createAction } from "~/actions";
import { createActionWithChildren, createInternalLinkAction } from "~/actions";
import {
ActiveCollectionSection,
DocumentSection,
TeamSection,
} from "~/actions/sections";
import useStores from "~/hooks/useStores";
import history from "~/utils/history";
import { newDocumentPath } from "~/utils/routeHelpers";
const useTemplatesAction = () => {
@@ -21,14 +20,18 @@ const useTemplatesAction = () => {
const actions = useMemo(
() =>
documents.templatesAlphabetical.map((template) =>
createAction({
createInternalLinkAction({
name: template.titleWithDefault,
analyticsName: "New document",
section: template.isWorkspaceTemplate
? TeamSection
: ActiveCollectionSection,
icon: template.icon ? (
<Icon value={template.icon} color={template.color ?? undefined} />
<Icon
value={template.icon}
initial={template.initial}
color={template.color ?? undefined}
/>
) : (
<NewDocumentIcon />
),
@@ -47,15 +50,20 @@ const useTemplatesAction = () => {
template.isWorkspaceTemplate
);
},
perform: ({ activeCollectionId, sidebarContext }) =>
history.push(
newDocumentPath(template.collectionId ?? activeCollectionId, {
templateId: template.id,
}),
to: ({ activeCollectionId, sidebarContext }) => {
const [pathname, search] = newDocumentPath(
template.collectionId ?? activeCollectionId,
{
sidebarContext,
templateId: template.id,
}
),
).split("?");
return {
pathname,
search,
state: { sidebarContext },
};
},
})
),
[documents.templatesAlphabetical]
@@ -63,7 +71,7 @@ const useTemplatesAction = () => {
const newFromTemplate = useMemo(
() =>
createAction({
createActionWithChildren({
id: "templates",
name: ({ t }) => t("New from template"),
placeholder: ({ t }) => t("Choose a template"),
@@ -78,7 +86,7 @@ const useTemplatesAction = () => {
stores.policies.abilities(currentTeamId).createDocument
);
},
children: () => actions,
children: actions,
}),
[actions]
);
+1 -1
View File
@@ -1,7 +1,7 @@
import { observer } from "mobx-react";
import { useTranslation, Trans } from "react-i18next";
import { toast } from "sonner";
import Comment from "~/models/Comment";
import type Comment from "~/models/Comment";
import ConfirmationDialog from "~/components/ConfirmationDialog";
import Text from "~/components/Text";
import useStores from "~/hooks/useStores";
+2 -1
View File
@@ -1,7 +1,8 @@
import { observer } from "mobx-react";
import { Trans, useTranslation } from "react-i18next";
import { toast } from "sonner";
import { CollectionPermission, NavigationNode } from "@shared/types";
import type { NavigationNode } from "@shared/types";
import { CollectionPermission } from "@shared/types";
import type Collection from "~/models/Collection";
import ConfirmationDialog from "~/components/ConfirmationDialog";
import useStores from "~/hooks/useStores";
+1 -1
View File
@@ -31,7 +31,7 @@ export type RefHandle = {
* Defines a content editable component with the same interface as a native
* HTMLInputElement (or, as close as we can get).
*/
const ContentEditable = React.forwardRef(function _ContentEditable(
const ContentEditable = React.forwardRef(function ContentEditable_(
{
disabled,
onChange,
@@ -3,7 +3,8 @@ import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import CollectionIcon from "~/components/Icons/CollectionIcon";
import { InputSelect, Option } from "~/components/InputSelect";
import type { Option } from "~/components/InputSelect";
import { InputSelect } from "~/components/InputSelect";
import useStores from "~/hooks/useStores";
type DefaultCollectionInputSelectProps = {
+2
View File
@@ -32,6 +32,8 @@ function Dialogs() {
}}
title={modal.title}
style={modal.style}
width={modal.width}
height={modal.height}
>
{modal.content}
</Modal>
+14 -9
View File
@@ -5,14 +5,14 @@ import { useTranslation } from "react-i18next";
import styled from "styled-components";
import Icon from "@shared/components/Icon";
import type { NavigationNode } from "@shared/types";
import Document from "~/models/Document";
import type Document from "~/models/Document";
import Breadcrumb from "~/components/Breadcrumb";
import CollectionIcon from "~/components/Icons/CollectionIcon";
import { useLocationSidebarContext } from "~/hooks/useLocationSidebarContext";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
import { archivePath, settingsPath, trashPath } from "~/utils/routeHelpers";
import { createInternalLinkActionV2 } from "~/actions";
import { createInternalLinkAction } from "~/actions";
import { ActiveDocumentSection } from "~/actions/sections";
type Props = {
@@ -53,28 +53,28 @@ function DocumentBreadcrumb(
}
const outputActions = [
createInternalLinkActionV2({
createInternalLinkAction({
name: t("Trash"),
section: ActiveDocumentSection,
icon: <TrashIcon />,
visible: document.isDeleted,
to: trashPath(),
}),
createInternalLinkActionV2({
createInternalLinkAction({
name: t("Archive"),
section: ActiveDocumentSection,
icon: <ArchiveIcon />,
visible: document.isArchived,
to: archivePath(),
}),
createInternalLinkActionV2({
createInternalLinkAction({
name: t("Templates"),
section: ActiveDocumentSection,
icon: <ShapesIcon />,
visible: document.template,
to: settingsPath("templates"),
}),
createInternalLinkActionV2({
createInternalLinkAction({
name: collection?.name,
section: ActiveDocumentSection,
icon: collection ? (
@@ -88,7 +88,7 @@ function DocumentBreadcrumb(
}
: "",
}),
createInternalLinkActionV2({
createInternalLinkAction({
name: t("Deleted Collection"),
section: ActiveDocumentSection,
visible: document.isCollectionDeleted,
@@ -96,10 +96,15 @@ function DocumentBreadcrumb(
}),
...path.map((node) => {
const title = node.title || t("Untitled");
return createInternalLinkActionV2({
return createInternalLinkAction({
name: node.icon ? (
<>
<StyledIcon value={node.icon} color={node.color} /> {title}
<StyledIcon
value={node.icon}
color={node.color}
initial={node.title.charAt(0).toUpperCase()}
/>{" "}
{title}
</>
) : (
title
+4 -4
View File
@@ -13,8 +13,8 @@ import Squircle from "@shared/components/Squircle";
import { s, hover, ellipsis } from "@shared/styles";
import { IconType } from "@shared/types";
import { determineIconType } from "@shared/utils/icon";
import Document from "~/models/Document";
import Pin from "~/models/Pin";
import type Document from "~/models/Document";
import type Pin from "~/models/Pin";
import Flex from "~/components/Flex";
import NudeButton from "~/components/NudeButton";
import Time from "~/components/Time";
@@ -187,12 +187,12 @@ function DocumentCard(props: Props) {
const DocumentSquircle = ({
icon,
color,
initial,
color,
}: {
icon: string;
initial: string;
color?: string;
initial?: string;
}) => {
const theme = useTheme();
const iconType = determineIconType(icon)!;
+5 -4
View File
@@ -1,8 +1,9 @@
import { action, computed, observable } from "mobx";
import { createContext, useContext, useMemo, PropsWithChildren } from "react";
import { Heading } from "@shared/utils/ProsemirrorHelper";
import Document from "~/models/Document";
import { Editor } from "~/editor";
import type { PropsWithChildren } from "react";
import { createContext, useContext, useMemo } from "react";
import type { Heading } from "@shared/utils/ProsemirrorHelper";
import type Document from "~/models/Document";
import type { Editor } from "~/editor";
class DocumentContext {
/** The current document */
+8 -4
View File
@@ -3,8 +3,8 @@ import * as React from "react";
import { Trans, useTranslation } from "react-i18next";
import { toast } from "sonner";
import styled from "styled-components";
import { NavigationNode } from "@shared/types";
import Document from "~/models/Document";
import type { NavigationNode } from "@shared/types";
import type Document from "~/models/Document";
import { FlexContainer, Footer, StyledText } from "~/scenes/DocumentMove";
import Button from "~/components/Button";
import DocumentExplorer from "~/components/DocumentExplorer";
@@ -24,6 +24,7 @@ function DocumentCopy({ document, onSubmit }: Props) {
const { policies } = useStores();
const collectionTrees = useCollectionTrees();
const [publish, setPublish] = React.useState<boolean>(!!document.publishedAt);
const [copying, setCopying] = React.useState<boolean>(false);
const [recursive, setRecursive] = React.useState<boolean>(true);
const [selectedPath, selectPath] = React.useState<NavigationNode | null>(
null
@@ -51,6 +52,7 @@ function DocumentCopy({ document, onSubmit }: Props) {
}
try {
setCopying(true);
const result = await document.duplicate({
publish,
recursive,
@@ -65,6 +67,8 @@ function DocumentCopy({ document, onSubmit }: Props) {
onSubmit(result);
} catch (_err) {
toast.error(t("Couldnt copy the document, try again?"));
} finally {
setCopying(false);
}
};
@@ -114,8 +118,8 @@ function DocumentCopy({ document, onSubmit }: Props) {
t("Select a location to copy")
)}
</StyledText>
<Button disabled={!selectedPath} onClick={copy}>
{t("Copy")}
<Button disabled={!selectedPath || copying} onClick={copy}>
{copying ? `${t("Copying")}` : t("Copy")}
</Button>
</Footer>
</FlexContainer>
+5 -3
View File
@@ -16,8 +16,9 @@ import scrollIntoView from "scroll-into-view-if-needed";
import styled, { useTheme } from "styled-components";
import breakpoint from "styled-components-breakpoint";
import Icon from "@shared/components/Icon";
import { NavigationNode } from "@shared/types";
import type { NavigationNode } from "@shared/types";
import { isModKey } from "@shared/utils/keyboard";
import { ancestors, descendants, flattenTree } from "@shared/utils/tree";
import DocumentExplorerNode from "~/components/DocumentExplorerNode";
import DocumentExplorerSearchResult from "~/components/DocumentExplorerSearchResult";
import Flex from "~/components/Flex";
@@ -27,7 +28,6 @@ import InputSearch from "~/components/InputSearch";
import Text from "~/components/Text";
import useMobile from "~/hooks/useMobile";
import useStores from "~/hooks/useStores";
import { ancestors, descendants, flattenTree } from "~/utils/tree";
type Props = {
/** Action taken upon submission of selected item, could be publish, move etc. */
@@ -268,7 +268,9 @@ function DocumentExplorer({ onSubmit, onSelect, items, defaultValue }: Props) {
title = doc?.title ?? node.title;
if (icon) {
renderedIcon = <Icon value={icon} color={color} />;
renderedIcon = (
<Icon value={icon} initial={node.title} color={color} />
);
} else if (doc?.isStarred) {
renderedIcon = <StarredIcon color={theme.yellow} />;
} else {
+67 -62
View File
@@ -6,12 +6,13 @@ import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import styled, { css } from "styled-components";
import { DocumentIcon } from "outline-icons";
import styled, { css, useTheme } from "styled-components";
import breakpoint from "styled-components-breakpoint";
import EventBoundary from "@shared/components/EventBoundary";
import Icon from "@shared/components/Icon";
import { s, hover } from "@shared/styles";
import Document from "~/models/Document";
import type Document from "~/models/Document";
import Badge from "~/components/Badge";
import DocumentMeta from "~/components/DocumentMeta";
import Flex from "~/components/Flex";
@@ -54,6 +55,7 @@ function DocumentListItem(
) {
const { t } = useTranslation();
const user = useCurrentUser();
const theme = useTheme();
const { userMemberships, groupMemberships } = useStores();
const locationSidebarContext = useLocationSidebarContext();
const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean();
@@ -127,56 +129,58 @@ function DocumentListItem(
{...rest}
{...rovingTabIndex}
>
<Content>
<Heading dir={document.dir}>
{document.icon && (
<>
<Icon
value={document.icon}
color={document.color ?? undefined}
initial={document.initial}
/>
&nbsp;
</>
<Flex gap={4} auto>
<IconWrapper>
{document.icon ? (
<Icon
value={document.icon}
color={document.color ?? undefined}
initial={document.initial}
/>
) : (
<DocumentIcon
outline={document.isDraft}
color={theme.textSecondary}
/>
)}
<Title
text={document.titleWithDefault}
highlight={highlight}
dir={document.dir}
/>
{document.isBadgedNew && document.createdBy?.id !== user.id && (
<Badge yellow>{t("New")}</Badge>
)}
{document.isDraft && showDraft && (
<Tooltip content={t("Only visible to you")} placement="top">
<Badge>{t("Draft")}</Badge>
</Tooltip>
)}
{canStar && (
<StarPositioner>
<StarButton document={document} />
</StarPositioner>
)}
{document.isTemplate && showTemplate && (
<Badge primary>{t("Template")}</Badge>
)}
</Heading>
</IconWrapper>
<Content>
<Heading dir={document.dir}>
<Title
text={document.titleWithDefault}
highlight={highlight}
dir={document.dir}
/>
{document.isBadgedNew && document.createdBy?.id !== user.id && (
<Badge yellow>{t("New")}</Badge>
)}
{document.isDraft && showDraft && (
<Tooltip content={t("Only visible to you")} placement="top">
<Badge>{t("Draft")}</Badge>
</Tooltip>
)}
{canStar && <StarButton document={document} />}
{document.isTemplate && showTemplate && (
<Badge primary>{t("Template")}</Badge>
)}
</Heading>
{!queryIsInTitle && (
<ResultContext
text={context}
highlight={highlight ? SEARCH_RESULT_REGEX : undefined}
processResult={replaceResultMarks}
{!queryIsInTitle && (
<ResultContext
text={context}
highlight={highlight ? SEARCH_RESULT_REGEX : undefined}
processResult={replaceResultMarks}
/>
)}
<DocumentMeta
document={document}
showCollection={showCollection}
showPublished={showPublished}
showParentDocuments={showParentDocuments}
showLastViewed
/>
)}
<DocumentMeta
document={document}
showCollection={showCollection}
showPublished={showPublished}
showParentDocuments={showParentDocuments}
showLastViewed
/>
</Content>
</Content>
</Flex>
<Actions>
<DocumentMenu
document={document}
@@ -190,6 +194,14 @@ function DocumentListItem(
);
}
const IconWrapper = styled.div`
flex-shrink: 0;
display: flex;
align-items: flex-start;
justify-content: flex-start;
width: 24px;
`;
const Content = styled.div`
flex-grow: 1;
flex-shrink: 1;
@@ -204,12 +216,9 @@ const Actions = styled(EventBoundary)`
flex-grow: 0;
color: ${s("textSecondary")};
${NudeButton} {
&:
${hover},
&[aria-expanded= "true"] {
background: ${s("sidebarControlHoverBackground")};
}
${NudeButton}:${hover},
${NudeButton}[aria-expanded= "true"] {
background: ${s("sidebarControlHoverBackground")};
}
${breakpoint("tablet")`
@@ -285,18 +294,14 @@ const Heading = styled.span<{ rtl?: boolean }>`
justify-content: ${(props) => (props.rtl ? "flex-end" : "flex-start")};
align-items: center;
margin-top: 0;
margin-bottom: 0.25em;
margin-bottom: 0.1em;
white-space: nowrap;
color: ${s("text")};
font-family: ${s("fontFamily")};
font-weight: 500;
font-size: 20px;
font-size: 18px;
line-height: 1.2;
`;
const StarPositioner = styled(Flex)`
margin-left: 4px;
align-items: center;
gap: 4px;
`;
const Title = styled(Highlight)`
+4 -4
View File
@@ -1,12 +1,12 @@
import { LocationDescriptor } from "history";
import type { LocationDescriptor } from "history";
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import styled from "styled-components";
import { s, ellipsis } from "@shared/styles";
import Document from "~/models/Document";
import Revision from "~/models/Revision";
import type Document from "~/models/Document";
import type Revision from "~/models/Revision";
import DocumentBreadcrumb from "~/components/DocumentBreadcrumb";
import DocumentTasks from "~/components/DocumentTasks";
import Flex from "~/components/Flex";
@@ -182,7 +182,7 @@ const DocumentMeta: React.FC<Props> = ({
<span>
&nbsp;{t("in")}&nbsp;
<Strong>
<DocumentBreadcrumb document={document} onlyText />
<DocumentBreadcrumb document={document} maxDepth={1} onlyText />
</Strong>
</span>
)}
+6 -5
View File
@@ -1,12 +1,13 @@
import { TFunction } from "i18next";
import type { TFunction } from "i18next";
import { observer } from "mobx-react";
import { DoneIcon } from "outline-icons";
import { useTranslation } from "react-i18next";
import styled, { useTheme } from "styled-components";
import Document from "~/models/Document";
import type Document from "~/models/Document";
import CircularProgressBar from "~/components/CircularProgressBar";
import usePrevious from "~/hooks/usePrevious";
import { bounceIn } from "~/styles/animations";
import Flex from "./Flex";
type Props = {
document: Document;
@@ -41,7 +42,7 @@ function DocumentTasks({ document }: Props) {
const message = getMessage(t, total, completed);
return (
<>
<Flex align="center" style={{ padding: "0 1px" }} gap={2} shrink={false}>
{completed === total ? (
<Done
color={theme.accent}
@@ -51,8 +52,8 @@ function DocumentTasks({ document }: Props) {
) : (
<CircularProgressBar percentage={tasksPercentage} />
)}
&nbsp;{message}
</>
{message}
</Flex>
);
}
+2 -2
View File
@@ -4,8 +4,8 @@ import { observer } from "mobx-react";
import { useMemo, useCallback } from "react";
import { useTranslation } from "react-i18next";
import { dateLocale, dateToRelative } from "@shared/utils/date";
import Document from "~/models/Document";
import User from "~/models/User";
import type Document from "~/models/Document";
import type User from "~/models/User";
import { Avatar, AvatarSize } from "~/components/Avatar";
import ListItem from "~/components/List/Item";
import PaginatedList from "~/components/PaginatedList";
+1 -1
View File
@@ -1,6 +1,6 @@
import { observer } from "mobx-react";
import * as React from "react";
import Collection from "~/models/Collection";
import type Collection from "~/models/Collection";
type Props = {
enabled: boolean;
+56 -25
View File
@@ -1,7 +1,8 @@
import * as React from "react";
import { toast } from "sonner";
import styled from "styled-components";
import { s, truncateMultiline } from "@shared/styles";
import { s, ellipsis } from "@shared/styles";
import EventBoundary from "@shared/components/EventBoundary";
type Props = Omit<React.HTMLAttributes<HTMLInputElement>, "onSubmit"> & {
/** A callback when the title is submitted. */
@@ -32,6 +33,7 @@ function EditableTitle(
const [isEditing, setIsEditing] = React.useState(rest.isEditing || false);
const [originalValue, setOriginalValue] = React.useState(title);
const [value, setValue] = React.useState(title);
const [isSubmitting, setIsSubmitting] = React.useState(false);
React.useImperativeHandle(ref, () => ({
setIsEditing,
@@ -41,30 +43,54 @@ function EditableTitle(
setValue(title);
}, [title]);
const handleChange = React.useCallback((event) => {
setValue(event.target.value);
}, []);
const handleChange = React.useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value);
},
[]
);
const handleDoubleClick = React.useCallback((event) => {
event.preventDefault();
event.stopPropagation();
setIsEditing(true);
}, []);
const handleDoubleClick = React.useCallback(
(event: React.MouseEvent<HTMLSpanElement>) => {
if (event.altKey) {
return;
}
event.preventDefault();
event.stopPropagation();
setIsEditing(true);
},
[]
);
const stopPropagation = React.useCallback((event) => {
event.preventDefault();
event.stopPropagation();
}, []);
const stopPropagation = React.useCallback(
(event: React.MouseEvent<HTMLSpanElement>) => {
event.preventDefault();
event.stopPropagation();
},
[]
);
const handleFocus = React.useCallback((event) => {
event.target.select();
}, []);
const handleFocus = React.useCallback(
(event: React.FocusEvent<HTMLInputElement>) => {
event.target.select();
},
[]
);
const handleSave = React.useCallback(
async (ev) => {
async (
ev:
| React.FocusEvent<HTMLInputElement>
| React.KeyboardEvent<HTMLInputElement>
| React.FormEvent<HTMLFormElement>
) => {
ev.preventDefault();
ev.stopPropagation();
if (isSubmitting) {
return;
}
const trimmedValue = value.trim();
if (trimmedValue === originalValue || trimmedValue.length === 0) {
@@ -74,22 +100,26 @@ function EditableTitle(
return;
}
setIsSubmitting(true);
try {
await onSubmit(trimmedValue);
setOriginalValue(trimmedValue);
setIsEditing(false);
} catch (error) {
setValue(originalValue);
setValue(value);
setIsEditing(true);
toast.error(error.message);
throw error;
} finally {
setIsEditing(false);
setIsSubmitting(false);
}
},
[originalValue, value, onCancel, onSubmit]
[originalValue, value, onCancel, onSubmit, isSubmitting]
);
const handleKeyDown = React.useCallback(
async (ev) => {
async (ev: React.KeyboardEvent<HTMLInputElement>) => {
if (ev.nativeEvent.isComposing) {
return;
}
@@ -112,11 +142,12 @@ function EditableTitle(
return (
<>
{isEditing ? (
<form onSubmit={handleSave}>
<EventBoundary as="form" onSubmit={handleSave}>
<Input
dir="auto"
type="text"
lang=""
name="title"
value={value}
onClick={stopPropagation}
onKeyDown={handleKeyDown}
@@ -126,7 +157,7 @@ function EditableTitle(
autoFocus
{...rest}
/>
</form>
</EventBoundary>
) : (
<Text
onDoubleClick={canUpdate ? handleDoubleClick : undefined}
@@ -139,8 +170,8 @@ function EditableTitle(
);
}
const Text = styled.span`
${truncateMultiline(3)}
const Text = styled.div`
${ellipsis()}
`;
const Input = styled.input`
+82 -9
View File
@@ -3,8 +3,9 @@ import { observer } from "mobx-react";
import { DOMParser as ProsemirrorDOMParser } from "prosemirror-model";
import { TextSelection } from "prosemirror-state";
import * as React from "react";
import { toast } from "sonner";
import { mergeRefs } from "react-merge-refs";
import { Optional } from "utility-types";
import type { Optional } from "utility-types";
import insertFiles from "@shared/editor/commands/insertFiles";
import EditorContainer from "@shared/editor/components/Styles";
import { AttachmentPreset } from "@shared/types";
@@ -21,6 +22,7 @@ import useEmbeds from "~/hooks/useEmbeds";
import useStores from "~/hooks/useStores";
import { uploadFile, uploadFileFromUrl } from "~/utils/files";
import lazyWithRetry from "~/utils/lazyWithRetry";
import useShare from "@shared/hooks/useShare";
const LazyLoadedEditor = lazyWithRetry(() => import("~/editor"));
@@ -33,7 +35,6 @@ export type Props = Optional<
| "dictionary"
| "extensions"
> & {
shareId?: string | undefined;
embedsDisabled?: boolean;
onSynced?: () => Promise<void>;
onPublish?: (event: React.MouseEvent) => void;
@@ -41,20 +42,43 @@ export type Props = Optional<
};
function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
const { id, shareId, onChange, onCreateCommentMark, onDeleteCommentMark } =
props;
const {
id,
onChange,
onCreateCommentMark,
onDeleteCommentMark,
onFileUploadStart,
onFileUploadStop,
} = props;
const { comments } = useStores();
const { shareId } = useShare();
const dictionary = useDictionary();
const embeds = useEmbeds(!shareId);
const localRef = React.useRef<SharedEditor>();
const preferences = useCurrentUser({ rejectOnEmpty: false })?.preferences;
const previousCommentIds = React.useRef<string[]>();
// Upload progress tracking for delayed toast
const progressMap = React.useMemo(() => new Map<string, number>(), []);
const uploadState = React.useRef<{
toastId?: string | number;
timeoutId?: ReturnType<typeof setTimeout>;
progress: Map<string, number>;
}>({ progress: progressMap });
const handleUploadFile = React.useCallback(
async (file: File | string) => {
async (
file: File | string,
uploadOptions?: {
id?: string;
onProgress?: (fractionComplete: number) => void;
}
) => {
const options = {
id: uploadOptions?.id,
documentId: id,
preset: AttachmentPreset.DocumentAttachment,
onProgress: uploadOptions?.onProgress,
};
const result =
file instanceof File
@@ -67,6 +91,49 @@ function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
const { handleClickLink } = useEditorClickHandlers({ shareId });
// Show toast only after uploads have been running for 2 seconds
const handleFileUploadStart = React.useCallback(() => {
uploadState.current.timeoutId = setTimeout(() => {
uploadState.current.toastId = toast.loading(
dictionary.uploadingWithProgress(0)
);
}, 2000);
onFileUploadStart?.();
}, [onFileUploadStart, dictionary.uploadingWithProgress]);
const handleFileUploadProgress = React.useCallback(
(fileId: string, fractionComplete: number) => {
uploadState.current.progress.set(fileId, fractionComplete);
// Calculate average progress across all files
const progressValues = Array.from(uploadState.current.progress.values());
const avgProgress =
progressValues.reduce((a, b) => a + b, 0) / progressValues.length;
const percent = Math.round(avgProgress * 100);
// Update toast if visible
if (uploadState.current.toastId) {
toast.loading(dictionary.uploadingWithProgress(percent), {
id: uploadState.current.toastId,
});
}
},
[dictionary.uploadingWithProgress]
);
const handleFileUploadStop = React.useCallback(() => {
if (uploadState.current.timeoutId) {
clearTimeout(uploadState.current.timeoutId);
uploadState.current.timeoutId = undefined;
}
if (uploadState.current.toastId) {
toast.dismiss(uploadState.current.toastId);
uploadState.current.toastId = undefined;
}
uploadState.current.progress.clear();
onFileUploadStop?.();
}, [onFileUploadStop]);
const focusAtEnd = React.useCallback(() => {
localRef?.current?.focusAtEnd();
}, [localRef]);
@@ -113,16 +180,18 @@ function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
return insertFiles(view, event, pos, files, {
uploadFile: handleUploadFile,
onFileUploadStart: props.onFileUploadStart,
onFileUploadStop: props.onFileUploadStop,
onFileUploadStart: handleFileUploadStart,
onFileUploadStop: handleFileUploadStop,
onFileUploadProgress: handleFileUploadProgress,
dictionary,
isAttachment,
});
},
[
localRef,
props.onFileUploadStart,
props.onFileUploadStop,
handleFileUploadStart,
handleFileUploadStop,
handleFileUploadProgress,
dictionary,
handleUploadFile,
]
@@ -202,6 +271,7 @@ function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
style={props.style}
editorStyle={props.editorStyle}
commenting={!!props.onClickCommentMark}
lang={props.lang}
>
<div className="ProseMirror">
{paragraphs.map((paragraph, index) => (
@@ -222,6 +292,9 @@ function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
{...props}
onClickLink={handleClickLink}
onChange={handleChange}
onFileUploadStart={handleFileUploadStart}
onFileUploadStop={handleFileUploadStop}
onFileUploadProgress={handleFileUploadProgress}
placeholder={props.placeholder || ""}
defaultValue={props.defaultValue || ""}
/>
+233
View File
@@ -0,0 +1,233 @@
import * as React from "react";
import { useDropzone } from "react-dropzone";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import styled from "styled-components";
import { s } from "@shared/styles";
import { AttachmentPreset } from "@shared/types";
import { getDataTransferFiles } from "@shared/utils/files";
import ConfirmationDialog from "~/components/ConfirmationDialog";
import Input, { LabelText } from "~/components/Input";
import Text from "~/components/Text";
import useStores from "~/hooks/useStores";
import { uploadFile } from "~/utils/files";
import { compressImage } from "~/utils/compressImage";
import { generateEmojiNameFromFilename } from "~/utils/emoji";
import { AttachmentValidation, EmojiValidation } from "@shared/validations";
import { bytesToHumanReadable } from "@shared/utils/files";
import { VStack } from "./primitives/VStack";
type Props = {
onSubmit: () => void;
};
export function EmojiCreateDialog({ onSubmit }: Props) {
const { t } = useTranslation();
const { emojis } = useStores();
const [name, setName] = React.useState("");
const [file, setFile] = React.useState<File | null>(null);
const [isUploading, setIsUploading] = React.useState(false);
const handleFileSelection = React.useCallback(
(file: File) => {
const isValidType = AttachmentValidation.emojiContentTypes.includes(
file.type
);
if (!isValidType) {
toast.error(
t("File type not supported. Please use PNG, JPG, GIF, or WebP.")
);
return;
}
// Validate file size
if (file.size > AttachmentValidation.emojiMaxFileSize) {
toast.error(
t("File size too large. Maximum size is {{ size }}.", {
size: bytesToHumanReadable(AttachmentValidation.emojiMaxFileSize),
})
);
return;
}
setFile(file);
// Auto-populate name field if it's empty
setName((currentName) => {
if (!currentName.trim()) {
const generatedName = generateEmojiNameFromFilename(file.name);
return generatedName || currentName;
}
return currentName;
});
},
[t]
);
const onDrop = React.useCallback(
(acceptedFiles: File[]) => {
if (acceptedFiles.length > 0) {
handleFileSelection(acceptedFiles[0]);
}
},
[handleFileSelection]
);
// Handle paste events
React.useEffect(() => {
const handlePaste = (event: ClipboardEvent) => {
const files = getDataTransferFiles(event);
if (files.length > 0) {
event.preventDefault();
handleFileSelection(files[0]);
}
};
document.addEventListener("paste", handlePaste);
return () => document.removeEventListener("paste", handlePaste);
}, [handleFileSelection]);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDropAccepted: onDrop,
accept: AttachmentValidation.emojiContentTypes,
maxSize: AttachmentValidation.emojiMaxFileSize,
maxFiles: 1,
});
const handleSubmit = async () => {
if (!name.trim()) {
toast.error(t("Please enter a name for the emoji"));
return;
}
if (!file) {
toast.error(t("Please select an image file"));
return;
}
setIsUploading(true);
try {
// Skip compression for GIFs to preserve animation
const fileToUpload =
file.type === "image/gif"
? file
: await compressImage(file, {
maxHeight: 64,
maxWidth: 64,
});
const attachment = await uploadFile(fileToUpload, {
name: file.name,
preset: AttachmentPreset.Emoji,
});
await emojis.create({
name: name.trim(),
attachmentId: attachment.id,
});
toast.success(t("Emoji created successfully"));
onSubmit();
} finally {
setIsUploading(false);
}
};
const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { value } = event.target;
setName(value);
};
const isValidName = EmojiValidation.allowedNameCharacters.test(name);
const isValid = name.trim().length > 0 && file && isValidName;
return (
<ConfirmationDialog
onSubmit={handleSubmit}
disabled={!isValid || isUploading}
savingText={isUploading ? `${t("Uploading")}` : undefined}
submitText={t("Add emoji")}
>
<Text as="p" type="secondary">
{t(
"Square images with transparent backgrounds work best. If your image is too large, well try to resize it for you."
)}
</Text>
<LabelText as="label">{t("Upload an image")}</LabelText>
<DropZone {...getRootProps()}>
<input {...getInputProps()} />
<VStack>
{file ? (
<>
<PreviewImage src={URL.createObjectURL(file)} alt="Preview" />
<Text size="medium">{file.name}</Text>
<Text size="medium" type="secondary">
{t("Click or drag to replace")}
</Text>
</>
) : (
<>
<Text size="medium">
{isDragActive
? t("Drop the image here")
: t("Click, drop, or paste an image here")}
</Text>
<Text size="medium" type="secondary">
{t("PNG, JPG, GIF, or WebP up to {{ size }}", {
size: bytesToHumanReadable(
AttachmentValidation.emojiMaxFileSize
),
})}
</Text>
</>
)}
</VStack>
</DropZone>
<Input
label={t("Choose a name")}
value={name}
onChange={handleNameChange}
placeholder="my_custom_emoji"
autoFocus
required
error={
!isValidName
? t(
"name can only contain lowercase letters, numbers, and underscores."
)
: undefined
}
/>
{name.trim() && isValidName && (
<Text type="secondary" style={{ marginTop: "8px" }}>
{t("This emoji will be available as")} <code>:{name}:</code>
</Text>
)}
</ConfirmationDialog>
);
}
const DropZone = styled.div`
border: 2px dashed ${s("inputBorder")};
border-radius: 8px;
padding: 24px;
text-align: center;
cursor: var(--pointer);
transition: border-color 0.2s;
margin-bottom: 1em;
&:hover {
border-color: ${s("inputBorderFocused")};
}
`;
const PreviewImage = styled.img`
width: 64px;
height: 64px;
object-fit: contain;
border-radius: 4px;
`;
+6 -3
View File
@@ -1,7 +1,8 @@
import { observable } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import { withTranslation, Trans, WithTranslation } from "react-i18next";
import type { WithTranslation } from "react-i18next";
import { withTranslation, Trans } from "react-i18next";
import styled from "styled-components";
import { s } from "@shared/styles";
import { UrlHelper } from "@shared/utils/UrlHelper";
@@ -46,20 +47,22 @@ class ErrorBoundary extends React.Component<Props> {
componentDidCatch(error: Error) {
this.error = error;
this.trackError();
if (
this.props.reloadOnChunkMissing &&
error.message &&
error.message.match(/dynamically imported module/)
error.message.match(/dynamically imported module/) &&
!this.isRepeatedError
) {
// If the editor bundle fails to load then reload the entire window. This
// can happen if a deploy happens between the user loading the initial JS
// bundle and the async-loaded editor JS bundle as the hash will change.
// Don't reload if this is a repeated error to avoid infinite reload loops.
window.location.reload();
return;
}
this.trackError();
Logger.error("ErrorBoundary", error);
}
+2 -2
View File
@@ -12,8 +12,8 @@ import {
import { useTranslation } from "react-i18next";
import styled, { css } from "styled-components";
import { s } from "@shared/styles";
import Document from "~/models/Document";
import Event from "~/models/Event";
import type Document from "~/models/Document";
import type Event from "~/models/Event";
import Time from "~/components/Time";
import Logger from "~/utils/Logger";
import Text from "./Text";
+78 -33
View File
@@ -4,7 +4,7 @@ import { Trans, useTranslation } from "react-i18next";
import { toast } from "sonner";
import styled from "styled-components";
import { FileOperationFormat, NotificationEventType } from "@shared/types";
import Collection from "~/models/Collection";
import type Collection from "~/models/Collection";
import ConfirmationDialog from "~/components/ConfirmationDialog";
import Flex from "~/components/Flex";
import Text from "~/components/Text";
@@ -13,6 +13,8 @@ import useCurrentUser from "~/hooks/useCurrentUser";
import useStores from "~/hooks/useStores";
import history from "~/utils/history";
import { settingsPath } from "~/utils/routeHelpers";
import usePolicy from "~/hooks/usePolicy";
import useCurrentTeam from "~/hooks/useCurrentTeam";
type Props = {
collection?: Collection;
@@ -25,7 +27,10 @@ function ExportDialog({ collection, onSubmit }: Props) {
);
const [includeAttachments, setIncludeAttachments] =
React.useState<boolean>(true);
const [includePrivate, setIncludePrivate] = React.useState<boolean>(true);
const user = useCurrentUser();
const team = useCurrentTeam();
const can = usePolicy(team);
const { collections } = useStores();
const { t } = useTranslation();
const appName = env.APP_NAME;
@@ -44,23 +49,36 @@ function ExportDialog({ collection, onSubmit }: Props) {
[]
);
const handleIncludePrivateChange = React.useCallback(
(ev: React.ChangeEvent<HTMLInputElement>) => {
setIncludePrivate(ev.target.checked);
},
[]
);
const handleSubmit = async () => {
const options = can.createExport
? {
description: t(`Your file will be available in {{ location }} soon`, {
location: `"${t("Settings")} > ${t("Export")}"`,
}),
action: {
label: t("View"),
onClick: () => {
history.push(settingsPath("export"));
},
},
}
: {
description: t(`A link to your file will be sent through email soon`),
};
if (collection) {
await collection.export(format, includeAttachments);
toast.success(t("Export started"), {
description: t(`Your file will be available in {{ location }} soon`, {
location: `"${t("Settings")} > ${t("Export")}"`,
}),
action: {
label: t("View"),
onClick: () => {
history.push(settingsPath("export"));
},
},
});
toast.success(t("Export started"), options);
} else {
await collections.export(format, includeAttachments);
toast.success(t("Export started"));
await collections.export({ format, includeAttachments, includePrivate });
toast.success(t("Export started"), options);
}
onSubmit();
};
@@ -123,37 +141,64 @@ function ExportDialog({ collection, onSubmit }: Props) {
<Text as="p" size="small" weight="bold">
{item.title}
</Text>
<Text size="small">{item.description}</Text>
<Text size="small" type="secondary">
{item.description}
</Text>
</div>
</Option>
))}
</Flex>
<hr />
<Option>
<input
type="checkbox"
name="includeAttachments"
checked={includeAttachments}
onChange={handleIncludeAttachmentsChange}
/>
<div>
<Text as="p" size="small" weight="bold">
{t("Include attachments")}
</Text>
<Text size="small">
{t("Including uploaded images and files in the exported data")}.
</Text>{" "}
</div>
</Option>
<HR />
<Flex gap={12} column>
<Option>
<input
type="checkbox"
name="includeAttachments"
checked={includeAttachments}
onChange={handleIncludeAttachmentsChange}
/>
<div>
<Text as="p" size="small" weight="bold">
{t("Include attachments")}
</Text>
<Text size="small" type="secondary">
{t("Including uploaded images and files in the exported data")}.
</Text>{" "}
</div>
</Option>
{!collection && (
<Option>
<input
type="checkbox"
name="includePrivate"
checked={includePrivate}
onChange={handleIncludePrivateChange}
/>
<div>
<Text as="p" size="small" weight="bold">
{t("Include private collections")}
</Text>
</div>
</Option>
)}
</Flex>
</ConfirmationDialog>
);
}
const HR = styled.hr`
margin: 16px 0;
`;
const Option = styled.label`
display: flex;
align-items: center;
align-items: start;
gap: 16px;
input {
margin-top: 4px;
}
p {
margin: 0;
}
+15 -2
View File
@@ -2,9 +2,11 @@ import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import User from "~/models/User";
import type User from "~/models/User";
import { Avatar, AvatarSize } from "~/components/Avatar";
import Flex from "~/components/Flex";
import { s } from "@shared/styles";
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
type Props = {
/** The users to display */
@@ -21,6 +23,8 @@ type Props = {
model: User;
}
>;
/** Whether to show tooltips on hover, defaults to true */
showTooltip?: boolean;
};
function Facepile({
@@ -29,6 +33,7 @@ function Facepile({
size = AvatarSize.Large,
limit = 8,
renderAvatar = Avatar,
showTooltip = true,
...rest
}: Props) {
const { t } = useTranslation();
@@ -51,6 +56,7 @@ function Facepile({
<Component
key={model.id}
{...{
showTooltip,
model,
size,
style: {
@@ -63,7 +69,9 @@ function Facepile({
/>
);
})}
<FacepileClip size={size} />
<VisuallyHidden>
<FacepileClip size={size} />
</VisuallyHidden>
</Avatars>
);
}
@@ -101,6 +109,11 @@ const Avatars = styled(Flex)`
align-items: center;
flex-direction: row-reverse;
cursor: var(--pointer);
*:hover {
clip-path: none !important;
box-shadow: 0 0 0 2px ${s("background")};
}
`;
export default observer(Facepile);
+50 -25
View File
@@ -7,9 +7,11 @@ import type { FetchPageParams } from "~/stores/base/Store";
import Button, { Inner } from "~/components/Button";
import Text from "~/components/Text";
import Input, { NativeInput, Outline } from "./Input";
import PaginatedList, { PaginatedItem } from "./PaginatedList";
import type { PaginatedItem } from "./PaginatedList";
import PaginatedList from "./PaginatedList";
import { MenuProvider } from "./primitives/Menu/MenuContext";
import { Menu, MenuContent, MenuTrigger, MenuButton } from "./primitives/Menu";
import { MenuIconWrapper } from "./primitives/components/Menu";
interface TFilterOption extends PaginatedItem {
key: string;
@@ -55,7 +57,11 @@ const FilterOptions = ({
(option) => (
<MenuButton
key={option.key}
icon={option.icon}
icon={
option.icon ? (
<MenuIconWrapper aria-hidden>{option.icon}</MenuIconWrapper>
) : undefined
}
label={option.label}
onClick={() => {
onSelect(option.key);
@@ -77,30 +83,45 @@ const FilterOptions = ({
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;
})
const filtered = query
? options.filter((option) =>
deburr(option.label).toLowerCase().includes(normalizedQuery)
)
: options;
}, [options, query]);
return filtered.sort((a, b) => {
const aSelected = selectedKeys.includes(a.key);
const bSelected = selectedKeys.includes(b.key);
// Selected items come first
if (aSelected && !bSelected) {
return -1;
}
if (!aSelected && bSelected) {
return 1;
}
// If both have the same selection state and there's a query,
// sort options starting with query first
if (query) {
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, query, selectedKeys]);
const handleKeyDown = React.useCallback(
(ev: React.KeyboardEvent) => {
@@ -108,6 +129,10 @@ const FilterOptions = ({
return;
}
// Stop all keyboard events from propagating to prevent Radix UI menu
// from handling them and potentially moving focus
ev.stopPropagation();
switch (ev.key) {
case "Escape":
setOpen(false);
+32 -4
View File
@@ -13,6 +13,7 @@ import useStores from "~/hooks/useStores";
import LoadingIndicator from "../LoadingIndicator";
import { CARD_MARGIN } from "./Components";
import HoverPreviewDocument from "./HoverPreviewDocument";
import HoverPreviewGroup from "./HoverPreviewGroup";
import HoverPreviewIssue from "./HoverPreviewIssue";
import HoverPreviewLink from "./HoverPreviewLink";
import HoverPreviewMention from "./HoverPreviewMention";
@@ -116,12 +117,31 @@ const HoverPreviewDesktop = observer(
<Position top={cardTop} left={cardLeft} aria-hidden>
{isVisible ? (
<Animate
initial={{ opacity: 0, y: -20, pointerEvents: "none" }}
initial={{
opacity: 0,
y: -20,
filter: "blur(5px)",
pointerEvents: "none",
}}
animate={{
opacity: 1,
y: 0,
filter: "blur(0px)",
transitionEnd: { pointerEvents: "auto" },
}}
transition={{
y: {
type: "spring",
stiffness: 400,
damping: 25,
},
opacity: {
duration: 0.2,
},
filter: {
duration: 0.2,
},
}}
>
{data.type === UnfurlResourceType.Mention ? (
<HoverPreviewMention
@@ -132,6 +152,14 @@ const HoverPreviewDesktop = observer(
lastActive={data.lastActive}
email={data.email}
/>
) : data.type === UnfurlResourceType.Group ? (
<HoverPreviewGroup
ref={cardRef}
name={data.name}
description={data.description}
memberCount={data.memberCount}
users={data.users}
/>
) : data.type === UnfurlResourceType.Document ? (
<HoverPreviewDocument
ref={cardRef}
@@ -295,10 +323,10 @@ const Pointer = styled.div<{ top: number; left: number; direction: Direction }>`
&:before {
border: 8px solid transparent;
${({ direction, theme }) =>
${({ direction }) =>
direction === Direction.UP
? `border-bottom-color: ${theme.menuBorder || "rgba(0, 0, 0, 0.1)"}`
: `border-top-color: ${theme.menuBorder || "rgba(0, 0, 0, 0.1)"}`};
? `border-bottom-color: rgba(0, 0, 0, 0.1)`
: `border-top-color: rgba(0, 0, 0, 0.1)`};
${({ direction }) =>
direction === Direction.UP ? "right: -1px" : "left: -1px"};
}
@@ -1,6 +1,6 @@
import * as React from "react";
import { richExtensions } from "@shared/editor/nodes";
import { UnfurlResourceType, UnfurlResponse } from "@shared/types";
import type { UnfurlResourceType, UnfurlResponse } from "@shared/types";
import Editor from "~/components/Editor";
import Flex from "~/components/Flex";
import ErrorBoundary from "../ErrorBoundary";
@@ -15,7 +15,7 @@ import {
type Props = Omit<UnfurlResponse[UnfurlResourceType.Document], "type">;
const HoverPreviewDocument = React.forwardRef(function _HoverPreviewDocument(
const HoverPreviewDocument = React.forwardRef(function HoverPreviewDocument_(
{ url, id, title, summary, lastActivityByViewer }: Props,
ref: React.Ref<HTMLDivElement>
) {
@@ -0,0 +1,70 @@
import * as React from "react";
import { useTranslation } from "react-i18next";
import type { UnfurlResourceType, UnfurlResponse } from "@shared/types";
import { MAX_AVATAR_DISPLAY } from "@shared/constants";
import type User from "~/models/User";
import Facepile from "~/components/Facepile";
import Flex from "~/components/Flex";
import {
Preview,
Title,
Info,
Card,
CardContent,
Description,
} from "./Components";
import ErrorBoundary from "../ErrorBoundary";
type Props = Omit<UnfurlResponse[UnfurlResourceType.Group], "type">;
const HoverPreviewGroup = React.forwardRef(function HoverPreviewGroup_(
{ name, description, memberCount, users }: Props,
ref: React.Ref<HTMLDivElement>
) {
const { t } = useTranslation();
return (
<Preview as="div">
<Card fadeOut={false} ref={ref}>
<CardContent>
<ErrorBoundary showTitle={false} reloadOnChunkMissing={false}>
<Flex column gap={2} align="start">
<Flex
justify="space-between"
gap={4}
style={{ width: "100%" }}
auto
>
<Flex column align="start">
<Title>{name}</Title>
<Info>
{t("{{ count }} members", { count: memberCount })}
</Info>
</Flex>
{users.length > 0 && (
<Facepile
users={users.map(
(member) =>
({
id: member.id,
name: member.name,
avatarUrl: member.avatarUrl,
color: member.color,
initial: member.name ? member.name[0] : "?",
}) as User
)}
overflow={Math.max(0, memberCount - users.length)}
limit={MAX_AVATAR_DISPLAY}
/>
)}
</Flex>
{description && <Description>{description}</Description>}
</Flex>
</ErrorBoundary>
</CardContent>
</Card>
</Preview>
);
});
export default HoverPreviewGroup;
@@ -3,11 +3,8 @@ import { Trans } from "react-i18next";
import styled from "styled-components";
import { Backticks } from "@shared/components/Backticks";
import { IssueStatusIcon } from "@shared/components/IssueStatusIcon";
import {
IntegrationService,
UnfurlResourceType,
UnfurlResponse,
} from "@shared/types";
import type { UnfurlResourceType, UnfurlResponse } from "@shared/types";
import { IntegrationService } from "@shared/types";
import { Avatar } from "~/components/Avatar";
import Flex from "~/components/Flex";
import Text from "../Text";
@@ -24,7 +21,7 @@ import {
type Props = Omit<UnfurlResponse[UnfurlResourceType.Issue], "type">;
const HoverPreviewIssue = React.forwardRef(function _HoverPreviewIssue(
const HoverPreviewIssue = React.forwardRef(function HoverPreviewIssue_(
{ url, id, title, description, author, labels, state, createdAt }: Props,
ref: React.Ref<HTMLDivElement>
) {
@@ -20,7 +20,7 @@ type Props = {
description: string;
};
const HoverPreviewLink = React.forwardRef(function _HoverPreviewLink(
const HoverPreviewLink = React.forwardRef(function HoverPreviewLink_(
{ url, thumbnailUrl, title, description }: Props,
ref: React.Ref<HTMLDivElement>
) {
@@ -1,13 +1,13 @@
import * as React from "react";
import { UnfurlResourceType, UnfurlResponse } from "@shared/types";
import type { UnfurlResourceType, UnfurlResponse } from "@shared/types";
import { Avatar, AvatarSize } from "~/components/Avatar";
import Flex from "~/components/Flex";
import { Preview, Title, Info, Card, CardContent } from "./Components";
type Props = Omit<UnfurlResponse[UnfurlResourceType.Mention], "type">;
const HoverPreviewMention = React.forwardRef(function _HoverPreviewMention(
{ avatarUrl, name, lastActive, color, email }: Props,
const HoverPreviewMention = React.forwardRef(function HoverPreviewMention_(
{ avatarUrl, name, lastActive, color }: Props,
ref: React.Ref<HTMLDivElement>
) {
return (
@@ -25,7 +25,6 @@ const HoverPreviewMention = React.forwardRef(function _HoverPreviewMention(
/>
<Flex column gap={2} justify="center">
<Title>{name}</Title>
{email && <Info>{email}</Info>}
<Info>{lastActive}</Info>
</Flex>
</Flex>
@@ -3,7 +3,7 @@ import { Trans } from "react-i18next";
import styled from "styled-components";
import { Backticks } from "@shared/components/Backticks";
import { PullRequestIcon } from "@shared/components/PullRequestIcon";
import { UnfurlResourceType, UnfurlResponse } from "@shared/types";
import type { UnfurlResourceType, UnfurlResponse } from "@shared/types";
import { Avatar } from "~/components/Avatar";
import Flex from "~/components/Flex";
import Text from "../Text";
@@ -20,7 +20,7 @@ import {
type Props = Omit<UnfurlResponse[UnfurlResourceType.PR], "type">;
const HoverPreviewPullRequest = React.forwardRef(
function _HoverPreviewPullRequest(
function HoverPreviewPullRequest_(
{ url, title, id, description, author, state, createdAt }: Props,
ref: React.Ref<HTMLDivElement>
) {
@@ -1,17 +1,11 @@
import { BackIcon } from "outline-icons";
import * as React from "react";
import debounce from "lodash/debounce";
import styled from "styled-components";
import { breakpoints, s, hover } from "@shared/styles";
import { s } from "@shared/styles";
import { colorPalette } from "@shared/utils/collections";
import { validateColorHex } from "@shared/utils/color";
import Flex from "~/components/Flex";
import NudeButton from "~/components/NudeButton";
import Text from "~/components/Text";
enum Panel {
Builtin,
Hex,
}
import { SwatchButton } from "~/components/SwatchButton";
import { ColorButton } from "~/components/ColorButton";
type Props = {
width: number;
@@ -19,128 +13,77 @@ type Props = {
onSelect: (color: string) => void;
};
const ColorPicker = ({ width, activeColor, onSelect }: Props) => {
const [localValue, setLocalValue] = React.useState(activeColor);
const ColorPicker = ({ activeColor, onSelect }: Props) => {
const [selectedColor, setSelectedColor] = React.useState(activeColor);
const isBuiltInColor = colorPalette.includes(selectedColor);
const color = isBuiltInColor ? undefined : selectedColor;
const [panel, setPanel] = React.useState(
colorPalette.includes(activeColor) ? Panel.Builtin : Panel.Hex
const debouncedOnSelect = React.useMemo(
() =>
debounce((color: string) => {
onSelect(color);
}, 250),
[onSelect]
);
const handleSwitcherClick = React.useCallback(() => {
setPanel(panel === Panel.Builtin ? Panel.Hex : Panel.Builtin);
}, [panel, setPanel]);
const isLargeMobile = width > breakpoints.mobileLarge + 12; // 12px for the Container padding
React.useEffect(
() => () => {
debouncedOnSelect.cancel();
},
[debouncedOnSelect]
);
React.useEffect(() => {
setLocalValue(activeColor);
setPanel(colorPalette.includes(activeColor) ? Panel.Builtin : Panel.Hex);
setSelectedColor(activeColor);
}, [activeColor]);
return isLargeMobile ? (
<Container justify="space-between">
<LargeMobileBuiltinColors activeColor={activeColor} onClick={onSelect} />
<LargeMobileCustomColor
value={localValue}
setLocalValue={setLocalValue}
onValidHex={onSelect}
const handleSelect = (color: string) => {
setSelectedColor(color);
debouncedOnSelect(color);
};
return (
<BuiltinColors activeColor={selectedColor} onClick={handleSelect}>
<Divider />
<SwatchButton
color={color}
active={!isBuiltInColor}
onChange={handleSelect}
pickerInModal
/>
</Container>
) : (
<Container gap={12}>
<PanelSwitcher align="center">
<SwitcherButton panel={panel} onClick={handleSwitcherClick}>
{panel === Panel.Builtin ? "#" : <BackIcon />}
</SwitcherButton>
</PanelSwitcher>
{panel === Panel.Builtin ? (
<BuiltinColors activeColor={activeColor} onClick={onSelect} />
) : (
<CustomColor
value={localValue}
setLocalValue={setLocalValue}
onValidHex={onSelect}
/>
)}
</Container>
</BuiltinColors>
);
};
const Divider = styled.div`
width: 1px;
height: 24px;
background-color: ${s("inputBorder")};
`;
const BuiltinColors = ({
activeColor,
onClick,
className,
children,
}: {
activeColor: string;
onClick: (color: string) => void;
className?: string;
children?: React.ReactNode;
}) => (
<Flex className={className} justify="space-between" align="center" auto>
<Container className={className} justify="space-between" align="center" auto>
{colorPalette.map((color) => (
<ColorButton
key={color}
$color={color}
$active={color === activeColor}
color={color}
active={color === activeColor}
onClick={() => onClick(color)}
>
<Selected />
</ColorButton>
))}
</Flex>
);
const CustomColor = ({
value,
setLocalValue,
onValidHex,
className,
}: {
value: string;
setLocalValue: (value: string) => void;
onValidHex: (color: string) => void;
className?: string;
}) => {
const hasHexChars = React.useCallback(
(color: string) => /(^#[0-9A-F]{1,6}$)/i.test(color),
[]
);
const handleInputChange = React.useCallback(
(ev: React.ChangeEvent<HTMLInputElement>) => {
const val = ev.target.value;
if (val === "" || val === "#") {
setLocalValue("#");
return;
}
const uppercasedVal = val.toUpperCase();
if (hasHexChars(uppercasedVal)) {
setLocalValue(uppercasedVal);
}
if (validateColorHex(uppercasedVal)) {
onValidHex(uppercasedVal);
}
},
[setLocalValue, hasHexChars, onValidHex]
);
return (
<Flex className={className} align="center" gap={8}>
<Text type="tertiary" size="small">
HEX
</Text>
<CustomColorInput
maxLength={7}
value={value}
onChange={handleInputChange}
autoFocus
/>
</Flex>
);
};
))}
{children}
</Container>
);
const Container = styled(Flex)`
height: 48px;
@@ -148,71 +91,4 @@ const Container = styled(Flex)`
border-bottom: 1px solid ${s("inputBorder")};
`;
const Selected = styled.span`
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 }>`
display: inline-flex;
justify-content: center;
align-items: center;
width: 24px;
height: 24px;
border-radius: 50%;
background-color: ${({ $color }) => $color};
&: ${hover} {
outline: 2px solid ${s("menuBackground")} !important;
box-shadow: ${({ $color }) => `0px 0px 3px 3px ${$color}`};
}
& ${Selected} {
display: ${({ $active }) => ($active ? "block" : "none")};
}
`;
const PanelSwitcher = styled(Flex)`
width: 40px;
border-right: 1px solid ${s("inputBorder")};
`;
const SwitcherButton = styled(NudeButton)<{ panel: Panel }>`
display: inline-flex;
justify-content: center;
align-items: center;
font-size: 14px;
border: 1px solid ${s("inputBorder")};
transition: all 100ms ease-in-out;
&: ${hover} {
border-color: ${s("inputBorderFocused")};
}
`;
const LargeMobileBuiltinColors = styled(BuiltinColors)`
max-width: 400px;
padding-right: 8px;
`;
const LargeMobileCustomColor = styled(CustomColor)`
padding-left: 8px;
border-left: 1px solid ${s("inputBorder")};
width: 120px;
`;
const CustomColorInput = styled.input.attrs(() => ({
type: "text",
autocomplete: "off",
}))`
font-size: 14px;
color: ${s("textSecondary")};
background: transparent;
border: 0;
outline: 0;
`;
export default ColorPicker;
@@ -0,0 +1,13 @@
import styled from "styled-components";
import InputSearch from "~/components/InputSearch";
import { HStack } from "~/components/primitives/HStack";
export const UserInputContainer = styled(HStack)`
height: 48px;
padding: 6px 12px 0px;
`;
export const StyledInputSearch = styled(InputSearch)`
flex-grow: 1;
min-width: 0;
`;
@@ -1,77 +1,28 @@
import concat from "lodash/concat";
import { PlusIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { EmojiCategory, EmojiSkinTone, IconType } from "@shared/types";
import type { EmojiSkinTone } from "@shared/types";
import { EmojiCategory, IconType } from "@shared/types";
import { getEmojis, getEmojisWithCategory, search } from "@shared/utils/emoji";
import Flex from "~/components/Flex";
import InputSearch from "~/components/InputSearch";
import usePersistedState from "~/hooks/usePersistedState";
import {
FREQUENTLY_USED_COUNT,
DisplayCategory,
emojiSkinToneKey,
emojisFreqKey,
lastEmojiKey,
sortFrequencies,
} from "../utils";
import GridTemplate, { DataNode } from "./GridTemplate";
import { EmojiCreateDialog } from "~/components/EmojiCreateDialog";
import { DisplayCategory } from "../utils";
import type { DataNode, EmojiNode } from "./GridTemplate";
import GridTemplate from "./GridTemplate";
import SkinTonePicker from "./SkinTonePicker";
import { StyledInputSearch, UserInputContainer } from "./Components";
import { useIconState } from "../useIconState";
import useStores from "~/hooks/useStores";
import type Emoji from "~/models/Emoji";
import { useComputed } from "~/hooks/useComputed";
import { MenuButton } from "./MenuButton";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import usePolicy from "~/hooks/usePolicy";
import { IconButton } from "./IconButton";
const GRID_HEIGHT = 410;
const useEmojiState = () => {
const [emojiSkinTone, setEmojiSkinTone] = usePersistedState<EmojiSkinTone>(
emojiSkinToneKey,
EmojiSkinTone.Default
);
const [emojisFreq, setEmojisFreq] = usePersistedState<Record<string, number>>(
emojisFreqKey,
{}
);
const [lastEmoji, setLastEmoji] = usePersistedState<string | undefined>(
lastEmojiKey,
undefined
);
const incrementEmojiCount = React.useCallback(
(emoji: string) => {
emojisFreq[emoji] = (emojisFreq[emoji] ?? 0) + 1;
setEmojisFreq({ ...emojisFreq });
setLastEmoji(emoji);
},
[emojisFreq, setEmojisFreq, setLastEmoji]
);
const getFreqEmojis = React.useCallback(() => {
const freqs = Object.entries(emojisFreq);
if (freqs.length > FREQUENTLY_USED_COUNT.Track) {
sortFrequencies(freqs).splice(FREQUENTLY_USED_COUNT.Track);
setEmojisFreq(Object.fromEntries(freqs));
}
const emojis = sortFrequencies(freqs)
.slice(0, FREQUENTLY_USED_COUNT.Get)
.map(([emoji, _]) => emoji);
const isLastPresent = emojis.includes(lastEmoji ?? "");
if (lastEmoji && !isLastPresent) {
emojis.pop();
emojis.push(lastEmoji);
}
return emojis;
}, [emojisFreq, setEmojisFreq, lastEmoji]);
return {
emojiSkinTone,
setEmojiSkinTone,
incrementEmojiCount,
getFreqEmojis,
};
};
type Props = {
panelWidth: number;
query: string;
@@ -90,18 +41,36 @@ const EmojiPanel = ({
height = GRID_HEIGHT,
}: Props) => {
const { t } = useTranslation();
const { emojis, dialogs } = useStores();
const team = useCurrentTeam();
const can = usePolicy(team);
const searchRef = React.useRef<HTMLInputElement | null>(null);
const scrollableRef = React.useRef<HTMLDivElement | null>(null);
const customEmojis = useComputed(
() => emojis.orderedData.map(toIcon),
[emojis.orderedData]
);
const {
emojiSkinTone: skinTone,
setEmojiSkinTone,
incrementEmojiCount,
getFreqEmojis,
} = useEmojiState();
incrementIconCount,
getFrequentIcons,
} = useIconState(IconType.Emoji);
const freqEmojis = React.useMemo(() => getFreqEmojis(), [getFreqEmojis]);
const {
incrementIconCount: incrementCustomIconCount,
getFrequentIcons: getFrequentCustomIcons,
} = useIconState(IconType.Custom);
const freqEmojis = React.useMemo(
() => getFrequentIcons(),
[getFrequentIcons]
);
const [freqCustomEmojis, setFreqCustomEmojis] = React.useState<EmojiNode[]>(
[]
);
const handleFilter = React.useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
@@ -117,23 +86,68 @@ const EmojiPanel = ({
[setEmojiSkinTone]
);
const handleUploadClick = React.useCallback(() => {
dialogs.openModal({
title: t("Upload emoji"),
content: <EmojiCreateDialog onSubmit={dialogs.closeAllModals} />,
});
}, [dialogs, t]);
const handleEmojiSelection = React.useCallback(
({ id, value }: { id: string; value: string }) => {
onEmojiChange(value);
incrementEmojiCount(id);
// Determine if this is a custom emoji by checking if it's in the custom emoji data
const isCustomEmoji =
customEmojis.some((emoji) => emoji.id === id) ||
freqCustomEmojis.some((emoji) => emoji.id === id);
if (isCustomEmoji) {
incrementCustomIconCount(id);
} else {
incrementIconCount(id);
}
},
[onEmojiChange, incrementEmojiCount]
[
onEmojiChange,
incrementIconCount,
incrementCustomIconCount,
customEmojis,
freqCustomEmojis,
]
);
React.useEffect(() => {
// Load frequent custom emojis
getFrequentCustomIcons().forEach((id) => {
emojis
.fetch(id)
.then((emoji) => {
setFreqCustomEmojis((prev) => {
if (prev.some((item) => item.id === id)) {
return prev;
}
return [...prev, toIcon(emoji)];
});
})
.catch(() => {
// ignore
});
});
}, [emojis, getFrequentCustomIcons]);
const isSearch = query !== "";
const templateData: DataNode[] = isSearch
? getSearchResults({
query,
skinTone,
customEmojis,
})
: getAllEmojis({
skinTone,
freqEmojis,
customEmojis,
freqCustomEmojis,
});
React.useLayoutEffect(() => {
@@ -146,7 +160,7 @@ const EmojiPanel = ({
return (
<Flex column>
<UserInputContainer align="center" gap={12}>
<UserInputContainer>
<StyledInputSearch
ref={searchRef}
value={query}
@@ -154,6 +168,14 @@ const EmojiPanel = ({
onChange={handleFilter}
/>
<SkinTonePicker skinTone={skinTone} onChange={handleSkinChange} />
{can.update && (
<MenuButton
onClick={handleUploadClick}
aria-label={t("Upload emoji")}
>
<PlusIcon />
</MenuButton>
)}
</UserInputContainer>
<GridTemplate
ref={scrollableRef}
@@ -161,6 +183,11 @@ const EmojiPanel = ({
height={height - 48}
data={templateData}
onIconSelect={handleEmojiSelection}
empty={
<IconButton onClick={handleUploadClick}>
<PlusIcon />
</IconButton>
}
/>
</Flex>
);
@@ -169,19 +196,32 @@ const EmojiPanel = ({
const getSearchResults = ({
query,
skinTone,
customEmojis,
}: {
query: string;
skinTone: EmojiSkinTone;
customEmojis: EmojiNode[];
}): DataNode[] => {
const emojis = search({ query, skinTone });
// Search custom emojis by name
const matchingCustomEmojis = customEmojis.filter((emoji) =>
emoji.name?.toLowerCase().includes(query.toLowerCase())
);
const allResults = [
...matchingCustomEmojis,
...emojis.map((emoji) => ({
type: IconType.Emoji as const,
id: emoji.id,
value: emoji.value,
})),
];
return [
{
category: DisplayCategory.Search,
icons: emojis.map((emoji) => ({
type: IconType.Emoji,
id: emoji.id,
value: emoji.value,
})),
icons: allResults,
},
];
};
@@ -189,21 +229,32 @@ const getSearchResults = ({
const getAllEmojis = ({
skinTone,
freqEmojis,
customEmojis,
freqCustomEmojis,
}: {
skinTone: EmojiSkinTone;
freqEmojis: string[];
customEmojis: EmojiNode[];
freqCustomEmojis: EmojiNode[];
}): DataNode[] => {
const emojisWithCategory = getEmojisWithCategory({ skinTone });
const getFrequentEmojis = (): DataNode => {
const getFrequentIcons = (): DataNode => {
const emojis = getEmojis({ ids: freqEmojis, skinTone });
return {
category: DisplayCategory.Frequent,
icons: emojis.map((emoji) => ({
type: IconType.Emoji,
// Combine frequent standard and custom emojis
const allFrequent = [
...emojis.map((emoji) => ({
type: IconType.Emoji as const,
id: emoji.id,
value: emoji.value,
})),
...freqCustomEmojis,
];
return {
category: DisplayCategory.Frequent,
icons: allFrequent,
};
};
@@ -219,8 +270,8 @@ const getAllEmojis = ({
};
};
return concat(
getFrequentEmojis(),
const allData = concat(
getFrequentIcons(),
getCategoryData(EmojiCategory.People),
getCategoryData(EmojiCategory.Nature),
getCategoryData(EmojiCategory.Foods),
@@ -230,15 +281,22 @@ const getAllEmojis = ({
getCategoryData(EmojiCategory.Symbols),
getCategoryData(EmojiCategory.Flags)
);
if (customEmojis.length) {
allData.push({
category: "Custom",
icons: customEmojis,
});
}
return allData;
};
const UserInputContainer = styled(Flex)`
height: 48px;
padding: 6px 12px 0px;
`;
const StyledInputSearch = styled(InputSearch)`
flex-grow: 1;
`;
const toIcon = (emoji: Emoji): EmojiNode => ({
type: IconType.Custom,
id: emoji.id,
value: emoji.id,
name: emoji.name,
});
export default EmojiPanel;
@@ -1,5 +1,6 @@
import React from "react";
import { FixedSizeList, ListChildComponentProps } from "react-window";
import type { ListChildComponentProps } from "react-window";
import { FixedSizeList } from "react-window";
import styled from "styled-components";
type Props = {
@@ -9,6 +9,7 @@ import Text from "~/components/Text";
import { TRANSLATED_CATEGORIES } from "../utils";
import Grid from "./Grid";
import { IconButton } from "./IconButton";
import { CustomEmoji } from "@shared/components/CustomEmoji";
/**
* icon/emoji size is 24px; and we add 4px padding on all sides,
@@ -23,10 +24,11 @@ type OutlineNode = {
delay: number;
};
type EmojiNode = {
type: IconType.Emoji;
export type EmojiNode = {
type: IconType.Emoji | IconType.Custom;
id: string;
value: string;
name?: string;
};
export type DataNode = {
@@ -35,14 +37,20 @@ export type DataNode = {
};
type Props = {
/** Width of the grid container */
width: number;
/** Height of the grid container */
height: number;
/** Data to be displayed in the grid */
data: DataNode[];
/** Content to display when search results are empty */
empty?: React.ReactNode;
/** Callback when an icon is selected */
onIconSelect: ({ id, value }: { id: string; value: string }) => void;
};
const GridTemplate = (
{ width, height, data, onIconSelect }: Props,
{ width, height, data, empty, onIconSelect }: Props,
ref: React.Ref<HTMLDivElement>
) => {
// 24px padding for the Grid Container
@@ -50,10 +58,6 @@ const GridTemplate = (
const gridItems = compact(
data.flatMap((node) => {
if (node.icons.length === 0) {
return [];
}
const category = (
<CategoryName
key={node.category}
@@ -65,6 +69,13 @@ const GridTemplate = (
</CategoryName>
);
if (node.icons.length === 0) {
if (node.category !== "Search") {
return [];
}
return [[category], [empty]];
}
const items = node.icons.map((item) => {
if (item.type === IconType.SVG) {
return (
@@ -86,7 +97,11 @@ const GridTemplate = (
onClick={() => onIconSelect({ id: item.id, value: item.value })}
>
<Emoji width={24} height={24}>
{item.value}
{item.type === IconType.Custom ? (
<CustomEmoji value={item.value} title={item.name} />
) : (
item.value
)}
</Emoji>
</IconButton>
);
@@ -5,16 +5,11 @@ import { IconType } from "@shared/types";
import { IconLibrary } from "@shared/utils/IconLibrary";
import Flex from "~/components/Flex";
import InputSearch from "~/components/InputSearch";
import usePersistedState from "~/hooks/usePersistedState";
import {
FREQUENTLY_USED_COUNT,
DisplayCategory,
iconsFreqKey,
lastIconKey,
sortFrequencies,
} from "../utils";
import { DisplayCategory } from "../utils";
import ColorPicker from "./ColorPicker";
import GridTemplate, { DataNode } from "./GridTemplate";
import type { DataNode } from "./GridTemplate";
import GridTemplate from "./GridTemplate";
import { useIconState } from "../useIconState";
const IconNames = Object.keys(IconLibrary.mapping);
const TotalIcons = IconNames.length;
@@ -25,52 +20,6 @@ const TotalIcons = IconNames.length;
*/
const GRID_HEIGHT = 314;
const useIconState = () => {
const [iconsFreq, setIconsFreq] = usePersistedState<Record<string, number>>(
iconsFreqKey,
{}
);
const [lastIcon, setLastIcon] = usePersistedState<string | undefined>(
lastIconKey,
undefined
);
const incrementIconCount = React.useCallback(
(icon: string) => {
iconsFreq[icon] = (iconsFreq[icon] ?? 0) + 1;
setIconsFreq({ ...iconsFreq });
setLastIcon(icon);
},
[iconsFreq, setIconsFreq, setLastIcon]
);
const getFreqIcons = React.useCallback(() => {
const freqs = Object.entries(iconsFreq);
if (freqs.length > FREQUENTLY_USED_COUNT.Track) {
sortFrequencies(freqs).splice(FREQUENTLY_USED_COUNT.Track);
setIconsFreq(Object.fromEntries(freqs));
}
const icons = sortFrequencies(freqs)
.slice(0, FREQUENTLY_USED_COUNT.Get)
.map(([icon, _]) => icon);
const isLastPresent = icons.includes(lastIcon ?? "");
if (lastIcon && !isLastPresent) {
icons.pop();
icons.push(lastIcon);
}
return icons;
}, [iconsFreq, setIconsFreq, lastIcon]);
return {
incrementIconCount,
getFreqIcons,
};
};
type Props = {
panelWidth: number;
initial: string;
@@ -97,9 +46,9 @@ const IconPanel = ({
const searchRef = React.useRef<HTMLInputElement | null>(null);
const scrollableRef = React.useRef<HTMLDivElement | null>(null);
const { incrementIconCount, getFreqIcons } = useIconState();
const { incrementIconCount, getFrequentIcons } = useIconState(IconType.SVG);
const freqIcons = React.useMemo(() => getFreqIcons(), [getFreqIcons]);
const freqIcons = React.useMemo(() => getFrequentIcons(), [getFrequentIcons]);
const totalFreqIcons = freqIcons.length;
const filteredIcons = React.useMemo(
@@ -0,0 +1,19 @@
import { hover, s } from "@shared/styles";
import styled from "styled-components";
import NudeButton from "~/components/NudeButton";
export const MenuButton = styled(NudeButton)`
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 32px;
height: 32px;
color: ${s("textSecondary")};
border: 1px solid ${s("inputBorder")};
padding: 4px;
&: ${hover} {
border: 1px solid ${s("inputBorderFocused")};
}
`;
@@ -1,18 +1,17 @@
import { useMemo, useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { s, hover } from "@shared/styles";
import { EmojiSkinTone } from "@shared/types";
import { getEmojiVariants } from "@shared/utils/emoji";
import { Emoji } from "~/components/Emoji";
import Flex from "~/components/Flex";
import NudeButton from "~/components/NudeButton";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "~/components/primitives/Popover";
import { IconButton } from "./IconButton";
import { MenuButton } from "./MenuButton";
const SkinTonePicker = ({
skinTone,
@@ -57,9 +56,9 @@ const SkinTonePicker = ({
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger>
<StyledMenuButton aria-label={t("Choose default skin tone")}>
<MenuButton aria-label={t("Choose default skin tone")}>
{handEmojiVariants[skinTone]?.value}
</StyledMenuButton>
</MenuButton>
</PopoverTrigger>
<PopoverContent
side="bottom"
@@ -79,15 +78,4 @@ const Emojis = styled(Flex)`
padding: 0 8px;
`;
const StyledMenuButton = styled(NudeButton)`
width: 32px;
height: 32px;
border: 1px solid ${s("inputBorder")};
padding: 4px;
&: ${hover} {
border: 1px solid ${s("inputBorderFocused")};
}
`;
export default SkinTonePicker;
+9 -2
View File
@@ -21,6 +21,7 @@ import { Drawer, DrawerContent, DrawerTrigger } from "../primitives/Drawer";
import EmojiPanel from "./components/EmojiPanel";
import IconPanel from "./components/IconPanel";
import { PopoverButton } from "./components/PopoverButton";
import useStores from "~/hooks/useStores";
const TAB_NAMES = {
Icon: "icon",
@@ -35,7 +36,7 @@ type Props = {
icon: string | null;
color: string;
size?: number;
initial?: string;
initial: string;
className?: string;
popoverPosition: "bottom-start" | "right";
allowDelete?: boolean;
@@ -61,7 +62,7 @@ const IconPicker = ({
children,
}: Props) => {
const { t } = useTranslation();
const { emojis } = useStores();
const { width: windowWidth } = useWindowSize();
const isMobile = useMobile();
@@ -168,6 +169,12 @@ const IconPicker = ({
setActiveTab(defaultTab);
}, [defaultTab]);
React.useEffect(() => {
if (open) {
void emojis.fetchAll();
}
}, [open, emojis]);
if (isMobile) {
return (
<Drawer open={open} onOpenChange={setOpen}>
@@ -0,0 +1,89 @@
import {
customEmojisFreqKey,
emojisFreqKey,
emojiSkinToneKey,
FREQUENTLY_USED_COUNT,
iconsFreqKey,
lastCustomEmojiKey,
lastEmojiKey,
lastIconKey,
sortFrequencies,
} from "./utils";
import usePersistedState from "~/hooks/usePersistedState";
import { EmojiSkinTone, IconType } from "@shared/types";
import React from "react";
const lastIconKeys = {
[IconType.Custom]: lastCustomEmojiKey,
[IconType.Emoji]: lastEmojiKey,
[IconType.SVG]: lastIconKey,
};
const freqIconKeys = {
[IconType.Custom]: customEmojisFreqKey,
[IconType.Emoji]: emojisFreqKey,
[IconType.SVG]: iconsFreqKey,
};
const skinToneKeys = {
[IconType.Custom]: "",
[IconType.Emoji]: emojiSkinToneKey,
[IconType.SVG]: "",
};
export const useIconState = (type: IconType) => {
const [emojiSkinTone, setEmojiSkinTone] = usePersistedState<EmojiSkinTone>(
skinToneKeys[type],
EmojiSkinTone.Default
);
const [iconFreq, setIconFreq] = usePersistedState<Record<string, number>>(
freqIconKeys[type],
{}
);
const [lastIcon, setLastIcon] = usePersistedState<string | undefined>(
lastIconKeys[type],
undefined
);
const incrementIconCount = React.useCallback(
(emoji: string) => {
iconFreq[emoji] = (iconFreq[emoji] ?? 0) + 1;
setIconFreq({ ...iconFreq });
setLastIcon(emoji);
},
[iconFreq, setIconFreq, setLastIcon]
);
const getFrequentIcons = React.useCallback((): string[] => {
const freqs = Object.entries(iconFreq);
if (freqs.length > FREQUENTLY_USED_COUNT.Track) {
const trimmed = sortFrequencies(freqs).slice(
0,
FREQUENTLY_USED_COUNT.Track
);
setIconFreq(Object.fromEntries(trimmed));
}
const emojis = sortFrequencies(freqs)
.slice(0, FREQUENTLY_USED_COUNT.Get)
.map(([emoji, _]) => emoji);
const isLastPresent = emojis.includes(lastIcon ?? "");
if (lastIcon && !isLastPresent) {
emojis.pop();
emojis.push(lastIcon);
}
return emojis;
}, [iconFreq, lastIcon, setIconFreq]);
return {
emojiSkinTone,
setEmojiSkinTone,
incrementIconCount,
getFrequentIcons,
};
};
+9
View File
@@ -18,6 +18,7 @@ export const TRANSLATED_CATEGORIES = {
Objects: i18next.t("Objects"),
Symbols: i18next.t("Symbols"),
Flags: i18next.t("Flags"),
Custom: i18next.t("Custom"),
};
export const FREQUENTLY_USED_COUNT = {
@@ -32,6 +33,8 @@ const STORAGE_KEYS = {
EmojisFrequency: "emojis-freq",
LastIcon: "last-icon",
LastEmoji: "last-emoji",
CustomEmojisFrequency: "custom-emojis-freq",
LastCustomEmoji: "last-custom-emoji",
};
const getStorageKey = (key: string) => `${STORAGE_KEYS.Base}.${key}`;
@@ -46,5 +49,11 @@ export const lastIconKey = getStorageKey(STORAGE_KEYS.LastIcon);
export const lastEmojiKey = getStorageKey(STORAGE_KEYS.LastEmoji);
export const customEmojisFreqKey = getStorageKey(
STORAGE_KEYS.CustomEmojisFrequency
);
export const lastCustomEmojiKey = getStorageKey(STORAGE_KEYS.LastCustomEmoji);
export const sortFrequencies = (freqs: [string, number][]) =>
freqs.sort((a, b) => (a[1] >= b[1] ? -1 : 1));

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