Compare commits

..

412 Commits

Author SHA1 Message Date
Salihu 9b9275dff0 re-run tests 2025-12-06 12:20:17 +01:00
Salihu 39f0f78ff4 code cleanup 2025-12-06 12:07:48 +01:00
Salihu b2a0a9cf21 use actions for bulk selection menu 2025-12-06 00:31:33 +01:00
Salihu c0ee1aa3d7 code cleanup 2025-12-04 23:12:52 +01:00
Salihu b9e34e4227 code cleanup 2025-12-04 22:55:42 +01:00
Salihu b405e1e985 clean up code 2025-12-04 22:24:35 +01:00
Salihu e0e6b3f3db fetch documents when selected 2025-12-04 21:24:03 +01:00
Salihu b2b0bd8c8f fix bugs 2025-12-04 21:21:19 +01:00
Salihu 1a8d75b81b fix bugs 2025-12-04 20:38:50 +01:00
Salihu 6c3816e07c use DocumentArchive component for single and bulk archiving 2025-12-04 20:38:50 +01:00
Salihu d264848024 use DocumentDelete component for bulk and single delete 2025-12-04 20:38:50 +01:00
Salihu 65a3d1ac47 use DocumentMove component for bulk and single move 2025-12-04 20:38:50 +01:00
Salihu af98549ca7 rework bulk selection functionality 2025-12-04 20:38:48 +01:00
Salihu ce1d2a90c0 select mutiple documents 2025-12-04 19:58:39 +01:00
dependabot[bot] 9746df193c chore(deps): bump jws from 3.2.2 to 3.2.3 (#10793)
Bumps [jws](https://github.com/brianloveswords/node-jws) from 3.2.2 to 3.2.3.
- [Release notes](https://github.com/brianloveswords/node-jws/releases)
- [Changelog](https://github.com/auth0/node-jws/blob/master/CHANGELOG.md)
- [Commits](https://github.com/brianloveswords/node-jws/compare/v3.2.2...v3.2.3)

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

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

* Add auto-generate emoji name from filename functionality

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

* Fix useCallback dependency issue in handleFileSelection

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

* Remove numbers from generated emoji names

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

---------

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

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

* Fix search filtering by collection to exclude other collections

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

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

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

* fix: Private collections

* refactor

---------

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

* cleanup

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

* Add clientType mutations

* tsc

* i18n

* fix: Invalid input handling

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

* revert

* fix: Copy/paste behavior

* fix: Public share rendering

* fixes

* fix: Click around emoji atom behavior

* fix: Cannot position caret next to heading

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

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-02 20:17:17 -05:00
dependabot[bot] 977e01e96a chore(deps): bump validator from 13.15.20 to 13.15.22 (#10763)
Bumps [validator](https://github.com/validatorjs/validator.js) from 13.15.20 to 13.15.22.
- [Release notes](https://github.com/validatorjs/validator.js/releases)
- [Changelog](https://github.com/validatorjs/validator.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/validatorjs/validator.js/compare/13.15.20...13.15.22)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-02 17:00:02 -05:00
dependabot[bot] 66c31c2b99 chore(deps): bump nodemailer from 7.0.7 to 7.0.11 (#10761)
Bumps [nodemailer](https://github.com/nodemailer/nodemailer) from 7.0.7 to 7.0.11.
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v7.0.7...v7.0.11)

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

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

* Add isDatabaseUrl validator for multi-host PostgreSQL URIs

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

* Update env.ts

---------

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

* Hide diagrams.net functionality in desktop app

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

* Add unit tests for menu filtering logic

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

* Fix missing ImageSource import in test

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

* Remove test files as requested

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

---------

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

* wip

* tsc

* lint

* Detect imported Draw.io

* Add empty diagram placeholder

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

* fix: Reduce image menu for diagrams

* Add custom server settings page

* refactor

* sp

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

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

* Implementation complete

* tidying

* test

* Address feedback

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

* Refactor to subclass, config to instance

* Refactor BaseTask to named export

* fix: Missing partition

* tsc

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

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

* Add READ_ONLY database connection

* UNNEST performs better

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

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

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

* Use findAllInBatches

* Add comments,views,revisions

* Add 'popular' tab to Home

* Apply suggestions from code review

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

* Add 'Popular' tab to collections

* Boost search results based on popularityScore

* Move to unlogged temp table

---------

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

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

* documents.templatize

* documents.archive

* documents.unpublish

* documents.create, documents.update

* documents.title_change event

* documents.move

* documents.delete

* tsc, tests

* tsc

* Copilot feedback

---------

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

* fix: Mobile treatment

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

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

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

* fix: Editing state

* fix: Shared sidebar

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

* perf

* Sidebar hover background

* Allow click toggle closed

* fix: Disclosure toggle

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

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

* access control list exceeds the size of parent container

* remove irrelevant changes

* Further tweaks

---------

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

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

* fix tests

* fix broken tests

* remove unworkable test blocks

* fix broken tests

* fix all broken tests

* minor fix

* remove public collection check

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: New English, United Kingdom translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: New English, United Kingdom translations from Crowdin [ci skip]

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

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

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

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

* fix: get rid of paste handler

* fix: inline actions

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-11 20:02:05 -05:00
Tom Moor 6dd4e846b7 fix: Avoid seeing lang unneccessarily (#10623) 2025-11-11 19:35:57 -05:00
Tom Moor 468620b208 fix: Webhook UI extends out of modal bounds (#10622)
* fix: Webhook UI overlap

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

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


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

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

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

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

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

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

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


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

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

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

---
updated-dependencies:
- dependency-name: terser
  dependency-version: 5.44.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-10 23:22:45 -05:00
dependabot[bot] e6100c4bc9 chore(deps): bump katex from 0.16.22 to 0.16.25 (#10615)
Bumps [katex](https://github.com/KaTeX/KaTeX) from 0.16.22 to 0.16.25.
- [Release notes](https://github.com/KaTeX/KaTeX/releases)
- [Changelog](https://github.com/KaTeX/KaTeX/blob/main/CHANGELOG.md)
- [Commits](https://github.com/KaTeX/KaTeX/compare/v0.16.22...v0.16.25)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-10 23:22:34 -05:00
Tom Moor e85e31a481 chore: UI tweaks (#10606)
* chore: Add transition on button mouseout

* Change default API key expiration to 1 month

* fix: Image upload triggered twice on New Application form

* Improve clickarea of sidebar disclosures

ref #10482

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

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

* simplify

---------

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

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

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

Fixes #10544

* Use relative URLs for document and collection mentions

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

---------

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

* fix: Focus lost when typing in filter options

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

* Auto detect language and adjust line-height accordingly

* Remove accidental commit

* Remove unneccessary adjustment

* test

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

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

* update top level document

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

* include document structure

* use more accurate structure

* minor fix

* debug test on remote

* remove failing test

* fix test

* fix test

* fix test

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

closes #10499

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: New English, United Kingdom translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

---
updated-dependencies:
- dependency-name: "@css-inline/css-inline-wasm"
  dependency-version: 0.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-04 19:27:05 -05:00
Tom Moor 5761903407 fix: Indent/outdent not appearing in mobile toolbar (#10558)
closes #10557
2025-11-04 19:26:53 -05:00
Cellivar 248cc1ba8b Send cookies with S3 POST upload (#10562) 2025-11-04 19:26:45 -05:00
dependabot[bot] ad0f0b39b8 chore(deps): bump dd-trace from 5.67.0 to 5.76.0 (#10552)
Bumps [dd-trace](https://github.com/DataDog/dd-trace-js) from 5.67.0 to 5.76.0.
- [Release notes](https://github.com/DataDog/dd-trace-js/releases)
- [Commits](https://github.com/DataDog/dd-trace-js/compare/v5.67.0...v5.76.0)

---
updated-dependencies:
- dependency-name: dd-trace
  dependency-version: 5.76.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-04 17:02:38 -05:00
dependabot[bot] cdcff3899d chore(deps): bump the aws group with 5 updates (#10551)
Bumps the aws group with 5 updates:

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


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

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

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

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

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

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

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

---
updated-dependencies:
- dependency-name: terser
  dependency-version: 5.44.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-04 17:02:09 -05:00
Tom Moor 5a77217d25 fix: Layout of todo summary in metadata on mobile (#10559) 2025-11-04 17:01:58 -05:00
Tom Moor 89425ccdab chore: Add a mutex lock around migrations to ensure in multi-instance deployments multiple machines don't attempt to run migrations at once (#10560) 2025-11-04 17:01:44 -05:00
dependabot[bot] 1c64b6c93f chore(deps): bump prosemirror-state from 1.4.3 to 1.4.4 (#10555)
Bumps [prosemirror-state](https://github.com/prosemirror/prosemirror-state) from 1.4.3 to 1.4.4.
- [Changelog](https://github.com/ProseMirror/prosemirror-state/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prosemirror/prosemirror-state/compare/1.4.3...1.4.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-04 17:01:33 -05:00
codegen-sh[bot] c2d516c5f1 Upgrade mermaid to 11.12.1 (#10564)
- Updated mermaid dependency from 11.10.1 to 11.12.1
- Updated yarn.lock with new dependency resolution

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

* fix: Use correct amount of spaces for tab

* fix: Pointer on public shares

* fix: Tweak AAA contrast

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

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

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

* Fix TypeScript error: Add missing description prop to HoverPreviewGroup

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

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

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

* Move 2000 char validation to shared constant

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

* Add description field to CreateGroupDialog and EditGroupDialog

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

* Add description column to GroupsTable

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

* tweaks

* animation

---------

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

* fix: restore click on search results

* fix: esc

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

* fixes

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

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

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

* Remove legacy moduleNameMapper

* Add lint rule

* lint - getRandomValues can be used without SSL

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

* chore: make anchorPlugin a top level plugin

* fix: className

* fix: review

* fix: tsc

* fix: checks

* Tweak menu order to match

---------

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

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


Updates `@babel/core` from 7.28.4 to 7.28.5
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.28.5/packages/babel-core)

Updates `@babel/plugin-transform-destructuring` from 7.28.0 to 7.28.5
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.28.5/packages/babel-plugin-transform-destructuring)

Updates `@babel/preset-env` from 7.28.3 to 7.28.5
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.28.5/packages/babel-preset-env)

Updates `@babel/preset-react` from 7.27.1 to 7.28.5
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.28.5/packages/babel-preset-react)

Updates `@babel/preset-typescript` from 7.27.1 to 7.28.5
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.28.5/packages/babel-preset-typescript)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 23:12:28 -04:00
dependabot[bot] 7da3108412 chore(deps): bump validator from 13.15.15 to 13.15.20 (#10490)
Bumps [validator](https://github.com/validatorjs/validator.js) from 13.15.15 to 13.15.20.
- [Release notes](https://github.com/validatorjs/validator.js/releases)
- [Changelog](https://github.com/validatorjs/validator.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/validatorjs/validator.js/compare/13.15.15...13.15.20)

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

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

---
updated-dependencies:
- dependency-name: ioredis
  dependency-version: 5.8.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 22:55:16 -04:00
dependabot[bot] 3987b7de3d chore(deps): bump the aws group with 5 updates (#10485)
Bumps the aws group with 5 updates:

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


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

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 22:55:01 -04:00
dependabot[bot] 6daed33b4a chore(deps-dev): bump @types/readable-stream from 4.0.21 to 4.0.22 (#10486)
Bumps [@types/readable-stream](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/readable-stream) from 4.0.21 to 4.0.22.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/readable-stream)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 22:54:53 -04:00
Tom Moor 3551d16bd8 v1.0.0 2025-10-26 11:36:27 -04:00
Translate-O-Tron 641c0da603 New Crowdin updates (#10347)
* fix: New Hungarian translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: New English, United Kingdom translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: New English, United Kingdom translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: New English, United Kingdom translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: New English, United Kingdom translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: New English, United Kingdom translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: New English, United Kingdom translations from Crowdin [ci skip]

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

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

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

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

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

* isInternalUrl

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

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

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

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

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

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

* refactor: extract listWrappingInputRule to shared helper

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

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

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

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

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

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

* Fix TypeScript error in GroupDialogs checkbox handler

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

* UI tweaks

* Add groups to suggestions endpoint

* Remove mentionableData

---------

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

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

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

Fixes #10418

* Fix TypeScript errors in group hover card implementation

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

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

* tweaks

* tweaks

---------

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

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

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

* subscriptions and pins to 204
2025-10-22 17:48:24 -04:00
Tom Moor a5d065e5ec chore: Annotate delayed notifs (#10447) 2025-10-22 17:48:12 -04:00
Tom Moor fc6152bd55 fix: Simplify logic for suppressing markdown copy (#10450) 2025-10-22 17:47:47 -04:00
Tom Moor 06d4d7e893 chore: Improve Redis retry behavior (#10440) 2025-10-20 23:54:13 -04:00
dependabot[bot] a85f36d896 chore(deps-dev): bump discord-api-types from 0.38.20 to 0.38.30 (#10435)
Bumps [discord-api-types](https://github.com/discordjs/discord-api-types) from 0.38.20 to 0.38.30.
- [Release notes](https://github.com/discordjs/discord-api-types/releases)
- [Changelog](https://github.com/discordjs/discord-api-types/blob/main/CHANGELOG.md)
- [Commits](https://github.com/discordjs/discord-api-types/compare/0.38.20...0.38.30)

---
updated-dependencies:
- dependency-name: discord-api-types
  dependency-version: 0.38.30
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-20 20:18:30 -04:00
dependabot[bot] 5231318e55 chore(deps): bump patch-package from 8.0.0 to 8.0.1 (#10434)
Bumps [patch-package](https://github.com/ds300/patch-package) from 8.0.0 to 8.0.1.
- [Release notes](https://github.com/ds300/patch-package/releases)
- [Changelog](https://github.com/ds300/patch-package/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ds300/patch-package/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-20 20:18:23 -04:00
dependabot[bot] 916032508c chore(deps-dev): bump react-refresh from 0.17.0 to 0.18.0 (#10436)
Bumps [react-refresh](https://github.com/facebook/react/tree/HEAD/packages/react) from 0.17.0 to 0.18.0.
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/HEAD/packages/react)

---
updated-dependencies:
- dependency-name: react-refresh
  dependency-version: 0.18.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-20 20:18:15 -04:00
dependabot[bot] 1a3478a228 chore(deps): bump the aws group with 5 updates (#10438)
Bumps the aws group with 5 updates:

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


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

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

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

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

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

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

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

* use menu dropdown for image replacement options

* copy

* keep editing state in selection toolbar

* avoid overly broad types

* use fixed toolbar width

* tweaks

---------

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

* group mention functionality

* add notification test

* fix: Group icon in mention menu

* language

* toast message

* fix: Group icon in mention menu light mode color

---------

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

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

Removed in #10219

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

* Added plantUML icon

* Updated alt of plantuml icon

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

* tweaks

---------

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

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

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

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

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

* Optimised images with calibre/image-actions

* fix: Write loaded to props to attrs

* Optimised images with calibre/image-actions

* white background

* Optimised images with calibre/image-actions

---------

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

---
updated-dependencies:
- dependency-name: rollup-plugin-webpack-stats
  dependency-version: 2.1.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-13 13:02:36 -04:00
dependabot[bot] b9a8b0f6d6 chore(deps): bump fs-extra from 11.3.1 to 11.3.2 (#10365)
Bumps [fs-extra](https://github.com/jprichardson/node-fs-extra) from 11.3.1 to 11.3.2.
- [Changelog](https://github.com/jprichardson/node-fs-extra/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jprichardson/node-fs-extra/compare/11.3.1...11.3.2)

---
updated-dependencies:
- dependency-name: fs-extra
  dependency-version: 11.3.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-13 13:02:27 -04:00
dependabot[bot] 34ee3b7ea7 chore(deps): bump the aws group with 5 updates (#10367)
Bumps the aws group with 5 updates:

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


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

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

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

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-13 12:40:09 -04:00
dependabot[bot] 670428d322 chore(deps-dev): bump @types/node from 20.17.30 to 20.19.21 (#10368)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.17.30 to 20.19.21.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 20.19.21
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-13 12:39:55 -04:00
Tom Moor 3e58a6ca46 fix: Single frame blank flash when saving comments (#10362) 2025-10-13 14:34:44 +00:00
Tom Moor b21d548d06 fix: Template settings should not show to guests (#10361) 2025-10-13 14:04:52 +00:00
ZhuoYang Wu(阿离) cadbd0d698 fix: repeat submission (#10355) 2025-10-12 21:32:23 -04:00
Tom Moor 6fdba0ecba fix: Icon in editor suggestions missing spacing (#10354) 2025-10-13 01:23:20 +00:00
Tom Moor bb72774f2d fix: Issue introduced when document.editorVersion is null (#10352) 2025-10-12 13:52:45 -04:00
Tom Moor 76868a3083 chore: Replace UUID package with standard module (#10351)
* fix: Missing replacements

* More
2025-10-12 13:15:53 -04:00
Tom Moor 0865052bb8 fix: Missing replacements (#10350) 2025-10-12 12:48:51 -04:00
Tom Moor de6bc9beca fix: Mispositioned toolbar (#10343)
* fix: Mispositioned toolbar

* tsc
2025-10-10 22:53:44 -04:00
Translate-O-Tron e97944ab40 New Crowdin updates (#10294)
* fix: New Ukrainian translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: New English, United Kingdom translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: New English, United Kingdom translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: New English, United Kingdom translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: New English, United Kingdom translations from Crowdin [ci skip]

* fix: New Norwegian Bokmal translations from Crowdin [ci skip]
2025-10-10 20:16:29 -04:00
Salihu 5cfea207e6 restore comment content on error (#10342) 2025-10-10 20:16:10 -04:00
Apoorv Mishra 95f0c42d56 Mention chip for regular URLs (#10327)
* fix: replace oembed with iframely

* feat: wip

fix: favicon

* fix: missing icon in API response
2025-10-10 19:40:05 -04:00
Tom Moor ee7738c141 fix: RedisAdapter does not respect url arg (#10341) 2025-10-10 18:09:24 -04:00
Alex 76701e35ec fix: replace uuid package with standard module (#10318) 2025-10-10 17:06:51 -04:00
codegen-sh[bot] ae8c2aae15 fix: Default destination path for nested document duplication (#10339)
* fix: Default destination path for nested document duplication

When duplicating nested documents, the DocumentExplorer component was only
searching top-level items to find the default node, causing the parent
document to not be found and selected as the default destination.

This fix updates the component to use flattenTree utility to search through
all nodes in the tree hierarchy, ensuring nested parent documents are
properly found and selected as the default destination.

Fixes #10333

* fix: Move flatten import to correct position to resolve TypeScript error

The flatten function from lodash was being used before it was imported,
causing a TypeScript compilation error. This commit moves the import
statement to the proper location with other lodash imports.

* fix: Move nodes declaration before useEffect to resolve temporal dead zone error

The nodes variable was being used in a useEffect dependency array before it was declared, causing TypeScript compilation errors. This commit moves the getNodes function and nodes declaration before the useEffect that uses them, resolving the temporal dead zone issue.

---------

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
2025-10-10 17:06:37 -04:00
Tom Moor a9fa2ed72b fix: User name should be selectable in members table (#10338) 2025-10-10 13:09:12 +00:00
Tom Moor 0deb7e7f09 fix: editorVersion property on document should be updated through collaborative service (#10325) 2025-10-10 09:07:15 -04:00
Tom Moor a544559de2 fix: Prevent reload loop when collaborative service editor version is ahead (#10326) 2025-10-10 09:07:07 -04:00
Nico Hülscher 79fe08e9b6 fix: mobile safari sidebar navigation issue (#10329)
* fix: mobile safari sidebar navigation issue

* fix: readd hover for possible edge cases
2025-10-10 09:06:57 -04:00
codegen-sh[bot] c8d8ba3914 Fix Redis reusing same property as (#10336)
The collaborationClient getter was incorrectly reusing the same this.client
property as defaultClient, causing it to return the already-initialized
connection to the main Redis instead of creating a new connection to
REDIS_COLLABORATION_URL.

This fix adds a separate private static collabClient property to maintain
a separate connection for collaboration operations.

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
2025-10-10 09:00:20 -04:00
codegen-sh[bot] 7a148b0353 Fix autolink when text is within inline code marks (#10322)
* Fix autolink when text is within inline code marks

- Use isInCode with inclusive: true option to properly detect when cursor is within inline code marks
- Prevents autolink from converting URLs to links when typing within backticks
- Fixes issue #10321

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

* Update Link.tsx

---------

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
Co-authored-by: Tom Moor <tom@getoutline.com>
2025-10-08 01:59:40 +00:00
Salihu ca891a56da change list children to match list parent when list style changes (#10315) 2025-10-07 21:16:11 -04:00
Apoorv Mishra 294d3e896a Pan & Zoom (#10271)
* feat: pan and zoom inside lightbox

* fix: cleanup

* fix: edge-to-edge panning

* fix: restore closing animation when lightbox is closed while it's still opening

* fix: zoom in/out action buttons

* fix: swipe

* fix: bg for action buttons

* fix: image err

* fix: comment

* fix: being explicit

* trigger ci

* Lockfile

* Update app/components/Lightbox.tsx

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>

---------

Co-authored-by: Tom Moor <tom@getoutline.com>
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
2025-10-07 20:36:53 -04:00
dependabot[bot] d947f8fda2 chore(deps): bump @bull-board/koa from 6.12.0 to 6.13.0 (#10312)
Bumps [@bull-board/koa](https://github.com/felixmosh/bull-board/tree/HEAD/packages/koa) from 6.12.0 to 6.13.0.
- [Release notes](https://github.com/felixmosh/bull-board/releases)
- [Changelog](https://github.com/felixmosh/bull-board/blob/master/CHANGELOG.md)
- [Commits](https://github.com/felixmosh/bull-board/commits/v6.13.0/packages/koa)

---
updated-dependencies:
- dependency-name: "@bull-board/koa"
  dependency-version: 6.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-07 19:53:23 -04:00
dependabot[bot] 6dd228a533 chore(deps): bump the fortawesome group with 3 updates (#10310)
Bumps the fortawesome group with 3 updates: [@fortawesome/fontawesome-svg-core](https://github.com/FortAwesome/Font-Awesome), [@fortawesome/free-brands-svg-icons](https://github.com/FortAwesome/Font-Awesome) and [@fortawesome/free-solid-svg-icons](https://github.com/FortAwesome/Font-Awesome).


Updates `@fortawesome/fontawesome-svg-core` from 7.0.1 to 7.1.0
- [Release notes](https://github.com/FortAwesome/Font-Awesome/releases)
- [Changelog](https://github.com/FortAwesome/Font-Awesome/blob/7.x/CHANGELOG.md)
- [Commits](https://github.com/FortAwesome/Font-Awesome/compare/7.0.1...7.1.0)

Updates `@fortawesome/free-brands-svg-icons` from 7.0.1 to 7.1.0
- [Release notes](https://github.com/FortAwesome/Font-Awesome/releases)
- [Changelog](https://github.com/FortAwesome/Font-Awesome/blob/7.x/CHANGELOG.md)
- [Commits](https://github.com/FortAwesome/Font-Awesome/compare/7.0.1...7.1.0)

Updates `@fortawesome/free-solid-svg-icons` from 7.0.1 to 7.1.0
- [Release notes](https://github.com/FortAwesome/Font-Awesome/releases)
- [Changelog](https://github.com/FortAwesome/Font-Awesome/blob/7.x/CHANGELOG.md)
- [Commits](https://github.com/FortAwesome/Font-Awesome/compare/7.0.1...7.1.0)

---
updated-dependencies:
- dependency-name: "@fortawesome/fontawesome-svg-core"
  dependency-version: 7.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: fortawesome
- dependency-name: "@fortawesome/free-brands-svg-icons"
  dependency-version: 7.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: fortawesome
- dependency-name: "@fortawesome/free-solid-svg-icons"
  dependency-version: 7.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: fortawesome
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-07 19:53:15 -04:00
dependabot[bot] c7d847215c chore(deps): bump the aws group with 5 updates (#10311)
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.896.0` | `3.901.0` |
| [@aws-sdk/lib-storage](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/lib/lib-storage) | `3.896.0` | `3.903.0` |
| [@aws-sdk/s3-presigned-post](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-presigned-post) | `3.896.0` | `3.901.0` |
| [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-request-presigner) | `3.896.0` | `3.901.0` |
| [@aws-sdk/signature-v4-crt](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/signature-v4-crt) | `3.896.0` | `3.901.0` |


Updates `@aws-sdk/client-s3` from 3.896.0 to 3.901.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.901.0/clients/client-s3)

Updates `@aws-sdk/lib-storage` from 3.896.0 to 3.903.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.903.0/lib/lib-storage)

Updates `@aws-sdk/s3-presigned-post` from 3.896.0 to 3.901.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.901.0/packages/s3-presigned-post)

Updates `@aws-sdk/s3-request-presigner` from 3.896.0 to 3.901.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.901.0/packages/s3-request-presigner)

Updates `@aws-sdk/signature-v4-crt` from 3.896.0 to 3.901.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.901.0/packages/signature-v4-crt)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-07 19:50:06 -04:00
dependabot[bot] 6995ca8521 chore(deps-dev): bump @types/validator from 13.15.2 to 13.15.3 (#10314)
Bumps [@types/validator](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/validator) from 13.15.2 to 13.15.3.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/validator)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-07 19:49:57 -04:00
dependabot[bot] 8a3452e664 chore(deps): bump nodemailer from 6.10.1 to 7.0.7 (#10320)
Bumps [nodemailer](https://github.com/nodemailer/nodemailer) from 6.10.1 to 7.0.7.
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v6.10.1...v7.0.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-07 16:43:49 -04:00
Tom Moor f6315875b4 fix: CSRF validation issues on Firefox (#10317) 2025-10-06 19:10:25 -04:00
Tom Moor f4e53da1bf fix: Flipped logic in export all (#10305) 2025-10-05 21:35:44 -04:00
github-actions[bot] 643188b2f3 chore: Compressed inefficient images automatically (#10303)
Co-authored-by: tommoor <tommoor@users.noreply.github.com>
2025-10-05 16:41:05 -04:00
Tobias Genannt 6f8f25b0d1 Small improvements to the Docker build (#10204)
- Use same Node.js version in build and runner image
- Reduce size of the image by applying the chown directly in the COPY
2025-10-05 16:29:37 -04:00
Tom Moor 10c3edded7 fix: Do not update lastModifiedById on deleted documents (edit history still stored in revisions) (#10302) 2025-10-05 16:08:13 -04:00
Tom Moor 398943d084 feat: Restore 'Copy' button on public code blocks (#10301)
closes #9897
2025-10-05 14:48:50 -04:00
Tom Moor a02677c2b1 fix: Empty state for no collections (#10300) 2025-10-05 14:48:38 -04:00
Tom Moor ebf2029539 fix: Allow formatting toolbar to appear with cell selection (#10299) 2025-10-05 10:54:30 -04:00
Tom Moor 0df42cb4c7 fix: Prefer non-deleted teams in teamProvisioner (#10298) 2025-10-04 14:28:09 -04:00
Salihu 72c9091b7e enhancement: add support for auto linking typed urls (#10266)
* add support for auto linking typed urls

* implement review fixes

* Minor changes

---------

Co-authored-by: Tom Moor <tom@getoutline.com>
2025-10-04 12:26:15 +00:00
Tom Moor 740e33156d Allow export_all endpoint to include all collections (#10291)
* Allow export_all endpoint to include collections the admin is not a member of

* Update ExportTask.ts
2025-10-04 08:16:22 -04:00
Apoorv Mishra d8ef7b2892 Include mermaid SVGs in Lightbox (#10146)
* fix: include mermaid svgs in lightbox

* Fixes:
1. Focus isn't restored back to mermaid code block when Lightbox is closed
2. Read-only mode requires extra click on to both open and close Lightbox for mermaid SVGs

* fix: `zoom-in` cursor for SVGs

* fix: make SVGs downloadable

* fix: tsc

* fix: graphite

* fix: zoom-in should span the wrapper

* fix: graphite

* fix: name

* fix: no need to re-render mermaid svg within lightbox

fix: rely on `code-block` as the `svg` is updated upon doc change

* fix: graphite

* fix: lightbox crash when mermaid block is deleted

* fix: render mermaid at pos `0`

* fix: graphite

* fix: refactor to simplify Lightbox

* fix: graphite
2025-10-04 08:16:02 -04:00
Tom Moor 0f9146066c fix: Overlap of unread badge on long titles in sidebar (#10296) 2025-10-04 11:56:43 +00:00
Salihu 06a1428cbc fix CORS err on img download (#10279)
* fix CORS err on img download

* add check to prevent accidental double download

* disable download button when downloading
2025-10-04 07:48:06 -04:00
Tom Moor e71a425268 fix: Letter icon not displayed correctly in Starred section (#10292) 2025-10-03 18:25:11 -04:00
wmTJc9IK0Q 12d31468f8 Fix print layout (#10264)
Add print media query to display body as block.
2025-10-03 06:55:42 -04:00
Translate-O-Tron 211c57f6aa New Crowdin updates (#10208)
* fix: New Portuguese, Brazilian translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: New English, United Kingdom translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: New English, United Kingdom translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: New English, United Kingdom translations from Crowdin [ci skip]

* fix: New Norwegian Bokmal translations from Crowdin [ci skip]
2025-10-03 06:43:48 -04:00
Tom Moor bb475f3e4e fix: Allow admins to bypass allowed domains (#10290) 2025-10-02 22:14:59 -04:00
Tom Moor 9b95a58822 feat: Add context menus to sidebar items (#10181)
* Add context menu to sidebar document link

* tsc

* tsc

* Add context menu for sidebar collections

* fix

* Starred document context menu
2025-10-02 06:58:05 -04:00
Tom Moor fce02996f9 Add option to choose default TOC visibility on public shares (#10283)
* Add show TOC option

* Revert copy change
2025-10-02 06:53:56 -04:00
codegen-sh[bot] 1aa05b797c Increase JSON payload limit to 5MB for API requests (#10287)
- Add jsonLimit: 5MB to bodyParser configuration in API routes
- Fixes issue with 413 'request entity too large' errors when uploading large documents via API
- Resolves #10239

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
2025-10-02 06:49:25 -04:00
dependabot[bot] b69feb50a7 chore(deps): bump prosemirror-view from 1.40.1 to 1.41.2 (#10276)
Bumps [prosemirror-view](https://github.com/prosemirror/prosemirror-view) from 1.40.1 to 1.41.2.
- [Changelog](https://github.com/ProseMirror/prosemirror-view/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prosemirror/prosemirror-view/compare/1.40.1...1.41.2)

---
updated-dependencies:
- dependency-name: prosemirror-view
  dependency-version: 1.41.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-02 06:24:14 -04:00
Tom Moor 640ecca9ca perf: Reduce upfront component loading (#10285)
* Reducing loading on first open, closes #10263

* perf: Prosemirror deps loaded with Document model

* More initial component reduction

* more

* refactor
2025-10-02 06:22:19 -04:00
dependabot[bot] 5fbaa32f18 chore(deps): bump dd-trace from 5.64.0 to 5.67.0 (#10272)
Bumps [dd-trace](https://github.com/DataDog/dd-trace-js) from 5.64.0 to 5.67.0.
- [Release notes](https://github.com/DataDog/dd-trace-js/releases)
- [Commits](https://github.com/DataDog/dd-trace-js/compare/v5.64.0...v5.67.0)

---
updated-dependencies:
- dependency-name: dd-trace
  dependency-version: 5.67.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 17:23:32 -04:00
dependabot[bot] 50b2cf2706 chore(deps): bump the aws group with 5 updates (#10273)
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.893.0` | `3.896.0` |
| [@aws-sdk/lib-storage](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/lib/lib-storage) | `3.893.0` | `3.896.0` |
| [@aws-sdk/s3-presigned-post](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-presigned-post) | `3.893.0` | `3.896.0` |
| [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-request-presigner) | `3.893.0` | `3.896.0` |
| [@aws-sdk/signature-v4-crt](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/signature-v4-crt) | `3.893.0` | `3.896.0` |


Updates `@aws-sdk/client-s3` from 3.893.0 to 3.896.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.896.0/clients/client-s3)

Updates `@aws-sdk/lib-storage` from 3.893.0 to 3.896.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.896.0/lib/lib-storage)

Updates `@aws-sdk/s3-presigned-post` from 3.893.0 to 3.896.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.896.0/packages/s3-presigned-post)

Updates `@aws-sdk/s3-request-presigner` from 3.893.0 to 3.896.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.896.0/packages/s3-request-presigner)

Updates `@aws-sdk/signature-v4-crt` from 3.893.0 to 3.896.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.896.0/packages/signature-v4-crt)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 17:23:24 -04:00
dependabot[bot] db9deb2a46 chore(deps): bump mammoth from 1.10.0 to 1.11.0 (#10274)
Bumps [mammoth](https://github.com/mwilliamson/mammoth.js) from 1.10.0 to 1.11.0.
- [Release notes](https://github.com/mwilliamson/mammoth.js/releases)
- [Changelog](https://github.com/mwilliamson/mammoth.js/blob/master/NEWS)
- [Commits](https://github.com/mwilliamson/mammoth.js/compare/1.10.0...1.11.0)

---
updated-dependencies:
- dependency-name: mammoth
  dependency-version: 1.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 17:23:16 -04:00
codegen-sh[bot] 72cc740b1c Add clipboard-read; clipboard-write permissions to embedded Frame (#10282)
* Add clipboard permissions to embedded Frame component

- Add clipboard-read and clipboard-write permissions to iframe allow policy
- Ensure clipboard permissions are always included even when custom allow prop is provided
- Update Frame component to properly handle allow prop parameter

* Simplify clipboard permissions implementation

- Remove allow prop handling from Frame component
- Simply add clipboard-read and clipboard-write to default permissions list
- Keep implementation minimal as requested

---------

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
2025-10-01 17:22:57 -04:00
Salihu 4d9717631d enhancement: return group total (#10268)
* return group total when retrieving all groups

* add tests

* add test case for group total
2025-09-29 16:26:59 -04:00
codegen-sh[bot] 69e07a9c21 Restrict document permanent deletion to admins only (#10254)
* Restrict document permanent deletion to admins only

- Split the document policy to separate restore and permanentDelete permissions
- permanentDelete now requires isTeamAdmin check
- Added test to verify non-admin users cannot permanently delete documents
- Updated existing test title for clarity

Fixes #10249

* Fix test: expect 'Authorization error' instead of 'Authorization required'

The policy system throws AuthorizationError with message 'Authorization error'
when authorization fails, not 'Authorization required'.

* Trigger CI: Code analysis confirms implementation is correct

- Policy correctly restricts permanentDelete to team admins only
- Test implementation follows established patterns and expects correct error message
- Linting and TypeScript compilation pass successfully
- Issue appears to be environmental rather than code-related

* Clean up temporary CI trigger file

* Fix failing tests after restricting permanent delete to admins

- Update tests to expect permanentDelete ability to be false for regular users
- Add test to verify admin users still have permanentDelete abilities
- This aligns with the policy changes that restrict permanent deletion to team admins only

---------

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
2025-09-26 00:55:55 -04:00
Luke Granger-Brown 7b27b74e24 fix: Recognise authentication_required for some OIDC providers (#10252)
Some OIDC providers return 401 Unauthorized errors with an empty body
when the access token has expired.

Avoid trying to parse the body as JSON before we've checked whether the
status code is OK.

Fixes #10251.
2025-09-26 00:52:53 -04:00
Tom Moor 92db179230 chore: Add script to reset encrypted data (#10238)
* chore: Add script to reset encrypted data

* Update server/models/WebhookSubscription.ts

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>

* Update server/scripts/reset-encrypted-data.ts

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>

* Add confirmation

---------

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
2025-09-23 08:34:46 -04:00
Tom Moor fa93092f79 fix: /desktop-redirect should never be post-login saved path (#10237) 2025-09-22 21:59:09 -04:00
Tom Moor 63c5938a43 fix: Allow new DATABASE_ env variables to work with migrations/creation (#10216)
* fix: Allow new DATABASE_ env variables to work with migrations/db creation

* Revert
2025-09-22 20:43:27 -04:00
dependabot[bot] a4f77e4438 chore(deps): bump the aws group with 5 updates (#10231)
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.888.0` | `3.893.0` |
| [@aws-sdk/lib-storage](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/lib/lib-storage) | `3.888.0` | `3.893.0` |
| [@aws-sdk/s3-presigned-post](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-presigned-post) | `3.888.0` | `3.893.0` |
| [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-request-presigner) | `3.888.0` | `3.893.0` |
| [@aws-sdk/signature-v4-crt](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/signature-v4-crt) | `3.888.0` | `3.893.0` |


Updates `@aws-sdk/client-s3` from 3.888.0 to 3.893.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.893.0/clients/client-s3)

Updates `@aws-sdk/lib-storage` from 3.888.0 to 3.893.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.893.0/lib/lib-storage)

Updates `@aws-sdk/s3-presigned-post` from 3.888.0 to 3.893.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.893.0/packages/s3-presigned-post)

Updates `@aws-sdk/s3-request-presigner` from 3.888.0 to 3.893.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.893.0/packages/s3-request-presigner)

Updates `@aws-sdk/signature-v4-crt` from 3.888.0 to 3.893.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.893.0/packages/signature-v4-crt)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.893.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/lib-storage"
  dependency-version: 3.893.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-presigned-post"
  dependency-version: 3.893.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-request-presigner"
  dependency-version: 3.893.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/signature-v4-crt"
  dependency-version: 3.893.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-09-22 19:58:01 -04:00
dependabot[bot] 7bb8ff4797 chore(deps-dev): bump @types/react-virtualized-auto-sizer (#10233)
Bumps [@types/react-virtualized-auto-sizer](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-virtualized-auto-sizer) from 1.0.4 to 1.0.8.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-virtualized-auto-sizer)

---
updated-dependencies:
- dependency-name: "@types/react-virtualized-auto-sizer"
  dependency-version: 1.0.8
  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-09-22 19:57:53 -04:00
Tom Moor bcdedd53d8 events.list (#10229) 2025-09-22 11:34:21 +00:00
Benjamin 37b18ab940 chore: Update emoji.ts, fix typos in emoji utilities (#10228)
Typos fixed are:

- "convas" -> "canvas"
- "show be" -> "should be"
- "Get am emoji" -> "Get an emoji"
2025-09-22 07:30:47 -04:00
Tom Moor 42d699fabe fix: Add additional checks to userInviter (#10226) 2025-09-22 07:26:28 -04:00
Tom Moor 8026dac146 fix: Non-reactive facepiles on group settings table (#10227) 2025-09-22 07:26:16 -04:00
Tom Moor a5e1f613fc chore: Remove reakit (#10219)
* First pass

* Remove reakit

* fix focus issues

* fix: Misalignment of toolbar after changing code language
2025-09-22 07:25:56 -04:00
Tom Moor 3d1f55b605 fix: size of undefined in ImportProcessor (#10225) 2025-09-21 21:31:28 +00:00
Tom Moor f25ba44477 fix: cap height of popovers (#10218) 2025-09-20 12:31:41 -04:00
Tom Moor cc052f75d1 fix: Expose originalDocumentId on sourceMetadata (#10217) 2025-09-20 12:22:15 -04:00
codegen-sh[bot] 38beca412e Add originalDocumentId to sourceMetadata when duplicating documents (#10215)
- Added originalDocumentId property to SourceMetadata type
- Updated documentDuplicator to set originalDocumentId for both parent and child documents
- Added comprehensive tests to verify the functionality works correctly
- Preserves existing sourceMetadata while adding the new property

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
2025-09-20 08:34:28 -04:00
Tom Moor fec16346db feat: Add six new shape icons (#10203)
fix: Better default color in IconPicker for dark theme
2025-09-17 23:39:21 -04:00
Tom Moor ca084c485d fix: Comment sidebar bouncing in and out (#10202) 2025-09-17 23:21:59 -04:00
Tom Moor 5d871beee2 v0.87.4 2025-09-17 20:38:17 -04:00
Tom Moor cf46f3e6ee fix: Workaround for validation error on Notion imports (#10200) 2025-09-17 20:35:58 -04:00
Translate-O-Tron 0784b9569c New Crowdin updates (#10166)
* fix: New Dutch 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 German translations from Crowdin [ci skip]

* fix: New Chinese Simplified translations from Crowdin [ci skip]
2025-09-17 09:12:13 -04:00
codegen-sh[bot] f3076ed418 Fix shares_collectionId_fkey constraint error with CASCADE delete (#10196)
* Fix foreign key constraint error for shares.collectionId

- Add migration to update shares_collectionId_fkey constraint with CASCADE delete
- This fixes the constraint violation when deleting collections that have associated shares
- Follows the same pattern as the existing shares_documentId_fkey cascade fix

Fixes #10185

* Simplify migration by removing unnecessary loop and constraint array

- Remove loop and constraintNames array since there's only one constraint
- Directly reference 'shares_collectionId_fkey' constraint name
- Keep the same functionality but with cleaner, simpler code

---------

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
2025-09-17 08:57:16 -04:00
codegen-sh[bot] 70321350d4 Add TeamPreference to prevent document embedding (#10192)
* Add TeamPreference to prevent document embedding

- Add PreventDocumentEmbedding enum value to TeamPreference
- Add default value (false) to maintain backward compatibility
- Update TeamPreferences interface with new boolean property
- Modify renderShare function to respect team preference
- When preference is true, X-Frame-Options header is kept to prevent iframe embedding
- When preference is false (default), X-Frame-Options is removed to allow embedding

* Add preventDocumentEmbedding to TeamsUpdateSchema

- Add preventDocumentEmbedding boolean field to the preferences object in TeamsUpdateSchema
- This allows the new TeamPreference to be modified through the API
- The existing teamUpdater function will automatically handle the new preference
- API responses will include the preference via the presentTeam function

---------

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
2025-09-16 21:36:16 -04:00
Tom Moor fc6d1a9be4 fix: Enter in empty heading (#10194) 2025-09-16 21:36:07 -04:00
dependabot[bot] 07694e06ff chore(deps): bump the babel group with 2 updates (#10189)
Bumps the babel group with 2 updates: [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) and [@babel/plugin-transform-regenerator](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-regenerator).


Updates `@babel/core` from 7.28.3 to 7.28.4
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.28.4/packages/babel-core)

Updates `@babel/plugin-transform-regenerator` from 7.28.3 to 7.28.4
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.28.4/packages/babel-plugin-transform-regenerator)

---
updated-dependencies:
- dependency-name: "@babel/core"
  dependency-version: 7.28.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: babel
- dependency-name: "@babel/plugin-transform-regenerator"
  dependency-version: 7.28.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: babel
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-16 20:49:26 -04:00
Tom Moor 5d1987fb0d chore: Update README (#10191) 2025-09-17 00:18:19 +00:00
Tom Moor 9bc6ae44ad fix: code cannot contain LaTeX syntax (#10179) 2025-09-15 20:58:20 -04:00
dependabot[bot] 792fd96f38 chore(deps-dev): bump @types/passport-oauth2 from 1.4.17 to 1.8.0 (#10186)
Bumps [@types/passport-oauth2](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/passport-oauth2) from 1.4.17 to 1.8.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/passport-oauth2)

---
updated-dependencies:
- dependency-name: "@types/passport-oauth2"
  dependency-version: 1.8.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-09-15 20:42:24 -04:00
dependabot[bot] b1011af016 chore(deps): bump the aws group with 5 updates (#10187)
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.879.0` | `3.888.0` |
| [@aws-sdk/lib-storage](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/lib/lib-storage) | `3.879.0` | `3.888.0` |
| [@aws-sdk/s3-presigned-post](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-presigned-post) | `3.879.0` | `3.888.0` |
| [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-request-presigner) | `3.879.0` | `3.888.0` |
| [@aws-sdk/signature-v4-crt](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/signature-v4-crt) | `3.879.0` | `3.888.0` |


Updates `@aws-sdk/client-s3` from 3.879.0 to 3.888.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.888.0/clients/client-s3)

Updates `@aws-sdk/lib-storage` from 3.879.0 to 3.888.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.888.0/lib/lib-storage)

Updates `@aws-sdk/s3-presigned-post` from 3.879.0 to 3.888.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.888.0/packages/s3-presigned-post)

Updates `@aws-sdk/s3-request-presigner` from 3.879.0 to 3.888.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.888.0/packages/s3-request-presigner)

Updates `@aws-sdk/signature-v4-crt` from 3.879.0 to 3.888.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.888.0/packages/signature-v4-crt)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.888.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/lib-storage"
  dependency-version: 3.888.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-presigned-post"
  dependency-version: 3.888.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-request-presigner"
  dependency-version: 3.888.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/signature-v4-crt"
  dependency-version: 3.888.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-09-15 20:41:24 -04:00
dependabot[bot] 69d6140ab3 chore(deps): bump ioredis from 5.6.0 to 5.7.0 (#10188)
Bumps [ioredis](https://github.com/luin/ioredis) from 5.6.0 to 5.7.0.
- [Release notes](https://github.com/luin/ioredis/releases)
- [Changelog](https://github.com/redis/ioredis/blob/main/CHANGELOG.md)
- [Commits](https://github.com/luin/ioredis/compare/v5.6.0...v5.7.0)

---
updated-dependencies:
- dependency-name: ioredis
  dependency-version: 5.7.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-09-15 20:40:46 -04:00
Tom Moor 01f1de21a8 fix: Allow documents imported in separate imports to retain links between (#10182) 2025-09-15 09:32:52 -04:00
Tom Moor 491442d20f Quick win - allow theme override with query string (#10180) 2025-09-14 21:19:02 -04:00
Tom Moor 7303970118 feat: Display right sidebar as drawer on mobile (#10175)
* wip

* Stack metadata on mobile

* fix: Allow viewing history
2025-09-14 17:09:33 -04:00
codegen-sh[bot] bf68a1d2bf feat: use sourceMetadata column for collections in import processes (#10168)
* feat: use sourceMetadata column for collections in import processes

- Update ImportsProcessor to populate sourceMetadata with externalId, externalName, and createdByName for collections
- Add sourceMetadata to collection presenter for API responses
- Ensure collections follow same metadata methodology as documents during imports

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

* Update collection.ts

---------

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
Co-authored-by: Tom Moor <tom@getoutline.com>
2025-09-14 08:23:45 -04:00
Tom Moor e1b29bd854 chore: Store refresh tokens for Linear integration (#10047)
* wip

* Store expiry

* refreshTokenIfNeeded

* toDate

* self review

* refactor
2025-09-13 22:55:38 -04:00
Translate-O-Tron d07453d108 New Crowdin updates (#10066)
* fix: New French translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: New English, United Kingdom translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: New English, United Kingdom translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: New English, United Kingdom translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: New English, United Kingdom translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: New English, United Kingdom translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: New English, United Kingdom translations from Crowdin [ci skip]

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

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

* fix: New Ukrainian translations from Crowdin [ci skip]
2025-09-13 20:28:43 -04:00
codegen-sh[bot] c40ccd32f5 Add sourceMetadata column to collections (#10165)
This adds the sourceMetadata column to the collections table to match
the one on documents. The column is defined as JSONB and allows null values.

Changes:
- Added migration to create sourceMetadata column on collections table
- Added sourceMetadata field to Collection model with SourceMetadata type
- Imported SourceMetadata type in Collection model

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
2025-09-13 20:28:29 -04:00
Tom Moor 3ab3117e11 fix: Undefined in MediaDimensions (#10164) 2025-09-13 19:10:05 +00:00
Tom Moor 7d69198c91 fix: Single shared doc uncentered, closes #10162 (#10163) 2025-09-13 14:27:49 +00:00
Tom Moor d29089c2ae fix: Enforce share loads team (#10160) 2025-09-13 09:35:58 -04:00
Tom Moor b39f231927 fix: Inline math formatting should trigger on last $ only (#10159) 2025-09-13 09:14:36 -04:00
Salihu f57a189077 feat: Ordered alphabetical lists (#10079)
* feat: letter-list

* simplify list toggle

* use more common shortcuts for list toggle

* fix toggle list conflict

* wrap letter index to avoid overflow

* ensure the markdown letter representation matches the css representation on overflow

* improve list style validation

* fix list indexing

* fix: Toggling ordered lists from formatting menu

* fix: Ordered list in block menu

---------

Co-authored-by: Tom Moor <tom@getoutline.com>
2025-09-13 13:03:19 +00:00
Apoorv Mishra fc469ef9c2 Fix: Deleted image zooms out to (0, 0) upon closing Lightbox (#10154)
* fix: when the lightbox active image is deleted in editor, it zooms out to (0, 0) upon closing lightbox

* Update Lightbox.tsx

---------

Co-authored-by: Tom Moor <tom@getoutline.com>
2025-09-13 11:49:03 +00:00
dependabot[bot] 24c01b1a9a chore(deps): bump axios from 1.8.2 to 1.12.1 (#10157)
Bumps [axios](https://github.com/axios/axios) from 1.8.2 to 1.12.1.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.8.2...v1.12.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-13 07:47:10 -04:00
Apoorv Mishra f1bc5f6216 Enable swipe actions on "Image failed to load" component within Lightbox (#10153)
* fix: apply swipe actions on error component

* fix: enable copy and download action btns only when the image is loaded

* Revert "fix: enable copy and download action btns only when the image is loaded"

This reverts commit 9228d5e5df.
2025-09-11 21:16:54 +05:30
Hemachandar f3fe7283f8 fix: Allow pasting simple lists (#10150) 2025-09-10 16:48:29 -04:00
Apoorv Mishra 839bf5cb91 Regression in Lightbox swipe gestures (#10148)
* fix: swipe

* fix: noTouchEnd not needed

* Revert "fix: noTouchEnd not needed"

This reverts commit cda9a0e49b.

* Revert "fix: swipe"

This reverts commit c05a50be2c.

* fix: fire upon single swipe detection

* chore: `useSwipe`
2025-09-10 16:48:20 -04:00
Apoorv Mishra 6fa98ffe3a fix: unset animation as the lightbox opens (#10144) 2025-09-10 16:48:10 -04:00
Tom Moor a35d84976e feat: Add move commands for columns and rows (#10143)
* feat: Add move commands for columns and rows

closes #7673

* Reuse icon
2025-09-09 21:28:17 -04:00
Tom Moor 19f9245e17 fix: Table row selection logic with merged cells (#10142)
closes #10128
2025-09-09 21:28:06 -04:00
Tom Moor 1da18c3101 chore: Refactor useActionContext to use React context (#10140)
* chore: Refactor useActionContext to use React context

* Self review

* PR feedback
2025-09-09 23:22:46 +00:00
Tom Moor be194558bf chore: Restore type aware linting (#10138)
* wip

* Upgrade oxlint
2025-09-09 19:20:18 -04:00
Tom Moor b945ac8999 feat: Add additional copy link control to lightbox (#10139) 2025-09-09 11:53:56 -04:00
Tom Moor 6ec557cd20 fix: False matches on wrap-around strings (#10136)
closes #10135
2025-09-09 06:42:50 -04:00
Tom Moor 866d30638e fix: Scope incorrectly translated on API key creation (#10134) 2025-09-08 21:58:52 -04:00
Tom Moor 75df8fc18b chore: Migrate FilterOptions component to new primitives (#10127)
* chore: Migrate FilterOptions component to new primitives

* alignemnt
2025-09-08 11:58:20 +00:00
Tom Moor a44a612387 perf: Add missing indexes (#10124) 2025-09-07 20:15:49 -04:00
Tom Moor 97fc848044 fix: Input labels are misaligned on workspace setup in Chrome (#10121) 2025-09-07 16:06:41 -04:00
Tom Moor ec0e7aaba4 fix: Middle click read-only doc link in FF opens duplicate tabs (#10122)
closes #10083
2025-09-07 16:06:31 -04:00
Tom Moor 5337770adb perf: Improve perf of findSourceDocumentIdsForUser (#10118)
* perf: Quick win to not join views table here

* Include views by default
2025-09-07 14:28:48 -04:00
Tom Moor b1b7b2b6fc fix: Truncation in sidebar links (#10120)
closes #10087
2025-09-07 11:10:33 -04:00
Tom Moor 1dcb8f8052 fix: Display column for admins on groups table (#10117) 2025-09-07 13:26:05 +00:00
Tom Moor 569c4b4849 fix: Incorrect translation (#10116) 2025-09-07 12:49:20 +00:00
Tom Moor 5d5bed8270 chore: Refactor auth/CSRF middleware (#10113)
* chore: Refactor auth/CSRF middleware

* sp
2025-09-07 08:36:46 -04:00
Tom Moor 58a41a6fde fix: Various accessibility issues (#10115)
* Round 1

* Round 2

* Shared page
2025-09-07 08:36:35 -04:00
Tom Moor 0bde1d5ef4 fix: Sidebar hidden editing outline (#10114) 2025-09-06 17:14:32 -04:00
Tom Moor 4a01fb7094 chore: Remove PaginatedList test (#10110)
* chore: Remove flaky,useless PaginatedList test

* test
2025-09-06 16:03:08 -04:00
github-actions[bot] a4ff9aa45c chore: Compressed inefficient images automatically (#10111)
Co-authored-by: tommoor <tommoor@users.noreply.github.com>
2025-09-06 16:02:55 -04:00
Tom Moor 1777e9b556 chore: Embed branding (#10109)
* Remove InVision from embed menu, company shut down

* Update Instagram logo
Improve dark mode styling

* Optimised images with calibre/image-actions

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-09-06 16:00:48 -04:00
Tom Moor 59e57d6171 fix: Add additional guards around get methods (#10108) 2025-09-06 19:29:24 +00:00
Tom Moor 9b17f91c9a fix: CSRF missing during email callback (#10107)
* fix: CSRF missing during email callback

* refactor
2025-09-06 11:26:03 -04:00
Apoorv Mishra 9854ce7c31 Lightbox for image navigation in a doc (#9704)
* feat: POC

* fix: cleanup

* fix: cleanup

* fix: can use existing `UiStore` in favor of `LightboxStore`

* fix: style dialog overlay and content

* fix: style and position action buttons

* fix: keyboard nav

* fix: display caption

* fix: cleanup

* fix: force `Lightbox` to unmount if `DocumentScene` re-renders

* fix: cleanup

* fix: images going out of bounds–trying `object-fit` for first pass

* fix: making `Figure` a flexbox and setting its size to img dimensions,
gets rid of the `object-fit` "letterboxing" effect

* feat: image transition

* fix: match animation to that of `ImageZoom`

* fix: `fade-in` overlay

* fix: match overlay background to main background

* fix: fade out nav if idle for 3s

* fix: width & height transition in firefox

* fix: move nav buttons sideways

* fix: button sizes

* fix: don't let nav buttons become invisible due to image in backdrop

* feat: download btn

* feat: handle swipe left, right & down on mobile, also double tap to open lightbox

* fix: off-by-one

* fix: for img to be visible, if not animating

* fix: cleanup

* just a showcase commit, it's broken and will be reverted

* Revert "just a showcase commit, it's broken and will be reverted"

This reverts commit 63d7cbda9895f92a530cfb3a8402eaacb2414947.

* feat: `animateOnClose`

* fix: prevent body from shifting horizontally

* Revert "fix: prevent body from shifting horizontally"

This reverts commit fcc2d0d1daaab458325ece880d13903b42560dfd.

* fix: lint

* fix: make lightbox resilient to doc changes

* fix: `VisuallyHidden` `Dialog.Title`

* fix: fade in action and nav buttons too

* fix: `fadeOut` overlay, action & nav buttons upon close

* fix: `dom.querySelector` is not a function

* fix: let `objectFit` compute the img dimensions

* fix: flashing caption

* fix: hide nav keys based on whether it's a first or last img

* fix: tooltip for action buttons

* Fixes:
1. `Space` with the image selected should trigger the lightbox
2. There should be a "Zoom in" cursor on the image when selected so you know you can click to zoom

* fix: fade out on close if there's img loading err

* fix: pull animation duration into a const

* fix: upscaled editor image animating unreliably

* fix: lint

* fixes:
1. Model as state machine
2. Replace `transition` with `animation`

* fix: cleanup

* fix: DRY

* fix: cleanup

* fix: img loading indicator

* fix: swipe

* fix: types

* fix: Incorrect transitions when image nodes are duplicated

* Tweaks, translations, aria

* fix: Incorrect transition if image is added while lightbox is open

* fix: review

* Remove ImageZoom, react-medium-image-zoom

* Tweak styling

* fix: Quick fix for multiple open editors

* refactor: Move active lightbox state into editor

* Apply graphite suggestions

---------

Co-authored-by: Tom Moor <tom@getoutline.com>
2025-09-06 09:43:53 -04:00
codegen-sh[bot] e28dfbe0bc Fix file size display to use binary units instead of decimal (#10095)
* Fix file size display to use binary units instead of decimal

- Update bytesToHumanReadable function to use 1024-based units (KB, MB, GB)
- This matches user expectations from operating systems like Windows
- Fixes issue where 87.2MB file was displayed as 91.53MB
- Update unit tests to reflect binary unit calculations
- Resolves GitHub issue #10085

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

* Make file size display platform-aware

- Use decimal units (base 1000) on macOS to match Finder behavior
- Use binary units (base 1024) on Windows to match Explorer behavior
- Import isMac utility from shared/utils/browser
- Update comprehensive tests for both platforms
- Resolves platform-specific file size display discrepancies

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

---------

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
Co-authored-by: Tom Moor <tom@getoutline.com>
2025-09-05 06:17:43 -04:00
Tom Moor e0e00bd93d fix: Show list indent controls on touch devices (#10098) 2025-09-05 06:12:26 -04:00
Tom Moor 9df6b9d1a5 v0.87.3 2025-09-04 22:22:17 -04:00
codegen-sh[bot] e2dfc4dd00 Add ALLOWED_PRIVATE_IP_ADDRESSES environment variable (#10093)
* Add ALLOW_IP_ADDRESS_LIST environment variable

This adds support for allowing specific private IP addresses to be accessed
by the request-filtering-agent, which is useful for OIDC providers and
webhooks on private networks.

The environment variable accepts a comma-separated list of IP addresses
that should be allowed even if they are private IP addresses.

Example: ALLOW_IP_ADDRESS_LIST=10.0.0.1,192.168.1.100

Fixes issue with OIDC providers on private IP addresses being blocked.

* Rename environment variable to ALLOWED_PRIVATE_IP_ADDRESSES

Changed from ALLOW_IP_ADDRESS_LIST to ALLOWED_PRIVATE_IP_ADDRESSES
for better clarity and naming consistency.

---------

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
2025-09-04 07:36:50 -04:00
dependabot[bot] 80f48152de chore(deps): bump prosemirror-keymap from 1.2.2 to 1.2.3 (#10080)
Bumps [prosemirror-keymap](https://github.com/prosemirror/prosemirror-keymap) from 1.2.2 to 1.2.3.
- [Changelog](https://github.com/ProseMirror/prosemirror-keymap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prosemirror/prosemirror-keymap/compare/1.2.2...1.2.3)

---
updated-dependencies:
- dependency-name: prosemirror-keymap
  dependency-version: 1.2.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-09-03 08:14:22 -04:00
dependabot[bot] 57ae4fd4fb chore(deps): bump turndown from 7.2.0 to 7.2.1 (#10078)
Bumps [turndown](https://github.com/mixmark-io/turndown) from 7.2.0 to 7.2.1.
- [Release notes](https://github.com/mixmark-io/turndown/releases)
- [Commits](https://github.com/mixmark-io/turndown/compare/v7.2.0...v7.2.1)

---
updated-dependencies:
- dependency-name: turndown
  dependency-version: 7.2.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-09-02 08:01:43 +00:00
dependabot[bot] be9a2b120b chore(deps): bump the aws group with 5 updates (#10075)
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.873.0` | `3.879.0` |
| [@aws-sdk/lib-storage](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/lib/lib-storage) | `3.873.0` | `3.879.0` |
| [@aws-sdk/s3-presigned-post](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-presigned-post) | `3.873.0` | `3.879.0` |
| [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-request-presigner) | `3.873.0` | `3.879.0` |
| [@aws-sdk/signature-v4-crt](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/signature-v4-crt) | `3.873.0` | `3.879.0` |


Updates `@aws-sdk/client-s3` from 3.873.0 to 3.879.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.879.0/clients/client-s3)

Updates `@aws-sdk/lib-storage` from 3.873.0 to 3.879.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.879.0/lib/lib-storage)

Updates `@aws-sdk/s3-presigned-post` from 3.873.0 to 3.879.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.879.0/packages/s3-presigned-post)

Updates `@aws-sdk/s3-request-presigner` from 3.873.0 to 3.879.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.879.0/packages/s3-request-presigner)

Updates `@aws-sdk/signature-v4-crt` from 3.873.0 to 3.879.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.879.0/packages/signature-v4-crt)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.879.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/lib-storage"
  dependency-version: 3.879.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-presigned-post"
  dependency-version: 3.879.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-request-presigner"
  dependency-version: 3.879.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/signature-v4-crt"
  dependency-version: 3.879.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-09-02 03:24:44 -04:00
dependabot[bot] 6c190ec308 chore(deps): bump dd-trace from 5.63.0 to 5.64.0 (#10074)
Bumps [dd-trace](https://github.com/DataDog/dd-trace-js) from 5.63.0 to 5.64.0.
- [Release notes](https://github.com/DataDog/dd-trace-js/releases)
- [Commits](https://github.com/DataDog/dd-trace-js/compare/v5.63.0...v5.64.0)

---
updated-dependencies:
- dependency-name: dd-trace
  dependency-version: 5.64.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-09-02 03:24:35 -04:00
dependabot[bot] e326e6c8f3 chore(deps): bump pg from 8.15.6 to 8.16.3 (#10076)
Bumps [pg](https://github.com/brianc/node-postgres/tree/HEAD/packages/pg) from 8.15.6 to 8.16.3.
- [Changelog](https://github.com/brianc/node-postgres/blob/master/CHANGELOG.md)
- [Commits](https://github.com/brianc/node-postgres/commits/pg@8.16.3/packages/pg)

---
updated-dependencies:
- dependency-name: pg
  dependency-version: 8.16.3
  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-09-02 03:24:25 -04:00
dependabot[bot] 46401701a0 chore(deps): bump emoji-regex from 10.4.0 to 10.5.0 (#10077)
Bumps [emoji-regex](https://github.com/mathiasbynens/emoji-regex) from 10.4.0 to 10.5.0.
- [Commits](https://github.com/mathiasbynens/emoji-regex/compare/v10.4.0...v10.5.0)

---
updated-dependencies:
- dependency-name: emoji-regex
  dependency-version: 10.5.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-09-02 03:24:13 -04:00
Tom Moor 2f2e7c3556 fix: One last spot that needs to allow private requests (#10069) 2025-09-01 11:03:25 -04:00
Tom Moor fedd983649 v0.87.2 2025-09-01 12:32:44 +02:00
Tom Moor 3b2833c752 Update sanitizeLists.ts (#10065)
closes #10042
2025-09-01 06:25:09 -04:00
Tom Moor f1dee53dc4 fix: Unable to access private OIDC server endpoints (#10062) 2025-09-01 05:12:49 -04:00
github-actions[bot] 7fb8706c30 chore: Compressed inefficient images automatically (#10063)
Co-authored-by: tommoor <tommoor@users.noreply.github.com>
2025-09-01 05:12:38 -04:00
Tom Moor 617504d8bb feat: Add group admin role (#10030)
* Add admin role to GroupUser

This change adds an admin role to GroupUser that allows group admins to:
1. Administer other users in the group
2. Change the group name

Changes include:
- Database migration to add isAdmin field to group_users table
- Updated GroupUser model to include isAdmin field
- Added isGroupAdmin policy utility function
- Updated group policy to allow group admins to update groups
- Added API endpoints for managing admin status
- Updated GroupUsersStore to handle admin functionality
- Added tests for the new functionality

* Replace isAdmin with role-based approach for GroupUser

- Added role field to GroupUser model using UserRole.Admin and UserRole.Member
- Created migration to convert isAdmin boolean to role enum
- Updated policies to be synchronous and require pre-loaded relationships
- Updated API endpoints to support both role and legacy isAdmin parameters
- Updated GroupUsersStore to handle role-based functionality
- Updated tests to use role instead of isAdmin
- Maintained backward compatibility with isAdmin in presenters

* Remove isAdmin logic from GroupUser implementation

- Removed isAdmin parameter from GroupUsersStore methods
- Removed isAdmin field from presenter output
- Removed isAdmin from API schemas
- Removed isAdmin parameter handling in API endpoints
- Updated tests to use role instead of isAdmin
- Simplified role handling throughout the codebase

* lint

* tests

* role -> permission

* fe

* test

* Change permission label from 'Admin' to 'Manage'

---------

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
2025-09-01 05:10:32 -04:00
github-actions[bot] 95537af5f3 chore: Compressed inefficient images automatically (#10060)
Co-authored-by: tommoor <tommoor@users.noreply.github.com>
2025-09-01 05:10:11 -04:00
Tom Moor 1765a19aab v0.87.1 2025-08-31 18:44:59 +02:00
Tom Moor a73a8626c5 fix: Allow access to private IP address for OIDC (#10059) 2025-08-31 12:44:37 -04:00
Tom Moor 88054a3899 fix: Include base url as script domain (#10058)
* fix: Include base url as script domain

* Update server/middlewares/csp.ts

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>

---------

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
2025-08-31 12:43:57 -04:00
Tom Moor 409313639d v0.87.0 2025-08-31 15:52:23 +02:00
Tom Moor 78ad61c9fb Bump editor version 2025-08-31 15:52:08 +02:00
Translate-O-Tron 2d9de26041 New Crowdin updates (#9910)
* fix: New Norwegian Bokmal translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

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

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

* fix: New Hungarian translations from Crowdin [ci skip]
2025-08-31 09:31:42 -04:00
Tom Moor 0a9bd39aac Add CSRF middleware (#10051)
ref OUT-Q325-03
2025-08-31 06:35:35 -04:00
vlad f614f3dd3f chore(queues): use Second from @shared/utils/time (#10031) 2025-08-31 06:23:43 -04:00
Hemachandar 7f818c7329 chore: Replace custom toPlainText serialization with leafText (#10039) 2025-08-30 20:26:07 +05:30
Hemachandar 27d116c8e2 Implement right-click context menu (#9883)
* base context menu

* extract document actions to `useDocumentMenuAction` hook

* context menu for DocumentListItem

* common menu component

* use Menu in dropdown and context

* menu context

* remove DropdownMenu and ContextMenu primitives

* update yarn.lock

* permissions recent bug fix
2025-08-30 20:25:03 +05:30
codegen-sh[bot] 7e962d36e6 feat: Add expiresAt column to IntegrationAuthentication (#10046)
This adds an expiresAt column to the IntegrationAuthentication model to store token expiration dates.

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
Co-authored-by: Tom Moor <tom.moor@gmail.com>
2025-08-30 02:26:47 -04:00
Tom Moor f09450e7ea fix: Update unique db constraint to account for revoked share links (#10022)
* fix: Update unique db constraint to account for revoked share links

closes #10017

* Remove pointless try/catch

* Switch order of migrations to ensure no 'dead zone' without constraint
2025-08-30 01:52:05 -04:00
Hemachandar 05b9c69da8 fix: Show api-key creator name in settings page (#10041) 2025-08-29 19:03:47 +05:30
Hemachandar ac55ad55dd fix documents.import permission checks for shared parent (#9996) 2025-08-28 05:37:42 -04:00
Tom Moor 8c11b6cfc8 Map export endpoint to read permissions (#10019) 2025-08-27 04:29:11 -04:00
Tom Moor d858289159 fix: Image caption parsed as sep paragraph on copy/paste (#10020)
closes #9985
2025-08-27 04:29:01 -04:00
Tom Moor 52d420bd98 chore: Suppress lint warnings in CI (confuses AI) (#10021)
Auto fix where possible in pre-commit hook
2025-08-27 03:03:14 -04:00
dependabot[bot] 386eebb117 chore(deps): bump tmp from 0.2.4 to 0.2.5 (#10023)
Bumps [tmp](https://github.com/raszi/node-tmp) from 0.2.4 to 0.2.5.
- [Changelog](https://github.com/raszi/node-tmp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/raszi/node-tmp/compare/v0.2.4...v0.2.5)

---
updated-dependencies:
- dependency-name: tmp
  dependency-version: 0.2.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-08-27 03:03:00 -04:00
dependabot[bot] d0993c3393 chore(deps): bump mermaid from 11.10.0 to 11.10.1 (#10024)
Bumps [mermaid](https://github.com/mermaid-js/mermaid) from 11.10.0 to 11.10.1.
- [Release notes](https://github.com/mermaid-js/mermaid/releases)
- [Commits](https://github.com/mermaid-js/mermaid/compare/mermaid@11.10.0...mermaid@11.10.1)

---
updated-dependencies:
- dependency-name: mermaid
  dependency-version: 11.10.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-08-27 03:02:52 -04:00
dependabot[bot] 54d17503bf chore(deps): bump @css-inline/css-inline-wasm from 0.14.3 to 0.17.0 (#10025)
Bumps [@css-inline/css-inline-wasm](https://github.com/Stranger6667/css-inline) from 0.14.3 to 0.17.0.
- [Release notes](https://github.com/Stranger6667/css-inline/releases)
- [Changelog](https://github.com/Stranger6667/css-inline/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stranger6667/css-inline/compare/v0.14.3...v0.17.0)

---
updated-dependencies:
- dependency-name: "@css-inline/css-inline-wasm"
  dependency-version: 0.17.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-08-27 03:02:41 -04:00
Hemachandar 0de2a3dc98 Skip Notion linked database views (#10018) 2025-08-26 16:14:44 -04:00
dependabot[bot] 73ac18bbde chore(deps): bump the aws group with 5 updates (#10006)
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.864.0` | `3.873.0` |
| [@aws-sdk/lib-storage](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/lib/lib-storage) | `3.864.0` | `3.873.0` |
| [@aws-sdk/s3-presigned-post](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-presigned-post) | `3.864.0` | `3.873.0` |
| [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-request-presigner) | `3.864.0` | `3.873.0` |
| [@aws-sdk/signature-v4-crt](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/signature-v4-crt) | `3.864.0` | `3.873.0` |


Updates `@aws-sdk/client-s3` from 3.864.0 to 3.873.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.873.0/clients/client-s3)

Updates `@aws-sdk/lib-storage` from 3.864.0 to 3.873.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.873.0/lib/lib-storage)

Updates `@aws-sdk/s3-presigned-post` from 3.864.0 to 3.873.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.873.0/packages/s3-presigned-post)

Updates `@aws-sdk/s3-request-presigner` from 3.864.0 to 3.873.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.873.0/packages/s3-request-presigner)

Updates `@aws-sdk/signature-v4-crt` from 3.864.0 to 3.873.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.873.0/packages/signature-v4-crt)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.873.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/lib-storage"
  dependency-version: 3.873.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-presigned-post"
  dependency-version: 3.873.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-request-presigner"
  dependency-version: 3.873.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/signature-v4-crt"
  dependency-version: 3.873.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-08-26 10:51:05 -04:00
Hemachandar 18dcef8ce4 Include collection attachments in json export (#10010) 2025-08-26 10:50:55 -04:00
Hemachandar 7458228df0 Use leafText when converting mention nodes to its text content (#10011) 2025-08-26 10:45:27 -04:00
dependabot[bot] 7c93f8a039 chore(deps): bump @linear/sdk from 39.0.0 to 39.2.1 (#10012)
Bumps [@linear/sdk](https://github.com/linear/linear) from 39.0.0 to 39.2.1.
- [Release notes](https://github.com/linear/linear/releases)
- [Commits](https://github.com/linear/linear/compare/@linear/sdk@39.0.0...@linear/sdk@39.2.1)

---
updated-dependencies:
- dependency-name: "@linear/sdk"
  dependency-version: 39.2.1
  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-08-26 10:39:49 -04:00
dependabot[bot] d6a126d974 chore(deps): bump the radix-ui group with 8 updates (#10013)
Bumps the radix-ui group with 8 updates:

| Package | From | To |
| --- | --- | --- |
| [@radix-ui/react-collapsible](https://github.com/radix-ui/primitives) | `1.1.11` | `1.1.12` |
| [@radix-ui/react-dialog](https://github.com/radix-ui/primitives) | `1.1.14` | `1.1.15` |
| [@radix-ui/react-one-time-password-field](https://github.com/radix-ui/primitives) | `0.1.7` | `0.1.8` |
| [@radix-ui/react-popover](https://github.com/radix-ui/primitives) | `1.1.14` | `1.1.15` |
| [@radix-ui/react-select](https://github.com/radix-ui/primitives) | `2.2.5` | `2.2.6` |
| [@radix-ui/react-switch](https://github.com/radix-ui/primitives) | `1.2.5` | `1.2.6` |
| [@radix-ui/react-tabs](https://github.com/radix-ui/primitives) | `1.1.12` | `1.1.13` |
| [@radix-ui/react-tooltip](https://github.com/radix-ui/primitives) | `1.2.7` | `1.2.8` |


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

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

Updates `@radix-ui/react-one-time-password-field` from 0.1.7 to 0.1.8
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

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

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

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

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

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

---
updated-dependencies:
- dependency-name: "@radix-ui/react-collapsible"
  dependency-version: 1.1.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-dialog"
  dependency-version: 1.1.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-one-time-password-field"
  dependency-version: 0.1.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-popover"
  dependency-version: 1.1.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-select"
  dependency-version: 2.2.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-switch"
  dependency-version: 1.2.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-tabs"
  dependency-version: 1.1.13
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-tooltip"
  dependency-version: 1.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tom Moor <tom@getoutline.com>
2025-08-26 10:39:38 -04:00
dependabot[bot] 779fb1d568 chore(deps): bump core-js from 3.41.0 to 3.45.1 (#10007)
Bumps [core-js](https://github.com/zloirock/core-js/tree/HEAD/packages/core-js) from 3.41.0 to 3.45.1.
- [Release notes](https://github.com/zloirock/core-js/releases)
- [Changelog](https://github.com/zloirock/core-js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zloirock/core-js/commits/v3.45.1/packages/core-js)

---
updated-dependencies:
- dependency-name: core-js
  dependency-version: 3.45.1
  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-08-26 03:04:29 -04:00
dependabot[bot] a0ce14f2a2 chore(deps): bump @dotenvx/dotenvx from 1.48.4 to 1.49.0 (#10008)
Bumps [@dotenvx/dotenvx](https://github.com/dotenvx/dotenvx) from 1.48.4 to 1.49.0.
- [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.48.4...v1.49.0)

---
updated-dependencies:
- dependency-name: "@dotenvx/dotenvx"
  dependency-version: 1.49.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-08-26 03:04:21 -04:00
dependabot[bot] 091abf0b9d chore(deps): bump @fortawesome/react-fontawesome (#10009) 2025-08-26 02:43:17 -04:00
dependabot[bot] 342c42194e chore(deps): bump @radix-ui/react-popover from 1.1.14 to 1.1.15 (#10004) 2025-08-26 02:02:05 -04:00
Hemachandar 8383a0ee1e fix: Sync draft comment from local storage when navigating between documents (#9997) 2025-08-26 02:15:43 +05:30
Hemachandar 19a696942e fix: Use event keycode for determining ToC shortcut keys (#10002) 2025-08-26 02:15:28 +05:30
Hemachandar f1a5e95f77 chore: Dependabot group for radix-ui (#10001) 2025-08-26 01:41:15 +05:30
dependabot[bot] 99fedfa354 chore(deps): bump mermaid from 11.9.0 to 11.10.0 (#9983)
Bumps [mermaid](https://github.com/mermaid-js/mermaid) from 11.9.0 to 11.10.0.
- [Release notes](https://github.com/mermaid-js/mermaid/releases)
- [Commits](https://github.com/mermaid-js/mermaid/compare/mermaid@11.9.0...mermaid@11.10.0)

---
updated-dependencies:
- dependency-name: mermaid
  dependency-version: 11.10.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-21 07:56:00 -04:00
codegen-sh[bot] 9da73202c7 chore: upgrade vite-plugin-pwa to v1.0.3 and rollup-plugin-webpack-stats to v2.1.3 (#9982)
Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
2025-08-21 07:55:13 -04:00
Tom Moor 30db7bc554 chore: Remove focused comment state from router (#9962)
* chore: Refactor comment state from router

* Handle edge cases

* refactor

* feedback
2025-08-19 10:51:08 -04:00
Tom Moor b40eaf4184 refactor: getByUrl (#9975) 2025-08-19 08:30:05 -04:00
Tom Moor 3aff344501 fix: Unable to use DATABASE_HOST env (#9977) 2025-08-19 08:29:53 -04:00
dependabot[bot] 0f812d70c1 chore(deps): bump @radix-ui/react-dropdown-menu from 2.1.15 to 2.1.16 (#9969)
Bumps [@radix-ui/react-dropdown-menu](https://github.com/radix-ui/primitives) from 2.1.15 to 2.1.16.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-dropdown-menu"
  dependency-version: 2.1.16
  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-08-18 20:03:53 -04:00
dependabot[bot] 125e9c2e0b chore(deps): bump the babel group with 4 updates (#9968)
Bumps the babel group with 4 updates: [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core), [@babel/plugin-transform-regenerator](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-regenerator), [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) and [@babel/cli](https://github.com/babel/babel/tree/HEAD/packages/babel-cli).


Updates `@babel/core` from 7.28.0 to 7.28.3
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.28.3/packages/babel-core)

Updates `@babel/plugin-transform-regenerator` from 7.28.1 to 7.28.3
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.28.3/packages/babel-plugin-transform-regenerator)

Updates `@babel/preset-env` from 7.28.0 to 7.28.3
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.28.3/packages/babel-preset-env)

Updates `@babel/cli` from 7.28.0 to 7.28.3
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.28.3/packages/babel-cli)

---
updated-dependencies:
- dependency-name: "@babel/core"
  dependency-version: 7.28.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: babel
- dependency-name: "@babel/plugin-transform-regenerator"
  dependency-version: 7.28.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: babel
- dependency-name: "@babel/preset-env"
  dependency-version: 7.28.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: babel
- dependency-name: "@babel/cli"
  dependency-version: 7.28.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: babel
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-18 19:32:55 -04:00
dependabot[bot] 95402b4b52 chore(deps): bump dd-trace from 5.62.0 to 5.63.0 (#9966)
Bumps [dd-trace](https://github.com/DataDog/dd-trace-js) from 5.62.0 to 5.63.0.
- [Release notes](https://github.com/DataDog/dd-trace-js/releases)
- [Commits](https://github.com/DataDog/dd-trace-js/compare/v5.62.0...v5.63.0)

---
updated-dependencies:
- dependency-name: dd-trace
  dependency-version: 5.63.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-08-18 19:32:42 -04:00
dependabot[bot] d01e3ad09c chore(deps): bump ukkonen from 2.1.0 to 2.2.0 (#9967)
Bumps [ukkonen](https://github.com/sunesimonsen/ukkonen) from 2.1.0 to 2.2.0.
- [Changelog](https://github.com/sunesimonsen/ukkonen/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sunesimonsen/ukkonen/compare/v2.1.0...v2.2.0)

---
updated-dependencies:
- dependency-name: ukkonen
  dependency-version: 2.2.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-08-18 19:32:17 -04:00
dependabot[bot] edb6d44bdc chore(deps-dev): bump discord-api-types from 0.37.119 to 0.38.20 (#9965)
Bumps [discord-api-types](https://github.com/discordjs/discord-api-types) from 0.37.119 to 0.38.20.
- [Release notes](https://github.com/discordjs/discord-api-types/releases)
- [Changelog](https://github.com/discordjs/discord-api-types/blob/main/CHANGELOG.md)
- [Commits](https://github.com/discordjs/discord-api-types/compare/0.37.119...0.38.20)

---
updated-dependencies:
- dependency-name: discord-api-types
  dependency-version: 0.38.20
  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-08-18 19:31:50 -04:00
853 changed files with 29070 additions and 14334 deletions
-4
View File
@@ -211,10 +211,6 @@ GITHUB_APP_PRIVATE_KEY=
LINEAR_CLIENT_ID=
LINEAR_CLIENT_SECRET=
# The GitLab integration allows previewing issue and merge request links as rich mentions
GITLAB_CLIENT_ID=
GITLAB_CLIENT_SECRET=
# For a complete Slack integration with search and posting to channels the
# following configs are also needed in addition to Slack authentication:
# DOCS: https://docs.getoutline.com/s/hosting/doc/slack-G2mc8DOJHk
+3
View File
@@ -26,3 +26,6 @@ updates:
aws:
patterns:
- "@aws-sdk/*"
radix-ui:
patterns:
- "@radix-ui/*"
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
steps:
- name: Close unsigned PRs
uses: actions/github-script@v6
uses: actions/github-script@v8
with:
script: |
const now = new Date();
@@ -40,7 +40,7 @@ jobs:
github.event.pull_request.head.repo.full_name == github.repository)
steps:
- name: Checkout Branch
uses: actions/checkout@v2
uses: actions/checkout@v5
- name: Compress Images
id: calibre
uses: calibreapp/image-actions@main
@@ -48,6 +48,7 @@ jobs:
githubToken: ${{ secrets.GITHUB_TOKEN }}
# For non-Pull Requests, run in compressOnly mode and we'll PR after.
compressOnly: ${{ github.event_name != 'pull_request' }}
minPctChange: "10"
- name: Create Pull Request
# If it's not a Pull Request then commit any changes as a new PR.
if: |
+14 -14
View File
@@ -25,9 +25,9 @@ jobs:
node-version: [20.x, 22.x]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: ${{ matrix.node-version }}
cache: "yarn"
@@ -38,20 +38,20 @@ jobs:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: 22.x
cache: "yarn"
- run: yarn install --frozen-lockfile --prefer-offline
- run: yarn lint
- run: yarn lint --quiet
types:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: 22.x
cache: "yarn"
@@ -65,7 +65,7 @@ jobs:
server: ${{ steps.filter.outputs.server }}
app: ${{ steps.filter.outputs.app }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: dorny/paths-filter@v2
id: filter
with:
@@ -92,8 +92,8 @@ jobs:
matrix:
test-group: [app, shared]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: 22.x
cache: "yarn"
@@ -124,8 +124,8 @@ jobs:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: 22.x
cache: "yarn"
@@ -141,8 +141,8 @@ jobs:
if: ${{ needs.changes.outputs.app == 'true' && github.repository == 'outline/outline' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: 22.x
cache: "yarn"
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v5
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
+2 -2
View File
@@ -14,7 +14,7 @@ jobs:
runs-on: ubicloud-standard-8-arm
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
@@ -93,7 +93,7 @@ jobs:
runs-on: ubicloud-standard-8
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-30
View File
@@ -1,30 +0,0 @@
name: Lint
on:
pull_request:
branches: [main]
jobs:
run-linters:
if: startsWith(github.actor, 'codegen-sh')
name: Run linters
runs-on: ubuntu-latest
permissions:
# Give the default GITHUB_TOKEN write permission to commit and push the
# added or changed files to the repository.
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20.x
cache: "yarn"
- run: yarn install --frozen-lockfile --prefer-offline
- run: yarn lint --fix
- name: Commit changes
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "Applied automatic fixes"
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v5
- uses: actions/stale@v10
with:
stale-pr-message: "This PR is stale because it has been open 90 days with no activity. Remove stale label or comment or this will be closed in 5 days"
stale-issue-message: "This issue is stale because it has been open 90 days with no activity. Remove stale label or comment or this will be closed in 5 days"
+5 -10
View File
@@ -1,14 +1,14 @@
{
"workerIdleMemoryLimit": "0.75",
"maxWorkers": "50%",
"transformIgnorePatterns": ["node_modules/(?!(franc|trigram-utils)/)"],
"projects": [
{
"displayName": "server",
"roots": ["<rootDir>/server", "<rootDir>/plugins"],
"moduleNameMapper": {
"^@server/(.*)$": "<rootDir>/server/$1",
"^@shared/(.*)$": "<rootDir>/shared/$1",
"react-medium-image-zoom": "<rootDir>/__mocks__/react-medium-image-zoom.js"
"^@shared/(.*)$": "<rootDir>/shared/$1"
},
"setupFiles": ["<rootDir>/__mocks__/console.js"],
"setupFilesAfterEnv": ["<rootDir>/server/test/setup.ts"],
@@ -21,9 +21,7 @@
"moduleNameMapper": {
"^~/(.*)$": "<rootDir>/app/$1",
"^@shared/(.*)$": "<rootDir>/shared/$1",
"^.*[.](gif|ttf|eot|svg)$": "<rootDir>/__test__/fileMock.js",
"^uuid$": "<rootDir>/node_modules/uuid/dist/index.js",
"react-medium-image-zoom": "<rootDir>/__mocks__/react-medium-image-zoom.js"
"^.*[.](gif|ttf|eot|svg)$": "<rootDir>/__test__/fileMock.js"
},
"modulePaths": ["<rootDir>/app"],
"setupFiles": ["<rootDir>/__mocks__/window.js"],
@@ -38,8 +36,7 @@
"roots": ["<rootDir>/shared"],
"moduleNameMapper": {
"^@server/(.*)$": "<rootDir>/server/$1",
"^@shared/(.*)$": "<rootDir>/shared/$1",
"react-medium-image-zoom": "<rootDir>/__mocks__/react-medium-image-zoom.js"
"^@shared/(.*)$": "<rootDir>/shared/$1"
},
"setupFiles": ["<rootDir>/__mocks__/console.js"],
"setupFilesAfterEnv": ["<rootDir>/shared/test/setup.ts"],
@@ -51,9 +48,7 @@
"moduleNameMapper": {
"^~/(.*)$": "<rootDir>/app/$1",
"^@shared/(.*)$": "<rootDir>/shared/$1",
"^.*[.](gif|ttf|eot|svg)$": "<rootDir>/__test__/fileMock.js",
"^uuid$": "<rootDir>/node_modules/uuid/dist/index.js",
"react-medium-image-zoom": "<rootDir>/__mocks__/react-medium-image-zoom.js"
"^.*[.](gif|ttf|eot|svg)$": "<rootDir>/__test__/fileMock.js"
},
"setupFiles": ["<rootDir>/__mocks__/window.js"],
"testEnvironment": "jsdom",
+1 -1
View File
@@ -5,7 +5,7 @@ require("@dotenvx/dotenvx").config({
var path = require('path');
module.exports = {
'config': path.resolve('server/config', 'database.json'),
'config': path.resolve('server/config', 'database.js'),
'migrations-path': path.resolve('server', 'migrations'),
'models-path': path.resolve('server', 'models'),
}
+192
View File
@@ -0,0 +1,192 @@
Outline is a fast, collaborative knowledge base built for teams. It's built with React and TypeScript in both frontend and backend, uses a real-time collaboration engine, and is designed for excellent performance and user experience. The backend is a Koa server with an RPC API and uses PostgreSQL and Redis. The application can be self-hosted or used as a cloud service.
There is a web client which is fully responsive and works on mobile devices.
**Monorepo Structure:**
- **`app/`** - React web application with MobX state management
- **`server/`** - Koa API server with Sequelize ORM and background workers
- **`shared/`** - Shared TypeScript types, utilities, and editor components
- **`plugins/`** - Plugin system for extending functionality
- **`public/`** - Static assets served directly
- **Various config files** - TypeScript, Vite, Jest, Prettier, Oxlint configurations
Refer to /docs/ARCHITECTURE.md for detailed architecture documentation.
## Instructions
You're an expert in the following areas:
- TypeScript
- React and React Router
- MobX and MobX-React
- Node.js and Koa
- Sequelize ORM
- PostgreSQL
- Redis
- HTML, CSS and Styled Components
- Prosemirror (rich text editor)
- WebSockets and real-time collaboration
## General Guidelines
- Use early returns for readability.
- Emphasize type safety and static analysis.
- Follow consistent Prettier formatting.
- Do not replace smart quotes ("") or ('') with simple quotes ("").
## Dependencies and Upgrading
- Use yarn for all dependency management.
- After updating dependency versions, install to update lockfiles:
```bash
yarn install
```
## TypeScript Usage
- Use strict mode.
- Avoid "unknown" unless absolutely necessary.
- Never use "any".
- Prefer type definitions; avoid type assertions (as, !).
- Always use curly braces for if statements.
- Avoid # for private properties.
- Prefer interface over type for object shapes.
## Classes & Code Organization
### Class Member Order
1. Public static variables
2. Public static methods
3. Public variables
4. Public methods
6. Protected variables & methods
8. Private variables & methods
### Exports
- Exported members must appear at the top of the file.
- Prefer named exports for components & classes.
- Document ALL public/exported functions with JSDoc.
## React Usage
- Use functional components with hooks.
- Event handlers should be prefixed with "handle", like "handleClick" for onClick.
- Avoid unnecessary re-renders by using React.memo, useMemo, and useCallback appropriately.
- Use descriptive prop types with TypeScript interfaces.
- You do not need to import React unless it is used directly.
- Use styled-components for component styling.
- Ensure high accessibility (a11y) standards using ARIA roles and semantic HTML.
## MobX State Management
- Use MobX stores for global state management.
- Keep stores in `app/stores/`.
- Use `observable`, `action`, and `computed` decorators appropriately.
- Prefer computed values over manual calculations in render.
- Keep business logic in stores, not components.
## Database & ORM
- Use Sequelize models in `server/models/`.
- Generate migrations with Sequelize CLI:
```bash
yarn sequelize migration:create --name=add-field-to-table
```
- Run migrations with `yarn db:migrate`.
- Use transactions for multi-table operations.
- Add appropriate indexes for query performance.
- Always handle database errors gracefully.
## API Design
- RESTful endpoints under `/api/`.
- Authentication endpoints under `/auth/`.
- Use consistent error responses.
- Validate request data using the validation middleware and schemas
- Use presenters to format API responses.
- Keep API routes thin, use model methods for business logic, or commands if logic spans multiple models.
## Authentication & Authorization
- JWT tokens for authentication.
- Policies in `server/policies/` for authorization.
- Use cancan-style ability checks.
- Use authenticated middleware for protected routes.
- Always verify user permissions before data access.
## Real-time Collaboration
- WebSocket connections for real-time updates.
- Use Y.js for collaborative editing.
- Handle connection state changes gracefully.
## Documentation
- All public/exported functions & classes must have JSDoc.
- Include:
- Description
- @param and @return (start lowercase, end with period)
- @throws if applicable
- Add a newline between the description and the @ block.
- Use correct punctuation.
## Testing
- Run tests with Jest:
```bash
# Run all tests
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
```
- Write unit tests for utilities and business logic in a collocated .test.ts file.
- Mock external dependencies appropriately in __mocks__ folder.
- Aim for high code coverage but focus on critical paths.
## Code Quality
- Use Oxlint for linting: `yarn lint`
- Format code with Prettier: `yarn format`
- Check types with TypeScript: `yarn tsc`
- Pre-commit hooks run automatically via Husky.
- Fix linting issues before committing.
## Error Handling
- Use custom error classes in `server/errors.ts`.
- Always catch and handle errors appropriately.
- Log errors with appropriate context.
- Return user-friendly error messages.
- Never expose sensitive information in errors.
## Performance
- Use React.memo for expensive components.
- Implement pagination for large lists.
- Use database indexes effectively.
- Cache expensive computations.
- Monitor performance with appropriate tools.
- Lazy load routes and components where appropriate.
## Security
- Sanitize all user input.
- Use CSRF protection.
- Use rateLimiter middleware for sensitive endpoints.
- Follow OWASP guidelines.
- Never store sensitive data in plain text.
- Use environment variables for secrets.
+12 -13
View File
@@ -6,7 +6,7 @@ ARG APP_PATH
WORKDIR $APP_PATH
# ---
FROM node:22-slim AS runner
FROM node:22.21.0-slim AS runner
LABEL org.opencontainers.image.source="https://github.com/outline/outline"
@@ -14,7 +14,13 @@ ARG APP_PATH
WORKDIR $APP_PATH
ENV NODE_ENV=production
COPY --from=base $APP_PATH/build ./build
# Create a non-root user compatible with Debian and BusyBox based images
RUN addgroup --gid 1001 nodejs && \
adduser --uid 1001 --ingroup nodejs nodejs && \
mkdir -p /var/lib/outline && \
chown -R nodejs:nodejs /var/lib/outline
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
@@ -23,20 +29,13 @@ COPY --from=base $APP_PATH/package.json ./package.json
# Install wget to healthcheck the server
RUN apt-get update \
&& apt-get install -y wget \
&& rm -rf /var/lib/apt/lists/*
# Create a non-root user compatible with Debian and BusyBox based images
RUN addgroup --gid 1001 nodejs && \
adduser --uid 1001 --ingroup nodejs nodejs && \
chown -R nodejs:nodejs $APP_PATH/build && \
mkdir -p /var/lib/outline && \
chown -R nodejs:nodejs /var/lib/outline
&& apt-get install -y wget \
&& rm -rf /var/lib/apt/lists/*
ENV FILE_STORAGE_LOCAL_ROOT_DIR=/var/lib/outline/data
RUN mkdir -p "$FILE_STORAGE_LOCAL_ROOT_DIR" && \
chown -R nodejs:nodejs "$FILE_STORAGE_LOCAL_ROOT_DIR" && \
chmod 1777 "$FILE_STORAGE_LOCAL_ROOT_DIR"
chown -R nodejs:nodejs "$FILE_STORAGE_LOCAL_ROOT_DIR" && \
chmod 1777 "$FILE_STORAGE_LOCAL_ROOT_DIR"
VOLUME /var/lib/outline/data
+1 -1
View File
@@ -1,5 +1,5 @@
ARG APP_PATH=/opt/outline
FROM node:20 AS deps
FROM node:22.21.0 AS deps
ARG APP_PATH
WORKDIR $APP_PATH
+2 -2
View File
@@ -3,7 +3,7 @@ Business Source License 1.1
Parameters
Licensor: General Outline, Inc.
Licensed Work: Outline 0.86.1
Licensed Work: Outline 1.1.0
The Licensed Work is (c) 2025 General Outline, Inc.
Additional Use Grant: You may make use of the Licensed Work, provided that
you may not use the Licensed Work for a Document
@@ -15,7 +15,7 @@ Additional Use Grant: You may make use of the Licensed Work, provided that
Licensed Work by creating teams and documents
controlled by such third parties.
Change Date: 2029-08-09
Change Date: 2029-11-16
Change License: Apache License, Version 2.0
+12 -11
View File
@@ -7,14 +7,13 @@
<img width="1640" alt="screenshot" src="https://user-images.githubusercontent.com/380914/110356468-26374600-7fef-11eb-9f6a-f2cc2c8c6590.png">
</p>
<p align="center">
<a href="https://circleci.com/gh/outline/outline" rel="nofollow"><img src="https://circleci.com/gh/outline/outline.svg?style=shield"></a>
<a href="http://www.typescriptlang.org" rel="nofollow"><img src="https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg" alt="TypeScript"></a>
<a href="https://github.com/prettier/prettier"><img src="https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat" alt="Prettier"></a>
<a href="https://github.com/styled-components/styled-components"><img src="https://img.shields.io/badge/style-%F0%9F%92%85%20styled--components-orange.svg" alt="Styled Components"></a>
<a href="https://translate.getoutline.com/project/outline" alt="Localized"><img src="https://badges.crowdin.net/outline/localized.svg"></a>
</p>
This is the source code that runs [**Outline**](https://www.getoutline.com) and all the associated services. If you want to use Outline then you don't need to run this code, we offer a hosted version of the app at [getoutline.com](https://www.getoutline.com). You can also find documentation on using Outline in [our guide](https://docs.getoutline.com/s/guide).
This is the source code that runs [**Outline**](https://www.getoutline.com) and all the associated services. If you want to use Outline then you don't need to run this code, A hosted version of the app is offered at [getoutline.com](https://www.getoutline.com). You can also find documentation on using Outline in [our guide](https://docs.getoutline.com/s/guide).
If you'd like to run your own copy of Outline or contribute to development then this is the place for you.
@@ -51,13 +50,14 @@ please refer to the [architecture document](docs/ARCHITECTURE.md) first for a hi
In development Outline outputs simple logging to the console, prefixed by categories. In production it outputs JSON logs, these can be easily parsed by your preferred log ingestion pipeline.
HTTP logging is disabled by default, but can be enabled by setting the `DEBUG=http` environment variable.
HTTP logging is disabled by default, but can be enabled by setting the `DEBUG=http` environment variable. logging
can be enabled for all categories by setting `DEBUG=*` or for specific categories such as `DEBUG=database` and `LOG_LEVEL=debug`, or `LOG_LEVEL=silly` for very verbose logging.
## Tests
We aim to have sufficient test coverage for critical parts of the application and aren't aiming for 100% unit test coverage. All API endpoints and anything authentication related should be thoroughly tested.
To add new tests, write your tests with [Jest](https://facebook.github.io/jest/) and add a file with `.test.js` extension next to the tested code.
To add new tests, write your tests with [Jest](https://facebook.github.io/jest/) and add a file with `.test.ts` extension next to the tested code.
```shell
# To run all tests
@@ -68,14 +68,14 @@ make watch
```
Once the test database is created with `make test` you may individually run
frontend and backend tests directly.
frontend and backend tests directly with jest:
```shell
# To run backend tests
yarn test:server
# To run a specific backend test
yarn test:server myTestFile
# To run a specific backend test in watch mode
yarn test path/to/file.test.ts --watch
# To run frontend tests
yarn test:app
@@ -86,14 +86,15 @@ yarn test:app
Sequelize is used to create and run migrations, for example:
```shell
yarn sequelize migration:generate --name my-migration
yarn sequelize db:migrate
yarn db:create-migration --name my-migration
yarn db:migrate
yarn db:rollback
```
Or to run migrations on test database:
Or, to run migrations on test database:
```shell
yarn sequelize db:migrate --env test
yarn db:migrate --env test
```
# Activity
+5
View File
@@ -194,6 +194,11 @@
"description": "Use a secure SMTP connection (optional)",
"required": false
},
"SMTP_DISABLE_STARTTLS": {
"value": "false",
"description": "Disable STARTTLS even if the server supports it (optional)",
"required": false
},
"SMTP_TLS_CIPHERS": {
"description": "Override SMTP cipher configuration (optional)",
"required": false
+7 -7
View File
@@ -5,6 +5,13 @@
{
"files": ["**/*.{jsx,tsx}"],
"rules": {
"no-restricted-globals": [
"error",
{
"name": "crypto",
"message": "Do not use, does not work in environments without SSL."
}
],
"no-restricted-imports": [
"error",
{
@@ -13,13 +20,6 @@
"group": ["mime-types"],
"message": "Do not use the mime-types package in the browser."
}
],
"paths": [
{
"name": "reakit/Menu",
"importNames": ["useMenuState"],
"message": "Do not use useMenuState from reakit/Menu. Use useMenuState instead."
}
]
}
]
+4 -4
View File
@@ -3,7 +3,7 @@ import stores from "~/stores";
import ApiKey from "~/models/ApiKey";
import ApiKeyNew from "~/scenes/ApiKeyNew";
import ApiKeyRevokeDialog from "~/scenes/Settings/components/ApiKeyRevokeDialog";
import { createAction, createActionV2 } from "..";
import { createAction } from "..";
import { SettingsSection } from "../sections";
export const createApiKey = createAction({
@@ -26,9 +26,9 @@ export const createApiKey = createAction({
});
export const revokeApiKeyFactory = ({ apiKey }: { apiKey: ApiKey }) =>
createActionV2({
name: ({ t, isContextMenu }) =>
isContextMenu
createAction({
name: ({ t, isMenu }) =>
isMenu
? apiKey.isExpired
? t("Delete")
: `${t("Revoke")}`
+123
View File
@@ -0,0 +1,123 @@
import { ArchiveIcon, MoveIcon, TrashIcon } from "outline-icons";
import DocumentMove from "~/scenes/DocumentMove";
import { createAction } from "~/actions";
import { ActiveDocumentSection } from "~/actions/sections";
import DocumentDelete from "~/scenes/DocumentDelete";
import DocumentArchive from "~/scenes/DocumentArchive";
import Document from "~/models/Document";
type Props = {
documents: Document[];
};
/**
* Archive multiple documents at once.
*/
export const bulkArchiveDocuments = ({ documents }: Props) =>
createAction({
name: ({ t }) => `${t("Archive")}`,
analyticsName: "Bulk archive documents",
section: ActiveDocumentSection,
icon: <ArchiveIcon />,
visible: ({ stores }) => {
if (documents.length === 0) {
return false;
}
return documents.every(({ id }) => stores.policies.abilities(id).archive);
},
perform: async ({ stores, t }) => {
const { dialogs, documents: documentsStore } = stores;
const count = documents.length;
if (count === 0) {
return;
}
dialogs.openModal({
title: t("Archive {{ count }} documents", { count }),
content: (
<DocumentArchive
documents={documents}
onSubmit={() => documentsStore.clearSelection()}
/>
),
});
},
});
/**
* Move multiple documents at once.
*/
export const bulkMoveDocuments = ({ documents }: Props) =>
createAction({
name: ({ t }) => `${t("Move")}`,
analyticsName: "Bulk move documents",
section: ActiveDocumentSection,
icon: <MoveIcon />,
visible: ({ stores }) => {
if (documents.length === 0) {
return false;
}
return documents.every(({ id }) => stores.policies.abilities(id).move);
},
perform: ({ stores, t }) => {
const { dialogs, documents: documentsStore } = stores;
const count = documents.length;
if (count === 0) {
return;
}
dialogs.openModal({
title: t("Move {{ count }} documents", { count }),
content: (
<DocumentMove
documents={documents}
onSubmit={() => documentsStore.clearSelection()}
/>
),
});
},
});
/**
* Delete multiple documents at once.
*/
export const bulkDeleteDocuments = ({ documents }: Props) =>
createAction({
name: ({ t }) => `${t("Delete")}`,
analyticsName: "Bulk delete documents",
section: ActiveDocumentSection,
icon: <TrashIcon />,
dangerous: true,
visible: ({ stores }) => {
if (documents.length === 0) {
return false;
}
return documents.every(({ id }) => stores.policies.abilities(id).delete);
},
perform: async ({ stores, t }) => {
const { dialogs, documents: documentsStore } = stores;
const count = documents.length;
if (count === 0) {
return;
}
dialogs.openModal({
title: t("Delete {{ count }} documents", { count }),
content: (
<DocumentDelete
documents={documents}
onSubmit={() => documentsStore.clearSelection()}
/>
),
});
},
});
export const rootBulkDocumentActions = [
bulkArchiveDocuments,
bulkMoveDocuments,
bulkDeleteDocuments,
];
+164 -34
View File
@@ -1,8 +1,12 @@
import {
AlphabeticalReverseSortIcon,
AlphabeticalSortIcon,
ArchiveIcon,
CollectionIcon,
EditIcon,
ExportIcon,
ImportIcon,
ManualSortIcon,
NewDocumentIcon,
PadlockIcon,
PlusIcon,
@@ -22,12 +26,11 @@ import { CollectionNew } from "~/components/Collection/CollectionNew";
import CollectionDeleteDialog from "~/components/CollectionDeleteDialog";
import ConfirmationDialog from "~/components/ConfirmationDialog";
import DynamicCollectionIcon from "~/components/Icons/CollectionIcon";
import SharePopover from "~/components/Sharing/Collection/SharePopover";
import { getHeaderExpandedKey } from "~/components/Sidebar/components/Header";
import {
createAction,
createActionV2,
createInternalLinkActionV2,
createActionWithChildren,
createInternalLinkAction,
} from "~/actions";
import { ActiveCollectionSection, CollectionSection } from "~/actions/sections";
import { setPersistedState } from "~/hooks/usePersistedState";
@@ -37,12 +40,18 @@ import {
searchPath,
} from "~/utils/routeHelpers";
import ExportDialog from "~/components/ExportDialog";
import { getEventFiles } from "@shared/utils/files";
import history from "~/utils/history";
import lazyWithRetry from "~/utils/lazyWithRetry";
const ColorCollectionIcon = ({ collection }: { collection: Collection }) => (
<DynamicCollectionIcon collection={collection} />
);
const SharePopover = lazyWithRetry(
() => import("~/components/Sharing/Collection/SharePopover")
);
export const openCollection = createAction({
export const openCollection = createActionWithChildren({
name: ({ t }) => t("Open collection"),
analyticsName: "Open collection",
section: CollectionSection,
@@ -50,15 +59,17 @@ export const openCollection = createAction({
icon: <CollectionIcon />,
children: ({ stores }) => {
const collections = stores.collections.orderedData;
return collections.map((collection) => ({
// Note: using url which includes the slug rather than id here to bust
// cache if the collection is renamed
id: collection.path,
name: collection.name,
icon: <ColorCollectionIcon collection={collection} />,
section: CollectionSection,
to: collection.path,
}));
return collections.map((collection) =>
createInternalLinkAction({
// Note: using url which includes the slug rather than id here to bust
// cache if the collection is renamed
id: collection.path,
name: collection.name,
icon: <ColorCollectionIcon collection={collection} />,
section: CollectionSection,
to: collection.path,
})
);
},
});
@@ -80,9 +91,8 @@ export const createCollection = createAction({
},
});
export const editCollection = createActionV2({
name: ({ t, isContextMenu }) =>
isContextMenu ? `${t("Edit")}` : t("Edit collection"),
export const editCollection = createAction({
name: ({ t, isMenu }) => (isMenu ? `${t("Edit")}` : t("Edit collection")),
analyticsName: "Edit collection",
section: ActiveCollectionSection,
icon: <EditIcon />,
@@ -106,9 +116,9 @@ export const editCollection = createActionV2({
},
});
export const editCollectionPermissions = createActionV2({
name: ({ t, isContextMenu }) =>
isContextMenu ? `${t("Permissions")}` : t("Collection permissions"),
export const editCollectionPermissions = createAction({
name: ({ t, isMenu }) =>
isMenu ? `${t("Permissions")}` : t("Collection permissions"),
analyticsName: "Collection permissions",
section: ActiveCollectionSection,
icon: <PadlockIcon />,
@@ -138,7 +148,130 @@ export const editCollectionPermissions = createActionV2({
},
});
export const searchInCollection = createInternalLinkActionV2({
export const importDocument = createAction({
name: ({ t }) => t("Import document"),
analyticsName: "Import document",
section: ActiveCollectionSection,
icon: <ImportIcon />,
visible: ({ activeCollectionId, stores }) => {
if (activeCollectionId) {
return !!stores.policies.abilities(activeCollectionId).createDocument;
}
return false;
},
perform: ({ activeCollectionId, stores }) => {
const { documents } = stores;
const input = document.createElement("input");
input.type = "file";
input.accept = documents.importFileTypesString;
input.onchange = async (ev) => {
const files = getEventFiles(ev);
const file = files[0];
try {
const document = await documents.import(
file,
null,
activeCollectionId,
{
publish: true,
}
);
history.push(document.url);
} catch (err) {
toast.error(err.message);
}
};
input.click();
},
});
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);
const sortAlphabetical = collection?.sort.field === "title";
const sortDir = collection?.sort.direction;
return sortAlphabetical ? (
sortDir === "asc" ? (
<AlphabeticalSortIcon />
) : (
<AlphabeticalReverseSortIcon />
)
) : (
<ManualSortIcon />
);
},
children: [
createAction({
name: ({ t }) => t("A-Z sort"),
section: ActiveCollectionSection,
selected: ({ activeCollectionId, stores }) => {
const collection = stores.collections.get(activeCollectionId);
return (
collection?.sort.field === "title" &&
collection?.sort.direction === "asc"
);
},
perform: ({ activeCollectionId, stores }) => {
const collection = stores.collections.get(activeCollectionId);
return collection?.save({
sort: {
field: "title",
direction: "asc",
},
});
},
}),
createAction({
name: ({ t }) => t("Z-A sort"),
section: ActiveCollectionSection,
selected: ({ activeCollectionId, stores }) => {
const collection = stores.collections.get(activeCollectionId);
return (
collection?.sort.field === "title" &&
collection?.sort.direction === "desc"
);
},
perform: ({ activeCollectionId, stores }) => {
const collection = stores.collections.get(activeCollectionId);
return collection?.save({
sort: {
field: "title",
direction: "desc",
},
});
},
}),
createAction({
name: ({ t }) => t("Manual sort"),
section: ActiveCollectionSection,
selected: ({ activeCollectionId, stores }) => {
const collection = stores.collections.get(activeCollectionId);
return collection?.sort.field !== "title";
},
perform: ({ activeCollectionId, stores }) => {
const collection = stores.collections.get(activeCollectionId);
return collection?.save({
sort: {
field: "index",
direction: "asc",
},
});
},
}),
],
});
export const searchInCollection = createInternalLinkAction({
name: ({ t }) => t("Search in collection"),
analyticsName: "Search collection",
section: ActiveCollectionSection,
@@ -169,7 +302,7 @@ export const searchInCollection = createInternalLinkActionV2({
},
});
export const starCollection = createActionV2({
export const starCollection = createAction({
name: ({ t }) => t("Star"),
analyticsName: "Star collection",
section: ActiveCollectionSection,
@@ -196,7 +329,7 @@ export const starCollection = createActionV2({
},
});
export const unstarCollection = createActionV2({
export const unstarCollection = createAction({
name: ({ t }) => t("Unstar"),
analyticsName: "Unstar collection",
section: ActiveCollectionSection,
@@ -222,7 +355,7 @@ export const unstarCollection = createActionV2({
},
});
export const subscribeCollection = createActionV2({
export const subscribeCollection = createAction({
name: ({ t }) => t("Subscribe"),
analyticsName: "Subscribe to collection",
section: ActiveCollectionSection,
@@ -253,7 +386,7 @@ export const subscribeCollection = createActionV2({
},
});
export const unsubscribeCollection = createActionV2({
export const unsubscribeCollection = createAction({
name: ({ t }) => t("Unsubscribe"),
analyticsName: "Unsubscribe from collection",
section: ActiveCollectionSection,
@@ -284,7 +417,7 @@ export const unsubscribeCollection = createActionV2({
},
});
export const archiveCollection = createActionV2({
export const archiveCollection = createAction({
name: ({ t }) => `${t("Archive")}`,
analyticsName: "Archive collection",
section: ActiveCollectionSection,
@@ -325,7 +458,7 @@ export const archiveCollection = createActionV2({
},
});
export const restoreCollection = createActionV2({
export const restoreCollection = createAction({
name: ({ t }) => t("Restore"),
analyticsName: "Restore collection",
section: CollectionSection,
@@ -350,7 +483,7 @@ export const restoreCollection = createActionV2({
},
});
export const deleteCollection = createActionV2({
export const deleteCollection = createAction({
name: ({ t }) => `${t("Delete")}`,
analyticsName: "Delete collection",
section: ActiveCollectionSection,
@@ -384,7 +517,7 @@ export const deleteCollection = createActionV2({
},
});
export const exportCollection = createActionV2({
export const exportCollection = createAction({
name: ({ t }) => `${t("Export")}`,
analyticsName: "Export collection",
section: ActiveCollectionSection,
@@ -394,10 +527,7 @@ export const exportCollection = createActionV2({
return false;
}
return (
!!stores.policies.abilities(currentTeamId).createExport &&
!!stores.policies.abilities(activeCollectionId).export
);
return !!stores.policies.abilities(activeCollectionId).export;
},
perform: async ({ activeCollectionId, stores, t }) => {
if (!activeCollectionId) {
@@ -420,7 +550,7 @@ export const exportCollection = createActionV2({
},
});
export const createDocument = createInternalLinkActionV2({
export const createDocument = createInternalLinkAction({
name: ({ t }) => t("New document"),
analyticsName: "New document",
section: ActiveCollectionSection,
@@ -442,7 +572,7 @@ export const createDocument = createInternalLinkActionV2({
},
});
export const createTemplate = createInternalLinkActionV2({
export const createTemplate = createInternalLinkAction({
name: ({ t }) => t("New template"),
analyticsName: "New template",
section: ActiveCollectionSection,
+5 -26
View File
@@ -3,8 +3,7 @@ import { toast } from "sonner";
import Comment from "~/models/Comment";
import CommentDeleteDialog from "~/components/CommentDeleteDialog";
import ViewReactionsDialog from "~/components/Reactions/ViewReactionsDialog";
import history from "~/utils/history";
import { createActionV2 } from "..";
import { createAction } from "..";
import { ActiveDocumentSection } from "../sections";
export const deleteCommentFactory = ({
@@ -14,7 +13,7 @@ export const deleteCommentFactory = ({
comment: Comment;
onDelete: () => void;
}) =>
createActionV2({
createAction({
name: ({ t }) => `${t("Delete")}`,
analyticsName: "Delete comment",
section: ActiveDocumentSection,
@@ -40,7 +39,7 @@ export const resolveCommentFactory = ({
comment: Comment;
onResolve: () => void;
}) =>
createActionV2({
createAction({
name: ({ t }) => t("Mark as resolved"),
analyticsName: "Resolve thread",
section: ActiveDocumentSection,
@@ -50,16 +49,6 @@ export const resolveCommentFactory = ({
stores.policies.abilities(comment.documentId).update,
perform: async ({ t }) => {
await comment.resolve();
const locationState = history.location.state as Record<string, unknown>;
history.replace({
...history.location,
state: {
sidebarContext: locationState["sidebarContext"],
commentId: undefined,
},
});
onResolve();
toast.success(t("Thread resolved"));
},
@@ -72,7 +61,7 @@ export const unresolveCommentFactory = ({
comment: Comment;
onUnresolve: () => void;
}) =>
createActionV2({
createAction({
name: ({ t }) => t("Mark as unresolved"),
analyticsName: "Unresolve thread",
section: ActiveDocumentSection,
@@ -82,16 +71,6 @@ export const unresolveCommentFactory = ({
stores.policies.abilities(comment.documentId).update,
perform: async () => {
await comment.unresolve();
const locationState = history.location.state as Record<string, unknown>;
history.replace({
...history.location,
state: {
sidebarContext: locationState["sidebarContext"],
commentId: undefined,
},
});
onUnresolve();
},
});
@@ -101,7 +80,7 @@ export const viewCommentReactionsFactory = ({
}: {
comment: Comment;
}) =>
createActionV2({
createAction({
name: ({ t }) => `${t("View reactions")}`,
analyticsName: "View comment reactions",
section: ActiveDocumentSection,
+20 -4
View File
@@ -9,7 +9,7 @@ import {
UserIcon,
} from "outline-icons";
import { toast } from "sonner";
import { createAction } from "~/actions";
import { createAction, createActionWithChildren } from "~/actions";
import { DeveloperSection } from "~/actions/sections";
import env from "~/env";
import { client } from "~/utils/ApiClient";
@@ -19,7 +19,7 @@ import { deleteAllDatabases } from "~/utils/developer";
import history from "~/utils/history";
import { homePath } from "~/utils/routeHelpers";
export const copyId = createAction({
export const copyId = createActionWithChildren({
name: ({ t }) => t("Copy ID"),
icon: <CopyIcon />,
keywords: "uuid",
@@ -176,7 +176,22 @@ export const toggleDebugLogging = createAction({
},
});
export const toggleFeatureFlag = createAction({
export const toggleDebugSafeArea = createAction({
name: () => "Toggle menu safe area debugging",
icon: <ToolsIcon />,
section: DeveloperSection,
visible: () => env.ENVIRONMENT === "development",
perform: ({ stores }) => {
stores.ui.toggleDebugSafeArea();
toast.message(
stores.ui.debugSafeArea
? "Menu safe area debugging enabled"
: "Menu safe area debugging disabled"
);
},
});
export const toggleFeatureFlag = createActionWithChildren({
name: "Toggle feature flag",
icon: <BeakerIcon />,
section: DeveloperSection,
@@ -200,7 +215,7 @@ export const toggleFeatureFlag = createAction({
),
});
export const developer = createAction({
export const developer = createActionWithChildren({
name: ({ t }) => t("Development"),
keywords: "debug",
icon: <ToolsIcon />,
@@ -209,6 +224,7 @@ export const developer = createAction({
children: [
copyId,
toggleDebugLogging,
toggleDebugSafeArea,
toggleFeatureFlag,
createToast,
createTestUsers,
+119 -118
View File
@@ -47,18 +47,15 @@ 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 MarkdownIcon from "~/components/Icons/MarkdownIcon";
import SharePopover from "~/components/Sharing/Document";
import { getHeaderExpandedKey } from "~/components/Sidebar/components/Header";
import DocumentTemplatizeDialog from "~/components/TemplatizeDialog";
import {
createAction,
createActionV2,
createActionV2Group,
createActionV2WithChildren,
createInternalLinkActionV2,
createActionGroup,
createActionWithChildren,
createInternalLinkAction,
} from "~/actions";
import {
ActiveDocumentSection,
@@ -81,10 +78,18 @@ import {
} from "~/utils/routeHelpers";
import capitalize from "lodash/capitalize";
import CollectionIcon from "~/components/Icons/CollectionIcon";
import { ActionV2, ActionV2Group, ActionV2Separator } from "~/types";
import Insights from "~/scenes/Document/components/Insights";
import { Action, ActionGroup, ActionSeparator } from "~/types";
import lazyWithRetry from "~/utils/lazyWithRetry";
import DocumentArchive from "~/scenes/DocumentArchive";
export const openDocument = createAction({
const Insights = lazyWithRetry(
() => import("~/scenes/Document/components/Insights")
);
const SharePopover = lazyWithRetry(
() => import("~/components/Sharing/Document/SharePopover")
);
export const openDocument = createActionWithChildren({
name: ({ t }) => t("Open document"),
analyticsName: "Open document",
section: DocumentSection,
@@ -98,23 +103,29 @@ export const openDocument = createAction({
);
const documents = stores.documents.orderedData;
return uniqBy([...documents, ...nodes], "id").map((item) => ({
// Note: using url which includes the slug rather than id here to bust
// cache if the document is renamed
id: item.url,
name: item.title,
icon: item.icon ? (
<Icon value={item.icon} color={item.color ?? undefined} />
) : (
<DocumentIcon />
),
section: DocumentSection,
to: item.url,
}));
return uniqBy([...documents, ...nodes], "id").map((item) =>
createInternalLinkAction({
// Note: using url which includes the slug rather than id here to bust
// cache if the document is renamed
id: item.url,
name: item.title,
icon: item.icon ? (
<Icon
value={item.icon}
initial={item.title}
color={item.color ?? undefined}
/>
) : (
<DocumentIcon />
),
section: DocumentSection,
to: item.url,
})
);
},
});
export const editDocument = createInternalLinkActionV2({
export const editDocument = createInternalLinkAction({
name: ({ t }) => t("Edit"),
analyticsName: "Edit document",
section: ActiveDocumentSection,
@@ -146,7 +157,7 @@ export const editDocument = createInternalLinkActionV2({
},
});
export const createDocument = createAction({
export const createDocument = createInternalLinkAction({
name: ({ t }) => t("New document"),
analyticsName: "New document",
section: DocumentSection,
@@ -164,13 +175,18 @@ export const createDocument = createAction({
!!currentTeamId && stores.policies.abilities(currentTeamId).createDocument
);
},
perform: ({ activeCollectionId, sidebarContext }) =>
history.push(newDocumentPath(activeCollectionId), {
sidebarContext,
}),
to: ({ activeCollectionId, sidebarContext }) => {
const [pathname, search] = newDocumentPath(activeCollectionId).split("?");
return {
pathname,
search,
state: { sidebarContext },
};
},
});
export const createDraftDocument = createAction({
export const createDraftDocument = createInternalLinkAction({
name: ({ t }) => t("New draft"),
analyticsName: "New document",
section: DocumentSection,
@@ -178,13 +194,13 @@ export const createDraftDocument = createAction({
keywords: "create document",
visible: ({ currentTeamId, stores }) =>
!!currentTeamId && stores.policies.abilities(currentTeamId).createDocument,
perform: ({ sidebarContext }) =>
history.push(newDocumentPath(), {
sidebarContext,
}),
to: ({ sidebarContext }) => ({
pathname: newDocumentPath(),
state: { sidebarContext },
}),
});
export const createDocumentFromTemplate = createInternalLinkActionV2({
export const createDocumentFromTemplate = createInternalLinkAction({
name: ({ t }) => t("New from template"),
analyticsName: "New document",
section: DocumentSection,
@@ -231,7 +247,7 @@ export const createDocumentFromTemplate = createInternalLinkActionV2({
},
});
export const createNestedDocument = createInternalLinkActionV2({
export const createNestedDocument = createInternalLinkAction({
name: ({ t }) => t("New nested document"),
analyticsName: "New document",
section: ActiveDocumentSection,
@@ -254,7 +270,7 @@ export const createNestedDocument = createInternalLinkActionV2({
},
});
export const starDocument = createActionV2({
export const starDocument = createAction({
name: ({ t }) => t("Star"),
analyticsName: "Star document",
section: ActiveDocumentSection,
@@ -280,7 +296,7 @@ export const starDocument = createActionV2({
},
});
export const unstarDocument = createActionV2({
export const unstarDocument = createAction({
name: ({ t }) => t("Unstar"),
analyticsName: "Unstar document",
section: ActiveDocumentSection,
@@ -306,7 +322,7 @@ export const unstarDocument = createActionV2({
},
});
export const publishDocument = createActionV2({
export const publishDocument = createAction({
name: ({ t }) => t("Publish"),
analyticsName: "Publish document",
section: ActiveDocumentSection,
@@ -348,7 +364,7 @@ export const publishDocument = createActionV2({
},
});
export const unpublishDocument = createActionV2({
export const unpublishDocument = createAction({
name: ({ t }) => t("Unpublish"),
analyticsName: "Unpublish document",
section: ActiveDocumentSection,
@@ -379,13 +395,13 @@ export const unpublishDocument = createActionV2({
},
});
export const subscribeDocument = createActionV2({
export const subscribeDocument = createAction({
name: ({ t }) => t("Subscribe"),
analyticsName: "Subscribe to document",
section: ActiveDocumentSection,
icon: <SubscribeIcon />,
tooltip: ({ activeCollectionId, isContextMenu, stores, t }) => {
if (!isContextMenu || !activeCollectionId) {
tooltip: ({ activeCollectionId, isMenu, stores, t }) => {
if (!isMenu || !activeCollectionId) {
return undefined;
}
@@ -393,8 +409,8 @@ export const subscribeDocument = createActionV2({
? t("Subscription inherited from collection")
: undefined;
},
disabled: ({ activeCollectionId, isContextMenu, stores }) => {
if (!isContextMenu || !activeCollectionId) {
disabled: ({ activeCollectionId, isMenu, stores }) => {
if (!isMenu || !activeCollectionId) {
return false;
}
@@ -425,13 +441,13 @@ export const subscribeDocument = createActionV2({
},
});
export const unsubscribeDocument = createActionV2({
export const unsubscribeDocument = createAction({
name: ({ t }) => t("Unsubscribe"),
analyticsName: "Unsubscribe from document",
section: ActiveDocumentSection,
icon: <UnsubscribeIcon />,
tooltip: ({ activeCollectionId, isContextMenu, stores, t }) => {
if (!isContextMenu || !activeCollectionId) {
tooltip: ({ activeCollectionId, isMenu, stores, t }) => {
if (!isMenu || !activeCollectionId) {
return undefined;
}
@@ -439,8 +455,8 @@ export const unsubscribeDocument = createActionV2({
? t("Subscription inherited from collection")
: undefined;
},
disabled: ({ activeCollectionId, isContextMenu, stores }) => {
if (!isContextMenu || !activeCollectionId) {
disabled: ({ activeCollectionId, isMenu, stores }) => {
if (!isMenu || !activeCollectionId) {
return false;
}
@@ -473,7 +489,7 @@ export const unsubscribeDocument = createActionV2({
},
});
export const shareDocument = createActionV2({
export const shareDocument = createAction({
name: ({ t }) => `${t("Permissions")}`,
analyticsName: "Share document",
section: ActiveDocumentSection,
@@ -506,7 +522,7 @@ export const shareDocument = createActionV2({
},
});
export const downloadDocumentAsHTML = createActionV2({
export const downloadDocumentAsHTML = createAction({
name: ({ t }) => t("HTML"),
analyticsName: "Download document as HTML",
section: ActiveDocumentSection,
@@ -525,7 +541,7 @@ export const downloadDocumentAsHTML = createActionV2({
},
});
export const downloadDocumentAsPDF = createActionV2({
export const downloadDocumentAsPDF = createAction({
name: ({ t }) => t("PDF"),
analyticsName: "Download document as PDF",
section: ActiveDocumentSection,
@@ -551,7 +567,7 @@ export const downloadDocumentAsPDF = createActionV2({
},
});
export const downloadDocumentAsMarkdown = createActionV2({
export const downloadDocumentAsMarkdown = createAction({
name: ({ t }) => t("Markdown"),
analyticsName: "Download document as Markdown",
section: ActiveDocumentSection,
@@ -570,9 +586,8 @@ export const downloadDocumentAsMarkdown = createActionV2({
},
});
export const downloadDocument = createActionV2WithChildren({
name: ({ t, isContextMenu }) =>
isContextMenu ? t("Download") : t("Download document"),
export const downloadDocument = createActionWithChildren({
name: ({ t, isMenu }) => (isMenu ? t("Download") : t("Download document")),
analyticsName: "Download document",
section: ActiveDocumentSection,
icon: <DownloadIcon />,
@@ -586,7 +601,7 @@ export const downloadDocument = createActionV2WithChildren({
],
});
export const copyDocumentAsMarkdown = createActionV2({
export const copyDocumentAsMarkdown = createAction({
name: ({ t }) => t("Copy as Markdown"),
section: ActiveDocumentSection,
keywords: "clipboard",
@@ -594,18 +609,21 @@ export const copyDocumentAsMarkdown = createActionV2({
iconInContextMenu: false,
visible: ({ activeDocumentId, stores }) =>
!!activeDocumentId && stores.policies.abilities(activeDocumentId).download,
perform: ({ stores, activeDocumentId, t }) => {
perform: async ({ stores, activeDocumentId, t }) => {
const document = activeDocumentId
? stores.documents.get(activeDocumentId)
: undefined;
if (document) {
copy(document.toMarkdown());
const { ProsemirrorHelper } = await import(
"~/models/helpers/ProsemirrorHelper"
);
copy(ProsemirrorHelper.toMarkdown(document));
toast.success(t("Markdown copied to clipboard"));
}
},
});
export const copyDocumentAsPlainText = createActionV2({
export const copyDocumentAsPlainText = createAction({
name: ({ t }) => t("Copy as text"),
section: ActiveDocumentSection,
keywords: "clipboard",
@@ -613,18 +631,21 @@ export const copyDocumentAsPlainText = createActionV2({
iconInContextMenu: false,
visible: ({ activeDocumentId, stores }) =>
!!activeDocumentId && stores.policies.abilities(activeDocumentId).download,
perform: ({ stores, activeDocumentId, t }) => {
perform: async ({ stores, activeDocumentId, t }) => {
const document = activeDocumentId
? stores.documents.get(activeDocumentId)
: undefined;
if (document) {
copy(document.toPlainText());
const { ProsemirrorHelper } = await import(
"~/models/helpers/ProsemirrorHelper"
);
copy(ProsemirrorHelper.toPlainText(document));
toast.success(t("Text copied to clipboard"));
}
},
});
export const copyDocumentShareLink = createActionV2({
export const copyDocumentShareLink = createAction({
name: ({ t }) => t("Copy public link"),
section: ActiveDocumentSection,
keywords: "clipboard share",
@@ -645,7 +666,7 @@ export const copyDocumentShareLink = createActionV2({
},
});
export const copyDocumentLink = createActionV2({
export const copyDocumentLink = createAction({
name: ({ t }) => t("Copy link"),
section: ActiveDocumentSection,
keywords: "clipboard",
@@ -663,7 +684,7 @@ export const copyDocumentLink = createActionV2({
},
});
export const copyDocument = createActionV2WithChildren({
export const copyDocument = createActionWithChildren({
name: ({ t }) => t("Copy"),
analyticsName: "Copy document",
section: ActiveDocumentSection,
@@ -677,9 +698,8 @@ export const copyDocument = createActionV2WithChildren({
],
});
export const duplicateDocument = createActionV2({
name: ({ t, isContextMenu }) =>
isContextMenu ? t("Duplicate") : t("Duplicate document"),
export const duplicateDocument = createAction({
name: ({ t, isMenu }) => (isMenu ? t("Duplicate") : t("Duplicate document")),
analyticsName: "Duplicate document",
section: ActiveDocumentSection,
icon: <DuplicateIcon />,
@@ -713,7 +733,7 @@ export const duplicateDocument = createActionV2({
* Pin a document to a collection. Pinned documents will be displayed at the top
* of the collection for all collection members to see.
*/
export const pinDocumentToCollection = createActionV2({
export const pinDocumentToCollection = createAction({
name: ({ activeDocumentId = "", t, stores }) => {
const selectedDocument = stores.documents.get(activeDocumentId);
const collectionName = selectedDocument
@@ -758,7 +778,7 @@ export const pinDocumentToCollection = createActionV2({
* Pin a document to team home. Pinned documents will be displayed at the top
* of the home screen for all team members to see.
*/
export const pinDocumentToHome = createActionV2({
export const pinDocumentToHome = createAction({
name: ({ t }) => t("Pin to home"),
analyticsName: "Pin document to home",
section: ActiveDocumentSection,
@@ -790,7 +810,7 @@ export const pinDocumentToHome = createActionV2({
},
});
export const pinDocument = createActionV2WithChildren({
export const pinDocument = createActionWithChildren({
name: ({ t }) => t("Pin"),
analyticsName: "Pin document",
section: ActiveDocumentSection,
@@ -798,7 +818,7 @@ export const pinDocument = createActionV2WithChildren({
children: [pinDocumentToCollection, pinDocumentToHome],
});
export const searchInDocument = createInternalLinkActionV2({
export const searchInDocument = createInternalLinkAction({
name: ({ t }) => t("Search in document"),
analyticsName: "Search document",
section: ActiveDocumentSection,
@@ -828,9 +848,8 @@ export const searchInDocument = createInternalLinkActionV2({
},
});
export const printDocument = createActionV2({
name: ({ t, isContextMenu }) =>
isContextMenu ? t("Print") : t("Print document"),
export const printDocument = createAction({
name: ({ t, isMenu }) => (isMenu ? t("Print") : t("Print document")),
analyticsName: "Print document",
section: ActiveDocumentSection,
icon: <PrintIcon />,
@@ -840,7 +859,7 @@ export const printDocument = createActionV2({
},
});
export const importDocument = createActionV2({
export const importDocument = createAction({
name: ({ t }) => t("Import document"),
analyticsName: "Import document",
section: DocumentSection,
@@ -852,7 +871,7 @@ export const importDocument = createActionV2({
}
if (activeCollectionId) {
return !!stores.policies.abilities(activeCollectionId).update;
return !!stores.policies.abilities(activeCollectionId).createDocument;
}
return false;
@@ -861,11 +880,10 @@ export const importDocument = createActionV2({
const { documents } = stores;
const input = document.createElement("input");
input.type = "file";
input.accept = documents.importFileTypes.join(", ");
input.accept = documents.importFileTypesString;
input.onchange = async (ev) => {
const files = getEventFiles(ev);
const file = files[0];
try {
@@ -887,7 +905,7 @@ export const importDocument = createActionV2({
},
});
export const createTemplateFromDocument = createActionV2({
export const createTemplateFromDocument = createAction({
name: ({ t }) => t("Templatize"),
analyticsName: "Templatize document",
section: ActiveDocumentSection,
@@ -938,7 +956,7 @@ export const openRandomDocument = createAction({
});
export const searchDocumentsForQuery = (query: string) =>
createAction({
createInternalLinkAction({
id: "search",
name: ({ t }) =>
t(`Search documents for "{{searchQuery}}"`, { searchQuery: query }),
@@ -949,7 +967,7 @@ export const searchDocumentsForQuery = (query: string) =>
visible: ({ location }) => location.pathname !== searchPath(),
});
export const moveTemplateToWorkspace = createActionV2({
export const moveTemplateToWorkspace = createAction({
name: ({ t }) => t("Move to workspace"),
analyticsName: "Move template to workspace",
section: DocumentSection,
@@ -979,7 +997,7 @@ export const moveTemplateToWorkspace = createActionV2({
},
});
export const moveDocumentToCollection = createActionV2({
export const moveDocumentToCollection = createAction({
name: ({ activeDocumentId, stores, t }) => {
if (!activeDocumentId) {
return t("Move");
@@ -1010,13 +1028,13 @@ export const moveDocumentToCollection = createActionV2({
title: t("Move {{ documentType }}", {
documentType: document.noun,
}),
content: <DocumentMove document={document} />,
content: <DocumentMove documents={[document]} />,
});
}
},
});
export const moveDocument = createActionV2({
export const moveDocument = createAction({
name: ({ t }) => t("Move"),
analyticsName: "Move document",
section: ActiveDocumentSection,
@@ -1035,7 +1053,7 @@ export const moveDocument = createActionV2({
perform: moveDocumentToCollection.perform,
});
export const moveTemplate = createActionV2WithChildren({
export const moveTemplate = createActionWithChildren({
name: ({ t }) => t("Move"),
analyticsName: "Move document",
section: ActiveDocumentSection,
@@ -1054,7 +1072,7 @@ export const moveTemplate = createActionV2WithChildren({
children: [moveTemplateToWorkspace, moveDocumentToCollection],
});
export const archiveDocument = createActionV2({
export const archiveDocument = createAction({
name: ({ t }) => `${t("Archive")}`,
analyticsName: "Archive document",
section: ActiveDocumentSection,
@@ -1076,25 +1094,13 @@ export const archiveDocument = createActionV2({
dialogs.openModal({
title: t("Are you sure you want to archive this document?"),
content: (
<ConfirmationDialog
onSubmit={async () => {
await document.archive();
toast.success(t("Document archived"));
}}
savingText={`${t("Archiving")}`}
>
{t(
"Archiving this document will remove it from the collection and search results."
)}
</ConfirmationDialog>
),
content: <DocumentArchive documents={[document]} />,
});
}
},
});
export const restoreDocument = createActionV2({
export const restoreDocument = createAction({
name: ({ t }) => `${t("Restore")}`,
analyticsName: "Restore document",
section: ActiveDocumentSection,
@@ -1134,7 +1140,7 @@ export const restoreDocument = createActionV2({
},
});
export const restoreDocumentToCollection = createActionV2WithChildren({
export const restoreDocumentToCollection = createActionWithChildren({
name: ({ t }) => `${t("Restore")}`,
analyticsName: "Restore document",
section: ActiveDocumentSection,
@@ -1169,7 +1175,7 @@ export const restoreDocumentToCollection = createActionV2WithChildren({
const actions = collections.orderedData.map((collection) => {
const can = policies.abilities(collection.id);
return createActionV2({
return createAction({
name: collection.name,
section: ActiveDocumentSection,
icon: <CollectionIcon collection={collection} />,
@@ -1185,11 +1191,11 @@ export const restoreDocumentToCollection = createActionV2WithChildren({
});
});
return [createActionV2Group({ name: t("Choose a collection"), actions })];
return [createActionGroup({ name: t("Choose a collection"), actions })];
},
});
export const deleteDocument = createActionV2({
export const deleteDocument = createAction({
name: ({ t }) => `${t("Delete")}`,
analyticsName: "Delete document",
section: ActiveDocumentSection,
@@ -1212,18 +1218,13 @@ export const deleteDocument = createActionV2({
title: t("Delete {{ documentName }}", {
documentName: document.noun,
}),
content: (
<DocumentDelete
document={document}
onSubmit={stores.dialogs.closeAllModals}
/>
),
content: <DocumentDelete documents={[document]} />,
});
}
},
});
export const permanentlyDeleteDocument = createActionV2({
export const permanentlyDeleteDocument = createAction({
name: ({ t }) => t("Permanently delete"),
analyticsName: "Permanently delete document",
section: ActiveDocumentSection,
@@ -1278,7 +1279,7 @@ export const permanentlyDeleteDocumentsInTrash = createAction({
},
});
export const openDocumentComments = createActionV2({
export const openDocumentComments = createAction({
name: ({ t }) => t("Comments"),
analyticsName: "Open comments",
section: ActiveDocumentSection,
@@ -1301,7 +1302,7 @@ export const openDocumentComments = createActionV2({
},
});
export const openDocumentHistory = createInternalLinkActionV2({
export const openDocumentHistory = createInternalLinkAction({
name: ({ t }) => t("History"),
analyticsName: "Open document history",
section: ActiveDocumentSection,
@@ -1328,7 +1329,7 @@ export const openDocumentHistory = createInternalLinkActionV2({
},
});
export const openDocumentInsights = createActionV2({
export const openDocumentInsights = createAction({
name: ({ t }) => t("Insights"),
analyticsName: "Open document insights",
section: ActiveDocumentSection,
@@ -1361,7 +1362,7 @@ export const openDocumentInsights = createActionV2({
},
});
export const leaveDocument = createActionV2({
export const leaveDocument = createAction({
name: ({ t }) => t("Leave document"),
analyticsName: "Leave document",
section: ActiveDocumentSection,
@@ -1400,9 +1401,9 @@ export const leaveDocument = createActionV2({
export const applyTemplateFactory = ({
actions,
}: {
actions: (ActionV2 | ActionV2Group | ActionV2Separator)[];
actions: (Action | ActionGroup | ActionSeparator)[];
}) =>
createActionV2WithChildren({
createActionWithChildren({
name: ({ t }) => t("Apply template"),
analyticsName: "Apply template",
section: ActiveDocumentSection,
+21
View File
@@ -0,0 +1,21 @@
import { PlusIcon } from "outline-icons";
import { createAction } from "~/actions";
import { TeamSection } from "../sections";
import stores from "~/stores";
import { EmojiCreateDialog } from "~/components/EmojiCreateDialog";
export const createEmoji = createAction({
name: ({ t }) => `${t("New emoji")}`,
analyticsName: "Create emoji",
icon: <PlusIcon />,
keywords: "emoji custom upload image",
section: TeamSection,
visible: () =>
stores.policies.abilities(stores.auth.team?.id || "").createEmoji,
perform: ({ t }) => {
stores.dialogs.openModal({
title: t("Upload emoji"),
content: <EmojiCreateDialog onSubmit={stores.dialogs.closeAllModals} />,
});
},
});
+20 -1
View File
@@ -3,8 +3,27 @@ import stores from "~/stores";
import { createAction } from "..";
import { SettingsSection } from "../sections";
import Integration from "~/models/Integration";
import { DisconnectAnalyticsDialog } from "~/scenes/Settings/components/DisconnectAnalyticsDialog";
import { IntegrationType } from "@shared/types";
import { DisconnectAnalyticsDialog } from "~/components/DisconnectAnalyticsDialog";
import { settingsPath } from "@shared/utils/routeHelpers";
import history from "~/utils/history";
export const disconnectIntegrationFactory = (integration?: Integration) =>
createAction({
name: ({ t }) => t("Disconnect"),
analyticsName: "Disconnect integration",
section: SettingsSection,
icon: <TrashIcon />,
keywords: "disconnect",
visible: () => !!integration,
perform: async ({ event }) => {
event?.preventDefault();
event?.stopPropagation();
await integration?.delete();
history.push(settingsPath("integrations"));
},
});
export const disconnectAnalyticsIntegrationFactory = (
integration?: Integration<IntegrationType.Analytics>
+26 -29
View File
@@ -21,9 +21,8 @@ import SearchQuery from "~/models/SearchQuery";
import KeyboardShortcuts from "~/scenes/KeyboardShortcuts";
import {
createAction,
createActionV2,
createExternalLinkActionV2,
createInternalLinkActionV2,
createExternalLinkAction,
createInternalLinkAction,
} from "~/actions";
import { NavigationSection, RecentSearchesSection } from "~/actions/sections";
import Desktop from "~/utils/Desktop";
@@ -37,7 +36,7 @@ import {
settingsPath,
} from "~/utils/routeHelpers";
export const navigateToHome = createAction({
export const navigateToHome = createInternalLinkAction({
name: ({ t }) => t("Home"),
analyticsName: "Navigate to home",
section: NavigationSection,
@@ -48,7 +47,7 @@ export const navigateToHome = createAction({
});
export const navigateToRecentSearchQuery = (searchQuery: SearchQuery) =>
createAction({
createInternalLinkAction({
section: RecentSearchesSection,
name: searchQuery.query,
analyticsName: "Navigate to recent search query",
@@ -56,7 +55,7 @@ export const navigateToRecentSearchQuery = (searchQuery: SearchQuery) =>
to: searchPath({ query: searchQuery.query }),
});
export const navigateToDrafts = createAction({
export const navigateToDrafts = createInternalLinkAction({
name: ({ t }) => t("Drafts"),
analyticsName: "Navigate to drafts",
section: NavigationSection,
@@ -65,7 +64,7 @@ export const navigateToDrafts = createAction({
visible: ({ location }) => location.pathname !== draftsPath(),
});
export const navigateToSearch = createAction({
export const navigateToSearch = createInternalLinkAction({
name: ({ t }) => t("Search"),
analyticsName: "Navigate to search",
section: NavigationSection,
@@ -74,7 +73,7 @@ export const navigateToSearch = createAction({
visible: ({ location }) => location.pathname !== searchPath(),
});
export const navigateToArchive = createAction({
export const navigateToArchive = createInternalLinkAction({
name: ({ t }) => t("Archive"),
analyticsName: "Navigate to archive",
section: NavigationSection,
@@ -84,7 +83,7 @@ export const navigateToArchive = createAction({
visible: ({ location }) => location.pathname !== archivePath(),
});
export const navigateToTrash = createAction({
export const navigateToTrash = createInternalLinkAction({
name: ({ t }) => t("Trash"),
analyticsName: "Navigate to trash",
section: NavigationSection,
@@ -93,7 +92,7 @@ export const navigateToTrash = createAction({
visible: ({ location }) => location.pathname !== trashPath(),
});
export const navigateToSettings = createAction({
export const navigateToSettings = createInternalLinkAction({
name: ({ t }) => t("Settings"),
analyticsName: "Navigate to settings",
section: NavigationSection,
@@ -103,7 +102,7 @@ export const navigateToSettings = createAction({
to: settingsPath(),
});
export const navigateToWorkspaceSettings = createInternalLinkActionV2({
export const navigateToWorkspaceSettings = createInternalLinkAction({
name: ({ t }) => t("Settings"),
analyticsName: "Navigate to workspace settings",
section: NavigationSection,
@@ -112,7 +111,7 @@ export const navigateToWorkspaceSettings = createInternalLinkActionV2({
to: settingsPath("details"),
});
export const navigateToProfileSettings = createInternalLinkActionV2({
export const navigateToProfileSettings = createInternalLinkAction({
name: ({ t }) => t("Profile"),
analyticsName: "Navigate to profile settings",
section: NavigationSection,
@@ -121,7 +120,7 @@ export const navigateToProfileSettings = createInternalLinkActionV2({
to: settingsPath(),
});
export const navigateToTemplateSettings = createAction({
export const navigateToTemplateSettings = createInternalLinkAction({
name: ({ t }) => t("Templates"),
analyticsName: "Navigate to template settings",
section: NavigationSection,
@@ -130,9 +129,9 @@ export const navigateToTemplateSettings = createAction({
to: settingsPath("templates"),
});
export const navigateToNotificationSettings = createInternalLinkActionV2({
name: ({ t, isContextMenu }) =>
isContextMenu ? t("Notification settings") : t("Notifications"),
export const navigateToNotificationSettings = createInternalLinkAction({
name: ({ t, isMenu }) =>
isMenu ? t("Notification settings") : t("Notifications"),
analyticsName: "Navigate to notification settings",
section: NavigationSection,
iconInContextMenu: false,
@@ -140,7 +139,7 @@ export const navigateToNotificationSettings = createInternalLinkActionV2({
to: settingsPath("notifications"),
});
export const navigateToAccountPreferences = createInternalLinkActionV2({
export const navigateToAccountPreferences = createInternalLinkAction({
name: ({ t }) => t("Preferences"),
analyticsName: "Navigate to account preferences",
section: NavigationSection,
@@ -149,7 +148,7 @@ export const navigateToAccountPreferences = createInternalLinkActionV2({
to: settingsPath("preferences"),
});
export const openDocumentation = createExternalLinkActionV2({
export const openDocumentation = createExternalLinkAction({
name: ({ t }) => t("Documentation"),
analyticsName: "Open documentation",
section: NavigationSection,
@@ -159,7 +158,7 @@ export const openDocumentation = createExternalLinkActionV2({
target: "_blank",
});
export const openAPIDocumentation = createExternalLinkActionV2({
export const openAPIDocumentation = createExternalLinkAction({
name: ({ t }) => t("API documentation"),
analyticsName: "Open API documentation",
section: NavigationSection,
@@ -177,7 +176,7 @@ export const toggleSidebar = createAction({
perform: () => stores.ui.toggleCollapsedSidebar(),
});
export const openFeedbackUrl = createExternalLinkActionV2({
export const openFeedbackUrl = createExternalLinkAction({
name: ({ t }) => t("Send us feedback"),
analyticsName: "Open feedback",
section: NavigationSection,
@@ -187,7 +186,7 @@ export const openFeedbackUrl = createExternalLinkActionV2({
target: "_blank",
});
export const openBugReportUrl = createExternalLinkActionV2({
export const openBugReportUrl = createExternalLinkAction({
name: ({ t }) => t("Report a bug"),
analyticsName: "Open bug report",
section: NavigationSection,
@@ -197,7 +196,7 @@ export const openBugReportUrl = createExternalLinkActionV2({
target: "_blank",
});
export const openChangelog = createExternalLinkActionV2({
export const openChangelog = createExternalLinkAction({
name: ({ t }) => t("Changelog"),
analyticsName: "Open changelog",
section: NavigationSection,
@@ -207,7 +206,7 @@ export const openChangelog = createExternalLinkActionV2({
target: "_blank",
});
export const openKeyboardShortcuts = createActionV2({
export const openKeyboardShortcuts = createAction({
name: ({ t }) => t("Keyboard shortcuts"),
analyticsName: "Open keyboard shortcuts",
section: NavigationSection,
@@ -222,7 +221,7 @@ export const openKeyboardShortcuts = createActionV2({
},
});
export const downloadApp = createAction({
export const downloadApp = createExternalLinkAction({
name: ({ t }) =>
t("Download {{ platform }} app", {
platform: isMac() ? "macOS" : "Windows",
@@ -232,13 +231,11 @@ export const downloadApp = createAction({
iconInContextMenu: false,
icon: <BrowserIcon />,
visible: () => !Desktop.isElectron() && isMac() && isCloudHosted,
to: {
url: "https://desktop.getoutline.com",
target: "_blank",
},
url: "https://desktop.getoutline.com",
target: "_blank",
});
export const logout = createActionV2({
export const logout = createAction({
name: ({ t }) => t("Log out"),
analyticsName: "Log out",
section: NavigationSection,
+2 -2
View File
@@ -1,5 +1,5 @@
import { ArchiveIcon, MarkAsReadIcon } from "outline-icons";
import { createAction, createActionV2 } from "..";
import { createAction } from "..";
import { NotificationSection } from "../sections";
export const markNotificationsAsRead = createAction({
@@ -12,7 +12,7 @@ export const markNotificationsAsRead = createAction({
visible: ({ stores }) => stores.notifications.approximateUnreadCount > 0,
});
export const markNotificationsAsArchived = createActionV2({
export const markNotificationsAsArchived = createAction({
name: ({ t }) => t("Archive all notifications"),
analyticsName: "Mark notifications as archived",
section: NotificationSection,
+3 -3
View File
@@ -3,7 +3,7 @@ import { LinkIcon, RestoreIcon, TrashIcon } from "outline-icons";
import { matchPath } from "react-router-dom";
import { toast } from "sonner";
import stores from "~/stores";
import { createAction, createActionV2 } from "~/actions";
import { createAction } from "~/actions";
import { RevisionSection } from "~/actions/sections";
import history from "~/utils/history";
import {
@@ -11,7 +11,7 @@ import {
matchDocumentHistory,
} from "~/utils/routeHelpers";
export const restoreRevision = createActionV2({
export const restoreRevision = createAction({
name: ({ t }) => t("Restore"),
analyticsName: "Restore revision",
icon: <RestoreIcon />,
@@ -73,7 +73,7 @@ export const deleteRevision = createAction({
},
});
export const copyLinkToRevision = createActionV2({
export const copyLinkToRevision = createAction({
name: ({ t }) => t("Copy link"),
analyticsName: "Copy link to revision",
icon: <LinkIcon />,
+6 -7
View File
@@ -1,9 +1,9 @@
import { SunIcon, MoonIcon, BrowserIcon } from "outline-icons";
import { Theme } from "~/stores/UiStore";
import { createActionV2, createActionV2WithChildren } from "~/actions";
import { createAction, createActionWithChildren } from "~/actions";
import { SettingsSection } from "~/actions/sections";
export const changeToDarkTheme = createActionV2({
export const changeToDarkTheme = createAction({
name: ({ t }) => t("Dark"),
analyticsName: "Change to dark theme",
icon: <MoonIcon />,
@@ -14,7 +14,7 @@ export const changeToDarkTheme = createActionV2({
perform: ({ stores }) => stores.ui.setTheme(Theme.Dark),
});
export const changeToLightTheme = createActionV2({
export const changeToLightTheme = createAction({
name: ({ t }) => t("Light"),
analyticsName: "Change to light theme",
icon: <SunIcon />,
@@ -25,7 +25,7 @@ export const changeToLightTheme = createActionV2({
perform: ({ stores }) => stores.ui.setTheme(Theme.Light),
});
export const changeToSystemTheme = createActionV2({
export const changeToSystemTheme = createAction({
name: ({ t }) => t("System"),
analyticsName: "Change to system theme",
icon: <BrowserIcon />,
@@ -36,9 +36,8 @@ export const changeToSystemTheme = createActionV2({
perform: ({ stores }) => stores.ui.setTheme(Theme.System),
});
export const changeTheme = createActionV2WithChildren({
name: ({ t, isContextMenu }) =>
isContextMenu ? t("Appearance") : t("Change theme"),
export const changeTheme = createActionWithChildren({
name: ({ t, isMenu }) => (isMenu ? t("Appearance") : t("Change theme")),
analyticsName: "Change theme",
placeholder: ({ t }) => t("Change theme to"),
icon: ({ stores }) =>
+4 -4
View File
@@ -1,13 +1,13 @@
import copy from "copy-to-clipboard";
import Share from "~/models/Share";
import { createActionV2, createInternalLinkActionV2 } from "..";
import { createAction, createInternalLinkAction } from "..";
import { ArrowIcon, CopyIcon, TrashIcon } from "outline-icons";
import { ShareSection } from "../sections";
import env from "~/env";
import { toast } from "sonner";
export const copyShareUrlFactory = ({ share }: { share: Share }) =>
createActionV2({
createAction({
name: ({ t }) => t("Copy link"),
analyticsName: "Copy share link",
section: ShareSection,
@@ -22,7 +22,7 @@ export const copyShareUrlFactory = ({ share }: { share: Share }) =>
});
export const goToShareSourceFactory = ({ share }: { share: Share }) =>
createInternalLinkActionV2({
createInternalLinkAction({
name: ({ t }) =>
share.collectionId ? t("Go to collection") : t("Go to document"),
analyticsName: "Go to share source",
@@ -41,7 +41,7 @@ export const revokeShareFactory = ({
share: Share;
can: Record<string, boolean>;
}) =>
createActionV2({
createAction({
name: ({ t }) => t("Revoke link"),
analyticsName: "Revoke share",
section: ShareSection,
+9 -9
View File
@@ -6,17 +6,17 @@ import { LoginDialog } from "~/scenes/Login/components/LoginDialog";
import TeamNew from "~/scenes/TeamNew";
import TeamLogo from "~/components/TeamLogo";
import {
createActionV2,
createActionV2WithChildren,
createExternalLinkActionV2,
createAction,
createActionWithChildren,
createExternalLinkAction,
} from "~/actions";
import { ActionContext, ExternalLinkActionV2 } from "~/types";
import { ActionContext, ExternalLinkAction } from "~/types";
import Desktop from "~/utils/Desktop";
import { TeamSection } from "../sections";
export const switchTeamsList = ({ stores }: { stores: RootStore }) =>
stores.auth.availableTeams?.map<ExternalLinkActionV2>((session) =>
createExternalLinkActionV2({
stores.auth.availableTeams?.map<ExternalLinkAction>((session) =>
createExternalLinkAction({
id: `switch-${session.id}`,
name: session.name,
analyticsName: "Switch workspace",
@@ -41,7 +41,7 @@ export const switchTeamsList = ({ stores }: { stores: RootStore }) =>
})
) ?? [];
export const switchTeam = createActionV2WithChildren({
export const switchTeam = createActionWithChildren({
name: ({ t }) => t("Switch workspace"),
placeholder: ({ t }) => t("Select a workspace"),
analyticsName: "Switch workspace",
@@ -52,7 +52,7 @@ export const switchTeam = createActionV2WithChildren({
children: switchTeamsList,
});
export const createTeam = createActionV2({
export const createTeam = createAction({
name: ({ t }) => `${t("New workspace")}`,
analyticsName: "New workspace",
keywords: "create change switch workspace organization team",
@@ -74,7 +74,7 @@ export const createTeam = createActionV2({
},
});
export const desktopLoginTeam = createActionV2({
export const desktopLoginTeam = createAction({
name: ({ t }) => t("Login to workspace"),
analyticsName: "Login to workspace",
keywords: "change switch workspace organization team",
+4 -3
View File
@@ -8,7 +8,7 @@ import {
UserChangeRoleDialog,
UserDeleteDialog,
} from "~/components/UserDialogs";
import { createAction, createActionV2 } from "~/actions";
import { createAction } from "~/actions";
import { UserSection } from "~/actions/sections";
export const inviteUser = createAction({
@@ -22,13 +22,14 @@ export const inviteUser = createAction({
perform: ({ t }) => {
stores.dialogs.openModal({
title: t("Invite to workspace"),
width: "500px",
content: <Invite onSubmit={stores.dialogs.closeAllModals} />,
});
},
});
export const updateUserRoleActionFactory = (user: User, role: UserRole) =>
createActionV2({
createAction({
name: ({ t }) =>
UserRoleHelper.isRoleHigher(role, user!.role)
? `${t("Promote to {{ role }}", {
@@ -63,7 +64,7 @@ export const updateUserRoleActionFactory = (user: User, role: UserRole) =>
});
export const deleteUserActionFactory = (userId: string) =>
createActionV2({
createAction({
name: ({ t }) => `${t("Delete user")}`,
analyticsName: "Delete user",
keywords: "leave",
+38 -192
View File
@@ -1,23 +1,17 @@
import { LocationDescriptor } from "history";
import flattenDeep from "lodash/flattenDeep";
import { toast } from "sonner";
import { Optional } from "utility-types";
import { v4 as uuidv4 } from "uuid";
import {
Action,
ActionContext,
ActionV2,
ActionV2Group,
ActionV2Separator as TActionV2Separator,
ActionV2Variant,
ActionV2WithChildren,
ExternalLinkActionV2,
InternalLinkActionV2,
MenuExternalLink,
MenuInternalLink,
Action,
ActionGroup,
ActionSeparator as TActionSeparator,
ActionVariant,
ActionWithChildren,
ExternalLinkAction,
InternalLinkAction,
MenuItem,
MenuItemButton,
MenuItemWithChildren,
} from "~/types";
import Analytics from "~/utils/Analytics";
import history from "~/utils/history";
@@ -27,161 +21,13 @@ export function resolve<T>(value: any, context: ActionContext): T {
return typeof value === "function" ? value(context) : value;
}
export function createAction(definition: Optional<Action, "id">): Action {
return {
...definition,
perform: definition.perform
? (context) => {
// We must use the specific analytics name here as the action name is
// translated and potentially contains user strings.
if (definition.analyticsName) {
Analytics.track("perform_action", definition.analyticsName, {
context: context.isButton
? "button"
: context.isCommandBar
? "commandbar"
: "contextmenu",
});
}
return definition.perform?.(context);
}
: undefined,
id: definition.id ?? uuidv4(),
};
}
export function actionToMenuItem(
action: Action,
context: ActionContext
): MenuItemButton | MenuExternalLink | MenuInternalLink | MenuItemWithChildren {
const resolvedIcon = resolve<React.ReactElement<any>>(action.icon, context);
const resolvedChildren = resolve<Action[]>(action.children, context);
const visible = action.visible ? action.visible(context) : true;
const title = resolve<string>(action.name, context);
const icon =
resolvedIcon && action.iconInContextMenu !== false
? resolvedIcon
: undefined;
if (resolvedChildren) {
const items = resolvedChildren
.map((a) => actionToMenuItem(a, context))
.filter(Boolean)
.filter((a) => a.visible);
return {
type: "submenu",
title,
icon,
items,
visible: visible && items.length > 0,
};
}
if (action.to) {
return typeof action.to === "string"
? {
type: "route",
title,
icon,
visible,
to: action.to,
selected: action.selected?.(context),
}
: {
type: "link",
title,
icon,
visible,
href: action.to,
selected: action.selected?.(context),
};
}
return {
type: "button",
title,
icon,
visible,
dangerous: action.dangerous,
onClick: () => performAction(action, context),
selected: action.selected?.(context),
};
}
export function actionToKBar(
action: Action,
context: ActionContext
): KbarAction[] {
if (typeof action.visible === "function" && !action.visible(context)) {
return [];
}
const resolvedIcon = resolve<React.ReactElement>(action.icon, context);
const resolvedChildren = resolve<Action[]>(action.children, context);
const resolvedSection = resolve<string>(action.section, context);
const resolvedName = resolve<string>(action.name, context);
const resolvedPlaceholder = resolve<string>(action.placeholder, context);
const children = resolvedChildren
? flattenDeep(resolvedChildren.map((a) => actionToKBar(a, context))).filter(
(a) => !!a
)
: [];
const sectionPriority =
typeof action.section !== "string" && "priority" in action.section
? ((action.section.priority as number) ?? 0)
: 0;
return [
{
id: action.id,
name: resolvedName,
analyticsName: action.analyticsName,
section: resolvedSection,
placeholder: resolvedPlaceholder,
keywords: action.keywords ?? "",
shortcut: action.shortcut || [],
icon: resolvedIcon,
priority: (1 + (action.priority ?? 0)) * (1 + (sectionPriority ?? 0)),
perform:
action.perform || action.to
? () => performAction(action, context)
: undefined,
},
].concat(
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
children.map((child) => ({ ...child, parent: child.parent ?? action.id }))
);
}
export async function performAction(action: Action, context: ActionContext) {
const result = action.perform
? action.perform(context)
: action.to
? typeof action.to === "string"
? history.push(action.to)
: window.open(action.to.url, action.to.target)
: undefined;
if (result instanceof Promise) {
return result.catch((err: Error) => {
toast.error(err.message);
});
}
return result;
}
/** Actions V2 */
export const ActionV2Separator: TActionV2Separator = {
export const ActionSeparator: TActionSeparator = {
type: "action_separator",
};
export function createActionV2(
definition: Optional<Omit<ActionV2, "type" | "variant">, "id">
): ActionV2 {
export function createAction(
definition: Optional<Omit<Action, "type" | "variant">, "id">
): Action {
return {
...definition,
type: "action",
@@ -206,9 +52,9 @@ export function createActionV2(
};
}
export function createInternalLinkActionV2(
definition: Optional<Omit<InternalLinkActionV2, "type" | "variant">, "id">
): InternalLinkActionV2 {
export function createInternalLinkAction(
definition: Optional<Omit<InternalLinkAction, "type" | "variant">, "id">
): InternalLinkAction {
return {
...definition,
type: "action",
@@ -217,9 +63,9 @@ export function createInternalLinkActionV2(
};
}
export function createExternalLinkActionV2(
definition: Optional<Omit<ExternalLinkActionV2, "type" | "variant">, "id">
): ExternalLinkActionV2 {
export function createExternalLinkAction(
definition: Optional<Omit<ExternalLinkAction, "type" | "variant">, "id">
): ExternalLinkAction {
return {
...definition,
type: "action",
@@ -228,9 +74,9 @@ export function createExternalLinkActionV2(
};
}
export function createActionV2WithChildren(
definition: Optional<Omit<ActionV2WithChildren, "type" | "variant">, "id">
): ActionV2WithChildren {
export function createActionWithChildren(
definition: Optional<Omit<ActionWithChildren, "type" | "variant">, "id">
): ActionWithChildren {
return {
...definition,
type: "action",
@@ -239,9 +85,9 @@ export function createActionV2WithChildren(
};
}
export function createActionV2Group(
definition: Omit<ActionV2Group, "type">
): ActionV2Group {
export function createActionGroup(
definition: Omit<ActionGroup, "type">
): ActionGroup {
return {
...definition,
type: "action_group",
@@ -249,8 +95,8 @@ export function createActionV2Group(
}
export function createRootMenuAction(
actions: (ActionV2Variant | ActionV2Group | TActionV2Separator)[]
): ActionV2WithChildren {
actions: (ActionVariant | ActionGroup | TActionSeparator)[]
): ActionWithChildren {
return {
id: uuidv4(),
type: "action",
@@ -261,8 +107,8 @@ export function createRootMenuAction(
};
}
export function actionV2ToMenuItem(
action: ActionV2Variant | ActionV2Group | TActionV2Separator,
export function actionToMenuItem(
action: ActionVariant | ActionGroup | TActionSeparator,
context: ActionContext
): MenuItem {
switch (action.type) {
@@ -286,7 +132,7 @@ export function actionV2ToMenuItem(
tooltip: resolve<React.ReactChild>(action.tooltip, context),
selected: resolve<boolean>(action.selected, context),
dangerous: action.dangerous,
onClick: () => performActionV2(action, context),
onClick: () => performAction(action, context),
};
case "internal_link": {
@@ -315,10 +161,10 @@ export function actionV2ToMenuItem(
case "action_with_children": {
const children = resolve<
(ActionV2Variant | ActionV2Group | TActionV2Separator)[]
(ActionVariant | ActionGroup | TActionSeparator)[]
>(action.children, context);
const subMenuItems = children.map((a) =>
actionV2ToMenuItem(a, context)
actionToMenuItem(a, context)
);
return {
type: "submenu",
@@ -337,7 +183,7 @@ export function actionV2ToMenuItem(
case "action_group": {
const groupItems = action.actions.map((a) =>
actionV2ToMenuItem(a, context)
actionToMenuItem(a, context)
);
return {
type: "group",
@@ -352,8 +198,8 @@ export function actionV2ToMenuItem(
}
}
export function actionV2ToKBar(
action: ActionV2Variant,
export function actionToKBar(
action: ActionVariant,
context: ActionContext
): KbarAction[] {
const visible = resolve<boolean>(action.visible, context);
@@ -385,18 +231,18 @@ export function actionV2ToKBar(
shortcut: action.shortcut,
icon,
priority,
perform: () => performActionV2(action, context),
perform: () => performAction(action, context),
},
];
}
case "action_with_children": {
const resolvedChildren = resolve<ActionV2Variant[]>(
const resolvedChildren = resolve<ActionVariant[]>(
action.children,
context
);
const children = resolvedChildren
.map((a) => actionV2ToKBar(a, context))
.map((a) => actionToKBar(a, context))
.flat()
.filter(Boolean);
@@ -422,8 +268,8 @@ export function actionV2ToKBar(
}
}
export async function performActionV2(
action: Exclude<ActionV2Variant, ActionV2WithChildren>,
export async function performAction(
action: Exclude<ActionVariant, ActionWithChildren>,
context: ActionContext
) {
const perform =
+2
View File
@@ -38,6 +38,8 @@ export const NotificationSection = ({ t }: ActionContext) => t("Notification");
export const GroupSection = ({ t }: ActionContext) => t("Groups");
export const EmojiSecion = ({ t }: ActionContext) => t("Emoji");
export const UserSection = ({ t }: ActionContext) => t("People");
UserSection.priority = 0.5;
+18 -28
View File
@@ -1,14 +1,10 @@
/* oxlint-disable react/prop-types */
import * as React from "react";
import Tooltip, { Props as TooltipProps } from "~/components/Tooltip";
import { performAction, performActionV2, resolve } from "~/actions";
import { performAction, resolve } from "~/actions";
import useIsMounted from "~/hooks/useIsMounted";
import {
Action,
ActionContext,
ActionV2Variant,
ActionV2WithChildren,
} from "~/types";
import useActionContext from "~/hooks/useActionContext";
import { ActionVariant, ActionWithChildren } from "~/types";
export type Props = React.HTMLAttributes<HTMLButtonElement> & {
/** Show the button in a disabled state */
@@ -16,9 +12,7 @@ export type Props = React.HTMLAttributes<HTMLButtonElement> & {
/** Hide the button entirely if action is not applicable */
hideOnActionDisabled?: boolean;
/** Action to use on button */
action?: Action | Exclude<ActionV2Variant, ActionV2WithChildren>;
/** Context of action, must be provided with action */
context?: ActionContext;
action?: Exclude<ActionVariant, ActionWithChildren>;
/** If tooltip props are provided the button will be wrapped in a tooltip */
tooltip?: Omit<TooltipProps, "children">;
};
@@ -28,34 +22,33 @@ export type Props = React.HTMLAttributes<HTMLButtonElement> & {
*/
const ActionButton = React.forwardRef<HTMLButtonElement, Props>(
function _ActionButton(
{ action, context, tooltip, hideOnActionDisabled, ...rest }: Props,
{ action, tooltip, hideOnActionDisabled, ...rest }: Props,
ref: React.Ref<HTMLButtonElement>
) {
const actionContext = useActionContext({
isButton: true,
});
const isMounted = useIsMounted();
const [executing, setExecuting] = React.useState(false);
const disabled = rest.disabled;
if (action && !context) {
throw new Error("Context must be provided with action");
}
if (!context || !action) {
if (!actionContext || !action) {
return <button {...rest} ref={ref} />;
}
const actionContext = { ...context, isButton: true };
const actionIsDisabled =
action.visible && !resolve<boolean>(action.visible, actionContext);
if (
action.visible &&
!resolve<boolean>(action.visible, actionContext) &&
hideOnActionDisabled
) {
if (actionIsDisabled && hideOnActionDisabled) {
return null;
}
const disabled = rest.disabled || actionIsDisabled;
const label =
typeof action.name === "function"
rest["aria-label"] ??
(typeof action.name === "function"
? action.name(actionContext)
: action.name;
: action.name);
const button = (
<button
@@ -68,10 +61,7 @@ const ActionButton = React.forwardRef<HTMLButtonElement, Props>(
? (ev) => {
ev.preventDefault();
ev.stopPropagation();
const response =
"variant" in action
? performActionV2(action, actionContext)
: performAction(action, actionContext);
const response = performAction(action, actionContext);
if (response?.finally) {
setExecuting(true);
void response.finally(
+1 -2
View File
@@ -6,7 +6,6 @@ import Flex from "~/components/Flex";
export const Action = styled(Flex)`
justify-content: center;
align-items: center;
padding: 0 0 0 12px;
height: 32px;
font-size: 15px;
flex-shrink: 0;
@@ -18,7 +17,6 @@ export const Action = styled(Flex)`
export const Separator = styled.div`
flex-shrink: 0;
margin-left: 12px;
width: 1px;
height: 28px;
background: ${s("divider")};
@@ -33,6 +31,7 @@ const Actions = styled(Flex)`
background: ${s("background")};
padding: 12px;
backdrop-filter: blur(20px);
gap: 12px;
@media print {
display: none;
+9 -13
View File
@@ -13,8 +13,6 @@ import ErrorSuspended from "~/scenes/Errors/ErrorSuspended";
import Layout from "~/components/Layout";
import RegisterKeyDown from "~/components/RegisterKeyDown";
import Sidebar from "~/components/Sidebar";
import SidebarRight from "~/components/Sidebar/Right";
import SettingsSidebar from "~/components/Sidebar/Settings";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import { usePostLoginPath } from "~/hooks/useLastVisitedPath";
import usePolicy from "~/hooks/usePolicy";
@@ -31,6 +29,7 @@ import {
import { DocumentContextProvider } from "./DocumentContext";
import Fade from "./Fade";
import { PortalContext } from "./Portal";
import CommandBar from "./CommandBar";
const DocumentComments = lazyWithRetry(
() => import("~/scenes/Document/components/Comments")
@@ -38,8 +37,9 @@ const DocumentComments = lazyWithRetry(
const DocumentHistory = lazyWithRetry(
() => import("~/scenes/Document/components/History")
);
const CommandBar = lazyWithRetry(() => import("~/components/CommandBar"));
const SettingsSidebar = lazyWithRetry(
() => import("~/components/Sidebar/Settings")
);
type Props = {
children?: React.ReactNode;
@@ -109,12 +109,10 @@ const AuthenticatedLayout: React.FC = ({ children }: Props) => {
>
{(showHistory || showComments) && (
<Route path={`/doc/${slug}`}>
<SidebarRight>
<React.Suspense fallback={null}>
{showHistory && <DocumentHistory />}
{showComments && <DocumentComments />}
</React.Suspense>
</SidebarRight>
<React.Suspense fallback={null}>
{showHistory && <DocumentHistory />}
{showComments && <DocumentComments />}
</React.Suspense>
</Route>
)}
</AnimatePresence>
@@ -133,9 +131,7 @@ const AuthenticatedLayout: React.FC = ({ children }: Props) => {
<RegisterKeyDown trigger="t" handler={goToSearch} />
<RegisterKeyDown trigger="/" handler={goToSearch} />
{children}
<React.Suspense fallback={null}>
<CommandBar />
</React.Suspense>
<CommandBar />
</Layout>
</PortalContext.Provider>
</DocumentContextProvider>
+15 -2
View File
@@ -2,6 +2,7 @@ import * as React from "react";
import styled from "styled-components";
import useBoolean from "~/hooks/useBoolean";
import Initials from "./Initials";
import Tooltip from "../Tooltip";
export enum AvatarSize {
Small = 16,
@@ -22,6 +23,7 @@ export interface IAvatar {
avatarUrl: string | null;
color?: string;
initial?: string;
name?: string;
id?: string;
}
@@ -42,6 +44,8 @@ type Props = {
className?: string;
/** Optional style */
style?: React.CSSProperties;
/** Whether to show a tooltip */
showTooltip?: boolean;
};
function Avatar(props: Props) {
@@ -50,12 +54,15 @@ function Avatar(props: Props) {
style,
variant = AvatarVariant.Round,
className,
showTooltip,
...rest
} = props;
const src = props.src || model?.avatarUrl;
const [error, handleError] = useBoolean(false);
const initial =
model?.initial || (model?.name ? model.name[0] : "").toUpperCase();
return (
const content = (
<Relative
style={style}
$variant={variant}
@@ -66,13 +73,19 @@ function Avatar(props: Props) {
<Image onError={handleError} src={src} {...rest} />
) : model ? (
<Initials color={model.color} {...rest}>
{model.initial}
{initial}
</Initials>
) : (
<Initials {...rest} />
)}
</Relative>
);
return showTooltip ? (
<Tooltip content={props.alt || model?.name || ""}>{content}</Tooltip>
) : (
content
);
}
Avatar.defaultProps = {
+4 -1
View File
@@ -25,6 +25,8 @@ type Props = {
onClick?: React.MouseEventHandler<HTMLImageElement>;
/** Size of the avatar, defaults to AvatarSize.Large */
size?: AvatarSize;
/** Optional alt text for the avatar image */
alt?: string;
/** Optional inline styles to apply to the avatar wrapper */
style?: React.CSSProperties;
};
@@ -53,6 +55,7 @@ function AvatarWithPresence({
isCurrentUser,
size = AvatarSize.Large,
style,
alt,
}: Props) {
const { t } = useTranslation();
const status = isPresent
@@ -83,7 +86,7 @@ function AvatarWithPresence({
$color={user.color}
style={style}
>
<Avatar model={user} onClick={onClick} size={size} />
<Avatar model={user} onClick={onClick} size={size} alt={alt} />
</AvatarPresence>
</Tooltip>
</>
+1
View File
@@ -26,6 +26,7 @@ export function GroupAvatar({
return (
<Squircle color={color ?? theme.text} size={size} className={className}>
<GroupIcon
data-fixed-color
color={backgroundColor ?? theme.background}
size={size * 0.75}
/>
+2 -1
View File
@@ -1,3 +1,4 @@
import * as React from "react";
import styled from "styled-components";
import { depths, s } from "@shared/styles";
import env from "~/env";
@@ -44,4 +45,4 @@ const Link = styled.a`
}
`;
export default Branding;
export default React.memo(Branding);
+8 -11
View File
@@ -6,17 +6,17 @@ import { s, ellipsis } from "@shared/styles";
import Flex from "~/components/Flex";
import BreadcrumbMenu from "~/menus/BreadcrumbMenu";
import { undraggableOnDesktop } from "~/styles";
import { InternalLinkActionV2, MenuInternalLink } from "~/types";
import { actionV2ToMenuItem } from "~/actions";
import { InternalLinkAction, MenuInternalLink } from "~/types";
import { actionToMenuItem } from "~/actions";
import useActionContext from "~/hooks/useActionContext";
import { useComputed } from "~/hooks/useComputed";
type TopLevelAction =
| InternalLinkActionV2
| { type: "menu"; actions: InternalLinkActionV2[] };
| InternalLinkAction
| { type: "menu"; actions: InternalLinkAction[] };
type Props = React.PropsWithChildren<{
actions: InternalLinkActionV2[];
actions: InternalLinkAction[];
max?: number;
highlightFirstItem?: boolean;
}>;
@@ -25,7 +25,7 @@ function Breadcrumb(
{ actions, highlightFirstItem, children, max = 2 }: Props,
ref: React.RefObject<HTMLDivElement> | null
) {
const actionContext = useActionContext({ isContextMenu: true });
const actionContext = useActionContext({ isMenu: true });
const visibleActions = useComputed(
() =>
@@ -46,7 +46,7 @@ function Breadcrumb(
const menuActions = topLevelActions.splice(
halfMax,
totalVisibleActions - max
) as InternalLinkActionV2[];
) as InternalLinkAction[];
topLevelActions.splice(halfMax, 0, {
type: "menu",
@@ -60,10 +60,7 @@ function Breadcrumb(
return <BreadcrumbMenu key="menu" actions={action.actions} />;
}
const item = actionV2ToMenuItem(
action,
actionContext
) as MenuInternalLink;
const item = actionToMenuItem(action, actionContext) as MenuInternalLink;
return (
<>
+108
View File
@@ -0,0 +1,108 @@
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { depths, s } from "@shared/styles";
import {
MenuHeader,
MenuSeparator,
} from "~/components/primitives/components/Menu";
import { Portal } from "~/components/Portal";
import { toMobileMenuItems } from "~/components/Menu/transformer";
import { actionToMenuItem } from "~/actions";
import { useBulkDocumentMenuAction } from "~/hooks/useBulkDocumentMenuAction";
import useActionContext from "~/hooks/useActionContext";
import useStores from "~/hooks/useStores";
import { ActionVariant } from "~/types";
import NudeButton from "./NudeButton";
import { CrossIcon } from "outline-icons";
function BulkSelectionToolbar() {
const { t } = useTranslation();
const { documents, ui } = useStores();
const selectedCount = documents.selectedDocumentIds.length;
const selectedDocuments = documents.selectedDocuments;
const sidebarWidth = ui.sidebarWidth;
const handleClearSelection = React.useCallback(() => {
documents.clearSelection();
}, [documents]);
const rootAction = useBulkDocumentMenuAction({
documents: selectedDocuments,
});
const actionContext = useActionContext({
isMenu: true,
});
const menuItems = React.useMemo(() => {
if (!rootAction.children || selectedCount === 0) {
return [];
}
return (rootAction.children as ActionVariant[]).map((childAction) =>
actionToMenuItem(childAction, actionContext)
);
}, [rootAction.children, selectedCount, actionContext]);
const content = toMobileMenuItems(menuItems, handleClearSelection, () => {});
if (selectedCount === 0) {
return null;
}
return (
<Portal>
<Wrapper $sidebarWidth={sidebarWidth}>
<MenuContainer>
<Header>
<MenuHeader>
{t("{{ count }} selected", { count: selectedCount })}
</MenuHeader>
<ClearButton
onClick={handleClearSelection}
tooltip={{
content: t("Clear selection"),
}}
>
<CrossIcon size={18} />
</ClearButton>
</Header>
<MenuSeparator />
{content}
</MenuContainer>
</Wrapper>
</Portal>
);
}
const ClearButton = styled(NudeButton)`
&:hover {
color: ${s("text")};
background: ${s("sidebarControlHoverBackground")};
}
`;
const Header = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
`;
const Wrapper = styled.div<{ $sidebarWidth: number }>`
position: fixed;
bottom: 24px;
left: ${(props) => props.$sidebarWidth + 16}px;
z-index: ${depths.menu};
`;
const MenuContainer = styled.div`
min-width: 180px;
background: ${s("menuBackground")};
box-shadow: ${s("menuShadow")};
border-radius: 6px;
padding: 6px;
`;
export default observer(BulkSelectionToolbar);
+4
View File
@@ -34,6 +34,7 @@ const RealButton = styled(ActionButton)<RealProps>`
cursor: var(--pointer);
user-select: none;
appearance: none !important;
transition: background 200ms ease-out;
${undraggableOnDesktop()}
&::-moz-focus-inner {
@@ -44,6 +45,7 @@ const RealButton = styled(ActionButton)<RealProps>`
&:hover:not(:disabled),
&[aria-expanded="true"] {
background: ${(props) => darken(0.05, props.theme.accent)};
transition: background 0s;
}
&:disabled {
@@ -78,6 +80,7 @@ const RealButton = styled(ActionButton)<RealProps>`
box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 2px, ${
props.theme.buttonNeutralBorder
} 0 0 0 1px inset;
transition: background 0s;
}
&:focus-visible {
@@ -103,6 +106,7 @@ const RealButton = styled(ActionButton)<RealProps>`
&:hover:not(:disabled),
&[aria-expanded="true"] {
background: ${darken(0.05, props.theme.danger)};
transition: background 0s;
}
&:disabled {
+2 -1
View File
@@ -125,13 +125,14 @@ function Collaborators(props: Props) {
return (
<AvatarWithPresence
key={collaborator.id}
{...rest}
key={collaborator.id}
user={collaborator}
isPresent={isPresent}
isEditing={isEditing}
isObserving={isObserving}
isCurrentUser={currentUserId === collaborator.id}
alt={t("Avatar of {{ name }}", { name: collaborator.name })}
onClick={
isObservable
? handleAvatarClick(
+7 -1
View File
@@ -67,7 +67,13 @@ export const CollectionForm = observer(function CollectionForm_({
const [hasOpenedIconPicker, setHasOpenedIconPicker] = useBoolean(false);
const iconColor = useIconColor(collection);
const fallbackIcon = <Icon value="collection" color={iconColor} />;
const fallbackIcon = (
<Icon
value="collection"
initial={collection?.initial ?? "?"}
color={iconColor}
/>
);
const {
register,
+3 -3
View File
@@ -5,7 +5,7 @@ import Collection from "~/models/Collection";
import CollectionIcon from "~/components/Icons/CollectionIcon";
import { archivePath, collectionPath } from "~/utils/routeHelpers";
import Breadcrumb from "./Breadcrumb";
import { createInternalLinkActionV2 } from "~/actions";
import { createInternalLinkAction } from "~/actions";
import { ActiveCollectionSection } from "~/actions/sections";
type Props = {
@@ -17,14 +17,14 @@ export const CollectionBreadcrumb: React.FC<Props> = ({ collection }) => {
const actions = React.useMemo(
() => [
createInternalLinkActionV2({
createInternalLinkAction({
name: t("Archive"),
section: ActiveCollectionSection,
icon: <ArchiveIcon />,
visible: collection.isArchived,
to: archivePath(),
}),
createInternalLinkActionV2({
createInternalLinkAction({
name: collection.name,
section: ActiveCollectionSection,
icon: <CollectionIcon collection={collection} expanded />,
+63
View File
@@ -0,0 +1,63 @@
import * as React from "react";
import styled from "styled-components";
import NudeButton from "./NudeButton";
import { hover, s } from "@shared/styles";
type Props = React.HTMLAttributes<HTMLButtonElement> & {
/** The current color value in hex format. If no color is passed a radial gradient will be shown */
color?: string;
/** Whether the button is currently active/selected */
active?: boolean;
/** The size of the button in pixels */
size?: number;
};
export const ColorButton = React.forwardRef(
(
{ color, active = false, size = 24, ...rest }: Props,
ref: React.Ref<HTMLButtonElement>
) => (
<ColorButtonInternal
$active={active}
$size={size}
{...rest}
style={{ "--color": color, ...rest.style } as React.CSSProperties}
ref={ref}
>
<Selected />
</ColorButtonInternal>
)
);
const Selected = styled.span`
width: 10px;
height: 5px;
border-left: 2px solid white;
border-bottom: 2px solid white;
transform: translateY(-25%) rotate(-45deg);
`;
const ColorButtonInternal = styled(NudeButton)<{
$active: boolean;
$size: number;
}>`
display: inline-flex;
justify-content: center;
align-items: center;
width: ${({ $size }) => $size}px;
height: ${({ $size }) => $size}px;
border-radius: 50%;
background: var(
--color,
linear-gradient(135deg, #ff5858 0%, #fbcc34 50%, #00c6ff 100%)
);
&: ${hover} {
outline: 2px solid ${s("menuBackground")} !important;
box-shadow: 0px 0px 3px 3px var(--color);
}
& ${Selected} {
display: ${({ $active }) => ($active ? "block" : "none")};
}
`;
@@ -1,7 +1,7 @@
import { DocumentIcon } from "outline-icons";
import { useMemo } from "react";
import Icon from "@shared/components/Icon";
import { createAction } from "~/actions";
import { createInternalLinkAction } from "~/actions";
import { RecentSection } from "~/actions/sections";
import useStores from "~/hooks/useStores";
import { documentPath } from "~/utils/routeHelpers";
@@ -15,12 +15,16 @@ const useRecentDocumentActions = (count = 6) => {
.filter((document) => document.id !== ui.activeDocumentId)
.slice(0, count)
.map((item) =>
createAction({
createInternalLinkAction({
name: item.titleWithDefault,
analyticsName: "Recently viewed document",
section: RecentSection,
icon: item.icon ? (
<Icon value={item.icon} color={item.color ?? undefined} />
<Icon
value={item.icon}
initial={item.initial}
color={item.color ?? undefined}
/>
) : (
<DocumentIcon />
),
@@ -1,6 +1,6 @@
import { SettingsIcon } from "outline-icons";
import { useMemo } from "react";
import { createAction } from "~/actions";
import { createActionWithChildren, createInternalLinkAction } from "~/actions";
import { NavigationSection } from "~/actions/sections";
import useSettingsConfig from "~/hooks/useSettingsConfig";
@@ -10,20 +10,20 @@ const useSettingsAction = () => {
() =>
config.map((item) => {
const Icon = item.icon;
return {
return createInternalLinkAction({
id: item.path,
name: item.name,
icon: <Icon />,
section: NavigationSection,
to: item.path,
};
});
}),
[config]
);
const navigateToSettings = useMemo(
() =>
createAction({
createActionWithChildren({
id: "settings",
name: ({ t }) => t("Settings"),
section: NavigationSection,
@@ -1,14 +1,13 @@
import { NewDocumentIcon, ShapesIcon } from "outline-icons";
import { useEffect, useMemo } from "react";
import Icon from "@shared/components/Icon";
import { createAction } from "~/actions";
import { createActionWithChildren, createInternalLinkAction } from "~/actions";
import {
ActiveCollectionSection,
DocumentSection,
TeamSection,
} from "~/actions/sections";
import useStores from "~/hooks/useStores";
import history from "~/utils/history";
import { newDocumentPath } from "~/utils/routeHelpers";
const useTemplatesAction = () => {
@@ -21,14 +20,18 @@ const useTemplatesAction = () => {
const actions = useMemo(
() =>
documents.templatesAlphabetical.map((template) =>
createAction({
createInternalLinkAction({
name: template.titleWithDefault,
analyticsName: "New document",
section: template.isWorkspaceTemplate
? TeamSection
: ActiveCollectionSection,
icon: template.icon ? (
<Icon value={template.icon} color={template.color ?? undefined} />
<Icon
value={template.icon}
initial={template.initial}
color={template.color ?? undefined}
/>
) : (
<NewDocumentIcon />
),
@@ -47,15 +50,20 @@ const useTemplatesAction = () => {
template.isWorkspaceTemplate
);
},
perform: ({ activeCollectionId, sidebarContext }) =>
history.push(
newDocumentPath(template.collectionId ?? activeCollectionId, {
templateId: template.id,
}),
to: ({ activeCollectionId, sidebarContext }) => {
const [pathname, search] = newDocumentPath(
template.collectionId ?? activeCollectionId,
{
sidebarContext,
templateId: template.id,
}
),
).split("?");
return {
pathname,
search,
state: { sidebarContext },
};
},
})
),
[documents.templatesAlphabetical]
@@ -63,7 +71,7 @@ const useTemplatesAction = () => {
const newFromTemplate = useMemo(
() =>
createAction({
createActionWithChildren({
id: "templates",
name: ({ t }) => t("New from template"),
placeholder: ({ t }) => t("Choose a template"),
@@ -78,7 +86,7 @@ const useTemplatesAction = () => {
stores.policies.abilities(currentTeamId).createDocument
);
},
children: () => actions,
children: actions,
}),
[actions]
);
+3 -2
View File
@@ -143,13 +143,14 @@ const ContentEditable = React.forwardRef(function _ContentEditable(
},
[]
);
const contentEditable = !disabled && !readOnly;
return (
<div className={className} dir={dir} onClick={onClick} tabIndex={-1}>
{children}
<Content
ref={contentRef}
contentEditable={!disabled && !readOnly}
contentEditable={contentEditable}
onInput={wrappedEvent(onInput)}
onFocus={wrappedEvent(onFocus)}
onBlur={wrappedEvent(onBlur)}
@@ -157,7 +158,7 @@ const ContentEditable = React.forwardRef(function _ContentEditable(
onPaste={handlePaste}
data-placeholder={placeholder}
suppressContentEditableWarning
role="textbox"
role={contentEditable ? "textbox" : undefined}
{...rest}
>
{innerValue}
-13
View File
@@ -1,13 +0,0 @@
import styled from "styled-components";
import { s } from "@shared/styles";
const Header = styled.h3`
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
color: ${s("sidebarText")};
letter-spacing: 0.04em;
margin: 1em 12px 0.5em;
`;
export default Header;
@@ -1,13 +0,0 @@
import styled from "styled-components";
import { s } from "@shared/styles";
const MenuIconWrapper = styled.span`
width: 24px;
height: 24px;
margin-right: 6px;
margin-left: -4px;
color: ${s("textSecondary")};
flex-shrink: 0;
`;
export default MenuIconWrapper;
-217
View File
@@ -1,217 +0,0 @@
import { LocationDescriptor } from "history";
import { CheckmarkIcon } from "outline-icons";
import { ellipsis, transparentize } from "polished";
import * as React from "react";
import { mergeRefs } from "react-merge-refs";
import { MenuItem as BaseMenuItem } from "reakit/Menu";
import styled, { css } from "styled-components";
import breakpoint from "styled-components-breakpoint";
import { s } from "@shared/styles";
import Text from "../Text";
import MenuIconWrapper from "./MenuIconWrapper";
type Props = {
id?: string;
onClick?: (event: React.MouseEvent) => void | Promise<void>;
onPointerMove?: (event: React.MouseEvent) => void | Promise<void>;
active?: boolean;
selected?: boolean;
disabled?: boolean;
dangerous?: boolean;
to?: LocationDescriptor;
href?: string;
target?: string;
as?: string | React.ComponentType<any>;
hide?: () => void;
level?: number;
icon?: React.ReactNode;
children?: React.ReactNode;
ref?: React.LegacyRef<HTMLButtonElement> | undefined;
};
const MenuItem = (
{
onClick,
onPointerMove,
children,
active,
selected,
disabled,
as,
hide,
icon,
...rest
}: Props,
ref: React.Ref<HTMLAnchorElement>
) => {
const content = React.useCallback(
(props) => {
// Preventing default mousedown otherwise menu items do not work in Firefox,
// which triggers the hideOnClickOutside handler first via mousedown hiding
// and un-rendering the menu contents.
const preventDefault = (ev: React.MouseEvent) => {
ev.preventDefault();
ev.stopPropagation();
};
const handleClick = async (ev: React.MouseEvent) => {
hide?.();
if (onClick) {
preventDefault(ev);
await onClick(ev);
}
};
return (
<MenuAnchor
{...props}
$active={active}
as={onClick ? "button" : as}
onClick={handleClick}
onPointerDown={preventDefault}
onMouseDown={preventDefault}
ref={mergeRefs([
ref,
props.ref as React.RefObject<HTMLAnchorElement>,
])}
>
{selected !== undefined && (
<SelectedWrapper aria-hidden>
{selected ? <CheckmarkIcon /> : <Spacer />}
</SelectedWrapper>
)}
{icon && <MenuIconWrapper aria-hidden>{icon}</MenuIconWrapper>}
<Title>{children}</Title>
</MenuAnchor>
);
},
[active, as, hide, icon, onClick, ref, children, selected]
);
return (
<BaseMenuItem
onClick={disabled ? undefined : onClick}
onPointerMove={disabled ? undefined : onPointerMove}
disabled={disabled}
hide={hide}
{...rest}
>
{content}
</BaseMenuItem>
);
};
const Spacer = styled.svg`
width: 24px;
height: 24px;
flex-shrink: 0;
`;
const Title = styled.div`
${ellipsis()}
flex-grow: 1;
display: flex;
align-items: center;
gap: 8px;
`;
type MenuAnchorProps = {
level?: number;
disabled?: boolean;
dangerous?: boolean;
disclosure?: boolean;
$active?: boolean;
};
export const MenuAnchorCSS = css<MenuAnchorProps>`
display: flex;
margin: 0;
border: 0;
padding: 12px;
border-radius: 4px;
padding-left: ${(props) => 12 + (props.level || 0) * 10}px;
width: 100%;
min-height: 32px;
background: none;
color: ${(props) =>
props.disabled ? props.theme.textTertiary : props.theme.textSecondary};
justify-content: left;
align-items: center;
font-size: 16px;
cursor: default;
user-select: none;
white-space: nowrap;
position: relative;
svg {
flex-shrink: 0;
opacity: ${(props) => (props.disabled ? ".5" : 1)};
}
${(props) => props.disabled && "pointer-events: none;"}
${(props) =>
props.$active === undefined &&
!props.disabled &&
`
@media (hover: hover) {
&:hover,
&:focus,
&:focus-visible {
color: ${props.theme.accentText};
background: ${props.dangerous ? props.theme.danger : props.theme.accent};
outline-color: ${
props.dangerous ? props.theme.danger : props.theme.accent
};
box-shadow: none;
cursor: var(--pointer);
svg {
color: ${props.theme.accentText};
fill: ${props.theme.accentText};
}
${Text} {
color: ${transparentize(0.5, props.theme.accentText)};
}
}
}
`}
${(props) =>
props.$active &&
!props.disabled &&
`
color: ${props.theme.accentText};
background: ${props.dangerous ? props.theme.danger : props.theme.accent};
box-shadow: none;
cursor: var(--pointer);
svg {
fill: ${props.theme.accentText};
}
`}
${breakpoint("tablet")`
padding: 4px 12px;
padding-right: ${(props: MenuAnchorProps) =>
props.disclosure ? 32 : 12}px;
font-size: 14px;
`}
`;
export const MenuAnchor = styled.a`
${MenuAnchorCSS}
`;
const SelectedWrapper = styled.span`
width: 24px;
height: 24px;
margin-right: 4px;
margin-left: -8px;
flex-shrink: 0;
color: ${s("textSecondary")};
`;
export default React.forwardRef<HTMLAnchorElement, Props>(MenuItem);
@@ -1,20 +0,0 @@
import { MoreIcon } from "outline-icons";
import * as React from "react";
import { MenuButton } from "reakit/Menu";
import NudeButton from "~/components/NudeButton";
type Props = React.ComponentProps<typeof MenuButton> & {
className?: string;
};
export default function OverflowMenuButton({ className, ...rest }: Props) {
return (
<MenuButton {...rest}>
{(props) => (
<NudeButton className={className} {...props}>
<MoreIcon />
</NudeButton>
)}
</MenuButton>
);
}
-15
View File
@@ -1,15 +0,0 @@
import * as React from "react";
import { MenuSeparator } from "reakit/Menu";
import styled from "styled-components";
export default function Separator(rest: React.HTMLAttributes<HTMLHRElement>) {
return (
<MenuSeparator {...rest}>
{(props) => <HorizontalRule {...props} />}
</MenuSeparator>
);
}
const HorizontalRule = styled.hr`
margin: 6px 0;
`;
-264
View File
@@ -1,264 +0,0 @@
import { ExpandedIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import {
MenuButton,
MenuItem as BaseMenuItem,
MenuStateReturn,
} from "reakit/Menu";
import styled, { useTheme } from "styled-components";
import MenuIconWrapper from "~/components/ContextMenu/MenuIconWrapper";
import Flex from "~/components/Flex";
import { actionToMenuItem } from "~/actions";
import useActionContext from "~/hooks/useActionContext";
import { useMenuState } from "~/hooks/useMenuState";
import {
Action,
ActionContext,
MenuSeparator,
MenuHeading,
MenuItem as TMenuItem,
} from "~/types";
import Tooltip from "../Tooltip";
import Header from "./Header";
import MenuItem, { MenuAnchor } from "./MenuItem";
import MouseSafeArea from "./MouseSafeArea";
import Separator from "./Separator";
import ContextMenu from ".";
type Props = Omit<MenuStateReturn, "items"> & {
actions?: (Action | MenuSeparator | MenuHeading)[];
context?: Partial<ActionContext>;
items?: TMenuItem[];
showIcons?: boolean;
};
const Disclosure = styled(ExpandedIcon)`
transform: rotate(270deg);
position: absolute;
right: 8px;
`;
type SubMenuProps = MenuStateReturn & {
templateItems: TMenuItem[];
parentMenuState: Omit<MenuStateReturn, "items">;
title: React.ReactNode;
};
const SubMenu = React.forwardRef(function _Template(
{ templateItems, title, parentMenuState, ...rest }: SubMenuProps,
ref: React.LegacyRef<HTMLButtonElement>
) {
const { t } = useTranslation();
const theme = useTheme();
const menu = useMenuState({
parentId: parentMenuState.baseId,
});
return (
<>
<MenuButton ref={ref} {...menu} {...rest}>
{(props) => (
<MenuAnchor disclosure {...props}>
{title} <Disclosure color={theme.textTertiary} />
</MenuAnchor>
)}
</MenuButton>
<ContextMenu
{...menu}
aria-label={t("Submenu")}
onClick={parentMenuState.hide}
parentMenuState={parentMenuState}
>
<MouseSafeArea parentRef={menu.unstable_popoverRef} />
<Template {...menu} items={templateItems} />
</ContextMenu>
</>
);
});
export function filterTemplateItems(items: TMenuItem[]): TMenuItem[] {
return items
.filter((item) => item.visible !== false)
.reduce((acc, item) => {
// trim separator if the previous item was a separator
if (
item.type === "separator" &&
acc[acc.length - 1]?.type === "separator"
) {
return acc;
}
return [...acc, item];
}, [] as TMenuItem[])
.filter((item, index, arr) => {
if (
item.type === "separator" &&
(index === 0 || index === arr.length - 1)
) {
return false;
}
return true;
});
}
function Template({ items, actions, context, showIcons, ...menu }: Props) {
const ctx = useActionContext({
isContextMenu: true,
});
const templateItems = actions
? actions.map((item) =>
item.type === "separator" || item.type === "heading"
? item
: actionToMenuItem(item, ctx)
)
: items || [];
const filteredTemplates = filterTemplateItems(templateItems);
const iconIsPresentInAnyMenuItem = filteredTemplates.find(
(item) =>
item.type !== "separator" && item.type !== "heading" && !!item.icon
);
return (
<>
{filteredTemplates.map((item, index) => {
if (
iconIsPresentInAnyMenuItem &&
item.type !== "separator" &&
item.type !== "heading" &&
showIcons !== false
) {
item.icon = item.icon || <MenuIconWrapper aria-hidden />;
}
if (item.type === "route") {
return (
<MenuItem
as={Link}
id={`${item.title}-${index}`}
to={item.to}
key={`${item.type}-${item.title}-${index}`}
disabled={item.disabled}
selected={item.selected}
icon={showIcons !== false ? item.icon : undefined}
{...menu}
>
{item.title}
</MenuItem>
);
}
if (item.type === "link") {
return (
<MenuItem
id={`${item.title}-${index}`}
href={typeof item.href === "string" ? item.href : item.href.url}
key={`${item.type}-${item.title}-${index}`}
disabled={item.disabled}
selected={item.selected}
level={item.level}
target={
typeof item.href === "string" ? undefined : item.href.target
}
icon={showIcons !== false ? item.icon : undefined}
{...menu}
>
{item.title}
</MenuItem>
);
}
if (item.type === "button") {
const menuItem = (
<MenuItem
as="button"
id={`${item.title}-${index}`}
onClick={item.onClick}
disabled={item.disabled}
selected={item.selected}
dangerous={item.dangerous}
key={`${item.type}-${item.title}-${index}`}
icon={showIcons !== false ? item.icon : undefined}
{...menu}
>
{item.title}
</MenuItem>
);
return item.tooltip ? (
<Tooltip
content={item.tooltip}
placement={"bottom"}
key={`tooltip-${item.title}-${index}`}
>
<div>{menuItem}</div>
</Tooltip>
) : (
<React.Fragment key={`${item.type}-${item.title}-${index}`}>
{menuItem}
</React.Fragment>
);
}
if (item.type === "submenu") {
// Skip rendering empty submenus
return item.items.length > 0 ? (
<BaseMenuItem
key={`${item.type}-${item.title}-${index}`}
as={SubMenu}
id={`${item.title}-${index}`}
templateItems={item.items}
parentMenuState={menu}
title={
<Title
title={item.title}
icon={showIcons !== false ? item.icon : undefined}
/>
}
{...menu}
/>
) : null;
}
if (item.type === "separator") {
return <Separator key={`separator-${index}`} />;
}
if (item.type === "heading") {
return (
<Header key={`heading-${item.title}-${index}`}>{item.title}</Header>
);
}
// This should never be reached for Reakit dropdown menu.
// Added for exhaustiveness check.
if (item.type === "group") {
return null;
}
const _exhaustiveCheck: never = item;
return _exhaustiveCheck;
})}
</>
);
}
function Title({
title,
icon,
}: {
title: React.ReactNode;
icon?: React.ReactNode;
}) {
return (
<Flex align="center">
{icon && <MenuIconWrapper aria-hidden>{icon}</MenuIconWrapper>}
{title}
</Flex>
);
}
export default React.memo<Props>(Template);
-317
View File
@@ -1,317 +0,0 @@
import { disableBodyScroll, enableBodyScroll } from "body-scroll-lock";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { Menu, MenuStateReturn } from "reakit/Menu";
import styled, { DefaultTheme } from "styled-components";
import breakpoint from "styled-components-breakpoint";
import { depths, s } from "@shared/styles";
import Scrollable from "~/components/Scrollable";
import useEventListener from "~/hooks/useEventListener";
import useMenuContext from "~/hooks/useMenuContext";
import useMenuHeight from "~/hooks/useMenuHeight";
import useMobile from "~/hooks/useMobile";
import usePrevious from "~/hooks/usePrevious";
import useStores from "~/hooks/useStores";
import useUnmount from "~/hooks/useUnmount";
import {
fadeIn,
fadeAndSlideUp,
fadeAndSlideDown,
mobileContextMenu,
} from "~/styles/animations";
export type Placement =
| "auto-start"
| "auto"
| "auto-end"
| "top-start"
| "top"
| "top-end"
| "right-start"
| "right"
| "right-end"
| "bottom-end"
| "bottom"
| "bottom-start"
| "left-end"
| "left"
| "left-start";
type Props = MenuStateReturn & {
"aria-label"?: string;
/** Reference to the rendered menu div element */
menuRef?: React.RefObject<HTMLDivElement>;
/** The parent menu state if this is a submenu. */
parentMenuState?: Omit<MenuStateReturn, "items">;
/** Called when the context menu is opened. */
onOpen?: () => void;
/** Called when the context menu is closed. */
onClose?: () => void;
/** Called when the context menu is clicked. */
onClick?: (ev: React.MouseEvent) => void;
/** The maximum width of the context menu. */
maxWidth?: number;
/** The minimum height of the context menu. */
minHeight?: number;
children?: React.ReactNode;
};
const ContextMenu: React.FC<Props> = ({
menuRef,
children,
onOpen,
onClose,
parentMenuState,
...rest
}: Props) => {
const previousVisible = usePrevious(rest.visible);
const { ui } = useStores();
const { t } = useTranslation();
const { setIsMenuOpen } = useMenuContext();
const isMobile = useMobile();
const isSubMenu = !!parentMenuState;
useUnmount(() => {
setIsMenuOpen(false);
});
React.useEffect(() => {
if (rest.visible && !previousVisible) {
onOpen?.();
if (!isSubMenu) {
setIsMenuOpen(true);
}
}
if (!rest.visible && previousVisible) {
onClose?.();
if (!isSubMenu) {
setIsMenuOpen(false);
}
}
}, [
onOpen,
onClose,
previousVisible,
rest.visible,
ui.sidebarCollapsed,
setIsMenuOpen,
isSubMenu,
t,
]);
// Perf win don't render anything until the menu has been opened
if (!rest.visible && !previousVisible) {
return null;
}
// sets the menu height based on the available space between the disclosure/
// trigger and the bottom of the window
return (
<>
<Menu
ref={menuRef}
hideOnClickOutside={!isMobile}
preventBodyScroll={false}
{...rest}
>
{(props) => (
<InnerContextMenu
// oxlint-disable-next-line @typescript-eslint/no-explicit-any
menuProps={props as any}
{...rest}
isSubMenu={isSubMenu}
>
{children}
</InnerContextMenu>
)}
</Menu>
</>
);
};
type InnerContextMenuProps = MenuStateReturn & {
isSubMenu: boolean;
menuProps: { style?: React.CSSProperties; placement: string };
children: React.ReactNode;
maxWidth?: number;
minHeight?: number;
};
/**
* Inner context menu allows deferring expensive window measurement hooks etc
* until the menu is actually opened.
*/
const InnerContextMenu = (props: InnerContextMenuProps) => {
const { menuProps } = props;
// kind of hacky, but this is an effective way of telling which way
// the menu will _actually_ be placed when taking into account screen
// positioning.
const topAnchor =
menuProps.style?.top === "0" || menuProps.style?.position === "fixed";
const rightAnchor = menuProps.placement === "bottom-end";
const backgroundRef = React.useRef<HTMLDivElement>(null);
const isMobile = useMobile();
const maxHeight = useMenuHeight({
visible: props.visible,
elementRef: props.unstable_disclosureRef,
});
// We must manually manage scroll lock for iOS support so that the scrollable
// element can be passed into body-scroll-lock. See:
// https://github.com/ariakit/ariakit/issues/469
React.useEffect(() => {
const scrollElement = backgroundRef.current;
if (props.visible && scrollElement && !props.isSubMenu) {
disableBodyScroll(scrollElement, {
reserveScrollBarGap: true,
});
}
return () => {
if (scrollElement && !props.isSubMenu) {
enableBodyScroll(scrollElement);
}
};
}, [props.isSubMenu, props.visible]);
useEventListener(
"animationstart",
(event) => {
if (event.target instanceof HTMLElement) {
const parent = event.target.parentElement;
if (parent) {
parent.style.pointerEvents = "none";
}
}
},
backgroundRef.current
);
useEventListener(
"animationend",
(event) => {
if (event.target instanceof HTMLElement) {
const parent = event.target.parentElement;
if (parent) {
parent.style.pointerEvents = "auto";
}
}
},
backgroundRef.current
);
const style =
topAnchor && !isMobile
? {
maxHeight,
}
: undefined;
return (
<>
{isMobile && (
<Backdrop
onClick={(ev) => {
ev.preventDefault();
ev.stopPropagation();
props.hide?.();
}}
/>
)}
<Position {...menuProps}>
<Background
dir="auto"
maxWidth={props.maxWidth}
minHeight={props.minHeight}
topAnchor={topAnchor}
rightAnchor={rightAnchor}
ref={backgroundRef}
hiddenScrollbars
style={style}
>
{props.visible || props.animating ? props.children : null}
</Background>
</Position>
</>
);
};
export default ContextMenu;
export const Backdrop = styled.div`
animation: ${fadeIn} 200ms ease-in-out;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: ${s("backdrop")};
z-index: ${depths.menu - 1};
`;
export const Position = styled.div`
position: absolute;
z-index: ${depths.menu};
// Note: pointer events are re-enabled after the animation ends, see event listeners above
pointer-events: none;
&:focus-visible {
transition-delay: 250ms;
transition-property: outline-width;
transition-duration: 0;
outline: none;
}
/*
* overrides make mobile-first coding style challenging
* so we explicitly define mobile breakpoint here
*/
${breakpoint("mobile", "tablet")`
position: fixed !important;
transform: none !important;
top: auto !important;
right: 8px !important;
bottom: 16px !important;
left: 8px !important;
`};
`;
type BackgroundProps = {
topAnchor?: boolean;
rightAnchor?: boolean;
maxWidth?: number;
minHeight?: number;
theme: DefaultTheme;
};
export const Background = styled(Scrollable)<BackgroundProps>`
animation: ${mobileContextMenu} 200ms ease;
transform-origin: 50% 100%;
max-width: 100%;
background: ${s("menuBackground")};
border-radius: 6px;
padding: 6px;
min-width: 180px;
min-height: ${(props) => props.minHeight || 44}px;
max-height: 75vh;
font-weight: normal;
@media print {
display: none;
}
${breakpoint("tablet")`
animation: ${(props: BackgroundProps) =>
props.topAnchor ? fadeAndSlideDown : fadeAndSlideUp} 200ms ease;
transform-origin: ${(props: BackgroundProps) =>
props.rightAnchor ? "75%" : "25%"} 0;
max-width: ${(props: BackgroundProps) => props.maxWidth ?? 276}px;
max-height: 100vh;
background: ${(props: BackgroundProps) => props.theme.menuBackground};
box-shadow: ${(props: BackgroundProps) => props.theme.menuShadow};
`};
`;
+9 -4
View File
@@ -1,7 +1,10 @@
import { observer } from "mobx-react";
import Guide from "~/components/Guide";
import Modal from "~/components/Modal";
import { Suspense } from "react";
import useStores from "~/hooks/useStores";
import lazyWithRetry from "~/utils/lazyWithRetry";
const Guide = lazyWithRetry(() => import("~/components/Guide"));
const Modal = lazyWithRetry(() => import("~/components/Modal"));
function Dialogs() {
const { dialogs } = useStores();
@@ -9,7 +12,7 @@ function Dialogs() {
const modals = [...modalStack];
return (
<>
<Suspense fallback={null}>
{guide ? (
<Guide
isOpen={guide.isOpen}
@@ -29,11 +32,13 @@ function Dialogs() {
}}
title={modal.title}
style={modal.style}
width={modal.width}
height={modal.height}
>
{modal.content}
</Modal>
))}
</>
</Suspense>
);
}
+13 -8
View File
@@ -12,7 +12,7 @@ import { useLocationSidebarContext } from "~/hooks/useLocationSidebarContext";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
import { archivePath, settingsPath, trashPath } from "~/utils/routeHelpers";
import { createInternalLinkActionV2 } from "~/actions";
import { createInternalLinkAction } from "~/actions";
import { ActiveDocumentSection } from "~/actions/sections";
type Props = {
@@ -53,28 +53,28 @@ function DocumentBreadcrumb(
}
const outputActions = [
createInternalLinkActionV2({
createInternalLinkAction({
name: t("Trash"),
section: ActiveDocumentSection,
icon: <TrashIcon />,
visible: document.isDeleted,
to: trashPath(),
}),
createInternalLinkActionV2({
createInternalLinkAction({
name: t("Archive"),
section: ActiveDocumentSection,
icon: <ArchiveIcon />,
visible: document.isArchived,
to: archivePath(),
}),
createInternalLinkActionV2({
createInternalLinkAction({
name: t("Templates"),
section: ActiveDocumentSection,
icon: <ShapesIcon />,
visible: document.template,
to: settingsPath("templates"),
}),
createInternalLinkActionV2({
createInternalLinkAction({
name: collection?.name,
section: ActiveDocumentSection,
icon: collection ? (
@@ -88,7 +88,7 @@ function DocumentBreadcrumb(
}
: "",
}),
createInternalLinkActionV2({
createInternalLinkAction({
name: t("Deleted Collection"),
section: ActiveDocumentSection,
visible: document.isCollectionDeleted,
@@ -96,10 +96,15 @@ function DocumentBreadcrumb(
}),
...path.map((node) => {
const title = node.title || t("Untitled");
return createInternalLinkActionV2({
return createInternalLinkAction({
name: node.icon ? (
<>
<StyledIcon value={node.icon} color={node.color} /> {title}
<StyledIcon
value={node.icon}
color={node.color}
initial={node.title.charAt(0).toUpperCase()}
/>{" "}
{title}
</>
) : (
title
+18 -25
View File
@@ -3,8 +3,8 @@ import { CSS } from "@dnd-kit/utilities";
import { subDays } from "date-fns";
import { m } from "framer-motion";
import { observer } from "mobx-react";
import { CloseIcon, DocumentIcon, ClockIcon, EyeIcon } from "outline-icons";
import { useRef, useCallback, useMemo } from "react";
import { CloseIcon, DocumentIcon, ClockIcon } from "outline-icons";
import { useRef, useCallback, Suspense } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import styled, { useTheme } from "styled-components";
@@ -19,10 +19,12 @@ import Flex from "~/components/Flex";
import NudeButton from "~/components/NudeButton";
import Time from "~/components/Time";
import useStores from "~/hooks/useStores";
import { useTextStats } from "~/hooks/useTextStats";
import CollectionIcon from "./Icons/CollectionIcon";
import Text from "./Text";
import Tooltip from "./Tooltip";
import lazyWithRetry from "~/utils/lazyWithRetry";
const ReadingTime = lazyWithRetry(() => import("./ReadingTime"));
type Props = {
/** The pin record */
@@ -76,6 +78,13 @@ function DocumentCard(props: Props) {
const isRecentlyUpdated =
new Date(document.updatedAt) > subDays(new Date(), 7);
const updatedAt = (
<>
<Clock size={18} />
<Time dateTime={document.updatedAt} addSuffix shorten />
</>
);
return (
<Reorderable
ref={setNodeRef}
@@ -150,12 +159,11 @@ function DocumentCard(props: Props) {
</Heading>
<DocumentMeta size="xsmall">
{isRecentlyUpdated ? (
<>
<Clock size={18} />
<Time dateTime={document.updatedAt} addSuffix shorten />
</>
updatedAt
) : (
<ReadingTime document={document} />
<Suspense fallback={updatedAt}>
<ReadingTime document={document} />
</Suspense>
)}
</DocumentMeta>
</div>
@@ -177,29 +185,14 @@ function DocumentCard(props: Props) {
);
}
const ReadingTime = ({ document }: { document: Document }) => {
const { t } = useTranslation();
const markdown = useMemo(() => document.toMarkdown(), [document]);
const stats = useTextStats(markdown);
return (
<>
<EyeIcon size={18} />
{t(`{{ minutes }}m read`, {
minutes: stats.total.readingTime,
})}
</>
);
};
const DocumentSquircle = ({
icon,
color,
initial,
color,
}: {
icon: string;
initial: string;
color?: string;
initial?: string;
}) => {
const theme = useTheme();
const iconType = determineIconType(icon)!;
+11
View File
@@ -11,9 +11,15 @@ class DocumentContext {
/** The editor instance for this document */
editor?: Editor;
/** The ID of the currently focused comment, or null if no comment is focused */
@observable
focusedCommentId: string | null = null;
/** Whether the editor has been initialized */
@observable
isEditorInitialized: boolean = false;
/** The headings in the document */
@observable
headings: Heading[] = [];
@@ -39,6 +45,11 @@ class DocumentContext {
this.isEditorInitialized = initialized;
};
@action
setFocusedCommentId = (commentId: string | null) => {
this.focusedCommentId = commentId;
};
@action
updateState = () => {
this.updateHeadings();
+28 -19
View File
@@ -3,6 +3,7 @@ import concat from "lodash/concat";
import difference from "lodash/difference";
import fill from "lodash/fill";
import filter from "lodash/filter";
import flatten from "lodash/flatten";
import includes from "lodash/includes";
import map from "lodash/map";
import { observer } from "mobx-react";
@@ -17,6 +18,7 @@ import breakpoint from "styled-components-breakpoint";
import Icon from "@shared/components/Icon";
import { NavigationNode } from "@shared/types";
import { isModKey } from "@shared/utils/keyboard";
import { ancestors, descendants, flattenTree } from "@shared/utils/tree";
import DocumentExplorerNode from "~/components/DocumentExplorerNode";
import DocumentExplorerSearchResult from "~/components/DocumentExplorerSearchResult";
import Flex from "~/components/Flex";
@@ -26,8 +28,6 @@ import InputSearch from "~/components/InputSearch";
import Text from "~/components/Text";
import useMobile from "~/hooks/useMobile";
import useStores from "~/hooks/useStores";
import { ancestors, descendants, flattenTree } from "~/utils/tree";
import flatten from "lodash/flatten";
type Props = {
/** Action taken upon submission of selected item, could be publish, move etc. */
@@ -49,8 +49,13 @@ function DocumentExplorer({ onSubmit, onSelect, items, defaultValue }: Props) {
const [searchTerm, setSearchTerm] = React.useState<string>();
const [selectedNode, selectNode] = React.useState<NavigationNode | null>(
() => {
const node =
defaultValue && items.find((item) => item.id === defaultValue);
if (!defaultValue) {
return null;
}
// Search through all nodes in the tree, not just top-level items
const allNodes = flatten(items.map(flattenTree));
const node = allNodes.find((item) => item.id === defaultValue);
return node || null;
}
);
@@ -59,7 +64,9 @@ function DocumentExplorer({ onSubmit, onSelect, items, defaultValue }: Props) {
const [activeNode, setActiveNode] = React.useState<number>(0);
const [expandedNodes, setExpandedNodes] = React.useState<string[]>(() => {
if (defaultValue) {
const node = items.find((item) => item.id === defaultValue);
// Search through all nodes in the tree, not just top-level items
const allNodes = flatten(items.map(flattenTree));
const node = allNodes.find((item) => item.id === defaultValue);
if (node) {
return ancestors(node).map((ancestorNode) => ancestorNode.id);
}
@@ -104,19 +111,6 @@ function DocumentExplorer({ onSubmit, onSelect, items, defaultValue }: Props) {
);
}, [items.length]);
React.useEffect(() => {
onSelect(selectedNode);
}, [selectedNode, onSelect]);
React.useEffect(() => {
if (defaultValue && selectedNode && listRef) {
const index = nodes.findIndex((node) => node.id === selectedNode.id);
if (index > 0) {
setTimeout(() => listRef.current?.scrollToItem(index, "center"), 50);
}
}
}, []);
function getNodes() {
function includeDescendants(item: NavigationNode): NavigationNode[] {
return expandedNodes.includes(item.id)
@@ -130,6 +124,19 @@ function DocumentExplorer({ onSubmit, onSelect, items, defaultValue }: Props) {
}
const nodes = getNodes();
React.useEffect(() => {
onSelect(selectedNode);
}, [selectedNode, onSelect]);
React.useEffect(() => {
if (defaultValue && selectedNode && listRef) {
const index = nodes.findIndex((node) => node.id === selectedNode.id);
if (index > 0) {
setTimeout(() => listRef.current?.scrollToItem(index, "center"), 50);
}
}
}, [defaultValue, selectedNode, nodes]);
const baseDepth = nodes.reduce(
(min, node) => (node.depth ? Math.min(min, node.depth) : min),
Infinity
@@ -261,7 +268,9 @@ function DocumentExplorer({ onSubmit, onSelect, items, defaultValue }: Props) {
title = doc?.title ?? node.title;
if (icon) {
renderedIcon = <Icon value={icon} color={color} />;
renderedIcon = (
<Icon value={icon} initial={node.title} color={color} />
);
} else if (doc?.isStarred) {
renderedIcon = <StarredIcon color={theme.yellow} />;
} else {
+105 -75
View File
@@ -25,6 +25,10 @@ import { useLocationSidebarContext } from "~/hooks/useLocationSidebarContext";
import DocumentMenu from "~/menus/DocumentMenu";
import { documentPath } from "~/utils/routeHelpers";
import { determineSidebarContext } from "./Sidebar/components/SidebarContext";
import { ActionContextProvider } from "~/hooks/useActionContext";
import { useDocumentMenuAction } from "~/hooks/useDocumentMenuAction";
import { ContextMenu } from "./Menu/ContextMenu";
import useStores from "~/hooks/useStores";
type Props = {
document: Document;
@@ -50,6 +54,7 @@ function DocumentListItem(
) {
const { t } = useTranslation();
const user = useCurrentUser();
const { userMemberships, groupMemberships } = useStores();
const locationSidebarContext = useLocationSidebarContext();
const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean();
@@ -78,87 +83,110 @@ function DocumentListItem(
!!document.title.toLowerCase().includes(highlight.toLowerCase());
const canStar = !document.isArchived && !document.isTemplate;
const isShared = !!(
userMemberships.getByDocumentId(document.id) ||
groupMemberships.getByDocumentId(document.id)
);
const sidebarContext = determineSidebarContext({
document,
user,
currentContext: locationSidebarContext,
});
return (
<DocumentLink
ref={itemRef}
dir={document.dir}
role="menuitem"
$isStarred={document.isStarred}
$menuOpen={menuOpen}
to={{
pathname: documentPath(document),
state: {
title: document.titleWithDefault,
sidebarContext,
},
}}
{...rest}
{...rovingTabIndex}
>
<Content>
<Heading dir={document.dir}>
{document.icon && (
<>
<Icon
value={document.icon}
color={document.color ?? undefined}
initial={document.initial}
/>
&nbsp;
</>
)}
<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>
const contextMenuAction = useDocumentMenuAction({ documentId: document.id });
{!queryIsInTitle && (
<ResultContext
text={context}
highlight={highlight ? SEARCH_RESULT_REGEX : undefined}
processResult={replaceResultMarks}
/>
)}
<DocumentMeta
document={document}
showCollection={showCollection}
showPublished={showPublished}
showParentDocuments={showParentDocuments}
showLastViewed
/>
</Content>
<Actions>
<DocumentMenu
document={document}
onOpen={handleMenuOpen}
onClose={handleMenuClose}
/>
</Actions>
</DocumentLink>
return (
<ActionContextProvider
value={{
activeDocumentId: document.id,
activeCollectionId:
!isShared && document.collectionId
? document.collectionId
: undefined,
}}
>
<ContextMenu
action={contextMenuAction}
ariaLabel={t("Document options")}
onOpen={handleMenuOpen}
onClose={handleMenuClose}
>
<DocumentLink
ref={itemRef}
dir={document.dir}
$isStarred={document.isStarred}
$menuOpen={menuOpen}
to={{
pathname: documentPath(document),
state: {
title: document.titleWithDefault,
sidebarContext,
},
}}
{...rest}
{...rovingTabIndex}
>
<Content>
<Heading dir={document.dir}>
{document.icon && (
<>
<Icon
value={document.icon}
color={document.color ?? undefined}
initial={document.initial}
/>
&nbsp;
</>
)}
<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>
{!queryIsInTitle && (
<ResultContext
text={context}
highlight={highlight ? SEARCH_RESULT_REGEX : undefined}
processResult={replaceResultMarks}
/>
)}
<DocumentMeta
document={document}
showCollection={showCollection}
showPublished={showPublished}
showParentDocuments={showParentDocuments}
showLastViewed
/>
</Content>
<Actions>
<DocumentMenu
document={document}
onOpen={handleMenuOpen}
onClose={handleMenuClose}
/>
</Actions>
</DocumentLink>
</ContextMenu>
</ActionContextProvider>
);
}
@@ -252,7 +280,7 @@ const DocumentLink = styled(Link)<{
`}
`;
const Heading = styled.h3<{ rtl?: boolean }>`
const Heading = styled.span<{ rtl?: boolean }>`
display: flex;
justify-content: ${(props) => (props.rtl ? "flex-end" : "flex-start")};
align-items: center;
@@ -262,6 +290,8 @@ const Heading = styled.h3<{ rtl?: boolean }>`
color: ${s("text")};
font-family: ${s("fontFamily")};
font-weight: 500;
font-size: 20px;
line-height: 1.2;
`;
const StarPositioner = styled(Flex)`
+17 -12
View File
@@ -155,26 +155,22 @@ const DocumentMeta: React.FC<Props> = ({
}
return (
<Viewed>
&nbsp;<Modified highlight>{t("Never viewed")}</Modified>
<Separator />
<Modified highlight>{t("Never viewed")}</Modified>
</Viewed>
);
}
return (
<Viewed>
&nbsp;{t("Viewed")} <Time dateTime={lastViewedAt} addSuffix shorten />
<Separator />
{t("Viewed")} <Time dateTime={lastViewedAt} addSuffix shorten />
</Viewed>
);
};
return (
<Container
align="center"
rtl={document.dir === "rtl"}
{...rest}
dir="ltr"
lang=""
>
<Container align="center" rtl={document.dir === "rtl"} {...rest} dir="ltr">
{to ? (
<Link to={to} replace={replace}>
{content}
@@ -192,16 +188,17 @@ const DocumentMeta: React.FC<Props> = ({
)}
{showParentDocuments && nestedDocumentsCount > 0 && (
<span>
&nbsp; {nestedDocumentsCount}{" "}
<Separator />
{nestedDocumentsCount}{" "}
{t("nested document", {
count: nestedDocumentsCount,
})}
</span>
)}
&nbsp;{timeSinceNow()}
{timeSinceNow()}
{canShowProgressBar && (
<>
&nbsp;&nbsp;
<Separator />
<DocumentTasks document={document} />
</>
)}
@@ -210,6 +207,14 @@ const DocumentMeta: React.FC<Props> = ({
);
};
export const Separator = styled.span`
padding: 0 0.4em;
&::after {
content: "•";
}
`;
const Strong = styled.strong`
font-weight: 550;
`;
+5 -3
View File
@@ -7,6 +7,7 @@ import Document from "~/models/Document";
import CircularProgressBar from "~/components/CircularProgressBar";
import usePrevious from "~/hooks/usePrevious";
import { bounceIn } from "~/styles/animations";
import Flex from "./Flex";
type Props = {
document: Document;
@@ -39,8 +40,9 @@ function DocumentTasks({ document }: Props) {
const done = completed === total;
const previousDone = usePrevious(done);
const message = getMessage(t, total, completed);
return (
<>
<Flex align="center" style={{ padding: "0 1px" }} gap={2}>
{completed === total ? (
<Done
color={theme.accent}
@@ -50,8 +52,8 @@ function DocumentTasks({ document }: Props) {
) : (
<CircularProgressBar percentage={tasksPercentage} />
)}
&nbsp;{message}
</>
{message}
</Flex>
);
}
+56 -23
View File
@@ -1,7 +1,7 @@
import * as React from "react";
import { toast } from "sonner";
import styled from "styled-components";
import { s } from "@shared/styles";
import { s, ellipsis } from "@shared/styles";
type Props = Omit<React.HTMLAttributes<HTMLInputElement>, "onSubmit"> & {
/** A callback when the title is submitted. */
@@ -32,6 +32,7 @@ function EditableTitle(
const [isEditing, setIsEditing] = React.useState(rest.isEditing || false);
const [originalValue, setOriginalValue] = React.useState(title);
const [value, setValue] = React.useState(title);
const [isSubmitting, setIsSubmitting] = React.useState(false);
React.useImperativeHandle(ref, () => ({
setIsEditing,
@@ -41,30 +42,54 @@ function EditableTitle(
setValue(title);
}, [title]);
const handleChange = React.useCallback((event) => {
setValue(event.target.value);
}, []);
const handleChange = React.useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value);
},
[]
);
const handleDoubleClick = React.useCallback((event) => {
event.preventDefault();
event.stopPropagation();
setIsEditing(true);
}, []);
const handleDoubleClick = React.useCallback(
(event: React.MouseEvent<HTMLSpanElement>) => {
if (event.altKey) {
return;
}
event.preventDefault();
event.stopPropagation();
setIsEditing(true);
},
[]
);
const stopPropagation = React.useCallback((event) => {
event.preventDefault();
event.stopPropagation();
}, []);
const stopPropagation = React.useCallback(
(event: React.MouseEvent<HTMLSpanElement>) => {
event.preventDefault();
event.stopPropagation();
},
[]
);
const handleFocus = React.useCallback((event) => {
event.target.select();
}, []);
const handleFocus = React.useCallback(
(event: React.FocusEvent<HTMLInputElement>) => {
event.target.select();
},
[]
);
const handleSave = React.useCallback(
async (ev) => {
async (
ev:
| React.FocusEvent<HTMLInputElement>
| React.KeyboardEvent<HTMLInputElement>
| React.FormEvent<HTMLFormElement>
) => {
ev.preventDefault();
ev.stopPropagation();
if (isSubmitting) {
return;
}
const trimmedValue = value.trim();
if (trimmedValue === originalValue || trimmedValue.length === 0) {
@@ -74,22 +99,26 @@ function EditableTitle(
return;
}
setIsSubmitting(true);
try {
await onSubmit(trimmedValue);
setOriginalValue(trimmedValue);
setIsEditing(false);
} catch (error) {
setValue(originalValue);
setValue(value);
setIsEditing(true);
toast.error(error.message);
throw error;
} finally {
setIsEditing(false);
setIsSubmitting(false);
}
},
[originalValue, value, onCancel, onSubmit]
[originalValue, value, onCancel, onSubmit, isSubmitting]
);
const handleKeyDown = React.useCallback(
async (ev) => {
async (ev: React.KeyboardEvent<HTMLInputElement>) => {
if (ev.nativeEvent.isComposing) {
return;
}
@@ -128,17 +157,21 @@ function EditableTitle(
/>
</form>
) : (
<span
<Text
onDoubleClick={canUpdate ? handleDoubleClick : undefined}
className={rest.className}
>
{value}
</span>
</Text>
)}
</>
);
}
const Text = styled.div`
${ellipsis()}
`;
const Input = styled.input`
color: ${s("text")};
background: ${s("background")};
+4 -3
View File
@@ -21,6 +21,7 @@ import useEmbeds from "~/hooks/useEmbeds";
import useStores from "~/hooks/useStores";
import { uploadFile, uploadFileFromUrl } from "~/utils/files";
import lazyWithRetry from "~/utils/lazyWithRetry";
import useShare from "@shared/hooks/useShare";
const LazyLoadedEditor = lazyWithRetry(() => import("~/editor"));
@@ -33,7 +34,6 @@ export type Props = Optional<
| "dictionary"
| "extensions"
> & {
shareId?: string | undefined;
embedsDisabled?: boolean;
onSynced?: () => Promise<void>;
onPublish?: (event: React.MouseEvent) => void;
@@ -41,9 +41,9 @@ export type Props = Optional<
};
function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
const { id, shareId, onChange, onCreateCommentMark, onDeleteCommentMark } =
props;
const { id, onChange, onCreateCommentMark, onDeleteCommentMark } = props;
const { comments } = useStores();
const { shareId } = useShare();
const dictionary = useDictionary();
const embeds = useEmbeds(!shareId);
const localRef = React.useRef<SharedEditor>();
@@ -202,6 +202,7 @@ function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
style={props.style}
editorStyle={props.editorStyle}
commenting={!!props.onClickCommentMark}
lang={props.lang}
>
<div className="ProseMirror">
{paragraphs.map((paragraph, index) => (
+227
View File
@@ -0,0 +1,227 @@
import * as React from "react";
import { useDropzone } from "react-dropzone";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import styled from "styled-components";
import { s } from "@shared/styles";
import { AttachmentPreset } from "@shared/types";
import { getDataTransferFiles } from "@shared/utils/files";
import ConfirmationDialog from "~/components/ConfirmationDialog";
import Flex from "~/components/Flex";
import Input from "~/components/Input";
import Text from "~/components/Text";
import useStores from "~/hooks/useStores";
import { uploadFile } from "~/utils/files";
import { compressImage } from "~/utils/compressImage";
import { generateEmojiNameFromFilename } from "~/utils/emoji";
import { AttachmentValidation, EmojiValidation } from "@shared/validations";
import { bytesToHumanReadable } from "@shared/utils/files";
type Props = {
onSubmit: () => void;
};
export function EmojiCreateDialog({ onSubmit }: Props) {
const { t } = useTranslation();
const { emojis } = useStores();
const [name, setName] = React.useState("");
const [file, setFile] = React.useState<File | null>(null);
const [isUploading, setIsUploading] = React.useState(false);
const handleFileSelection = React.useCallback(
(file: File) => {
const isValidType = AttachmentValidation.emojiContentTypes.includes(
file.type
);
if (!isValidType) {
toast.error(
t("File type not supported. Please use PNG, JPG, GIF, or WebP.")
);
return;
}
// Validate file size
if (file.size > AttachmentValidation.emojiMaxFileSize) {
toast.error(
t("File size too large. Maximum size is {{ size }}.", {
size: bytesToHumanReadable(AttachmentValidation.emojiMaxFileSize),
})
);
return;
}
setFile(file);
// Auto-populate name field if it's empty
setName((currentName) => {
if (!currentName.trim()) {
const generatedName = generateEmojiNameFromFilename(file.name);
return generatedName || currentName;
}
return currentName;
});
},
[t]
);
const onDrop = React.useCallback(
(acceptedFiles: File[]) => {
if (acceptedFiles.length > 0) {
handleFileSelection(acceptedFiles[0]);
}
},
[handleFileSelection]
);
// Handle paste events
React.useEffect(() => {
const handlePaste = (event: ClipboardEvent) => {
const files = getDataTransferFiles(event);
if (files.length > 0) {
event.preventDefault();
handleFileSelection(files[0]);
}
};
document.addEventListener("paste", handlePaste);
return () => document.removeEventListener("paste", handlePaste);
}, [handleFileSelection]);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDropAccepted: onDrop,
accept: AttachmentValidation.emojiContentTypes,
maxSize: AttachmentValidation.emojiMaxFileSize,
maxFiles: 1,
});
const handleSubmit = async () => {
if (!name.trim()) {
toast.error(t("Please enter a name for the emoji"));
return;
}
if (!file) {
toast.error(t("Please select an image file"));
return;
}
setIsUploading(true);
try {
const compressed = await compressImage(file, {
maxHeight: 64,
maxWidth: 64,
});
const attachment = await uploadFile(compressed, {
name: file.name,
preset: AttachmentPreset.Emoji,
});
await emojis.create({
name: name.trim(),
attachmentId: attachment.id,
});
toast.success(t("Emoji created successfully"));
onSubmit();
} finally {
setIsUploading(false);
}
};
const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { value } = event.target;
setName(value);
};
const isValidName = EmojiValidation.allowedNameCharacters.test(name);
const isValid = name.trim().length > 0 && file && isValidName;
return (
<ConfirmationDialog
onSubmit={handleSubmit}
disabled={!isValid || isUploading}
savingText={isUploading ? `${t("Uploading")}` : undefined}
submitText={t("Add emoji")}
>
<Text as="p" type="secondary">
{t(
"The emoji name should be unique and contain only lowercase letters, numbers, and underscores."
)}
</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
}
/>
<DropZone {...getRootProps()}>
<input {...getInputProps()} />
<Flex column align="center" gap={8}>
{file ? (
<>
<PreviewImage src={URL.createObjectURL(file)} alt="Preview" />
<Text size="medium">{file.name}</Text>
<Text size="medium" type="secondary">
{t("Click or drag to replace")}
</Text>
</>
) : (
<>
<Text size="medium">
{isDragActive
? t("Drop the image here")
: t("Click, drop, or paste an image here")}
</Text>
<Text size="medium" type="secondary">
{t("PNG, JPG, GIF, or WebP up to {{ size }}", {
size: bytesToHumanReadable(
AttachmentValidation.emojiMaxFileSize
),
})}
</Text>
</>
)}
</Flex>
</DropZone>
{name.trim() && isValidName && (
<Text type="secondary" style={{ marginTop: "8px" }}>
{t("This emoji will be available as")} <code>:{name}:</code>
</Text>
)}
</ConfirmationDialog>
);
}
const DropZone = styled.div`
border: 2px dashed ${s("inputBorder")};
border-radius: 8px;
padding: 24px;
text-align: center;
cursor: var(--pointer);
transition: border-color 0.2s;
&:hover {
border-color: ${s("inputBorderFocused")};
}
`;
const PreviewImage = styled.img`
width: 64px;
height: 64px;
object-fit: contain;
border-radius: 4px;
`;
+4 -2
View File
@@ -46,20 +46,22 @@ class ErrorBoundary extends React.Component<Props> {
componentDidCatch(error: Error) {
this.error = error;
this.trackError();
if (
this.props.reloadOnChunkMissing &&
error.message &&
error.message.match(/dynamically imported module/)
error.message.match(/dynamically imported module/) &&
!this.isRepeatedError
) {
// If the editor bundle fails to load then reload the entire window. This
// can happen if a deploy happens between the user loading the initial JS
// bundle and the async-loaded editor JS bundle as the hash will change.
// Don't reload if this is a repeated error to avoid infinite reload loops.
window.location.reload();
return;
}
this.trackError();
Logger.error("ErrorBoundary", error);
}
+55 -20
View File
@@ -25,6 +25,7 @@ function ExportDialog({ collection, onSubmit }: Props) {
);
const [includeAttachments, setIncludeAttachments] =
React.useState<boolean>(true);
const [includePrivate, setIncludePrivate] = React.useState<boolean>(true);
const user = useCurrentUser();
const { collections } = useStores();
const { t } = useTranslation();
@@ -44,6 +45,13 @@ function ExportDialog({ collection, onSubmit }: Props) {
[]
);
const handleIncludePrivateChange = React.useCallback(
(ev: React.ChangeEvent<HTMLInputElement>) => {
setIncludePrivate(ev.target.checked);
},
[]
);
const handleSubmit = async () => {
if (collection) {
await collection.export(format, includeAttachments);
@@ -59,7 +67,7 @@ function ExportDialog({ collection, onSubmit }: Props) {
},
});
} else {
await collections.export(format, includeAttachments);
await collections.export({ format, includeAttachments, includePrivate });
toast.success(t("Export started"));
}
onSubmit();
@@ -123,37 +131,64 @@ function ExportDialog({ collection, onSubmit }: Props) {
<Text as="p" size="small" weight="bold">
{item.title}
</Text>
<Text size="small">{item.description}</Text>
<Text size="small" type="secondary">
{item.description}
</Text>
</div>
</Option>
))}
</Flex>
<hr />
<Option>
<input
type="checkbox"
name="includeAttachments"
checked={includeAttachments}
onChange={handleIncludeAttachmentsChange}
/>
<div>
<Text as="p" size="small" weight="bold">
{t("Include attachments")}
</Text>
<Text size="small">
{t("Including uploaded images and files in the exported data")}.
</Text>{" "}
</div>
</Option>
<HR />
<Flex gap={12} column>
<Option>
<input
type="checkbox"
name="includeAttachments"
checked={includeAttachments}
onChange={handleIncludeAttachmentsChange}
/>
<div>
<Text as="p" size="small" weight="bold">
{t("Include attachments")}
</Text>
<Text size="small" type="secondary">
{t("Including uploaded images and files in the exported data")}.
</Text>{" "}
</div>
</Option>
{!collection && (
<Option>
<input
type="checkbox"
name="includePrivate"
checked={includePrivate}
onChange={handleIncludePrivateChange}
/>
<div>
<Text as="p" size="small" weight="bold">
{t("Include private collections")}
</Text>
</div>
</Option>
)}
</Flex>
</ConfirmationDialog>
);
}
const HR = styled.hr`
margin: 16px 0;
`;
const Option = styled.label`
display: flex;
align-items: center;
align-items: start;
gap: 16px;
input {
margin-top: 4px;
}
p {
margin: 0;
}
+14 -1
View File
@@ -5,6 +5,8 @@ import styled from "styled-components";
import User from "~/models/User";
import { Avatar, AvatarSize } from "~/components/Avatar";
import Flex from "~/components/Flex";
import { s } from "@shared/styles";
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
type Props = {
/** The users to display */
@@ -21,6 +23,8 @@ type Props = {
model: User;
}
>;
/** Whether to show tooltips on hover, defaults to true */
showTooltip?: boolean;
};
function Facepile({
@@ -29,6 +33,7 @@ function Facepile({
size = AvatarSize.Large,
limit = 8,
renderAvatar = Avatar,
showTooltip = true,
...rest
}: Props) {
const { t } = useTranslation();
@@ -51,6 +56,7 @@ function Facepile({
<Component
key={model.id}
{...{
showTooltip,
model,
size,
style: {
@@ -63,7 +69,9 @@ function Facepile({
/>
);
})}
<FacepileClip size={size} />
<VisuallyHidden>
<FacepileClip size={size} />
</VisuallyHidden>
</Avatars>
);
}
@@ -101,6 +109,11 @@ const Avatars = styled(Flex)`
align-items: center;
flex-direction: row-reverse;
cursor: var(--pointer);
*:hover {
clip-path: none !important;
box-shadow: 0 0 0 2px ${s("background")};
}
`;
export default observer(Facepile);
+96 -101
View File
@@ -1,22 +1,20 @@
import deburr from "lodash/deburr";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { MenuButton } from "reakit/Menu";
import styled from "styled-components";
import { s } from "@shared/styles";
import type { FetchPageParams } from "~/stores/base/Store";
import Button, { Inner } from "~/components/Button";
import ContextMenu from "~/components/ContextMenu";
import MenuItem from "~/components/ContextMenu/MenuItem";
import Text from "~/components/Text";
import { useMenuState } from "~/hooks/useMenuState";
import Input, { NativeInput, Outline } from "./Input";
import PaginatedList, { PaginatedItem } from "./PaginatedList";
import { MenuProvider } from "./primitives/Menu/MenuContext";
import { Menu, MenuContent, MenuTrigger, MenuButton } from "./primitives/Menu";
import { MenuIconWrapper } from "./primitives/components/Menu";
interface TFilterOption extends PaginatedItem {
key: string;
label: string;
note?: string;
icon?: React.ReactNode;
}
@@ -34,19 +32,17 @@ type Props = {
const FilterOptions = ({
options,
selectedKeys = [],
defaultLabel = "Filter options",
className,
onSelect,
showFilter,
fetchQuery,
fetchQueryOptions,
...rest
}: Props) => {
const { t } = useTranslation();
const searchInputRef = React.useRef<HTMLInputElement>(null);
const listRef = React.useRef<HTMLDivElement | null>(null);
const menu = useMenuState({
modal: false,
});
const [open, setOpen] = React.useState(false);
const selectedItems = options.filter((option) =>
selectedKeys.includes(option.key)
);
@@ -58,60 +54,73 @@ const FilterOptions = ({
const renderItem = React.useCallback(
(option) => (
<MenuItem
<MenuButton
key={option.key}
icon={
option.icon ? (
<MenuIconWrapper aria-hidden>{option.icon}</MenuIconWrapper>
) : undefined
}
label={option.label}
onClick={() => {
onSelect(option.key);
menu.hide();
setOpen(false);
}}
selected={selectedKeys.includes(option.key)}
{...menu}
>
{option.icon}
{option.note ? (
<LabelWithNote>
{option.label}
<Note>{option.note}</Note>
</LabelWithNote>
) : (
option.label
)}
</MenuItem>
/>
),
[menu, onSelect, selectedKeys]
[onSelect, selectedKeys]
);
const handleFilter = (ev: React.ChangeEvent<HTMLInputElement>) => {
setQuery(ev.target.value);
};
const handleFilter = React.useCallback(
(ev: React.ChangeEvent<HTMLInputElement>) => {
setQuery(ev.target.value);
},
[]
);
const filteredOptions = React.useMemo(() => {
const normalizedQuery = deburr(query.toLowerCase());
return query
? options
.filter((option) =>
deburr(option.label).toLowerCase().includes(normalizedQuery)
)
// sort options starting with query first
.sort((a, b) => {
const aStartsWith = deburr(a.label)
.toLowerCase()
.startsWith(normalizedQuery);
const bStartsWith = deburr(b.label)
.toLowerCase()
.startsWith(normalizedQuery);
if (aStartsWith && !bStartsWith) {
return -1;
}
if (!aStartsWith && bStartsWith) {
return 1;
}
return 0;
})
const filtered = query
? options.filter((option) =>
deburr(option.label).toLowerCase().includes(normalizedQuery)
)
: options;
}, [options, query]);
return filtered.sort((a, b) => {
const aSelected = selectedKeys.includes(a.key);
const bSelected = selectedKeys.includes(b.key);
// Selected items come first
if (aSelected && !bSelected) {
return -1;
}
if (!aSelected && bSelected) {
return 1;
}
// If both have the same selection state and there's a query,
// sort options starting with query first
if (query) {
const aStartsWith = deburr(a.label)
.toLowerCase()
.startsWith(normalizedQuery);
const bStartsWith = deburr(b.label)
.toLowerCase()
.startsWith(normalizedQuery);
if (aStartsWith && !bStartsWith) {
return -1;
}
if (!aStartsWith && bStartsWith) {
return 1;
}
}
return 0;
});
}, [options, query, selectedKeys]);
const handleKeyDown = React.useCallback(
(ev: React.KeyboardEvent) => {
@@ -119,15 +128,19 @@ const FilterOptions = ({
return;
}
// Stop all keyboard events from propagating to prevent Radix UI menu
// from handling them and potentially moving focus
ev.stopPropagation();
switch (ev.key) {
case "Escape":
menu.hide();
setOpen(false);
break;
case "Enter":
if (filteredOptions.length === 1) {
ev.preventDefault();
onSelect(filteredOptions[0].key);
menu.hide();
setOpen(false);
}
break;
case "ArrowDown":
@@ -138,7 +151,7 @@ const FilterOptions = ({
break;
}
},
[filteredOptions, menu, onSelect]
[filteredOptions, onSelect]
);
const handleEscapeFromList = React.useCallback((ev: React.KeyboardEvent) => {
@@ -150,21 +163,21 @@ const FilterOptions = ({
}, []);
React.useEffect(() => {
if (menu.visible) {
if (open) {
searchInputRef.current?.focus();
} else {
setQuery("");
}
}, [menu.visible]);
}, [open]);
const showFilterInput = showFilter || options.length > 10;
const defaultLabel = rest.defaultLabel || t("Filter options");
return (
<>
<MenuButton {...menu}>
{(props) => (
<MenuProvider variant="dropdown">
<Menu open={open} onOpenChange={setOpen}>
<MenuTrigger>
<StyledButton
{...props}
className={className}
icon={selectedItems[0]?.key && selectedItems[0]?.icon}
neutral
@@ -172,31 +185,31 @@ const FilterOptions = ({
>
{selectedItems.length ? selectedLabel : defaultLabel}
</StyledButton>
)}
</MenuButton>
<ContextMenu aria-label={defaultLabel} minHeight={66} {...menu}>
<PaginatedList<TFilterOption>
listRef={listRef}
options={{ query, ...fetchQueryOptions }}
items={filteredOptions}
fetch={fetchQuery}
renderItem={renderItem}
onEscape={handleEscapeFromList}
heading={showFilterInput ? <Spacer /> : undefined}
empty={<Empty />}
/>
{showFilterInput && (
<SearchInput
ref={searchInputRef}
value={query}
onChange={handleFilter}
onKeyDown={handleKeyDown}
placeholder={`${t("Filter")}`}
autoFocus
</MenuTrigger>
<MenuContent aria-label={defaultLabel} align="start">
<PaginatedList<TFilterOption>
listRef={listRef}
options={{ query, ...fetchQueryOptions }}
items={filteredOptions}
fetch={fetchQuery}
renderItem={renderItem}
onEscape={handleEscapeFromList}
heading={showFilterInput ? <Spacer /> : undefined}
empty={<Empty />}
/>
)}
</ContextMenu>
</>
{showFilterInput && (
<SearchInput
ref={searchInputRef}
value={query}
onChange={handleFilter}
onKeyDown={handleKeyDown}
placeholder={`${t("Filter")}`}
autoFocus
/>
)}
</MenuContent>
</Menu>
</MenuProvider>
);
};
@@ -242,24 +255,6 @@ const SearchInput = styled(Input)`
}
`;
const Note = styled(Text)`
display: block;
margin: 2px 0;
line-height: 1.2em;
font-size: 14px;
font-weight: 500;
color: ${s("textTertiary")};
`;
const LabelWithNote = styled.div`
font-weight: 500;
text-align: left;
&:hover ${Note} {
color: ${(props) => props.theme.white50};
}
`;
export const StyledButton = styled(Button)`
box-shadow: none;
text-transform: none;
+1
View File
@@ -125,6 +125,7 @@ const Actions = styled(Flex)`
flex-basis: 0;
min-width: auto;
padding-left: 8px;
gap: 12px;
${breakpoint("tablet")`
position: unset;
+32 -4
View File
@@ -13,6 +13,7 @@ import useStores from "~/hooks/useStores";
import LoadingIndicator from "../LoadingIndicator";
import { CARD_MARGIN } from "./Components";
import HoverPreviewDocument from "./HoverPreviewDocument";
import HoverPreviewGroup from "./HoverPreviewGroup";
import HoverPreviewIssue from "./HoverPreviewIssue";
import HoverPreviewLink from "./HoverPreviewLink";
import HoverPreviewMention from "./HoverPreviewMention";
@@ -116,12 +117,31 @@ const HoverPreviewDesktop = observer(
<Position top={cardTop} left={cardLeft} aria-hidden>
{isVisible ? (
<Animate
initial={{ opacity: 0, y: -20, pointerEvents: "none" }}
initial={{
opacity: 0,
y: -20,
filter: "blur(5px)",
pointerEvents: "none",
}}
animate={{
opacity: 1,
y: 0,
filter: "blur(0px)",
transitionEnd: { pointerEvents: "auto" },
}}
transition={{
y: {
type: "spring",
stiffness: 400,
damping: 25,
},
opacity: {
duration: 0.2,
},
filter: {
duration: 0.2,
},
}}
>
{data.type === UnfurlResourceType.Mention ? (
<HoverPreviewMention
@@ -132,6 +152,14 @@ const HoverPreviewDesktop = observer(
lastActive={data.lastActive}
email={data.email}
/>
) : data.type === UnfurlResourceType.Group ? (
<HoverPreviewGroup
ref={cardRef}
name={data.name}
description={data.description}
memberCount={data.memberCount}
users={data.users}
/>
) : data.type === UnfurlResourceType.Document ? (
<HoverPreviewDocument
ref={cardRef}
@@ -295,10 +323,10 @@ const Pointer = styled.div<{ top: number; left: number; direction: Direction }>`
&:before {
border: 8px solid transparent;
${({ direction, theme }) =>
${({ direction }) =>
direction === Direction.UP
? `border-bottom-color: ${theme.menuBorder || "rgba(0, 0, 0, 0.1)"}`
: `border-top-color: ${theme.menuBorder || "rgba(0, 0, 0, 0.1)"}`};
? `border-bottom-color: rgba(0, 0, 0, 0.1)`
: `border-top-color: rgba(0, 0, 0, 0.1)`};
${({ direction }) =>
direction === Direction.UP ? "right: -1px" : "left: -1px"};
}
@@ -0,0 +1,70 @@
import * as React from "react";
import { useTranslation } from "react-i18next";
import { UnfurlResourceType, UnfurlResponse } from "@shared/types";
import { MAX_AVATAR_DISPLAY } from "@shared/constants";
import User from "~/models/User";
import Facepile from "~/components/Facepile";
import Flex from "~/components/Flex";
import {
Preview,
Title,
Info,
Card,
CardContent,
Description,
} from "./Components";
import ErrorBoundary from "../ErrorBoundary";
type Props = Omit<UnfurlResponse[UnfurlResourceType.Group], "type">;
const HoverPreviewGroup = React.forwardRef(function _HoverPreviewGroup(
{ name, description, memberCount, users }: Props,
ref: React.Ref<HTMLDivElement>
) {
const { t } = useTranslation();
return (
<Preview as="div">
<Card fadeOut={false} ref={ref}>
<CardContent>
<ErrorBoundary showTitle={false} reloadOnChunkMissing={false}>
<Flex column gap={2} align="start">
<Flex
justify="space-between"
gap={4}
style={{ width: "100%" }}
auto
>
<Flex column align="start">
<Title>{name}</Title>
<Info>
{t("{{ count }} members", { count: memberCount })}
</Info>
</Flex>
{users.length > 0 && (
<Facepile
users={users.map(
(member) =>
({
id: member.id,
name: member.name,
avatarUrl: member.avatarUrl,
color: member.color,
initial: member.name ? member.name[0] : "?",
}) as User
)}
overflow={Math.max(0, memberCount - users.length)}
limit={MAX_AVATAR_DISPLAY}
/>
)}
</Flex>
{description && <Description>{description}</Description>}
</Flex>
</ErrorBoundary>
</CardContent>
</Card>
</Preview>
);
});
export default HoverPreviewGroup;
@@ -30,15 +30,10 @@ const HoverPreviewIssue = React.forwardRef(function _HoverPreviewIssue(
) {
const authorName = author.name;
const urlObj = new URL(url);
let service;
if (urlObj.hostname === "github.com") {
service = IntegrationService.GitHub;
} else if (urlObj.hostname === "gitlab.com") {
service = IntegrationService.GitLab;
} else {
service = IntegrationService.Linear;
}
const service =
urlObj.hostname === "github.com"
? IntegrationService.GitHub
: IntegrationService.Linear;
return (
<Preview as="a" href={url} target="_blank" rel="noopener noreferrer">
@@ -7,7 +7,7 @@ import { Preview, Title, Info, Card, CardContent } from "./Components";
type Props = Omit<UnfurlResponse[UnfurlResourceType.Mention], "type">;
const HoverPreviewMention = React.forwardRef(function _HoverPreviewMention(
{ avatarUrl, name, lastActive, color, email }: Props,
{ avatarUrl, name, lastActive, color }: Props,
ref: React.Ref<HTMLDivElement>
) {
return (
@@ -25,7 +25,6 @@ const HoverPreviewMention = React.forwardRef(function _HoverPreviewMention(
/>
<Flex column gap={2} justify="center">
<Title>{name}</Title>
{email && <Info>{email}</Info>}
<Info>{lastActive}</Info>
</Flex>
</Flex>
@@ -1,17 +1,11 @@
import { BackIcon } from "outline-icons";
import * as React from "react";
import debounce from "lodash/debounce";
import styled from "styled-components";
import { breakpoints, s, hover } from "@shared/styles";
import { s } from "@shared/styles";
import { colorPalette } from "@shared/utils/collections";
import { validateColorHex } from "@shared/utils/color";
import Flex from "~/components/Flex";
import NudeButton from "~/components/NudeButton";
import Text from "~/components/Text";
enum Panel {
Builtin,
Hex,
}
import { SwatchButton } from "~/components/SwatchButton";
import { ColorButton } from "~/components/ColorButton";
type Props = {
width: number;
@@ -19,128 +13,77 @@ type Props = {
onSelect: (color: string) => void;
};
const ColorPicker = ({ width, activeColor, onSelect }: Props) => {
const [localValue, setLocalValue] = React.useState(activeColor);
const ColorPicker = ({ activeColor, onSelect }: Props) => {
const [selectedColor, setSelectedColor] = React.useState(activeColor);
const isBuiltInColor = colorPalette.includes(selectedColor);
const color = isBuiltInColor ? undefined : selectedColor;
const [panel, setPanel] = React.useState(
colorPalette.includes(activeColor) ? Panel.Builtin : Panel.Hex
const debouncedOnSelect = React.useMemo(
() =>
debounce((color: string) => {
onSelect(color);
}, 250),
[onSelect]
);
const handleSwitcherClick = React.useCallback(() => {
setPanel(panel === Panel.Builtin ? Panel.Hex : Panel.Builtin);
}, [panel, setPanel]);
const isLargeMobile = width > breakpoints.mobileLarge + 12; // 12px for the Container padding
React.useEffect(
() => () => {
debouncedOnSelect.cancel();
},
[debouncedOnSelect]
);
React.useEffect(() => {
setLocalValue(activeColor);
setPanel(colorPalette.includes(activeColor) ? Panel.Builtin : Panel.Hex);
setSelectedColor(activeColor);
}, [activeColor]);
return isLargeMobile ? (
<Container justify="space-between">
<LargeMobileBuiltinColors activeColor={activeColor} onClick={onSelect} />
<LargeMobileCustomColor
value={localValue}
setLocalValue={setLocalValue}
onValidHex={onSelect}
const handleSelect = (color: string) => {
setSelectedColor(color);
debouncedOnSelect(color);
};
return (
<BuiltinColors activeColor={selectedColor} onClick={handleSelect}>
<Divider />
<SwatchButton
color={color}
active={!isBuiltInColor}
onChange={handleSelect}
pickerInModal
/>
</Container>
) : (
<Container gap={12}>
<PanelSwitcher align="center">
<SwitcherButton panel={panel} onClick={handleSwitcherClick}>
{panel === Panel.Builtin ? "#" : <BackIcon />}
</SwitcherButton>
</PanelSwitcher>
{panel === Panel.Builtin ? (
<BuiltinColors activeColor={activeColor} onClick={onSelect} />
) : (
<CustomColor
value={localValue}
setLocalValue={setLocalValue}
onValidHex={onSelect}
/>
)}
</Container>
</BuiltinColors>
);
};
const Divider = styled.div`
width: 1px;
height: 24px;
background-color: ${s("inputBorder")};
`;
const BuiltinColors = ({
activeColor,
onClick,
className,
children,
}: {
activeColor: string;
onClick: (color: string) => void;
className?: string;
children?: React.ReactNode;
}) => (
<Flex className={className} justify="space-between" align="center" auto>
<Container className={className} justify="space-between" align="center" auto>
{colorPalette.map((color) => (
<ColorButton
key={color}
$color={color}
$active={color === activeColor}
color={color}
active={color === activeColor}
onClick={() => onClick(color)}
>
<Selected />
</ColorButton>
))}
</Flex>
);
const CustomColor = ({
value,
setLocalValue,
onValidHex,
className,
}: {
value: string;
setLocalValue: (value: string) => void;
onValidHex: (color: string) => void;
className?: string;
}) => {
const hasHexChars = React.useCallback(
(color: string) => /(^#[0-9A-F]{1,6}$)/i.test(color),
[]
);
const handleInputChange = React.useCallback(
(ev: React.ChangeEvent<HTMLInputElement>) => {
const val = ev.target.value;
if (val === "" || val === "#") {
setLocalValue("#");
return;
}
const uppercasedVal = val.toUpperCase();
if (hasHexChars(uppercasedVal)) {
setLocalValue(uppercasedVal);
}
if (validateColorHex(uppercasedVal)) {
onValidHex(uppercasedVal);
}
},
[setLocalValue, hasHexChars, onValidHex]
);
return (
<Flex className={className} align="center" gap={8}>
<Text type="tertiary" size="small">
HEX
</Text>
<CustomColorInput
maxLength={7}
value={value}
onChange={handleInputChange}
autoFocus
/>
</Flex>
);
};
))}
{children}
</Container>
);
const Container = styled(Flex)`
height: 48px;
@@ -148,71 +91,4 @@ const Container = styled(Flex)`
border-bottom: 1px solid ${s("inputBorder")};
`;
const Selected = styled.span`
width: 10px;
height: 5px;
border-left: 2px solid white;
border-bottom: 2px solid white;
transform: translateY(-25%) rotate(-45deg);
`;
const ColorButton = styled(NudeButton)<{ $color: string; $active: boolean }>`
display: inline-flex;
justify-content: center;
align-items: center;
width: 24px;
height: 24px;
border-radius: 50%;
background-color: ${({ $color }) => $color};
&: ${hover} {
outline: 2px solid ${s("menuBackground")} !important;
box-shadow: ${({ $color }) => `0px 0px 3px 3px ${$color}`};
}
& ${Selected} {
display: ${({ $active }) => ($active ? "block" : "none")};
}
`;
const PanelSwitcher = styled(Flex)`
width: 40px;
border-right: 1px solid ${s("inputBorder")};
`;
const SwitcherButton = styled(NudeButton)<{ panel: Panel }>`
display: inline-flex;
justify-content: center;
align-items: center;
font-size: 14px;
border: 1px solid ${s("inputBorder")};
transition: all 100ms ease-in-out;
&: ${hover} {
border-color: ${s("inputBorderFocused")};
}
`;
const LargeMobileBuiltinColors = styled(BuiltinColors)`
max-width: 400px;
padding-right: 8px;
`;
const LargeMobileCustomColor = styled(CustomColor)`
padding-left: 8px;
border-left: 1px solid ${s("inputBorder")};
width: 120px;
`;
const CustomColorInput = styled.input.attrs(() => ({
type: "text",
autocomplete: "off",
}))`
font-size: 14px;
color: ${s("textSecondary")};
background: transparent;
border: 0;
outline: 0;
`;
export default ColorPicker;
@@ -0,0 +1,12 @@
import Flex from "@shared/components/Flex";
import styled from "styled-components";
import InputSearch from "~/components/InputSearch";
export const UserInputContainer = styled(Flex)`
height: 48px;
padding: 6px 12px 0px;
`;
export const StyledInputSearch = styled(InputSearch)`
flex-grow: 1;
`;
@@ -0,0 +1,170 @@
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,77 +1,17 @@
import concat from "lodash/concat";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { EmojiCategory, EmojiSkinTone, IconType } from "@shared/types";
import { getEmojis, getEmojisWithCategory, search } from "@shared/utils/emoji";
import Flex from "~/components/Flex";
import InputSearch from "~/components/InputSearch";
import usePersistedState from "~/hooks/usePersistedState";
import {
FREQUENTLY_USED_COUNT,
DisplayCategory,
emojiSkinToneKey,
emojisFreqKey,
lastEmojiKey,
sortFrequencies,
} from "../utils";
import { DisplayCategory } from "../utils";
import GridTemplate, { DataNode } from "./GridTemplate";
import SkinTonePicker from "./SkinTonePicker";
import { StyledInputSearch, UserInputContainer } from "./Components";
import { useIconState } from "../useIconState";
const GRID_HEIGHT = 410;
const useEmojiState = () => {
const [emojiSkinTone, setEmojiSkinTone] = usePersistedState<EmojiSkinTone>(
emojiSkinToneKey,
EmojiSkinTone.Default
);
const [emojisFreq, setEmojisFreq] = usePersistedState<Record<string, number>>(
emojisFreqKey,
{}
);
const [lastEmoji, setLastEmoji] = usePersistedState<string | undefined>(
lastEmojiKey,
undefined
);
const incrementEmojiCount = React.useCallback(
(emoji: string) => {
emojisFreq[emoji] = (emojisFreq[emoji] ?? 0) + 1;
setEmojisFreq({ ...emojisFreq });
setLastEmoji(emoji);
},
[emojisFreq, setEmojisFreq, setLastEmoji]
);
const getFreqEmojis = React.useCallback(() => {
const freqs = Object.entries(emojisFreq);
if (freqs.length > FREQUENTLY_USED_COUNT.Track) {
sortFrequencies(freqs).splice(FREQUENTLY_USED_COUNT.Track);
setEmojisFreq(Object.fromEntries(freqs));
}
const emojis = sortFrequencies(freqs)
.slice(0, FREQUENTLY_USED_COUNT.Get)
.map(([emoji, _]) => emoji);
const isLastPresent = emojis.includes(lastEmoji ?? "");
if (lastEmoji && !isLastPresent) {
emojis.pop();
emojis.push(lastEmoji);
}
return emojis;
}, [emojisFreq, setEmojisFreq, lastEmoji]);
return {
emojiSkinTone,
setEmojiSkinTone,
incrementEmojiCount,
getFreqEmojis,
};
};
type Props = {
panelWidth: number;
query: string;
@@ -97,11 +37,14 @@ const EmojiPanel = ({
const {
emojiSkinTone: skinTone,
setEmojiSkinTone,
incrementEmojiCount,
getFreqEmojis,
} = useEmojiState();
incrementIconCount,
getFrequentIcons,
} = useIconState(IconType.Emoji);
const freqEmojis = React.useMemo(() => getFreqEmojis(), [getFreqEmojis]);
const freqEmojis = React.useMemo(
() => getFrequentIcons(),
[getFrequentIcons]
);
const handleFilter = React.useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
@@ -120,9 +63,9 @@ const EmojiPanel = ({
const handleEmojiSelection = React.useCallback(
({ id, value }: { id: string; value: string }) => {
onEmojiChange(value);
incrementEmojiCount(id);
incrementIconCount(id);
},
[onEmojiChange, incrementEmojiCount]
[onEmojiChange, incrementIconCount]
);
const isSearch = query !== "";
@@ -195,7 +138,7 @@ const getAllEmojis = ({
}): DataNode[] => {
const emojisWithCategory = getEmojisWithCategory({ skinTone });
const getFrequentEmojis = (): DataNode => {
const getFrequentIcons = (): DataNode => {
const emojis = getEmojis({ ids: freqEmojis, skinTone });
return {
category: DisplayCategory.Frequent,
@@ -220,7 +163,7 @@ const getAllEmojis = ({
};
return concat(
getFrequentEmojis(),
getFrequentIcons(),
getCategoryData(EmojiCategory.People),
getCategoryData(EmojiCategory.Nature),
getCategoryData(EmojiCategory.Foods),
@@ -232,13 +175,4 @@ const getAllEmojis = ({
);
};
const UserInputContainer = styled(Flex)`
height: 48px;
padding: 6px 12px 0px;
`;
const StyledInputSearch = styled(InputSearch)`
flex-grow: 1;
`;
export default EmojiPanel;
@@ -9,6 +9,7 @@ import Text from "~/components/Text";
import { TRANSLATED_CATEGORIES } from "../utils";
import Grid from "./Grid";
import { IconButton } from "./IconButton";
import { CustomEmoji } from "@shared/components/CustomEmoji";
/**
* icon/emoji size is 24px; and we add 4px padding on all sides,
@@ -23,10 +24,11 @@ type OutlineNode = {
delay: number;
};
type EmojiNode = {
type: IconType.Emoji;
export type EmojiNode = {
type: IconType.Emoji | IconType.Custom;
id: string;
value: string;
name?: string;
};
export type DataNode = {
@@ -86,7 +88,11 @@ const GridTemplate = (
onClick={() => onIconSelect({ id: item.id, value: item.value })}
>
<Emoji width={24} height={24}>
{item.value}
{item.type === IconType.Custom ? (
<CustomEmoji value={item.value} title={item.name} />
) : (
item.value
)}
</Emoji>
</IconButton>
);
@@ -5,16 +5,10 @@ import { IconType } from "@shared/types";
import { IconLibrary } from "@shared/utils/IconLibrary";
import Flex from "~/components/Flex";
import InputSearch from "~/components/InputSearch";
import usePersistedState from "~/hooks/usePersistedState";
import {
FREQUENTLY_USED_COUNT,
DisplayCategory,
iconsFreqKey,
lastIconKey,
sortFrequencies,
} from "../utils";
import { DisplayCategory } from "../utils";
import ColorPicker from "./ColorPicker";
import GridTemplate, { DataNode } from "./GridTemplate";
import { useIconState } from "../useIconState";
const IconNames = Object.keys(IconLibrary.mapping);
const TotalIcons = IconNames.length;
@@ -25,52 +19,6 @@ const TotalIcons = IconNames.length;
*/
const GRID_HEIGHT = 314;
const useIconState = () => {
const [iconsFreq, setIconsFreq] = usePersistedState<Record<string, number>>(
iconsFreqKey,
{}
);
const [lastIcon, setLastIcon] = usePersistedState<string | undefined>(
lastIconKey,
undefined
);
const incrementIconCount = React.useCallback(
(icon: string) => {
iconsFreq[icon] = (iconsFreq[icon] ?? 0) + 1;
setIconsFreq({ ...iconsFreq });
setLastIcon(icon);
},
[iconsFreq, setIconsFreq, setLastIcon]
);
const getFreqIcons = React.useCallback(() => {
const freqs = Object.entries(iconsFreq);
if (freqs.length > FREQUENTLY_USED_COUNT.Track) {
sortFrequencies(freqs).splice(FREQUENTLY_USED_COUNT.Track);
setIconsFreq(Object.fromEntries(freqs));
}
const icons = sortFrequencies(freqs)
.slice(0, FREQUENTLY_USED_COUNT.Get)
.map(([icon, _]) => icon);
const isLastPresent = icons.includes(lastIcon ?? "");
if (lastIcon && !isLastPresent) {
icons.pop();
icons.push(lastIcon);
}
return icons;
}, [iconsFreq, setIconsFreq, lastIcon]);
return {
incrementIconCount,
getFreqIcons,
};
};
type Props = {
panelWidth: number;
initial: string;
@@ -97,9 +45,9 @@ const IconPanel = ({
const searchRef = React.useRef<HTMLInputElement | null>(null);
const scrollableRef = React.useRef<HTMLDivElement | null>(null);
const { incrementIconCount, getFreqIcons } = useIconState();
const { incrementIconCount, getFrequentIcons } = useIconState(IconType.SVG);
const freqIcons = React.useMemo(() => getFreqIcons(), [getFreqIcons]);
const freqIcons = React.useMemo(() => getFrequentIcons(), [getFrequentIcons]);
const totalFreqIcons = freqIcons.length;
const filteredIcons = React.useMemo(
+27 -2
View File
@@ -21,10 +21,13 @@ import { Drawer, DrawerContent, DrawerTrigger } from "../primitives/Drawer";
import EmojiPanel from "./components/EmojiPanel";
import IconPanel from "./components/IconPanel";
import { PopoverButton } from "./components/PopoverButton";
import CustomEmojiPanel from "./components/CustomEmojiPanel";
import useStores from "~/hooks/useStores";
const TAB_NAMES = {
Icon: "icon",
Emoji: "emoji",
Custom: "custom",
} as const;
type TabName = (typeof TAB_NAMES)[keyof typeof TAB_NAMES];
@@ -35,7 +38,7 @@ type Props = {
icon: string | null;
color: string;
size?: number;
initial?: string;
initial: string;
className?: string;
popoverPosition: "bottom-start" | "right";
allowDelete?: boolean;
@@ -61,7 +64,7 @@ const IconPicker = ({
children,
}: Props) => {
const { t } = useTranslation();
const { emojis } = useStores();
const { width: windowWidth } = useWindowSize();
const isMobile = useMobile();
@@ -168,6 +171,12 @@ const IconPicker = ({
setActiveTab(defaultTab);
}, [defaultTab]);
React.useEffect(() => {
if (open) {
void emojis.fetchAll();
}
}, [open]);
if (isMobile) {
return (
<Drawer open={open} onOpenChange={setOpen}>
@@ -245,6 +254,13 @@ const Content = ({
>
{t("Emojis")}
</StyledTab>
<StyledTab
value={TAB_NAMES["Custom"]}
aria-label={t("Custom Emojis")}
$active={activeTab === TAB_NAMES["Custom"]}
>
{t("Custom")}
</StyledTab>
</Tabs.List>
{allowDelete && (
<RemoveButton onClick={onIconRemove}>{t("Remove")}</RemoveButton>
@@ -271,6 +287,15 @@ const Content = ({
onQueryChange={onQueryChange}
/>
</StyledTabContent>
<StyledTabContent value={TAB_NAMES["Custom"]}>
<CustomEmojiPanel
panelWidth={panelWidth}
query={query}
panelActive={open && activeTab === TAB_NAMES["Custom"]}
onEmojiChange={onIconChange}
onQueryChange={onQueryChange}
/>
</StyledTabContent>
</Tabs.Root>
);
};
@@ -0,0 +1,89 @@
import {
customEmojisFreqKey,
emojisFreqKey,
emojiSkinToneKey,
FREQUENTLY_USED_COUNT,
iconsFreqKey,
lastCustomEmojiKey,
lastEmojiKey,
lastIconKey,
sortFrequencies,
} from "./utils";
import usePersistedState from "~/hooks/usePersistedState";
import { EmojiSkinTone, IconType } from "@shared/types";
import React from "react";
const lastIconKeys = {
[IconType.Custom]: lastCustomEmojiKey,
[IconType.Emoji]: lastEmojiKey,
[IconType.SVG]: lastIconKey,
};
const freqIconKeys = {
[IconType.Custom]: customEmojisFreqKey,
[IconType.Emoji]: emojisFreqKey,
[IconType.SVG]: iconsFreqKey,
};
const skinToneKeys = {
[IconType.Custom]: "",
[IconType.Emoji]: emojiSkinToneKey,
[IconType.SVG]: "",
};
export const useIconState = (type: IconType) => {
const [emojiSkinTone, setEmojiSkinTone] = usePersistedState<EmojiSkinTone>(
skinToneKeys[type],
EmojiSkinTone.Default
);
const [iconFreq, setIconFreq] = usePersistedState<Record<string, number>>(
freqIconKeys[type],
{}
);
const [lastIcon, setLastIcon] = usePersistedState<string | undefined>(
lastIconKeys[type],
undefined
);
const incrementIconCount = React.useCallback(
(emoji: string) => {
iconFreq[emoji] = (iconFreq[emoji] ?? 0) + 1;
setIconFreq({ ...iconFreq });
setLastIcon(emoji);
},
[iconFreq, setIconFreq, setLastIcon]
);
const getFrequentIcons = React.useCallback((): string[] => {
const freqs = Object.entries(iconFreq);
if (freqs.length > FREQUENTLY_USED_COUNT.Track) {
const trimmed = sortFrequencies(freqs).slice(
0,
FREQUENTLY_USED_COUNT.Track
);
setIconFreq(Object.fromEntries(trimmed));
}
const emojis = sortFrequencies(freqs)
.slice(0, FREQUENTLY_USED_COUNT.Get)
.map(([emoji, _]) => emoji);
const isLastPresent = emojis.includes(lastIcon ?? "");
if (lastIcon && !isLastPresent) {
emojis.pop();
emojis.push(lastIcon);
}
return emojis;
}, [iconFreq, lastIcon, setIconFreq]);
return {
emojiSkinTone,
setEmojiSkinTone,
incrementIconCount,
getFrequentIcons,
};
};
+9
View File
@@ -18,6 +18,7 @@ export const TRANSLATED_CATEGORIES = {
Objects: i18next.t("Objects"),
Symbols: i18next.t("Symbols"),
Flags: i18next.t("Flags"),
Custom: i18next.t("Custom"),
};
export const FREQUENTLY_USED_COUNT = {
@@ -32,6 +33,8 @@ const STORAGE_KEYS = {
EmojisFrequency: "emojis-freq",
LastIcon: "last-icon",
LastEmoji: "last-emoji",
CustomEmojisFrequency: "custom-emojis-freq",
LastCustomEmoji: "last-custom-emoji",
};
const getStorageKey = (key: string) => `${STORAGE_KEYS.Base}.${key}`;
@@ -46,5 +49,11 @@ export const lastIconKey = getStorageKey(STORAGE_KEYS.LastIcon);
export const lastEmojiKey = getStorageKey(STORAGE_KEYS.LastEmoji);
export const customEmojisFreqKey = getStorageKey(
STORAGE_KEYS.CustomEmojisFrequency
);
export const lastCustomEmojiKey = getStorageKey(STORAGE_KEYS.LastCustomEmoji);
export const sortFrequencies = (freqs: [string, number][]) =>
freqs.sort((a, b) => (a[1] >= b[1] ? -1 : 1));
+16
View File
@@ -0,0 +1,16 @@
import { ArrowIcon as ArrowRightIcon } from "outline-icons";
import styled from "styled-components";
export { ArrowIcon as ArrowRightIcon } from "outline-icons";
export const ArrowUpIcon = styled(ArrowRightIcon)`
transform: rotate(-90deg);
`;
export const ArrowDownIcon = styled(ArrowRightIcon)`
transform: rotate(90deg);
`;
export const ArrowLeftIcon = styled(ArrowRightIcon)`
transform: rotate(180deg);
`;
+25 -69
View File
@@ -1,85 +1,41 @@
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { s } from "@shared/styles";
import lazyWithRetry from "~/utils/lazyWithRetry";
import DelayedMount from "./DelayedMount";
import Input, { Props as InputProps } from "./Input";
import NudeButton from "./NudeButton";
import Relative from "./Sidebar/components/Relative";
import Text from "./Text";
import { Popover, PopoverContent, PopoverTrigger } from "./primitives/Popover";
import { SwatchButton } from "./SwatchButton";
/**
* Props for the InputColor component.
*/
type Props = Omit<InputProps, "onChange"> & {
/** The current color value in hex format */
value: string | undefined;
/** Callback function invoked when the color value changes */
onChange: (value: string) => void;
};
const InputColor: React.FC<Props> = ({ value, onChange, ...rest }: Props) => {
const { t } = useTranslation();
/**
* A color input component that combines a text input with a color picker swatch button.
* Automatically formats hex color values with a leading # character.
*/
const InputColor: React.FC<Props> = ({ value, onChange, ...rest }: Props) => (
<Relative>
<Input
value={value}
onChange={(event) => onChange(event.target.value.replace(/^#?/, "#"))}
placeholder="#"
maxLength={7}
{...rest}
/>
<PositionedSwatchButton color={value} onChange={onChange} size={22} />
</Relative>
);
return (
<Relative>
<Input
value={value}
onChange={(event) => onChange(event.target.value.replace(/^#?/, "#"))}
placeholder="#"
maxLength={7}
{...rest}
/>
<Popover modal={true}>
<PopoverTrigger>
<SwatchButton aria-label={t("Show menu")} $background={value} />
</PopoverTrigger>
<StyledContent aria-label={t("Select a color")} align="end">
<React.Suspense
fallback={
<DelayedMount>
<Text>{t("Loading")}</Text>
</DelayedMount>
}
>
<StyledColorPicker
disableAlpha
color={value}
onChange={(color) => onChange(color.hex)}
/>
</React.Suspense>
</StyledContent>
</Popover>
</Relative>
);
};
const SwatchButton = styled(NudeButton)<{ $background: string | undefined }>`
background: ${(props) => props.$background};
border: 1px solid ${s("inputBorder")};
border-radius: 50%;
const PositionedSwatchButton = styled(SwatchButton)`
border: 1px solid ${(props) => props.theme.inputBorder};
position: absolute;
bottom: 20px;
bottom: 21px;
right: 6px;
`;
const StyledContent = styled(PopoverContent)`
width: auto;
padding: 8px;
`;
const ColorPicker = lazyWithRetry(
() => import("react-color/lib/components/chrome/Chrome")
);
const StyledColorPicker = styled(ColorPicker)`
background: inherit !important;
box-shadow: none !important;
border: 0 !important;
border-radius: 0 !important;
user-select: none;
input {
user-select: text;
color: ${s("text")} !important;
}
`;
export default InputColor;
+5 -16
View File
@@ -1,12 +1,10 @@
import * as VisuallyHidden from "@radix-ui/react-visually-hidden";
import { QuestionMarkIcon } from "outline-icons";
import { transparentize } from "polished";
import * as React from "react";
import styled from "styled-components";
import { s } from "@shared/styles";
import Text from "~/components/Text";
import useMobile from "~/hooks/useMobile";
import Separator from "./ContextMenu/Separator";
import Flex from "./Flex";
import { LabelText } from "./Input";
import NudeButton from "./NudeButton";
@@ -52,7 +50,7 @@ export type Item = {
export type Option = Item | Separator;
type Props = {
type Props = Omit<React.HTMLAttributes<HTMLButtonElement>, "onChange"> & {
/* Options to display in the select menu. */
options: Option[];
/* Current chosen value. */
@@ -219,9 +217,9 @@ const MobileSelect = React.forwardRef<HTMLButtonElement, MobileSelectProps>(
);
const renderOption = React.useCallback(
(option: Option) => {
(option: Option, idx: number) => {
if (option.type === "separator") {
return <Separator />;
return <InputSelectSeparator key={`separator-${idx}`} />;
}
const isSelected = option === selectedOption;
@@ -343,9 +341,9 @@ function Option({
{option.description && (
<>
&nbsp;
<Description type="tertiary" size="small" ellipsis>
<Text type="tertiary" size="small" ellipsis>
{option.description}
</Description>
</Text>
</>
)}
</OptionContainer>
@@ -361,15 +359,6 @@ const OptionContainer = styled(Flex)`
min-height: 24px;
`;
const Description = styled(Text)`
@media (hover: hover) {
&:hover,
&:focus {
color: ${(props) => transparentize(0.5, props.theme.accentText)};
}
}
`;
const IconWrapper = styled.span`
display: flex;
justify-content: center;
+31 -41
View File
@@ -4,21 +4,21 @@ import { Helmet } from "react-helmet-async";
import styled, { DefaultTheme } from "styled-components";
import breakpoint from "styled-components-breakpoint";
import { s } from "@shared/styles";
import { isModKey } from "@shared/utils/keyboard";
import Flex from "~/components/Flex";
import { LoadingIndicatorBar } from "~/components/LoadingIndicator";
import SkipNavContent from "~/components/SkipNavContent";
import SkipNavLink from "~/components/SkipNavLink";
import env from "~/env";
import useAutoRefresh from "~/hooks/useAutoRefresh";
import useKeyDown from "~/hooks/useKeyDown";
import { MenuProvider } from "~/hooks/useMenuContext";
import useStores from "~/hooks/useStores";
type Props = {
/** Main content to render in the layout. */
children?: React.ReactNode;
/** Page title to display in the browser tab. Defaults to app name if not provided. */
title?: string;
/** Left sidebar content. */
sidebar?: React.ReactNode;
/** Right sidebar content. */
sidebarRight?: React.ReactNode;
};
@@ -29,50 +29,40 @@ const Layout = React.forwardRef(function Layout_(
const { ui } = useStores();
const sidebarCollapsed = !sidebar || ui.sidebarIsClosed;
useAutoRefresh();
useKeyDown(".", (event) => {
if (isModKey(event)) {
ui.toggleCollapsedSidebar();
}
});
return (
<MenuProvider>
<Container column auto ref={ref}>
<Helmet>
<title>{title ? title : env.APP_NAME}</title>
</Helmet>
<Container column auto ref={ref}>
<Helmet>
<title>{title ? title : env.APP_NAME}</title>
</Helmet>
<SkipNavLink />
<SkipNavLink />
{ui.progressBarVisible && <LoadingIndicatorBar />}
{ui.progressBarVisible && <LoadingIndicatorBar />}
<Container auto>
<MenuProvider>{sidebar}</MenuProvider>
<Container auto>
{sidebar}
<SkipNavContent />
<Content
auto
justify="center"
$isResizing={ui.sidebarIsResizing}
$sidebarCollapsed={sidebarCollapsed}
$hasSidebar={!!sidebar}
style={
sidebarCollapsed
? undefined
: {
marginLeft: `${ui.sidebarWidth}px`,
}
}
>
{children}
</Content>
<SkipNavContent />
<Content
auto
justify="center"
$isResizing={ui.sidebarIsResizing}
$sidebarCollapsed={sidebarCollapsed}
$hasSidebar={!!sidebar}
style={
sidebarCollapsed
? undefined
: {
marginLeft: `${ui.sidebarWidth}px`,
}
}
>
{children}
</Content>
{sidebarRight}
</Container>
{sidebarRight}
</Container>
</MenuProvider>
</Container>
);
});
+2 -2
View File
@@ -1,7 +1,7 @@
import * as React from "react";
import lazyWithRetry from "~/utils/lazyWithRetry";
export interface LazyComponent<T extends React.ComponentType<unknown>> {
export interface LazyComponent<T extends React.ComponentType<any>> {
Component: React.LazyExoticComponent<T>;
preload: () => Promise<{ default: T }>;
}
@@ -34,7 +34,7 @@ interface LazyLoadOptions {
* MyComponent.preload();
* ```
*/
export function createLazyComponent<T extends React.ComponentType<unknown>>(
export function createLazyComponent<T extends React.ComponentType<any>>(
factory: () => Promise<{ default: T }>,
options: LazyLoadOptions = {}
): LazyComponent<T> {
File diff suppressed because it is too large Load Diff
@@ -2,7 +2,7 @@ import * as React from "react";
import useMeasure from "react-use-measure";
export const MeasuredContainer = <T extends React.ElementType>({
as: As,
as: As = "div",
name,
children,
...rest
+91
View File
@@ -0,0 +1,91 @@
import * as React from "react";
import { actionToMenuItem } from "~/actions";
import useActionContext from "~/hooks/useActionContext";
import useMobile from "~/hooks/useMobile";
import { ActionVariant, ActionWithChildren } from "~/types";
import { toMenuItems } from "./transformer";
import { observer } from "mobx-react";
import { useComputed } from "~/hooks/useComputed";
import { Menu, MenuContent, MenuTrigger } from "~/components/primitives/Menu";
import { MenuProvider } from "~/components/primitives/Menu/MenuContext";
type Props = {
/** Root action with children representing the menu items */
action?: ActionWithChildren;
/** Trigger for the menu */
children: React.ReactNode;
/** ARIA label for the menu */
ariaLabel: string;
/** Callback when menu is opened */
onOpen?: () => void;
/** Callback when menu is closed */
onClose?: () => void;
};
export const ContextMenu = observer(
({ action, children, ariaLabel, onOpen, onClose }: Props) => {
const isMobile = useMobile();
const contentRef = React.useRef<React.ElementRef<typeof MenuContent>>(null);
const actionContext = useActionContext({
isMenu: true,
});
const menuItems = useComputed(
() =>
((action?.children as ActionVariant[]) ?? []).map((childAction) =>
actionToMenuItem(childAction, actionContext)
),
[action?.children, actionContext]
);
const handleOpenChange = React.useCallback(
(open: boolean) => {
if (open) {
onOpen?.();
} else {
onClose?.();
}
},
[open, onOpen, onClose]
);
const enablePointerEvents = React.useCallback(() => {
if (contentRef.current) {
contentRef.current.style.pointerEvents = "auto";
}
}, []);
const disablePointerEvents = React.useCallback(() => {
if (contentRef.current) {
contentRef.current.style.pointerEvents = "none";
}
}, []);
const handleCloseAutoFocus = React.useCallback(
(e: Event) => e.preventDefault(),
[]
);
if (isMobile || !action || menuItems.length === 0) {
return <>{children}</>;
}
const content = toMenuItems(menuItems);
return (
<MenuProvider variant="context">
<Menu onOpenChange={handleOpenChange}>
<MenuTrigger aria-label={ariaLabel}>{children}</MenuTrigger>
<MenuContent
aria-label={ariaLabel}
onAnimationStart={disablePointerEvents}
onAnimationEnd={enablePointerEvents}
onCloseAutoFocus={handleCloseAutoFocus}
>
{content}
</MenuContent>
</Menu>
</MenuProvider>
);
}
);
+31 -39
View File
@@ -8,30 +8,24 @@ import {
DrawerTitle,
DrawerTrigger,
} from "~/components/primitives/Drawer";
import {
DropdownMenu as DropdownMenuRoot,
DropdownMenuTrigger,
DropdownMenuContent,
} from "~/components/primitives/DropdownMenu";
import { actionV2ToMenuItem } from "~/actions";
import { Menu, MenuContent, MenuTrigger } from "~/components/primitives/Menu";
import { MenuProvider } from "~/components/primitives/Menu/MenuContext";
import { actionToMenuItem } from "~/actions";
import useActionContext from "~/hooks/useActionContext";
import useMobile from "~/hooks/useMobile";
import {
ActionContext,
ActionV2Variant,
ActionV2WithChildren,
ActionVariant,
ActionWithChildren,
MenuItem,
MenuItemWithChildren,
} from "~/types";
import { toDropdownMenuItems, toMobileMenuItems } from "./transformer";
import { toMenuItems, toMobileMenuItems } from "./transformer";
import { observer } from "mobx-react";
import { useComputed } from "~/hooks/useComputed";
type Props = {
/** Root action with children representing the menu items */
action: ActionV2WithChildren;
/** Action context to use - new context will be created if not provided */
context?: ActionContext;
action: ActionWithChildren;
/** Trigger for the menu */
children: React.ReactNode;
/** Alignment w.r.t trigger - defaults to start */
@@ -52,7 +46,6 @@ export const DropdownMenu = observer(
(
{
action,
context,
children,
align = "start",
ariaLabel,
@@ -66,21 +59,18 @@ export const DropdownMenu = observer(
const [open, setOpen] = React.useState(false);
const isMobile = useMobile();
const contentRef =
React.useRef<React.ElementRef<typeof DropdownMenuContent>>(null);
const actionContext =
context ??
useActionContext({
isContextMenu: true,
});
React.useRef<React.ElementRef<typeof MenuContent>>(null);
const actionContext = useActionContext({
isMenu: true,
});
const menuItems = useComputed(() => {
if (!open) {
return [];
}
return (action.children as ActionV2Variant[]).map((childAction) =>
actionV2ToMenuItem(childAction, actionContext)
return (action.children as ActionVariant[]).map((childAction) =>
actionToMenuItem(childAction, actionContext)
);
}, [open, action.children, actionContext]);
@@ -126,24 +116,26 @@ export const DropdownMenu = observer(
);
}
const content = toDropdownMenuItems(menuItems);
const content = toMenuItems(menuItems);
return (
<DropdownMenuRoot open={open} onOpenChange={handleOpenChange}>
<DropdownMenuTrigger ref={ref} aria-label={ariaLabel} {...rest}>
{children}
</DropdownMenuTrigger>
<DropdownMenuContent
align={align}
aria-label={ariaLabel}
onAnimationStart={disablePointerEvents}
onAnimationEnd={enablePointerEvents}
onCloseAutoFocus={handleCloseAutoFocus}
>
{content}
{append}
</DropdownMenuContent>
</DropdownMenuRoot>
<MenuProvider variant="dropdown">
<Menu open={open} onOpenChange={handleOpenChange}>
<MenuTrigger ref={ref} aria-label={ariaLabel} {...rest}>
{children}
</MenuTrigger>
<MenuContent
align={align}
aria-label={ariaLabel}
onAnimationStart={disablePointerEvents}
onAnimationEnd={enablePointerEvents}
onCloseAutoFocus={handleCloseAutoFocus}
>
{content}
{append}
</MenuContent>
</Menu>
</MenuProvider>
);
}
)
+46 -50
View File
@@ -1,29 +1,22 @@
import { CheckmarkIcon } from "outline-icons";
import {
DropdownMenuButton,
DropdownMenuExternalLink,
DropdownMenuGroup,
DropdownMenuInternalLink,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownSubMenu,
DropdownSubMenuContent,
DropdownSubMenuTrigger,
} from "~/components/primitives/DropdownMenu";
import {
MenuButton,
MenuIconWrapper,
MenuInternalLink,
MenuExternalLink,
MenuLabel,
MenuSeparator,
MenuDisclosure,
SelectedIconWrapper,
} from "~/components/primitives/components/Menu";
SubMenu,
SubMenuTrigger,
SubMenuContent,
MenuGroup,
} from "~/components/primitives/Menu";
import * as Components from "~/components/primitives/components/Menu";
import { MenuItem } from "~/types";
import { MouseSafeArea } from "~/components/MouseSafeArea";
import { createRef } from "react";
export function toDropdownMenuItems(items: MenuItem[]) {
export function toMenuItems(items: MenuItem[]) {
const filteredItems = filterMenuItems(items);
const parentRef = createRef<HTMLDivElement>();
if (!filteredItems.length) {
return null;
@@ -39,15 +32,15 @@ export function toDropdownMenuItems(items: MenuItem[]) {
return filteredItems.map((item, index) => {
const icon = showIcon ? (
<MenuIconWrapper aria-hidden>
<Components.MenuIconWrapper aria-hidden>
{"icon" in item ? item.icon : null}
</MenuIconWrapper>
</Components.MenuIconWrapper>
) : undefined;
switch (item.type) {
case "button":
return (
<DropdownMenuButton
<MenuButton
key={`${item.type}-${item.title}-${index}`}
label={item.title as string}
icon={icon}
@@ -61,7 +54,7 @@ export function toDropdownMenuItems(items: MenuItem[]) {
case "route":
return (
<DropdownMenuInternalLink
<MenuInternalLink
key={`${item.type}-${item.title}-${index}`}
label={item.title as string}
icon={icon}
@@ -72,7 +65,7 @@ export function toDropdownMenuItems(items: MenuItem[]) {
case "link":
return (
<DropdownMenuExternalLink
<MenuExternalLink
key={`${item.type}-${item.title}-${index}`}
label={item.title as string}
icon={icon}
@@ -85,33 +78,36 @@ export function toDropdownMenuItems(items: MenuItem[]) {
);
case "submenu": {
const submenuItems = toDropdownMenuItems(item.items);
const submenuItems = toMenuItems(item.items);
if (!submenuItems?.length) {
return null;
}
return (
<DropdownSubMenu key={`${item.type}-${item.title}-${index}`}>
<DropdownSubMenuTrigger
<SubMenu key={`${item.type}-${item.title}-${index}`}>
<SubMenuTrigger
label={item.title as string}
icon={icon}
disabled={item.disabled}
/>
<DropdownSubMenuContent>{submenuItems}</DropdownSubMenuContent>
</DropdownSubMenu>
<SubMenuContent ref={parentRef}>
<MouseSafeArea parentRef={parentRef} />
{submenuItems}
</SubMenuContent>
</SubMenu>
);
}
case "group": {
const groupItems = toDropdownMenuItems(item.items);
const groupItems = toMenuItems(item.items);
if (!groupItems?.length) {
return null;
}
return (
<DropdownMenuGroup
<MenuGroup
key={`${item.type}-${item.title}-${index}`}
label={item.title as string}
items={groupItems}
@@ -120,7 +116,7 @@ export function toDropdownMenuItems(items: MenuItem[]) {
}
case "separator":
return <DropdownMenuSeparator key={`${item.type}-${index}`} />;
return <MenuSeparator key={`${item.type}-${index}`} />;
default:
return null;
@@ -149,15 +145,15 @@ export function toMobileMenuItems(
return filteredItems.map((item, index) => {
const icon = showIcon ? (
<MenuIconWrapper aria-hidden>
<Components.MenuIconWrapper aria-hidden>
{"icon" in item ? item.icon : null}
</MenuIconWrapper>
</Components.MenuIconWrapper>
) : undefined;
switch (item.type) {
case "button":
return (
<MenuButton
<Components.MenuButton
key={`${item.type}-${item.title}-${index}`}
disabled={item.disabled}
$dangerous={item.dangerous}
@@ -167,31 +163,31 @@ export function toMobileMenuItems(
}}
>
{icon}
<MenuLabel>{item.title}</MenuLabel>
<Components.MenuLabel>{item.title}</Components.MenuLabel>
{item.selected !== undefined && (
<SelectedIconWrapper aria-hidden>
{item.selected ? <CheckmarkIcon /> : null}
</SelectedIconWrapper>
<Components.SelectedIconWrapper aria-hidden>
{item.selected ? <CheckmarkIcon size={18} /> : null}
</Components.SelectedIconWrapper>
)}
</MenuButton>
</Components.MenuButton>
);
case "route":
return (
<MenuInternalLink
<Components.MenuInternalLink
key={`${item.type}-${item.title}-${index}`}
to={item.to}
disabled={item.disabled}
onClick={closeMenu}
>
{icon}
<MenuLabel>{item.title}</MenuLabel>
</MenuInternalLink>
<Components.MenuLabel>{item.title}</Components.MenuLabel>
</Components.MenuInternalLink>
);
case "link":
return (
<MenuExternalLink
<Components.MenuExternalLink
key={`${item.type}-${item.title}-${index}`}
href={typeof item.href === "string" ? item.href : item.href.url}
target={
@@ -201,8 +197,8 @@ export function toMobileMenuItems(
onClick={closeMenu}
>
{icon}
<MenuLabel>{item.title}</MenuLabel>
</MenuExternalLink>
<Components.MenuLabel>{item.title}</Components.MenuLabel>
</Components.MenuExternalLink>
);
case "submenu": {
@@ -217,7 +213,7 @@ export function toMobileMenuItems(
}
return (
<MenuButton
<Components.MenuButton
key={`${item.type}-${item.title}-${index}`}
disabled={item.disabled}
onClick={() => {
@@ -225,9 +221,9 @@ export function toMobileMenuItems(
}}
>
{icon}
<MenuLabel>{item.title}</MenuLabel>
<MenuDisclosure />
</MenuButton>
<Components.MenuLabel>{item.title}</Components.MenuLabel>
<Components.MenuDisclosure />
</Components.MenuButton>
);
}
@@ -244,14 +240,14 @@ export function toMobileMenuItems(
return (
<div key={`${item.type}-${item.title}-${index}`}>
<DropdownMenuLabel>{item.title}</DropdownMenuLabel>
<Components.MenuHeader>{item.title}</Components.MenuHeader>
{groupItems}
</div>
);
}
case "separator":
return <MenuSeparator key={`${item.type}-${index}`} />;
return <Components.MenuSeparator key={`${item.type}-${index}`} />;
default:
return null;
+22 -14
View File
@@ -22,6 +22,8 @@ type Props = {
isOpen: boolean;
title?: React.ReactNode;
style?: React.CSSProperties;
width?: number | string;
height?: number | string;
onRequestClose: () => void;
};
@@ -30,6 +32,8 @@ const Modal: React.FC<Props> = ({
isOpen,
title = "Untitled",
style,
width,
height,
onRequestClose,
}: Props) => {
const wasOpen = usePrevious(isOpen);
@@ -57,7 +61,7 @@ const Modal: React.FC<Props> = ({
>
{isMobile ? (
<Mobile>
<Content>
<MobileContent>
<Centered onClick={(ev) => ev.stopPropagation()} column>
{title && (
<Text size="xlarge" weight="bold">
@@ -66,7 +70,7 @@ const Modal: React.FC<Props> = ({
)}
<ErrorBoundary>{children}</ErrorBoundary>
</Centered>
</Content>
</MobileContent>
<Close onClick={onRequestClose}>
<CloseIcon size={32} />
</Close>
@@ -76,7 +80,7 @@ const Modal: React.FC<Props> = ({
</Back>
</Mobile>
) : (
<Small>
<Wrapper $width={width} $height={height}>
<Centered
onClick={(ev) => ev.stopPropagation()}
// maxHeight needed for proper overflow behavior in Safari
@@ -84,9 +88,9 @@ const Modal: React.FC<Props> = ({
column
reverse
>
<SmallContent style={style} shadow>
<DesktopContent style={style} topShadow>
<ErrorBoundary component="div">{children}</ErrorBoundary>
</SmallContent>
</DesktopContent>
<Header>
{title && <Text size="large">{title}</Text>}
<NudeButton onClick={onRequestClose}>
@@ -94,7 +98,7 @@ const Modal: React.FC<Props> = ({
</NudeButton>
</Header>
</Centered>
</Small>
</Wrapper>
)}
</StyledContent>
</Dialog.Portal>
@@ -142,7 +146,7 @@ const Mobile = styled.div`
outline: none;
`;
const Content = styled(Scrollable)`
const MobileContent = styled(Scrollable)`
width: 100%;
padding: 8vh 12px;
@@ -151,6 +155,10 @@ const Content = styled(Scrollable)`
`};
`;
const DesktopContent = styled(Scrollable)`
padding: 8px 24px 24px;
`;
const Centered = styled(Flex)`
width: 640px;
max-width: 100%;
@@ -205,16 +213,20 @@ const Header = styled(Flex)`
justify-content: space-between;
font-weight: 600;
padding: 24px 24px 12px;
flex-shrink: 0;
`;
const Small = styled.div`
const Wrapper = styled.div<{
$width?: number | string;
$height?: number | string;
}>`
animation: ${fadeAndScaleIn} 250ms ease;
margin: 25vh auto auto auto;
width: 75vw;
min-width: 350px;
max-width: 450px;
max-height: 65vh;
max-width: ${(props) => props.$width || "450px"};
max-height: ${(props) => props.$height || "70vh"};
z-index: ${depths.modal};
display: flex;
justify-content: center;
@@ -237,8 +249,4 @@ const Small = styled.div`
}
`;
const SmallContent = styled(Scrollable)`
padding: 8px 24px 24px;
`;
export default observer(Modal);

Some files were not shown because too many files have changed in this diff Show More