Compare commits

...

61 Commits

Author SHA1 Message Date
google-labs-jules[bot] 57a2068d5c Refactor: Convert SelectionToolbar to an editor widget extension
This commit refactors the `SelectionToolbar` to operate as a Prosemirror editor extension using the widget interface. This aligns its architecture with other dynamic editor elements like `HoverPreviews`.

Key changes include:

-   **New `SelectionToolbarExtension`**: Created in `app/editor/extensions/` to manage the lifecycle, visibility, and state of the selection toolbar. It uses a Prosemirror plugin to monitor selection changes and updates an observable `isActive` state.
-   **Modified `SelectionToolbar` Component**: The React component in `app/editor/components/` has been simplified. It no longer manages its own visibility or selection-tracking state. Instead, it receives an `active` prop from the extension and focuses solely on rendering the appropriate menu items based on the current editor state and props.
-   **Editor Integration**: The main editor setup in `app/editor/index.tsx` (and its usage in `app/components/Editor.tsx`) has been updated to include `SelectionToolbarExtension` and remove the old direct rendering of the `SelectionToolbar` component.
-   **Prop Handling**: The `SelectionToolbarExtension` now correctly sources necessary props (like `rtl`, `isTemplate`, `readOnly`, `canComment`, `canUpdate`, `onClickLink`) from the editor's props and passes them to the `SelectionToolbar` component.
-   **Testing**: Added new unit tests for `SelectionToolbarExtension` in `app/editor/extensions/SelectionToolbarExtension.test.tsx`. These tests cover the core visibility logic (activation/deactivation based on selection) and serve as placeholders for verifying menu contexts.

