Compare commits

...

253 Commits

Author SHA1 Message Date
Tom Moor 6a4b99ca43 refactor 2025-02-08 19:17:06 -05:00
Tom Moor 9bf8c5c633 fix: autoFocus new docs 2025-02-08 17:56:24 -05:00
Tom Moor fe3e712555 Add inline creation for collection 2025-02-08 16:54:54 -05:00
Tom Moor 6e85e99f78 Untitled document name missing in breadcrumb 2025-02-08 16:17:40 -05:00
Tom Moor 0e07d06a91 wip 2025-02-08 15:56:07 -05:00
Tom Moor cc38c4fedb fix: Copy and paste embed results in link 2025-02-08 10:20:15 -05:00
Tom Moor 749b9cc6b8 fix: Mis-sized frame embeds in tables, closes #8357 2025-02-08 09:52:37 -05:00
Tom Moor be4ce4ba2e fix: Remove remobe link button in read-only mode, closes #8350 2025-02-07 22:20:40 -05:00
Tom Moor 7afcce47ae fix: One source of scroll movement from remote edits 2025-02-07 22:20:40 -05:00
Hemachandar 7eb2bc9a16 Unsubscribe from document updates when a user is removed from a document (#8349)
* Unsubscribe from document updates when a user is removed from a document

* dummy default
2025-02-07 05:31:46 -08:00
Tom Moor 67adb66c8b fix: toMarkdown not implemented when copying an individual table cell 2025-02-06 21:25:48 -05:00
Tom Moor 247a50be62 perf: Remove anonymous method in Collaborators component 2025-02-06 20:27:24 -05:00
Tom Moor 225449796a fix: Fix flickering of avatar when multiple windows open for the same user 2025-02-06 20:24:21 -05:00
Hemachandar 7e1adab035 fix: Flaky subscriptionCreator test (#8345) 2025-02-06 18:22:24 -05:00
Tom Moor aca6f55ea0 Copy to clipboard as Markdown (#8342)
* Copy as Markdown

* Avoid instantiating serializer on each copy
2025-02-06 04:29:35 -08:00
Tom Moor ce51fa9957 fix: useComponentSize should run in useLayoutEffect 2025-02-05 22:39:10 -05:00
Hemachandar 676e89a58e fix: Skip permission checks on the app when moving document using DnD (#8333) 2025-02-05 19:10:44 -08:00
Translate-O-Tron c1d4a8e373 New Crowdin updates (#8321) 2025-02-04 19:59:29 -08:00
Tom Moor 7801bcb8e7 chore: Add default values for local development 2025-02-04 22:34:49 -05:00
Johnr24 d4cdf4288e Update .env.sample (#8323) 2025-02-04 19:33:58 -08:00
dependabot[bot] efcea0a7f2 chore(deps): bump the aws group with 5 updates (#8328)
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.735.0` | `3.740.0` |
| [@aws-sdk/lib-storage](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/lib/lib-storage) | `3.735.0` | `3.740.0` |
| [@aws-sdk/s3-presigned-post](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-presigned-post) | `3.735.0` | `3.740.0` |
| [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-request-presigner) | `3.735.0` | `3.740.0` |
| [@aws-sdk/signature-v4-crt](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/signature-v4-crt) | `3.734.0` | `3.740.0` |


Updates `@aws-sdk/client-s3` from 3.735.0 to 3.740.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.740.0/clients/client-s3)

Updates `@aws-sdk/lib-storage` from 3.735.0 to 3.740.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.740.0/lib/lib-storage)

Updates `@aws-sdk/s3-presigned-post` from 3.735.0 to 3.740.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.740.0/packages/s3-presigned-post)

Updates `@aws-sdk/s3-request-presigner` from 3.735.0 to 3.740.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.740.0/packages/s3-request-presigner)

Updates `@aws-sdk/signature-v4-crt` from 3.734.0 to 3.740.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.740.0/packages/signature-v4-crt)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-s3"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/lib-storage"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-presigned-post"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-request-presigner"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/signature-v4-crt"
  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-02-04 19:04:45 -08:00
Tom Moor 5004281077 Combine useComponentSize hooks, possible fix for #8337 (#8338) 2025-02-04 18:49:13 -08:00
Tom Moor 2443be9329 fix: hash-api-keys migration accesses columns before creation, closes #8336 2025-02-04 19:41:00 -05:00
Apoorv Mishra 6f49cb62c3 fix: remove legacy code (#8335) 2025-02-04 16:06:01 -08:00
Tom Moor 6f50ea1d60 fix: Regression, cannot mention in comments 2025-02-03 20:32:30 -05:00
Tom Moor 52679db853 fix: Cannot read properties of null (reading 'data') 2025-02-03 20:08:02 -05:00
Tom Moor 9a94e2dcf2 fix: Cannot read properties of null (reading '0') 2025-02-03 20:00:48 -05:00
dependabot[bot] c990ace2e2 chore(deps): bump i18next-http-backend from 2.7.1 to 2.7.3 (#8330)
Bumps [i18next-http-backend](https://github.com/i18next/i18next-http-backend) from 2.7.1 to 2.7.3.
- [Changelog](https://github.com/i18next/i18next-http-backend/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next-http-backend/commits/v2.7.3)

---
updated-dependencies:
- dependency-name: i18next-http-backend
  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-02-03 16:59:32 -08:00
dependabot[bot] 7a7912b07e chore(deps-dev): bump terser from 5.36.0 to 5.37.0 (#8329)
Bumps [terser](https://github.com/terser/terser) from 5.36.0 to 5.37.0.
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/compare/v5.36.0...v5.37.0)

---
updated-dependencies:
- dependency-name: terser
  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-02-03 16:59:17 -08:00
dependabot[bot] 05a7627148 chore(deps-dev): bump @types/express-useragent from 1.0.2 to 1.0.5 (#8331)
Bumps [@types/express-useragent](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/express-useragent) from 1.0.2 to 1.0.5.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/express-useragent)

---
updated-dependencies:
- dependency-name: "@types/express-useragent"
  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-02-03 16:58:58 -08:00
dependabot[bot] 5ba613ac27 chore(deps): bump winston from 3.13.0 to 3.17.0 (#8332)
Bumps [winston](https://github.com/winstonjs/winston) from 3.13.0 to 3.17.0.
- [Release notes](https://github.com/winstonjs/winston/releases)
- [Changelog](https://github.com/winstonjs/winston/blob/master/CHANGELOG.md)
- [Commits](https://github.com/winstonjs/winston/compare/v3.13.0...v3.17.0)

---
updated-dependencies:
- dependency-name: winston
  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-02-03 16:58:48 -08:00
Tom Moor c717e8e3eb Move collection description into dedicated tab (#8326)
* Move collection description into dedicated tab

* Dynamic default

* refactor
2025-02-03 16:58:35 -08:00
Tom Moor 144d83e68c fix: Missing key prop on facepile 2025-02-01 10:59:37 -05:00
Tom Moor abd6518854 fix: Sidebar active state lost on collection tabs 2025-02-01 10:12:49 -05:00
Tom Moor 9c12498162 Change facepile to clip path (#8325)
* Change to clip path

* tsc

* Remove showBorder prop

* fix: Facepile size prop, tons of cleanup
2025-02-01 06:42:51 -08:00
Tom Moor aa879d8fab chore: fix production build 2025-01-30 21:38:04 -05:00
Tom Moor 28aebc9fbf feat: Upload remote-hosted images on paste (#8301)
* First pass

* fix

* tidy, tidy

* Determine dimensions

* docs

* test getFileNameFromUrl

* PR feedback

* tsc
2025-01-30 17:24:07 -08:00
Translate-O-Tron abaeba5952 New Crowdin updates (#8278) 2025-01-28 17:54:47 -08:00
Tom Moor b666d8f13d fix: Dropbox OIDC requires POST to userinfo endpoint (#8282) 2025-01-28 17:54:04 -08:00
Hemachandar 8e4844fd84 Convert UserMembership mutations (#8285)
* handle collections.add_user, collections.remove_user

* handle documents.add_user, documents.remove_user

* don't publish event in collection creation flow
2025-01-28 17:52:31 -08:00
Tom Moor 15892a9364 feat: API key resource scoping (#8297) 2025-01-28 16:50:22 -08:00
dependabot[bot] 23a89c4d7b chore(deps): bump the aws group with 5 updates (#8309)
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.693.0` | `3.735.0` |
| [@aws-sdk/lib-storage](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/lib/lib-storage) | `3.693.0` | `3.735.0` |
| [@aws-sdk/s3-presigned-post](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-presigned-post) | `3.693.0` | `3.735.0` |
| [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-request-presigner) | `3.693.0` | `3.735.0` |
| [@aws-sdk/signature-v4-crt](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/signature-v4-crt) | `3.693.0` | `3.734.0` |


Updates `@aws-sdk/client-s3` from 3.693.0 to 3.735.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.735.0/clients/client-s3)

Updates `@aws-sdk/lib-storage` from 3.693.0 to 3.735.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.735.0/lib/lib-storage)

Updates `@aws-sdk/s3-presigned-post` from 3.693.0 to 3.735.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.735.0/packages/s3-presigned-post)

Updates `@aws-sdk/s3-request-presigner` from 3.693.0 to 3.735.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.735.0/packages/s3-request-presigner)

Updates `@aws-sdk/signature-v4-crt` from 3.693.0 to 3.734.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.734.0/packages/signature-v4-crt)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-s3"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/lib-storage"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-presigned-post"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/s3-request-presigner"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: "@aws-sdk/signature-v4-crt"
  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-01-28 07:02:05 +05:30
dependabot[bot] f1c5b145a4 chore(deps-dev): bump @types/node from 20.17.14 to 20.17.16 (#8311)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.17.14 to 20.17.16.
- [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-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-01-27 17:27:51 -08:00
dependabot[bot] 4c7b36dfca chore(deps): bump react-hook-form from 7.53.1 to 7.54.2 (#8310)
Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 7.53.1 to 7.54.2.
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.53.1...v7.54.2)

---
updated-dependencies:
- dependency-name: react-hook-form
  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-01-27 17:27:40 -08:00
dependabot[bot] e1d0d4717c chore(deps): bump @tanstack/react-virtual from 3.10.9 to 3.11.3 (#8312)
Bumps [@tanstack/react-virtual](https://github.com/TanStack/virtual/tree/HEAD/packages/react-virtual) from 3.10.9 to 3.11.3.
- [Release notes](https://github.com/TanStack/virtual/releases)
- [Commits](https://github.com/TanStack/virtual/commits/v3.11.3/packages/react-virtual)

---
updated-dependencies:
- dependency-name: "@tanstack/react-virtual"
  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-01-27 17:27:03 -08:00
dependabot[bot] e3f836c22b chore(deps): bump the babel group with 6 updates (#8308)
Bumps the babel group with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) | `7.25.2` | `7.26.7` |
| [@babel/plugin-proposal-decorators](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-proposal-decorators) | `7.24.7` | `7.25.9` |
| [@babel/plugin-transform-class-properties](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-class-properties) | `7.25.7` | `7.25.9` |
| [@babel/plugin-transform-destructuring](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-destructuring) | `7.25.7` | `7.25.9` |
| [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) | `7.25.8` | `7.26.7` |
| [@babel/preset-typescript](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-typescript) | `7.24.1` | `7.26.0` |


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

Updates `@babel/plugin-proposal-decorators` from 7.24.7 to 7.25.9
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.25.9/packages/babel-plugin-proposal-decorators)

Updates `@babel/plugin-transform-class-properties` from 7.25.7 to 7.25.9
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.25.9/packages/babel-plugin-transform-class-properties)

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-27 17:26:32 -08:00
dependabot[bot] e9602ada24 chore(deps): bump @dnd-kit/core from 6.1.0 to 6.3.1 (#8304)
Bumps [@dnd-kit/core](https://github.com/clauderic/dnd-kit/tree/HEAD/packages/core) from 6.1.0 to 6.3.1.
- [Release notes](https://github.com/clauderic/dnd-kit/releases)
- [Changelog](https://github.com/clauderic/dnd-kit/blob/master/packages/core/CHANGELOG.md)
- [Commits](https://github.com/clauderic/dnd-kit/commits/@dnd-kit/core@6.3.1/packages/core)

---
updated-dependencies:
- dependency-name: "@dnd-kit/core"
  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-01-27 17:18:50 -08:00
Tom Moor 0ff4bed18f chore: Add groups to dependabot 2025-01-27 20:13:10 -05:00
dependabot[bot] 6b49d91f2f chore(deps): bump mailparser and @types/mailparser (#8306)
Bumps [mailparser](https://github.com/nodemailer/mailparser) and [@types/mailparser](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/mailparser). These dependencies needed to be updated together.

Updates `mailparser` from 3.7.1 to 3.7.2
- [Release notes](https://github.com/nodemailer/mailparser/releases)
- [Changelog](https://github.com/nodemailer/mailparser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/mailparser/compare/v3.7.1...v3.7.2)

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

---
updated-dependencies:
- dependency-name: mailparser
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: "@types/mailparser"
  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-01-27 17:03:36 -08:00
dependabot[bot] 77f0572445 chore(deps): bump @tanstack/react-table from 8.20.5 to 8.20.6 (#8307)
Bumps [@tanstack/react-table](https://github.com/TanStack/table/tree/HEAD/packages/react-table) from 8.20.5 to 8.20.6.
- [Release notes](https://github.com/TanStack/table/releases)
- [Commits](https://github.com/TanStack/table/commits/v8.20.6/packages/react-table)

---
updated-dependencies:
- dependency-name: "@tanstack/react-table"
  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-01-27 17:03:21 -08:00
Tom Moor 5b11a0cc16 feat: Upgrade FA, add new icons 2025-01-25 15:19:58 -05:00
Tom Moor dfe97bee50 fix: Comment sidebar should not overtake scrolling if linked to anchor, closes #8296 2025-01-25 11:44:38 -05:00
Tom Moor 500730b243 chore: React warning 2025-01-25 08:09:43 -05:00
Tom Moor ec6ed809a4 Add triggers to keyboard shortcut help, make search input sticky 2025-01-25 07:49:06 -05:00
Viorel Cojocaru 08385b8a9e chore: Bump rollup-plugin-webpack-stats to v2.0.1 (#8293) 2025-01-24 15:04:07 -08:00
Tom Moor 9929020b44 chore: fix WSS connection issue in local development 2025-01-24 09:44:54 -05:00
Tom Moor 48a330347f chore: fix CORS issue in local development 2025-01-24 09:21:54 -05:00
Tom Moor 5b6bebc308 fix: Email content should account for untitled documents 2025-01-23 23:46:02 -05:00
Tom Moor c831c71c51 fix: Incorrect horizontal borders on Settings -> Profile 2025-01-23 23:41:55 -05:00
Tom Moor 90350e82fe fix: Events lacking teamId published for sourced memberships (#8295) 2025-01-23 20:19:53 -08:00
Tom Moor b7bbaac2eb fix: Default to user mention for backwards compat 2025-01-23 22:03:27 -05:00
Hemachandar 5a45b95a48 fix: Render TOC only when the shared document has headings (#8264)
* fix: Render TOC only when the shared document has headings

* simplify condition

* fix inconsistent toc button state

* toc visible check

* remove shareHasHeadings prop
2025-01-23 05:12:34 -08:00
Hemachandar 9deb9268b5 fix: Skip events for sourced group memberships (#8286) 2025-01-23 05:06:57 -08:00
Tom Moor 53f4c724bb chore: Remove duplicate trigger definition for suggestion extensions 2025-01-22 22:29:36 -05:00
Tom Moor 184e56264c feat: Add reading time on pinned documents 2025-01-22 21:17:26 -05:00
Tom Moor ffa7043cf0 fix: Outgoing emails trigger spoofing warnings due to exact matching from name 2025-01-22 20:55:10 -05:00
Tom Moor ff3c157554 fix: Crash in share menu when query looks like regex 2025-01-22 20:52:00 -05:00
Tom Moor 13f23d19fc fix: JS error selecting 'Keep as link' with keyboard.
Hacky quick fix, better coming soon
closes #8276
2025-01-22 20:47:38 -05:00
Hemachandar b527048b76 Remove namespace filter for publishing events (#8252)
* groupuser namespace

* remove namespace

* handle reactions

* handle group memberships

* cache changeset before all create and update flows
2025-01-22 17:16:05 -08:00
Apoorv Mishra e1b0cfb6a0 Use explicitly passed title and text in favor of template title and text (#8274)
* fix: use explicitly passed title and text in favor of template title and text while creating doc from the template

* fix: retain template variables when creating a template from another template
2025-01-22 05:41:18 -08:00
ZhuoYang Wu(阿离) 2205b9ee87 feat: add paste menu (#8229)
* feat: add paste menu

* fix: for comment

* Add supported embed detection

* fix: Menu is modified before it closes

* Add multiplayer support

* refactor

* perf: Avoid unneccessary mapping

---------

Co-authored-by: Tom Moor <tom.moor@gmail.com>
2025-01-22 05:41:05 -08:00
dependabot[bot] 1122f030a9 chore(deps): bump vite from 5.4.11 to 5.4.12 (#8273)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.4.11 to 5.4.12.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.12/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.12/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  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-01-22 05:15:10 -08:00
Translate-O-Tron 4cc0beb90d New Crowdin updates (#8257)
* fix: New Norwegian Bokmal translations from Crowdin [ci skip]

* fix: New Norwegian Bokmal translations from Crowdin [ci skip]
2025-01-20 16:00:33 -08:00
dependabot[bot] 16084322ca chore(deps-dev): bump @types/node from 20.14.2 to 20.17.14 (#8261)
* chore(deps-dev): bump @types/node from 20.14.2 to 20.17.14

Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.14.2 to 20.17.14.
- [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-type: direct:development
  update-type: version-update:semver-minor
...

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

* tsc

* tsc

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tom Moor <tom.moor@gmail.com>
2025-01-20 16:00:08 -08:00
dependabot[bot] fa70735585 chore(deps): bump dotenv from 16.4.5 to 16.4.7 (#8258)
Bumps [dotenv](https://github.com/motdotla/dotenv) from 16.4.5 to 16.4.7.
- [Changelog](https://github.com/motdotla/dotenv/blob/master/CHANGELOG.md)
- [Commits](https://github.com/motdotla/dotenv/compare/v16.4.5...v16.4.7)

---
updated-dependencies:
- dependency-name: dotenv
  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-01-20 14:24:33 -08:00
dependabot[bot] 8d694e666c chore(deps): bump react-window from 1.8.10 to 1.8.11 (#8259)
Bumps [react-window](https://github.com/bvaughn/react-window) from 1.8.10 to 1.8.11.
- [Release notes](https://github.com/bvaughn/react-window/releases)
- [Changelog](https://github.com/bvaughn/react-window/blob/master/CHANGELOG.md)
- [Commits](https://github.com/bvaughn/react-window/compare/1.8.10...1.8.11)

---
updated-dependencies:
- dependency-name: react-window
  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-01-20 14:24:19 -08:00
dependabot[bot] 324ce96aaf chore(deps): bump umzug from 3.8.1 to 3.8.2 (#8260)
Bumps [umzug](https://github.com/sequelize/umzug) from 3.8.1 to 3.8.2.
- [Release notes](https://github.com/sequelize/umzug/releases)
- [Changelog](https://github.com/sequelize/umzug/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sequelize/umzug/compare/v3.8.1...v3.8.2)

---
updated-dependencies:
- dependency-name: umzug
  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-01-20 14:24:07 -08:00
Tom Moor cc7f9d1a72 Improve canva share link support, closes #8140 2025-01-19 23:24:37 -05:00
Tom Moor 0116441a58 fix: findByQuery with no query 2025-01-19 22:33:03 -05:00
Tom Moor be93b4ffe9 fix: Various bugs in the suggestion menu logic (#8256) 2025-01-19 17:41:34 -08:00
Tom Moor 11cb90b4fa chore: Simplify Enter rule on mentions 2025-01-19 11:46:11 -05:00
Tom Moor d1b7d0ee45 findByQuery 2025-01-18 22:56:24 -05:00
Tom Moor 029161002b Move hover helper to shared 2025-01-18 21:14:00 -05:00
Tom Moor 1e10985626 Add hover states to mention 2025-01-18 20:58:54 -05:00
Tom Moor e5fdaae09a feat: Add prefetching to shared document sidebar items 2025-01-18 16:35:35 -05:00
Tom Moor cfdb213cc1 Ensure both people and documents are showin in mention menu with no search term 2025-01-18 14:51:40 -05:00
Tom Moor 64106979ba Merge branch 'main' of github.com:outline/outline 2025-01-18 10:49:33 -05:00
Tom Moor 6dffa023b1 fix: Empty title column on shares management 2025-01-17 22:56:30 -05:00
Translate-O-Tron 869b6e7394 New Crowdin updates (#8235) 2025-01-17 18:27:57 -08:00
Tom Moor 73086139d2 Document mentions (#8225) 2025-01-17 15:56:38 -08:00
dependabot[bot] 92b257381b chore(deps): bump katex from 0.16.11 to 0.16.21 (#8253)
Bumps [katex](https://github.com/KaTeX/KaTeX) from 0.16.11 to 0.16.21.
- [Release notes](https://github.com/KaTeX/KaTeX/releases)
- [Changelog](https://github.com/KaTeX/KaTeX/blob/main/CHANGELOG.md)
- [Commits](https://github.com/KaTeX/KaTeX/compare/v0.16.11...v0.16.21)

---
updated-dependencies:
- dependency-name: katex
  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-01-17 15:15:48 -08:00
Tom Moor 79df75e09d chore: Update bug_report.yml 2025-01-16 17:26:28 -08:00
Tom Moor 4517cd6ab1 Move bug report to form format 2025-01-16 20:22:51 -05:00
Hemachandar 3c86b48533 Convert GroupMembership mutations (#8242)
* Convert 'GroupMembership' mutations

* cleanup collectionGroupMemberships

* remove duplicate data
2025-01-16 15:23:09 -08:00
Tom Moor bcba35550a fix: NodeView does not re-render when editable prop changes (#8237) 2025-01-16 14:28:50 -08:00
Tom Moor 4af3ac98d1 fix: Improve styling with mixed RTL content (#8247)
* fix: Improve styling with mixed RTL content

* fix
2025-01-16 13:35:04 -08:00
Tom Moor 7421a9fbdc fix: Mentions should not be able to contain node content (#8246)
closes #8238
2025-01-16 03:49:38 -08:00
Tom Moor 56b9c60388 Revert "Updated ImportJsonTask file mapDocuments method to use unshift instea…" (#8241)
This reverts commit 9cab404194.
2025-01-15 07:16:41 -08:00
Tom Moor 8fec6758b8 fix: Move compression middleware to cover all /api and /auth routes 2025-01-14 19:01:51 -05:00
Tom Moor 1aaabf113b fix: Incorrect plain text serialization in exportTable (#8234) 2025-01-14 06:35:43 -08:00
Translate-O-Tron a0d78378d7 New Crowdin updates (#8204) 2025-01-13 19:35:08 -08:00
dependabot[bot] 78bf8fd641 chore(deps): bump @sentry/react from 7.119.0 to 7.120.3 (#8233)
Bumps [@sentry/react](https://github.com/getsentry/sentry-javascript) from 7.119.0 to 7.120.3.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/7.120.3/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.119.0...7.120.3)

---
updated-dependencies:
- dependency-name: "@sentry/react"
  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-01-13 19:34:24 -08:00
dependabot[bot] 5374d32801 chore(deps): bump @sentry/node from 7.120.0 to 7.120.3 (#8232)
Bumps [@sentry/node](https://github.com/getsentry/sentry-javascript) from 7.120.0 to 7.120.3.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/7.120.3/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.120.0...7.120.3)

---
updated-dependencies:
- dependency-name: "@sentry/node"
  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-01-13 18:52:28 -08:00
dependabot[bot] 68de78ead8 chore(deps): bump sonner from 1.4.3 to 1.7.1 (#8231)
Bumps [sonner](https://github.com/emilkowalski/sonner) from 1.4.3 to 1.7.1.
- [Release notes](https://github.com/emilkowalski/sonner/releases)
- [Commits](https://github.com/emilkowalski/sonner/compare/v1.4.3...v1.7.1)

---
updated-dependencies:
- dependency-name: sonner
  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-01-13 18:52:17 -08:00
WEI-HUA CHIEN 3998a80ae9 fix: Handle nested collapsed headings in findCollapsedNodes (#8223) 2025-01-11 08:59:40 -08:00
Hemachandar e910ecf559 fix: Update counter cache when a user is deleted (or) suspended (#8222) 2025-01-10 19:36:39 -08:00
Hemachandar e42b533b07 Move group management to table (#8212)
* convert to table

* refactor edit group modal

* refactor delete group modal

* refactor add people modal

* refactor create group modal

* rebased changes

* filter works

* empty group message

* retain group title click

* fade

* cleanup

* pre-filtered for determining isEmpty

* remove fade, unnecessary role check

* StickyFilters component

* createdAt column

* Remove DelayedMount
Add 'External ID' in menu when present

---------

Co-authored-by: Tom Moor <tom.moor@gmail.com>
2025-01-09 20:06:09 -05:00
Hemachandar 81d7492e5e Convert Comment and Reaction mutations (#8181)
* handle create, update, delete

* handle resolve, unresolve

* handle add_reaction, remove_reaction

* cleanup

* fix type

* afterDestroy hook

* remove unnecessary 'hooks:false' added in this PR

* tests
2025-01-09 15:48:09 -08:00
Hemachandar 3c5ce8cb3d Publish event in withCtx flow only (#8188)
* api key

* attachment

* file operation

* group

* share

* star

* subscription

* publish events in withCtx flow only

* cleanup GroupUser hooks:false

* type and rename

* rename publish to create
2025-01-08 05:27:49 -08:00
Hemachandar cf3e29bbab Improve useTableRequest for better reactivity (#8206)
* use data from store directly

* load active users only when no filter is set

* return invited user email in users.invite response

* shares
2025-01-08 05:27:36 -08:00
Translate-O-Tron 92a5954ec7 New Crowdin updates (#8193) 2025-01-07 05:29:21 -08:00
Hemachandar 4afa225967 Simplify email references determination (#8189)
* Simplify email references determination

* individual thread for comments

* use toPlainText
2025-01-07 05:29:04 -08:00
dependabot[bot] 48feaf9bc0 chore(deps): bump nodemailer and @types/nodemailer (#8197)
Bumps [nodemailer](https://github.com/nodemailer/nodemailer) and [@types/nodemailer](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/nodemailer). These dependencies needed to be updated together.

Updates `nodemailer` from 6.9.14 to 6.9.16
- [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.9.14...v6.9.16)

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

---
updated-dependencies:
- dependency-name: nodemailer
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: "@types/nodemailer"
  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-01-06 17:14:23 -08:00
dependabot[bot] 3f2ac2d23b chore(deps-dev): bump @babel/cli from 7.25.9 to 7.26.4 (#8198)
Bumps [@babel/cli](https://github.com/babel/babel/tree/HEAD/packages/babel-cli) from 7.25.9 to 7.26.4.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.26.4/packages/babel-cli)

---
updated-dependencies:
- dependency-name: "@babel/cli"
  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-01-06 17:14:06 -08:00
dependabot[bot] 38c12bd2a9 chore(deps-dev): bump @types/resolve-path from 1.4.2 to 1.4.3 (#8199)
Bumps [@types/resolve-path](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/resolve-path) from 1.4.2 to 1.4.3.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/resolve-path)

---
updated-dependencies:
- dependency-name: "@types/resolve-path"
  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-01-06 17:13:52 -08:00
Hemachandar fafaddf07f feat: Option to return anchor text for comments (#8196)
* feat: Option to return anchor text for comments

* cleanup anchorText presentation

* consolidated anchor text

* cleanup unused method
2025-01-06 17:13:37 -08:00
Hemachandar 25f264a763 fix: Notify previously mentioned users when new comment is added to a thread (#8194) 2025-01-05 18:23:16 -08:00
Tom Moor 085785a94c Remove yellow badge for guests 2025-01-05 19:40:30 -05:00
Tom Moor 9c71566d66 fix: Filter input divider in light theme 2025-01-05 19:40:30 -05:00
Hemachandar 4a64a767e1 Convert GroupUser mutations (#8187)
* Convert 'GroupUser' mutations

* cleanup commands
2025-01-05 16:33:51 -08:00
Hemachandar 9bc1788bc0 Upgrade and virtualize table component (#8157)
* Upgrade and virtualize table component

* width in column def

* container height

* share query options

* full page scroll

* change z-index and remove shrink

* non-modal menu
2025-01-05 04:55:05 -08:00
Tom Moor e93ef8b392 fix: shares.update written on every view, regressed in #8177 2025-01-04 21:10:18 -05:00
Tom Moor db30d080ae fix: Highlight matching mentions in find and replace (#8184) 2025-01-02 19:06:42 -08:00
Translate-O-Tron 63d70c2cd5 New Crowdin updates (#8144) 2025-01-02 12:06:20 -08:00
Hemachandar 98fef1bb1f Convert Share mutations (#8177)
* Convert 'Share' mutations

* createContext

* name override in share.revoke method
2025-01-02 12:06:02 -08:00
Prasad Bandaru 9cab404194 Updated ImportJsonTask file mapDocuments method to use unshift instead of push for sorting order (#8183) 2025-01-02 12:05:50 -08:00
Tom Moor d6459150fe feat: Allow resizing of embed height (#8154)
* stash

* tsc

* remove console log

* Restore bottom bar on embeds

* fix: Cannot see selected state

* fix layout issue
2025-01-02 05:49:51 -08:00
Hemachandar 4789ddd947 Skip unnecessary update of comment sort preference (#8182) 2025-01-02 05:49:33 -08:00
Tom Moor 1c179a3c6b Move Group to model event writing (#8179)
* Move Group to model event writing

* cleanup type
2025-01-02 07:45:09 -05:00
Tom Moor b8c07eb298 chore: Cleanup unused pinDestroyer (#8180) 2025-01-01 14:02:45 -08:00
Tom Moor adfca1e5ca fix: Attempting to split undefined 2024-12-31 17:33:47 -05:00
Tom Moor 6ca3c25d35 fix: Do not report errors due to unsupported file types 2024-12-31 08:26:34 -05:00
Tom Moor 05a2c6ae1e fix: Zoom cursor shown while drag-resizing image 2024-12-31 08:26:34 -05:00
Hemachandar 234915f4a0 Convert Subscription mutations (#8166)
* createContext accepts object

* handle subscriptions

* use createContext

* should've done this on the initial attempt...
2024-12-31 05:25:43 -08:00
Tom Moor 538a1274ab fix: Scale width of caption with image (#8174) 2024-12-31 03:36:14 -08:00
Hemachandar 63422373ac Add teamId index on attachments table (#8175) 2024-12-31 03:36:05 -08:00
dependabot[bot] 708bd8a544 chore(deps): bump prosemirror-view from 1.36.0 to 1.37.1 (#8172)
Bumps [prosemirror-view](https://github.com/prosemirror/prosemirror-view) from 1.36.0 to 1.37.1.
- [Changelog](https://github.com/ProseMirror/prosemirror-view/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prosemirror/prosemirror-view/compare/1.36.0...1.37.1)

---
updated-dependencies:
- dependency-name: prosemirror-view
  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>
2024-12-30 16:30:23 -08:00
dependabot[bot] 120191d4d7 chore(deps-dev): bump eslint-plugin-react from 7.35.0 to 7.37.3 (#8169)
Bumps [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) from 7.35.0 to 7.37.3.
- [Release notes](https://github.com/jsx-eslint/eslint-plugin-react/releases)
- [Changelog](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jsx-eslint/eslint-plugin-react/compare/v7.35.0...v7.37.3)

---
updated-dependencies:
- dependency-name: eslint-plugin-react
  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>
2024-12-30 16:29:40 -08:00
dependabot[bot] 6a2ab299a8 chore(deps): bump @babel/preset-react from 7.25.9 to 7.26.3 (#8170)
Bumps [@babel/preset-react](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-react) from 7.25.9 to 7.26.3.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.26.3/packages/babel-preset-react)

---
updated-dependencies:
- dependency-name: "@babel/preset-react"
  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>
2024-12-30 16:29:27 -08:00
dependabot[bot] 74dc7094e1 chore(deps): bump i18next-http-backend from 2.5.0 to 2.7.1 (#8171)
Bumps [i18next-http-backend](https://github.com/i18next/i18next-http-backend) from 2.5.0 to 2.7.1.
- [Changelog](https://github.com/i18next/i18next-http-backend/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next-http-backend/commits)

---
updated-dependencies:
- dependency-name: i18next-http-backend
  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>
2024-12-30 16:29:09 -08:00
Hemachandar 5dd993adf5 Convert WebhookSubscription mutations (#8161)
* Convert 'WebhookSubscription' mutations

* add tests

* remove unnecessary assignment
2024-12-30 16:11:32 -08:00
Hemachandar 41832bbaf1 fix: Use parent transaction for findOrCreate after-commit hook (#8173) 2024-12-30 16:11:11 -08:00
Tom Moor f448be5830 feat: Allow querying groups by externalId 2024-12-27 16:48:19 +00:00
Tom Moor f0fcb26b50 fix: Cannot read properties of undefined (reading 'replace'), closes #8123 2024-12-27 10:41:45 +00:00
Tom Moor ad237a619c fix: Avoid document scrolling behavior when auto-scrolling sidebar 2024-12-26 21:41:25 +00:00
Tom Moor 5f49938267 chore: Fix react key warning 2024-12-26 17:40:22 +00:00
Tom Moor 68a469daa7 Add externalId property on groups (#8127)
* Add 'externalId' property on groups

* Remove clientside Field decorator

* Allow querying by externalId
2024-12-26 08:44:04 -08:00
Tom Moor 3d5a167f7f fix: textBetween line breaks (#8145)
* fix: textBetween line breaks

* test
2024-12-26 03:31:12 -08:00
Tom Moor b58671cbd1 Exclude state column by default in document queries (#8139)
* Exclude state column by default in document queries

* restore withoutState scope
2024-12-26 03:30:48 -08:00
Tom Moor b3a3b0763f fix: Exported HTML does not include table column sizes (#8128) 2024-12-26 03:06:03 -08:00
Tom Moor a4becd66bd feat: Add 'Protobuf' highlighting, closes #8141 2024-12-26 11:05:25 +00:00
Tom Moor 3437bd3a6c fix: Additional Canva embed format, closes #8140 2024-12-25 11:28:17 +00:00
Tom Moor 86cfd62afa feat: Allow users to change email in-app (#8119) 2024-12-25 02:58:26 -08:00
Translate-O-Tron 85b62d3146 New Crowdin updates (#8132)
* fix: New Dutch translations from Crowdin [ci skip]

* fix: New Korean translations from Crowdin [ci skip]
2024-12-24 09:38:02 -08:00
dependabot[bot] 1fa0a5ea98 chore(deps): bump i18next-fs-backend from 2.3.2 to 2.6.0 (#8136)
Bumps [i18next-fs-backend](https://github.com/i18next/i18next-fs-backend) from 2.3.2 to 2.6.0.
- [Changelog](https://github.com/i18next/i18next-fs-backend/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next-fs-backend/compare/v2.3.2...v2.6.0)

---
updated-dependencies:
- dependency-name: i18next-fs-backend
  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>
2024-12-24 09:30:57 -08:00
dependabot[bot] 2b4c8d981c chore(deps-dev): bump nodemon from 3.1.7 to 3.1.9 (#8135)
Bumps [nodemon](https://github.com/remy/nodemon) from 3.1.7 to 3.1.9.
- [Release notes](https://github.com/remy/nodemon/releases)
- [Commits](https://github.com/remy/nodemon/compare/v3.1.7...v3.1.9)

---
updated-dependencies:
- dependency-name: nodemon
  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>
2024-12-24 09:30:23 -08:00
Tom Moor ce55719626 chore: Print emails to console when Ethereal unavailable (offline) 2024-12-19 17:07:10 +09:00
Tom Moor b9f0f67fb2 chore: Tidy mention menu, remove unneccessary component 2024-12-19 14:39:16 +09:00
Tom Moor 02aa4c2928 fix: Consider CDN urls to not be internal 2024-12-19 09:48:21 +09:00
Tom Moor 77e8dbefd6 fix: Ensure signed urls on shared documents are valid longer than 60s 2024-12-19 09:43:49 +09:00
Tom Moor 1e5d281870 chore: Improve warning for SMTP_FROM_EMAIL not set, closes #8125 2024-12-19 06:57:05 +09:00
Tom Moor 9b68e6835e fix: Reduce visual strength of collection in doc breadcrumb 2024-12-18 11:07:39 +09:00
Tom Moor f17926f912 fix: Update slate to WCAG AA compliant, closes #8113 2024-12-18 10:46:06 +09:00
Tom Moor 2397196be8 fix: Shared document header always in mobile styling, closes #8121 2024-12-18 10:27:26 +09:00
Tom Moor 133db9c22c Improve error message when database URI contains invalid characters, closes #8110 2024-12-18 10:18:09 +09:00
Translate-O-Tron 0dd14cdf1a New Crowdin updates (#8058) 2024-12-17 16:56:43 -08:00
dependabot[bot] cc8ec28a39 chore(deps-dev): bump typescript from 5.6.3 to 5.7.2 (#8118)
* chore(deps-dev): bump typescript from 5.6.3 to 5.7.2

Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.6.3 to 5.7.2.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release.yml)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.6.3...v5.7.2)

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

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

* tsc

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tom Moor <tom.moor@gmail.com>
2024-12-18 09:56:31 +09:00
Tom Moor c8cbb9ef9c Add HEIC to supported mimes, closes #8122 2024-12-18 09:49:28 +09:00
dependabot[bot] 4af07ab6c4 chore(deps-dev): bump eslint-plugin-import from 2.29.1 to 2.31.0 (#8116)
Bumps [eslint-plugin-import](https://github.com/import-js/eslint-plugin-import) from 2.29.1 to 2.31.0.
- [Release notes](https://github.com/import-js/eslint-plugin-import/releases)
- [Changelog](https://github.com/import-js/eslint-plugin-import/blob/main/CHANGELOG.md)
- [Commits](https://github.com/import-js/eslint-plugin-import/compare/v2.29.1...v2.31.0)

---
updated-dependencies:
- dependency-name: eslint-plugin-import
  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>
2024-12-17 16:22:49 -08:00
dependabot[bot] 742c138b3d chore(deps): bump mermaid from 11.4.0 to 11.4.1 (#8117)
Bumps [mermaid](https://github.com/mermaid-js/mermaid) from 11.4.0 to 11.4.1.
- [Release notes](https://github.com/mermaid-js/mermaid/releases)
- [Changelog](https://github.com/mermaid-js/mermaid/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/mermaid-js/mermaid/compare/mermaid@11.4.0...mermaid@11.4.1)

---
updated-dependencies:
- dependency-name: mermaid
  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>
2024-12-16 20:36:59 -08:00
Tom Moor ec1eacaeea fix: Cannot leave overlapping comments (#8107) 2024-12-16 20:36:32 -08:00
Tom Moor 8b15cc45b0 fix: Permissions checks on notification emails do not take into account shares (#8109)
* fix: Comment notifications not sent on drafts outside collection, shared docs

* fix: DocumentPublishedOrUpdatedEmail not sent for drafts

* tsc
2024-12-15 20:53:06 -08:00
Tom Moor e89c32424f fix: Subscribe to document automatically on share (#8108) 2024-12-15 17:37:20 -08:00
Tom Moor a458690bfc fix: Non-unique key parameter, closes #8104 2024-12-12 11:39:19 -05:00
Tom Moor df03a6da8c fix: Markdown escape characters left in titles on import (#8102) 2024-12-12 05:15:45 -08:00
Tom Moor 6dfe7d707a fix: Token type not supported by Markdown parser, closes #8101 2024-12-11 21:07:15 -05:00
Tom Moor c063709f1c Allow resizing final table column 2024-12-11 20:39:02 -05:00
Tom Moor dd8f6a987c perf: Avoid iterating child documents in documents.info when direct descendant 2024-12-09 22:13:04 -05:00
Tom Moor fa117870a2 perf: One less query in documents.info 2024-12-09 21:52:32 -05:00
dependabot[bot] 40b1e3c8c6 chore(deps): bump prosemirror-model from 1.23.0 to 1.24.0 (#8092)
* chore(deps): bump prosemirror-model from 1.23.0 to 1.24.0

Bumps [prosemirror-model](https://github.com/prosemirror/prosemirror-model) from 1.23.0 to 1.24.0.
- [Changelog](https://github.com/ProseMirror/prosemirror-model/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prosemirror/prosemirror-model/compare/1.23.0...1.24.0)

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

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

* tsc

* tsc

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tom Moor <tom.moor@gmail.com>
2024-12-09 17:43:04 -08:00
Tom Moor e3b0f7db86 fix: Parsing of grist links with utm parameters, closes #8082 2024-12-09 20:42:36 -05:00
dependabot[bot] 6fddb29ff6 chore(deps): bump nanoid from 3.3.7 to 3.3.8 (#8098)
Bumps [nanoid](https://github.com/ai/nanoid) from 3.3.7 to 3.3.8.
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.3.7...3.3.8)

---
updated-dependencies:
- dependency-name: nanoid
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-09 17:37:36 -08:00
dependabot[bot] 569a7876ae chore(deps): bump utility-types from 3.10.0 to 3.11.0 (#8093)
Bumps [utility-types](https://github.com/piotrwitek/utility-types) from 3.10.0 to 3.11.0.
- [Release notes](https://github.com/piotrwitek/utility-types/releases)
- [Commits](https://github.com/piotrwitek/utility-types/compare/v3.10.0...v3.11.0)

---
updated-dependencies:
- dependency-name: utility-types
  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>
2024-12-09 17:31:03 -08:00
dependabot[bot] bea56159ec chore(deps-dev): bump eslint-import-resolver-typescript from 3.6.3 to 3.7.0 (#8096)
Bumps [eslint-import-resolver-typescript](https://github.com/import-js/eslint-import-resolver-typescript) from 3.6.3 to 3.7.0.
- [Release notes](https://github.com/import-js/eslint-import-resolver-typescript/releases)
- [Changelog](https://github.com/import-js/eslint-import-resolver-typescript/blob/master/CHANGELOG.md)
- [Commits](https://github.com/import-js/eslint-import-resolver-typescript/compare/v3.6.3...v3.7.0)

---
updated-dependencies:
- dependency-name: eslint-import-resolver-typescript
  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>
2024-12-09 17:30:41 -08:00
Tom Moor 908f053920 Add UI element to images that are externally hosted 2024-12-08 12:51:55 -05:00
Tom Moor 033c298bff fix: Retrieve image dimensions for all types, not only PNG 2024-12-07 17:15:25 -05:00
Tom Moor 22f02ad713 feat: Add 'Neon' highlighter color, increase presence of highlights 2024-12-07 16:27:57 -05:00
Tom Moor 92b1c578f6 feat: Improve inline rule matching (#8085)
* stash

* fix: Allow inline mark matching to work with preceding brackets
Refactor markInputRule, add markInputRuleForPattern

* docs
2024-12-07 12:46:25 -08:00
Tom Moor a738ea97b5 feat: Dropping a remote image will now upload (#8086)
* feat: Dropping a remote image will now upload

* refactor,DRY

* guard

* Parse correct file name from url where possible
2024-12-07 12:46:14 -08:00
Tom Moor 7fbe442863 Show editor tooltip shortcuts on separate line 2024-12-07 14:10:36 -05:00
Tom Moor 2db7690e27 feat: Triple clicking in code mark should select entire mark, closes #8072 2024-12-07 12:58:15 -05:00
Tom Moor 06b89635be Improved tooltip context – separate for header,sidebar,editor. 2024-12-07 12:45:29 -05:00
Tom Moor 1ff23756ac fix: Make FindAndReplace popover dynamic, fixes button overflow.
closes #8079
2024-12-06 19:55:45 -05:00
Tom Moor a00b677076 fix: Use sidebarContext in header breadcrumbs (#8077) 2024-12-06 08:00:50 -05:00
Tom Moor 6c1e4a5b40 Add shortcuts to formatting menu tooltips (#8080)
* Add shortcuts to formatting menu tooltips

* Tooltip styling

* tsc
2024-12-05 20:50:16 -08:00
Tom Moor 59078704c8 fix: Embed toggle is unresponsive (#8078)
* fix: Embed toggle is unresponsive

* fix: View recorded when toggling embeds
2024-12-05 20:01:16 -08:00
Hemachandar f1a20b27fd fix: auto-scroll sidebar to show active document (#7956) 2024-12-05 17:23:13 -08:00
Tom Moor 313b046e4e fix: Use singleton for tooltips, ensures that only one is visible at a time. (#8069)
* fix: Use singleton for tooltips, ensures that only one is visible at a time and animations are shared

* fix: give toolbar menu its own context

* Remove duplicate props
2024-12-05 16:10:12 -08:00
Tom Moor 1154432924 Adds count of occurences and index to find and replace (#8070)
* Adds count of occurences and index to find and replace

* Disable replace buttons also
2024-12-05 15:58:24 -08:00
infinite-persistence e8bddbe104 Notification for resolved comment (#8045)
* fix: probably copy-pasted function description

* fix: userIdsMentioned was always empty

* add: NotificationEventType.ResolveComment

* move: split handler for "mentioned" vs. "resolved"

The recipients for "resolved" will include more people (creator, repliers, mentioned), so it's easier to just split the handler than trying to augment it.

* implement: handleResolvedComment

* clone: CommentMentionedEmail as CommentResolvedEmail

Changes coming up in next commit...

* implement: CommentResolvedEmail

* Fix "New Comment↓" incorrectly showing in Resolved

## Repro 1 (with production code)
1. In a list of long resolved comments, scroll up and select the first one.
2. From another account, resolve another comment. The hint appears.

## Repro 2 (with production code)
1. Select Most-Recent, then Resolved.
2. F5. It's scrolled all the way to the bottom.

## Repro 3 (after this PR)
1. Click on the notification when someone resolved a comment. The screen jumps to "Resolved" + showing hint unnecessarily.

## Fix
The scrolling and hint was meant for Most Recent only, but missed out this case since "Resolve" is not part of the enum.

* Better sentences

* Refactor "mentions + author" calculation

* Remove unnecessary check

The resolver is already added to `userIdsNotified` from the start, so no point checking it again here.
2024-12-04 15:10:03 -08:00
Tom Moor dddb12027c fix: Crash in header ref, regressed in 7a6f75c34f closes #8068 2024-12-04 08:35:55 -05:00
dependabot[bot] 5cb3da82bc chore(deps): bump socket.io from 4.7.5 to 4.8.1 (#8056)
Bumps [socket.io](https://github.com/socketio/socket.io) from 4.7.5 to 4.8.1.
- [Release notes](https://github.com/socketio/socket.io/releases)
- [Changelog](https://github.com/socketio/socket.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io/compare/socket.io@4.7.5...socket.io@4.8.1)

---
updated-dependencies:
- dependency-name: socket.io
  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>
2024-12-03 19:15:07 -08:00
Tom Moor 7a6f75c34f fix: Improved responsiveness of document header elements (#8066)
* fix: Made the document header components more responsive to the available space

* doc
2024-12-03 19:11:36 -08:00
Tom Moor 5d09be4add More improvements to LaTeX fence detection 2024-12-02 22:28:59 -05:00
Tom Moor 48cae96a56 fix: Improve validation around emoji node serialization/deserialization 2024-12-02 21:58:22 -05:00
Tom Moor e8ab7a4885 chore: Add additional node validation 2024-12-02 21:22:58 -05:00
dependabot[bot] 183d02d5c6 chore(deps-dev): bump react-refresh from 0.14.0 to 0.14.2 (#8053)
Bumps [react-refresh](https://github.com/facebook/react/tree/HEAD/packages/react) from 0.14.0 to 0.14.2.
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v0.14.2/packages/react)

---
updated-dependencies:
- dependency-name: react-refresh
  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>
2024-12-02 18:10:54 -08:00
Tom Moor 4b833b3e2e Add screen for API key management (#8049)
* API keys

* Add api key list for admins

* permissions
2024-12-02 18:10:36 -08:00
Translate-O-Tron d1b75d44f6 New Crowdin updates (#8040) 2024-12-02 14:36:10 -08:00
Tom Moor 8de59f0a2f fix: 'Resolve' button appearing on resolved threads, closes #8047 2024-12-01 09:53:38 -05:00
Tom Moor d8fbe35455 fix: Template variables are not applied on client (#8044)
* fix: Template variables are not applied on client

* test
2024-11-30 07:13:44 -08:00
Tom Moor 514a724d9d fix: Add default value for attachment preset for easier API use 2024-11-29 23:05:30 -05:00
Tom Moor d66f41c854 fix: Improve behavior of LaTeX at small screensizes, closes #8032 2024-11-29 11:20:01 -05:00
Tom Moor b2d6c40ea8 chore: Add warning for problematic selfhosted config, closes #8025 2024-11-29 11:07:23 -05:00
infinite-persistence c98d6aa33a Allow user to select doc-copy destination (#8030)
* DocumentExplorer: make style extensible

* Allow user to select doc-copy dest

The change in `documentDuplicator` essentially alters the fallback from "parent" to "top of collection". But there is only 1 place that uses it so far, so I think it's fine to support this PR.

In the next commit, the caller side will restore the default to "parent".

* Auto select parent as initial target (to retain existing behavior)

Otherwise, user would need to always search/expand the tree. I have a feeling that people might want the last selection to be persistent, but ignoring that for now.

The 50ms timeout feels dirty, but 0 was too fast, at least on my machine. I couldn't find anything in react-window for a "ready" flag.

* Rename: DuplicateDialog -> DocumentCopy

This begins the switch to DocumentCopy's look in the next few commits

* Revert DocumentExplorer style override

No longer needed since we won't be using it under a ConfirmationDialog anymore in the next commit.

* Switch to DocumentMove's style

* initialSelectionId -> defaultValue

---------

Co-authored-by: Tom Moor <tom.moor@gmail.com>
2024-11-29 10:33:38 -05:00
Hemachandar 554c2a5cdb Simplify determining from email address (#8039)
* Simplify determining from email address

* override only for cloud-hosted
2024-11-29 06:41:48 -08:00
Hemachandar ee426de942 Cleanup random text (#8036) 2024-11-29 08:14:59 -05:00
Tom Moor 746e65e658 fix: Recursively filter source document from explorer, closes #8028 2024-11-27 23:04:37 -05:00
Tom Moor 8a3a3453e7 fix: The operation was unable to achieve a quorum during its retry window 2024-11-27 23:04:37 -05:00
Tom Moor c7d339ded5 Tracking of total uploaded attachments / team (#8031)
* Add column and task to calculate size

* Store in MB, rather than Bytes

* Add cron task to recalculate attachment sizes

* findAllInBatches

* Index createdAt

* fix: Index on incorrect table
2024-11-27 18:42:23 -08:00
Tom Moor ed25554607 fix: Hide TOC on templates 2024-11-27 18:20:49 -05:00
Tom Moor 29329daf15 chore: Record on users.signin event 2024-11-27 17:59:46 -05:00
Tom Moor 3f6390ff18 chore: Remove error on double reload 2024-11-27 17:56:02 -05:00
Translate-O-Tron 54b43c6e6f New Crowdin updates (#8029)
* fix: New Chinese Simplified translations from Crowdin [ci skip]

* fix: New Chinese Simplified translations from Crowdin [ci skip]
2024-11-27 08:38:31 -08:00
Tom Moor 8c9c83eb5a fix: Improve contrast on context menus in dark mode 2024-11-27 10:16:22 -05:00
Tom Moor 63171e5da2 fix: Incorrect cursor on sortable table headers 2024-11-27 09:33:52 -05:00
Tom Moor bfd84681d7 fix: Jank in domain management screen 2024-11-26 22:29:26 -05:00
Tom Moor 7d6a47ce86 chore: Remove unused undo/redo methods 2024-11-26 20:53:44 -05:00
Tom Moor 68f715b607 chore: Remove unused typing tracking logic 2024-11-26 20:50:57 -05:00
Tom Moor ea2e7a4d0f chore: Remove duplicate ID annotations 2024-11-26 20:43:01 -05:00
Translate-O-Tron 26948af1b8 New Crowdin updates (#7967) 2024-11-26 17:24:29 -08:00
Tom Moor 816a6715c5 chore: Simplify comment sidebar persistence to be per-user (#8022) 2024-11-26 17:24:07 -08:00
Tom Moor 4579594c63 fix: Relayout jank on document references 2024-11-26 09:05:14 -05:00
Tom Moor 88f7705fd4 fix: Starred documents do not expand when focusing, related #7956 2024-11-25 23:30:01 -05:00
Hemachandar 8393847910 Check flag emoji is supported (#8009) 2024-11-25 19:32:41 -08:00
dependabot[bot] b9adfa175d chore(deps-dev): bump @types/readable-stream from 4.0.15 to 4.0.18 (#8019)
Bumps [@types/readable-stream](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/readable-stream) from 4.0.15 to 4.0.18.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/readable-stream)

---
updated-dependencies:
- dependency-name: "@types/readable-stream"
  dependency-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>
2024-11-25 19:31:39 -08:00
dependabot[bot] 7fff8161ff chore(deps): bump vite from 5.4.10 to 5.4.11 (#8021)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.4.10 to 5.4.11.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.11/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.11/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  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>
2024-11-25 19:31:30 -08:00
Tom Moor 0ef9f1aea1 fix: Improve fast-click functionality in sidebar 2024-11-25 22:22:21 -05:00
Tom Moor fe63c5d706 fix: JS error in usePosition hook 2024-11-25 20:42:31 -05:00
Tom Moor 7749f0ab9f fix: Undo/redo regression 2024-11-25 20:36:23 -05:00
Tom Moor 763b911dfd fix: Named commands broken, regressed in 921e89d7b7 2024-11-24 23:34:48 -05:00
Tom Moor 99e541ede8 fix: Ensure logout OIDC never immediately relogin 2024-11-24 22:34:16 -05:00
Tom Moor 06f48ec79a Add active/hover state to collapsed thread 2024-11-24 19:59:31 -05:00
infinite-persistence 5566d995bd Comment: collapse long replies (#7941)
* Comment: collapse long replies

## Ticket
Closes 5079

## Review
- For the case of RTL, followed how "Reply" is implemented (assumed that is the desired). If it need to be re-aligned, it can be fixed together with "Reply" later.
- The threshold number can be moved to constants.ts if we don't want to pollute the props.

* Card-style + Facepile
2024-11-24 16:20:46 -08:00
Tom Moor 921e89d7b7 fix: Undo/redo behavior incorrect in multiplayer editor (#8015) 2024-11-24 16:19:52 -08:00
Tom Moor 32602f89dd fix: Flash of styles when printing dark mode (#8010) 2024-11-24 06:15:34 -08:00
Tom Moor 2cce95488c fix: S3 expiry not passed correctly (#8013) 2024-11-24 06:15:19 -08:00
Tom Moor 0663d191fc fix: Lists with negative margin are cut off when printing to PDF. This is a pragmatic fix for the issue closes #7958 2024-11-23 12:00:05 -05:00
Tom Moor 84eb1b801d fix: 'Replace all' functionality replacing offset incorrectly 2024-11-23 00:47:26 -05:00
Hemachandar 5102cfe8eb Persist theme after update (#7997) 2024-11-21 05:18:11 -05:00
Tom Moor 1d0617dbd6 fix: Edge case where heading in first table cell changes margin on focus 2024-11-20 20:52:11 -05:00
Tom Moor eedfd549b3 fix: Rare loop of storage events between tabs causing flickering UI (#7996)
* fix: Rare loop of storage events between tabs causing flickering UI

* Types cleanup
2024-11-20 16:17:28 -08:00
Hemachandar 28cb5aa379 Convert pin mutations to use auto event insertion (#7993) 2024-11-20 16:14:11 -08:00
507 changed files with 14563 additions and 11330 deletions
+3
View File
@@ -1,5 +1,8 @@
URL=https://local.outline.dev:3000
DATABASE_URL=postgres://user:pass@127.0.0.1:5432/outline
REDIS_URL=redis://127.0.0.1:6379
SMTP_FROM_EMAIL=hello@example.com
# Enable unsafe-inline in script-src CSP directive
+2 -2
View File
@@ -12,14 +12,14 @@ UTILS_SECRET=generate_a_new_key
# For production point these at your databases, in development the default
# should work out of the box.
DATABASE_URL=postgres://user:pass@localhost:5432/outline
DATABASE_URL=postgres://user:pass@postgres:5432/outline
DATABASE_CONNECTION_POOL_MIN=
DATABASE_CONNECTION_POOL_MAX=
# Uncomment this to disable SSL for connecting to Postgres
# PGSSLMODE=disable
# For redis you can either specify an ioredis compatible url like this
REDIS_URL=redis://localhost:6379
REDIS_URL=redis://redis:6379
# or alternatively, if you would like to provide additional connection options,
# use a base64 encoded JSON connection option object. Refer to the ioredis documentation
# for a list of available options.
-37
View File
@@ -1,37 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots or videos to help explain your problem.
**Outline (please complete the following information):**
- Install: [getoutline.com or self hosted]
- Version: [commit sha if self hosted]
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Mobile (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
+63
View File
@@ -0,0 +1,63 @@
name: Bug report
description: File a bug to help us improve
labels: ["bug"]
body:
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the bug you encountered.
options:
- label: I have searched the existing issues
required: true
- type: checkboxes
attributes:
label: This is not related to configuring Outline
description: I understand that questions related to configuring self-hosted Outline should be asked in the [community forum](https://github.com/outline/outline/discussions/categories/self-hosting).
options:
- label: The issue is not related to self-hosting config
required: true
- type: textarea
attributes:
label: Current Behavior
description: A concise description of what you're experiencing.
validations:
required: false
- type: textarea
attributes:
label: Expected Behavior
description: A concise description of what you expected to happen.
validations:
required: false
- type: textarea
attributes:
label: Steps To Reproduce
description: Steps to reproduce the behavior.
placeholder: |
1. In this environment...
1. With this config...
1. Run '...'
1. See error...
validations:
required: false
- type: textarea
attributes:
label: Environment
description: |
examples:
- **Outline**: Outline 0.80.0
- **Browser**: Safari
value: |
- Outline:
- Browser:
render: markdown
validations:
required: false
- type: textarea
attributes:
label: Anything else?
description: |
Links? References? Anything that will give us more context about the issue you are encountering!
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
validations:
required: false
+13
View File
@@ -13,3 +13,16 @@ updates:
update-types: ["version-update:semver-major"]
schedule:
interval: "weekly"
groups:
babel:
patterns:
- "@babel/*"
sentry:
patterns:
- "@sentry/*"
fortawesome:
patterns:
- "@fortawesome/*"
aws:
patterns:
- "@aws-sdk/*"
+10 -2
View File
@@ -53,9 +53,13 @@ export const resolveCommentFactory = ({
perform: async ({ t }) => {
await comment.resolve();
const locationState = history.location.state as Record<string, unknown>;
history.replace({
...history.location,
state: null,
state: {
sidebarContext: locationState["sidebarContext"],
commentId: undefined,
},
});
onResolve();
@@ -81,9 +85,13 @@ export const unresolveCommentFactory = ({
perform: async () => {
await comment.unresolve();
const locationState = history.location.state as Record<string, unknown>;
history.replace({
...history.location,
state: null,
state: {
sidebarContext: locationState["sidebarContext"],
commentId: undefined,
},
});
onUnresolve();
+4 -5
View File
@@ -32,6 +32,7 @@ import {
} from "outline-icons";
import * as React from "react";
import { toast } from "sonner";
import Icon from "@shared/components/Icon";
import {
ExportContentType,
TeamPreference,
@@ -45,8 +46,7 @@ import DocumentPermanentDelete from "~/scenes/DocumentPermanentDelete";
import DocumentPublish from "~/scenes/DocumentPublish";
import DeleteDocumentsInTrash from "~/scenes/Trash/components/DeleteDocumentsInTrash";
import ConfirmationDialog from "~/components/ConfirmationDialog";
import DuplicateDialog from "~/components/DuplicateDialog";
import Icon from "~/components/Icon";
import DocumentCopy from "~/components/DocumentCopy";
import MarkdownIcon from "~/components/Icons/MarkdownIcon";
import SharePopover from "~/components/Sharing/Document";
import { getHeaderExpandedKey } from "~/components/Sidebar/components/Header";
@@ -562,7 +562,7 @@ export const duplicateDocument = createAction({
stores.dialogs.openModal({
title: t("Copy document"),
content: (
<DuplicateDialog
<DocumentCopy
document={document}
onSubmit={(response) => {
stores.dialogs.closeAllModals();
@@ -732,7 +732,6 @@ export const importDocument = createAction({
history.push(document.url);
} catch (err) {
toast.error(err.message);
throw err;
}
};
@@ -1054,7 +1053,7 @@ export const openDocumentComments = createAction({
return;
}
stores.ui.toggleComments(activeDocumentId);
stores.ui.toggleComments();
},
});
+4
View File
@@ -13,6 +13,8 @@ export const DeveloperSection = ({ t }: ActionContext) => t("Debug");
export const DocumentSection = ({ t }: ActionContext) => t("Document");
export const DocumentsSection = ({ t }: ActionContext) => t("Documents");
export const ActiveDocumentSection = ({ t, stores }: ActionContext) => {
const activeDocument = stores.documents.active;
return `${t("Document")} · ${activeDocument?.titleWithDefault}`;
@@ -34,6 +36,8 @@ export const NotificationSection = ({ t }: ActionContext) => t("Notification");
export const UserSection = ({ t }: ActionContext) => t("People");
UserSection.priority = 0.5;
export const TeamSection = ({ t }: ActionContext) => t("Workspace");
export const RecentSearchesSection = ({ t }: ActionContext) =>
-1
View File
@@ -31,7 +31,6 @@ const Actions = styled(Flex)`
left: 0;
border-radius: 3px;
background: ${s("background")};
transition: ${s("backgroundTransition")};
padding: 12px;
backdrop-filter: blur(20px);
+2 -1
View File
@@ -5,6 +5,7 @@ import { Redirect } from "react-router-dom";
import useCurrentUser from "~/hooks/useCurrentUser";
import useStores from "~/hooks/useStores";
import { changeLanguage } from "~/utils/language";
import { logoutPath } from "~/utils/routeHelpers";
import LoadingIndicator from "./LoadingIndicator";
type Props = {
@@ -32,7 +33,7 @@ const Authenticated = ({ children }: Props) => {
}
void auth.logout(true);
return <Redirect to="/" />;
return <Redirect to={logoutPath()} />;
};
export default observer(Authenticated);
+1 -1
View File
@@ -94,7 +94,7 @@ const AuthenticatedLayout: React.FC = ({ children }: Props) => {
!showHistory &&
can.comment &&
ui.activeDocumentId &&
ui.commentsExpanded.includes(ui.activeDocumentId) &&
ui.commentsExpanded &&
team.getPreference(TeamPreference.Commenting);
const sidebarRight = (
+16 -18
View File
@@ -7,9 +7,10 @@ export enum AvatarSize {
Small = 16,
Toast = 18,
Medium = 24,
Large = 32,
XLarge = 48,
XXLarge = 64,
Large = 28,
XLarge = 32,
XXLarge = 48,
Upload = 64,
}
export interface IAvatar {
@@ -20,36 +21,37 @@ export interface IAvatar {
}
type Props = {
/** The size of the avatar */
size: AvatarSize;
/** The source of the avatar image, if not passing a model. */
src?: string;
/** The avatar model, if not passing a source. */
model?: IAvatar;
/** The alt text for the image */
alt?: string;
showBorder?: boolean;
/** Optional click handler */
onClick?: React.MouseEventHandler<HTMLImageElement>;
/** Optional class name */
className?: string;
/** Optional style */
style?: React.CSSProperties;
};
function Avatar(props: Props) {
const { showBorder, model, style, ...rest } = props;
const { model, style, ...rest } = props;
const src = props.src || model?.avatarUrl;
const [error, handleError] = useBoolean(false);
return (
<Relative style={style}>
{src && !error ? (
<CircleImg
onError={handleError}
src={src}
$showBorder={showBorder}
{...rest}
/>
<CircleImg onError={handleError} src={src} {...rest} />
) : model ? (
<Initials color={model.color} $showBorder={showBorder} {...rest}>
<Initials color={model.color} {...rest}>
{model.initial}
</Initials>
) : (
<Initials $showBorder={showBorder} {...rest} />
<Initials {...rest} />
)}
</Relative>
);
@@ -65,15 +67,11 @@ const Relative = styled.div`
flex-shrink: 0;
`;
const CircleImg = styled.img<{ size: number; $showBorder?: boolean }>`
const CircleImg = styled.img<{ size: number }>`
display: block;
width: ${(props) => props.size}px;
height: ${(props) => props.size}px;
border-radius: 50%;
border: ${(props) =>
props.$showBorder === false
? "none"
: `2px solid ${props.theme.background}`};
flex-shrink: 0;
overflow: hidden;
`;
+10 -5
View File
@@ -5,7 +5,7 @@ import styled, { css } from "styled-components";
import { s } from "@shared/styles";
import User from "~/models/User";
import Tooltip from "~/components/Tooltip";
import Avatar from "./Avatar";
import Avatar, { AvatarSize } from "./Avatar";
type Props = {
user: User;
@@ -14,6 +14,8 @@ type Props = {
isObserving: boolean;
isCurrentUser: boolean;
onClick?: React.MouseEventHandler<HTMLImageElement>;
size?: AvatarSize;
style?: React.CSSProperties;
};
function AvatarWithPresence({
@@ -23,6 +25,8 @@ function AvatarWithPresence({
isEditing,
isObserving,
isCurrentUser,
size = AvatarSize.Large,
style,
}: Props) {
const { t } = useTranslation();
const status = isPresent
@@ -47,13 +51,14 @@ function AvatarWithPresence({
}
placement="bottom"
>
<AvatarWrapper
<AvatarPresence
$isPresent={isPresent}
$isObserving={isObserving}
$color={user.color}
style={style}
>
<Avatar model={user} onClick={onClick} size={32} />
</AvatarWrapper>
<Avatar model={user} onClick={onClick} size={size} />
</AvatarPresence>
</Tooltip>
</>
);
@@ -69,7 +74,7 @@ type AvatarWrapperProps = {
$color: string;
};
const AvatarWrapper = styled.div<AvatarWrapperProps>`
const AvatarPresence = styled.div<AvatarWrapperProps>`
opacity: ${(props) => (props.$isPresent ? 1 : 0.5)};
transition: opacity 250ms ease-in-out;
border-radius: 50%;
+8 -6
View File
@@ -3,9 +3,12 @@ import { s } from "@shared/styles";
import Flex from "~/components/Flex";
const Initials = styled(Flex)<{
/** The color of the background, defaults to textTertiary. */
color?: string;
/** Content is only used to calculate font size, use children to render. */
content?: string;
/** The size of the avatar */
size: number;
$showBorder?: boolean;
}>`
align-items: center;
justify-content: center;
@@ -13,15 +16,14 @@ const Initials = styled(Flex)<{
width: 100%;
height: 100%;
color: ${s("white75")};
background-color: ${(props) => props.color};
background-color: ${(props) => props.color ?? props.theme.textTertiary};
width: ${(props) => props.size}px;
height: ${(props) => props.size}px;
border-radius: 50%;
border: 2px solid
${(props) =>
props.$showBorder === false ? "transparent" : props.theme.background};
flex-shrink: 0;
font-size: ${(props) => props.size / 2}px;
// adjust font size down for each additional character
font-size: ${(props) => props.size / 2 - (props.content?.length ?? 0)}px;
font-weight: 500;
`;
+15 -12
View File
@@ -8,18 +8,16 @@ import BreadcrumbMenu from "~/menus/BreadcrumbMenu";
import { undraggableOnDesktop } from "~/styles";
import { MenuInternalLink } from "~/types";
type Props = {
type Props = React.PropsWithChildren<{
items: MenuInternalLink[];
max?: number;
highlightFirstItem?: boolean;
};
}>;
function Breadcrumb({
items,
highlightFirstItem,
children,
max = 2,
}: React.PropsWithChildren<Props>) {
function Breadcrumb(
{ items, highlightFirstItem, children, max = 2 }: Props,
ref: React.RefObject<HTMLDivElement> | null
) {
const totalItems = items.length;
const topLevelItems: MenuInternalLink[] = [...items];
let overflowItems;
@@ -37,9 +35,13 @@ function Breadcrumb({
}
return (
<Flex justify="flex-start" align="center">
<Flex justify="flex-start" align="center" ref={ref}>
{topLevelItems.map((item, index) => (
<React.Fragment key={String(item.to) || index}>
<React.Fragment
key={
(typeof item.to === "string" ? item.to : item.to.pathname) || index
}
>
{item.icon}
{item.to ? (
<Item
@@ -67,6 +69,8 @@ const Slash = styled(GoToIcon)`
const Item = styled(Link)<{ $highlight: boolean; $withIcon: boolean }>`
${ellipsis()}
${undraggableOnDesktop()}
display: flex;
flex-shrink: 1;
min-width: 0;
@@ -76,7 +80,6 @@ const Item = styled(Link)<{ $highlight: boolean; $withIcon: boolean }>`
height: 24px;
font-weight: ${(props) => (props.$highlight ? "500" : "inherit")};
margin-left: ${(props) => (props.$withIcon ? "4px" : "0")};
${undraggableOnDesktop()}
svg {
flex-shrink: 0;
@@ -87,4 +90,4 @@ const Item = styled(Link)<{ $highlight: boolean; $withIcon: boolean }>`
}
`;
export default Breadcrumb;
export default React.forwardRef<HTMLDivElement, Props>(Breadcrumb);
+43 -34
View File
@@ -7,7 +7,7 @@ import * as React from "react";
import { useTranslation } from "react-i18next";
import { usePopoverState, PopoverDisclosure } from "reakit/Popover";
import Document from "~/models/Document";
import { AvatarWithPresence } from "~/components/Avatar";
import { AvatarSize, AvatarWithPresence } from "~/components/Avatar";
import DocumentViews from "~/components/DocumentViews";
import Facepile from "~/components/Facepile";
import NudeButton from "~/components/NudeButton";
@@ -18,6 +18,8 @@ import useStores from "~/hooks/useStores";
type Props = {
/** The document to display live collaborators for */
document: Document;
/** The maximum number of collaborators to display, defaults to 6 */
limit?: number;
};
/**
@@ -25,6 +27,7 @@ type Props = {
* and presence status.
*/
function Collaborators(props: Props) {
const { limit = 6 } = props;
const { t } = useTranslation();
const user = useCurrentUser();
const currentUserId = user?.id;
@@ -75,50 +78,56 @@ function Collaborators(props: Props) {
placement: "bottom-end",
});
const limit = 8;
const renderAvatar = React.useCallback(
({ model: collaborator, ...rest }) => {
const isPresent = presentIds.includes(collaborator.id);
const isEditing = editingIds.includes(collaborator.id);
const isObserving = ui.observingUserId === collaborator.id;
const isObservable = collaborator.id !== currentUserId;
return (
<AvatarWithPresence
{...rest}
key={collaborator.id}
user={collaborator}
isPresent={isPresent}
isEditing={isEditing}
isObserving={isObserving}
isCurrentUser={currentUserId === collaborator.id}
onClick={
isObservable
? (ev) => {
if (isPresent) {
ev.preventDefault();
ev.stopPropagation();
ui.setObservingUser(
isObserving ? undefined : collaborator.id
);
}
}
: undefined
}
/>
);
},
[presentIds, ui, currentUserId, editingIds]
);
return (
<>
<PopoverDisclosure {...popover}>
{(popoverProps) => (
<NudeButton
width={Math.min(collaborators.length, limit) * 32}
height={32}
width={Math.min(collaborators.length, limit) * AvatarSize.Large}
height={AvatarSize.Large}
{...popoverProps}
>
<Facepile
size={AvatarSize.Large}
limit={limit}
overflow={Math.max(0, collaborators.length - limit)}
users={collaborators}
renderAvatar={(collaborator) => {
const isPresent = presentIds.includes(collaborator.id);
const isEditing = editingIds.includes(collaborator.id);
const isObserving = ui.observingUserId === collaborator.id;
const isObservable = collaborator.id !== user.id;
return (
<AvatarWithPresence
key={collaborator.id}
user={collaborator}
isPresent={isPresent}
isEditing={isEditing}
isObserving={isObserving}
isCurrentUser={currentUserId === collaborator.id}
onClick={
isObservable
? (ev) => {
if (isPresent) {
ev.preventDefault();
ev.stopPropagation();
ui.setObservingUser(
isObserving ? undefined : collaborator.id
);
}
}
: undefined
}
/>
);
}}
renderAvatar={renderAvatar}
/>
</NudeButton>
)}
+1 -1
View File
@@ -3,6 +3,7 @@ import * as React from "react";
import { Controller, useForm } from "react-hook-form";
import { Trans, useTranslation } from "react-i18next";
import styled from "styled-components";
import Icon from "@shared/components/Icon";
import { randomElement } from "@shared/random";
import { CollectionPermission } from "@shared/types";
import { IconLibrary } from "@shared/utils/IconLibrary";
@@ -11,7 +12,6 @@ import { CollectionValidation } from "@shared/validations";
import Collection from "~/models/Collection";
import Button from "~/components/Button";
import Flex from "~/components/Flex";
import Icon from "~/components/Icon";
import Input from "~/components/Input";
import InputSelectPermission from "~/components/InputSelectPermission";
import Switch from "~/components/Switch";
+31 -186
View File
@@ -1,6 +1,5 @@
import debounce from "lodash/debounce";
import { observer } from "mobx-react";
import { transparentize } from "polished";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
@@ -8,23 +7,14 @@ import styled from "styled-components";
import { richExtensions } from "@shared/editor/nodes";
import { s } from "@shared/styles";
import Collection from "~/models/Collection";
import Arrow from "~/components/Arrow";
import ButtonLink from "~/components/ButtonLink";
import Editor from "~/components/Editor";
import LoadingIndicator from "~/components/LoadingIndicator";
import NudeButton from "~/components/NudeButton";
import BlockMenuExtension from "~/editor/extensions/BlockMenu";
import EmojiMenuExtension from "~/editor/extensions/EmojiMenu";
import HoverPreviewsExtension from "~/editor/extensions/HoverPreviews";
import { withUIExtensions } from "~/editor/extensions";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
import Text from "./Text";
const extensions = [
...richExtensions,
BlockMenuExtension,
EmojiMenuExtension,
HoverPreviewsExtension,
];
const extensions = withUIExtensions(richExtensions);
type Props = {
collection: Collection;
@@ -33,33 +23,8 @@ type Props = {
function CollectionDescription({ collection }: Props) {
const { collections } = useStores();
const { t } = useTranslation();
const [isExpanded, setExpanded] = React.useState(false);
const [isEditing, setEditing] = React.useState(false);
const [isDirty, setDirty] = React.useState(false);
const can = usePolicy(collection);
const handleStartEditing = React.useCallback(() => {
setEditing(true);
}, []);
const handleStopEditing = React.useCallback(() => {
setEditing(false);
}, []);
const handleClickDisclosure = React.useCallback(
(event) => {
event.preventDefault();
if (isExpanded && document.activeElement) {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'blur' does not exist on type 'Element'.
document.activeElement.blur();
}
setExpanded(!isExpanded);
},
[isExpanded]
);
const handleSave = React.useMemo(
() =>
debounce(async (getValue) => {
@@ -67,7 +32,6 @@ function CollectionDescription({ collection }: Props) {
await collection.save({
data: getValue(false),
});
setDirty(false);
} catch (err) {
toast.error(t("Sorry, an error occurred saving the collection"));
throw err;
@@ -76,163 +40,44 @@ function CollectionDescription({ collection }: Props) {
[collection, t]
);
const handleChange = React.useCallback(
async (getValue) => {
setDirty(true);
await handleSave(getValue);
},
[handleSave]
const childRef = React.useRef<HTMLDivElement>(null);
const childOffsetHeight = childRef.current?.offsetHeight || 0;
const editorStyle = React.useMemo(
() => ({
padding: "0 32px",
margin: "0 -32px",
paddingBottom: `calc(50vh - ${childOffsetHeight}px)`,
}),
[childOffsetHeight]
);
React.useEffect(() => {
setEditing(false);
}, [collection.id]);
const placeholder = `${t("Add a description")}`;
const key = isEditing || isDirty ? "draft" : collection.updatedAt;
return (
<MaxHeight data-editing={isEditing} data-expanded={isExpanded}>
<Input data-editing={isEditing} data-expanded={isExpanded}>
<span onClick={can.update ? handleStartEditing : undefined}>
{collections.isSaving && <LoadingIndicator />}
{collection.hasDescription || isEditing || isDirty ? (
<React.Suspense
fallback={
<Placeholder
onClick={() => {
//
}}
>
Loading
</Placeholder>
}
>
<Editor
key={key}
defaultValue={collection.data}
onChange={handleChange}
placeholder={placeholder}
readOnly={!isEditing}
autoFocus={isEditing}
onBlur={handleStopEditing}
extensions={extensions}
maxLength={1000}
embedsDisabled
canUpdate
/>
</React.Suspense>
) : (
can.update && (
<Placeholder
onClick={() => {
//
}}
>
{placeholder}
</Placeholder>
)
)}
</span>
</Input>
{!isEditing && (
<Disclosure
onClick={handleClickDisclosure}
aria-label={isExpanded ? t("Collapse") : t("Expand")}
size={30}
>
<Arrow />
</Disclosure>
<>
{collections.isSaving && <LoadingIndicator />}
{(collection.hasDescription || can.update) && (
<React.Suspense fallback={<Placeholder>Loading</Placeholder>}>
<Editor
defaultValue={collection.data}
onChange={handleSave}
placeholder={`${t("Add a description")}`}
extensions={extensions}
maxLength={1000}
canUpdate={can.update}
readOnly={!can.update}
editorStyle={editorStyle}
embedsDisabled
/>
<div ref={childRef} />
</React.Suspense>
)}
</MaxHeight>
</>
);
}
const Disclosure = styled(NudeButton)`
opacity: 0;
color: ${s("divider")};
position: absolute;
top: calc(25vh - 50px);
left: 50%;
z-index: 1;
transform: rotate(-90deg) translateX(-50%);
transition: opacity 100ms ease-in-out;
&:focus,
&:hover {
opacity: 1;
}
&:active {
color: ${s("sidebarText")};
}
`;
const Placeholder = styled(ButtonLink)`
const Placeholder = styled(Text)`
color: ${s("placeholder")};
cursor: text;
min-height: 27px;
`;
const MaxHeight = styled.div`
position: relative;
max-height: 25vh;
overflow: hidden;
margin: 8px -8px -8px;
padding: 8px;
&[data-editing="true"],
&[data-expanded="true"] {
max-height: initial;
overflow: initial;
${Disclosure} {
top: initial;
bottom: 0;
transform: rotate(90deg) translateX(-50%);
}
}
&:hover ${Disclosure} {
opacity: 1;
}
`;
const Input = styled.div`
margin: -8px;
padding: 8px;
border-radius: 8px;
transition: ${s("backgroundTransition")};
&:after {
content: "";
position: absolute;
top: calc(25vh - 50px);
left: 0;
right: 0;
height: 50px;
pointer-events: none;
background: linear-gradient(
180deg,
${(props) => transparentize(1, props.theme.background)} 0%,
${s("background")} 100%
);
}
&[data-editing="true"],
&[data-expanded="true"] {
&:after {
background: transparent;
}
}
&[data-editing="true"] {
background: ${s("backgroundSecondary")};
}
.block-menu-trigger,
.heading-anchor {
display: none !important;
}
`;
export default observer(CollectionDescription);
@@ -1,6 +1,6 @@
import { DocumentIcon } from "outline-icons";
import * as React from "react";
import Icon from "~/components/Icon";
import Icon from "@shared/components/Icon";
import { createAction } from "~/actions";
import { RecentSection } from "~/actions/sections";
import useStores from "~/hooks/useStores";
@@ -1,6 +1,6 @@
import { NewDocumentIcon, ShapesIcon } from "outline-icons";
import * as React from "react";
import Icon from "~/components/Icon";
import Icon from "@shared/components/Icon";
import { createAction } from "~/actions";
import {
ActiveCollectionSection,
+25 -6
View File
@@ -1,10 +1,12 @@
import { observer } from "mobx-react";
import * as React from "react";
import { Trans, useTranslation } from "react-i18next";
import { toast } from "sonner";
import { CollectionPermission, NavigationNode } from "@shared/types";
import type Collection from "~/models/Collection";
import ConfirmationDialog from "~/components/ConfirmationDialog";
import useStores from "~/hooks/useStores";
import { AuthorizationError } from "~/utils/errors";
type Props = {
/** The navigation node to move, must represent a document. */
@@ -30,12 +32,29 @@ function ConfirmMoveDialog({ collection, item, ...rest }: Props) {
};
const handleSubmit = async () => {
await documents.move({
documentId: item.id,
collectionId: collection.id,
...rest,
});
dialogs.closeAllModals();
try {
await documents.move({
documentId: item.id,
collectionId: collection.id,
...rest,
});
} catch (err) {
if (err instanceof AuthorizationError) {
toast.error(
t(
"You do not have permission to move {{ documentName }} to the {{ collectionName }} collection",
{
documentName: item.title,
collectionName: collection.name,
}
)
);
} else {
toast.error(err.message);
}
} finally {
dialogs.closeAllModals();
}
};
return (
+6 -3
View File
@@ -8,8 +8,8 @@ import Text from "~/components/Text";
import useStores from "~/hooks/useStores";
type Props = {
/** Callback when the dialog is submitted */
onSubmit: () => Promise<void> | void;
/** Callback when the dialog is submitted. Return false to prevent closing. */
onSubmit: () => Promise<void | boolean> | void;
/** Text to display on the submit button */
submitText?: string;
/** Text to display while the form is saving */
@@ -38,7 +38,10 @@ const ConfirmationDialog: React.FC<Props> = ({
ev.preventDefault();
setIsSaving(true);
try {
await onSubmit();
const res = await onSubmit();
if (res === false) {
return;
}
dialogs.closeAllModals();
} catch (err) {
toast.error(err.message);
-1
View File
@@ -182,7 +182,6 @@ function placeCaret(element: HTMLElement, atStart: boolean) {
const Content = styled.span`
background: ${s("background")};
transition: ${s("backgroundTransition")};
color: ${s("text")};
-webkit-text-fill-color: ${s("text")};
outline: none;
+3 -1
View File
@@ -23,7 +23,7 @@ type Props = {
as?: string | React.ComponentType<any>;
hide?: () => void;
level?: number;
icon?: React.ReactElement;
icon?: React.ReactNode;
children?: React.ReactNode;
ref?: React.LegacyRef<HTMLButtonElement> | undefined;
};
@@ -109,6 +109,8 @@ const Title = styled.div`
${ellipsis()}
flex-grow: 1;
display: flex;
align-items: center;
gap: 8px;
`;
type MenuAnchorProps = {
-16
View File
@@ -262,22 +262,6 @@ export const Position = styled.div`
transition-property: outline-width;
transition-duration: 0;
outline: none;
&:after {
content: "";
position: absolute;
top: 1px;
left: 1px;
right: 1px;
bottom: 1px;
pointer-events: none;
border-radius: 4px;
outline-color: ${s("accent")};
outline-width: initial;
outline-offset: -1px;
outline-style: solid;
}
}
/*
+24 -21
View File
@@ -3,20 +3,16 @@ import { ArchiveIcon, GoToIcon, ShapesIcon, TrashIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import Icon from "@shared/components/Icon";
import type { NavigationNode } from "@shared/types";
import Document from "~/models/Document";
import Breadcrumb from "~/components/Breadcrumb";
import Icon from "~/components/Icon";
import CollectionIcon from "~/components/Icons/CollectionIcon";
import { useLocationSidebarContext } from "~/hooks/useLocationSidebarContext";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
import { MenuInternalLink } from "~/types";
import {
archivePath,
collectionPath,
settingsPath,
trashPath,
} from "~/utils/routeHelpers";
import { archivePath, settingsPath, trashPath } from "~/utils/routeHelpers";
type Props = {
children?: React.ReactNode;
@@ -57,14 +53,14 @@ function useCategory(document: Document): MenuInternalLink | null {
return null;
}
const DocumentBreadcrumb: React.FC<Props> = ({
document,
children,
onlyText,
}: Props) => {
function DocumentBreadcrumb(
{ document, children, onlyText }: Props,
ref: React.RefObject<HTMLDivElement> | null
) {
const { collections } = useStores();
const { t } = useTranslation();
const category = useCategory(document);
const sidebarContext = useLocationSidebarContext();
const collection = document.collectionId
? collections.get(document.collectionId)
: undefined;
@@ -81,7 +77,10 @@ const DocumentBreadcrumb: React.FC<Props> = ({
type: "route",
title: collection.name,
icon: <CollectionIcon collection={collection} expanded />,
to: collectionPath(collection.path),
to: {
pathname: collection.path,
state: { sidebarContext },
},
};
} else if (document.isCollectionDeleted) {
collectionNode = {
@@ -106,20 +105,24 @@ const DocumentBreadcrumb: React.FC<Props> = ({
}
path.slice(0, -1).forEach((node: NavigationNode) => {
const title = node.title || t("Untitled");
output.push({
type: "route",
title: node.icon ? (
<>
<StyledIcon value={node.icon} color={node.color} /> {node.title}
<StyledIcon value={node.icon} color={node.color} /> {title}
</>
) : (
node.title
title
),
to: node.url,
to: {
pathname: node.url,
state: { sidebarContext },
},
});
});
return output;
}, [path, category, collectionNode]);
}, [t, path, category, sidebarContext, collectionNode]);
if (!collections.isLoaded) {
return null;
@@ -132,7 +135,7 @@ const DocumentBreadcrumb: React.FC<Props> = ({
{path.slice(0, -1).map((node: NavigationNode) => (
<React.Fragment key={node.id}>
<SmallSlash />
{node.title}
{node.title || t("Untitled")}
</React.Fragment>
))}
</>
@@ -140,11 +143,11 @@ const DocumentBreadcrumb: React.FC<Props> = ({
}
return (
<Breadcrumb items={items} highlightFirstItem>
<Breadcrumb items={items} ref={ref} highlightFirstItem>
{children}
</Breadcrumb>
);
};
}
const StyledIcon = styled(Icon)`
margin-right: 2px;
@@ -160,4 +163,4 @@ const SmallSlash = styled(GoToIcon)`
opacity: 0.5;
`;
export default observer(DocumentBreadcrumb);
export default observer(React.forwardRef(DocumentBreadcrumb));
+32 -11
View File
@@ -1,24 +1,25 @@
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { subDays } from "date-fns";
import { m } from "framer-motion";
import { observer } from "mobx-react";
import { CloseIcon, DocumentIcon, ClockIcon } from "outline-icons";
import { CloseIcon, DocumentIcon, ClockIcon, EyeIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import styled, { useTheme } from "styled-components";
import Icon from "@shared/components/Icon";
import Squircle from "@shared/components/Squircle";
import { s, ellipsis } from "@shared/styles";
import { s, hover, ellipsis } from "@shared/styles";
import { IconType } from "@shared/types";
import { determineIconType } from "@shared/utils/icon";
import Document from "~/models/Document";
import Pin from "~/models/Pin";
import Flex from "~/components/Flex";
import Icon from "~/components/Icon";
import NudeButton from "~/components/NudeButton";
import Time from "~/components/Time";
import useStores from "~/hooks/useStores";
import { hover } from "~/styles";
import { useTextStats } from "~/hooks/useTextStats";
import CollectionIcon from "./Icons/CollectionIcon";
import Text from "./Text";
import Tooltip from "./Tooltip";
@@ -71,6 +72,10 @@ function DocumentCard(props: Props) {
[pin]
);
// If the document was updated within the last 7 days, show a timestamp instead of reading time
const isRecentlyUpdated =
new Date(document.updatedAt) > subDays(new Date(), 7);
return (
<Reorderable
ref={setNodeRef}
@@ -143,13 +148,14 @@ function DocumentCard(props: Props) {
: document.titleWithDefault}
</Heading>
<DocumentMeta size="xsmall">
<Clock size={18} />
<Time
dateTime={document.updatedAt}
tooltipDelay={500}
addSuffix
shorten
/>
{isRecentlyUpdated ? (
<>
<Clock size={18} />
<Time dateTime={document.updatedAt} addSuffix shorten />
</>
) : (
<ReadingTime document={document} />
)}
</DocumentMeta>
</div>
</Content>
@@ -170,6 +176,21 @@ function DocumentCard(props: Props) {
);
}
const ReadingTime = ({ document }: { document: Document }) => {
const { t } = useTranslation();
const markdown = React.useMemo(() => document.toMarkdown(), [document]);
const stats = useTextStats(markdown);
return (
<>
<EyeIcon size={18} />
{t(`{{ minutes }}m read`, {
minutes: stats.total.readingTime,
})}
</>
);
};
const DocumentSquircle = ({
icon,
color,
+149
View File
@@ -0,0 +1,149 @@
import flatten from "lodash/flatten";
import { observer } from "mobx-react";
import * as React from "react";
import { Trans, useTranslation } from "react-i18next";
import { toast } from "sonner";
import styled from "styled-components";
import { NavigationNode } from "@shared/types";
import Document from "~/models/Document";
import { FlexContainer, Footer, StyledText } from "~/scenes/DocumentMove";
import Button from "~/components/Button";
import DocumentExplorer from "~/components/DocumentExplorer";
import useCollectionTrees from "~/hooks/useCollectionTrees";
import useStores from "~/hooks/useStores";
import { flattenTree } from "~/utils/tree";
import Switch from "./Switch";
import Text from "./Text";
type Props = {
/** The original document to duplicate */
document: Document;
onSubmit: (documents: Document[]) => void;
};
function DocumentCopy({ document, onSubmit }: Props) {
const { t } = useTranslation();
const { policies } = useStores();
const collectionTrees = useCollectionTrees();
const [publish, setPublish] = React.useState<boolean>(!!document.publishedAt);
const [recursive, setRecursive] = React.useState<boolean>(true);
const [selectedPath, selectPath] = React.useState<NavigationNode | null>(
null
);
const items = React.useMemo(() => {
const nodes = flatten(collectionTrees.map(flattenTree)).filter((node) =>
node.collectionId
? policies.get(node.collectionId)?.abilities.createDocument
: true
);
if (document.isTemplate) {
return nodes
.filter((node) => node.type === "collection")
.map((node) => ({ ...node, children: [] }));
}
return nodes;
}, [policies, collectionTrees, document.isTemplate]);
const handlePublishChange = React.useCallback(
(ev: React.ChangeEvent<HTMLInputElement>) => {
setPublish(ev.target.checked);
},
[]
);
const handleRecursiveChange = React.useCallback(
(ev: React.ChangeEvent<HTMLInputElement>) => {
setRecursive(ev.target.checked);
},
[]
);
const copy = async () => {
if (!selectedPath) {
toast.message(t("Select a location to copy"));
return;
}
try {
const result = await document.duplicate({
publish,
recursive,
title: document.title,
collectionId: selectedPath.collectionId,
...(selectedPath.type === "document"
? { parentDocumentId: selectedPath.id }
: {}),
});
toast.success(t("Document copied"));
onSubmit(result);
} catch (err) {
toast.error(t("Couldnt copy the document, try again?"));
}
};
return (
<FlexContainer column>
<DocumentExplorer
items={items}
onSubmit={copy}
onSelect={selectPath}
defaultValue={document.parentDocumentId || document.collectionId || ""}
/>
<OptionsContainer>
{!document.isTemplate && (
<>
{document.collectionId && (
<Text size="small">
<Switch
name="publish"
label={t("Publish")}
labelPosition="right"
checked={publish}
onChange={handlePublishChange}
/>
</Text>
)}
{document.publishedAt && document.childDocuments.length > 0 && (
<Text size="small">
<Switch
name="recursive"
label={t("Include nested documents")}
labelPosition="right"
checked={recursive}
onChange={handleRecursiveChange}
/>
</Text>
)}
</>
)}
</OptionsContainer>
<Footer justify="space-between" align="center" gap={8}>
<StyledText type="secondary">
{selectedPath ? (
<Trans
defaults="Copy to <em>{{ location }}</em>"
values={{ location: selectedPath.title }}
components={{ em: <strong /> }}
/>
) : (
t("Select a location to copy")
)}
</StyledText>
<Button disabled={!selectedPath} onClick={copy}>
{t("Copy")}
</Button>
</Footer>
</FlexContainer>
);
}
const OptionsContainer = styled.div`
margin: 16px 0 8px 0;
padding-left: 24px;
padding-right: 24px;
`;
export default observer(DocumentCopy);
+29 -7
View File
@@ -14,32 +14,32 @@ import { FixedSizeList as List } from "react-window";
import scrollIntoView from "scroll-into-view-if-needed";
import styled, { useTheme } from "styled-components";
import breakpoint from "styled-components-breakpoint";
import Icon from "@shared/components/Icon";
import { NavigationNode } from "@shared/types";
import { isModKey } from "@shared/utils/keyboard";
import DocumentExplorerNode from "~/components/DocumentExplorerNode";
import DocumentExplorerSearchResult from "~/components/DocumentExplorerSearchResult";
import Flex from "~/components/Flex";
import Icon from "~/components/Icon";
import CollectionIcon from "~/components/Icons/CollectionIcon";
import { Outline } from "~/components/Input";
import InputSearch from "~/components/InputSearch";
import Text from "~/components/Text";
import useMobile from "~/hooks/useMobile";
import useStores from "~/hooks/useStores";
import { isModKey } from "~/utils/keyboard";
import { ancestors, descendants } from "~/utils/tree";
type Props = {
/** Action taken upon submission of selected item, could be publish, move etc. */
onSubmit: () => void;
/** A side-effect of item selection */
onSelect: (item: NavigationNode | null) => void;
/** Items to be shown in explorer */
items: NavigationNode[];
/** Automatically expand to and select item with the given id */
defaultValue?: string;
};
function DocumentExplorer({ onSubmit, onSelect, items }: Props) {
function DocumentExplorer({ onSubmit, onSelect, items, defaultValue }: Props) {
const isMobile = useMobile();
const { collections, documents } = useStores();
const { t } = useTranslation();
@@ -47,12 +47,25 @@ function DocumentExplorer({ onSubmit, onSelect, items }: Props) {
const [searchTerm, setSearchTerm] = React.useState<string>();
const [selectedNode, selectNode] = React.useState<NavigationNode | null>(
null
() => {
const node =
defaultValue && items.find((item) => item.id === defaultValue);
return node || null;
}
);
const [initialScrollOffset, setInitialScrollOffset] =
React.useState<number>(0);
const [activeNode, setActiveNode] = React.useState<number>(0);
const [expandedNodes, setExpandedNodes] = React.useState<string[]>([]);
const [expandedNodes, setExpandedNodes] = React.useState<string[]>(() => {
if (defaultValue) {
const node = items.find((item) => item.id === defaultValue);
if (node) {
return ancestors(node).map((node) => node.id);
}
}
return [];
});
const [itemRefs, setItemRefs] = React.useState<
React.RefObject<HTMLSpanElement>[]
>([]);
@@ -94,6 +107,15 @@ function DocumentExplorer({ onSubmit, onSelect, items }: Props) {
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)
+13 -8
View File
@@ -9,21 +9,22 @@ import { Link } from "react-router-dom";
import styled, { css } from "styled-components";
import breakpoint from "styled-components-breakpoint";
import EventBoundary from "@shared/components/EventBoundary";
import { s } from "@shared/styles";
import Icon from "@shared/components/Icon";
import { s, hover } from "@shared/styles";
import Document from "~/models/Document";
import Badge from "~/components/Badge";
import DocumentMeta from "~/components/DocumentMeta";
import Flex from "~/components/Flex";
import Highlight from "~/components/Highlight";
import Icon from "~/components/Icon";
import NudeButton from "~/components/NudeButton";
import StarButton, { AnimatedStar } from "~/components/Star";
import Tooltip from "~/components/Tooltip";
import useBoolean from "~/hooks/useBoolean";
import useCurrentUser from "~/hooks/useCurrentUser";
import { useLocationSidebarContext } from "~/hooks/useLocationSidebarContext";
import DocumentMenu from "~/menus/DocumentMenu";
import { hover } from "~/styles";
import { documentPath } from "~/utils/routeHelpers";
import { determineSidebarContext } from "./Sidebar/components/SidebarContext";
type Props = {
document: Document;
@@ -50,6 +51,7 @@ function DocumentListItem(
) {
const { t } = useTranslation();
const user = useCurrentUser();
const locationSidebarContext = useLocationSidebarContext();
const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean();
let itemRef: React.Ref<HTMLAnchorElement> =
@@ -78,6 +80,12 @@ function DocumentListItem(
!!document.title.toLowerCase().includes(highlight.toLowerCase());
const canStar = !document.isArchived && !document.isTemplate;
const sidebarContext = determineSidebarContext({
document,
user,
currentContext: locationSidebarContext,
});
return (
<DocumentLink
ref={itemRef}
@@ -89,6 +97,7 @@ function DocumentListItem(
pathname: documentPath(document),
state: {
title: document.titleWithDefault,
sidebarContext,
},
}}
{...rest}
@@ -111,11 +120,7 @@ function DocumentListItem(
<Badge yellow>{t("New")}</Badge>
)}
{document.isDraft && showDraft && (
<Tooltip
content={t("Only visible to you")}
delay={500}
placement="top"
>
<Tooltip content={t("Only visible to you")} placement="top">
<Badge>{t("Draft")}</Badge>
</Tooltip>
)}
+6 -2
View File
@@ -185,9 +185,9 @@ const DocumentMeta: React.FC<Props> = ({
{showCollection && collection && (
<span>
&nbsp;{t("in")}&nbsp;
<strong>
<Strong>
<DocumentBreadcrumb document={document} onlyText />
</strong>
</Strong>
</span>
)}
{showParentDocuments && nestedDocumentsCount > 0 && (
@@ -210,6 +210,10 @@ const DocumentMeta: React.FC<Props> = ({
);
};
const Strong = styled.strong`
font-weight: 550;
`;
const Container = styled(Flex)<{ rtl?: boolean }>`
justify-content: ${(props) => (props.rtl ? "flex-end" : "flex-start")};
color: ${s("textTertiary")};
+8 -2
View File
@@ -6,7 +6,7 @@ import { useTranslation } from "react-i18next";
import { dateLocale, dateToRelative } from "@shared/utils/date";
import Document from "~/models/Document";
import User from "~/models/User";
import { Avatar } from "~/components/Avatar";
import { Avatar, AvatarSize } from "~/components/Avatar";
import ListItem from "~/components/List/Item";
import PaginatedList from "~/components/PaginatedList";
import useCurrentUser from "~/hooks/useCurrentUser";
@@ -71,7 +71,13 @@ function DocumentViews({ document, isOpen }: Props) {
key={model.id}
title={model.name}
subtitle={subtitle}
image={<Avatar key={model.id} model={model} size={32} />}
image={
<Avatar
key={model.id}
model={model}
size={AvatarSize.Large}
/>
}
border={false}
small
/>
-97
View File
@@ -1,97 +0,0 @@
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { DocumentValidation } from "@shared/validations";
import Document from "~/models/Document";
import ConfirmationDialog from "~/components/ConfirmationDialog";
import Input from "./Input";
import Switch from "./Switch";
import Text from "./Text";
type Props = {
/** The original document to duplicate */
document: Document;
onSubmit: (documents: Document[]) => void;
};
function DuplicateDialog({ document, onSubmit }: Props) {
const { t } = useTranslation();
const defaultTitle = t(`Copy of {{ documentName }}`, {
documentName: document.title,
});
const [publish, setPublish] = React.useState<boolean>(!!document.publishedAt);
const [recursive, setRecursive] = React.useState<boolean>(true);
const [title, setTitle] = React.useState<string>(defaultTitle);
const handlePublishChange = React.useCallback(
(ev: React.ChangeEvent<HTMLInputElement>) => {
setPublish(ev.target.checked);
},
[]
);
const handleRecursiveChange = React.useCallback(
(ev: React.ChangeEvent<HTMLInputElement>) => {
setRecursive(ev.target.checked);
},
[]
);
const handleTitleChange = React.useCallback(
(ev: React.ChangeEvent<HTMLInputElement>) => {
setTitle(ev.target.value);
},
[]
);
const handleSubmit = async () => {
const result = await document.duplicate({
publish,
recursive,
title,
});
onSubmit(result);
};
return (
<ConfirmationDialog onSubmit={handleSubmit} submitText={t("Duplicate")}>
<Input
autoFocus
autoSelect
name="title"
label={t("Title")}
onChange={handleTitleChange}
maxLength={DocumentValidation.maxTitleLength}
defaultValue={defaultTitle}
/>
{!document.isTemplate && (
<>
{document.collectionId && (
<Text size="small">
<Switch
name="publish"
label={t("Publish")}
labelPosition="right"
checked={publish}
onChange={handlePublishChange}
/>
</Text>
)}
{document.publishedAt && document.childDocuments.length > 0 && (
<Text size="small">
<Switch
name="recursive"
label={t("Include nested documents")}
labelPosition="right"
checked={recursive}
onChange={handleRecursiveChange}
/>
</Text>
)}
</>
)}
</ConfirmationDialog>
);
}
export default observer(DuplicateDialog);
+9 -78
View File
@@ -1,6 +1,4 @@
import deburr from "lodash/deburr";
import difference from "lodash/difference";
import sortBy from "lodash/sortBy";
import { observer } from "mobx-react";
import { DOMParser as ProsemirrorDOMParser } from "prosemirror-model";
import { TextSelection } from "prosemirror-state";
@@ -9,10 +7,7 @@ import { mergeRefs } from "react-merge-refs";
import { Optional } from "utility-types";
import insertFiles from "@shared/editor/commands/insertFiles";
import { AttachmentPreset } from "@shared/types";
import { dateLocale, dateToRelative } from "@shared/utils/date";
import { getDataTransferFiles } from "@shared/utils/files";
import parseDocumentSlug from "@shared/utils/parseDocumentSlug";
import { isInternalUrl } from "@shared/utils/urls";
import { AttachmentValidation } from "@shared/validations";
import ClickablePadding from "~/components/ClickablePadding";
import ErrorBoundary from "~/components/ErrorBoundary";
@@ -22,12 +17,8 @@ import useDictionary from "~/hooks/useDictionary";
import useEditorClickHandlers from "~/hooks/useEditorClickHandlers";
import useEmbeds from "~/hooks/useEmbeds";
import useStores from "~/hooks/useStores";
import useUserLocale from "~/hooks/useUserLocale";
import { NotFoundError } from "~/utils/errors";
import { uploadFile } from "~/utils/files";
import { uploadFile, uploadFileFromUrl } from "~/utils/files";
import lazyWithRetry from "~/utils/lazyWithRetry";
import DocumentBreadcrumb from "./DocumentBreadcrumb";
import Icon from "./Icon";
const LazyLoadedEditor = lazyWithRetry(() => import("~/editor"));
@@ -50,82 +41,23 @@ export type Props = Optional<
function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
const { id, shareId, onChange, onCreateCommentMark, onDeleteCommentMark } =
props;
const userLocale = useUserLocale();
const locale = dateLocale(userLocale);
const { comments, documents } = useStores();
const { comments } = useStores();
const dictionary = useDictionary();
const embeds = useEmbeds(!shareId);
const localRef = React.useRef<SharedEditor>();
const preferences = useCurrentUser({ rejectOnEmpty: false })?.preferences;
const previousCommentIds = React.useRef<string[]>();
const handleSearchLink = React.useCallback(
async (term: string) => {
if (isInternalUrl(term)) {
// search for exact internal document
const slug = parseDocumentSlug(term);
if (!slug) {
return [];
}
try {
const document = await documents.fetch(slug);
const time = dateToRelative(Date.parse(document.updatedAt), {
addSuffix: true,
shorten: true,
locale,
});
return [
{
title: document.title,
subtitle: `Updated ${time}`,
url: document.url,
icon: document.icon ? (
<Icon
value={document.icon}
color={document.color ?? undefined}
/>
) : undefined,
},
];
} catch (error) {
// NotFoundError could not find document for slug
if (!(error instanceof NotFoundError)) {
throw error;
}
}
}
// default search for anything that doesn't look like a URL
const results = await documents.searchTitles({ query: term });
return sortBy(
results.map(({ document }) => ({
title: document.title,
subtitle: <DocumentBreadcrumb document={document} onlyText />,
url: document.url,
icon: document.icon ? (
<Icon value={document.icon} color={document.color ?? undefined} />
) : undefined,
})),
(document) =>
deburr(document.title)
.toLowerCase()
.startsWith(deburr(term).toLowerCase())
? -1
: 1
);
},
[locale, documents]
);
const handleUploadFile = React.useCallback(
async (file: File) => {
const result = await uploadFile(file, {
async (file: File | string) => {
const options = {
documentId: id,
preset: AttachmentPreset.DocumentAttachment,
});
};
const result =
file instanceof File
? await uploadFile(file, options)
: await uploadFileFromUrl(file, options);
return result.url;
},
[id]
@@ -263,7 +195,6 @@ function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
dictionary={dictionary}
{...props}
onClickLink={handleClickLink}
onSearchLink={handleSearchLink}
onChange={handleChange}
placeholder={props.placeholder || ""}
defaultValue={props.defaultValue || ""}
+9 -6
View File
@@ -13,15 +13,15 @@ import { useTranslation } from "react-i18next";
import { useLocation } from "react-router-dom";
import styled, { css } from "styled-components";
import EventBoundary from "@shared/components/EventBoundary";
import { s } from "@shared/styles";
import { s, hover } from "@shared/styles";
import Document from "~/models/Document";
import Event from "~/models/Event";
import { Avatar } from "~/components/Avatar";
import { Avatar, AvatarSize } from "~/components/Avatar";
import Item, { Actions, Props as ItemProps } from "~/components/List/Item";
import Time from "~/components/Time";
import { useLocationSidebarContext } from "~/hooks/useLocationSidebarContext";
import useStores from "~/hooks/useStores";
import RevisionMenu from "~/menus/RevisionMenu";
import { hover } from "~/styles";
import Logger from "~/utils/Logger";
import { documentHistoryPath } from "~/utils/routeHelpers";
@@ -35,6 +35,7 @@ const EventListItem = ({ event, latest, document, ...rest }: Props) => {
const { t } = useTranslation();
const { revisions } = useStores();
const location = useLocation();
const sidebarContext = useLocationSidebarContext();
const opts = {
userName: event.actor.name,
};
@@ -66,7 +67,10 @@ const EventListItem = ({ event, latest, document, ...rest }: Props) => {
);
to = {
pathname: documentHistoryPath(document, event.modelId || "latest"),
state: { retainScrollPosition: true },
state: {
sidebarContext,
retainScrollPosition: true,
},
};
break;
@@ -140,7 +144,6 @@ const EventListItem = ({ event, latest, document, ...rest }: Props) => {
title={
<Time
dateTime={event.createdAt}
tooltipDelay={500}
format={{
en_US: "MMM do, h:mm a",
fr_FR: "'Le 'd MMMM 'à' H:mm",
@@ -150,7 +153,7 @@ const EventListItem = ({ event, latest, document, ...rest }: Props) => {
onClick={handleTimeClick}
/>
}
image={<Avatar model={event.actor} size={32} />}
image={<Avatar model={event.actor} size={AvatarSize.Large} />}
subtitle={
<Subtitle>
{icon}
+62 -38
View File
@@ -1,17 +1,26 @@
import { observer } from "mobx-react";
import * as React from "react";
import styled from "styled-components";
import { s } from "@shared/styles";
import User from "~/models/User";
import { Avatar, AvatarSize } from "~/components/Avatar";
import Flex from "~/components/Flex";
import Initials from "./Avatar/Initials";
type Props = {
/** The users to display */
users: User[];
/** The size of the avatars, defaults to AvatarSize.Large */
size?: number;
/** A number to show as the number of additional users */
overflow?: number;
/** The maximum number of users to display, defaults to 8 */
limit?: number;
renderAvatar?: (user: User) => React.ReactNode;
/** A component to render the avatar, defaults to Avatar. */
renderAvatar?: React.ComponentType<
React.ComponentProps<typeof Avatar> & {
model: User;
}
>;
};
function Facepile({
@@ -19,55 +28,70 @@ function Facepile({
overflow = 0,
size = AvatarSize.Large,
limit = 8,
renderAvatar = DefaultAvatar,
renderAvatar = Avatar,
...rest
}: Props) {
const filtered = users.filter(Boolean).slice(-limit);
const Component = renderAvatar;
return (
<Avatars {...rest}>
{overflow > 0 && (
<More size={size}>
<span>
{users.length ? "+" : ""}
{overflow}
</span>
</More>
<Initials size={size} content={String(overflow)}>
{users.length ? "+" : ""}
{overflow}
</Initials>
)}
{users
.filter(Boolean)
.slice(0, limit)
.map((user) => (
<AvatarWrapper key={user.id}>{renderAvatar(user)}</AvatarWrapper>
))}
{filtered.map((model, index) => {
const lastChild = index === 0 && overflow <= 0;
return (
<Component
key={model.id}
{...{
model,
size,
style: {
marginRight: lastChild ? 0 : -4,
...(lastChild || filtered.length === 1
? {}
: { clipPath: `url(#${clipPathId(size)})` }),
},
}}
/>
);
})}
<FacepileClip size={size} />
</Avatars>
);
}
function DefaultAvatar(user: User) {
return <Avatar model={user} size={AvatarSize.Large} />;
function FacepileClip({ size }: { size: number }) {
return (
<SVG
width="25"
height="28"
viewBox="0 0 25 28"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<clipPath id={clipPathId(size)}>
<path
transform={size !== 28 ? `scale(${size / 28})` : ""}
d="M14.0633 0.5C18.1978 0.5 21.8994 2.34071 24.3876 5.24462C22.8709 7.81315 22.0012 10.8061 22.0012 14C22.0012 17.1939 22.8709 20.1868 24.3876 22.7554C21.8994 25.6593 18.1978 27.5 14.0633 27.5C6.57035 27.5 0.5 21.4537 0.5 14C0.5 6.54628 6.57035 0.5 14.0633 0.5Z"
/>
</clipPath>
</SVG>
);
}
const AvatarWrapper = styled.div`
margin-right: -8px;
function clipPathId(size: number) {
return `facepile-${size}`;
}
&:first-child {
margin-right: 0;
}
`;
const More = styled.div<{ size: number }>`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-width: ${(props) => props.size}px;
height: ${(props) => props.size}px;
border-radius: 100%;
background: ${(props) => props.theme.textTertiary};
color: ${s("white")};
border: 2px solid ${s("background")};
text-align: center;
font-size: 12px;
font-weight: 600;
const SVG = styled.svg`
position: absolute;
top: 0;
left: 0;
`;
const Avatars = styled(Flex)`
+2 -2
View File
@@ -46,7 +46,7 @@ const FilterOptions = ({
const searchInputRef = React.useRef<HTMLInputElement>(null);
const listRef = React.useRef<HTMLDivElement | null>(null);
const menu = useMenuState({
modal: true,
modal: false,
});
const selectedItems = options.filter((option) =>
selectedKeys.includes(option.key)
@@ -229,7 +229,7 @@ const SearchInput = styled(Input)`
${Outline} {
border: none;
border-radius: 0;
border-bottom: 1px solid ${s("inputBorder")};
border-bottom: 1px solid ${s("divider")};
background: ${s("menuBackground")};
}
-89
View File
@@ -1,89 +0,0 @@
import { observer } from "mobx-react";
import { GroupIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { MAX_AVATAR_DISPLAY } from "@shared/constants";
import { s } from "@shared/styles";
import Group from "~/models/Group";
import GroupMembership from "~/models/GroupMembership";
import GroupMembers from "~/scenes/GroupMembers";
import Facepile from "~/components/Facepile";
import Flex from "~/components/Flex";
import ListItem from "~/components/List/Item";
import Modal from "~/components/Modal";
import useBoolean from "~/hooks/useBoolean";
import { hover } from "~/styles";
import NudeButton from "./NudeButton";
type Props = {
group: Group;
membership?: GroupMembership;
showFacepile?: boolean;
showAvatar?: boolean;
renderActions: (params: { openMembersModal: () => void }) => React.ReactNode;
};
function GroupListItem({ group, showFacepile, renderActions }: Props) {
const { t } = useTranslation();
const [membersModalOpen, setMembersModalOpen, setMembersModalClosed] =
useBoolean();
const memberCount = group.memberCount;
const users = group.users.slice(0, MAX_AVATAR_DISPLAY);
const overflow = memberCount - users.length;
return (
<>
<ListItem
image={
<Image>
<GroupIcon size={24} />
</Image>
}
title={<Title onClick={setMembersModalOpen}>{group.name}</Title>}
subtitle={t("{{ count }} member", { count: memberCount })}
actions={
<Flex align="center" gap={8}>
{showFacepile && (
<NudeButton
width="auto"
height="auto"
onClick={setMembersModalOpen}
>
<Facepile users={users} overflow={overflow} />
</NudeButton>
)}
{renderActions({
openMembersModal: setMembersModalOpen,
})}
</Flex>
}
/>
<Modal
title={t("Group members")}
onRequestClose={setMembersModalClosed}
isOpen={membersModalOpen}
>
<GroupMembers group={group} />
</Modal>
</>
);
}
const Image = styled(Flex)`
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: ${s("backgroundSecondary")};
border-radius: 32px;
`;
const Title = styled.span`
&: ${hover} {
text-decoration: underline;
cursor: var(--pointer);
}
`;
export default observer(GroupListItem);
-1
View File
@@ -94,7 +94,6 @@ const Scene = styled.div`
align-items: flex-start;
width: 350px;
background: ${s("background")};
transition: ${s("backgroundTransition")};
border-radius: 8px;
outline: none;
opacity: 0;
+56 -37
View File
@@ -3,8 +3,10 @@ import { observer } from "mobx-react";
import { MenuIcon } from "outline-icons";
import { transparentize } from "polished";
import * as React from "react";
import { mergeRefs } from "react-merge-refs";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import { useComponentSize } from "@shared/hooks/useComponentSize";
import { depths, s } from "@shared/styles";
import { supportsPassiveListener } from "@shared/utils/browser";
import Button from "~/components/Button";
@@ -15,22 +17,29 @@ import useMobile from "~/hooks/useMobile";
import useStores from "~/hooks/useStores";
import { draggableOnDesktop, fadeOnDesktopBackgrounded } from "~/styles";
import Desktop from "~/utils/Desktop";
import { TooltipProvider } from "./TooltipContext";
export const HEADER_HEIGHT = 64;
type Props = {
left?: React.ReactNode;
title: React.ReactNode;
actions?: React.ReactNode;
actions?:
| ((props: { isCompact: boolean }) => React.ReactNode)
| React.ReactNode;
hasSidebar?: boolean;
className?: string;
};
function Header({ left, title, actions, hasSidebar, className }: Props) {
function Header(
{ left, title, actions, hasSidebar, className }: Props,
ref: React.RefObject<HTMLDivElement> | null
) {
const { ui } = useStores();
const isMobile = useMobile();
const hasMobileSidebar = hasSidebar && isMobile;
const internalRef = React.useRef<HTMLDivElement | null>(null);
const breadcrumbsRef = React.useRef<HTMLDivElement | null>(null);
const passThrough = !actions && !left && !title;
const [isScrolled, setScrolled] = React.useState(false);
@@ -53,38 +62,50 @@ function Header({ left, title, actions, hasSidebar, className }: Props) {
});
}, []);
return (
<Wrapper
align="center"
shrink={false}
className={className}
$passThrough={passThrough}
$insetTitleAdjust={ui.sidebarIsClosed && Desktop.hasInsetTitlebar()}
>
{left || hasMobileSidebar ? (
<Breadcrumbs>
{hasMobileSidebar && (
<MobileMenuButton
onClick={ui.toggleMobileSidebar}
icon={<MenuIcon />}
neutral
/>
)}
{left}
</Breadcrumbs>
) : null}
const setBreadcrumbRef = React.useCallback((node: HTMLDivElement | null) => {
breadcrumbsRef.current = node?.firstElementChild as HTMLDivElement;
}, []);
{isScrolled ? (
<Title onClick={handleClickTitle}>
<Fade>{title}</Fade>
</Title>
) : (
<div />
)}
<Actions align="center" justify="flex-end">
{actions}
</Actions>
</Wrapper>
const size = useComponentSize(internalRef);
const breadcrumbsSize = useComponentSize(breadcrumbsRef);
const breadcrumbMakesCompact = breadcrumbsSize.width > size.width / 3;
const isCompact = size.width < 1000 || breadcrumbMakesCompact;
return (
<TooltipProvider>
<Wrapper
ref={mergeRefs([ref, internalRef])}
align="center"
shrink={false}
className={className}
$passThrough={passThrough}
$insetTitleAdjust={ui.sidebarIsClosed && Desktop.hasInsetTitlebar()}
>
{left || hasMobileSidebar ? (
<Breadcrumbs ref={setBreadcrumbRef}>
{hasMobileSidebar && (
<MobileMenuButton
onClick={ui.toggleMobileSidebar}
icon={<MenuIcon />}
neutral
/>
)}
{left}
</Breadcrumbs>
) : null}
{isScrolled && !isCompact ? (
<Title onClick={handleClickTitle}>
<Fade>{title}</Fade>
</Title>
) : (
<div />
)}
<Actions align="center" justify="flex-end">
{typeof actions === "function" ? actions({ isCompact }) : actions}
</Actions>
</Wrapper>
</TooltipProvider>
);
}
@@ -130,7 +151,6 @@ const Wrapper = styled(Flex)<WrapperProps>`
`};
padding: 12px;
transition: all 100ms ease-out;
transform: translate3d(0, 0, 0);
min-height: ${HEADER_HEIGHT}px;
justify-content: flex-start;
@@ -152,7 +172,6 @@ const Wrapper = styled(Flex)<WrapperProps>`
${breakpoint("tablet")`
padding: 16px;
justify-content: center;
${(props: WrapperProps) => props.$insetTitleAdjust && `padding-left: 64px;`}
`};
`;
@@ -191,4 +210,4 @@ const MobileMenuButton = styled(Button)`
}
`;
export default observer(Header);
export default observer(React.forwardRef(Header));
@@ -1,13 +1,12 @@
import { BackIcon } from "outline-icons";
import React from "react";
import styled from "styled-components";
import { breakpoints, s } from "@shared/styles";
import { breakpoints, s, hover } from "@shared/styles";
import { colorPalette } from "@shared/utils/collections";
import { validateColorHex } from "@shared/utils/color";
import Flex from "~/components/Flex";
import NudeButton from "~/components/NudeButton";
import Text from "~/components/Text";
import { hover } from "~/styles";
enum Panel {
Builtin,
@@ -1,7 +1,6 @@
import styled from "styled-components";
import { s } from "@shared/styles";
import { s, hover } from "@shared/styles";
import NudeButton from "~/components/NudeButton";
import { hover } from "~/styles";
export const IconButton = styled(NudeButton)<{ delay?: number }>`
width: 32px;
@@ -1,7 +1,6 @@
import styled, { css } from "styled-components";
import { s } from "@shared/styles";
import { s, hover } from "@shared/styles";
import NudeButton from "~/components/NudeButton";
import { hover } from "~/styles";
export const PopoverButton = styled(NudeButton)<{ $borderOnHover?: boolean }>`
&: ${hover},
@@ -2,13 +2,12 @@ import React from "react";
import { useTranslation } from "react-i18next";
import { Menu, MenuButton, MenuItem, useMenuState } from "reakit";
import styled from "styled-components";
import { depths, s } from "@shared/styles";
import { depths, s, hover } from "@shared/styles";
import { EmojiSkinTone } from "@shared/types";
import { getEmojiVariants } from "@shared/utils/emoji";
import { Emoji } from "~/components/Emoji";
import Flex from "~/components/Flex";
import NudeButton from "~/components/NudeButton";
import { hover } from "~/styles";
import { IconButton } from "./IconButton";
const SkinTonePicker = ({
+2 -3
View File
@@ -10,19 +10,18 @@ import {
useTabState,
} from "reakit";
import styled, { css } from "styled-components";
import { s } from "@shared/styles";
import Icon from "@shared/components/Icon";
import { s, hover } from "@shared/styles";
import theme from "@shared/styles/theme";
import { IconType } from "@shared/types";
import { determineIconType } from "@shared/utils/icon";
import Flex from "~/components/Flex";
import Icon from "~/components/Icon";
import NudeButton from "~/components/NudeButton";
import Popover from "~/components/Popover";
import useMobile from "~/hooks/useMobile";
import useOnClickOutside from "~/hooks/useOnClickOutside";
import usePrevious from "~/hooks/usePrevious";
import useWindowSize from "~/hooks/useWindowSize";
import { hover } from "~/styles";
import EmojiPanel from "./components/EmojiPanel";
import IconPanel from "./components/IconPanel";
import { PopoverButton } from "./components/PopoverButton";
+1 -1
View File
@@ -2,9 +2,9 @@ import { observer } from "mobx-react";
import { CollectionIcon, PrivateCollectionIcon } from "outline-icons";
import { getLuminance } from "polished";
import * as React from "react";
import Icon from "@shared/components/Icon";
import { colorPalette } from "@shared/utils/collections";
import Collection from "~/models/Collection";
import Icon from "~/components/Icon";
import useStores from "~/hooks/useStores";
type Props = {
+1 -1
View File
@@ -4,9 +4,9 @@ import * as React from "react";
import { useTranslation } from "react-i18next";
import { useHistory } from "react-router-dom";
import styled, { useTheme } from "styled-components";
import { isModKey } from "@shared/utils/keyboard";
import useBoolean from "~/hooks/useBoolean";
import useKeyDown from "~/hooks/useKeyDown";
import { isModKey } from "~/utils/keyboard";
import { searchPath } from "~/utils/routeHelpers";
import Input, { Outline } from "./Input";
+4 -2
View File
@@ -47,14 +47,16 @@ export default function LanguagePrompt() {
<br />
<Link
onClick={async () => {
ui.setLanguagePromptDismissed();
ui.set({ languagePromptDismissed: true });
await user.save({ language });
}}
>
{t("Change Language")}
</Link>{" "}
&middot;{" "}
<Link onClick={ui.setLanguagePromptDismissed}>{t("Dismiss")}</Link>
<Link onClick={() => ui.set({ languagePromptDismissed: true })}>
{t("Dismiss")}
</Link>
</span>
</Flex>
</Wrapper>
+1 -2
View File
@@ -4,6 +4,7 @@ import { Helmet } from "react-helmet-async";
import styled, { DefaultTheme } from "styled-components";
import breakpoint from "styled-components-breakpoint";
import { s } from "@shared/styles";
import { isModKey } from "@shared/utils/keyboard";
import Flex from "~/components/Flex";
import { LoadingIndicatorBar } from "~/components/LoadingIndicator";
import SkipNavContent from "~/components/SkipNavContent";
@@ -13,7 +14,6 @@ import useAutoRefresh from "~/hooks/useAutoRefresh";
import useKeyDown from "~/hooks/useKeyDown";
import { MenuProvider } from "~/hooks/useMenuContext";
import useStores from "~/hooks/useStores";
import { isModKey } from "~/utils/keyboard";
type Props = {
children?: React.ReactNode;
@@ -76,7 +76,6 @@ const Layout = React.forwardRef(function Layout_(
const Container = styled(Flex)`
background: ${s("background")};
transition: ${s("backgroundTransition")};
position: relative;
width: 100%;
min-height: 100%;
+1 -2
View File
@@ -6,10 +6,9 @@ import { LocationDescriptor } from "history";
import * as React from "react";
import scrollIntoView from "scroll-into-view-if-needed";
import styled, { useTheme } from "styled-components";
import { s, ellipsis } from "@shared/styles";
import { s, hover, ellipsis } from "@shared/styles";
import Flex from "~/components/Flex";
import NavLink from "~/components/NavLink";
import { hover } from "~/styles";
export type Props = Omit<React.HTMLAttributes<HTMLAnchorElement>, "title"> & {
/** An icon or image to display to the left of the list item */
+1 -3
View File
@@ -23,7 +23,6 @@ function eachMinute(fn: () => void) {
export type Props = {
children?: React.ReactNode;
dateTime: string;
tooltipDelay?: number;
addSuffix?: boolean;
shorten?: boolean;
relative?: boolean;
@@ -37,7 +36,6 @@ const LocaleTime: React.FC<Props> = ({
shorten,
format,
relative,
tooltipDelay,
}: Props) => {
const userLocale = useUserLocale();
const dateFormatLong: Record<string, string> = {
@@ -82,7 +80,7 @@ const LocaleTime: React.FC<Props> = ({
});
return (
<Tooltip content={tooltipContent} delay={tooltipDelay} placement="bottom">
<Tooltip content={tooltipContent} placement="bottom">
<time dateTime={dateTime}>{children || content}</time>
</Tooltip>
);
-2
View File
@@ -174,7 +174,6 @@ const Fullscreen = styled.div<FullscreenProps>`
justify-content: center;
align-items: flex-start;
background: ${s("background")};
transition: ${s("backgroundTransition")};
outline: none;
${breakpoint("tablet")`
@@ -265,7 +264,6 @@ const Small = styled.div`
justify-content: center;
align-items: flex-start;
background: ${s("modalBackground")};
transition: ${s("backgroundTransition")};
box-shadow: ${s("modalShadow")};
border-radius: 8px;
outline: none;
@@ -4,11 +4,10 @@ import * as React from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import styled from "styled-components";
import { s } from "@shared/styles";
import { s, hover, truncateMultiline } from "@shared/styles";
import Notification from "~/models/Notification";
import CommentEditor from "~/scenes/Document/components/CommentEditor";
import useStores from "~/hooks/useStores";
import { hover, truncateMultiline } from "~/styles";
import { Avatar, AvatarSize } from "../Avatar";
import Flex from "../Flex";
import Text from "../Text";
@@ -52,11 +51,7 @@ function NotificationListItem({ notification, onNavigate }: Props) {
<Text weight="bold">{notification.subject}</Text>
</Text>
<Text type="tertiary" size="xsmall">
<Time
dateTime={notification.createdAt}
tooltipDelay={1000}
addSuffix
/>{" "}
<Time dateTime={notification.createdAt} addSuffix />{" "}
{collection && <>&middot; {collection.name}</>}
</Text>
{notification.comment && (
@@ -3,13 +3,12 @@ import { MarkAsReadIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { s } from "@shared/styles";
import { s, hover } from "@shared/styles";
import Notification from "~/models/Notification";
import { markNotificationsAsRead } from "~/actions/definitions/notifications";
import useActionContext from "~/hooks/useActionContext";
import useStores from "~/hooks/useStores";
import NotificationMenu from "~/menus/NotificationMenu";
import { hover } from "~/styles";
import Desktop from "~/utils/Desktop";
import Empty from "../Empty";
import ErrorBoundary from "../ErrorBoundary";
@@ -60,7 +59,7 @@ function Notifications(
</Text>
<Flex gap={8}>
{notifications.approximateUnreadCount > 0 && (
<Tooltip delay={500} content={t("Mark all as read")}>
<Tooltip content={t("Mark all as read")}>
<Button action={markNotificationsAsRead} context={context}>
<MarkAsReadIcon />
</Button>
+15 -1
View File
@@ -10,13 +10,23 @@ import { fadeAndScaleIn } from "~/styles/animations";
type Props = PopoverProps & {
children: React.ReactNode;
/** The width of the popover, defaults to 380px. */
width?: number;
/** The minimum width of the popover, use instead of width if contents adjusts size. */
minWidth?: number;
/** Shrink the padding of the popover */
shrink?: boolean;
/** Make the popover flex */
flex?: boolean;
/** The tab index of the popover */
tabIndex?: number;
/** Whether the popover should be scrollable, defaults to true. */
scrollable?: boolean;
/** The position of the popover on mobile, defaults to "top". */
mobilePosition?: "top" | "bottom";
/** Function to show the popover */
show: () => void;
/** Function to hide the popover */
hide: () => void;
};
@@ -25,6 +35,7 @@ const Popover = (
children,
shrink,
width = 380,
minWidth,
scrollable = true,
flex,
mobilePosition,
@@ -71,6 +82,7 @@ const Popover = (
ref={ref}
$shrink={shrink}
$width={width}
$minWidth={minWidth}
$scrollable={scrollable}
$flex={flex}
>
@@ -83,6 +95,7 @@ const Popover = (
type ContentsProps = {
$shrink?: boolean;
$width?: number;
$minWidth?: number;
$flex?: boolean;
$scrollable: boolean;
$mobilePosition?: "top" | "bottom";
@@ -101,7 +114,8 @@ const Contents = styled.div<ContentsProps>`
padding: ${(props) => (props.$shrink ? "6px 0" : "12px 24px")};
max-height: 75vh;
box-shadow: ${s("menuShadow")};
width: ${(props) => props.$width}px;
${(props) => props.$width && `width: ${props.$width}px`};
${(props) => props.$minWidth && `min-width: ${props.$minWidth}px`};
${(props) =>
props.$scrollable
+2 -4
View File
@@ -3,7 +3,7 @@ import { transparentize } from "polished";
import React from "react";
import { useTranslation } from "react-i18next";
import styled, { css } from "styled-components";
import { s } from "@shared/styles";
import { s, hover } from "@shared/styles";
import type { ReactionSummary } from "@shared/types";
import { getEmojiId } from "@shared/utils/emoji";
import User from "~/models/User";
@@ -13,7 +13,6 @@ import NudeButton from "~/components/NudeButton";
import Text from "~/components/Text";
import Tooltip from "~/components/Tooltip";
import useCurrentUser from "~/hooks/useCurrentUser";
import { hover } from "~/styles";
type Props = {
/** Thin reaction data - contains the emoji & active user ids for this reaction. */
@@ -128,7 +127,7 @@ const Reaction: React.FC<Props> = ({
);
return tooltipContent ? (
<Tooltip content={tooltipContent} delay={250} placement="bottom">
<Tooltip content={tooltipContent} placement="bottom">
{DisplayedEmoji}
</Tooltip>
) : (
@@ -144,7 +143,6 @@ const EmojiButton = styled(NudeButton)<{
height: 28px;
padding: 6px;
border-radius: 12px;
transition: ${s("backgroundTransition")};
background: ${s("backgroundTertiary")};
pointer-events: ${({ disabled }) => disabled && "none"};
+1 -6
View File
@@ -98,12 +98,7 @@ const ReactionPicker: React.FC<Props> = ({
<>
<PopoverDisclosure {...popover}>
{(props) => (
<Tooltip
content={t("Add reaction")}
placement="top"
delay={500}
hideOnClick
>
<Tooltip content={t("Add reaction")} placement="top" hideOnClick>
<NudeButton
{...props}
aria-label={t("Reaction picker")}
@@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next";
import { Tab, TabPanel, useTabState } from "reakit";
import { toast } from "sonner";
import styled, { css } from "styled-components";
import { s } from "@shared/styles";
import { s, hover } from "@shared/styles";
import Comment from "~/models/Comment";
import { Avatar, AvatarSize } from "~/components/Avatar";
import { Emoji } from "~/components/Emoji";
@@ -13,7 +13,6 @@ import Flex from "~/components/Flex";
import PlaceholderText from "~/components/PlaceholderText";
import Text from "~/components/Text";
import useStores from "~/hooks/useStores";
import { hover } from "~/styles";
type Props = {
/** Model for which to show the reactions. */
+1 -1
View File
@@ -1,7 +1,7 @@
import { m, TargetAndTransition } from "framer-motion";
import * as React from "react";
import { mergeRefs } from "react-merge-refs";
import useComponentSize from "~/hooks/useComponentSize";
import { useComponentSize } from "@shared/hooks/useComponentSize";
type Props = {
/** The children to render */
+1 -2
View File
@@ -7,10 +7,9 @@ import * as React from "react";
import { Link } from "react-router-dom";
import styled, { css } from "styled-components";
import breakpoint from "styled-components-breakpoint";
import { s, ellipsis } from "@shared/styles";
import { s, hover, ellipsis } from "@shared/styles";
import Document from "~/models/Document";
import Highlight, { Mark } from "~/components/Highlight";
import { hover } from "~/styles";
import { sharedDocumentPath } from "~/utils/routeHelpers";
type Props = {
@@ -201,11 +201,7 @@ export const AccessControlList = observer(
<ListItem
key={membership.id}
image={
<Avatar
model={membership.user}
size={AvatarSize.Medium}
showBorder={false}
/>
<Avatar model={membership.user} size={AvatarSize.Medium} />
}
title={membership.user.name}
subtitle={membership.user.email}
@@ -146,7 +146,7 @@ export const AccessControlList = observer(
/>
) : (
<ListItem
image={<Avatar model={user} showBorder={false} />}
image={<Avatar model={user} />}
title={user.name}
subtitle={t("You have full access")}
actions={<AccessTooltip>{t("Can edit")}</AccessTooltip>}
@@ -160,9 +160,7 @@ export const AccessControlList = observer(
) : document.isDraft ? (
<>
<ListItem
image={
<Avatar model={document.createdBy} showBorder={false} />
}
image={<Avatar model={document.createdBy} />}
title={document.createdBy?.name}
actions={
<AccessTooltip content={t("Created the document")}>
@@ -73,9 +73,7 @@ const DocumentMemberListItem = ({
return (
<ListItem
title={user.name}
image={
<Avatar model={user} size={AvatarSize.Medium} showBorder={false} />
}
image={<Avatar model={user} size={AvatarSize.Medium} />}
subtitle={
membership?.sourceId ? (
<Trans>
@@ -119,7 +119,7 @@ function PublicAccess({ document, share, sharedParent }: Props) {
: share?.url ?? "";
const copyButton = (
<Tooltip content={t("Copy public link")} delay={500} placement="top">
<Tooltip content={t("Copy public link")} placement="top">
<CopyToClipboard text={shareUrl} onCopy={handleCopied}>
<NudeButton type="button" disabled={!share} style={{ marginRight: 3 }}>
<CopyIcon color={theme.placeholder} size={18} />
@@ -31,7 +31,7 @@ export function CopyLinkButton({
}, [onCopy, t]);
return (
<Tooltip content={t("Copy link")} delay={500} placement="top">
<Tooltip content={t("Copy link")} placement="top">
<CopyToClipboard text={url} onCopy={handleCopied}>
<NudeButton type="button">
<LinkIcon size={20} />
@@ -1,7 +1,7 @@
import { PlusIcon } from "outline-icons";
import styled from "styled-components";
import { hover } from "@shared/styles";
import BaseListItem from "~/components/List/Item";
import { hover } from "~/styles";
export const InviteIcon = styled(PlusIcon)`
opacity: 0;
@@ -5,7 +5,7 @@ import { CheckmarkIcon, CloseIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { s } from "@shared/styles";
import { s, hover } from "@shared/styles";
import { stringToColor } from "@shared/utils/color";
import Collection from "~/models/Collection";
import Document from "~/models/Document";
@@ -20,7 +20,6 @@ import useCurrentUser from "~/hooks/useCurrentUser";
import useMaxHeight from "~/hooks/useMaxHeight";
import useStores from "~/hooks/useStores";
import useThrottledCallback from "~/hooks/useThrottledCallback";
import { hover } from "~/styles";
import { InviteIcon, ListItem } from "./ListItem";
type Suggestion = IAvatar & {
@@ -159,13 +158,7 @@ export const Suggestions = observer(
: suggestion.isViewer
? t("Viewer")
: t("Editor"),
image: (
<Avatar
model={suggestion}
size={AvatarSize.Medium}
showBorder={false}
/>
),
image: <Avatar model={suggestion} size={AvatarSize.Medium} />,
};
}
+1 -2
View File
@@ -1,9 +1,8 @@
import { darken } from "polished";
import styled from "styled-components";
import Flex from "@shared/components/Flex";
import { s } from "@shared/styles";
import { s, hover } from "@shared/styles";
import NudeButton from "~/components/NudeButton";
import { hover } from "~/styles";
// TODO: Temp until Button/NudeButton styles are normalized
export const Wrapper = styled.div`
+1 -2
View File
@@ -5,6 +5,7 @@ import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { metaDisplay } from "@shared/utils/keyboard";
import Flex from "~/components/Flex";
import Scrollable from "~/components/Scrollable";
import Text from "~/components/Text";
@@ -14,7 +15,6 @@ import useCurrentUser from "~/hooks/useCurrentUser";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
import OrganizationMenu from "~/menus/OrganizationMenu";
import { metaDisplay } from "~/utils/keyboard";
import { homePath, draftsPath, searchPath } from "~/utils/routeHelpers";
import TeamLogo from "../TeamLogo";
import Tooltip from "../Tooltip";
@@ -80,7 +80,6 @@ function AppSidebar() {
<Tooltip
content={t("Toggle sidebar")}
shortcut={`${metaDisplay}+.`}
delay={500}
>
<ToggleButton
position="bottom"
+3 -3
View File
@@ -32,13 +32,13 @@ function Right({ children, border, className }: Props) {
Math.min(window.innerWidth - event.pageX, maxWidth),
minWidth
);
ui.setRightSidebarWidth(width);
ui.set({ sidebarRightWidth: width });
},
[minWidth, maxWidth, ui]
);
const handleReset = React.useCallback(() => {
ui.setRightSidebarWidth(theme.sidebarRightWidth);
ui.set({ sidebarRightWidth: theme.sidebarRightWidth });
}, [ui, theme.sidebarRightWidth]);
const handleStopDrag = React.useCallback(() => {
@@ -128,7 +128,7 @@ const Sidebar = styled(m.div)<{
max-width: 80%;
border-left: 1px solid ${s("divider")};
transition: border-left 100ms ease-in-out;
z-index: 1;
z-index: ${depths.sidebar};
${breakpoint("mobile", "tablet")`
display: flex;
+2 -6
View File
@@ -5,12 +5,12 @@ import * as React from "react";
import { useTranslation } from "react-i18next";
import { useHistory, useLocation } from "react-router-dom";
import styled from "styled-components";
import { metaDisplay } from "@shared/utils/keyboard";
import Flex from "~/components/Flex";
import Scrollable from "~/components/Scrollable";
import useSettingsConfig from "~/hooks/useSettingsConfig";
import useStores from "~/hooks/useStores";
import isCloudHosted from "~/utils/isCloudHosted";
import { metaDisplay } from "~/utils/keyboard";
import { settingsPath } from "~/utils/routeHelpers";
import Tooltip from "../Tooltip";
import Sidebar from "./Sidebar";
@@ -42,11 +42,7 @@ function SettingsSidebar() {
image={<StyledBackIcon />}
onClick={returnToApp}
>
<Tooltip
content={t("Toggle sidebar")}
shortcut={`${metaDisplay}+.`}
delay={500}
>
<Tooltip content={t("Toggle sidebar")} shortcut={`${metaDisplay}+.`}>
<ToggleButton
position="bottom"
image={<SidebarIcon />}
+8 -8
View File
@@ -3,17 +3,18 @@ import { SidebarIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { hover } from "@shared/styles";
import { NavigationNode } from "@shared/types";
import { metaDisplay } from "@shared/utils/keyboard";
import Flex from "~/components/Flex";
import Scrollable from "~/components/Scrollable";
import SearchPopover from "~/components/SearchPopover";
import Tooltip from "~/components/Tooltip";
import useCurrentUser from "~/hooks/useCurrentUser";
import useStores from "~/hooks/useStores";
import { hover } from "~/styles";
import history from "~/utils/history";
import { metaDisplay } from "~/utils/keyboard";
import { homePath, sharedDocumentPath } from "~/utils/routeHelpers";
import { AvatarSize } from "../Avatar";
import { useTeamContext } from "../TeamContext";
import TeamLogo from "../TeamLogo";
import Sidebar from "./Sidebar";
@@ -40,7 +41,9 @@ function SharedSidebar({ rootNode, shareId }: Props) {
{teamAvailable && (
<SidebarButton
title={team.name}
image={<TeamLogo model={team} size={32} alt={t("Logo")} />}
image={
<TeamLogo model={team} size={AvatarSize.XLarge} alt={t("Logo")} />
}
onClick={() =>
history.push(
user ? homePath() : sharedDocumentPath(shareId, rootNode.url)
@@ -67,6 +70,7 @@ function SharedSidebar({ rootNode, shareId }: Props) {
depth={0}
shareId={shareId}
node={rootNode}
prefetchDocument={documents.prefetchDocument}
activeDocumentId={ui.activeDocumentId}
activeDocument={documents.active}
/>
@@ -81,11 +85,7 @@ const ToggleSidebar = () => {
const { ui } = useStores();
return (
<Tooltip
content={t("Toggle sidebar")}
shortcut={`${metaDisplay}+.`}
delay={500}
>
<Tooltip content={t("Toggle sidebar")} shortcut={`${metaDisplay}+.`}>
<ToggleButton
position="bottom"
image={<SidebarIcon />}
+18 -19
View File
@@ -17,6 +17,7 @@ import { fadeIn } from "~/styles/animations";
import Desktop from "~/utils/Desktop";
import NotificationIcon from "../Notifications/NotificationIcon";
import NotificationsPopover from "../Notifications/NotificationsPopover";
import { TooltipProvider } from "../TooltipContext";
import ResizeBorder from "./components/ResizeBorder";
import SidebarButton, { SidebarButtonProps } from "./components/SidebarButton";
import ToggleButton from "./components/ToggleButton";
@@ -46,7 +47,6 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
const maxWidth = theme.sidebarMaxWidth;
const minWidth = theme.sidebarMinWidth + 16; // padding
const setWidth = ui.setSidebarWidth;
const [offset, setOffset] = React.useState(0);
const [isHovering, setHovering] = React.useState(false);
const [isAnimating, setAnimating] = React.useState(false);
@@ -62,13 +62,13 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
const width = Math.min(event.pageX - offset, maxWidth);
const isSmallerThanCollapsePoint = width < minWidth / 2;
if (isSmallerThanCollapsePoint) {
setWidth(theme.sidebarCollapsedWidth);
} else {
setWidth(width);
}
ui.set({
sidebarWidth: isSmallerThanCollapsePoint
? theme.sidebarCollapsedWidth
: width,
});
},
[theme, offset, minWidth, maxWidth, setWidth]
[ui, theme, offset, minWidth, maxWidth]
);
const handleStopDrag = React.useCallback(() => {
@@ -86,13 +86,13 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
setCollapsing(true);
ui.collapseSidebar();
} else {
setWidth(minWidth);
ui.set({ sidebarWidth: minWidth });
setAnimating(true);
}
} else {
setWidth(width);
ui.set({ sidebarWidth: width });
}
}, [ui, isSmallerThanMinimum, minWidth, width, setWidth]);
}, [ui, isSmallerThanMinimum, minWidth, width]);
const handleBlur = React.useCallback(() => {
setHovering(false);
@@ -149,11 +149,11 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
React.useEffect(() => {
if (isCollapsing) {
setTimeout(() => {
setWidth(minWidth);
ui.set({ sidebarWidth: minWidth });
setCollapsing(false);
}, ANIMATION_MS);
}
}, [setWidth, minWidth, isCollapsing]);
}, [ui, minWidth, isCollapsing]);
React.useEffect(() => {
if (isResizing) {
@@ -174,7 +174,7 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
}, [isResizing, handleDrag, handleBlur, handleStopDrag]);
const handleReset = React.useCallback(() => {
ui.setSidebarWidth(theme.sidebarWidth);
ui.set({ sidebarWidth: theme.sidebarWidth });
}, [ui, theme.sidebarWidth]);
React.useEffect(() => {
@@ -195,8 +195,9 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
);
return (
<>
<TooltipProvider>
<Container
id="sidebar"
ref={ref}
style={style}
$hidden={hidden}
@@ -227,7 +228,6 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
alt={user.name}
model={user}
size={24}
showBorder={false}
style={{ marginLeft: 4 }}
/>
}
@@ -243,7 +243,7 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
/>
</Container>
{ui.mobileSidebarVisible && <Backdrop onClick={ui.toggleMobileSidebar} />}
</>
</TooltipProvider>
);
});
@@ -299,9 +299,8 @@ const Container = styled(Flex)<ContainerProps>`
width: 100%;
background: ${s("sidebarBackground")};
transition: box-shadow 150ms ease-in-out, transform 150ms ease-out,
${s("backgroundTransition")}
${(props: ContainerProps) =>
props.$isAnimating ? `,width ${ANIMATION_MS}ms ease-out` : ""};
${(props: ContainerProps) =>
props.$isAnimating ? `,width ${ANIMATION_MS}ms ease-out` : ""};
transform: translateX(
${(props) => (props.$mobileSidebarVisible ? 0 : "-100%")}
);
@@ -2,26 +2,29 @@ import { Location } from "history";
import { observer } from "mobx-react";
import { PlusIcon } from "outline-icons";
import * as React from "react";
import { useDrop } from "react-dnd";
import { useTranslation } from "react-i18next";
import { CollectionValidation } from "@shared/validations";
import { mergeRefs } from "react-merge-refs";
import { useHistory } from "react-router-dom";
import { UserPreference } from "@shared/types";
import { ProsemirrorHelper } from "@shared/utils/ProsemirrorHelper";
import { CollectionValidation, DocumentValidation } from "@shared/validations";
import Collection from "~/models/Collection";
import Document from "~/models/Document";
import ConfirmMoveDialog from "~/components/ConfirmMoveDialog";
import Fade from "~/components/Fade";
import CollectionIcon from "~/components/Icons/CollectionIcon";
import NudeButton from "~/components/NudeButton";
import { createDocument } from "~/actions/definitions/documents";
import useActionContext from "~/hooks/useActionContext";
import useBoolean from "~/hooks/useBoolean";
import useCurrentUser from "~/hooks/useCurrentUser";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
import CollectionMenu from "~/menus/CollectionMenu";
import { documentEditPath } from "~/utils/routeHelpers";
import { useDropToChangeCollection } from "../hooks/useDragAndDrop";
import DropToImport from "./DropToImport";
import EditableTitle, { RefHandle } from "./EditableTitle";
import Relative from "./Relative";
import { SidebarContextType, useSidebarContext } from "./SidebarContext";
import SidebarLink, { DragObject } from "./SidebarLink";
import SidebarLink from "./SidebarLink";
type Props = {
collection: Collection;
@@ -41,12 +44,14 @@ const CollectionLink: React.FC<Props> = ({
depth,
onClick,
}: Props) => {
const { dialogs, documents, collections } = useStores();
const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean();
const [isEditing, setIsEditing] = React.useState(false);
const { documents } = useStores();
const history = useHistory();
const can = usePolicy(collection);
const { t } = useTranslation();
const sidebarContext = useSidebarContext();
const user = useCurrentUser();
const editableTitleRef = React.useRef<RefHandle>(null);
const handleTitleChange = React.useCallback(
@@ -58,119 +63,127 @@ const CollectionLink: React.FC<Props> = ({
[collection]
);
// Drop to re-parent document
const [{ isOver, canDrop }, drop] = useDrop({
accept: "document",
drop: async (item: DragObject, monitor) => {
const { id, collectionId } = item;
if (monitor.didDrop()) {
return;
}
if (!collection) {
return;
}
const handleExpand = React.useCallback(() => {
if (!expanded) {
onDisclosureClick();
}
}, [expanded, onDisclosureClick]);
const document = documents.get(id);
if (collection.id === collectionId && !document?.parentDocumentId) {
return;
}
const prevCollection = collections.get(collectionId);
if (
prevCollection &&
prevCollection.permission !== collection.permission &&
!document?.isDraft
) {
dialogs.openModal({
title: t("Change permissions?"),
content: <ConfirmMoveDialog item={item} collection={collection} />,
});
} else {
await documents.move({ documentId: id, collectionId: collection.id });
if (!expanded) {
onDisclosureClick();
}
}
},
canDrop: () => can.createDocument,
collect: (monitor) => ({
isOver: !!monitor.isOver({
shallow: true,
}),
canDrop: monitor.canDrop(),
}),
});
const parentRef = React.useRef<HTMLDivElement>(null);
const [{ isOver, canDrop }, dropRef] = useDropToChangeCollection(
collection,
handleExpand,
parentRef
);
const handlePrefetch = React.useCallback(() => {
void collection.fetchDocuments();
}, [collection]);
const context = useActionContext({
activeCollectionId: collection.id,
sidebarContext,
});
const handleRename = React.useCallback(() => {
editableTitleRef.current?.setIsEditing(true);
}, [editableTitleRef]);
const [isAddingNewChild, setIsAddingNewChild, closeAddingNewChild] =
useBoolean();
const handleNewDoc = React.useCallback(
async (input) => {
const newDocument = await documents.create(
{
collectionId: collection.id,
title: input,
fullWidth: user.getPreference(UserPreference.FullWidthDocuments),
data: ProsemirrorHelper.getEmptyDocument(),
},
{ publish: true }
);
collection?.addDocument(newDocument);
closeAddingNewChild();
history.replace(documentEditPath(newDocument));
},
[user, closeAddingNewChild, history, collection, documents]
);
return (
<Relative ref={drop}>
<DropToImport collectionId={collection.id}>
<>
<Relative ref={mergeRefs([parentRef, dropRef])}>
<DropToImport collectionId={collection.id}>
<SidebarLink
onClick={onClick}
to={{
pathname: collection.path,
state: { sidebarContext },
}}
expanded={expanded}
onDisclosureClick={onDisclosureClick}
onClickIntent={handlePrefetch}
icon={
<CollectionIcon collection={collection} expanded={expanded} />
}
showActions={menuOpen}
isActiveDrop={isOver && canDrop}
isActive={(
match,
location: Location<{ sidebarContext?: SidebarContextType }>
) => !!match && location.state?.sidebarContext === sidebarContext}
label={
<EditableTitle
title={collection.name}
onSubmit={handleTitleChange}
onEditing={setIsEditing}
canUpdate={can.update}
maxLength={CollectionValidation.maxNameLength}
ref={editableTitleRef}
/>
}
exact={false}
depth={depth ? depth : 0}
menu={
!isEditing &&
!isDraggingAnyCollection && (
<Fade>
<NudeButton
tooltip={{ content: t("New doc"), delay: 500 }}
onClick={(ev) => {
ev.preventDefault();
setIsAddingNewChild();
handleExpand();
}}
>
<PlusIcon />
</NudeButton>
<CollectionMenu
collection={collection}
onRename={handleRename}
onOpen={handleMenuOpen}
onClose={handleMenuClose}
/>
</Fade>
)
}
/>
</DropToImport>
</Relative>
{isAddingNewChild && (
<SidebarLink
onClick={onClick}
to={{
pathname: collection.path,
state: { sidebarContext },
}}
expanded={expanded}
onDisclosureClick={onDisclosureClick}
onClickIntent={handlePrefetch}
icon={<CollectionIcon collection={collection} expanded={expanded} />}
showActions={menuOpen}
isActiveDrop={isOver && canDrop}
isActive={(
match,
location: Location<{ sidebarContext?: SidebarContextType }>
) => !!match && location.state?.sidebarContext === sidebarContext}
depth={2}
isActive={() => true}
label={
<EditableTitle
title={collection.name}
onSubmit={handleTitleChange}
onEditing={setIsEditing}
canUpdate={can.update}
maxLength={CollectionValidation.maxNameLength}
ref={editableTitleRef}
title=""
canUpdate
isEditing
placeholder={`${t("New doc")}`}
onCancel={closeAddingNewChild}
onSubmit={handleNewDoc}
maxLength={DocumentValidation.maxTitleLength}
/>
}
exact={false}
depth={depth ? depth : 0}
menu={
!isEditing &&
!isDraggingAnyCollection && (
<Fade>
<NudeButton
tooltip={{ content: t("New doc"), delay: 500 }}
action={createDocument}
context={context}
hideOnActionDisabled
>
<PlusIcon />
</NudeButton>
<CollectionMenu
collection={collection}
onRename={handleRename}
onOpen={handleMenuOpen}
onClose={handleMenuClose}
/>
</Fade>
)
}
/>
</DropToImport>
</Relative>
)}
</>
);
};
@@ -1,25 +1,23 @@
import noop from "lodash/noop";
import { observer } from "mobx-react";
import * as React from "react";
import { useDrop } from "react-dnd";
import { useTranslation } from "react-i18next";
import { Waypoint } from "react-waypoint";
import { toast } from "sonner";
import styled from "styled-components";
import Collection from "~/models/Collection";
import Document from "~/models/Document";
import ConfirmMoveDialog from "~/components/ConfirmMoveDialog";
import DocumentsLoader from "~/components/DocumentsLoader";
import { ResizingHeightContainer } from "~/components/ResizingHeightContainer";
import Text from "~/components/Text";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
import history from "~/utils/history";
import useCollectionDocuments from "../hooks/useCollectionDocuments";
import { useDropToChangeCollection } from "../hooks/useDragAndDrop";
import DocumentLink from "./DocumentLink";
import DropCursor from "./DropCursor";
import Folder from "./Folder";
import PlaceholderCollections from "./PlaceholderCollections";
import SidebarLink, { DragObject } from "./SidebarLink";
import SidebarLink from "./SidebarLink";
type Props = {
/** The collection to render the children of. */
@@ -36,55 +34,17 @@ function CollectionLinkChildren({
prefetchDocument,
}: Props) {
const pageSize = 250;
const can = usePolicy(collection);
const manualSort = collection.sort.field === "index";
const { documents, dialogs, collections } = useStores();
const { documents } = useStores();
const { t } = useTranslation();
const childDocuments = useCollectionDocuments(collection, documents.active);
const [showing, setShowing] = React.useState(pageSize);
const dummyRef = React.useRef<HTMLDivElement>(null);
// Drop to reorder document
const [{ isOverReorder, isDraggingAnyDocument }, dropToReorder] = useDrop({
accept: "document",
drop: (item: DragObject) => {
if (!manualSort && item.collectionId === collection?.id) {
toast.message(
t(
"You can't reorder documents in an alphabetically sorted collection"
)
);
return;
}
if (!collection) {
return;
}
const prevCollection = collections.get(item.collectionId);
if (
prevCollection &&
prevCollection.permission !== collection.permission
) {
dialogs.openModal({
title: t("Change permissions?"),
content: (
<ConfirmMoveDialog item={item} collection={collection} index={0} />
),
});
} else {
void documents.move({
documentId: item.id,
collectionId: collection.id,
index: 0,
});
}
},
collect: (monitor) => ({
isOverReorder: !!monitor.isOver(),
isDraggingAnyDocument: !!monitor.canDrop(),
}),
});
const [{ isOver, canDrop }, dropRef] = useDropToChangeCollection(
collection,
noop,
dummyRef
);
React.useEffect(() => {
if (!expanded) {
@@ -100,12 +60,8 @@ function CollectionLinkChildren({
return (
<Folder expanded={expanded}>
{isDraggingAnyDocument && can.createDocument && manualSort && (
<DropCursor
isActiveDrop={isOverReorder}
innerRef={dropToReorder}
position="top"
/>
{canDrop && collection.isManualSort && (
<DropCursor isActiveDrop={isOver} innerRef={dropRef} position="top" />
)}
<DocumentsLoader collection={collection} enabled={expanded}>
{!childDocuments && (
@@ -10,13 +10,14 @@ import Error from "~/components/List/Error";
import PaginatedList from "~/components/PaginatedList";
import { createCollection } from "~/actions/definitions/collections";
import useStores from "~/hooks/useStores";
import { DragObject } from "../hooks/useDragAndDrop";
import DraggableCollectionLink from "./DraggableCollectionLink";
import DropCursor from "./DropCursor";
import Header from "./Header";
import PlaceholderCollections from "./PlaceholderCollections";
import Relative from "./Relative";
import SidebarAction from "./SidebarAction";
import { DragObject } from "./SidebarLink";
import SidebarContext from "./SidebarContext";
function Collections() {
const { documents, collections } = useStores();
@@ -49,38 +50,40 @@ function Collections() {
});
return (
<Flex column>
<Header id="collections" title={t("Collections")}>
<Relative>
<PaginatedList
options={params}
aria-label={t("Collections")}
items={collections.allActive}
loading={<PlaceholderCollections />}
heading={
isDraggingAnyCollection ? (
<DropCursor
isActiveDrop={isCollectionDropping}
innerRef={dropToReorderCollection}
position="top"
<SidebarContext.Provider value="collections">
<Flex column>
<Header id="collections" title={t("Collections")}>
<Relative>
<PaginatedList
options={params}
aria-label={t("Collections")}
items={collections.allActive}
loading={<PlaceholderCollections />}
heading={
isDraggingAnyCollection ? (
<DropCursor
isActiveDrop={isCollectionDropping}
innerRef={dropToReorderCollection}
position="top"
/>
) : undefined
}
renderError={(props) => <StyledError {...props} />}
renderItem={(item: Collection, index) => (
<DraggableCollectionLink
key={item.id}
collection={item}
activeDocument={documents.active}
prefetchDocument={documents.prefetchDocument}
belowCollection={orderedCollections[index + 1]}
/>
) : undefined
}
renderError={(props) => <StyledError {...props} />}
renderItem={(item: Collection, index) => (
<DraggableCollectionLink
key={item.id}
collection={item}
activeDocument={documents.active}
prefetchDocument={documents.prefetchDocument}
belowCollection={orderedCollections[index + 1]}
/>
)}
/>
<SidebarAction action={createCollection} depth={0} />
</Relative>
</Header>
</Flex>
)}
/>
<SidebarAction action={createCollection} depth={0} />
</Relative>
</Header>
</Flex>
</SidebarContext.Provider>
);
}
@@ -3,22 +3,24 @@ import { observer } from "mobx-react";
import { PlusIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { useHistory } from "react-router-dom";
import styled from "styled-components";
import { NavigationNode } from "@shared/types";
import Icon from "@shared/components/Icon";
import { NavigationNode, UserPreference } from "@shared/types";
import { ProsemirrorHelper } from "@shared/utils/ProsemirrorHelper";
import { sortNavigationNodes } from "@shared/utils/collections";
import { DocumentValidation } from "@shared/validations";
import Collection from "~/models/Collection";
import Document from "~/models/Document";
import Fade from "~/components/Fade";
import Icon from "~/components/Icon";
import NudeButton from "~/components/NudeButton";
import Tooltip from "~/components/Tooltip";
import useBoolean from "~/hooks/useBoolean";
import useCurrentUser from "~/hooks/useCurrentUser";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
import DocumentMenu from "~/menus/DocumentMenu";
import { newNestedDocumentPath } from "~/utils/routeHelpers";
import { documentEditPath } from "~/utils/routeHelpers";
import {
useDragDocument,
useDropToReorderDocument,
@@ -58,6 +60,7 @@ function InnerDocumentLink(
) {
const { documents, policies } = useStores();
const { t } = useTranslation();
const history = useHistory();
const canUpdate = usePolicy(node.id).update;
const isActiveDocument = activeDocument && activeDocument.id === node.id;
const hasChildDocuments =
@@ -67,6 +70,7 @@ function InnerDocumentLink(
const [isEditing, setIsEditing] = React.useState(false);
const editableTitleRef = React.useRef<RefHandle>(null);
const sidebarContext = useSidebarContext();
const user = useCurrentUser();
React.useEffect(() => {
if (
@@ -216,6 +220,31 @@ function InnerDocumentLink(
[setExpanded, setCollapsed, hasChildren, expanded]
);
const [isAddingNewChild, setIsAddingNewChild, closeAddingNewChild] =
useBoolean();
const handleNewDoc = React.useCallback(
async (input) => {
const newDocument = await documents.create(
{
collectionId: collection?.id,
parentDocumentId: node.id,
fullWidth:
doc?.fullWidth ??
user.getPreference(UserPreference.FullWidthDocuments),
title: input,
data: ProsemirrorHelper.getEmptyDocument(),
},
{ publish: true }
);
collection?.addDocument(newDocument, node.id);
closeAddingNewChild();
history.replace(documentEditPath(newDocument));
},
[documents, collection, user, node, doc, history, closeAddingNewChild]
);
return (
<>
<Relative ref={parentRef}>
@@ -278,12 +307,15 @@ function InnerDocumentLink(
!isDraggingAnyDocument ? (
<Fade>
{can.createChildDocument && (
<Tooltip content={t("New doc")} delay={500}>
<Tooltip content={t("New doc")}>
<NudeButton
type={undefined}
aria-label={t("New nested document")}
as={Link}
to={newNestedDocumentPath(document.id)}
onClick={(ev) => {
ev.preventDefault();
setIsAddingNewChild();
setExpanded();
}}
>
<PlusIcon />
</NudeButton>
@@ -308,8 +340,25 @@ function InnerDocumentLink(
<DropCursor isActiveDrop={isOverReorder} innerRef={dropToReorder} />
)}
</Relative>
{isAddingNewChild && (
<SidebarLink
isActive={() => true}
depth={depth + 1}
label={
<EditableTitle
title=""
canUpdate
isEditing
placeholder={`${t("New doc")}`}
onCancel={closeAddingNewChild}
onSubmit={handleNewDoc}
maxLength={DocumentValidation.maxTitleLength}
/>
}
/>
)}
<Folder expanded={expanded && !isDragging}>
{nodeChildren.map((childNode, index) => (
{nodeChildren.map((childNode, childIndex) => (
<DocumentLink
key={childNode.id}
collection={collection}
@@ -318,7 +367,7 @@ function InnerDocumentLink(
prefetchDocument={prefetchDocument}
isDraft={childNode.isDraft}
depth={depth + 1}
index={index}
index={childIndex}
parentId={node.id}
/>
))}
@@ -7,14 +7,14 @@ import styled from "styled-components";
import Collection from "~/models/Collection";
import Document from "~/models/Document";
import CollectionIcon from "~/components/Icons/CollectionIcon";
import { useLocationSidebarContext } from "~/hooks/useLocationSidebarContext";
import useStores from "~/hooks/useStores";
import { useLocationState } from "../hooks/useLocationState";
import { DragObject } from "../hooks/useDragAndDrop";
import CollectionLink from "./CollectionLink";
import CollectionLinkChildren from "./CollectionLinkChildren";
import DropCursor from "./DropCursor";
import Relative from "./Relative";
import { useSidebarContext } from "./SidebarContext";
import { DragObject } from "./SidebarLink";
type Props = {
collection: Collection;
@@ -29,7 +29,7 @@ function DraggableCollectionLink({
prefetchDocument,
belowCollection,
}: Props) {
const locationSidebarContext = useLocationState();
const locationSidebarContext = useLocationSidebarContext();
const sidebarContext = useSidebarContext();
const { ui, policies, collections } = useStores();
const [expanded, setExpanded] = React.useState(
@@ -3,23 +3,33 @@ import { toast } from "sonner";
import styled from "styled-components";
import { s } from "@shared/styles";
type Props = {
onSubmit: (title: string) => Promise<void>;
type Props = Omit<React.HTMLAttributes<HTMLInputElement>, "onSubmit"> & {
/** A callback when the title is submitted. */
onSubmit: (title: string) => Promise<void> | void;
/** A callback when the editing status changes. */
onEditing?: (isEditing: boolean) => void;
/** A callback when editing is canceled. */
onCancel?: () => void;
/** The default title. */
title: string;
/** Whether the user can update the title. */
canUpdate: boolean;
/** The maximum length of the title. */
maxLength?: number;
/** The default editing state. */
isEditing?: boolean;
};
export type RefHandle = {
/** A function to set the editing state. */
setIsEditing: (isEditing: boolean) => void;
};
function EditableTitle(
{ title, onSubmit, canUpdate, onEditing, ...rest }: Props,
{ title, onSubmit, canUpdate, onEditing, onCancel, ...rest }: Props,
ref: React.RefObject<RefHandle>
) {
const [isEditing, setIsEditing] = React.useState(false);
const [isEditing, setIsEditing] = React.useState(rest.isEditing || false);
const [originalValue, setOriginalValue] = React.useState(title);
const [value, setValue] = React.useState(title);
@@ -59,6 +69,7 @@ function EditableTitle(
if (trimmedValue === originalValue || trimmedValue.length === 0) {
setValue(originalValue);
onCancel?.();
return;
}
@@ -73,7 +84,7 @@ function EditableTitle(
}
}
},
[originalValue, value, onSubmit]
[originalValue, value, onCancel, onSubmit]
);
const handleKeyDown = React.useCallback(
@@ -83,13 +94,14 @@ function EditableTitle(
}
if (ev.key === "Escape") {
setIsEditing(false);
onCancel?.();
setValue(originalValue);
}
if (ev.key === "Enter") {
await handleSave(ev);
}
},
[handleSave, originalValue]
[handleSave, onCancel, originalValue]
);
React.useEffect(() => {
@@ -2,10 +2,11 @@ import { observer } from "mobx-react";
import { GroupIcon } from "outline-icons";
import * as React from "react";
import Group from "~/models/Group";
import { useLocationSidebarContext } from "~/hooks/useLocationSidebarContext";
import Folder from "./Folder";
import Relative from "./Relative";
import SharedWithMeLink from "./SharedWithMeLink";
import SidebarContext from "./SidebarContext";
import SidebarContext, { groupSidebarContext } from "./SidebarContext";
import SidebarLink from "./SidebarLink";
type Props = {
@@ -14,13 +15,23 @@ type Props = {
};
const GroupLink: React.FC<Props> = ({ group }) => {
const [expanded, setExpanded] = React.useState(false);
const locationSidebarContext = useLocationSidebarContext();
const sidebarContext = groupSidebarContext(group.id);
const [expanded, setExpanded] = React.useState(
locationSidebarContext === sidebarContext
);
const handleDisclosureClick = React.useCallback((ev) => {
ev?.preventDefault();
setExpanded((e) => !e);
}, []);
React.useEffect(() => {
if (locationSidebarContext === sidebarContext) {
setExpanded(true);
}
}, [sidebarContext, locationSidebarContext, setExpanded]);
return (
<Relative>
<SidebarLink
@@ -30,7 +41,7 @@ const GroupLink: React.FC<Props> = ({ group }) => {
onClick={handleDisclosureClick}
depth={0}
/>
<SidebarContext.Provider value={group.id}>
<SidebarContext.Provider value={sidebarContext}>
<Folder expanded={expanded}>
{group.documentMemberships.map((membership) => (
<SharedWithMeLink
@@ -43,12 +43,12 @@ function HistoryNavigation(props: React.ComponentProps<typeof Flex>) {
return (
<Navigation gap={4} {...props}>
<Tooltip content={t("Go back")} delay={500}>
<Tooltip content={t("Go back")}>
<NudeButton onClick={() => Desktop.bridge?.goBack()}>
<Back $active={back} />
</NudeButton>
</Tooltip>
<Tooltip content={t("Go forward")} delay={500}>
<Tooltip content={t("Go forward")}>
<NudeButton onClick={() => Desktop.bridge?.goForward()}>
<Forward $active={forward} />
</NudeButton>
+9 -13
View File
@@ -93,15 +93,11 @@ const NavLink = ({
React.useLayoutEffect(() => {
if (isActive && linkRef.current && scrollIntoViewIfNeeded !== false) {
// If the page has an anchor hash then this means we're linking to an
// anchor in the document smooth scrolling the sidebar may the scrolling
// to the anchor of the document so we must avoid it.
if (!window.location.hash) {
scrollIntoView(linkRef.current, {
scrollMode: "if-needed",
behavior: "auto",
});
}
scrollIntoView(linkRef.current, {
scrollMode: "if-needed",
behavior: "auto",
boundary: (parent) => parent.id !== "sidebar",
});
}
}, [linkRef, scrollIntoViewIfNeeded, isActive]);
@@ -112,8 +108,9 @@ const NavLink = ({
!rest.target &&
!event.altKey &&
!event.metaKey &&
!event.ctrlKey,
[rest.target]
!event.ctrlKey &&
!isActive,
[rest.target, isActive]
);
const navigateTo = React.useCallback(() => {
@@ -153,14 +150,13 @@ const NavLink = ({
<Link
key={isActive ? "active" : "inactive"}
ref={linkRef}
// onMouseDown={handleClick}
onClick={handleClick}
onKeyDown={(event) => {
if (["Enter", " "].includes(event.key)) {
navigateTo();
event.currentTarget?.blur();
}
}}
onClick={handleClick}
aria-current={(isActive && ariaCurrent) || undefined}
className={className}
style={style}
@@ -2,10 +2,10 @@ import includes from "lodash/includes";
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import Icon from "@shared/components/Icon";
import { NavigationNode } from "@shared/types";
import Collection from "~/models/Collection";
import Document from "~/models/Document";
import Icon from "~/components/Icon";
import useStores from "~/hooks/useStores";
import { sharedDocumentPath } from "~/utils/routeHelpers";
import { descendants } from "~/utils/tree";
@@ -16,6 +16,7 @@ type Props = {
collection?: Collection;
activeDocumentId?: string;
activeDocument?: Document;
prefetchDocument?: (documentId: string) => Promise<Document | void>;
isDraft?: boolean;
depth: number;
index: number;
@@ -29,6 +30,7 @@ function DocumentLink(
collection,
activeDocument,
activeDocumentId,
prefetchDocument,
isDraft,
depth,
shareId,
@@ -97,6 +99,10 @@ function DocumentLink(
node,
]);
const handlePrefetch = React.useCallback(() => {
void prefetchDocument?.(node.id);
}, [prefetchDocument, node]);
const title =
(activeDocument?.id === node.id ? activeDocument.title : node.title) ||
t("Untitled");
@@ -114,6 +120,7 @@ function DocumentLink(
}}
expanded={hasChildDocuments && depth !== 0 ? expanded : undefined}
onDisclosureClick={handleDisclosureClick}
onClickIntent={handlePrefetch}
icon={icon && <Icon value={icon} color={node.color} />}
label={title}
depth={depth}
@@ -132,6 +139,7 @@ function DocumentLink(
node={childNode}
activeDocumentId={activeDocumentId}
activeDocument={activeDocument}
prefetchDocument={prefetchDocument}
isDraft={childNode.isDraft}
depth={depth + 1}
index={index}
@@ -9,6 +9,7 @@ import GroupMembership from "~/models/GroupMembership";
import UserMembership from "~/models/UserMembership";
import Fade from "~/components/Fade";
import useBoolean from "~/hooks/useBoolean";
import { useLocationSidebarContext } from "~/hooks/useLocationSidebarContext";
import useStores from "~/hooks/useStores";
import DocumentMenu from "~/menus/DocumentMenu";
import {
@@ -16,7 +17,6 @@ import {
useDropToReorderUserMembership,
useDropToReparentDocument,
} from "../hooks/useDragAndDrop";
import { useLocationState } from "../hooks/useLocationState";
import { useSidebarLabelAndIcon } from "../hooks/useSidebarLabelAndIcon";
import DocumentLink from "./DocumentLink";
import DropCursor from "./DropCursor";
@@ -36,7 +36,7 @@ function SharedWithMeLink({ membership, depth = 0 }: Props) {
const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean();
const { documentId } = membership;
const isActiveDocument = documentId === ui.activeDocumentId;
const locationSidebarContext = useLocationState();
const locationSidebarContext = useLocationSidebarContext();
const sidebarContext = useSidebarContext();
const document = documentId ? documents.get(documentId) : undefined;
@@ -105,7 +105,6 @@ const Button = styled(Flex)<{
&:hover,
&[aria-expanded="true"] {
color: ${s("sidebarText")};
transition: background 100ms ease-in-out;
background: ${s("sidebarActiveBackground")};
}
@@ -1,9 +1,57 @@
import * as React from "react";
import Document from "~/models/Document";
import User from "~/models/User";
export type SidebarContextType = "collections" | "starred" | string | undefined;
export type SidebarContextType =
| "collections"
| "shared"
| `group-${string}`
| `starred-${string}`
| undefined;
const SidebarContext = React.createContext<SidebarContextType>(undefined);
export const useSidebarContext = () => React.useContext(SidebarContext);
export const groupSidebarContext = (groupId: string): SidebarContextType =>
`group-${groupId}`;
export const starredSidebarContext = (modelId: string): SidebarContextType =>
`starred-${modelId}`;
export const determineSidebarContext = ({
document,
user,
currentContext,
}: {
document: Document;
user: User;
currentContext?: SidebarContextType;
}): SidebarContextType => {
const isStarred = document.isStarred || !!document.collection?.isStarred;
const preferStarred = !currentContext || currentContext.startsWith("starred");
if (isStarred && preferStarred) {
const currentlyInStarredCollection =
currentContext === starredSidebarContext(document.collectionId ?? "");
return document.isStarred && !currentlyInStarredCollection
? starredSidebarContext(document.id)
: starredSidebarContext(document.collectionId!);
}
if (document.collection) {
return "collections";
} else if (
user.documentMemberships.find((m) => m.documentId === document.id)
) {
return "shared";
} else {
const group = user.groupsWithDocumentMemberships.find(
(g) => !!g.documentMemberships.find((m) => m.documentId === document.id)
);
return groupSidebarContext(group?.id ?? "");
}
};
export default SidebarContext;
@@ -4,7 +4,6 @@ import styled, { useTheme, css } from "styled-components";
import breakpoint from "styled-components-breakpoint";
import EventBoundary from "@shared/components/EventBoundary";
import { s } from "@shared/styles";
import { NavigationNode } from "@shared/types";
import NudeButton from "~/components/NudeButton";
import { UnreadBadge } from "~/components/UnreadBadge";
import useUnmount from "~/hooks/useUnmount";
@@ -12,11 +11,6 @@ import { undraggableOnDesktop } from "~/styles";
import Disclosure from "./Disclosure";
import NavLink, { Props as NavLinkProps } from "./NavLink";
export type DragObject = NavigationNode & {
depth: number;
collectionId: string;
};
type Props = Omit<NavLinkProps, "to"> & {
to?: LocationDescriptor;
innerRef?: (ref: HTMLElement | null | undefined) => void;
@@ -78,7 +72,6 @@ function SidebarLink(
const activeStyle = React.useMemo(
() => ({
fontWeight: 600,
color: theme.text,
background: theme.sidebarActiveBackground,
...style,
@@ -202,10 +195,10 @@ const Link = styled(NavLink)<{
display: flex;
position: relative;
text-overflow: ellipsis;
font-weight: 475;
padding: 6px 16px;
border-radius: 4px;
min-height: 32px;
transition: background 50ms, color 50ms;
user-select: none;
background: ${(props) =>
props.$isActiveDrop ? props.theme.slateDark : "inherit"};
+40 -43
View File
@@ -15,7 +15,6 @@ import DropCursor from "./DropCursor";
import Header from "./Header";
import PlaceholderCollections from "./PlaceholderCollections";
import Relative from "./Relative";
import SidebarContext from "./SidebarContext";
import SidebarLink from "./SidebarLink";
import StarredLink from "./StarredLink";
@@ -42,48 +41,46 @@ function Starred() {
}
return (
<SidebarContext.Provider value="starred">
<Flex column>
<Header id="starred" title={t("Starred")}>
<Relative>
{reorderStarProps.isDragging && (
<DropCursor
isActiveDrop={reorderStarProps.isOverCursor}
innerRef={dropToReorder}
position="top"
/>
)}
{createStarProps.isDragging && (
<DropCursor
isActiveDrop={createStarProps.isOverCursor}
innerRef={dropToStarRef}
position="top"
/>
)}
{stars.orderedData
.slice(0, page * STARRED_PAGINATION_LIMIT)
.map((star) => (
<StarredLink key={star.id} star={star} />
))}
{!end && (
<SidebarLink
onClick={next}
label={`${t("Show more")}`}
disabled={stars.isFetching}
depth={0}
/>
)}
{loading && (
<Flex column>
<DelayedMount>
<PlaceholderCollections />
</DelayedMount>
</Flex>
)}
</Relative>
</Header>
</Flex>
</SidebarContext.Provider>
<Flex column>
<Header id="starred" title={t("Starred")}>
<Relative>
{reorderStarProps.isDragging && (
<DropCursor
isActiveDrop={reorderStarProps.isOverCursor}
innerRef={dropToReorder}
position="top"
/>
)}
{createStarProps.isDragging && (
<DropCursor
isActiveDrop={createStarProps.isOverCursor}
innerRef={dropToStarRef}
position="top"
/>
)}
{stars.orderedData
.slice(0, page * STARRED_PAGINATION_LIMIT)
.map((star) => (
<StarredLink key={star.id} star={star} />
))}
{!end && (
<SidebarLink
onClick={next}
label={`${t("Show more")}`}
disabled={stars.isFetching}
depth={0}
/>
)}
{loading && (
<Flex column>
<DelayedMount>
<PlaceholderCollections />
</DelayedMount>
</Flex>
)}
</Relative>
</Header>
</Flex>
);
}
@@ -8,6 +8,7 @@ import styled, { useTheme } from "styled-components";
import Star from "~/models/Star";
import Fade from "~/components/Fade";
import useBoolean from "~/hooks/useBoolean";
import { useLocationSidebarContext } from "~/hooks/useLocationSidebarContext";
import useStores from "~/hooks/useStores";
import DocumentMenu from "~/menus/DocumentMenu";
import {
@@ -15,7 +16,6 @@ import {
useDropToCreateStar,
useDropToReorderStar,
} from "../hooks/useDragAndDrop";
import { useLocationState } from "../hooks/useLocationState";
import { useSidebarLabelAndIcon } from "../hooks/useSidebarLabelAndIcon";
import CollectionLink from "./CollectionLink";
import CollectionLinkChildren from "./CollectionLinkChildren";
@@ -25,7 +25,7 @@ import Folder from "./Folder";
import Relative from "./Relative";
import SidebarContext, {
SidebarContextType,
useSidebarContext,
starredSidebarContext,
} from "./SidebarContext";
import SidebarLink from "./SidebarLink";
@@ -39,22 +39,33 @@ function StarredLink({ star }: Props) {
const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean();
const { documentId, collectionId } = star;
const collection = collections.get(collectionId);
const locationSidebarContext = useLocationState();
const sidebarContext = useSidebarContext();
const locationSidebarContext = useLocationSidebarContext();
const sidebarContext = starredSidebarContext(
star.documentId ?? star.collectionId
);
const [expanded, setExpanded] = useState(
star.collectionId === ui.activeCollectionId &&
(star.documentId
? star.documentId === ui.activeDocumentId
: star.collectionId === ui.activeCollectionId) &&
sidebarContext === locationSidebarContext
);
React.useEffect(() => {
if (
star.documentId === ui.activeDocumentId &&
sidebarContext === locationSidebarContext
) {
setExpanded(true);
} else if (
star.collectionId === ui.activeCollectionId &&
sidebarContext === locationSidebarContext
) {
setExpanded(true);
}
}, [
star.documentId,
star.collectionId,
ui.activeDocumentId,
ui.activeCollectionId,
sidebarContext,
locationSidebarContext,
@@ -152,7 +163,7 @@ function StarredLink({ star }: Props) {
}
/>
</Draggable>
<SidebarContext.Provider value={document.id}>
<SidebarContext.Provider value={sidebarContext}>
<Relative>
<Folder expanded={displayChildDocuments}>
{childDocuments.map((node, index) => (
@@ -176,7 +187,7 @@ function StarredLink({ star }: Props) {
if (collection) {
return (
<>
<SidebarContext.Provider value={sidebarContext}>
<Draggable key={star?.id} ref={draggableRef} $isDragging={isDragging}>
<CollectionLink
collection={collection}
@@ -186,16 +197,14 @@ function StarredLink({ star }: Props) {
isDraggingAnyCollection={reorderStarProps.isDragging}
/>
</Draggable>
<SidebarContext.Provider value={collection.id}>
<Relative>
<CollectionLinkChildren
collection={collection}
expanded={displayChildDocuments}
/>
{cursor}
</Relative>
</SidebarContext.Provider>
</>
<Relative>
<CollectionLinkChildren
collection={collection}
expanded={displayChildDocuments}
/>
{cursor}
</Relative>
</SidebarContext.Provider>
);
}
@@ -1,5 +1,5 @@
import styled from "styled-components";
import { hover } from "~/styles";
import { hover } from "@shared/styles";
import SidebarButton from "./SidebarButton";
const ToggleButton = styled(SidebarButton)`
@@ -6,7 +6,8 @@ import { useTranslation } from "react-i18next";
import DocumentDelete from "~/scenes/DocumentDelete";
import useStores from "~/hooks/useStores";
import { trashPath } from "~/utils/routeHelpers";
import SidebarLink, { DragObject } from "./SidebarLink";
import { DragObject } from "../hooks/useDragAndDrop";
import SidebarLink from "./SidebarLink";
function TrashLink() {
const { policies, dialogs, documents } = useStores();
+171 -61
View File
@@ -6,6 +6,7 @@ import { getEmptyImage } from "react-dnd-html5-backend";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { useTheme } from "styled-components";
import Icon from "@shared/components/Icon";
import { NavigationNode } from "@shared/types";
import Collection from "~/models/Collection";
import Document from "~/models/Document";
@@ -13,12 +14,50 @@ import GroupMembership from "~/models/GroupMembership";
import Star from "~/models/Star";
import UserMembership from "~/models/UserMembership";
import ConfirmMoveDialog from "~/components/ConfirmMoveDialog";
import Icon from "~/components/Icon";
import useCurrentUser from "~/hooks/useCurrentUser";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
import { DragObject } from "../components/SidebarLink";
import { AuthorizationError } from "~/utils/errors";
import { useSidebarLabelAndIcon } from "./useSidebarLabelAndIcon";
export type DragObject = NavigationNode & {
depth: number;
collectionId: string;
};
function useHover(
elementRef: React.RefObject<HTMLDivElement>,
callback: () => void
) {
const hoverTimeoutRef = React.useRef<ReturnType<typeof setTimeout>>();
const startHover = React.useCallback(() => {
if (!hoverTimeoutRef.current) {
hoverTimeoutRef.current = setTimeout(() => {
hoverTimeoutRef.current = undefined;
callback();
}, 500);
}
}, [callback]);
const unsetHover = React.useCallback(() => {
if (hoverTimeoutRef.current) {
clearTimeout(hoverTimeoutRef.current);
hoverTimeoutRef.current = undefined;
}
}, []);
// We set a timeout when the user first starts hovering over the document link,
// to trigger expansion of children. Clear this timeout when they stop hovering.
React.useEffect(() => {
const element = elementRef.current;
element?.addEventListener("dragleave", unsetHover);
return () => element?.removeEventListener("dragleave", unsetHover);
}, [elementRef, unsetHover]);
return startHover;
}
/**
* Hook for shared logic that allows dragging a Starred item
*
@@ -162,6 +201,84 @@ export function useDragDocument(
return [{ isDragging }, draggableRef] as const;
}
export function useDropToChangeCollection(
collection: Collection,
expandNode: () => void,
parentRef: React.RefObject<HTMLDivElement>
) {
const { t } = useTranslation();
const { documents, collections, dialogs } = useStores();
const can = usePolicy(collection);
const startHover = useHover(parentRef, expandNode);
return useDrop<
DragObject,
Promise<void>,
{ isOver: boolean; canDrop: boolean }
>({
accept: "document",
drop: async (item, monitor) => {
if (monitor.didDrop()) {
return;
}
const { id, collectionId } = item;
const prevCollection = collections.get(collectionId);
const document = documents.get(id);
if (
prevCollection &&
prevCollection.permission !== collection.permission &&
!document?.isDraft
) {
dialogs.openModal({
title: t("Change permissions?"),
content: (
<ConfirmMoveDialog item={item} collection={collection} index={0} />
),
});
} else {
try {
await documents.move({
documentId: id,
collectionId: collection.id,
index: 0,
});
expandNode();
} catch (err) {
if (err instanceof AuthorizationError) {
toast.error(
t(
"You do not have permission to move {{ documentName }} to the {{ collectionName }} collection",
{
documentName: item.title,
collectionName: collection.name,
}
)
);
} else {
toast.error(err.message);
}
}
}
},
canDrop: () => can.createDocument,
hover: (_, monitor) => {
if (
collection.hasDocuments &&
monitor.canDrop() &&
monitor.isOver({ shallow: true })
) {
startHover();
}
},
collect: (monitor) => ({
isOver: monitor.isOver({ shallow: true }),
canDrop: monitor.canDrop(),
}),
});
}
/**
* Hook for shared logic that allows dropping documents to reparent
*
@@ -175,7 +292,7 @@ export function useDropToReparentDocument(
parentRef: React.RefObject<HTMLDivElement>
) {
const { t } = useTranslation();
const { documents, collections, dialogs, policies } = useStores();
const { documents, collections, dialogs } = useStores();
const hasChildDocuments = !!node?.children.length;
const document = node ? documents.get(node.id) : undefined;
const pathToNode = React.useMemo(
@@ -183,25 +300,7 @@ export function useDropToReparentDocument(
[document]
);
const hoverExpanding = React.useRef<ReturnType<typeof setTimeout>>();
// We set a timeout when the user first starts hovering over the document link,
// to trigger expansion of children. Clear this timeout when they stop hovering.
React.useEffect(() => {
const resetHoverExpanding = () => {
if (hoverExpanding.current) {
clearTimeout(hoverExpanding.current);
hoverExpanding.current = undefined;
}
};
const element = parentRef.current;
element?.addEventListener("dragleave", resetHoverExpanding);
return () => {
element?.removeEventListener("dragleave", resetHoverExpanding);
};
}, [parentRef]);
const startHover = useHover(parentRef, setExpanded);
return useDrop<
DragObject,
@@ -214,7 +313,9 @@ export function useDropToReparentDocument(
return;
}
const collection = documents.get(node.id)?.collection;
const collection = node.collectionId
? collections.get(node.collectionId)
: undefined;
const prevCollection = collections.get(item.collectionId);
if (
@@ -233,22 +334,40 @@ export function useDropToReparentDocument(
),
});
} else {
await documents.move({
documentId: item.id,
parentDocumentId: node.id,
});
try {
await documents.move({
documentId: item.id,
parentDocumentId: node.id,
});
setExpanded();
} catch (err) {
if (err instanceof AuthorizationError) {
toast.error(
t(
"{{ documentName }} cannot be moved within {{ parentDocumentName }}",
{
documentName: item.title,
parentDocumentName: node.title,
}
)
);
} else {
toast.error(err.message);
}
}
}
},
canDrop: (item) => {
if (!node || item.id === node.id) {
return false;
}
setExpanded();
if (!document) {
return true; // optimistic, in case the document is not loaded yet; server will check for permissions before performing the move.
}
return document.isActive && !!pathToNode && !pathToNode.includes(item.id);
},
canDrop: (item, monitor) =>
!!node &&
!!pathToNode &&
!pathToNode.includes(monitor.getItem().id) &&
item.id !== node.id &&
!!document?.isActive &&
policies.abilities(node.id).update &&
policies.abilities(item.id).move,
hover: (_item, monitor) => {
// Enables expansion of document children when hovering over the document
// for more than half a second.
@@ -259,15 +378,7 @@ export function useDropToReparentDocument(
shallow: true,
})
) {
if (!hoverExpanding.current) {
hoverExpanding.current = setTimeout(() => {
hoverExpanding.current = undefined;
if (monitor.isOver({ shallow: true })) {
setExpanded();
}
}, 500);
}
startHover();
}
},
collect: (monitor) => ({
@@ -297,7 +408,7 @@ export function useDropToReorderDocument(
}
) {
const { t } = useTranslation();
const { documents, collections, dialogs, policies } = useStores();
const { documents, collections, dialogs } = useStores();
const document = documents.get(node.id);
@@ -308,22 +419,9 @@ export function useDropToReorderDocument(
>({
accept: "document",
canDrop: (item: DragObject) => {
if (
item.id === node.id ||
!policies.abilities(item.id)?.move ||
!document?.isActive
) {
if (item.id === node.id || (document && !document.isActive)) {
return false;
}
const params = getMoveParams(item);
if (params?.collectionId) {
return policies.abilities(params.collectionId)?.updateDocument;
}
if (params?.parentDocumentId) {
return policies.abilities(params.parentDocumentId)?.update;
}
return true;
},
drop: async (item) => {
@@ -357,7 +455,19 @@ export function useDropToReorderDocument(
),
});
} else {
void documents.move(params);
try {
await documents.move(params);
} catch (err) {
if (err instanceof AuthorizationError) {
toast.error(
t("The {{ documentName }} cannot be moved here", {
documentName: item.title,
})
);
} else {
toast.error(err.message);
}
}
}
}
},
@@ -1,6 +1,6 @@
import { DocumentIcon } from "outline-icons";
import * as React from "react";
import Icon from "~/components/Icon";
import Icon from "@shared/components/Icon";
import CollectionIcon from "~/components/Icons/CollectionIcon";
import useStores from "~/hooks/useStores";
+31
View File
@@ -0,0 +1,31 @@
import { ColumnSort } from "@tanstack/react-table";
import * as React from "react";
import { useHistory, useLocation } from "react-router-dom";
import useQuery from "~/hooks/useQuery";
import lazyWithRetry from "~/utils/lazyWithRetry";
import type { Props as TableProps } from "./Table";
const Table = lazyWithRetry(() => import("~/components/Table"));
export type Props<T> = Omit<TableProps<T>, "onChangeSort">;
export function SortableTable<T>(props: Props<T>) {
const location = useLocation();
const history = useHistory();
const params = useQuery();
const handleChangeSort = React.useCallback(
(sort: ColumnSort) => {
params.set("sort", sort.id);
params.set("direction", sort.desc ? "desc" : "asc");
history.replace({
pathname: location.pathname,
search: params.toString(),
});
},
[params, history, location.pathname]
);
return <Table onChangeSort={handleChangeSort} {...props} />;
}
+1 -1
View File
@@ -3,6 +3,7 @@ import { StarredIcon, UnstarredIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled, { useTheme } from "styled-components";
import { hover } from "@shared/styles";
import Collection from "~/models/Collection";
import Document from "~/models/Document";
import {
@@ -11,7 +12,6 @@ import {
} from "~/actions/definitions/collections";
import { starDocument, unstarDocument } from "~/actions/definitions/documents";
import useActionContext from "~/hooks/useActionContext";
import { hover } from "~/styles";
import NudeButton from "./NudeButton";
type Props = {
-1
View File
@@ -31,7 +31,6 @@ const Background = styled.div<{ sticky?: boolean }>`
margin: 0 -8px;
padding: 0 8px;
background: ${s("background")};
transition: ${s("backgroundTransition")};
z-index: 1;
`;
+1 -2
View File
@@ -4,9 +4,8 @@ import isEqual from "lodash/isEqual";
import queryString from "query-string";
import * as React from "react";
import styled, { useTheme } from "styled-components";
import { s } from "@shared/styles";
import { s, hover } from "@shared/styles";
import NavLink from "~/components/NavLink";
import { hover } from "~/styles";
type Props = Omit<React.ComponentProps<typeof NavLink>, "children"> & {
/**
+303 -242
View File
@@ -1,231 +1,283 @@
import isEqual from "lodash/isEqual";
import {
useReactTable,
getCoreRowModel,
SortingState,
flexRender,
ColumnSort,
functionalUpdate,
Row as TRow,
createColumnHelper,
AccessorFn,
CellContext,
} from "@tanstack/react-table";
import { useWindowVirtualizer } from "@tanstack/react-virtual";
import { observer } from "mobx-react";
import { CollapsedIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { useTable, useSortBy, usePagination } from "react-table";
import { Waypoint } from "react-waypoint";
import styled from "styled-components";
import { s } from "@shared/styles";
import Button from "~/components/Button";
import DelayedMount from "~/components/DelayedMount";
import Empty from "~/components/Empty";
import Flex from "~/components/Flex";
import NudeButton from "~/components/NudeButton";
import PlaceholderText from "~/components/PlaceholderText";
import usePrevious from "~/hooks/usePrevious";
export type Props = {
data: any[];
offset?: number;
isLoading: boolean;
empty?: React.ReactNode;
currentPage?: number;
page: number;
pageSize?: number;
totalPages?: number;
defaultSort?: string;
topRef?: React.Ref<any>;
onChangePage: (index: number) => void;
onChangeSort: (
sort: string | null | undefined,
direction: "ASC" | "DESC"
) => void;
columns: any;
defaultSortDirection: "ASC" | "DESC";
const HEADER_HEIGHT = 40;
type DataColumn<TData> = {
type: "data";
header: string;
accessor: AccessorFn<TData>;
sortable?: boolean;
};
function Table({
data,
isLoading,
totalPages,
empty,
columns,
page,
pageSize = 50,
defaultSort = "name",
topRef,
onChangeSort,
onChangePage,
defaultSortDirection,
}: Props) {
const { t } = useTranslation();
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
canNextPage,
nextPage,
canPreviousPage,
previousPage,
state: { pageIndex, sortBy },
} = useTable(
{
columns,
data,
manualPagination: true,
manualSortBy: true,
autoResetSortBy: false,
autoResetPage: false,
pageCount: totalPages,
initialState: {
sortBy: [
{
id: defaultSort,
desc: defaultSortDirection === "DESC" ? true : false,
},
],
pageSize,
pageIndex: page,
},
stateReducer: (newState, action, prevState) => {
if (!isEqual(newState.sortBy, prevState.sortBy)) {
return { ...newState, pageIndex: 0 };
}
type ActionColumn = {
type: "action";
header?: string;
};
return newState;
},
},
useSortBy,
usePagination
export type Column<TData> = {
id: string;
component: (data: TData) => React.ReactNode;
width: string;
} & (DataColumn<TData> | ActionColumn);
export type Props<TData> = {
data: TData[];
columns: Column<TData>[];
sort: ColumnSort;
onChangeSort: (sort: ColumnSort) => void;
loading: boolean;
page: {
hasNext: boolean;
fetchNext?: () => void;
};
rowHeight: number;
stickyOffset?: number;
};
function Table<TData>({
data,
columns,
sort,
onChangeSort,
loading,
page,
rowHeight,
stickyOffset = 0,
}: Props<TData>) {
const { t } = useTranslation();
const virtualContainerRef = React.useRef<HTMLDivElement>(null);
const [virtualContainerTop, setVirtualContainerTop] =
React.useState<number>();
const columnHelper = React.useMemo(() => createColumnHelper<TData>(), []);
const observedColumns = React.useMemo(
() =>
columns.map((column) => {
const cell = ({ row }: CellContext<TData, unknown>) => (
<ObservedCell data={row.original} render={column.component} />
);
return column.type === "data"
? columnHelper.accessor(column.accessor, {
id: column.id,
header: column.header,
enableSorting: column.sortable ?? true,
cell,
})
: columnHelper.display({
id: column.id,
header: column.header ?? "",
cell,
});
}),
[columns, columnHelper]
);
const prevSortBy = React.useRef(sortBy);
const gridColumns = React.useMemo(
() => columns.map((column) => column.width).join(" "),
[columns]
);
const handleChangeSort = React.useCallback(
(sortState: SortingState) => {
const newState = functionalUpdate(sortState, [sort]);
const newSort = newState[0];
onChangeSort(newSort);
},
[sort, onChangeSort]
);
const prevSort = usePrevious(sort);
const sortChanged = sort !== prevSort;
const isEmpty = !loading && data.length === 0;
const showPlaceholder = loading && data.length === 0;
const table = useReactTable({
data,
columns: observedColumns,
getCoreRowModel: getCoreRowModel(),
manualSorting: true,
enableMultiSort: false,
enableSortingRemoval: false,
state: {
sorting: [sort],
},
onSortingChange: handleChangeSort,
});
const { rows } = table.getRowModel();
const rowVirtualizer = useWindowVirtualizer({
count: rows.length,
estimateSize: () => rowHeight,
scrollMargin: virtualContainerTop,
overscan: 5,
});
React.useEffect(() => {
if (!isEqual(sortBy, prevSortBy.current)) {
prevSortBy.current = sortBy;
onChangePage(0);
onChangeSort(
sortBy.length ? sortBy[0].id : undefined,
!sortBy.length ? defaultSortDirection : sortBy[0].desc ? "DESC" : "ASC"
if (!sortChanged || !virtualContainerTop) {
return;
}
const scrollThreshold =
virtualContainerTop - (stickyOffset + HEADER_HEIGHT);
const reset = window.scrollY > scrollThreshold;
if (reset) {
rowVirtualizer.scrollToOffset(scrollThreshold, {
behavior: "smooth",
});
}
}, [rowVirtualizer, sortChanged, virtualContainerTop, stickyOffset]);
React.useLayoutEffect(() => {
if (virtualContainerRef.current) {
// determine the scrollable virtual container offsetTop on mount
setVirtualContainerTop(
virtualContainerRef.current.getBoundingClientRect().top
);
}
}, [defaultSortDirection, onChangePage, onChangeSort, sortBy]);
const handleNextPage = () => {
nextPage();
onChangePage(pageIndex + 1);
};
const handlePreviousPage = () => {
previousPage();
onChangePage(pageIndex - 1);
};
const isEmpty = !isLoading && data.length === 0;
const showPlaceholder = isLoading && data.length === 0;
}, []);
return (
<div style={{ overflowX: "auto" }}>
<Anchor ref={topRef} />
<InnerTable {...getTableProps()}>
<thead>
{headerGroups.map((headerGroup) => {
const groupProps = headerGroup.getHeaderGroupProps();
return (
<tr {...groupProps} key={groupProps.key}>
{headerGroup.headers.map((column) => (
<Head
{...column.getHeaderProps(column.getSortByToggleProps())}
key={column.id}
<>
<InnerTable role="table">
<THead role="rowgroup" $topPos={stickyOffset}>
{table.getHeaderGroups().map((headerGroup) => (
<TR role="row" key={headerGroup.id} $columns={gridColumns}>
{headerGroup.headers.map((header) => (
<TH role="columnheader" key={header.id}>
<SortWrapper
align="center"
gap={4}
onClick={header.column.getToggleSortingHandler()}
$sortable={header.column.getCanSort()}
>
<SortWrapper
align="center"
$sortable={!column.disableSortBy}
gap={4}
>
{column.render("Header")}
{column.isSorted &&
(column.isSortedDesc ? (
<DescSortIcon />
) : (
<AscSortIcon />
))}
</SortWrapper>
</Head>
))}
</tr>
);
})}
</thead>
<tbody {...getTableBodyProps()}>
{rows.map((row) => {
prepareRow(row);
return (
<Row {...row.getRowProps()} key={row.id}>
{row.cells.map((cell) => (
<Cell
{...cell.getCellProps([
{
// @ts-expect-error ts-migrate(2339) FIXME: Property 'className' does not exist on type 'Colum... Remove this comment to see the full error message
className: cell.column.className,
},
])}
key={cell.column.id}
>
{cell.render("Cell")}
</Cell>
))}
</Row>
);
})}
</tbody>
{showPlaceholder && <Placeholder columns={columns.length} />}
</InnerTable>
{isEmpty ? (
empty || <Empty>{t("No results")}</Empty>
) : (
<Pagination
justify={canPreviousPage ? "space-between" : "flex-end"}
gap={8}
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
{header.column.getIsSorted() === "asc" ? (
<AscSortIcon />
) : header.column.getIsSorted() === "desc" ? (
<DescSortIcon />
) : (
<div />
)}
</SortWrapper>
</TH>
))}
</TR>
))}
</THead>
<TBody
ref={virtualContainerRef}
role="rowgroup"
$height={rowVirtualizer.getTotalSize()}
>
{/* Note: the page > 0 check shouldn't be needed here but is */}
{canPreviousPage && page > 0 && (
<Button onClick={handlePreviousPage} neutral>
{t("Previous page")}
</Button>
)}
{canNextPage && (
<Button onClick={handleNextPage} neutral>
{t("Next page")}
</Button>
)}
</Pagination>
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
const row = rows[virtualRow.index] as TRow<TData>;
return (
<TR
role="row"
key={row.id}
data-index={virtualRow.index}
style={{
position: "absolute",
transform: `translateY(${
virtualRow.start - rowVirtualizer.options.scrollMargin
}px)`,
height: `${virtualRow.size}px`,
}}
$columns={gridColumns}
>
{row.getAllCells().map((cell) => (
<TD role="cell" key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TD>
))}
</TR>
);
})}
</TBody>
{showPlaceholder && (
<Placeholder columns={columns.length} gridColumns={gridColumns} />
)}
</InnerTable>
{page.hasNext && (
<Waypoint
key={data?.length}
onEnter={page.fetchNext}
bottomOffset={-rowHeight * 5}
/>
)}
</div>
{isEmpty && <Empty>{t("No results")}</Empty>}
</>
);
}
export const Placeholder = ({
const ObservedCell = observer(function <TData>({
data,
render,
}: {
data: TData;
render: (data: TData) => React.ReactNode;
}) {
return <>{render(data)}</>;
});
function Placeholder({
columns,
rows = 3,
gridColumns,
}: {
columns: number;
rows?: number;
}) => (
<DelayedMount>
<tbody>
{new Array(rows).fill(1).map((_, row) => (
<Row key={row}>
{new Array(columns).fill(1).map((_, col) => (
<Cell key={col}>
<PlaceholderText minWidth={25} maxWidth={75} />
</Cell>
))}
</Row>
))}
</tbody>
</DelayedMount>
);
const Anchor = styled.div`
top: -32px;
position: relative;
`;
const Pagination = styled(Flex)`
margin: 0 0 32px;
`;
gridColumns: string;
}) {
return (
<DelayedMount>
<TBody $height={150}>
{new Array(rows).fill(1).map((_r, row) => (
<TR key={row} $columns={gridColumns}>
{new Array(columns).fill(1).map((_c, col) => (
<TD key={col}>
<PlaceholderText minWidth={25} maxWidth={75} />
</TD>
))}
</TR>
))}
</TBody>
</DelayedMount>
);
}
const DescSortIcon = styled(CollapsedIcon)`
margin-left: -2px;
@@ -239,12 +291,6 @@ const AscSortIcon = styled(DescSortIcon)`
transform: rotate(180deg);
`;
const InnerTable = styled.table`
border-collapse: collapse;
margin: 16px 0;
min-width: 100%;
`;
const SortWrapper = styled(Flex)<{ $sortable: boolean }>`
display: inline-flex;
height: 24px;
@@ -253,6 +299,7 @@ const SortWrapper = styled(Flex)<{ $sortable: boolean }>`
white-space: nowrap;
margin: 0 -4px;
padding: 0 4px;
cursor: ${(props) => (props.$sortable ? `var(--pointer)` : "")};
&:hover {
background: ${(props) =>
@@ -260,15 +307,66 @@ const SortWrapper = styled(Flex)<{ $sortable: boolean }>`
}
`;
const Cell = styled.td`
padding: 10px 6px;
border-bottom: 1px solid ${s("divider")};
const InnerTable = styled.div`
width: 100%;
`;
const THead = styled.div<{ $topPos: number }>`
position: sticky;
top: ${({ $topPos }) => `${$topPos}px`};
height: ${HEADER_HEIGHT}px;
z-index: 1;
font-size: 14px;
text-wrap: nowrap;
color: ${s("textSecondary")};
font-weight: 500;
border-bottom: 1px solid ${s("divider")};
background: ${s("background")};
`;
const TBody = styled.div<{ $height: number }>`
position: relative;
height: ${({ $height }) => `${$height}px`};
`;
const TR = styled.div<{ $columns: string }>`
width: 100%;
display: grid;
grid-template-columns: ${({ $columns }) => `${$columns}`};
align-items: center;
border-bottom: 1px solid ${s("divider")};
&:last-child {
border-bottom: 0;
}
`;
const TH = styled.span`
padding: 6px 6px 2px;
&:first-child {
padding-left: 0;
}
&:last-child {
padding-right: 0;
}
`;
const TD = styled.span`
padding: 10px 6px;
font-size: 14px;
text-wrap: wrap;
word-break: break-word;
&:first-child {
font-size: 15px;
font-weight: 500;
padding-left: 0;
}
&:last-child {
padding-right: 0;
}
&.actions,
@@ -291,41 +389,4 @@ const Cell = styled.td`
}
`;
const Row = styled.tr`
${Cell} {
&:first-child {
padding-left: 0;
}
&:last-child {
padding-right: 0;
}
}
&:last-child {
${Cell} {
border-bottom: 0;
}
}
`;
const Head = styled.th`
text-align: left;
padding: 6px 6px 0;
border-bottom: 1px solid ${s("divider")};
background: ${s("background")};
transition: ${s("backgroundTransition")};
font-size: 14px;
color: ${s("textSecondary")};
font-weight: 500;
z-index: 1;
cursor: var(--pointer) !important;
:first-child {
padding-left: 0;
}
:last-child {
padding-right: 0;
}
`;
export default observer(Table);
-71
View File
@@ -1,71 +0,0 @@
import { observer } from "mobx-react";
import * as React from "react";
import { useHistory, useLocation } from "react-router-dom";
import scrollIntoView from "scroll-into-view-if-needed";
import useQuery from "~/hooks/useQuery";
import lazyWithRetry from "~/utils/lazyWithRetry";
import type { Props } from "./Table";
const Table = lazyWithRetry(() => import("~/components/Table"));
const TableFromParams = (
props: Omit<Props, "onChangeSort" | "onChangePage" | "topRef">
) => {
const topRef = React.useRef();
const location = useLocation();
const history = useHistory();
const params = useQuery();
const handleChangeSort = React.useCallback(
(sort, direction) => {
if (sort) {
params.set("sort", sort);
} else {
params.delete("sort");
}
params.set("direction", direction.toLowerCase());
history.replace({
pathname: location.pathname,
search: params.toString(),
});
},
[params, history, location.pathname]
);
const handleChangePage = React.useCallback(
(page) => {
if (page) {
params.set("page", page.toString());
} else {
params.delete("page");
}
history.replace({
pathname: location.pathname,
search: params.toString(),
});
if (topRef.current) {
scrollIntoView(topRef.current, {
scrollMode: "if-needed",
behavior: "auto",
block: "start",
});
}
},
[params, history, location.pathname]
);
return (
<Table
topRef={topRef}
onChangeSort={handleChangeSort}
onChangePage={handleChangePage}
{...props}
/>
);
};
export default observer(TableFromParams);
-1
View File
@@ -45,7 +45,6 @@ const Sticky = styled.div`
margin: 0 -8px;
padding: 0 8px;
background: ${s("background")};
transition: ${s("backgroundTransition")};
z-index: 1;
`;

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