Compare commits

..

401 Commits

Author SHA1 Message Date
tommoor 9a94b74827 chore: Compressed inefficient images automatically 2025-09-21 20:06:10 +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
Hemachandar bd5de2e185 Display correct child document structure & auto open share section (#9854)
* Show correct child structure for shared documents

* useMemo dep

* auto open tree, show child docs in references

* document.children
2025-08-17 23:09:49 -04:00
codegen-sh[bot] b8eefe4b78 Add functionality to split code blocks with triple backticks (#9959)
* Add functionality to split code blocks with triple backticks

This change allows users to split a code block into two by typing three backticks within it.
When a user types three backticks inside a code block, the block will be split at that point,
creating a new code block below with the same language.

Fixes #9958

* testing

* refactor

---------

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
Co-authored-by: Tom Moor <tom@getoutline.com>
2025-08-17 22:08:20 -04:00
Tom Moor cc8a3d8b5e chore: Still seeing redis connection failures in CI (#9957) 2025-08-17 18:42:20 -04:00
Tom Moor dd061790a8 fix: Frontend requests do not send Content-Type header in request (#9956)
* Revert "Revert "fix: Frontend requests do not send Content-Type in request (#…"

This reverts commit 7fddd99c28.

* Update authentication.ts
2025-08-17 21:47:06 +00:00
Tom Moor 7fddd99c28 Revert "fix: Frontend requests do not send Content-Type in request (#9954)" (#9955)
This reverts commit 7ab7e6efb7.
2025-08-17 21:42:28 +00:00
Tom Moor 7ab7e6efb7 fix: Frontend requests do not send Content-Type in request (#9954) 2025-08-17 16:02:56 -04:00
Tom Moor 4464d3c8b4 chore: More CSP hardening (#9951) 2025-08-16 22:10:28 +00:00
Tom Moor e0f40f9bc1 chore: Make no-unused-vars rule strict (#9950) 2025-08-16 11:29:11 -04:00
Tom Moor c83a6b4f41 fix: Usage of ctx.attachment overrides explicit Content-Type (#9949) 2025-08-16 11:29:00 -04:00
Tom Moor 82994c7b7b fix: Scroll to anchor reliability (#9945)
* fix: Scroll to anchor reliability

* feedback
2025-08-16 08:18:35 -04:00
Tom Moor e891de7f49 fix: Document move/copy/publish dialog behavior (#9947)
* fix: Document move/copy/publish dialog behavior

* More fixes
2025-08-16 08:18:25 -04:00
dependabot[bot] fc2648becf chore(deps): bump mammoth from 1.9.1 to 1.10.0 (#9908)
Bumps [mammoth](https://github.com/mwilliamson/mammoth.js) from 1.9.1 to 1.10.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.9.1...1.10.0)

---
updated-dependencies:
- dependency-name: mammoth
  dependency-version: 1.10.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-15 20:41:40 -04:00
Tom Moor ff548eae5c chore: Remove usage of vite-static-copy plugin (#9916) 2025-08-15 20:41:27 -04:00
Tom Moor 866a7f264b Upgrade vite-plugin-pwa (#9943) 2025-08-15 20:33:13 -04:00
Tom Moor 8fcb629bdf fix: Standardize request filtering between cloud / self-hosted (#9914)
* fix: Add request-filtering-agent to self-hosted environment

* refactor

* Debug logging

* self-review

* Remove unused AbortController

* test

* test

* Address feedback
2025-08-15 07:16:29 -04:00
Tom Moor 99655c65d4 fix: Drafts without a collection should be publishable by all members with update rights (#9941) 2025-08-15 07:16:09 -04:00
Tom Moor 5e176415ab fix: Line-height too compact on editor headings (#9942)
closes #9932
2025-08-15 07:16:00 -04:00
Tom Moor 34555bce86 fix: Properly truncate multiline labels in sidebar (#9940) 2025-08-15 00:10:44 -04:00
Tom Moor dcd7a050bd chore: Formatting (#9939) 2025-08-14 22:49:07 -04:00
Tom Moor dc0df7c7e9 fix: Prevent both img parseDOM rules matching (#9938)
closes #9930
2025-08-14 22:09:24 -04:00
codegen-sh[bot] e2c8ee7b54 chore: Migrate from dotenv to dotenvx with minimal changes (#9921)
Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
2025-08-14 19:43:42 -04:00
Tom Moor d2a0ddab12 fix: Increase timeout on remote file storage operations, make configurable for edge cases (#9936) 2025-08-14 19:39:36 -04:00
Tom Moor 31b254ff09 chore: Upgrade request-filtering-agent (#9937) 2025-08-14 19:39:14 -04:00
Volodymyr Koval 025af4f9fd docs: Update architecture docs to reflect Oxlint usage (#9935)
🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-14 17:44:51 -04:00
Tom Moor 221169db51 fix: Remove mime-types usage from the browser – fixes dev/vite warnings (#9926) 2025-08-14 08:02:13 -04:00
Tom Moor d2a50256b0 fix: Remove attachments.redirect sw caching (#9927) 2025-08-14 08:02:00 -04:00
Tom Moor 6bc80720c9 fix: Allow user account lookup with mismatching email capitalization (#9929) 2025-08-14 08:01:51 -04:00
Tom Moor 23106bfce8 fix: Use safeEqual in VerificationCode verify method (#9915) 2025-08-13 22:45:11 -04:00
Tom Moor e8046f0d2f fix: Tighten rate limits on email.callback endpoint from defaults (#9917) 2025-08-13 22:45:03 -04:00
Tom Moor ba8ade0244 chore: Add some additional debugging around auth failures (#9924) 2025-08-13 22:44:53 -04:00
Translate-O-Tron 119eb92f27 New Crowdin updates (#9805)
* 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 Dutch translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: New Korean translations from Crowdin [ci skip]
2025-08-11 22:11:12 -04:00
dependabot[bot] c26a75af27 chore(deps-dev): bump rollup-plugin-webpack-stats from 2.1.0 to 2.1.3 (#9907)
Bumps [rollup-plugin-webpack-stats](https://github.com/relative-ci/rollup-plugin-webpack-stats) from 2.1.0 to 2.1.3.
- [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.0...v2.1.3)

---
updated-dependencies:
- dependency-name: rollup-plugin-webpack-stats
  dependency-version: 2.1.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-08-11 20:45:05 -04:00
dependabot[bot] 10f508a8dd chore(deps-dev): bump typescript from 5.8.3 to 5.9.2 (#9906)
Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.8.3 to 5.9.2.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release-publish.yml)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.8.3...v5.9.2)

---
updated-dependencies:
- dependency-name: typescript
  dependency-version: 5.9.2
  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-11 19:59:02 -04:00
dependabot[bot] d872293551 chore(deps): bump validator and @types/validator (#9905)
Bumps [validator](https://github.com/validatorjs/validator.js) and [@types/validator](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/validator). These dependencies needed to be updated together.

Updates `validator` from 13.15.0 to 13.15.15
- [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.0...13.15.15)

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

---
updated-dependencies:
- dependency-name: validator
  dependency-version: 13.15.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: "@types/validator"
  dependency-version: 13.15.2
  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-08-11 19:35:00 -04:00
dependabot[bot] e419aa6c3a chore(deps): bump nodemailer from 6.10.0 to 6.10.1 (#9904)
Bumps [nodemailer](https://github.com/nodemailer/nodemailer) from 6.10.0 to 6.10.1.
- [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.0...v6.10.1)

---
updated-dependencies:
- dependency-name: nodemailer
  dependency-version: 6.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-11 19:22:49 -04:00
dependabot[bot] ba60d4bc0a chore(deps): bump @octokit/webhooks from 13.8.0 to 13.9.1 (#9903)
Bumps [@octokit/webhooks](https://github.com/octokit/webhooks.js) from 13.8.0 to 13.9.1.
- [Release notes](https://github.com/octokit/webhooks.js/releases)
- [Commits](https://github.com/octokit/webhooks.js/compare/v13.8.0...v13.9.1)

---
updated-dependencies:
- dependency-name: "@octokit/webhooks"
  dependency-version: 13.9.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-11 19:22:28 -04:00
codegen-sh[bot] b1dffc3486 Convert insights from sidebar to modal dialog (#9892)
* Convert insights from sidebar to modal dialog

- Remove insights routing and sidebar layout
- Update DocumentMeta to use modal action instead of navigation
- Convert Insights component to modal-ready format
- Clean up route helpers and authenticated layout
- Remove insights route from authenticated routes

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

* Applied automatic fixes

* refactor

* singular

* refactor

---------

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
Co-authored-by: Tom Moor <tom@getoutline.com>
2025-08-11 18:57:27 -04:00
codegen-sh[bot] 4fc6ac1f15 Add in-app reaction notifications (#9893)
* Add ReactionsCreate notification event type

- Add ReactionsCreate to NotificationEventType enum and defaults
- Add notification settings UI with SmileyIcon and proper labels
- Create ReactionsCreateNotificationsTask to handle comment reactions
- Update NotificationsProcessor to handle comments.add_reaction events
- Add eventText and path handling in client Notification model
- Notifications are enabled by default but never send emails

* Applied automatic fixes

* Show the actual emoji in the notification

* Cleanup notifications if reaction is removed

* PR feedback

---------

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
Co-authored-by: Tom Moor <tom@getoutline.com>
2025-08-11 18:54:43 -04:00
dependabot[bot] 289302fd2e chore(deps): bump fs-extra from 11.3.0 to 11.3.1 (#9899)
Bumps [fs-extra](https://github.com/jprichardson/node-fs-extra) from 11.3.0 to 11.3.1.
- [Changelog](https://github.com/jprichardson/node-fs-extra/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jprichardson/node-fs-extra/compare/11.3.0...11.3.1)

---
updated-dependencies:
- dependency-name: fs-extra
  dependency-version: 11.3.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-11 18:26:53 -04:00
dependabot[bot] 00db4010d0 chore(deps): bump the aws group with 5 updates (#9900)
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.859.0` | `3.864.0` |
| [@aws-sdk/lib-storage](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/lib/lib-storage) | `3.859.0` | `3.864.0` |
| [@aws-sdk/s3-presigned-post](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-presigned-post) | `3.859.0` | `3.864.0` |
| [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-request-presigner) | `3.859.0` | `3.864.0` |
| [@aws-sdk/signature-v4-crt](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/signature-v4-crt) | `3.858.0` | `3.864.0` |


Updates `@aws-sdk/client-s3` from 3.859.0 to 3.864.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.864.0/clients/client-s3)

Updates `@aws-sdk/lib-storage` from 3.859.0 to 3.864.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.864.0/lib/lib-storage)

Updates `@aws-sdk/s3-presigned-post` from 3.859.0 to 3.864.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.864.0/packages/s3-presigned-post)

Updates `@aws-sdk/s3-request-presigner` from 3.859.0 to 3.864.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.864.0/packages/s3-request-presigner)

Updates `@aws-sdk/signature-v4-crt` from 3.858.0 to 3.864.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.864.0/packages/signature-v4-crt)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.864.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/lib-storage"
  dependency-version: 3.864.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-presigned-post"
  dependency-version: 3.864.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-request-presigner"
  dependency-version: 3.864.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/signature-v4-crt"
  dependency-version: 3.864.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-11 18:26:45 -04:00
dependabot[bot] bc14699994 chore(deps): bump dd-trace from 5.41.1 to 5.62.0 (#9901)
Bumps [dd-trace](https://github.com/DataDog/dd-trace-js) from 5.41.1 to 5.62.0.
- [Release notes](https://github.com/DataDog/dd-trace-js/releases)
- [Commits](https://github.com/DataDog/dd-trace-js/compare/v5.41.1...v5.62.0)

---
updated-dependencies:
- dependency-name: dd-trace
  dependency-version: 5.62.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-11 18:26:37 -04:00
Tom Moor 013a7a6d39 Improved error boundary with option to clear cache on repeated errors… (#9891)
* Improved error boundary with option to clear cache on repeated errors and more detail for reporting

* fix

* db
2025-08-10 15:24:12 -04:00
codegen-sh[bot] c3f93a3e9d Add relationships API endpoints (#9402)
* Migrate Backlink model to generic Relationship model

- Create new Relationship model with type field to support different relationship types
- Add database migration to create relationships table and migrate existing backlinks
- Update Backlink model to delegate to Relationship model for backward compatibility
- Update BacklinksProcessor to use Relationship model with backlink type
- Update API routes to use new Relationship model
- Update test files to use Relationship model
- Maintain backward compatibility through database view and model delegation

Fixes #9366

* Update migration to rename table instead of creating new one

- Rename existing backlinks table to relationships instead of creating new table
- Add type column with default value to existing table
- Update existing rows to have type='backlink'
- Avoid expensive data migration by keeping existing data in place
- Maintain backward compatibility with database view
- Update rollback to reverse table rename and column addition

This approach is much more efficient for large datasets as it avoids copying millions of rows.

* Remove unnecessary UPDATE statement from migration

The UPDATE statement is not needed since defaultValue automatically
applies to existing rows when adding a column with a default value.

Thanks @tommoor for catching this!

* Wrap up migration in transaction

- Wrap all migration operations in a transaction for atomicity
- Add transaction parameter to all queryInterface calls
- Follow the same pattern as other migrations in the codebase
- Ensures all operations succeed or fail together

* Remove Backlink class entirely and use Relationship everywhere

- Delete server/models/Backlink.ts
- Remove Backlink export from server/models/index.ts
- Remove Backlink import and association from Document model
- All functionality now uses Relationship model with RelationshipType.Backlink
- Maintains same API through Relationship model methods
- Cleaner architecture with single relationship model

* Update documents.test.ts to use RelationshipType enum instead of string

- Import RelationshipType from Relationship model
- Replace type: "backlink" with type: RelationshipType.Backlink
- Improves type safety and consistency with enum usage

* Address code review feedback

- Add transaction wrapper to migration down method for safer rollback
- Remove unused findByTypeForUser method from Relationship model
- Method wasn't used and won't work for all relationship types (e.g., user mentions)
- Clean up code structure and improve safety

* Restore imports

* Add relationships API endpoints

- Create relationships API following stars pattern
- Add CRUD operations: create, list, delete
- Include proper validation, authentication, and authorization
- Support filtering by relationship type and document IDs
- Add relationship presenter and policies
- Register routes in main API router

* Remove relationships.create and relationships.delete endpoints

- Keep only relationships.list endpoint as requested
- Remove create and delete schemas from validation
- Update policies to only allow read operations
- Relationships will be managed internally, not via external API

* Add relationships.info endpoint

- Use Document.findByPk for authorization as requested
- Find relationship by ID and verify user has access to related document
- Return relationship details with accessible documents
- Include proper validation schema for UUID parameter

* Update 20250601223331-migrate-backlink-to-relationship.js

* Update Relationship.ts

* wip

* test

* Final tweaks

---------

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
Co-authored-by: Tom Moor <tom@getoutline.com>
2025-08-10 15:24:01 -04:00
Tom Moor c5cd4d9335 fix: Remove nullable on teams.name column (#9890)
* fix: Remove nullable on teams.name column

* Add test
2025-08-10 14:02:01 -04:00
Hemachandar c2f84466df chore: Move TableOfContentsMenu & BreadcrumbMenu to Radix (#9882)
* mobile toc menu

* indent heading levels, filter > H3

* breadcrumb menu

* tiny
2025-08-09 21:59:46 -04:00
Tom Moor 00d7239601 v0.86.1 (#9885) 2025-08-09 18:00:47 -04:00
Hemachandar 5c070d0428 chore: Move more dropdowns to Radix (part 2) (#9880)
* revision menu

* new child document menu; dropdown trigger now supports tooltip

* import menu

* oauth authentication

* oauth client

* tsc
2025-08-09 14:18:19 -04:00
Hemachandar 4822261187 chore: Move DocumentMenu to Radix (#9879)
* works without custom items in menu

* tooltip support for dropdown button

* fix: menu goes out of bounds

* custom node, lazy render menu for perf

* bug fixes

* fix: useTemplateActions re-computed templates on every render

* fix: hide subscribe/unsubscribe for inactive documents & collections

* cleanup `MenuContext` & `useTemplateMenuItems`

* rename prop, increase dropdown max-height

* jsdoc
2025-08-09 18:11:20 +05:30
Tom Moor cda503e7af fix: Correct transaction usage through team creation process (#9878)
* fix: Correct transaction usage through team creation process

* refactor
2025-08-09 07:47:39 -04:00
Hemachandar 45cc5ee20f chore: Move CollectionMenu to Radix (#9874)
* chore: Move `CollectionMenu` to Radix

* action button supports v2

* rename button

* respect disabled pointer-events

* separator should go all the way

* Revert "separator should go all the way"

This reverts commit c76afa1252.
2025-08-09 13:47:13 +05:30
Neha Prasad ecf8783af0 fix: undefined width/height when pasting HTML images (#9868)
* Remove the extra padding from the transform calculation to match the image implementation
Improve the scrollable area handling for full-width tables

* Fix image dimensions parsing for HTML paste operations

* Update Styles.ts

---------

Co-authored-by: Tom Moor <tom@getoutline.com>
2025-08-08 11:52:54 +00:00
Tom Moor 7b7af55826 feat: Convert attachment links to absolute when copying Markdown (#9873) 2025-08-08 07:49:27 -04:00
Hemachandar 5c0fc87504 chore: Move more dropdowns to Radix (#9871)
* collection group member menu

* comment menu

* group member menu

* group menu; added support for disabled items

* insights menu

* member menu

* notifications menu

* share menu

* alignment

* tsc, graphite feedback

* remove unused `CollectionGroupMenu` & `MemberMenu`
2025-08-08 08:54:11 +05:30
Tom Moor 93cda9327a fix: Admins unable to revoke workspace API keys created by other users (#9865)
* fix: Admins unable to revoke workspace API keys created by other users

closes #9864

* fix policy
2025-08-07 22:49:23 -04:00
Hemachandar 888add7920 Use shareLoader for loading public docs in documents.info endpoint (#9822)
* Use `shareLoader` for loading public docs in `documents.info` endpoint

* cleanup sharedCache in documents store

* remove shareId path param for authenticated routes

* require auth in document loader

---------

Co-authored-by: Tom Moor <tom@getoutline.com>
2025-08-07 22:41:04 -04:00
Apoorv Mishra 7dfc4fb57d Don't shift body horizontally when dialogs open (#9872)
* fix: don't shift body horizontally when dialogs open

* Update globals.ts

---------

Co-authored-by: Tom Moor <tom@getoutline.com>
2025-08-08 01:39:56 +00:00
Tom Moor 6ef03d3aa7 chore: Move SkinTonePicker to Radix popover (#9866)
* chore: Move SkinTonePicker to Radix popover

towards #6456

* fix: Graphite PR review

* fix: Even up padding
2025-08-07 21:31:14 -04:00
Hemachandar d5510f4454 fix: Allow users to revoke a collection share (#9870) 2025-08-08 00:07:04 +05:30
Hemachandar 5b19f7c711 fix: Prefetch non-archived notificatios only (#9869) 2025-08-07 22:22:53 +05:30
Hemachandar f28f41eb76 Add sub-menu support for dropdown (#9787)
* sub menu primitives

* user menu

* mobile sub menu

* fluid transition for mobile sub-menu

* account menu

* selected icon

* submenu icon

* tsc: migrate TeamMenu

* reactivity
2025-08-07 08:21:27 +05:30
Hemachandar 80b9fb1daa fix: Full-width nodes misaligned when ToC is open in full-width document (#9860)
* fix: Full-width nodes misaligned when ToC is open in full-width document

* Remove extra div

---------

Co-authored-by: Tom Moor <tom@getoutline.com>
2025-08-06 20:46:23 -04:00
Tom Moor 80c547c1fc Merge branch 'main' of github.com:outline/outline 2025-08-06 20:06:49 -04:00
Hemachandar 04c3d81b1f chore: Setup missing oxlint configs (#9862)
* shared

* server

* app

* remove vestigial eslintrc files

* update comment directives
2025-08-06 19:54:22 -04:00
Hemachandar f41f93d6c9 Move ColorPicker to Radix popover (#9863) 2025-08-06 19:52:15 -04:00
dependabot[bot] 74acf9601b chore(deps): bump tmp from 0.2.3 to 0.2.4 (#9861)
Bumps [tmp](https://github.com/raszi/node-tmp) from 0.2.3 to 0.2.4.
- [Changelog](https://github.com/raszi/node-tmp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/raszi/node-tmp/compare/v0.2.3...v0.2.4)

---
updated-dependencies:
- dependency-name: tmp
  dependency-version: 0.2.4
  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-06 18:25:49 -04:00
Tom Moor 5bdabc13a6 v0.86.0 2025-08-06 07:58:46 -04:00
Hemachandar 6a6e77b52a Cleanup reakit popover (#9853) 2025-08-06 07:33:25 -04:00
Tom Moor a97908518a fix: Various issues with JSON import and UTF8 or very long filenames (#9850) 2025-08-05 21:15:45 -04:00
Tom Moor a48aabdd66 fix: /auth/ and /s/ url paths should not be allowed as post-login redirect (#9849) 2025-08-05 18:58:28 -04:00
Tom Moor 84f45b7fb7 fix: refresh token rotation is broken in OAuth provider implementation (#9847)
* fix: Incorrect clientId used in getAccessToken response

* fix: getRefreshToken as well
2025-08-05 18:34:52 -04:00
Hemachandar 9bb802c35c Migrate SearchPopover to Radix (#9844) 2025-08-05 17:42:06 -04:00
Hemachandar 8ad097aef2 fix: Flash of 404 page due to timing diff between React & MobX state (#9845) 2025-08-05 17:39:44 -04:00
Hemachandar 0cfd968f1e chore: Fix console errors (#9841) 2025-08-05 11:32:05 -04:00
codegen-sh[bot] c6fc6230ff Replace useComponentSize with react-use-measure (#9838)
* Replace useComponentSize with react-use-measure

- Add react-use-measure dependency
- Replace all useComponentSize usages with useMeasure hook
- Remove useComponentSize hook file
- Update imports across all affected components:
  - app/components/Header.tsx
  - app/components/ResizingHeightContainer.tsx
  - app/scenes/Document/components/Header.tsx
  - app/scenes/Document/components/MeasuredContainer.tsx
  - shared/editor/embeds/Berrycast.tsx

Fixes #9812

* Remove unused breadcrumbsRef

The breadcrumbsRef is no longer needed since we're using breadcrumbsMeasureRef directly for measuring the breadcrumbs element.

---------

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
2025-08-05 10:40:10 -04:00
Hemachandar ca2258ecc7 Ensure auth when loading non-public share info (#9840) 2025-08-05 09:01:56 -04:00
Hemachandar bf87f2dfee Autofocus search input when opening icon picker (#9839) 2025-08-05 08:47:16 -04:00
Hemachandar b4757dfc6c Allow disconnecting analytics integrations (#9823)
* Allow disconnecting analytic integrations

* review
2025-08-05 07:36:48 -04:00
dependabot[bot] b028b541a8 chore(deps): bump the sentry group with 2 updates (#9832)
Bumps the sentry group with 2 updates: [@sentry/node](https://github.com/getsentry/sentry-javascript) and [@sentry/react](https://github.com/getsentry/sentry-javascript).


Updates `@sentry/node` from 7.120.3 to 7.120.4
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/7.120.4/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.120.3...7.120.4)

Updates `@sentry/react` from 7.120.3 to 7.120.4
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/7.120.4/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.120.3...7.120.4)

---
updated-dependencies:
- dependency-name: "@sentry/node"
  dependency-version: 7.120.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: sentry
- dependency-name: "@sentry/react"
  dependency-version: 7.120.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: sentry
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-04 22:24:34 -04:00
Tom Moor 8dc6bcba22 fix: Add option as to whether state cookie is considered for team context (#9831)
* fix: Do not consider host in cookies state unless its a workspace subdomain

* fix: Add option as to whether state cookie is considered in team context
2025-08-04 19:50:32 -04:00
Tom Moor 2f418e7c2e fix: Missing ROOT_SHARE_ID default (#9830) 2025-08-04 23:30:29 +00:00
dependabot[bot] 02e5c7663b chore(deps): bump the aws group with 5 updates (#9829)
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.850.0` | `3.859.0` |
| [@aws-sdk/lib-storage](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/lib/lib-storage) | `3.850.0` | `3.859.0` |
| [@aws-sdk/s3-presigned-post](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-presigned-post) | `3.850.0` | `3.859.0` |
| [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-request-presigner) | `3.850.0` | `3.859.0` |
| [@aws-sdk/signature-v4-crt](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/signature-v4-crt) | `3.848.0` | `3.858.0` |


Updates `@aws-sdk/client-s3` from 3.850.0 to 3.859.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.859.0/clients/client-s3)

Updates `@aws-sdk/lib-storage` from 3.850.0 to 3.859.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.859.0/lib/lib-storage)

Updates `@aws-sdk/s3-presigned-post` from 3.850.0 to 3.859.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.859.0/packages/s3-presigned-post)

Updates `@aws-sdk/s3-request-presigner` from 3.850.0 to 3.859.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.859.0/packages/s3-request-presigner)

Updates `@aws-sdk/signature-v4-crt` from 3.848.0 to 3.858.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.858.0/packages/signature-v4-crt)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.859.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/lib-storage"
  dependency-version: 3.859.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-presigned-post"
  dependency-version: 3.859.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-request-presigner"
  dependency-version: 3.859.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/signature-v4-crt"
  dependency-version: 3.858.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-04 19:22:19 -04:00
dependabot[bot] ca0c7d256b chore(deps): bump zod from 3.24.2 to 3.25.76 (#9827)
Bumps [zod](https://github.com/colinhacks/zod) from 3.24.2 to 3.25.76.
- [Release notes](https://github.com/colinhacks/zod/releases)
- [Commits](https://github.com/colinhacks/zod/compare/v3.24.2...v3.25.76)

---
updated-dependencies:
- dependency-name: zod
  dependency-version: 3.25.76
  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-04 19:22:06 -04:00
dependabot[bot] 2a16d76cb3 chore(deps): bump prosemirror-model from 1.25.0 to 1.25.2 (#9828)
Bumps [prosemirror-model](https://github.com/prosemirror/prosemirror-model) from 1.25.0 to 1.25.2.
- [Changelog](https://github.com/ProseMirror/prosemirror-model/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prosemirror/prosemirror-model/compare/1.25.0...1.25.2)

---
updated-dependencies:
- dependency-name: prosemirror-model
  dependency-version: 1.25.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-08-04 19:21:54 -04:00
dependabot[bot] 3c251adf96 chore(deps): bump @fast-csv/parse from 5.0.2 to 5.0.5 (#9826)
Bumps [@fast-csv/parse](https://github.com/C2FO/fast-csv/tree/HEAD/packages/parse) from 5.0.2 to 5.0.5.
- [Release notes](https://github.com/C2FO/fast-csv/releases)
- [Changelog](https://github.com/C2FO/fast-csv/blob/main/packages/parse/CHANGELOG.md)
- [Commits](https://github.com/C2FO/fast-csv/commits/v5.0.5/packages/parse)

---
updated-dependencies:
- dependency-name: "@fast-csv/parse"
  dependency-version: 5.0.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-04 19:21:40 -04:00
Hemachandar 129f8f0520 fix: Generate sitemap for shares with custom slugs (#9820)
* fix: Generate sitemap for shares with custom slugs

* throw 400 when teamId is not present
2025-08-04 09:21:58 -04:00
Max Koon 6cde7fed51 fix: duplicate highlight shortcut key label (#9806)
* fix: duplicate key label

* Update shortcut

---------

Co-authored-by: Tom Moor <tom@getoutline.com>
2025-08-03 22:48:09 -04:00
Tom Moor 34059ce0ef chore: More missing transactions (#9811)
* fix: Missing transaction in notifications.update_all

* lint
2025-08-03 21:52:10 -04:00
Tom Moor 08a0b72706 fix: Unable to load share from custom slug (#9808)
* fix: Unable to load share from custom slug

closes #9807
regressed in #9529

* fix: Incorrect share ID passed through with custom slug
2025-08-03 18:52:20 -04:00
codegen-sh[bot] 6c2313919b Improve webhook disabling heuristic with time-based analysis (#9800)
* Improve webhook disabling heuristic with time-based analysis

- Add WEBHOOK_FAILURE_TIME_WINDOW environment variable (default: 24 hours)
- Add WEBHOOK_FAILURE_RATE_THRESHOLD environment variable (default: 80%)
- Replace simple count-based logic with time-window failure rate analysis
- Require minimum 5 deliveries in time window before disabling
- Add comprehensive logging for webhook failure analysis
- Update email template to reflect new time-based approach

Addresses GitHub issue #9788 by making webhook disabling smarter and
considering time factors instead of just consecutive failure counts.

* Fix Logger.warn type error

Remove extra 'task' parameter from Logger.warn call to match the correct
signature: Logger.warn(message: string, extra?: Extra)

* Update WEBHOOK_FAILURE_TIME_WINDOW to use seconds instead of hours

- Change default value from 24 (hours) to 86400 (seconds)
- Update time conversion logic from hours*60*60*1000 to seconds*1000
- Rename timeWindowHours variable to timeWindowSeconds for clarity
- Update documentation comments to reflect seconds-based configuration

This provides more granular control while maintaining the same default
behavior (24 hours = 86400 seconds).

* Improve webhook failure logging and analysis threshold

- Only log webhook failure analysis when failedDeliveries.length > 0
- Extract hardcoded minimum deliveries threshold to constant
- Update minimum deliveries threshold from 5 to 10 for more reliable analysis

This reduces log noise when there are no failures and requires more data
points before considering webhook disabling.

---------

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
2025-08-03 15:39:04 -04:00
Tom Moor 73f079f32b fix: Missing transaction in UploadAttachmentFromUrlTask (#9803) 2025-08-03 13:43:47 -04:00
Hemachandar d3eb3db7ba feat: Public sharing of collections (#9529)
* shares.info, collections.info, documents.info

* shares.list, shares.create, shares.update

* shares.sitemap

* parity with existing document shared screen

* collection share popover

* parent share and table

* collection scene

* collection link in sidebar

* sidebar and breadcrumb collection link click

* collection link click in editor

* meta

* more meta + 404 page

* map internal link, remove showLastUpdated option

* fix shares.list pagination

* show last updated

* shareLoader tests

* lint

* sidebar context for collection link

* badge in shares table

* fix existing tests

* tsc

* update failing test snapshot

* env

* signed url for collection attachments

* include collection content in SSR for screen readers

* search

* drafts can be shared

* review

* tsc, remove old shared-doc scene

* tweaks

* DRY

* refactor loader

* Remove share/collection urls

* fix: Collection overview should not be editable when viewing shared link and logged in

* Tweak public breadcrumb

* fix: Deleted documents should never be exposed through share

* empty sharedTree array where includeChildDocuments is false

* revert includeChildDocs guard for logical correctness + SSR bug fix

* fix: check document is part of share

---------

Co-authored-by: Tom Moor <tom@getoutline.com>
2025-08-03 13:07:39 -04:00
dependabot[bot] f7826c7d91 chore(deps): bump koa from 2.16.1 to 3.0.1 (#9766)
* chore(deps): bump koa from 2.16.1 to 3.0.1

Bumps [koa](https://github.com/koajs/koa) from 2.16.1 to 3.0.1.
- [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/v2.16.1...v3.0.1)

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

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

* Update ctx.throw usage

---------

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-02 19:31:23 -04:00
Tom Moor 09e835d935 fix: Mutation endpoints missing transactions (#9799) 2025-08-02 11:14:32 -04:00
Tom Moor a139d599b7 fix: Nested lists flatten in confluence import (#9797)
Towards #9789
2025-08-02 09:04:26 -04:00
Tom Moor 114e240958 fix: Remove revision data from events as it is immutable anyway (#9796) 2025-08-02 08:17:24 -04:00
Tom Moor ce3bba9f33 fix: Error in TableLayoutPlugin when table does not exist in previous state (#9795) 2025-08-02 12:10:57 +00:00
Translate-O-Tron 6ee275acc7 New Crowdin updates (#9725)
* fix: New Polish translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

* fix: New German translations from Crowdin [ci skip]
2025-08-02 07:17:53 -04:00
Tom Moor bbe16f26e4 fix: Toggling table full width should always go fullwidth (#9792) 2025-08-02 07:17:37 -04:00
Tom Moor a0308cf809 fix: Allow <br> tags to represent breaks in API markdown input (#9794)
* fix: Allow <br> tags to represent breaks in API markdown input

* tests
2025-08-02 07:17:29 -04:00
Tom Moor e2d6c89549 fix: Remote results causes cmd+k state reset (#9793) 2025-08-02 07:17:19 -04:00
Tom Moor 1d0c2225d0 fix: Col/row selection on table with merged cells (#9786)
* fix: Col/row selection on table with merged cells

* skip unrelated tests
2025-07-31 22:07:57 -04:00
Tom Moor 6efd23ad41 fix: Alignment not copied onto new rows (#9785) 2025-07-31 20:02:26 -04:00
Tom Moor da86f38e6f chore: Include stack traces with warnings and sample at 10% (#9784) 2025-07-31 18:16:46 -04:00
Tom Moor 86e76dbe9b fix: Brand positioning on mobile shared docs (#9780) 2025-07-31 07:48:43 -04:00
Tom Moor a4d59dfc88 fix: Loosen Tella.tv embed regex (#9779) 2025-07-31 11:07:27 +00:00
Tom Moor 481adc6ecb fix: Anchor with some UTF8 characters not correctly decoding (#9778) 2025-07-31 07:05:26 -04:00
Tom Moor 3f4950031b feat: Add support for Tella.tv embed mapping (#9775)
* feat: Add support for Tella.tv embed mapping

* Optimised images with calibre/image-actions

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-30 08:54:42 -04:00
Tom Moor 737e7ba4b4 fix: Improve importer to be more resilient to hidden directories (#9762) 2025-07-28 20:36:45 -04:00
Tom Moor af041a5a35 fix: Allow toggline sidebar item without activating item (#9761) 2025-07-28 19:26:27 -04:00
dependabot[bot] d4d91c33f3 chore(deps): bump the aws group with 5 updates (#9757)
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.844.0` | `3.850.0` |
| [@aws-sdk/lib-storage](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/lib/lib-storage) | `3.844.0` | `3.850.0` |
| [@aws-sdk/s3-presigned-post](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-presigned-post) | `3.844.0` | `3.850.0` |
| [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-request-presigner) | `3.844.0` | `3.850.0` |
| [@aws-sdk/signature-v4-crt](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/signature-v4-crt) | `3.844.0` | `3.848.0` |


Updates `@aws-sdk/client-s3` from 3.844.0 to 3.850.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.850.0/clients/client-s3)

Updates `@aws-sdk/lib-storage` from 3.844.0 to 3.850.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.850.0/lib/lib-storage)

Updates `@aws-sdk/s3-presigned-post` from 3.844.0 to 3.850.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.850.0/packages/s3-presigned-post)

Updates `@aws-sdk/s3-request-presigner` from 3.844.0 to 3.850.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.850.0/packages/s3-request-presigner)

Updates `@aws-sdk/signature-v4-crt` from 3.844.0 to 3.848.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.848.0/packages/signature-v4-crt)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.850.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/lib-storage"
  dependency-version: 3.850.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-presigned-post"
  dependency-version: 3.850.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-request-presigner"
  dependency-version: 3.850.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/signature-v4-crt"
  dependency-version: 3.848.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-07-28 18:26:45 -04:00
dependabot[bot] 0439a2ac27 chore(deps): bump @fortawesome/react-fontawesome (#9756)
Bumps the fortawesome group with 1 update: [@fortawesome/react-fontawesome](https://github.com/FortAwesome/react-fontawesome).


Updates `@fortawesome/react-fontawesome` from 0.2.2 to 0.2.3
- [Release notes](https://github.com/FortAwesome/react-fontawesome/releases)
- [Changelog](https://github.com/FortAwesome/react-fontawesome/blob/0.2.x/CHANGELOG.md)
- [Commits](https://github.com/FortAwesome/react-fontawesome/compare/0.2.2...0.2.3)

---
updated-dependencies:
- dependency-name: "@fortawesome/react-fontawesome"
  dependency-version: 0.2.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: fortawesome
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-28 18:22:03 -04:00
dependabot[bot] 69516f7ab9 chore(deps): bump prosemirror-dropcursor from 1.8.1 to 1.8.2 (#9758)
Bumps [prosemirror-dropcursor](https://github.com/prosemirror/prosemirror-dropcursor) from 1.8.1 to 1.8.2.
- [Changelog](https://github.com/ProseMirror/prosemirror-dropcursor/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prosemirror/prosemirror-dropcursor/compare/1.8.1...1.8.2)

---
updated-dependencies:
- dependency-name: prosemirror-dropcursor
  dependency-version: 1.8.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-07-28 18:21:42 -04:00
dependabot[bot] b5165f231e chore(deps): bump @bull-board/koa from 6.9.6 to 6.12.0 (#9760)
Bumps [@bull-board/koa](https://github.com/felixmosh/bull-board/tree/HEAD/packages/koa) from 6.9.6 to 6.12.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.12.0/packages/koa)

---
updated-dependencies:
- dependency-name: "@bull-board/koa"
  dependency-version: 6.12.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-07-28 18:21:10 -04:00
codegen-sh[bot] 7e11b23f87 Migrate Guide.tsx from Reakit to Radix Dialog (#9752)
* Migrate Guide.tsx from Reakit to Radix Dialog

- Replace Reakit Dialog imports with @radix-ui/react-dialog
- Update component structure to use Dialog.Root, Dialog.Portal, Dialog.Overlay, and Dialog.Content
- Remove useDialogState hook in favor of direct open/onOpenChange props
- Preserve identical styling and animations using data-state attributes
- Maintain slide-in animation from right side with 250ms transition
- Keep all existing props and behavior intact

* stash

* fix animations

---------

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
Co-authored-by: Tom Moor <tom@getoutline.com>
2025-07-28 07:54:29 -04:00
Tom Moor 520a2e363e fix: emails parameter on users.list should be case insensitive (#9755) 2025-07-28 07:14:41 -04:00
codegen-sh[bot] 32d894bf84 Migrate ViewReactionsDialog from reakit tabs to radix tabs (#9750)
- Replace reakit Tab, TabPanel, useTabState with @radix-ui/react-tabs
- Maintain identical styling and behavior
- Use Tabs.Root, Tabs.List, Tabs.Trigger, and Tabs.Content components
- Add state management for selected tab with React.useState
- Preserve all existing styling including hover effects and active tab indicator

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
2025-07-27 22:47:24 -04:00
Tom Moor 64550ae790 fix: Base model logic (#9749) 2025-07-27 17:57:29 -04:00
Tom Moor f5d795dbce fix: Cannot tap-scroll on mobile sidebar (#9748) 2025-07-27 21:16:00 +00:00
Tom Moor cdd4cc56c2 fix: Special case actorId for user creation from SSO signup (#9747) 2025-07-27 21:13:12 +00:00
Tom Moor 48fcb4e1ca fix: Various small issues in settings pages (#9745) 2025-07-27 18:22:25 +00:00
Tom Moor f8e6acc0bd perf: Restore fast-click functionality (#9733)
* perf: Restore fast click behavior

* navlink
2025-07-27 13:16:02 -04:00
Tom Moor cc591ebc66 Conversion of User to event system (#9741)
* Conversion of User to event system

* fix

* warning

* fixes

* Skip lastActiveAt in changeset

* fix: Skip count in view changeset

* refactor: Remove userDestroyer

* refactor: Remove userSuspender

* refactor: Remove userUnsuspender

* tests
2025-07-27 13:15:21 -04:00
Neha Prasad 27d0fe86d1 fix: extract domain from user email in Slack authentication (#9742)
* fix: extract domain from user email in Slack authentication

Fixes #9641 - Slack auth fails when OIDC is enabled

* Added tests for domain extraction functionality
Added parseEmail import

* Fix Slack OIDC authentication to retain original subdomain logic (profile.team.domain) while adding domain extraction from user email

* Update slack.ts

---------

Co-authored-by: Tom Moor <tom@getoutline.com>
2025-07-27 12:24:33 -04:00
Tom Moor 807b26c762 fix: Modal background animation is missing (#9739) 2025-07-26 18:53:31 +00:00
Tom Moor c678701bd0 fix: Documents accessible only through group memberships do not appear in search results (#9738)
* first pass

* Add falsy test case coverage
2025-07-26 12:31:11 -04:00
Tom Moor 8c8f7774af fix: TOC unreliable on iOS (#9737)
closes #9654
2025-07-26 12:57:26 +00:00
Tom Moor eaf9e08184 fix: More locations where LetterIcon is incorrect (#9734)
Related #9720
2025-07-25 22:26:12 -04:00
Tom Moor bcb4d7c7da chore: Improve group membership dialogs (#9693)
* chore: Improve group membership dialogs
Remove fullscreen dialog styling

* tidying
2025-07-25 22:26:03 -04:00
Tom Moor 0125e2b05d chore: Define classname in EditorStyleHelper (#9735) 2025-07-26 02:08:54 +00:00
Tom Moor 80090f12be fix: Hide comment marks when disabled (#9732) 2025-07-25 17:52:34 -04:00
charliecreates[bot] 9c564ba557 fix: Unneccessary remounts at breakpoints (#9730)
* fix(document): normalize DataLoader key to avoid remounts

* fix: Remount of PageScroll children at breakpoint

* tsc

---------

Co-authored-by: CharlieHelps <charlie@charlielabs.ai>
Co-authored-by: Tom Moor <tom@getoutline.com>
2025-07-25 08:20:56 -04:00
Tom Moor 980220d2ac feat: Up arrow in comment input should jump to editing previous comment (#9727)
closes #9700
2025-07-25 08:20:47 -04:00
Tom Moor e1fe955a76 fix: Wrap words in inline span to ensure correct break wrapping (#9724)
* fix: Wrap words in inline span to ensure correct break wrapping

* docs
2025-07-24 22:03:21 -04:00
Tom Moor 46ae6d8e6d Double timeout on Notion imports to 24h (#9723) 2025-07-24 01:46:53 +00:00
Translate-O-Tron 6bf83f2517 New Crowdin updates (#9669)
* fix: New Norwegian Bokmal translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: New Chinese Simplified translations from Crowdin [ci skip]
2025-07-23 21:43:00 -04:00
Tom Moor b3e91a2acc fix: Letter icon incorrect in picker and sidebar (#9720) 2025-07-23 21:42:49 -04:00
Tom Moor df28641677 perf: Prefetch references on hover (#9722) 2025-07-23 21:42:44 -04:00
Tom Moor 877073b538 chore: Alternative solution for Redis mock race (#9719)
* chore: Alternative solution for redis mock race

* docs
2025-07-23 20:42:57 -04:00
Tom Moor cf2f13193f chore: Fix Redis mock not used consistently in tests (#9716) 2025-07-23 09:38:24 -04:00
Tom Moor b4836cd922 fix: Title content after period sometimes stripped in import (#9712)
closes #9694
2025-07-22 13:00:23 +00:00
Tom Moor 3466909666 fix: Add additional allow properties for Frame (#9711) 2025-07-22 12:41:14 +00:00
dependabot[bot] 3b317c105b chore(deps): bump form-data from 4.0.2 to 4.0.4 (#9708)
---
updated-dependencies:
- dependency-name: form-data
  dependency-version: 4.0.4
  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-07-22 07:53:30 -04:00
Tom Moor 4f3e8ef4af fix: Add improved logs for incorrect userinfo endpoint OIDC (#9707) 2025-07-22 02:10:02 +00:00
Tom Moor 11ee274e5b fix: YouTube popups are blocked (#9706)
closes #9702
2025-07-22 01:53:45 +00:00
Tom Moor 875d0c1d55 fix: Element overlaying last backlink (#9699)
closes #9696
2025-07-21 08:35:57 -04:00
Cameron bdf89e5de8 chore(linter): improve oxlint setup (#9695) 2025-07-21 06:57:00 -04:00
Tom Moor f23559d1b8 chore: Integration to event backend (#9688) 2025-07-20 13:13:27 -04:00
Tom Moor 5902563ea6 fix: Linking from one shared sub-document to another share (#9689)
* fix: Linking from one shared sub-document to another share

closes #9647

* fix
2025-07-20 13:13:19 -04:00
Tom Moor f6c15fc795 fix: Relationship registration from decorator broken after rolldown (#9690) 2025-07-20 13:13:02 -04:00
Tom Moor 33038a6450 chore: Add additional logging (#9686)
Related #9680
2025-07-19 20:31:49 -04:00
codegen-sh[bot] d188b7b53b Fix foreign key constraint error in teamPermanentDeleter (#9685) 2025-07-19 18:54:25 -04:00
codegen-sh[bot] 8e4dfa65f0 Migrate from ESLint to oxlint for 50x performance improvement (#9682)
* Migrate from ESLint to oxlint

- Upgraded ESLint to v9 and created flat config for migration compatibility
- Used @oxlint/migrate tool to generate .oxlintrc.json configuration
- Installed oxlint v1.7.0 for 50-100x performance improvement
- Updated package.json scripts to use oxlint instead of eslint
- Updated lint-staged configuration for pre-commit hooks
- Configured ignore patterns for migrations, scripts, and build files
- Removed old ESLint dependencies and configuration files
- Maintained equivalent linting behavior with much faster execution

Performance improvement: ~17 seconds → ~350ms (48x faster)

* rules

* --prefer-offline to speed up yarn install

---------

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
Co-authored-by: Tom Moor <tom@getoutline.com>
2025-07-19 15:42:55 -04:00
Tom Moor 6e33b90f62 chore: Convert View to event backend (#9683) 2025-07-19 15:01:25 -04:00
Hemachandar 5db3df35f5 chore: Move Collection event writing to model layer (#9663)
* collections.create, collections.update, collections.delete API

* collections.archive, collections.restore

* collections.move

* file imports

* remove collectionDestroyer

* remove data field

* remove data field for collections.move

* remove data field for import flow

* use hook for permission_changed event

* simplify event type

* tiny
2025-07-19 13:38:22 -04:00
Hemachandar 69a4dc0950 feat: Radix dropdown menu (#9578)
* base menu works

* action internal link & group

* action external link

* simplify NewTemplatesMenu

* animation

* tiny cleanup

* simplify group api

* icon spacer

* root action

* mobile menu

* decrease drawer animation speed

* format

* animation, const separator

* memo..

* TemplatesMenu + Scrollable dropdown

* delete file-ops

* cap height
2025-07-19 12:48:46 -04:00
Tom Moor 41df837435 chore: Add exponential backoff / retry for deadlocks (#9671)
* chore: Add exponential backoff / retry for deadlocks

* feedback
2025-07-18 20:19:18 -04:00
Tom Moor e6683b84da fix: Mermaid flowchart not rendering (#9676) 2025-07-18 15:50:07 -04:00
Tom Moor da8fa90e2d fix: Double tap required in sidebar on iOS (#9665)
* fix: Hover state on mobile sidebar

* refactor
2025-07-18 07:46:49 -04:00
Hemachandar 58d3fad84f Stricter guards for text nodes in Notion import (#9675) 2025-07-18 07:46:34 -04:00
Tom Moor 697bca9932 fix: Invariant violation with addToArchive called with non-model (#9672) 2025-07-18 07:45:48 -04:00
Tom Moor ccac9379ca fix: Frontend errors not properly filtered when source is minified (#9666)
* fix: Sentry filtering when source is minified

* refactor
2025-07-17 10:05:56 -04:00
Tom Moor a83adc4ecf feat: Allow horizontal scaling of collaboration service (#9625)
* stash

* Horizontal scaling of collaboration service
2025-07-17 08:53:52 -04:00
Translate-O-Tron 6f663e5de2 New Crowdin updates (#9660)
* fix: New Korean translations from Crowdin [ci skip]

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

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

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

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

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

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

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

* fix: New Danish 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 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 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]
2025-07-17 08:22:33 -04:00
Hemachandar d78b0440df fix: Skip overwriting non-transaction fields in firstCollectionForTeam query (#9662) 2025-07-17 08:22:09 -04:00
Hemachandar d3115e3557 Allow replacing embed links (#9479)
* Allow replacing embed links

* better error msg

* add missing GitLabSnippet props

* add embed link toolbar

* cleanup
2025-07-16 21:59:06 -04:00
Tom Moor 417216cb16 Revert "chore: Patch prop-types default export (#9656)" (#9659)
This reverts commit f5db27396a.
2025-07-17 01:56:18 +00:00
Tom Moor 3449c7b8ea Revert "chore: Keep classnames, Sentry depends on consistency (#9649)" (#9658)
This reverts commit cd83f41294.
2025-07-16 21:29:31 -04:00
Tom Moor 0d03a4ac13 chore: Remove redis in CI (#9657) 2025-07-16 21:17:32 -04:00
Tom Moor f5db27396a chore: Patch prop-types default export (#9656)
* chore: Patch prop-types default export

* touch
2025-07-16 20:28:14 -04:00
Hemachandar 4c1caf6025 Show paste menu to convert a list of URLs to mentions (#9646) 2025-07-16 20:25:24 -04:00
Hemachandar c7231ffd8b chore: Move Revision event writing to model layer (#9650) 2025-07-16 17:28:29 -04:00
Apoorv Mishra c4a7692724 fix: EventBoundary wrapper component further down the react tree (#9655)
renders these handlers vestigial
2025-07-16 17:22:38 -04:00
Tom Moor bf447ec72a Convert team to model events (#9628) 2025-07-16 07:06:59 -04:00
Translate-O-Tron f058dc7f53 New Crowdin updates (#9627)
* fix: New Korean translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

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

* fix: New Korean translations from Crowdin [ci skip]
2025-07-15 23:07:27 -04:00
Hemachandar 7d315288dd Listen to GitHub webhooks to update issueSources cache (#9414)
* Listen to GitHub webhooks to update issue-sources cache

* Add `GitHubWebhookTask`

* review
2025-07-15 23:07:14 -04:00
Tom Moor cd83f41294 chore: Keep classnames, Sentry depends on consistency (#9649) 2025-07-15 23:06:34 -04:00
Tom Moor a9e40b8709 fix: Remove custom scrollbar styling (#9640) 2025-07-15 12:15:24 +00:00
dependabot[bot] 8a3a4f3a30 chore(deps): bump mailparser from 3.7.3 to 3.7.4 (#9633)
Bumps [mailparser](https://github.com/nodemailer/mailparser) from 3.7.3 to 3.7.4.
- [Release notes](https://github.com/nodemailer/mailparser/releases)
- [Changelog](https://github.com/nodemailer/mailparser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/mailparser/compare/v3.7.3...v3.7.4)

---
updated-dependencies:
- dependency-name: mailparser
  dependency-version: 3.7.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-07-14 21:27:16 -04:00
dependabot[bot] 42f23d9582 chore(deps): bump @tanstack/react-virtual from 3.13.6 to 3.13.12 (#9634)
Bumps [@tanstack/react-virtual](https://github.com/TanStack/virtual/tree/HEAD/packages/react-virtual) from 3.13.6 to 3.13.12.
- [Release notes](https://github.com/TanStack/virtual/releases)
- [Changelog](https://github.com/TanStack/virtual/blob/main/packages/react-virtual/CHANGELOG.md)
- [Commits](https://github.com/TanStack/virtual/commits/@tanstack/react-virtual@3.13.12/packages/react-virtual)

---
updated-dependencies:
- dependency-name: "@tanstack/react-virtual"
  dependency-version: 3.13.12
  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-07-14 21:27:04 -04:00
dependabot[bot] 4354c791f1 chore(deps): bump the aws group with 5 updates (#9636)
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.842.0` | `3.844.0` |
| [@aws-sdk/lib-storage](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/lib/lib-storage) | `3.842.0` | `3.844.0` |
| [@aws-sdk/s3-presigned-post](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-presigned-post) | `3.842.0` | `3.844.0` |
| [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-request-presigner) | `3.842.0` | `3.844.0` |
| [@aws-sdk/signature-v4-crt](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/signature-v4-crt) | `3.840.0` | `3.844.0` |


Updates `@aws-sdk/client-s3` from 3.842.0 to 3.844.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.844.0/clients/client-s3)

Updates `@aws-sdk/lib-storage` from 3.842.0 to 3.844.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.844.0/lib/lib-storage)

Updates `@aws-sdk/s3-presigned-post` from 3.842.0 to 3.844.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.844.0/packages/s3-presigned-post)

Updates `@aws-sdk/s3-request-presigner` from 3.842.0 to 3.844.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.844.0/packages/s3-request-presigner)

Updates `@aws-sdk/signature-v4-crt` from 3.840.0 to 3.844.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.844.0/packages/signature-v4-crt)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.844.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/lib-storage"
  dependency-version: 3.844.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-presigned-post"
  dependency-version: 3.844.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-request-presigner"
  dependency-version: 3.844.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/signature-v4-crt"
  dependency-version: 3.844.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-07-14 21:26:34 -04:00
codegen-sh[bot] 35b3c59136 Fix middle mouse button link behavior (#9632)
- Add check for middle mouse button (event.button === 1) in link click handler
- Middle mouse button clicks now properly open links in new tabs instead of current tab
- Fixes issue where MMB acted like LMB and caused duplicate tab opening
- Issue was present since version 0.83.0 and reported in 0.85.1

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
2025-07-14 21:26:24 -04:00
dependabot[bot] b8ab063cc9 chore(deps): bump mammoth from 1.9.0 to 1.9.1 (#9637)
Bumps [mammoth](https://github.com/mwilliamson/mammoth.js) from 1.9.0 to 1.9.1.
- [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.9.0...1.9.1)

---
updated-dependencies:
- dependency-name: mammoth
  dependency-version: 1.9.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-07-14 16:57:32 -04:00
dependabot[bot] f3c09ef797 chore(deps): bump @octokit/auth-app from 6.1.3 to 6.1.4 (#9638)
Bumps [@octokit/auth-app](https://github.com/octokit/auth-app.js) from 6.1.3 to 6.1.4.
- [Release notes](https://github.com/octokit/auth-app.js/releases)
- [Commits](https://github.com/octokit/auth-app.js/compare/v6.1.3...v6.1.4)

---
updated-dependencies:
- dependency-name: "@octokit/auth-app"
  dependency-version: 6.1.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-07-14 16:57:02 -04:00
Tom Moor 91c3c0e9ff fix/error-404 (#9626) 2025-07-14 08:00:18 -04:00
Alexander Lichter 7176a7b8e8 build: try native plugin (#9629) 2025-07-14 07:47:29 -04:00
Tom Moor 772eb2f1d4 fix: randomstring dep does not work in browser with rolldown-vite (#9624)
* fix: randomstring dep does not work in browser with rolldown-vite

* fix: Last usage of randomstring, docs

* feedback
2025-07-13 09:33:16 -04:00
Tom Moor 40e6a87d15 chore: Refactor revisions list (#9623)
* fix: Remove force render

* chore: Refactor revisions list to be reactive

* fix: Access of undefined

* refactor
2025-07-12 19:29:57 -04:00
codegen-sh[bot] a57d90fdf1 Add collaboratorIds support to revisions (#9343)
* Add collaboratorIds support to revision events

- Add database migration to add collaboratorIds column to revisions table
- Update server Revision model to include collaboratorIds field
- Update client Revision model to include collaboratorIds field
- Modify Revision.buildFromDocument to capture document collaborators
- Update revisionCreator to include collaboratorIds in event data

Fixes #6975

* fix to actually work

* test: Add missing methods to mock

* Return collaborators to client and display

---------

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
Co-authored-by: Tom Moor <tom@getoutline.com>
2025-07-12 09:24:53 -04:00
Translate-O-Tron ad9674faa7 New Crowdin updates (#9564)
* fix: New Korean translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: New Danish 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 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 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-07-12 08:01:36 -04:00
dependabot[bot] bc7a0cc18b chore(deps): bump the babel group across 1 directory with 5 updates (#9619)
Bumps the babel group with 5 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@babel/plugin-proposal-decorators](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-proposal-decorators) | `7.27.1` | `7.28.0` |
| [@babel/plugin-transform-destructuring](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-destructuring) | `7.27.7` | `7.28.0` |
| [@babel/plugin-transform-regenerator](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-regenerator) | `7.27.5` | `7.28.1` |
| [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) | `7.27.2` | `7.28.0` |
| [@babel/cli](https://github.com/babel/babel/tree/HEAD/packages/babel-cli) | `7.27.2` | `7.28.0` |



Updates `@babel/plugin-proposal-decorators` from 7.27.1 to 7.28.0
- [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.0/packages/babel-plugin-proposal-decorators)

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

Updates `@babel/plugin-transform-regenerator` from 7.27.5 to 7.28.1
- [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.1/packages/babel-plugin-transform-regenerator)

Updates `@babel/preset-env` from 7.27.2 to 7.28.0
- [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.0/packages/babel-preset-env)

Updates `@babel/cli` from 7.27.2 to 7.28.0
- [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.0/packages/babel-cli)

---
updated-dependencies:
- dependency-name: "@babel/plugin-proposal-decorators"
  dependency-version: 7.28.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: babel
- dependency-name: "@babel/plugin-transform-destructuring"
  dependency-version: 7.28.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: babel
- dependency-name: "@babel/plugin-transform-regenerator"
  dependency-version: 7.28.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: babel
- dependency-name: "@babel/preset-env"
  dependency-version: 7.28.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: babel
- dependency-name: "@babel/cli"
  dependency-version: 7.28.0
  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-07-12 07:03:39 -04:00
codegen-sh[bot] 3c513b319c Switch from vite to rolldown-vite for improved build performance (#9523)
* Switch from vite to rolldown-vite for improved build performance

- Add vite resolution to npm:rolldown-vite@latest in package.json
- rolldown-vite is a drop-in replacement that uses Rust-based Rolldown bundler
- Expected benefits: 3x-16x faster builds, up to 100x less memory usage
- No configuration changes needed - rolldown-vite maintains full compatibility

* Switch minifier from terser to oxc

- Change minify option from 'terser' to 'oxc' in vite.config.ts
- Remove terserOptions since they're not needed with oxc minifier
- Fix ESLint error by prefixing unused catch variable with underscore
- Oxc is the high-performance Rust-based minifier used by rolldown-vite
- Expected benefits: faster minification with better performance

* Fix TypeScript compatibility issues with rolldown-vite

- Add type assertion for minify: 'oxc' option which is supported by rolldown-vite but not in standard Vite types
- Update comment for webpackStats plugin to reflect rolldown-vite compatibility
- Resolves TypeScript compilation errors in CI

* fix

* pin

* node-20

* Node 22 support

* Use oxc variant of react plugin

* tsc

* fix: Remove node 21 from engines

It has problematic CJS behavior that was fixed in 22

---------

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
Co-authored-by: Tom Moor <tom@getoutline.com>
2025-07-12 10:52:19 +00:00
Tom Moor 932f3333f1 fix: Refresh tokens do not work with OIDC discovery (#9618) 2025-07-12 06:44:23 -04:00
Tom Moor 2dc214393c chore: Add event writing for team domain mutation (#9617)
* chore: Add event writing for team domain mutation
closes #9612

* fix
2025-07-11 21:32:25 -04:00
Tom Moor 928ec064ee fix: Viewers unable to connect personal Slack (#9614) 2025-07-11 17:41:59 -04:00
Hemachandar 066b6dab43 Remove @renderlesskit/react package (#9615)
* remove ariaLabel

* remove @renderlesskit/react
2025-07-11 17:41:47 -04:00
Tom Moor 66964ad5a1 fix: Blank page is rendered when link to draft without collection is shared (#9605)
* fix: 404 response for public shared collection-less draft

* fix: sharedTree undefined in response for public shared draft in no collection

* test
2025-07-11 07:47:03 -04:00
Tom Moor 9193e606b9 perf: Avoid sidebar collection re-render on drag state change (#9602)
* perf: Avoid sidebar collection re-render on drag state change

* Restore case

* perf: Reduce waypoint size

* perf: More render optimizations in DocumentLink

* feedback
2025-07-11 11:10:43 +00:00
Tom Moor ff75a70c74 InputSelectNew -> InputSelect (#9601) 2025-07-10 23:43:46 -04:00
Hemachandar e469e5771e Migrate remaining select menus to Radix (#9599)
* InputMemberPermissionSelect

* api key expiry

* invite permission

* templatize location
2025-07-10 22:56:04 -04:00
Tom Moor 2e31f87b34 fix: EditorUpdateError code outside valid range (#9600) 2025-07-11 01:08:31 +00:00
Tom Moor cda88daf3f Merge branch 'releases/v0.85.0' 2025-07-10 21:07:00 -04:00
Hemachandar d4cdf4202f InputSelectPermission component (#9585) 2025-07-10 08:46:19 -04:00
Tom Moor 7dd0616b8c fix: Remote table modifications cause CellSelection to be lost (#9596)
* fix: Table modifications lose any existing CellSelection

* tsc

* doc

* Remove unused imports
2025-07-10 08:46:08 -04:00
Tom Moor 210e960a98 fix: Remove bugging recently viewed docs from explorer (#9597) 2025-07-10 03:58:13 +00:00
Tom Moor 473bed604b lockfile (#9595) 2025-07-10 02:39:36 +00:00
Tom Moor cabae7a91e fix: Empty placeholder shows up when no text but image in paragraph (#9593) 2025-07-09 23:56:29 +00:00
Tom Moor ecf53866b5 fix: Buttons on not found page don't work (#9592) 2025-07-09 23:32:29 +00:00
Tom Moor aee117ba0b fix: Restore preloading of notifications (#9583) 2025-07-09 07:20:42 -04:00
Tom Moor 45b865604d fix: JSON export->import loses content (#9582) 2025-07-09 07:02:47 -04:00
Tom Moor 6dfc75f090 perf: Some small performance improvements in SearchHelper (#9580) 2025-07-08 22:19:02 +00:00
codegen-sh[bot] 97f8d0f265 Separate Prettier and ESLint according to best practices (#9565)
* Separate Prettier and ESLint according to best practices

- Create standalone .prettierrc configuration file
- Remove eslint-plugin-prettier integration from ESLint config
- Replace with eslint-config-prettier to disable conflicting rules
- Remove eslint-plugin-prettier dependency
- Add dedicated format and format:check scripts
- Update lint-staged to run Prettier and ESLint separately
- Format entire codebase with new Prettier configuration

This follows the recommended approach from Prettier documentation:
https://prettier.io/docs/integrating-with-linters#notes

* Remove test comment

---------

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
2025-07-08 18:01:48 -04:00
Tom Moor 68f87f7254 perf: Move recalculation of memberships to async job (#9567)
* perf: Move recalculation of memberships to async job

* tsc
2025-07-08 18:01:18 -04:00
Hemachandar a60634139e fix: Use compatible versions of Radix internals (#9576) 2025-07-08 18:01:07 -04:00
Tom Moor d5504be686 chore: Remove OTP for desktop login for now (#9566) 2025-07-08 02:30:57 +00:00
Tom Moor a6b0fcff48 feat: Add OTP sign-in for PWA (#9556)
* wip

* wip

* wip

* Only use code for desktop and PWA
2025-07-07 18:36:43 -04:00
dependabot[bot] ad13e28ce9 chore(deps-dev): bump eslint-plugin-react from 7.37.3 to 7.37.5 (#9562)
---
updated-dependencies:
- dependency-name: eslint-plugin-react
  dependency-version: 7.37.5
  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-07-07 17:16:57 -04:00
dependabot[bot] 466033c6b8 chore(deps): bump the aws group with 4 updates (#9559)
Bumps the aws group with 4 updates: [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3), [@aws-sdk/lib-storage](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/lib/lib-storage), [@aws-sdk/s3-presigned-post](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-presigned-post) and [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-request-presigner).


Updates `@aws-sdk/client-s3` from 3.840.0 to 3.842.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.842.0/clients/client-s3)

Updates `@aws-sdk/lib-storage` from 3.840.0 to 3.842.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.842.0/lib/lib-storage)

Updates `@aws-sdk/s3-presigned-post` from 3.840.0 to 3.842.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.842.0/packages/s3-presigned-post)

Updates `@aws-sdk/s3-request-presigner` from 3.840.0 to 3.842.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.842.0/packages/s3-request-presigner)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.842.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/lib-storage"
  dependency-version: 3.842.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-presigned-post"
  dependency-version: 3.842.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-request-presigner"
  dependency-version: 3.842.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-07-07 15:13:42 -04:00
dependabot[bot] ac56515527 chore(deps): bump sequelize from 6.37.3 to 6.37.7 (#9561)
Bumps [sequelize](https://github.com/sequelize/sequelize) from 6.37.3 to 6.37.7.
- [Release notes](https://github.com/sequelize/sequelize/releases)
- [Changelog](https://github.com/sequelize/sequelize/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sequelize/sequelize/compare/v6.37.3...v6.37.7)

---
updated-dependencies:
- dependency-name: sequelize
  dependency-version: 6.37.7
  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-07-07 15:13:34 -04:00
dependabot[bot] fb052ec8b7 chore(deps-dev): bump @relative-ci/agent from 4.3.0 to 4.3.1 (#9560)
Bumps [@relative-ci/agent](https://github.com/relative-ci/agent) from 4.3.0 to 4.3.1.
- [Release notes](https://github.com/relative-ci/agent/releases)
- [Commits](https://github.com/relative-ci/agent/compare/v4.3.0...v4.3.1)

---
updated-dependencies:
- dependency-name: "@relative-ci/agent"
  dependency-version: 4.3.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-07-07 15:13:26 -04:00
Hemachandar 29c07d6dee Migrate remaining popovers to Radix (#9555)
* reaction picker

* api key expiry date picker

* find and replace popover

* slack list item
2025-07-06 08:53:47 -04:00
Hemachandar b0a2a02166 Migrate share popovers to Radix (#9547)
* common popover primitive

* move collection/document share popovers

* cleanup dummy file

* snappy animation
2025-07-05 22:24:00 -04:00
Tom Moor 127b7fe986 fix: Include image caption as alt attribute (#9554) 2025-07-05 22:23:35 -04:00
Translate-O-Tron 31d790d7d9 New Crowdin updates (#9510)
* fix: New Korean translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: New Korean translations from Crowdin [ci skip]
2025-07-05 22:21:22 -04:00
Newton d36f98bc9e disable search engine indexing for documents by default (#9551) 2025-07-05 22:17:55 -04:00
Hemachandar b03880d121 fix: Navigate routes and links in kbar (#9552) 2025-07-05 17:07:00 -04:00
Tom Moor 12d084e8fa fix: Map hardbreaks to breaks (#9550) 2025-07-05 00:09:50 +00:00
Tom Moor 0589f62bde fix: Make shared document content available to screenreaders (#9549) 2025-07-04 16:45:21 -04:00
Tom Moor 58bfb1b79b fix: Incorrect parsing of hard breaks from Markdown (#9548)
* fix: Incorrect parsing of hard breaks from Markdown

* revert

* Add br to simple editor
2025-07-04 16:38:51 -04:00
Tom Moor ebb06a7c34 fix: Importer truncates newlines in captions (#9544)
* fix: Importer truncates newlines in captions

* fix: SimpleImage also
2025-07-04 14:32:08 -04:00
Tom Moor 064adb72e1 fix: ctx.url is actually the path, thanks koa (#9545) 2025-07-04 14:47:14 +00:00
Tom Moor f6d28811e5 fix: Cannot ctrl-click to open in a new tab some menu items (#9542)
* wip

* done
2025-07-04 09:09:55 -04:00
codegen-sh[bot] e21bd23bd8 Update markdown-it to 14.1.0 and markdown-it-emoji to 3.0.0 (#9541)
* Update markdown-it to 14.1.0 and markdown-it-emoji to 3.0.0

- Updated markdown-it from ^13.0.2 to ^14.1.0
- Updated markdown-it-emoji from ^2.0.0 to ^3.0.0
- Fixed emoji plugin import to use new named export syntax
- Changed from 'import emojiPlugin from "markdown-it-emoji"' to 'import { full as emojiPlugin } from "markdown-it-emoji"'
- Verified builds and tests pass successfully

* Update @types/markdown-it-emoji to 3.0.1

- Updated @types/markdown-it-emoji from ^2.0.4 to ^3.0.1 to match the updated markdown-it-emoji package
- This ensures TypeScript definitions are compatible with the new plugin signature

---------

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
2025-07-04 08:12:16 -04:00
codegen-sh[bot] 3fd09ca0bf Fix OIDC well-known discovery for subdirectories (#9540)
* Fix OIDC well-known discovery for subdirectories

- Fix URL construction in fetchOIDCConfiguration to properly handle issuer URLs with subdirectories
- Replace incorrect use of new URL() constructor that was treating well-known path as absolute
- Add proper path concatenation that preserves subdirectories in issuer URLs
- Add comprehensive test cases for subdirectory scenarios
- Fixes issue where https://auth.example.com/application/o/outline/ would incorrectly resolve to https://auth.example.com/.well-known/openid-configuration instead of https://auth.example.com/application/o/outline/.well-known/openid-configuration

Fixes #9535

* Refactor to use wellKnownPath variable instead of hardcoded path

- Use wellKnownPath.substring(1) to remove leading slash when appending to pathname
- Eliminates duplication of the .well-known/openid-configuration path
- Improves maintainability by using the existing variable consistently

* Simplify logic by checking pathname does not end with slash

- If pathname doesn't end with slash, append full wellKnownPath (with leading slash)
- If pathname ends with slash, append wellKnownPath without leading slash
- Eliminates need for substring() by using the slash logic more elegantly

---------

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
2025-07-04 08:01:55 -04:00
Tom Moor bc5270c220 v0.85.0 2025-07-03 19:29:36 -04:00
dependabot[bot] d57c991d06 chore(deps): bump the aws group with 5 updates (#9516)
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.832.0` | `3.839.0` |
| [@aws-sdk/lib-storage](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/lib/lib-storage) | `3.832.0` | `3.839.0` |
| [@aws-sdk/s3-presigned-post](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-presigned-post) | `3.832.0` | `3.839.0` |
| [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-request-presigner) | `3.832.0` | `3.839.0` |
| [@aws-sdk/signature-v4-crt](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/signature-v4-crt) | `3.828.0` | `3.839.0` |


Updates `@aws-sdk/client-s3` from 3.832.0 to 3.839.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.839.0/clients/client-s3)

Updates `@aws-sdk/lib-storage` from 3.832.0 to 3.839.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.839.0/lib/lib-storage)

Updates `@aws-sdk/s3-presigned-post` from 3.832.0 to 3.839.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.839.0/packages/s3-presigned-post)

Updates `@aws-sdk/s3-request-presigner` from 3.832.0 to 3.839.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.839.0/packages/s3-request-presigner)

Updates `@aws-sdk/signature-v4-crt` from 3.828.0 to 3.839.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.839.0/packages/signature-v4-crt)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.839.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/lib-storage"
  dependency-version: 3.839.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-presigned-post"
  dependency-version: 3.839.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-request-presigner"
  dependency-version: 3.839.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/signature-v4-crt"
  dependency-version: 3.839.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-07-02 09:36:47 -04:00
Tom Moor 71459e648f fix: Do not print canonical url if same as current (#9525) 2025-07-02 07:25:09 -04:00
Tom Moor 5de549c882 fix: Double title on import of some documents (#9522) 2025-07-01 17:10:01 -04:00
Tom Moor f0a7cbd193 Merge branch 'main' of github.com:outline/outline 2025-07-01 21:36:33 +01:00
dependabot[bot] 453ec0e3e9 chore(deps): bump prosemirror-tables from 1.6.4 to 1.7.1 (#9457)
Bumps [prosemirror-tables](https://github.com/prosemirror/prosemirror-tables) from 1.6.4 to 1.7.1.
- [Release notes](https://github.com/prosemirror/prosemirror-tables/releases)
- [Changelog](https://github.com/ProseMirror/prosemirror-tables/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prosemirror/prosemirror-tables/compare/v1.6.4...v1.7.1)

---
updated-dependencies:
- dependency-name: prosemirror-tables
  dependency-version: 1.7.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-07-01 09:26:46 -04:00
dependabot[bot] 3199aad7cf chore(deps): bump the babel group with 2 updates (#9514)
Bumps the babel group with 2 updates: [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) and [@babel/plugin-transform-destructuring](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-destructuring).


Updates `@babel/core` from 7.27.4 to 7.27.7
- [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.27.7/packages/babel-core)

Updates `@babel/plugin-transform-destructuring` from 7.27.3 to 7.27.7
- [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.27.7/packages/babel-plugin-transform-destructuring)

---
updated-dependencies:
- dependency-name: "@babel/core"
  dependency-version: 7.27.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: babel
- dependency-name: "@babel/plugin-transform-destructuring"
  dependency-version: 7.27.7
  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-07-01 09:25:23 -04:00
dependabot[bot] 54914aa312 chore(deps-dev): bump @types/readable-stream from 4.0.18 to 4.0.21 (#9515)
---
updated-dependencies:
- dependency-name: "@types/readable-stream"
  dependency-version: 4.0.21
  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-07-01 03:37:52 -04:00
dependabot[bot] 5c60053802 chore(deps): bump bull from 4.16.3 to 4.16.5 (#9517)
---
updated-dependencies:
- dependency-name: bull
  dependency-version: 4.16.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-07-01 03:37:42 -04:00
Andy Copland 50fe0bb746 fix: Add OAuth support to search query source enum (#9511)
- Add 'oauth' to SearchQuery enum in server and client models
- Add database migration to include 'oauth' in enum_search_queries_source
- Fixes 400 validation error when OAuth users search with parameters

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

Co-authored-by: Claude <noreply@anthropic.com>
2025-07-01 03:37:25 -04:00
Tom Moor 8c7c47ff95 Scrollable css 2025-06-29 23:49:36 +01:00
Tom Moor c4d798d70b fix: Various fixes for HTML -> Markdown conversion (#9509)
* chore: List conversion should use a single space between marker and content

* Simplify table header detection
2025-06-29 11:57:40 -04:00
Tom Moor ba20eb4040 fix: OIDC logout redirect unreliable (#9508) 2025-06-29 07:57:49 -04:00
codegen-sh[bot] c97e5fd181 feat: Add block movement with Cmd+Alt+Arrow keys (#9502)
* feat: Add block movement with Cmd+Alt+Arrow keys

- Add getCurrentBlock helper function to find current block node
- Implement moveBlockUp and moveBlockDown commands in Keys extension
- Support Cmd+Alt+ArrowUp to move current block up
- Support Cmd+Alt+ArrowDown to move current block down
- Follow ProseMirror best practices for node manipulation
- Maintain cursor position after block movement

Fixes #9486

* feat: Add block movement shortcuts to KeyboardShortcuts component

- Add Cmd+Alt+↑ shortcut for moving blocks up
- Add Cmd+Alt+↓ shortcut for moving blocks down
- Added to Formatting section alongside other editing shortcuts

---------

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
2025-06-29 04:56:48 -04:00
Tom Moor 8e56f58102 chore: Add additional validation to SMTP_SERVICE env (#9506)
Related #9505
2025-06-29 04:34:47 -04:00
Translate-O-Tron 3347101c84 New Crowdin updates (#9461)
* fix: New Korean translations from Crowdin [ci skip]

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

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

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

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

* fix: New French translations from Crowdin [ci skip]
2025-06-29 04:34:35 -04:00
dependabot[bot] 6d387c61c3 chore(deps): bump the aws group with 5 updates (#9456)
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.826.0` | `3.828.0` |
| [@aws-sdk/lib-storage](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/lib/lib-storage) | `3.826.0` | `3.828.0` |
| [@aws-sdk/s3-presigned-post](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-presigned-post) | `3.826.0` | `3.828.0` |
| [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-request-presigner) | `3.826.0` | `3.828.0` |
| [@aws-sdk/signature-v4-crt](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/signature-v4-crt) | `3.826.0` | `3.828.0` |


Updates `@aws-sdk/client-s3` from 3.826.0 to 3.828.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.828.0/clients/client-s3)

Updates `@aws-sdk/lib-storage` from 3.826.0 to 3.828.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.828.0/lib/lib-storage)

Updates `@aws-sdk/s3-presigned-post` from 3.826.0 to 3.828.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.828.0/packages/s3-presigned-post)

Updates `@aws-sdk/s3-request-presigner` from 3.826.0 to 3.828.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.828.0/packages/s3-request-presigner)

Updates `@aws-sdk/signature-v4-crt` from 3.826.0 to 3.828.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.828.0/packages/signature-v4-crt)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.828.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/lib-storage"
  dependency-version: 3.828.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-presigned-post"
  dependency-version: 3.828.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-request-presigner"
  dependency-version: 3.828.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/signature-v4-crt"
  dependency-version: 3.828.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-06-29 04:34:11 -04:00
codegen-sh[bot] 2f06ae9e48 Fix OIDC login failures with Base64 avatar URLs (#9501) 2025-06-28 10:47:51 -04:00
codegen-sh[bot] 2a962efe57 feat: upgrade Node.js support to include Node 22 (#9503) 2025-06-28 10:47:14 -04:00
codegen-sh[bot] 879c568a2c Upgrade Prettier to v3.6.2 (#9500)
* Upgrade Prettier to v3.6.2 and eslint-plugin-prettier to v5.5.1

- Upgraded prettier from ^2.8.8 to ^3.6.2 (latest version)
- Upgraded eslint-plugin-prettier from ^4.2.1 to ^5.5.1 for compatibility
- Applied automatic formatting changes from new Prettier version
- All existing ESLint and Prettier configurations remain compatible

* Applied automatic fixes

* Trigger CI

---------

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
Co-authored-by: Tom Moor <tom@getoutline.com>
2025-06-28 10:22:28 -04:00
dependabot[bot] 76b54fc234 chore(deps): bump mermaid from 11.4.1 to 11.7.0 (#9481)
Bumps [mermaid](https://github.com/mermaid-js/mermaid) from 11.4.1 to 11.7.0.
- [Release notes](https://github.com/mermaid-js/mermaid/releases)
- [Changelog](https://github.com/mermaid-js/mermaid/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/mermaid-js/mermaid/compare/mermaid@11.4.1...mermaid@11.7.0)

---
updated-dependencies:
- dependency-name: mermaid
  dependency-version: 11.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-06-28 09:50:41 -04:00
Tom Moor 2e53145532 fix: Notion API timed out during import (#9498) 2025-06-28 09:50:32 -04:00
Tom Moor f6f831f3f6 fix: Enable PKCE if OIDC discovery endpoint supports it (#9478)
* fix: Enable PKCE if OIDC discovery endpoint supports it

* fix: Ensure code_verifier is passed through state

* facepalm
2025-06-27 11:06:45 -04:00
Tom Moor 3700342b35 fix: Not correctly catching linked databases (#9497) 2025-06-27 13:50:36 +00:00
Tom Moor 0244ac2a84 feat: Allow allowIndexing and showLastUpdated to be set in shares.create endpoint (#9476) 2025-06-24 17:21:06 -04:00
dependabot[bot] bf54e639a7 chore(deps-dev): bump terser from 5.42.0 to 5.43.1 (#9482)
Bumps [terser](https://github.com/terser/terser) from 5.42.0 to 5.43.1.
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/compare/v5.42.0...v5.43.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-24 05:07:29 -04:00
Tom Moor 083e5bb7c4 fix/pl-rendering (#9484) 2025-06-23 22:01:41 +00:00
Tom Moor ca5c51a712 fix: Client and server validation differ for subdomains (#9468)
* fix: Client and server validation differ for subdomains

* Validation message

* Lower min subdomain length to 2
2025-06-18 16:50:17 -04:00
Hemachandar a3b3e9e0be Use Retry-After header for Notion rate-limit retries (#9467) 2025-06-18 16:39:58 -04:00
Tom Moor ecd5afa6cd fix: Public share search offset is incorrect (#9465) 2025-06-18 16:01:42 +00:00
dependabot[bot] 4562edfda0 chore(deps): bump class-validator from 0.14.1 to 0.14.2 (#9459)
Bumps [class-validator](https://github.com/typestack/class-validator) from 0.14.1 to 0.14.2.
- [Release notes](https://github.com/typestack/class-validator/releases)
- [Changelog](https://github.com/typestack/class-validator/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/typestack/class-validator/compare/v0.14.1...v0.14.2)

---
updated-dependencies:
- dependency-name: class-validator
  dependency-version: 0.14.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-06-18 11:58:35 -04:00
dependabot[bot] af74535333 chore(deps): bump mailparser from 3.7.2 to 3.7.3 (#9458)
Bumps [mailparser](https://github.com/nodemailer/mailparser) from 3.7.2 to 3.7.3.
- [Release notes](https://github.com/nodemailer/mailparser/releases)
- [Changelog](https://github.com/nodemailer/mailparser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/mailparser/compare/v3.7.2...v3.7.3)

---
updated-dependencies:
- dependency-name: mailparser
  dependency-version: 3.7.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-06-18 11:58:21 -04:00
dependabot[bot] 18ffccc333 chore(deps): bump octokit from 3.2.1 to 3.2.2 (#9460)
Bumps [octokit](https://github.com/octokit/octokit.js) from 3.2.1 to 3.2.2.
- [Release notes](https://github.com/octokit/octokit.js/releases)
- [Commits](https://github.com/octokit/octokit.js/compare/v3.2.1...v3.2.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-18 11:58:10 -04:00
Tom Moor 120a3388ed fix: Commenting unavailable on individually shared documents (#9449)
Refactor to use policies for comment
2025-06-15 09:45:42 -04:00
Tom Moor 4689d5e88d fix: PNG without dimension data should fallback to async dimension loading (#9453)
closes #9442
2025-06-15 09:44:38 -04:00
Hemachandar 2183c6d1d2 fix: Skip showing archived docs in shared section (#9451) 2025-06-14 10:46:25 -04:00
Tom Moor 75f173c6ff Add option to publish but not persist events (#9448)
* Add option to publish but not persist events

* tsc
2025-06-14 10:46:01 -04:00
Tom Moor 8b6d9589f9 fix: Switches not working in react-hook-form (#9450) 2025-06-14 00:02:58 -04:00
Translate-O-Tron 92bd67c104 New Crowdin updates (#9438)
* fix: New Portuguese, Brazilian translations from Crowdin [ci skip]

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

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

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

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

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

* fix: New English, United Kingdom translations from Crowdin [ci skip]
2025-06-13 20:09:47 -04:00
Tom Moor 84c6bd18ce fix: All documents should show in CMD+K when searching by title (#9445)
closes #8438
2025-06-13 20:09:38 -04:00
Tom Moor 89099ccf58 fix: Notion import failure details are not exposed to admin (#9443) 2025-06-13 07:43:37 -04:00
Tom Moor 0536c108eb fix: Login via email does not properly redirect to desktop app (#9440) 2025-06-12 21:12:58 -04:00
Tom Moor d1ad2f20e1 fix: Facepile overflow became square (#9439) 2025-06-13 00:41:31 +00:00
Tom Moor ca0e37063c fix: Sporadic rate limiting errors from Notion (#9436) 2025-06-12 16:20:32 -04:00
Tom Moor 98366e55e9 fix: Improve table merge/split icons (#9432) 2025-06-12 01:59:20 +00:00
Tom Moor a9c4dd43d6 fix: options vs rest usage (#9431)
No functional difference here, just avoid extra passed attributes
2025-06-11 20:04:43 -04:00
Hemachandar c9f25546e8 fix: Handle Notion linked database errors (#9429) 2025-06-11 09:10:00 -04:00
Translate-O-Tron 276ca1bbf2 New Crowdin updates (#9335)
* fix: New Chinese Simplified translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: New English, United Kingdom translations from Crowdin [ci skip]
2025-06-10 23:32:20 -04:00
927 changed files with 29144 additions and 18483 deletions
+4 -13
View File
@@ -21,10 +21,7 @@
[
"transform-inline-environment-variables",
{
"include": [
"SOURCE_COMMIT",
"SOURCE_VERSION"
]
"include": ["SOURCE_COMMIT", "SOURCE_VERSION"]
}
],
"tsconfig-paths-module-resolver"
@@ -39,16 +36,10 @@
}
]
],
"ignore": [
"**/__mocks__",
"**/*.test.ts"
]
"ignore": ["**/__mocks__", "**/*.test.ts"]
},
"development": {
"ignore": [
"**/__mocks__",
"**/*.test.ts"
]
"ignore": ["**/__mocks__", "**/*.test.ts"]
},
"test": {
"presets": [
@@ -65,4 +56,4 @@
]
}
}
}
}
+1
View File
@@ -6,6 +6,7 @@ __mocks__
.DS_Store
.env*
.eslint*
.oxlintrc*
.log
Makefile
Procfile
+1
View File
@@ -202,6 +202,7 @@ RATE_LIMITER_DURATION_WINDOW=60
# DOCS: https://docs.getoutline.com/s/hosting/doc/github-GchT3NNxI9
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
GITHUB_WEBHOOK_SECRET=
GITHUB_APP_NAME=
GITHUB_APP_ID=
GITHUB_APP_PRIVATE_KEY=
-1
View File
@@ -1 +0,0 @@
server/migrations/*.js
-190
View File
@@ -1,190 +0,0 @@
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"sourceType": "module",
"extraFileExtensions": [
".json"
],
"project": "./tsconfig.json",
"ecmaFeatures": {
"jsx": true
}
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:import/recommended",
"plugin:import/typescript",
"plugin:prettier/recommended"
],
"plugins": [
"es",
"react",
"@typescript-eslint",
"eslint-plugin-import",
"eslint-plugin-node",
"eslint-plugin-react",
"eslint-plugin-lodash"
],
"rules": {
"no-restricted-imports": [
"error",
{
"paths": [
{
"name": "reakit/Menu",
"importNames": [
"useMenuState"
],
"message": "Do not use useMenuState from reakit/Menu. Use useMenuState instead."
}
]
}
],
"eqeqeq": 2,
"curly": 2,
"no-console": "error",
"arrow-body-style": [
"error",
"as-needed"
],
"spaced-comment": "error",
"object-shorthand": "error",
"no-mixed-operators": "off",
"no-useless-escape": "off",
"no-shadow": "off",
"es/no-regexp-lookbehind-assertions": "error",
"react/react-in-jsx-scope": "off",
"react/self-closing-comp": [
"error",
{
"component": true,
"html": true
}
],
"@typescript-eslint/no-shadow": [
"warn",
{
"allow": [
"transaction"
],
"hoist": "all",
"ignoreTypeValueShadow": true
}
],
"@typescript-eslint/no-require-imports": "off",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/await-thenable": "error",
"@typescript-eslint/no-misused-promises": [
"error",
{
"checksVoidReturn": false
}
],
"@typescript-eslint/no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_",
"args": "after-used",
"ignoreRestSiblings": true
}
],
"padding-line-between-statements": [
"error",
{
"blankLine": "always",
"prev": "*",
"next": "export"
}
],
"lines-between-class-members": [
"error",
"always",
{
"exceptAfterSingleLine": true
}
],
"lodash/import-scope": [
"error",
"method"
],
"import/no-named-as-default": "off",
"import/no-named-as-default-member": "off",
"import/newline-after-import": 2,
"import/order": [
"error",
{
"alphabetize": {
"order": "asc"
},
"pathGroups": [
{
"pattern": "@shared/**",
"group": "external",
"position": "after"
},
{
"pattern": "@server/**",
"group": "external",
"position": "after"
},
{
"pattern": "~/stores",
"group": "external",
"position": "after"
},
{
"pattern": "~/stores/**",
"group": "external",
"position": "after"
},
{
"pattern": "~/models/**",
"group": "external",
"position": "after"
},
{
"pattern": "~/scenes/**",
"group": "external",
"position": "after"
},
{
"pattern": "~/components/**",
"group": "external",
"position": "after"
},
{
"pattern": "~/**",
"group": "external",
"position": "after"
}
]
}
],
"prettier/prettier": [
"error",
{
"printWidth": 80,
"trailingComma": "es5"
}
]
},
"settings": {
"react": {
"createClass": "createReactClass",
"pragma": "React",
"version": "detect"
},
"import/parsers": {
"@typescript-eslint/parser": [
".ts",
".tsx"
]
},
"import/resolver": {
"typescript": {}
}
}
}
+56 -56
View File
@@ -2,62 +2,62 @@ name: Bug report
description: File a bug to help us improve
labels: ["bug"]
body:
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the bug you encountered.
options:
- label: I have searched the existing issues
required: true
- type: checkboxes
attributes:
label: This is not related to configuring Outline
description: I understand that questions related to configuring self-hosted Outline should be asked in the [community forum](https://github.com/outline/outline/discussions/categories/self-hosting).
options:
- label: The issue is not related to self-hosting config
required: true
- type: textarea
attributes:
label: Current Behavior
description: A concise description of what you're experiencing.
validations:
required: false
- type: textarea
attributes:
label: Expected Behavior
description: A concise description of what you expected to happen.
validations:
required: false
- type: textarea
attributes:
label: Steps To Reproduce
description: Steps to reproduce the behavior.
placeholder: |
1. In this environment...
1. With this config...
1. Run '...'
1. See error...
validations:
required: false
- type: textarea
attributes:
label: Environment
description: |
examples:
- **Outline**: Outline 0.80.0
- **Browser**: Safari
value: |
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the bug you encountered.
options:
- label: I have searched the existing issues
required: true
- type: checkboxes
attributes:
label: This is not related to configuring Outline
description: I understand that questions related to configuring self-hosted Outline should be asked in the [community forum](https://github.com/outline/outline/discussions/categories/self-hosting).
options:
- label: The issue is not related to self-hosting config
required: true
- type: textarea
attributes:
label: Current Behavior
description: A concise description of what you're experiencing.
validations:
required: false
- type: textarea
attributes:
label: Expected Behavior
description: A concise description of what you expected to happen.
validations:
required: false
- type: textarea
attributes:
label: Steps To Reproduce
description: Steps to reproduce the behavior.
placeholder: |
1. In this environment...
1. With this config...
1. Run '...'
1. See error...
validations:
required: false
- type: textarea
attributes:
label: Environment
description: |
examples:
- **Outline**: Outline 0.80.0
- **Browser**: Safari
value: |
- Outline:
- Browser:
render: markdown
validations:
required: false
- type: textarea
attributes:
label: Anything else?
description: |
Links? References? Anything that will give us more context about the issue you are encountering!
render: markdown
validations:
required: false
- type: textarea
attributes:
label: Anything else?
description: |
Links? References? Anything that will give us more context about the issue you are encountering!
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
validations:
required: false
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
validations:
required: false
+2 -2
View File
@@ -2,9 +2,9 @@
addReviewers: true
# A list of reviewers to be added to pull requests (GitHub user name)
reviewers:
reviewers:
- tommoor
# A list of keywords to be skipped the process that add reviewers if pull requests include it
# A list of keywords to be skipped the process that add reviewers if pull requests include it
skipKeywords:
- wip
+3
View File
@@ -26,3 +26,6 @@ updates:
aws:
patterns:
- "@aws-sdk/*"
radix-ui:
patterns:
- "@radix-ui/*"
+1 -1
View File
@@ -2,7 +2,7 @@ name: Auto Close Unsigned PRs
on:
schedule:
- cron: '0 0 * * *' # Run daily at midnight UTC
- cron: "0 0 * * *" # Run daily at midnight UTC
jobs:
close-unsigned-prs:
+66 -73
View File
@@ -2,9 +2,9 @@ name: CI
on:
push:
branches: [ main ]
branches: [main]
pull_request:
branches: [ main ]
branches: [main]
env:
NODE_ENV: test
@@ -22,46 +22,46 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.x]
node-version: [20.x, 22.x]
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'yarn'
- name: Install dependencies
run: yarn install --frozen-lockfile
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: "yarn"
- name: Install dependencies
run: yarn install --frozen-lockfile --prefer-offline
lint:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20.x
cache: 'yarn'
- run: yarn install --frozen-lockfile
- run: yarn lint
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22.x
cache: "yarn"
- run: yarn install --frozen-lockfile --prefer-offline
- run: yarn lint --quiet
types:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20.x
cache: 'yarn'
- run: yarn install --frozen-lockfile
- run: yarn tsc
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22.x
cache: "yarn"
- run: yarn install --frozen-lockfile --prefer-offline
- run: yarn tsc
changes:
runs-on: ubuntu-latest
outputs:
config: ${{ steps.filter.outputs.config }}
server: ${{ steps.filter.outputs.server }}
app: ${{ steps.filter.outputs.app }}
steps:
@@ -70,6 +70,9 @@ jobs:
id: filter
with:
filters: |
config:
- '.github/**'
- 'vite.config.ts'
server:
- 'server/**'
- 'shared/**'
@@ -83,23 +86,23 @@ jobs:
test:
needs: [build, changes]
if: ${{ needs.changes.outputs.app == 'true' }}
if: ${{ needs.changes.outputs.app == 'true' || needs.changes.outputs.config == 'true' }}
runs-on: ubuntu-latest
strategy:
matrix:
test-group: [app, shared]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20.x
cache: 'yarn'
- run: yarn install --frozen-lockfile
- run: yarn test:${{ matrix.test-group }}
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22.x
cache: "yarn"
- run: yarn install --frozen-lockfile --prefer-offline
- run: yarn test:${{ matrix.test-group }}
test-server:
needs: [build, changes]
if: ${{ needs.changes.outputs.server == 'true' }}
if: ${{ needs.changes.outputs.server == 'true' || needs.changes.outputs.config == 'true' }}
runs-on: ubuntu-latest
services:
postgres:
@@ -115,51 +118,41 @@ jobs:
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:5.0
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
strategy:
matrix:
shard: [1, 2, 3]
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20.x
cache: 'yarn'
- run: yarn install --frozen-lockfile
- run: yarn sequelize db:migrate
- name: Run server tests
run: |
TESTFILES=$(find . -name "*.test.ts" -path "*/server/*" | sort | split -n -d -l $(($(find . -name "*.test.ts" -path "*/server/*" | wc -l)/${{ matrix.shard }})) - | sed -n "${{ matrix.shard }}p")
yarn test --maxWorkers=2 $TESTFILES
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22.x
cache: "yarn"
- run: yarn install --frozen-lockfile --prefer-offline
- run: yarn sequelize db:migrate
- name: Run server tests
run: |
TESTFILES=$(find . -name "*.test.ts" -path "*/server/*" | sort | split -n -d -l $(($(find . -name "*.test.ts" -path "*/server/*" | wc -l)/${{ matrix.shard }})) - | sed -n "${{ matrix.shard }}p")
yarn test --maxWorkers=2 $TESTFILES
bundle-size:
needs: [build, types, changes]
if: ${{ needs.changes.outputs.app == 'true' && github.repository == 'outline/outline' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20.x
cache: 'yarn'
- run: yarn install --frozen-lockfile
- name: Set environment to production
run: echo "NODE_ENV=production" >> $GITHUB_ENV
- run: yarn vite:build
- name: Send bundle stats to RelativeCI
uses: relative-ci/agent-action@v2
with:
key: ${{ secrets.RELATIVE_CI_KEY }}
token: ${{ secrets.GITHUB_TOKEN }}
webpackStatsFile: ./build/app/webpack-stats.json
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22.x
cache: "yarn"
- run: yarn install --frozen-lockfile --prefer-offline
- name: Set environment to production
run: echo "NODE_ENV=production" >> $GITHUB_ENV
- run: yarn vite:build
- name: Send bundle stats to RelativeCI
uses: relative-ci/agent-action@v2
with:
key: ${{ secrets.RELATIVE_CI_KEY }}
token: ${{ secrets.GITHUB_TOKEN }}
webpackStatsFile: ./build/app/webpack-stats.json
+29 -29
View File
@@ -13,12 +13,12 @@ name: "CodeQL"
on:
push:
branches: [ main ]
branches: [main]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
branches: [main]
schedule:
- cron: '28 15 * * 2'
- cron: "28 15 * * 2"
jobs:
analyze:
@@ -32,39 +32,39 @@ jobs:
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
language: ["javascript"]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://git.io/codeql-language-support
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
+1 -1
View File
@@ -209,4 +209,4 @@ jobs:
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}
docker buildx imagetools inspect ${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}
+4 -4
View File
@@ -2,7 +2,7 @@ name: Lint
on:
pull_request:
branches: [ main ]
branches: [main]
jobs:
run-linters:
@@ -20,11 +20,11 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 20.x
cache: 'yarn'
- run: yarn install --frozen-lockfile
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'
commit_message: "Applied automatic fixes"
+4 -9
View File
@@ -7,12 +7,10 @@
"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"],
"globalSetup": "<rootDir>/server/test/globalSetup.js",
"globalTeardown": "<rootDir>/server/test/globalTeardown.js",
"testEnvironment": "node"
},
@@ -23,8 +21,7 @@
"^~/(.*)$": "<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"
"^uuid$": "<rootDir>/node_modules/uuid/dist/index.js"
},
"modulePaths": ["<rootDir>/app"],
"setupFiles": ["<rootDir>/__mocks__/window.js"],
@@ -39,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"],
@@ -53,8 +49,7 @@
"^~/(.*)$": "<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"
"^uuid$": "<rootDir>/node_modules/uuid/dist/index.js"
},
"setupFiles": ["<rootDir>/__mocks__/window.js"],
"testEnvironment": "jsdom",
+1
View File
@@ -0,0 +1 @@
22
+102
View File
@@ -0,0 +1,102 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"ignorePatterns": [
"build/**",
"node_modules/**",
"public/**",
"server/migrations/**",
"server/scripts/**",
"patches/**",
"*.d.ts"
],
"rules": {
"for-direction": "error",
"no-async-promise-executor": "error",
"no-case-declarations": "error",
"no-class-assign": "error",
"no-compare-neg-zero": "error",
"no-cond-assign": "error",
"no-const-assign": "error",
"no-constant-binary-expression": "error",
"no-constant-condition": "error",
"no-control-regex": "error",
"no-debugger": "error",
"no-delete-var": "error",
"no-dupe-class-members": "error",
"no-dupe-else-if": "error",
"no-dupe-keys": "error",
"no-duplicate-case": "error",
"no-empty": "error",
"no-empty-character-class": "error",
"no-empty-pattern": "error",
"no-empty-static-block": "error",
"no-ex-assign": "error",
"no-explicit-any": "warn",
"no-extra-boolean-cast": "error",
"no-fallthrough": "error",
"no-func-assign": "error",
"no-global-assign": "error",
"no-import-assign": "error",
"no-invalid-regexp": "error",
"no-irregular-whitespace": "error",
"no-loss-of-precision": "error",
"no-new-native-nonconstructor": "error",
"no-nonoctal-decimal-escape": "error",
"no-obj-calls": "error",
"no-prototype-builtins": "error",
"no-redeclare": "error",
"no-regex-spaces": "error",
"no-self-assign": "error",
"no-setter-return": "error",
"no-shadow-restricted-names": "error",
"no-sparse-arrays": "error",
"no-this-before-super": "error",
"no-unexpected-multiline": "error",
"no-unsafe-finally": "error",
"no-unsafe-negation": "error",
"no-unsafe-optional-chaining": "error",
"no-unused-labels": "error",
"no-unused-private-class-members": "error",
"no-unused-vars": "error",
"no-useless-backreference": "error",
"no-useless-catch": "error",
"no-useless-escape": "error",
"no-with": "error",
"require-yield": "error",
"use-isnan": "error",
"valid-typeof": "error"
},
"overrides": [
{
"files": ["**/*.{js,jsx,ts,tsx}"],
"rules": {
"eqeqeq": "error",
"curly": "error",
"no-console": "error",
"arrow-body-style": ["error", "as-needed"],
"no-useless-escape": "off",
"react/react-in-jsx-scope": "off",
"react/self-closing-comp": [
"error",
{
"component": true,
"html": true
}
],
"@typescript-eslint/no-require-imports": "off",
"import/no-named-as-default": "off",
"import/no-named-as-default-member": "off",
"no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_",
"args": "after-used",
"ignoreRestSiblings": true
}
]
},
"plugins": ["eslint", "oxc", "react", "typescript", "import"]
}
]
}
+4
View File
@@ -0,0 +1,4 @@
{
"printWidth": 80,
"trailingComma": "es5"
}
+1 -1
View File
@@ -1,4 +1,4 @@
require("dotenv").config({
require("@dotenvx/dotenvx").config({
path: process.env.NODE_ENV === "test" ? ".env.test" : ".env",
});
+1 -1
View File
@@ -6,7 +6,7 @@ ARG APP_PATH
WORKDIR $APP_PATH
# ---
FROM node:20-slim AS runner
FROM node:22-slim AS runner
LABEL org.opencontainers.image.source="https://github.com/outline/outline"
+2 -2
View File
@@ -3,7 +3,7 @@ Business Source License 1.1
Parameters
Licensor: General Outline, Inc.
Licensed Work: Outline 0.84.0
Licensed Work: Outline 0.87.4
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-05-11
Change Date: 2029-09-18
Change License: Apache License, Version 2.0
+1 -1
View File
@@ -8,7 +8,7 @@ build:
docker compose build --pull outline
test:
docker compose up -d redis postgres
docker compose up -d postgres
NODE_ENV=test yarn sequelize db:drop
NODE_ENV=test yarn sequelize db:create
NODE_ENV=test yarn sequelize db:migrate
+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
+1 -1
View File
@@ -1 +1 @@
export default '';
export default "";
+5 -5
View File
@@ -1,19 +1,19 @@
const storage = {};
export default {
setItem: function(key, value) {
storage[key] = value || '';
setItem: function (key, value) {
storage[key] = value || "";
},
getItem: function(key) {
getItem: function (key) {
return key in storage ? storage[key] : null;
},
removeItem: function(key) {
removeItem: function (key) {
delete storage[key];
},
get length() {
return Object.keys(storage).length;
},
key: function(i) {
key: function (i) {
var keys = Object.keys(storage);
return keys[i] || null;
},
+2 -8
View File
@@ -3,13 +3,7 @@
"description": "Open source wiki and knowledge base for growing teams",
"website": "https://www.getoutline.com/",
"repository": "https://github.com/outline/outline",
"keywords": [
"wiki",
"team",
"node",
"markdown",
"slack"
],
"keywords": ["wiki", "team", "node", "markdown", "slack"],
"success_url": "/",
"formation": {
"web": {
@@ -222,4 +216,4 @@
"required": false
}
}
}
}
-17
View File
@@ -1,17 +0,0 @@
{
"extends": [
"../.eslintrc",
"plugin:react/recommended",
"plugin:react-hooks/recommended"
],
"plugins": [
"eslint-plugin-react-hooks"
],
"rules": {
"react/react-in-jsx-scope": "off"
},
"env": {
"jest": true,
"browser": true
}
}
+34
View File
@@ -0,0 +1,34 @@
{
"extends": ["../.oxlintrc.json"],
"plugins": ["oxc", "eslint", "typescript", "react"],
"overrides": [
{
"files": ["**/*.{jsx,tsx}"],
"rules": {
"no-restricted-imports": [
"error",
{
"patterns": [
{
"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."
}
]
}
]
},
"plugins": ["import"]
}
],
"env": {
"jest": true,
"browser": true
}
}
+38 -2
View File
@@ -1,7 +1,9 @@
import { PlusIcon } from "outline-icons";
import { PlusIcon, TrashIcon } from "outline-icons";
import stores from "~/stores";
import ApiKey from "~/models/ApiKey";
import ApiKeyNew from "~/scenes/ApiKeyNew";
import { createAction } from "..";
import ApiKeyRevokeDialog from "~/scenes/Settings/components/ApiKeyRevokeDialog";
import { createAction, createActionV2 } from "..";
import { SettingsSection } from "../sections";
export const createApiKey = createAction({
@@ -22,3 +24,37 @@ export const createApiKey = createAction({
});
},
});
export const revokeApiKeyFactory = ({ apiKey }: { apiKey: ApiKey }) =>
createActionV2({
name: ({ t, isMenu }) =>
isMenu
? apiKey.isExpired
? t("Delete")
: `${t("Revoke")}`
: t("Revoke API key"),
analyticsName: "Revoke API key",
section: SettingsSection,
icon: <TrashIcon />,
keywords: "revoke delete remove",
dangerous: true,
perform: async ({ t, event }) => {
event?.preventDefault();
event?.stopPropagation();
if (apiKey.isExpired) {
await apiKey.delete();
return;
}
stores.dialogs.openModal({
title: t("Revoke token"),
content: (
<ApiKeyRevokeDialog
onSubmit={stores.dialogs.closeAllModals}
apiKey={apiKey}
/>
),
});
},
});
+106 -29
View File
@@ -2,6 +2,8 @@ import {
ArchiveIcon,
CollectionIcon,
EditIcon,
ExportIcon,
NewDocumentIcon,
PadlockIcon,
PlusIcon,
RestoreIcon,
@@ -22,11 +24,19 @@ 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 } from "~/actions";
import {
createAction,
createActionV2,
createInternalLinkActionV2,
} from "~/actions";
import { ActiveCollectionSection, CollectionSection } from "~/actions/sections";
import { setPersistedState } from "~/hooks/usePersistedState";
import history from "~/utils/history";
import { newTemplatePath, searchPath } from "~/utils/routeHelpers";
import {
newDocumentPath,
newTemplatePath,
searchPath,
} from "~/utils/routeHelpers";
import ExportDialog from "~/components/ExportDialog";
const ColorCollectionIcon = ({ collection }: { collection: Collection }) => (
<DynamicCollectionIcon collection={collection} />
@@ -47,7 +57,7 @@ export const openCollection = createAction({
name: collection.name,
icon: <ColorCollectionIcon collection={collection} />,
section: CollectionSection,
perform: () => history.push(collection.path),
to: collection.path,
}));
},
});
@@ -70,9 +80,8 @@ export const createCollection = createAction({
},
});
export const editCollection = createAction({
name: ({ t, isContextMenu }) =>
isContextMenu ? `${t("Edit")}` : t("Edit collection"),
export const editCollection = createActionV2({
name: ({ t, isMenu }) => (isMenu ? `${t("Edit")}` : t("Edit collection")),
analyticsName: "Edit collection",
section: ActiveCollectionSection,
icon: <EditIcon />,
@@ -96,9 +105,9 @@ export const editCollection = createAction({
},
});
export const editCollectionPermissions = createAction({
name: ({ t, isContextMenu }) =>
isContextMenu ? `${t("Permissions")}` : t("Collection permissions"),
export const editCollectionPermissions = createActionV2({
name: ({ t, isMenu }) =>
isMenu ? `${t("Permissions")}` : t("Collection permissions"),
analyticsName: "Collection permissions",
section: ActiveCollectionSection,
icon: <PadlockIcon />,
@@ -128,7 +137,7 @@ export const editCollectionPermissions = createAction({
},
});
export const searchInCollection = createAction({
export const searchInCollection = createInternalLinkActionV2({
name: ({ t }) => t("Search in collection"),
analyticsName: "Search collection",
section: ActiveCollectionSection,
@@ -146,13 +155,20 @@ export const searchInCollection = createAction({
return stores.policies.abilities(activeCollectionId).readDocument;
},
to: ({ activeCollectionId, sidebarContext }) => {
const [pathname, search] = searchPath({
collectionId: activeCollectionId,
}).split("?");
perform: ({ activeCollectionId }) => {
history.push(searchPath({ collectionId: activeCollectionId }));
return {
pathname,
search,
state: { sidebarContext },
};
},
});
export const starCollection = createAction({
export const starCollection = createActionV2({
name: ({ t }) => t("Star"),
analyticsName: "Star collection",
section: ActiveCollectionSection,
@@ -179,7 +195,7 @@ export const starCollection = createAction({
},
});
export const unstarCollection = createAction({
export const unstarCollection = createActionV2({
name: ({ t }) => t("Unstar"),
analyticsName: "Unstar collection",
section: ActiveCollectionSection,
@@ -205,7 +221,7 @@ export const unstarCollection = createAction({
},
});
export const subscribeCollection = createAction({
export const subscribeCollection = createActionV2({
name: ({ t }) => t("Subscribe"),
analyticsName: "Subscribe to collection",
section: ActiveCollectionSection,
@@ -218,6 +234,7 @@ export const subscribeCollection = createAction({
const collection = stores.collections.get(activeCollectionId);
return (
!!collection?.isActive &&
!collection?.isSubscribed &&
stores.policies.abilities(activeCollectionId).subscribe
);
@@ -235,7 +252,7 @@ export const subscribeCollection = createAction({
},
});
export const unsubscribeCollection = createAction({
export const unsubscribeCollection = createActionV2({
name: ({ t }) => t("Unsubscribe"),
analyticsName: "Unsubscribe from collection",
section: ActiveCollectionSection,
@@ -248,6 +265,7 @@ export const unsubscribeCollection = createAction({
const collection = stores.collections.get(activeCollectionId);
return (
!!collection?.isActive &&
!!collection?.isSubscribed &&
stores.policies.abilities(activeCollectionId).unsubscribe
);
@@ -265,10 +283,10 @@ export const unsubscribeCollection = createAction({
},
});
export const archiveCollection = createAction({
export const archiveCollection = createActionV2({
name: ({ t }) => `${t("Archive")}`,
analyticsName: "Archive collection",
section: CollectionSection,
section: ActiveCollectionSection,
icon: <ArchiveIcon />,
visible: ({ activeCollectionId, stores }) => {
if (!activeCollectionId) {
@@ -306,7 +324,7 @@ export const archiveCollection = createAction({
},
});
export const restoreCollection = createAction({
export const restoreCollection = createActionV2({
name: ({ t }) => t("Restore"),
analyticsName: "Restore collection",
section: CollectionSection,
@@ -331,7 +349,7 @@ export const restoreCollection = createAction({
},
});
export const deleteCollection = createAction({
export const deleteCollection = createActionV2({
name: ({ t }) => `${t("Delete")}`,
analyticsName: "Delete collection",
section: ActiveCollectionSection,
@@ -365,7 +383,65 @@ export const deleteCollection = createAction({
},
});
export const createTemplate = createAction({
export const exportCollection = createActionV2({
name: ({ t }) => `${t("Export")}`,
analyticsName: "Export collection",
section: ActiveCollectionSection,
icon: <ExportIcon />,
visible: ({ currentTeamId, activeCollectionId, stores }) => {
if (!currentTeamId || !activeCollectionId) {
return false;
}
return (
!!stores.policies.abilities(currentTeamId).createExport &&
!!stores.policies.abilities(activeCollectionId).export
);
},
perform: async ({ activeCollectionId, stores, t }) => {
if (!activeCollectionId) {
return;
}
const collection = stores.collections.get(activeCollectionId);
if (!collection) {
return;
}
stores.dialogs.openModal({
title: t("Export collection"),
content: (
<ExportDialog
collection={collection}
onSubmit={stores.dialogs.closeAllModals}
/>
),
});
},
});
export const createDocument = createInternalLinkActionV2({
name: ({ t }) => t("New document"),
analyticsName: "New document",
section: ActiveCollectionSection,
icon: <NewDocumentIcon />,
keywords: "new create document",
visible: ({ activeCollectionId, stores }) =>
!!(
!!activeCollectionId &&
stores.policies.abilities(activeCollectionId).createDocument
),
to: ({ activeCollectionId, sidebarContext }) => {
const [pathname, search] = newDocumentPath(activeCollectionId).split("?");
return {
pathname,
search,
state: { sidebarContext },
};
},
});
export const createTemplate = createInternalLinkActionV2({
name: ({ t }) => t("New template"),
analyticsName: "New template",
section: ActiveCollectionSection,
@@ -376,13 +452,14 @@ export const createTemplate = createAction({
!!activeCollectionId &&
stores.policies.abilities(activeCollectionId).createDocument
),
perform: ({ activeCollectionId, event }) => {
if (!activeCollectionId) {
return;
}
event?.preventDefault();
event?.stopPropagation();
history.push(newTemplatePath(activeCollectionId));
to: ({ activeCollectionId, sidebarContext }) => {
const [pathname, search] = newTemplatePath(activeCollectionId).split("?");
return {
pathname,
search,
state: { sidebarContext },
};
},
});
+16 -38
View File
@@ -1,12 +1,10 @@
import { DoneIcon, SmileyIcon, TrashIcon } from "outline-icons";
import { toast } from "sonner";
import stores from "~/stores";
import Comment from "~/models/Comment";
import CommentDeleteDialog from "~/components/CommentDeleteDialog";
import ViewReactionsDialog from "~/components/Reactions/ViewReactionsDialog";
import history from "~/utils/history";
import { createAction } from "..";
import { DocumentSection } from "../sections";
import { createActionV2 } from "..";
import { ActiveDocumentSection } from "../sections";
export const deleteCommentFactory = ({
comment,
@@ -15,15 +13,15 @@ export const deleteCommentFactory = ({
comment: Comment;
onDelete: () => void;
}) =>
createAction({
createActionV2({
name: ({ t }) => `${t("Delete")}`,
analyticsName: "Delete comment",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <TrashIcon />,
keywords: "trash",
dangerous: true,
visible: () => stores.policies.abilities(comment.id).delete,
perform: ({ t, event }) => {
visible: ({ stores }) => stores.policies.abilities(comment.id).delete,
perform: ({ t, stores, event }) => {
event?.preventDefault();
event?.stopPropagation();
@@ -41,26 +39,16 @@ export const resolveCommentFactory = ({
comment: Comment;
onResolve: () => void;
}) =>
createAction({
createActionV2({
name: ({ t }) => t("Mark as resolved"),
analyticsName: "Resolve thread",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <DoneIcon outline />,
visible: () =>
visible: ({ stores }) =>
stores.policies.abilities(comment.id).resolve &&
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"));
},
@@ -73,26 +61,16 @@ export const unresolveCommentFactory = ({
comment: Comment;
onUnresolve: () => void;
}) =>
createAction({
createActionV2({
name: ({ t }) => t("Mark as unresolved"),
analyticsName: "Unresolve thread",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <DoneIcon outline />,
visible: () =>
visible: ({ stores }) =>
stores.policies.abilities(comment.id).unresolve &&
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();
},
});
@@ -102,15 +80,15 @@ export const viewCommentReactionsFactory = ({
}: {
comment: Comment;
}) =>
createAction({
createActionV2({
name: ({ t }) => `${t("View reactions")}`,
analyticsName: "View comment reactions",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <SmileyIcon />,
visible: () =>
visible: ({ stores }) =>
stores.policies.abilities(comment.id).read &&
comment.reactions.length > 0,
perform: ({ t, event }) => {
perform: ({ t, stores, event }) => {
event?.preventDefault();
event?.stopPropagation();
+13
View File
@@ -1,3 +1,4 @@
import Storage from "@shared/utils/Storage";
import copy from "copy-to-clipboard";
import {
BeakerIcon,
@@ -127,6 +128,17 @@ export const clearIndexedDB = createAction({
},
});
export const clearStorage = createAction({
name: ({ t }) => t("Clear local storage"),
icon: <TrashIcon />,
keywords: "cache clear localstorage",
section: DeveloperSection,
perform: ({ t }) => {
Storage.clear();
toast.success(t("Local storage cleared"));
},
});
export const createTestUsers = createAction({
name: "Create 10 test users",
icon: <UserIcon />,
@@ -201,6 +213,7 @@ export const developer = createAction({
createToast,
createTestUsers,
clearIndexedDB,
clearStorage,
startTyping,
],
});
+307 -113
View File
@@ -1,5 +1,6 @@
import copy from "copy-to-clipboard";
import invariant from "invariant";
import uniqBy from "lodash/uniqBy";
import {
DownloadIcon,
DuplicateIcon,
@@ -25,11 +26,12 @@ import {
PublishIcon,
CommentIcon,
CopyIcon,
EyeIcon,
PadlockIcon,
GlobeIcon,
LogoutIcon,
CaseSensitiveIcon,
RestoreIcon,
EditIcon,
} from "outline-icons";
import { toast } from "sonner";
import Icon from "@shared/components/Icon";
@@ -51,7 +53,13 @@ import MarkdownIcon from "~/components/Icons/MarkdownIcon";
import SharePopover from "~/components/Sharing/Document";
import { getHeaderExpandedKey } from "~/components/Sidebar/components/Header";
import DocumentTemplatizeDialog from "~/components/TemplatizeDialog";
import { createAction } from "~/actions";
import {
createAction,
createActionV2,
createActionV2Group,
createActionV2WithChildren,
createInternalLinkActionV2,
} from "~/actions";
import {
ActiveDocumentSection,
DocumentSection,
@@ -61,7 +69,6 @@ import env from "~/env";
import { setPersistedState } from "~/hooks/usePersistedState";
import history from "~/utils/history";
import {
documentInsightsPath,
documentHistoryPath,
homePath,
newDocumentPath,
@@ -70,7 +77,12 @@ import {
documentPath,
urlify,
trashPath,
documentEditPath,
} 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";
export const openDocument = createAction({
name: ({ t }) => t("Open document"),
@@ -84,8 +96,9 @@ export const openDocument = createAction({
(acc, node) => [...acc, ...node.children],
[] as NavigationNode[]
);
const documents = stores.documents.orderedData;
return nodes.map((item) => ({
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,
@@ -96,11 +109,43 @@ export const openDocument = createAction({
<DocumentIcon />
),
section: DocumentSection,
perform: () => history.push(item.url),
to: item.url,
}));
},
});
export const editDocument = createInternalLinkActionV2({
name: ({ t }) => t("Edit"),
analyticsName: "Edit document",
section: ActiveDocumentSection,
keywords: "edit",
icon: <EditIcon />,
visible: ({ activeDocumentId, stores }) => {
const { auth, documents, policies } = stores;
const document = activeDocumentId
? documents.get(activeDocumentId)
: undefined;
const can = activeDocumentId
? policies.abilities(activeDocumentId)
: undefined;
return (
!!can?.update && !!auth.user?.separateEditMode && !document?.template
);
},
to: ({ activeDocumentId, stores }) => {
const document = activeDocumentId
? stores.documents.get(activeDocumentId)
: undefined;
if (!document) {
return "";
}
return documentEditPath(document);
},
});
export const createDocument = createAction({
name: ({ t }) => t("New document"),
analyticsName: "New document",
@@ -139,7 +184,7 @@ export const createDraftDocument = createAction({
}),
});
export const createDocumentFromTemplate = createAction({
export const createDocumentFromTemplate = createInternalLinkActionV2({
name: ({ t }) => t("New from template"),
analyticsName: "New document",
section: DocumentSection,
@@ -169,16 +214,24 @@ export const createDocumentFromTemplate = createAction({
}
return stores.policies.abilities(currentTeamId).createDocument;
},
perform: ({ activeCollectionId, activeDocumentId, sidebarContext }) =>
history.push(
newDocumentPath(activeCollectionId, { templateId: activeDocumentId }),
{
sidebarContext,
}
),
to: ({ activeDocumentId, activeCollectionId, sidebarContext }) => {
if (!activeDocumentId || !activeCollectionId) {
return "";
}
const [pathname, search] = newDocumentPath(activeCollectionId, {
templateId: activeDocumentId,
}).split("?");
return {
pathname,
search,
state: { sidebarContext },
};
},
});
export const createNestedDocument = createAction({
export const createNestedDocument = createInternalLinkActionV2({
name: ({ t }) => t("New nested document"),
analyticsName: "New document",
section: ActiveDocumentSection,
@@ -189,13 +242,19 @@ export const createNestedDocument = createAction({
!!activeDocumentId &&
stores.policies.abilities(currentTeamId).createDocument &&
stores.policies.abilities(activeDocumentId).createChildDocument,
perform: ({ activeDocumentId, sidebarContext }) =>
history.push(newNestedDocumentPath(activeDocumentId), {
sidebarContext,
}),
to: ({ activeDocumentId, sidebarContext }) => {
const [pathname, search] =
newNestedDocumentPath(activeDocumentId).split("?");
return {
pathname,
search,
state: { sidebarContext },
};
},
});
export const starDocument = createAction({
export const starDocument = createActionV2({
name: ({ t }) => t("Star"),
analyticsName: "Star document",
section: ActiveDocumentSection,
@@ -221,7 +280,7 @@ export const starDocument = createAction({
},
});
export const unstarDocument = createAction({
export const unstarDocument = createActionV2({
name: ({ t }) => t("Unstar"),
analyticsName: "Unstar document",
section: ActiveDocumentSection,
@@ -247,7 +306,7 @@ export const unstarDocument = createAction({
},
});
export const publishDocument = createAction({
export const publishDocument = createActionV2({
name: ({ t }) => t("Publish"),
analyticsName: "Publish document",
section: ActiveDocumentSection,
@@ -289,7 +348,7 @@ export const publishDocument = createAction({
},
});
export const unpublishDocument = createAction({
export const unpublishDocument = createActionV2({
name: ({ t }) => t("Unpublish"),
analyticsName: "Unpublish document",
section: ActiveDocumentSection,
@@ -320,11 +379,27 @@ export const unpublishDocument = createAction({
},
});
export const subscribeDocument = createAction({
export const subscribeDocument = createActionV2({
name: ({ t }) => t("Subscribe"),
analyticsName: "Subscribe to document",
section: ActiveDocumentSection,
icon: <SubscribeIcon />,
tooltip: ({ activeCollectionId, isMenu, stores, t }) => {
if (!isMenu || !activeCollectionId) {
return undefined;
}
return stores.collections.get(activeCollectionId)?.isSubscribed
? t("Subscription inherited from collection")
: undefined;
},
disabled: ({ activeCollectionId, isMenu, stores }) => {
if (!isMenu || !activeCollectionId) {
return false;
}
return !!stores.collections.get(activeCollectionId)?.isSubscribed;
},
visible: ({ activeDocumentId, stores }) => {
if (!activeDocumentId) {
return false;
@@ -333,6 +408,7 @@ export const subscribeDocument = createAction({
const document = stores.documents.get(activeDocumentId);
return (
!!document?.isActive &&
!document?.collection?.isSubscribed &&
!document?.isSubscribed &&
stores.policies.abilities(activeDocumentId).subscribe
@@ -349,11 +425,27 @@ export const subscribeDocument = createAction({
},
});
export const unsubscribeDocument = createAction({
export const unsubscribeDocument = createActionV2({
name: ({ t }) => t("Unsubscribe"),
analyticsName: "Unsubscribe from document",
section: ActiveDocumentSection,
icon: <UnsubscribeIcon />,
tooltip: ({ activeCollectionId, isMenu, stores, t }) => {
if (!isMenu || !activeCollectionId) {
return undefined;
}
return stores.collections.get(activeCollectionId)?.isSubscribed
? t("Subscription inherited from collection")
: undefined;
},
disabled: ({ activeCollectionId, isMenu, stores }) => {
if (!isMenu || !activeCollectionId) {
return false;
}
return !!stores.collections.get(activeCollectionId)?.isSubscribed;
},
visible: ({ activeDocumentId, stores }) => {
if (!activeDocumentId) {
return false;
@@ -362,9 +454,10 @@ export const unsubscribeDocument = createAction({
const document = stores.documents.get(activeDocumentId);
return (
!!document?.collection?.isSubscribed ||
(!!document?.isSubscribed &&
stores.policies.abilities(activeDocumentId).unsubscribe)
!!document?.isActive &&
(!!document?.collection?.isSubscribed ||
(!!document?.isSubscribed &&
stores.policies.abilities(activeDocumentId).unsubscribe))
);
},
perform: async ({ activeDocumentId, stores, currentUserId, t }) => {
@@ -380,7 +473,7 @@ export const unsubscribeDocument = createAction({
},
});
export const shareDocument = createAction({
export const shareDocument = createActionV2({
name: ({ t }) => `${t("Permissions")}`,
analyticsName: "Share document",
section: ActiveDocumentSection,
@@ -413,7 +506,7 @@ export const shareDocument = createAction({
},
});
export const downloadDocumentAsHTML = createAction({
export const downloadDocumentAsHTML = createActionV2({
name: ({ t }) => t("HTML"),
analyticsName: "Download document as HTML",
section: ActiveDocumentSection,
@@ -432,7 +525,7 @@ export const downloadDocumentAsHTML = createAction({
},
});
export const downloadDocumentAsPDF = createAction({
export const downloadDocumentAsPDF = createActionV2({
name: ({ t }) => t("PDF"),
analyticsName: "Download document as PDF",
section: ActiveDocumentSection,
@@ -440,9 +533,11 @@ export const downloadDocumentAsPDF = createAction({
icon: <DownloadIcon />,
iconInContextMenu: false,
visible: ({ activeDocumentId, stores }) =>
!!activeDocumentId &&
stores.policies.abilities(activeDocumentId).download &&
env.PDF_EXPORT_ENABLED,
!!(
activeDocumentId &&
stores.policies.abilities(activeDocumentId).download &&
env.PDF_EXPORT_ENABLED
),
perform: ({ activeDocumentId, t, stores }) => {
if (!activeDocumentId) {
return;
@@ -456,7 +551,7 @@ export const downloadDocumentAsPDF = createAction({
},
});
export const downloadDocumentAsMarkdown = createAction({
export const downloadDocumentAsMarkdown = createActionV2({
name: ({ t }) => t("Markdown"),
analyticsName: "Download document as Markdown",
section: ActiveDocumentSection,
@@ -475,9 +570,8 @@ export const downloadDocumentAsMarkdown = createAction({
},
});
export const downloadDocument = createAction({
name: ({ t, isContextMenu }) =>
isContextMenu ? t("Download") : t("Download document"),
export const downloadDocument = createActionV2WithChildren({
name: ({ t, isMenu }) => (isMenu ? t("Download") : t("Download document")),
analyticsName: "Download document",
section: ActiveDocumentSection,
icon: <DownloadIcon />,
@@ -491,7 +585,7 @@ export const downloadDocument = createAction({
],
});
export const copyDocumentAsMarkdown = createAction({
export const copyDocumentAsMarkdown = createActionV2({
name: ({ t }) => t("Copy as Markdown"),
section: ActiveDocumentSection,
keywords: "clipboard",
@@ -510,7 +604,7 @@ export const copyDocumentAsMarkdown = createAction({
},
});
export const copyDocumentAsPlainText = createAction({
export const copyDocumentAsPlainText = createActionV2({
name: ({ t }) => t("Copy as text"),
section: ActiveDocumentSection,
keywords: "clipboard",
@@ -529,7 +623,7 @@ export const copyDocumentAsPlainText = createAction({
},
});
export const copyDocumentShareLink = createAction({
export const copyDocumentShareLink = createActionV2({
name: ({ t }) => t("Copy public link"),
section: ActiveDocumentSection,
keywords: "clipboard share",
@@ -550,7 +644,7 @@ export const copyDocumentShareLink = createAction({
},
});
export const copyDocumentLink = createAction({
export const copyDocumentLink = createActionV2({
name: ({ t }) => t("Copy link"),
section: ActiveDocumentSection,
keywords: "clipboard",
@@ -568,7 +662,7 @@ export const copyDocumentLink = createAction({
},
});
export const copyDocument = createAction({
export const copyDocument = createActionV2WithChildren({
name: ({ t }) => t("Copy"),
analyticsName: "Copy document",
section: ActiveDocumentSection,
@@ -582,9 +676,8 @@ export const copyDocument = createAction({
],
});
export const duplicateDocument = createAction({
name: ({ t, isContextMenu }) =>
isContextMenu ? t("Duplicate") : t("Duplicate document"),
export const duplicateDocument = createActionV2({
name: ({ t, isMenu }) => (isMenu ? t("Duplicate") : t("Duplicate document")),
analyticsName: "Duplicate document",
section: ActiveDocumentSection,
icon: <DuplicateIcon />,
@@ -618,7 +711,7 @@ export const duplicateDocument = createAction({
* 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 = createAction({
export const pinDocumentToCollection = createActionV2({
name: ({ activeDocumentId = "", t, stores }) => {
const selectedDocument = stores.documents.get(activeDocumentId);
const collectionName = selectedDocument
@@ -663,7 +756,7 @@ export const pinDocumentToCollection = createAction({
* 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 = createAction({
export const pinDocumentToHome = createActionV2({
name: ({ t }) => t("Pin to home"),
analyticsName: "Pin document to home",
section: ActiveDocumentSection,
@@ -695,7 +788,7 @@ export const pinDocumentToHome = createAction({
},
});
export const pinDocument = createAction({
export const pinDocument = createActionV2WithChildren({
name: ({ t }) => t("Pin"),
analyticsName: "Pin document",
section: ActiveDocumentSection,
@@ -703,7 +796,7 @@ export const pinDocument = createAction({
children: [pinDocumentToCollection, pinDocumentToHome],
});
export const searchInDocument = createAction({
export const searchInDocument = createInternalLinkActionV2({
name: ({ t }) => t("Search in document"),
analyticsName: "Search document",
section: ActiveDocumentSection,
@@ -716,14 +809,25 @@ export const searchInDocument = createAction({
const document = stores.documents.get(activeDocumentId);
return !!document?.isActive;
},
perform: ({ activeDocumentId }) => {
history.push(searchPath({ documentId: activeDocumentId }));
to: ({ activeDocumentId, sidebarContext }) => {
if (!activeDocumentId) {
return "";
}
const [pathname, search] = searchPath({
documentId: activeDocumentId,
}).split("?");
return {
pathname,
search,
state: { sidebarContext },
};
},
});
export const printDocument = createAction({
name: ({ t, isContextMenu }) =>
isContextMenu ? t("Print") : t("Print document"),
export const printDocument = createActionV2({
name: ({ t, isMenu }) => (isMenu ? t("Print") : t("Print document")),
analyticsName: "Print document",
section: ActiveDocumentSection,
icon: <PrintIcon />,
@@ -733,7 +837,7 @@ export const printDocument = createAction({
},
});
export const importDocument = createAction({
export const importDocument = createActionV2({
name: ({ t }) => t("Import document"),
analyticsName: "Import document",
section: DocumentSection,
@@ -780,7 +884,7 @@ export const importDocument = createAction({
},
});
export const createTemplateFromDocument = createAction({
export const createTemplateFromDocument = createActionV2({
name: ({ t }) => t("Templatize"),
analyticsName: "Templatize document",
section: ActiveDocumentSection,
@@ -838,11 +942,11 @@ export const searchDocumentsForQuery = (query: string) =>
analyticsName: "Search documents",
section: DocumentSection,
icon: <SearchIcon />,
perform: () => history.push(searchPath({ query })),
to: searchPath({ query }),
visible: ({ location }) => location.pathname !== searchPath(),
});
export const moveTemplateToWorkspace = createAction({
export const moveTemplateToWorkspace = createActionV2({
name: ({ t }) => t("Move to workspace"),
analyticsName: "Move template to workspace",
section: DocumentSection,
@@ -872,7 +976,7 @@ export const moveTemplateToWorkspace = createAction({
},
});
export const moveDocumentToCollection = createAction({
export const moveDocumentToCollection = createActionV2({
name: ({ activeDocumentId, stores, t }) => {
if (!activeDocumentId) {
return t("Move");
@@ -909,7 +1013,7 @@ export const moveDocumentToCollection = createAction({
},
});
export const moveDocument = createAction({
export const moveDocument = createActionV2({
name: ({ t }) => t("Move"),
analyticsName: "Move document",
section: ActiveDocumentSection,
@@ -928,7 +1032,7 @@ export const moveDocument = createAction({
perform: moveDocumentToCollection.perform,
});
export const moveTemplate = createAction({
export const moveTemplate = createActionV2WithChildren({
name: ({ t }) => t("Move"),
analyticsName: "Move document",
section: ActiveDocumentSection,
@@ -947,7 +1051,7 @@ export const moveTemplate = createAction({
children: [moveTemplateToWorkspace, moveDocumentToCollection],
});
export const archiveDocument = createAction({
export const archiveDocument = createActionV2({
name: ({ t }) => `${t("Archive")}`,
analyticsName: "Archive document",
section: ActiveDocumentSection,
@@ -987,7 +1091,102 @@ export const archiveDocument = createAction({
},
});
export const deleteDocument = createAction({
export const restoreDocument = createActionV2({
name: ({ t }) => `${t("Restore")}`,
analyticsName: "Restore document",
section: ActiveDocumentSection,
icon: <RestoreIcon />,
visible: ({ activeDocumentId, stores }) => {
const document = activeDocumentId
? stores.documents.get(activeDocumentId)
: undefined;
if (!document) {
return false;
}
const collection = document.collectionId
? stores.collections.get(document.collectionId)
: undefined;
const can = stores.policies.abilities(document.id);
return (
!!(document.isWorkspaceTemplate || collection?.isActive) &&
!!(can.restore || can.unarchive)
);
},
perform: async ({ t, stores, activeDocumentId }) => {
const document = activeDocumentId
? stores.documents.get(activeDocumentId)
: undefined;
if (!document) {
return;
}
await document.restore();
toast.success(
t("{{ documentName }} restored", {
documentName: capitalize(document.noun),
})
);
},
});
export const restoreDocumentToCollection = createActionV2WithChildren({
name: ({ t }) => `${t("Restore")}`,
analyticsName: "Restore document",
section: ActiveDocumentSection,
icon: <RestoreIcon />,
visible: ({ stores, activeDocumentId }) => {
const document = activeDocumentId
? stores.documents.get(activeDocumentId)
: undefined;
if (!document) {
return false;
}
const can = stores.policies.abilities(document.id);
const collection = document.collectionId
? stores.collections.get(document.collectionId)
: undefined;
return (
!(document.isWorkspaceTemplate || collection?.isActive) &&
!!(can.restore || can.unarchive)
);
},
children: ({ t, activeDocumentId, stores }) => {
const { collections, documents, policies } = stores;
const document = activeDocumentId
? documents.get(activeDocumentId)
: undefined;
if (!document) {
return [];
}
const actions = collections.orderedData.map((collection) => {
const can = policies.abilities(collection.id);
return createActionV2({
name: collection.name,
section: ActiveDocumentSection,
icon: <CollectionIcon collection={collection} />,
visible: can.createDocument,
perform: async () => {
await document.restore({ collectionId: collection.id });
toast.success(
t("{{ documentName }} restored", {
documentName: capitalize(document.noun),
})
);
},
});
});
return [createActionV2Group({ name: t("Choose a collection"), actions })];
},
});
export const deleteDocument = createActionV2({
name: ({ t }) => `${t("Delete")}`,
analyticsName: "Delete document",
section: ActiveDocumentSection,
@@ -1021,7 +1220,7 @@ export const deleteDocument = createAction({
},
});
export const permanentlyDeleteDocument = createAction({
export const permanentlyDeleteDocument = createActionV2({
name: ({ t }) => t("Permanently delete"),
analyticsName: "Permanently delete document",
section: ActiveDocumentSection,
@@ -1076,22 +1275,18 @@ export const permanentlyDeleteDocumentsInTrash = createAction({
},
});
export const openDocumentComments = createAction({
export const openDocumentComments = createActionV2({
name: ({ t }) => t("Comments"),
analyticsName: "Open comments",
section: ActiveDocumentSection,
icon: <CommentIcon />,
visible: ({ activeCollectionId, activeDocumentId, stores }) => {
visible: ({ activeDocumentId, stores }) => {
const can = stores.policies.abilities(activeDocumentId ?? "");
const collection = activeCollectionId
? stores.collections.get(activeCollectionId)
: undefined;
return (
!!activeDocumentId &&
can.comment &&
(collection?.canCreateComment ??
!!stores.auth.team?.getPreference(TeamPreference.Commenting))
!!stores.auth.team?.getPreference(TeamPreference.Commenting)
);
},
perform: ({ activeDocumentId, stores }) => {
@@ -1103,7 +1298,7 @@ export const openDocumentComments = createAction({
},
});
export const openDocumentHistory = createAction({
export const openDocumentHistory = createInternalLinkActionV2({
name: ({ t }) => t("History"),
analyticsName: "Open document history",
section: ActiveDocumentSection,
@@ -1112,19 +1307,25 @@ export const openDocumentHistory = createAction({
const can = stores.policies.abilities(activeDocumentId ?? "");
return !!activeDocumentId && can.listRevisions;
},
perform: ({ activeDocumentId, stores }) => {
if (!activeDocumentId) {
return;
}
const document = stores.documents.get(activeDocumentId);
to: ({ activeDocumentId, stores, sidebarContext }) => {
const document = activeDocumentId
? stores.documents.get(activeDocumentId)
: undefined;
if (!document) {
return;
return "";
}
history.push(documentHistoryPath(document));
const [pathname, search] = documentHistoryPath(document).split("?");
return {
pathname,
search,
state: { sidebarContext },
};
},
});
export const openDocumentInsights = createAction({
export const openDocumentInsights = createActionV2({
name: ({ t }) => t("Insights"),
analyticsName: "Open document insights",
section: ActiveDocumentSection,
@@ -1142,50 +1343,22 @@ export const openDocumentInsights = createAction({
!document?.isDeleted
);
},
perform: ({ activeDocumentId, stores }) => {
if (!activeDocumentId) {
return;
}
const document = stores.documents.get(activeDocumentId);
if (!document) {
return;
}
history.push(documentInsightsPath(document));
},
});
export const toggleViewerInsights = createAction({
name: ({ t, stores, activeDocumentId }) => {
perform: ({ activeDocumentId, stores, t }) => {
const document = activeDocumentId
? stores.documents.get(activeDocumentId)
: undefined;
return document?.insightsEnabled
? t("Disable viewer insights")
: t("Enable viewer insights");
},
analyticsName: "Toggle viewer insights",
section: ActiveDocumentSection,
icon: <EyeIcon />,
visible: ({ activeDocumentId, stores }) => {
const can = stores.policies.abilities(activeDocumentId ?? "");
return can.updateInsights;
},
perform: async ({ activeDocumentId, stores }) => {
if (!activeDocumentId) {
return;
}
const document = stores.documents.get(activeDocumentId);
if (!document) {
return;
}
await document.save({
insightsEnabled: !document.insightsEnabled,
stores.dialogs.openModal({
title: t("Insights"),
content: <Insights document={document} />,
});
},
});
export const leaveDocument = createAction({
export const leaveDocument = createActionV2({
name: ({ t }) => t("Leave document"),
analyticsName: "Leave document",
section: ActiveDocumentSection,
@@ -1221,6 +1394,27 @@ export const leaveDocument = createAction({
},
});
export const applyTemplateFactory = ({
actions,
}: {
actions: (ActionV2 | ActionV2Group | ActionV2Separator)[];
}) =>
createActionV2WithChildren({
name: ({ t }) => t("Apply template"),
analyticsName: "Apply template",
section: ActiveDocumentSection,
icon: <ShapesIcon />,
visible: ({ activeDocumentId, stores }) => {
const { policies } = stores;
const can = activeDocumentId
? policies.abilities(activeDocumentId)
: undefined;
return !!can?.update;
},
children: actions,
});
export const rootDocumentActions = [
openDocument,
archiveDocument,
+28
View File
@@ -0,0 +1,28 @@
import { TrashIcon } from "outline-icons";
import stores from "~/stores";
import { createAction } from "..";
import { SettingsSection } from "../sections";
import Integration from "~/models/Integration";
import { IntegrationType } from "@shared/types";
import { DisconnectAnalyticsDialog } from "~/components/DisconnectAnalyticsDialog";
export const disconnectAnalyticsIntegrationFactory = (
integration?: Integration<IntegrationType.Analytics>
) =>
createAction({
name: ({ t }) => t("Disconnect analytics"),
analyticsName: "Disconnect analytics",
section: SettingsSection,
icon: <TrashIcon />,
keywords: "disconnect",
visible: () => !!integration,
perform: ({ t, event }) => {
event?.preventDefault();
event?.stopPropagation();
stores.dialogs.openModal({
title: t("Disconnect analytics"),
content: <DisconnectAnalyticsDialog integration={integration!} />,
});
},
});
+48 -40
View File
@@ -12,17 +12,21 @@ import {
BrowserIcon,
ShapesIcon,
DraftsIcon,
BugIcon,
} from "outline-icons";
import { UrlHelper } from "@shared/utils/UrlHelper";
import { isMac } from "@shared/utils/browser";
import stores from "~/stores";
import SearchQuery from "~/models/SearchQuery";
import KeyboardShortcuts from "~/scenes/KeyboardShortcuts";
import { createAction } from "~/actions";
import {
createAction,
createActionV2,
createExternalLinkActionV2,
createInternalLinkActionV2,
} from "~/actions";
import { NavigationSection, RecentSearchesSection } from "~/actions/sections";
import env from "~/env";
import Desktop from "~/utils/Desktop";
import history from "~/utils/history";
import isCloudHosted from "~/utils/isCloudHosted";
import {
homePath,
@@ -39,7 +43,7 @@ export const navigateToHome = createAction({
section: NavigationSection,
shortcut: ["d"],
icon: <HomeIcon />,
perform: () => history.push(homePath()),
to: homePath(),
visible: ({ location }) => location.pathname !== homePath(),
});
@@ -49,7 +53,7 @@ export const navigateToRecentSearchQuery = (searchQuery: SearchQuery) =>
name: searchQuery.query,
analyticsName: "Navigate to recent search query",
icon: <SearchIcon />,
perform: () => history.push(searchPath({ query: searchQuery.query })),
to: searchPath({ query: searchQuery.query }),
});
export const navigateToDrafts = createAction({
@@ -57,7 +61,7 @@ export const navigateToDrafts = createAction({
analyticsName: "Navigate to drafts",
section: NavigationSection,
icon: <DraftsIcon />,
perform: () => history.push(draftsPath()),
to: draftsPath(),
visible: ({ location }) => location.pathname !== draftsPath(),
});
@@ -66,7 +70,7 @@ export const navigateToSearch = createAction({
analyticsName: "Navigate to search",
section: NavigationSection,
icon: <SearchIcon />,
perform: () => history.push(searchPath()),
to: searchPath(),
visible: ({ location }) => location.pathname !== searchPath(),
});
@@ -76,7 +80,7 @@ export const navigateToArchive = createAction({
section: NavigationSection,
shortcut: ["g", "a"],
icon: <ArchiveIcon />,
perform: () => history.push(archivePath()),
to: archivePath(),
visible: ({ location }) => location.pathname !== archivePath(),
});
@@ -85,7 +89,7 @@ export const navigateToTrash = createAction({
analyticsName: "Navigate to trash",
section: NavigationSection,
icon: <TrashIcon />,
perform: () => history.push(trashPath()),
to: trashPath(),
visible: ({ location }) => location.pathname !== trashPath(),
});
@@ -96,25 +100,25 @@ export const navigateToSettings = createAction({
shortcut: ["g", "s"],
icon: <SettingsIcon />,
visible: () => stores.policies.abilities(stores.auth.team?.id || "").update,
perform: () => history.push(settingsPath()),
to: settingsPath(),
});
export const navigateToWorkspaceSettings = createAction({
export const navigateToWorkspaceSettings = createInternalLinkActionV2({
name: ({ t }) => t("Settings"),
analyticsName: "Navigate to workspace settings",
section: NavigationSection,
icon: <SettingsIcon />,
visible: () => stores.policies.abilities(stores.auth.team?.id || "").update,
perform: () => history.push(settingsPath("details")),
to: settingsPath("details"),
});
export const navigateToProfileSettings = createAction({
export const navigateToProfileSettings = createInternalLinkActionV2({
name: ({ t }) => t("Profile"),
analyticsName: "Navigate to profile settings",
section: NavigationSection,
iconInContextMenu: false,
icon: <ProfileIcon />,
perform: () => history.push(settingsPath()),
to: settingsPath(),
});
export const navigateToTemplateSettings = createAction({
@@ -123,43 +127,46 @@ export const navigateToTemplateSettings = createAction({
section: NavigationSection,
iconInContextMenu: false,
icon: <ShapesIcon />,
perform: () => history.push(settingsPath("templates")),
to: settingsPath("templates"),
});
export const navigateToNotificationSettings = createAction({
name: ({ t }) => t("Notifications"),
export const navigateToNotificationSettings = createInternalLinkActionV2({
name: ({ t, isMenu }) =>
isMenu ? t("Notification settings") : t("Notifications"),
analyticsName: "Navigate to notification settings",
section: NavigationSection,
iconInContextMenu: false,
icon: <EmailIcon />,
perform: () => history.push(settingsPath("notifications")),
to: settingsPath("notifications"),
});
export const navigateToAccountPreferences = createAction({
export const navigateToAccountPreferences = createInternalLinkActionV2({
name: ({ t }) => t("Preferences"),
analyticsName: "Navigate to account preferences",
section: NavigationSection,
iconInContextMenu: false,
icon: <SettingsIcon />,
perform: () => history.push(settingsPath("preferences")),
to: settingsPath("preferences"),
});
export const openDocumentation = createAction({
export const openDocumentation = createExternalLinkActionV2({
name: ({ t }) => t("Documentation"),
analyticsName: "Open documentation",
section: NavigationSection,
iconInContextMenu: false,
icon: <OpenIcon />,
perform: () => window.open(UrlHelper.guide),
url: UrlHelper.guide,
target: "_blank",
});
export const openAPIDocumentation = createAction({
export const openAPIDocumentation = createExternalLinkActionV2({
name: ({ t }) => t("API documentation"),
analyticsName: "Open API documentation",
section: NavigationSection,
iconInContextMenu: false,
icon: <OpenIcon />,
perform: () => window.open(UrlHelper.developers),
url: UrlHelper.developers,
target: "_blank",
});
export const toggleSidebar = createAction({
@@ -170,32 +177,37 @@ export const toggleSidebar = createAction({
perform: () => stores.ui.toggleCollapsedSidebar(),
});
export const openFeedbackUrl = createAction({
export const openFeedbackUrl = createExternalLinkActionV2({
name: ({ t }) => t("Send us feedback"),
analyticsName: "Open feedback",
section: NavigationSection,
iconInContextMenu: false,
icon: <EmailIcon />,
perform: () => window.open(UrlHelper.contact),
url: UrlHelper.contact,
target: "_blank",
});
export const openBugReportUrl = createAction({
export const openBugReportUrl = createExternalLinkActionV2({
name: ({ t }) => t("Report a bug"),
analyticsName: "Open bug report",
section: NavigationSection,
perform: () => window.open(UrlHelper.github),
iconInContextMenu: false,
icon: <BugIcon />,
url: UrlHelper.github,
target: "_blank",
});
export const openChangelog = createAction({
export const openChangelog = createExternalLinkActionV2({
name: ({ t }) => t("Changelog"),
analyticsName: "Open changelog",
section: NavigationSection,
iconInContextMenu: false,
icon: <OpenIcon />,
perform: () => window.open(UrlHelper.changelog),
url: UrlHelper.changelog,
target: "_blank",
});
export const openKeyboardShortcuts = createAction({
export const openKeyboardShortcuts = createActionV2({
name: ({ t }) => t("Keyboard shortcuts"),
analyticsName: "Open keyboard shortcuts",
section: NavigationSection,
@@ -220,23 +232,19 @@ export const downloadApp = createAction({
iconInContextMenu: false,
icon: <BrowserIcon />,
visible: () => !Desktop.isElectron() && isMac() && isCloudHosted,
perform: () => {
window.open("https://desktop.getoutline.com");
to: {
url: "https://desktop.getoutline.com",
target: "_blank",
},
});
export const logout = createAction({
export const logout = createActionV2({
name: ({ t }) => t("Log out"),
analyticsName: "Log out",
section: NavigationSection,
icon: <LogoutIcon />,
perform: async () => {
await stores.auth.logout();
if (env.OIDC_LOGOUT_URI) {
setTimeout(() => {
window.location.replace(env.OIDC_LOGOUT_URI);
}, 200);
}
await stores.auth.logout({ userInitiated: true });
},
});
+2 -2
View File
@@ -1,5 +1,5 @@
import { ArchiveIcon, MarkAsReadIcon } from "outline-icons";
import { createAction } from "..";
import { createAction, createActionV2 } 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 = createAction({
export const markNotificationsAsArchived = createActionV2({
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 } from "~/actions";
import { createAction, createActionV2 } 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 = createAction({
export const restoreRevision = createActionV2({
name: ({ t }) => t("Restore"),
analyticsName: "Restore revision",
icon: <RestoreIcon />,
@@ -73,7 +73,7 @@ export const deleteRevision = createAction({
},
});
export const copyLinkToRevision = createAction({
export const copyLinkToRevision = createActionV2({
name: ({ t }) => t("Copy link"),
analyticsName: "Copy link to revision",
icon: <LinkIcon />,
+14 -17
View File
@@ -1,50 +1,47 @@
import { SunIcon, MoonIcon, BrowserIcon } from "outline-icons";
import stores from "~/stores";
import { Theme } from "~/stores/UiStore";
import { createAction } from "~/actions";
import { createActionV2, createActionV2WithChildren } from "~/actions";
import { SettingsSection } from "~/actions/sections";
export const changeToDarkTheme = createAction({
export const changeToDarkTheme = createActionV2({
name: ({ t }) => t("Dark"),
analyticsName: "Change to dark theme",
icon: <MoonIcon />,
iconInContextMenu: false,
keywords: "theme dark night",
section: SettingsSection,
selected: () => stores.ui.theme === "dark",
perform: () => stores.ui.setTheme(Theme.Dark),
selected: ({ stores }) => stores.ui.theme === "dark",
perform: ({ stores }) => stores.ui.setTheme(Theme.Dark),
});
export const changeToLightTheme = createAction({
export const changeToLightTheme = createActionV2({
name: ({ t }) => t("Light"),
analyticsName: "Change to light theme",
icon: <SunIcon />,
iconInContextMenu: false,
keywords: "theme light day",
section: SettingsSection,
selected: () => stores.ui.theme === "light",
perform: () => stores.ui.setTheme(Theme.Light),
selected: ({ stores }) => stores.ui.theme === "light",
perform: ({ stores }) => stores.ui.setTheme(Theme.Light),
});
export const changeToSystemTheme = createAction({
export const changeToSystemTheme = createActionV2({
name: ({ t }) => t("System"),
analyticsName: "Change to system theme",
icon: <BrowserIcon />,
iconInContextMenu: false,
keywords: "theme system default",
section: SettingsSection,
selected: () => stores.ui.theme === "system",
perform: () => stores.ui.setTheme(Theme.System),
selected: ({ stores }) => stores.ui.theme === "system",
perform: ({ stores }) => stores.ui.setTheme(Theme.System),
});
export const changeTheme = createAction({
name: ({ t, isContextMenu }) =>
isContextMenu ? t("Appearance") : t("Change theme"),
export const changeTheme = createActionV2WithChildren({
name: ({ t, isMenu }) => (isMenu ? t("Appearance") : t("Change theme")),
analyticsName: "Change theme",
placeholder: ({ t }) => t("Change theme to"),
icon: function _Icon() {
return stores.ui.resolvedTheme === "light" ? <SunIcon /> : <MoonIcon />;
},
icon: ({ stores }) =>
stores.ui.resolvedTheme === "light" ? <SunIcon /> : <MoonIcon />,
keywords: "appearance display",
section: SettingsSection,
children: [changeToLightTheme, changeToDarkTheme, changeToSystemTheme],
+59
View File
@@ -0,0 +1,59 @@
import copy from "copy-to-clipboard";
import Share from "~/models/Share";
import { createActionV2, createInternalLinkActionV2 } 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({
name: ({ t }) => t("Copy link"),
analyticsName: "Copy share link",
section: ShareSection,
icon: <CopyIcon />,
perform: ({ t }) => {
copy(share.url, {
debug: env.ENVIRONMENT !== "production",
format: "text/plain",
});
toast.success(t("Share link copied"));
},
});
export const goToShareSourceFactory = ({ share }: { share: Share }) =>
createInternalLinkActionV2({
name: ({ t }) =>
share.collectionId ? t("Go to collection") : t("Go to document"),
analyticsName: "Go to share source",
section: ShareSection,
icon: <ArrowIcon />,
to: {
pathname: share.sourcePathWithFallback,
state: { sidebarContext: "collections" }, // optimistic preference of "collections"
},
});
export const revokeShareFactory = ({
share,
can,
}: {
share: Share;
can: Record<string, boolean>;
}) =>
createActionV2({
name: ({ t }) => t("Revoke link"),
analyticsName: "Revoke share",
section: ShareSection,
icon: <TrashIcon />,
dangerous: true,
visible: !!can.revoke,
perform: async ({ t, stores }) => {
try {
await stores.shares.revoke(share);
toast.message(t("Share link revoked"));
} catch (err) {
toast.error(err.message);
}
},
});
+24 -19
View File
@@ -5,20 +5,24 @@ import RootStore from "~/stores/RootStore";
import { LoginDialog } from "~/scenes/Login/components/LoginDialog";
import TeamNew from "~/scenes/TeamNew";
import TeamLogo from "~/components/TeamLogo";
import { createAction } from "~/actions";
import { ActionContext } from "~/types";
import {
createActionV2,
createActionV2WithChildren,
createExternalLinkActionV2,
} from "~/actions";
import { ActionContext, ExternalLinkActionV2 } from "~/types";
import Desktop from "~/utils/Desktop";
import { TeamSection } from "../sections";
export const switchTeamsList = ({ stores }: { stores: RootStore }) =>
stores.auth.availableTeams?.map((session) => ({
id: `switch-${session.id}`,
name: session.name,
analyticsName: "Switch workspace",
section: TeamSection,
keywords: "change switch workspace organization team",
icon: function _Icon() {
return (
stores.auth.availableTeams?.map<ExternalLinkActionV2>((session) =>
createExternalLinkActionV2({
id: `switch-${session.id}`,
name: session.name,
analyticsName: "Switch workspace",
section: TeamSection,
keywords: "change switch workspace organization team",
icon: (
<StyledTeamLogo
alt={session.name}
model={{
@@ -29,13 +33,15 @@ export const switchTeamsList = ({ stores }: { stores: RootStore }) =>
}}
size={24}
/>
);
},
visible: ({ currentTeamId }: ActionContext) => currentTeamId !== session.id,
perform: () => (window.location.href = session.url),
})) ?? [];
),
visible: ({ currentTeamId }: ActionContext) =>
currentTeamId !== session.id,
url: session.url,
target: "_self",
})
) ?? [];
export const switchTeam = createAction({
export const switchTeam = createActionV2WithChildren({
name: ({ t }) => t("Switch workspace"),
placeholder: ({ t }) => t("Select a workspace"),
analyticsName: "Switch workspace",
@@ -46,7 +52,7 @@ export const switchTeam = createAction({
children: switchTeamsList,
});
export const createTeam = createAction({
export const createTeam = createActionV2({
name: ({ t }) => `${t("New workspace")}`,
analyticsName: "New workspace",
keywords: "create change switch workspace organization team",
@@ -62,14 +68,13 @@ export const createTeam = createAction({
if (user) {
stores.dialogs.openModal({
title: t("Create a workspace"),
fullscreen: true,
content: <TeamNew user={user} />,
});
}
},
});
export const desktopLoginTeam = createAction({
export const desktopLoginTeam = createActionV2({
name: ({ t }) => t("Login to workspace"),
analyticsName: "Login to workspace",
keywords: "change switch workspace organization team",
+5 -5
View File
@@ -8,7 +8,7 @@ import {
UserChangeRoleDialog,
UserDeleteDialog,
} from "~/components/UserDialogs";
import { createAction } from "~/actions";
import { createAction, createActionV2 } from "~/actions";
import { UserSection } from "~/actions/sections";
export const inviteUser = createAction({
@@ -28,7 +28,7 @@ export const inviteUser = createAction({
});
export const updateUserRoleActionFactory = (user: User, role: UserRole) =>
createAction({
createActionV2({
name: ({ t }) =>
UserRoleHelper.isRoleHigher(role, user!.role)
? `${t("Promote to {{ role }}", {
@@ -45,8 +45,8 @@ export const updateUserRoleActionFactory = (user: User, role: UserRole) =>
return UserRoleHelper.isRoleHigher(role, user.role)
? can.promote
: UserRoleHelper.isRoleLower(role, user.role)
? can.demote
: false;
? can.demote
: false;
},
perform: ({ t }) => {
stores.dialogs.openModal({
@@ -63,7 +63,7 @@ export const updateUserRoleActionFactory = (user: User, role: UserRole) =>
});
export const deleteUserActionFactory = (userId: string) =>
createAction({
createActionV2({
name: ({ t }) => `${t("Delete user")}`,
analyticsName: "Delete user",
keywords: "leave",
+329 -13
View File
@@ -1,3 +1,4 @@
import { LocationDescriptor } from "history";
import flattenDeep from "lodash/flattenDeep";
import { toast } from "sonner";
import { Optional } from "utility-types";
@@ -5,13 +6,24 @@ import { v4 as uuidv4 } from "uuid";
import {
Action,
ActionContext,
CommandBarAction,
ActionV2,
ActionV2Group,
ActionV2Separator as TActionV2Separator,
ActionV2Variant,
ActionV2WithChildren,
ExternalLinkActionV2,
InternalLinkActionV2,
MenuExternalLink,
MenuInternalLink,
MenuItem,
MenuItemButton,
MenuItemWithChildren,
} from "~/types";
import Analytics from "~/utils/Analytics";
import history from "~/utils/history";
import { Action as KbarAction } from "kbar";
function resolve<T>(value: any, context: ActionContext): T {
export function resolve<T>(value: any, context: ActionContext): T {
return typeof value === "function" ? value(context) : value;
}
@@ -20,18 +32,17 @@ export function createAction(definition: Optional<Action, "id">): Action {
...definition,
perform: definition.perform
? (context) => {
// We muse use the specific analytics name here as the action name is
// 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",
? "commandbar"
: "contextmenu",
});
}
return definition.perform?.(context);
}
: undefined,
@@ -42,7 +53,7 @@ export function createAction(definition: Optional<Action, "id">): Action {
export function actionToMenuItem(
action: Action,
context: ActionContext
): MenuItemButton | MenuItemWithChildren {
): 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;
@@ -67,6 +78,26 @@ export function actionToMenuItem(
};
}
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,
@@ -81,7 +112,7 @@ export function actionToMenuItem(
export function actionToKBar(
action: Action,
context: ActionContext
): CommandBarAction[] {
): KbarAction[] {
if (typeof action.visible === "function" && !action.visible(context)) {
return [];
}
@@ -99,7 +130,7 @@ export function actionToKBar(
const sectionPriority =
typeof action.section !== "string" && "priority" in action.section
? (action.section.priority as number) ?? 0
? ((action.section.priority as number) ?? 0)
: 0;
return [
@@ -113,9 +144,10 @@ export function actionToKBar(
shortcut: action.shortcut || [],
icon: resolvedIcon,
priority: (1 + (action.priority ?? 0)) * (1 + (sectionPriority ?? 0)),
perform: action.perform
? () => performAction(action, context)
: undefined,
perform:
action.perform || action.to
? () => performAction(action, context)
: undefined,
},
].concat(
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
@@ -124,7 +156,13 @@ export function actionToKBar(
}
export async function performAction(action: Action, context: ActionContext) {
const result = action.perform?.(context);
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) => {
@@ -134,3 +172,281 @@ export async function performAction(action: Action, context: ActionContext) {
return result;
}
/** Actions V2 */
export const ActionV2Separator: TActionV2Separator = {
type: "action_separator",
};
export function createActionV2(
definition: Optional<Omit<ActionV2, "type" | "variant">, "id">
): ActionV2 {
return {
...definition,
type: "action",
variant: "action",
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);
}
: () => {},
id: definition.id ?? uuidv4(),
};
}
export function createInternalLinkActionV2(
definition: Optional<Omit<InternalLinkActionV2, "type" | "variant">, "id">
): InternalLinkActionV2 {
return {
...definition,
type: "action",
variant: "internal_link",
id: definition.id ?? uuidv4(),
};
}
export function createExternalLinkActionV2(
definition: Optional<Omit<ExternalLinkActionV2, "type" | "variant">, "id">
): ExternalLinkActionV2 {
return {
...definition,
type: "action",
variant: "external_link",
id: definition.id ?? uuidv4(),
};
}
export function createActionV2WithChildren(
definition: Optional<Omit<ActionV2WithChildren, "type" | "variant">, "id">
): ActionV2WithChildren {
return {
...definition,
type: "action",
variant: "action_with_children",
id: definition.id ?? uuidv4(),
};
}
export function createActionV2Group(
definition: Omit<ActionV2Group, "type">
): ActionV2Group {
return {
...definition,
type: "action_group",
};
}
export function createRootMenuAction(
actions: (ActionV2Variant | ActionV2Group | TActionV2Separator)[]
): ActionV2WithChildren {
return {
id: uuidv4(),
type: "action",
variant: "action_with_children",
name: "root_action",
section: "Root",
children: actions,
};
}
export function actionV2ToMenuItem(
action: ActionV2Variant | ActionV2Group | TActionV2Separator,
context: ActionContext
): MenuItem {
switch (action.type) {
case "action": {
const title = resolve<string>(action.name, context);
const visible = resolve<boolean>(action.visible, context) ?? true;
const disabled = resolve<boolean>(action.disabled, context);
const icon =
!!action.icon && action.iconInContextMenu !== false
? resolve<React.ReactNode>(action.icon, context)
: undefined;
switch (action.variant) {
case "action":
return {
type: "button",
title,
icon,
visible,
disabled,
tooltip: resolve<React.ReactChild>(action.tooltip, context),
selected: resolve<boolean>(action.selected, context),
dangerous: action.dangerous,
onClick: () => performActionV2(action, context),
};
case "internal_link": {
const to = resolve<LocationDescriptor>(action.to, context);
return {
type: "route",
title,
icon,
visible,
disabled,
to,
};
}
case "external_link":
return {
type: "link",
title,
icon,
visible,
disabled,
href: action.target
? { url: action.url, target: action.target }
: action.url,
};
case "action_with_children": {
const children = resolve<
(ActionV2Variant | ActionV2Group | TActionV2Separator)[]
>(action.children, context);
const subMenuItems = children.map((a) =>
actionV2ToMenuItem(a, context)
);
return {
type: "submenu",
title,
icon,
items: subMenuItems,
disabled,
visible: visible && hasVisibleItems(subMenuItems),
};
}
default:
throw Error("invalid action variant");
}
}
case "action_group": {
const groupItems = action.actions.map((a) =>
actionV2ToMenuItem(a, context)
);
return {
type: "group",
title: resolve<string>(action.name, context),
visible: hasVisibleItems(groupItems),
items: groupItems,
};
}
case "action_separator":
return { type: "separator" };
}
}
export function actionV2ToKBar(
action: ActionV2Variant,
context: ActionContext
): KbarAction[] {
const visible = resolve<boolean>(action.visible, context);
if (visible === false) {
return [];
}
const name = resolve<string>(action.name, context);
const icon = resolve<React.ReactElement>(action.icon, context);
const section = resolve<string>(action.section, context);
const sectionPriority =
typeof action.section !== "string" && "priority" in action.section
? ((action.section.priority as number) ?? 0)
: 0;
const priority = (1 + (action.priority ?? 0)) * (1 + (sectionPriority ?? 0));
switch (action.variant) {
case "action":
case "internal_link":
case "external_link": {
return [
{
id: action.id,
name,
section,
keywords: action.keywords,
shortcut: action.shortcut,
icon,
priority,
perform: () => performActionV2(action, context),
},
];
}
case "action_with_children": {
const resolvedChildren = resolve<ActionV2Variant[]>(
action.children,
context
);
const children = resolvedChildren
.map((a) => actionV2ToKBar(a, context))
.flat()
.filter(Boolean);
return [
{
id: action.id,
name,
section,
keywords: action.keywords,
shortcut: action.shortcut,
icon,
priority,
},
...children.map((child) => ({
...child,
parent: child.parent ?? action.id,
})),
];
}
default:
throw Error("invalid action variant");
}
}
export async function performActionV2(
action: Exclude<ActionV2Variant, ActionV2WithChildren>,
context: ActionContext
) {
const perform =
action.variant === "action"
? () => action.perform(context)
: action.variant === "internal_link"
? () => history.push(resolve<LocationDescriptor>(action.to, context))
: () => window.open(action.url, action.target);
const result = perform();
if (result instanceof Promise) {
return result.catch((err: Error) => {
toast.error(err.message);
});
}
return result;
}
function hasVisibleItems(items: MenuItem[]) {
const applicableTypes = ["button", "link", "route", "group", "submenu"];
return items.some(
(item) => applicableTypes.includes(item.type) && item.visible
);
}
+4
View File
@@ -36,10 +36,14 @@ export const NavigationSection = ({ t }: ActionContext) => t("Navigation");
export const NotificationSection = ({ t }: ActionContext) => t("Notification");
export const GroupSection = ({ t }: ActionContext) => t("Groups");
export const UserSection = ({ t }: ActionContext) => t("People");
UserSection.priority = 0.5;
export const ShareSection = ({ t }: ActionContext) => t("Share");
export const TeamSection = ({ t }: ActionContext) => t("Workspace");
export const RecentSearchesSection = ({ t }: ActionContext) =>
+20 -19
View File
@@ -1,9 +1,10 @@
/* eslint-disable react/prop-types */
/* oxlint-disable react/prop-types */
import * as React from "react";
import Tooltip, { Props as TooltipProps } from "~/components/Tooltip";
import { performAction } from "~/actions";
import { performAction, performActionV2, resolve } from "~/actions";
import useIsMounted from "~/hooks/useIsMounted";
import { Action, ActionContext } from "~/types";
import { Action, ActionV2Variant, ActionV2WithChildren } from "~/types";
import useActionContext from "~/hooks/useActionContext";
export type Props = React.HTMLAttributes<HTMLButtonElement> & {
/** Show the button in a disabled state */
@@ -11,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;
/** Context of action, must be provided with action */
context?: ActionContext;
action?: Action | Exclude<ActionV2Variant, ActionV2WithChildren>;
/** If tooltip props are provided the button will be wrapped in a tooltip */
tooltip?: Omit<TooltipProps, "children">;
};
@@ -23,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 };
if (
action?.visible &&
!action.visible(actionContext) &&
action.visible &&
!resolve<boolean>(action.visible, actionContext) &&
hideOnActionDisabled
) {
return null;
}
const label =
typeof action.name === "function"
rest["aria-label"] ??
(typeof action.name === "function"
? action.name(actionContext)
: action.name;
: action.name);
const button = (
<button
@@ -59,11 +57,14 @@ const ActionButton = React.forwardRef<HTMLButtonElement, Props>(
disabled={disabled || executing}
ref={ref}
onClick={
action?.perform && actionContext
actionContext
? (ev) => {
ev.preventDefault();
ev.stopPropagation();
const response = performAction(action, actionContext);
const response =
"variant" in action
? performActionV2(action, actionContext)
: 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;
+1 -1
View File
@@ -1,4 +1,4 @@
/* eslint-disable prefer-rest-params */
/* oxlint-disable prefer-rest-params */
/* global ga */
import escape from "lodash/escape";
import * as React from "react";
+6 -1
View File
@@ -31,7 +31,12 @@ const Authenticated = ({ children }: Props) => {
return <LoadingIndicator />;
}
void auth.logout(true);
void auth.logout({ savePath: true });
if (auth.logoutRedirectUri) {
window.location.href = auth.logoutRedirectUri;
return null;
}
return <Redirect to="/" />;
};
+8 -22
View File
@@ -13,7 +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";
@@ -27,7 +26,6 @@ import {
settingsPath,
matchDocumentHistory,
matchDocumentSlug as slug,
matchDocumentInsights,
} from "~/utils/routeHelpers";
import { DocumentContextProvider } from "./DocumentContext";
import Fade from "./Fade";
@@ -39,9 +37,7 @@ const DocumentComments = lazyWithRetry(
const DocumentHistory = lazyWithRetry(
() => import("~/scenes/Document/components/History")
);
const DocumentInsights = lazyWithRetry(
() => import("~/scenes/Document/components/Insights")
);
const CommandBar = lazyWithRetry(() => import("~/components/CommandBar"));
type Props = {
@@ -49,7 +45,7 @@ type Props = {
};
const AuthenticatedLayout: React.FC = ({ children }: Props) => {
const { ui, auth, collections } = useStores();
const { ui, auth } = useStores();
const location = useLocation();
const layoutRef = React.useRef<HTMLDivElement>(null);
const can = usePolicy(ui.activeDocumentId);
@@ -98,34 +94,24 @@ const AuthenticatedLayout: React.FC = ({ children }: Props) => {
!!matchPath(location.pathname, {
path: matchDocumentHistory,
}) && can.listRevisions;
const showInsights =
!!matchPath(location.pathname, {
path: matchDocumentInsights,
}) && can.listViews;
const showComments =
!showInsights &&
!showHistory &&
can.comment &&
ui.activeDocumentId &&
ui.commentsExpanded &&
(ui.activeCollectionId
? collections.get(ui.activeCollectionId)?.canCreateComment
: !!team.getPreference(TeamPreference.Commenting));
!!team.getPreference(TeamPreference.Commenting);
const sidebarRight = (
<AnimatePresence
initial={false}
key={ui.activeDocumentId ? "active" : "inactive"}
>
{(showHistory || showInsights || showComments) && (
{(showHistory || showComments) && (
<Route path={`/doc/${slug}`}>
<SidebarRight>
<React.Suspense fallback={null}>
{showHistory && <DocumentHistory />}
{showInsights && <DocumentInsights />}
{showComments && <DocumentComments />}
</React.Suspense>
</SidebarRight>
<React.Suspense fallback={null}>
{showHistory && <DocumentHistory />}
{showComments && <DocumentComments />}
</React.Suspense>
</Route>
)}
</AnimatePresence>
+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>
</>
+2 -2
View File
@@ -10,8 +10,8 @@ const Badge = styled.span<{ yellow?: boolean; primary?: boolean }>`
primary
? theme.accentText
: yellow
? theme.almostBlack
: theme.textTertiary};
? theme.almostBlack
: theme.textTertiary};
border: 1px solid
${({ primary, yellow, theme }) =>
primary || yellow
+10 -13
View File
@@ -1,5 +1,4 @@
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import { depths, s } from "@shared/styles";
import env from "~/env";
import OutlineIcon from "./Icons/OutlineIcon";
@@ -10,7 +9,7 @@ type Props = {
function Branding({ href = env.URL }: Props) {
return (
<Link href={href}>
<Link href={href} target="_blank">
<OutlineIcon size={20} />
&nbsp;{env.APP_NAME}
</Link>
@@ -33,18 +32,16 @@ const Link = styled.a`
fill: ${s("text")};
}
${breakpoint("tablet")`
z-index: ${depths.sidebar + 1};
background: ${s("sidebarBackground")};
position: fixed;
bottom: 0;
right: 0;
padding: 16px;
z-index: ${depths.sidebar + 1};
background: ${s("sidebarBackground")};
position: fixed;
bottom: 0;
right: 0;
padding: 16px;
&:hover {
background: ${s("sidebarControlHoverBackground")};
}
`};
&:hover {
background: ${s("sidebarControlHoverBackground")};
}
`;
export default Branding;
+65 -31
View File
@@ -6,55 +6,89 @@ import { s, ellipsis } from "@shared/styles";
import Flex from "~/components/Flex";
import BreadcrumbMenu from "~/menus/BreadcrumbMenu";
import { undraggableOnDesktop } from "~/styles";
import { MenuInternalLink } from "~/types";
import { InternalLinkActionV2, MenuInternalLink } from "~/types";
import { actionV2ToMenuItem } from "~/actions";
import useActionContext from "~/hooks/useActionContext";
import { useComputed } from "~/hooks/useComputed";
type TopLevelAction =
| InternalLinkActionV2
| { type: "menu"; actions: InternalLinkActionV2[] };
type Props = React.PropsWithChildren<{
items: MenuInternalLink[];
actions: InternalLinkActionV2[];
max?: number;
highlightFirstItem?: boolean;
}>;
function Breadcrumb(
{ items, highlightFirstItem, children, max = 2 }: Props,
{ actions, highlightFirstItem, children, max = 2 }: Props,
ref: React.RefObject<HTMLDivElement> | null
) {
const totalItems = items.length;
const topLevelItems: MenuInternalLink[] = [...items];
let overflowItems;
const actionContext = useActionContext({ isMenu: true });
const visibleActions = useComputed(
() =>
actions.filter((action) =>
typeof action.visible === "function"
? action.visible(actionContext)
: (action.visible ?? true)
),
[actions, actionContext]
);
const totalVisibleActions = visibleActions.length;
const topLevelActions: TopLevelAction[] = [...visibleActions];
// chop middle breadcrumbs and present a "..." menu instead
if (totalItems > max) {
if (totalVisibleActions > max) {
const halfMax = Math.floor(max / 2);
overflowItems = topLevelItems.splice(halfMax, totalItems - max);
const menuActions = topLevelActions.splice(
halfMax,
totalVisibleActions - max
) as InternalLinkActionV2[];
topLevelItems.splice(halfMax, 0, {
to: "",
type: "route",
title: <BreadcrumbMenu items={overflowItems as MenuInternalLink[]} />,
topLevelActions.splice(halfMax, 0, {
type: "menu",
actions: menuActions,
});
}
const toBreadcrumb = React.useCallback(
(action: TopLevelAction, index: number) => {
if (action.type === "menu") {
return <BreadcrumbMenu key="menu" actions={action.actions} />;
}
const item = actionV2ToMenuItem(
action,
actionContext
) as MenuInternalLink;
return (
<>
{item.icon}
<Item
to={item.to}
$withIcon={!!item.icon}
$highlight={!!highlightFirstItem && index === 0}
>
{item.title}
</Item>
</>
);
},
[actionContext, highlightFirstItem]
);
return (
<Flex justify="flex-start" align="center" ref={ref}>
{topLevelItems.map((item, index) => (
<React.Fragment
key={
(typeof item.to === "string" ? item.to : item.to.pathname) || index
}
>
{item.icon}
{item.to ? (
<Item
to={item.to}
$withIcon={!!item.icon}
$highlight={!!highlightFirstItem && index === 0}
>
{item.title}
</Item>
) : (
item.title
)}
{index !== topLevelItems.length - 1 || !!children ? <Slash /> : null}
{topLevelActions.map((action, index) => (
<React.Fragment key={action.type === "menu" ? "menu" : `item-${index}`}>
{toBreadcrumb(action, index)}
{index !== topLevelActions.length - 1 || !!children ? (
<Slash />
) : null}
</React.Fragment>
))}
{children}
+1 -1
View File
@@ -176,7 +176,7 @@ const Button = <T extends React.ElementType = "button">(
...rest
} = props;
const hasText = !!children || value !== undefined;
const ic = hideIcon ? undefined : action?.icon ?? icon;
const ic = hideIcon ? undefined : (action?.icon ?? icon);
const hasIcon = ic !== undefined;
return (
+26 -43
View File
@@ -1,4 +1,3 @@
import * as Popover from "@radix-ui/react-popover";
import filter from "lodash/filter";
import isEqual from "lodash/isEqual";
import orderBy from "lodash/orderBy";
@@ -6,16 +5,18 @@ import uniq from "lodash/uniq";
import { observer } from "mobx-react";
import { useState, useMemo, useEffect, useCallback } from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { depths, s } from "@shared/styles";
import Document from "~/models/Document";
import { AvatarSize, AvatarWithPresence } from "~/components/Avatar";
import DocumentViews from "~/components/DocumentViews";
import Facepile from "~/components/Facepile";
import NudeButton from "~/components/NudeButton";
import {
Popover,
PopoverTrigger,
PopoverContent,
} from "~/components/primitives/Popover";
import useCurrentUser from "~/hooks/useCurrentUser";
import useStores from "~/hooks/useStores";
import { fadeAndScaleIn } from "~/styles/animations";
type Props = {
/** The document to display live collaborators for */
@@ -24,21 +25,6 @@ type Props = {
limit?: number;
};
// Styled components to match the original Popover styling
const StyledPopoverContent = styled(Popover.Content)`
animation: ${fadeAndScaleIn} 200ms ease;
transform-origin: 75% 0;
background: ${s("menuBackground")};
border-radius: 6px;
padding: 12px 24px;
max-height: 75vh;
box-shadow: ${s("menuShadow")};
z-index: ${depths.modal};
overflow-x: hidden;
overflow-y: auto;
outline: none;
`;
/**
* Displays a list of live collaborators for a document, including their avatars
* and presence status.
@@ -49,14 +35,14 @@ function Collaborators(props: Props) {
const user = useCurrentUser();
const currentUserId = user?.id;
const [requestedUserIds, setRequestedUserIds] = useState<string[]>([]);
const [popoverOpen, setPopoverOpen] = useState(false);
const { users, presence, ui } = useStores();
const { document } = props;
const { observingUserId } = ui;
const documentPresence = presence.get(document.id);
const documentPresenceArray = documentPresence
? Array.from(documentPresence.values())
: [];
const documentPresenceArray = useMemo(
() => (documentPresence ? Array.from(documentPresence.values()) : []),
[documentPresence]
);
// Use Set for O(1) lookups and stable references
const presentIds = useMemo(
@@ -115,11 +101,11 @@ function Collaborators(props: Props) {
// Memoize onClick handler to avoid inline function creation
const handleAvatarClick = useCallback(
(
collaboratorId: string,
isPresent: boolean,
isObserving: boolean,
isObservable: boolean
) =>
collaboratorId: string,
isPresent: boolean,
isObserving: boolean,
isObservable: boolean
) =>
(ev: React.MouseEvent) => {
if (isObservable && isPresent) {
ev.preventDefault();
@@ -146,6 +132,7 @@ function Collaborators(props: Props) {
isEditing={isEditing}
isObserving={isObserving}
isCurrentUser={currentUserId === collaborator.id}
alt={t("Avatar of {{ name }}", { name: collaborator.name })}
onClick={
isObservable
? handleAvatarClick(
@@ -162,9 +149,13 @@ function Collaborators(props: Props) {
[presentIds, editingIds, observingUserId, currentUserId, handleAvatarClick]
);
if (!document.insightsEnabled) {
return null;
}
return (
<Popover.Root open={popoverOpen} onOpenChange={setPopoverOpen}>
<Popover.Trigger asChild>
<Popover>
<PopoverTrigger>
<NudeButton
width={Math.min(collaborators.length, limit) * AvatarSize.Large}
height={AvatarSize.Large}
@@ -177,19 +168,11 @@ function Collaborators(props: Props) {
renderAvatar={renderAvatar}
/>
</NudeButton>
</Popover.Trigger>
<Popover.Portal>
<StyledPopoverContent
side="bottom"
align="end"
sideOffset={0}
aria-label={t("Viewers")}
style={{ width: 300 }}
>
<DocumentViews document={document} />
</StyledPopoverContent>
</Popover.Portal>
</Popover.Root>
</PopoverTrigger>
<PopoverContent aria-label={t("Viewers")} side="bottom" align="end">
<DocumentViews document={document} />
</PopoverContent>
</Popover>
);
}
+32 -17
View File
@@ -14,7 +14,7 @@ import Collection from "~/models/Collection";
import Button from "~/components/Button";
import Flex from "~/components/Flex";
import Input from "~/components/Input";
import InputSelectPermission from "~/components/InputSelectPermission";
import { InputSelectPermission } from "~/components/InputSelectPermission";
import { createLazyComponent } from "~/components/LazyLoad";
import Switch from "~/components/Switch";
import Text from "~/components/Text";
@@ -22,7 +22,6 @@ import useBoolean from "~/hooks/useBoolean";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import useStores from "~/hooks/useStores";
import { EmptySelectValue } from "~/types";
import { createSwitchRegister } from "~/utils/forms";
const IconPicker = createLazyComponent(() => import("~/components/IconPicker"));
@@ -126,6 +125,8 @@ export const CollectionForm = observer(function CollectionForm_({
[setFocus, setValue, values.icon]
);
const initial = values.name.charAt(0).toUpperCase();
return (
<form onSubmit={formHandleSubmit(handleSubmit)}>
<Text as="p">
@@ -146,7 +147,7 @@ export const CollectionForm = observer(function CollectionForm_({
<StyledIconPicker
icon={values.icon}
color={values.color ?? iconColor}
initial={values.name[0]}
initial={initial}
popoverPosition="right"
onOpen={setHasOpenedIconPicker}
onChange={handleIconChange}
@@ -173,7 +174,7 @@ export const CollectionForm = observer(function CollectionForm_({
) => {
field.onChange(value === EmptySelectValue ? null : value);
}}
note={t(
help={t(
"The default access for workspace members, you can share with more users or groups later."
)}
/>
@@ -182,22 +183,36 @@ export const CollectionForm = observer(function CollectionForm_({
)}
{team.sharing && (
<Switch
id="sharing"
label={t("Public document sharing")}
note={t(
"Allow documents within this collection to be shared publicly on the internet."
<Controller
control={control}
name="sharing"
render={({ field }) => (
<Switch
id="sharing"
label={t("Public document sharing")}
note={t(
"Allow documents within this collection to be shared publicly on the internet."
)}
checked={field.value}
onChange={field.onChange}
/>
)}
{...createSwitchRegister(register, "sharing")}
/>
)}
{team.getPreference(TeamPreference.Commenting) && (
<Switch
id="commenting"
label={t("Commenting")}
note={t("Allow commenting on documents within this collection.")}
{...createSwitchRegister(register, "commenting")}
<Controller
control={control}
name="commenting"
render={({ field }) => (
<Switch
id="commenting"
label={t("Commenting")}
note={t("Allow commenting on documents within this collection.")}
checked={!!field.value}
onChange={field.onChange}
/>
)}
/>
)}
@@ -211,8 +226,8 @@ export const CollectionForm = observer(function CollectionForm_({
? `${t("Saving")}`
: t("Save")
: formState.isSubmitting
? `${t("Creating")}`
: t("Create")}
? `${t("Creating")}`
: t("Create")}
</Button>
</Flex>
</form>
+21 -28
View File
@@ -3,9 +3,10 @@ import * as React from "react";
import { useTranslation } from "react-i18next";
import Collection from "~/models/Collection";
import CollectionIcon from "~/components/Icons/CollectionIcon";
import { MenuInternalLink } from "~/types";
import { archivePath, collectionPath } from "~/utils/routeHelpers";
import Breadcrumb from "./Breadcrumb";
import { createInternalLinkActionV2 } from "~/actions";
import { ActiveCollectionSection } from "~/actions/sections";
type Props = {
collection: Collection;
@@ -14,32 +15,24 @@ type Props = {
export const CollectionBreadcrumb: React.FC<Props> = ({ collection }) => {
const { t } = useTranslation();
const items = React.useMemo(() => {
const collectionNode: MenuInternalLink = {
type: "route",
title: collection.name,
icon: <CollectionIcon collection={collection} expanded />,
to: collectionPath(collection.path),
};
const actions = React.useMemo(
() => [
createInternalLinkActionV2({
name: t("Archive"),
section: ActiveCollectionSection,
icon: <ArchiveIcon />,
visible: collection.isArchived,
to: archivePath(),
}),
createInternalLinkActionV2({
name: collection.name,
section: ActiveCollectionSection,
icon: <CollectionIcon collection={collection} expanded />,
to: collectionPath(collection.path),
}),
],
[collection, t]
);
const category: MenuInternalLink | undefined = collection.isArchived
? {
type: "route",
icon: <ArchiveIcon />,
title: t("Archive"),
to: archivePath(),
}
: undefined;
const output = [];
if (category) {
output.push(category);
}
output.push(collectionNode);
return output;
}, [collection, t]);
return <Breadcrumb items={items} highlightFirstItem />;
return <Breadcrumb actions={actions} highlightFirstItem />;
};
@@ -4,7 +4,6 @@ import Icon from "@shared/components/Icon";
import { createAction } from "~/actions";
import { RecentSection } from "~/actions/sections";
import useStores from "~/hooks/useStores";
import history from "~/utils/history";
import { documentPath } from "~/utils/routeHelpers";
const useRecentDocumentActions = (count = 6) => {
@@ -25,7 +24,7 @@ const useRecentDocumentActions = (count = 6) => {
) : (
<DocumentIcon />
),
perform: () => history.push(documentPath(item)),
to: documentPath(item),
})
),
[count, ui.activeDocumentId, documents.recentlyViewed]
@@ -3,7 +3,6 @@ import { useMemo } from "react";
import { createAction } from "~/actions";
import { NavigationSection } from "~/actions/sections";
import useSettingsConfig from "~/hooks/useSettingsConfig";
import history from "~/utils/history";
const useSettingsAction = () => {
const config = useSettingsConfig();
@@ -16,7 +15,7 @@ const useSettingsAction = () => {
name: item.name,
icon: <Icon />,
section: NavigationSection,
perform: () => history.push(item.path),
to: item.path,
};
}),
[config]
+1 -1
View File
@@ -64,7 +64,7 @@ const ConfirmationDialog: React.FC<Props> = ({
danger={danger}
autoFocus
>
{isSaving && savingText ? savingText : submitText ?? t("Confirm")}
{isSaving && savingText ? savingText : (submitText ?? t("Confirm"))}
</Button>
</Flex>
</Flex>
+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}
+1 -1
View File
@@ -20,7 +20,7 @@ type Props = {
dangerous?: boolean;
to?: LocationDescriptor;
href?: string;
target?: "_blank";
target?: string;
as?: string | React.ComponentType<any>;
hide?: () => void;
level?: number;
@@ -1,5 +1,6 @@
import { MoreIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { MenuButton } from "reakit/Menu";
import NudeButton from "~/components/NudeButton";
@@ -8,10 +9,16 @@ type Props = React.ComponentProps<typeof MenuButton> & {
};
export default function OverflowMenuButton({ className, ...rest }: Props) {
const { t } = useTranslation();
return (
<MenuButton {...rest}>
{(props) => (
<NudeButton className={className} {...props}>
<NudeButton
className={className}
aria-label={t("More options")}
{...props}
>
<MoreIcon />
</NudeButton>
)}
+11 -3
View File
@@ -104,7 +104,7 @@ export function filterTemplateItems(items: TMenuItem[]): TMenuItem[] {
function Template({ items, actions, context, showIcons, ...menu }: Props) {
const ctx = useActionContext({
isContextMenu: true,
isMenu: true,
});
const templateItems = actions
@@ -155,12 +155,14 @@ function Template({ items, actions, context, showIcons, ...menu }: Props) {
return (
<MenuItem
id={`${item.title}-${index}`}
href={item.href}
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={item.href.startsWith("#") ? undefined : "_blank"}
target={
typeof item.href === "string" ? undefined : item.href.target
}
icon={showIcons !== false ? item.icon : undefined}
{...menu}
>
@@ -231,6 +233,12 @@ function Template({ items, actions, context, showIcons, ...menu }: Props) {
);
}
// 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;
})}
+1 -1
View File
@@ -119,7 +119,7 @@ const ContextMenu: React.FC<Props> = ({
>
{(props) => (
<InnerContextMenu
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// oxlint-disable-next-line @typescript-eslint/no-explicit-any
menuProps={props as any}
{...rest}
isSubMenu={isSubMenu}
@@ -3,7 +3,7 @@ import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import CollectionIcon from "~/components/Icons/CollectionIcon";
import { InputSelectNew, Option } from "~/components/InputSelectNew";
import { InputSelect, Option } from "~/components/InputSelect";
import useStores from "~/hooks/useStores";
type DefaultCollectionInputSelectProps = {
@@ -70,11 +70,10 @@ const DefaultCollectionInputSelect = ({
}
return (
<InputSelectNew
<InputSelect
options={options}
value={defaultCollectionId ?? "home"}
onChange={onSelectCollection}
ariaLabel={t("Default collection")}
label={t("Start view")}
hideLabel
short
+3 -2
View File
@@ -6,6 +6,8 @@ import useStores from "~/hooks/useStores";
function Dialogs() {
const { dialogs } = useStores();
const { guide, modalStack } = dialogs;
const modals = [...modalStack];
return (
<>
{guide ? (
@@ -17,11 +19,10 @@ function Dialogs() {
{guide.content}
</Guide>
) : undefined}
{[...modalStack].map(([id, modal]) => (
{modals.map(([id, modal]) => (
<Modal
key={id}
isOpen={modal.isOpen}
fullscreen={modal.fullscreen ?? false}
onRequestClose={() => {
modal.onClose?.();
dialogs.closeModal(id);
@@ -0,0 +1,50 @@
import { Trans, useTranslation } from "react-i18next";
import ConfirmationDialog from "~/components/ConfirmationDialog";
import Text from "~/components/Text";
import { IntegrationType } from "@shared/types";
import Integration from "~/models/Integration";
import useStores from "~/hooks/useStores";
import { useHistory } from "react-router-dom";
import { settingsPath } from "~/utils/routeHelpers";
import { observer } from "mobx-react";
import capitalize from "lodash/capitalize";
type Props = {
integration: Integration<IntegrationType.Analytics>;
};
export const DisconnectAnalyticsDialog = observer(({ integration }: Props) => {
const { t } = useTranslation();
const { dialogs } = useStores();
const history = useHistory();
const handleSubmit = async () => {
await integration.delete();
history.push(settingsPath("integrations"));
dialogs.closeAllModals();
};
return (
<ConfirmationDialog
onSubmit={handleSubmit}
submitText={t("Disconnect")}
savingText={`${t("Disconnecting")}`}
danger
>
<Text as="p" type="secondary">
<Trans
defaults="Are you sure you want to disconnect the <em>{{ service }}</em> integration?"
values={{
service: capitalize(integration.service),
}}
components={{
em: <strong />,
}}
/>
</Text>
<Text as="p" type="secondary">
<Trans defaults="This will stop sending analytics events to the configured instance." />
</Text>
</ConfirmationDialog>
);
});
+79 -90
View File
@@ -11,8 +11,9 @@ import CollectionIcon from "~/components/Icons/CollectionIcon";
import { useLocationSidebarContext } from "~/hooks/useLocationSidebarContext";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
import { MenuInternalLink } from "~/types";
import { archivePath, settingsPath, trashPath } from "~/utils/routeHelpers";
import { createInternalLinkActionV2 } from "~/actions";
import { ActiveDocumentSection } from "~/actions/sections";
type Props = {
children?: React.ReactNode;
@@ -27,46 +28,12 @@ type Props = {
maxDepth?: number;
};
function useCategory(document: Document): MenuInternalLink | null {
const { t } = useTranslation();
if (document.isDeleted) {
return {
type: "route",
icon: <TrashIcon />,
title: t("Trash"),
to: trashPath(),
};
}
if (document.isArchived) {
return {
type: "route",
icon: <ArchiveIcon />,
title: t("Archive"),
to: archivePath(),
};
}
if (document.template) {
return {
type: "route",
icon: <ShapesIcon />,
title: t("Templates"),
to: settingsPath("templates"),
};
}
return null;
}
function DocumentBreadcrumb(
{ document, children, onlyText, reverse = false, maxDepth }: Props,
ref: React.RefObject<HTMLDivElement> | null
) {
const { collections } = useStores();
const { t } = useTranslation();
const category = useCategory(document);
const sidebarContext = useLocationSidebarContext();
const collection = document.collectionId
? collections.get(document.collectionId)
@@ -78,69 +45,91 @@ function DocumentBreadcrumb(
void document.loadRelations({ withoutPolicies: true });
}, [document]);
let collectionNode: MenuInternalLink | undefined;
if (collection && can.readDocument) {
collectionNode = {
type: "route",
title: collection.name,
icon: <CollectionIcon collection={collection} expanded />,
to: {
pathname: collection.path,
state: { sidebarContext },
},
};
} else if (document.isCollectionDeleted) {
collectionNode = {
type: "route",
title: t("Deleted Collection"),
icon: undefined,
to: "",
};
}
const path = document.pathTo.slice(0, -1);
const items = React.useMemo(() => {
const output: MenuInternalLink[] = [];
const actions = React.useMemo(() => {
if (depth === 0) {
return output;
return [];
}
if (category) {
output.push(category);
}
if (collectionNode) {
output.push(collectionNode);
}
path.forEach((node: NavigationNode) => {
const title = node.title || t("Untitled");
output.push({
type: "route",
title: node.icon ? (
<>
<StyledIcon value={node.icon} color={node.color} /> {title}
</>
) : (
title
),
to: {
pathname: node.url,
state: { sidebarContext },
},
});
});
const outputActions = [
createInternalLinkActionV2({
name: t("Trash"),
section: ActiveDocumentSection,
icon: <TrashIcon />,
visible: document.isDeleted,
to: trashPath(),
}),
createInternalLinkActionV2({
name: t("Archive"),
section: ActiveDocumentSection,
icon: <ArchiveIcon />,
visible: document.isArchived,
to: archivePath(),
}),
createInternalLinkActionV2({
name: t("Templates"),
section: ActiveDocumentSection,
icon: <ShapesIcon />,
visible: document.template,
to: settingsPath("templates"),
}),
createInternalLinkActionV2({
name: collection?.name,
section: ActiveDocumentSection,
icon: collection ? (
<CollectionIcon collection={collection} expanded />
) : undefined,
visible: !!(collection && can.readDocument),
to: collection
? {
pathname: collection.path,
state: { sidebarContext },
}
: "",
}),
createInternalLinkActionV2({
name: t("Deleted Collection"),
section: ActiveDocumentSection,
visible: document.isCollectionDeleted,
to: "",
}),
...path.map((node) => {
const title = node.title || t("Untitled");
return createInternalLinkActionV2({
name: node.icon ? (
<>
<StyledIcon value={node.icon} color={node.color} /> {title}
</>
) : (
title
),
section: ActiveDocumentSection,
to: {
pathname: node.url,
state: { sidebarContext },
},
});
}),
];
return reverse
? depth !== undefined
? output.slice(-depth)
: output
? outputActions.slice(-depth)
: outputActions
: depth !== undefined
? output.slice(0, depth)
: output;
}, [t, path, category, sidebarContext, collectionNode, reverse, depth]);
? outputActions.slice(0, depth)
: outputActions;
}, [
t,
document,
collection,
can.readDocument,
sidebarContext,
path,
reverse,
depth,
]);
if (!collections.isLoaded) {
return null;
@@ -176,7 +165,7 @@ function DocumentBreadcrumb(
}
return (
<Breadcrumb items={items} ref={ref} highlightFirstItem>
<Breadcrumb actions={actions} ref={ref} highlightFirstItem>
{children}
</Breadcrumb>
);
+8 -2
View File
@@ -123,6 +123,7 @@ function DocumentCard(props: Props) {
<DocumentSquircle
icon={document.icon}
color={document.color ?? undefined}
initial={document.initial}
/>
) : (
<Squircle
@@ -194,17 +195,22 @@ const ReadingTime = ({ document }: { document: Document }) => {
const DocumentSquircle = ({
icon,
color,
initial,
}: {
icon: string;
color?: string;
initial?: string;
}) => {
const theme = useTheme();
const iconType = determineIconType(icon)!;
const squircleColor = iconType === IconType.SVG ? color : theme.slateLight;
const style = {
"--background": squircleColor,
} as React.CSSProperties;
return (
<Squircle color={squircleColor}>
<Icon value={icon} color={theme.white} forceColor />
<Squircle color={squircleColor} style={style}>
<Icon value={icon} color={theme.white} initial={initial} forceColor />
</Squircle>
);
};
+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();
+32 -34
View File
@@ -1,4 +1,3 @@
import flatten from "lodash/flatten";
import { observer } from "mobx-react";
import * as React from "react";
import { Trans, useTranslation } from "react-i18next";
@@ -11,7 +10,6 @@ import Button from "~/components/Button";
import DocumentExplorer from "~/components/DocumentExplorer";
import useCollectionTrees from "~/hooks/useCollectionTrees";
import useStores from "~/hooks/useStores";
import { flattenTree } from "~/utils/tree";
import Switch from "./Switch";
import Text from "./Text";
@@ -32,7 +30,7 @@ function DocumentCopy({ document, onSubmit }: Props) {
);
const items = React.useMemo(() => {
const nodes = flatten(collectionTrees.map(flattenTree)).filter((node) =>
const nodes = collectionTrees.filter((node) =>
node.collectionId
? policies.get(node.collectionId)?.abilities.createDocument
: true
@@ -78,34 +76,32 @@ function DocumentCopy({ document, onSubmit }: Props) {
onSelect={selectPath}
defaultValue={document.parentDocumentId || document.collectionId || ""}
/>
<OptionsContainer>
{!document.isTemplate && (
<>
{document.collectionId && (
<Text size="small">
<Switch
name="publish"
label={t("Publish")}
labelPosition="right"
checked={publish}
onChange={setPublish}
/>
</Text>
)}
{document.publishedAt && document.childDocuments.length > 0 && (
<Text size="small">
<Switch
name="recursive"
label={t("Include nested documents")}
labelPosition="right"
checked={recursive}
onChange={setRecursive}
/>
</Text>
)}
</>
)}
</OptionsContainer>
{!document.isTemplate && (
<OptionsContainer>
{document.collectionId && (
<Text size="small">
<Switch
name="publish"
label={t("Publish")}
labelPosition="right"
checked={publish}
onChange={setPublish}
/>
</Text>
)}
{document.publishedAt && document.childDocuments.length > 0 && (
<Text size="small">
<Switch
name="recursive"
label={t("Include nested documents")}
labelPosition="right"
checked={recursive}
onChange={setRecursive}
/>
</Text>
)}
</OptionsContainer>
)}
<Footer justify="space-between" align="center" gap={8}>
<StyledText type="secondary">
{selectedPath ? (
@@ -127,9 +123,11 @@ function DocumentCopy({ document, onSubmit }: Props) {
}
const OptionsContainer = styled.div`
margin: 16px 0 8px 0;
padding-left: 24px;
padding-right: 24px;
border-top: 1px solid ${(props) => props.theme.horizontalRule};
padding: 16px 24px 0;
margin-bottom: -1px;
background: ${(props) => props.theme.modalBackground};
z-index: 1;
`;
export default observer(DocumentCopy);
+7 -14
View File
@@ -15,7 +15,7 @@ import scrollIntoView from "scroll-into-view-if-needed";
import styled, { useTheme } from "styled-components";
import breakpoint from "styled-components-breakpoint";
import Icon from "@shared/components/Icon";
import { NavigationNode, NavigationNodeType } from "@shared/types";
import { NavigationNode } from "@shared/types";
import { isModKey } from "@shared/utils/keyboard";
import DocumentExplorerNode from "~/components/DocumentExplorerNode";
import DocumentExplorerSearchResult from "~/components/DocumentExplorerSearchResult";
@@ -26,7 +26,8 @@ import InputSearch from "~/components/InputSearch";
import Text from "~/components/Text";
import useMobile from "~/hooks/useMobile";
import useStores from "~/hooks/useStores";
import { ancestors, descendants } from "~/utils/tree";
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. */
@@ -78,13 +79,9 @@ function DocumentExplorer({ onSubmit, onSelect, items, defaultValue }: Props) {
const VERTICAL_PADDING = 6;
const HORIZONTAL_PADDING = 24;
const recentlyViewedItemIds = documents.recentlyViewed
.slice(0, 5)
.map((item) => item.id);
const searchIndex = React.useMemo(
() =>
new FuzzySearch(items, ["title"], {
new FuzzySearch(flatten(items.map(flattenTree)), ["title"], {
caseSensitive: false,
}),
[items]
@@ -129,12 +126,7 @@ function DocumentExplorer({ onSubmit, onSelect, items, defaultValue }: Props) {
return searchTerm
? searchIndex.search(searchTerm)
: items
.filter((item) => recentlyViewedItemIds.includes(item.id))
.concat(
items.filter((item) => item.type === NavigationNodeType.Collection)
)
.flatMap(includeDescendants);
: items.flatMap(includeDescendants);
}
const nodes = getNodes();
@@ -142,6 +134,7 @@ function DocumentExplorer({ onSubmit, onSelect, items, defaultValue }: Props) {
(min, node) => (node.depth ? Math.min(min, node.depth) : min),
Infinity
);
const normalizedBaseDepth = baseDepth === Infinity ? 0 : baseDepth;
const scrollNodeIntoView = React.useCallback(
(node: number) => {
@@ -315,7 +308,7 @@ function DocumentExplorer({ onSubmit, onSelect, items, defaultValue }: Props) {
expanded={isExpanded(index)}
icon={renderedIcon}
title={title}
depth={(node.depth ?? 0) - baseDepth}
depth={(node.depth ?? 0) - normalizedBaseDepth}
hasChildren={hasChildren(index)}
ref={itemRefs[index]}
/>
+108 -76
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;
@@ -33,7 +37,6 @@ type Props = {
showParentDocuments?: boolean;
showCollection?: boolean;
showPublished?: boolean;
showPin?: boolean;
showDraft?: boolean;
showTemplate?: boolean;
};
@@ -51,6 +54,7 @@ function DocumentListItem(
) {
const { t } = useTranslation();
const user = useCurrentUser();
const { userMemberships, groupMemberships } = useStores();
const locationSidebarContext = useLocationSidebarContext();
const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean();
@@ -68,7 +72,6 @@ function DocumentListItem(
showParentDocuments,
showCollection,
showPublished,
showPin,
showDraft = true,
showTemplate,
highlight,
@@ -80,85 +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} />
&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({ document });
{!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}
showPin={showPin}
onOpen={handleMenuOpen}
onClose={handleMenuClose}
modal={false}
/>
</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>
);
}
@@ -177,7 +205,9 @@ const Actions = styled(EventBoundary)`
color: ${s("textSecondary")};
${NudeButton} {
&: ${hover}, &[aria-expanded= "true"] {
&:
${hover},
&[aria-expanded= "true"] {
background: ${s("sidebarControlHoverBackground")};
}
}
@@ -250,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;
@@ -260,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;
`;
+7 -3
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, truncateMultiline } from "@shared/styles";
type Props = Omit<React.HTMLAttributes<HTMLInputElement>, "onSubmit"> & {
/** A callback when the title is submitted. */
@@ -128,17 +128,21 @@ function EditableTitle(
/>
</form>
) : (
<span
<Text
onDoubleClick={canUpdate ? handleDoubleClick : undefined}
className={rest.className}
>
{value}
</span>
</Text>
)}
</>
);
}
const Text = styled.span`
${truncateMultiline(3)}
`;
const Input = styled.input`
color: ${s("text")};
background: ${s("background")};
+1
View File
@@ -201,6 +201,7 @@ function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
grow={props.grow}
style={props.style}
editorStyle={props.editorStyle}
commenting={!!props.onClickCommentMark}
>
<div className="ProseMirror">
{paragraphs.map((paragraph, index) => (
+89 -15
View File
@@ -13,6 +13,9 @@ import Text from "~/components/Text";
import env from "~/env";
import Logger from "~/utils/Logger";
import isCloudHosted from "~/utils/isCloudHosted";
import Storage from "@shared/utils/Storage";
import { deleteAllDatabases } from "~/utils/developer";
import Flex from "./Flex";
type Props = WithTranslation & {
/** Whether to reload the page if a chunk fails to load. */
@@ -23,6 +26,9 @@ type Props = WithTranslation & {
component?: React.ComponentType | string;
};
const ERROR_TRACKING_KEY = "error-boundary-tracking";
const ERROR_TRACKING_WINDOW_MS = 5 * 60 * 1000; // 5 minutes
@observer
class ErrorBoundary extends React.Component<Props> {
@observable
@@ -31,6 +37,13 @@ class ErrorBoundary extends React.Component<Props> {
@observable
showDetails = false;
@observable
isRepeatedError = false;
componentDidMount() {
this.checkForPreviousErrors();
}
componentDidCatch(error: Error) {
this.error = error;
@@ -46,9 +59,47 @@ class ErrorBoundary extends React.Component<Props> {
return;
}
this.trackError();
Logger.error("ErrorBoundary", error);
}
private checkForPreviousErrors = () => {
try {
const stored = Storage.get(ERROR_TRACKING_KEY);
if (!stored) {
return;
}
const errors: number[] = JSON.parse(stored);
const cutoff = Date.now() - ERROR_TRACKING_WINDOW_MS;
const recentErrors = errors.filter((timestamp) => timestamp > cutoff);
this.isRepeatedError = recentErrors.length > 0;
} catch (err) {
Logger.warn("Failed to parse stored errors for error boundary", { err });
}
};
private trackError = () => {
try {
const stored = Storage.get(ERROR_TRACKING_KEY);
const errors: number[] = stored ? JSON.parse(stored) : [];
const cutoff = Date.now() - ERROR_TRACKING_WINDOW_MS;
// Filter out old errors and add current one
const updatedErrors = [
...errors.filter((timestamp) => timestamp > cutoff),
Date.now(),
];
Storage.set(ERROR_TRACKING_KEY, JSON.stringify(updatedErrors));
this.isRepeatedError = updatedErrors.length > 1;
} catch (err) {
Logger.warn("Failed to track error in error boundary", { err });
}
};
handleReload = () => {
window.location.reload();
};
@@ -61,6 +112,12 @@ class ErrorBoundary extends React.Component<Props> {
window.open(isCloudHosted ? UrlHelper.contact : UrlHelper.github);
};
handleClearCache = async () => {
await deleteAllDatabases();
Storage.clear();
window.location.reload();
};
render() {
const { t, component: Component = CenteredContent, showTitle } = this.props;
@@ -107,29 +164,46 @@ class ErrorBoundary extends React.Component<Props> {
</Heading>
</>
)}
<Text as="p" type="secondary">
<Trans
defaults="Sorry, an unrecoverable error occurred{{notified}}. Please try reloading the page, it may have been a temporary glitch."
values={{
notified: isReported
? ` ${t("our engineers have been notified")}`
: undefined,
}}
/>
</Text>
{this.showDetails && <Pre>{error.toString()}</Pre>}
<p>
<Button onClick={this.handleReload}>{t("Reload")}</Button>{" "}
{this.isRepeatedError ? (
<Text as="p" type="secondary">
<Trans>
An error has occurred multiple times recently. If it continues
please try clearing the cache or using a different browser.
</Trans>
</Text>
) : (
<Text as="p" type="secondary">
<Trans
defaults="Sorry, an unrecoverable error occurred{{notified}}. Please try reloading the page, it may have been a temporary glitch."
values={{
notified: isReported
? ` ${t("our engineers have been notified")}`
: undefined,
}}
/>
</Text>
)}
{this.showDetails && <Pre>{error.stack}</Pre>}
<Flex gap={8} wrap>
{this.isRepeatedError && (
<Button onClick={this.handleClearCache}>
<Trans>Clear cache + reload</Trans>
</Button>
)}
<Button onClick={this.handleReload} neutral={this.isRepeatedError}>
{t("Reload")}
</Button>
{this.showDetails ? (
<Button onClick={this.handleReportBug} neutral>
<Trans>Report a bug</Trans>
<Trans>Report a bug</Trans>
</Button>
) : (
<Button onClick={this.handleShowDetails} neutral>
<Trans>Show detail</Trans>
</Button>
)}
</p>
</Flex>
</Component>
);
}
+15 -174
View File
@@ -1,9 +1,7 @@
import { LocationDescriptor } from "history";
import { observer } from "mobx-react";
import {
TrashIcon,
ArchiveIcon,
EditIcon,
PublishIcon,
MoveIcon,
UnpublishIcon,
@@ -11,120 +9,28 @@ import {
UserIcon,
CrossIcon,
} from "outline-icons";
import { useRef } from "react";
import { useTranslation } from "react-i18next";
import { useLocation } from "react-router-dom";
import styled, { css } from "styled-components";
import EventBoundary from "@shared/components/EventBoundary";
import { s, hover } from "@shared/styles";
import { RevisionHelper } from "@shared/utils/RevisionHelper";
import { s } from "@shared/styles";
import Document from "~/models/Document";
import { Avatar, AvatarSize } from "~/components/Avatar";
import Item, { Actions } from "~/components/List/Item";
import Event from "~/models/Event";
import Time from "~/components/Time";
import { useLocationSidebarContext } from "~/hooks/useLocationSidebarContext";
import useStores from "~/hooks/useStores";
import RevisionMenu from "~/menus/RevisionMenu";
import Logger from "~/utils/Logger";
import { documentHistoryPath } from "~/utils/routeHelpers";
import Text from "./Text";
export type RevisionEvent = {
name: "revisions.create";
latest: boolean;
};
export type DocumentEvent = {
name:
| "documents.publish"
| "documents.unpublish"
| "documents.archive"
| "documents.unarchive"
| "documents.delete"
| "documents.restore"
| "documents.add_user"
| "documents.remove_user"
| "documents.move";
userId: string;
};
export type Event = {
id: string;
actorId: string;
createdAt: string;
deletedAt?: string;
} & (RevisionEvent | DocumentEvent);
type Props = {
document: Document;
event: Event;
item: Event<Document>;
};
const EventListItem = ({ event, document, ...rest }: Props) => {
const EventListItem = ({ item }: Props) => {
const { t } = useTranslation();
const { revisions, users } = useStores();
const actor = "actorId" in event ? users.get(event.actorId) : undefined;
const user = "userId" in event ? users.get(event.userId) : undefined;
const location = useLocation();
const sidebarContext = useLocationSidebarContext();
const revisionLoadedRef = useRef(false);
const opts = {
userName: actor?.name,
userName: item.actor?.name,
};
const isRevision = event.name === "revisions.create";
const isDerivedFromDocument =
event.id === RevisionHelper.latestId(document.id);
let meta, icon, to: LocationDescriptor | undefined;
const ref = useRef<HTMLAnchorElement>(null);
// the time component tends to steal focus when clicked
// ...so forward the focus back to the parent item
const handleTimeClick = () => {
ref.current?.focus();
};
const prefetchRevision = async () => {
if (
!document.isDeleted &&
event.name === "revisions.create" &&
!event.deletedAt &&
!isDerivedFromDocument &&
!revisionLoadedRef.current
) {
await revisions.fetch(event.id, { force: true });
revisionLoadedRef.current = true;
}
};
switch (event.name) {
case "revisions.create":
{
if (event.deletedAt) {
icon = <TrashIcon />;
meta = t("Revision deleted");
} else {
icon = <EditIcon size={16} />;
meta = event.latest ? (
<>
{t("Current version")} &middot; {actor?.name}
</>
) : (
t("{{userName}} edited", opts)
);
to = {
pathname: documentHistoryPath(
document,
isDerivedFromDocument ? "latest" : event.id
),
state: {
sidebarContext,
retainScrollPosition: true,
},
};
}
}
break;
let meta, icon;
switch (item.name) {
case "documents.archive":
icon = <ArchiveIcon />;
meta = t("{{userName}} archived", opts);
@@ -143,14 +49,14 @@ const EventListItem = ({ event, document, ...rest }: Props) => {
icon = <UserIcon />;
meta = t("{{userName}} added {{addedUserName}}", {
...opts,
addedUserName: user?.name ?? t("a user"),
addedUserName: item.user?.name ?? t("a user"),
});
break;
case "documents.remove_user":
icon = <CrossIcon />;
meta = t("{{userName}} removed {{removedUserName}}", {
...opts,
removedUserName: user?.name ?? t("a user"),
removedUserName: item.user?.name ?? t("a user"),
});
break;
@@ -175,71 +81,27 @@ const EventListItem = ({ event, document, ...rest }: Props) => {
break;
default:
Logger.warn("Unhandled event", { event });
Logger.warn("Unhandled item", { item });
}
if (!meta) {
return null;
}
const isActive =
typeof to === "string"
? location.pathname === to
: location.pathname === to?.pathname;
if (document.isDeleted) {
to = undefined;
}
return event.name === "revisions.create" && !event.deletedAt ? (
<RevisionItem
small
exact
to={to}
title={
<Time
dateTime={event.createdAt}
format={{
en_US: "MMM do, h:mm a",
fr_FR: "'Le 'd MMMM 'à' H:mm",
}}
relative={false}
addSuffix
onClick={handleTimeClick}
/>
}
image={<Avatar model={actor} size={AvatarSize.Large} />}
subtitle={meta}
actions={
isRevision && isActive && !event.latest ? (
<StyledEventBoundary>
<RevisionMenu document={document} revisionId={event.id} />
</StyledEventBoundary>
) : undefined
}
onMouseEnter={prefetchRevision}
ref={ref}
{...rest}
/>
) : (
return (
<EventItem>
<IconWrapper size="xsmall" type="secondary">
{icon}
</IconWrapper>
<Text size="xsmall" type="secondary">
{meta} &middot;{" "}
<Time
dateTime={event.deletedAt ?? event.createdAt}
relative
shorten
addSuffix
/>
<Time dateTime={item.createdAt} relative shorten addSuffix />
</Text>
</EventItem>
);
};
const lineStyle = css`
export const lineStyle = css`
&::before {
content: "";
display: block;
@@ -285,9 +147,10 @@ const lineStyle = css`
const IconWrapper = styled(Text)`
height: 24px;
min-width: 24px;
`;
const EventItem = styled.li`
export const EventItem = styled.li`
display: flex;
align-items: center;
gap: 8px;
@@ -308,26 +171,4 @@ const EventItem = styled.li`
${lineStyle}
`;
const StyledEventBoundary = styled(EventBoundary)`
height: 24px;
`;
const RevisionItem = styled(Item)`
border: 0;
position: relative;
margin: 8px 0;
padding: 8px;
border-radius: 8px;
${lineStyle}
${Actions} {
opacity: 0.5;
&: ${hover} {
opacity: 1;
}
}
`;
export default observer(EventListItem);
+11 -8
View File
@@ -1,10 +1,10 @@
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import User from "~/models/User";
import { Avatar, AvatarSize } from "~/components/Avatar";
import Flex from "~/components/Flex";
import Initials from "./Avatar/Initials";
type Props = {
/** The users to display */
@@ -31,19 +31,22 @@ function Facepile({
renderAvatar = Avatar,
...rest
}: Props) {
const { t } = useTranslation();
const filtered = users.filter(Boolean).slice(-limit);
const Component = renderAvatar;
if (overflow > 0) {
filtered.unshift({
id: "overflow",
initial: `${users.length ? "+" : ""}${overflow}`,
name: t(`{{count}} more user`, { count: overflow }),
} as User);
}
return (
<Avatars {...rest}>
{overflow > 0 && (
<Initials size={size} content={String(overflow)}>
{users.length ? "+" : ""}
{overflow}
</Initials>
)}
{filtered.map((model, index) => {
const lastChild = index === 0 && overflow <= 0;
const lastChild = index === 0;
return (
<Component
key={model.id}
+49 -78
View File
@@ -1,22 +1,19 @@
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";
interface TFilterOption extends PaginatedItem {
key: string;
label: string;
note?: string;
icon?: React.ReactNode;
}
@@ -34,19 +31,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,32 +53,26 @@ const FilterOptions = ({
const renderItem = React.useCallback(
(option) => (
<MenuItem
<MenuButton
key={option.key}
icon={option.icon}
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());
@@ -121,13 +110,13 @@ const FilterOptions = ({
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 +127,7 @@ const FilterOptions = ({
break;
}
},
[filteredOptions, menu, onSelect]
[filteredOptions, onSelect]
);
const handleEscapeFromList = React.useCallback((ev: React.KeyboardEvent) => {
@@ -150,21 +139,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 +161,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 +231,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;
+42 -57
View File
@@ -1,9 +1,14 @@
import * as React from "react";
import { Dialog, DialogBackdrop, useDialogState } from "reakit/Dialog";
import * as Dialog from "@radix-ui/react-dialog";
import styled from "styled-components";
import { depths, s } from "@shared/styles";
import Scrollable from "~/components/Scrollable";
import usePrevious from "~/hooks/usePrevious";
import {
fadeIn,
fadeOut,
fadeInAndSlideLeft,
fadeOutAndSlideRight,
} from "~/styles/animations";
type Props = {
children?: React.ReactNode;
@@ -18,55 +23,33 @@ const Guide: React.FC<Props> = ({
title = "Untitled",
onRequestClose,
...rest
}: Props) => {
const dialog = useDialogState({
animated: 250,
});
const wasOpen = usePrevious(isOpen);
}: Props) => (
<Dialog.Root open={isOpen} onOpenChange={(open) => !open && onRequestClose()}>
<Dialog.Portal>
<StyledOverlay>
<Scene
onEscapeKeyDown={onRequestClose}
onPointerDownOutside={onRequestClose}
aria-describedby={undefined}
{...rest}
>
<Content>
{title && <Header>{title}</Header>}
{children}
</Content>
</Scene>
</StyledOverlay>
</Dialog.Portal>
</Dialog.Root>
);
React.useEffect(() => {
if (!wasOpen && isOpen) {
dialog.show();
}
if (wasOpen && !isOpen) {
dialog.hide();
}
}, [dialog, wasOpen, isOpen]);
return (
<DialogBackdrop {...dialog}>
{(backdropProps) => (
<Backdrop {...backdropProps}>
<Dialog
{...dialog}
aria-label={title}
preventBodyScroll
hideOnEsc
hide={onRequestClose}
>
{(dialogProps) => (
<Scene {...dialogProps} {...rest}>
<Content>
{title && <Header>{title}</Header>}
{children}
</Content>
</Scene>
)}
</Dialog>
</Backdrop>
)}
</DialogBackdrop>
);
};
const Header = styled.h1`
const Header = styled(Dialog.Title)`
font-size: 18px;
margin-top: 0;
margin-bottom: 1em;
`;
const Backdrop = styled.div`
const StyledOverlay = styled(Dialog.Overlay)`
position: fixed;
top: 0;
left: 0;
@@ -74,35 +57,37 @@ const Backdrop = styled.div`
bottom: 0;
background-color: ${s("backdrop")} !important;
z-index: ${depths.overlay};
transition: opacity 200ms ease-in-out;
opacity: 0;
&[data-enter] {
opacity: 1;
&[data-state="open"] {
animation: ${fadeIn} 200ms ease;
}
&[data-state="closed"] {
animation: ${fadeOut} 200ms ease;
}
`;
const Scene = styled.div`
const Scene = styled(Dialog.Content)`
position: absolute;
top: 0;
right: 0;
bottom: 0;
margin: 12px;
z-index: ${depths.modal};
display: flex;
z-index: ${depths.modal};
justify-content: center;
align-items: flex-start;
width: 350px;
background: ${s("background")};
border-radius: 8px;
outline: none;
opacity: 0;
transform: translateX(16px);
transition: transform 250ms ease, opacity 250ms ease;
&[data-enter] {
opacity: 1;
transform: translateX(0px);
&[data-state="open"] {
animation: ${fadeInAndSlideLeft} 200ms ease;
}
&[data-state="closed"] {
animation: ${fadeOutAndSlideRight} 200ms ease;
}
`;
+13 -9
View File
@@ -6,7 +6,7 @@ import * as React from "react";
import { mergeRefs } from "react-merge-refs";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import { useComponentSize } from "@shared/hooks/useComponentSize";
import useMeasure from "react-use-measure";
import { depths, s } from "@shared/styles";
import { supportsPassiveListener } from "@shared/utils/browser";
import Button from "~/components/Button";
@@ -38,8 +38,8 @@ function Header(
const { ui } = useStores();
const isMobile = useMobile();
const hasMobileSidebar = hasSidebar && isMobile;
const internalRef = React.useRef<HTMLDivElement | null>(null);
const breadcrumbsRef = React.useRef<HTMLDivElement | null>(null);
const [internalMeasureRef, size] = useMeasure();
const [breadcrumbsMeasureRef, breadcrumbsSize] = useMeasure();
const passThrough = !actions && !left && !title;
const [isScrolled, setScrolled] = React.useState(false);
@@ -62,19 +62,22 @@ function Header(
});
}, []);
const setBreadcrumbRef = React.useCallback((node: HTMLDivElement | null) => {
breadcrumbsRef.current = node?.firstElementChild as HTMLDivElement;
}, []);
const setBreadcrumbRef = React.useCallback(
(node: HTMLDivElement | null) => {
if (node?.firstElementChild) {
breadcrumbsMeasureRef(node.firstElementChild as HTMLDivElement);
}
},
[breadcrumbsMeasureRef]
);
const size = useComponentSize(internalRef);
const breadcrumbsSize = useComponentSize(breadcrumbsRef);
const breadcrumbMakesCompact = breadcrumbsSize.width > size.width / 3;
const isCompact = size.width < 1000 || breadcrumbMakesCompact;
return (
<TooltipProvider>
<Wrapper
ref={mergeRefs([ref, internalRef])}
ref={mergeRefs([ref, internalMeasureRef])}
align="center"
shrink={false}
className={className}
@@ -122,6 +125,7 @@ const Actions = styled(Flex)`
flex-basis: 0;
min-width: auto;
padding-left: 8px;
gap: 12px;
${breakpoint("tablet")`
position: unset;
+2 -1
View File
@@ -20,7 +20,8 @@ export const Preview = styled(Link)`
cursor: ${(props: { as?: string }) =>
props.as === "div" ? "default" : "var(--pointer)"};
border-radius: 4px;
box-shadow: 0 30px 90px -20px rgba(0, 0, 0, 0.3),
box-shadow:
0 30px 90px -20px rgba(0, 0, 0, 0.3),
0 0 1px 1px rgba(0, 0, 0, 0.05);
overflow: hidden;
position: absolute;
@@ -136,6 +136,7 @@ const CustomColor = ({
maxLength={7}
value={value}
onChange={handleInputChange}
autoFocus
/>
</Flex>
);
@@ -136,11 +136,12 @@ const EmojiPanel = ({
freqEmojis,
});
React.useEffect(() => {
if (scrollableRef.current) {
scrollableRef.current.scrollTop = 0;
React.useLayoutEffect(() => {
if (!panelActive) {
return;
}
searchRef.current?.focus();
scrollableRef.current?.scroll({ top: 0 });
requestAnimationFrame(() => searchRef.current?.focus());
}, [panelActive]);
return (
@@ -115,7 +115,9 @@ const CategoryName = styled(Text)`
`;
const Icon = styled.svg`
transition: color 150ms ease-in-out, fill 150ms ease-in-out;
transition:
color 150ms ease-in-out,
fill 150ms ease-in-out;
transition-delay: var(--delay);
`;
@@ -155,11 +155,12 @@ const IconPanel = ({
baseIcons,
];
React.useEffect(() => {
if (scrollableRef.current) {
scrollableRef.current.scrollTop = 0;
React.useLayoutEffect(() => {
if (!panelActive) {
return;
}
searchRef.current?.focus();
scrollableRef.current?.scroll({ top: 0 });
requestAnimationFrame(() => searchRef.current?.focus());
}, [panelActive]);
return (
@@ -12,7 +12,8 @@ export const PopoverButton = styled(NudeButton)<{ $borderOnHover?: boolean }>`
$borderOnHover &&
css`
background: ${s("buttonNeutralBackground")};
box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 2px,
box-shadow:
rgba(0, 0, 0, 0.07) 0px 1px 2px,
${s("buttonNeutralBorder")} 0 0 0 1px inset;
`};
}
@@ -1,14 +1,17 @@
import { useMemo, useCallback } from "react";
import { useMemo, useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { Menu, MenuButton, MenuItem } from "reakit";
import styled from "styled-components";
import { depths, s, hover } from "@shared/styles";
import { s, hover } from "@shared/styles";
import { EmojiSkinTone } from "@shared/types";
import { getEmojiVariants } from "@shared/utils/emoji";
import { Emoji } from "~/components/Emoji";
import Flex from "~/components/Flex";
import NudeButton from "~/components/NudeButton";
import { useMenuState } from "~/hooks/useMenuState";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "~/components/primitives/Popover";
import { IconButton } from "./IconButton";
const SkinTonePicker = ({
@@ -19,62 +22,61 @@ const SkinTonePicker = ({
onChange: (skin: EmojiSkinTone) => void;
}) => {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const handEmojiVariants = useMemo(() => getEmojiVariants({ id: "hand" }), []);
const menu = useMenuState({
placement: "bottom-end",
});
const handleSkinClick = useCallback(
(emojiSkin) => {
menu.hide();
(emojiSkin: EmojiSkinTone) => {
setOpen(false);
onChange(emojiSkin);
},
[menu, onChange]
[onChange]
);
const menuItems = useMemo(
() =>
Object.entries(handEmojiVariants).map(([eskin, emoji]) => (
<MenuItem {...menu} key={emoji.value}>
{(menuprops) => (
<IconButton {...menuprops} onClick={() => handleSkinClick(eskin)}>
Object.values(EmojiSkinTone)
.map((skinTone) => {
const emoji = handEmojiVariants[skinTone];
return emoji ? (
<IconButton
key={emoji.value}
onClick={() => handleSkinClick(skinTone)}
>
<Emoji width={24} height={24}>
{emoji.value}
</Emoji>
</IconButton>
)}
</MenuItem>
)),
[menu, handEmojiVariants, handleSkinClick]
) : null;
})
.filter(Boolean),
[handEmojiVariants, handleSkinClick]
);
return (
<>
<MenuButton {...menu}>
{(props) => (
<StyledMenuButton
{...props}
aria-label={t("Choose default skin tone")}
>
{handEmojiVariants[skinTone]!.value}
</StyledMenuButton>
)}
</MenuButton>
<Menu {...menu} aria-label={t("Choose default skin tone")}>
{(props) => <MenuContainer {...props}>{menuItems}</MenuContainer>}
</Menu>
</>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger>
<StyledMenuButton aria-label={t("Choose default skin tone")}>
{handEmojiVariants[skinTone]?.value}
</StyledMenuButton>
</PopoverTrigger>
<PopoverContent
side="bottom"
align="end"
aria-label={t("Choose default skin tone")}
width={208}
scrollable={false}
shrink
>
<Emojis>{menuItems}</Emojis>
</PopoverContent>
</Popover>
);
};
const MenuContainer = styled(Flex)`
z-index: ${depths.menu};
padding: 4px;
border-radius: 4px;
background: ${s("menuBackground")};
box-shadow: ${s("menuShadow")};
const Emojis = styled(Flex)`
padding: 0 8px;
`;
const StyledMenuButton = styled(NudeButton)`
+39 -74
View File
@@ -1,19 +1,22 @@
import * as Popover from "@radix-ui/react-popover";
import * as Tabs from "@radix-ui/react-tabs";
import { SmileyIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled, { css } from "styled-components";
import Icon from "@shared/components/Icon";
import { s, hover, depths } from "@shared/styles";
import { s, hover } from "@shared/styles";
import theme from "@shared/styles/theme";
import { IconType } from "@shared/types";
import { determineIconType } from "@shared/utils/icon";
import Flex from "~/components/Flex";
import NudeButton from "~/components/NudeButton";
import {
Popover,
PopoverTrigger,
PopoverContent,
} from "~/components/primitives/Popover";
import useMobile from "~/hooks/useMobile";
import useWindowSize from "~/hooks/useWindowSize";
import { fadeAndScaleIn } from "~/styles/animations";
import { Drawer, DrawerContent, DrawerTrigger } from "../primitives/Drawer";
import EmojiPanel from "./components/EmojiPanel";
import IconPanel from "./components/IconPanel";
@@ -126,7 +129,24 @@ const IconPicker = ({
onChange(null, null);
}, [setOpen, onChange]);
const PickerContent = (
const pickerTrigger = (
<PopoverButton
aria-label={t("Show menu")}
className={className}
size={size}
$borderOnHover={borderOnHover}
>
{children ? (
children
) : iconType && icon ? (
<Icon value={icon} color={color} size={size} initial={initial} />
) : (
<StyledSmileyIcon color={theme.placeholder} size={size} />
)}
</PopoverButton>
);
const pickerContent = (
<Content
open={open}
activeTab={activeTab}
@@ -151,60 +171,28 @@ const IconPicker = ({
if (isMobile) {
return (
<Drawer open={open} onOpenChange={setOpen}>
<DrawerTrigger asChild>
<PopoverButton
aria-label={t("Show menu")}
className={className}
size={size}
$borderOnHover={borderOnHover}
>
{children ? (
children
) : iconType && icon ? (
<Icon value={icon} color={color} size={size} initial={initial} />
) : (
<StyledSmileyIcon color={theme.placeholder} size={size} />
)}
</PopoverButton>
</DrawerTrigger>
<DrawerTrigger asChild>{pickerTrigger}</DrawerTrigger>
<DrawerContent aria-label={t("Icon Picker")}>
{PickerContent}
{pickerContent}
</DrawerContent>
</Drawer>
);
}
return (
<Popover.Root open={open} onOpenChange={handleOpenChange} modal={true}>
<Popover.Trigger asChild>
<PopoverButton
aria-label={t("Show menu")}
className={className}
size={size}
$borderOnHover={borderOnHover}
>
{children ? (
children
) : iconType && icon ? (
<Icon value={icon} color={color} size={size} initial={initial} />
) : (
<StyledSmileyIcon color={theme.placeholder} size={size} />
)}
</PopoverButton>
</Popover.Trigger>
<Popover.Portal>
<StyledPopoverContent
side={popoverPosition === "right" ? "right" : "bottom"}
align={popoverPosition === "bottom-start" ? "start" : "center"}
sideOffset={0}
width={popoverWidth}
aria-label={t("Icon Picker")}
onClick={(e) => e.stopPropagation()}
>
{PickerContent}
</StyledPopoverContent>
</Popover.Portal>
</Popover.Root>
<Popover open={open} onOpenChange={handleOpenChange} modal={true}>
<PopoverTrigger>{pickerTrigger}</PopoverTrigger>
<PopoverContent
aria-label={t("Icon Picker")}
width={popoverWidth}
side={popoverPosition === "right" ? "right" : "bottom"}
align={popoverPosition === "bottom-start" ? "start" : "center"}
scrollable={false}
shrink
>
{pickerContent}
</PopoverContent>
</Popover>
);
};
@@ -348,27 +336,4 @@ const StyledTabContent = styled(Tabs.Content)`
overflow-y: auto;
`;
const StyledPopoverContent = styled(Popover.Content)<{ width: number }>`
animation: ${fadeAndScaleIn} 200ms ease;
transform-origin: var(--radix-popover-content-transform-origin);
background: ${s("menuBackground")};
border-radius: 6px;
padding: 6px 0;
max-height: 75vh;
box-shadow: ${s("menuShadow")};
z-index: ${depths.modal};
width: ${(props) => props.width}px;
overflow: hidden;
outline: none;
@media (max-width: 768px) {
position: fixed;
z-index: ${depths.menu};
top: 50px;
left: 8px;
right: 8px;
width: auto;
}
`;
export default React.memo(IconPicker);
+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);
`;
+2 -2
View File
@@ -107,8 +107,8 @@ export const Outline = styled(Flex)<{
props.hasError
? props.theme.danger
: props.focused
? props.theme.inputBorderFocused
: props.theme.inputBorder};
? props.theme.inputBorderFocused
: props.theme.inputBorder};
border-radius: 4px;
font-weight: normal;
align-items: center;
+26 -31
View File
@@ -1,16 +1,14 @@
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 { useMenuState } from "~/hooks/useMenuState";
import lazyWithRetry from "~/utils/lazyWithRetry";
import ContextMenu from "./ContextMenu";
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";
type Props = Omit<InputProps, "onChange"> & {
value: string | undefined;
@@ -19,10 +17,6 @@ type Props = Omit<InputProps, "onChange"> & {
const InputColor: React.FC<Props> = ({ value, onChange, ...rest }: Props) => {
const { t } = useTranslation();
const menu = useMenuState({
modal: true,
placement: "bottom-end",
});
return (
<Relative>
@@ -33,30 +27,26 @@ const InputColor: React.FC<Props> = ({ value, onChange, ...rest }: Props) => {
maxLength={7}
{...rest}
/>
<MenuButton {...menu}>
{(props) => (
<SwatchButton
aria-label={t("Show menu")}
{...props}
$background={value}
/>
)}
</MenuButton>
<ContextMenu {...menu} aria-label={t("Select a color")}>
<React.Suspense
fallback={
<DelayedMount>
<Text>{t("Loading")}</Text>
</DelayedMount>
}
>
<StyledColorPicker
disableAlpha
color={value}
onChange={(color) => onChange(color.hex)}
/>
</React.Suspense>
</ContextMenu>
<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>
);
};
@@ -70,6 +60,11 @@ const SwatchButton = styled(NudeButton)<{ $background: string | undefined }>`
right: 6px;
`;
const StyledContent = styled(PopoverContent)`
width: auto;
padding: 8px;
`;
const ColorPicker = lazyWithRetry(
() => import("react-color/lib/components/chrome/Chrome")
);
+27 -16
View File
@@ -2,23 +2,42 @@ import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { s } from "@shared/styles";
import InputSelect, { Props as SelectProps } from "~/components/InputSelect";
import { InputSelect, Option } from "~/components/InputSelect";
import { EmptySelectValue, Permission } from "~/types";
type Props = Pick<
React.ComponentProps<typeof InputSelect>,
"value" | "onChange" | "disabled" | "className"
>;
export default function InputMemberPermissionSelect(
props: Partial<SelectProps> & { permissions: Permission[] }
props: Props & { permissions: Permission[] }
) {
const { value, onChange, ...rest } = props;
const { t } = useTranslation();
const options = React.useMemo<Option[]>(
() =>
props.permissions.reduce((acc, permission) => {
if (permission.divider) {
acc.push({ type: "separator" });
}
acc.push({
...permission,
type: "item",
});
return acc;
}, [] as Option[]),
[props.permissions]
);
return (
<Select
label={t("Permissions")}
options={props.permissions}
ariaLabel={t("Permissions")}
onChange={onChange}
options={options}
value={value || EmptySelectValue}
labelHidden
onChange={onChange}
label={t("Permissions")}
hideLabel
nude
{...rest}
/>
@@ -26,13 +45,5 @@ export default function InputMemberPermissionSelect(
}
const Select = styled(InputSelect)`
margin: 0;
font-size: 14px;
border-color: transparent;
box-shadow: none;
color: ${s("textSecondary")};
select {
margin: 0;
}
` as React.ComponentType<SelectProps>;
`;
+354 -335
View File
@@ -1,383 +1,402 @@
import * as VisuallyHidden from "@radix-ui/react-visually-hidden";
import {
Select,
SelectOption,
useSelectState,
useSelectPopover,
SelectPopover,
} from "@renderlesskit/react";
import { CheckmarkIcon } from "outline-icons";
import { QuestionMarkIcon } from "outline-icons";
import { transparentize } from "polished";
import * as React from "react";
import styled, { css } from "styled-components";
import styled from "styled-components";
import { s } from "@shared/styles";
import Button, { Props as ButtonProps, Inner } from "~/components/Button";
import Text from "~/components/Text";
import useMenuHeight from "~/hooks/useMenuHeight";
import useMobile from "~/hooks/useMobile";
import useOnClickOutside from "~/hooks/useOnClickOutside";
import { fadeAndScaleIn } from "~/styles/animations";
import {
Position,
Background as ContextMenuBackground,
Backdrop,
Placement,
} from "./ContextMenu";
import { MenuAnchorCSS } from "./ContextMenu/MenuItem";
import Separator from "./ContextMenu/Separator";
import Flex from "./Flex";
import { LabelText } from "./Input";
import NudeButton from "./NudeButton";
import Scrollable from "./Scrollable";
import Tooltip from "./Tooltip";
import {
Drawer,
DrawerContent,
DrawerTitle,
DrawerTrigger,
} from "./primitives/Drawer";
import {
InputSelectRoot,
InputSelectContent,
InputSelectItem,
InputSelectSeparator,
InputSelectTrigger,
type TriggerButtonProps,
} from "./primitives/InputSelect";
import {
SelectItemIndicator,
SelectItem as SelectItemWrapper,
SelectButton,
} from "./primitives/components/InputSelect";
export type Option = {
label: string | JSX.Element;
type Separator = {
/* Denotes a horizontal divider line to be rendered in the menu, */
type: "separator";
};
export type Item = {
/* Denotes a selectable option in the menu. */
type: "item";
/* Representative text shown in the menu for this option. */
label: string;
/* Actual value of this option. */
value: string;
/* Additional info shown alongside the label. */
description?: string;
divider?: boolean;
/* An icon shown alongside the label. */
icon?: React.ReactElement;
};
export type Props = Omit<ButtonProps<any>, "onChange"> & {
id?: string;
name?: string;
value?: string | null;
label?: React.ReactNode;
nude?: boolean;
ariaLabel: string;
short?: boolean;
disabled?: boolean;
className?: string;
labelHidden?: boolean;
icon?: React.ReactNode;
export type Option = Item | Separator;
type Props = {
/* Options to display in the select menu. */
options: Option[];
/** @deprecated Removing soon, do not use. */
note?: React.ReactNode;
/** Callback function that is called when the value changes. Return false to cancel the change. */
onChange?: (value: string | null) => void | Promise<boolean | void>;
style?: React.CSSProperties;
/**
* Set to true if this component is rendered inside a Modal.
* The Modal will take care of preventing body scroll behaviour.
*/
skipBodyScroll?: boolean;
};
/* Current chosen value. */
value?: string | null;
/* Callback when an option is selected. */
onChange: (value: string) => void;
/* Label for the select menu. */
label: string;
/* When true, label is hidden in an accessible manner. */
hideLabel?: boolean;
/* When true, menu is disabled. */
disabled?: boolean;
/* When true, width of the menu trigger is restricted. Otherwise, takes up the full width of parent. */
short?: boolean;
/** Display a tooltip with the descriptive help text about the select menu. */
help?: string;
} & TriggerButtonProps;
export interface InputSelectRef {
value: string | null;
focus: () => void;
blur: () => void;
}
export const InputSelect = React.forwardRef<HTMLButtonElement, Props>(
(props, ref) => {
const {
options,
value,
onChange,
label,
hideLabel,
short,
help,
...triggerProps
} = props;
interface InnerProps extends React.HTMLAttributes<HTMLDivElement> {
placement: Placement;
}
const [localValue, setLocalValue] = React.useState(value);
const [open, setOpen] = React.useState(false);
const getOptionFromValue = (options: Option[], value: string | null) =>
options.find((option) => option.value === value);
const contentRef =
React.useRef<React.ElementRef<typeof InputSelectContent>>(null);
const InputSelect = (props: Props, ref: React.RefObject<InputSelectRef>) => {
const {
value = null,
label,
className,
labelHidden,
options,
short,
ariaLabel,
onChange,
disabled,
note,
icon,
nude,
skipBodyScroll,
...rest
} = props;
const isMobile = useMobile();
const select = useSelectState({
gutter: 0,
modal: true,
selectedValue: value,
});
const placeholder = `Select a ${label.toLowerCase()}`;
const optionsHaveIcon = options.some(
(opt) => opt.type === "item" && !!opt.icon
);
const popover = useSelectPopover({
...select,
hideOnClickOutside: false,
preventBodyScroll: skipBodyScroll ? false : true,
disabled,
});
const isMobile = useMobile();
const previousValue = React.useRef<string | null>(value);
const selectedRef = React.useRef<HTMLDivElement>(null);
const buttonRef = React.useRef<HTMLButtonElement>(null);
const contentRef = React.useRef<HTMLDivElement>(null);
const minWidth = buttonRef.current?.offsetWidth || 0;
const margin = 8;
const menuMaxHeight = useMenuHeight({
visible: select.visible,
elementRef: select.unstable_disclosureRef,
margin,
});
const maxHeight = Math.min(
menuMaxHeight ?? 0,
window.innerHeight -
(buttonRef.current?.getBoundingClientRect().bottom ?? 0) -
margin
);
const wrappedLabel = <LabelText>{label}</LabelText>;
const selectedValueIndex = options.findIndex(
(opt) => opt.value === select.selectedValue
);
// Custom click outside handling rather than using `hideOnClickOutside` from reakit so that we can
// prevent event bubbling.
useOnClickOutside(
contentRef,
(event) => {
if (buttonRef.current?.contains(event.target as Node)) {
return;
}
if (select.visible) {
event.stopPropagation();
event.preventDefault();
select.hide();
}
},
{ capture: true }
);
React.useImperativeHandle(ref, () => ({
focus: () => {
buttonRef.current?.focus();
},
blur: () => {
buttonRef.current?.blur();
},
value: select.selectedValue,
}));
React.useEffect(() => {
previousValue.current = value;
// Update the selected value if it changes from the outside both of these lines are needed
// for correct functioning
select.selectedValue = value;
select.setSelectedValue(value);
}, [value]);
React.useEffect(() => {
if (previousValue.current === select.selectedValue) {
return;
}
const previous = previousValue.current;
previousValue.current = select.selectedValue;
const response = onChange?.(select.selectedValue);
if (response && response instanceof Promise) {
void response.then((success) => {
if (success === false) {
select.selectedValue = previous;
select.setSelectedValue(previous);
const renderOption = React.useCallback(
(option: Option, idx: number) => {
if (option.type === "separator") {
return <InputSelectSeparator key={`separator-${idx}`} />;
}
});
}
}, [onChange, select.selectedValue]);
React.useLayoutEffect(() => {
if (select.visible) {
requestAnimationFrame(() => {
if (contentRef.current) {
contentRef.current.scrollTop = selectedValueIndex * 32;
}
});
}
}, [select.visible, selectedValueIndex]);
return (
<InputSelectItem key={option.value} value={option.value}>
<Option option={option} optionsHaveIcon={optionsHaveIcon} />
</InputSelectItem>
);
},
[optionsHaveIcon]
);
const onValueChange = React.useCallback(
async (val: string) => {
setLocalValue(val);
onChange(val);
},
[onChange, setLocalValue]
);
const enablePointerEvents = React.useCallback(() => {
if (contentRef.current) {
contentRef.current.style.pointerEvents = "auto";
}
}, []);
const disablePointerEvents = React.useCallback(() => {
if (contentRef.current) {
contentRef.current.style.pointerEvents = "none";
}
}, []);
React.useEffect(() => {
setLocalValue(value);
}, [value]);
if (isMobile) {
return (
<MobileSelect
ref={ref}
{...props}
value={localValue}
onChange={onValueChange}
placeholder={placeholder}
optionsHaveIcon={optionsHaveIcon}
/>
);
}
function labelForOption(opt: Option) {
return (
<>
{opt.label}
{opt.description && (
<>
&nbsp;
<Text as="span" type="tertiary" size="small" ellipsis>
{opt.description}
</Text>
</>
)}
</>
<Wrapper short={short}>
<Label text={label} hidden={hideLabel ?? false} help={help} />
<InputSelectRoot
open={open}
onOpenChange={setOpen}
value={localValue ?? undefined}
onValueChange={onValueChange}
>
<InputSelectTrigger
ref={ref}
placeholder={placeholder}
{...triggerProps}
/>
<InputSelectContent
ref={contentRef}
aria-label={label}
onAnimationStart={disablePointerEvents}
onAnimationEnd={enablePointerEvents}
>
{options.map(renderOption)}
</InputSelectContent>
</InputSelectRoot>
</Wrapper>
);
}
);
InputSelect.displayName = "InputSelect";
const option = getOptionFromValue(options, select.selectedValue);
return (
<>
<Wrapper short={short}>
{label &&
(labelHidden ? (
<VisuallyHidden.Root>{wrappedLabel}</VisuallyHidden.Root>
) : (
wrappedLabel
))}
<Select {...select} disabled={disabled} {...rest} ref={buttonRef}>
{(buttonProps) => (
<StyledButton
neutral
disclosure
className={className}
icon={icon}
$nude={nude}
{...buttonProps}
>
{option ? (
labelForOption(option)
) : (
<Placeholder>Select a {ariaLabel.toLowerCase()}</Placeholder>
)}
</StyledButton>
)}
</Select>
<SelectPopover
{...select}
{...popover}
aria-label={ariaLabel}
preventBodyScroll={skipBodyScroll ? false : true}
>
{(popoverProps: InnerProps) => {
const topAnchor = popoverProps.style?.top === "0";
const rightAnchor = popoverProps.placement === "bottom-end";
return (
<Positioner {...popoverProps}>
<Background
dir="auto"
ref={contentRef}
topAnchor={topAnchor}
rightAnchor={rightAnchor}
hiddenScrollbars
maxWidth={400}
style={
maxHeight && topAnchor
? {
maxHeight,
minWidth,
}
: {
minWidth,
}
}
>
{select.visible
? options.map((opt) => {
const isSelected = select.selectedValue === opt.value;
const Icon = isSelected ? CheckmarkIcon : Spacer;
return (
<React.Fragment key={opt.value}>
{opt.divider && <Separator />}
<StyledSelectOption
{...select}
value={opt.value}
key={opt.value}
ref={isSelected ? selectedRef : undefined}
>
<Icon />
&nbsp;
{labelForOption(opt)}
</StyledSelectOption>
</React.Fragment>
);
})
: null}
</Background>
</Positioner>
);
}}
</SelectPopover>
</Wrapper>
{note && (
<Text as="p" type="secondary" size="small">
{note}
</Text>
)}
{select.visible && isMobile && <Backdrop />}
</>
);
type MobileSelectProps = Props & {
placeholder: string;
optionsHaveIcon: boolean;
};
const Background = styled(ContextMenuBackground)`
animation: ${fadeAndScaleIn} 200ms ease;
`;
const MobileSelect = React.forwardRef<HTMLButtonElement, MobileSelectProps>(
(props, ref) => {
const {
options,
value,
onChange,
label,
hideLabel,
disabled,
short,
placeholder,
optionsHaveIcon,
...triggerProps
} = props;
const Placeholder = styled.span`
color: ${s("placeholder")};
`;
const [open, setOpen] = React.useState(false);
const contentRef =
React.useRef<React.ElementRef<typeof DrawerContent>>(null);
const Spacer = styled.div`
width: 24px;
height: 24px;
flex-shrink: 0;
`;
const selectedOption = React.useMemo(
() =>
value
? options.find((opt) => opt.type === "item" && opt.value === value)
: undefined,
[value, options]
);
const StyledButton = styled(Button)<{ $nude?: boolean }>`
font-weight: normal;
text-transform: none;
margin-bottom: 16px;
display: block;
width: 100%;
cursor: var(--pointer);
const handleSelect = React.useCallback(
async (val: string) => {
setOpen(false);
onChange(val);
},
[onChange]
);
&:hover:not(:disabled) {
background: ${s("buttonNeutralBackground")};
const renderOption = React.useCallback(
(option: Option) => {
if (option.type === "separator") {
return <Separator />;
}
const isSelected = option === selectedOption;
return (
<SelectItemWrapper
key={option.value}
onClick={() => handleSelect(option.value)}
data-state={isSelected ? "checked" : "unchecked"}
>
<Option option={option} optionsHaveIcon={optionsHaveIcon} />
{isSelected && <SelectItemIndicator />}
</SelectItemWrapper>
);
},
[handleSelect, selectedOption, optionsHaveIcon]
);
const enablePointerEvents = React.useCallback(() => {
if (contentRef.current) {
contentRef.current.style.pointerEvents = "auto";
}
}, []);
const disablePointerEvents = React.useCallback(() => {
if (contentRef.current) {
contentRef.current.style.pointerEvents = "none";
}
}, []);
return (
<Wrapper>
<Label text={label} hidden={hideLabel ?? false} />
<Drawer open={open} onOpenChange={setOpen}>
<DrawerTrigger asChild>
<SelectButton
ref={ref}
{...triggerProps}
neutral
disclosure
data-placeholder={selectedOption ? false : ""}
>
{selectedOption ? (
<Option
option={selectedOption as Item}
optionsHaveIcon={optionsHaveIcon}
/>
) : (
<>{placeholder}</>
)}
</SelectButton>
</DrawerTrigger>
<DrawerContent
ref={contentRef}
aria-label={label}
onAnimationStart={disablePointerEvents}
onAnimationEnd={enablePointerEvents}
>
<DrawerTitle hidden={!label}>{label}</DrawerTitle>
<StyledScrollable hiddenScrollbars>
{options.map(renderOption)}
</StyledScrollable>
</DrawerContent>
</Drawer>
</Wrapper>
);
}
);
MobileSelect.displayName = "InputSelect";
${(props) =>
props.$nude &&
css`
border-color: transparent;
box-shadow: none;
`}
function Label({
text,
hidden,
help,
}: {
text: string;
hidden: boolean;
help?: string;
}) {
const content = (
<Flex align="center" gap={2} style={{ marginBottom: "4px" }}>
<LabelText style={{ paddingBottom: 0 }}>{text}</LabelText>
{help ? (
<Tooltip content={help}>
<TooltipButton size={18}>
<QuestionMarkIcon size={18} />
</TooltipButton>
</Tooltip>
) : null}
</Flex>
);
${Inner} {
line-height: 28px;
padding-left: 12px;
padding-right: 4px;
}
return hidden ? (
<VisuallyHidden.Root>{content}</VisuallyHidden.Root>
) : (
content
);
}
svg {
justify-self: flex-end;
margin-left: auto;
}
`;
function Option({
option,
optionsHaveIcon,
}: {
option: Item;
optionsHaveIcon: boolean;
}) {
const icon = optionsHaveIcon ? (
option.icon ? (
<IconWrapper>{option.icon}</IconWrapper>
) : (
<IconSpacer />
)
) : null;
export const StyledSelectOption = styled(SelectOption)`
${MenuAnchorCSS}
/* overriding the styles from MenuAnchorCSS because we use &nbsp; here */
svg:not(:last-child) {
margin-right: 0px;
}
`;
return (
<OptionContainer align="center">
{icon}
{option.label}
{option.description && (
<>
&nbsp;
<Description type="tertiary" size="small" ellipsis>
{option.description}
</Description>
</>
)}
</OptionContainer>
);
}
const Wrapper = styled.label<{ short?: boolean }>`
display: block;
max-width: ${(props) => (props.short ? "350px" : "100%")};
`;
export const Positioner = styled(Position)`
pointer-events: all;
const OptionContainer = styled(Flex)`
min-height: 24px;
`;
&:focus-visible {
${StyledSelectOption} {
&[aria-selected="true"] {
color: ${(props) => props.theme.white};
background: ${s("accent")};
box-shadow: none;
cursor: var(--pointer);
svg {
fill: ${(props) => props.theme.white};
}
}
const Description = styled(Text)`
@media (hover: hover) {
&:hover,
&:focus {
color: ${(props) => transparentize(0.5, props.theme.accentText)};
}
}
`;
export default React.forwardRef(InputSelect);
const IconWrapper = styled.span`
display: flex;
justify-content: center;
align-items: center;
width: 24px;
height: 24px;
margin-left: -4px;
margin-right: 4px;
overflow: hidden;
flex-shrink: 0;
`;
const IconSpacer = styled.div`
width: 24px;
height: 24px;
flex-shrink: 0;
`;
const StyledScrollable = styled(Scrollable)`
max-height: 75vh;
`;
const TooltipButton = styled(NudeButton)`
color: ${s("textSecondary")};
&:hover,
&[aria-expanded="true"] {
background: none !important;
}
`;
-354
View File
@@ -1,354 +0,0 @@
import * as VisuallyHidden from "@radix-ui/react-visually-hidden";
import { transparentize } from "polished";
import * as React from "react";
import styled from "styled-components";
import Text from "~/components/Text";
import useMobile from "~/hooks/useMobile";
import Separator from "./ContextMenu/Separator";
import Flex from "./Flex";
import { LabelText } from "./Input";
import Scrollable from "./Scrollable";
import { IconWrapper } from "./Sidebar/components/SidebarLink";
import {
Drawer,
DrawerContent,
DrawerTitle,
DrawerTrigger,
} from "./primitives/Drawer";
import {
InputSelectRoot,
InputSelectContent,
InputSelectItem,
InputSelectSeparator,
InputSelectTrigger,
type TriggerButtonProps,
} from "./primitives/InputSelect";
import {
SelectItemIndicator,
SelectItem as SelectItemWrapper,
SelectButton,
} from "./primitives/components/InputSelect";
type Separator = {
/* Denotes a horizontal divider line to be rendered in the menu, */
type: "separator";
};
export type Item = {
/* Denotes a selectable option in the menu. */
type: "item";
/* Representative text shown in the menu for this option. */
label: string;
/* Actual value of this option. */
value: string;
/* Additional info shown alongside the label. */
description?: string;
/* An icon shown alongside the label. */
icon?: React.ReactElement;
};
export type Option = Item | Separator;
type Props = {
/* Options to display in the select menu. */
options: Option[];
/* Current chosen value. */
value?: string;
/* Callback when an option is selected. */
onChange: (value: string) => void;
/* ARIA label for accessibility. */
ariaLabel: string;
/* Label for the select menu. */
label: string;
/* When true, label is hidden in an accessible manner. */
hideLabel?: boolean;
/* When true, menu is disabled. */
disabled?: boolean;
/* When true, width of the menu trigger is restricted. Otherwise, takes up the full width of parent. */
short?: boolean;
} & TriggerButtonProps;
export function InputSelectNew(props: Props) {
const {
options,
value,
onChange,
ariaLabel,
label,
hideLabel,
disabled,
short,
...triggerProps
} = props;
const [localValue, setLocalValue] = React.useState(value);
const [open, setOpen] = React.useState(false);
const triggerRef =
React.useRef<React.ElementRef<typeof InputSelectTrigger>>(null);
const contentRef =
React.useRef<React.ElementRef<typeof InputSelectContent>>(null);
const isMobile = useMobile();
const placeholder = `Select a ${ariaLabel.toLowerCase()}`;
const optionsHaveIcon = options.some(
(opt) => opt.type === "item" && !!opt.icon
);
const renderOption = React.useCallback(
(option: Option) => {
if (option.type === "separator") {
return <InputSelectSeparator />;
}
return (
<InputSelectItem key={option.value} value={option.value}>
<Option option={option} optionsHaveIcon={optionsHaveIcon} />
</InputSelectItem>
);
},
[optionsHaveIcon]
);
const onValueChange = React.useCallback(
async (val: string) => {
setLocalValue(val);
onChange(val);
},
[onChange, setLocalValue]
);
const enablePointerEvents = React.useCallback(() => {
if (contentRef.current) {
contentRef.current.style.pointerEvents = "auto";
}
}, []);
const disablePointerEvents = React.useCallback(() => {
if (contentRef.current) {
contentRef.current.style.pointerEvents = "none";
}
}, []);
React.useEffect(() => {
setLocalValue(value);
}, [value]);
if (isMobile) {
return (
<MobileSelect
{...props}
value={localValue}
onChange={onValueChange}
placeholder={placeholder}
optionsHaveIcon={optionsHaveIcon}
/>
);
}
return (
<Wrapper short={short}>
<Label text={label} hidden={hideLabel ?? false} />
<InputSelectRoot
open={open}
onOpenChange={setOpen}
value={localValue}
onValueChange={onValueChange}
>
<InputSelectTrigger
ref={triggerRef}
placeholder={placeholder}
{...triggerProps}
/>
<InputSelectContent
ref={contentRef}
aria-label={ariaLabel}
onAnimationStart={disablePointerEvents}
onAnimationEnd={enablePointerEvents}
>
{options.map(renderOption)}
</InputSelectContent>
</InputSelectRoot>
</Wrapper>
);
}
type MobileSelectProps = Props & {
placeholder: string;
optionsHaveIcon: boolean;
};
function MobileSelect(props: MobileSelectProps) {
const {
options,
value,
onChange,
ariaLabel,
label,
hideLabel,
disabled,
short,
placeholder,
optionsHaveIcon,
...triggerProps
} = props;
const [open, setOpen] = React.useState(false);
const contentRef = React.useRef<React.ElementRef<typeof DrawerContent>>(null);
const selectedOption = React.useMemo(
() =>
value
? options.find((opt) => opt.type === "item" && opt.value === value)
: undefined,
[value, options]
);
const handleSelect = React.useCallback(
async (val: string) => {
setOpen(false);
onChange(val);
},
[onChange]
);
const renderOption = React.useCallback(
(option: Option) => {
if (option.type === "separator") {
return <Separator />;
}
const isSelected = option === selectedOption;
return (
<SelectItemWrapper
key={option.value}
onClick={() => handleSelect(option.value)}
data-state={isSelected ? "checked" : "unchecked"}
>
<Option option={option} optionsHaveIcon={optionsHaveIcon} />
{isSelected && <SelectItemIndicator />}
</SelectItemWrapper>
);
},
[handleSelect, selectedOption, optionsHaveIcon]
);
const enablePointerEvents = React.useCallback(() => {
if (contentRef.current) {
contentRef.current.style.pointerEvents = "auto";
}
}, []);
const disablePointerEvents = React.useCallback(() => {
if (contentRef.current) {
contentRef.current.style.pointerEvents = "none";
}
}, []);
return (
<Wrapper>
<Label text={label} hidden={hideLabel ?? false} />
<Drawer open={open} onOpenChange={setOpen}>
<DrawerTrigger asChild>
<SelectButton
{...triggerProps}
neutral
disclosure
data-placeholder={selectedOption ? false : ""}
>
{selectedOption ? (
<Option
option={selectedOption as Item}
optionsHaveIcon={optionsHaveIcon}
/>
) : (
<>{placeholder}</>
)}
</SelectButton>
</DrawerTrigger>
<DrawerContent
ref={contentRef}
aria-label={ariaLabel}
onAnimationStart={disablePointerEvents}
onAnimationEnd={enablePointerEvents}
>
<DrawerTitle hidden={!label}>{label ?? ariaLabel}</DrawerTitle>
<StyledScrollable hiddenScrollbars>
{options.map(renderOption)}
</StyledScrollable>
</DrawerContent>
</Drawer>
</Wrapper>
);
}
function Label({ text, hidden }: { text: string; hidden: boolean }) {
const labelText = <LabelText>{text}</LabelText>;
return hidden ? (
<VisuallyHidden.Root>{labelText}</VisuallyHidden.Root>
) : (
labelText
);
}
function Option({
option,
optionsHaveIcon,
}: {
option: Item;
optionsHaveIcon: boolean;
}) {
const icon = optionsHaveIcon ? (
option.icon ? (
<IconWrapper>{option.icon}</IconWrapper>
) : (
<IconSpacer />
)
) : null;
return (
<OptionContainer align="center">
{icon}
{option.label}
{option.description && (
<>
&nbsp;
<Description type="tertiary" size="small" ellipsis>
{option.description}
</Description>
</>
)}
</OptionContainer>
);
}
const Wrapper = styled.label<{ short?: boolean }>`
display: block;
max-width: ${(props) => (props.short ? "350px" : "100%")};
`;
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 IconSpacer = styled.div`
width: 24px;
height: 24px;
flex-shrink: 0;
`;
const StyledScrollable = styled(Scrollable)`
max-height: 75vh;
`;
+40 -31
View File
@@ -1,54 +1,63 @@
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { $Diff } from "utility-types";
import { s } from "@shared/styles";
import { CollectionPermission } from "@shared/types";
import { InputSelect, Option } from "~/components/InputSelect";
import { EmptySelectValue } from "~/types";
import InputSelect, { Props, Option, InputSelectRef } from "./InputSelect";
function InputSelectPermission(
props: $Diff<
Props,
{
options: Array<Option>;
ariaLabel: string;
}
>,
ref: React.RefObject<InputSelectRef>
) {
const { value, onChange, ...rest } = props;
const { t } = useTranslation();
type Props = {
shrink?: boolean;
} & Pick<
React.ComponentProps<typeof InputSelect>,
"value" | "onChange" | "disabled" | "hideLabel" | "nude" | "help"
>;
return (
<Select
ref={ref}
label={t("Permission")}
options={[
export const InputSelectPermission = React.forwardRef<HTMLButtonElement, Props>(
(props, ref) => {
const { value, onChange, shrink, ...rest } = props;
const { t } = useTranslation();
const options = React.useMemo<Option[]>(
() => [
{
type: "item",
label: t("View only"),
value: CollectionPermission.Read,
},
{
type: "item",
label: t("Can edit"),
value: CollectionPermission.ReadWrite,
},
{
divider: true,
type: "separator",
},
{
type: "item",
label: t("No access"),
value: EmptySelectValue,
},
]}
ariaLabel={t("Default access")}
value={value || EmptySelectValue}
onChange={onChange}
{...rest}
/>
);
}
],
[t]
);
const Select = styled(InputSelect)`
return (
<Select
ref={ref}
options={options}
value={value || EmptySelectValue}
onChange={onChange}
label={t("Permission")}
$shrink={shrink}
{...rest}
/>
);
}
);
InputSelectPermission.displayName = "InputSelectPermission";
const Select = styled(InputSelect)<{ $shrink?: boolean }>`
color: ${s("textSecondary")};
${({ $shrink }) => !$shrink && "margin-bottom: 16px;"}
`;
export default React.forwardRef(InputSelectPermission);
+823
View File
@@ -0,0 +1,823 @@
import { useEditor } from "~/editor/components/EditorContext";
import { observer } from "mobx-react";
import * as Dialog from "@radix-ui/react-dialog";
import { findChildren } from "@shared/editor/queries/findChildren";
import findIndex from "lodash/findIndex";
import styled, { css, Keyframes, keyframes } from "styled-components";
import { forwardRef, useEffect, useMemo, useRef, useState } from "react";
import { sanitizeUrl } from "@shared/utils/urls";
import { Error } from "@shared/editor/components/Image";
import {
BackIcon,
CloseIcon,
CrossIcon,
DownloadIcon,
LinkIcon,
NextIcon,
} from "outline-icons";
import { depths, extraArea, s } from "@shared/styles";
import NudeButton from "./NudeButton";
import useIdle from "~/hooks/useIdle";
import { Second } from "@shared/utils/time";
import { downloadImageNode } from "@shared/editor/nodes/Image";
import * as VisuallyHidden from "@radix-ui/react-visually-hidden";
import { useTranslation } from "react-i18next";
import Tooltip from "~/components/Tooltip";
import LoadingIndicator from "./LoadingIndicator";
import Fade from "./Fade";
import Button from "./Button";
import CopyToClipboard from "./CopyToClipboard";
import { Separator } from "./Actions";
import useSwipe from "~/hooks/useSwipe";
export enum LightboxStatus {
READY_TO_OPEN,
OPENING,
OPENED,
READY_TO_CLOSE,
CLOSING,
CLOSED,
}
export enum ImageStatus {
LOADING,
ERROR,
LOADED,
}
type Status = {
lightbox: LightboxStatus | null;
image: ImageStatus | null;
};
type Animation = {
fadeIn?: { apply: () => Keyframes; duration: number };
fadeOut?: { apply: () => Keyframes; duration: number };
zoomIn?: { apply: () => Keyframes; duration: number };
zoomOut?: { apply: () => Keyframes; duration: number };
startTime?: number;
};
const ANIMATION_DURATION = 0.3 * Second.ms;
type Props = {
/** Callback triggered when the active image position is updated */
onUpdate: (pos: number | null) => void;
/** The position of the currently active image in the document */
activePos: number | null;
};
function Lightbox({ onUpdate, activePos }: Props) {
const { view } = useEditor();
const isIdle = useIdle(3 * Second.ms);
const { t } = useTranslation();
const imgRef = useRef<HTMLImageElement | null>(null);
const overlayRef = useRef<HTMLDivElement | null>(null);
const [status, setStatus] = useState<Status>({ lightbox: null, image: null });
const [imageElements] = useState(
view?.dom.querySelectorAll(".component-image img")
);
const animation = useRef<Animation | null>(null);
const finalImage = useRef<{
center: { x: number; y: number };
width: number;
height: number;
} | null>(null);
const imageNodes = useMemo(
() =>
view
? findChildren(
view.state.doc,
(child) => child.type === view.state.schema.nodes.image,
true
)
: [],
[view]
);
const currentImageIndex = findIndex(
imageNodes,
(node) => node.pos === activePos
);
const currentImageNode =
currentImageIndex >= 0 ? imageNodes[currentImageIndex].node : undefined;
// Debugging status changes
// useEffect(() => {
// console.log(
// `lstat:${status.lightbox === null ? status.lightbox : LightboxStatus[status.lightbox]}, istat:${status.image === null ? status.image : ImageStatus[status.image]}`
// );
// }, [status]);
useEffect(() => () => view.focus(), []);
useEffect(() => {
!!activePos &&
setStatus({
lightbox: LightboxStatus.READY_TO_OPEN,
image: status.image,
});
}, [!!activePos]);
useEffect(() => {
if (status.image === ImageStatus.LOADED) {
rememberImagePosition();
}
}, [status.image]);
useEffect(() => {
if (
(status.image === ImageStatus.ERROR ||
status.image === ImageStatus.LOADED) &&
status.lightbox === LightboxStatus.READY_TO_OPEN
) {
setupFadeIn();
setupZoomIn();
setStatus({
lightbox: LightboxStatus.OPENING,
image: status.image,
});
}
}, [status.image, status.lightbox]);
useEffect(() => {
if (status.lightbox === LightboxStatus.READY_TO_CLOSE) {
setupFadeOut();
setupZoomOut();
setStatus({
lightbox: LightboxStatus.CLOSING,
image: status.image,
});
}
}, [status.lightbox]);
useEffect(() => {
if (status.lightbox === LightboxStatus.CLOSED) {
onUpdate(null);
}
}, [status.lightbox]);
const rememberImagePosition = () => {
if (imgRef.current) {
const lightboxImgDOMRect = imgRef.current.getBoundingClientRect();
const {
top: lightboxImgTop,
left: lightboxImgLeft,
width: lightboxImgWidth,
height: lightboxImgHeight,
} = lightboxImgDOMRect;
finalImage.current = {
center: {
x: lightboxImgLeft + lightboxImgWidth / 2,
y: lightboxImgTop + lightboxImgHeight / 2,
},
width: lightboxImgWidth,
height: lightboxImgHeight,
};
}
};
const setupZoomIn = () => {
if (imgRef.current) {
// in editor
const editorImageEl = imageElements[currentImageIndex];
if (!editorImageEl) {
return;
}
const editorImgDOMRect = editorImageEl.getBoundingClientRect();
const {
top: editorImgTop,
left: editorImgLeft,
width: editorImgWidth,
height: editorImgHeight,
} = editorImgDOMRect;
const from = {
center: {
x: editorImgLeft + editorImgWidth / 2,
y: editorImgTop + editorImgHeight / 2,
},
width: editorImgWidth,
height: editorImgHeight,
};
// in lightbox
const lightboxImgDOMRect = imgRef.current.getBoundingClientRect();
const {
top: lightboxImgTop,
left: lightboxImgLeft,
width: lightboxImgWidth,
height: lightboxImgHeight,
} = lightboxImgDOMRect;
const to = {
center: {
x: lightboxImgLeft + lightboxImgWidth / 2,
y: lightboxImgTop + lightboxImgHeight / 2,
},
width: lightboxImgWidth,
height: lightboxImgHeight,
};
const zoomIn = () => {
const tx = from.center.x - to.center.x;
const ty = from.center.y - to.center.y;
return keyframes`
from {
translate: ${tx}px ${ty}px;
scale: ${from.width / to.width};
}
to {
translate: 0;
scale: 1;
}
`;
};
animation.current = {
...(animation.current ?? {}),
zoomOut: undefined,
zoomIn: { apply: zoomIn, duration: ANIMATION_DURATION },
};
}
};
const setupFadeIn = () => {
const fadeIn = () => keyframes`
from { opacity: 0; }
to { opacity: 1; }
`;
animation.current = {
...(animation.current ?? {}),
fadeIn: { apply: fadeIn, duration: ANIMATION_DURATION },
fadeOut: undefined,
};
};
const setupFadeOut = () => {
const fadeOut = () => keyframes`
from { opacity: ${overlayRef.current ? window.getComputedStyle(overlayRef.current).opacity : 1}; }
to { opacity: 0; }
`;
animation.current = {
...(animation.current ?? {}),
fadeIn: undefined,
fadeOut: {
apply: fadeOut,
duration: animation.current?.startTime
? Date.now() - animation.current.startTime
: ANIMATION_DURATION,
},
};
};
const setupZoomOut = () => {
if (imgRef.current) {
// in lightbox
const lightboxImgDOMRect = imgRef.current.getBoundingClientRect();
const {
top: lightboxImgTop,
left: lightboxImgLeft,
width: lightboxImgWidth,
height: lightboxImgHeight,
} = lightboxImgDOMRect;
const from = {
center: {
x: lightboxImgLeft + lightboxImgWidth / 2,
y: lightboxImgTop + lightboxImgHeight / 2,
},
width: lightboxImgWidth,
height: lightboxImgHeight,
};
// in editor
const editorImageEl = imageElements[currentImageIndex];
let to;
if (editorImageEl?.isConnected) {
const editorImgDOMRect = editorImageEl.getBoundingClientRect();
const {
top: editorImgTop,
left: editorImgLeft,
width: editorImgWidth,
height: editorImgHeight,
} = editorImgDOMRect;
to = {
center: {
x: editorImgLeft + editorImgWidth / 2,
y:
editorImgTop + editorImgHeight / 2 >
window.innerHeight + editorImgHeight / 2
? window.innerHeight + editorImgHeight / 2
: editorImgTop + editorImgHeight / 2 < -editorImgHeight / 2
? -editorImgHeight / 2
: editorImgTop + editorImgHeight / 2,
},
width: editorImgWidth,
height: editorImgHeight,
};
} else {
to = {
center: {
x: from.center.x,
y: window.innerHeight + lightboxImgHeight / 2,
},
width: lightboxImgWidth,
height: lightboxImgHeight,
};
}
const zoomOut = () => {
const final = finalImage.current;
if (!final) {
return keyframes``;
}
const fromTx = from.center.x - final.center.x;
const fromTy = from.center.y - final.center.y;
const toTx = to.center.x - final.center.x;
const toTy = to.center.y - final.center.y;
const fromSx = from.width / final.width;
const fromSy = from.height / final.height;
const toSx = to.width / final.width;
const toSy = to.height / final.height;
return keyframes`
from {
translate: ${fromTx}px ${fromTy}px;
scale: ${fromSx} ${fromSy};
}
to {
translate: ${toTx}px ${toTy}px;
scale: ${toSx} ${toSy};
}
`;
};
animation.current = {
...(animation.current ?? {}),
zoomIn: undefined,
zoomOut: {
apply: zoomOut,
duration: animation.current?.startTime
? Date.now() - animation.current.startTime
: ANIMATION_DURATION,
},
};
}
};
if (!activePos) {
return null;
}
const prev = () => {
if (status.lightbox === LightboxStatus.OPENED) {
if (!activePos) {
return;
}
const prevIndex = currentImageIndex - 1;
if (prevIndex < 0) {
return;
}
onUpdate(imageNodes[prevIndex].pos);
}
};
const next = () => {
if (status.lightbox === LightboxStatus.OPENED) {
if (!activePos) {
return;
}
const nextIndex = currentImageIndex + 1;
if (nextIndex >= imageNodes.length) {
return;
}
onUpdate(imageNodes[nextIndex].pos);
}
};
const close = () => {
if (
status.lightbox === LightboxStatus.OPENING ||
status.lightbox === LightboxStatus.OPENED
) {
setStatus({
lightbox: LightboxStatus.READY_TO_CLOSE,
image: status.image,
});
}
};
const download = () => {
if (currentImageNode && status.lightbox === LightboxStatus.OPENED) {
void downloadImageNode(currentImageNode);
}
};
const handleKeyDown = (ev: React.KeyboardEvent<HTMLDivElement>) => {
ev.preventDefault();
switch (ev.key) {
case "ArrowLeft": {
prev();
break;
}
case "ArrowRight": {
next();
break;
}
case "Escape": {
close();
break;
}
}
};
const handleFadeStart = () => {
if (animation.current?.fadeIn) {
animation.current = {
...(animation.current ?? {}),
startTime: Date.now(),
};
}
};
const handleFadeEnd = () => {
if (animation.current?.fadeIn) {
animation.current = {
...(animation.current ?? {}),
zoomIn: undefined,
fadeIn: undefined,
startTime: undefined,
};
setStatus({
lightbox: LightboxStatus.OPENED,
image: status.image,
});
} else if (animation.current?.fadeOut) {
setStatus({
lightbox: LightboxStatus.CLOSED,
image: null,
});
}
};
if (!currentImageNode) {
return null;
}
const src = sanitizeUrl(currentImageNode.attrs.src) ?? "";
return (
<Dialog.Root open={!!activePos}>
<Dialog.Portal>
<StyledOverlay
ref={overlayRef}
animation={animation.current}
onAnimationStart={handleFadeStart}
onAnimationEnd={handleFadeEnd}
/>
<StyledContent onKeyDown={handleKeyDown}>
<VisuallyHidden.Root>
<Dialog.Title>{t("Lightbox")}</Dialog.Title>
<Dialog.Description>
{t("View, navigate, or download images in the document")}
</Dialog.Description>
</VisuallyHidden.Root>
<Actions animation={animation.current}>
<Tooltip content={t("Copy link")} placement="bottom">
<CopyToClipboard text={imgRef.current?.src ?? ""}>
<Button
tabIndex={-1}
aria-label={t("Copy link")}
size={32}
icon={<LinkIcon />}
borderOnHover
neutral
/>
</CopyToClipboard>
</Tooltip>
<Tooltip content={t("Download")} placement="bottom">
<Button
tabIndex={-1}
onClick={download}
aria-label={t("Download")}
size={32}
icon={<DownloadIcon />}
borderOnHover
neutral
/>
</Tooltip>
<Separator />
<Dialog.Close asChild>
<Tooltip content={t("Close")} shortcut="Esc" placement="bottom">
<Button
tabIndex={-1}
onClick={close}
aria-label={t("Close")}
size={32}
icon={<CloseIcon />}
borderOnHover
neutral
/>
</Tooltip>
</Dialog.Close>
</Actions>
{currentImageIndex > 0 && (
<Nav dir="left" $hidden={isIdle} animation={animation.current}>
<NavButton onClick={prev} size={32} aria-label={t("Previous")}>
<BackIcon size={32} />
</NavButton>
</Nav>
)}
<Image
ref={imgRef}
src={src}
alt={currentImageNode.attrs.alt ?? ""}
onLoading={() =>
setStatus({
lightbox: status.lightbox,
image: ImageStatus.LOADING,
})
}
onLoad={() =>
setStatus({
lightbox: status.lightbox,
image: ImageStatus.LOADED,
})
}
onError={() =>
setStatus({
lightbox: status.lightbox,
image: ImageStatus.ERROR,
})
}
onSwipeRight={prev}
onSwipeLeft={next}
onSwipeUp={close}
onSwipeDown={close}
status={status}
animation={animation.current}
/>
{currentImageIndex < imageNodes.length - 1 && (
<Nav dir="right" $hidden={isIdle} animation={animation.current}>
<NavButton onClick={next} size={32} aria-label={t("Next")}>
<NextIcon size={32} />
</NavButton>
</Nav>
)}
</StyledContent>
</Dialog.Portal>
</Dialog.Root>
);
}
type ImageProps = {
src: string;
alt: string;
onLoading: () => void;
onLoad: () => void;
onError: () => void;
onSwipeRight: () => void;
onSwipeLeft: () => void;
onSwipeUp: () => void;
onSwipeDown: () => void;
status: Status;
animation: Animation | null;
};
const Image = forwardRef<HTMLImageElement, ImageProps>(function _Image(
{
src,
alt,
onLoading,
onLoad,
onError,
onSwipeRight,
onSwipeLeft,
onSwipeUp,
onSwipeDown,
status,
animation,
}: ImageProps,
ref
) {
const { t } = useTranslation();
const swipeHandlers = useSwipe({
onSwipeRight,
onSwipeLeft,
onSwipeUp,
onSwipeDown,
});
const [hidden, setHidden] = useState(
status.image === null || status.image === ImageStatus.LOADING
);
useEffect(() => {
onLoading();
}, [src]);
useEffect(() => {
if (status.image === null || status.image === ImageStatus.LOADING) {
setHidden(true);
} else if (status.image === ImageStatus.LOADED) {
setHidden(false);
}
}, [status.image]);
return status.image === ImageStatus.ERROR ? (
<StyledError animation={animation} {...swipeHandlers}>
<CrossIcon size={16} /> {t("Image failed to load")}
</StyledError>
) : (
<>
{status.image === ImageStatus.LOADING && <LoadingIndicator />}
<Figure>
<StyledImg
ref={ref}
src={src}
alt={alt}
animation={animation}
onAnimationStart={() => setHidden(false)}
{...swipeHandlers}
onError={onError}
onLoad={onLoad}
$hidden={hidden}
/>
<Caption>
{status.image === ImageStatus.LOADED &&
status.lightbox === LightboxStatus.OPENED ? (
<Fade>{alt}</Fade>
) : null}
</Caption>
</Figure>
</>
);
});
const Figure = styled("figure")`
width: 100%;
height: 100%;
margin: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
`;
const Caption = styled("figcaption")`
font-size: 14px;
min-height: 1.5em;
font-weight: normal;
margin-top: 8px;
color: ${s("textSecondary")};
flex-shrink: 0;
`;
const StyledOverlay = styled(Dialog.Overlay)<{
animation: Animation | null;
}>`
position: fixed;
inset: 0;
background-color: ${s("background")};
z-index: ${depths.overlay};
${(props) =>
props.animation === null
? css`
opacity: 0;
`
: props.animation.fadeIn
? css`
animation: ${props.animation.fadeIn.apply()}
${props.animation.fadeIn.duration}ms;
`
: props.animation.fadeOut
? css`
animation: ${props.animation.fadeOut.apply()}
${props.animation.fadeOut.duration}ms;
`
: ""}
`;
const StyledImg = styled.img<{
$hidden: boolean;
animation: Animation | null;
}>`
visibility: ${(props) => (props.$hidden ? "hidden" : "visible")};
max-width: 100%;
min-height: 0;
object-fit: contain;
${(props) =>
props.animation?.zoomIn
? css`
animation: ${props.animation.zoomIn.apply()}
${props.animation.zoomIn.duration}ms;
`
: props.animation?.zoomOut
? css`
animation: ${props.animation.zoomOut.apply()}
${props.animation.zoomOut.duration}ms;
`
: ""}
`;
const StyledContent = styled(Dialog.Content)`
position: fixed;
inset: 0;
z-index: ${depths.modal};
display: flex;
justify-content: center;
align-items: center;
outline: none;
padding: 56px;
`;
const Actions = styled.div<{
animation: Animation | null;
}>`
position: absolute;
top: 0;
right: 0;
margin: 16px 12px;
display: flex;
align-items: center;
gap: 8px;
${(props) =>
props.animation === null
? css`
opacity: 0;
`
: props.animation.fadeIn
? css`
animation: ${props.animation.fadeIn.apply()}
${props.animation.fadeIn.duration}ms;
`
: props.animation.fadeOut
? css`
animation: ${props.animation.fadeOut.apply()}
${props.animation.fadeOut.duration}ms;
`
: ""}
`;
const Nav = styled.div<{
$hidden: boolean;
dir: "left" | "right";
animation: Animation | null;
}>`
position: absolute;
${(props) => (props.dir === "left" ? "left: 0;" : "right: 0;")}
transition: opacity 500ms ease-in-out;
${(props) => props.$hidden && "opacity: 0;"}
${(props) =>
props.animation === null
? css`
opacity: 0;
`
: props.animation.fadeIn
? css`
animation: ${props.animation.fadeIn.apply()}
${props.animation.fadeIn.duration}ms;
`
: props.animation.fadeOut
? css`
animation: ${props.animation.fadeOut.apply()}
${props.animation.fadeOut.duration}ms;
`
: ""}
`;
const StyledError = styled(Error)<{
animation: Animation | null;
}>`
${(props) =>
props.animation === null
? css`
opacity: 0;
`
: props.animation.fadeIn
? css`
animation: ${props.animation.fadeIn.apply()}
${props.animation.fadeIn.duration}ms;
`
: props.animation.fadeOut
? css`
animation: ${props.animation.fadeOut.apply()}
${props.animation.fadeOut.duration}ms;
`
: ""}
`;
const NavButton = styled(NudeButton)`
margin: 16px;
opacity: 0.75;
color: ${s("text")};
outline: none;
${extraArea(12)}
&:hover {
opacity: 1;
}
`;
export default observer(Lightbox);
+1
View File
@@ -231,6 +231,7 @@ const Subtitle = styled.p<{ $small?: boolean; $selected?: boolean }>`
font-size: ${(props) => (props.$small ? 13 : 14)}px;
color: ${s("textTertiary")};
margin-top: -2px;
overflow-wrap: break-word;
`;
export const Actions = styled(Flex)<{ $selected?: boolean }>`
+93
View File
@@ -0,0 +1,93 @@
import * as React from "react";
import { actionV2ToMenuItem } from "~/actions";
import useActionContext from "~/hooks/useActionContext";
import useMobile from "~/hooks/useMobile";
import { ActionV2Variant, ActionV2WithChildren } 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: ActionV2WithChildren;
/** 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(() => {
if (!open) {
return [];
}
return (action.children as ActionV2Variant[]).map((childAction) =>
actionV2ToMenuItem(childAction, actionContext)
);
}, [open, action.children, actionContext]);
const handleOpenChange = React.useCallback(
(open: boolean) => {
if (open) {
onOpen?.();
} else {
onClose?.();
}
},
[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) {
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>
);
}
);
+228
View File
@@ -0,0 +1,228 @@
import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import styled from "styled-components";
import Scrollable from "~/components/Scrollable";
import {
Drawer,
DrawerContent,
DrawerTitle,
DrawerTrigger,
} from "~/components/primitives/Drawer";
import { Menu, MenuContent, MenuTrigger } from "~/components/primitives/Menu";
import { MenuProvider } from "~/components/primitives/Menu/MenuContext";
import { actionV2ToMenuItem } from "~/actions";
import useActionContext from "~/hooks/useActionContext";
import useMobile from "~/hooks/useMobile";
import {
ActionV2Variant,
ActionV2WithChildren,
MenuItem,
MenuItemWithChildren,
} from "~/types";
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;
/** Trigger for the menu */
children: React.ReactNode;
/** Alignment w.r.t trigger - defaults to start */
align?: "start" | "end";
/** ARIA label for the menu */
ariaLabel: string;
/** Additional component to display at the bottom of the top-level menu */
append?: React.ReactNode;
/** Callback when menu is opened */
onOpen?: () => void;
/** Callback when menu is closed */
onClose?: () => void;
// TODO: Invert the dependency chain by forwarding dropdown ref and props to Tooltip component
} & React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Trigger>;
export const DropdownMenu = observer(
React.forwardRef<React.ElementRef<typeof TooltipPrimitive.Trigger>, Props>(
(
{
action,
children,
align = "start",
ariaLabel,
append,
onOpen,
onClose,
...rest
},
ref
) => {
const [open, setOpen] = React.useState(false);
const isMobile = useMobile();
const contentRef =
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)
);
}, [open, action.children, actionContext]);
const handleOpenChange = React.useCallback(
(open: boolean) => {
setOpen(open);
if (open) {
onOpen?.();
} else {
onClose?.();
}
},
[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) {
return (
<MobileDropdown
open={open}
onOpenChange={handleOpenChange}
items={menuItems}
trigger={children}
ariaLabel={ariaLabel}
append={append}
/>
);
}
const content = toMenuItems(menuItems);
return (
<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>
);
}
)
);
type MobileDropdownProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
items: MenuItem[];
trigger: React.ReactNode;
} & Pick<Props, "ariaLabel" | "append">;
function MobileDropdown({
open,
onOpenChange,
items,
trigger,
ariaLabel,
append,
}: MobileDropdownProps) {
const [submenuName, setSubmenuName] = React.useState<string>();
const contentRef = React.useRef<React.ElementRef<typeof DrawerContent>>(null);
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 closeDrawer = React.useCallback(() => {
onOpenChange(false);
setTimeout(() => setSubmenuName(undefined), 500); // needed for a Vaul bug where 'onAnimationEnd' is not called for controlled state.
}, [onOpenChange]);
const resetSubmenu = React.useCallback((isOpen: boolean) => {
if (!isOpen) {
setSubmenuName(undefined);
}
}, []);
const menuItems = React.useMemo(() => {
if (!items.length || !submenuName) {
return items;
}
const submenu = items.find(
(item) =>
item.type === "submenu" && (item.title as string) === submenuName
)! as MenuItemWithChildren;
return submenu.items;
}, [items, submenuName]);
const content = toMobileMenuItems(menuItems, closeDrawer, setSubmenuName);
return (
<Drawer
open={open}
onOpenChange={onOpenChange}
onAnimationEnd={resetSubmenu}
>
<DrawerTrigger aria-label={ariaLabel} asChild>
{trigger}
</DrawerTrigger>
<DrawerContent
ref={contentRef}
aria-label={ariaLabel}
aria-describedby={undefined}
onAnimationStart={disablePointerEvents}
onAnimationEnd={enablePointerEvents}
>
<DrawerTitle>{ariaLabel}</DrawerTitle>
<StyledScrollable hiddenScrollbars>
{content}
{!submenuName ? append : null}
</StyledScrollable>
</DrawerContent>
</Drawer>
);
}
const StyledScrollable = styled(Scrollable)`
max-height: 75vh;
`;

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