Compare commits

...

125 Commits

Author SHA1 Message Date
Tom Moor 66ae221aae cleanup type 2025-01-02 07:44:01 -05:00
Tom Moor 2b8f6752c0 Move Group to model event writing 2025-01-01 15:19:26 -05:00
Tom Moor adfca1e5ca fix: Attempting to split undefined 2024-12-31 17:33:47 -05:00
Tom Moor 6ca3c25d35 fix: Do not report errors due to unsupported file types 2024-12-31 08:26:34 -05:00
Tom Moor 05a2c6ae1e fix: Zoom cursor shown while drag-resizing image 2024-12-31 08:26:34 -05:00
Hemachandar 234915f4a0 Convert Subscription mutations (#8166)
* createContext accepts object

* handle subscriptions

* use createContext

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-30 16:30:23 -08:00
dependabot[bot] 120191d4d7 chore(deps-dev): bump eslint-plugin-react from 7.35.0 to 7.37.3 (#8169)
Bumps [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) from 7.35.0 to 7.37.3.
- [Release notes](https://github.com/jsx-eslint/eslint-plugin-react/releases)
- [Changelog](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jsx-eslint/eslint-plugin-react/compare/v7.35.0...v7.37.3)

---
updated-dependencies:
- dependency-name: eslint-plugin-react
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-30 16:29:40 -08:00
dependabot[bot] 6a2ab299a8 chore(deps): bump @babel/preset-react from 7.25.9 to 7.26.3 (#8170)
Bumps [@babel/preset-react](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-react) from 7.25.9 to 7.26.3.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.26.3/packages/babel-preset-react)

---
updated-dependencies:
- dependency-name: "@babel/preset-react"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-30 16:29:27 -08:00
dependabot[bot] 74dc7094e1 chore(deps): bump i18next-http-backend from 2.5.0 to 2.7.1 (#8171)
Bumps [i18next-http-backend](https://github.com/i18next/i18next-http-backend) from 2.5.0 to 2.7.1.
- [Changelog](https://github.com/i18next/i18next-http-backend/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next-http-backend/commits)

---
updated-dependencies:
- dependency-name: i18next-http-backend
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-30 16:29:09 -08:00
Hemachandar 5dd993adf5 Convert WebhookSubscription mutations (#8161)
* Convert 'WebhookSubscription' mutations

* add tests

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

* Remove clientside Field decorator

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

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

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

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

---
updated-dependencies:
- dependency-name: i18next-fs-backend
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-24 09:30:57 -08:00
dependabot[bot] 2b4c8d981c chore(deps-dev): bump nodemon from 3.1.7 to 3.1.9 (#8135)
Bumps [nodemon](https://github.com/remy/nodemon) from 3.1.7 to 3.1.9.
- [Release notes](https://github.com/remy/nodemon/releases)
- [Commits](https://github.com/remy/nodemon/compare/v3.1.7...v3.1.9)

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

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

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

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

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

* tsc

---------

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-17 16:22:49 -08:00
dependabot[bot] 742c138b3d chore(deps): bump mermaid from 11.4.0 to 11.4.1 (#8117)
Bumps [mermaid](https://github.com/mermaid-js/mermaid) from 11.4.0 to 11.4.1.
- [Release notes](https://github.com/mermaid-js/mermaid/releases)
- [Changelog](https://github.com/mermaid-js/mermaid/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/mermaid-js/mermaid/compare/mermaid@11.4.0...mermaid@11.4.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-16 20:36:59 -08:00
Tom Moor ec1eacaeea fix: Cannot leave overlapping comments (#8107) 2024-12-16 20:36:32 -08:00
Tom Moor 8b15cc45b0 fix: Permissions checks on notification emails do not take into account shares (#8109)
* fix: Comment notifications not sent on drafts outside collection, shared docs

* fix: DocumentPublishedOrUpdatedEmail not sent for drafts

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

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

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

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

* tsc

* tsc

---------

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

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

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

---
updated-dependencies:
- dependency-name: utility-types
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-09 17:31:03 -08:00
dependabot[bot] bea56159ec chore(deps-dev): bump eslint-import-resolver-typescript from 3.6.3 to 3.7.0 (#8096)
Bumps [eslint-import-resolver-typescript](https://github.com/import-js/eslint-import-resolver-typescript) from 3.6.3 to 3.7.0.
- [Release notes](https://github.com/import-js/eslint-import-resolver-typescript/releases)
- [Changelog](https://github.com/import-js/eslint-import-resolver-typescript/blob/master/CHANGELOG.md)
- [Commits](https://github.com/import-js/eslint-import-resolver-typescript/compare/v3.6.3...v3.7.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-09 17:30:41 -08:00
Tom Moor 908f053920 Add UI element to images that are externally hosted 2024-12-08 12:51:55 -05:00
Tom Moor 033c298bff fix: Retrieve image dimensions for all types, not only PNG 2024-12-07 17:15:25 -05:00
Tom Moor 22f02ad713 feat: Add 'Neon' highlighter color, increase presence of highlights 2024-12-07 16:27:57 -05:00
Tom Moor 92b1c578f6 feat: Improve inline rule matching (#8085)
* stash

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

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

* refactor,DRY

* guard

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

* Tooltip styling

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

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

* fix: give toolbar menu its own context

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

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

* fix: userIdsMentioned was always empty

* add: NotificationEventType.ResolveComment

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

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

* implement: handleResolvedComment

* clone: CommentMentionedEmail as CommentResolvedEmail

Changes coming up in next commit...

* implement: CommentResolvedEmail

* Fix "New Comment↓" incorrectly showing in Resolved

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

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

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

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

* Better sentences

* Refactor "mentions + author" calculation

* Remove unnecessary check

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

---
updated-dependencies:
- dependency-name: socket.io
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-03 19:15:07 -08:00
Tom Moor 7a6f75c34f fix: Improved responsiveness of document header elements (#8066)
* fix: Made the document header components more responsive to the available space

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-02 18:10:54 -08:00
Tom Moor 4b833b3e2e Add screen for API key management (#8049)
* API keys

* Add api key list for admins

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

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

* Allow user to select doc-copy dest

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

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

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

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

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

* Rename: DuplicateDialog -> DocumentCopy

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

* Revert DocumentExplorer style override

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

* Switch to DocumentMove's style

* initialSelectionId -> defaultValue

---------

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

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

* Store in MB, rather than Bytes

* Add cron task to recalculate attachment sizes

* findAllInBatches

* Index createdAt

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-25 19:31:39 -08:00
dependabot[bot] 7fff8161ff chore(deps): bump vite from 5.4.10 to 5.4.11 (#8021)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.4.10 to 5.4.11.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.11/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.11/packages/vite)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-25 19:31:30 -08:00
Tom Moor 0ef9f1aea1 fix: Improve fast-click functionality in sidebar 2024-11-25 22:22:21 -05:00
Tom Moor fe63c5d706 fix: JS error in usePosition hook 2024-11-25 20:42:31 -05:00
Tom Moor 7749f0ab9f fix: Undo/redo regression 2024-11-25 20:36:23 -05:00
Tom Moor 763b911dfd fix: Named commands broken, regressed in 921e89d7b7 2024-11-24 23:34:48 -05:00
Tom Moor 99e541ede8 fix: Ensure logout OIDC never immediately relogin 2024-11-24 22:34:16 -05:00
Tom Moor 06f48ec79a Add active/hover state to collapsed thread 2024-11-24 19:59:31 -05:00
infinite-persistence 5566d995bd Comment: collapse long replies (#7941)
* Comment: collapse long replies

## Ticket
Closes 5079

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

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

* Types cleanup
2024-11-20 16:17:28 -08:00
Hemachandar 28cb5aa379 Convert pin mutations to use auto event insertion (#7993) 2024-11-20 16:14:11 -08:00
301 changed files with 5350 additions and 2912 deletions
+10 -2
View File
@@ -53,9 +53,13 @@ export const resolveCommentFactory = ({
perform: async ({ t }) => {
await comment.resolve();
const locationState = history.location.state as Record<string, unknown>;
history.replace({
...history.location,
state: null,
state: {
sidebarContext: locationState["sidebarContext"],
commentId: undefined,
},
});
onResolve();
@@ -81,9 +85,13 @@ export const unresolveCommentFactory = ({
perform: async () => {
await comment.unresolve();
const locationState = history.location.state as Record<string, unknown>;
history.replace({
...history.location,
state: null,
state: {
sidebarContext: locationState["sidebarContext"],
commentId: undefined,
},
});
onUnresolve();
+3 -4
View File
@@ -45,7 +45,7 @@ import DocumentPermanentDelete from "~/scenes/DocumentPermanentDelete";
import DocumentPublish from "~/scenes/DocumentPublish";
import DeleteDocumentsInTrash from "~/scenes/Trash/components/DeleteDocumentsInTrash";
import ConfirmationDialog from "~/components/ConfirmationDialog";
import DuplicateDialog from "~/components/DuplicateDialog";
import DocumentCopy from "~/components/DocumentCopy";
import Icon from "~/components/Icon";
import MarkdownIcon from "~/components/Icons/MarkdownIcon";
import SharePopover from "~/components/Sharing/Document";
@@ -562,7 +562,7 @@ export const duplicateDocument = createAction({
stores.dialogs.openModal({
title: t("Copy document"),
content: (
<DuplicateDialog
<DocumentCopy
document={document}
onSubmit={(response) => {
stores.dialogs.closeAllModals();
@@ -732,7 +732,6 @@ export const importDocument = createAction({
history.push(document.url);
} catch (err) {
toast.error(err.message);
throw err;
}
};
@@ -1054,7 +1053,7 @@ export const openDocumentComments = createAction({
return;
}
stores.ui.toggleComments(activeDocumentId);
stores.ui.toggleComments();
},
});
-1
View File
@@ -31,7 +31,6 @@ const Actions = styled(Flex)`
left: 0;
border-radius: 3px;
background: ${s("background")};
transition: ${s("backgroundTransition")};
padding: 12px;
backdrop-filter: blur(20px);
+2 -1
View File
@@ -5,6 +5,7 @@ import { Redirect } from "react-router-dom";
import useCurrentUser from "~/hooks/useCurrentUser";
import useStores from "~/hooks/useStores";
import { changeLanguage } from "~/utils/language";
import { logoutPath } from "~/utils/routeHelpers";
import LoadingIndicator from "./LoadingIndicator";
type Props = {
@@ -32,7 +33,7 @@ const Authenticated = ({ children }: Props) => {
}
void auth.logout(true);
return <Redirect to="/" />;
return <Redirect to={logoutPath()} />;
};
export default observer(Authenticated);
+1 -1
View File
@@ -94,7 +94,7 @@ const AuthenticatedLayout: React.FC = ({ children }: Props) => {
!showHistory &&
can.comment &&
ui.activeDocumentId &&
ui.commentsExpanded.includes(ui.activeDocumentId) &&
ui.commentsExpanded &&
team.getPreference(TeamPreference.Commenting);
const sidebarRight = (
+15 -12
View File
@@ -8,18 +8,16 @@ import BreadcrumbMenu from "~/menus/BreadcrumbMenu";
import { undraggableOnDesktop } from "~/styles";
import { MenuInternalLink } from "~/types";
type Props = {
type Props = React.PropsWithChildren<{
items: MenuInternalLink[];
max?: number;
highlightFirstItem?: boolean;
};
}>;
function Breadcrumb({
items,
highlightFirstItem,
children,
max = 2,
}: React.PropsWithChildren<Props>) {
function Breadcrumb(
{ items, highlightFirstItem, children, max = 2 }: Props,
ref: React.RefObject<HTMLDivElement> | null
) {
const totalItems = items.length;
const topLevelItems: MenuInternalLink[] = [...items];
let overflowItems;
@@ -37,9 +35,13 @@ function Breadcrumb({
}
return (
<Flex justify="flex-start" align="center">
<Flex justify="flex-start" align="center" ref={ref}>
{topLevelItems.map((item, index) => (
<React.Fragment key={String(item.to) || index}>
<React.Fragment
key={
(typeof item.to === "string" ? item.to : item.to.pathname) || index
}
>
{item.icon}
{item.to ? (
<Item
@@ -67,6 +69,8 @@ const Slash = styled(GoToIcon)`
const Item = styled(Link)<{ $highlight: boolean; $withIcon: boolean }>`
${ellipsis()}
${undraggableOnDesktop()}
display: flex;
flex-shrink: 1;
min-width: 0;
@@ -76,7 +80,6 @@ const Item = styled(Link)<{ $highlight: boolean; $withIcon: boolean }>`
height: 24px;
font-weight: ${(props) => (props.$highlight ? "500" : "inherit")};
margin-left: ${(props) => (props.$withIcon ? "4px" : "0")};
${undraggableOnDesktop()}
svg {
flex-shrink: 0;
@@ -87,4 +90,4 @@ const Item = styled(Link)<{ $highlight: boolean; $withIcon: boolean }>`
}
`;
export default Breadcrumb;
export default React.forwardRef<HTMLDivElement, Props>(Breadcrumb);
+4 -2
View File
@@ -18,6 +18,8 @@ import useStores from "~/hooks/useStores";
type Props = {
/** The document to display live collaborators for */
document: Document;
/** The maximum number of collaborators to display, defaults to 6 */
limit?: number;
};
/**
@@ -25,6 +27,7 @@ type Props = {
* and presence status.
*/
function Collaborators(props: Props) {
const { limit = 6 } = props;
const { t } = useTranslation();
const user = useCurrentUser();
const currentUserId = user?.id;
@@ -75,8 +78,6 @@ function Collaborators(props: Props) {
placement: "bottom-end",
});
const limit = 8;
return (
<>
<PopoverDisclosure {...popover}>
@@ -88,6 +89,7 @@ function Collaborators(props: Props) {
>
<Facepile
limit={limit}
overflow={collaborators.length - limit}
users={collaborators}
renderAvatar={(collaborator) => {
const isPresent = presentIds.includes(collaborator.id);
-1
View File
@@ -201,7 +201,6 @@ const Input = styled.div`
margin: -8px;
padding: 8px;
border-radius: 8px;
transition: ${s("backgroundTransition")};
&:after {
content: "";
+6 -3
View File
@@ -8,8 +8,8 @@ import Text from "~/components/Text";
import useStores from "~/hooks/useStores";
type Props = {
/** Callback when the dialog is submitted */
onSubmit: () => Promise<void> | void;
/** Callback when the dialog is submitted. Return false to prevent closing. */
onSubmit: () => Promise<void | boolean> | void;
/** Text to display on the submit button */
submitText?: string;
/** Text to display while the form is saving */
@@ -38,7 +38,10 @@ const ConfirmationDialog: React.FC<Props> = ({
ev.preventDefault();
setIsSaving(true);
try {
await onSubmit();
const res = await onSubmit();
if (res === false) {
return;
}
dialogs.closeAllModals();
} catch (err) {
toast.error(err.message);
-1
View File
@@ -182,7 +182,6 @@ function placeCaret(element: HTMLElement, atStart: boolean) {
const Content = styled.span`
background: ${s("background")};
transition: ${s("backgroundTransition")};
color: ${s("text")};
-webkit-text-fill-color: ${s("text")};
outline: none;
+2
View File
@@ -109,6 +109,8 @@ const Title = styled.div`
${ellipsis()}
flex-grow: 1;
display: flex;
align-items: center;
gap: 8px;
`;
type MenuAnchorProps = {
-16
View File
@@ -262,22 +262,6 @@ export const Position = styled.div`
transition-property: outline-width;
transition-duration: 0;
outline: none;
&:after {
content: "";
position: absolute;
top: 1px;
left: 1px;
right: 1px;
bottom: 1px;
pointer-events: none;
border-radius: 4px;
outline-color: ${s("accent")};
outline-width: initial;
outline-offset: -1px;
outline-style: solid;
}
}
/*
+19 -17
View File
@@ -8,15 +8,11 @@ import Document from "~/models/Document";
import Breadcrumb from "~/components/Breadcrumb";
import Icon from "~/components/Icon";
import CollectionIcon from "~/components/Icons/CollectionIcon";
import { useLocationSidebarContext } from "~/hooks/useLocationSidebarContext";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
import { MenuInternalLink } from "~/types";
import {
archivePath,
collectionPath,
settingsPath,
trashPath,
} from "~/utils/routeHelpers";
import { archivePath, settingsPath, trashPath } from "~/utils/routeHelpers";
type Props = {
children?: React.ReactNode;
@@ -57,14 +53,14 @@ function useCategory(document: Document): MenuInternalLink | null {
return null;
}
const DocumentBreadcrumb: React.FC<Props> = ({
document,
children,
onlyText,
}: Props) => {
function DocumentBreadcrumb(
{ document, children, onlyText }: Props,
ref: React.RefObject<HTMLDivElement> | null
) {
const { collections } = useStores();
const { t } = useTranslation();
const category = useCategory(document);
const sidebarContext = useLocationSidebarContext();
const collection = document.collectionId
? collections.get(document.collectionId)
: undefined;
@@ -81,7 +77,10 @@ const DocumentBreadcrumb: React.FC<Props> = ({
type: "route",
title: collection.name,
icon: <CollectionIcon collection={collection} expanded />,
to: collectionPath(collection.path),
to: {
pathname: collection.path,
state: { sidebarContext },
},
};
} else if (document.isCollectionDeleted) {
collectionNode = {
@@ -115,11 +114,14 @@ const DocumentBreadcrumb: React.FC<Props> = ({
) : (
node.title
),
to: node.url,
to: {
pathname: node.url,
state: { sidebarContext },
},
});
});
return output;
}, [path, category, collectionNode]);
}, [path, category, sidebarContext, collectionNode]);
if (!collections.isLoaded) {
return null;
@@ -140,11 +142,11 @@ const DocumentBreadcrumb: React.FC<Props> = ({
}
return (
<Breadcrumb items={items} highlightFirstItem>
<Breadcrumb items={items} ref={ref} highlightFirstItem>
{children}
</Breadcrumb>
);
};
}
const StyledIcon = styled(Icon)`
margin-right: 2px;
@@ -160,4 +162,4 @@ const SmallSlash = styled(GoToIcon)`
opacity: 0.5;
`;
export default observer(DocumentBreadcrumb);
export default observer(React.forwardRef(DocumentBreadcrumb));
+1 -6
View File
@@ -144,12 +144,7 @@ function DocumentCard(props: Props) {
</Heading>
<DocumentMeta size="xsmall">
<Clock size={18} />
<Time
dateTime={document.updatedAt}
tooltipDelay={500}
addSuffix
shorten
/>
<Time dateTime={document.updatedAt} addSuffix shorten />
</DocumentMeta>
</div>
</Content>
+149
View File
@@ -0,0 +1,149 @@
import flatten from "lodash/flatten";
import { observer } from "mobx-react";
import * as React from "react";
import { Trans, useTranslation } from "react-i18next";
import { toast } from "sonner";
import styled from "styled-components";
import { NavigationNode } from "@shared/types";
import Document from "~/models/Document";
import { FlexContainer, Footer, StyledText } from "~/scenes/DocumentMove";
import Button from "~/components/Button";
import DocumentExplorer from "~/components/DocumentExplorer";
import useCollectionTrees from "~/hooks/useCollectionTrees";
import useStores from "~/hooks/useStores";
import { flattenTree } from "~/utils/tree";
import Switch from "./Switch";
import Text from "./Text";
type Props = {
/** The original document to duplicate */
document: Document;
onSubmit: (documents: Document[]) => void;
};
function DocumentCopy({ document, onSubmit }: Props) {
const { t } = useTranslation();
const { policies } = useStores();
const collectionTrees = useCollectionTrees();
const [publish, setPublish] = React.useState<boolean>(!!document.publishedAt);
const [recursive, setRecursive] = React.useState<boolean>(true);
const [selectedPath, selectPath] = React.useState<NavigationNode | null>(
null
);
const items = React.useMemo(() => {
const nodes = flatten(collectionTrees.map(flattenTree)).filter((node) =>
node.collectionId
? policies.get(node.collectionId)?.abilities.createDocument
: true
);
if (document.isTemplate) {
return nodes
.filter((node) => node.type === "collection")
.map((node) => ({ ...node, children: [] }));
}
return nodes;
}, [policies, collectionTrees, document.isTemplate]);
const handlePublishChange = React.useCallback(
(ev: React.ChangeEvent<HTMLInputElement>) => {
setPublish(ev.target.checked);
},
[]
);
const handleRecursiveChange = React.useCallback(
(ev: React.ChangeEvent<HTMLInputElement>) => {
setRecursive(ev.target.checked);
},
[]
);
const copy = async () => {
if (!selectedPath) {
toast.message(t("Select a location to copy"));
return;
}
try {
const result = await document.duplicate({
publish,
recursive,
title: document.title,
collectionId: selectedPath.collectionId,
...(selectedPath.type === "document"
? { parentDocumentId: selectedPath.id }
: {}),
});
toast.success(t("Document copied"));
onSubmit(result);
} catch (err) {
toast.error(t("Couldnt copy the document, try again?"));
}
};
return (
<FlexContainer column>
<DocumentExplorer
items={items}
onSubmit={copy}
onSelect={selectPath}
defaultValue={document.parentDocumentId || document.collectionId || ""}
/>
<OptionsContainer>
{!document.isTemplate && (
<>
{document.collectionId && (
<Text size="small">
<Switch
name="publish"
label={t("Publish")}
labelPosition="right"
checked={publish}
onChange={handlePublishChange}
/>
</Text>
)}
{document.publishedAt && document.childDocuments.length > 0 && (
<Text size="small">
<Switch
name="recursive"
label={t("Include nested documents")}
labelPosition="right"
checked={recursive}
onChange={handleRecursiveChange}
/>
</Text>
)}
</>
)}
</OptionsContainer>
<Footer justify="space-between" align="center" gap={8}>
<StyledText type="secondary">
{selectedPath ? (
<Trans
defaults="Copy to <em>{{ location }}</em>"
values={{ location: selectedPath.title }}
components={{ em: <strong /> }}
/>
) : (
t("Select a location to copy")
)}
</StyledText>
<Button disabled={!selectedPath} onClick={copy}>
{t("Copy")}
</Button>
</Footer>
</FlexContainer>
);
}
const OptionsContainer = styled.div`
margin: 16px 0 8px 0;
padding-left: 24px;
padding-right: 24px;
`;
export default observer(DocumentCopy);
+28 -6
View File
@@ -15,6 +15,7 @@ import scrollIntoView from "scroll-into-view-if-needed";
import styled, { useTheme } from "styled-components";
import breakpoint from "styled-components-breakpoint";
import { NavigationNode } from "@shared/types";
import { isModKey } from "@shared/utils/keyboard";
import DocumentExplorerNode from "~/components/DocumentExplorerNode";
import DocumentExplorerSearchResult from "~/components/DocumentExplorerSearchResult";
import Flex from "~/components/Flex";
@@ -25,21 +26,20 @@ import InputSearch from "~/components/InputSearch";
import Text from "~/components/Text";
import useMobile from "~/hooks/useMobile";
import useStores from "~/hooks/useStores";
import { isModKey } from "~/utils/keyboard";
import { ancestors, descendants } from "~/utils/tree";
type Props = {
/** Action taken upon submission of selected item, could be publish, move etc. */
onSubmit: () => void;
/** A side-effect of item selection */
onSelect: (item: NavigationNode | null) => void;
/** Items to be shown in explorer */
items: NavigationNode[];
/** Automatically expand to and select item with the given id */
defaultValue?: string;
};
function DocumentExplorer({ onSubmit, onSelect, items }: Props) {
function DocumentExplorer({ onSubmit, onSelect, items, defaultValue }: Props) {
const isMobile = useMobile();
const { collections, documents } = useStores();
const { t } = useTranslation();
@@ -47,12 +47,25 @@ function DocumentExplorer({ onSubmit, onSelect, items }: Props) {
const [searchTerm, setSearchTerm] = React.useState<string>();
const [selectedNode, selectNode] = React.useState<NavigationNode | null>(
null
() => {
const node =
defaultValue && items.find((item) => item.id === defaultValue);
return node || null;
}
);
const [initialScrollOffset, setInitialScrollOffset] =
React.useState<number>(0);
const [activeNode, setActiveNode] = React.useState<number>(0);
const [expandedNodes, setExpandedNodes] = React.useState<string[]>([]);
const [expandedNodes, setExpandedNodes] = React.useState<string[]>(() => {
if (defaultValue) {
const node = items.find((item) => item.id === defaultValue);
if (node) {
return ancestors(node).map((node) => node.id);
}
}
return [];
});
const [itemRefs, setItemRefs] = React.useState<
React.RefObject<HTMLSpanElement>[]
>([]);
@@ -94,6 +107,15 @@ function DocumentExplorer({ onSubmit, onSelect, items }: Props) {
onSelect(selectedNode);
}, [selectedNode, onSelect]);
React.useEffect(() => {
if (defaultValue && selectedNode && listRef) {
const index = nodes.findIndex((node) => node.id === selectedNode.id);
if (index > 0) {
setTimeout(() => listRef.current?.scrollToItem(index, "center"), 50);
}
}
}, []);
function getNodes() {
function includeDescendants(item: NavigationNode): NavigationNode[] {
return expandedNodes.includes(item.id)
+11 -5
View File
@@ -21,9 +21,11 @@ import StarButton, { AnimatedStar } from "~/components/Star";
import Tooltip from "~/components/Tooltip";
import useBoolean from "~/hooks/useBoolean";
import useCurrentUser from "~/hooks/useCurrentUser";
import { useLocationSidebarContext } from "~/hooks/useLocationSidebarContext";
import DocumentMenu from "~/menus/DocumentMenu";
import { hover } from "~/styles";
import { documentPath } from "~/utils/routeHelpers";
import { determineSidebarContext } from "./Sidebar/components/SidebarContext";
type Props = {
document: Document;
@@ -50,6 +52,7 @@ function DocumentListItem(
) {
const { t } = useTranslation();
const user = useCurrentUser();
const locationSidebarContext = useLocationSidebarContext();
const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean();
let itemRef: React.Ref<HTMLAnchorElement> =
@@ -78,6 +81,12 @@ function DocumentListItem(
!!document.title.toLowerCase().includes(highlight.toLowerCase());
const canStar = !document.isArchived && !document.isTemplate;
const sidebarContext = determineSidebarContext({
document,
user,
currentContext: locationSidebarContext,
});
return (
<DocumentLink
ref={itemRef}
@@ -89,6 +98,7 @@ function DocumentListItem(
pathname: documentPath(document),
state: {
title: document.titleWithDefault,
sidebarContext,
},
}}
{...rest}
@@ -111,11 +121,7 @@ function DocumentListItem(
<Badge yellow>{t("New")}</Badge>
)}
{document.isDraft && showDraft && (
<Tooltip
content={t("Only visible to you")}
delay={500}
placement="top"
>
<Tooltip content={t("Only visible to you")} placement="top">
<Badge>{t("Draft")}</Badge>
</Tooltip>
)}
+6 -2
View File
@@ -185,9 +185,9 @@ const DocumentMeta: React.FC<Props> = ({
{showCollection && collection && (
<span>
&nbsp;{t("in")}&nbsp;
<strong>
<Strong>
<DocumentBreadcrumb document={document} onlyText />
</strong>
</Strong>
</span>
)}
{showParentDocuments && nestedDocumentsCount > 0 && (
@@ -210,6 +210,10 @@ const DocumentMeta: React.FC<Props> = ({
);
};
const Strong = styled.strong`
font-weight: 550;
`;
const Container = styled(Flex)<{ rtl?: boolean }>`
justify-content: ${(props) => (props.rtl ? "flex-end" : "flex-start")};
color: ${s("textTertiary")};
-97
View File
@@ -1,97 +0,0 @@
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { DocumentValidation } from "@shared/validations";
import Document from "~/models/Document";
import ConfirmationDialog from "~/components/ConfirmationDialog";
import Input from "./Input";
import Switch from "./Switch";
import Text from "./Text";
type Props = {
/** The original document to duplicate */
document: Document;
onSubmit: (documents: Document[]) => void;
};
function DuplicateDialog({ document, onSubmit }: Props) {
const { t } = useTranslation();
const defaultTitle = t(`Copy of {{ documentName }}`, {
documentName: document.title,
});
const [publish, setPublish] = React.useState<boolean>(!!document.publishedAt);
const [recursive, setRecursive] = React.useState<boolean>(true);
const [title, setTitle] = React.useState<string>(defaultTitle);
const handlePublishChange = React.useCallback(
(ev: React.ChangeEvent<HTMLInputElement>) => {
setPublish(ev.target.checked);
},
[]
);
const handleRecursiveChange = React.useCallback(
(ev: React.ChangeEvent<HTMLInputElement>) => {
setRecursive(ev.target.checked);
},
[]
);
const handleTitleChange = React.useCallback(
(ev: React.ChangeEvent<HTMLInputElement>) => {
setTitle(ev.target.value);
},
[]
);
const handleSubmit = async () => {
const result = await document.duplicate({
publish,
recursive,
title,
});
onSubmit(result);
};
return (
<ConfirmationDialog onSubmit={handleSubmit} submitText={t("Duplicate")}>
<Input
autoFocus
autoSelect
name="title"
label={t("Title")}
onChange={handleTitleChange}
maxLength={DocumentValidation.maxTitleLength}
defaultValue={defaultTitle}
/>
{!document.isTemplate && (
<>
{document.collectionId && (
<Text size="small">
<Switch
name="publish"
label={t("Publish")}
labelPosition="right"
checked={publish}
onChange={handlePublishChange}
/>
</Text>
)}
{document.publishedAt && document.childDocuments.length > 0 && (
<Text size="small">
<Switch
name="recursive"
label={t("Include nested documents")}
labelPosition="right"
checked={recursive}
onChange={handleRecursiveChange}
/>
</Text>
)}
</>
)}
</ConfirmationDialog>
);
}
export default observer(DuplicateDialog);
+6 -2
View File
@@ -19,6 +19,7 @@ import Event from "~/models/Event";
import { Avatar } from "~/components/Avatar";
import Item, { Actions, Props as ItemProps } from "~/components/List/Item";
import Time from "~/components/Time";
import { useLocationSidebarContext } from "~/hooks/useLocationSidebarContext";
import useStores from "~/hooks/useStores";
import RevisionMenu from "~/menus/RevisionMenu";
import { hover } from "~/styles";
@@ -35,6 +36,7 @@ const EventListItem = ({ event, latest, document, ...rest }: Props) => {
const { t } = useTranslation();
const { revisions } = useStores();
const location = useLocation();
const sidebarContext = useLocationSidebarContext();
const opts = {
userName: event.actor.name,
};
@@ -66,7 +68,10 @@ const EventListItem = ({ event, latest, document, ...rest }: Props) => {
);
to = {
pathname: documentHistoryPath(document, event.modelId || "latest"),
state: { retainScrollPosition: true },
state: {
sidebarContext,
retainScrollPosition: true,
},
};
break;
@@ -140,7 +145,6 @@ const EventListItem = ({ event, latest, document, ...rest }: Props) => {
title={
<Time
dateTime={event.createdAt}
tooltipDelay={500}
format={{
en_US: "MMM do, h:mm a",
fr_FR: "'Le 'd MMMM 'à' H:mm",
+1 -1
View File
@@ -229,7 +229,7 @@ const SearchInput = styled(Input)`
${Outline} {
border: none;
border-radius: 0;
border-bottom: 1px solid ${s("inputBorder")};
border-bottom: 1px solid rgb(34 40 52);
background: ${s("menuBackground")};
}
-1
View File
@@ -94,7 +94,6 @@ const Scene = styled.div`
align-items: flex-start;
width: 350px;
background: ${s("background")};
transition: ${s("backgroundTransition")};
border-radius: 8px;
outline: none;
opacity: 0;
+56 -37
View File
@@ -3,6 +3,7 @@ import { observer } from "mobx-react";
import { MenuIcon } from "outline-icons";
import { transparentize } from "polished";
import * as React from "react";
import { mergeRefs } from "react-merge-refs";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import { depths, s } from "@shared/styles";
@@ -10,27 +11,35 @@ import { supportsPassiveListener } from "@shared/utils/browser";
import Button from "~/components/Button";
import Fade from "~/components/Fade";
import Flex from "~/components/Flex";
import useComponentSize from "~/hooks/useComponentSize";
import useEventListener from "~/hooks/useEventListener";
import useMobile from "~/hooks/useMobile";
import useStores from "~/hooks/useStores";
import { draggableOnDesktop, fadeOnDesktopBackgrounded } from "~/styles";
import Desktop from "~/utils/Desktop";
import { TooltipProvider } from "./TooltipContext";
export const HEADER_HEIGHT = 64;
type Props = {
left?: React.ReactNode;
title: React.ReactNode;
actions?: React.ReactNode;
actions?:
| ((props: { isCompact: boolean }) => React.ReactNode)
| React.ReactNode;
hasSidebar?: boolean;
className?: string;
};
function Header({ left, title, actions, hasSidebar, className }: Props) {
function Header(
{ left, title, actions, hasSidebar, className }: Props,
ref: React.RefObject<HTMLDivElement> | null
) {
const { ui } = useStores();
const isMobile = useMobile();
const hasMobileSidebar = hasSidebar && isMobile;
const internalRef = React.useRef<HTMLDivElement | null>(null);
const breadcrumbsRef = React.useRef<HTMLDivElement | null>(null);
const passThrough = !actions && !left && !title;
const [isScrolled, setScrolled] = React.useState(false);
@@ -53,38 +62,50 @@ function Header({ left, title, actions, hasSidebar, className }: Props) {
});
}, []);
return (
<Wrapper
align="center"
shrink={false}
className={className}
$passThrough={passThrough}
$insetTitleAdjust={ui.sidebarIsClosed && Desktop.hasInsetTitlebar()}
>
{left || hasMobileSidebar ? (
<Breadcrumbs>
{hasMobileSidebar && (
<MobileMenuButton
onClick={ui.toggleMobileSidebar}
icon={<MenuIcon />}
neutral
/>
)}
{left}
</Breadcrumbs>
) : null}
const setBreadcrumbRef = React.useCallback((node: HTMLDivElement | null) => {
breadcrumbsRef.current = node?.firstElementChild as HTMLDivElement;
}, []);
{isScrolled ? (
<Title onClick={handleClickTitle}>
<Fade>{title}</Fade>
</Title>
) : (
<div />
)}
<Actions align="center" justify="flex-end">
{actions}
</Actions>
</Wrapper>
const size = useComponentSize(internalRef);
const breadcrumbsSize = useComponentSize(breadcrumbsRef);
const breadcrumbMakesCompact = breadcrumbsSize.width > size.width / 3;
const isCompact = size.width < 1000 || breadcrumbMakesCompact;
return (
<TooltipProvider>
<Wrapper
ref={mergeRefs([ref, internalRef])}
align="center"
shrink={false}
className={className}
$passThrough={passThrough}
$insetTitleAdjust={ui.sidebarIsClosed && Desktop.hasInsetTitlebar()}
>
{left || hasMobileSidebar ? (
<Breadcrumbs ref={setBreadcrumbRef}>
{hasMobileSidebar && (
<MobileMenuButton
onClick={ui.toggleMobileSidebar}
icon={<MenuIcon />}
neutral
/>
)}
{left}
</Breadcrumbs>
) : null}
{isScrolled && !isCompact ? (
<Title onClick={handleClickTitle}>
<Fade>{title}</Fade>
</Title>
) : (
<div />
)}
<Actions align="center" justify="flex-end">
{typeof actions === "function" ? actions({ isCompact }) : actions}
</Actions>
</Wrapper>
</TooltipProvider>
);
}
@@ -130,7 +151,6 @@ const Wrapper = styled(Flex)<WrapperProps>`
`};
padding: 12px;
transition: all 100ms ease-out;
transform: translate3d(0, 0, 0);
min-height: ${HEADER_HEIGHT}px;
justify-content: flex-start;
@@ -152,7 +172,6 @@ const Wrapper = styled(Flex)<WrapperProps>`
${breakpoint("tablet")`
padding: 16px;
justify-content: center;
${(props: WrapperProps) => props.$insetTitleAdjust && `padding-left: 64px;`}
`};
`;
@@ -191,4 +210,4 @@ const MobileMenuButton = styled(Button)`
}
`;
export default observer(Header);
export default observer(React.forwardRef(Header));
+1 -1
View File
@@ -4,9 +4,9 @@ import * as React from "react";
import { useTranslation } from "react-i18next";
import { useHistory } from "react-router-dom";
import styled, { useTheme } from "styled-components";
import { isModKey } from "@shared/utils/keyboard";
import useBoolean from "~/hooks/useBoolean";
import useKeyDown from "~/hooks/useKeyDown";
import { isModKey } from "~/utils/keyboard";
import { searchPath } from "~/utils/routeHelpers";
import Input, { Outline } from "./Input";
+4 -2
View File
@@ -47,14 +47,16 @@ export default function LanguagePrompt() {
<br />
<Link
onClick={async () => {
ui.setLanguagePromptDismissed();
ui.set({ languagePromptDismissed: true });
await user.save({ language });
}}
>
{t("Change Language")}
</Link>{" "}
&middot;{" "}
<Link onClick={ui.setLanguagePromptDismissed}>{t("Dismiss")}</Link>
<Link onClick={() => ui.set({ languagePromptDismissed: true })}>
{t("Dismiss")}
</Link>
</span>
</Flex>
</Wrapper>
+1 -2
View File
@@ -4,6 +4,7 @@ import { Helmet } from "react-helmet-async";
import styled, { DefaultTheme } from "styled-components";
import breakpoint from "styled-components-breakpoint";
import { s } from "@shared/styles";
import { isModKey } from "@shared/utils/keyboard";
import Flex from "~/components/Flex";
import { LoadingIndicatorBar } from "~/components/LoadingIndicator";
import SkipNavContent from "~/components/SkipNavContent";
@@ -13,7 +14,6 @@ import useAutoRefresh from "~/hooks/useAutoRefresh";
import useKeyDown from "~/hooks/useKeyDown";
import { MenuProvider } from "~/hooks/useMenuContext";
import useStores from "~/hooks/useStores";
import { isModKey } from "~/utils/keyboard";
type Props = {
children?: React.ReactNode;
@@ -76,7 +76,6 @@ const Layout = React.forwardRef(function Layout_(
const Container = styled(Flex)`
background: ${s("background")};
transition: ${s("backgroundTransition")};
position: relative;
width: 100%;
min-height: 100%;
+1 -3
View File
@@ -23,7 +23,6 @@ function eachMinute(fn: () => void) {
export type Props = {
children?: React.ReactNode;
dateTime: string;
tooltipDelay?: number;
addSuffix?: boolean;
shorten?: boolean;
relative?: boolean;
@@ -37,7 +36,6 @@ const LocaleTime: React.FC<Props> = ({
shorten,
format,
relative,
tooltipDelay,
}: Props) => {
const userLocale = useUserLocale();
const dateFormatLong: Record<string, string> = {
@@ -82,7 +80,7 @@ const LocaleTime: React.FC<Props> = ({
});
return (
<Tooltip content={tooltipContent} delay={tooltipDelay} placement="bottom">
<Tooltip content={tooltipContent} placement="bottom">
<time dateTime={dateTime}>{children || content}</time>
</Tooltip>
);
-2
View File
@@ -174,7 +174,6 @@ const Fullscreen = styled.div<FullscreenProps>`
justify-content: center;
align-items: flex-start;
background: ${s("background")};
transition: ${s("backgroundTransition")};
outline: none;
${breakpoint("tablet")`
@@ -265,7 +264,6 @@ const Small = styled.div`
justify-content: center;
align-items: flex-start;
background: ${s("modalBackground")};
transition: ${s("backgroundTransition")};
box-shadow: ${s("modalShadow")};
border-radius: 8px;
outline: none;
@@ -52,11 +52,7 @@ function NotificationListItem({ notification, onNavigate }: Props) {
<Text weight="bold">{notification.subject}</Text>
</Text>
<Text type="tertiary" size="xsmall">
<Time
dateTime={notification.createdAt}
tooltipDelay={1000}
addSuffix
/>{" "}
<Time dateTime={notification.createdAt} addSuffix />{" "}
{collection && <>&middot; {collection.name}</>}
</Text>
{notification.comment && (
@@ -60,7 +60,7 @@ function Notifications(
</Text>
<Flex gap={8}>
{notifications.approximateUnreadCount > 0 && (
<Tooltip delay={500} content={t("Mark all as read")}>
<Tooltip content={t("Mark all as read")}>
<Button action={markNotificationsAsRead} context={context}>
<MarkAsReadIcon />
</Button>
+15 -1
View File
@@ -10,13 +10,23 @@ import { fadeAndScaleIn } from "~/styles/animations";
type Props = PopoverProps & {
children: React.ReactNode;
/** The width of the popover, defaults to 380px. */
width?: number;
/** The minimum width of the popover, use instead of width if contents adjusts size. */
minWidth?: number;
/** Shrink the padding of the popover */
shrink?: boolean;
/** Make the popover flex */
flex?: boolean;
/** The tab index of the popover */
tabIndex?: number;
/** Whether the popover should be scrollable, defaults to true. */
scrollable?: boolean;
/** The position of the popover on mobile, defaults to "top". */
mobilePosition?: "top" | "bottom";
/** Function to show the popover */
show: () => void;
/** Function to hide the popover */
hide: () => void;
};
@@ -25,6 +35,7 @@ const Popover = (
children,
shrink,
width = 380,
minWidth,
scrollable = true,
flex,
mobilePosition,
@@ -71,6 +82,7 @@ const Popover = (
ref={ref}
$shrink={shrink}
$width={width}
$minWidth={minWidth}
$scrollable={scrollable}
$flex={flex}
>
@@ -83,6 +95,7 @@ const Popover = (
type ContentsProps = {
$shrink?: boolean;
$width?: number;
$minWidth?: number;
$flex?: boolean;
$scrollable: boolean;
$mobilePosition?: "top" | "bottom";
@@ -101,7 +114,8 @@ const Contents = styled.div<ContentsProps>`
padding: ${(props) => (props.$shrink ? "6px 0" : "12px 24px")};
max-height: 75vh;
box-shadow: ${s("menuShadow")};
width: ${(props) => props.$width}px;
${(props) => props.$width && `width: ${props.$width}px`};
${(props) => props.$minWidth && `min-width: ${props.$minWidth}px`};
${(props) =>
props.$scrollable
+1 -2
View File
@@ -128,7 +128,7 @@ const Reaction: React.FC<Props> = ({
);
return tooltipContent ? (
<Tooltip content={tooltipContent} delay={250} placement="bottom">
<Tooltip content={tooltipContent} placement="bottom">
{DisplayedEmoji}
</Tooltip>
) : (
@@ -144,7 +144,6 @@ const EmojiButton = styled(NudeButton)<{
height: 28px;
padding: 6px;
border-radius: 12px;
transition: ${s("backgroundTransition")};
background: ${s("backgroundTertiary")};
pointer-events: ${({ disabled }) => disabled && "none"};
+1 -6
View File
@@ -98,12 +98,7 @@ const ReactionPicker: React.FC<Props> = ({
<>
<PopoverDisclosure {...popover}>
{(props) => (
<Tooltip
content={t("Add reaction")}
placement="top"
delay={500}
hideOnClick
>
<Tooltip content={t("Add reaction")} placement="top" hideOnClick>
<NudeButton
{...props}
aria-label={t("Reaction picker")}
@@ -119,7 +119,7 @@ function PublicAccess({ document, share, sharedParent }: Props) {
: share?.url ?? "";
const copyButton = (
<Tooltip content={t("Copy public link")} delay={500} placement="top">
<Tooltip content={t("Copy public link")} placement="top">
<CopyToClipboard text={shareUrl} onCopy={handleCopied}>
<NudeButton type="button" disabled={!share} style={{ marginRight: 3 }}>
<CopyIcon color={theme.placeholder} size={18} />
@@ -31,7 +31,7 @@ export function CopyLinkButton({
}, [onCopy, t]);
return (
<Tooltip content={t("Copy link")} delay={500} placement="top">
<Tooltip content={t("Copy link")} placement="top">
<CopyToClipboard text={url} onCopy={handleCopied}>
<NudeButton type="button">
<LinkIcon size={20} />
+1 -2
View File
@@ -5,6 +5,7 @@ import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { metaDisplay } from "@shared/utils/keyboard";
import Flex from "~/components/Flex";
import Scrollable from "~/components/Scrollable";
import Text from "~/components/Text";
@@ -14,7 +15,6 @@ import useCurrentUser from "~/hooks/useCurrentUser";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
import OrganizationMenu from "~/menus/OrganizationMenu";
import { metaDisplay } from "~/utils/keyboard";
import { homePath, draftsPath, searchPath } from "~/utils/routeHelpers";
import TeamLogo from "../TeamLogo";
import Tooltip from "../Tooltip";
@@ -80,7 +80,6 @@ function AppSidebar() {
<Tooltip
content={t("Toggle sidebar")}
shortcut={`${metaDisplay}+.`}
delay={500}
>
<ToggleButton
position="bottom"
+3 -3
View File
@@ -32,13 +32,13 @@ function Right({ children, border, className }: Props) {
Math.min(window.innerWidth - event.pageX, maxWidth),
minWidth
);
ui.setRightSidebarWidth(width);
ui.set({ sidebarRightWidth: width });
},
[minWidth, maxWidth, ui]
);
const handleReset = React.useCallback(() => {
ui.setRightSidebarWidth(theme.sidebarRightWidth);
ui.set({ sidebarRightWidth: theme.sidebarRightWidth });
}, [ui, theme.sidebarRightWidth]);
const handleStopDrag = React.useCallback(() => {
@@ -128,7 +128,7 @@ const Sidebar = styled(m.div)<{
max-width: 80%;
border-left: 1px solid ${s("divider")};
transition: border-left 100ms ease-in-out;
z-index: 1;
z-index: ${depths.sidebar};
${breakpoint("mobile", "tablet")`
display: flex;
+2 -6
View File
@@ -5,12 +5,12 @@ import * as React from "react";
import { useTranslation } from "react-i18next";
import { useHistory, useLocation } from "react-router-dom";
import styled from "styled-components";
import { metaDisplay } from "@shared/utils/keyboard";
import Flex from "~/components/Flex";
import Scrollable from "~/components/Scrollable";
import useSettingsConfig from "~/hooks/useSettingsConfig";
import useStores from "~/hooks/useStores";
import isCloudHosted from "~/utils/isCloudHosted";
import { metaDisplay } from "~/utils/keyboard";
import { settingsPath } from "~/utils/routeHelpers";
import Tooltip from "../Tooltip";
import Sidebar from "./Sidebar";
@@ -42,11 +42,7 @@ function SettingsSidebar() {
image={<StyledBackIcon />}
onClick={returnToApp}
>
<Tooltip
content={t("Toggle sidebar")}
shortcut={`${metaDisplay}+.`}
delay={500}
>
<Tooltip content={t("Toggle sidebar")} shortcut={`${metaDisplay}+.`}>
<ToggleButton
position="bottom"
image={<SidebarIcon />}
+2 -6
View File
@@ -4,6 +4,7 @@ import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { NavigationNode } from "@shared/types";
import { metaDisplay } from "@shared/utils/keyboard";
import Flex from "~/components/Flex";
import Scrollable from "~/components/Scrollable";
import SearchPopover from "~/components/SearchPopover";
@@ -12,7 +13,6 @@ import useCurrentUser from "~/hooks/useCurrentUser";
import useStores from "~/hooks/useStores";
import { hover } from "~/styles";
import history from "~/utils/history";
import { metaDisplay } from "~/utils/keyboard";
import { homePath, sharedDocumentPath } from "~/utils/routeHelpers";
import { useTeamContext } from "../TeamContext";
import TeamLogo from "../TeamLogo";
@@ -81,11 +81,7 @@ const ToggleSidebar = () => {
const { ui } = useStores();
return (
<Tooltip
content={t("Toggle sidebar")}
shortcut={`${metaDisplay}+.`}
delay={500}
>
<Tooltip content={t("Toggle sidebar")} shortcut={`${metaDisplay}+.`}>
<ToggleButton
position="bottom"
image={<SidebarIcon />}
+18 -18
View File
@@ -17,6 +17,7 @@ import { fadeIn } from "~/styles/animations";
import Desktop from "~/utils/Desktop";
import NotificationIcon from "../Notifications/NotificationIcon";
import NotificationsPopover from "../Notifications/NotificationsPopover";
import { TooltipProvider } from "../TooltipContext";
import ResizeBorder from "./components/ResizeBorder";
import SidebarButton, { SidebarButtonProps } from "./components/SidebarButton";
import ToggleButton from "./components/ToggleButton";
@@ -46,7 +47,6 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
const maxWidth = theme.sidebarMaxWidth;
const minWidth = theme.sidebarMinWidth + 16; // padding
const setWidth = ui.setSidebarWidth;
const [offset, setOffset] = React.useState(0);
const [isHovering, setHovering] = React.useState(false);
const [isAnimating, setAnimating] = React.useState(false);
@@ -62,13 +62,13 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
const width = Math.min(event.pageX - offset, maxWidth);
const isSmallerThanCollapsePoint = width < minWidth / 2;
if (isSmallerThanCollapsePoint) {
setWidth(theme.sidebarCollapsedWidth);
} else {
setWidth(width);
}
ui.set({
sidebarWidth: isSmallerThanCollapsePoint
? theme.sidebarCollapsedWidth
: width,
});
},
[theme, offset, minWidth, maxWidth, setWidth]
[ui, theme, offset, minWidth, maxWidth]
);
const handleStopDrag = React.useCallback(() => {
@@ -86,13 +86,13 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
setCollapsing(true);
ui.collapseSidebar();
} else {
setWidth(minWidth);
ui.set({ sidebarWidth: minWidth });
setAnimating(true);
}
} else {
setWidth(width);
ui.set({ sidebarWidth: width });
}
}, [ui, isSmallerThanMinimum, minWidth, width, setWidth]);
}, [ui, isSmallerThanMinimum, minWidth, width]);
const handleBlur = React.useCallback(() => {
setHovering(false);
@@ -149,11 +149,11 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
React.useEffect(() => {
if (isCollapsing) {
setTimeout(() => {
setWidth(minWidth);
ui.set({ sidebarWidth: minWidth });
setCollapsing(false);
}, ANIMATION_MS);
}
}, [setWidth, minWidth, isCollapsing]);
}, [ui, minWidth, isCollapsing]);
React.useEffect(() => {
if (isResizing) {
@@ -174,7 +174,7 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
}, [isResizing, handleDrag, handleBlur, handleStopDrag]);
const handleReset = React.useCallback(() => {
ui.setSidebarWidth(theme.sidebarWidth);
ui.set({ sidebarWidth: theme.sidebarWidth });
}, [ui, theme.sidebarWidth]);
React.useEffect(() => {
@@ -195,8 +195,9 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
);
return (
<>
<TooltipProvider>
<Container
id="sidebar"
ref={ref}
style={style}
$hidden={hidden}
@@ -243,7 +244,7 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
/>
</Container>
{ui.mobileSidebarVisible && <Backdrop onClick={ui.toggleMobileSidebar} />}
</>
</TooltipProvider>
);
});
@@ -299,9 +300,8 @@ const Container = styled(Flex)<ContainerProps>`
width: 100%;
background: ${s("sidebarBackground")};
transition: box-shadow 150ms ease-in-out, transform 150ms ease-out,
${s("backgroundTransition")}
${(props: ContainerProps) =>
props.$isAnimating ? `,width ${ANIMATION_MS}ms ease-out` : ""};
${(props: ContainerProps) =>
props.$isAnimating ? `,width ${ANIMATION_MS}ms ease-out` : ""};
transform: translateX(
${(props) => (props.$mobileSidebarVisible ? 0 : "-100%")}
);
@@ -16,6 +16,7 @@ import Header from "./Header";
import PlaceholderCollections from "./PlaceholderCollections";
import Relative from "./Relative";
import SidebarAction from "./SidebarAction";
import SidebarContext from "./SidebarContext";
import { DragObject } from "./SidebarLink";
function Collections() {
@@ -49,38 +50,40 @@ function Collections() {
});
return (
<Flex column>
<Header id="collections" title={t("Collections")}>
<Relative>
<PaginatedList
options={params}
aria-label={t("Collections")}
items={collections.allActive}
loading={<PlaceholderCollections />}
heading={
isDraggingAnyCollection ? (
<DropCursor
isActiveDrop={isCollectionDropping}
innerRef={dropToReorderCollection}
position="top"
<SidebarContext.Provider value="collections">
<Flex column>
<Header id="collections" title={t("Collections")}>
<Relative>
<PaginatedList
options={params}
aria-label={t("Collections")}
items={collections.allActive}
loading={<PlaceholderCollections />}
heading={
isDraggingAnyCollection ? (
<DropCursor
isActiveDrop={isCollectionDropping}
innerRef={dropToReorderCollection}
position="top"
/>
) : undefined
}
renderError={(props) => <StyledError {...props} />}
renderItem={(item: Collection, index) => (
<DraggableCollectionLink
key={item.id}
collection={item}
activeDocument={documents.active}
prefetchDocument={documents.prefetchDocument}
belowCollection={orderedCollections[index + 1]}
/>
) : undefined
}
renderError={(props) => <StyledError {...props} />}
renderItem={(item: Collection, index) => (
<DraggableCollectionLink
key={item.id}
collection={item}
activeDocument={documents.active}
prefetchDocument={documents.prefetchDocument}
belowCollection={orderedCollections[index + 1]}
/>
)}
/>
<SidebarAction action={createCollection} depth={0} />
</Relative>
</Header>
</Flex>
)}
/>
<SidebarAction action={createCollection} depth={0} />
</Relative>
</Header>
</Flex>
</SidebarContext.Provider>
);
}
@@ -278,7 +278,7 @@ function InnerDocumentLink(
!isDraggingAnyDocument ? (
<Fade>
{can.createChildDocument && (
<Tooltip content={t("New doc")} delay={500}>
<Tooltip content={t("New doc")}>
<NudeButton
type={undefined}
aria-label={t("New nested document")}
@@ -7,8 +7,8 @@ import styled from "styled-components";
import Collection from "~/models/Collection";
import Document from "~/models/Document";
import CollectionIcon from "~/components/Icons/CollectionIcon";
import { useLocationSidebarContext } from "~/hooks/useLocationSidebarContext";
import useStores from "~/hooks/useStores";
import { useLocationState } from "../hooks/useLocationState";
import CollectionLink from "./CollectionLink";
import CollectionLinkChildren from "./CollectionLinkChildren";
import DropCursor from "./DropCursor";
@@ -29,7 +29,7 @@ function DraggableCollectionLink({
prefetchDocument,
belowCollection,
}: Props) {
const locationSidebarContext = useLocationState();
const locationSidebarContext = useLocationSidebarContext();
const sidebarContext = useSidebarContext();
const { ui, policies, collections } = useStores();
const [expanded, setExpanded] = React.useState(
@@ -2,10 +2,11 @@ import { observer } from "mobx-react";
import { GroupIcon } from "outline-icons";
import * as React from "react";
import Group from "~/models/Group";
import { useLocationSidebarContext } from "~/hooks/useLocationSidebarContext";
import Folder from "./Folder";
import Relative from "./Relative";
import SharedWithMeLink from "./SharedWithMeLink";
import SidebarContext from "./SidebarContext";
import SidebarContext, { groupSidebarContext } from "./SidebarContext";
import SidebarLink from "./SidebarLink";
type Props = {
@@ -14,13 +15,23 @@ type Props = {
};
const GroupLink: React.FC<Props> = ({ group }) => {
const [expanded, setExpanded] = React.useState(false);
const locationSidebarContext = useLocationSidebarContext();
const sidebarContext = groupSidebarContext(group.id);
const [expanded, setExpanded] = React.useState(
locationSidebarContext === sidebarContext
);
const handleDisclosureClick = React.useCallback((ev) => {
ev?.preventDefault();
setExpanded((e) => !e);
}, []);
React.useEffect(() => {
if (locationSidebarContext === sidebarContext) {
setExpanded(true);
}
}, [sidebarContext, locationSidebarContext, setExpanded]);
return (
<Relative>
<SidebarLink
@@ -30,7 +41,7 @@ const GroupLink: React.FC<Props> = ({ group }) => {
onClick={handleDisclosureClick}
depth={0}
/>
<SidebarContext.Provider value={group.id}>
<SidebarContext.Provider value={sidebarContext}>
<Folder expanded={expanded}>
{group.documentMemberships.map((membership) => (
<SharedWithMeLink
@@ -43,12 +43,12 @@ function HistoryNavigation(props: React.ComponentProps<typeof Flex>) {
return (
<Navigation gap={4} {...props}>
<Tooltip content={t("Go back")} delay={500}>
<Tooltip content={t("Go back")}>
<NudeButton onClick={() => Desktop.bridge?.goBack()}>
<Back $active={back} />
</NudeButton>
</Tooltip>
<Tooltip content={t("Go forward")} delay={500}>
<Tooltip content={t("Go forward")}>
<NudeButton onClick={() => Desktop.bridge?.goForward()}>
<Forward $active={forward} />
</NudeButton>
+9 -13
View File
@@ -93,15 +93,11 @@ const NavLink = ({
React.useLayoutEffect(() => {
if (isActive && linkRef.current && scrollIntoViewIfNeeded !== false) {
// If the page has an anchor hash then this means we're linking to an
// anchor in the document smooth scrolling the sidebar may the scrolling
// to the anchor of the document so we must avoid it.
if (!window.location.hash) {
scrollIntoView(linkRef.current, {
scrollMode: "if-needed",
behavior: "auto",
});
}
scrollIntoView(linkRef.current, {
scrollMode: "if-needed",
behavior: "auto",
boundary: (parent) => parent.id !== "sidebar",
});
}
}, [linkRef, scrollIntoViewIfNeeded, isActive]);
@@ -112,8 +108,9 @@ const NavLink = ({
!rest.target &&
!event.altKey &&
!event.metaKey &&
!event.ctrlKey,
[rest.target]
!event.ctrlKey &&
!isActive,
[rest.target, isActive]
);
const navigateTo = React.useCallback(() => {
@@ -153,14 +150,13 @@ const NavLink = ({
<Link
key={isActive ? "active" : "inactive"}
ref={linkRef}
// onMouseDown={handleClick}
onClick={handleClick}
onKeyDown={(event) => {
if (["Enter", " "].includes(event.key)) {
navigateTo();
event.currentTarget?.blur();
}
}}
onClick={handleClick}
aria-current={(isActive && ariaCurrent) || undefined}
className={className}
style={style}
@@ -9,6 +9,7 @@ import GroupMembership from "~/models/GroupMembership";
import UserMembership from "~/models/UserMembership";
import Fade from "~/components/Fade";
import useBoolean from "~/hooks/useBoolean";
import { useLocationSidebarContext } from "~/hooks/useLocationSidebarContext";
import useStores from "~/hooks/useStores";
import DocumentMenu from "~/menus/DocumentMenu";
import {
@@ -16,7 +17,6 @@ import {
useDropToReorderUserMembership,
useDropToReparentDocument,
} from "../hooks/useDragAndDrop";
import { useLocationState } from "../hooks/useLocationState";
import { useSidebarLabelAndIcon } from "../hooks/useSidebarLabelAndIcon";
import DocumentLink from "./DocumentLink";
import DropCursor from "./DropCursor";
@@ -36,7 +36,7 @@ function SharedWithMeLink({ membership, depth = 0 }: Props) {
const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean();
const { documentId } = membership;
const isActiveDocument = documentId === ui.activeDocumentId;
const locationSidebarContext = useLocationState();
const locationSidebarContext = useLocationSidebarContext();
const sidebarContext = useSidebarContext();
const document = documentId ? documents.get(documentId) : undefined;
@@ -105,7 +105,6 @@ const Button = styled(Flex)<{
&:hover,
&[aria-expanded="true"] {
color: ${s("sidebarText")};
transition: background 100ms ease-in-out;
background: ${s("sidebarActiveBackground")};
}
@@ -1,9 +1,57 @@
import * as React from "react";
import Document from "~/models/Document";
import User from "~/models/User";
export type SidebarContextType = "collections" | "starred" | string | undefined;
export type SidebarContextType =
| "collections"
| "shared"
| `group-${string}`
| `starred-${string}`
| undefined;
const SidebarContext = React.createContext<SidebarContextType>(undefined);
export const useSidebarContext = () => React.useContext(SidebarContext);
export const groupSidebarContext = (groupId: string): SidebarContextType =>
`group-${groupId}`;
export const starredSidebarContext = (modelId: string): SidebarContextType =>
`starred-${modelId}`;
export const determineSidebarContext = ({
document,
user,
currentContext,
}: {
document: Document;
user: User;
currentContext?: SidebarContextType;
}): SidebarContextType => {
const isStarred = document.isStarred || !!document.collection?.isStarred;
const preferStarred = !currentContext || currentContext.startsWith("starred");
if (isStarred && preferStarred) {
const currentlyInStarredCollection =
currentContext === starredSidebarContext(document.collectionId ?? "");
return document.isStarred && !currentlyInStarredCollection
? starredSidebarContext(document.id)
: starredSidebarContext(document.collectionId!);
}
if (document.collection) {
return "collections";
} else if (
user.documentMemberships.find((m) => m.documentId === document.id)
) {
return "shared";
} else {
const group = user.groupsWithDocumentMemberships.find(
(g) => !!g.documentMemberships.find((m) => m.documentId === document.id)
);
return groupSidebarContext(group?.id ?? "");
}
};
export default SidebarContext;
@@ -78,7 +78,6 @@ function SidebarLink(
const activeStyle = React.useMemo(
() => ({
fontWeight: 600,
color: theme.text,
background: theme.sidebarActiveBackground,
...style,
@@ -202,10 +201,10 @@ const Link = styled(NavLink)<{
display: flex;
position: relative;
text-overflow: ellipsis;
font-weight: 475;
padding: 6px 16px;
border-radius: 4px;
min-height: 32px;
transition: background 50ms, color 50ms;
user-select: none;
background: ${(props) =>
props.$isActiveDrop ? props.theme.slateDark : "inherit"};
+40 -43
View File
@@ -15,7 +15,6 @@ import DropCursor from "./DropCursor";
import Header from "./Header";
import PlaceholderCollections from "./PlaceholderCollections";
import Relative from "./Relative";
import SidebarContext from "./SidebarContext";
import SidebarLink from "./SidebarLink";
import StarredLink from "./StarredLink";
@@ -42,48 +41,46 @@ function Starred() {
}
return (
<SidebarContext.Provider value="starred">
<Flex column>
<Header id="starred" title={t("Starred")}>
<Relative>
{reorderStarProps.isDragging && (
<DropCursor
isActiveDrop={reorderStarProps.isOverCursor}
innerRef={dropToReorder}
position="top"
/>
)}
{createStarProps.isDragging && (
<DropCursor
isActiveDrop={createStarProps.isOverCursor}
innerRef={dropToStarRef}
position="top"
/>
)}
{stars.orderedData
.slice(0, page * STARRED_PAGINATION_LIMIT)
.map((star) => (
<StarredLink key={star.id} star={star} />
))}
{!end && (
<SidebarLink
onClick={next}
label={`${t("Show more")}`}
disabled={stars.isFetching}
depth={0}
/>
)}
{loading && (
<Flex column>
<DelayedMount>
<PlaceholderCollections />
</DelayedMount>
</Flex>
)}
</Relative>
</Header>
</Flex>
</SidebarContext.Provider>
<Flex column>
<Header id="starred" title={t("Starred")}>
<Relative>
{reorderStarProps.isDragging && (
<DropCursor
isActiveDrop={reorderStarProps.isOverCursor}
innerRef={dropToReorder}
position="top"
/>
)}
{createStarProps.isDragging && (
<DropCursor
isActiveDrop={createStarProps.isOverCursor}
innerRef={dropToStarRef}
position="top"
/>
)}
{stars.orderedData
.slice(0, page * STARRED_PAGINATION_LIMIT)
.map((star) => (
<StarredLink key={star.id} star={star} />
))}
{!end && (
<SidebarLink
onClick={next}
label={`${t("Show more")}`}
disabled={stars.isFetching}
depth={0}
/>
)}
{loading && (
<Flex column>
<DelayedMount>
<PlaceholderCollections />
</DelayedMount>
</Flex>
)}
</Relative>
</Header>
</Flex>
);
}
@@ -8,6 +8,7 @@ import styled, { useTheme } from "styled-components";
import Star from "~/models/Star";
import Fade from "~/components/Fade";
import useBoolean from "~/hooks/useBoolean";
import { useLocationSidebarContext } from "~/hooks/useLocationSidebarContext";
import useStores from "~/hooks/useStores";
import DocumentMenu from "~/menus/DocumentMenu";
import {
@@ -15,7 +16,6 @@ import {
useDropToCreateStar,
useDropToReorderStar,
} from "../hooks/useDragAndDrop";
import { useLocationState } from "../hooks/useLocationState";
import { useSidebarLabelAndIcon } from "../hooks/useSidebarLabelAndIcon";
import CollectionLink from "./CollectionLink";
import CollectionLinkChildren from "./CollectionLinkChildren";
@@ -25,7 +25,7 @@ import Folder from "./Folder";
import Relative from "./Relative";
import SidebarContext, {
SidebarContextType,
useSidebarContext,
starredSidebarContext,
} from "./SidebarContext";
import SidebarLink from "./SidebarLink";
@@ -39,22 +39,33 @@ function StarredLink({ star }: Props) {
const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean();
const { documentId, collectionId } = star;
const collection = collections.get(collectionId);
const locationSidebarContext = useLocationState();
const sidebarContext = useSidebarContext();
const locationSidebarContext = useLocationSidebarContext();
const sidebarContext = starredSidebarContext(
star.documentId ?? star.collectionId
);
const [expanded, setExpanded] = useState(
star.collectionId === ui.activeCollectionId &&
(star.documentId
? star.documentId === ui.activeDocumentId
: star.collectionId === ui.activeCollectionId) &&
sidebarContext === locationSidebarContext
);
React.useEffect(() => {
if (
star.documentId === ui.activeDocumentId &&
sidebarContext === locationSidebarContext
) {
setExpanded(true);
} else if (
star.collectionId === ui.activeCollectionId &&
sidebarContext === locationSidebarContext
) {
setExpanded(true);
}
}, [
star.documentId,
star.collectionId,
ui.activeDocumentId,
ui.activeCollectionId,
sidebarContext,
locationSidebarContext,
@@ -152,7 +163,7 @@ function StarredLink({ star }: Props) {
}
/>
</Draggable>
<SidebarContext.Provider value={document.id}>
<SidebarContext.Provider value={sidebarContext}>
<Relative>
<Folder expanded={displayChildDocuments}>
{childDocuments.map((node, index) => (
@@ -176,7 +187,7 @@ function StarredLink({ star }: Props) {
if (collection) {
return (
<>
<SidebarContext.Provider value={sidebarContext}>
<Draggable key={star?.id} ref={draggableRef} $isDragging={isDragging}>
<CollectionLink
collection={collection}
@@ -186,16 +197,14 @@ function StarredLink({ star }: Props) {
isDraggingAnyCollection={reorderStarProps.isDragging}
/>
</Draggable>
<SidebarContext.Provider value={collection.id}>
<Relative>
<CollectionLinkChildren
collection={collection}
expanded={displayChildDocuments}
/>
{cursor}
</Relative>
</SidebarContext.Provider>
</>
<Relative>
<CollectionLinkChildren
collection={collection}
expanded={displayChildDocuments}
/>
{cursor}
</Relative>
</SidebarContext.Provider>
);
}
-1
View File
@@ -31,7 +31,6 @@ const Background = styled.div<{ sticky?: boolean }>`
margin: 0 -8px;
padding: 0 8px;
background: ${s("background")};
transition: ${s("backgroundTransition")};
z-index: 1;
`;
+2 -3
View File
@@ -253,6 +253,7 @@ const SortWrapper = styled(Flex)<{ $sortable: boolean }>`
white-space: nowrap;
margin: 0 -4px;
padding: 0 4px;
cursor: ${(props) => (props.$sortable ? `var(--pointer)` : "")};
&:hover {
background: ${(props) =>
@@ -309,15 +310,13 @@ const Row = styled.tr`
const Head = styled.th`
text-align: left;
padding: 6px 6px 0;
padding: 6px 6px 2px;
border-bottom: 1px solid ${s("divider")};
background: ${s("background")};
transition: ${s("backgroundTransition")};
font-size: 14px;
color: ${s("textSecondary")};
font-weight: 500;
z-index: 1;
cursor: var(--pointer) !important;
:first-child {
padding-left: 0;
-1
View File
@@ -45,7 +45,6 @@ const Sticky = styled.div`
margin: 0 -8px;
padding: 0 8px;
background: ${s("background")};
transition: ${s("backgroundTransition")};
z-index: 1;
`;
+1 -1
View File
@@ -22,7 +22,7 @@ function Time({ onClick, ...props }: Props) {
<time dateTime={props.dateTime}>{props.children || content}</time>
}
>
<LocaleTime tooltipDelay={250} {...props} />
<LocaleTime {...props} />
</React.Suspense>
</span>
);
+41 -9
View File
@@ -1,19 +1,37 @@
import Tippy, { TippyProps } from "@tippyjs/react";
import { transparentize } from "polished";
import * as React from "react";
import styled, { createGlobalStyle } from "styled-components";
import { roundArrow } from "tippy.js";
import { s } from "@shared/styles";
import useMobile from "~/hooks/useMobile";
import { useTooltipContext } from "./TooltipContext";
export type Props = Omit<TippyProps, "content" | "theme"> & {
/** The content to display in the tooltip. */
content?: React.ReactChild | React.ReactChild[];
/** A keyboard shortcut to display next to the content */
shortcut?: React.ReactNode;
/** Whether to show the shortcut on a new line */
shortcutOnNewline?: boolean;
};
function Tooltip({ shortcut, content: tooltip, delay = 50, ...rest }: Props) {
/**
* A tooltip component that wraps Tippy and provides a consistent look and feel. Optionally
* displays a keyboard shortcut next to the content.
*
* Wrap this component in a TooltipProvider to allow multiple tooltips to share the same
* singleton instance (delay, animation, etc).
*/
function Tooltip({
shortcut,
shortcutOnNewline,
content: tooltip,
delay = 500,
...rest
}: Props) {
const isMobile = useMobile();
const singleton = useTooltipContext();
let content = <>{tooltip}</>;
@@ -24,7 +42,19 @@ function Tooltip({ shortcut, content: tooltip, delay = 50, ...rest }: Props) {
if (shortcut) {
content = (
<>
{tooltip} &middot; <Shortcut>{shortcut}</Shortcut>
{tooltip}
{shortcutOnNewline ? <br /> : " "}
{typeof shortcut === "string" ? (
shortcut
.split("+")
.map((key, i) => (
<Shortcut key={`${key}${i}`}>
{key.length === 1 ? key.toUpperCase() : key}
</Shortcut>
))
) : (
<Shortcut>{shortcut}</Shortcut>
)}
</>
);
}
@@ -32,9 +62,10 @@ function Tooltip({ shortcut, content: tooltip, delay = 50, ...rest }: Props) {
return (
<Tippy
arrow={roundArrow}
animation="shift-away"
content={content}
delay={delay}
animation="shift-away"
singleton={singleton}
duration={[200, 150]}
inertia
{...rest}
@@ -44,16 +75,17 @@ function Tooltip({ shortcut, content: tooltip, delay = 50, ...rest }: Props) {
const Shortcut = styled.kbd`
position: relative;
top: -2px;
top: -1px;
margin-left: 2px;
display: inline-block;
padding: 2px 4px;
font: 10px "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier,
monospace;
font-size: 12px;
font-family: ${s("fontFamilyMono")};
line-height: 10px;
color: ${s("tooltipBackground")};
color: ${s("tooltipText")};
border: 1px solid ${(props) => transparentize(0.75, props.theme.tooltipText)};
vertical-align: middle;
background-color: ${s("tooltipText")};
border-radius: 3px;
`;
@@ -132,7 +164,7 @@ export const TooltipStyles = createGlobalStyle`
padding:5px 9px;
z-index:1
}
/* Arrow Styles */
.tippy-box[data-placement^=top]>.tippy-svg-arrow{
bottom:0
+40
View File
@@ -0,0 +1,40 @@
import Tippy, { useSingleton, TippyProps } from "@tippyjs/react";
import * as React from "react";
import { roundArrow } from "tippy.js";
export const TooltipContext =
React.createContext<TippyProps["singleton"]>(undefined);
export function useTooltipContext() {
return React.useContext(TooltipContext);
}
type Props = {
children: React.ReactNode;
/** Props to pass to the Tippy component */
tippyProps?: TippyProps;
};
/**
* Wrap a collection of tooltips in a provider to allow them to share the same singleton instance.
*/
export function TooltipProvider({ children, tippyProps }: Props) {
const [source, target] = useSingleton();
return (
<>
<Tippy
delay={500}
arrow={roundArrow}
animation="shift-away"
singleton={source}
duration={[200, 150]}
inertia
{...tippyProps}
/>
<TooltipContext.Provider value={target}>
{children}
</TooltipContext.Provider>
</>
);
}
-42
View File
@@ -1,42 +0,0 @@
import * as React from "react";
import styled from "styled-components";
import { s } from "@shared/styles";
type Props = {
/** The size to render the indicator, defaults to 24px */
size?: number;
};
/**
* A component to show an animated typing indicator.
*/
export default function Typing({ size = 24 }: Props) {
return (
<Wrapper height={size} width={size}>
<Circle cx={size / 4} cy={size / 2} r="2" />
<Circle cx={size / 2} cy={size / 2} r="2" />
<Circle cx={size / 1.33333} cy={size / 2} r="2" />
</Wrapper>
);
}
const Wrapper = styled.svg`
fill: ${s("textTertiary")};
@keyframes blink {
50% {
fill: transparent;
}
}
`;
const Circle = styled.circle`
animation: 1s blink infinite;
&:nth-child(2) {
animation-delay: 250ms;
}
&:nth-child(3) {
animation-delay: 500ms;
}
`;
+72 -2
View File
@@ -1,10 +1,14 @@
import * as React from "react";
import { useTranslation } from "react-i18next";
import { Trans, useTranslation } from "react-i18next";
import { toast } from "sonner";
import { UserRole } from "@shared/types";
import User from "~/models/User";
import ConfirmationDialog from "~/components/ConfirmationDialog";
import Input from "~/components/Input";
import useCurrentUser from "~/hooks/useCurrentUser";
import useStores from "~/hooks/useStores";
import { client } from "~/utils/ApiClient";
import Text from "./Text";
type Props = {
user: User;
@@ -85,7 +89,11 @@ export function UserSuspendDialog({ user, onSubmit }: Props) {
};
return (
<ConfirmationDialog onSubmit={handleSubmit} savingText={`${t("Saving")}`}>
<ConfirmationDialog
onSubmit={handleSubmit}
savingText={`${t("Saving")}`}
danger
>
{t(
"Are you sure you want to suspend {{ userName }}? Suspended users will be prevented from logging in.",
{
@@ -123,6 +131,68 @@ export function UserChangeNameDialog({ user, onSubmit }: Props) {
onChange={handleChange}
error={!name ? t("Name can't be empty") : undefined}
value={name}
autoSelect
required
flex
/>
</ConfirmationDialog>
);
}
export function UserChangeEmailDialog({ user, onSubmit }: Props) {
const { t } = useTranslation();
const actor = useCurrentUser();
const [email, setEmail] = React.useState<string>(user.email);
const [error, setError] = React.useState<string | undefined>();
const handleSubmit = async () => {
try {
await client.post(`/users.updateEmail`, { id: user.id, email });
onSubmit();
toast.info(
actor.id === user.id
? t("Check your email to verify the new address.")
: t("The email will be changed once verified.")
);
return true;
} catch (err) {
setError(err.message);
return false;
}
};
const handleChange = (ev: React.ChangeEvent<HTMLInputElement>) => {
setEmail(ev.target.value);
};
return (
<ConfirmationDialog
onSubmit={handleSubmit}
submitText={t("Save")}
savingText={`${t("Saving")}`}
disabled={!email || email === user.email}
>
<Text as="p">
{actor.id === user.id ? (
<Trans>
You will receive an email to verify your new address. It must be
unique in the workspace.
</Trans>
) : (
<Trans>
A confirmation email will be sent to the new address before it is
changed.
</Trans>
)}
</Text>
<Input
type="email"
name="email"
label={t("New email")}
onChange={handleChange}
error={!email ? t("Email can't be empty") : error}
value={email}
autoSelect
required
flex
/>
-7
View File
@@ -529,13 +529,6 @@ class WebsocketProvider extends React.Component<Props> {
stars.remove(event.modelId);
});
this.socket.on(
"user.typing",
(event: { userId: string; documentId: string; commentId: string }) => {
comments.setTyping(event);
}
);
this.socket.on("collections.add_user", async (event: Membership) => {
memberships.add(event);
await collections.fetch(event.collectionId, {
+48 -21
View File
@@ -10,6 +10,7 @@ import { useTranslation } from "react-i18next";
import { usePopoverState } from "reakit/Popover";
import styled, { useTheme } from "styled-components";
import { depths, s } from "@shared/styles";
import { altDisplay, isModKey, metaDisplay } from "@shared/utils/keyboard";
import Button from "~/components/Button";
import Flex from "~/components/Flex";
import Input from "~/components/Input";
@@ -21,14 +22,21 @@ import Tooltip from "~/components/Tooltip";
import useKeyDown from "~/hooks/useKeyDown";
import useOnClickOutside from "~/hooks/useOnClickOutside";
import Desktop from "~/utils/Desktop";
import { altDisplay, isModKey, metaDisplay } from "~/utils/keyboard";
import { useEditor } from "./EditorContext";
type Props = {
/** Whether the find and replace popover is open */
open: boolean;
/** Callback when the find and replace popover is opened */
onOpen: () => void;
/** Callback when the find and replace popover is closed */
onClose: () => void;
/** Whether the editor is in read-only mode */
readOnly?: boolean;
/** The current highlighted index in the search results */
currentIndex: number;
/** The total number of search results */
totalResults: number;
};
export default function FindAndReplace({
@@ -36,6 +44,8 @@ export default function FindAndReplace({
open,
onOpen,
onClose,
currentIndex,
totalResults,
}: Props) {
const editor = useEditor();
const finalFocusRef = React.useRef<HTMLElement>(
@@ -270,25 +280,26 @@ export default function FindAndReplace({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [popover.visible]);
const disabled = totalResults === 0;
const navigation = (
<>
<Tooltip
content={t("Previous match")}
shortcut="shift+enter"
delay={500}
placement="bottom"
>
<ButtonLarge onClick={() => editor.commands.prevSearchMatch()}>
<ButtonLarge
disabled={disabled}
onClick={() => editor.commands.prevSearchMatch()}
>
<CaretUpIcon />
</ButtonLarge>
</Tooltip>
<Tooltip
content={t("Next match")}
shortcut="enter"
delay={500}
placement="bottom"
>
<ButtonLarge onClick={() => editor.commands.nextSearchMatch()}>
<Tooltip content={t("Next match")} shortcut="enter" placement="bottom">
<ButtonLarge
disabled={disabled}
onClick={() => editor.commands.nextSearchMatch()}
>
<CaretDownIcon />
</ButtonLarge>
</Tooltip>
@@ -303,10 +314,11 @@ export default function FindAndReplace({
style={style}
aria-label={t("Find and replace")}
scrollable={false}
width={420}
minWidth={420}
width={0}
>
<Content column>
<Flex gap={8}>
<Flex gap={4}>
<StyledInput
ref={inputRef}
maxLength={255}
@@ -319,7 +331,6 @@ export default function FindAndReplace({
<Tooltip
content={t("Match case")}
shortcut={`${altDisplay}+${metaDisplay}+c`}
delay={500}
placement="bottom"
>
<ButtonSmall onClick={handleCaseSensitive}>
@@ -331,7 +342,6 @@ export default function FindAndReplace({
<Tooltip
content={t("Enable regex")}
shortcut={`${altDisplay}+${metaDisplay}+r`}
delay={500}
placement="bottom"
>
<ButtonSmall onClick={handleRegex}>
@@ -344,16 +354,15 @@ export default function FindAndReplace({
</StyledInput>
{navigation}
{!readOnly && (
<Tooltip
content={t("Replace options")}
delay={500}
placement="bottom"
>
<Tooltip content={t("Replace options")} placement="bottom">
<ButtonLarge onClick={handleMore}>
<ReplaceIcon color={theme.textSecondary} />
</ButtonLarge>
</Tooltip>
)}
<Results>
{totalResults > 0 ? currentIndex + 1 : 0} / {totalResults}
</Results>
</Flex>
<ResizingHeightContainer>
{showReplace && !readOnly && (
@@ -367,10 +376,10 @@ export default function FindAndReplace({
onRequestSubmit={handleReplaceAll}
onChange={(ev) => setReplaceTerm(ev.currentTarget.value)}
/>
<Button onClick={handleReplace} neutral>
<Button onClick={handleReplace} disabled={disabled} neutral>
{t("Replace")}
</Button>
<Button onClick={handleReplaceAll} neutral>
<Button onClick={handleReplaceAll} disabled={disabled} neutral>
{t("Replace all")}
</Button>
</Flex>
@@ -396,6 +405,12 @@ const ButtonSmall = styled(NudeButton)`
&[aria-expanded="true"] {
background: ${s("sidebarControlHoverBackground")};
}
&:disabled {
color: ${s("textTertiary")};
background: none;
cursor: default;
}
`;
const ButtonLarge = styled(ButtonSmall)`
@@ -408,3 +423,15 @@ const Content = styled(Flex)`
margin-bottom: -16px;
position: static;
`;
const Results = styled.span`
color: ${s("textSecondary")};
font-size: 12px;
font-weight: 500;
font-variant-numeric: tabular-nums;
line-height: 32px;
min-width: 32px;
letter-spacing: -0.5px;
text-align: right;
user-select: none;
`;
+6 -4
View File
@@ -131,13 +131,15 @@ function usePosition({
// Images need their own positioning to get the toolbar in the center
if (isImageSelection) {
const element = view.nodeDOM(selection.from) as HTMLElement;
const element = view.nodeDOM(selection.from);
// Images are wrapped which impacts positioning - need to get the element
// specifically tagged as the handle
const imageElement = element.getElementsByClassName(
EditorStyleHelper.imageHandle
)[0];
const imageElement = element
? (element as HTMLElement).getElementsByClassName(
EditorStyleHelper.imageHandle
)[0]
: undefined;
if (imageElement) {
const { left, top, width } = imageElement.getBoundingClientRect();
+2 -3
View File
@@ -13,10 +13,10 @@ import Flex from "~/components/Flex";
import useRequest from "~/hooks/useRequest";
import useStores from "~/hooks/useStores";
import { client } from "~/utils/ApiClient";
import MentionMenuItem from "./MentionMenuItem";
import SuggestionsMenu, {
Props as SuggestionsMenuProps,
} from "./SuggestionsMenu";
import SuggestionsMenuItem from "./SuggestionsMenuItem";
interface MentionItem extends MenuItem {
name: string;
@@ -122,11 +122,10 @@ function MentionMenu({ search, isActive, ...rest }: Props) {
search={search}
onSelect={handleSelect}
renderMenuItem={(item, _index, options) => (
<MentionMenuItem
<SuggestionsMenuItem
onClick={options.onClick}
selected={options.selected}
title={item.title}
label={item.attrs.label}
icon={
<Flex
align="center"
-18
View File
@@ -1,18 +0,0 @@
import * as React from "react";
import SuggestionsMenuItem, {
Props as SuggestionsMenuItemProps,
} from "./SuggestionsMenuItem";
type MentionMenuItemProps = Omit<
SuggestionsMenuItemProps,
"shortcut" | "theme"
> & {
label: string;
};
export default function MentionMenuItem({
label,
...rest
}: MentionMenuItemProps) {
return <SuggestionsMenuItem {...rest} title={label} />;
}
@@ -15,6 +15,8 @@ export type Props = {
icon?: React.ReactElement;
/** The title of the item */
title: React.ReactNode;
/** An optional subtitle for the item */
subtitle?: React.ReactNode;
/** A string representing the keyboard shortcut for the item */
shortcut?: string;
};
@@ -24,6 +26,7 @@ function SuggestionsMenuItem({
disabled,
onClick,
title,
subtitle,
shortcut,
icon,
}: Props) {
@@ -53,11 +56,17 @@ function SuggestionsMenuItem({
icon={icon}
>
{title}
{subtitle && <Subtitle $active={selected}>{subtitle}</Subtitle>}
{shortcut && <Shortcut $active={selected}>{shortcut}</Shortcut>}
</MenuItem>
);
}
const Subtitle = styled.span<{ $active?: boolean }>`
color: ${(props) =>
props.$active ? props.theme.white50 : props.theme.textTertiary};
`;
const Shortcut = styled.span<{ $active?: boolean }>`
color: ${(props) =>
props.$active ? props.theme.white50 : props.theme.textTertiary};
+36 -29
View File
@@ -1,3 +1,4 @@
import { TippyProps } from "@tippyjs/react";
import * as React from "react";
import { useMenuState } from "reakit";
import { MenuButton } from "reakit/Menu";
@@ -7,6 +8,7 @@ import { MenuItem } from "@shared/editor/types";
import { s } from "@shared/styles";
import ContextMenu from "~/components/ContextMenu";
import Template from "~/components/ContextMenu/Template";
import { TooltipProvider } from "~/components/TooltipContext";
import { MenuItem as TMenuItem } from "~/types";
import { useEditor } from "./EditorContext";
import ToolbarButton from "./ToolbarButton";
@@ -75,6 +77,8 @@ function ToolbarDropdown(props: { active: boolean; item: MenuItem }) {
);
}
const tippyProps = { placement: "top" } as TippyProps;
function ToolbarMenu(props: Props) {
const { commands, view } = useEditor();
const { items } = props;
@@ -91,36 +95,39 @@ function ToolbarMenu(props: Props) {
};
return (
<FlexibleWrapper>
{items.map((item, index) => {
if (item.name === "separator" && item.visible !== false) {
return <ToolbarSeparator key={index} />;
}
if (item.visible === false || !item.icon) {
return null;
}
const isActive = item.active ? item.active(state) : false;
<TooltipProvider tippyProps={tippyProps}>
<FlexibleWrapper>
{items.map((item, index) => {
if (item.name === "separator" && item.visible !== false) {
return <ToolbarSeparator key={index} />;
}
if (item.visible === false || !item.icon) {
return null;
}
const isActive = item.active ? item.active(state) : false;
return (
<Tooltip
content={item.label === item.tooltip ? undefined : item.tooltip}
key={index}
>
{item.children ? (
<ToolbarDropdown active={isActive && !item.label} item={item} />
) : (
<ToolbarButton
onClick={handleClick(item)}
active={isActive && !item.label}
>
{item.label && <Label>{item.label}</Label>}
{item.icon}
</ToolbarButton>
)}
</Tooltip>
);
})}
</FlexibleWrapper>
return (
<Tooltip
key={index}
shortcut={item.shortcut}
content={item.label === item.tooltip ? undefined : item.tooltip}
>
{item.children ? (
<ToolbarDropdown active={isActive && !item.label} item={item} />
) : (
<ToolbarButton
onClick={handleClick(item)}
active={isActive && !item.label}
>
{item.label && <Label>{item.label}</Label>}
{item.icon}
</ToolbarButton>
)}
</Tooltip>
);
})}
</FlexibleWrapper>
</TooltipProvider>
);
}
+14 -9
View File
@@ -1,15 +1,20 @@
import * as React from "react";
import styled from "styled-components";
import Tooltip from "~/components/Tooltip";
import Tooltip, { Props } from "~/components/Tooltip";
type Props = {
/** The content to display in the tooltip. */
content?: string;
children?: React.ReactNode;
};
const WrappedTooltip: React.FC<Props> = ({ children, content }: Props) => (
<Tooltip offset={[0, 16]} delay={150} content={content} placement="top">
const WrappedTooltip: React.FC<Props> = ({
children,
content,
...rest
}: Props) => (
<Tooltip
offset={[0, 16]}
delay={150}
content={content}
placement="top"
shortcutOnNewline
{...rest}
>
<TooltipContent>{children}</TooltipContent>
</Tooltip>
);
+4 -4
View File
@@ -1,6 +1,7 @@
import { action } from "mobx";
import * as React from "react";
import { WidgetProps } from "@shared/editor/lib/Extension";
import { isBrowser } from "@shared/utils/browser";
import Suggestion from "~/editor/extensions/Suggestion";
import EmojiMenu from "../components/EmojiMenu";
@@ -13,10 +14,9 @@ const languagesUsingColon = ["fr"];
export default class EmojiMenuExtension extends Suggestion {
get defaultOptions() {
const languageIsUsingColon =
typeof window === "undefined"
? false
: languagesUsingColon.includes(window.navigator.language.slice(0, 2));
const languageIsUsingColon = isBrowser
? languagesUsingColon.includes(window.navigator.language.slice(0, 2))
: false;
return {
openRegex: new RegExp(
+26 -6
View File
@@ -92,6 +92,10 @@ export default class FindAndReplaceExtension extends Extension {
public replace(replace: string): Command {
return (state, dispatch) => {
// Redo the search to ensure we have the latest results, the document may
// have changed underneath us since the last search.
this.search(state.doc);
const result = this.results[this.currentResultIndex];
if (!result) {
@@ -106,7 +110,12 @@ export default class FindAndReplaceExtension extends Extension {
}
public replaceAll(replace: string): Command {
return ({ tr }, dispatch) => {
return (state, dispatch) => {
// Redo the search to ensure we have the latest results, the document may
// have changed underneath us since the last search.
this.search(state.doc);
const tr = state.tr;
let offset: number | undefined;
if (!this.results.length) {
@@ -256,12 +265,17 @@ export default class FindAndReplaceExtension extends Extension {
}
// Reconstruct the correct match position
const i = m.index > text.length ? m.index - text.length : m.index;
const i = m.index >= text.length ? m.index - text.length : m.index;
const from = pos + i;
const to = from + m[0].length;
this.results.push({
from: pos + i,
to: pos + i + m[0].length,
});
// Check if already exists in results, possible due to duplicated
// search string on L257
if (this.results.some((r) => r.from === from && r.to === to)) {
continue;
}
this.results.push({ from, to });
}
} catch (e) {
// Invalid RegExp
@@ -318,6 +332,8 @@ export default class FindAndReplaceExtension extends Extension {
public widget = ({ readOnly }: WidgetProps) => (
<FindAndReplace
currentIndex={this.currentResultIndex}
totalResults={this.results.length}
readOnly={readOnly}
open={this.open}
onOpen={() => {
@@ -332,7 +348,11 @@ export default class FindAndReplaceExtension extends Extension {
@observable
private open = false;
@observable
private results: { from: number; to: number }[] = [];
@observable
private currentResultIndex = 0;
private searchTerm = "";
}
+7 -6
View File
@@ -1,5 +1,4 @@
import isEqual from "lodash/isEqual";
import { keymap } from "prosemirror-keymap";
import {
ySyncPlugin,
yCursorPlugin,
@@ -104,11 +103,13 @@ export default class Multiplayer extends Extension {
selectionBuilder,
}),
yUndoPlugin(),
keymap({
"Mod-z": undo,
"Mod-y": redo,
"Mod-Shift-z": redo,
}),
];
}
commands() {
return {
undo: () => undo,
redo: () => redo,
};
}
}
-15
View File
@@ -4,7 +4,6 @@ import { darken, transparentize } from "polished";
import { baseKeymap } from "prosemirror-commands";
import { dropCursor } from "prosemirror-dropcursor";
import { gapCursor } from "prosemirror-gapcursor";
import { redo, undo } from "prosemirror-history";
import { inputRules, InputRule } from "prosemirror-inputrules";
import { keymap } from "prosemirror-keymap";
import { MarkdownParser } from "prosemirror-markdown";
@@ -608,20 +607,6 @@ export class Editor extends React.PureComponent<
this.props
);
/**
* Undo the last change in the editor.
*
* @returns True if the undo was successful
*/
public undo = () => undo(this.view.state, this.view.dispatch, this.view);
/**
* Redo the last change in the editor.
*
* @returns True if the change was successful
*/
public redo = () => redo(this.view.state, this.view.dispatch, this.view);
/**
* Returns true if the trimmed content of the editor is an empty string.
*
+1 -1
View File
@@ -26,8 +26,8 @@ import * as React from "react";
import styled from "styled-components";
import Image from "@shared/editor/components/Img";
import { MenuItem } from "@shared/editor/types";
import { metaDisplay } from "@shared/utils/keyboard";
import { Dictionary } from "~/hooks/useDictionary";
import { metaDisplay } from "~/utils/keyboard";
const Img = styled(Image)`
border-radius: 2px;
+25 -1
View File
@@ -28,6 +28,7 @@ import { isInList } from "@shared/editor/queries/isInList";
import { isMarkActive } from "@shared/editor/queries/isMarkActive";
import { isNodeActive } from "@shared/editor/queries/isNodeActive";
import { MenuItem } from "@shared/editor/types";
import { metaDisplay } from "@shared/utils/keyboard";
import CircleIcon from "~/components/Icons/CircleIcon";
import { Dictionary } from "~/hooks/useDictionary";
@@ -63,6 +64,7 @@ export default function formattingMenuItems(
{
name: "strong",
tooltip: dictionary.strong,
shortcut: `${metaDisplay}+B`,
icon: <BoldIcon />,
active: isMarkActive(schema.marks.strong),
visible: !isCode && (!isMobile || !isEmpty),
@@ -70,6 +72,7 @@ export default function formattingMenuItems(
{
name: "em",
tooltip: dictionary.em,
shortcut: `${metaDisplay}+I`,
icon: <ItalicIcon />,
active: isMarkActive(schema.marks.em),
visible: !isCode && (!isMobile || !isEmpty),
@@ -77,12 +80,14 @@ export default function formattingMenuItems(
{
name: "strikethrough",
tooltip: dictionary.strikethrough,
shortcut: `${metaDisplay}+D`,
icon: <StrikethroughIcon />,
active: isMarkActive(schema.marks.strikethrough),
visible: !isCode && (!isMobile || !isEmpty),
},
{
tooltip: dictionary.mark,
shortcut: `${metaDisplay}+Ctrl+H`,
icon: highlight ? (
<CircleIcon color={highlight.mark.attrs.color || Highlight.colors[0]} />
) : (
@@ -114,6 +119,7 @@ export default function formattingMenuItems(
{
name: "code_inline",
tooltip: dictionary.codeInline,
shortcut: `${metaDisplay}+E`,
icon: <CodeIcon />,
active: isMarkActive(schema.marks.code_inline),
visible: !isCodeBlock && (!isMobile || !isEmpty),
@@ -125,6 +131,7 @@ export default function formattingMenuItems(
{
name: "heading",
tooltip: dictionary.heading,
shortcut: `⇧+Ctrl+1`,
icon: <Heading1Icon />,
active: isNodeActive(schema.nodes.heading, { level: 1 }),
attrs: { level: 1 },
@@ -133,6 +140,7 @@ export default function formattingMenuItems(
{
name: "heading",
tooltip: dictionary.subheading,
shortcut: `⇧+Ctrl+2`,
icon: <Heading2Icon />,
active: isNodeActive(schema.nodes.heading, { level: 2 }),
attrs: { level: 2 },
@@ -141,6 +149,7 @@ export default function formattingMenuItems(
{
name: "heading",
tooltip: dictionary.subheading,
shortcut: `⇧+Ctrl+3`,
icon: <Heading3Icon />,
active: isNodeActive(schema.nodes.heading, { level: 3 }),
attrs: { level: 3 },
@@ -149,6 +158,7 @@ export default function formattingMenuItems(
{
name: "blockquote",
tooltip: dictionary.quote,
shortcut: `${metaDisplay}+]`,
icon: <BlockQuoteIcon />,
active: isNodeActive(schema.nodes.blockquote),
attrs: { level: 2 },
@@ -161,6 +171,7 @@ export default function formattingMenuItems(
{
name: "checkbox_list",
tooltip: dictionary.checkboxList,
shortcut: `⇧+Ctrl+7`,
icon: <TodoListIcon />,
keywords: "checklist checkbox task",
active: isNodeActive(schema.nodes.checkbox_list),
@@ -169,6 +180,7 @@ export default function formattingMenuItems(
{
name: "bullet_list",
tooltip: dictionary.bulletList,
shortcut: `⇧+Ctrl+8`,
icon: <BulletedListIcon />,
active: isNodeActive(schema.nodes.bullet_list),
visible: !isCodeBlock && (!isMobile || isEmpty),
@@ -176,6 +188,7 @@ export default function formattingMenuItems(
{
name: "ordered_list",
tooltip: dictionary.orderedList,
shortcut: `⇧+Ctrl+9`,
icon: <OrderedListIcon />,
active: isNodeActive(schema.nodes.ordered_list),
visible: !isCodeBlock && (!isMobile || isEmpty),
@@ -183,6 +196,7 @@ export default function formattingMenuItems(
{
name: "outdentList",
tooltip: dictionary.outdent,
shortcut: `⇧+Tab`,
icon: <OutdentIcon />,
visible:
isMobile && isInList(state, { types: ["ordered_list", "bullet_list"] }),
@@ -190,6 +204,7 @@ export default function formattingMenuItems(
{
name: "indentList",
tooltip: dictionary.indent,
shortcut: `Tab`,
icon: <IndentIcon />,
visible:
isMobile && isInList(state, { types: ["ordered_list", "bullet_list"] }),
@@ -197,12 +212,14 @@ export default function formattingMenuItems(
{
name: "outdentCheckboxList",
tooltip: dictionary.outdent,
shortcut: `⇧+Tab`,
icon: <OutdentIcon />,
visible: isMobile && isInList(state, { types: ["checkbox_list"] }),
},
{
name: "indentCheckboxList",
tooltip: dictionary.indent,
shortcut: `Tab`,
icon: <IndentIcon />,
visible: isMobile && isInList(state, { types: ["checkbox_list"] }),
},
@@ -213,6 +230,7 @@ export default function formattingMenuItems(
{
name: "link",
tooltip: dictionary.createLink,
shortcut: `${metaDisplay}+K`,
icon: <LinkIcon />,
attrs: { href: "" },
visible: !isCodeBlock && (!isMobile || !isEmpty),
@@ -220,9 +238,14 @@ export default function formattingMenuItems(
{
name: "comment",
tooltip: dictionary.comment,
shortcut: `${metaDisplay}+⌥+M`,
icon: <CommentIcon />,
label: isCodeBlock ? dictionary.comment : undefined,
active: isMarkActive(schema.marks.comment, { resolved: false }),
active: isMarkActive(
schema.marks.comment,
{ resolved: false },
{ exact: true }
),
visible: !isMobile || !isEmpty,
},
{
@@ -233,6 +256,7 @@ export default function formattingMenuItems(
name: "copyToClipboard",
icon: <CopyIcon />,
tooltip: dictionary.copy,
shortcut: `${metaDisplay}+C`,
visible: isCode && !isCodeBlock && (!isMobile || !isEmpty),
},
];
+1 -1
View File
@@ -31,7 +31,7 @@ export default function useAutoRefresh() {
return;
}
if (isReloaded) {
Logger.error("lifecycle", new Error("Attempted to reload twice"));
Logger.warn("Attempted to reload twice");
return;
}
+5 -4
View File
@@ -6,10 +6,7 @@ export default function useComponentSize(
width: number;
height: number;
} {
const [size, setSize] = useState({
width: ref.current?.clientWidth || 0,
height: ref.current?.clientHeight || 0,
});
const [size, setSize] = useState({ width: 0, height: 0 });
useLayoutEffect(() => {
const sizeObserver = new ResizeObserver((entries) => {
@@ -24,6 +21,10 @@ export default function useComponentSize(
});
if (ref.current) {
setSize({
width: ref.current?.clientWidth,
height: ref.current?.clientHeight,
});
sizeObserver.observe(ref.current);
}
+2 -2
View File
@@ -1,7 +1,7 @@
import * as React from "react";
import { useHistory } from "react-router-dom";
import { isModKey } from "@shared/utils/keyboard";
import { isInternalUrl } from "@shared/utils/urls";
import { isModKey } from "~/utils/keyboard";
import { sharedDocumentPath } from "~/utils/routeHelpers";
import { isHash } from "~/utils/urls";
@@ -56,7 +56,7 @@ export default function useEditorClickHandlers({ shareId }: Params) {
}
if (!isModKey(event) && !event.shiftKey) {
history.push(navigateTo);
history.push(navigateTo, { sidebarContext: "collections" }); // optimistic preference of "collections"
} else {
window.open(navigateTo, "_blank");
}
+9 -5
View File
@@ -45,12 +45,16 @@ export default function useImportDocument(
}
for (const file of files) {
const doc = await documents.import(file, documentId, cId, {
publish: true,
});
try {
const doc = await documents.import(file, documentId, cId, {
publish: true,
});
if (redirect) {
history.push(documentPath(doc));
if (redirect) {
history.push(documentPath(doc));
}
} catch (err) {
toast.error(err.message);
}
}
} catch (err) {
+1 -1
View File
@@ -1,6 +1,6 @@
import * as React from "react";
import { isModKey } from "@shared/utils/keyboard";
import isTextInput from "~/utils/isTextInput";
import { isModKey } from "~/utils/keyboard";
type Callback = (event: KeyboardEvent) => void;
@@ -1,10 +1,10 @@
import { useLocation } from "react-router-dom";
import { SidebarContextType } from "../components/SidebarContext";
import { SidebarContextType } from "../components/Sidebar/components/SidebarContext";
/**
* Hook to retrieve the sidebar context from the current location state.
*/
export function useLocationState() {
export function useLocationSidebarContext() {
const location = useLocation<{
sidebarContext?: SidebarContextType;
}>();
+2 -1
View File
@@ -1,6 +1,7 @@
import * as React from "react";
import { Primitive } from "utility-types";
import Storage from "@shared/utils/Storage";
import { isBrowser } from "@shared/utils/browser";
import Logger from "~/utils/Logger";
import useEventListener from "./useEventListener";
@@ -41,7 +42,7 @@ export default function usePersistedState<T extends Primitive | object>(
options?: Options
): [T, (value: T) => void] {
const [storedValue, setStoredValue] = React.useState(() => {
if (typeof window === "undefined") {
if (!isBrowser) {
return defaultValue;
}
return Storage.get(key) ?? defaultValue;
+13 -4
View File
@@ -29,6 +29,7 @@ import useCurrentUser from "./useCurrentUser";
import usePolicy from "./usePolicy";
const ApiKeys = lazy(() => import("~/scenes/Settings/ApiKeys"));
const PersonalApiKeys = lazy(() => import("~/scenes/Settings/PersonalApiKeys"));
const Details = lazy(() => import("~/scenes/Settings/Details"));
const Export = lazy(() => import("~/scenes/Settings/Export"));
const Features = lazy(() => import("~/scenes/Settings/Features"));
@@ -87,10 +88,10 @@ const useSettingsConfig = () => {
icon: EmailIcon,
},
{
name: t("API"),
path: settingsPath("tokens"),
component: ApiKeys,
enabled: can.createApiKey,
name: t("API Keys"),
path: settingsPath("personal-api-keys"),
component: PersonalApiKeys,
enabled: can.createApiKey && !can.listApiKeys,
group: t("Account"),
icon: CodeIcon,
},
@@ -143,6 +144,14 @@ const useSettingsConfig = () => {
group: t("Workspace"),
icon: ShapesIcon,
},
{
name: t("API Keys"),
path: settingsPath("api-keys"),
component: ApiKeys,
enabled: can.listApiKeys,
group: t("Workspace"),
icon: CodeIcon,
},
{
name: t("Shared Links"),
path: settingsPath("shares"),
-1
View File
@@ -123,7 +123,6 @@ function CollectionMenu({
history.push(document.url);
} catch (err) {
toast.error(err.message);
throw err;
} finally {
ev.target.value = "";
}
+3 -3
View File
@@ -136,14 +136,14 @@ type MenuContentProps = {
showToggleEmbeds?: boolean;
};
const MenuContent: React.FC<MenuContentProps> = ({
const MenuContent: React.FC<MenuContentProps> = observer(function MenuContent_({
onOpen,
onClose,
onFindAndReplace,
onRename,
showDisplayOptions,
showToggleEmbeds,
}) => {
}) {
const user = useCurrentUser();
const { model: document, menuState } = useMenuContext<Document>();
const can = usePolicy(document);
@@ -348,7 +348,7 @@ const MenuContent: React.FC<MenuContentProps> = ({
)}
</ContextMenu>
) : null;
};
});
function DocumentMenu({
document,
+1 -6
View File
@@ -19,12 +19,7 @@ function NewDocumentMenu() {
}
return (
<Tooltip
content={t("New document")}
shortcut="n"
delay={500}
placement="bottom"
>
<Tooltip content={t("New document")} shortcut="n" placement="bottom">
<Button as={Link} to={newDocumentPath()} icon={<PlusIcon />}>
{t("New doc")}
</Button>
+15 -6
View File
@@ -1,8 +1,9 @@
import { observer } from "mobx-react";
import { DocumentIcon } from "outline-icons";
import { DocumentIcon, ShapesIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { MenuButton, useMenuState } from "reakit/Menu";
import { TextHelper } from "@shared/utils/TextHelper";
import Document from "~/models/Document";
import Button from "~/components/Button";
import ContextMenu from "~/components/ContextMenu";
@@ -11,14 +12,17 @@ import Icon from "~/components/Icon";
import useCurrentUser from "~/hooks/useCurrentUser";
import useStores from "~/hooks/useStores";
import { MenuItem } from "~/types";
import { replaceTitleVariables } from "~/utils/date";
type Props = {
/** The document to which the templates will be applied */
document: Document;
/** Whether to render the button as a compact icon */
isCompact?: boolean;
/** Callback to handle when a template is selected */
onSelectTemplate: (template: Document) => void;
};
function TemplatesMenu({ onSelectTemplate, document }: Props) {
function TemplatesMenu({ isCompact, onSelectTemplate, document }: Props) {
const menu = useMenuState({
modal: true,
});
@@ -29,7 +33,7 @@ function TemplatesMenu({ onSelectTemplate, document }: Props) {
const templateToMenuItem = React.useCallback(
(tmpl: Document): MenuItem => ({
type: "button",
title: replaceTitleVariables(tmpl.titleWithDefault, user),
title: TextHelper.replaceTemplateVariables(tmpl.titleWithDefault, user),
icon: tmpl.icon ? (
<Icon value={tmpl.icon} color={tmpl.color ?? undefined} />
) : (
@@ -79,8 +83,13 @@ function TemplatesMenu({ onSelectTemplate, document }: Props) {
<>
<MenuButton {...menu}>
{(props) => (
<Button {...props} disclosure neutral>
{t("Templates")}
<Button
{...props}
icon={isCompact ? <ShapesIcon /> : undefined}
disclosure={!isCompact}
neutral
>
{isCompact ? undefined : t("Templates")}
</Button>
)}
</MenuButton>
+25 -1
View File
@@ -11,6 +11,7 @@ import Template from "~/components/ContextMenu/Template";
import {
UserSuspendDialog,
UserChangeNameDialog,
UserChangeEmailDialog,
} from "~/components/UserDialogs";
import { actionToMenuItem } from "~/actions";
import {
@@ -49,6 +50,22 @@ function UserMenu({ user }: Props) {
[dialogs, t, user]
);
const handleChangeEmail = React.useCallback(
(ev: React.SyntheticEvent) => {
ev.preventDefault();
dialogs.openModal({
title: t("Change email"),
content: (
<UserChangeEmailDialog
user={user}
onSubmit={dialogs.closeAllModals}
/>
),
});
},
[dialogs, t, user]
);
const handleSuspend = React.useCallback(
(ev: React.SyntheticEvent) => {
ev.preventDefault();
@@ -117,7 +134,13 @@ function UserMenu({ user }: Props) {
type: "button",
title: `${t("Change name")}`,
onClick: handleChangeName,
visible: can.update && user.role !== "admin",
visible: can.update,
},
{
type: "button",
title: `${t("Change email")}`,
onClick: handleChangeEmail,
visible: can.update,
},
{
type: "button",
@@ -144,6 +167,7 @@ function UserMenu({ user }: Props) {
{
type: "button",
title: `${t("Suspend user")}`,
dangerous: true,
onClick: handleSuspend,
visible: !user.isInvited && !user.isSuspended,
},
-24
View File
@@ -1,8 +1,6 @@
import { subSeconds } from "date-fns";
import invariant from "invariant";
import uniq from "lodash/uniq";
import { action, computed, observable } from "mobx";
import { now } from "mobx-utils";
import { Pagination } from "@shared/constants";
import type { ProsemirrorData, ReactionSummary } from "@shared/types";
import User from "~/models/User";
@@ -15,17 +13,6 @@ import Relation from "./decorators/Relation";
class Comment extends Model {
static modelName = "Comment";
/**
* Map to keep track of which users are currently typing a reply in this
* comments thread.
*/
@observable
typingUsers: Map<string, Date> = new Map();
@Field
@observable
id: string;
/**
* The Prosemirror data representing the comment content
*/
@@ -107,17 +94,6 @@ class Comment extends Model {
*/
private reactedUsersLoading = false;
/**
* An array of users that are currently typing a reply in this comments thread.
*/
@computed
public get currentlyTypingUsers(): User[] {
return Array.from(this.typingUsers.entries())
.filter(([, lastReceivedDate]) => lastReceivedDate > subSeconds(now(), 3))
.map(([userId]) => this.store.rootStore.users.get(userId))
.filter(Boolean) as User[];
}
/**
* Whether the comment is resolved
*/
+2 -4
View File
@@ -65,10 +65,6 @@ export default class Document extends ArchivableModel {
store: DocumentsStore;
@Field
@observable
id: string;
@observable.shallow
data: ProsemirrorData;
@@ -577,6 +573,8 @@ export default class Document extends ArchivableModel {
title?: string;
publish?: boolean;
recursive?: boolean;
collectionId?: string | null;
parentDocumentId?: string;
}) => this.store.duplicate(this, options);
/**
+3 -4
View File
@@ -6,14 +6,13 @@ import Field from "./decorators/Field";
class Group extends Model {
static modelName = "Group";
@Field
@observable
id: string;
@Field
@observable
name: string;
@observable
externalId: string | undefined;
@observable
memberCount: number;
+3 -4
View File
@@ -18,10 +18,6 @@ import Relation from "./decorators/Relation";
class Notification extends Model {
static modelName = "Notification";
@Field
@observable
id: string;
/**
* The date the notification was marked as read.
*/
@@ -123,6 +119,8 @@ class Notification extends Model {
return t("mentioned you in");
case NotificationEventType.CreateComment:
return t("left a comment on");
case NotificationEventType.ResolveComment:
return t("resolved a comment on");
case NotificationEventType.AddUserToDocument:
return t("shared");
case NotificationEventType.AddUserToCollection:
@@ -174,6 +172,7 @@ class Notification extends Model {
return this.document?.path;
}
case NotificationEventType.MentionedInComment:
case NotificationEventType.ResolveComment:
case NotificationEventType.CreateComment: {
return this.document && this.comment
? commentPath(this.document, this.comment)
-4
View File
@@ -8,10 +8,6 @@ import Field from "./decorators/Field";
class Team extends Model {
static modelName = "Team";
@Field
@observable
id: string;
@Field
@observable
name: string;
-4
View File
@@ -22,10 +22,6 @@ import Field from "./decorators/Field";
class User extends ParanoidModel {
static modelName = "User";
@Field
@observable
id: string;
@Field
@observable
avatarUrl: string;
-4
View File
@@ -5,10 +5,6 @@ import Field from "./decorators/Field";
class WebhookSubscription extends Model {
static modelName = "WebhookSubscription";
@Field
@observable
id: string;
@Field
@observable
name: string;
@@ -27,7 +27,6 @@ function Actions({ collection }: Props) {
<Tooltip
content={t("New document")}
shortcut="n"
delay={500}
placement="bottom"
>
<Button
@@ -49,8 +49,6 @@ type Props = {
highlightedText?: string;
/** The text direction of the editor */
dir?: "rtl" | "ltr";
/** Callback when the user is typing in the editor */
onTyping?: () => void;
/** Callback when the editor is focused */
onFocus?: () => void;
/** Callback when the editor is blurred */
@@ -63,7 +61,6 @@ function CommentForm({
draft,
onSubmit,
onSaveDraft,
onTyping,
onFocus,
onBlur,
autoFocus,
@@ -182,7 +179,6 @@ function CommentForm({
) => {
const text = value(true, true);
onSaveDraft(text ? value(false, true) : undefined);
onTyping?.();
};
const handleSave = () => {
@@ -320,7 +316,7 @@ function CommentForm({
{t("Cancel")}
</ButtonSmall>
</Flex>
<Tooltip delay={500} content={t("Upload image")} placement="top">
<Tooltip content={t("Upload image")} placement="top">
<NudeButton onClick={handleImageUpload}>
<ImageIcon color={theme.textTertiary} />
</NudeButton>
@@ -7,12 +7,14 @@ import { s } from "@shared/styles";
import { UserPreference } from "@shared/types";
import InputSelect from "~/components/InputSelect";
import useCurrentUser from "~/hooks/useCurrentUser";
import { useLocationSidebarContext } from "~/hooks/useLocationSidebarContext";
import useQuery from "~/hooks/useQuery";
import { CommentSortType } from "~/types";
const CommentSortMenu = () => {
const { t } = useTranslation();
const location = useLocation();
const sidebarContext = useLocationSidebarContext();
const history = useHistory();
const user = useCurrentUser();
const params = useQuery();
@@ -39,6 +41,7 @@ const CommentSortMenu = () => {
resolved: "",
}),
pathname: location.pathname,
state: { sidebarContext },
});
};
@@ -49,6 +52,7 @@ const CommentSortMenu = () => {
resolved: undefined,
}),
pathname: location.pathname,
state: { sidebarContext },
});
};
@@ -1,5 +1,5 @@
import throttle from "lodash/throttle";
import { observer } from "mobx-react";
import { darken } from "polished";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { useHistory, useLocation } from "react-router-dom";
@@ -10,14 +10,12 @@ import { s } from "@shared/styles";
import { ProsemirrorData } from "@shared/types";
import Comment from "~/models/Comment";
import Document from "~/models/Document";
import { Avatar } from "~/components/Avatar";
import { Avatar, AvatarSize } from "~/components/Avatar";
import { useDocumentContext } from "~/components/DocumentContext";
import Facepile from "~/components/Facepile";
import Fade from "~/components/Fade";
import Flex from "~/components/Flex";
import { ResizingHeightContainer } from "~/components/ResizingHeightContainer";
import Typing from "~/components/Typing";
import { WebsocketContext } from "~/components/WebsocketProvider";
import useCurrentUser from "~/hooks/useCurrentUser";
import { useLocationSidebarContext } from "~/hooks/useLocationSidebarContext";
import useOnClickOutside from "~/hooks/useOnClickOutside";
import usePersistedState from "~/hooks/usePersistedState";
import usePolicy from "~/hooks/usePolicy";
@@ -40,28 +38,12 @@ type Props = {
enableScroll: () => void;
/** Disable scroll for the comments container */
disableScroll: () => void;
/** Number of replies before collapsing */
collapseThreshold?: number;
/** Number of replies to display when collapsed */
collapseNumDisplayed?: number;
};
function useTypingIndicator({
document,
comment,
}: Pick<Props, "document" | "comment">): [undefined, () => void] {
const socket = React.useContext(WebsocketContext);
const setIsTyping = React.useMemo(
() =>
throttle(() => {
socket?.emit("typing", {
documentId: document.id,
commentId: comment.id,
});
}, 500),
[socket, document.id, comment.id]
);
return [undefined, setIsTyping];
}
function CommentThread({
comment: thread,
document,
@@ -69,21 +51,20 @@ function CommentThread({
focused,
enableScroll,
disableScroll,
collapseThreshold = 5,
collapseNumDisplayed = 3,
}: Props) {
const [focusedOnMount] = React.useState(focused);
const { editor } = useDocumentContext();
const { comments } = useStores();
const topRef = React.useRef<HTMLDivElement>(null);
const replyRef = React.useRef<HTMLDivElement>(null);
const user = useCurrentUser();
const { t } = useTranslation();
const history = useHistory();
const location = useLocation();
const sidebarContext = useLocationSidebarContext();
const [autoFocus, setAutoFocus] = React.useState(thread.isNew);
const [, setIsTyping] = useTypingIndicator({
document,
comment: thread,
});
const can = usePolicy(document);
const [draft, onSaveDraft] = usePersistedState<ProsemirrorData | undefined>(
@@ -102,6 +83,17 @@ function CommentThread({
.inThread(thread.id)
.filter((comment) => !comment.isNew);
const [collapse, setCollapse] = React.useState(() => {
const numReplies = commentsInThread.length - 1;
if (numReplies >= collapseThreshold) {
return {
begin: 1,
final: commentsInThread.length - collapseNumDisplayed - 1,
};
}
return null;
});
useOnClickOutside(topRef, (event) => {
if (
focused &&
@@ -111,7 +103,10 @@ function CommentThread({
history.replace({
search: location.search,
pathname: location.pathname,
state: { commentId: undefined },
state: {
commentId: undefined,
sidebarContext,
},
});
}
});
@@ -125,10 +120,43 @@ function CommentThread({
// Clear any commentId from the URL when explicitly focusing a thread
search: thread.isResolved ? "resolved=" : "",
pathname: location.pathname.replace(/\/history$/, ""),
state: { commentId: thread.id },
state: {
commentId: thread.id,
sidebarContext,
},
});
};
const handleClickExpand = (ev: React.SyntheticEvent) => {
ev.stopPropagation();
setCollapse(null);
};
const renderShowMore = (collapse: { begin: number; final: number }) => {
const count = collapse.final - collapse.begin + 1;
const createdBy = commentsInThread
.slice(collapse.begin, collapse.final + 1)
.map((c) => c.createdBy);
const users = Array.from(new Set(createdBy));
const limit = 3;
const overflow = users.length - limit;
return (
<ShowMore onClick={handleClickExpand} key="show-more">
{t("Show {{ count }} reply", { count })}
<Facepile
users={users}
limit={limit}
overflow={overflow}
size={AvatarSize.Medium}
renderAvatar={(item) => (
<Avatar size={AvatarSize.Medium} model={item} />
)}
/>
</ShowMore>
);
};
React.useEffect(() => {
if (!focused && autoFocus) {
setAutoFocus(false);
@@ -192,8 +220,17 @@ function CommentThread({
onClick={handleClickThread}
>
{commentsInThread.map((comment, index) => {
if (collapse !== null) {
if (index === collapse.begin) {
return renderShowMore(collapse);
} else if (index > collapse.begin && index <= collapse.final) {
return null;
}
}
const firstOfAuthor =
index === 0 ||
(collapse && index === collapse.final + 1) ||
comment.createdById !== commentsInThread[index - 1].createdById;
const lastOfAuthor =
index === commentsInThread.length - 1 ||
@@ -219,15 +256,6 @@ function CommentThread({
);
})}
{thread.currentlyTypingUsers
.filter((typing) => typing.id !== user.id)
.map((typing) => (
<Flex gap={8} key={typing.id}>
<Avatar model={typing} size={24} />
<Typing />
</Flex>
))}
<ResizingHeightContainer hideOverflow={false} ref={replyRef}>
{(focused || draft || commentsInThread.length === 0) && canReply && (
<Fade timing={100}>
@@ -237,7 +265,6 @@ function CommentThread({
draft={draft}
documentId={document.id}
thread={thread}
onTyping={setIsTyping}
standalone={commentsInThread.length === 0}
dir={document.dir}
autoFocus={autoFocus}
@@ -276,6 +303,29 @@ const Reply = styled.button`
`}
`;
const ShowMore = styled.div<{ $dir?: "rtl" | "ltr" }>`
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1px;
margin-left: ${(props) => (props.$dir === "rtl" ? 0 : 32)}px;
margin-right: ${(props) => (props.$dir !== "rtl" ? 0 : 32)}px;
padding: 8px 12px;
color: ${s("textTertiary")};
background: ${(props) => darken(0.015, props.theme.backgroundSecondary)};
cursor: var(--pointer);
font-size: 13px;
&: ${hover} {
color: ${s("textSecondary")};
background: ${s("backgroundTertiary")};
}
* {
border-color: ${(props) => darken(0.015, props.theme.backgroundSecondary)};
}
`;
const Thread = styled.div<{
$focused: boolean;
$recessed: boolean;
@@ -197,21 +197,12 @@ function CommentThreadItem({
{showAuthor && <em>{comment.createdBy.name}</em>}
{showAuthor && showTime && <> &middot; </>}
{showTime && (
<Time
dateTime={comment.createdAt}
tooltipDelay={500}
addSuffix
shorten
/>
<Time dateTime={comment.createdAt} addSuffix shorten />
)}
{showEdited && (
<>
{" "}
(
<Time dateTime={comment.updatedAt} tooltipDelay={500}>
{t("edited")}
</Time>
)
(<Time dateTime={comment.updatedAt}>{t("edited")}</Time>)
</>
)}
</Meta>
@@ -264,17 +255,19 @@ function CommentThreadItem({
<EventBoundary>
{!isEditing && (
<Actions gap={4} dir={dir}>
{firstOfThread && (
<ResolveButton onUpdate={handleUpdate} comment={comment} />
)}
{!comment.isResolved && (
<Action
as={ReactionPicker}
onSelect={handleAddReaction}
onOpen={disableScroll}
onClose={enableScroll}
rounded
/>
<>
{firstOfThread && (
<ResolveButton onUpdate={handleUpdate} comment={comment} />
)}
<Action
as={ReactionPicker}
onSelect={handleAddReaction}
onOpen={disableScroll}
onClose={enableScroll}
rounded
/>
</>
)}
<Action
as={CommentMenu}
@@ -302,12 +295,7 @@ const ResolveButton = ({
const { t } = useTranslation();
return (
<Tooltip
content={t("Mark as resolved")}
placement="top"
delay={500}
hideOnClick
>
<Tooltip content={t("Mark as resolved")} placement="top" hideOnClick>
<Action
as={NudeButton}
context={context}
@@ -419,7 +407,7 @@ export const Bubble = styled(Flex)<{
min-width: 2em;
margin-bottom: 1px;
padding: 8px 12px;
transition: color 100ms ease-out, ${s("backgroundTransition")};
transition: color 100ms ease-out, background 100ms ease-out;
${({ $lastOfThread, $canReply }) =>
$lastOfThread &&
+10 -6
View File
@@ -44,7 +44,7 @@ function Comments() {
const isAtBottom = React.useRef(true);
const [showJumpToRecentBtn, setShowJumpToRecentBtn] = React.useState(false);
useKeyDown("Escape", () => document && ui.collapseComments(document?.id));
useKeyDown("Escape", () => document && ui.set({ commentsExpanded: false }));
const [draft, onSaveDraft] = usePersistedState<ProsemirrorData | undefined>(
`draft-${document?.id}-new`,
@@ -94,14 +94,18 @@ function Comments() {
React.useEffect(() => {
// Handles: 1. on refresh 2. when switching sort setting
const readyToDisplay = Boolean(document && isEditorInitialized);
if (readyToDisplay && sortOption.type === CommentSortType.MostRecent) {
if (
readyToDisplay &&
sortOption.type === CommentSortType.MostRecent &&
!viewingResolved
) {
scrollToBottom();
}
}, [sortOption.type, document, isEditorInitialized]);
}, [sortOption.type, document, isEditorInitialized, viewingResolved]);
React.useEffect(() => {
setShowJumpToRecentBtn(false);
if (sortOption.type === CommentSortType.MostRecent) {
if (sortOption.type === CommentSortType.MostRecent && !viewingResolved) {
const commentsAdded = threads.length > prevThreadCount.current;
if (commentsAdded) {
if (isAtBottom.current) {
@@ -112,7 +116,7 @@ function Comments() {
}
}
prevThreadCount.current = threads.length;
}, [sortOption.type, threads.length]);
}, [sortOption.type, threads.length, viewingResolved]);
if (!document || !isEditorInitialized) {
return null;
@@ -126,7 +130,7 @@ function Comments() {
<CommentSortMenu />
</Flex>
}
onClose={() => ui.collapseComments(document?.id)}
onClose={() => ui.set({ commentsExpanded: false })}
scrollable={false}
>
<Scrollable

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