Compare commits

...

551 Commits

Author SHA1 Message Date
Salihu 2c0f375901 requested changes 2026-03-21 16:07:08 +01:00
Salihu 4894e92d6b add gradient 2026-03-21 16:07:08 +01:00
Salihu 5e6eebd0ec collapsible code blocks 2026-03-21 16:07:03 +01:00
Tom Moor a4badbea9c feat: Role preference for collection template mangement (#11821)
* wip

* ui

* test

* Apply suggestion from @Copilot

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-20 23:57:38 -04:00
Tom Moor f22bc4a0b2 Update README.md 2026-03-20 23:35:32 -04:00
Tom Moor 5693618de4 Add translation hooks to transactional emails (#11785)
* First pass

* fix: Missing translations

* fix: Missing translations

* welcome

* Apply suggestions from code review

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

* translations

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-20 23:28:51 -04:00
Tom Moor a0039b2a09 Add keyboard access to mermaid diagram editing (#11834) 2026-03-20 23:25:43 -04:00
Tom Moor fa17f78ae6 fix: Disable embed option for internal link pastes (#11837)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 23:25:26 -04:00
Tom Moor beec9f5675 fix: Empty screen after login with post-redirect (#11833)
closes #11811
2026-03-20 12:09:18 -04:00
Tom Moor 5256cdc185 fix: Guard editDiagram usage (#11830)
closes #11827
2026-03-20 11:18:19 -04:00
Tom Moor 1bd6ad830e MCP improvements (#11822)
* fix: Data always included in list_documents response

* Remove resources, add fetch tool
Fix pagination arguments do not accept string

* type -> resource

* Add URL resolving
2026-03-20 09:45:50 -04:00
Tom Moor 9efcb2d534 fix: GitLab work_items paste detection (#11820)
closes #11819
2026-03-19 17:56:33 -04:00
Copilot 14fc3b01db Fix suggestion menus blocked by placeholder marks (#11796)
* Initial plan

* fix: suggestion menus not opening when typing trigger inside placeholder mark

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-03-19 17:45:03 -04:00
Tom Moor 05eac5bc3b v1.6.1 2026-03-18 19:49:24 -04:00
Igor Loskutov 64dc5e8ea7 fix: guard against concurrent restore in documentPermanentDeleter (#11775)
* fix: guard against concurrent restore in documentPermanentDeleter

* fix: bake deletedAt check into documentPermanentDeleter destroy WHERE clause
2026-03-18 08:33:09 -04:00
Tom Moor f03ac1f8de Add "Create a nested doc" to @mention (#11800)
* Mention menu nested doc

* refactor
2026-03-18 08:32:56 -04:00
Apoorv Mishra 07099bb4f6 fix: restore image upload (#11803) 2026-03-18 08:32:34 -04:00
Tom Moor 4673ff0840 fix: Clicking on templates in settings table does nothing 2026-03-17 23:47:00 -04:00
Copilot 500c3f91b0 Support GitLab work_items URL structure in unfurl integration (#11795)
* Initial plan

* Support GitLab work_items URL structure in parseUrl

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-03-17 22:40:47 -04:00
Tom Moor f8098ab464 feat: Adds API key authentication for MCP server (#11798)
* feat: Adds API key authentication for MCP server

* Add AuthenticationHelper test
2026-03-17 22:40:35 -04:00
Tom Moor 3740e09e5c feat: Expose moving documents within a collection (#11799) 2026-03-17 22:40:25 -04:00
Tom Moor 62cfd4e9bc chore: Cleanup working tables left in db if midrun abort (#11786) 2026-03-17 08:05:32 -04:00
Copilot 85072dab92 fix: Preserve port in OAuth metadata URLs when self-hosted behind a reverse proxy (#11791)
* Initial plan

* fix: Preserve port in OAuth metadata URLs when behind reverse proxy

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-03-17 08:04:40 -04:00
Copilot 1e8d9b5f80 fix: Support mailbox format for SMTP_FROM_EMAIL and SMTP_REPLY_EMAIL (#11784)
* Initial plan

* fix: Handle SMTP_FROM_EMAIL/SMTP_REPLY_EMAIL in mailbox format

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-03-16 22:48:56 -04:00
Tom Moor 613877714b fix: Page hang with corrupted PNG upload (#11783) 2026-03-16 21:43:38 -04:00
wmTJc9IK0Q cc1c4b22d4 Apply full width to print layout (#11768)
* Apply full width to print layout

* Fix closing parens
2026-03-16 08:51:03 -04:00
Tom Moor a9401c9bb6 fix: Race condition when editing title while doc is saving (#11764) 2026-03-15 15:47:41 -04:00
github-actions[bot] 1345471338 chore: Compressed inefficient images automatically (#11763)
Co-authored-by: tommoor <tommoor@users.noreply.github.com>
2026-03-15 15:46:14 -04:00
Tom Moor 0ddddac9c9 Add maskable and monochrome icon variants (#11762)
* Add maskable and monochrome icon variants

* Optimised images with calibre/image-actions

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-15 15:43:44 -04:00
Tom Moor 24954204ea v1.6.0 2026-03-15 12:18:08 -04:00
Tom Moor 1a893b0e45 Group sync framework (#11684)
Adds group sync from external authentication providers, allowing team group memberships to be automatically managed based on provider data on sign-in in the future.
2026-03-14 23:02:20 -04:00
Translate-O-Tron 255efe9844 New Crowdin updates (#11688)
* 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]

* fix: New French 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 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]

* fix: New Hebrew 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 Korean 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 Italian translations from Crowdin [ci skip]
2026-03-14 21:35:57 -04:00
Tom Moor 20e55141de Move toggle container up in block menu 2026-03-14 21:17:17 -04:00
Tom Moor 9940f48efa Add flags to Team model to match User (#11758) 2026-03-14 20:17:03 -04:00
Liam Stanley b1a192c078 fix: don't force prompt for Discord OAuth2 (#11757)
Signed-off-by: Liam Stanley <liam@liam.sh>
2026-03-14 19:20:13 -04:00
Copilot 22138957ab Add Project unfurl support to GitLab plugin (#11752)
* Initial plan

* Add GitLab Project unfurl support

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

* Fix TypeScript errors: add explicit return type to parseUrl

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

* tweaks

* progress

* Remove log noise

---------

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-03-14 19:14:35 -04:00
Tom Moor ff0a1766f8 fix: Add exact ID/slug lookup for list_documents and list_collections (#11756) 2026-03-14 17:42:14 -04:00
Copilot d1203408b5 Add GitHub Project V2 unfurl support (#11753)
* Initial plan

* Add GitHub Project V2 unfurl support to the GitHub plugin

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

* Various fixes

---------

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-03-14 17:13:35 -04:00
Tom Moor 576117e27b Update AGENTS.md 2026-03-14 16:19:51 -04:00
Tom Moor 4bc0f15323 Move group management to sub-page (#11755)
* chore: Move group management to sub-page

* refactor
2026-03-14 16:00:33 -04:00
Copilot 36d555f3fb Add Linear project unfurling support (#11525)
* Initial plan

* Add Project type and unfurl implementation for Linear projects

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

* Fix linter issues - remove unused import and rename unused parameter

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

* Make actor parameter optional in unfurl helper methods

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

* fix: Resolve type errors in Linear project unfurl

Use project.status (ProjectStatus object) instead of the deprecated
project.state (string) field, add satisfies constraint, and fix
exhaustive return in unfurl switch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Determine mention type

* styling

---------

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>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 11:03:04 -04:00
Tom Moor 350f69e194 fix: Stale collaborator IDs (#11749) 2026-03-14 09:38:38 -04:00
Copilot a92a1785ff Enable CMD+Shift+L theme toggle on publicly shared pages (#11750)
* Initial plan

* Add CMD+Shift+L keyboard shortcut to toggle theme on shared pages

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-03-14 09:38:27 -04:00
Copilot 631a4b0efa Default PDF attachments to non-embedded on upload (#11745)
* Initial plan

* Default PDF preview to false when uploading via drag and drop

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

* Preserve PDF preview for block menu option and attachment replacement

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-03-13 22:22:11 -04:00
Copilot 52077e4d47 Add PDF preview toggle button to attachment formatting toolbar (#11746)
* Initial plan

* Add PDF preview toggle button to attachment formatting toolbar

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

* Tweak icon

---------

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-03-13 22:07:48 -04:00
Copilot 79fc0b90b9 Only include passkeys in auth.config providers when team has registered passkeys (#11748)
* Initial plan

* Only return passkeys auth provider if team has at least one registered passkey

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-03-13 21:46:56 -04:00
Tom Moor ea4fbdb7bb fix: Filter relationships returned from list endpoint (#11738)
* fix: Filter relationships returned from list endpoint

* fix: BacklinksProcessor does not check teamId

* Port from upstream
2026-03-12 22:09:31 -04:00
Tom Moor 88f7ef9d03 fix: Hide fullscreen control in present mode on mobile iOS (#11737) 2026-03-12 20:50:25 -04:00
Copilot 951fb8a34a Add "Open in Desktop" option to document menu (#11729)
* Initial plan

* Add Open in Desktop option to document menu

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

* Tweak naming

---------

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-03-12 09:33:11 -04:00
Daniel Lo Nigro 0b5bd31017 Update comment about auth providers in .env.sample (#11731) 2026-03-12 08:35:07 -04:00
Tom Moor 48c7bd990a fix: Incorrect policy on file operations (#11728) 2026-03-11 23:55:45 -04:00
Tom Moor 54f2994b13 fix: DocumentExplorer jump on mouse hover (#11727)
* fix: DocumentExplorer jump on nav

* refactor
2026-03-11 20:25:33 -04:00
Tom Moor 8d9cd25b4e perf: Query pagination (#11726)
* Add client version header

* Include commit sha in x-client-version header

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* perf: Removes count query for client requests

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 20:25:23 -04:00
Tom Moor 16a4b8417e fix: MobX observable warning in dialog store 2026-03-11 20:00:45 -04:00
Tom Moor c993305c1b fix: MobX warning in SearchActions 2026-03-11 19:30:02 -04:00
Tom Moor 70891d5fa7 fix: Document actions showing in cmd-k without context 2026-03-11 19:22:54 -04:00
Tom Moor 89511d4026 fix: MobX warning in BlockMenu 2026-03-11 19:03:15 -04:00
Copilot bd573c44c1 Add ABAP to supported code formatting languages (#11721)
* Initial plan

* Add ABAP as a supported code formatting language

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-03-11 17:33:32 -04:00
Tom Moor 2e50fb0344 feat: Add toggle for all notifications (#11713)
* feat: Add toggle for all notifications

* tidy
2026-03-11 08:38:12 -04:00
Tom Moor fee9791cc9 Add instructions slide (#11710) 2026-03-11 08:31:21 -04:00
Akshat Singhal e913075d75 Removed usage of ALLOWED_DOMAINS and GOOGLE_ALLOWED_DOMAINS. (#11718) 2026-03-11 08:31:12 -04:00
Ilja Lukin bb3d72cb83 Add German (de_DE) long‑date format to useLocaleTime hook (#11720) 2026-03-11 12:17:39 +00:00
Tom Moor 0d8d9a1798 chore: Move warning logs from Sentry to standard logs (#11708) 2026-03-10 23:37:02 +00:00
Copilot 0c6e4f349b Add FontAwesome icon support for Mermaid diagrams (#11704)
* Initial plan

* Implement FontAwesome icon support for Mermaid diagrams

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-03-10 19:25:17 -04:00
Copilot a8b701aff3 fix: Correctly strip node comments on duplication (#11700)
* Initial plan

* fix: preserve table row background colors when duplicating documents

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-03-10 19:25:04 -04:00
Copilot 83977f85bd Use filtered fetch in Figma and Linear plugins (#11701)
* Initial plan

* chore: use filtered fetch in Figma and Linear plugins

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-03-09 23:04:18 -04:00
Tom Moor 9f1e6d8b40 fix: Increase code block font size (#11690) 2026-03-08 22:07:08 -04:00
Tom Moor 257d01af89 fix: Missing check for enabled passkeys in verification endpoint (#11689) 2026-03-08 18:46:13 -04:00
Tom Moor 1a54625cdb feat: Insert templates from block menu (#11647)
* chore: Move SuggestionsMenu to Radix

* Restore bounce anim

* fix: Clear query on button open

* Sub-menu support

* fix bugs

* PR feedback

* Insert templates from block menu

* refactor
2026-03-08 18:27:04 -04:00
Tom Moor 1a380f844c perf: Avoids instantiating editor extensions not required in read-only (#11681)
* perf: Avoids instantiating editor extensions not required in read-only

* Now class-based extensions are checked via ext.prototype before new ext() is called
2026-03-08 18:26:52 -04:00
Tom Moor 03a78ab6d6 feat: Use web haptics lib (#11685) 2026-03-08 18:26:42 -04:00
Tom Moor b63225fa73 Improve user membership policy check (#11687) 2026-03-08 18:26:33 -04:00
Tom Moor 3066b7ba4e feat: Presentation mode (#11678)
* wip

* fix scaling, query string, icons, refactor

* refactor
2026-03-07 09:17:47 -05:00
Tom Moor aeb6d12f17 fix: Incorrect nesting in document explorer (#11680)
* fix: Incorrect nesting in document explorer

* fix: Disclosure position in explorer
2026-03-07 00:15:51 -05:00
Tom Moor db19a5cf0d fix: Incorrect insertion position of mentions (#11671)
closes #11461
2026-03-05 21:34:14 -05:00
Tom Moor c875930430 fix: Improved resiliency to invalid GitLab data (#11669) 2026-03-05 19:48:17 -05:00
Tom Moor 3d1c2a8759 chore: Remove datadog-metrics lib (#11665)
* chore: Remove datadog-metrics lib

* Restore Pako transient dep types

* PR feedback
2026-03-05 19:27:11 -05:00
Tom Moor 2681a2cfaf feat: Support rendering shared docs as Markdown with .md extension (#11668) 2026-03-05 19:27:03 -05:00
Tom Moor 1b88189f9c fix: INSERT into subscriptions deadlocked transactions (#11667) 2026-03-05 18:53:13 -05:00
Tom Moor ace351035a chore: Move SuggestionsMenu to Radix (#11644)
* chore: Move SuggestionsMenu to Radix

* Restore bounce anim

* fix: Clear query on button open

* Sub-menu support

* fix bugs

* PR feedback
2026-03-05 17:45:19 -05:00
Copilot 3222a77cc3 Remove star control from DocumentListItem on mobile (#11655)
* Initial plan

* Remove star control on DocumentListItem on mobile

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-03-05 17:45:05 -05:00
Tom Moor d8b0e731ef fix: ESC for back in SharePopover not working (#11662)
* fix: ESC for back in SharePopover not working

closes #11656

* Normalize prevent default callbacks
2026-03-05 17:44:48 -05:00
Apoorv Mishra 80c1e5a10b fix: reset focused comment on drawer close (#11661) 2026-03-05 17:44:39 -05:00
Copilot 5b89882e5e Highlight parent menu item when submenu is open (#11659)
* Initial plan

* fix: highlight parent menu item when sub-menu is open

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-03-05 13:46:30 +00:00
Translate-O-Tron f85ad1a7e1 New Crowdin updates (#11455)
* 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 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 Czech translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

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

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

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

* fix: New Korean 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 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]

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

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

* fix: New Korean translations from Crowdin [ci skip]
2026-03-05 08:22:17 -05:00
Salihu 38a3e651a7 fix: Rebuild of sourced permissions on document move (#11229)
* only recalculate permssions for moved document

* stop duplicating permissions

* add tests

* fix comment

* filter duplicates

* filter duplicates

* minor fixes

* fix child document permissions not being recalculated

* expand tests

* Update DocumentMovedProcessor.test.ts

* Update server/queues/processors/DocumentMovedProcessor.test.ts

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

* requested changes

* remove all sourced permissions before calculating new ones

* remove all sourced permissions before recalculating

* Add additional tests

---------

Co-authored-by: Tom Moor <tom@getoutline.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-05 08:22:03 -05:00
Apoorv Mishra e06ff9d7d2 Make history drawer on mobile usable again (#11649)
* fix: history drawer height

* fix: history drawer content overflow

* fix: comment
2026-03-05 08:13:09 -05:00
Copilot 89cfc76d8f Remove artificial statement timeout from popularity score calculation (#11652)
* Initial plan

* fix: remove transaction wrapper and artificial statement timeout from calculateScoresForDocuments

The method was wrapping a read-only CTE query in a transaction solely to
SET LOCAL statement_timeout = '30000ms'. On instances with large tables this
30-second cap caused SequelizeDatabaseError: canceling statement due to
statement timeout. Removed the unnecessary transaction and the STATEMENT_TIMEOUT_MS
constant so the query runs unimpeded under the database server's own timeout policy.

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-03-05 08:11:48 -05:00
Copilot c22ba4fa0c Validate oauthClientId as UUID before database query (#11653)
* Initial plan

* Fix SequelizeDatabaseError by adding UUID validation to oauthClientId

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-03-05 08:01:19 -05:00
Tom Moor 8dabc7f3cf Enable the rate limiter by default 2026-03-04 20:39:52 -05:00
Tom Moor bd7f0cdc12 Adding framer-motion to the vendor chunk config consolidates all framer-motion code into a single eagerly-loaded chunk, ensuring featureNames is computed exactly once before any mutation happens. (#11643) 2026-03-04 18:23:40 -05:00
Tom Moor 9a849418b1 fix: Upgrade framer-motion to v6, fix tab animation (#11637)
* Revert "Revert "fix: Upgrade framer-motion to v5, fix tab animation (#11632)"…"

This reverts commit 2c7ec179fa.

* fix: Race condition
2026-03-04 17:35:18 -05:00
Copilot 0ab3a962d9 Skip unfurl attempts for non-HTTP(S) URL schemes (#11640)
* Initial plan

* Fix: Skip unfurling for non-http(s) and non-mention URL schemes

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-03-04 09:12:13 -05:00
Tom Moor 2c7ec179fa Revert "fix: Upgrade framer-motion to v5, fix tab animation (#11632)" (#11636)
This reverts commit 526833ec6e.
2026-03-04 07:14:25 -05:00
Tom Moor 526833ec6e fix: Upgrade framer-motion to v5, fix tab animation (#11632) 2026-03-03 21:18:09 -05:00
Tom Moor d8f39e3b67 fix: Sidebar does not appear in some situations 2026-03-03 19:54:46 -05:00
Tom Moor 0565616b02 fix: Add skip error for multi-source Notion databases 2026-03-03 19:32:33 -05:00
Tom Moor dd9b28e898 feat: Add async loading ELK layout engine (#11631) 2026-03-03 19:29:46 -05:00
Tom Moor f5ef2f2b30 perf: Add request deduping in ApiClient (#11629) 2026-03-03 18:06:48 -05:00
Tom Moor 1f251d4829 perf: Include collection updatedAt property in websocket payloads (#11628) 2026-03-03 18:06:41 -05:00
Tom Moor 749bf49ebe fix: Avatar upload (#11624)
* fix: Avatar upload

* Restore internal public ACL
2026-03-03 08:06:17 -05:00
Tom Moor 371665b35c fix: Cannot access attribute text on document omitted from attributes 2026-03-02 22:09:55 -05:00
Tom Moor d20779e7ea fix: Settings layout tweaks 2026-03-02 19:12:49 -05:00
Tom Moor 1d86a4aee4 Add limit of 10 attempts for OTP login (#11623)
* Add limit of 10 attempts for OTP login

* test
2026-03-02 18:41:03 -05:00
Copilot 19e5888216 fix: Preserve diagram file format (PNG/SVG) when editing (#11622)
* Initial plan

* Implement format retention for diagram editing

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

* Address code review feedback: improve comments and property order

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>
2026-03-02 18:12:52 -05:00
Apoorv Mishra a4039ccc81 fix: update url when moving away from history sidebar (#11618) 2026-03-02 17:51:43 -05:00
Tom Moor f0a7ee6df8 chore: Update rate limiter to use a combination of ID and IP (#11613)
* chore: Update rate limiter to combo ID and IP

* test
2026-03-02 17:47:16 -05:00
Tom Moor c428d551b8 perf: Check socket is still connected before querying db (#11620) 2026-03-02 17:47:06 -05:00
Tom Moor 904f1a9726 tsc 2026-03-02 07:21:23 -05:00
Tom Moor 8dc4f8b422 feat: Add text wrap option for code blocks (#11614) 2026-03-02 07:08:12 -05:00
Tom Moor 1ceb476a04 fix: Public docs not visible 2026-03-02 07:06:39 -05:00
Tom Moor 5439afa5c8 fix: Minor language and position tweaks 2026-03-01 20:12:52 -05:00
Tom Moor 8619b219e7 feat: Configurable slash embeds (#11612)
* wip

* Use id instead of title
Settings UI tweaks

* test

* Add toggle for all providers

* Remove 'Abstract' embed, no longer available
2026-03-01 17:47:29 -05:00
Copilot 5a14944d0c Remove starred document when actor archives it (#11611)
* Initial plan

* Add DocumentArchivedProcessor to remove archived documents from actor's stars

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

* Fix unused variable in test

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

* Remove unnecessary transaction wrapper for single operation

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

* Add JSDoc documentation to perform method

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

* Address code review: add @throws annotation and fix spacing

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

* Fix TypeScript errors in test file - add non-null assertions for collectionId

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

* Use ctx method for events

---------

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-03-01 17:47:17 -05:00
Tom Moor 93509564e0 Update template management policies (#11608)
* Template management policies

* PR feedback

* fix: Should not be able to navigate to template edit screen
Remove unused action
Add 'New document from template' to template menu
2026-03-01 09:48:17 -05:00
Tom Moor aacad48585 fix: Flashing of sidebar on initial load (#11607)
* fix: Flashing of sidebar on initial load

* refactor

* refactor

* Remove toggleComments

* fix: Skip animation on page load

* feedback
2026-02-28 21:54:16 -05:00
Tom Moor 8dbbcb6dce perf: Variety of perf fixes in ProsemirrorHelper (#11554)
* perf: Variety of perf fixes in ProsemirrorHelper

* doc

* feedback
2026-02-28 14:12:13 -05:00
Tom Moor 597da9f1ee fix: Search sorting wraps onto its own line (#11606)
* fix: Sort wrapping on search

* styling
2026-02-28 13:39:40 -05:00
Tom Moor eab57a7144 fix: Mermaid diagram (#11604)
* fix: New mermaid diagram should auto-edit
* fix: Empty mermaid diagram should not show zoom control
* fix node selection
2026-02-28 12:47:37 -05:00
Tom Moor 20f928332e fix: Resolve jsx-key and no-this-alias lint warnings
Move key props before spreads, add missing keys to iterator fragments,
and replace this-destructuring with direct property access.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 12:20:48 -05:00
Apoorv Mishra 12847d3d79 Fix: Right sidebar header misaligned (#11539)
* fix: right sidebar header misaligned

* fix: detect if vertical window scrollbar is visible

* fix: detect appearance of scrollbar using ResizeObserver

* Update Right.tsx

---------

Co-authored-by: Tom Moor <tom@getoutline.com>
2026-02-28 14:02:56 +00:00
Copilot b298456126 Increase request timeout for files.create to support large file uploads (#11570)
* Initial plan

* Add 30-minute timeout for files.create endpoint to handle large file uploads

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-02-28 09:02:28 -05:00
Tom Moor 4e773d7cb0 fix: Notifications popover (#11602)
* fix: Stable ordering of notification items

* fix: Editor in notification list items capturing clicks
2026-02-28 08:53:28 -05:00
Tom Moor 0dfab5b245 perf: Raise Notion pageSize 25 -> 100 2026-02-28 08:52:23 -05:00
Tom Moor a40ab4867d Improved search popover on public docs (#11601)
* stash

* refactor
2026-02-27 22:44:31 -05:00
Tom Moor 3270e05abc fix: Unneccessary hover state on sidebar 2026-02-27 21:02:01 -05:00
Tom Moor 36f5ce26e2 fix: Search rank ordering (#11599)
* fix: Search ranking

* test
2026-02-27 20:43:56 -05:00
Tom Moor 10303220a5 Public docs search should not include internal popularity score (#11598) 2026-02-27 20:25:50 -05:00
Copilot 1c52a3e7da Fix custom emoji UUID escaping in API document creation (#11594)
* Initial plan

* Add custom emoji UUID parsing support

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

* Fix custom emoji UUID parsing - working solution

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

* Add JSDoc and improve code quality based on review

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

* Add JSON content verification to custom emoji test

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-02-27 19:32:06 -05:00
Tom Moor 8a7501d0ad chore: Upgrade js-yaml (#11597) 2026-02-27 19:31:55 -05:00
Ashley Sommer 8048b7e530 feat: Add support for configurable proxy IP header in environment settings (#11595)
* feat: Add support for configurable proxy IP header in environment settings

* Update server/env.ts

Remove mention of Koa from docs

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

* Update .env.sample

Remove mention of Koa from env sample.

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

---------

Co-authored-by: Tom Moor <tom.moor@gmail.com>
2026-02-27 19:27:24 -05:00
Tom Moor d54167dbdf perf: Presenting lists of imported documents causes database lockup (#11591) 2026-02-27 08:35:11 -05:00
Tom Moor 6b98da54bd fix: Collection overview edit state (#11586) 2026-02-26 18:03:10 -05:00
Tom Moor 03e322f1ad fix: Flashing empty state in mention menu (#11587) 2026-02-26 18:02:19 -05:00
Tom Moor e876b2131e fix: Checkbox list completed items toggle does not take into account nested lists (#11583) 2026-02-26 15:50:12 -05:00
Apoorv Mishra 1e62c09dd0 fix: email horizontal overflow (#11584) 2026-02-26 15:50:04 -05:00
Tom Moor 0f23b63e35 perf: Remove unnecessary complex joins (#11581)
* perf: Remove unneccessary complex joins

* Further reduce joins and data loading
2026-02-26 09:11:37 -05:00
Tom Moor 658be50dcd Move 'Failed to unfurl' error -> warning 2026-02-26 09:10:59 -05:00
Tom Moor 9288ac87e0 fix: Webhook held in memory after timeout (#11580) 2026-02-26 08:52:40 -05:00
Tom Moor 6aa7f34b01 perf: Hold 10s cache on user.collectionIds (#11579) 2026-02-26 08:15:35 -05:00
Apoorv Mishra 7999cf0b98 fix: prevent horizontal overflow for toggle block content (#11577) 2026-02-26 06:51:42 -05:00
Tom Moor 4ebb0e1790 fix: Missing /mcp well-known routes (#11575) 2026-02-25 16:47:06 -05:00
Tom Moor 0ce992e87a fix: Misuse of transactions in revision endpoints (#11574) 2026-02-25 16:44:10 -05:00
Tom Moor 3c3d18637e fix: Ignored child page mentions in Notion importer (#11567) 2026-02-25 16:28:48 -05:00
Copilot 6708f5dbc2 Add dedicated tracing for MCP resources (#11569)
* Initial plan

* feat: Add separate tracing for MCP resources

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-02-25 14:40:08 -05:00
Tom Moor 38920cd1fe fix: Read only guard on table cell selection styling (#11555)
closes #11530
2026-02-25 07:35:19 -05:00
Tom Moor 053133f2b7 perf/separate-query (#11553) 2026-02-24 22:59:57 -05:00
Tom Moor 505e41661a fix: Include text in revisions payload as documented 2026-02-24 21:53:59 -05:00
Tom Moor 3c4ed666ce fix: Collection overview not appearing on public share 2026-02-24 21:36:32 -05:00
Copilot 25e222bb22 Fix print error caused by queueMicrotask timing with React (#11551)
* Initial plan

* Fix print error by replacing queueMicrotask with setTimeout

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-02-24 20:05:32 -05:00
Tom Moor 7c84c67077 perf: slice before map 2026-02-24 19:31:59 -05:00
Tom Moor d3fd0fc537 Revert "chore: Move to findAndCountAll instead of separate queries (#11536)" (#11550)
This reverts commit c68c6af8f4.
2026-02-24 19:30:23 -05:00
Copilot 7f88ab55fb Handle network failures in installation.info endpoint for isolated environments (#11546)
* Initial plan

* Add error handling for Installation.info endpoint in isolated environments

* Return -1 for versionsBehind when network request fails

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com>
2026-02-24 15:20:48 -05:00
Tom Moor 9038db525e fix: Split comment mark is not correctly updated/deleted (#11537)
* fix: Split comment mark is not correctly updated/deleted

* fix: Mark order matters, comment and link should span around other marks

* fixes:
1. underline spans all descendants of comment-marker span
2. override background of all descendants of comment-marker span, upon
   hover & focus

---------

Co-authored-by: Apoorv Mishra <apoorvmishra101092@gmail.com>
2026-02-24 08:40:05 -05:00
Tom Moor 9c7a53102b chore: Ensure ip address is passed through mcp events (#11542) 2026-02-24 08:31:25 -05:00
huiseo 7dc00b7d31 fix: prevent duplicate Korean IME character on Enter in search (#11543)
When typing Korean with IME on Mac and pressing Enter, the last
composed character was duplicated due to the SearchInput remounting
during active composition. Added isComposing check to skip keydown
handling during IME composition, consistent with other input components.
2026-02-24 08:31:13 -05:00
Tom Moor 112731a52b fix: Incorrect spread in DocumentImportTask
closes #11538
2026-02-23 23:50:45 -05:00
Tom Moor c68c6af8f4 chore: Move to findAndCountAll instead of separate queries (#11536)
* perf: Move to findAndCountAll instead of separate queries

* perf: slice before map
2026-02-23 22:48:07 -05:00
Tom Moor 957ce69d2e perf: Move image download out of transaction (#11528)
* perf: Move image download out of transaction

* loop
2026-02-23 22:14:03 -05:00
Tom Moor ff6a20ef38 perf: Slow query in NotificationHelper (#11534)
* perf: Slow query in NotificationHelper

* Create index concurrently
2026-02-23 21:31:52 -05:00
Tom Moor 6a69990833 Attribute MCP mutations separately in audit log events (#11533) 2026-02-23 21:31:34 -05:00
Tom Moor 0f45778d79 perf: Protect against ValidateSSOAccessTask thundering herd (#11532)
* perf: Protect against many tabs reloading at once

* PR feedback
2026-02-23 21:05:01 -05:00
Tom Moor 3886c179c5 Remove default user scope on GroupUser (#11531)
* Remove default user scope on GroupUser

* revert
2026-02-23 20:24:22 -05:00
Tom Moor ccde98ce82 fix: Handle template delete and error cases 2026-02-22 16:54:14 -05:00
Tom Moor 86a106e8e9 perf: Improved vendor chunking (#11518)
* perf/improve-vendor-chunking

* Enable bundle-size run on config change

* fix: Modules that should be lazy loaded

* fix: Mermaid in initial chunk

* tsc

* test

* Defer refractor core loading

* test

* test

* remove vendor chunk

* fix: prosemirror dragged into initial chunk
2026-02-22 16:25:00 -05:00
Tom Moor 3fe5f907db fix: Add CORS header to locale file route
Allow cross-origin requests for locale JSON files, consistent with
other static asset routes. Needed when loading translations via CDN.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 15:30:17 -05:00
Tom Moor 496b89c7f8 chore: Remove gitbeaker dep on client (#11517)
Add dupe detection to gitlab install
2026-02-22 00:38:10 -05:00
Tom Moor 46dd13fc7f Update integrations directory to color icons (#11516) 2026-02-22 02:37:04 +00:00
Tom Moor ac29295dd2 fix: Tighten touch device detection (#11515) 2026-02-21 18:27:00 -05:00
Apoorv Mishra 0e8fde3bb1 Perf: apply initial decorations early on for toggle blocks (#11493)
* draft

* Revert "draft"

This reverts commit 911c2996be.

* fix: that simple, huh
2026-02-21 18:26:52 -05:00
Salihu cad670f19c feat: GitLab integration (#10861)
Co-authored-by: Tom Moor <tom@getoutline.com>
closes #6795
2026-02-21 17:52:27 -05:00
Tom Moor 00ef17b913 fix: Flaky i18n test (#11514) 2026-02-21 12:35:01 -05:00
Tom Moor 05381ff101 Add move_document tool (#11510) 2026-02-20 21:39:14 -05:00
Tom Moor 519fd024f9 Add Datadog tracing to MCP tool handlers (#11509)
Wraps all MCP tool and resource handlers with Datadog APM spans so that
each invocation is visible in traces under the `outline-mcp` service.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 21:08:46 -05:00
Tom Moor 7be893f9a3 Refactor templates (#11027)
closes #8674
2026-02-20 18:53:00 -05:00
Tom Moor 52448714d9 fix: documents.import no longer allows direct upload (#11506) 2026-02-20 12:18:22 -05:00
Tom Moor 6e92313f73 Refactor ActionContextProvider to allow more model coverage (#11503) 2026-02-19 23:10:13 -05:00
Tom Moor dfd969084b fix: Sticky header transparent background (#11501)
fix: Custom header color incorrect text color
2026-02-19 21:15:07 -05:00
Tom Moor 758d2b62f5 fix: Overly greedy background -> highlight (#11500) 2026-02-20 01:44:39 +00:00
Tom Moor b90ff98cef fix: Read-only collection editor does not remount correctly on nav (#11499) 2026-02-20 01:28:54 +00:00
Tom Moor 23642fbd85 fix: Ignore browser cache for diagram extension (#11498)
closes #11496
2026-02-19 20:03:30 -05:00
Tom Moor 3fa429977a fix: Find and replace option immediately closes when opening on mobile (#11497)
* fix: Index out of bounds

* fix: Find and replace auto dismissal

* fix: Lost focus after vaul close
2026-02-19 19:18:03 -05:00
Copilot 8ddebb920e Add child documents list to markdown export for shared documents (#11495)
* Initial plan

* Add child documents list to markdown export for shared documents

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

* Apply prettier formatting to app.ts

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

* fix tree context

* test: Account for document share

---------

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-02-19 18:45:13 -05:00
Tom Moor 7ff6f1defb feat: Add webhooks for file attachments (#11494) 2026-02-19 17:28:50 -05:00
Tom Moor f2016bb1ca fix: Pagination on search (#11489) 2026-02-19 17:28:35 -05:00
Tom Moor ba5e4dddbc Add missing shortcut (#11492) 2026-02-19 17:28:19 -05:00
Tom Moor bb8f73cb8d fix: Default scopes not provided in OAuthAuthorize 2026-02-18 22:19:05 -05:00
Tom Moor 4aeea4f73c mcp: Add draft and publish (#11488)
* mcp: Add draft and publish

* refactor

* touch
2026-02-18 21:23:27 -05:00
Copilot 2e0bc66ad1 Fix React Doctor error-level issues (#11483)
* Initial plan

* Fix React Doctor errors: aria-selected, key props, alt attributes, layout animation, nested component, reduced motion

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

* Fix remaining React Doctor errors: refactor useTrackLastVisitedPath to avoid useEffect

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

* Revert useMeasure 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>
2026-02-18 19:47:56 -05:00
Tom Moor c4d861e0ae fix: Overflowing long redirect url (#11486) 2026-02-19 00:39:22 +00:00
Apoorv Mishra f02520444e Toggle toggle block's state upon clicking its head (#11469)
* fix: toggle block upon clicking toggle head in read-only mode

* fix: show pointer

* fix: prevent default

* fix: simplify
2026-02-17 18:59:17 -05:00
Tom Moor 6695ae1f3e Reduce popularity boost in search results (#11481) 2026-02-17 18:51:46 -05:00
Tom Moor 924db0a3fd fix: Handle invalid claudeai scope (#11484)
* fix: Handle invalid 'claudeai' scope

* Add docs link
2026-02-17 18:51:36 -05:00
Tom Moor c9fe7b3d5c Ensure full urls are returned from MCP (#11482)
* Ensure full urls are returned from MCP

towards #11474

* Fix pathToUrl using path.join for URLs and add test coverage

path.join collapsed https:// to https:/ — use URL constructor instead.
Added assertions verifying full URLs are returned for collections and
documents across list, create, update, and resource endpoints.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add scopes_supported

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 18:05:43 -05:00
Tom Moor 1937043aed feat: MCP Server (#11464)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 20:14:18 -05:00
Tom Moor 957648a588 feat: OAuth dynamic client registration (#11462)
* feat: DCR first pass

* Add cleanup task, management endpoints

* Apply suggestions from code review

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

* wip

* Combine migrations

* Self review

* fix: Guard OAuth policies

* fix: Application access list not updating on deletion

* feat: Add OAUTH_DISABLE_DCR env var to disable dynamic client registration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: Validate max length of redirect URIs in DCR schemas

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Self review

* Use withCtx methods for correct event creation

* Remove incorrect scopes_supported

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 17:30:19 -05:00
Copilot 5c01909909 Add Fortran language support to code blocks (#11471)
* Initial plan

* Add Fortran language support to code blocks

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-02-16 10:49:24 -05:00
Tom Moor 84d6ed01e3 chore: Remove 'Features' settings 2026-02-16 10:31:47 -05:00
Tom Moor c758f0d93a chore: Upgrade Zod to version 4 (#11465) 2026-02-15 22:54:50 -05:00
Tom Moor c54194f97a fix: Unobserved components (#11460)
* fix: Unobserved components

* mas

* More missing observers
2026-02-15 15:14:53 -05:00
Tom Moor a860cfc9ec v1.5.0 2026-02-15 12:54:42 -05:00
Tom Moor 08d58f7a6d fix: Small race conditions in diagrams.net integration (#11458) 2026-02-15 12:45:40 -05:00
Tom Moor 45a19d52cf Update documents.import to accept attachmentId (#11457)
* Update documents.import to accept attachmentId

* Add toast
2026-02-15 11:47:08 -05:00
Tom Moor de69a4e671 chore: Cleanup collection create dialog (#11454) 2026-02-14 17:39:53 -05:00
Tom Moor 7824f6b363 feat: Allow creating new doc before/after (#11453) 2026-02-14 17:24:52 -05:00
Tom Moor f6709520fa Add missing tooltips (#11452) 2026-02-14 21:44:45 +00:00
Tom Moor 66b0341cfa fix: Synthetic 'latest' revision fails to load (#11451)
closes #11449
2026-02-14 16:09:10 -05:00
Copilot 057d57e21a Add alphabetic ordered list support to markdown parser (#11446)
* Initial plan

* Add alpha list markdown parsing support

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

* Add integration tests for alpha list parsing and serialization

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

* Address code review feedback - improve marker matching logic

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

* Add explanatory comment for line offset constant

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-02-14 16:08:58 -05:00
Tom Moor 13c00c4663 chore: Convert rtl prop to transient, addresses warnings (#11450) 2026-02-14 15:53:06 -05:00
Tom Moor eb584ed6b6 perf: Load translation locale files over CDN URL (#11445)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 17:34:08 +00:00
Tom Moor 40c81a5e30 fix: Notification badge does not appear until notification popover opened (#11444) 2026-02-14 10:01:19 -05:00
Translate-O-Tron 5e976fe732 New Crowdin updates (#11380)
* fix: New Chinese Simplified translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

* fix: New Korean 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 Korean translations from Crowdin [ci skip]
2026-02-14 09:19:45 -05:00
Tom Moor fe9daa0a75 fix: Collections with the same name overwrite in export (#11443) 2026-02-14 09:19:30 -05:00
Tom Moor 08227ce4da fix/edit-redirect (#11442) 2026-02-14 13:40:27 +00:00
Tom Moor 4f6ee1a00b feat: Add a preference for desktop notification badge off/count/indicator (#11436) 2026-02-13 18:04:10 -05:00
Tom Moor 797c28a12e fix: Edits that only include a mention below edit distance do not trigger mention (#11434) 2026-02-13 18:02:47 -05:00
Salihu 129e872578 filter group members (#11403)
* filter group members

* requested changes
2026-02-13 17:35:54 -05:00
Copilot b4053f344f Add Alt-click to recursively expand/collapse sidebar documents and collections (#11432)
* Initial plan

* Add alt-click to expand/collapse all nested documents

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

* Fix callback stability to prevent unnecessary re-renders

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

* Add alt-click expand/collapse support for CollectionLink

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

* refactor

* Add support for other link types

* Handle unloaded

* refactor

* 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>
2026-02-13 17:27:20 -05:00
Tom Moor ffe7cda26b fix: Mispositioned toolbar on first document open (#11437)
closes #11423
2026-02-13 17:13:48 -05:00
Tom Moor 38880f8335 fix: Missing check for disabled group mentions (#11435) 2026-02-13 08:06:24 -05:00
Tom Moor 1caca05876 fix: No longer use public acl for avatars (#11427)
Related #11367
2026-02-12 21:56:21 -05:00
Tom Moor 0722b42613 fix: Potential task queue saturation in Notion importer (#11428)
* fix: Potential task queue saturation in Notion import

* Reduces concurrent Notion API pressure from 3× the recursive call depth down to 1
2026-02-12 21:56:00 -05:00
Tom Moor 5d749efd84 fix: Issue in active context creation due to fallback (#11426) 2026-02-12 20:10:53 -05:00
Copilot 0363481a6a Add "Rename" option to sidebar context menus (#11425)
* Initial plan

* Add Rename option to context menus for sidebar items

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-02-13 00:25:03 +00:00
Copilot c8fbdc35fb Ignore table_of_contents blocks in Notion import (#11424)
* Initial plan

* feat: Add handler to ignore table_of_contents Notion block

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-02-12 18:51:43 -05:00
Copilot c382e1233b Convert markdown frontmatter to YAML codeblocks on import (#11420)
* Initial plan

* Add frontmatter to YAML codeblock conversion

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

* Add edge case tests and fix frontmatter regex, install types

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

* Address code review feedback - improve template literal readability

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-02-12 18:32:15 -05:00
Tom Moor 3a875d4466 Add more ignore rules (#11419) 2026-02-12 18:27:54 -05:00
Tom Moor 66f9113975 fix: Exporting document with table causes crash (#11422)
* fix: Exporting document with table causes crash

* fix: Same issue for checkbox lists
2026-02-12 18:27:42 -05:00
Tom Moor a52391842f chore: Add application_name to postgres logging (#11415) 2026-02-11 20:59:39 -05:00
Tom Moor 20e84c8e1d chore: Allowlist more methods for CSRF skip (#11414) 2026-02-11 20:38:40 -05:00
Tom Moor 1488341f66 fix: Remove unnecessary loading of authentication rows in userProvisioner (#11413)
* fix: Remove unneccessary loading of authentication rows in userProvisioner

* test
2026-02-11 18:45:47 -05:00
Tom Moor a06174b627 Revert "perf: Reduce database contention in ImportTask (#11361)" (#11411)
This reverts commit 8209f56e56.
2026-02-10 22:59:46 -05:00
Tom Moor 22556b2121 fix: More selection toolbar fixes around link selection (#11408) 2026-02-10 21:26:11 -05:00
Copilot 7252701e9b Preserve alignment and caption when replacing images (#11407)
* Initial plan

* Preserve alignment, caption, and height when replacing images

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-02-10 18:16:22 -05:00
Tom Moor 5fd6ef646a fix: Sentry error resulting from browser extensions using MobX (#11399) 2026-02-10 06:46:36 -05:00
Copilot 0e9f34bd6a Add hide/show completed items control for checkbox lists (#11379)
* Initial plan

* Add hide/show completed items feature for checkbox lists

- Add id attribute to checkbox_list nodes
- Create CheckboxListNodeView with toggle button
- Store hide state in localStorage per user and list
- Add CSS styles for wrapper and toggle button
- Hide completed items when state is active

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

* Address code review feedback - improve boolean handling and default values

- Change id default from undefined to null for consistent serialization
- Use !! for boolean coercion instead of === true for Storage.get
- More robust handling of truthy values

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>
2026-02-09 16:35:58 -05:00
Copilot 23177578b2 Add context menu support for table rows in settings (#11378)
* Initial plan

* Add context menu support for table rows in settings

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

* Fix file formatting

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

* Add context menu support to all settings tables

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

* refactor

* Reuse hooks

* EmojiMenu

---------

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-02-09 14:43:17 -05:00
Copilot 40bbfc78cd Refactor: Extract Redis cache key generation to RedisPrefixHelper (#11376)
* Initial plan

* Refactor Redis cache keys: delegate CacheHelper to RedisPrefixHelper and update callers

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

* Add JSDoc documentation to getCollectionDocumentsKey method

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

* Remove unused indirection

* Remove mock

---------

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-02-09 14:03:02 -05:00
Tom Moor dc9aad99e9 fix: Test snapshot (#11395) 2026-02-08 18:28:35 -05:00
Copilot ea9e9675fb Fix document creation routing to use correct parameter name (#11369)
* Initial plan

* Fix: Use correct route parameter name in DocumentNew

The route parameter is 'collectionSlug', not 'id'. This caused documents
created through /collection/:collectionSlug/new to not have a collectionId,
making them go to drafts instead of the collection.

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-02-08 18:21:06 -05:00
github-actions[bot] db42af7fe1 chore: Compressed inefficient images automatically (#11394)
Co-authored-by: tommoor <tommoor@users.noreply.github.com>
2026-02-08 16:26:32 -05:00
Tom Moor eb59aed5b7 test: Fix snap (#11391) 2026-02-07 22:07:41 +00:00
Tom Moor 8209f56e56 perf: Reduce database contention in ImportTask (#11361)
* perf: Reduce database contention in ImportTask

* fix: Reuse transaction when available
2026-02-07 17:02:35 -05:00
Copilot a097676e9c Map Notion toggle blocks to container_toggle nodes (#11371)
* Initial plan

* Add toggle block support to Notion importer

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

* Support toggle headings

---------

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-02-07 16:40:06 -05:00
Copilot 2da35f2504 Add dark mode logo support to README (#11375)
* Initial plan

* Add dark mode logo support to README

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-02-06 06:32:29 -05:00
Tom Moor b17f3c7f6c fix: Missing outline on focused editable (#11362)
* fix: Missing outline on focused editable

* tighter fix
2026-02-04 23:59:20 -05:00
Tom Moor 30d1900f41 chore: Refactor of activeDocumentId (#11144)
* wip: Refactor of activeDocumentId

* Remove legacy usage from definitions/collections
2026-02-04 19:37:34 -05:00
Translate-O-Tron 44eaf97ea4 New Crowdin updates (#11296)
* 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 Dutch translations from Crowdin [ci skip]

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

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

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

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

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

* fix: New German translations from Crowdin [ci skip]
2026-02-04 19:11:46 -05:00
Copilot d9e15d2441 Fix passkey registration with non-standard HTTPS ports (#11329)
* Initial plan

* Add getExpectedOrigin helper to handle non-standard ports in passkeys

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

* Fix linting error in passkeys test

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

* Fix trailing whitespace in passkeys.ts

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

* Export getExpectedOrigin and test it directly

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-02-04 18:43:34 -05:00
Copilot 7b2cfbde5b Add document highlight colors to highlight menu (#11345)
* Initial plan

* Add document highlight colors to highlight menu

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

* Add tests for getDocumentHighlightColors function

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

* Optimize performance based on code review feedback

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

* perf: Prevent calculation on every selection change

* Add the same logic for table background colors

---------

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-02-04 18:42:27 -05:00
Copilot d2f54502f3 Add sticky positioning to table headers (#11353)
* Initial plan

* Add sticky positioning to table headers

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

* Fix table header background for sticky positioning

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

* wip

* 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>
2026-02-04 08:20:09 -05:00
Tom Moor 7cd00f3465 fix: Prefetch document structure for starred docs (#11355)
* fix: Prefetch document structure for starred docs

* fix: Disclosure does not work correctly
2026-02-03 20:40:03 -05:00
Tom Moor e1598c08d8 fix: Table with colspan should sort without breaking (#11351)
Table with rowspan should be unsortable
2026-02-03 11:39:44 -05:00
Tom Moor 0b85cbd586 fix: Remove hanging toggle sidebar button with no functionaliyu (#11350)
closes #11348
2026-02-03 07:34:35 -05:00
Tom Moor 369b309527 fix: Catch-all on root share too broad (#11346) 2026-02-03 02:57:29 +00:00
Copilot f48a34fbdc Add custom color picker for highlight mark (#11337)
* Initial plan

* Add custom color picker for Highlight mark

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

* Address code review feedback - fix active state and add color validation

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

* Update app/editor/components/HighlightColorPicker.tsx

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

* Add proper hex color validation and remove command prop

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

* Use existing isHexColor from class-validator

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: Tom Moor <tom@getoutline.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-02 19:26:28 -05:00
Copilot 6398829106 Fix: Remove permanently deleted documents from local store (#11344)
* Initial plan

* Fix: Hard delete documents from store on permanent deletion

When documents are permanently deleted, they need to be completely
removed from the local MobX store, not just soft-deleted. This change
ensures that after permanent deletion, the document is immediately
removed from the UI without requiring a page reload.

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

* Fix: Also hard delete documents in emptyTrash method

The emptyTrash method also needs to hard delete documents from the
store after the backend permanently deletes them, not just soft delete.

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-02-02 19:09:38 -05:00
Copilot 22ffda7078 Add group members popover to sharing dialogs (#11338)
* Initial plan

* Add GroupMembersPopover component and integrate with sharing components

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

* Fix type safety in GroupMembersPopover by using PAGINATION_SYMBOL

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

* Add explanatory comment for type assertion in GroupMembersPopover

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

* Styling

---------

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-02-02 19:08:58 -05:00
Tom Moor 7c6c833d91 fix: Safari print offset (#11339) 2026-02-01 22:38:35 -05:00
Tom Moor 9b4af148a4 fix: Add parseDOM rules for toggles (#11335) 2026-02-01 22:08:58 -05:00
Tom Moor a80dc8f103 Map background-color to Highlight mark (#11336) 2026-02-01 22:08:48 -05:00
Tom Moor 72c2664478 Allow currency sorting in tables (#11332)
* Allow currency sorting in tables

* Update shared/editor/commands/table.ts

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-01 10:43:10 -05:00
Tom Moor 9aa666e708 fix: Restore col/row selection after sort/align (#11333) 2026-02-01 10:43:01 -05:00
Tom Moor 9c38ce71dc chore: Rename DATABASE_READ_ONLY_URL (#11334) 2026-02-01 15:41:57 +00:00
Tom Moor bb128318da perf: Remove turndown (#11331)
* Remove turndown

* Refactor htmlToProsemirror

* fix: Bug in CSV import

* refactor
2026-01-31 20:56:36 -05:00
Tom Moor 51dd516679 fix: Expand collapsed toggles that contain current highlighted search (#11330)
closes #10479
2026-01-31 17:30:24 -05:00
Tom Moor 49f918e7f5 perf: Remove Markdown conversion when importing HTML (#11315)
* wip

* fix: Remove unused variable in ProsemirrorHelper.extractEmojiFromStart

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test

* DRY

* Restore cleanup

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 16:26:33 -05:00
Tom Moor 06d0932e0d fix: Choosing from suggestion menu with mouse inserts at incorrect position (#11327)
* fix: Minor styling fixes in suggestion menu

* fix: Selector toolbar click outside logic affects insertion position

closes #11309
2026-01-31 15:53:33 -05:00
Tom Moor 03493ea3dc fix: Non-members cannot see public ACL attachments (#11326)
closes #11324
2026-01-31 15:53:03 -05:00
Tom Moor 446a0e1071 fix: Do not show embed option for unembeddable links (#11323)
* fix: Do not show embed option for unembeddable links

* test
2026-01-31 13:57:25 -05:00
Tom Moor af6eb6b6ec feat: Allow cmd+enter to toggle all checkboxes in selection (#11322) 2026-01-31 08:17:29 -05:00
Tom Moor 08fb38148e fix: Share panel state for drafts in shared parent (#11320) 2026-01-30 11:48:57 -05:00
Tom Moor 7e3f71d67e perf: Minor React performance optimizations (#11319)
- Replace .map().includes() with .some() for early exit in DocumentLink and WebsocketProvider
- Use Set for O(1) lookups instead of array.includes() in Suggestions filter
- Use lazy useState initializer in DomainManagement

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 12:47:34 +00:00
Tom Moor 3e1daf4ab8 chore: Remove babel from production deps (#11318) 2026-01-30 07:14:22 -05:00
Tom Moor 0354548f22 perf: Improve slow query in CleanupDeletedDocumentsTask (#11317) 2026-01-30 06:50:30 -05:00
Tom Moor ee2054f333 fix: 'New draft' from command menu leads to 'Not found' (#11316) 2026-01-30 11:37:10 +00:00
Tom Moor 2c3650ec4f Shortcut help (#11313)
* Allow filtering keyboard shortcuts by heading

* Allow opening keyboard shortcut help from querystring
2026-01-30 11:17:14 +00:00
Copilot 9858d160d5 Separate user input errors from internal errors for Sentry reporting (#11308)
* Initial plan

* Add explicit error categorization with isSentryReported property

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

* Rename isSentryReported to isReportable

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

* Update Discord plugin errors to use isReportable

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-30 05:55:28 -05:00
Tom Moor f065db5415 fix: Editor focus loss on text link interaction (#11310)
* fix: Editor focus loss on text link interaction

closes #11294

* fix: Toggle icon, tooltip
2026-01-30 05:54:26 -05:00
Copilot a4784ca2d2 Fix text truncation with emoji icons in sidebar (#11307)
* Initial plan

* Fix emoji truncation in SidebarLink by always applying ellipsis styling

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-30 05:54:14 -05:00
Tom Moor e1e82ef4ac perf: Remove remaining usage of problematic findOrCreate (#11306)
* perf: Remove remaining usage of problematic findOrCreate

* fix spread order
2026-01-29 22:54:09 -05:00
Tom Moor c853917502 perf: More popularity scoring performance improvements (#11305)
* perf: More popularity scoring performance improvements

* Update server/queues/tasks/UpdateDocumentsPopularityScoreTask.ts

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-29 22:25:18 -05:00
Tom Moor 4aa4868a54 feat: Add Heroku Review Apps support for preview deployments (#11304)
- Add pr-predeploy script and review environment config in app.json
- Use smaller addon tiers for review apps to reduce costs
- Auto-derive URL from HEROKU_APP_NAME when URL is not set
- Make URL optional for review apps

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 21:08:35 -05:00
Copilot 7b6293637c Add diff language highlighting for code blocks (#11301)
* Initial plan

* Add diff language highlighting support

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

* Styling

---------

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-29 08:44:18 -05:00
Tom Moor 626b3a79b1 Limit parameters in events.list for non-admins (#11302) 2026-01-29 08:34:50 -05:00
Salihu 30fff9b070 feat: Sort on search screen (#11242)
* sort document searches

* remove tests

* add tests

* minor fix

* suggested changes

* requested changes

* minor fixes

* fix broken tests

* Style tweaks

---------

Co-authored-by: Tom Moor <tom@getoutline.com>
2026-01-28 23:12:33 -05:00
Tom Moor 9a7d6c5fc8 fix: Cannot download mermaid image from lightbox (#11300) 2026-01-28 22:28:25 -05:00
Tom Moor dbfe9eb0e4 perf: More tweaks to popularity scoring job (#11293) 2026-01-27 20:06:34 -05:00
Tom Moor 70b007d534 v1.4.0 2026-01-27 18:42:55 -05:00
Tom Moor 5ea3d57352 Update version.ts 2026-01-27 18:36:37 -05:00
Apoorv Mishra f1e8c0bcc2 fix: Colocate preset color codes and their names in color utils (#11289) 2026-01-27 18:08:19 -05:00
Tom Moor bdb97d63d8 fix: Improve error when passkey is missing in db (#11291) 2026-01-27 22:50:50 +00:00
Apoorv Mishra c6177bc4d8 fix: prevent history search dropdown from opening upon cmd-k on selected text (#11288) 2026-01-27 17:47:33 -05:00
Raphael 8324859de9 fix(passkeys): show Passkeys settings to non-admin users (#11286) 2026-01-27 08:14:36 -05:00
Translate-O-Tron 2957ce6c55 New Crowdin updates (#11216)
* fix: New Czech translations from Crowdin [ci skip]

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

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

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

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

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

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

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

* fix: New Italian 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 Korean 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-26 22:51:39 -05:00
Tom Moor 529b7e45de fix: Restore policy for documents too broad (#11279) 2026-01-27 03:46:02 +00:00
Tom Moor fcd40a93a4 feat: Make markdown importer generic (#11278) 2026-01-26 22:11:04 -05:00
Tom Moor 00fb4d1af7 chore: Update node style imports (#11277)
- crypto → node:crypto
  - fs → node:fs
  - fs/promises → node:fs/promises
  - path → node:path
  - http → node:http
  - https → node:https
  - stream → node:stream
  - buffer → node:buffer
  - url → node:url
  - os → node:os
  - net → node:net
  - dns → node:dns
  - events → node:events
  - readline → node:readline
  - querystring → node:querystring
  - util → node:util
2026-01-26 20:51:50 -05:00
Tom Moor 838a1e7428 fix: Suppress ECONNRESET (#11276) 2026-01-27 01:31:52 +00:00
Apoorv Mishra ebe0e5bc3a Replace react-color with react-colorful (#11269)
* enable passing alpha as prop to color picker

* replace react-color with react-colorful

* mv to shared

* fix: double debounce

* rename to IconColorPicker

* cleanup IconColorPicker

* cleanup ColorPicker
2026-01-27 06:57:11 +05:30
Tom Moor 126e876f0c fix: Allow custom emoji in TOC (#11275) 2026-01-26 19:04:08 -05:00
Apoorv Mishra e9ed1ba5d1 Toggle block (#8317)
* fix: modify input rules for heading to wrap it in a toggle block

* fix: leave heading node untouched

* feat: add toggle block menu item

* feat: first prototype

* toggle_head and toggle_body

* fix: indent toggle block

* fix: cleanup

* fix: allow only one heading or one para inside toggle head

* fix: cleanup

* fix: cursor becomes invisible as soon as toggle block is inserted

This happened because the containing paragraph had ~0px as width which
hid the cursor. I attemped setting the `style.minWidth` to 1px for the
containing `span` and cursor became visible. Hence, set the `flexGrow`
prop so that it occupies all the avaible space.

* fix: keep the toggle button vertically center-aligned

Adjusts the toggle button and keeps it center-aligned vertically as the toggle head's
content node changes from, say a paragraph to a heading(of any level),
or the other way around...

* chore: style using css

* fix: nesting of toggle blocks

`toggleWrap` resorted to lifting out the active node when attempting
to create a new nested toggle block inside existing toggle block, which
made it impossible to nest toggle block. Hence, bypassing the
`toggleWrap` flow in favor of `wrapIn`, which provides nesting of toggle
blocks.

* fix: assign unique id to each toggle head node

This will be later used to persist toggle state of the toggle block

* feat: attempt at using node view for toggle block

* fix: get rid of nanoid, we can use existing uuid pkg

* feat: plugin to manage toggling behaviour

This includes a plugin which, for now handles the following behaviours:

1. Sync collapsed state from localStorage, and correspondingly initialize
   decorations for all the toggle block nodes in doc
2. Handle the fold/unfold behaviour of toggle block, triggered through
   the toggle button from within the node view

* fix: don't trigger toggle behavior if secondary button on mouse is pressed

* fix: restore decorations which are removed upon `joinBackward`

When the selection is at the start of block node just
after the toggle block node, pressing backspace triggers `joinBackward`
command which attempts to "join" this block node with the toggle block
node just before it. The `joinBackward` command works by adding a
replacement step, which, in turn drops the decorations in the affected
range. As a result, the toggle block collapses. In order to prevent the
collapse, we restore back the dropped decoration to its corresponding
node(if it exists).

* chore: can find spec using id now

* chore: this.name in favor of hardcoding

* fix: collapse all children of toggle block except the first

As a result of setting `content: block+` for the toggle block node, all
its children are rendered flat in DOM, making it difficult to distinguish which
one corresponds to its first child node. This, in turn, makes it hard to
identify which one should not be collapsed when pressing the toggle
button to collapse. The solution here is to wrap the first child node in
a separate DOM node via a decoration. So, hopefully, we don't need to
break up `toggle_block` node as one containing `toggle_head` and
`toggle_body` nodes and setting `content: block+` should suffice.

* fix: properly restore lost decorations

A weird issue surfaced when a toggle block was erased and then the erase
was undone. It was observed that all the toggle blocks ended up being
collapsed and the one restored by undo had lost the decoration on first
child(which prevents first child from hiding), and, as a result ended up
with all its children hidden.

Here, we collect all dropped decorations in a single array and later add them
back in a single pass.

* fix: we don't yet need  and  as nodes

* fix: command mapped to `Backspace` key for `ToggleBlock` failed to invoke

It happened because of `Math` node being placed before `ToggleBlock` in
the array of exported nodes, which caused the `Math` node's `Backspace`
handling command to _successfully_ run before `ToggleBlock` node's
`Backspace` handling command. Therefore, moving `ToggleBlock` before
`Math` fixed the issue.

* fix: nested toggle blocks behaving differently than top level ones

It was observed that the decoration which wraps the first child of a
toggle block in a separate DOM wasn't being restored as and when
expected for nested toggle blocks while it worked fine for top level
toggle blocks. The reason was that the `findBlockNodes` skips the nested
blocks in its default behavior, so, passing `descend` as `true` fixed
the issue.

* feat: lift all children up upon backspacing at the start of toggle block

* fix: remove/reapply decorations which get misaligned as a result of doc
change

The decoration which is applied to the first child of toggle block, may
get misaligned and go out of sync with the corresponding decoration on
the toggle block as a result of document changes. Here, we simply remove
*all* the decorations and then reapply to their target i.e, first child of each
toggle block.

We're trading off perf with implementation simplicity here since we
don't actually need to remove all decos, only the misaligned ones.

An alternate solution here might be to map the first child decoration
in accordance with corresponding decoration on the toggle block, such
that the first child deco _never_ goes out of sync with its parent
decoration. As for now, not sure how that could be achieved.

* fix: bring cursor out and set at the end of toggle block head upon folding

* fix: cleanup

* fix: if cursor ends up within the hidden range of toggle block while it is folded, immediately unfold it

* fix: check parentNode

* feat: `prependParagraph` and `splitHead`

* fix: cleanup

* fix: handle `Delete` when the node just after the cursor is a toggle
block

* fix: cleanup

* fix: restrict toggle block head to contain heading or paragraph

Certain behavioral aspects of toggle block are implemented assuming head
to be a heading or paragraph, for simplicity. Makes sense for majority
of use cases but still something presumptuous about user expectation.
Proposition is to lauch it with this restriction and see if the users
actually start requesting otherwise. Till then, this keeps things
simple.

* feat: `Tab` into a toggle block, `Shift-Tab` out of toggle block

As part of this, we've modified handling `Enter` within a toggle block
in a way that prevents it to trigger `liftEmptyBlock`, so that the
cursor remains within the toggle block body and is only taken out of it
when pressing `Shift-Tab` combo.

* fix: Toggle block not unfolding when a node having `block+` content
attempted to `Tab` into it

* feat: beautification

* feat: markdown for `toggle_block`

This declares markdown spec for a toggle block, which enables users to
download a doc containing toggle blocks, as markdown. Also, supports
importing a markdown doc containing markdown corresponding to a toggle
block.

* fix: margins between toggle block contents

* fix: `Action.INIT` for publicly shared and deleted docs

It was observed that decorations weren't initialized for publicly shared
and deleted docs because of init being under the `docChanged` cond. This
change fixes the issue.

* fix: all toggle blocks end up folded when navigated from collection to
doc page

This happened because `{ fold: true }` was forcibly set. This is fixed
by applying decorations in accordance with the fold state fetched from local storage.

* fix: disable overflow being set to scroll on Brave

* fix: cleanup

* fix: prevent joining two toggle blocks when backspaced from the start of
a text block between them

Consider two toggle blocks with a text block between
them. If backspaced from the beginning of the text block, the toggle
block below is joined to the toggle block before along with the text
block, because of https://github.com/ProseMirror/prosemirror-commands/blob/20c7d42ab8b5d8642fb9efc6261b7541c9dc23c2/src/commands.ts#L468-L469. On the contrary, what's desirable is just joining back of the text block, retaining the toggle block below as it is.

* fix: sync collapsed state across browser tabs

* fix: cleanup

* fix: upon unfold, append an empty para if toggle block's body is empty

* fix: unfold upon `Enter` if the toggle block body is empty

* feat: placeholder

* feat: inputRule

* feat: `Mod-Enter` shortcut to toggle

* fix: do not split when body is empty

* fix: do not unfold is head is empty

* fix: assign uuid to newly split toggle block

* feat: list keyboard shortcuts

* fix: replace with `wrapIn`

* fix: `container_toggle_block` -> `container_toggle`

* fix: importing a markdown doc with toggle blocks let to them being created without ids

* fix: pressing `Enter` at the end of list item within toggle block should
create a new list item below

* fix: repeated backspacing from an empty list item within toggle block

* fix: prevent joining back when input rule is matched

* fix: prevent button from shrinking when an image is added under content area

* fix: tsc

* Fixes:

1. Toggle block starts off unfolded when created

2. Trigger `liftEmptyBlock` when a toggle block consists of just an empty head, without any body

3. `Shift-Tab` behavior confuses when all nodes following the cursor position, inclusive of the one holding cursor, are empty. It seems at first, that it should simply outdent except it doesn't, because the node holding cursor isn't the `lastChild` of toggle block.

So, `Shift-Tab` behavior is modified such that all nodes following the cursor, up till the last node of toggle block, should be lifted out of it

* Fixes:

1. Upon pressing Enter, lift out all children of toggle block if cursor lies at the start,
   there's no text content
2. Prevent lifting out of an empty block when it's a direct child of a toggle
   block

* fix: lint

* fix: placeholder to inform how to exit toggle block

* fix: prevent tables within toggle block from horizontally scrolling

* fix: align toggle block with lists

* fix: push toggle block down if it ends up as the first child of a list item

* fix: don't consider toggle body empty if it consists of an empty table or notice

* fix: CollapsedIcon

* fix: Delete press

* fix: mainly early return when `deleteBarrier` or `joinMaybeClear`
succeed, rest is cleanup

* fix: rename

* fix: simplify commands

* fix: simplify commands

* fix: remove unused commands

* fix: lint

* chore: cleanup `splitBlockPreservingBody`

* chore: `sinkBlockInto` -> `indentBlock` & `liftConsecutiveBlocks` ->
`dedentBlocks`

* fix: cleanup related to `getUtils`

* fix: simplify `bodyIsEmpty`

* fix: no need of separate func

* fix: `bodyIsEmpty` -> `ToggleBlock.isBodyEmpty` && `headIsEmpty` ->
`ToggleBlock.isHeadEmpty`

* fix: cleanup

* fix: move to utils

* fix: update comment

* fix: rename

* fix: update comment

* fix: update comment

* fix: comments

* fix: cleanup

* fix: `splitBlock` was problematic here because it would run for block
nodes other than toggle block. As an example, consider a `blockquote` containing and empty
para. Here, `liftEmptyBlock` should run upon `Enter`, instead
`splitBlock` runs. Same goes for other nodes like task lists. This
commit fixes the issue.

* fix: wrap heading and its children together within toggle block

* trigger ci

* fix: copy for used funcs from sorted-array-functions

* fix: remove pkg

* fix: remove pkg

* fix: rearrage dep

* fix: restore yarn.lock from main branch

* fix: restore yarn.lock from main branch

* feat: triggering toggle block on an already wrapped toggle block should unwrap it

* fix: get rid of `HeadingTracker` in favor of directly querying doc

* fix: headings under toggle blocks weren't tracked in toc

* fix: don't hide the anchor associated with a heading otherwise the heading can't be scrolled to

* fix: unfold toggle block when hidden heading is clicked from toc

* fix: backspacing into an unfolded toggle block should attempt to join with the last node of body

* `Tab` in a selected `embed` within a folded toggle block. Notice that toggle block remains folded. It should be unfolded as the `embed` is pushed inside. Same goes for some other nodes like `math_block`

* Can only `Tab` in and `Shift-Tab` out once when a node of type `attachment|video|hr` is the last node of a toggle block. Beyond once, pressing those keys have no effect.

* fix: Server build

* refactor

* Combine enum

* perf: Merge plugins, avoid multiple appendTransaction

* Remove getUtils

* fix: Default new toggle blocks to closed

* fix: Infinite loop

* refactor: Separate ToggleBlockView

* Centralize class names

* fix: Align nested headings with lists

* fix: Toggle block disclosure different sizes

* fix: Toggle flash when clicking fold button while focus within toggle content

* refactor: Plugin keys

* Exit toggle block when pressing Enter in the last empty paragraph within the body

* Placeholders

* fix: Fallback line-height, font-size for empty title

* fix: Incorrect decoration on title node change

* doc

* fix: Enter in last list item in toggle body exits

* fix: Allow toggling headings in diff viewer

* fix: Toggle button animation on first load

---------

Co-authored-by: Tom Moor <tom@getoutline.com>
2026-01-26 18:26:41 -05:00
Twometer f6d46a07ec feat: Use diagrams.net theme that matches editor theme (#11267) 2026-01-25 14:19:27 +00:00
Tom Moor ef0f8301bc Allow non-platform Passkeys (#11265) 2026-01-25 03:31:05 +00:00
Apoorv Mishra abd7abcc18 Choose table cell background (#10930)
* feat: table cell bgcolor

* fix: review

* fix: cleanup

* fix: new color picker

* fix: transparentize bg preset colors

* fix: show selected color in color list

* fix: pass active color to picker

* fix: make color picker command agnostic

* fix: tsc

* fix: table row and col background menu

* getColorSetForSelectedCells

* toggleCellBackground to toggleCellSelectionBackground

* cellHasBackground to cellSelectionHasBackground

* useless spread

* presetColors

* get rid of hasMultipleColors

* get rid of customColor

* be explicit in passing color

* alpha controls

* remove new highligh command

* DRY DottedCircleIcon

* restore ff fix

* merge createCellBackground into updateCelllBackground

* default color

* Merge

---------

Co-authored-by: Tom Moor <tom@getoutline.com>
2026-01-24 22:13:00 -05:00
Tom Moor 4b146de583 fix: Hide tooltip in ToolbarMenu when submenu is open (#11263) 2026-01-24 14:26:33 -05:00
Tom Moor b73bd2a621 feat: Highlight search results when navigating to document from search (#11262)
* feat: Highlight search term on docs from search results

closes #8251

* Use query string instead

* Search highlight on public shares
2026-01-24 19:19:56 +00:00
Tom Moor db179a8086 fix: Comment shortcut without selection (#11261)
closes #11073
2026-01-24 11:15:35 -05:00
RudraPrasad001 6d6a42b805 fix : Handles Adding Text below pictures for doc import (#11257) 2026-01-24 15:09:45 +00:00
Tom Moor 74ce4052c4 fix: Unused subdomains should redirect to root (#11259)
* fix: Unused subdomains should redirect to root

* test
2026-01-24 09:59:20 -05:00
Tom Moor 3118721b21 fix: Split cell should work without accurate selection (#11256)
closes #11248
2026-01-23 23:29:54 -05:00
Copilot 07514cb692 Allow http:// loopback redirect URIs for native OAuth apps (RFC 8252) (#11255)
* Initial plan

* Add RFC 8252 loopback redirect URI support for native OAuth apps

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-23 21:54:59 -05:00
Tom Moor b9c065f0ad fix: Avoid clearing local cache on token expiry (#11246) 2026-01-23 00:05:07 +00:00
Tom Moor d16bf03e47 fix: Row inserted incorrectly with merged first column cells (#11245)
closes #11241
2026-01-22 23:55:07 +00:00
Tom Moor 747a833a4d fix: Remote edit kicks out of Mermaid editing state (#11244)
closes #11240
2026-01-22 18:53:28 -05:00
Tom Moor d49b223126 fix: Mermaid diagram rendering on first paste (#11243)
closes #11228
2026-01-22 23:29:12 +00:00
Tom Moor 3df2d362b6 fix: Spurious link appears when typing enter at the end of a list with a mention (#11238) 2026-01-22 11:23:36 -05:00
Tom Moor 86811a5556 fix: Hanging read stream handle (#11237)
* fix: Hanging read stream handle

closes #11236

* comment
2026-01-21 22:53:49 -05:00
Tom Moor 017660337c fix: Sp in translation source (#11235) 2026-01-21 13:09:51 +00:00
Tom Moor ed03b9d548 fix: ImportJSONTask file resolution (#11231) 2026-01-21 04:16:41 +00:00
Tom Moor f423171a6d stash (#11215) 2026-01-20 18:11:26 -05:00
Tom Moor 1cc4d879dc fix: Sync API changes to clients in realtime (#11186)
* fix: Sync API changes to clients in realtime

* Add editMode parameter

* rename

* Add error handling

* refactor
2026-01-19 13:28:44 -05:00
Nguyễn Anh Bình f009375fbc fix: skip ACL parameter for GCS uniform bucket-level access (#11222)
When using Google Cloud Storage with uniform bucket-level access enabled,
object-level ACLs are not supported and cause InvalidArgument errors.

This change makes the ACL parameter conditional in both `store()` and
`getPresignedPost()` methods, allowing users to set `AWS_S3_ACL=` (empty)
to disable object-level ACLs.

Fixes #11221
2026-01-19 13:24:05 -05:00
Tom Moor f0ba8c819f fix: Deserialization of markdown checkboxes in table cells (#11217)
* Checkboxes in table cells

* test
2026-01-18 23:06:24 -05:00
Tom Moor 06e005cab9 fix-trigger (#11214) 2026-01-18 15:32:29 +00:00
Tom Moor f12e865e5c Use drawer menu for suggestions on mobile (#11213)
* Use drawer menu for suggestions on mobile

* fix: Content injection from drawer
2026-01-18 09:47:57 -05:00
Tom Moor 24f377d945 Updated sort icons (#11209)
* Update sort icons

* SortManualIcon
2026-01-17 19:41:48 -05:00
Tom Moor 0e21552b34 fix: Gracefully handle http links in imported content (#11210) 2026-01-17 19:41:41 -05:00
Tom Moor a9493ecd54 fix: Infinite recursion in dd-trace dep (#11211)
closes #11195
2026-01-17 19:41:33 -05:00
Luca Wimmer 06938561a6 fix: add date sorting support to table columns (#11198)
* fix: add date sorting support to table columns

* fix: fixed lint errors, removed unnecessary non-null check

* fix: European slash format should not always be preferred

---------

Co-authored-by: Tom Moor <tom@getoutline.com>
2026-01-17 16:32:35 -05:00
Translate-O-Tron 7b9e1b1c57 New Crowdin updates (#11074)
* 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]

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

* fix: New German 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 French 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]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: New Czech translations from Crowdin [ci skip]
2026-01-17 16:32:18 -05:00
Tom Moor 60eb3f8503 fix: Edit diagram button in read-only mode (#11207)
* fix: Edit diagram button in read-only mode

closes #11206

* tsc
2026-01-17 16:31:45 -05:00
Tom Moor cc14f18fd2 fix: Incorrect reuse of NodeViews with drag and drop (#11208)
closes #11199
2026-01-17 16:31:36 -05:00
Tom Moor 8b18d5e93f Merge branch 'release/1.3.0' 2026-01-17 11:14:00 -05:00
Tom Moor 0780fe2347 v1.3.0 2026-01-17 11:13:21 -05:00
yy 0230e1c5a5 chore: fix typos in comments and messages (#11203) 2026-01-17 10:50:06 -05:00
Hemachandar 5584089441 feat: Figma integration (#11044)
* OAuth

* store logo

* unfurl support

* refresh token

* support for list

* embed list

* mention menu for all embeds in a list

* multi-level list

* logo

* account level connection

* tsc

* Update Icon.tsx

* coderabbit feedback

* RFC 6749 suggestion

---------

Co-authored-by: Tom Moor <tom@getoutline.com>
2026-01-15 20:27:00 -05:00
Tom Moor bffd11b593 fix: urls.unfurl thundering herd (#11194)
* Add tracing around unfurl calls

* fix: Prevent thundering-herd unfurls

* tracer -> traceFunction
2026-01-15 19:52:26 -05:00
Copilot 77ad224709 Fix S3 presigned URL expiration exceeding AWS 7-day limit (#11191)
* Initial plan

* Fix S3 presigned URL expiration exceeding AWS 7-day limit

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

* Add comprehensive tests for S3 presigned URL expiration limits

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-15 19:51:21 -05:00
Tom Moor 4d3f9bf0b4 fix: includeTitle option does not work with collection input (#11185)
* fix: includeTitle option does not work with collection input

* test
2026-01-14 23:48:06 -05:00
Tom Moor ec87cb0308 fix: alt+shift header selection in Safari (#11184)
* fix: Header selection in Safari

* Move browser constants to constants
2026-01-14 21:29:02 -05:00
Tom Moor 9fd54fd83a fix: Render list breaks as <br> tags inside tables in Markdown output (#11183)
* fix: Render list breaks as <br> tags inside tables in Markdown output
* fix: Minimum cell width based on header

closes #10334
2026-01-14 21:03:47 -05:00
Tom Moor 945d9908ae feat: Allow public shares to be retrieved as MD with accept text/markdown header (#11182) 2026-01-14 20:17:32 -05:00
Tom Moor fd9216541c chore: Add tests covering fixes in #11178 (#11179) 2026-01-14 08:26:26 -05:00
Tom Moor c0ac317329 fix: Various issues with tables (#11178)
* fix: This means the single-column table fix never actually works.

* fix: parseInt in map passed index to radix

* fix: Column sizing

* fix: Incorrect corner radius on single column table
2026-01-13 21:04:02 -05:00
Tom Moor f3d1d4e0b2 fix: Center text on failed images (#11177) 2026-01-13 20:10:00 -05:00
Tom Moor 094ff17f74 fix: Update auth provider instead of throwing (#11131) 2026-01-13 19:58:47 -05:00
Tom Moor d2c70ea5fa fix: Theme override does not persist between navigations (#11176) 2026-01-13 19:30:58 -05:00
Tom Moor 1675c42364 Remove documents.diff endpoint (#11175) 2026-01-13 19:11:13 -05:00
Tom Moor 831f038e3c perf: Vendorize rfc6902 (#11174)
* perf: Vendorize rfc6902 with fix
2026-01-13 18:13:42 -05:00
Tom Moor 08c97389b3 Improve download toast to show immediate link where possible (#11167)
* Improve download toast to show immediate link where possible

* fix: Missing translations
2026-01-13 07:27:48 -05:00
Tom Moor d7272b242c fix: Empty notification panel is too tall (#11166)
* fix: Misaligned empty state

* fix: Empty state height

* Update translation.json
2026-01-12 23:48:08 -05:00
Tom Moor 650c3b5ead fix: Minor styling fixes (#11164)
* fix: Shrink of info icon in share popover

* fix: Hover styling on comment actions
2026-01-13 01:04:00 +00:00
Tom Moor b343e70b84 Adjust list spacing (#11163) 2026-01-13 00:48:21 +00:00
Tom Moor 7532c428bd feat: Auto-embed plain urls in API usage (#11148)
closes #9375
2026-01-12 19:44:58 -05:00
Tom Moor 536888b076 fix: Context menu navigates to element (#11162) 2026-01-12 23:42:08 +00:00
Tom Moor 448ba9c0a6 Update .env.sample 2026-01-12 17:08:51 -05:00
Tom Moor d5c7e0f748 fix: No mentions in comments (#11156) 2026-01-12 09:38:46 -05:00
Tom Moor dfc2102450 fix: Export no longer includes private collections (#11153) 2026-01-11 22:06:14 -05:00
Newton 2d1092a2ca feat: Use signed urls in "copy as markdown" (#10821)
* feat: Use signed urls (ttl configurable) for attachment urls in "copy markdown"

* make signed urls a parameter, not a instance configuration

* feat(fix): copy as markdown: refactor toMarkdown (use signed urls)

* attempt to fix tests failing

* adjust markdown exporting in revisions too

* formatting

---------

Co-authored-by: Tom Moor <tom@getoutline.com>
2026-01-10 21:42:44 -05:00
Hemachandar 50759d40e8 feat: Option to export nested documents (#9679)
* add migration file

* documents.export API

* download dialog

* file ops list item

* export task

* download modal styling

* cleanup

* lint

* Restore individual download actions

---------

Co-authored-by: Tom Moor <tom@getoutline.com>
2026-01-10 21:19:33 -05:00
Tom Moor bcee4893f4 perf: Add timeout and optimize URL unfurl performance (#11149)
* perf: Add timeout and optimize URL unfurl performance

Fixes issue where urls.unfurl endpoint could take 15+ seconds due to external API timeouts and sequential plugin execution.

Changes:
- Add timeout support to fetch utility with AbortController (defaults to no timeout, configurable per request)
- Add 10 second timeout to Iframely plugin requests
- Add early URL pattern validation to GitHub and Linear plugins to avoid unnecessary database queries
- Add try-catch error handling to URL parsing in GitHub and Linear plugins

This reduces worst-case unfurl time from 15+ seconds to ~10 seconds maximum, and eliminates unnecessary overhead for URLs that don't match plugin patterns.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* lint

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-10 18:03:19 -05:00
Tom Moor 23d4374cb0 fix: Inline LaTeX active border on FF (#11147) 2026-01-10 17:22:49 -05:00
Tom Moor 7a7c0ff082 fix: Clear document cache on logout (#11135)
* fix: Clear document cache on logout

* Update AuthStore.ts
2026-01-10 16:38:17 -05:00
Tom Moor 7663c2a643 feat: Add backlinks to publicly shared documents (#11141)
* Add backlinks support for publicly shared documents

Include backlinks in the documents.info API response for publicly shared documents, filtering to only show backlinks that exist within the shared tree.

Changes:
- Add findSourceDocumentIdsInSharedTree method to Relationship model to find backlinks within allowed document IDs
- Export getAllIdsInSharedTree helper from shareLoader for reuse
- Update presentDocument to accept and include backlinkIds in response
- Modify documents.info endpoint to fetch and include backlinks for public shares
- Add backlinkIds property to Document model and update backlinks getter to use it when available

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* refactor

* refactor

* wip

* tsc

* fix: Signed-out view throw, spacing

* revert

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-10 16:18:45 -05:00
Tom Moor c468019204 feat: SVG support for diagrams.net (#11128)
* feat: SVG support for diagrams.net

closes #11118

* Remove unused method
2026-01-10 14:31:27 -05:00
Tom Moor 5279a58753 feat: Dragging of table rows and columns (#11138)
* wip: Dragging of table rows and columns

* wip

* grabbing cursor

* refactor

* Update shared/editor/plugins/TableDragDropPlugin.ts

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

* Update shared/editor/plugins/TableDragDropPlugin.ts

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-10 12:32:36 -05:00
Tom Moor 221a7ce19e Change Mermaid code block language from "mermaidjs" to "mermaid" (#11132)
- Add "mermaid" as the primary language identifier for new diagrams
- Maintain backward compatibility with existing "mermaidjs" blocks
- Create isMermaid() helper function for consistent language checking
- Update all Mermaid-related code to support both language variants
- Update CSS selectors to style both "mermaid" and "mermaidjs" blocks
- Update server-side HTML export to handle both language formats
- Add tests to verify backward compatibility

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

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-10 12:30:56 -05:00
Tom Moor ae59a8b25e fix: Skip math content in tables rule to preserve LaTeX escape sequences (#11134)
Fixes #10255 where LaTeX commands containing \n (like \neg, \neq, \nu)
were being incorrectly interpreted as newline characters when pasted.

The tables rule was processing all inline content and splitting on \n
sequences, which broke LaTeX math expressions. This fix skips processing
for math_inline tokens, similar to the existing skip for code_inline.

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

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-10 12:29:47 -05:00
Tom Moor 6c3a0f7cb3 feat: Simplify todo input (#11129)
closes #11123
2026-01-10 12:28:29 -05:00
Tom Moor cd84ca2fa6 feat: Add keyboard shortcut to toggle theme (#11127)
closes #11122
2026-01-10 12:28:20 -05:00
Tom Moor 8d5b9b6ac4 Remove space from markdown trigger for horizontal rule (#11130)
closes #11126
2026-01-10 12:28:11 -05:00
Tom Moor d9aec40313 chore: Store service in JWT (#11136)
* chore: Store service in JWT

* docs

* fix: Remove early return
2026-01-10 12:28:00 -05:00
Tom Moor 77a125d290 fix: test (#11140) 2026-01-10 16:29:44 +00:00
Tom Moor b123762e86 chore: Remove collection.url, fix post-signin redirect (#11139) 2026-01-10 16:19:23 +00: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
1459 changed files with 91532 additions and 34165 deletions
+9 -1
View File
@@ -1,4 +1,3 @@
__mocks__
.git
.vscode
.github
@@ -8,10 +7,19 @@ __mocks__
.eslint*
.oxlintrc*
.log
*.md
Makefile
Procfile
app.json
crowdin.yml
lint-staged.config.mjs
build
docker-compose.yml
node_modules
.yarn
**/*.test.ts
**/*.test.tsx
**/*.test.js
**/*.test.jsx
**/__tests__
**/__mocks__
+23 -5
View File
@@ -61,6 +61,11 @@ DATABASE_CONNECTION_POOL_MAX=
# DOCS: https://docs.getoutline.com/s/hosting/doc/redis-LGM4BFXYp4
REDIS_URL=redis://redis:6379
# To enable horizontal scaling of the collaboration service you must provide a Redis URL, it may
# be the same as above, or a different server.
# DOCS: https://docs.getoutline.com/s/hosting/doc/horizontal-scaling-hkfU5Stao7
REDIS_COLLABORATION_URL=
# ––––––––––––––––––––––––––––––––––––––
# ––––––––––– FILE STORAGE –––––––––––
@@ -114,14 +119,18 @@ SSL_CERT=
# false if you can be sure that SSL is terminated at an external loadbalancer.
FORCE_HTTPS=true
# When behind a reverse proxy, the header to use for the client IP.
# The default value is "X-Forwarded-For", common values are "X-Real-IP"
# and "X-Client-IP".
# PROXY_IP_HEADER=
# ––––––––––––––––––––––––––––––––––––––
# –––––––––– AUTHENTICATION ––––––––––
# ––––––––––––––––––––––––––––––––––––––
# Third party signin credentials, at least ONE OF EITHER Google, Slack,
# Discord, or Microsoft is required for a working installation or you'll
# have no sign-in options.
# Third party signin credentials, at least ONE OF these is required for a
# working installation or you'll have no sign-in options.
# Slack sign-in provider
# DOCS: https://docs.getoutline.com/s/hosting/doc/slack-sgMujR8J9J
@@ -198,7 +207,7 @@ RATE_LIMITER_DURATION_WINDOW=60
# ––––––––––– INTEGRATIONS –––––––––––
# ––––––––––––––––––––––––––––––––––––––
# The GitHub integration allows previewing issue and pull request links
# GitHub integration allows previewing issue and pull request links
# DOCS: https://docs.getoutline.com/s/hosting/doc/github-GchT3NNxI9
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
@@ -207,7 +216,12 @@ GITHUB_APP_NAME=
GITHUB_APP_ID=
GITHUB_APP_PRIVATE_KEY=
# The Linear integration allows previewing issue links as rich mentions
# The GitLab integration allows previewing issue and merge request links
# DOCS:
GITLAB_CLIENT_ID=
GITLAB_CLIENT_SECRET=
# Linear integration allows previewing issue links as rich mentions
LINEAR_CLIENT_ID=
LINEAR_CLIENT_SECRET=
@@ -218,6 +232,10 @@ SLACK_VERIFICATION_TOKEN=your_token
SLACK_APP_ID=A0XXXXXXX
SLACK_MESSAGE_ACTIONS=true
# Figma integration allows previewing design files as rich mentions
FIGMA_CLIENT_ID=
FIGMA_CLIENT_SECRET=
# For Dropbox integration, follow these instructions to get the key https://www.dropbox.com/developers/embedder#setup
# and do not forget to whitelist your domain name in the app settings
DROPBOX_APP_KEY=
+12
View File
@@ -12,11 +12,15 @@ 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;
GITHUB_APP_NAME=outline-test;
GITLAB_CLIENT_ID=123
GITLAB_CLIENT_SECRET=123
OIDC_CLIENT_ID=client-id
OIDC_CLIENT_SECRET=client-secret
OIDC_AUTH_URI=http://localhost/authorize
@@ -29,3 +33,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
+55 -20
View File
@@ -18,44 +18,61 @@ env:
SMTP_USERNAME: localhost
jobs:
build:
setup:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.x, 22.x]
steps:
- uses: actions/checkout@v5
- name: Use Node.js ${{ matrix.node-version }}
- name: Enable Corepack
run: corepack enable
- name: Use Node.js 22.x
uses: actions/setup-node@v5
with:
node-version: ${{ matrix.node-version }}
node-version: 22.x
cache: "yarn"
- 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
run: yarn install --frozen-lockfile --prefer-offline
if: steps.cache-node-modules.outputs.cache-hit != 'true'
run: yarn install --immutable
lint:
needs: build
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"
- 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 lint --quiet
types:
needs: build
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"
- 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:
@@ -85,7 +102,7 @@ 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:
@@ -93,15 +110,21 @@ jobs:
test-group: [app, shared]
steps:
- 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:
@@ -125,28 +148,40 @@ jobs:
steps:
- 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]
if: ${{ needs.changes.outputs.app == 'true' && github.repository == 'outline/outline' }}
needs: [setup, types, changes]
if: ${{ (needs.changes.outputs.app == 'true' || needs.changes.outputs.config == 'true') && github.repository == 'outline/outline' }}
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"
- 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
+8
View File
@@ -14,3 +14,11 @@ data/*
*.pem
*.key
*.cert
# Yarn Berry
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
.yarn/releases
!.yarn/sdks
+4 -1
View File
@@ -10,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"
+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"]
}
+6
View File
@@ -0,0 +1,6 @@
nodeLinker: node-modules
npmMinimalAgeGate: 86400
npmPreapprovedPackages:
- outline-icons
+16 -13
View File
@@ -30,10 +30,12 @@ You're an expert in the following areas:
## 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
@@ -62,13 +64,13 @@ yarn install
2. Public static methods
3. Public variables
4. Public methods
6. Protected variables & methods
8. Private variables & 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.
- Always use named exports for new components & classes.
- Document ALL public/exported functions with JSDoc.
## React Usage
@@ -77,7 +79,7 @@ yarn install
- 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.
- You do not need to import React unless it is used directly.
- 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.
@@ -141,20 +143,21 @@ yarn sequelize migration:create --name=add-field-to-table
- Run tests with Jest:
```bash
# Run all tests
# Run a specific test file (preferred)
yarn test path/to/test.spec.ts
# Run every test (avoid)
yarn test
# Run specific test suites
yarn test:app # Frontend tests
yarn test:server # Backend tests
yarn test:shared # Shared code tests
# Run specific test file
yarn test path/to/test.spec.ts
# 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.
- Mock external dependencies appropriately in __mocks__ folder.
- 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
Symlink
+1
View File
@@ -0,0 +1 @@
AGENTS.md
+8 -8
View File
@@ -18,15 +18,15 @@ ENV NODE_ENV=production
RUN addgroup --gid 1001 nodejs && \
adduser --uid 1001 --ingroup nodejs nodejs && \
mkdir -p /var/lib/outline && \
chown -R nodejs:nodejs /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 \
@@ -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"]
+4 -5
View File
@@ -3,22 +3,21 @@ 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 1.1.0
The Licensed Work is (c) 2025 General Outline, Inc.
Licensed Work: Outline 1.6.1
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-11-16
Change Date: 2030-03-18
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:
+7 -3
View File
@@ -1,5 +1,9 @@
<p align="center">
<img src="https://user-images.githubusercontent.com/31465/34380645-bd67f474-eb0b-11e7-8d03-0151c1730654.png" height="29" />
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./public/logos/outline-logo-dark.png" height="29">
<source media="(prefers-color-scheme: light)" srcset="./public/logos/outline-logo-light.png" height="29">
<img src="./public/logos/outline-logo-light.png" height="29" alt="Outline" />
</picture>
</p>
<p align="center">
<i>A fast, collaborative, knowledge base for your team built using React and Node.js.<br/>Try out Outline using our hosted version at <a href="https://www.getoutline.com">www.getoutline.com</a>.</i>
@@ -29,9 +33,9 @@ There is a short guide for [setting up a development environment](https://docs.g
## Contributing
Outline is built and maintained by a small team we'd love your help to fix bugs and add features!
Outline is built and maintained by a small team your help finding and fixing bugs is appreciated, though AI assisted PR's from new contributors are discouraged and unlikely to be merged.
Before submitting a pull request _please_ discuss with the core team by creating or commenting in an issue on [GitHub](https://www.github.com/outline/outline/issues) we'd also love to hear from you in the [discussions](https://www.github.com/outline/outline/discussions). This way we can ensure that an approach is agreed on before code is written. This will result in a much higher likelihood of your code being accepted.
Before submitting a pull request _you must_ discuss with the core team by creating or commenting in an issue on [GitHub](https://www.github.com/outline/outline/issues) we'd also love to hear from you in the [discussions](https://www.github.com/outline/outline/discussions). This way we can ensure that an approach is agreed on before code is written and that you have read these instructions. This will result in a much higher likelihood of your code being accepted.
If youre looking for ways to get started, here's a list of ways to help us improve Outline:
+23 -3
View File
@@ -21,7 +21,23 @@
}
],
"scripts": {
"postdeploy": "yarn sequelize db:migrate"
"postdeploy": "yarn sequelize db:migrate",
"pr-predeploy": "yarn sequelize db:migrate"
},
"environments": {
"review": {
"scripts": {
"postdeploy": "yarn sequelize db:migrate"
},
"addons": [
{
"plan": "heroku-redis:mini"
},
{
"plan": "heroku-postgresql:essential-0"
}
]
}
},
"env": {
"NODE_ENV": {
@@ -43,8 +59,12 @@
"required": true
},
"URL": {
"description": "https://{your app name}.herokuapp.com, or the domain you are binding to",
"required": true
"description": "https://{your app name}.herokuapp.com, or the domain you are binding to. For review apps, this is auto-generated.",
"required": false
},
"HEROKU_APP_NAME": {
"description": "Automatically set by Heroku for review apps",
"required": false
},
"GOOGLE_CLIENT_ID": {
"description": "See https://developers.google.com/identity/protocols/OAuth2 to create a new Google OAuth client. You must configure at least one of Slack or Google to control login.",
+1 -1
View File
@@ -1,6 +1,6 @@
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 } from "..";
+121 -179
View File
@@ -1,12 +1,12 @@
import {
AlphabeticalReverseSortIcon,
AlphabeticalSortIcon,
SortAlphabeticalReverseIcon,
SortAlphabeticalIcon,
ArchiveIcon,
CollectionIcon,
EditIcon,
ExportIcon,
ImportIcon,
ManualSortIcon,
SortManualIcon,
NewDocumentIcon,
PadlockIcon,
PlusIcon,
@@ -29,8 +29,8 @@ import DynamicCollectionIcon from "~/components/Icons/CollectionIcon";
import { getHeaderExpandedKey } from "~/components/Sidebar/components/Header";
import {
createAction,
createActionWithChildren,
createInternalLinkAction,
createActionWithChildren,
} from "~/actions";
import { ActiveCollectionSection, CollectionSection } from "~/actions/sections";
import { setPersistedState } from "~/hooks/usePersistedState";
@@ -96,11 +96,11 @@ export const editCollection = createAction({
analyticsName: "Edit collection",
section: ActiveCollectionSection,
icon: <EditIcon />,
visible: ({ activeCollectionId, stores }) =>
!!activeCollectionId &&
stores.policies.abilities(activeCollectionId).update,
perform: ({ t, activeCollectionId, stores }) => {
if (!activeCollectionId) {
visible: ({ getActivePolicies }) =>
getActivePolicies(Collection).some((policy) => policy.abilities.update),
perform: ({ t, getActiveModel, stores }) => {
const collection = getActiveModel(Collection);
if (!collection) {
return;
}
@@ -109,7 +109,7 @@ export const editCollection = createAction({
content: (
<CollectionEdit
onSubmit={stores.dialogs.closeAllModals}
collectionId={activeCollectionId}
collectionId={collection.id}
/>
),
});
@@ -122,21 +122,16 @@ export const editCollectionPermissions = createAction({
analyticsName: "Collection permissions",
section: ActiveCollectionSection,
icon: <PadlockIcon />,
visible: ({ activeCollectionId, stores }) =>
!!activeCollectionId &&
stores.policies.abilities(activeCollectionId).update,
perform: ({ t, activeCollectionId, stores }) => {
if (!activeCollectionId) {
return;
}
const collection = stores.collections.get(activeCollectionId);
visible: ({ getActivePolicies }) =>
getActivePolicies(Collection).some((policy) => policy.abilities.update),
perform: ({ t, getActiveModel, stores }) => {
const collection = getActiveModel(Collection);
if (!collection) {
return;
}
stores.dialogs.openModal({
title: t("Share this collection"),
style: { marginBottom: -12 },
content: (
<SharePopover
collection={collection}
@@ -153,15 +148,16 @@ export const importDocument = createAction({
analyticsName: "Import document",
section: ActiveCollectionSection,
icon: <ImportIcon />,
visible: ({ activeCollectionId, stores }) => {
if (activeCollectionId) {
return !!stores.policies.abilities(activeCollectionId).createDocument;
}
return false;
},
perform: ({ activeCollectionId, stores }) => {
visible: ({ getActivePolicies }) =>
getActivePolicies(Collection).some(
(policy) => policy.abilities.createDocument
),
perform: ({ t, getActiveModel, stores }) => {
const { documents } = stores;
const collection = getActiveModel(Collection);
if (!collection) {
return;
}
const input = document.createElement("input");
input.type = "file";
input.accept = documents.importFileTypesString;
@@ -169,19 +165,17 @@ export const importDocument = createAction({
input.onchange = async (ev) => {
const files = getEventFiles(ev);
const file = files[0];
const toastId = toast.loading(`${t("Uploading")}`);
try {
const document = await documents.import(
file,
null,
activeCollectionId,
{
publish: true,
}
);
history.push(document.url);
const document = await documents.import(file, null, collection.id, {
publish: true,
});
history.push(document.path);
} catch (err) {
toast.error(err.message);
} finally {
toast.dismiss(toastId);
}
};
@@ -192,37 +186,36 @@ export const importDocument = createAction({
export const sortCollection = createActionWithChildren({
name: ({ t }) => t("Sort in sidebar"),
section: ActiveCollectionSection,
visible: ({ activeCollectionId, stores }) =>
!!activeCollectionId &&
!!stores.policies.abilities(activeCollectionId).update,
icon: ({ activeCollectionId, stores }) => {
const collection = stores.collections.get(activeCollectionId);
visible: ({ getActivePolicies }) =>
getActivePolicies(Collection).some((policy) => policy.abilities.update),
icon: ({ getActiveModel }) => {
const collection = getActiveModel(Collection);
const sortAlphabetical = collection?.sort.field === "title";
const sortDir = collection?.sort.direction;
return sortAlphabetical ? (
sortDir === "asc" ? (
<AlphabeticalSortIcon />
<SortAlphabeticalIcon />
) : (
<AlphabeticalReverseSortIcon />
<SortAlphabeticalReverseIcon />
)
) : (
<ManualSortIcon />
<SortManualIcon />
);
},
children: [
createAction({
name: ({ t }) => t("A-Z sort"),
section: ActiveCollectionSection,
selected: ({ activeCollectionId, stores }) => {
const collection = stores.collections.get(activeCollectionId);
selected: ({ getActiveModel }) => {
const collection = getActiveModel(Collection);
return (
collection?.sort.field === "title" &&
collection?.sort.direction === "asc"
);
},
perform: ({ activeCollectionId, stores }) => {
const collection = stores.collections.get(activeCollectionId);
perform: ({ getActiveModel }) => {
const collection = getActiveModel(Collection);
return collection?.save({
sort: {
field: "title",
@@ -234,15 +227,15 @@ export const sortCollection = createActionWithChildren({
createAction({
name: ({ t }) => t("Z-A sort"),
section: ActiveCollectionSection,
selected: ({ activeCollectionId, stores }) => {
const collection = stores.collections.get(activeCollectionId);
selected: ({ getActiveModel }) => {
const collection = getActiveModel(Collection);
return (
collection?.sort.field === "title" &&
collection?.sort.direction === "desc"
);
},
perform: ({ activeCollectionId, stores }) => {
const collection = stores.collections.get(activeCollectionId);
perform: ({ getActiveModel }) => {
const collection = getActiveModel(Collection);
return collection?.save({
sort: {
field: "title",
@@ -254,12 +247,12 @@ export const sortCollection = createActionWithChildren({
createAction({
name: ({ t }) => t("Manual sort"),
section: ActiveCollectionSection,
selected: ({ activeCollectionId, stores }) => {
const collection = stores.collections.get(activeCollectionId);
selected: ({ getActiveModel }) => {
const collection = getActiveModel(Collection);
return collection?.sort.field !== "title";
},
perform: ({ activeCollectionId, stores }) => {
const collection = stores.collections.get(activeCollectionId);
perform: ({ getActiveModel }) => {
const collection = getActiveModel(Collection);
return collection?.save({
sort: {
field: "index",
@@ -276,22 +269,19 @@ export const searchInCollection = createInternalLinkAction({
analyticsName: "Search collection",
section: ActiveCollectionSection,
icon: <SearchIcon />,
visible: ({ activeCollectionId, stores }) => {
if (!activeCollectionId) {
return false;
}
const collection = stores.collections.get(activeCollectionId);
visible: ({ getActiveModel, stores }) => {
const collection = getActiveModel(Collection);
if (!collection?.isActive) {
return false;
}
return stores.policies.abilities(activeCollectionId).readDocument;
return stores.policies.abilities(collection.id).readDocument;
},
to: ({ activeCollectionId, sidebarContext }) => {
to: ({ getActiveModel, sidebarContext }) => {
const collection = getActiveModel(Collection);
const [pathname, search] = searchPath({
collectionId: activeCollectionId,
collectionId: collection?.id,
}).split("?");
return {
@@ -308,23 +298,22 @@ export const starCollection = createAction({
section: ActiveCollectionSection,
icon: <StarredIcon />,
keywords: "favorite bookmark",
visible: ({ activeCollectionId, stores }) => {
if (!activeCollectionId) {
visible: ({ getActiveModel, stores }) => {
const collection = getActiveModel(Collection);
if (!collection) {
return false;
}
const collection = stores.collections.get(activeCollectionId);
return (
!collection?.isStarred &&
stores.policies.abilities(activeCollectionId).star
!collection.isStarred && stores.policies.abilities(collection.id).star
);
},
perform: async ({ activeCollectionId, stores }) => {
if (!activeCollectionId) {
perform: async ({ getActiveModel }) => {
const collection = getActiveModel(Collection);
if (!collection) {
return;
}
const collection = stores.collections.get(activeCollectionId);
await collection?.star();
await collection.star();
setPersistedState(getHeaderExpandedKey("starred"), true);
},
});
@@ -335,22 +324,18 @@ export const unstarCollection = createAction({
section: ActiveCollectionSection,
icon: <UnstarredIcon />,
keywords: "unfavorite unbookmark",
visible: ({ activeCollectionId, stores }) => {
if (!activeCollectionId) {
visible: ({ getActiveModel, stores }) => {
const collection = getActiveModel(Collection);
if (!collection) {
return false;
}
const collection = stores.collections.get(activeCollectionId);
return (
!!collection?.isStarred &&
stores.policies.abilities(activeCollectionId).unstar
!!collection.isStarred && stores.policies.abilities(collection.id).unstar
);
},
perform: async ({ activeCollectionId, stores }) => {
if (!activeCollectionId) {
return;
}
const collection = stores.collections.get(activeCollectionId);
perform: async ({ getActiveModel }) => {
const collection = getActiveModel(Collection);
await collection?.unstar();
},
});
@@ -360,28 +345,25 @@ export const subscribeCollection = createAction({
analyticsName: "Subscribe to collection",
section: ActiveCollectionSection,
icon: <SubscribeIcon />,
visible: ({ activeCollectionId, stores }) => {
if (!activeCollectionId) {
visible: ({ getActiveModel, stores }) => {
const collection = getActiveModel(Collection);
if (!collection) {
return false;
}
const collection = stores.collections.get(activeCollectionId);
return (
!!collection?.isActive &&
!collection?.isSubscribed &&
stores.policies.abilities(activeCollectionId).subscribe
!!collection.isActive &&
!collection.isSubscribed &&
stores.policies.abilities(collection.id).subscribe
);
},
perform: async ({ activeCollectionId, stores, t }) => {
if (!activeCollectionId) {
perform: async ({ getActiveModel, t }) => {
const collection = getActiveModel(Collection);
if (!collection) {
return;
}
const collection = stores.collections.get(activeCollectionId);
await collection?.subscribe();
await collection.subscribe();
toast.success(t("Subscribed to document notifications"));
},
});
@@ -391,28 +373,25 @@ export const unsubscribeCollection = createAction({
analyticsName: "Unsubscribe from collection",
section: ActiveCollectionSection,
icon: <UnsubscribeIcon />,
visible: ({ activeCollectionId, stores }) => {
if (!activeCollectionId) {
visible: ({ getActiveModel, stores }) => {
const collection = getActiveModel(Collection);
if (!collection) {
return false;
}
const collection = stores.collections.get(activeCollectionId);
return (
!!collection?.isActive &&
!!collection?.isSubscribed &&
stores.policies.abilities(activeCollectionId).unsubscribe
!!collection.isActive &&
!!collection.isSubscribed &&
stores.policies.abilities(collection.id).unsubscribe
);
},
perform: async ({ activeCollectionId, currentUserId, stores, t }) => {
if (!activeCollectionId || !currentUserId) {
perform: async ({ getActiveModel, t }) => {
const collection = getActiveModel(Collection);
if (!collection) {
return;
}
const collection = stores.collections.get(activeCollectionId);
await collection?.unsubscribe();
await collection.unsubscribe();
toast.success(t("Unsubscribed from document notifications"));
},
});
@@ -422,23 +401,15 @@ export const archiveCollection = createAction({
analyticsName: "Archive collection",
section: ActiveCollectionSection,
icon: <ArchiveIcon />,
visible: ({ activeCollectionId, stores }) => {
if (!activeCollectionId) {
return false;
}
return !!stores.policies.abilities(activeCollectionId).archive;
},
perform: async ({ activeCollectionId, stores, t }) => {
const { dialogs, collections } = stores;
if (!activeCollectionId) {
return;
}
const collection = collections.get(activeCollectionId);
visible: ({ getActivePolicies }) =>
getActivePolicies(Collection).some((policy) => policy.abilities.archive),
perform: async ({ getActiveModel, stores, t }) => {
const collection = getActiveModel(Collection);
if (!collection) {
return;
}
dialogs.openModal({
stores.dialogs.openModal({
title: t("Archive collection"),
content: (
<ConfirmationDialog
@@ -463,17 +434,10 @@ export const restoreCollection = createAction({
analyticsName: "Restore collection",
section: CollectionSection,
icon: <RestoreIcon />,
visible: ({ activeCollectionId, stores }) => {
if (!activeCollectionId) {
return false;
}
return !!stores.policies.abilities(activeCollectionId).restore;
},
perform: async ({ activeCollectionId, stores, t }) => {
if (!activeCollectionId) {
return;
}
const collection = stores.collections.get(activeCollectionId);
visible: ({ getActivePolicies }) =>
getActivePolicies(Collection).some((policy) => policy.abilities.restore),
perform: async ({ getActiveModel, t }) => {
const collection = getActiveModel(Collection);
if (!collection) {
return;
}
@@ -489,18 +453,10 @@ export const deleteCollection = createAction({
section: ActiveCollectionSection,
dangerous: true,
icon: <TrashIcon />,
visible: ({ activeCollectionId, stores }) => {
if (!activeCollectionId) {
return false;
}
return stores.policies.abilities(activeCollectionId).delete;
},
perform: ({ activeCollectionId, t, stores }) => {
if (!activeCollectionId) {
return;
}
const collection = stores.collections.get(activeCollectionId);
visible: ({ getActivePolicies }) =>
getActivePolicies(Collection).some((policy) => policy.abilities.delete),
perform: ({ getActiveModel, t, stores }) => {
const collection = getActiveModel(Collection);
if (!collection) {
return;
}
@@ -522,18 +478,10 @@ export const exportCollection = createAction({
analyticsName: "Export collection",
section: ActiveCollectionSection,
icon: <ExportIcon />,
visible: ({ currentTeamId, activeCollectionId, stores }) => {
if (!currentTeamId || !activeCollectionId) {
return false;
}
return !!stores.policies.abilities(activeCollectionId).export;
},
perform: async ({ activeCollectionId, stores, t }) => {
if (!activeCollectionId) {
return;
}
const collection = stores.collections.get(activeCollectionId);
visible: ({ getActivePolicies }) =>
getActivePolicies(Collection).some((policy) => policy.abilities.export),
perform: async ({ getActiveModel, stores, t }) => {
const collection = getActiveModel(Collection);
if (!collection) {
return;
}
@@ -556,13 +504,13 @@ export const createDocument = createInternalLinkAction({
section: ActiveCollectionSection,
icon: <NewDocumentIcon />,
keywords: "new create document",
visible: ({ activeCollectionId, stores }) =>
!!(
!!activeCollectionId &&
stores.policies.abilities(activeCollectionId).createDocument
visible: ({ getActivePolicies }) =>
getActivePolicies(Collection).some(
(policy) => policy.abilities.createDocument
),
to: ({ activeCollectionId, sidebarContext }) => {
const [pathname, search] = newDocumentPath(activeCollectionId).split("?");
to: ({ getActiveModel, sidebarContext }) => {
const collection = getActiveModel(Collection);
const [pathname, search] = newDocumentPath(collection?.id).split("?");
return {
pathname,
@@ -578,19 +526,13 @@ export const createTemplate = createInternalLinkAction({
section: ActiveCollectionSection,
icon: <ShapesIcon />,
keywords: "new create template",
visible: ({ activeCollectionId, stores }) =>
!!(
!!activeCollectionId &&
stores.policies.abilities(activeCollectionId).createDocument
visible: ({ getActivePolicies }) =>
getActivePolicies(Collection).some(
(policy) => policy.abilities.createTemplate
),
to: ({ activeCollectionId, sidebarContext }) => {
const [pathname, search] = newTemplatePath(activeCollectionId).split("?");
return {
pathname,
search,
state: { sidebarContext },
};
to: ({ getActiveModel }) => {
const collection = getActiveModel(Collection);
return newTemplatePath(collection?.id);
},
});
+1 -1
View File
@@ -1,6 +1,6 @@
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 { createAction } from "..";
+12 -1
View File
@@ -17,7 +17,17 @@ 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 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"),
@@ -222,6 +232,7 @@ export const developer = createActionWithChildren({
iconInContextMenu: false,
section: DeveloperSection,
children: [
goToDebug,
copyId,
toggleDebugLogging,
toggleDebugSafeArea,
+284 -201
View File
@@ -32,23 +32,24 @@ import {
CaseSensitiveIcon,
RestoreIcon,
EditIcon,
EmbedIcon,
OpenIcon,
} 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 { Week } from "@shared/utils/time";
import type UserMembership from "~/models/UserMembership";
import { client } from "~/utils/ApiClient";
import DocumentDelete from "~/scenes/DocumentDelete";
import DocumentMove from "~/scenes/DocumentMove";
import DocumentPermanentDelete from "~/scenes/DocumentPermanentDelete";
import DocumentPublish from "~/scenes/DocumentPublish";
import DeleteDocumentsInTrash from "~/scenes/Trash/components/DeleteDocumentsInTrash";
import ConfirmationDialog from "~/components/ConfirmationDialog";
import DocumentCopy from "~/components/DocumentCopy";
import DocumentCopy from "~/components/DocumentExplorer/DocumentCopy";
import { DocumentDownload } from "~/components/DocumentDownload";
import MarkdownIcon from "~/components/Icons/MarkdownIcon";
import { getHeaderExpandedKey } from "~/components/Sidebar/components/Header";
import DocumentTemplatizeDialog from "~/components/TemplatizeDialog";
@@ -63,7 +64,6 @@ import {
DocumentSection,
TrashSection,
} from "~/actions/sections";
import env from "~/env";
import { setPersistedState } from "~/hooks/usePersistedState";
import history from "~/utils/history";
import {
@@ -71,16 +71,27 @@ import {
homePath,
newDocumentPath,
newNestedDocumentPath,
newSiblingDocumentPath,
searchPath,
documentPath,
urlify,
desktopify,
trashPath,
documentEditPath,
} from "~/utils/routeHelpers";
import capitalize from "lodash/capitalize";
import CollectionIcon from "~/components/Icons/CollectionIcon";
import { Action, ActionGroup, ActionSeparator } from "~/types";
import type {
Action,
ActionContext,
ActionGroup,
ActionSeparator,
} from "~/types";
import lazyWithRetry from "~/utils/lazyWithRetry";
import env from "~/env";
import { isMac, isWindows } from "@shared/utils/browser";
import isCloudHosted from "~/utils/isCloudHosted";
import DocumentMove from "~/components/DocumentExplorer/DocumentMove";
const Insights = lazyWithRetry(
() => import("~/scenes/Document/components/Insights")
@@ -132,18 +143,13 @@ export const editDocument = createInternalLinkAction({
keywords: "edit",
icon: <EditIcon />,
visible: ({ activeDocumentId, stores }) => {
const { auth, documents, policies } = stores;
const { auth, policies } = stores;
const document = activeDocumentId
? documents.get(activeDocumentId)
: undefined;
const can = activeDocumentId
? policies.abilities(activeDocumentId)
: undefined;
return (
!!can?.update && !!auth.user?.separateEditMode && !document?.template
);
return !!can?.update && !!auth.user?.separateEditMode;
},
to: ({ activeDocumentId, stores }) => {
const document = activeDocumentId
@@ -200,59 +206,41 @@ export const createDraftDocument = createInternalLinkAction({
}),
});
export const createDocumentFromTemplate = createInternalLinkAction({
name: ({ t }) => t("New from template"),
analyticsName: "New document",
section: DocumentSection,
icon: <NewDocumentIcon />,
keywords: "create",
visible: ({
currentTeamId,
activeCollectionId,
activeDocumentId,
stores,
}) => {
const document = activeDocumentId
? stores.documents.get(activeDocumentId)
: undefined;
/**
* Finds the index of a document among its siblings in the collection tree.
*
* @param stores - the root stores.
* @param document - the document to find the index of.
* @returns the index of the document among its siblings, or -1 if not found.
*/
function findDocumentSiblingIndex(
stores: ActionContext["stores"],
document: {
id: string;
collectionId?: string | null;
parentDocumentId?: string;
}
): number {
if (!document.collectionId) {
return -1;
}
const collection = stores.collections.get(document.collectionId);
if (!collection) {
return -1;
}
if (
!currentTeamId ||
!document?.isTemplate ||
!!document?.isDraft ||
!!document?.isDeleted
) {
return false;
}
const siblings = document.parentDocumentId
? collection.getChildrenForDocument(document.parentDocumentId)
: collection.sortedDocuments;
if (activeCollectionId) {
return stores.policies.abilities(activeCollectionId).createDocument;
}
return stores.policies.abilities(currentTeamId).createDocument;
},
to: ({ activeDocumentId, activeCollectionId, sidebarContext }) => {
if (!activeDocumentId || !activeCollectionId) {
return "";
}
const [pathname, search] = newDocumentPath(activeCollectionId, {
templateId: activeDocumentId,
}).split("?");
return {
pathname,
search,
state: { sidebarContext },
};
},
});
return siblings?.findIndex((node) => node.id === document.id) ?? -1;
}
export const createNestedDocument = createInternalLinkAction({
name: ({ t }) => t("New nested document"),
name: ({ t }) => t("Nested document"),
analyticsName: "New document",
section: ActiveDocumentSection,
icon: <NewDocumentIcon />,
keywords: "create",
keywords: "create nested",
visible: ({ currentTeamId, activeDocumentId, stores }) =>
!!currentTeamId &&
!!activeDocumentId &&
@@ -270,6 +258,100 @@ export const createNestedDocument = createInternalLinkAction({
},
});
const createDocumentBefore = createInternalLinkAction({
name: ({ t }) => t("Before"),
analyticsName: "New document before",
section: ActiveDocumentSection,
keywords: "create before",
visible: ({ currentTeamId, activeDocumentId, stores }) => {
if (!currentTeamId || !activeDocumentId) {
return false;
}
const document = stores.documents.get(activeDocumentId);
return (
!!document?.collectionId &&
stores.policies.abilities(currentTeamId).createDocument
);
},
to: ({ activeDocumentId, stores, sidebarContext }) => {
const document = activeDocumentId
? stores.documents.get(activeDocumentId)
: undefined;
if (!document) {
return "";
}
const index = findDocumentSiblingIndex(stores, document);
const [pathname, search] = newSiblingDocumentPath({
collectionId: document.collectionId,
parentDocumentId: document.parentDocumentId,
index: Math.max(0, index),
}).split("?");
return {
pathname,
search,
state: { sidebarContext },
};
},
});
const createDocumentAfter = createInternalLinkAction({
name: ({ t }) => t("After"),
analyticsName: "New document after",
section: ActiveDocumentSection,
keywords: "create after",
visible: ({ currentTeamId, activeDocumentId, stores }) => {
if (!currentTeamId || !activeDocumentId) {
return false;
}
const document = stores.documents.get(activeDocumentId);
return (
!!document?.collectionId &&
stores.policies.abilities(currentTeamId).createDocument
);
},
to: ({ activeDocumentId, stores, sidebarContext }) => {
const document = activeDocumentId
? stores.documents.get(activeDocumentId)
: undefined;
if (!document) {
return "";
}
const index = findDocumentSiblingIndex(stores, document);
const [pathname, search] = newSiblingDocumentPath({
collectionId: document.collectionId,
parentDocumentId: document.parentDocumentId,
index: index + 1,
}).split("?");
return {
pathname,
search,
state: { sidebarContext },
};
},
});
export const createNewDocument = createActionWithChildren({
name: ({ t }) => t("New document"),
analyticsName: "New document",
section: ActiveDocumentSection,
icon: <NewDocumentIcon />,
keywords: "create",
visible: ({ currentTeamId, activeDocumentId, stores }) => {
if (!activeDocumentId) {
return false;
}
return (
!!currentTeamId && stores.policies.abilities(currentTeamId).createDocument
);
},
children: [createDocumentBefore, createDocumentAfter, createNestedDocument],
});
export const starDocument = createAction({
name: ({ t }) => t("Star"),
analyticsName: "Star document",
@@ -346,7 +428,7 @@ export const publishDocument = createAction({
return;
}
if (document?.collectionId || document?.template) {
if (document?.collectionId) {
await document.save(undefined, {
publish: true,
});
@@ -495,7 +577,10 @@ export const shareDocument = createAction({
section: ActiveDocumentSection,
icon: <PadlockIcon />,
visible: ({ stores, activeDocumentId }) => {
const can = stores.policies.abilities(activeDocumentId!);
if (!activeDocumentId) {
return false;
}
const can = stores.policies.abilities(activeDocumentId);
return can.manageUsers || can.share;
},
perform: async ({ activeDocumentId, stores, currentUserId, t }) => {
@@ -509,7 +594,6 @@ export const shareDocument = createAction({
}
stores.dialogs.openModal({
style: { marginBottom: -12 },
title: t("Share this document"),
content: (
<SharePopover
@@ -522,13 +606,40 @@ export const shareDocument = createAction({
},
});
export const downloadDocumentAsHTML = createAction({
name: ({ t }) => t("HTML"),
analyticsName: "Download document as HTML",
export const downloadDocument = createAction({
name: ({ t, isMenu }) => (isMenu ? t("Download") : t("Download document")),
analyticsName: "Download document",
section: ActiveDocumentSection,
keywords: "html export",
icon: <DownloadIcon />,
iconInContextMenu: false,
keywords: "export md markdown html",
visible: ({ activeDocumentId, stores }) =>
!!activeDocumentId && stores.policies.abilities(activeDocumentId).download,
perform: ({ activeDocumentId, t, stores }) => {
if (!activeDocumentId) {
return;
}
const document = stores.documents.get(activeDocumentId);
invariant(document, "Document must exist");
stores.dialogs.openModal({
title: t("Download document"),
content: (
<DocumentDownload
document={document}
onSubmit={stores.dialogs.closeAllModals}
/>
),
});
},
});
export const downloadDocumentAsMarkdown = createAction({
name: ({ t }) => t("Download as Markdown"),
analyticsName: "Download document as Markdown",
section: ActiveDocumentSection,
keywords: "md markdown export",
icon: <DownloadIcon />,
visible: ({ activeDocumentId, stores }) =>
!!activeDocumentId && stores.policies.abilities(activeDocumentId).download,
perform: async ({ activeDocumentId, stores }) => {
@@ -537,70 +648,59 @@ export const downloadDocumentAsHTML = createAction({
}
const document = stores.documents.get(activeDocumentId);
await document?.download(ExportContentType.Html);
await document?.download({
contentType: ExportContentType.Markdown,
includeChildDocuments: false,
});
},
});
export const downloadDocumentAsHTML = createAction({
name: ({ t }) => t("Download as HTML"),
analyticsName: "Download document as HTML",
section: ActiveDocumentSection,
keywords: "xml html export",
icon: <DownloadIcon />,
visible: ({ activeDocumentId, stores }) =>
!!activeDocumentId && stores.policies.abilities(activeDocumentId).download,
perform: async ({ activeDocumentId, stores }) => {
if (!activeDocumentId) {
return;
}
const document = stores.documents.get(activeDocumentId);
await document?.download({
contentType: ExportContentType.Html,
includeChildDocuments: false,
});
},
});
export const downloadDocumentAsPDF = createAction({
name: ({ t }) => t("PDF"),
name: ({ t }) => t("Download as PDF"),
analyticsName: "Download document as PDF",
section: ActiveDocumentSection,
keywords: "export",
keywords: "pdf export",
icon: <DownloadIcon />,
iconInContextMenu: false,
visible: ({ activeDocumentId, stores }) =>
!!(
activeDocumentId &&
stores.policies.abilities(activeDocumentId).download &&
env.PDF_EXPORT_ENABLED
),
perform: ({ activeDocumentId, t, stores }) => {
if (!activeDocumentId) {
return;
}
const id = toast.loading(`${t("Exporting")}`);
const document = stores.documents.get(activeDocumentId);
return document
?.download(ExportContentType.Pdf)
.finally(() => id && toast.dismiss(id));
},
});
export const downloadDocumentAsMarkdown = createAction({
name: ({ t }) => t("Markdown"),
analyticsName: "Download document as Markdown",
section: ActiveDocumentSection,
keywords: "md markdown export",
icon: <DownloadIcon />,
iconInContextMenu: false,
visible: ({ activeDocumentId, stores }) =>
!!activeDocumentId && stores.policies.abilities(activeDocumentId).download,
perform: async ({ activeDocumentId, stores }) => {
if (!activeDocumentId) {
return;
}
const document = stores.documents.get(activeDocumentId);
await document?.download(ExportContentType.Markdown);
await document?.download({
contentType: ExportContentType.Pdf,
includeChildDocuments: false,
});
},
});
export const downloadDocument = createActionWithChildren({
name: ({ t, isMenu }) => (isMenu ? t("Download") : t("Download document")),
analyticsName: "Download document",
section: ActiveDocumentSection,
icon: <DownloadIcon />,
keywords: "export",
visible: ({ activeDocumentId, stores }) =>
!!activeDocumentId && stores.policies.abilities(activeDocumentId).download,
children: [
downloadDocumentAsHTML,
downloadDocumentAsPDF,
downloadDocumentAsMarkdown,
],
});
export const copyDocumentAsMarkdown = createAction({
name: ({ t }) => t("Copy as Markdown"),
section: ActiveDocumentSection,
@@ -614,10 +714,11 @@ export const copyDocumentAsMarkdown = createAction({
? stores.documents.get(activeDocumentId)
: undefined;
if (document) {
const { ProsemirrorHelper } = await import(
"~/models/helpers/ProsemirrorHelper"
);
copy(ProsemirrorHelper.toMarkdown(document));
const res = await client.post("/documents.export", {
id: document.id,
signedUrls: Week.seconds, // 7 days (AWS S3 max for presigned URLs)
});
copy(res.data);
toast.success(t("Markdown copied to clipboard"));
}
},
@@ -636,9 +737,8 @@ export const copyDocumentAsPlainText = createAction({
? 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"));
}
@@ -855,7 +955,50 @@ export const printDocument = createAction({
icon: <PrintIcon />,
visible: ({ activeDocumentId }) => !!(activeDocumentId && window.print),
perform: () => {
queueMicrotask(window.print);
setTimeout(window.print, 0);
},
});
export const openDocumentInDesktop = createAction({
name: ({ t }) => t("Open in desktop app"),
analyticsName: "Open in desktop",
section: ActiveDocumentSection,
icon: <OpenIcon />,
visible: ({ activeDocumentId, stores }) => {
if (!activeDocumentId) {
return false;
}
const document = stores.documents.get(activeDocumentId);
return (
isCloudHosted && (isMac || isWindows) && !!document && !document.isDeleted
);
},
perform: ({ activeDocumentId, stores }) => {
const document = activeDocumentId
? stores.documents.get(activeDocumentId)
: undefined;
if (document) {
window.location.href = desktopify(documentPath(document));
}
},
});
export const presentDocument = createAction({
name: ({ t, isMenu }) => (isMenu ? t("Present") : t("Present document")),
analyticsName: "Present document",
section: ActiveDocumentSection,
icon: <EmbedIcon />,
shortcut: ["Meta+Alt+p"],
visible: ({ activeDocumentId }) => !!activeDocumentId,
perform: ({ activeDocumentId, stores }) => {
const document = activeDocumentId
? stores.documents.get(activeDocumentId)
: undefined;
if (!document) {
return;
}
stores.ui.setPresentingDocument(document);
},
});
@@ -876,7 +1019,7 @@ export const importDocument = createAction({
return false;
},
perform: ({ activeDocumentId, activeCollectionId, stores }) => {
perform: ({ t, activeDocumentId, activeCollectionId, stores }) => {
const { documents } = stores;
const input = document.createElement("input");
input.type = "file";
@@ -885,6 +1028,7 @@ export const importDocument = createAction({
input.onchange = async (ev) => {
const files = getEventFiles(ev);
const file = files[0];
const toastId = toast.loading(`${t("Uploading")}`);
try {
const document = await documents.import(
@@ -898,6 +1042,8 @@ export const importDocument = createAction({
history.push(document.url);
} catch (err) {
toast.error(err.message);
} finally {
toast.dismiss(toastId);
}
};
@@ -915,12 +1061,12 @@ export const createTemplateFromDocument = createAction({
const document = activeDocumentId
? stores.documents.get(activeDocumentId)
: undefined;
if (document?.isTemplate || !document?.isActive) {
if (!document?.isActive) {
return false;
}
return !!(
!!activeCollectionId &&
stores.policies.abilities(activeCollectionId).updateDocument
stores.policies.abilities(activeCollectionId).createTemplate
);
},
perform: ({ activeDocumentId, stores, t, event }) => {
@@ -967,46 +1113,8 @@ export const searchDocumentsForQuery = (query: string) =>
visible: ({ location }) => location.pathname !== searchPath(),
});
export const moveTemplateToWorkspace = createAction({
name: ({ t }) => t("Move to workspace"),
analyticsName: "Move template to workspace",
section: DocumentSection,
icon: <MoveIcon />,
iconInContextMenu: false,
visible: ({ activeDocumentId, stores }) => {
if (!activeDocumentId) {
return false;
}
const document = stores.documents.get(activeDocumentId);
if (!document || !document.template || document.isWorkspaceTemplate) {
return false;
}
return !!stores.policies.abilities(activeDocumentId).move;
},
perform: async ({ activeDocumentId, stores }) => {
if (activeDocumentId) {
const document = stores.documents.get(activeDocumentId);
if (!document) {
return;
}
await document.move({
collectionId: null,
});
}
},
});
export const moveDocumentToCollection = createAction({
name: ({ activeDocumentId, stores, t }) => {
if (!activeDocumentId) {
return t("Move");
}
const document = stores.documents.get(activeDocumentId);
return document?.template && document?.collectionId
? t("Move to collection")
: t("Move");
},
name: ({ t }) => t("Move"),
analyticsName: "Move document",
section: ActiveDocumentSection,
icon: <MoveIcon />,
@@ -1044,8 +1152,7 @@ export const moveDocument = createAction({
return false;
}
const document = stores.documents.get(activeDocumentId);
// Don't show the button if this is a non-workspace template.
if (!document || (document.template && !document.isWorkspaceTemplate)) {
if (!document) {
return false;
}
return !!stores.policies.abilities(activeDocumentId).move;
@@ -1053,25 +1160,6 @@ export const moveDocument = createAction({
perform: moveDocumentToCollection.perform,
});
export const moveTemplate = createActionWithChildren({
name: ({ t }) => t("Move"),
analyticsName: "Move document",
section: ActiveDocumentSection,
icon: <MoveIcon />,
visible: ({ activeDocumentId, stores }) => {
if (!activeDocumentId) {
return false;
}
const document = stores.documents.get(activeDocumentId);
// Don't show the menu if this is not a template (or) a workspace template.
if (!document || !document.template || document.isWorkspaceTemplate) {
return false;
}
return !!stores.policies.abilities(activeDocumentId).move;
},
children: [moveTemplateToWorkspace, moveDocumentToCollection],
});
export const archiveDocument = createAction({
name: ({ t }) => `${t("Archive")}`,
analyticsName: "Archive document",
@@ -1130,10 +1218,7 @@ export const restoreDocument = createAction({
: undefined;
const can = stores.policies.abilities(document.id);
return (
!!(document.isWorkspaceTemplate || collection?.isActive) &&
!!(can.restore || can.unarchive)
);
return !!collection?.isActive && !!(can.restore || can.unarchive);
},
perform: async ({ t, stores, activeDocumentId }) => {
const document = activeDocumentId
@@ -1170,10 +1255,7 @@ export const restoreDocumentToCollection = createActionWithChildren({
? stores.collections.get(document.collectionId)
: undefined;
return (
!(document.isWorkspaceTemplate || collection?.isActive) &&
!!(can.restore || can.unarchive)
);
return !collection?.isActive && !!(can.restore || can.unarchive);
},
children: ({ t, activeDocumentId, stores }) => {
const { collections, documents, policies } = stores;
@@ -1315,7 +1397,7 @@ export const openDocumentComments = createAction({
return;
}
stores.ui.toggleComments();
stores.ui.set({ rightSidebar: "comments" });
},
});
@@ -1350,6 +1432,7 @@ export const openDocumentInsights = createAction({
name: ({ t }) => t("Insights"),
analyticsName: "Open document insights",
section: ActiveDocumentSection,
shortcut: [`Meta+Shift+I`],
icon: <GraphIcon />,
visible: ({ activeDocumentId, stores }) => {
const can = stores.policies.abilities(activeDocumentId ?? "");
@@ -1357,12 +1440,7 @@ export const openDocumentInsights = createAction({
? stores.documents.get(activeDocumentId)
: undefined;
return (
!!activeDocumentId &&
can.listViews &&
!document?.isTemplate &&
!document?.isDeleted
);
return !!activeDocumentId && can.listViews && !document?.isDeleted;
},
perform: ({ activeDocumentId, stores, t }) => {
const document = activeDocumentId
@@ -1441,11 +1519,15 @@ export const rootDocumentActions = [
archiveDocument,
createDocument,
createDraftDocument,
createNewDocument,
createNestedDocument,
createTemplateFromDocument,
deleteDocument,
importDocument,
downloadDocument,
downloadDocumentAsMarkdown,
downloadDocumentAsHTML,
downloadDocumentAsPDF,
copyDocumentLink,
copyDocumentShareLink,
copyDocumentAsMarkdown,
@@ -1459,16 +1541,17 @@ export const rootDocumentActions = [
searchInDocument,
duplicateDocument,
leaveDocument,
moveTemplateToWorkspace,
moveDocumentToCollection,
openRandomDocument,
permanentlyDeleteDocument,
permanentlyDeleteDocumentsInTrash,
presentDocument,
printDocument,
pinDocumentToCollection,
pinDocumentToHome,
openDocumentComments,
openDocumentHistory,
openDocumentInsights,
openDocumentInDesktop,
shareDocument,
];
+2 -2
View File
@@ -2,9 +2,9 @@ import { TrashIcon } from "outline-icons";
import stores from "~/stores";
import { createAction } from "..";
import { SettingsSection } from "../sections";
import Integration from "~/models/Integration";
import type Integration from "~/models/Integration";
import { DisconnectAnalyticsDialog } from "~/scenes/Settings/components/DisconnectAnalyticsDialog";
import { IntegrationType } from "@shared/types";
import type { IntegrationType } from "@shared/types";
import { settingsPath } from "@shared/utils/routeHelpers";
import history from "~/utils/history";
+3 -3
View File
@@ -17,7 +17,7 @@ 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,
@@ -224,13 +224,13 @@ export const openKeyboardShortcuts = createAction({
export const downloadApp = createExternalLinkAction({
name: ({ t }) =>
t("Download {{ platform }} app", {
platform: isMac() ? "macOS" : "Windows",
platform: isMac ? "macOS" : "Windows",
}),
analyticsName: "Download app",
section: NavigationSection,
iconInContextMenu: false,
icon: <BrowserIcon />,
visible: () => !Desktop.isElectron() && isMac() && isCloudHosted,
visible: () => !Desktop.isElectron() && isMac && isCloudHosted,
url: "https://desktop.getoutline.com",
target: "_blank",
});
+32 -1
View File
@@ -1,6 +1,7 @@
import { ArchiveIcon, MarkAsReadIcon } from "outline-icons";
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"),
@@ -22,6 +23,36 @@ export const markNotificationsAsArchived = createAction({
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,
+102 -31
View File
@@ -1,14 +1,17 @@
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 } 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 = createAction({
@@ -73,37 +76,105 @@ export const deleteRevision = createAction({
},
});
export const copyLinkToRevision = createAction({
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 = [];
+16 -1
View File
@@ -25,6 +25,21 @@ export const changeToLightTheme = createAction({
perform: ({ stores }) => stores.ui.setTheme(Theme.Light),
});
export const toggleTheme = createAction({
name: ({ t }) => t("Toggle theme"),
analyticsName: "Change theme",
iconInContextMenu: false,
icon: ({ stores }) =>
stores.ui.resolvedTheme === "light" ? <MoonIcon /> : <SunIcon />,
keywords: "theme light day",
section: SettingsSection,
shortcut: ["Meta+Shift+l"],
perform: ({ stores }) =>
stores.ui.setTheme(
stores.ui.resolvedTheme === "light" ? Theme.Dark : Theme.Light
),
});
export const changeToSystemTheme = createAction({
name: ({ t }) => t("System"),
analyticsName: "Change to system theme",
@@ -47,4 +62,4 @@ export const changeTheme = createActionWithChildren({
children: [changeToLightTheme, changeToDarkTheme, changeToSystemTheme],
});
export const rootSettingsActions = [changeTheme];
export const rootSettingsActions = [changeTheme, toggleTheme];
+1 -1
View File
@@ -1,5 +1,5 @@
import copy from "copy-to-clipboard";
import Share from "~/models/Share";
import type Share from "~/models/Share";
import { createAction, createInternalLinkAction } from "..";
import { ArrowIcon, CopyIcon, TrashIcon } from "outline-icons";
import { ShareSection } from "../sections";
+2 -2
View File
@@ -1,7 +1,7 @@
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";
@@ -10,7 +10,7 @@ import {
createActionWithChildren,
createExternalLinkAction,
} from "~/actions";
import { ActionContext, ExternalLinkAction } from "~/types";
import type { ActionContext, ExternalLinkAction } from "~/types";
import Desktop from "~/utils/Desktop";
import { TeamSection } from "../sections";
+231
View File
@@ -0,0 +1,231 @@
import copy from "copy-to-clipboard";
import {
CaseSensitiveIcon,
CollectionIcon,
CopyIcon,
MoveIcon,
NewDocumentIcon,
PlusIcon,
PrintIcon,
TrashIcon,
} from "outline-icons";
import { Trans } from "react-i18next";
import { toast } from "sonner";
import ConfirmationDialog from "~/components/ConfirmationDialog";
import TemplateMove from "~/components/DocumentExplorer/TemplateMove";
import {
createAction,
createActionWithChildren,
createInternalLinkAction,
} from "~/actions";
import history from "~/utils/history";
import {
newDocumentPath,
newTemplatePath,
settingsPath,
urlify,
} from "~/utils/routeHelpers";
import { ActiveTemplateSection, TemplateSection } from "../sections";
import Template from "~/models/Template";
import { AvatarSize } from "~/components/Avatar";
import TeamLogo from "~/components/TeamLogo";
export const createTemplate = createInternalLinkAction({
name: ({ t }) => t("New template"),
analyticsName: "New template",
section: TemplateSection,
icon: <PlusIcon />,
keywords: "new create template",
visible: ({ currentTeamId, stores }) =>
!!stores.policies.abilities(currentTeamId!).createTemplate,
to: newTemplatePath(),
});
export const deleteTemplate = createAction({
name: ({ t }) => `${t("Delete")}`,
analyticsName: "Delete template",
section: ActiveTemplateSection,
icon: <TrashIcon />,
dangerous: true,
visible: ({ getActivePolicies }) =>
getActivePolicies(Template).some((policy) => policy.abilities.delete),
perform: ({ getActiveModel, stores, t }) => {
const template = getActiveModel(Template);
if (!template) {
return;
}
stores.dialogs.openModal({
title: t("Delete {{ documentName }}", {
documentName: t("template"),
}),
content: (
<ConfirmationDialog
onSubmit={async () => {
await template.delete();
history.push(settingsPath("templates"));
toast.success(t("Template deleted"));
}}
savingText={`${t("Deleting")}`}
danger
>
<Trans
defaults="Are you sure about that? Deleting the <em>{{ templateName }}</em> template is permanent."
values={{
templateName: template.titleWithDefault,
}}
components={{
em: <strong />,
}}
/>
</ConfirmationDialog>
),
});
},
});
export const moveTemplateToWorkspace = createAction({
name: ({ t }) => t("Move to workspace"),
analyticsName: "Move template to workspace",
section: ActiveTemplateSection,
icon: ({ stores }) => {
const { team } = stores.auth;
return <TeamLogo model={team} size={AvatarSize.Small} />;
},
visible: ({ getActiveModel }) => {
const template = getActiveModel(Template);
return !!template?.collectionId;
},
perform: async ({ getActiveModel, stores, t }) => {
const template = getActiveModel(Template);
if (!template) {
return;
}
try {
await template.save({ collectionId: null });
toast.success(t("Template moved"));
stores.dialogs.closeAllModals();
} catch (_err) {
toast.error(t("Couldn't move the template, try again?"));
}
},
});
export const moveTemplateToCollection = createAction({
name: ({ t }) => t("Move to collection"),
analyticsName: "Move template to collection",
section: ActiveTemplateSection,
icon: <CollectionIcon />,
perform: ({ getActiveModel, stores, t }) => {
const template = getActiveModel(Template);
if (!template) {
return;
}
stores.dialogs.openModal({
title: t("Move template"),
content: <TemplateMove template={template} />,
});
},
});
export const moveTemplate = createActionWithChildren({
name: ({ t }) => t("Move"),
analyticsName: "Move template",
section: ActiveTemplateSection,
icon: <MoveIcon />,
visible: ({ getActivePolicies }) =>
getActivePolicies(Template).some((policy) => policy.abilities.move),
children: [moveTemplateToWorkspace, moveTemplateToCollection],
});
export const createDocumentFromTemplate = createInternalLinkAction({
name: ({ t }) => t("New document"),
analyticsName: "New document from template",
section: ActiveTemplateSection,
icon: <NewDocumentIcon />,
keywords: "create",
visible: ({ currentTeamId, getActiveModel, stores }) => {
const template = getActiveModel(Template);
if (!template || !currentTeamId) {
return false;
}
if (template.collectionId) {
return !!stores.policies.abilities(template.collectionId).createDocument;
}
return !!stores.policies.abilities(currentTeamId).createDocument;
},
to: ({ getActiveModel, activeCollectionId, sidebarContext }) => {
const template = getActiveModel(Template);
if (!template) {
return "";
}
const collectionId = template?.collectionId ?? activeCollectionId;
const [pathname, search] = newDocumentPath(collectionId, {
templateId: template.id,
}).split("?");
return {
pathname,
search,
state: { sidebarContext },
};
},
});
export const copyTemplateLink = createAction({
name: ({ t }) => t("Copy link"),
analyticsName: "Copy template link",
section: ActiveTemplateSection,
icon: <CopyIcon />,
iconInContextMenu: false,
perform: ({ getActiveModel, t }) => {
const template = getActiveModel(Template);
if (template) {
copy(urlify(template.path));
toast.success(t("Link copied to clipboard"));
}
},
});
export const copyTemplateAsPlainText = createAction({
name: ({ t }) => t("Copy as text"),
analyticsName: "Copy template as text",
section: ActiveTemplateSection,
icon: <CaseSensitiveIcon />,
iconInContextMenu: false,
perform: async ({ getActiveModel, t }) => {
const template = getActiveModel(Template);
if (template) {
const { ProsemirrorHelper } =
await import("~/models/helpers/ProsemirrorHelper");
copy(ProsemirrorHelper.toPlainText(template));
toast.success(t("Text copied to clipboard"));
}
},
});
export const copyTemplate = createActionWithChildren({
name: ({ t }) => t("Copy"),
analyticsName: "Copy template",
section: ActiveTemplateSection,
icon: <CopyIcon />,
keywords: "clipboard",
children: [copyTemplateLink, copyTemplateAsPlainText],
});
export const printTemplate = createAction({
name: ({ t, isMenu }) => (isMenu ? t("Print") : t("Print template")),
analyticsName: "Print template",
section: ActiveTemplateSection,
icon: <PrintIcon />,
visible: ({ getActiveModel }) => !!getActiveModel(Template) && !!window.print,
perform: () => {
setTimeout(window.print, 0);
},
});
export const rootTemplateActions = [moveTemplate, createDocumentFromTemplate];
+2 -2
View File
@@ -1,8 +1,8 @@
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,
+4 -4
View File
@@ -1,8 +1,8 @@
import { LocationDescriptor } from "history";
import type { LocationDescriptor } from "history";
import { toast } from "sonner";
import { Optional } from "utility-types";
import type { Optional } from "utility-types";
import { v4 as uuidv4 } from "uuid";
import {
import type {
ActionContext,
Action,
ActionGroup,
@@ -15,7 +15,7 @@ import {
} 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;
+10 -1
View File
@@ -1,4 +1,4 @@
import { ActionContext } from "~/types";
import type { ActionContext } from "~/types";
export const CollectionSection = ({ t }: ActionContext) => t("Collection");
@@ -24,6 +24,15 @@ export const ActiveDocumentSection = ({ t, stores }: ActionContext) => {
ActiveDocumentSection.priority = 0.9;
export const TemplateSection = ({ t }: ActionContext) => t("Template");
export const ActiveTemplateSection = ({ t, stores }: ActionContext) => {
const activeTemplate = stores.templates.active;
return `${t("Template")} · ${activeTemplate?.titleWithDefault}`;
};
ActiveTemplateSection.priority = 0.9;
export const RecentSection = ({ t }: ActionContext) => t("Recently viewed");
RecentSection.priority = 1;
+4 -3
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 type { Props as TooltipProps } from "~/components/Tooltip";
import Tooltip from "~/components/Tooltip";
import { performAction, resolve } from "~/actions";
import useIsMounted from "~/hooks/useIsMounted";
import useActionContext from "~/hooks/useActionContext";
import { ActionVariant, ActionWithChildren } from "~/types";
import type { ActionVariant, ActionWithChildren } from "~/types";
export type Props = React.HTMLAttributes<HTMLButtonElement> & {
/** Show the button in a disabled state */
@@ -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>
) {
+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 = {
+22 -65
View File
@@ -1,17 +1,10 @@
import { AnimatePresence } from "framer-motion";
import { observer } from "mobx-react";
import * as React from "react";
import {
Switch,
Route,
useLocation,
matchPath,
Redirect,
} from "react-router-dom";
import { TeamPreference } from "@shared/types";
import { Switch, Route } from "react-router-dom";
import ErrorSuspended from "~/scenes/Errors/ErrorSuspended";
import Layout from "~/components/Layout";
import RegisterKeyDown from "~/components/RegisterKeyDown";
import { RightSidebarProvider } from "~/components/RightSidebarContext";
import Sidebar from "~/components/Sidebar";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import { usePostLoginPath } from "~/hooks/useLastVisitedPath";
@@ -23,20 +16,13 @@ import {
searchPath,
newDocumentPath,
settingsPath,
matchDocumentHistory,
matchDocumentSlug as slug,
} from "~/utils/routeHelpers";
import { DocumentContextProvider } from "./DocumentContext";
import Fade from "./Fade";
import NotificationBadge from "./NotificationBadge";
import { PortalContext } from "./Portal";
import CommandBar from "./CommandBar";
const DocumentComments = lazyWithRetry(
() => import("~/scenes/Document/components/Comments")
);
const DocumentHistory = lazyWithRetry(
() => import("~/scenes/Document/components/History")
);
const SettingsSidebar = lazyWithRetry(
() => import("~/components/Sidebar/Settings")
);
@@ -47,9 +33,7 @@ type Props = {
const AuthenticatedLayout: React.FC = ({ children }: Props) => {
const { ui, auth } = useStores();
const location = useLocation();
const layoutRef = React.useRef<HTMLDivElement>(null);
const can = usePolicy(ui.activeDocumentId);
const canCollection = usePolicy(ui.activeCollectionId);
const team = useCurrentTeam();
const [spendPostLoginPath] = usePostLoginPath();
@@ -73,15 +57,17 @@ const AuthenticatedLayout: React.FC = ({ children }: Props) => {
history.push(newDocumentPath(activeCollectionId));
};
React.useEffect(() => {
const postLoginPath = spendPostLoginPath();
if (postLoginPath) {
history.replace(postLoginPath);
}
}, [spendPostLoginPath]);
if (auth.isSuspended) {
return <ErrorSuspended />;
}
const postLoginPath = spendPostLoginPath();
if (postLoginPath) {
return <Redirect to={postLoginPath} />;
}
const sidebar = (
<Fade>
<Switch>
@@ -91,49 +77,20 @@ const AuthenticatedLayout: React.FC = ({ children }: Props) => {
</Fade>
);
const showHistory =
!!matchPath(location.pathname, {
path: matchDocumentHistory,
}) && can.listRevisions;
const showComments =
!showHistory &&
can.comment &&
ui.activeDocumentId &&
ui.commentsExpanded &&
!!team.getPreference(TeamPreference.Commenting);
const sidebarRight = (
<AnimatePresence
initial={false}
key={ui.activeDocumentId ? "active" : "inactive"}
>
{(showHistory || showComments) && (
<Route path={`/doc/${slug}`}>
<React.Suspense fallback={null}>
{showHistory && <DocumentHistory />}
{showComments && <DocumentComments />}
</React.Suspense>
</Route>
)}
</AnimatePresence>
);
return (
<DocumentContextProvider>
<PortalContext.Provider value={layoutRef.current}>
<Layout
title={team.name}
sidebar={sidebar}
sidebarRight={sidebarRight}
ref={layoutRef}
>
<RegisterKeyDown trigger="n" handler={goToNewDocument} />
<RegisterKeyDown trigger="t" handler={goToSearch} />
<RegisterKeyDown trigger="/" handler={goToSearch} />
{children}
<CommandBar />
</Layout>
</PortalContext.Provider>
<RightSidebarProvider>
<PortalContext.Provider value={layoutRef.current}>
<Layout title={team.name} sidebar={sidebar} ref={layoutRef}>
<RegisterKeyDown trigger="n" handler={goToNewDocument} />
<RegisterKeyDown trigger="t" handler={goToSearch} />
<RegisterKeyDown trigger="/" handler={goToSearch} />
{children}
<CommandBar />
<NotificationBadge />
</Layout>
</PortalContext.Provider>
</RightSidebarProvider>
</DocumentContextProvider>
);
};
+2 -1
View File
@@ -1,3 +1,4 @@
import { observer } from "mobx-react";
import * as React from "react";
import styled from "styled-components";
import useBoolean from "~/hooks/useBoolean";
@@ -109,4 +110,4 @@ const Image = styled.img<{ size: number }>`
height: ${(props) => props.size}px;
`;
export default Avatar;
export default observer(Avatar);
+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";
+1 -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 = {
+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;
+3 -2
View File
@@ -1,4 +1,5 @@
import { GoToIcon } from "outline-icons";
import { observer } from "mobx-react";
import * as React from "react";
import { Link } from "react-router-dom";
import styled from "styled-components";
@@ -6,7 +7,7 @@ import { s, ellipsis } from "@shared/styles";
import Flex from "~/components/Flex";
import BreadcrumbMenu from "~/menus/BreadcrumbMenu";
import { undraggableOnDesktop } from "~/styles";
import { InternalLinkAction, MenuInternalLink } from "~/types";
import type { InternalLinkAction, MenuInternalLink } from "~/types";
import { actionToMenuItem } from "~/actions";
import useActionContext from "~/hooks/useActionContext";
import { useComputed } from "~/hooks/useComputed";
@@ -121,4 +122,4 @@ const Item = styled(Link)<{ $highlight: boolean; $withIcon: boolean }>`
}
`;
export default React.forwardRef<HTMLDivElement, Props>(Breadcrumb);
export default observer(React.forwardRef<HTMLDivElement, Props>(Breadcrumb));
+10 -4
View File
@@ -1,12 +1,13 @@
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 type { HapticInput } from "web-haptics";
import { useWebHaptics } from "web-haptics/react";
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 = {
@@ -153,6 +154,8 @@ export type Props<T> = ActionButtonProps & {
fullwidth?: boolean;
as?: T;
to?: LocationDescriptor;
/** Haptic feedback to trigger on click. Pass a preset name or custom pattern. */
haptic?: HapticInput;
borderOnHover?: boolean;
hideIcon?: boolean;
href?: string;
@@ -177,11 +180,13 @@ const Button = <T extends React.ElementType = "button">(
hideIcon,
fullwidth,
danger,
haptic,
...rest
} = props;
const hasText = !!children || value !== undefined;
const ic = hideIcon ? undefined : (action?.icon ?? icon);
const hasIcon = ic !== undefined;
const { trigger } = useWebHaptics();
return (
<RealButton
@@ -192,6 +197,7 @@ const Button = <T extends React.ElementType = "button">(
$danger={danger}
$fullwidth={fullwidth}
$borderOnHover={borderOnHover}
onClickCapture={haptic ? () => void trigger(haptic) : undefined}
{...rest}
>
<Inner hasIcon={hasIcon} hasText={hasText} disclosure={disclosure}>
+3 -5
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;
@@ -22,12 +23,9 @@ const Container = styled.div<Props>`
type ContentProps = { $maxWidth?: string };
const Content = styled.div<ContentProps>`
max-width: ${(props) => props.$maxWidth ?? "46em"};
max-width: ${(props: ContentProps) =>
props.$maxWidth ?? EditorStyleHelper.documentWidth};
margin: 0 auto;
${breakpoint("desktopLarge")`
max-width: ${(props: ContentProps) => props.$maxWidth ?? "52em"};
`};
`;
const CenteredContent: React.FC<Props> = ({
+2 -2
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";
@@ -125,8 +125,8 @@ function Collaborators(props: Props) {
return (
<AvatarWithPresence
{...rest}
key={collaborator.id}
{...rest}
user={collaborator}
isPresent={isPresent}
isEditing={isEditing}
+112
View File
@@ -0,0 +1,112 @@
import * as RadixCollapsible from "@radix-ui/react-collapsible";
import { ExpandedIcon } from "outline-icons";
import * as React from "react";
import styled from "styled-components";
import { s } from "@shared/styles";
interface CollapsibleProps {
/** The label displayed on the trigger button. */
label: React.ReactNode;
/** The content to show/hide inside the collapsible panel. */
children: React.ReactNode;
/** Whether the collapsible is open by default. */
defaultOpen?: boolean;
/** Controlled open state. */
open?: boolean;
/** Callback fired when the open state changes. */
onOpenChange?: (open: boolean) => void;
/** Additional class name for the root element. */
className?: string;
}
/**
* An accessible collapsible section built on Radix UI Collapsible.
* Renders a trigger button with a disclosure chevron and animated content panel.
*
* @param props - component props.
* @returns the collapsible component.
*/
export function Collapsible({
label,
children,
defaultOpen = false,
open,
onOpenChange,
className,
}: CollapsibleProps) {
return (
<RadixCollapsible.Root
defaultOpen={defaultOpen}
open={open}
onOpenChange={onOpenChange}
className={className}
>
<StyledTrigger>
<StyledExpandedIcon aria-hidden="true" />
{label}
</StyledTrigger>
<StyledContent>{children}</StyledContent>
</RadixCollapsible.Root>
);
}
const StyledExpandedIcon = styled(ExpandedIcon)`
flex-shrink: 0;
transition: transform 150ms ease-out;
margin-left: -4px;
`;
const StyledTrigger = styled(RadixCollapsible.Trigger)`
display: flex;
align-items: center;
background: none;
border: none;
padding: 0 0 8px 0;
cursor: var(--pointer);
color: ${s("textTertiary")};
font-size: 14pxte
&:hover {
color: ${s("textSecondary")};
}
&[data-state="closed"] {
${StyledExpandedIcon} {
transform: rotate(-90deg);
}
}
`;
const StyledContent = styled(RadixCollapsible.Content)`
overflow: hidden;
&[data-state="open"] {
animation: slideDown 200ms ease-out;
}
&[data-state="closed"] {
animation: slideUp 200ms ease-out;
}
@keyframes slideDown {
from {
height: 0;
opacity: 0;
}
to {
height: var(--radix-collapsible-content-height);
opacity: 1;
}
}
@keyframes slideUp {
from {
height: var(--radix-collapsible-content-height);
opacity: 1;
}
to {
height: 0;
opacity: 0;
}
}
`;
+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;
+114 -55
View File
@@ -7,13 +7,15 @@ import styled from "styled-components";
import Icon from "@shared/components/Icon";
import { randomElement } from "@shared/random";
import { CollectionPermission, TeamPreference } from "@shared/types";
import type { Option } from "~/components/InputSelect";
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 { Collapsible } from "~/components/Collapsible";
import Input from "~/components/Input";
import { InputSelect } from "~/components/InputSelect";
import { InputSelectPermission } from "~/components/InputSelectPermission";
import { createLazyComponent } from "~/components/LazyLoad";
import Switch from "~/components/Switch";
@@ -22,6 +24,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"));
@@ -32,6 +35,7 @@ export interface FormData {
sharing: boolean;
permission: CollectionPermission | undefined;
commenting?: boolean | null;
templateManagement: CollectionPermission;
}
const useIconColor = (collection?: Collection) => {
@@ -66,6 +70,22 @@ export const CollectionForm = observer(function CollectionForm_({
const [hasOpenedIconPicker, setHasOpenedIconPicker] = useBoolean(false);
const templateManagementOptions = useMemo<Option[]>(
() => [
{
type: "item",
label: t("Managers"),
value: CollectionPermission.Admin,
},
{
type: "item",
label: t("Members"),
value: CollectionPermission.ReadWrite,
},
],
[t]
);
const iconColor = useIconColor(collection);
const fallbackIcon = (
<Icon
@@ -91,6 +111,8 @@ export const CollectionForm = observer(function CollectionForm_({
sharing: collection?.sharing ?? true,
permission: collection?.permission,
commenting: collection?.commenting ?? true,
templateManagement:
collection?.templateManagement ?? CollectionPermission.Admin,
color: iconColor,
},
});
@@ -133,60 +155,34 @@ export const CollectionForm = observer(function CollectionForm_({
const initial = values.name.charAt(0).toUpperCase();
return (
<form onSubmit={formHandleSubmit(handleSubmit)}>
<Text as="p">
<Trans>
Collections are used to group documents and choose permissions
</Trans>
</Text>
<Flex gap={8}>
<Input
type="text"
placeholder={t("Name")}
{...register("name", {
required: true,
maxLength: CollectionValidation.maxNameLength,
})}
prefix={
<Suspense fallback={fallbackIcon}>
<StyledIconPicker
icon={values.icon}
color={values.color ?? iconColor}
initial={initial}
popoverPosition="right"
onOpen={setHasOpenedIconPicker}
onChange={handleIconChange}
/>
</Suspense>
}
autoComplete="off"
autoFocus
flex
/>
</Flex>
{/* Following controls are available in create flow, but moved elsewhere for edit */}
{!collection && (
<Controller
control={control}
name="permission"
render={({ field }) => (
<InputSelectPermission
ref={field.ref}
const options = (
<>
<Controller
control={control}
name="templateManagement"
render={({ field }) => (
<>
<InputSelect
value={field.value}
onChange={(
value: CollectionPermission | typeof EmptySelectValue
) => {
field.onChange(value === EmptySelectValue ? null : value);
onChange={(value: string) => {
field.onChange(value as CollectionPermission);
}}
help={t(
"The default access for workspace members, you can share with more users or groups later."
)}
options={templateManagementOptions}
label={t("Manage templates")}
/>
)}
/>
)}
<Text
type="secondary"
size="small"
as="p"
style={{ paddingTop: 4 }}
>
{t(
"Choose who can create and edit templates in this collection."
)}
</Text>
</>
)}
/>
{team.sharing && (
<Controller
@@ -221,8 +217,71 @@ export const CollectionForm = observer(function CollectionForm_({
)}
/>
)}
</>
);
<Flex justify="flex-end">
return (
<form onSubmit={formHandleSubmit(handleSubmit)}>
<Text as="p">
<Trans>
Collections are used to group documents and choose permissions
</Trans>
</Text>
<HStack>
<Input
type="text"
label={t("Name")}
{...register("name", {
required: true,
maxLength: CollectionValidation.maxNameLength,
})}
prefix={
<Suspense fallback={fallbackIcon}>
<StyledIconPicker
icon={values.icon}
color={values.color ?? iconColor}
initial={initial}
popoverPosition="right"
onOpen={setHasOpenedIconPicker}
onChange={handleIconChange}
/>
</Suspense>
}
autoComplete="off"
autoFocus
flex
/>
</HStack>
{/* Following controls are available in create flow, but moved elsewhere for edit */}
{!collection && (
<Controller
control={control}
name="permission"
render={({ field }) => (
<InputSelectPermission
ref={field.ref}
value={field.value}
onChange={(
value: CollectionPermission | typeof EmptySelectValue
) => {
field.onChange(value === EmptySelectValue ? null : value);
}}
help={t(
"The default access for workspace members, you can share with more users or groups later."
)}
/>
)}
/>
)}
{collection ? (
options
) : (
<Collapsible label={t("Advanced options")}>{options}</Collapsible>
)}
<HStack justify="flex-end">
<Button
type="submit"
disabled={formState.isSubmitting || !formState.isValid}
@@ -235,7 +294,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;
+2 -2
View File
@@ -1,7 +1,7 @@
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";
@@ -28,7 +28,7 @@ export const CollectionBreadcrumb: React.FC<Props> = ({ collection }) => {
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";
+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;
`;
@@ -11,15 +11,15 @@ import useStores from "~/hooks/useStores";
import { newDocumentPath } from "~/utils/routeHelpers";
const useTemplatesAction = () => {
const { documents } = useStores();
const { templates } = useStores();
useEffect(() => {
void documents.fetchAllTemplates();
}, [documents]);
void templates.fetchAll();
}, [templates]);
const actions = useMemo(
() =>
documents.templatesAlphabetical.map((template) =>
templates.alphabetical.map((template) =>
createInternalLinkAction({
name: template.titleWithDefault,
analyticsName: "New document",
@@ -66,7 +66,7 @@ const useTemplatesAction = () => {
},
})
),
[documents.templatesAlphabetical]
[templates.alphabetical]
);
const newFromTemplate = useMemo(
+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";
+9 -2
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,
@@ -128,7 +128,14 @@ const ContentEditable = React.forwardRef(function _ContentEditable(
React.useEffect(() => {
if (contentRef.current && value !== contentRef.current.textContent) {
setInnerValue(value);
if (document.activeElement === contentRef.current) {
// Don't reset content while the user is actively editing. Update
// lastValue so that the next input or blur event will push the
// current DOM text back to the model via onChange.
lastValue.current = value;
} else {
setInnerValue(value);
}
}
}, [value, contentRef]);
+107 -61
View File
@@ -1,9 +1,17 @@
import { HomeIcon } from "outline-icons";
import {
CollectionIcon as CollectionIconComponent,
HomeIcon,
PrivateCollectionIcon,
} from "outline-icons";
import { observer } from "mobx-react";
import { getLuminance } from "polished";
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 Icon from "@shared/components/Icon";
import { colorPalette } from "@shared/utils/collections";
import type { Option } from "~/components/InputSelect";
import { InputSelect } from "~/components/InputSelect";
import useStores from "~/hooks/useStores";
type DefaultCollectionInputSelectProps = {
@@ -11,74 +19,112 @@ type DefaultCollectionInputSelectProps = {
defaultCollectionId: string | null;
};
const DefaultCollectionInputSelect = ({
onSelectCollection,
defaultCollectionId,
}: DefaultCollectionInputSelectProps) => {
const { t } = useTranslation();
const { collections } = useStores();
const [fetching, setFetching] = useState(false);
const [fetchError, setFetchError] = useState();
const DefaultCollectionInputSelect = observer(
({
onSelectCollection,
defaultCollectionId,
}: DefaultCollectionInputSelectProps) => {
const { t } = useTranslation();
const { collections, ui } = useStores();
const [fetching, setFetching] = useState(false);
const [fetchError, setFetchError] = useState();
React.useEffect(() => {
async function fetchData() {
if (!collections.isLoaded && !fetching && !fetchError) {
try {
setFetching(true);
await collections.fetchPage({
limit: 100,
});
} catch (error) {
toast.error(
t("Collections could not be loaded, please reload the app")
);
setFetchError(error);
} finally {
setFetching(false);
React.useEffect(() => {
async function fetchData() {
if (!collections.isLoaded && !fetching && !fetchError) {
try {
setFetching(true);
await collections.fetchPage({
limit: 100,
});
} catch (error) {
toast.error(
t("Collections could not be loaded, please reload the app")
);
setFetchError(error);
} finally {
setFetching(false);
}
}
}
}
void fetchData();
}, [fetchError, t, fetching, collections]);
void fetchData();
}, [fetchError, t, fetching, collections]);
const options: Option[] = React.useMemo(
() =>
collections.nonPrivate.reduce(
(acc, collection) => [
if (fetching) {
return null;
}
const isDark = ui.resolvedTheme === "dark";
// Eagerly resolve collection icon properties within this observer context
// to avoid MobX warnings when Radix Select clones elements for the trigger.
const options: Option[] = collections.nonPrivate.reduce(
(acc, collection) => {
const collectionIcon = collection.icon;
const rawColor = collection.color ?? colorPalette[0];
let icon: React.ReactElement;
if (!collectionIcon || collectionIcon === "collection") {
const color =
isDark && rawColor !== "currentColor"
? getLuminance(rawColor) > 0.09
? rawColor
: "currentColor"
: rawColor;
const Component = collection.isPrivate
? PrivateCollectionIcon
: CollectionIconComponent;
icon = <Component color={color} />;
} else {
let color = rawColor;
if (color !== "currentColor") {
if (isDark) {
color = getLuminance(color) > 0.09 ? color : "currentColor";
} else {
color = getLuminance(color) < 0.9 ? color : "currentColor";
}
}
icon = (
<Icon
value={collectionIcon}
color={color}
initial={collection.initial}
forceColor
/>
);
}
return [
...acc,
{
type: "item",
type: "item" as const,
label: collection.name,
value: collection.id,
icon: <CollectionIcon collection={collection} />,
icon,
},
],
[
{
type: "item",
label: t("Home"),
value: "home",
icon: <HomeIcon />,
},
] satisfies Option[]
),
[collections.nonPrivate, t]
);
];
},
[
{
type: "item",
label: t("Home"),
value: "home",
icon: <HomeIcon />,
},
] satisfies Option[]
);
if (fetching) {
return null;
return (
<InputSelect
options={options}
value={defaultCollectionId ?? "home"}
onChange={onSelectCollection}
label={t("Start view")}
hideLabel
short
/>
);
}
return (
<InputSelect
options={options}
value={defaultCollectionId ?? "home"}
onChange={onSelectCollection}
label={t("Start view")}
hideLabel
short
/>
);
};
);
export default DefaultCollectionInputSelect;
+3 -10
View File
@@ -1,17 +1,17 @@
import { observer } from "mobx-react";
import { ArchiveIcon, GoToIcon, ShapesIcon, TrashIcon } from "outline-icons";
import { ArchiveIcon, GoToIcon, TrashIcon } from "outline-icons";
import * as React from "react";
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 { archivePath, trashPath } from "~/utils/routeHelpers";
import { createInternalLinkAction } from "~/actions";
import { ActiveDocumentSection } from "~/actions/sections";
@@ -67,13 +67,6 @@ function DocumentBreadcrumb(
visible: document.isArchived,
to: archivePath(),
}),
createInternalLinkAction({
name: t("Templates"),
section: ActiveDocumentSection,
icon: <ShapesIcon />,
visible: document.template,
to: settingsPath("templates"),
}),
createInternalLinkAction({
name: collection?.name,
section: ActiveDocumentSection,
+2 -2
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";
+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 */
+185
View File
@@ -0,0 +1,185 @@
import { observer } from "mobx-react";
import { useCallback, useMemo, useState } from "react";
import { Trans, useTranslation } from "react-i18next";
import { toast } from "sonner";
import styled from "styled-components";
import { ExportContentType, NotificationEventType } from "@shared/types";
import type Document from "~/models/Document";
import ConfirmationDialog from "~/components/ConfirmationDialog";
import Flex from "~/components/Flex";
import Text from "~/components/Text";
import env from "~/env";
import useCurrentUser from "~/hooks/useCurrentUser";
import useStores from "~/hooks/useStores";
type Props = {
document: Document;
onSubmit: () => void;
};
export const DocumentDownload = observer(({ document, onSubmit }: Props) => {
const { t } = useTranslation();
const { ui } = useStores();
const user = useCurrentUser();
const hasChildDocuments = !!document.childDocuments.length;
const [contentType, setContentType] = useState<ExportContentType>(
ExportContentType.Markdown
);
const [includeChildDocuments, setIncludeChildDocuments] =
useState<boolean>(hasChildDocuments);
const handleContentTypeChange = useCallback(
(ev: React.ChangeEvent<HTMLInputElement>) => {
setContentType(ev.target.value as ExportContentType);
},
[]
);
const handleIncludeChildDocumentsChange = useCallback(
(ev: React.ChangeEvent<HTMLInputElement>) => {
setIncludeChildDocuments(ev.target.checked);
},
[]
);
const handleSubmit = useCallback(async () => {
const response = await document.download({
contentType,
includeChildDocuments,
});
if (includeChildDocuments && response?.data?.fileOperation) {
const fileOperationId = response.data.fileOperation.id;
const toastId = `export-${fileOperationId}`;
const timeoutId = setTimeout(() => {
toast.success(t("Export started"), {
id: toastId,
description: t("A link to your file will be sent through email soon"),
duration: 3000,
});
ui.exportToasts.delete(fileOperationId);
}, 6000);
ui.registerExportToast(fileOperationId, toastId, timeoutId);
toast.loading(t("Export started"), {
id: toastId,
description: `${t("Preparing your download")}`,
duration: Infinity,
});
}
onSubmit();
}, [t, ui, document, contentType, includeChildDocuments, onSubmit]);
const items = useMemo(() => {
const radioItems = [
{
title: "Markdown",
description: t(
"A file containing the selected documents in Markdown format."
),
value: ExportContentType.Markdown,
},
{
title: "HTML",
description: t(
"A file containing the selected documents in HTML format."
),
value: ExportContentType.Html,
},
];
if (env.PDF_EXPORT_ENABLED) {
radioItems.push({
title: "PDF",
description: t(
"A file containing the selected documents in PDF format."
),
value: ExportContentType.Pdf,
});
}
return radioItems;
}, [t]);
return (
<ConfirmationDialog
onSubmit={handleSubmit}
submitText={includeChildDocuments ? t("Export") : t("Download")}
>
<Flex gap={12} column>
{items.map((item) => (
<Option key={item.value}>
<StyledInput
type="radio"
name="format"
value={item.value}
checked={contentType === item.value}
onChange={handleContentTypeChange}
/>
<div>
<Text as="p" size="small" weight="bold">
{item.title}
</Text>
{item.description ? (
<Text size="small" type="secondary">
{item.description}
</Text>
) : null}
</div>
</Option>
))}
</Flex>
{hasChildDocuments && (
<>
<hr style={{ margin: "16px 0 " }} />
<Option>
<StyledInput
type="checkbox"
name="includeChildDocuments"
checked={includeChildDocuments}
onChange={handleIncludeChildDocumentsChange}
/>
<Flex column gap={4}>
<Text as="p" size="small" weight="bold">
{t("Include child documents")}
</Text>
<Text as="p" size="small" type="secondary">
<Trans
defaults="When selected, exporting the document <em>{{documentName}}</em> may take some time."
values={{
documentName: document.titleWithDefault,
}}
components={{
em: <strong />,
}}
/>{" "}
{user.subscribedToEventType(
NotificationEventType.ExportCompleted
) && t("You will receive an email when it's complete.")}
</Text>
</Flex>
</Option>
</>
)}
</ConfirmationDialog>
);
});
const Option = styled.label`
display: flex;
align-items: baseline;
gap: 16px;
p {
margin: 0;
}
`;
const StyledInput = styled.input`
position: relative;
top: 1.5px;
`;
@@ -0,0 +1,17 @@
import styled from "styled-components";
import Flex from "../Flex";
export const FlexContainer = styled(Flex)`
margin-left: -24px;
margin-right: -24px;
margin-bottom: -24px;
outline: none;
`;
export const Footer = styled(Flex)`
height: 64px;
border-top: 1px solid ${(props) => props.theme.horizontalRule};
padding-left: 24px;
padding-right: 24px;
flex-shrink: 0;
`;
@@ -3,15 +3,15 @@ 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 { FlexContainer, Footer, StyledText } from "~/scenes/DocumentMove";
import type { NavigationNode } from "@shared/types";
import type Document from "~/models/Document";
import Button from "~/components/Button";
import DocumentExplorer from "~/components/DocumentExplorer";
import Switch from "~/components/Switch";
import Text from "~/components/Text";
import useCollectionTrees from "~/hooks/useCollectionTrees";
import useStores from "~/hooks/useStores";
import Switch from "./Switch";
import Text from "./Text";
import { FlexContainer, Footer } from "./Components";
import DocumentExplorer from "./DocumentExplorer";
type Props = {
/** The original document to duplicate */
@@ -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
@@ -36,13 +37,8 @@ function DocumentCopy({ document, onSubmit }: Props) {
: true
);
if (document.isTemplate) {
return nodes
.filter((node) => node.type === "collection")
.map((node) => ({ ...node, children: [] }));
}
return nodes;
}, [policies, collectionTrees, document.isTemplate]);
}, [policies, collectionTrees]);
const copy = async () => {
if (!selectedPath) {
@@ -51,6 +47,7 @@ function DocumentCopy({ document, onSubmit }: Props) {
}
try {
setCopying(true);
const result = await document.duplicate({
publish,
recursive,
@@ -65,6 +62,8 @@ function DocumentCopy({ document, onSubmit }: Props) {
onSubmit(result);
} catch (_err) {
toast.error(t("Couldnt copy the document, try again?"));
} finally {
setCopying(false);
}
};
@@ -76,34 +75,32 @@ function DocumentCopy({ document, onSubmit }: Props) {
onSelect={selectPath}
defaultValue={document.parentDocumentId || document.collectionId || ""}
/>
{!document.isTemplate && (
<OptionsContainer>
{document.collectionId && (
<Text size="small">
<Switch
name="publish"
label={t("Publish")}
labelPosition="right"
checked={publish}
onChange={setPublish}
/>
</Text>
)}
{document.publishedAt && document.childDocuments.length > 0 && (
<Text size="small">
<Switch
name="recursive"
label={t("Include nested documents")}
labelPosition="right"
checked={recursive}
onChange={setRecursive}
/>
</Text>
)}
</OptionsContainer>
)}
<OptionsContainer>
{document.collectionId && (
<Text size="small">
<Switch
name="publish"
label={t("Publish")}
labelPosition="right"
checked={publish}
onChange={setPublish}
/>
</Text>
)}
{document.publishedAt && document.childDocuments.length > 0 && (
<Text size="small">
<Switch
name="recursive"
label={t("Include nested documents")}
labelPosition="right"
checked={recursive}
onChange={setRecursive}
/>
</Text>
)}
</OptionsContainer>
<Footer justify="space-between" align="center" gap={8}>
<StyledText type="secondary">
<Text ellipsis type="secondary">
{selectedPath ? (
<Trans
defaults="Copy to <em>{{ location }}</em>"
@@ -113,9 +110,9 @@ function DocumentCopy({ document, onSubmit }: Props) {
) : (
t("Select a location to copy")
)}
</StyledText>
<Button disabled={!selectedPath} onClick={copy}>
{t("Copy")}
</Text>
<Button disabled={!selectedPath || copying} onClick={copy}>
{copying ? `${t("Copying")}` : t("Copy")}
</Button>
</Footer>
</FlexContainer>
@@ -12,15 +12,14 @@ import * as React from "react";
import { useTranslation } from "react-i18next";
import AutoSizer from "react-virtualized-auto-sizer";
import { FixedSizeList as List } from "react-window";
import scrollIntoView from "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 DocumentExplorerNode from "./DocumentExplorerNode";
import DocumentExplorerSearchResult from "./DocumentExplorerSearchResult";
import Flex from "~/components/Flex";
import CollectionIcon from "~/components/Icons/CollectionIcon";
import { Outline } from "~/components/Input";
@@ -38,9 +37,39 @@ type Props = {
items: NavigationNode[];
/** Automatically expand to and select item with the given id */
defaultValue?: string;
/** Whether to show child documents */
showDocuments?: boolean;
};
function DocumentExplorer({ onSubmit, onSelect, items, defaultValue }: Props) {
const VERTICAL_PADDING = 6;
const HORIZONTAL_PADDING = 24;
const innerElementType = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(function innerElementType(
{ style, ...rest }: React.HTMLAttributes<HTMLDivElement>,
ref
) {
return (
<div
ref={ref}
style={{
...style,
height: `${parseFloat(style?.height + "") + VERTICAL_PADDING * 2}px`,
}}
{...rest}
/>
);
});
function DocumentExplorer({
onSubmit,
onSelect,
items,
defaultValue,
showDocuments,
}: Props) {
const isMobile = useMobile();
const { collections, documents } = useStores();
const { t } = useTranslation();
@@ -59,8 +88,6 @@ function DocumentExplorer({ onSubmit, onSelect, items, defaultValue }: Props) {
return node || null;
}
);
const [initialScrollOffset, setInitialScrollOffset] =
React.useState<number>(0);
const [activeNode, setActiveNode] = React.useState<number>(0);
const [expandedNodes, setExpandedNodes] = React.useState<string[]>(() => {
if (defaultValue) {
@@ -83,9 +110,6 @@ function DocumentExplorer({ onSubmit, onSelect, items, defaultValue }: Props) {
);
const listRef = React.useRef<List<NavigationNode[]>>(null);
const VERTICAL_PADDING = 6;
const HORIZONTAL_PADDING = 24;
const searchIndex = React.useMemo(
() =>
new FuzzySearch(flatten(items.map(flattenTree)), ["title"], {
@@ -136,24 +160,18 @@ function DocumentExplorer({ onSubmit, onSelect, items, defaultValue }: Props) {
setTimeout(() => listRef.current?.scrollToItem(index, "center"), 50);
}
}
}, [defaultValue, selectedNode, nodes]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [defaultValue]);
const baseDepth = nodes.reduce(
(min, node) => (node.depth ? Math.min(min, node.depth) : min),
Infinity
);
const normalizedBaseDepth = baseDepth === Infinity ? 0 : baseDepth;
const normalizedBaseDepth =
(baseDepth === Infinity ? 0 : baseDepth) + (showDocuments ? 0 : 1);
const scrollNodeIntoView = React.useCallback(
(node: number) => {
if (itemRefs[node] && itemRefs[node].current) {
scrollIntoView(itemRefs[node].current as HTMLSpanElement, {
behavior: "auto",
block: "center",
});
}
},
[itemRefs]
);
const scrollNodeIntoView = React.useCallback((node: number) => {
listRef.current?.scrollToItem(node, "smart");
}, []);
const handleSearch = (ev: React.ChangeEvent<HTMLInputElement>) => {
setSearchTerm(ev.target.value);
@@ -161,16 +179,16 @@ function DocumentExplorer({ onSubmit, onSelect, items, defaultValue }: Props) {
const isExpanded = (node: number) => includes(expandedNodes, nodes[node].id);
const calculateInitialScrollOffset = (itemCount: number) => {
const preserveScrollOffset = (itemCount: number) => {
if (listRef.current) {
const { height, itemSize } = listRef.current.props;
const { scrollOffset } = listRef.current.state as {
scrollOffset: number;
};
const itemsHeight = itemCount * itemSize;
return itemsHeight < Number(height) ? 0 : scrollOffset;
const offset = itemsHeight < Number(height) ? 0 : scrollOffset;
setTimeout(() => listRef.current?.scrollTo(offset), 0);
}
return 0;
};
const collapse = (node: number) => {
@@ -181,8 +199,7 @@ function DocumentExplorer({ onSubmit, onSelect, items, defaultValue }: Props) {
// remove children
const newNodes = filter(nodes, (n) => !includes(descendantIds, n.id));
const scrollOffset = calculateInitialScrollOffset(newNodes.length);
setInitialScrollOffset(scrollOffset);
preserveScrollOffset(newNodes.length);
};
const expand = (node: number) => {
@@ -191,8 +208,7 @@ function DocumentExplorer({ onSubmit, onSelect, items, defaultValue }: Props) {
// add children
const newNodes = nodes.slice();
newNodes.splice(node + 1, 0, ...descendants(nodes[node], 1));
const scrollOffset = calculateInitialScrollOffset(newNodes.length);
setInitialScrollOffset(scrollOffset);
preserveScrollOffset(newNodes.length);
};
React.useEffect(() => {
@@ -216,7 +232,8 @@ function DocumentExplorer({ onSubmit, onSelect, items, defaultValue }: Props) {
};
const hasChildren = (node: number) =>
nodes[node].children.length > 0 || nodes[node].type === "collection";
nodes[node].children.length > 0 ||
(showDocuments !== false && nodes[node].type === "collection");
const toggleCollapse = (node: number) => {
if (!hasChildren(node)) {
@@ -378,31 +395,16 @@ function DocumentExplorer({ onSubmit, onSelect, items, defaultValue }: Props) {
}
};
const innerElementType = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(function innerElementType(
{ style, ...rest }: React.HTMLAttributes<HTMLDivElement>,
ref
) {
return (
<div
ref={ref}
style={{
...style,
height: `${parseFloat(style?.height + "") + VERTICAL_PADDING * 2}px`,
}}
{...rest}
/>
);
});
return (
<Container tabIndex={-1} onKeyDown={handleKeyDown}>
<ListSearch
ref={inputSearchRef}
onChange={handleSearch}
placeholder={`${t("Search collections & documents")}`}
placeholder={
showDocuments
? `${t("Search collections & documents")}`
: `${t("Search collections")}`
}
autoFocus
/>
<ListContainer>
@@ -412,14 +414,12 @@ function DocumentExplorer({ onSubmit, onSelect, items, defaultValue }: Props) {
<Flex role="listbox" column>
<List
ref={listRef}
key={nodes.length}
width={width}
height={height}
itemData={nodes}
itemCount={nodes.length}
itemSize={isMobile ? 48 : 32}
innerElementType={innerElementType}
initialScrollOffset={initialScrollOffset}
itemKey={(index, results) => results[index].id}
>
{ListItem}
@@ -40,10 +40,8 @@ function DocumentExplorerNode(
ref: React.RefObject<HTMLSpanElement>
) {
const { t } = useTranslation();
const OFFSET = 12;
const DISCLOSURE = 20;
const width = depth ? depth * DISCLOSURE + OFFSET : DISCLOSURE;
const DISCLOSURE = 24;
const width = (depth + (hasChildren ? 2 : 1)) * DISCLOSURE;
return (
<Node
@@ -54,6 +52,7 @@ function DocumentExplorerNode(
style={style}
onPointerMove={onPointerMove}
role="option"
aria-selected={selected}
>
<Spacer width={width}>
{hasChildren && (
@@ -79,7 +78,7 @@ const Title = styled(Text)`
const StyledDisclosure = styled(Disclosure)`
position: relative;
left: auto;
margin-top: 2px;
margin: 2px 0;
`;
const Spacer = styled(Flex)<{ width: number }>`
@@ -1,10 +1,9 @@
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import scrollIntoView from "scroll-into-view-if-needed";
import styled from "styled-components";
import { ellipsis } from "@shared/styles";
import { Node as SearchResult } from "~/components/DocumentExplorerNode";
import { Node as SearchResult } from "./DocumentExplorerNode";
import Flex from "~/components/Flex";
import Text from "~/components/Text";
@@ -32,28 +31,15 @@ function DocumentExplorerSearchResult({
}: Props) {
const { t } = useTranslation();
const ref = React.useCallback(
(node: HTMLSpanElement | null) => {
if (active && node) {
scrollIntoView(node, {
scrollMode: "if-needed",
behavior: "auto",
block: "nearest",
});
}
},
[active]
);
return (
<SearchResult
ref={ref}
selected={selected}
active={active}
onClick={onClick}
style={style}
onPointerMove={onPointerMove}
role="option"
aria-selected={selected}
>
{icon}
<Flex>
@@ -2,16 +2,14 @@ import { observer } from "mobx-react";
import { useState, useMemo } from "react";
import { useTranslation, Trans } from "react-i18next";
import { toast } from "sonner";
import styled from "styled-components";
import { ellipsis } from "@shared/styles";
import { NavigationNode } from "@shared/types";
import Document from "~/models/Document";
import type { NavigationNode } from "@shared/types";
import type Document from "~/models/Document";
import Button from "~/components/Button";
import DocumentExplorer from "~/components/DocumentExplorer";
import Flex from "~/components/Flex";
import Text from "~/components/Text";
import useCollectionTrees from "~/hooks/useCollectionTrees";
import useStores from "~/hooks/useStores";
import { FlexContainer, Footer } from "./Components";
import DocumentExplorer from "./DocumentExplorer";
type Props = {
document: Document;
@@ -21,6 +19,7 @@ function DocumentMove({ document }: Props) {
const { dialogs, policies } = useStores();
const { t } = useTranslation();
const collectionTrees = useCollectionTrees();
const [moving, setMoving] = useState<boolean>(false);
const [selectedPath, selectPath] = useState<NavigationNode | null>(null);
const items = useMemo(() => {
@@ -43,21 +42,8 @@ function DocumentMove({ document }: Props) {
: true
);
// If the document we're moving is a template, only show collections as
// move targets.
if (document.isTemplate) {
return nodes
.filter((node) => node.type === "collection")
.map((node) => ({ ...node, children: [] }));
}
return nodes;
}, [
policies,
collectionTrees,
document.id,
document.parentDocumentId,
document.isTemplate,
]);
}, [policies, collectionTrees, document.id, document.parentDocumentId]);
const move = async () => {
if (!selectedPath) {
@@ -66,6 +52,7 @@ function DocumentMove({ document }: Props) {
}
try {
setMoving(true);
const { type, id: parentDocumentId } = selectedPath;
const collectionId = selectedPath.collectionId as string;
@@ -81,6 +68,8 @@ function DocumentMove({ document }: Props) {
dialogs.closeAllModals();
} catch (_err) {
toast.error(t("Couldnt move the document, try again?"));
} finally {
setMoving(false);
}
};
@@ -88,7 +77,7 @@ function DocumentMove({ document }: Props) {
<FlexContainer column>
<DocumentExplorer items={items} onSubmit={move} onSelect={selectPath} />
<Footer justify="space-between" align="center" gap={8}>
<StyledText type="secondary">
<Text ellipsis type="secondary">
{selectedPath ? (
<Trans
defaults="Move to <em>{{ location }}</em>"
@@ -102,32 +91,13 @@ function DocumentMove({ document }: Props) {
) : (
t("Select a location to move")
)}
</StyledText>
<Button disabled={!selectedPath} onClick={move}>
{t("Move")}
</Text>
<Button disabled={!selectedPath || moving} onClick={move}>
{moving ? `${t("Moving")}` : t("Move")}
</Button>
</Footer>
</FlexContainer>
);
}
export const FlexContainer = styled(Flex)`
margin-left: -24px;
margin-right: -24px;
margin-bottom: -24px;
outline: none;
`;
export const Footer = styled(Flex)`
height: 64px;
border-top: 1px solid ${(props) => props.theme.horizontalRule};
padding-left: 24px;
padding-right: 24px;
`;
export const StyledText = styled(Text)`
${ellipsis()}
margin-bottom: 0;
`;
export default observer(DocumentMove);
@@ -0,0 +1,87 @@
import { observer } from "mobx-react";
import { useState, useMemo } from "react";
import { useTranslation, Trans } from "react-i18next";
import { toast } from "sonner";
import type { NavigationNode } from "@shared/types";
import type Template from "~/models/Template";
import Button from "~/components/Button";
import Text from "~/components/Text";
import useCollectionTrees from "~/hooks/useCollectionTrees";
import useStores from "~/hooks/useStores";
import { FlexContainer, Footer } from "./Components";
import DocumentExplorer from "./DocumentExplorer";
type Props = {
template: Template;
};
function TemplateMove({ template }: Props) {
const { dialogs, policies } = useStores();
const { t } = useTranslation();
const collectionTrees = useCollectionTrees();
const [selectedPath, selectPath] = useState<NavigationNode | null>(null);
const items = useMemo(
() =>
collectionTrees
.map((node) => ({ ...node, children: [] }))
.filter((node) =>
node.collectionId
? policies.get(node.collectionId)?.abilities.createDocument
: true
),
[policies, collectionTrees]
);
const move = async () => {
if (!selectedPath) {
toast.message(t("Select a location to move"));
return;
}
try {
const collectionId = (selectedPath.collectionId ??
selectedPath.id) as string;
await template.save({ collectionId });
toast.success(t("Template moved"));
dialogs.closeAllModals();
} catch (_err) {
toast.error(t("Couldnt move the template, try again?"));
}
};
return (
<FlexContainer column>
<DocumentExplorer
items={items}
onSubmit={move}
onSelect={selectPath}
showDocuments={false}
/>
<Footer justify="space-between" align="center" gap={8}>
<Text ellipsis type="secondary">
{selectedPath ? (
<Trans
defaults="Move to <em>{{ location }}</em>"
values={{
location: selectedPath.title,
}}
components={{
em: <strong />,
}}
/>
) : (
t("Select a location to move")
)}
</Text>
<Button disabled={!selectedPath} onClick={move}>
{t("Move")}
</Button>
</Footer>
</FlexContainer>
);
}
export default observer(TemplateMove);
+3
View File
@@ -0,0 +1,3 @@
import DocumentExplorer from "./DocumentExplorer";
export default DocumentExplorer;
+74 -70
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";
@@ -21,6 +22,7 @@ import StarButton, { AnimatedStar } from "~/components/Star";
import Tooltip from "~/components/Tooltip";
import useBoolean from "~/hooks/useBoolean";
import useCurrentUser from "~/hooks/useCurrentUser";
import useMobile from "~/hooks/useMobile";
import { useLocationSidebarContext } from "~/hooks/useLocationSidebarContext";
import DocumentMenu from "~/menus/DocumentMenu";
import { documentPath } from "~/utils/routeHelpers";
@@ -38,7 +40,6 @@ type Props = {
showCollection?: boolean;
showPublished?: boolean;
showDraft?: boolean;
showTemplate?: boolean;
};
const SEARCH_RESULT_REGEX = /<b\b[^>]*>(.*?)<\/b>/gi;
@@ -54,9 +55,11 @@ function DocumentListItem(
) {
const { t } = useTranslation();
const user = useCurrentUser();
const theme = useTheme();
const { userMemberships, groupMemberships } = useStores();
const locationSidebarContext = useLocationSidebarContext();
const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean();
const isMobile = useMobile();
let itemRef: React.Ref<HTMLAnchorElement> =
React.useRef<HTMLAnchorElement>(null);
@@ -73,7 +76,6 @@ function DocumentListItem(
showCollection,
showPublished,
showDraft = true,
showTemplate,
highlight,
context,
...rest
@@ -81,7 +83,7 @@ function DocumentListItem(
const queryIsInTitle =
!!highlight &&
!!document.title.toLowerCase().includes(highlight.toLowerCase());
const canStar = !document.isArchived && !document.isTemplate;
const canStar = !document.isArchived;
const isShared = !!(
userMemberships.getByDocumentId(document.id) ||
@@ -99,11 +101,10 @@ function DocumentListItem(
return (
<ActionContextProvider
value={{
activeDocumentId: document.id,
activeCollectionId:
!isShared && document.collectionId
? document.collectionId
: undefined,
activeModels: [
document,
...(!isShared && document.collection ? [document.collection] : []),
],
}}
>
<ContextMenu
@@ -119,6 +120,9 @@ function DocumentListItem(
$menuOpen={menuOpen}
to={{
pathname: documentPath(document),
search: highlight
? `?q=${encodeURIComponent(highlight)}`
: undefined,
state: {
title: document.titleWithDefault,
sidebarContext,
@@ -127,56 +131,55 @@ 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 && !isMobile && <StarButton document={document} />}
</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 +193,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 +215,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 +293,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)`
+8 -9
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";
@@ -52,7 +52,6 @@ const DocumentMeta: React.FC<Props> = ({
isDraft,
lastViewedAt,
isTasks,
isTemplate,
} = document;
// Prevent meta information from displaying if updatedBy is not available.
@@ -142,7 +141,7 @@ const DocumentMeta: React.FC<Props> = ({
const nestedDocumentsCount = collection
? collection.getChildrenForDocument(document.id).length
: 0;
const canShowProgressBar = isTasks && !isTemplate;
const canShowProgressBar = isTasks;
const timeSinceNow = () => {
if (isDraft || !showLastViewed) {
@@ -170,7 +169,7 @@ const DocumentMeta: React.FC<Props> = ({
};
return (
<Container align="center" rtl={document.dir === "rtl"} {...rest} dir="ltr">
<Container align="center" $rtl={document.dir === "rtl"} {...rest} dir="ltr">
{to ? (
<Link to={to} replace={replace}>
{content}
@@ -182,7 +181,7 @@ const DocumentMeta: React.FC<Props> = ({
<span>
&nbsp;{t("in")}&nbsp;
<Strong>
<DocumentBreadcrumb document={document} onlyText />
<DocumentBreadcrumb document={document} maxDepth={1} onlyText />
</Strong>
</span>
)}
@@ -219,8 +218,8 @@ const Strong = styled.strong`
font-weight: 550;
`;
const Container = styled(Flex)<{ rtl?: boolean }>`
justify-content: ${(props) => (props.rtl ? "flex-end" : "flex-start")};
const Container = styled(Flex)<{ $rtl?: boolean }>`
justify-content: ${(props) => (props.$rtl ? "flex-end" : "flex-start")};
color: ${s("textTertiary")};
font-size: 13px;
white-space: nowrap;
+3 -3
View File
@@ -1,9 +1,9 @@
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";
@@ -42,7 +42,7 @@ function DocumentTasks({ document }: Props) {
const message = getMessage(t, total, completed);
return (
<Flex align="center" style={{ padding: "0 1px" }} gap={2}>
<Flex align="center" style={{ padding: "0 1px" }} gap={2} shrink={false}>
{completed === total ? (
<Done
color={theme.accent}
+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;
+4 -2
View File
@@ -2,6 +2,7 @@ import * as React from "react";
import { toast } from "sonner";
import styled from "styled-components";
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. */
@@ -141,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}
@@ -155,7 +157,7 @@ function EditableTitle(
autoFocus
{...rest}
/>
</form>
</EventBoundary>
) : (
<Text
onDoubleClick={canUpdate ? handleDoubleClick : undefined}
+80 -8
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";
@@ -41,7 +42,14 @@ export type Props = Optional<
};
function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
const { id, onChange, onCreateCommentMark, onDeleteCommentMark } = props;
const {
id,
onChange,
onCreateCommentMark,
onDeleteCommentMark,
onFileUploadStart,
onFileUploadStop,
} = props;
const { comments } = useStores();
const { shareId } = useShare();
const dictionary = useDictionary();
@@ -50,11 +58,27 @@ function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
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,
]
@@ -197,7 +266,7 @@ function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
<>
{paragraphs ? (
<EditorContainer
rtl={props.dir === "rtl"}
$rtl={props.dir === "rtl"}
grow={props.grow}
style={props.style}
editorStyle={props.editorStyle}
@@ -223,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 || ""}
/>
+32 -26
View File
@@ -7,8 +7,7 @@ import { s } from "@shared/styles";
import { AttachmentPreset } from "@shared/types";
import { getDataTransferFiles } from "@shared/utils/files";
import ConfirmationDialog from "~/components/ConfirmationDialog";
import Flex from "~/components/Flex";
import Input from "~/components/Input";
import Input, { LabelText } from "~/components/Input";
import Text from "~/components/Text";
import useStores from "~/hooks/useStores";
import { uploadFile } from "~/utils/files";
@@ -16,6 +15,7 @@ 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;
@@ -108,12 +108,16 @@ export function EmojiCreateDialog({ onSubmit }: Props) {
setIsUploading(true);
try {
const compressed = await compressImage(file, {
maxHeight: 64,
maxWidth: 64,
});
// 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(compressed, {
const attachment = await uploadFile(fileToUpload, {
name: file.name,
preset: AttachmentPreset.Emoji,
});
@@ -147,29 +151,14 @@ export function EmojiCreateDialog({ onSubmit }: Props) {
>
<Text as="p" type="secondary">
{t(
"The emoji name should be unique and contain only lowercase letters, numbers, and underscores."
"Square images with transparent backgrounds work best. If your image is too large, well try to resize it for you."
)}
</Text>
<Input
label={t("Name")}
value={name}
onChange={handleNameChange}
placeholder="my_custom_emoji"
autoFocus
required
error={
!isValidName
? t(
"name can only contain lowercase letters, numbers, and underscores."
)
: undefined
}
/>
<LabelText as="label">{t("Upload an image")}</LabelText>
<DropZone {...getRootProps()}>
<input {...getInputProps()} />
<Flex column align="center" gap={8}>
<VStack>
{file ? (
<>
<PreviewImage src={URL.createObjectURL(file)} alt="Preview" />
@@ -194,9 +183,25 @@ export function EmojiCreateDialog({ onSubmit }: Props) {
</Text>
</>
)}
</Flex>
</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>
@@ -213,6 +218,7 @@ const DropZone = styled.div`
text-align: center;
cursor: var(--pointer);
transition: border-color 0.2s;
margin-bottom: 1em;
&:hover {
border-color: ${s("inputBorderFocused")};
+2 -1
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";
+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";
+33 -18
View File
@@ -4,15 +4,13 @@ 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";
import env from "~/env";
import useCurrentUser from "~/hooks/useCurrentUser";
import useStores from "~/hooks/useStores";
import history from "~/utils/history";
import { settingsPath } from "~/utils/routeHelpers";
type Props = {
collection?: Collection;
@@ -27,7 +25,7 @@ function ExportDialog({ collection, onSubmit }: Props) {
React.useState<boolean>(true);
const [includePrivate, setIncludePrivate] = React.useState<boolean>(true);
const user = useCurrentUser();
const { collections } = useStores();
const { collections, ui } = useStores();
const { t } = useTranslation();
const appName = env.APP_NAME;
@@ -53,23 +51,40 @@ function ExportDialog({ collection, onSubmit }: Props) {
);
const handleSubmit = async () => {
let response;
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"));
},
},
});
response = await collection.export(format, includeAttachments);
} else {
await collections.export({ format, includeAttachments, includePrivate });
toast.success(t("Export started"));
response = await collections.export({
format,
includeAttachments,
includePrivate,
});
}
if (response?.data?.fileOperation) {
const fileOperationId = response.data.fileOperation.id;
const toastId = `export-${fileOperationId}`;
const timeoutId = setTimeout(() => {
toast.success(t("Export started"), {
id: toastId,
description: t("A link to your file will be sent through email soon"),
duration: 3000,
});
ui.exportToasts.delete(fileOperationId);
}, 6000);
ui.registerExportToast(fileOperationId, toastId, timeoutId);
toast.loading(t("Export started"), {
id: toastId,
description: `${t("Preparing your download")}`,
duration: Infinity,
});
}
onSubmit();
};
+1 -1
View File
@@ -2,7 +2,7 @@ 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";
+9 -4
View File
@@ -7,7 +7,8 @@ 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";
@@ -25,8 +26,10 @@ type Props = {
className?: string;
onSelect: (key: string | null | undefined) => void;
showFilter?: boolean;
showIcons?: boolean;
fetchQuery?: (options: FetchPageParams) => Promise<PaginatedItem[]>;
fetchQueryOptions?: Record<string, string>;
disclosure?: boolean;
};
const FilterOptions = ({
@@ -35,8 +38,10 @@ const FilterOptions = ({
className,
onSelect,
showFilter,
showIcons = true,
fetchQuery,
fetchQueryOptions,
disclosure = true,
...rest
}: Props) => {
const { t } = useTranslation();
@@ -57,7 +62,7 @@ const FilterOptions = ({
<MenuButton
key={option.key}
icon={
option.icon ? (
option.icon && showIcons ? (
<MenuIconWrapper aria-hidden>{option.icon}</MenuIconWrapper>
) : undefined
}
@@ -69,7 +74,7 @@ const FilterOptions = ({
selected={selectedKeys.includes(option.key)}
/>
),
[onSelect, selectedKeys]
[onSelect, showIcons, selectedKeys]
);
const handleFilter = React.useCallback(
@@ -180,8 +185,8 @@ const FilterOptions = ({
<StyledButton
className={className}
icon={selectedItems[0]?.key && selectedItems[0]?.icon}
disclosure={disclosure}
neutral
disclosure
>
{selectedItems.length ? selectedLabel : defaultLabel}
</StyledButton>
+1
View File
@@ -88,6 +88,7 @@ function Header(
<Breadcrumbs ref={setBreadcrumbRef}>
{hasMobileSidebar && (
<MobileMenuButton
haptic="light"
onClick={ui.toggleMobileSidebar}
icon={<MenuIcon />}
neutral
+4 -6
View File
@@ -43,9 +43,9 @@ export const Info = styled(StyledText).attrs(() => ({
white-space: nowrap;
`;
export const Description = styled(StyledText)`
export const Description = styled(StyledText)<{ $margin?: string }>`
${sharedVars}
margin-top: 0.5em;
margin-top: ${(props) => props.$margin ?? "0.5em"};
line-height: var(--line-height);
max-height: calc(var(--line-height) * ${NUMBER_OF_LINES});
overflow: hidden;
@@ -64,8 +64,6 @@ export const Label = styled(Text).attrs({ size: "xsmall", weight: "bold" })<{
width: fit-content;
border-radius: 2em;
padding: 1px 8px 1px 20px;
margin-right: 0.5em;
margin-top: 0.5em;
position: relative;
flex-shrink: 0;
@@ -75,8 +73,8 @@ export const Label = styled(Text).attrs({ size: "xsmall", weight: "bold" })<{
left: 8px;
top: 50%;
transform: translateY(-50%);
width: 6px;
height: 6px;
width: 8px;
height: 8px;
border-radius: 50%;
background-color: ${(props) =>
props.color || props.theme.backgroundSecondary};
@@ -17,6 +17,7 @@ import HoverPreviewGroup from "./HoverPreviewGroup";
import HoverPreviewIssue from "./HoverPreviewIssue";
import HoverPreviewLink from "./HoverPreviewLink";
import HoverPreviewMention from "./HoverPreviewMention";
import HoverPreviewProject from "./HoverPreviewProject";
import HoverPreviewPullRequest from "./HoverPreviewPullRequest";
const DELAY_CLOSE = 500;
@@ -192,6 +193,18 @@ const HoverPreviewDesktop = observer(
createdAt={data.createdAt}
state={data.state}
/>
) : data.type === UnfurlResourceType.Project ? (
<HoverPreviewProject
ref={cardRef}
url={data.url}
name={data.name}
color={data.color}
lead={data.lead}
labels={data.labels}
description={data.description}
state={data.state}
targetDate={data.targetDate}
/>
) : (
<HoverPreviewLink
ref={cardRef}
@@ -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>
) {
@@ -1,8 +1,8 @@
import * as React from "react";
import { useTranslation } from "react-i18next";
import { UnfurlResourceType, UnfurlResponse } from "@shared/types";
import type { UnfurlResourceType, UnfurlResponse } from "@shared/types";
import { MAX_AVATAR_DISPLAY } from "@shared/constants";
import User from "~/models/User";
import type User from "~/models/User";
import Facepile from "~/components/Facepile";
import Flex from "~/components/Flex";
import {
@@ -17,7 +17,7 @@ import ErrorBoundary from "../ErrorBoundary";
type Props = Omit<UnfurlResponse[UnfurlResourceType.Group], "type">;
const HoverPreviewGroup = React.forwardRef(function _HoverPreviewGroup(
const HoverPreviewGroup = React.forwardRef(function HoverPreviewGroup_(
{ name, description, memberCount, users }: Props,
ref: React.Ref<HTMLDivElement>
) {
@@ -3,12 +3,11 @@ 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 { richExtensions } from "@shared/editor/nodes";
import type { UnfurlResourceType, UnfurlResponse } from "@shared/types";
import { IntegrationService } from "@shared/types";
import { Avatar } from "~/components/Avatar";
import Editor from "~/components/Editor";
import Flex from "~/components/Flex";
import Text from "../Text";
import Time from "../Time";
@@ -24,16 +23,18 @@ 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>
) {
const authorName = author.name;
const urlObj = new URL(url);
const service =
urlObj.hostname === "github.com"
? IntegrationService.GitHub
: IntegrationService.Linear;
urlObj.hostname === "linear.app"
? IntegrationService.Linear
: urlObj.hostname === "github.com"
? IntegrationService.GitHub
: IntegrationService.GitLab;
return (
<Preview as="a" href={url} target="_blank" rel="noopener noreferrer">
@@ -61,9 +62,20 @@ const HoverPreviewIssue = React.forwardRef(function _HoverPreviewIssue(
</Trans>
</Info>
</Flex>
<Description>{description}</Description>
{description && (
<Description as="div">
<React.Suspense fallback={<div />}>
<Editor
extensions={richExtensions}
defaultValue={description}
embedsDisabled
readOnly
/>
</React.Suspense>
</Description>
)}
<Flex wrap>
<Flex wrap gap={6} style={{ marginTop: 8 }}>
{labels.map((label, index) => (
<Label key={index} color={label.color}>
{label.name}
@@ -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,12 +1,12 @@
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(
const HoverPreviewMention = React.forwardRef(function HoverPreviewMention_(
{ avatarUrl, name, lastActive, color }: Props,
ref: React.Ref<HTMLDivElement>
) {
@@ -0,0 +1,148 @@
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { s } from "@shared/styles";
import { Backticks } from "@shared/components/Backticks";
import Squircle from "@shared/components/Squircle";
import Editor from "~/components/Editor";
import type { UnfurlResourceType, UnfurlResponse } from "@shared/types";
import { Avatar, AvatarSize } from "~/components/Avatar";
import Flex from "~/components/Flex";
import Text from "../Text";
import Time from "../Time";
import {
Preview,
Title,
Card,
CardContent,
Label,
Description,
} from "./Components";
import { richExtensions } from "@shared/editor/nodes";
type Props = Pick<
UnfurlResponse[UnfurlResourceType.Project],
| "url"
| "name"
| "color"
| "lead"
| "labels"
| "state"
| "targetDate"
| "description"
>;
const HoverPreviewProject = React.forwardRef(function HoverPreviewProject_(
{ url, name, color, lead, labels, state, description, targetDate }: Props,
ref: React.Ref<HTMLDivElement>
) {
const { t } = useTranslation();
return (
<Preview as="a" href={url} target="_blank" rel="noopener noreferrer">
<Flex column ref={ref}>
<Card fadeOut={false}>
<CardContent>
<Flex gap={4} column>
<Title>
<StyledSquircle color={color} size={16} />
<span>
<Backticks content={name} />
</span>
</Title>
{description && (
<Description as="div" $margin="0">
<React.Suspense fallback={<div />}>
<Editor
extensions={richExtensions}
defaultValue={description}
embedsDisabled
readOnly
/>
</React.Suspense>
</Description>
)}
<Text
type="tertiary"
size="small"
style={{ textTransform: "capitalize" }}
>
{state.name}
</Text>
{(lead || targetDate) && (
<>
<Divider />
{lead && (
<MetadataRow>
<MetadataLabel>{t("Lead")}</MetadataLabel>
<Flex align="center" gap={6}>
<Avatar src={lead.avatarUrl} size={AvatarSize.Toast} />
<Text size="small">{lead.name}</Text>
</Flex>
</MetadataRow>
)}
{targetDate && (
<MetadataRow>
<MetadataLabel>{t("Target date")}</MetadataLabel>
<Text size="small">
<Time dateTime={targetDate} addSuffix />
</Text>
</MetadataRow>
)}
</>
)}
{labels.length > 0 && (
<>
<Divider />
<MetadataRow>
<MetadataLabel>{t("Labels")}</MetadataLabel>
<Flex wrap gap={6}>
{labels.map((label, index) => (
<Label key={index} color={label.color}>
{label.name}
</Label>
))}
</Flex>
</MetadataRow>
</>
)}
</Flex>
</CardContent>
</Card>
</Flex>
</Preview>
);
});
const StyledSquircle = styled(Squircle)`
flex-shrink: 0;
margin-top: 4px;
`;
const Divider = styled.div`
height: 1px;
background: ${s("divider")};
margin: 4px 0;
`;
const MetadataRow = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
min-height: 28px;
`;
const MetadataLabel = styled(Text).attrs({
type: "tertiary",
size: "small",
})`
flex-shrink: 0;
min-width: 80px;
`;
export default HoverPreviewProject;
@@ -3,8 +3,10 @@ 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 { richExtensions } from "@shared/editor/nodes";
import type { UnfurlResourceType, UnfurlResponse } from "@shared/types";
import { Avatar } from "~/components/Avatar";
import Editor from "~/components/Editor";
import Flex from "~/components/Flex";
import Text from "../Text";
import Time from "../Time";
@@ -20,7 +22,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>
) {
@@ -48,7 +50,18 @@ const HoverPreviewPullRequest = React.forwardRef(
</Trans>
</Info>
</Flex>
<Description>{description}</Description>
{description && (
<Description as="div">
<React.Suspense fallback={<div />}>
<Editor
extensions={richExtensions}
defaultValue={description}
embedsDisabled
readOnly
/>
</React.Suspense>
</Description>
)}
</Flex>
</CardContent>
</Card>
@@ -1,12 +1,13 @@
import Flex from "@shared/components/Flex";
import styled from "styled-components";
import InputSearch from "~/components/InputSearch";
import { HStack } from "~/components/primitives/HStack";
export const UserInputContainer = styled(Flex)`
export const UserInputContainer = styled(HStack)`
height: 48px;
padding: 6px 12px 0px;
`;
export const StyledInputSearch = styled(InputSearch)`
flex-grow: 1;
min-width: 0;
`;
@@ -1,170 +0,0 @@
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import Flex from "~/components/Flex";
import useStores from "~/hooks/useStores";
import GridTemplate, { DataNode, EmojiNode } from "./GridTemplate";
import { IconType } from "@shared/types";
import { DisplayCategory } from "../utils";
import { StyledInputSearch, UserInputContainer } from "./Components";
import { useIconState } from "../useIconState";
import Emoji from "~/models/Emoji";
const GRID_HEIGHT = 410;
type Props = {
panelWidth: number;
height?: number;
query: string;
panelActive: boolean;
onEmojiChange: (emoji: string) => void;
onQueryChange: (query: string) => void;
};
const CustomEmojiPanel = ({
query,
panelActive,
panelWidth,
height = GRID_HEIGHT,
onEmojiChange,
onQueryChange,
}: Props) => {
const { t } = useTranslation();
const searchRef = React.useRef<HTMLInputElement | null>(null);
const scrollableRef = React.useRef<HTMLDivElement | null>(null);
const [searchData, setSearchData] = useState<DataNode[]>([]);
const [freqEmojis, setFreqEmojis] = useState<EmojiNode[]>([]);
const { getFrequentIcons, incrementIconCount } = useIconState(
IconType.Custom
);
const { emojis } = useStores();
const handleFilter = React.useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
onQueryChange(event.target.value);
},
[onQueryChange]
);
useEffect(() => {
if (query.trim()) {
const initialData = emojis.findByQuery(query);
if (initialData.length) {
setSearchData([
{
category: DisplayCategory.Search,
icons: initialData?.map(toIcon),
},
]);
}
emojis
.fetchAll({
query,
})
.then((data) => {
if (data.length) {
const iconMap = new Map([
...initialData.map((emoji): [string, EmojiNode] => [
emoji.name,
toIcon(emoji),
]),
...data.map((emoji): [string, EmojiNode] => [
emoji.name,
toIcon(emoji),
]),
]);
setSearchData([
{
category: DisplayCategory.Search,
icons: Array.from(iconMap.values()),
},
]);
return;
}
setSearchData([]);
});
} else {
setSearchData([]);
}
}, [query, emojis]);
useEffect(() => {
getFrequentIcons().forEach((id) => {
emojis
.fetch(id)
.then((emoji) => {
setFreqEmojis((prev) => {
if (prev.some((item) => item.id === id)) {
return prev;
}
return [...prev, toIcon(emoji)];
});
})
.catch(() => {
// ignore
});
});
}, [getFrequentIcons, emojis]);
const handleEmojiSelection = React.useCallback(
({ id }: { id: string }) => {
onEmojiChange(id);
incrementIconCount(id);
},
[onEmojiChange, incrementIconCount]
);
const templateData: DataNode[] = React.useMemo(
() => [
{
category: DisplayCategory.Frequent,
icons: freqEmojis,
},
{
category: DisplayCategory.All,
icons: emojis.orderedData.map(toIcon),
},
],
[emojis.orderedData, freqEmojis]
);
React.useLayoutEffect(() => {
if (!panelActive) {
return;
}
scrollableRef.current?.scroll({ top: 0 });
requestAnimationFrame(() => searchRef.current?.focus());
}, [panelActive]);
return (
<Flex column>
<UserInputContainer align="center" gap={12}>
<StyledInputSearch
ref={searchRef}
value={query}
placeholder={`${t("Search")}`}
onChange={handleFilter}
/>
</UserInputContainer>
<GridTemplate
ref={scrollableRef}
width={panelWidth}
height={height - 48}
data={searchData.length ? searchData : templateData}
onIconSelect={handleEmojiSelection}
/>
</Flex>
);
};
const toIcon = (emoji: Emoji): EmojiNode => ({
type: IconType.Custom,
id: emoji.id,
value: emoji.id,
name: emoji.name,
});
export default CustomEmojiPanel;
@@ -1,14 +1,25 @@
import concat from "lodash/concat";
import { PlusIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
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 { EmojiCreateDialog } from "~/components/EmojiCreateDialog";
import { DisplayCategory } from "../utils";
import GridTemplate, { DataNode } from "./GridTemplate";
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;
@@ -30,9 +41,15 @@ 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,
@@ -41,11 +58,20 @@ const EmojiPanel = ({
getFrequentIcons,
} = useIconState(IconType.Emoji);
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>) => {
onQueryChange(event.target.value);
@@ -60,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);
incrementIconCount(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, incrementIconCount]
[
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(() => {
@@ -89,7 +160,7 @@ const EmojiPanel = ({
return (
<Flex column>
<UserInputContainer align="center" gap={12}>
<UserInputContainer>
<StyledInputSearch
ref={searchRef}
value={query}
@@ -97,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}
@@ -104,6 +183,11 @@ const EmojiPanel = ({
height={height - 48}
data={templateData}
onIconSelect={handleEmojiSelection}
empty={
<IconButton onClick={handleUploadClick}>
<PlusIcon />
</IconButton>
}
/>
</Flex>
);
@@ -112,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,
},
];
};
@@ -132,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 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,
};
};
@@ -162,7 +270,7 @@ const getAllEmojis = ({
};
};
return concat(
const allData = concat(
getFrequentIcons(),
getCategoryData(EmojiCategory.People),
getCategoryData(EmojiCategory.Nature),
@@ -173,6 +281,22 @@ const getAllEmojis = ({
getCategoryData(EmojiCategory.Symbols),
getCategoryData(EmojiCategory.Flags)
);
if (customEmojis.length) {
allData.push({
category: "Custom",
icons: customEmojis,
});
}
return allData;
};
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 = {
@@ -37,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
@@ -52,10 +58,6 @@ const GridTemplate = (
const gridItems = compact(
data.flatMap((node) => {
if (node.icons.length === 0) {
return [];
}
const category = (
<CategoryName
key={node.category}
@@ -67,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 (
@@ -1,5 +1,4 @@
import * as React from "react";
import debounce from "lodash/debounce";
import styled from "styled-components";
import { s } from "@shared/styles";
import { colorPalette } from "@shared/utils/collections";
@@ -13,37 +12,23 @@ type Props = {
onSelect: (color: string) => void;
};
const ColorPicker = ({ activeColor, onSelect }: Props) => {
const IconColorPicker = ({ activeColor, onSelect }: Props) => {
const [selectedColor, setSelectedColor] = React.useState(activeColor);
const isBuiltInColor = colorPalette.includes(selectedColor);
const color = isBuiltInColor ? undefined : selectedColor;
const debouncedOnSelect = React.useMemo(
() =>
debounce((color: string) => {
onSelect(color);
}, 250),
[onSelect]
);
React.useEffect(
() => () => {
debouncedOnSelect.cancel();
},
[debouncedOnSelect]
);
React.useEffect(() => {
setSelectedColor(activeColor);
}, [activeColor]);
const handleSelect = (color: string) => {
setSelectedColor(color);
debouncedOnSelect(color);
onSelect(color);
};
return (
<BuiltinColors activeColor={selectedColor} onClick={handleSelect}>
<Container justify="space-between" align="center" auto>
<PresetColors activeColor={selectedColor} onClick={handleSelect} />
<Divider />
<SwatchButton
color={color}
@@ -51,7 +36,7 @@ const ColorPicker = ({ activeColor, onSelect }: Props) => {
onChange={handleSelect}
pickerInModal
/>
</BuiltinColors>
</Container>
);
};
@@ -61,18 +46,14 @@ const Divider = styled.div`
background-color: ${s("inputBorder")};
`;
const BuiltinColors = ({
const PresetColors = ({
activeColor,
onClick,
className,
children,
}: {
activeColor: string;
onClick: (color: string) => void;
className?: string;
children?: React.ReactNode;
}) => (
<Container className={className} justify="space-between" align="center" auto>
<>
{colorPalette.map((color) => (
<ColorButton
key={color}
@@ -81,8 +62,7 @@ const BuiltinColors = ({
onClick={() => onClick(color)}
/>
))}
{children}
</Container>
</>
);
const Container = styled(Flex)`
@@ -91,4 +71,4 @@ const Container = styled(Flex)`
border-bottom: 1px solid ${s("inputBorder")};
`;
export default ColorPicker;
export default IconColorPicker;
@@ -6,8 +6,9 @@ import { IconLibrary } from "@shared/utils/IconLibrary";
import Flex from "~/components/Flex";
import InputSearch from "~/components/InputSearch";
import { DisplayCategory } from "../utils";
import ColorPicker from "./ColorPicker";
import GridTemplate, { DataNode } from "./GridTemplate";
import IconColorPicker from "./IconColorPicker";
import type { DataNode } from "./GridTemplate";
import GridTemplate from "./GridTemplate";
import { useIconState } from "../useIconState";
const IconNames = Object.keys(IconLibrary.mapping);
@@ -121,7 +122,7 @@ const IconPanel = ({
onChange={handleFilter}
/>
</InputSearchContainer>
<ColorPicker
<IconColorPicker
width={panelWidth}
activeColor={color}
onSelect={onColorChange}
@@ -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;

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