This refactoring improves the modularity of the editor, centralizes the logic for the selection toolbar's behavior within an extension, and follows the established pattern for editor UI widgets.
2025-05-22 02:40:12 +00:00
Tom Moor 76d9a02fee fix: Use team name on link metadata if public branding enabled (#9273) 2025-05-21 21:01:31 -04:00
Tom Moor 1752c04c06 fix: Initial avatars in notification popover malformed (#9274)
closes #9272
2025-05-21 21:01:23 -04:00
Tom Moor 084490ba6b chore: Remove React in scope requirement (#9261)
* Add rules

* codemod: update-react-imports

* Update babelrc
2025-05-20 19:26:11 -04:00
codegen-sh[bot] eaf2fd102e Add attachments.list API endpoint (#9259)
* Add attachments.list endpoint to retrieve user's attachments

* Fix TypeScript error in attachments.list endpoint

* Address review comments: restore tests, properly type WhereOptions, remove comments

* fix: Pagination, user filtering

---------

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
Co-authored-by: Codegen Bot <codegen@example.com>
Co-authored-by: Tom Moor <tom@getoutline.com>
2025-05-20 19:25:55 -04:00
codegen-sh[bot] 3bc566915e Fix document URL generation for titles with only special characters (#9258)
* Fix document URL generation for titles with only special characters

* Refactor: Update instance path method to use static getPath method

---------

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
2025-05-20 07:32:07 -04:00
Tom Moor 19627f4d07 fix: Guard all import tasks being filtered out (#9256) 2025-05-19 23:18:31 -04:00
codegen-sh[bot] 5274b99277 fix: Update handleDisclosureClick to handle undefined events (#9254)
* fix: Pass synthetic event to onDisclosureClick to prevent TypeError

* fix: Update handleDisclosureClick to handle undefined events

---------

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
2025-05-19 22:34:01 -04:00
dependabot[bot] 7cabefaf34 chore(deps): bump the aws group with 5 updates (#9250)
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.806.0` | `3.812.0` |
| [@aws-sdk/lib-storage](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/lib/lib-storage) | `3.806.0` | `3.812.0` |
| [@aws-sdk/s3-presigned-post](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-presigned-post) | `3.806.0` | `3.812.0` |
| [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-request-presigner) | `3.806.0` | `3.812.0` |
| [@aws-sdk/signature-v4-crt](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/signature-v4-crt) | `3.806.0` | `3.812.0` |


Updates `@aws-sdk/client-s3` from 3.806.0 to 3.812.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.812.0/clients/client-s3)

Updates `@aws-sdk/lib-storage` from 3.806.0 to 3.812.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.812.0/lib/lib-storage)

Updates `@aws-sdk/s3-presigned-post` from 3.806.0 to 3.812.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.812.0/packages/s3-presigned-post)

Updates `@aws-sdk/s3-request-presigner` from 3.806.0 to 3.812.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.812.0/packages/s3-request-presigner)

Updates `@aws-sdk/signature-v4-crt` from 3.806.0 to 3.812.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.812.0/packages/signature-v4-crt)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-19 19:31:32 -04:00
dependabot[bot] 81729ae72b chore(deps-dev): bump eslint-import-resolver-typescript (#9252)
Bumps [eslint-import-resolver-typescript](https://github.com/import-js/eslint-import-resolver-typescript) from 3.8.0 to 3.10.1.
- [Release notes](https://github.com/import-js/eslint-import-resolver-typescript/releases)
- [Changelog](https://github.com/import-js/eslint-import-resolver-typescript/blob/v3.10.1/CHANGELOG.md)
- [Commits](https://github.com/import-js/eslint-import-resolver-typescript/compare/v3.8.0...v3.10.1)

---
updated-dependencies:
- dependency-name: eslint-import-resolver-typescript
  dependency-version: 3.10.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-19 17:56:34 -04:00
dependabot[bot] cd2d9fc218 chore(deps): bump @tanstack/react-table from 8.20.6 to 8.21.3 (#9249)
Bumps [@tanstack/react-table](https://github.com/TanStack/table/tree/HEAD/packages/react-table) from 8.20.6 to 8.21.3.
- [Release notes](https://github.com/TanStack/table/releases)
- [Commits](https://github.com/TanStack/table/commits/v8.21.3/packages/react-table)

---
updated-dependencies:
- dependency-name: "@tanstack/react-table"
  dependency-version: 8.21.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-19 17:56:18 -04:00
dependabot[bot] 4d7340d70b chore(deps): bump @radix-ui/react-visually-hidden from 1.2.0 to 1.2.2 (#9248)
Bumps [@radix-ui/react-visually-hidden](https://github.com/radix-ui/primitives) from 1.2.0 to 1.2.2.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-19 17:56:11 -04:00
Tom Moor e596b57cc2 chore: Add additional details to validation errors (#9243)
This will help us get to the bottom of Notion importer failures
2025-05-18 20:27:47 -04:00
Translate-O-Tron 58b6901b7b New Crowdin updates (#9046)
* fix: New Korean translations from Crowdin [ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* touch

---------

Co-authored-by: Tom Moor <tom@getoutline.com>
2025-05-18 23:27:23 +00:00
Tom Moor b8fd239f2e Add additional logging to getFileStream failures (#9242) 2025-05-18 19:24:50 -04:00
Tom Moor 201fbb56eb perf: Add cache for document structure (#9196)
* Normalize Collection.findByPk

* Add caching of documentStructure

* fix: Do not set cache before transaction is flushed

* Mock Redis
2025-05-18 18:45:00 -04:00
Tom Moor 823b0442a2 fix: Image caption uncentered (#9239) 2025-05-18 18:51:50 +00:00
Tom Moor 4ff663e112 Adds menu option to apply template to existing document (#9236)
* Adds menu option to apply template to existing document

* Memoize, docs

* docs
2025-05-18 09:40:37 -04:00
codegen-sh[bot] e5ded0a6a5 Fix misalignment between email and comment mentions (#9234)
* Fix misalignment between email and comment mentions

* Add test

* Update ProsemirrorHelper.tsx

* Optimize user mention processing with batch loading

* Add test for multiple mentions

---------

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
Co-authored-by: Tom Moor <tom@getoutline.com>
2025-05-17 12:45:45 -04:00
Balázs Úr 784d075233 feat: Add Hungarian translations (#9230) 2025-05-17 12:22:03 -04:00
codegen-sh[bot] 1c9b300e25 feat: add lint:changed script to lint only unstaged files (#9235)
Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
2025-05-17 12:21:15 -04:00
codegen-sh[bot] 870bf1157b fix: Increase defaultSignedUrlExpires from 60s to 5 minutes (#9233)
Closes #8921

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
2025-05-17 13:07:01 +00:00
codegen-sh[bot] d2aba1de96 feat: Add POST method option to redirectOnClient (#9228)
* feat: Add POST method option to redirectOnClient helper

* Applied automatic fixes

* fix: Add missing closing HTML tag in redirectOnClient GET method

* fix: Use lodash escape for form field values to prevent XSS

* Applied automatic fixes

* fix: Add missing lodash/escape import

* Applied automatic fixes

* fix: Escape all URLs in redirectOnClient function

* Update index.ts

* fix: CSP

* Refactor CSP middleware

* docs, only use for email signin

---------

Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
Co-authored-by: Tom Moor <tom@getoutline.com>
2025-05-17 09:06:32 -04:00
Hemachandar 052924d816 Resolve index collision when restoring collection (#9229)
* Resolve index collision when restoring collection

* use beforeUpdate hook
2025-05-16 23:56:46 -04:00
Apoorv Mishra 2fe887ef57 Refactor Placeholder plugin to make it configurable (#9224)
* fix: refactor to make configurable

* fix: make it a reusable plugin so that it can be colocated with nodes eligible for placeholders

* fix: cond -> condition

* fix: `pos` -> `$start` as param

* fix: cleanup
2025-05-16 20:58:06 -04:00
Tom Moor e288a5d38e fix: Remove # from stored filesystem keys (#9231) 2025-05-17 00:14:42 +00:00
Hemachandar dc5c3f5280 fix: Consider non-archived collections only for index computation (#9225) 2025-05-16 09:39:32 -04:00
Hemachandar 610721eed6 fix: Ensure collection withUser scope is not overriden (#9226) 2025-05-16 08:16:25 -04:00
Hemachandar d50f0986bb fix: Reset editing state when collection/document title is unmodified (#9221) 2025-05-15 14:12:26 +00:00
Tom Moor 90af35d4bd Update .env.sample (#9217) 2025-05-15 02:08:15 +00:00
Tom Moor 3810373195 fix: CleanupDeletedDocumentsTask attribute selection (#9215) 2025-05-14 20:45:29 -04:00
Tom Moor 3fd893e728 Add new markdown shortcut for tables to shortcut UI (#9216) 2025-05-14 23:52:32 +00:00
Tom Moor 13e3aaf861 chore: Add Sentry breadcrumbs to websocket messages for debugging (#9211) 2025-05-14 19:37:29 -04:00
Tom Moor b43ebabbaf fix: Apex redirect regression (#9214) 2025-05-14 19:37:16 -04:00
Tom Moor 42550a003a fix: Icon on collection home does not match sidebar when private (#9209) 2025-05-14 04:23:55 +00:00
Tom Moor 08b7c11461 Normalize Collection.findByPk (#9193) 2025-05-14 00:05:45 -04:00
Tom Moor 8a9a8cf751 fix: Archived documents should not show in @mention suggestions (#9208) 2025-05-13 22:47:58 -04:00
Tom Moor 2d6167e933 fix: Add encoding meta tag to exported HTML (#9207)
closes #9194
2025-05-13 22:47:48 -04:00
dependabot[bot] 6b05b101d0 chore(deps): bump the aws group with 5 updates (#9186)
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.803.0` | `3.806.0` |
| [@aws-sdk/lib-storage](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/lib/lib-storage) | `3.803.0` | `3.806.0` |
| [@aws-sdk/s3-presigned-post](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-presigned-post) | `3.803.0` | `3.806.0` |
| [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-request-presigner) | `3.803.0` | `3.806.0` |
| [@aws-sdk/signature-v4-crt](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/signature-v4-crt) | `3.803.0` | `3.806.0` |


Updates `@aws-sdk/client-s3` from 3.803.0 to 3.806.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.806.0/clients/client-s3)

Updates `@aws-sdk/lib-storage` from 3.803.0 to 3.806.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.806.0/lib/lib-storage)

Updates `@aws-sdk/s3-presigned-post` from 3.803.0 to 3.806.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.806.0/packages/s3-presigned-post)

Updates `@aws-sdk/s3-request-presigner` from 3.803.0 to 3.806.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.806.0/packages/s3-request-presigner)

Updates `@aws-sdk/signature-v4-crt` from 3.803.0 to 3.806.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.806.0/packages/signature-v4-crt)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-13 21:06:24 -04:00
Adam Roe 79fe73fbe1 fix: fall back to id_token when OIDC userinfo endpoint is sparse (#9172)
* Fall back to id_token if profile does not contain username or email

* More comments

* Add error handling to id_token decode

* simplify username fallback logic using nullish coalescing

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

* make id_token decoding more tolerant of malformed or invalid tokens

---------

Co-authored-by: Tom Moor <tom.moor@gmail.com>
2025-05-13 21:06:14 -04:00
MehdiBouzouaya 63376ed9c8 Fix flash of previous value after editing title in sidebar for documents (#9197)
* Fix flash of previous value after editing title in sidebar for documents

* Fix flash of previous value after editing title in sidebar for documents
2025-05-13 22:36:47 +00:00
MehdiBouzouaya 0cec66b3bb Fix: Add missing key prop to context menu items to avoid React warning (#9202) 2025-05-13 18:31:21 -04:00
dependabot[bot] fcc73e772b chore(deps): bump the babel group with 2 updates (#9185)
Bumps the babel group with 2 updates: [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) and [@babel/cli](https://github.com/babel/babel/tree/HEAD/packages/babel-cli).


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

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

---
updated-dependencies:
- dependency-name: "@babel/preset-env"
  dependency-version: 7.27.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: babel
- dependency-name: "@babel/cli"
  dependency-version: 7.27.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: babel
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-12 19:29:38 -04:00
dependabot[bot] b5cb6128c4 chore(deps-dev): bump react-refresh from 0.14.2 to 0.17.0 (#9187)
Bumps [react-refresh](https://github.com/facebook/react/tree/HEAD/packages/react) from 0.14.2 to 0.17.0.
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/HEAD/packages/react)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-12 19:29:22 -04:00
dependabot[bot] 261226c110 chore(deps): bump @types/mailparser from 3.4.5 to 3.4.6 (#9188)
Bumps [@types/mailparser](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/mailparser) from 3.4.5 to 3.4.6.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/mailparser)

---
updated-dependencies:
- dependency-name: "@types/mailparser"
  dependency-version: 3.4.6
  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-05-12 19:05:25 -04:00
Tom Moor 6fff437196 fix: Incorrect translation string on disabled API keys (#9183) 2025-05-12 07:56:46 -04:00
Tom Moor 4f34e70d32 chore: Add automation to close PRs that never accept CLA (#9179)
* Add script to auto-close stale PRs

* Improve specificity
2025-05-11 17:14:55 -04:00
Tom Moor 4c04bd9359 v0.84.0 (#9176) 2025-05-11 15:58:16 +00:00
Tom Moor 16c8ae6132 Create README.md (#9174) 2025-05-11 12:56:26 +00:00
Tom Moor 30bba3a69b fix: JS error when no integrations are connected (#9170) 2025-05-11 02:05:18 +00:00
Tom Moor 32c1712fdc fix: Various cases that could leave file handles open on export (#9168)
* fix: Various cases that could leave file handles open on export

* Consolidate error handling
2025-05-10 17:48:24 -04:00
Tom Moor d392149860 fix: Non-integration plugins missing in settings (#9167)
Other minor refactors
2025-05-10 12:45:06 -04:00
Tom Moor 30108ebded chore: Move Zapier settings page to plugin (#9166) 2025-05-10 10:25:46 -04:00
Tom Moor d0bd2baa9f Add integrations page (#9155)
* update useSettings

* Integration page skeleton

* add descriptions

* update design

* Integration page style update

* clean up

* update integration card

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

* Update integration icon size

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

* Update all integrations menu item

* update IntegrationCard to use the `Text` component

* update card status

* fix: Google analytics never shows as installed
fix: Styling tweaks
Move webhooks out of integrations

* Add breadcrumbs

* Add filtering

* refactor

* Add hover state, tweak descriptions

---------

Co-authored-by: Tess99854 <tesnimesb@gmail.com>
Co-authored-by: Mahmoud Mohammed Ali <ibn.el4ai5@gmail.com>
Co-authored-by: Mahmoud Ali <mahmoud.ali.khallaf@gmail.com>
2025-05-10 09:59:41 -04:00
Tom Moor fd984774d0 Add smart preloading of settings screens to reduce flicker (#9165) 2025-05-10 09:17:43 -04:00
Tom Moor e216c68f6d fix: CMD+F with in-app find interface open should open native find interface (#9153) 2025-05-08 21:40:01 -04:00
Tom Moor 2e2a8bcc94 fix: Allow searching for current user in collection permissions (#9154) 2025-05-08 22:15:16 +00:00
Tom Moor 245d14f905 fix: Upgrade KaTeX (#9151) 2025-05-08 00:40:50 +00:00
Tom Moor 8717d160ce fix: Backlinks are limited at 25 (#9150) 2025-05-07 20:36:56 -04:00
Tom Moor 587ba85cc9 fix: LaTeX blocks show vertical scrollbar (#9149) 2025-05-08 00:17:47 +00:00
Tom Moor 80bb1ce977 fix: ExportDocumentTreeTask needs documentStructure (#9148) 2025-05-07 23:42:59 +00:00
396 changed files with 6421 additions and 2807 deletions
+7 -2
View File
@@ -1,6 +1,11 @@
{
"presets": [
"@babel/preset-react",
[
"@babel/preset-react",
{
"runtime": "automatic"
}
],
"@babel/preset-env",
"@babel/preset-typescript"
],
@@ -60,4 +65,4 @@
]
}
}
}
}
+165 -140
View File
@@ -1,48 +1,80 @@
# –––––––––––––––– REQUIRED ––––––––––––––––
NODE_ENV=production
# Generate a hex-encoded 32-byte random key. You should use `openssl rand -hex 32`
# in your terminal to generate a random value.
SECRET_KEY=generate_a_new_key
# Generate a unique random key. The format is not important but you could still use
# `openssl rand -hex 32` in your terminal to produce this.
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@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://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.
# Example: Use Redis Sentinel for high availability
# {"sentinels":[{"host":"sentinel-0","port":26379},{"host":"sentinel-1","port":26379}],"name":"mymaster"}
# REDIS_URL=ioredis://eyJzZW50aW5lbHMiOlt7Imhvc3QiOiJzZW50aW5lbC0wIiwicG9ydCI6MjYzNzl9LHsiaG9zdCI6InNlbnRpbmVsLTEiLCJwb3J0IjoyNjM3OX1dLCJuYW1lIjoibXltYXN0ZXIifQ==
# URL should point to the fully qualified, publicly accessible URL. If using a
# proxy the port in URL and PORT may be different.
# This URL should point to the fully qualified, publicly accessible, URL. If using a
# proxy this will be the proxy's URL.
URL=
# The port to expose the Outline server on, this should match what is configured
# in your docker-compose.yml
PORT=3000
# See [documentation](docs/SERVICES.md) on running a separate collaboration
# server, for normal operation this does not need to be set.
COLLABORATION_URL=
# If using a Cloudfront/Cloudflare distribution or similar it can be set below.
# This will cause paths to javascript, stylesheets, and images to be updated to
# the hostname defined in CDN_URL. In your CDN configuration the origin server
# should be set to the same as URL.
CDN_URL=
# How many processes should be spawned. As a reasonable rule divide your servers
# available memory by 512 for a rough estimate
WEB_CONCURRENCY=1
# Generate a hex-encoded 32-byte random key. Use `openssl rand -hex 32` in your
# terminal to generate a random value.
SECRET_KEY=generate_a_new_key
# Generate a unique random key. The format is not important but you could still use
# `openssl rand -hex 32` in your terminal to generate a random value.
UTILS_SECRET=generate_a_new_key
# The default interface language. See translate.getoutline.com for a list of
# available language codes and their rough percentage translated.
DEFAULT_LANGUAGE=en_US
# ––––––––––––––––––––––––––––––––––––––
# ––––––––––––– DATABASE –––––––––––––
# ––––––––––––––––––––––––––––––––––––––
# The database URL for your production database, including username, password, and database name.
DATABASE_URL=postgres://user:pass@postgres:5432/outline
# The in-memory database pool per-process settings. Ensure that the pool size that will not exceed
# the maximum number of connections allowed by your database. Defaults to 0 and 5.
DATABASE_CONNECTION_POOL_MIN=
DATABASE_CONNECTION_POOL_MAX=
# Uncomment this line if you will not use SSL for connecting to Postgres. This is acceptable
# if the database and the application are on the same machine.
# PGSSLMODE=disable
# ––––––––––––––––––––––––––––––––––––––
# –––––––––––––– REDIS –––––––––––––––
# ––––––––––––––––––––––––––––––––––––––
# The Redis URL for your environment you can either specify an ioredis compatible url or a Base64
# encoded configuration object.
# DOCS: https://docs.getoutline.com/s/hosting/doc/redis-LGM4BFXYp4
REDIS_URL=redis://redis:6379
# ––––––––––––––––––––––––––––––––––––––
# ––––––––––– FILE STORAGE –––––––––––
# ––––––––––––––––––––––––––––––––––––––
# Specify what storage system to use. Possible value is one of "s3" or "local".
# For "local", the avatar images and document attachments will be saved on local disk.
# For "local" images and document attachments will be saved on local disk, for "s3" they
# will be stored in an S3-compatible network store.
# DOCS: https://docs.getoutline.com/s/hosting/doc/file-storage-N4M0T6Ypu7
FILE_STORAGE=local
# If "local" is configured for FILE_STORAGE above, then this sets the parent directory under
# which all attachments/images go. Make sure that the process has permissions to create
# this path and also to write files to it.
# which all attachments/images are stored. Make sure that the process has permissions to
# create this path and also to write files to it.
FILE_STORAGE_LOCAL_ROOT_DIR=/var/lib/outline/data
# Maximum allowed size for the uploaded attachment.
@@ -56,8 +88,8 @@ FILE_STORAGE_IMPORT_MAX_SIZE=
# and the files are temporary being automatically deleted after a period of time.
FILE_STORAGE_WORKSPACE_IMPORT_MAX_SIZE=
# To support uploading of images for avatars and document attachments in a distributed
# architecture an s3-compatible storage can be configured if FILE_STORAGE=s3 above.
# To support uploading of images for avatars and document attachments in a distributed
# architecture, an s3-compatible storage can be configured if FILE_STORAGE=s3 above.
AWS_ACCESS_KEY_ID=get_a_key_from_aws
AWS_SECRET_ACCESS_KEY=get_the_secret_of_above_key
AWS_REGION=xx-xxxx-x
@@ -67,38 +99,55 @@ AWS_S3_UPLOAD_BUCKET_NAME=bucket_name_here
AWS_S3_FORCE_PATH_STYLE=true
AWS_S3_ACL=private
# –––––––––––––– AUTHENTICATION ––––––––––––––
# ––––––––––––––––––––––––––––––––––––––
# –––––––––––––––– SSL –––––––––––––––
# ––––––––––––––––––––––––––––––––––––––
# Base64 encoded private key and certificate for HTTPS termination. This is one
# of three ways to configure SSL and can be left empty.
# DOCS: https://docs.getoutline.com/s/hosting/doc/ssl-pzk7WO8d1n
SSL_KEY=
SSL_CERT=
# Auto-redirect to https in production. The default is true but you may set to
# false if you can be sure that SSL is terminated at an external loadbalancer.
FORCE_HTTPS=true
# ––––––––––––––––––––––––––––––––––––––
# –––––––––– AUTHENTICATION ––––––––––
# ––––––––––––––––––––––––––––––––––––––
# Third party signin credentials, at least ONE OF EITHER Google, Slack,
# or Microsoft is required for a working installation or you'll have no sign-in
# options.
# Discord, or Microsoft is required for a working installation or you'll
# have no sign-in options.
# To configure Slack auth, you'll need to create an Application at
# => https://api.slack.com/apps
#
# When configuring the Client ID, add a redirect URL under "OAuth & Permissions":
# https://<URL>/auth/slack.callback
# Slack sign-in provider
# DOCS: https://docs.getoutline.com/s/hosting/doc/slack-sgMujR8J9J
SLACK_CLIENT_ID=get_a_key_from_slack
SLACK_CLIENT_SECRET=get_the_secret_of_above_key
# To configure Google auth, you'll need to create an OAuth Client ID at
# => https://console.cloud.google.com/apis/credentials
#
# When configuring the Client ID, add an Authorized redirect URI:
# https://<URL>/auth/google.callback
# Google sign-in provider
# DOCS: https://docs.getoutline.com/s/hosting/doc/google-hOuvtCmTqQ
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# To configure Microsoft/Azure auth, you'll need to create an OAuth Client. See
# the guide for details on setting up your Azure App:
# => https://wiki.generaloutline.com/share/dfa77e56-d4d2-4b51-8ff8-84ea6608faa4
# Microsoft Entra / Azure AD sign-in provider
# DOCS: https://docs.getoutline.com/s/hosting/doc/microsoft-entra-UVz6jsIOcv
AZURE_CLIENT_ID=
AZURE_CLIENT_SECRET=
AZURE_RESOURCE_APP_ID=
# To configure generic OIDC auth, you'll need some kind of identity provider.
# See documentation for whichever IdP you use to acquire the following info:
# Redirect URI is https://<URL>/auth/oidc.callback
# Discord sign-in provider
# DOCS: https://docs.getoutline.com/s/hosting/doc/discord-g4JdWFFub6
DISCORD_CLIENT_ID=
DISCORD_CLIENT_SECRET=
DISCORD_SERVER_ID=
DISCORD_SERVER_ROLES=
# Generic OIDC provider
# DOCS: https://docs.getoutline.com/s/hosting/doc/oidc-8CPBm6uC0I
OIDC_CLIENT_ID=
OIDC_CLIENT_SECRET=
OIDC_AUTH_URI=
@@ -116,83 +165,54 @@ OIDC_DISPLAY_NAME=OpenID Connect
# Space separated auth scopes.
OIDC_SCOPES=openid profile email
# To configure the GitHub integration, you'll need to create a GitHub App at
# => https://github.com/settings/apps
#
# When configuring the Client ID, add a redirect URL under "Permissions & events":
# https://<URL>/api/github.callback
# ––––––––––––––––––––––––––––––––––––––
# –––––––––––––– EMAIL –––––––––––––––
# ––––––––––––––––––––––––––––––––––––––
# To support sending outgoing transactional emails such as "document updated" or
# email sign-in you'll need to connect an SMTP server. Service can be configured
# with any service from this list: https://community.nodemailer.com/2-0-0-beta/setup-smtp/well-known-services/
# DOCS: https://docs.getoutline.com/s/hosting/doc/smtp-cqCJyZGMIB
SMTP_SERVICE=
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_FROM_EMAIL=
# ––––––––––––––––––––––––––––––––––––––
# –––––––––– RATE LIMITER ––––––––––––
# ––––––––––––––––––––––––––––––––––––––
# Whether the rate limiter is enabled or not
RATE_LIMITER_ENABLED=true
# Individual endpoints have hardcoded rate limits that are enabled
# with the above setting, however this is a global rate limiter
# across all requests
RATE_LIMITER_REQUESTS=1000
RATE_LIMITER_DURATION_WINDOW=60
# ––––––––––––––––––––––––––––––––––––––
# ––––––––––– INTEGRATIONS –––––––––––
# ––––––––––––––––––––––––––––––––––––––
# The GitHub integration allows previewing issue and pull request links
# DOCS: https://docs.getoutline.com/s/hosting/doc/github-GchT3NNxI9
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
GITHUB_APP_NAME=
GITHUB_APP_ID=
GITHUB_APP_PRIVATE_KEY=
# Linear
# The Linear integration allows previewing issue links as rich mentions
LINEAR_CLIENT_ID=
LINEAR_CLIENT_SECRET=
# To configure Discord auth, you'll need to create a Discord Application at
# => https://discord.com/developers/applications/
#
# When configuring the Client ID, add a redirect URL under "OAuth2":
# https://<URL>/auth/discord.callback
DISCORD_CLIENT_ID=
DISCORD_CLIENT_SECRET=
# DISCORD_SERVER_ID should be the ID of the Discord server that Outline is
# integrated with.
# Used to verify that the user is a member of the server as well as server
# metadata such as nicknames, server icon and name.
DISCORD_SERVER_ID=
# DISCORD_SERVER_ROLES should be a comma separated list of role IDs that are
# allowed to access Outline. If this is not set, all members of the server
# will be allowed to access Outline.
# DISCORD_SERVER_ID and DISCORD_SERVER_ROLES must be set together.
DISCORD_SERVER_ROLES=
# –––––––––––––– IMPORTS ––––––––––––––
NOTION_CLIENT_ID=
NOTION_CLIENT_SECRET=
# –––––––––––––––– OPTIONAL ––––––––––––––––
# Base64 encoded private key and certificate for HTTPS termination. This is only
# required if you do not use an external reverse proxy. See documentation:
# https://wiki.generaloutline.com/share/1c922644-40d8-41fe-98f9-df2b67239d45
SSL_KEY=
SSL_CERT=
# If using a Cloudfront/Cloudflare distribution or similar it can be set below.
# This will cause paths to javascript, stylesheets, and images to be updated to
# the hostname defined in CDN_URL. In your CDN configuration the origin server
# should be set to the same as URL.
CDN_URL=
# Auto-redirect to https in production. The default is true but you may set to
# false if you can be sure that SSL is terminated at an external loadbalancer.
FORCE_HTTPS=true
# Have the installation check for updates by sending anonymized statistics to
# the maintainers
ENABLE_UPDATES=true
# How many processes should be spawned. As a reasonable rule divide your servers
# available memory by 512 for a rough estimate
WEB_CONCURRENCY=1
# You can remove this line if your reverse proxy already logs incoming http
# requests and this ends up being duplicative
DEBUG=http
# Configure lowest severity level for server logs. Should be one of
# error, warn, info, http, verbose, debug and silly
LOG_LEVEL=info
# For a complete Slack integration with search and posting to channels the
# following configs are also needed, some more details
# => https://wiki.generaloutline.com/share/be25efd1-b3ef-4450-b8e5-c4a4fc11e02a
#
# following configs are also needed in addition to Slack authentication:
# DOCS: https://docs.getoutline.com/s/hosting/doc/slack-G2mc8DOJHk
SLACK_VERIFICATION_TOKEN=your_token
SLACK_APP_ID=A0XXXXXXX
SLACK_MESSAGE_ACTIONS=true
@@ -202,29 +222,34 @@ SLACK_MESSAGE_ACTIONS=true
DROPBOX_APP_KEY=
# Optionally enable Sentry (sentry.io) to track errors and performance,
# and optionally add a Sentry proxy tunnel for bypassing ad blockers in the UI:
# https://docs.sentry.io/platforms/javascript/troubleshooting/#using-the-tunnel-option)
# DOCS: https://docs.getoutline.com/s/hosting/doc/sentry-jxcFttcDl5
SENTRY_DSN=
SENTRY_TUNNEL=
# To support sending outgoing transactional emails such as "document updated" or
# "you've been invited" you'll need to provide authentication for an SMTP server
SMTP_SERVICE=
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_FROM_EMAIL=
# Enable importing pages from a Notion workspace
# DOCS: https://docs.getoutline.com/s/hosting/doc/notion-2v6g7WY3l3
NOTION_CLIENT_ID=
NOTION_CLIENT_SECRET=
# The default interface language. See translate.getoutline.com for a list of
# available language codes and their rough percentage translated.
DEFAULT_LANGUAGE=en_US
# Optionally enable rate limiter at application web server
RATE_LIMITER_ENABLED=true
# Configure default throttling parameters for rate limiter
RATE_LIMITER_REQUESTS=1000
RATE_LIMITER_DURATION_WINDOW=60
# Iframely API config
# The Iframely integration allows previews of third-party content within Outline.
# For example, hovering over an external link will show a preview.
# DOCS: https://docs.getoutline.com/s/hosting/doc/iframely-HwLF1EZ9mo
IFRAMELY_URL=
IFRAMELY_API_KEY=
# ––––––––––––––––––––––––––––––––––––––
# ––––––––––––– DEBUGGING ––––––––––––
# ––––––––––––––––––––––––––––––––––––––
# Have the installation check for updates by sending anonymized statistics to
# the maintainers
ENABLE_UPDATES=true
# Debugging categories to enable you can remove the default "http" value if
# your proxy already logs incoming http requests and this ends up being duplicative
DEBUG=http
# Configure lowest severity level for server logs. Should be one of
# error, warn, info, http, verbose, debug, or silly
LOG_LEVEL=info
+43 -12
View File
@@ -2,7 +2,9 @@
"parser": "@typescript-eslint/parser",
"parserOptions": {
"sourceType": "module",
"extraFileExtensions": [".json"],
"extraFileExtensions": [
".json"
],
"project": "./tsconfig.json",
"ecmaFeatures": {
"jsx": true
@@ -17,6 +19,7 @@
],
"plugins": [
"es",
"react",
"@typescript-eslint",
"eslint-plugin-import",
"eslint-plugin-node",
@@ -27,21 +30,30 @@
"eqeqeq": 2,
"curly": 2,
"no-console": "error",
"arrow-body-style": ["error", "as-needed"],
"arrow-body-style": [
"error",
"as-needed"
],
"spaced-comment": "error",
"object-shorthand": "error",
"no-mixed-operators": "off",
"no-useless-escape": "off",
"no-shadow": "off",
"es/no-regexp-lookbehind-assertions": "error",
"react/self-closing-comp": ["error", {
"component": true,
"html": true
}],
"react/react-in-jsx-scope": "off",
"react/self-closing-comp": [
"error",
{
"component": true,
"html": true
}
],
"@typescript-eslint/no-shadow": [
"warn",
{
"allow": ["transaction"],
"allow": [
"transaction"
],
"hoist": "all",
"ignoreTypeValueShadow": true
}
@@ -63,9 +75,25 @@
"ignoreRestSiblings": true
}
],
"padding-line-between-statements": ["error", { "blankLine": "always", "prev": "*", "next": "export" }],
"lines-between-class-members": ["error", "always", { "exceptAfterSingleLine": true }],
"lodash/import-scope": ["error", "method"],
"padding-line-between-statements": [
"error",
{
"blankLine": "always",
"prev": "*",
"next": "export"
}
],
"lines-between-class-members": [
"error",
"always",
{
"exceptAfterSingleLine": true
}
],
"lodash/import-scope": [
"error",
"method"
],
"import/no-named-as-default": "off",
"import/no-named-as-default-member": "off",
"import/newline-after-import": 2,
@@ -134,10 +162,13 @@
"version": "detect"
},
"import/parsers": {
"@typescript-eslint/parser": [".ts", ".tsx"]
"@typescript-eslint/parser": [
".ts",
".tsx"
]
},
"import/resolver": {
"typescript": {}
}
}
}
}
+59
View File
@@ -0,0 +1,59 @@
name: Auto Close Unsigned PRs
on:
schedule:
- cron: '0 0 * * *' # Run daily at midnight UTC
jobs:
close-unsigned-prs:
runs-on: ubuntu-latest
permissions:
pull-requests: write
issues: write
steps:
- name: Close unsigned PRs
uses: actions/github-script@v6
with:
script: |
const now = new Date();
const TWO_WEEKS = 14 * 24 * 60 * 60 * 1000; // 14 days in milliseconds
const prs = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open'
});
for (const pr of prs.data) {
const prCreatedAt = new Date(pr.created_at);
const prAge = now - prCreatedAt;
if (prAge < TWO_WEEKS) continue;
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number
});
const hasNotSignedComment = comments.data.some(comment =>
comment.body.toLowerCase().includes('https://cla-assistant.io/pull/badge/not_signed')
);
if (hasNotSignedComment) {
await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
state: 'closed'
});
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body: 'This PR has been automatically closed because it has been open for more than 14 days and has not accepted the CLA.'
});
}
}
+2 -2
View File
@@ -3,7 +3,7 @@ Business Source License 1.1
Parameters
Licensor: General Outline, Inc.
Licensed Work: Outline 0.83.0
Licensed Work: Outline 0.84.0
The Licensed Work is (c) 2025 General Outline, Inc.
Additional Use Grant: You may make use of the Licensed Work, provided that
you may not use the Licensed Work for a Document
@@ -15,7 +15,7 @@ Additional Use Grant: You may make use of the Licensed Work, provided that
Licensed Work by creating teams and documents
controlled by such third parties.
Change Date: 2029-04-11
Change Date: 2029-05-11
Change License: Apache License, Version 2.0
+3
View File
@@ -7,6 +7,9 @@
"plugins": [
"eslint-plugin-react-hooks"
],
"rules": {
"react/react-in-jsx-scope": "off"
},
"env": {
"jest": true,
"browser": true
-1
View File
@@ -1,5 +1,4 @@
import { PlusIcon } from "outline-icons";
import * as React from "react";
import stores from "~/stores";
import ApiKeyNew from "~/scenes/ApiKeyNew";
import { createAction } from "..";
-1
View File
@@ -13,7 +13,6 @@ import {
UnstarredIcon,
UnsubscribeIcon,
} from "outline-icons";
import * as React from "react";
import { toast } from "sonner";
import Collection from "~/models/Collection";
import { CollectionEdit } from "~/components/Collection/CollectionEdit";
-1
View File
@@ -1,5 +1,4 @@
import { DoneIcon, SmileyIcon, TrashIcon } from "outline-icons";
import * as React from "react";
import { toast } from "sonner";
import stores from "~/stores";
import Comment from "~/models/Comment";
-1
View File
@@ -7,7 +7,6 @@ import {
TrashIcon,
UserIcon,
} from "outline-icons";
import * as React from "react";
import { toast } from "sonner";
import { createAction } from "~/actions";
import { DeveloperSection } from "~/actions/sections";
-1
View File
@@ -31,7 +31,6 @@ import {
LogoutIcon,
CaseSensitiveIcon,
} from "outline-icons";
import * as React from "react";
import { toast } from "sonner";
import Icon from "@shared/components/Icon";
import {
-1
View File
@@ -13,7 +13,6 @@ import {
ShapesIcon,
DraftsIcon,
} from "outline-icons";
import * as React from "react";
import { UrlHelper } from "@shared/utils/UrlHelper";
import { isMac } from "@shared/utils/browser";
import stores from "~/stores";
@@ -1,5 +1,4 @@
import { ArchiveIcon, MarkAsReadIcon } from "outline-icons";
import * as React from "react";
import { createAction } from "..";
import { NotificationSection } from "../sections";
-1
View File
@@ -1,5 +1,4 @@
import { PlusIcon } from "outline-icons";
import * as React from "react";
import stores from "~/stores";
import { OAuthClientNew } from "~/components/OAuthClient/OAuthClientNew";
import { createAction } from "..";
-1
View File
@@ -1,6 +1,5 @@
import copy from "copy-to-clipboard";
import { LinkIcon, RestoreIcon } from "outline-icons";
import * as React from "react";
import { matchPath } from "react-router-dom";
import { toast } from "sonner";
import stores from "~/stores";
-1
View File
@@ -1,5 +1,4 @@
import { SunIcon, MoonIcon, BrowserIcon } from "outline-icons";
import * as React from "react";
import stores from "~/stores";
import { Theme } from "~/stores/UiStore";
import { createAction } from "~/actions";
-1
View File
@@ -1,5 +1,4 @@
import { ArrowIcon, PlusIcon } from "outline-icons";
import * as React from "react";
import styled from "styled-components";
import { stringToColor } from "@shared/utils/color";
import RootStore from "~/stores/RootStore";
-1
View File
@@ -1,5 +1,4 @@
import { PlusIcon } from "outline-icons";
import * as React from "react";
import { UserRole } from "@shared/types";
import { UserRoleHelper } from "@shared/utils/UserRoleHelper";
import stores from "~/stores";
-1
View File
@@ -1,5 +1,4 @@
import flattenDeep from "lodash/flattenDeep";
import * as React from "react";
import { toast } from "sonner";
import { Optional } from "utility-types";
import { v4 as uuidv4 } from "uuid";
-2
View File
@@ -1,5 +1,3 @@
import * as React from "react";
export default function Arrow() {
return (
<svg
+2 -2
View File
@@ -1,5 +1,5 @@
import { observer } from "mobx-react";
import * as React from "react";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Redirect } from "react-router-dom";
import useCurrentUser from "~/hooks/useCurrentUser";
@@ -19,7 +19,7 @@ const Authenticated = ({ children }: Props) => {
// Watching for language changes here as this is the earliest point we might have the user
// available and means we can start loading translations faster
React.useEffect(() => {
useEffect(() => {
void changeLanguage(language, i18n);
}, [i18n, language]);
+15 -2
View File
@@ -45,12 +45,23 @@ type Props = {
};
function Avatar(props: Props) {
const { model, style, variant = AvatarVariant.Round, ...rest } = props;
const {
model,
style,
variant = AvatarVariant.Round,
className,
...rest
} = props;
const src = props.src || model?.avatarUrl;
const [error, handleError] = useBoolean(false);
return (
<Relative style={style} $variant={variant} $size={props.size}>
<Relative
style={style}
$variant={variant}
$size={props.size}
className={className}
>
{src && !error ? (
<Image onError={handleError} src={src} {...rest} />
) : model ? (
@@ -75,6 +86,8 @@ const Relative = styled.div<{ $variant: AvatarVariant; $size: AvatarSize }>`
border-radius: ${(props) =>
props.$variant === AvatarVariant.Round ? "50%" : `${props.$size / 8}px`};
overflow: hidden;
width: ${(props) => props.$size}px;
height: ${(props) => props.$size}px;
`;
const Image = styled.img<{ size: number }>`
-1
View File
@@ -1,5 +1,4 @@
import { GroupIcon } from "outline-icons";
import * as React from "react";
import { useTheme } from "styled-components";
import Squircle from "@shared/components/Squircle";
import Group from "~/models/Group";
+2 -2
View File
@@ -1,7 +1,7 @@
import Avatar, { IAvatar, AvatarSize } from "./Avatar";
import Avatar, { IAvatar, AvatarSize, AvatarVariant } from "./Avatar";
import AvatarWithPresence from "./AvatarWithPresence";
import { GroupAvatar } from "./GroupAvatar";
export { Avatar, GroupAvatar, AvatarSize, AvatarWithPresence };
export { Avatar, GroupAvatar, AvatarSize, AvatarVariant, AvatarWithPresence };
export type { IAvatar };
-1
View File
@@ -1,4 +1,3 @@
import * as React from "react";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import { depths, s } from "@shared/styles";
+2 -2
View File
@@ -1,4 +1,4 @@
import * as React from "react";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { changeLanguage } from "~/utils/language";
@@ -9,7 +9,7 @@ type Props = {
export default function ChangeLanguage({ locale }: Props) {
const { i18n } = useTranslation();
React.useEffect(() => {
useEffect(() => {
void changeLanguage(locale, i18n);
}, [locale, i18n]);
-1
View File
@@ -1,4 +1,3 @@
import React from "react";
import styled, { useTheme } from "styled-components";
const cleanPercentage = (percentage: number) => {
+5 -5
View File
@@ -3,7 +3,7 @@ import isEqual from "lodash/isEqual";
import orderBy from "lodash/orderBy";
import uniq from "lodash/uniq";
import { observer } from "mobx-react";
import * as React from "react";
import { useState, useMemo, useEffect, useCallback } from "react";
import { useTranslation } from "react-i18next";
import { usePopoverState, PopoverDisclosure } from "reakit/Popover";
import Document from "~/models/Document";
@@ -31,7 +31,7 @@ function Collaborators(props: Props) {
const { t } = useTranslation();
const user = useCurrentUser();
const currentUserId = user?.id;
const [requestedUserIds, setRequestedUserIds] = React.useState<string[]>([]);
const [requestedUserIds, setRequestedUserIds] = useState<string[]>([]);
const { users, presence, ui } = useStores();
const { document } = props;
const documentPresence = presence.get(document.id);
@@ -45,7 +45,7 @@ function Collaborators(props: Props) {
.map((p) => p.userId);
// ensure currently present via websocket are always ordered first
const collaborators = React.useMemo(
const collaborators = useMemo(
() =>
orderBy(
filter(
@@ -62,7 +62,7 @@ function Collaborators(props: Props) {
);
// load any users we don't yet have in memory
React.useEffect(() => {
useEffect(() => {
const ids = uniq([...document.collaboratorIds, ...presentIds])
.filter((userId) => !users.get(userId))
.sort();
@@ -78,7 +78,7 @@ function Collaborators(props: Props) {
placement: "bottom-end",
});
const renderAvatar = React.useCallback(
const renderAvatar = useCallback(
({ model: collaborator, ...rest }) => {
const isPresent = presentIds.includes(collaborator.id);
const isEditing = editingIds.includes(collaborator.id);
+2 -2
View File
@@ -1,5 +1,5 @@
import { observer } from "mobx-react";
import * as React from "react";
import { useCallback } from "react";
import { toast } from "sonner";
import useStores from "~/hooks/useStores";
import { CollectionForm, FormData } from "./CollectionForm";
@@ -16,7 +16,7 @@ export const CollectionEdit = observer(function CollectionEdit_({
const { collections } = useStores();
const collection = collections.get(collectionId);
const handleSubmit = React.useCallback(
const handleSubmit = useCallback(
async (data: FormData) => {
try {
await collection?.save(data);
+15 -9
View File
@@ -1,6 +1,6 @@
import uniq from "lodash/uniq";
import { observer } from "mobx-react";
import * as React from "react";
import { useMemo, useEffect, useCallback, Suspense } from "react";
import { Controller, useForm } from "react-hook-form";
import { Trans, useTranslation } from "react-i18next";
import styled from "styled-components";
@@ -15,6 +15,7 @@ import Button from "~/components/Button";
import Flex from "~/components/Flex";
import Input from "~/components/Input";
import InputSelectPermission from "~/components/InputSelectPermission";
import { createLazyComponent } from "~/components/LazyLoad";
import Switch from "~/components/Switch";
import Text from "~/components/Text";
import useBoolean from "~/hooks/useBoolean";
@@ -22,7 +23,7 @@ import useCurrentTeam from "~/hooks/useCurrentTeam";
import useStores from "~/hooks/useStores";
import { EmptySelectValue } from "~/types";
const IconPicker = React.lazy(() => import("~/components/IconPicker"));
const IconPicker = createLazyComponent(() => import("~/components/IconPicker"));
export interface FormData {
name: string;
@@ -39,7 +40,7 @@ const useIconColor = (collection?: Collection) => {
collections.orderedData.map((c) => c.color).filter(Boolean)
) as string[];
const iconColor = React.useMemo(
const iconColor = useMemo(
() =>
collection?.color ??
// If all the existing collections have the same color, use that color,
@@ -88,7 +89,12 @@ export const CollectionForm = observer(function CollectionForm_({
const values = watch();
React.useEffect(() => {
// Preload the IconPicker component on mount
useEffect(() => {
void IconPicker.preload();
}, []);
useEffect(() => {
// If the user hasn't picked an icon yet, go ahead and suggest one based on
// the name of the collection. It's the little things sometimes.
if (!hasOpenedIconPicker && !collection) {
@@ -101,11 +107,11 @@ export const CollectionForm = observer(function CollectionForm_({
}
}, [collection, hasOpenedIconPicker, setValue, values.name, values.icon]);
React.useEffect(() => {
useEffect(() => {
setTimeout(() => setFocus("name", { shouldSelect: true }), 100);
}, [setFocus]);
const handleIconChange = React.useCallback(
const handleIconChange = useCallback(
(icon: string, color: string | null) => {
if (icon !== values.icon) {
setFocus("name");
@@ -134,7 +140,7 @@ export const CollectionForm = observer(function CollectionForm_({
maxLength: CollectionValidation.maxNameLength,
})}
prefix={
<React.Suspense fallback={fallbackIcon}>
<Suspense fallback={fallbackIcon}>
<StyledIconPicker
icon={values.icon}
color={values.color ?? iconColor}
@@ -143,7 +149,7 @@ export const CollectionForm = observer(function CollectionForm_({
onOpen={setHasOpenedIconPicker}
onChange={handleIconChange}
/>
</React.Suspense>
</Suspense>
}
autoComplete="off"
autoFocus
@@ -202,7 +208,7 @@ export const CollectionForm = observer(function CollectionForm_({
);
});
const StyledIconPicker = styled(IconPicker)`
const StyledIconPicker = styled(IconPicker.Component)`
margin-left: 4px;
margin-right: 4px;
`;
+2 -2
View File
@@ -1,6 +1,6 @@
import { runInAction } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import { useCallback } from "react";
import { toast } from "sonner";
import useStores from "~/hooks/useStores";
import history from "~/utils/history";
@@ -14,7 +14,7 @@ export const CollectionNew = observer(function CollectionNew_({
onSubmit,
}: Props) {
const { collections } = useStores();
const handleSubmit = React.useCallback(
const handleSubmit = useCallback(
async (data: FormData) => {
try {
const collection = await collections.save(data);
@@ -1,5 +1,4 @@
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
import { useHistory } from "react-router-dom";
import { toast } from "sonner";
@@ -1,5 +1,4 @@
import { useMatches, KBarResults } from "kbar";
import * as React from "react";
import styled from "styled-components";
import Text from "~/components/Text";
import CommandBarItem from "./CommandBarItem";
@@ -1,5 +1,5 @@
import { DocumentIcon } from "outline-icons";
import * as React from "react";
import { useMemo } from "react";
import Icon from "@shared/components/Icon";
import { createAction } from "~/actions";
import { RecentSection } from "~/actions/sections";
@@ -10,7 +10,7 @@ import { documentPath } from "~/utils/routeHelpers";
const useRecentDocumentActions = (count = 6) => {
const { documents, ui } = useStores();
return React.useMemo(
return useMemo(
() =>
documents.recentlyViewed
.filter((document) => document.id !== ui.activeDocumentId)
@@ -1,5 +1,5 @@
import { SettingsIcon } from "outline-icons";
import * as React from "react";
import { useMemo } from "react";
import { createAction } from "~/actions";
import { NavigationSection } from "~/actions/sections";
import useSettingsConfig from "~/hooks/useSettingsConfig";
@@ -7,7 +7,7 @@ import history from "~/utils/history";
const useSettingsAction = () => {
const config = useSettingsConfig();
const actions = React.useMemo(
const actions = useMemo(
() =>
config.map((item) => {
const Icon = item.icon;
@@ -22,7 +22,7 @@ const useSettingsAction = () => {
[config]
);
const navigateToSettings = React.useMemo(
const navigateToSettings = useMemo(
() =>
createAction({
id: "settings",
@@ -1,5 +1,5 @@
import { NewDocumentIcon, ShapesIcon } from "outline-icons";
import * as React from "react";
import { useEffect, useMemo } from "react";
import Icon from "@shared/components/Icon";
import { createAction } from "~/actions";
import {
@@ -14,11 +14,11 @@ import { newDocumentPath } from "~/utils/routeHelpers";
const useTemplatesAction = () => {
const { documents } = useStores();
React.useEffect(() => {
useEffect(() => {
void documents.fetchAllTemplates();
}, [documents]);
const actions = React.useMemo(
const actions = useMemo(
() =>
documents.templatesAlphabetical.map((template) =>
createAction({
@@ -61,7 +61,7 @@ const useTemplatesAction = () => {
[documents.templatesAlphabetical]
);
const newFromTemplate = React.useMemo(
const newFromTemplate = useMemo(
() =>
createAction({
id: "templates",
-1
View File
@@ -1,5 +1,4 @@
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
import { toast } from "sonner";
import Comment from "~/models/Comment";
-1
View File
@@ -1,5 +1,4 @@
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";
-1
View File
@@ -1,6 +1,5 @@
import { observer } from "mobx-react";
import { DisconnectedIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
+19 -10
View File
@@ -138,7 +138,7 @@ function Template({ items, actions, context, showIcons, ...menu }: Props) {
as={Link}
id={`${item.title}-${index}`}
to={item.to}
key={index}
key={`${item.type}-${item.title}-${index}`}
disabled={item.disabled}
selected={item.selected}
icon={showIcons !== false ? item.icon : undefined}
@@ -154,7 +154,7 @@ function Template({ items, actions, context, showIcons, ...menu }: Props) {
<MenuItem
id={`${item.title}-${index}`}
href={item.href}
key={index}
key={`${item.type}-${item.title}-${index}`}
disabled={item.disabled}
selected={item.selected}
level={item.level}
@@ -176,7 +176,7 @@ function Template({ items, actions, context, showIcons, ...menu }: Props) {
disabled={item.disabled}
selected={item.selected}
dangerous={item.dangerous}
key={index}
key={`${item.type}-${item.title}-${index}`}
icon={showIcons !== false ? item.icon : undefined}
{...menu}
>
@@ -185,18 +185,25 @@ function Template({ items, actions, context, showIcons, ...menu }: Props) {
);
return item.tooltip ? (
<Tooltip content={item.tooltip} placement={"bottom"}>
<Tooltip
content={item.tooltip}
placement={"bottom"}
key={`tooltip-${item.title}-${index}`}
>
<div>{menuItem}</div>
</Tooltip>
) : (
<>{menuItem}</>
<React.Fragment key={`${item.type}-${item.title}-${index}`}>
{menuItem}
</React.Fragment>
);
}
if (item.type === "submenu") {
return (
// Skip rendering empty submenus
return item.items.length > 0 ? (
<BaseMenuItem
key={index}
key={`${item.type}-${item.title}-${index}`}
as={SubMenu}
id={`${item.title}-${index}`}
templateItems={item.items}
@@ -209,15 +216,17 @@ function Template({ items, actions, context, showIcons, ...menu }: Props) {
}
{...menu}
/>
);
) : null;
}
if (item.type === "separator") {
return <Separator key={index} />;
return <Separator key={`separator-${index}`} />;
}
if (item.type === "heading") {
return <Header key={index}>{item.title}</Header>;
return (
<Header key={`heading-${item.title}-${index}`}>{item.title}</Header>
);
}
const _exhaustiveCheck: never = item;
+3 -3
View File
@@ -1,4 +1,4 @@
import * as React from "react";
import { useState, useEffect } from "react";
type Props = {
delay?: number;
@@ -6,9 +6,9 @@ type Props = {
};
export default function DelayedMount({ delay = 250, children }: Props) {
const [isShowing, setShowing] = React.useState(false);
const [isShowing, setShowing] = useState(false);
React.useEffect(() => {
useEffect(() => {
const timeout = setTimeout(() => setShowing(true), delay);
return () => {
clearTimeout(timeout);
+3 -3
View File
@@ -1,4 +1,4 @@
import * as React from "react";
import { useRef, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useHistory } from "react-router-dom";
import { toast } from "sonner";
@@ -12,9 +12,9 @@ export default function DesktopEventHandler() {
const { t } = useTranslation();
const history = useHistory();
const { dialogs } = useStores();
const hasDisabledUpdateMessage = React.useRef(false);
const hasDisabledUpdateMessage = useRef(false);
React.useEffect(() => {
useEffect(() => {
Desktop.bridge?.redirect((path: string, replace = false) => {
if (replace) {
history.replace(path);
-1
View File
@@ -1,5 +1,4 @@
import { observer } from "mobx-react";
import * as React from "react";
import Guide from "~/components/Guide";
import Modal from "~/components/Modal";
import useStores from "~/hooks/useStores";
+4 -4
View File
@@ -4,7 +4,7 @@ import { subDays } from "date-fns";
import { m } from "framer-motion";
import { observer } from "mobx-react";
import { CloseIcon, DocumentIcon, ClockIcon, EyeIcon } from "outline-icons";
import * as React from "react";
import { useRef, useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import styled, { useTheme } from "styled-components";
@@ -40,7 +40,7 @@ function DocumentCard(props: Props) {
const { collections } = useStores();
const theme = useTheme();
const { document, pin, canUpdatePin, isDraggable } = props;
const pinnedToHome = React.useRef(!pin?.collectionId).current;
const pinnedToHome = useRef(!pin?.collectionId).current;
const collection = document.collectionId
? collections.get(document.collectionId)
: undefined;
@@ -63,7 +63,7 @@ function DocumentCard(props: Props) {
transition,
};
const handleUnpin = React.useCallback(
const handleUnpin = useCallback(
async (ev) => {
ev.preventDefault();
ev.stopPropagation();
@@ -178,7 +178,7 @@ function DocumentCard(props: Props) {
const ReadingTime = ({ document }: { document: Document }) => {
const { t } = useTranslation();
const markdown = React.useMemo(() => document.toMarkdown(), [document]);
const markdown = useMemo(() => document.toMarkdown(), [document]);
const stats = useTextStats(markdown);
return (
+4 -4
View File
@@ -1,5 +1,5 @@
import { action, computed, observable } from "mobx";
import React, { PropsWithChildren } from "react";
import { createContext, useContext, useMemo, PropsWithChildren } from "react";
import { Heading } from "@shared/utils/ProsemirrorHelper";
import Document from "~/models/Document";
import { Editor } from "~/editor";
@@ -64,10 +64,10 @@ class DocumentContext {
}
}
const Context = React.createContext<DocumentContext | null>(null);
const Context = createContext<DocumentContext | null>(null);
export const useDocumentContext = () => {
const ctx = React.useContext(Context);
const ctx = useContext(Context);
if (!ctx) {
throw new Error(
"useDocumentContext must be used within DocumentContextProvider"
@@ -79,6 +79,6 @@ export const useDocumentContext = () => {
export const DocumentContextProvider = ({
children,
}: PropsWithChildren<unknown>) => {
const context = React.useMemo(() => new DocumentContext(), []);
const context = useMemo(() => new DocumentContext(), []);
return <Context.Provider value={context}>{children}</Context.Provider>;
};
-1
View File
@@ -1,7 +1,6 @@
import { TFunction } from "i18next";
import { observer } from "mobx-react";
import { DoneIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled, { useTheme } from "styled-components";
import Document from "~/models/Document";
+2 -2
View File
@@ -1,7 +1,7 @@
import compact from "lodash/compact";
import sortBy from "lodash/sortBy";
import { observer } from "mobx-react";
import * as React from "react";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { dateLocale, dateToRelative } from "@shared/utils/date";
import Document from "~/models/Document";
@@ -38,7 +38,7 @@ function DocumentViews({ document, isOpen }: Props) {
documentViews,
(view) => !presentIds.includes(view.userId)
);
const users = React.useMemo(
const users = useMemo(
() => compact(sortedViews.map((v) => v.user)),
[sortedViews]
);
+4 -1
View File
@@ -64,11 +64,12 @@ function EditableTitle(
async (ev) => {
ev.preventDefault();
ev.stopPropagation();
setIsEditing(false);
const trimmedValue = value.trim();
if (trimmedValue === originalValue || trimmedValue.length === 0) {
setValue(originalValue);
setIsEditing(false);
onCancel?.();
return;
}
@@ -80,6 +81,8 @@ function EditableTitle(
setValue(originalValue);
toast.error(error.message);
throw error;
} finally {
setIsEditing(false);
}
},
[originalValue, value, onCancel, onSubmit]
+3 -3
View File
@@ -11,7 +11,7 @@ import {
UserIcon,
CrossIcon,
} from "outline-icons";
import * as React from "react";
import { useRef } from "react";
import { useTranslation } from "react-i18next";
import { useLocation } from "react-router-dom";
import styled, { css } from "styled-components";
@@ -65,7 +65,7 @@ const EventListItem = ({ event, document, ...rest }: Props) => {
const user = "userId" in event ? users.get(event.userId) : undefined;
const location = useLocation();
const sidebarContext = useLocationSidebarContext();
const revisionLoadedRef = React.useRef(false);
const revisionLoadedRef = useRef(false);
const opts = {
userName: actor?.name,
};
@@ -74,7 +74,7 @@ const EventListItem = ({ event, document, ...rest }: Props) => {
event.id === RevisionHelper.latestId(document.id);
let meta, icon, to: LocationDescriptor | undefined;
const ref = React.useRef<HTMLAnchorElement>(null);
const ref = useRef<HTMLAnchorElement>(null);
// the time component tends to steal focus when clicked
// ...so forward the focus back to the parent item
const handleTimeClick = () => {
+2 -2
View File
@@ -1,4 +1,4 @@
import React from "react";
import { useState } from "react";
import styled from "styled-components";
import { fadeIn } from "~/styles/animations";
@@ -21,7 +21,7 @@ type Props = {
* Wraps children in a <Fade> if loading is true on mount.
*/
export const ConditionalFade = ({ animate, children }: Props) => {
const [isAnimated] = React.useState(animate);
const [isAnimated] = useState(animate);
return isAnimated ? <Fade>{children}</Fade> : <>{children}</>;
};
-1
View File
@@ -1,4 +1,3 @@
import * as React from "react";
import styled from "styled-components";
import Empty from "~/components/Empty";
import Fade from "~/components/Fade";
@@ -1,5 +1,5 @@
import { BackIcon } from "outline-icons";
import React from "react";
import * as React from "react";
import styled from "styled-components";
import { breakpoints, s, hover } from "@shared/styles";
import { colorPalette } from "@shared/utils/collections";
@@ -1,5 +1,5 @@
import concat from "lodash/concat";
import React from "react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { EmojiCategory, EmojiSkinTone, IconType } from "@shared/types";
@@ -1,6 +1,6 @@
import chunk from "lodash/chunk";
import compact from "lodash/compact";
import React from "react";
import * as React from "react";
import styled from "styled-components";
import { IconType } from "@shared/types";
import { IconLibrary } from "@shared/utils/IconLibrary";
@@ -1,4 +1,4 @@
import React from "react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { IconType } from "@shared/types";
@@ -1,4 +1,4 @@
import React from "react";
import { useMemo, useCallback } from "react";
import { useTranslation } from "react-i18next";
import { Menu, MenuButton, MenuItem, useMenuState } from "reakit";
import styled from "styled-components";
@@ -19,16 +19,13 @@ const SkinTonePicker = ({
}) => {
const { t } = useTranslation();
const handEmojiVariants = React.useMemo(
() => getEmojiVariants({ id: "hand" }),
[]
);
const handEmojiVariants = useMemo(() => getEmojiVariants({ id: "hand" }), []);
const menu = useMenuState({
placement: "bottom-end",
});
const handleSkinClick = React.useCallback(
const handleSkinClick = useCallback(
(emojiSkin) => {
menu.hide();
onChange(emojiSkin);
@@ -36,7 +33,7 @@ const SkinTonePicker = ({
[menu, onChange]
);
const menuItems = React.useMemo(
const menuItems = useMemo(
() =>
Object.entries(handEmojiVariants).map(([eskin, emoji]) => (
<MenuItem {...menu} key={emoji.value}>
+5 -1
View File
@@ -45,6 +45,7 @@ type Props = {
onChange: (icon: string | null, color: string | null) => void;
onOpen?: () => void;
onClose?: () => void;
children?: React.ReactNode;
};
const IconPicker = ({
@@ -59,6 +60,7 @@ const IconPicker = ({
onOpen,
onClose,
borderOnHover,
children,
}: Props) => {
const { t } = useTranslation();
@@ -174,7 +176,9 @@ const IconPicker = ({
onClick={handlePopoverButtonClick}
$borderOnHover={borderOnHover}
>
{iconType && icon ? (
{children ? (
children
) : iconType && icon ? (
<Icon value={icon} color={color} size={size} initial={initial} />
) : (
<StyledSmileyIcon color={theme.placeholder} size={size} />
-2
View File
@@ -1,5 +1,3 @@
import * as React from "react";
type Props = {
/** The size of the icon, 24px is default to match standard icons */
size?: number;
-1
View File
@@ -1,7 +1,6 @@
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";
-2
View File
@@ -1,5 +1,3 @@
import * as React from "react";
type Props = {
/** The size of the icon, 24px is default to match standard icons */
size?: number;
-2
View File
@@ -1,5 +1,3 @@
import * as React from "react";
export function LanguageIcon({ className }: { className?: string }) {
return (
<svg
-2
View File
@@ -1,5 +1,3 @@
import * as React from "react";
type Props = {
/** The size of the icon, 24px is default to match standard icons */
size?: number;
-2
View File
@@ -1,5 +1,3 @@
import * as React from "react";
type Props = {
/** The size of the icon, 24px is default to match standard icons */
size?: number;
+1 -1
View File
@@ -1,6 +1,6 @@
import * as VisuallyHidden from "@radix-ui/react-visually-hidden";
import { transparentize } from "polished";
import React from "react";
import * as React from "react";
import styled from "styled-components";
import Text from "~/components/Text";
import useMobile from "~/hooks/useMobile";
-1
View File
@@ -1,6 +1,5 @@
import { m } from "framer-motion";
import find from "lodash/find";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { languages, languageOptions } from "@shared/i18n";
+47
View File
@@ -0,0 +1,47 @@
import * as React from "react";
import lazyWithRetry from "~/utils/lazyWithRetry";
export interface LazyComponent<T extends React.ComponentType<any>> {
Component: React.LazyExoticComponent<T>;
preload: () => Promise<{ default: T }>;
}
interface LazyLoadOptions {
retries?: number;
interval?: number;
}
/**
* Creates a lazy-loaded component with preloading capability and automatic retries on failure.
*
* @param factory A function that returns a promise of a component (eg: () => import('./MyComponent'))
* @param options Optional configuration for retry behavior
* @returns An object containing the lazy Component and a preload function
*
* @example
* ```typescript
* const MyComponent = createLazyComponent(() => import('./MyComponent'));
*
* function App() {
* return (
* <Suspense fallback={<div>Loading...</div>}>
* <MyComponent.Component />
* </Suspense>
* );
* }
*
* // Preload when needed:
* MyComponent.preload();
* ```
*/
export function createLazyComponent<T extends React.ComponentType<any>>(
factory: () => Promise<{ default: T }>,
options: LazyLoadOptions = {}
): LazyComponent<T> {
const { retries, interval } = options;
return {
Component: lazyWithRetry(factory, retries, interval),
preload: factory,
};
}
-1
View File
@@ -1,5 +1,4 @@
import { DisconnectedIcon, WarningIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { s } from "@shared/styles";
-1
View File
@@ -1,5 +1,4 @@
import times from "lodash/times";
import * as React from "react";
import styled from "styled-components";
import Fade from "~/components/Fade";
import Flex from "~/components/Flex";
@@ -1,11 +1,11 @@
import { observer } from "mobx-react";
import * as React from "react";
import { useEffect } from "react";
import useStores from "~/hooks/useStores";
function LoadingIndicator() {
const { ui } = useStores();
React.useEffect(() => {
useEffect(() => {
ui.enableProgressBar();
return () => ui.disableProgressBar();
}, [ui]);
@@ -1,4 +1,3 @@
import * as React from "react";
import styled, { keyframes } from "styled-components";
import { depths, s } from "@shared/styles";
+1 -1
View File
@@ -1,4 +1,4 @@
import React from "react";
import * as React from "react";
import styled from "styled-components";
import { s } from "@shared/styles";
import Flex from "./Flex";
@@ -1,6 +1,5 @@
import { observer } from "mobx-react";
import { SubscribeIcon } from "outline-icons";
import * as React from "react";
import styled from "styled-components";
import { s } from "@shared/styles";
import useStores from "~/hooks/useStores";
@@ -8,7 +8,7 @@ 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 { Avatar, AvatarSize } from "../Avatar";
import { Avatar, AvatarSize, AvatarVariant } from "../Avatar";
import Flex from "../Flex";
import Text from "../Text";
import Time from "../Time";
@@ -41,7 +41,7 @@ function NotificationListItem({ notification, onNavigate }: Props) {
return (
<StyledLink to={notification.path ?? ""} onClick={handleClick}>
<Container gap={8} $unread={!notification.viewedAt}>
<StyledAvatar model={notification.actor} size={AvatarSize.Large} />
<StyledAvatar model={notification.actor} />
<Flex column>
<Text as="div" size="small">
<Text weight="bold">
@@ -79,7 +79,10 @@ const StyledCommentEditor = styled(CommentEditor)`
${truncateMultiline(3)}
`;
const StyledAvatar = styled(Avatar)`
const StyledAvatar = styled(Avatar).attrs({
variant: AvatarVariant.Round,
size: AvatarSize.Medium,
})`
margin-top: 4px;
`;
@@ -1,5 +1,5 @@
import { observer } from "mobx-react";
import * as React from "react";
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { OAuthClientValidation } from "@shared/validations";
@@ -49,7 +49,7 @@ export const OAuthClientForm = observer(function OAuthClientForm_({
},
});
React.useEffect(() => {
useEffect(() => {
setTimeout(() => setFocus("name", { shouldSelect: true }), 100);
}, [setFocus]);
@@ -1,5 +1,5 @@
import { observer } from "mobx-react";
import * as React from "react";
import { useCallback } from "react";
import { useHistory } from "react-router-dom";
import { toast } from "sonner";
import useStores from "~/hooks/useStores";
@@ -16,7 +16,7 @@ export const OAuthClientNew = observer(function OAuthClientNew_({
const { oauthClients } = useStores();
const history = useHistory();
const handleSubmit = React.useCallback(
const handleSubmit = useCallback(
async (data: FormData) => {
try {
const oauthClient = await oauthClients.save(data);
+2 -2
View File
@@ -1,4 +1,4 @@
import * as React from "react";
import { useEffect } from "react";
import { useTheme } from "styled-components";
import useStores from "~/hooks/useStores";
@@ -6,7 +6,7 @@ export default function PageTheme() {
const { ui } = useStores();
const theme = useTheme();
React.useEffect(() => {
useEffect(() => {
// wider page background beyond the React root
if (document.body) {
document.body.style.background = theme.background;
-1
View File
@@ -2,7 +2,6 @@ import "../stores";
import { render } from "@testing-library/react";
import { TFunction } from "i18next";
import { Provider } from "mobx-react";
import * as React from "react";
import { getI18n } from "react-i18next";
import { Pagination } from "@shared/constants";
import PaginatedList from "./PaginatedList";
+5 -5
View File
@@ -17,7 +17,7 @@ import {
import fractionalIndex from "fractional-index";
import { AnimatePresence } from "framer-motion";
import { observer } from "mobx-react";
import * as React from "react";
import { useState, useRef, useEffect, useCallback } from "react";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import Pin from "~/models/Pin";
@@ -44,12 +44,12 @@ function PinnedDocuments({
...rest
}: Props) {
const { documents } = useStores();
const [items, setItems] = React.useState(pins.map((pin) => pin.documentId));
const showPlaceholderRef = React.useRef(true);
const [items, setItems] = useState(pins.map((pin) => pin.documentId));
const showPlaceholderRef = useRef(true);
const showPlaceholder =
placeholderCount && !items.length && showPlaceholderRef.current;
React.useEffect(() => {
useEffect(() => {
setItems(pins.map((pin) => pin.documentId));
}, [pins]);
@@ -65,7 +65,7 @@ function PinnedDocuments({
})
);
const handleDragEnd = React.useCallback(
const handleDragEnd = useCallback(
(event: DragEndEvent) => {
const { active, over } = event;
-1
View File
@@ -1,4 +1,3 @@
import * as React from "react";
import styled from "styled-components";
import DelayedMount from "~/components/DelayedMount";
import Fade from "~/components/Fade";
-1
View File
@@ -1,5 +1,4 @@
import { observer } from "mobx-react";
import * as React from "react";
import styled from "styled-components";
import Logger from "~/utils/Logger";
import { Hook, usePluginValue } from "~/utils/PluginManager";
+1 -1
View File
@@ -1,6 +1,6 @@
import { observer } from "mobx-react";
import { transparentize } from "polished";
import React from "react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled, { css } from "styled-components";
import { s, hover } from "@shared/styles";
+1 -1
View File
@@ -1,6 +1,6 @@
import compact from "lodash/compact";
import { observer } from "mobx-react";
import React from "react";
import * as React from "react";
import Comment from "~/models/Comment";
import useHover from "~/hooks/useHover";
import useStores from "~/hooks/useStores";
+5 -3
View File
@@ -1,9 +1,10 @@
import { ReactionIcon } from "outline-icons";
import React from "react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { PopoverDisclosure, usePopoverState } from "reakit";
import EventBoundary from "@shared/components/EventBoundary";
import Flex from "~/components/Flex";
import { createLazyComponent } from "~/components/LazyLoad";
import NudeButton from "~/components/NudeButton";
import PlaceholderText from "~/components/PlaceholderText";
import Popover from "~/components/Popover";
@@ -12,7 +13,7 @@ import useOnClickOutside from "~/hooks/useOnClickOutside";
import useWindowSize from "~/hooks/useWindowSize";
import Tooltip from "../Tooltip";
const EmojiPanel = React.lazy(
const EmojiPanel = createLazyComponent(
() => import("~/components/IconPicker/components/EmojiPanel")
);
@@ -104,6 +105,7 @@ const ReactionPicker: React.FC<Props> = ({
aria-label={t("Reaction picker")}
className={className}
onClick={handlePopoverButtonClick}
onMouseEnter={() => EmojiPanel.preload()}
size={size}
>
<ReactionIcon size={22} />
@@ -123,7 +125,7 @@ const ReactionPicker: React.FC<Props> = ({
{popover.visible && (
<React.Suspense fallback={<Placeholder />}>
<EventBoundary>
<EmojiPanel
<EmojiPanel.Component
height={300}
panelWidth={panelWidth}
query={query}
@@ -1,6 +1,6 @@
import compact from "lodash/compact";
import { observer } from "mobx-react";
import React from "react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { Tab, TabPanel, useTabState } from "reakit";
import { toast } from "sonner";
+3 -3
View File
@@ -1,15 +1,15 @@
import * as React from "react";
import { createContext, useContext } from "react";
/**
* Context to provide a reference to the scrollable container
*/
const ScrollContext = React.createContext<
const ScrollContext = createContext<
React.RefObject<HTMLDivElement> | undefined
>(undefined);
/**
* Hook to get the scrollable container reference
*/
export const useScrollContext = () => React.useContext(ScrollContext);
export const useScrollContext = () => useContext(ScrollContext);
export default ScrollContext;
+2 -2
View File
@@ -1,5 +1,5 @@
// based on: https://reacttraining.com/react-router/web/guides/scroll-restoration
import * as React from "react";
import { useEffect } from "react";
import { useLocation } from "react-router-dom";
import usePrevious from "~/hooks/usePrevious";
import { useScrollContext } from "./ScrollContext";
@@ -13,7 +13,7 @@ export default function ScrollToTop({ children }: Props) {
const previousLocationPathname = usePrevious(location.pathname);
const scrollContainerRef = useScrollContext();
React.useEffect(() => {
useEffect(() => {
if (
location.pathname === previousLocationPathname ||
location.state?.retainScrollPosition
+2 -2
View File
@@ -1,5 +1,5 @@
import { useKBar } from "kbar";
import * as React from "react";
import { useEffect } from "react";
import { searchDocumentsForQuery } from "~/actions/definitions/documents";
import { navigateToRecentSearchQuery } from "~/actions/definitions/navigation";
@@ -9,7 +9,7 @@ import useStores from "~/hooks/useStores";
export default function SearchActions() {
const { searches } = useStores();
React.useEffect(() => {
useEffect(() => {
if (!searches.isLoaded && !searches.isFetching) {
void searches.fetchPage({
source: "app",
@@ -1,5 +1,5 @@
import { observer } from "mobx-react";
import * as React from "react";
import { useCallback, Fragment } from "react";
import { Trans, useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import styled from "styled-components";
@@ -31,7 +31,7 @@ const DocumentMemberListItem = ({
}: Props) => {
const { t } = useTranslation();
const handleChange = React.useCallback(
const handleChange = useCallback(
(permission: DocumentPermission | typeof EmptySelectValue) => {
if (permission === EmptySelectValue) {
onRemove?.();
@@ -68,7 +68,7 @@ const DocumentMemberListItem = ({
if (!currentPermission) {
return null;
}
const MaybeLink = membership?.source ? StyledLink : React.Fragment;
const MaybeLink = membership?.source ? StyledLink : Fragment;
return (
<ListItem
@@ -1,5 +1,5 @@
import { LinkIcon } from "outline-icons";
import * as React from "react";
import { useRef, useCallback } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import CopyToClipboard from "~/components/CopyToClipboard";
@@ -14,9 +14,9 @@ export function CopyLinkButton({
onCopy: () => void;
}) {
const { t } = useTranslation();
const timeout = React.useRef<ReturnType<typeof setTimeout>>();
const timeout = useRef<ReturnType<typeof setTimeout>>();
const handleCopied = React.useCallback(() => {
const handleCopied = useCallback(() => {
onCopy();
timeout.current = setTimeout(() => {
@@ -1,4 +1,3 @@
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import Flex from "@shared/components/Flex";
@@ -1,5 +1,4 @@
import times from "lodash/times";
import * as React from "react";
import { AvatarSize } from "~/components/Avatar";
import Fade from "~/components/Fade";
import PlaceholderText from "~/components/PlaceholderText";
@@ -93,11 +93,13 @@ export const Suggestions = observer(
const suggestions = React.useMemo(() => {
const filtered: Suggestion[] = (
document
? users.notInDocument(document.id, query)
? users
.notInDocument(document.id, query)
.filter((u) => u.id !== user.id)
: collection
? users.notInCollection(collection.id, query)
: users.activeOrInvited
).filter((u) => !u.isSuspended && u.id !== user.id);
).filter((u) => !u.isSuspended);
if (isEmail(query)) {
filtered.push(getSuggestionForEmail(query));
+5 -5
View File
@@ -1,6 +1,6 @@
import { observer } from "mobx-react";
import { SearchIcon, HomeIcon, SidebarIcon } from "outline-icons";
import * as React from "react";
import { useEffect, useState, useCallback, useMemo } from "react";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { useTranslation } from "react-i18next";
@@ -38,7 +38,7 @@ function AppSidebar() {
const user = useCurrentUser();
const can = usePolicy(team);
React.useEffect(() => {
useEffect(() => {
void collections.fetchAll();
if (!user.isViewer) {
@@ -46,9 +46,9 @@ function AppSidebar() {
}
}, [documents, collections, user.isViewer]);
const [dndArea, setDndArea] = React.useState();
const handleSidebarRef = React.useCallback((node) => setDndArea(node), []);
const html5Options = React.useMemo(
const [dndArea, setDndArea] = useState();
const handleSidebarRef = useCallback((node) => setDndArea(node), []);
const html5Options = useMemo(
() => ({
rootElement: dndArea,
}),
+14 -5
View File
@@ -1,7 +1,7 @@
import groupBy from "lodash/groupBy";
import { observer } from "mobx-react";
import { BackIcon, SidebarIcon } from "outline-icons";
import * as React from "react";
import { useCallback } from "react";
import { useTranslation } from "react-i18next";
import { useHistory, useLocation } from "react-router-dom";
import styled from "styled-components";
@@ -23,14 +23,22 @@ import ToggleButton from "./components/ToggleButton";
import Version from "./components/Version";
function SettingsSidebar() {
const { ui } = useStores();
const { ui, integrations } = useStores();
const { t } = useTranslation();
const history = useHistory();
const location = useLocation();
const configs = useSettingsConfig();
const groupedConfig = groupBy(configs, "group");
const returnToApp = React.useCallback(() => {
const groupedConfig = groupBy(
configs.filter((item) =>
item.group === "Integrations" && item.pluginId
? integrations.findByService(item.pluginId)
: true
),
"group"
);
const returnToApp = useCallback(() => {
history.push("/home");
}, [history]);
@@ -63,8 +71,9 @@ function SettingsSidebar() {
<SidebarLink
key={item.path}
to={item.path}
onClickIntent={item.preload}
active={
item.path !== settingsPath()
item.path.startsWith(settingsPath("templates"))
? location.pathname.startsWith(item.path)
: undefined
}
-1
View File
@@ -1,6 +1,5 @@
import { observer } from "mobx-react";
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";
@@ -1,7 +1,7 @@
import isUndefined from "lodash/isUndefined";
import { observer } from "mobx-react";
import { ArchiveIcon } from "outline-icons";
import * as React from "react";
import { useState, useEffect, useCallback } from "react";
import { useTranslation } from "react-i18next";
import Flex from "@shared/components/Flex";
import Collection from "~/models/Collection";
@@ -21,43 +21,43 @@ function ArchiveLink() {
const { collections } = useStores();
const { t } = useTranslation();
const [disclosure, setDisclosure] = React.useState<boolean>(false);
const [expanded, setExpanded] = React.useState<boolean | undefined>();
const [disclosure, setDisclosure] = useState<boolean>(false);
const [expanded, setExpanded] = useState<boolean | undefined>();
const { request, data, loading, error } = useRequest(
collections.fetchArchived,
true
);
React.useEffect(() => {
useEffect(() => {
if (!isUndefined(data) && !loading && isUndefined(error)) {
setDisclosure(data.length > 0);
}
}, [data, loading, error]);
React.useEffect(() => {
useEffect(() => {
setDisclosure(collections.archived.length > 0);
}, [collections.archived]);
React.useEffect(() => {
useEffect(() => {
if (disclosure && isUndefined(expanded)) {
setExpanded(false);
}
}, [disclosure]);
React.useEffect(() => {
useEffect(() => {
if (expanded) {
void request();
}
}, [expanded, request]);
const handleDisclosureClick = React.useCallback((ev) => {
const handleDisclosureClick = useCallback((ev) => {
ev.preventDefault();
ev.stopPropagation();
setExpanded((e) => !e);
}, []);
const handleClick = React.useCallback(() => {
const handleClick = useCallback(() => {
setExpanded(true);
}, []);
@@ -1,4 +1,4 @@
import * as React from "react";
import { useState, useCallback } from "react";
import Collection from "~/models/Collection";
import useStores from "~/hooks/useStores";
import CollectionLink from "./CollectionLink";
@@ -13,15 +13,15 @@ type Props = {
export function ArchivedCollectionLink({ collection, depth }: Props) {
const { documents } = useStores();
const [expanded, setExpanded] = React.useState(false);
const [expanded, setExpanded] = useState(false);
const handleDisclosureClick = React.useCallback((ev) => {
const handleDisclosureClick = useCallback((ev) => {
ev.preventDefault();
ev.stopPropagation();
setExpanded((e) => !e);
}, []);
const handleClick = React.useCallback(() => {
const handleClick = useCallback(() => {
setExpanded(true);
}, []);
@@ -1,6 +1,6 @@
import noop from "lodash/noop";
import { observer } from "mobx-react";
import * as React from "react";
import { useState, useRef, useEffect, useCallback } from "react";
import { useTranslation } from "react-i18next";
import { Waypoint } from "react-waypoint";
import styled from "styled-components";
@@ -37,8 +37,8 @@ function CollectionLinkChildren({
const { documents } = useStores();
const { t } = useTranslation();
const childDocuments = useCollectionDocuments(collection, documents.active);
const [showing, setShowing] = React.useState(pageSize);
const dummyRef = React.useRef<HTMLDivElement>(null);
const [showing, setShowing] = useState(pageSize);
const dummyRef = useRef<HTMLDivElement>(null);
const [{ isOver, canDrop }, dropRef] = useDropToChangeCollection(
collection,
@@ -46,13 +46,13 @@ function CollectionLinkChildren({
dummyRef
);
React.useEffect(() => {
useEffect(() => {
if (!expanded) {
setShowing(pageSize);
}
}, [expanded]);
const showMore = React.useCallback(() => {
const showMore = useCallback(() => {
if (childDocuments && childDocuments.length > showing) {
setShowing((value) => value + pageSize);
}
@@ -1,6 +1,6 @@
import fractionalIndex from "fractional-index";
import { observer } from "mobx-react";
import * as React from "react";
import { useMemo } from "react";
import { useDrop } from "react-dnd";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
@@ -22,9 +22,9 @@ import SidebarContext from "./SidebarContext";
function Collections() {
const { documents, collections } = useStores();
const { t } = useTranslation();
const orderedCollections = collections.orderedData;
const orderedCollections = collections.allActive;
const params = React.useMemo(
const params = useMemo(
() => ({
limit: 100,
}),
@@ -1,6 +1,5 @@
import { observer } from "mobx-react";
import { DraftsIcon } from "outline-icons";
import React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import Flex from "~/components/Flex";

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