Compare commits

...

37 Commits

Author SHA1 Message Date
tommoor 514566ee75 chore: Compressed inefficient images automatically 2025-10-15 00:38:04 +00:00
Tom Moor 908d0408f5 fix: Display fallback instead of error if cannot unfurl URL (#10370)
* fix: Display fallback instead of error if cannot unfurl URL

* Optimised images with calibre/image-actions

* fix: Write loaded to props to attrs

* Optimised images with calibre/image-actions

* white background

* Optimised images with calibre/image-actions

---------

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* feat: wip

fix: favicon

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

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

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

Fixes #10333

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

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

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

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

---------

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

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

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

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

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

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

* Update Link.tsx

---------

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

* fix: cleanup

* fix: edge-to-edge panning

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

* fix: zoom in/out action buttons

* fix: swipe

* fix: bg for action buttons

* fix: image err

* fix: comment

* fix: being explicit

* trigger ci

* Lockfile

* Update app/components/Lightbox.tsx

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

---------

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

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

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


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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-07 16:43:49 -04:00
Tom Moor f6315875b4 fix: CSRF validation issues on Firefox (#10317) 2025-10-06 19:10:25 -04:00
Tom Moor f4e53da1bf fix: Flipped logic in export all (#10305) 2025-10-05 21:35:44 -04:00
122 changed files with 1929 additions and 1262 deletions
+6 -7
View File
@@ -2,7 +2,6 @@ import { LocationDescriptor } from "history";
import flattenDeep from "lodash/flattenDeep";
import { toast } from "sonner";
import { Optional } from "utility-types";
import { v4 as uuidv4 } from "uuid";
import {
Action,
ActionContext,
@@ -46,7 +45,7 @@ export function createAction(definition: Optional<Action, "id">): Action {
return definition.perform?.(context);
}
: undefined,
id: definition.id ?? uuidv4(),
id: definition.id ?? crypto.randomUUID(),
};
}
@@ -202,7 +201,7 @@ export function createActionV2(
return definition.perform(context);
}
: () => {},
id: definition.id ?? uuidv4(),
id: definition.id ?? crypto.randomUUID(),
};
}
@@ -213,7 +212,7 @@ export function createInternalLinkActionV2(
...definition,
type: "action",
variant: "internal_link",
id: definition.id ?? uuidv4(),
id: definition.id ?? crypto.randomUUID(),
};
}
@@ -224,7 +223,7 @@ export function createExternalLinkActionV2(
...definition,
type: "action",
variant: "external_link",
id: definition.id ?? uuidv4(),
id: definition.id ?? crypto.randomUUID(),
};
}
@@ -235,7 +234,7 @@ export function createActionV2WithChildren(
...definition,
type: "action",
variant: "action_with_children",
id: definition.id ?? uuidv4(),
id: definition.id ?? crypto.randomUUID(),
};
}
@@ -252,7 +251,7 @@ export function createRootMenuAction(
actions: (ActionV2Variant | ActionV2Group | TActionV2Separator)[]
): ActionV2WithChildren {
return {
id: uuidv4(),
id: crypto.randomUUID(),
type: "action",
variant: "action_with_children",
name: "root_action",
+24 -17
View File
@@ -3,6 +3,7 @@ import concat from "lodash/concat";
import difference from "lodash/difference";
import fill from "lodash/fill";
import filter from "lodash/filter";
import flatten from "lodash/flatten";
import includes from "lodash/includes";
import map from "lodash/map";
import { observer } from "mobx-react";
@@ -27,7 +28,6 @@ import Text from "~/components/Text";
import useMobile from "~/hooks/useMobile";
import useStores from "~/hooks/useStores";
import { ancestors, descendants, flattenTree } from "~/utils/tree";
import flatten from "lodash/flatten";
type Props = {
/** Action taken upon submission of selected item, could be publish, move etc. */
@@ -49,8 +49,13 @@ function DocumentExplorer({ onSubmit, onSelect, items, defaultValue }: Props) {
const [searchTerm, setSearchTerm] = React.useState<string>();
const [selectedNode, selectNode] = React.useState<NavigationNode | null>(
() => {
const node =
defaultValue && items.find((item) => item.id === defaultValue);
if (!defaultValue) {
return null;
}
// Search through all nodes in the tree, not just top-level items
const allNodes = flatten(items.map(flattenTree));
const node = allNodes.find((item) => item.id === defaultValue);
return node || null;
}
);
@@ -59,7 +64,9 @@ function DocumentExplorer({ onSubmit, onSelect, items, defaultValue }: Props) {
const [activeNode, setActiveNode] = React.useState<number>(0);
const [expandedNodes, setExpandedNodes] = React.useState<string[]>(() => {
if (defaultValue) {
const node = items.find((item) => item.id === defaultValue);
// Search through all nodes in the tree, not just top-level items
const allNodes = flatten(items.map(flattenTree));
const node = allNodes.find((item) => item.id === defaultValue);
if (node) {
return ancestors(node).map((ancestorNode) => ancestorNode.id);
}
@@ -104,19 +111,6 @@ function DocumentExplorer({ onSubmit, onSelect, items, defaultValue }: Props) {
);
}, [items.length]);
React.useEffect(() => {
onSelect(selectedNode);
}, [selectedNode, onSelect]);
React.useEffect(() => {
if (defaultValue && selectedNode && listRef) {
const index = nodes.findIndex((node) => node.id === selectedNode.id);
if (index > 0) {
setTimeout(() => listRef.current?.scrollToItem(index, "center"), 50);
}
}
}, []);
function getNodes() {
function includeDescendants(item: NavigationNode): NavigationNode[] {
return expandedNodes.includes(item.id)
@@ -130,6 +124,19 @@ function DocumentExplorer({ onSubmit, onSelect, items, defaultValue }: Props) {
}
const nodes = getNodes();
React.useEffect(() => {
onSelect(selectedNode);
}, [selectedNode, onSelect]);
React.useEffect(() => {
if (defaultValue && selectedNode && listRef) {
const index = nodes.findIndex((node) => node.id === selectedNode.id);
if (index > 0) {
setTimeout(() => listRef.current?.scrollToItem(index, "center"), 50);
}
}
}, [defaultValue, selectedNode, nodes]);
const baseDepth = nodes.reduce(
(min, node) => (node.depth ? Math.min(min, node.depth) : min),
Infinity
+8 -1
View File
@@ -32,6 +32,7 @@ function EditableTitle(
const [isEditing, setIsEditing] = React.useState(rest.isEditing || false);
const [originalValue, setOriginalValue] = React.useState(title);
const [value, setValue] = React.useState(title);
const [isSubmitting, setIsSubmitting] = React.useState(false);
React.useImperativeHandle(ref, () => ({
setIsEditing,
@@ -65,6 +66,10 @@ function EditableTitle(
ev.preventDefault();
ev.stopPropagation();
if (isSubmitting) {
return;
}
const trimmedValue = value.trim();
if (trimmedValue === originalValue || trimmedValue.length === 0) {
@@ -74,6 +79,7 @@ function EditableTitle(
return;
}
setIsSubmitting(true);
try {
await onSubmit(trimmedValue);
setOriginalValue(trimmedValue);
@@ -82,10 +88,11 @@ function EditableTitle(
toast.error(error.message);
throw error;
} finally {
setIsSubmitting(false);
setIsEditing(false);
}
},
[originalValue, value, onCancel, onSubmit]
[originalValue, value, onCancel, onSubmit, isSubmitting]
);
const handleKeyDown = React.useCallback(
+339 -53
View File
@@ -1,7 +1,17 @@
import { observer } from "mobx-react";
import * as Dialog from "@radix-ui/react-dialog";
import styled, { css, Keyframes, keyframes } from "styled-components";
import { forwardRef, useCallback, useEffect, useRef, useState } from "react";
import {
ComponentProps,
createContext,
forwardRef,
ReactNode,
useCallback,
useContext,
useEffect,
useRef,
useState,
} from "react";
import { isInternalUrl } from "@shared/utils/urls";
import { Error as ImageError } from "@shared/editor/components/Image";
import {
@@ -11,6 +21,8 @@ import {
DownloadIcon,
LinkIcon,
NextIcon,
ZoomInIcon,
ZoomOutIcon,
} from "outline-icons";
import { depths, extraArea, s } from "@shared/styles";
import NudeButton from "./NudeButton";
@@ -28,6 +40,13 @@ import useSwipe from "~/hooks/useSwipe";
import { toast } from "sonner";
import { findIndex } from "lodash";
import { LightboxImage } from "@shared/editor/lib/Lightbox";
import {
TransformWrapper,
TransformComponent,
useTransformEffect,
ReactZoomPanPinchRef,
} from "react-zoom-pan-pinch";
import { transparentize } from "polished";
export enum LightboxStatus {
READY_TO_OPEN,
@@ -42,6 +61,9 @@ export enum ImageStatus {
LOADING,
ERROR,
LOADED,
MIN_ZOOM,
MAX_ZOOM,
ZOOMED,
}
type Status = {
lightbox: LightboxStatus | null;
@@ -69,11 +91,102 @@ type Props = {
onClose: () => void;
};
const ZoomPanPinchContext = createContext({ isImagePanning: false });
type ZoomablePannablePinchableProps = {
children: ReactNode;
panningDisabled: boolean;
disabled: boolean;
};
const ZoomablePannablePinchable = forwardRef<
ReactZoomPanPinchRef,
ZoomablePannablePinchableProps
>(({ children, panningDisabled, disabled }, ref) => {
const { isPanning, ...panningHandlers } = usePanning();
return (
<ZoomPanPinchContext.Provider value={{ isImagePanning: isPanning }}>
<TransformWrapper
ref={ref}
disabled={disabled}
doubleClick={{ disabled: true }}
minScale={1}
maxScale={8}
panning={{
disabled: panningDisabled,
}}
{...panningHandlers}
>
<TransformComponent
wrapperStyle={{ width: "100%", height: "100%" }}
contentStyle={{
width: "100%",
height: "100%",
padding: "56px",
justifyContent: "center",
alignItems: "center",
}}
>
{children}
</TransformComponent>
</TransformWrapper>
</ZoomPanPinchContext.Provider>
);
});
function usePanning() {
const [isPanning, setPanning] = useState(false);
const dragged = useRef(false);
const onPanningStart: ComponentProps<
typeof TransformWrapper
>["onPanningStart"] = (ref, event) => {
if (!(event.target instanceof HTMLImageElement)) {
return;
}
const zoomedIn = ref.state.scale > 1;
if (zoomedIn) {
setPanning(ref.instance.isPanning);
}
};
const onPanning: ComponentProps<
typeof TransformWrapper
>["onPanning"] = () => {
dragged.current = true;
};
const onPanningStop: ComponentProps<
typeof TransformWrapper
>["onPanningStop"] = (ref, event) => {
if (!(event.target instanceof HTMLImageElement)) {
return;
}
setPanning(ref.instance.isPanning);
if (dragged.current) {
dragged.current = false;
} else {
const zoomedOut = Math.abs(ref.state.scale - 1) < 0.001;
if (zoomedOut) {
ref.zoomIn();
} else {
ref.resetTransform();
}
}
};
return {
isPanning,
onPanningStart,
onPanning,
onPanningStop,
};
}
function Lightbox({ images, activeImage, onUpdate, onClose }: Props) {
const isIdle = useIdle(3 * Second.ms);
const { t } = useTranslation();
const imgRef = useRef<HTMLImageElement | null>(null);
const overlayRef = useRef<HTMLDivElement | null>(null);
const contentRef = useRef<HTMLDivElement | null>(null);
const [status, setStatus] = useState<Status>({ lightbox: null, image: null });
const animation = useRef<Animation | null>(null);
const finalImage = useRef<{
@@ -81,6 +194,7 @@ function Lightbox({ images, activeImage, onUpdate, onClose }: Props) {
width: number;
height: number;
} | null>(null);
const zoomPanPinchRef = useRef<ReactZoomPanPinchRef>(null);
const currentImageIndex = findIndex(
images,
@@ -131,6 +245,18 @@ function Lightbox({ images, activeImage, onUpdate, onClose }: Props) {
}
}, [status.image, status.lightbox]);
useEffect(() => {
if (
status.lightbox === LightboxStatus.OPENED &&
status.image === ImageStatus.LOADED
) {
setStatus({
lightbox: LightboxStatus.OPENED,
image: ImageStatus.MIN_ZOOM,
});
}
}, [status.lightbox, status.image]);
useEffect(() => {
if (status.lightbox === LightboxStatus.READY_TO_CLOSE) {
setupFadeOut();
@@ -148,6 +274,15 @@ function Lightbox({ images, activeImage, onUpdate, onClose }: Props) {
}
}, [status.lightbox]);
useEffect(() => {
if (status.image === ImageStatus.MIN_ZOOM) {
// It was observed that focus went to `body` as the zoom out button was disabled
// upon clicking it. This stopped navigating to next/previous image using arrow keys.
// So focusing the content div here to restore the functionality.
contentRef.current?.focus();
}
}, [status.image]);
const rememberImagePosition = () => {
if (imgRef.current) {
const lightboxImgDOMRect = imgRef.current.getBoundingClientRect();
@@ -261,7 +396,13 @@ function Lightbox({ images, activeImage, onUpdate, onClose }: Props) {
};
const setupZoomOut = () => {
if (imgRef.current) {
if (
imgRef.current &&
!(
status.image === ImageStatus.ZOOMED ||
status.image === ImageStatus.MAX_ZOOM
)
) {
// in lightbox
const lightboxImgDOMRect = imgRef.current.getBoundingClientRect();
const {
@@ -356,7 +497,11 @@ function Lightbox({ images, activeImage, onUpdate, onClose }: Props) {
};
const prev = () => {
if (status.lightbox === LightboxStatus.OPENED) {
if (
status.lightbox === LightboxStatus.OPENED &&
(status.image === ImageStatus.MIN_ZOOM ||
status.image === ImageStatus.ERROR)
) {
const prevIndex = currentImageIndex - 1;
if (prevIndex < 0) {
return;
@@ -366,7 +511,11 @@ function Lightbox({ images, activeImage, onUpdate, onClose }: Props) {
};
const next = () => {
if (status.lightbox === LightboxStatus.OPENED) {
if (
status.lightbox === LightboxStatus.OPENED &&
(status.image === ImageStatus.MIN_ZOOM ||
status.image === ImageStatus.ERROR)
) {
const nextIndex = currentImageIndex + 1;
if (nextIndex >= images.length) {
return;
@@ -500,7 +649,7 @@ function Lightbox({ images, activeImage, onUpdate, onClose }: Props) {
onAnimationStart={handleFadeStart}
onAnimationEnd={handleFadeEnd}
/>
<StyledContent onKeyDown={handleKeyDown}>
<StyledContent onKeyDown={handleKeyDown} ref={contentRef}>
<VisuallyHidden.Root>
<Dialog.Title>{t("Lightbox")}</Dialog.Title>
<Dialog.Description>
@@ -508,10 +657,52 @@ function Lightbox({ images, activeImage, onUpdate, onClose }: Props) {
</Dialog.Description>
</VisuallyHidden.Root>
<Actions animation={animation.current}>
<Tooltip content={t("Zoom in")} placement="bottom">
<ActionButton
tabIndex={-1}
disabled={
status.image === ImageStatus.MAX_ZOOM ||
status.image === ImageStatus.ERROR
}
onClick={() => {
if (zoomPanPinchRef.current) {
zoomPanPinchRef.current.zoomIn();
}
}}
aria-label={t("Zoom in")}
size={32}
icon={<ZoomInIcon />}
borderOnHover
neutral
/>
</Tooltip>
<Tooltip content={t("Zoom out")} placement="bottom">
<ActionButton
tabIndex={-1}
disabled={
!(
status.image === ImageStatus.ZOOMED ||
status.image === ImageStatus.MAX_ZOOM
)
}
onClick={() => {
if (zoomPanPinchRef.current) {
zoomPanPinchRef.current.zoomOut();
}
}}
aria-label={t("Zoom out")}
size={32}
icon={<ZoomOutIcon />}
borderOnHover
neutral
/>
</Tooltip>
<Separator />
<Tooltip content={t("Copy link")} placement="bottom">
<CopyToClipboard text={imgRef.current?.src ?? ""}>
<Button
<ActionButton
tabIndex={-1}
disabled={status.image === ImageStatus.ERROR}
aria-label={t("Copy link")}
size={32}
icon={<LinkIcon />}
@@ -521,8 +712,9 @@ function Lightbox({ images, activeImage, onUpdate, onClose }: Props) {
</CopyToClipboard>
</Tooltip>
<Tooltip content={t("Download")} placement="bottom">
<Button
<ActionButton
tabIndex={-1}
disabled={status.image === ImageStatus.ERROR}
onClick={download}
aria-label={t("Download")}
size={32}
@@ -534,7 +726,7 @@ function Lightbox({ images, activeImage, onUpdate, onClose }: Props) {
<Separator />
<Dialog.Close asChild>
<Tooltip content={t("Close")} shortcut="Esc" placement="bottom">
<Button
<ActionButton
tabIndex={-1}
onClick={close}
aria-label={t("Close")}
@@ -546,49 +738,86 @@ function Lightbox({ images, activeImage, onUpdate, onClose }: Props) {
</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={activeImage.getSrc()}
alt={activeImage.getAlt()}
onLoading={() =>
setStatus({
lightbox: status.lightbox,
image: ImageStatus.LOADING,
})
{currentImageIndex > 0 &&
!(
status.image === ImageStatus.ZOOMED ||
status.image === ImageStatus.MAX_ZOOM
) && (
<Nav dir="left" $hidden={isIdle} animation={animation.current}>
<NavButton onClick={prev} size={32} aria-label={t("Previous")}>
<BackIcon size={32} />
</NavButton>
</Nav>
)}
<ZoomablePannablePinchable
panningDisabled={
!(
status.image === ImageStatus.ZOOMED ||
status.image === ImageStatus.MAX_ZOOM
)
}
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 < images.length - 1 && (
<Nav dir="right" $hidden={isIdle} animation={animation.current}>
<NavButton onClick={next} size={32} aria-label={t("Next")}>
<NextIcon size={32} />
</NavButton>
</Nav>
)}
disabled={status.image === ImageStatus.ERROR}
ref={zoomPanPinchRef}
>
<Image
ref={imgRef}
src={activeImage.getSrc()}
alt={activeImage.getAlt()}
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}
onMinZoom={() => {
setStatus({
lightbox: status.lightbox,
image: ImageStatus.MIN_ZOOM,
});
}}
onZoom={() =>
setStatus({
lightbox: status.lightbox,
image: ImageStatus.ZOOMED,
})
}
onMaxZoom={() =>
setStatus({
lightbox: status.lightbox,
image: ImageStatus.MAX_ZOOM,
})
}
/>
</ZoomablePannablePinchable>
{currentImageIndex < images.length - 1 &&
!(
status.image === ImageStatus.ZOOMED ||
status.image === ImageStatus.MAX_ZOOM
) && (
<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>
@@ -607,6 +836,9 @@ type ImageProps = {
onSwipeDown: () => void;
status: Status;
animation: Animation | null;
onMinZoom: () => void;
onZoom: () => void;
onMaxZoom: () => void;
};
const Image = forwardRef<HTMLImageElement, ImageProps>(function _Image(
@@ -622,6 +854,9 @@ const Image = forwardRef<HTMLImageElement, ImageProps>(function _Image(
onSwipeDown,
status,
animation,
onMinZoom,
onZoom,
onMaxZoom,
}: ImageProps,
ref
) {
@@ -634,6 +869,25 @@ const Image = forwardRef<HTMLImageElement, ImageProps>(function _Image(
onSwipeDown,
});
const { isImagePanning } = useContext(ZoomPanPinchContext);
useTransformEffect(({ state, instance }) => {
const minScale = instance.props.minScale ?? 1;
const maxScale = instance.props.maxScale ?? 8;
const { scale } = state;
if (scale === minScale && status.image === ImageStatus.ZOOMED) {
onMinZoom();
} else if (scale === maxScale && status.image === ImageStatus.ZOOMED) {
onMaxZoom();
} else if (
scale > minScale &&
scale < maxScale &&
status.image !== ImageStatus.ZOOMED
) {
onZoom();
}
});
const [hidden, setHidden] = useState(
status.image === null || status.image === ImageStatus.LOADING
);
@@ -668,9 +922,15 @@ const Image = forwardRef<HTMLImageElement, ImageProps>(function _Image(
onError={onError}
onLoad={onLoad}
$hidden={hidden}
$zoomedIn={
status.image === ImageStatus.ZOOMED ||
status.image === ImageStatus.MAX_ZOOM
}
$zoomedOut={status.image === ImageStatus.MIN_ZOOM}
$panning={isImagePanning}
/>
<Caption>
{status.image === ImageStatus.LOADED &&
{status.image === ImageStatus.MIN_ZOOM &&
status.lightbox === LightboxStatus.OPENED ? (
<Fade>{alt}</Fade>
) : null}
@@ -726,12 +986,25 @@ const StyledOverlay = styled(Dialog.Overlay)<{
const StyledImg = styled.img<{
$hidden: boolean;
$zoomedIn: boolean;
$zoomedOut: boolean;
$panning: boolean;
animation: Animation | null;
}>`
visibility: ${(props) => (props.$hidden ? "hidden" : "visible")};
pointer-events: auto !important;
max-width: 100%;
min-height: 0;
object-fit: contain;
cursor: ${(props) =>
props.$panning
? "grab"
: props.$zoomedOut
? "zoom-in"
: props.$zoomedIn
? "zoom-out"
: "default"};
${(props) =>
props.animation?.zoomIn
? css`
@@ -743,7 +1016,12 @@ const StyledImg = styled.img<{
animation: ${props.animation.zoomOut.apply()}
${props.animation.zoomOut.duration}ms;
`
: ""}
: props.animation?.fadeOut
? css`
animation: ${props.animation.fadeOut.apply()}
${props.animation.fadeOut.duration}ms;
`
: ""}
`;
const StyledContent = styled(Dialog.Content)`
@@ -754,7 +1032,10 @@ const StyledContent = styled(Dialog.Content)`
justify-content: center;
align-items: center;
outline: none;
padding: 56px;
`;
const ActionButton = styled(Button)`
background: transparent;
`;
const Actions = styled.div<{
@@ -767,6 +1048,10 @@ const Actions = styled.div<{
display: flex;
align-items: center;
gap: 8px;
z-index: ${depths.modal};
background: ${(props) => transparentize(0.2, props.theme.background)};
backdrop-filter: blur(4px);
border-radius: 6px;
${(props) =>
props.animation === null
@@ -794,6 +1079,7 @@ const Nav = styled.div<{
position: absolute;
${(props) => (props.dir === "left" ? "left: 0;" : "right: 0;")}
transition: opacity 500ms ease-in-out;
z-index: ${depths.modal};
${(props) => props.$hidden && "opacity: 0;"}
${(props) =>
props.animation === null
+3 -1
View File
@@ -146,7 +146,8 @@ const StyledSidebar = styled(Sidebar)<{ $hoverTransition: boolean }>`
${({ $hoverTransition }) =>
$hoverTransition &&
`
&: ${hover} {
@media (hover: hover) {
&:${hover} {
${StyledSearchPopover} {
width: 85%;
}
@@ -154,6 +155,7 @@ const StyledSidebar = styled(Sidebar)<{ $hoverTransition: boolean }>`
${ToggleWrapper} {
opacity: 1;
transform: translateX(0);
}
}
}
`}
+15 -9
View File
@@ -48,11 +48,7 @@ function usePosition({
const { view } = useEditor();
const { selection } = view.state;
const menuWidth = menuRef.current?.offsetWidth ?? 0;
const menuHeight = menuRef.current?.offsetHeight ?? 0;
if (!active || !menuRef.current) {
return defaultPosition;
}
const menuHeight = 36;
// based on the start and end of the selection calculate the position at
// the center top
@@ -74,7 +70,7 @@ function usePosition({
right: Math.max(fromPos.right, toPos.right),
};
const offsetParent = menuRef.current.offsetParent
const offsetParent = menuRef.current?.offsetParent
? menuRef.current.offsetParent.getBoundingClientRect()
: ({
width: window.innerWidth,
@@ -99,12 +95,16 @@ function usePosition({
if (position !== null) {
const element = view.nodeDOM(position);
const bounds = (element as HTMLElement).getBoundingClientRect();
selectionBounds.top = bounds.top;
selectionBounds.top = bounds.top + menuHeight;
selectionBounds.left = bounds.right;
selectionBounds.right = bounds.right;
}
}
if (!active || !menuRef.current || !menuHeight) {
return defaultPosition;
}
// tables are an oddity, and need their own positioning logic
const isColSelection =
selection instanceof ColumnSelection && selection.isColSelection();
@@ -166,6 +166,8 @@ function usePosition({
top: Math.round(top - menuHeight - offsetParent.top),
offset: 0,
visible: true,
blockSelection: false,
maxWidth: width,
};
}
}
@@ -210,8 +212,12 @@ function usePosition({
top: Math.round(top - offsetParent.top),
offset: Math.round(offset),
maxWidth: Math.min(window.innerWidth, offsetParent.width) - margin * 2,
blockSelection:
codeBlock || isColSelection || isRowSelection || noticeBlock,
blockSelection: !!(
codeBlock ||
isColSelection ||
isRowSelection ||
noticeBlock
),
visible: true,
};
}
+5 -6
View File
@@ -5,7 +5,6 @@ import { useState, useCallback, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useLocation } from "react-router-dom";
import { toast } from "sonner";
import { v4 } from "uuid";
import Icon from "@shared/components/Icon";
import { MenuItem } from "@shared/editor/types";
import { MentionType } from "@shared/types";
@@ -92,7 +91,7 @@ function MentionMenu({ search, isActive, ...rest }: Props) {
section: UserSection,
appendSpace: true,
attrs: {
id: v4(),
id: crypto.randomUUID(),
type: MentionType.User,
modelId: user.id,
actorId,
@@ -124,7 +123,7 @@ function MentionMenu({ search, isActive, ...rest }: Props) {
section: DocumentsSection,
appendSpace: true,
attrs: {
id: v4(),
id: crypto.randomUUID(),
type: MentionType.Document,
modelId: doc.id,
actorId,
@@ -152,7 +151,7 @@ function MentionMenu({ search, isActive, ...rest }: Props) {
section: CollectionsSection,
appendSpace: true,
attrs: {
id: v4(),
id: crypto.randomUUID(),
type: MentionType.Collection,
modelId: collection.id,
actorId,
@@ -172,9 +171,9 @@ function MentionMenu({ search, isActive, ...rest }: Props) {
priority: -1,
appendSpace: true,
attrs: {
id: v4(),
id: crypto.randomUUID(),
type: MentionType.Document,
modelId: v4(),
modelId: crypto.randomUUID(),
actorId,
label: search,
},
+3 -4
View File
@@ -2,7 +2,6 @@ import { observer } from "mobx-react";
import { EmailIcon, LinkIcon } from "outline-icons";
import React from "react";
import { useTranslation } from "react-i18next";
import { v4 } from "uuid";
import { EmbedDescriptor } from "@shared/editor/embeds";
import { MenuItem } from "@shared/editor/types";
import { MentionType } from "@shared/types";
@@ -82,7 +81,7 @@ function useItems({
mentionType = integration
? determineMentionType({ url, integration })
: undefined;
: MentionType.URL;
}
return [
@@ -97,11 +96,11 @@ function useItems({
icon: <EmailIcon />,
visible: !!mentionType,
attrs: {
id: v4(),
id: crypto.randomUUID(),
type: mentionType,
label: pastedText,
href: pastedText,
modelId: v4(),
modelId: crypto.randomUUID(),
actorId: user?.id,
},
appendSpace: true,
@@ -3,7 +3,11 @@ import * as React from "react";
import scrollIntoView from "scroll-into-view-if-needed";
import styled from "styled-components";
import { usePortalContext } from "~/components/Portal";
import { MenuButton, MenuLabel } from "~/components/primitives/components/Menu";
import {
MenuButton,
MenuIconWrapper,
MenuLabel,
} from "~/components/primitives/components/Menu";
export type Props = {
/** Whether the item is selected */
@@ -60,7 +64,7 @@ function SuggestionsMenuItem({
onPointerMove={disabled ? undefined : onPointerMove}
$active={selected}
>
{icon}
<MenuIconWrapper>{icon}</MenuIconWrapper>
<MenuLabel>
{title}
{subtitle && (
+2 -3
View File
@@ -8,7 +8,6 @@ import {
TextSelection,
} from "prosemirror-state";
import { Decoration, DecorationSet } from "prosemirror-view";
import { v4 } from "uuid";
import Extension, { WidgetProps } from "@shared/editor/lib/Extension";
import { codeLanguages } from "@shared/editor/lib/code";
import isMarkdown from "@shared/editor/lib/isMarkdown";
@@ -144,7 +143,7 @@ export default class PasteHandler extends Extension {
type: MentionType.Document,
modelId: document.id,
label: document.titleWithDefault,
id: v4(),
id: crypto.randomUUID(),
})
)
);
@@ -189,7 +188,7 @@ export default class PasteHandler extends Extension {
type: MentionType.Collection,
modelId: collection.id,
label: collection.name,
id: v4(),
id: crypto.randomUUID(),
})
)
);
+1 -1
View File
@@ -158,7 +158,7 @@ const useSettingsConfig = () => {
path: settingsPath("templates"),
component: Templates.Component,
preload: Templates.preload,
enabled: can.readTemplate,
enabled: can.updateTemplate,
group: t("Workspace"),
icon: ShapesIcon,
},
+18 -9
View File
@@ -26,13 +26,22 @@ export default function useSwipe({
touchYEnd.current = undefined;
};
const onTouchStart = (e: React.TouchEvent<HTMLImageElement>) => {
touchXStart.current = e.changedTouches[0].screenX;
touchYStart.current = e.changedTouches[0].screenY;
const onTouchStartCapture = (e: React.TouchEvent<HTMLImageElement>) => {
if (e.touches.length === 1) {
// Stop propagation only for single touch gestures, otherwise it prevents
// multi-touch gestures like pinch to zoom to take effect
e.stopPropagation();
touchXStart.current = e.changedTouches[0].screenX;
touchYStart.current = e.changedTouches[0].screenY;
}
};
const onTouchMove = (e: React.TouchEvent<HTMLImageElement>) => {
if (isNumber(touchXStart.current) && isNumber(touchYStart.current)) {
const onTouchMoveCapture = (e: React.TouchEvent<HTMLImageElement>) => {
if (
isNumber(touchXStart.current) &&
isNumber(touchYStart.current) &&
e.touches.length === 1
) {
touchXEnd.current = e.changedTouches[0].screenX;
touchYEnd.current = e.changedTouches[0].screenY;
const dx = touchXEnd.current - touchXStart.current;
@@ -64,13 +73,13 @@ export default function useSwipe({
}
};
const onTouchCancel = () => {
const onTouchCancelCapture = () => {
resetTouchPoints();
};
return {
onTouchStart,
onTouchMove,
onTouchCancel,
onTouchStartCapture,
onTouchMoveCapture,
onTouchCancelCapture,
};
}
@@ -7,7 +7,6 @@ import * as React from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { useTheme } from "styled-components";
import { v4 as uuidv4 } from "uuid";
import { ProsemirrorData } from "@shared/types";
import { getEventFiles } from "@shared/utils/files";
import { AttachmentValidation, CommentValidation } from "@shared/validations";
@@ -107,6 +106,7 @@ function CommentForm({
setForceRender((s) => ++s);
setInputFocused(false);
const commentDraft = draft;
const comment =
thread ??
new Comment(
@@ -126,6 +126,9 @@ function CommentForm({
})
.then(() => onSubmit?.())
.catch(() => {
onSaveDraft(commentDraft);
setForceRender((s) => ++s);
comment.isNew = true;
toast.error(t("Error creating comment"));
});
@@ -142,6 +145,7 @@ function CommentForm({
return;
}
const commentDraft = draft;
onSaveDraft(undefined);
setForceRender((s) => ++s);
@@ -156,13 +160,16 @@ function CommentForm({
comments
);
comment.id = uuidv4();
comment.id = crypto.randomUUID();
comments.add(comment);
comment
.save()
.then(() => onSubmit?.())
.catch(() => {
onSaveDraft(commentDraft);
setForceRender((s) => ++s);
comments.remove(comment.id);
comment.isNew = true;
toast.error(t("Error creating comment"));
@@ -1,5 +1,5 @@
import { differenceInMilliseconds } from "date-fns";
import { action } from "mobx";
import { runInAction } from "mobx";
import { observer } from "mobx-react";
import { DoneIcon } from "outline-icons";
import { darken } from "polished";
@@ -179,18 +179,18 @@ function CommentThreadItem({
);
}, []);
const handleSubmit = action(async (event: React.FormEvent) => {
const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();
try {
handleSetReadOnly();
comment.data = data;
runInAction(() => (comment.data = data));
await comment.save();
} catch (_err) {
setEditing();
toast.error(t("Error updating comment"));
}
});
};
const handleCancel = () => {
setData(comment.data);
@@ -7,6 +7,7 @@ import {
AuthenticationFailed,
AuthorizationFailed,
DocumentTooLarge,
EditorUpdateError,
TooManyConnections,
} from "@shared/collaboration/CloseEvents";
import Fade from "~/components/Fade";
@@ -37,6 +38,10 @@ function ConnectionStatus() {
title: t("Too many users connected to document"),
body: t("Your edits will sync once other users leave the document"),
},
[EditorUpdateError.code]: {
title: t("New version available"),
body: t("Please reload the page to update to the latest version"),
},
};
const message = ui.multiplayerErrorCode
@@ -63,20 +68,29 @@ function ConnectionStatus() {
}
placement="bottom"
>
<Button>
<Fade>
<Fade>
<Button width="auto">
{message?.title ?? t("Offline")}
<DisconnectedIcon />
</Fade>
</Button>
</Button>
</Fade>
</Tooltip>
) : null;
}
const Button = styled(NudeButton)`
display: none;
background: ${(props) => props.theme.backgroundTertiary};
color: ${(props) => props.theme.textSecondary};
font-size: 14px;
font-weight: 500;
padding-left: 6px;
padding-right: 6px;
${breakpoint("tablet")`
display: block;
display: flex;
gap: 4px;
align-items: center;
`};
@media print {
+1 -1
View File
@@ -10,9 +10,9 @@ type Props = {
export const Footer = ({ document }: Props) => (
<FooterWrapper>
<KeyboardShortcutsButton />
<ConnectionStatus />
<SizeWarning document={document} />
<KeyboardShortcutsButton />
</FooterWrapper>
);
@@ -57,6 +57,7 @@ function MultiplayerEditor({ onSynced, ...props }: Props, ref: any) {
const { t } = useTranslation();
const currentUser = useCurrentUser();
const { presence, auth, ui } = useStores();
const [editorVersionBehind, setEditorVersionBehind] = useState(false);
const [showCursorNames, setShowCursorNames] = useState(false);
const [remoteProvider, setRemoteProvider] =
useState<HocuspocusProvider | null>(null);
@@ -161,7 +162,7 @@ function MultiplayerEditor({ onSynced, ...props }: Props, ref: any) {
ui.setMultiplayerStatus("disconnected", ev.event.code);
if (ev.event.code === EditorUpdateError.code) {
window.location.reload();
setEditorVersionBehind(true);
}
}
});
@@ -309,6 +310,7 @@ function MultiplayerEditor({ onSynced, ...props }: Props, ref: any) {
)}
<Editor
{...props}
readOnly={props.readOnly || editorVersionBehind}
value={undefined}
defaultValue={undefined}
extensions={extensions}
+1 -2
View File
@@ -6,7 +6,6 @@ import { useHistory, useLocation, useRouteMatch } from "react-router-dom";
import { Waypoint } from "react-waypoint";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import { v4 as uuidv4 } from "uuid";
import { Pagination } from "@shared/constants";
import { hideScrollbars } from "@shared/styles";
import {
@@ -105,7 +104,7 @@ function Search() {
// without a flash of loading.
if (query) {
searches.add({
id: uuidv4(),
id: crypto.randomUUID(),
query,
createdAt: new Date().toISOString(),
});
@@ -43,11 +43,13 @@ export function MembersTable({ canManage, ...rest }: Props) {
<Flex align="center" gap={8}>
<Avatar model={user} size={AvatarSize.Large} />{" "}
<Flex column>
<Text>
<Text selectable>
{user.name} {currentUser.id === user.id && `(${t("You")})`}
</Text>
{isMobile && canManage && (
<Text type="tertiary">{user.email}</Text>
<Text type="tertiary" selectable>
{user.email}
</Text>
)}
</Flex>
</Flex>
+1 -2
View File
@@ -1,6 +1,5 @@
import { observable, action } from "mobx";
import * as React from "react";
import { v4 as uuidv4 } from "uuid";
type DialogDefinition = {
title: string;
@@ -66,7 +65,7 @@ export default class DialogsStore {
this.modalStack.clear();
}
this.modalStack.set(id ?? replaceId ?? uuidv4(), {
this.modalStack.set(id ?? replaceId ?? crypto.randomUUID(), {
title,
content,
style,
+17 -17
View File
@@ -51,11 +51,11 @@
"> 0.25%, not dead"
],
"dependencies": {
"@aws-sdk/client-s3": "3.896.0",
"@aws-sdk/lib-storage": "3.896.0",
"@aws-sdk/s3-presigned-post": "3.896.0",
"@aws-sdk/s3-request-presigner": "3.896.0",
"@aws-sdk/signature-v4-crt": "^3.896.0",
"@aws-sdk/client-s3": "3.908.0",
"@aws-sdk/lib-storage": "3.908.0",
"@aws-sdk/s3-presigned-post": "3.908.0",
"@aws-sdk/s3-request-presigner": "3.908.0",
"@aws-sdk/signature-v4-crt": "^3.908.0",
"@babel/core": "^7.28.4",
"@babel/plugin-proposal-decorators": "^7.28.0",
"@babel/plugin-transform-class-properties": "^7.27.1",
@@ -65,7 +65,7 @@
"@babel/preset-react": "^7.27.1",
"@benrbray/prosemirror-math": "^0.2.2",
"@bull-board/api": "^6.7.10",
"@bull-board/koa": "^6.12.0",
"@bull-board/koa": "^6.13.0",
"@css-inline/css-inline-wasm": "^0.17.0",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^6.0.1",
@@ -73,9 +73,9 @@
"@dotenvx/dotenvx": "^1.49.0",
"@emoji-mart/data": "^1.2.1",
"@fast-csv/parse": "^5.0.5",
"@fortawesome/fontawesome-svg-core": "^7.0.1",
"@fortawesome/free-brands-svg-icons": "^7.0.1",
"@fortawesome/free-solid-svg-icons": "^7.0.1",
"@fortawesome/fontawesome-svg-core": "^7.1.0",
"@fortawesome/free-brands-svg-icons": "^7.1.0",
"@fortawesome/free-solid-svg-icons": "^7.1.0",
"@fortawesome/react-fontawesome": "^0.2.6",
"@getoutline/react-roving-tabindex": "^3.2.4",
"@hocuspocus/extension-redis": "1.1.2",
@@ -133,14 +133,14 @@
"diff": "^5.2.0",
"email-providers": "^1.14.0",
"emoji-mart": "^5.6.0",
"emoji-regex": "^10.5.0",
"emoji-regex": "^10.6.0",
"es6-error": "^4.1.1",
"fast-deep-equal": "^3.1.3",
"fetch-retry": "^5.0.6",
"form-data": "^4.0.4",
"fractional-index": "^1.0.0",
"framer-motion": "^4.1.17",
"fs-extra": "^11.3.1",
"fs-extra": "^11.3.2",
"fuzzy-search": "^3.2.1",
"glob": "^8.1.0",
"http-errors": "2.0.0",
@@ -179,9 +179,9 @@
"mobx-utils": "^4.0.1",
"natural-sort": "^1.0.0",
"node-fetch": "2.7.0",
"nodemailer": "^6.10.1",
"nodemailer": "^7.0.7",
"octokit": "^3.2.2",
"outline-icons": "^3.12.1",
"outline-icons": "^3.13.0",
"oy-vey": "^0.12.1",
"passport": "^0.7.0",
"passport-google-oauth2": "^0.2.0",
@@ -228,6 +228,7 @@
"react-virtualized-auto-sizer": "^1.0.26",
"react-waypoint": "^10.3.0",
"react-window": "^1.8.11",
"react-zoom-pan-pinch": "^3.7.0",
"redlock": "^5.0.0-beta.2",
"reflect-metadata": "^0.2.2",
"refractor": "^3.6.0",
@@ -261,7 +262,6 @@
"ukkonen": "^2.2.0",
"umzug": "^3.8.2",
"utility-types": "^3.11.0",
"uuid": "^8.3.2",
"validator": "13.15.15",
"vaul": "^1.1.2",
"vite": "npm:rolldown-vite@latest",
@@ -312,7 +312,7 @@
"@types/markdown-it-emoji": "^3.0.1",
"@types/mime-types": "^3.0.1",
"@types/natural-sort": "^0.0.24",
"@types/node": "20.17.30",
"@types/node": "20.19.21",
"@types/node-fetch": "^2.6.9",
"@types/nodemailer": "^6.4.17",
"@types/passport-oauth2": "^1.8.0",
@@ -342,7 +342,7 @@
"@types/tmp": "^0.2.6",
"@types/turndown": "^5.0.5",
"@types/utf8": "^3.0.3",
"@types/validator": "^13.15.2",
"@types/validator": "^13.15.3",
"@types/yauzl": "^2.10.3",
"babel-jest": "^29.7.0",
"babel-plugin-transform-inline-environment-variables": "^0.4.4",
@@ -365,7 +365,7 @@
"prettier": "^3.6.2",
"react-refresh": "^0.17.0",
"rimraf": "^2.5.4",
"rollup-plugin-webpack-stats": "2.1.3",
"rollup-plugin-webpack-stats": "2.1.6",
"terser": "^5.43.1",
"typescript": "^5.9.2",
"yarn-deduplicate": "^6.0.2"
+2 -2
View File
@@ -9,7 +9,7 @@ class Iframely {
public static async requestResource(
url: string,
type = "oembed"
type = "iframely"
): Promise<JSONObject | UnfurlError> {
const isDefaultHost = env.IFRAMELY_URL === this.defaultUrl;
@@ -38,7 +38,7 @@ class Iframely {
const data = await Iframely.requestResource(url);
return "error" in data // In addition to our custom UnfurlError, sometimes iframely returns error in the response body.
? ({ error: data.error } as UnfurlError)
: { ...data, type: UnfurlResourceType.OEmbed };
: { ...data, type: UnfurlResourceType.URL };
};
}
@@ -1,4 +1,3 @@
import { v4 as uuidv4 } from "uuid";
import { IntegrationService, IntegrationType } from "@shared/types";
import { Integration } from "@server/models";
import { Buckets } from "@server/models/helpers/AttachmentHelper";
@@ -27,7 +26,7 @@ export default class UploadLinearWorkspaceLogoTask extends BaseTask<Props> {
const res = await FileStorage.storeFromUrl(
props.logoUrl,
`${Buckets.avatars}/${integration.teamId}/${uuidv4()}`,
`${Buckets.avatars}/${integration.teamId}/${crypto.randomUUID()}`,
"public-read",
{
headers: {
+5 -6
View File
@@ -3,7 +3,6 @@ import { readFile } from "fs/promises";
import path from "path";
import FormData from "form-data";
import { ensureDirSync } from "fs-extra";
import { v4 as uuidV4 } from "uuid";
import { FileOperationState, FileOperationType } from "@shared/types";
import env from "@server/env";
import { Buckets } from "@server/models/helpers/AttachmentHelper";
@@ -135,7 +134,7 @@ describe("#files.get", () => {
it("should fail with status 404 if existing file is requested with key", async () => {
const user = await buildUser();
const fileName = "images.docx";
const key = path.join("uploads", user.id, uuidV4(), fileName);
const key = path.join("uploads", user.id, crypto.randomUUID(), fileName);
ensureDirSync(
path.dirname(path.join(env.FILE_STORAGE_LOCAL_ROOT_DIR, key))
@@ -153,7 +152,7 @@ describe("#files.get", () => {
it("should fail with status 404 if non-existing file is requested with key", async () => {
const user = await buildUser();
const fileName = "images.docx";
const key = path.join("uploads", user.id, uuidV4(), fileName);
const key = path.join("uploads", user.id, crypto.randomUUID(), fileName);
const res = await server.get(`/api/files.get?key=${key}`);
expect(res.status).toEqual(404);
});
@@ -279,7 +278,7 @@ describe("#files.get", () => {
it("should succeed with status 200 ok when avatar is requested using key", async () => {
const user = await buildUser();
const key = path.join("avatars", user.id, uuidV4());
const key = path.join("avatars", user.id, crypto.randomUUID());
const attachment = await buildAttachment({
key,
teamId: user.teamId,
@@ -308,7 +307,7 @@ describe("#files.get", () => {
it("should succeed with status 200 ok when avatar is requested using key", async () => {
const user = await buildUser();
const key = path.join("avatars", user.id, uuidV4());
const key = path.join("avatars", user.id, crypto.randomUUID());
await buildAttachment({
key,
teamId: user.teamId,
@@ -335,7 +334,7 @@ describe("#files.get", () => {
it("should succeed with status 200 ok when exported file is requested using signature", async () => {
const user = await buildUser();
const fileName = "export-markdown.zip";
const key = `${Buckets.uploads}/${user.teamId}/${uuidV4()}/${fileName}`;
const key = `${Buckets.uploads}/${user.teamId}/${crypto.randomUUID()}/${fileName}`;
await buildFileOperation({
userId: user.id,
@@ -1,5 +1,4 @@
import fetchMock from "jest-fetch-mock";
import { v4 as uuidv4 } from "uuid";
import { WebhookDelivery } from "@server/models";
import {
buildUser,
@@ -99,7 +98,7 @@ describe("DeliverWebhookTask", () => {
url: "http://example.com",
events: ["*"],
});
const deletedUserId = uuidv4();
const deletedUserId = crypto.randomUUID();
const signedInUser = await buildUser({ teamId: subscription.teamId });
const task = new DeliverWebhookTask();
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 977 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 965 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 693 B

After

Width:  |  Height:  |  Size: 638 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 657 B

After

Width:  |  Height:  |  Size: 619 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 252 B

After

Width:  |  Height:  |  Size: 239 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 828 B

After

Width:  |  Height:  |  Size: 764 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1013 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 988 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 631 B

After

Width:  |  Height:  |  Size: 513 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1010 B

After

Width:  |  Height:  |  Size: 803 B

@@ -94,8 +94,10 @@ export default class PersistenceExtension implements Extension {
context,
documentName,
clientsCount,
requestParameters,
}: onStoreDocumentPayload) {
const [, documentId] = documentName.split(".");
const clientVersion = requestParameters.get("editorVersion");
const key = Document.getCollaboratorKey(documentId);
const sessionCollaboratorIds = await Redis.defaultClient.smembers(key);
@@ -110,6 +112,7 @@ export default class PersistenceExtension implements Extension {
ydoc: document,
sessionCollaboratorIds,
isLastConnection: clientsCount === 0,
clientVersion,
});
} catch (err) {
Logger.error("Unable to persist document", err, {
+9 -9
View File
@@ -1,5 +1,5 @@
import { faker } from "@faker-js/faker";
import { v4 as uuidv4 } from "uuid";
import { randomUUID } from "crypto";
import WelcomeEmail from "@server/emails/templates/WelcomeEmail";
import { TeamDomain } from "@server/models";
import Collection from "@server/models/Collection";
@@ -35,7 +35,7 @@ describe("accountProvisioner", () => {
providerId: faker.internet.domainName(),
},
authentication: {
providerId: uuidv4(),
providerId: randomUUID(),
accessToken: "123",
scopes: ["read"],
},
@@ -137,7 +137,7 @@ describe("accountProvisioner", () => {
providerId: authenticationProvider.providerId,
},
authentication: {
providerId: uuidv4(),
providerId: randomUUID(),
accessToken: "123",
scopes: ["read"],
},
@@ -271,7 +271,7 @@ describe("accountProvisioner", () => {
providerId: authenticationProvider.providerId,
},
authentication: {
providerId: uuidv4(),
providerId: randomUUID(),
accessToken: "123",
scopes: ["read"],
},
@@ -313,7 +313,7 @@ describe("accountProvisioner", () => {
providerId: authenticationProvider.providerId,
},
authentication: {
providerId: uuidv4(),
providerId: randomUUID(),
accessToken: "123",
scopes: ["read"],
},
@@ -361,7 +361,7 @@ describe("accountProvisioner", () => {
providerId: authenticationProvider.providerId,
},
authentication: {
providerId: uuidv4(),
providerId: randomUUID(),
accessToken: "123",
scopes: ["read"],
},
@@ -405,7 +405,7 @@ describe("accountProvisioner", () => {
providerId: faker.internet.domainName(),
},
authentication: {
providerId: uuidv4(),
providerId: randomUUID(),
accessToken: "123",
scopes: ["read"],
},
@@ -458,7 +458,7 @@ describe("accountProvisioner", () => {
providerId: faker.internet.domainName(),
},
authentication: {
providerId: uuidv4(),
providerId: randomUUID(),
accessToken: "123",
scopes: ["read"],
},
@@ -491,7 +491,7 @@ describe("accountProvisioner", () => {
providerId: domain,
},
authentication: {
providerId: uuidv4(),
providerId: randomUUID(),
accessToken: "123",
scopes: ["read"],
},
+1 -1
View File
@@ -94,8 +94,8 @@ async function accountProvisioner(
try {
result = await teamProvisioner(ctx, {
name: "Wiki",
...teamParams,
name: teamParams.name || "Wiki",
authenticationProvider: authenticationProviderParams,
});
} catch (err) {
+2 -2
View File
@@ -1,4 +1,4 @@
import { v4 as uuidv4 } from "uuid";
import { randomUUID } from "crypto";
import { AttachmentPreset } from "@shared/types";
import { Attachment, User } from "@server/models";
import AttachmentHelper from "@server/models/helpers/AttachmentHelper";
@@ -47,7 +47,7 @@ export default async function attachmentCreator({
const acl = AttachmentHelper.presetToAcl(preset);
const key = AttachmentHelper.getKey({
acl,
id: uuidv4(),
id: randomUUID(),
name,
userId: user.id,
});
+2 -2
View File
@@ -1,4 +1,4 @@
import { v4 as uuidv4 } from "uuid";
import { randomUUID } from "crypto";
import {
FileOperationFormat,
FileOperationType,
@@ -25,7 +25,7 @@ function getKeyForFileOp(
) {
return `${
Buckets.uploads
}/${teamId}/${uuidv4()}/${name}-export.${format.replace(/outline-/, "")}.zip`;
}/${teamId}/${randomUUID()}/${name}-export.${format.replace(/outline-/, "")}.zip`;
}
async function collectionExporter({
@@ -7,6 +7,7 @@ import Logger from "@server/logging/Logger";
import { Document, Event } from "@server/models";
import { sequelize } from "@server/storage/database";
import { AuthenticationType } from "@server/types";
import semver from "semver";
type Props = {
/** The document ID to update. */
@@ -17,6 +18,8 @@ type Props = {
sessionCollaboratorIds: string[];
/** Whether the last connection to the document left. */
isLastConnection: boolean;
/** The client version, if available. */
clientVersion: string | null;
};
export default async function documentCollaborativeUpdater({
@@ -24,6 +27,7 @@ export default async function documentCollaborativeUpdater({
ydoc,
sessionCollaboratorIds,
isLastConnection,
clientVersion,
}: Props) {
return sequelize.transaction(async (transaction) => {
const document = await Document.unscoped()
@@ -68,12 +72,24 @@ export default async function documentCollaborativeUpdater({
...pudIds,
]);
// Either the client or server version could be null, or they could both be
// set. In that case we want to use the greater (newer) version.
const editorVersion =
document.editorVersion && clientVersion
? semver.gt(clientVersion, document.editorVersion)
? clientVersion
: document.editorVersion
: clientVersion
? clientVersion
: document.editorVersion;
await document.update(
{
content,
state: Buffer.from(state),
lastModifiedById,
collaboratorIds,
editorVersion,
},
{
transaction,
+4 -4
View File
@@ -1,5 +1,5 @@
import { faker } from "@faker-js/faker";
import { v4 as uuidv4 } from "uuid";
import { randomUUID } from "crypto";
import { UserRole } from "@shared/types";
import { TeamDomain } from "@server/models";
import {
@@ -60,7 +60,7 @@ describe("userProvisioner", () => {
teamId: existing.teamId,
authentication: {
authenticationProviderId: authenticationProvider.id,
providerId: uuidv4(),
providerId: randomUUID(),
accessToken: "123",
scopes: ["read"],
},
@@ -94,7 +94,7 @@ describe("userProvisioner", () => {
teamId: existing.teamId,
authentication: {
authenticationProviderId: authenticationProvider.id,
providerId: uuidv4(),
providerId: randomUUID(),
accessToken: "123",
scopes: ["read"],
},
@@ -148,7 +148,7 @@ describe("userProvisioner", () => {
email: "test@example.com",
teamId: existing.teamId,
authentication: {
authenticationProviderId: uuidv4(),
authenticationProviderId: randomUUID(),
providerId: existingAuth.providerId,
accessToken: "123",
scopes: ["read"],
@@ -1,7 +1,5 @@
"use strict";
const { v4 } = require("uuid");
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable("pins", {
@@ -85,7 +83,7 @@ module.exports = {
`,
{
replacements: {
id: v4(),
id: crypto.randomUUID(),
documentId: document.id,
collectionId: document.collectionId,
teamId: document.teamId,
@@ -1,7 +1,5 @@
"use strict";
const { v4 } = require("uuid");
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.sequelize.transaction(async (transaction) => {
@@ -80,7 +78,7 @@ module.exports = {
`,
{
replacements: {
id: v4(),
id: crypto.randomUUID(),
teamId: team.id,
createdById: adminUserID,
name: domain,
+8 -8
View File
@@ -1,4 +1,4 @@
import { v4 as uuidv4 } from "uuid";
import { randomUUID } from "crypto";
import { randomString } from "@shared/random";
import slugify from "@shared/utils/slugify";
import {
@@ -101,7 +101,7 @@ describe("getDocumentTree", () => {
describe("#addDocumentToStructure", () => {
it("should add as last element without index", async () => {
const collection = await buildCollection();
const id = uuidv4();
const id = randomUUID();
const newDocument = await buildDocument({
id,
title: "New end node",
@@ -119,7 +119,7 @@ describe("#addDocumentToStructure", () => {
it("should add with an index", async () => {
const collection = await buildCollection();
const id = uuidv4();
const id = randomUUID();
const newDocument = await buildDocument({
id,
title: "New end node",
@@ -136,7 +136,7 @@ describe("#addDocumentToStructure", () => {
const document = await buildDocument({ collectionId: collection.id });
await collection.reload();
const id = uuidv4();
const id = randomUUID();
const newDocument = await buildDocument({
id,
title: "New end node",
@@ -156,12 +156,12 @@ describe("#addDocumentToStructure", () => {
await collection.reload();
const newDocument = await buildDocument({
id: uuidv4(),
id: randomUUID(),
title: "node",
parentDocumentId: document.id,
teamId: collection.teamId,
});
const id = uuidv4();
const id = randomUUID();
const secondDocument = await buildDocument({
id,
title: "New start node",
@@ -239,9 +239,9 @@ describe("#addDocumentToStructure", () => {
describe("options: documentJson", () => {
it("should append supplied json over document's own", async () => {
const collection = await buildCollection();
const id = uuidv4();
const id = randomUUID();
const newDocument = await buildDocument({
id: uuidv4(),
id: randomUUID(),
title: "New end node",
parentDocumentId: null,
teamId: collection.teamId,
+1 -1
View File
@@ -315,7 +315,7 @@ class Document extends ArchivableModel<
msg: `editorVersion must be 255 characters or less`,
})
@Column
editorVersion: string;
editorVersion: string | null;
/** An icon to use as the document icon. */
@Length({
+1 -1
View File
@@ -48,7 +48,7 @@ class Revision extends ParanoidModel<
})
@Column
@SkipChangeset
editorVersion: string;
editorVersion: string | null;
/** The document title at the time of the revision */
@Length({
+2 -2
View File
@@ -1,5 +1,5 @@
import { faker } from "@faker-js/faker";
import { v4 as uuid } from "uuid";
import { randomUUID } from "crypto";
import { TeamPreference } from "@shared/types";
import { buildDocument, buildTeam } from "@server/test/factories";
import User from "../User";
@@ -40,7 +40,7 @@ describe("Model", () => {
});
it("should return full array if value changed", async () => {
const collaboratorId = uuid();
const collaboratorId = randomUUID();
const document = await buildDocument();
const prev = document.collaboratorIds;
@@ -1,12 +1,12 @@
import { v4 as uuidv4 } from "uuid";
import { randomUUID } from "crypto";
import env from "@server/env";
import SubscriptionHelper from "./SubscriptionHelper";
describe("SubscriptionHelper", () => {
describe("unsubscribeUrl", () => {
it("should return a valid unsubscribe URL", () => {
const userId = uuidv4();
const documentId = uuidv4();
const userId = randomUUID();
const documentId = randomUUID();
const unsubscribeUrl = SubscriptionHelper.unsubscribeUrl(
userId,
+8 -7
View File
@@ -19,18 +19,19 @@ async function presentUnfurl(
case UnfurlResourceType.Issue:
return presentIssue(data);
default:
return presentOEmbed(data);
return presentURL(data);
}
}
const presentOEmbed = (
const presentURL = (
data: Record<string, any>
): UnfurlResponse[UnfurlResourceType.OEmbed] => ({
type: UnfurlResourceType.OEmbed,
): UnfurlResponse[UnfurlResourceType.URL] => ({
type: UnfurlResourceType.URL,
url: data.url,
title: data.title,
description: data.description,
thumbnailUrl: data.thumbnail_url,
title: data.meta.title,
description: data.meta.description,
thumbnailUrl: (data.links.thumbnail ?? [])[0]?.href ?? "",
faviconUrl: (data.links.icon ?? [])[0]?.href ?? "",
});
const presentMention = async (
+3 -3
View File
@@ -9,7 +9,7 @@ import {
Transaction,
UniqueConstraintError,
} from "sequelize";
import { v4 as uuidv4 } from "uuid";
import { randomUUID } from "crypto";
import { randomElement } from "@shared/random";
import { ImportInput, ImportTaskInput } from "@shared/schema";
import {
@@ -514,7 +514,7 @@ export default abstract class ImportsProcessor<
const json = node.toJSON() as ProsemirrorData;
const attrs = json.attrs ?? {};
attrs.id = uuidv4();
attrs.id = randomUUID();
attrs.actorId = actorId;
const externalId = attrs.modelId as string;
@@ -597,7 +597,7 @@ export default abstract class ImportsProcessor<
}
}
idMap[externalId] = internalId ?? uuidv4();
idMap[externalId] = internalId ?? randomUUID();
return idMap[externalId];
}
+2 -2
View File
@@ -4,7 +4,7 @@ import truncate from "lodash/truncate";
import uniqBy from "lodash/uniqBy";
import { Fragment, Node } from "prosemirror-model";
import { Transaction, WhereOptions } from "sequelize";
import { v4 as uuidv4 } from "uuid";
import { randomUUID } from "crypto";
import { ImportTaskInput, ImportTaskOutput } from "@shared/schema";
import {
AttachmentPreset,
@@ -290,7 +290,7 @@ export default abstract class APIImportTask<
await sequelize.transaction(async (transaction) => {
const dbPromises = attachmentsData.map(async (item) => {
const modelId = uuidv4();
const modelId = randomUUID();
const acl = AttachmentHelper.presetToAcl(
AttachmentPreset.DocumentAttachment
);
+1 -1
View File
@@ -49,7 +49,7 @@ export default abstract class ExportTask extends BaseTask<Props> {
: {
teamId: user.teamId,
archivedAt: {
[Op.ne]: null,
[Op.eq]: null,
},
};
+4 -4
View File
@@ -3,7 +3,7 @@ import fs from "fs-extra";
import find from "lodash/find";
import mime from "mime-types";
import { Fragment, Node } from "prosemirror-model";
import { v4 as uuidv4 } from "uuid";
import { randomUUID } from "crypto";
import { ProsemirrorData } from "@shared/types";
import { schema, serializer } from "@server/editor";
import Logger from "@server/logging/Logger";
@@ -72,7 +72,7 @@ export default class ImportJSONTask extends ImportTask {
collectionId: string
) {
Object.values(documents).forEach((node) => {
const id = uuidv4();
const id = randomUUID();
output.documents.push({
...node,
path: "",
@@ -101,7 +101,7 @@ export default class ImportJSONTask extends ImportTask {
[id: string]: AttachmentJSONExport;
}) {
Object.values(attachments).forEach((node) => {
const id = uuidv4();
const id = randomUUID();
const mimeType = mime.lookup(node.key) || "application/octet-stream";
output.attachments.push({
@@ -128,7 +128,7 @@ export default class ImportJSONTask extends ImportTask {
throw new Error(`Could not parse ${node.path}. ${err.message}`);
}
const collectionId = uuidv4();
const collectionId = randomUUID();
output.collections.push({
...item.collection,
+3 -3
View File
@@ -2,7 +2,7 @@ import path from "path";
import fs from "fs-extra";
import escapeRegExp from "lodash/escapeRegExp";
import mime from "mime-types";
import { v4 as uuidv4 } from "uuid";
import { randomUUID } from "crypto";
import documentImporter from "@server/commands/documentImporter";
import { createContext } from "@server/context";
import Logger from "@server/logging/Logger";
@@ -66,7 +66,7 @@ export default class ImportMarkdownZipTask extends ImportTask {
return parseNodeChildren(child.children, collectionId);
}
const id = uuidv4();
const id = randomUUID();
// this is an attachment
if (
@@ -144,7 +144,7 @@ export default class ImportMarkdownZipTask extends ImportTask {
// All nodes in the root level should be collections
for (const node of tree) {
if (node.children.length > 0) {
const collectionId = uuidv4();
const collectionId = randomUUID();
output.collections.push({
id: collectionId,
name: node.title,
+2 -2
View File
@@ -1,4 +1,4 @@
import { v4 as uuidv4 } from "uuid";
import { randomUUID } from "crypto";
import { Team } from "@server/models";
import { Buckets } from "@server/models/helpers/AttachmentHelper";
import FileStorage from "@server/storage/files";
@@ -23,7 +23,7 @@ export default class UploadTeamAvatarTask extends BaseTask<Props> {
const res = await FileStorage.storeFromUrl(
props.avatarUrl,
`${Buckets.avatars}/${team.id}/${uuidv4()}`,
`${Buckets.avatars}/${team.id}/${randomUUID()}`,
"public-read"
);
+2 -2
View File
@@ -1,4 +1,4 @@
import { v4 as uuidv4 } from "uuid";
import { randomUUID } from "crypto";
import { User } from "@server/models";
import { Buckets } from "@server/models/helpers/AttachmentHelper";
import FileStorage from "@server/storage/files";
@@ -23,7 +23,7 @@ export default class UploadUserAvatarTask extends BaseTask<Props> {
const res = await FileStorage.storeFromUrl(
props.avatarUrl,
`${Buckets.avatars}/${user.id}/${uuidv4()}`,
`${Buckets.avatars}/${user.id}/${randomUUID()}`,
"public-read"
);
+3 -3
View File
@@ -1,6 +1,6 @@
import Router from "koa-router";
import { WhereOptions } from "sequelize";
import { v4 as uuidv4 } from "uuid";
import { randomUUID } from "crypto";
import { AttachmentPreset } from "@shared/types";
import { bytesToHumanReadable, getFileNameFromUrl } from "@shared/utils/files";
import { AttachmentValidation } from "@shared/validations";
@@ -113,7 +113,7 @@ router.post(
);
}
const modelId = uuidv4();
const modelId = randomUUID();
const acl = AttachmentHelper.presetToAcl(preset);
const key = AttachmentHelper.getKey({
acl,
@@ -185,7 +185,7 @@ router.post(
authorize(user, "update", document);
const name = getFileNameFromUrl(url) ?? "file";
const modelId = uuidv4();
const modelId = randomUUID();
const acl = AttachmentHelper.presetToAcl(preset);
const key = AttachmentHelper.getKey({
acl,
+8 -8
View File
@@ -1,9 +1,9 @@
import { faker } from "@faker-js/faker";
import { v4 as uuidv4 } from "uuid";
import { randomUUID } from "crypto";
import { buildUser, buildTeam } from "@server/test/factories";
import { getTestServer, setSelfHosted } from "@server/test/support";
const mockTeamInSessionId = uuidv4();
const mockTeamInSessionId = randomUUID();
jest.mock("@server/utils/authentication", () => ({
getSessionsInCookie() {
@@ -107,7 +107,7 @@ describe("#auth.config", () => {
authenticationProviders: [
{
name: "slack",
providerId: uuidv4(),
providerId: randomUUID(),
},
],
});
@@ -130,7 +130,7 @@ describe("#auth.config", () => {
authenticationProviders: [
{
name: "slack",
providerId: uuidv4(),
providerId: randomUUID(),
},
],
});
@@ -153,7 +153,7 @@ describe("#auth.config", () => {
authenticationProviders: [
{
name: "slack",
providerId: uuidv4(),
providerId: randomUUID(),
},
],
});
@@ -177,7 +177,7 @@ describe("#auth.config", () => {
authenticationProviders: [
{
name: "slack",
providerId: uuidv4(),
providerId: randomUUID(),
enabled: false,
},
],
@@ -201,7 +201,7 @@ describe("#auth.config", () => {
authenticationProviders: [
{
name: "slack",
providerId: uuidv4(),
providerId: randomUUID(),
},
],
});
@@ -220,7 +220,7 @@ describe("#auth.config", () => {
authenticationProviders: [
{
name: "slack",
providerId: uuidv4(),
providerId: randomUUID(),
},
],
});
@@ -1,4 +1,4 @@
import { v4 as uuidv4 } from "uuid";
import { randomUUID } from "crypto";
import { buildUser, buildAdmin, buildTeam } from "@server/test/factories";
import { getTestServer, setSelfHosted } from "@server/test/support";
@@ -77,7 +77,7 @@ describe("#authenticationProviders.update", () => {
});
const googleProvider = await team.$create("authenticationProvider", {
name: "google",
providerId: uuidv4(),
providerId: randomUUID(),
});
const res = await server.post("/api/authenticationProviders.update", {
body: {
+2 -2
View File
@@ -12,7 +12,7 @@ import remove from "lodash/remove";
import uniq from "lodash/uniq";
import mime from "mime-types";
import { Op, ScopeOptions, Sequelize, WhereOptions } from "sequelize";
import { v4 as uuidv4 } from "uuid";
import { randomUUID } from "crypto";
import { NavigationNode, StatusFilter, UserRole } from "@shared/types";
import { subtractDate } from "@shared/utils/date";
import slugify from "@shared/utils/slugify";
@@ -1607,7 +1607,7 @@ router.post(
const key = AttachmentHelper.getKey({
acl,
id: uuidv4(),
id: randomUUID(),
name: fileName,
userId: user.id,
});
@@ -1,5 +1,5 @@
import queryString from "query-string";
import { v4 as uuidv4 } from "uuid";
import { randomUUID } from "crypto";
import { randomElement } from "@shared/random";
import { NotificationEventType } from "@shared/types";
import NotificationSettingsHelper from "@server/models/helpers/NotificationSettingsHelper";
@@ -335,7 +335,7 @@ describe("#notifications.pixel", () => {
it("should return 404 for notification that does not exist", async () => {
const res = await server.get(
`/api/notifications.pixel?${queryString.stringify({
id: uuidv4(),
id: randomUUID(),
token: "invalid-token",
})}`
);
+28 -7
View File
@@ -162,11 +162,32 @@ describe("#urls.unfurl", () => {
Promise.resolve({
url: "https://www.flickr.com",
type: "rich",
title: "Flickr",
description:
"The safest and most inclusive global community of photography enthusiasts. The best place for inspiration, connection, and sharing!",
thumbnail_url:
"https://farm4.staticflickr.com/3914/15118079089_489aa62638_b.jpg",
meta: {
title: "Flickr",
description:
"The safest and most inclusive global community of photography enthusiasts. The best place for inspiration, connection, and sharing!",
},
links: {
thumbnail: [
{
href: "https://combo.staticflickr.com/66a031f9fc343c5e42d965ca/671aaf5d51c929e483e8b26d_Open%20Graph%20Home.jpg",
type: "image/jpg",
rel: ["twitter", "thumbnail", "ssl", "og"],
content_length: 412824,
media: {
width: 1200,
height: 630,
},
},
],
icon: [
{
href: "https://combo.staticflickr.com/66a031f9fc343c5e42d965ca/67167dd041b0982f0f230dab_flickr-webclip.png",
rel: ["apple-touch-icon", "icon", "ssl"],
type: "image/png",
},
],
},
})
);
@@ -182,13 +203,13 @@ describe("#urls.unfurl", () => {
expect(res.status).toEqual(200);
expect(body.url).toEqual("https://www.flickr.com");
expect(body.type).toEqual(UnfurlResourceType.OEmbed);
expect(body.type).toEqual(UnfurlResourceType.URL);
expect(body.title).toEqual("Flickr");
expect(body.description).toEqual(
"The safest and most inclusive global community of photography enthusiasts. The best place for inspiration, connection, and sharing!"
);
expect(body.thumbnailUrl).toEqual(
"https://farm4.staticflickr.com/3914/15118079089_489aa62638_b.jpg"
"https://combo.staticflickr.com/66a031f9fc343c5e42d965ca/671aaf5d51c929e483e8b26d_Open%20Graph%20Home.jpg"
);
});
+3 -1
View File
@@ -46,7 +46,6 @@ export default function init(app: Koa = new Koa(), server?: Server) {
}
app.use(compress());
app.use(attachCSRFToken());
// Monitor server connections
if (server) {
@@ -66,6 +65,9 @@ export default function init(app: Koa = new Koa(), server?: Server) {
app.use(mount("/api", api));
// Generate and attach a CSRF token to the session on non-API requests
app.use(attachCSRFToken());
// Apply CSP middleware after API as these responses are rendered in the browser
app.use(csp());
+8 -3
View File
@@ -47,7 +47,7 @@ export default class RedisAdapter extends Redis {
if (!url || !url.startsWith("ioredis://")) {
super(
env.REDIS_URL ?? "",
url || env.REDIS_URL || "",
defaults(options, { connectionName }, defaultOptions)
);
} else {
@@ -76,6 +76,7 @@ export default class RedisAdapter extends Redis {
private static client: RedisAdapter;
private static subscriber: RedisAdapter;
private static collabClient: RedisAdapter;
public static get defaultClient(): RedisAdapter {
return (
@@ -100,9 +101,13 @@ export default class RedisAdapter extends Redis {
* A Redis adapter for collaboration-related operations.
*/
public static get collaborationClient(): RedisAdapter {
if (!env.REDIS_COLLABORATION_URL) {
return this.defaultClient;
}
return (
this.client ||
(this.client = new this(env.REDIS_COLLABORATION_URL, {
this.collabClient ||
(this.collabClient = new this(env.REDIS_COLLABORATION_URL, {
connectionNameSuffix: "collab",
}))
);
+3 -3
View File
@@ -4,7 +4,7 @@ import isNull from "lodash/isNull";
import { Node } from "prosemirror-model";
import { InferCreationAttributes } from "sequelize";
import { DeepPartial } from "utility-types";
import { v4 as uuidv4 } from "uuid";
import { randomUUID } from "crypto";
import { randomString } from "@shared/random";
import {
CollectionPermission,
@@ -282,7 +282,7 @@ export async function buildIntegration(overrides: Partial<Integration> = {}) {
type: IntegrationType.Post,
events: ["documents.update", "documents.publish"],
settings: {
serviceTeamId: uuidv4(),
serviceTeamId: randomUUID(),
},
authenticationId: authentication.id,
...overrides,
@@ -559,7 +559,7 @@ export async function buildAttachment(
overrides.documentId = document.id;
}
const id = uuidv4();
const id = randomUUID();
const acl = overrides.acl || "public-read";
const name = fileName || faker.system.fileName();
return Attachment.create({
+3 -3
View File
@@ -1,4 +1,4 @@
import { v4 } from "uuid";
import { randomUUID } from "crypto";
import { Scope } from "@shared/types";
import { OAuthInterface } from "./OAuthInterface";
import {
@@ -9,10 +9,10 @@ import {
describe("OAuthInterface", () => {
const user = {
id: v4(),
id: randomUUID(),
};
const client = {
id: v4(),
id: randomUUID(),
grants: ["authorization_code", "refresh_token"],
redirectUris: ["https://example.com/callback"],
};
+10 -10
View File
@@ -1,5 +1,5 @@
import { expect } from "@jest/globals";
import { v4 as uuidv4 } from "uuid";
import { randomUUID } from "crypto";
import env from "@server/env";
import parseAttachmentIds from "./parseAttachmentIds";
@@ -8,7 +8,7 @@ it("should return an empty array with no matches", () => {
});
it("should not return orphaned UUID's", () => {
const uuid = uuidv4();
const uuid = randomUUID();
expect(
parseAttachmentIds(`some random text with a uuid ${uuid}
@@ -17,7 +17,7 @@ it("should not return orphaned UUID's", () => {
});
it("should parse attachment ID from markdown", () => {
const uuid = uuidv4();
const uuid = randomUUID();
const results = parseAttachmentIds(
`![caption text](/api/attachments.redirect?id=${uuid})`
);
@@ -26,7 +26,7 @@ it("should parse attachment ID from markdown", () => {
});
it("should parse attachment ID from markdown with additional query params", () => {
const uuid = uuidv4();
const uuid = randomUUID();
const results = parseAttachmentIds(
`![caption text](/api/attachments.redirect?id=${uuid}&size=2)`
);
@@ -35,7 +35,7 @@ it("should parse attachment ID from markdown with additional query params", () =
});
it("should parse attachment ID from markdown with fully qualified url", () => {
const uuid = uuidv4();
const uuid = randomUUID();
const results = parseAttachmentIds(
`![caption text](${env.URL}/api/attachments.redirect?id=${uuid})`
);
@@ -44,7 +44,7 @@ it("should parse attachment ID from markdown with fully qualified url", () => {
});
it("should parse attachment ID from markdown with title", () => {
const uuid = uuidv4();
const uuid = randomUUID();
const results = parseAttachmentIds(
`![caption text](/api/attachments.redirect?id=${uuid} "align-left")`
);
@@ -53,8 +53,8 @@ it("should parse attachment ID from markdown with title", () => {
});
it("should parse multiple attachment IDs from markdown", () => {
const uuid = uuidv4();
const uuid2 = uuidv4();
const uuid = randomUUID();
const uuid2 = randomUUID();
const results =
parseAttachmentIds(`![caption text](/api/attachments.redirect?id=${uuid})
@@ -67,7 +67,7 @@ some text
});
it("should parse attachment ID from html", () => {
const uuid = uuidv4();
const uuid = randomUUID();
const results = parseAttachmentIds(
`<img src="/api/attachments.redirect?id=${uuid}" />`
);
@@ -76,7 +76,7 @@ it("should parse attachment ID from html", () => {
});
it("should parse attachment ID from html with fully qualified url", () => {
const uuid = uuidv4();
const uuid = randomUUID();
const results = parseAttachmentIds(
`<img src="${env.URL}/api/attachments.redirect?id=${uuid}" />`
);
+15 -15
View File
@@ -1,65 +1,65 @@
import { v4 as uuidv4 } from "uuid";
import { randomUUID } from "crypto";
import { Buckets } from "./models/helpers/AttachmentHelper";
import { ValidateKey } from "./validation";
describe("#ValidateKey.isValid", () => {
it("should return false if number of key components are not equal to 4", () => {
expect(
ValidateKey.isValid(`${Buckets.uploads}/${uuidv4()}/${uuidv4()}`)
ValidateKey.isValid(`${Buckets.uploads}/${randomUUID()}/${randomUUID()}`)
).toBe(false);
expect(
ValidateKey.isValid(`${Buckets.uploads}/${uuidv4()}/${uuidv4()}/foo/bar`)
ValidateKey.isValid(`${Buckets.uploads}/${randomUUID()}/${randomUUID()}/foo/bar`)
).toBe(false);
});
it("should return false if the first key component is not a valid bucket", () => {
expect(ValidateKey.isValid(`foo/${uuidv4()}/${uuidv4()}/bar.png`)).toBe(
expect(ValidateKey.isValid(`foo/${randomUUID()}/${randomUUID()}/bar.png`)).toBe(
false
);
});
it("should return false if second and third key components are not UUID", () => {
expect(
ValidateKey.isValid(`${Buckets.uploads}/foo/${uuidv4()}/bar.png`)
ValidateKey.isValid(`${Buckets.uploads}/foo/${randomUUID()}/bar.png`)
).toBe(false);
expect(
ValidateKey.isValid(`${Buckets.uploads}/${uuidv4()}/foo/bar.png`)
ValidateKey.isValid(`${Buckets.uploads}/${randomUUID()}/foo/bar.png`)
).toBe(false);
});
it("should return true successfully validating key", () => {
expect(
ValidateKey.isValid(`${Buckets.public}/${uuidv4()}/${uuidv4()}/foo.png`)
ValidateKey.isValid(`${Buckets.public}/${randomUUID()}/${randomUUID()}/foo.png`)
).toBe(true);
expect(
ValidateKey.isValid(`${Buckets.uploads}/${uuidv4()}/${uuidv4()}/foo.png`)
ValidateKey.isValid(`${Buckets.uploads}/${randomUUID()}/${randomUUID()}/foo.png`)
).toBe(true);
expect(
ValidateKey.isValid(`${Buckets.avatars}/${uuidv4()}/${uuidv4()}`)
ValidateKey.isValid(`${Buckets.avatars}/${randomUUID()}/${randomUUID()}`)
).toBe(true);
});
});
describe("#ValidateKey.sanitize", () => {
it("should sanitize malicious looking keys", () => {
const uuid1 = uuidv4();
const uuid2 = uuidv4();
const uuid1 = randomUUID();
const uuid2 = randomUUID();
expect(
ValidateKey.sanitize(`public/${uuid1}/${uuid2}/~\.\u0000\malicious_key`)
).toEqual(`public/${uuid1}/${uuid2}/~.malicious_key`);
});
it("should remove potential path traversal", () => {
const uuid1 = uuidv4();
const uuid2 = uuidv4();
const uuid1 = randomUUID();
const uuid2 = randomUUID();
expect(
ValidateKey.sanitize(`public/${uuid1}/${uuid2}/../../malicious_key`)
).toEqual(`public/${uuid1}/${uuid2}/malicious_key`);
});
it("should remove problematic characters", () => {
const uuid1 = uuidv4();
const uuid2 = uuidv4();
const uuid1 = randomUUID();
const uuid2 = randomUUID();
expect(ValidateKey.sanitize(`public/${uuid1}/${uuid2}/test#:*?`)).toEqual(
`public/${uuid1}/${uuid2}/test`
);
+1 -2
View File
@@ -1,7 +1,6 @@
import * as Sentry from "@sentry/react";
import { EditorView } from "prosemirror-view";
import { toast } from "sonner";
import { v4 as uuidv4 } from "uuid";
import type { Dictionary } from "~/hooks/useDictionary";
import FileHelper from "../lib/FileHelper";
import uploadPlaceholderPlugin, {
@@ -72,7 +71,7 @@ const insertFiles = async function (
: undefined;
return {
id: `upload-${uuidv4()}`,
id: `upload-${crypto.randomUUID()}`,
dimensions: await getDimensions?.(file),
isImage,
isVideo,
+8 -4
View File
@@ -39,7 +39,7 @@ export default function toggleList(
const currentItemType = parentList.node.content.firstChild?.type;
const differentType = currentItemType && currentItemType !== itemType;
if (differentType || differentListStyle) {
if (differentType) {
return chainTransactions(
clearNodes(),
wrapInList(listType, { listStyle })
@@ -50,10 +50,14 @@ export default function toggleList(
isList(parentList.node, schema) &&
listType.validContent(parentList.node.content)
) {
tr.setNodeMarkup(
tr.doc.nodesBetween(
parentList.pos,
listType,
listStyle ? { listStyle } : {}
parentList.pos + parentList.node.nodeSize,
(node, pos) => {
if (isList(node, schema)) {
tr.setNodeMarkup(pos, listType, listStyle ? { listStyle } : {});
}
}
);
dispatch?.(tr);
+90 -1
View File
@@ -22,12 +22,13 @@ import useStores from "../../hooks/useStores";
import theme from "../../styles/theme";
import {
IntegrationService,
UnfurlResourceType,
type JSONValue,
type UnfurlResourceType,
type UnfurlResponse,
} from "../../types";
import { cn } from "../styles/utils";
import { ComponentProps } from "../types";
import { toDisplayUrl, cdnPath } from "../../utils/urls";
type Attrs = {
className: string;
@@ -143,6 +144,89 @@ type IssuePrProps = ComponentProps & {
) => void;
};
type IssueUrlProps = ComponentProps & {
onChangeUnfurl: (unfurl: UnfurlResponse[UnfurlResourceType.URL]) => void;
};
export const MentionURL = (props: IssueUrlProps) => {
const { unfurls } = useStores();
const isMounted = useIsMounted();
const [loaded, setLoaded] = React.useState(false);
const onChangeUnfurl = React.useRef(props.onChangeUnfurl).current; // stable reference to callback function.
const { isSelected, node } = props;
const {
className,
unfurl: unfurlAttr,
...attrs
} = getAttributesFromNode(node);
const url = String(attrs.href);
const unfurl = unfurls.get(attrs.href)?.data ?? unfurlAttr;
React.useEffect(() => {
const fetchUnfurl = async () => {
try {
const unfurlModel = await unfurls.fetchUnfurl({ url });
if (!isMounted()) {
return;
}
if (unfurlModel) {
onChangeUnfurl(
unfurlModel.data satisfies UnfurlResponse[UnfurlResourceType.URL]
);
} else {
// If we didn't get a result back, we still want to add a basic unfurl
// to avoid refetching again in future. This will just show the URL
// with a generic link icon.
unfurls.add({
id: url,
type: UnfurlResourceType.URL,
fetchedAt: new Date().toISOString(),
data: {
title: toDisplayUrl(url),
faviconUrl: cdnPath("/images/link.png"),
},
});
}
} finally {
setLoaded(true);
}
};
void fetchUnfurl();
}, [unfurls, attrs.href, isMounted]);
if (!unfurl) {
return !loaded ? (
<MentionLoading className={className} />
) : (
<MentionError className={className} />
);
}
return (
<a
{...attrs}
className={cn(className, {
"ProseMirror-selectednode": isSelected,
})}
href={attrs.href as string}
target="_blank"
rel="noopener noreferrer nofollow"
>
<Flex align="center" gap={6}>
{unfurl.faviconUrl ? <Logo src={unfurl.faviconUrl} alt="" /> : null}
<Text>
<Backticks content={unfurl.title} />
</Text>
</Flex>
</a>
);
};
export const MentionIssue = observer((props: IssuePrProps) => {
const { unfurls } = useStores();
const isMounted = useIsMounted();
@@ -316,3 +400,8 @@ const MentionError = ({ className }: { className: string }) => {
const StyledWarningIcon = styled(WarningIcon)`
margin: 0 -2px;
`;
const Logo = styled.img`
width: 16px;
height: 16px;
`;
+1 -2
View File
@@ -10,7 +10,6 @@ import {
Transaction,
} from "prosemirror-state";
import { Decoration, DecorationSet } from "prosemirror-view";
import { v4 as uuidv4 } from "uuid";
import { isCode } from "../lib/isCode";
import { isRemoteTransaction } from "../lib/multiplayer";
import { findBlockNodes } from "../queries/findChildren";
@@ -54,7 +53,7 @@ class MermaidRenderer {
readonly editor: Editor;
constructor(editor: Editor) {
this.diagramId = uuidv4();
this.diagramId = crypto.randomUUID();
this.elementId = `mermaid-diagram-wrapper-${this.diagramId}`;
this.element =
document.getElementById(this.elementId) || document.createElement("div");
+2 -3
View File
@@ -1,6 +1,5 @@
import { Node, Schema } from "prosemirror-model";
import { Primitive } from "utility-types";
import { v4 } from "uuid";
import { isList } from "../queries/isList";
export function transformListToMentions(
@@ -34,11 +33,11 @@ function transformListItemToMentions(
node.type.create(
node.attrs,
schema.nodes.mention.create({
id: v4(),
id: crypto.randomUUID(),
type: mentionType,
label: link,
href: link,
modelId: v4(),
modelId: crypto.randomUUID(),
actorId: attrs.actorId,
})
)
+2 -3
View File
@@ -1,7 +1,6 @@
import { toggleMark } from "prosemirror-commands";
import { MarkSpec, MarkType, Schema, Mark as PMMark } from "prosemirror-model";
import { Command, Plugin } from "prosemirror-state";
import { v4 as uuidv4 } from "uuid";
import { addMark } from "../commands/addMark";
import { collapseSelection } from "../commands/collapseSelection";
import { chainTransactions } from "../lib/chainTransactions";
@@ -82,7 +81,7 @@ export default class Comment extends Mark {
chainTransactions(
toggleMark(type, {
id: uuidv4(),
id: crypto.randomUUID(),
userId: this.options.userId,
draft: true,
}),
@@ -112,7 +111,7 @@ export default class Comment extends Mark {
chainTransactions(
addMark(type, {
id: uuidv4(),
id: crypto.randomUUID(),
userId: this.options.userId,
draft: true,
}),
+9 -1
View File
@@ -268,7 +268,15 @@ export default class Link extends Mark {
if (!words.length) {
return false;
}
if (isInCode(view.state)) {
// check if there is a code mark at the current cursor position
const hasCodeMark = schema.marks.code_inline.isInSet(selection.$from.marks());
if (hasCodeMark) {
return false;
}
// check if we are in a code block or code fence
if (isInCode(view.state, { onlyBlock: true })) {
return false;
}
+1 -2
View File
@@ -9,7 +9,6 @@ import toggleCheckboxItem from "../commands/toggleCheckboxItem";
import { MarkdownSerializerState } from "../lib/markdown/serializer";
import checkboxRule from "../rules/checkboxes";
import Node from "./Node";
import { v4 } from "uuid";
export default class CheckboxItem extends Node {
get name() {
@@ -35,7 +34,7 @@ export default class CheckboxItem extends Node {
},
],
toDOM: (node) => {
const id = `checkbox-${v4()}`;
const id = `checkbox-${crypto.randomUUID()}`;
const checked = node.attrs.checked.toString();
let input;
if (typeof document !== "undefined") {
+11 -4
View File
@@ -12,9 +12,7 @@ import {
Plugin,
TextSelection,
} from "prosemirror-state";
import * as React from "react";
import { Primitive } from "utility-types";
import { v4 as uuidv4 } from "uuid";
import env from "../../env";
import { MentionType, UnfurlResourceType, UnfurlResponse } from "../../types";
import {
@@ -22,6 +20,7 @@ import {
MentionDocument,
MentionIssue,
MentionPullRequest,
MentionURL,
MentionUser,
} from "../components/Mentions";
import { MarkdownSerializerState } from "../lib/markdown/serializer";
@@ -145,6 +144,13 @@ export default class Mention extends Node {
onChangeUnfurl={this.handleChangeUnfurl(props)}
/>
);
case MentionType.URL:
return (
<MentionURL
{...props}
onChangeUnfurl={this.handleChangeUnfurl(props)}
/>
);
default:
return null;
}
@@ -169,7 +175,7 @@ export default class Mention extends Node {
node.type.name === this.name &&
(!nodeId || existingIds.has(nodeId))
) {
nodeId = uuidv4();
nodeId = crypto.randomUUID();
modified = true;
tr.setNodeAttribute(pos, "id", nodeId);
}
@@ -322,7 +328,8 @@ export default class Mention extends Node {
const label =
unfurl.type === UnfurlResourceType.Issue ||
unfurl.type === UnfurlResourceType.PR
unfurl.type === UnfurlResourceType.PR ||
unfurl.type === UnfurlResourceType.URL
? unfurl.title
: undefined;
+7 -1
View File
@@ -319,8 +319,11 @@
"Permission": "Oprávnění",
"Change Language": "Změnit jazyk",
"Dismiss": "Zavřít",
"Unable to download image": "Unable to download image",
"Lightbox": "Lightbox",
"View, navigate, or download images in the document": "View, navigate, or download images in the document",
"Zoom in": "Zoom in",
"Zoom out": "Zoom out",
"Close": "Zavřít",
"Previous": "Previous",
"Next": "Next",
@@ -418,6 +421,7 @@
"Archived collections": "Archivované sbírky",
"New doc": "Nový dokument",
"Empty": "Prázdné",
"No collections": "No collections",
"Collapse": "Sbalit",
"Expand": "Rozbalit",
"Document not supported try Markdown, Plain text, HTML, or Word": "Dokument není podporován zkuste Markdown, Plain text, HTML nebo Word",
@@ -684,8 +688,11 @@
"You may have lost access to this document, try reloading": "Možná jste ztratili přístup k tomuto dokumentu, zkuste jej znovu načíst",
"Too many users connected to document": "Příliš mnoho uživatelů připojených k dokumentu",
"Your edits will sync once other users leave the document": "Vaše úpravy budou synchronizovány, jakmile ostatní uživatelé opustí dokument",
"New version available": "New version available",
"Please reload the page to update to the latest version": "Please reload the page to update to the latest version",
"Server connection lost": "Připojení k serveru bylo ztraceno",
"Edits you make will sync once youre online": "Úpravy, které provedete, se synchronizují, jakmile budete online",
"Offline": "Offline",
"Document restored": "Dokument obnoven",
"Images are still uploading.\nAre you sure you want to discard them?": "Obrázky se stále nahrávají.\nOpravdu je chcete zahodit?",
"{{ count }} comment": "{{ count }} komentář",
@@ -761,7 +768,6 @@
"Please request access from the document owner.": "Please request access from the document owner.",
"Not found": "Nenalezeno",
"The page youre looking for cannot be found. It might have been deleted or the link is incorrect.": "Stránka, kterou hledáte, nebyla nalezena. Možná byla odstraněna nebo odkaz není správný.",
"Offline": "Offline",
"We were unable to load the document while offline.": "V režimu offline se nepodařilo načíst dokument.",
"Your account has been suspended": "Váš účet byl pozastaven",
"Warning Sign": "Výstraha",
+7 -1
View File
@@ -319,8 +319,11 @@
"Permission": "Permission",
"Change Language": "Skift sprog",
"Dismiss": "Afvis",
"Unable to download image": "Unable to download image",
"Lightbox": "Lightbox",
"View, navigate, or download images in the document": "View, navigate, or download images in the document",
"Zoom in": "Zoom in",
"Zoom out": "Zoom out",
"Close": "Close",
"Previous": "Previous",
"Next": "Next",
@@ -418,6 +421,7 @@
"Archived collections": "Archived collections",
"New doc": "Nyt dokument",
"Empty": "Tom",
"No collections": "No collections",
"Collapse": "Collapse",
"Expand": "Expand",
"Document not supported try Markdown, Plain text, HTML, or Word": "Dokumentet understøttes ikke prøv Markdown, Almindelig tekst, HTML, eller Word",
@@ -684,8 +688,11 @@
"You may have lost access to this document, try reloading": "You may have lost access to this document, try reloading",
"Too many users connected to document": "Too many users connected to document",
"Your edits will sync once other users leave the document": "Your edits will sync once other users leave the document",
"New version available": "New version available",
"Please reload the page to update to the latest version": "Please reload the page to update to the latest version",
"Server connection lost": "Server connection lost",
"Edits you make will sync once youre online": "Edits you make will sync once youre online",
"Offline": "Offline",
"Document restored": "Document restored",
"Images are still uploading.\nAre you sure you want to discard them?": "Images are still uploading.\nAre you sure you want to discard them?",
"{{ count }} comment": "{{ count }} comment",
@@ -761,7 +768,6 @@
"Please request access from the document owner.": "Please request access from the document owner.",
"Not found": "Not found",
"The page youre looking for cannot be found. It might have been deleted or the link is incorrect.": "The page youre looking for cannot be found. It might have been deleted or the link is incorrect.",
"Offline": "Offline",
"We were unable to load the document while offline.": "We were unable to load the document while offline.",
"Your account has been suspended": "Your account has been suspended",
"Warning Sign": "Warning Sign",
+7 -1
View File
@@ -319,8 +319,11 @@
"Permission": "Berechtigung",
"Change Language": "Sprache ändern",
"Dismiss": "Ablehnen",
"Unable to download image": "Unable to download image",
"Lightbox": "LightBox",
"View, navigate, or download images in the document": "Bilder im Dokument anzeigen, navigieren oder herunterladen",
"Zoom in": "Zoom in",
"Zoom out": "Zoom out",
"Close": "Schließen",
"Previous": "Zurück",
"Next": "Nächste",
@@ -418,6 +421,7 @@
"Archived collections": "Archivierte Sammlungen",
"New doc": "Neues Dokument",
"Empty": "Leer",
"No collections": "No collections",
"Collapse": "Zusammenklappen",
"Expand": "Ausklappen",
"Document not supported try Markdown, Plain text, HTML, or Word": "Dokument nicht unterstützt - versuche Markdown, Klartext, HTML oder Word",
@@ -684,8 +688,11 @@
"You may have lost access to this document, try reloading": "Sie haben möglicherweise den Zugriff auf dieses Dokument verloren, versuchen Sie es neu zu laden",
"Too many users connected to document": "Zu viele Benutzer sind mit dem Dokument verbunden",
"Your edits will sync once other users leave the document": "Ihre Änderungen werden synchronisiert, sobald andere Benutzer das Dokument verlassen",
"New version available": "New version available",
"Please reload the page to update to the latest version": "Please reload the page to update to the latest version",
"Server connection lost": "Verbindung zum Server verloren",
"Edits you make will sync once youre online": "Änderungen, die du vornimmst, werden synchronisiert, sobald du online bist",
"Offline": "Offline",
"Document restored": "Dokument wiederhergestellt",
"Images are still uploading.\nAre you sure you want to discard them?": "Bilder werden noch hochgeladen.\nMöchtest du sie wirklich verwerfen?",
"{{ count }} comment": "{{ count }} Kommentar",
@@ -761,7 +768,6 @@
"Please request access from the document owner.": "Bitte fordern Sie den Zugriff beim Eigentümer des Dokuments an.",
"Not found": "Nicht gefunden",
"The page youre looking for cannot be found. It might have been deleted or the link is incorrect.": "Die von Ihnen gesuchte Seite kann nicht gefunden werden. Möglicherweise wurde sie gelöscht oder der Link ist fehlerhaft.",
"Offline": "Offline",
"We were unable to load the document while offline.": "Wir konnten das Dokument nicht offline laden.",
"Your account has been suspended": "Ihr Konto wurde gesperrt",
"Warning Sign": "Warnzeichen",
+7 -1
View File
@@ -319,8 +319,11 @@
"Permission": "Permission",
"Change Language": "Change Language",
"Dismiss": "Dismiss",
"Unable to download image": "Unable to download image",
"Lightbox": "Lightbox",
"View, navigate, or download images in the document": "View, navigate, or download images in the document",
"Zoom in": "Zoom in",
"Zoom out": "Zoom out",
"Close": "Close",
"Previous": "Previous",
"Next": "Next",
@@ -418,6 +421,7 @@
"Archived collections": "Archived collections",
"New doc": "New doc",
"Empty": "Empty",
"No collections": "No collections",
"Collapse": "Collapse",
"Expand": "Expand",
"Document not supported try Markdown, Plain text, HTML, or Word": "Document not supported try Markdown, Plain text, HTML, or Word",
@@ -684,8 +688,11 @@
"You may have lost access to this document, try reloading": "You may have lost access to this document, please try reloading",
"Too many users connected to document": "Too many users connected to the document",
"Your edits will sync once other users leave the document": "Your edits will sync once other users leave the document",
"New version available": "New version available",
"Please reload the page to update to the latest version": "Please reload the page to update to the latest version",
"Server connection lost": "Server connection lost",
"Edits you make will sync once youre online": "Edits you make will sync once youre online",
"Offline": "Offline",
"Document restored": "Document restored",
"Images are still uploading.\nAre you sure you want to discard them?": "Images are still uploading.\nAre you sure you want to discard them?",
"{{ count }} comment": "{{ count }} comment",
@@ -761,7 +768,6 @@
"Please request access from the document owner.": "Please request access from the document owner.",
"Not found": "Not found",
"The page youre looking for cannot be found. It might have been deleted or the link is incorrect.": "The page youre looking for cannot be found. It might have been deleted or the link is incorrect.",
"Offline": "Offline",
"We were unable to load the document while offline.": "We were unable to load the document while offline.",
"Your account has been suspended": "Your account has been suspended",
"Warning Sign": "Warning Sign",
+5 -1
View File
@@ -322,6 +322,8 @@
"Unable to download image": "Unable to download image",
"Lightbox": "Lightbox",
"View, navigate, or download images in the document": "View, navigate, or download images in the document",
"Zoom in": "Zoom in",
"Zoom out": "Zoom out",
"Close": "Close",
"Previous": "Previous",
"Next": "Next",
@@ -686,8 +688,11 @@
"You may have lost access to this document, try reloading": "You may have lost access to this document, try reloading",
"Too many users connected to document": "Too many users connected to document",
"Your edits will sync once other users leave the document": "Your edits will sync once other users leave the document",
"New version available": "New version available",
"Please reload the page to update to the latest version": "Please reload the page to update to the latest version",
"Server connection lost": "Server connection lost",
"Edits you make will sync once youre online": "Edits you make will sync once youre online",
"Offline": "Offline",
"Document restored": "Document restored",
"Images are still uploading.\nAre you sure you want to discard them?": "Images are still uploading.\nAre you sure you want to discard them?",
"{{ count }} comment": "{{ count }} comment",
@@ -763,7 +768,6 @@
"Please request access from the document owner.": "Please request access from the document owner.",
"Not found": "Not found",
"The page youre looking for cannot be found. It might have been deleted or the link is incorrect.": "The page youre looking for cannot be found. It might have been deleted or the link is incorrect.",
"Offline": "Offline",
"We were unable to load the document while offline.": "We were unable to load the document while offline.",
"Your account has been suspended": "Your account has been suspended",
"Warning Sign": "Warning Sign",
+7 -1
View File
@@ -319,8 +319,11 @@
"Permission": "Permiso",
"Change Language": "Cambiar Idioma",
"Dismiss": "Descartar",
"Unable to download image": "Unable to download image",
"Lightbox": "Caja de luz\t",
"View, navigate, or download images in the document": "View, navigate, or download images in the document",
"Zoom in": "Zoom in",
"Zoom out": "Zoom out",
"Close": "Cerrar",
"Previous": "Previous",
"Next": "Siguiente",
@@ -418,6 +421,7 @@
"Archived collections": "Colecciones archivadas",
"New doc": "Nuevo doc",
"Empty": "Vacío",
"No collections": "No collections",
"Collapse": "Colapsar",
"Expand": "Expandir",
"Document not supported try Markdown, Plain text, HTML, or Word": "Documento no compatible intenta Markdown, Texto sin formato, HTML o Word",
@@ -684,8 +688,11 @@
"You may have lost access to this document, try reloading": "Puede que hayas perdido acceso a este documento, intenta recargar la página",
"Too many users connected to document": "Demasiados usuarios conectados al documento",
"Your edits will sync once other users leave the document": "Tus ediciones se sincronizarán una vez los demás usuarios salgan del documento",
"New version available": "New version available",
"Please reload the page to update to the latest version": "Please reload the page to update to the latest version",
"Server connection lost": "Conexión al servidor perdida",
"Edits you make will sync once youre online": "Las ediciones que realices se sincronizarán una vez que estés en línea",
"Offline": "Sin conexión",
"Document restored": "Documento restaurado",
"Images are still uploading.\nAre you sure you want to discard them?": "Las imágenes aún se están cargando.\n¿Estás seguro de que quieres descartarlas?",
"{{ count }} comment": "{{ count }} comentario",
@@ -761,7 +768,6 @@
"Please request access from the document owner.": "Solicita acceso al propietario del documento.",
"Not found": "No encontrado",
"The page youre looking for cannot be found. It might have been deleted or the link is incorrect.": "No se puede encontrar la página que estás buscando. Es posible que haya sido eliminada o que el enlace sea incorrecto.",
"Offline": "Sin conexión",
"We were unable to load the document while offline.": "No pudimos cargar el documento sin conexión.",
"Your account has been suspended": "Tu cuenta ha sido suspendida",
"Warning Sign": "Señal de Advertencia",
+7 -1
View File
@@ -319,8 +319,11 @@
"Permission": "مجوز",
"Change Language": "تغییر زبان",
"Dismiss": "رد کردن",
"Unable to download image": "Unable to download image",
"Lightbox": "Lightbox",
"View, navigate, or download images in the document": "View, navigate, or download images in the document",
"Zoom in": "Zoom in",
"Zoom out": "Zoom out",
"Close": "Close",
"Previous": "Previous",
"Next": "Next",
@@ -418,6 +421,7 @@
"Archived collections": "Archived collections",
"New doc": "سند جدید",
"Empty": "خالی",
"No collections": "No collections",
"Collapse": "جمع کردن",
"Expand": "باز کردن",
"Document not supported try Markdown, Plain text, HTML, or Word": "نوع سند پشتیبانی نمی‌شود - از Markdown، متن ساده، HTML، یا Word استفاده کنید",
@@ -684,8 +688,11 @@
"You may have lost access to this document, try reloading": "You may have lost access to this document, try reloading",
"Too many users connected to document": "Too many users connected to document",
"Your edits will sync once other users leave the document": "Your edits will sync once other users leave the document",
"New version available": "New version available",
"Please reload the page to update to the latest version": "Please reload the page to update to the latest version",
"Server connection lost": "اتصال سرور قطع شد",
"Edits you make will sync once youre online": "ویرایش هایی که انجام می دهید پس از آنلاین بودن همگام سازی می شوند",
"Offline": "آفلاین",
"Document restored": "سند بازیابی شد",
"Images are still uploading.\nAre you sure you want to discard them?": "تصاویر هنوز در حال بارگذاری هستند.\nآیا مطمئن هستید که می خواهید آنها را نادیده بگیرید؟",
"{{ count }} comment": "{{ count }} comment",
@@ -761,7 +768,6 @@
"Please request access from the document owner.": "Please request access from the document owner.",
"Not found": "Not found",
"The page youre looking for cannot be found. It might have been deleted or the link is incorrect.": "The page youre looking for cannot be found. It might have been deleted or the link is incorrect.",
"Offline": "آفلاین",
"We were unable to load the document while offline.": "امکان بارگیری سند در حالت آفلاین وجود نداشت.",
"Your account has been suspended": "حساب شما معلق شده است",
"Warning Sign": "Warning Sign",
+7 -1
View File
@@ -319,8 +319,11 @@
"Permission": "Permission",
"Change Language": "Changer de langue",
"Dismiss": "Fermer",
"Unable to download image": "Unable to download image",
"Lightbox": "Visionneuse",
"View, navigate, or download images in the document": "Afficher, naviguer ou télécharger des images dans le document",
"Zoom in": "Zoom in",
"Zoom out": "Zoom out",
"Close": "Fermer",
"Previous": "Précédent",
"Next": "Suivant",
@@ -418,6 +421,7 @@
"Archived collections": "Collections archivées",
"New doc": "Nouveau doc",
"Empty": "Vide",
"No collections": "No collections",
"Collapse": "Réduire",
"Expand": "Développer",
"Document not supported try Markdown, Plain text, HTML, or Word": "Document non pris en charge - essayez un format Markdown, Text, HTML ou Word",
@@ -684,8 +688,11 @@
"You may have lost access to this document, try reloading": "Vous avez peut-être perdu l'accès à ce document, essayez de recharger",
"Too many users connected to document": "Trop d'utilisateurs connectés au document",
"Your edits will sync once other users leave the document": "Vos modifications seront synchronisées une fois que les autres utilisateurs quitteront le document",
"New version available": "New version available",
"Please reload the page to update to the latest version": "Please reload the page to update to the latest version",
"Server connection lost": "Connexion au serveur perdue",
"Edits you make will sync once youre online": "Les modifications que vous effectuez seront synchronisées lorsque vous serez en ligne",
"Offline": "Hors-ligne",
"Document restored": "Document restauré",
"Images are still uploading.\nAre you sure you want to discard them?": "Des images sont toujours en cours de téléchargement.\nÊtes-vous sûr de vouloir les supprimer ?",
"{{ count }} comment": "{{ count }} commentaire",
@@ -761,7 +768,6 @@
"Please request access from the document owner.": "Veuillez demander l'accès au propriétaire du document.",
"Not found": "Non trouvé",
"The page youre looking for cannot be found. It might have been deleted or the link is incorrect.": "La page que vous cherchez est introuvable. Elle a peut-être été supprimée ou le lien est incorrect.",
"Offline": "Hors-ligne",
"We were unable to load the document while offline.": "Impossible de charger le document en mode hors-ligne.",
"Your account has been suspended": "Votre compte a été suspendu",
"Warning Sign": "Signe d'avertissement",

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