Compare commits

...

312 Commits

Author SHA1 Message Date
Tom Moor 878860215a docs 2024-12-07 13:56:35 -05:00
Tom Moor cb5ca55d9e fix: Allow inline mark matching to work with preceding brackets
Refactor markInputRule, add markInputRuleForPattern
2024-12-07 13:53:07 -05:00
Tom Moor 8b2ccc6caf stash 2024-12-07 13:35:10 -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
Tom Moor fd5391cbb6 Cache diff generation for email notifications (#7987)
* Cache diff generation, closes #7982

* Handle cannot acquire lock

* Refactor to guard
2024-11-20 14:45:12 -08:00
Hemachandar 6e685ee8d9 store pin location on mount (#7994) 2024-11-20 14:27:16 -08:00
Tom Moor b595a0d427 fix: No-op sending emails in self-hosted if configuration is unavailable rather than retrying
towards #7982
2024-11-19 20:45:32 -05:00
Tom Moor 1c86119065 fix: Cannot sort by role on member settings, closes #7986 2024-11-19 19:22:53 -05:00
Tom Moor c629006642 Add inline resolve action on comment threads (#7977)
* Add inline resolve action on comment threads

* perf refactor
2024-11-18 18:36:44 -08:00
Tom Moor 326f733d4c fix: Further improvements to diacritics matching in CMD+F 2024-11-18 18:04:10 -05:00
Tom Moor d4d683c046 fix: Missing space character in invite modal, related #7968 2024-11-18 17:51:49 -05:00
Tom Moor 8204ac343f chore: Upgrade Sentry/AWS 2024-11-18 17:48:36 -05:00
dependabot[bot] cae8de7c7a chore(deps): bump @octokit/auth-app from 6.1.2 to 6.1.3 (#7974)
Bumps [@octokit/auth-app](https://github.com/octokit/auth-app.js) from 6.1.2 to 6.1.3.
- [Release notes](https://github.com/octokit/auth-app.js/releases)
- [Commits](https://github.com/octokit/auth-app.js/compare/v6.1.2...v6.1.3)

---
updated-dependencies:
- dependency-name: "@octokit/auth-app"
  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-18 14:41:05 -08:00
dependabot[bot] 8efa601967 chore(deps-dev): bump @relative-ci/agent from 4.2.12 to 4.2.13 (#7975)
Bumps [@relative-ci/agent](https://github.com/relative-ci/agent) from 4.2.12 to 4.2.13.
- [Release notes](https://github.com/relative-ci/agent/releases)
- [Commits](https://github.com/relative-ci/agent/compare/v4.2.12...v4.2.13)

---
updated-dependencies:
- dependency-name: "@relative-ci/agent"
  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-18 14:40:53 -08:00
Tom Moor 86c3ea8e9d fix: Copy toolbar positioning 2024-11-17 10:00:52 -05:00
Tom Moor c222782534 Upgrade Mermaid script in exported HTML, related #7964 2024-11-17 09:51:46 -05:00
Tom Moor 19ea7ee52b Remove sourcemap generation in bundle size calc (#7966) 2024-11-16 16:57:19 -08:00
Tom Moor d1de84a07e Reduce build time (#7965)
* Test using xlarge

* wip

* wip
2024-11-16 10:45:14 -08:00
Tom Moor d73b4c55bf Mermaid v11 (#7964)
* mermaid-v11

* fix: Various rendering incompatibilities
2024-11-16 08:10:55 -08:00
Hemachandar 9843c4c995 Improvements around templates (#7952)
* hide new-doc-from-template menu item

* change trash path for deleted templates

* conditional show templates in command bar
2024-11-16 07:48:58 -08:00
dependabot[bot] 685397b057 chore(deps): bump cross-spawn from 7.0.3 to 7.0.5 (#7963)
Bumps [cross-spawn](https://github.com/moxystudio/node-cross-spawn) from 7.0.3 to 7.0.5.
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.5)

---
updated-dependencies:
- dependency-name: cross-spawn
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-16 06:43:06 -08:00
Tom Moor 13d37d4207 perf: Fix increase in initial bundle size, prosemirror-transform must be fixed at 1.10.0 until paragraph join is fixed 2024-11-16 09:14:27 -05:00
Tom Moor 7bedfab301 fix: Mermaid diagrams in collapsed headings do not render, closes #7960 2024-11-15 23:43:05 -05:00
Translate-O-Tron db5850ac0d New Crowdin updates (#7898) 2024-11-15 18:15:32 -08:00
Hemachandar a4c40ce25e show resolved view when a resolved comment is opened from notif email (#7959)
* show resolved view when a resolved comment is opened from notif email

* check and replace state
2024-11-15 18:14:30 -08:00
Tom Moor f5457e79cd fix: Mermaid diagram moves up and down when focused in read-only editor 2024-11-14 20:00:55 -05:00
Hemachandar 73eeeefb25 fix: restore workspace templates (#7951) 2024-11-14 16:32:02 -08:00
Hemachandar 54f82cac96 fix: show all templates in the menu (#7950)
* fetch all templates

* websocket events for workspace templates
2024-11-14 16:31:48 -08:00
Tom Moor bb43c24efe fix: Improve handling of unknown errors – closes #7933 2024-11-13 21:22:06 -05:00
Tom Moor acf3d7cd08 fix: Add latex fences as markdown signal 2024-11-13 21:03:44 -05:00
github-actions[bot] 5245f93642 chore: Compressed inefficient images automatically (#7946)
Co-authored-by: tommoor <tommoor@users.noreply.github.com>
2024-11-13 20:47:49 -05:00
Benjamin Kramser cfce55250e Add Pinterest embed (#7930)
* add pinterest embed

* improved profile detection
2024-11-13 17:46:07 -08:00
Tom Moor 6421995b29 fix: Do not override from address in self-hosted env, closes #7929 2024-11-13 20:19:17 -05:00
Tom Moor 8cfd8e25db fix: Event should not be written when API key is used 2024-11-13 09:10:30 -05:00
Tom Moor 1282e9653e fix: Excess padding on internal read-only docs, should only have applied to shares 2024-11-13 08:16:06 -05:00
Tom Moor f1edaecf49 perf: Fix observable changing on every keydown 2024-11-13 08:16:06 -05:00
Tom Moor f7d737ca45 fix: Missing 'Untitled' in reference list 2024-11-13 08:16:06 -05:00
Tom Moor 41c2c760d4 v0.81.0 2024-11-13 08:16:06 -05:00
dependabot[bot] f692d1bc3a chore(deps-dev): bump terser from 5.32.0 to 5.36.0 (#7928)
Bumps [terser](https://github.com/terser/terser) from 5.32.0 to 5.36.0.
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/compare/v5.32.0...v5.36.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-13 05:06:33 -08:00
dependabot[bot] 5197d6e18c chore(deps): bump prosemirror-view from 1.34.3 to 1.36.0 (#7925)
Bumps [prosemirror-view](https://github.com/prosemirror/prosemirror-view) from 1.34.3 to 1.36.0.
- [Changelog](https://github.com/ProseMirror/prosemirror-view/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prosemirror/prosemirror-view/compare/1.34.3...1.36.0)

---
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-11-13 05:06:16 -08:00
dependabot[bot] b901ea7b30 chore(deps-dev): bump eslint-plugin-react-hooks from 4.6.0 to 4.6.2 (#7924)
Bumps [eslint-plugin-react-hooks](https://github.com/facebook/react/tree/HEAD/packages/eslint-plugin-react-hooks) from 4.6.0 to 4.6.2.
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/HEAD/packages/eslint-plugin-react-hooks)

---
updated-dependencies:
- dependency-name: eslint-plugin-react-hooks
  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-13 05:05:21 -08:00
dependabot[bot] 3820499856 chore(deps-dev): bump @types/jest from 29.5.13 to 29.5.14 (#7927)
Bumps [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest) from 29.5.13 to 29.5.14.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jest)

---
updated-dependencies:
- dependency-name: "@types/jest"
  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-13 05:05:11 -08:00
dependabot[bot] 0cffde63ab chore(deps): bump zod from 3.22.4 to 3.23.8 (#7926)
Bumps [zod](https://github.com/colinhacks/zod) from 3.22.4 to 3.23.8.
- [Release notes](https://github.com/colinhacks/zod/releases)
- [Changelog](https://github.com/colinhacks/zod/blob/main/CHANGELOG.md)
- [Commits](https://github.com/colinhacks/zod/compare/v3.22.4...v3.23.8)

---
updated-dependencies:
- dependency-name: zod
  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-11-13 05:05:01 -08:00
Hemachandar 449ba6488e hide share option for templates (#7937) 2024-11-13 05:03:36 -08:00
Tom Moor 62f3e6921f Add changes property to client event model 2024-11-10 22:58:51 -05:00
Tom Moor bc259316f7 Include changes in event presenter 2024-11-10 22:35:11 -05:00
Tom Moor 98e03cc227 Convert stars, towards #7920 (#7921) 2024-11-10 19:26:27 -08:00
Tom Moor 633e547d3e Refactor of event insertion (#5909) 2024-11-10 16:26:20 -08:00
Tom Moor d5de69fd4b fix: Exception for Notion import of a single document 2024-11-09 19:26:44 -05:00
Hemachandar feec01f160 fix: don't show comment marks for other users' drafts (#7838)
* fix: don't show comment marks for other users' drafts

* remove unnecessary draft check
2024-11-09 10:43:59 -08:00
Tom Moor aa5813032e fix: Click image to focus 2024-11-09 13:02:55 -05:00
Tom Moor a6ba189180 Add menu item to leave document that has been shared with current user (#7918)
* Add menu item to leave document that has been shared with current user

* Only redirect if viewing doc
2024-11-09 06:45:59 -08:00
Tom Moor 4c65bbc57c fix: Improved toolbar behavior with partial link selection, closes #7890 2024-11-08 22:41:58 -05:00
Tom Moor c76b4f46aa Tweak sharing UI 2024-11-08 21:35:55 -05:00
infinite-persistence ca17b41c53 share: add allowIndexing (#7896)
* share: add `allowIndexing`

## Ticket
Closes 7486

* i18n: follow existing no-punctuation style
2024-11-08 17:28:30 -08:00
Tom Moor 9747c6ba5d fix: Document mentions can be incorrectly attributed during collab session (#7913) 2024-11-08 05:35:49 -08:00
Tom Moor 55ffd6d098 feat: Adds support for importing CSV files (#7912)
* feat: Adds support for importing CSV files

* test

* tsc
2024-11-07 19:09:02 -08:00
Tom Moor 9b26ccda19 fix: Switching edit mode scrolls to page top, closes #7910 2024-11-07 22:04:15 -05:00
Hemachandar 56b38b9dbd fix: restore documents from a deleted collection (#7909) 2024-11-07 18:03:30 -08:00
Hemachandar 0a3a684493 fix: collection archival post-process parity with deletion (#7906) 2024-11-07 18:02:51 -08:00
Tom Moor 24548dc7ee fix: Cannot pick the same file twice for import 2024-11-06 23:28:23 -05:00
Tom Moor 28cc83ad05 test 2024-11-06 21:49:18 -05:00
Tom Moor c57b845093 fix: Sentry.configureScope silently throwing error 2024-11-06 21:33:13 -05:00
Hemachandar 62ee075a6f feat: store user timezone (#7902)
* feat: store user timezone

* tz validation
2024-11-06 18:06:19 -08:00
Tom Moor 356b0916fd fix: Spacing below document editor should also be rendered in read-only 2024-11-06 20:56:16 -05:00
Tom Moor 03160c44d4 fix: Line numbers are not immediately visible when pasting code blocks 2024-11-06 20:31:44 -05:00
Tom Moor bf65d80fc8 Refactor SmartText disabling to use existing pattern (forgot this exists) 2024-11-06 20:18:08 -05:00
infinite-persistence 3d0df9c612 Jump to 'workspace' settings section if invoked from Org Menu (#7900)
## Issue
When clicking on the top-left "OrganizationMenu > Settings", I always get confused why it is showing me the Profiles page, given that the current context is workspace/org.

## Change
Switch the shortcut to point to "Workspace::details" instead.
For Profile, it's more natural to click the profile button from the bottom-left.
2024-11-06 05:57:50 -08:00
infinite-persistence 9de95ff658 Fix header key for react-table v7 (#7894)
`header.id` does not exist in v7 (it does in v8). `@types` lied.

The returned props actually includes a `key`..

```
      return utils.propGetter({
        key: headerGroup.id,
        role: 'row'
      }, userProps);
```

... so we could have just spread it in `tr`, but we still had to explicitly define a `key` to satisfy lint.

## Stashed v7 documentation:
https://github.com/TanStack/table/blob/v7/docs/src/pages/docs/api/useTable.md#headergroup-properties
2024-11-06 05:55:25 -08:00
Hemachandar 55bdd6fbc0 fix: heading disclosure transform (#7904) 2024-11-06 05:41:15 -08:00
Tom Moor fec91fb210 fix: Comment button appearing on mobile with no text selection 2024-11-05 19:59:44 -05:00
Tom Moor abb1d3a923 Restore fallback for storing IP address on revisions.create 2024-11-05 19:54:08 -05:00
infinite-persistence f5de2834d6 Add user preference to disable smart quotes (#7881) 2024-11-05 16:45:06 -08:00
Hemachandar 68377c3c46 fix: stop propagating click events outside EventBoundary (#7897) 2024-11-05 12:29:05 -08:00
dependabot[bot] 9661e18cbd chore(deps-dev): bump eslint-plugin-jsx-a11y from 6.7.1 to 6.10.2 (#7889)
Bumps [eslint-plugin-jsx-a11y](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y) from 6.7.1 to 6.10.2.
- [Release notes](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/releases)
- [Changelog](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/compare/v6.7.1...v6.10.2)

---
updated-dependencies:
- dependency-name: eslint-plugin-jsx-a11y
  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-11-05 08:09:37 -08:00
Tom Moor 08d210e483 fix: Enter with image selected should insert new paragraph below 2024-11-04 21:36:43 -05:00
Tom Moor 5a0ce58fa0 fix: Error backwards joining paragraphs to lists 2024-11-04 21:02:37 -05:00
dependabot[bot] 08eeac2049 chore(deps): bump @babel/plugin-transform-regenerator from 7.25.7 to 7.25.9 (#7887)
Bumps [@babel/plugin-transform-regenerator](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-regenerator) from 7.25.7 to 7.25.9.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.25.9/packages/babel-plugin-transform-regenerator)

---
updated-dependencies:
- dependency-name: "@babel/plugin-transform-regenerator"
  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-04 15:47:59 -08:00
dependabot[bot] 08a49378ea chore(deps): bump react-hook-form from 7.53.0 to 7.53.1 (#7886)
Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 7.53.0 to 7.53.1.
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.53.0...v7.53.1)

---
updated-dependencies:
- dependency-name: react-hook-form
  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-04 15:47:41 -08:00
dependabot[bot] a4a068a3ba chore(deps): bump @babel/preset-react from 7.24.7 to 7.25.9 (#7888)
Bumps [@babel/preset-react](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-react) from 7.24.7 to 7.25.9.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.25.9/packages/babel-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-11-04 15:47:27 -08:00
Tom Moor 27cbf5a56a fix: Image zoom behavior, closes #7883 2024-11-04 09:04:18 -05:00
Translate-O-Tron f3daf45ccc New Crowdin updates (#7850) 2024-11-03 15:00:09 -08:00
Hemachandar c1c20f1ff9 feat: allow search without a search term (#7765)
* feat: allow search without a search term

* tests

* conditional filter visibility

* add icon to collection filter
2024-11-03 14:59:48 -08:00
Tom Moor e4d60382fd fix: Regression in 763dd28829 2024-11-03 14:00:07 -05:00
Tom Moor 763dd28829 documentUpdater 2024-11-03 12:29:48 -05:00
Tom Moor 93f7fa8c89 fix: notifications.pixel errors, regressed in 5780959e93 2024-11-03 11:46:11 -05:00
Tom Moor 24e50d9290 fix: Overlapping UI elements when resizing sidebar beyond minimum width 2024-11-03 10:12:15 -05:00
Tom Moor c1b19ef86c userDestroyer 2024-11-03 09:41:07 -05:00
Tom Moor 5780959e93 notificationUpdater 2024-11-03 09:32:45 -05:00
Tom Moor 74192040a2 Add new security preference (#7879)
* Add security preference to remove document content in email notifications

* Refactor, reduce chance of misuse
2024-11-03 05:59:11 -08:00
Tom Moor 7b3eba0f2f fix: More consistent dark mode colors 2024-11-02 21:50:36 -04:00
Tom Moor 9b03b529f8 fix: Adding reaction unfocuses comment thread
fix: Scrollable area of reaction picker larger than dialog
2024-11-02 21:23:38 -04:00
Tom Moor aa579412d0 Remove explicit passing of transaction to createWithContext 2024-11-02 19:27:17 -04:00
Tom Moor 774402560e fix: Remove marine from user color rotation as it clashes with comment marks, closes #7846 2024-11-02 18:46:20 -04:00
Tom Moor 0a875d4738 Tweak colors 2024-11-02 18:40:52 -04:00
Hemachandar de04d1c0c5 feat: Comment reactions (#7790)
Co-authored-by: Tom Moor <tom@getoutline.com>
2024-11-02 10:58:03 -07:00
Tom Moor d87e1f6264 fix: Cannot use Discord authentication if guild name looks like a URL, closes #7776 2024-11-02 13:40:11 -04:00
Tom Moor 0e249951ab chore: Event.createFromContext usage (#7877)
* revisions.create

* Automatically pass transaction in state to createFromContext
2024-11-02 10:16:15 -07:00
Tom Moor 398be02091 Add authType column to events (#7872)
* Add authType column to events

* Record authType with createFromContext
2024-11-02 06:21:43 -07:00
infinite-persistence 83f0d34430 Comments: scroll to most-recent during load and when switching sort setting (#7825)
Co-authored-by: Tom Moor <tom.moor@gmail.com>
2024-11-02 06:15:12 -07:00
Tom Moor 21723d3ca2 fix: Triple click of last line in code block on Firefox (#7876) 2024-11-01 05:36:50 -07:00
Tom Moor bc4f0b926d Set reply-to email address to actor when permissions allow (#7840)
* Set reply-to email address to actor when permissions allow

* tsc

* Add reply email for Invite too
2024-11-01 05:36:29 -07:00
Tom Moor 4d67d47795 fix: Do not consider hover activity when page/window is out of focus (#7871) 2024-10-31 19:19:08 -07:00
Tom Moor d372ccf5b6 fix: Decrease sensitivity of markdown detection, closes #7873 2024-10-31 22:14:45 -04:00
Tom Moor d78eeaba84 fix: Group membership addition UI shows incorrect options after pagination, closes #7875 2024-10-31 21:23:34 -04:00
Tom Moor fc333abb86 perf: Improve sidebar performance when collection has large amount of root documents 2024-10-31 20:35:13 -04:00
Tom Moor 73ef9f9a05 fix: Close collapsed sidebar when window loses focus, related #7857 2024-10-30 19:10:44 -05:00
Tom Moor 670ddda3a4 fix: Quick fix for toolbar behind header, closes #7826 2024-10-30 17:28:47 -05:00
Tom Moor 6e74ccf61f fix: Allow single character workspace names. 2024-10-30 17:03:40 -05:00
Tom Moor f3f7189c93 test 2024-10-30 15:27:08 -05:00
Tom Moor 50e680aaaf fix: Deprecated shares do not load 2024-10-30 14:03:20 -05:00
Tom Moor 373ffba384 fix: Search bar overlaid by menu on large documents 2024-10-30 13:46:44 -05:00
dependabot[bot] b0182dfc76 chore(deps): bump vite from 5.4.8 to 5.4.10 (#7852)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.4.8 to 5.4.10.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.10/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.10/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-10-29 20:50:07 -07:00
dependabot[bot] 2084c4ff8e chore(deps): bump i18next-fs-backend from 2.3.1 to 2.3.2 (#7851)
Bumps [i18next-fs-backend](https://github.com/i18next/i18next-fs-backend) from 2.3.1 to 2.3.2.
- [Commits](https://github.com/i18next/i18next-fs-backend/compare/v2.3.1...v2.3.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-29 20:49:48 -07:00
dependabot[bot] 29fce45a6e chore(deps-dev): bump @types/dotenv from 8.2.0 to 8.2.3 (#7853)
Bumps [@types/dotenv](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/dotenv) from 8.2.0 to 8.2.3.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/dotenv)

---
updated-dependencies:
- dependency-name: "@types/dotenv"
  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-10-29 14:35:01 -07:00
dependabot[bot] e524699a8c chore(deps-dev): bump @babel/cli from 7.23.4 to 7.25.9 (#7854)
Bumps [@babel/cli](https://github.com/babel/babel/tree/HEAD/packages/babel-cli) from 7.23.4 to 7.25.9.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.25.9/packages/babel-cli)

---
updated-dependencies:
- dependency-name: "@babel/cli"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-29 14:34:51 -07:00
Hemachandar 5e74554f4b fix: check collection group membership for backlinks (#7856) 2024-10-28 21:07:19 -07:00
Tom Moor 2a3909f65a Make from address for authentication related emails unguessable (#7844)
* Make from address for authentication-related emails unguessable

* feedback
2024-10-27 13:25:01 -07:00
Translate-O-Tron b91c06d26a New Crowdin updates (#7764) 2024-10-27 07:42:06 -07:00
Hemachandar f8433bc65e fix: add sidebar toggle for public docs (#7842) 2024-10-27 07:40:15 -07:00
Tom Moor 7bdae0cbda Revert "fix: Remove overflow from floating toolbar in desktop, as it sometimes causes the content to be misplaced"
This reverts commit bb988b551d.

Closes #7836
2024-10-26 10:14:49 -04:00
Hemachandar 3692d9c930 fix: move editor init to dispatchTransaction (#7833) 2024-10-25 08:33:18 -07:00
Alexandr Zagorskiy 2e1a827157 Feat/installation info endpoint (#7744)
* feat: add installation.info endpoint using DockerHub API

* feat: UI use an server-side API to show version info

* fix: review fixes

* test: installation.info endpoint

* feat: filtering pre-releases in installation.info endpoint

* fix: change fetch to ApiClient usage for getting version info

* Undo translation change

---------

Co-authored-by: Tom Moor <tom.moor@gmail.com>
2024-10-25 06:39:47 -07:00
Hemachandar fe33871dfe fix: wait for shared document to load (#7830) 2024-10-25 05:37:59 -07:00
Tom Moor f22bd1d7c8 perf: Multitude of small perf wins around comment sidebar, closes #7823 2024-10-24 19:22:22 -04:00
Tom Moor 48ff0ad84b fix: Duplicate threads in sidebar when comment mark crosses boundary 2024-10-24 09:45:20 -04:00
Tom Moor 4f626c08c2 perf: Fix comments double rendering on mount (#7824) 2024-10-24 05:46:43 -07:00
Hemachandar 57e9abd77f feat: allow sort by position for comments (#7770)
* feat: allow sort by position for comments

* wait for prosemirror nodes to load

* Move to menu

* remove sort; rename enum

* asc sort for in-thread display

* revert sort

---------

Co-authored-by: Tom Moor <tom.moor@gmail.com>
2024-10-22 20:18:33 -07:00
Apoorv Mishra 0d7ce76c21 Allow querying by user emails in order to @mention them (#7807)
* fix: readEmail permission

* fix: allow querying over user email in users.list

* fix: allow searching by email in @mention

* fix: include email in mentioned user's hover card

* fix: put email on separate line in hover card
2024-10-22 20:24:11 +05:30
Tom Moor c8d307c2d4 fix: Improve safety around image toolbar, related #7815 2024-10-21 21:42:27 -04:00
Tom Moor 10c51ef08d fix: Add syntax highlighting for Mermaid diagrams 2024-10-21 21:22:48 -04:00
Tom Moor bb988b551d fix: Remove overflow from floating toolbar in desktop, as it sometimes causes the content to be misplaced 2024-10-21 21:03:29 -04:00
dependabot[bot] 0e75edf7e3 chore(deps): bump prosemirror-markdown from 1.13.0 to 1.13.1 (#7811)
* chore(deps): bump prosemirror-markdown from 1.13.0 to 1.13.1

Bumps [prosemirror-markdown](https://github.com/prosemirror/prosemirror-markdown) from 1.13.0 to 1.13.1.
- [Changelog](https://github.com/ProseMirror/prosemirror-markdown/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prosemirror/prosemirror-markdown/compare/1.13.0...1.13.1)

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

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-10-21 17:50:32 -07:00
dependabot[bot] 3523ee4c35 chore(deps-dev): bump @relative-ci/agent from 4.2.9 to 4.2.12 (#7810)
Bumps [@relative-ci/agent](https://github.com/relative-ci/agent) from 4.2.9 to 4.2.12.
- [Release notes](https://github.com/relative-ci/agent/releases)
- [Commits](https://github.com/relative-ci/agent/compare/v4.2.9...v4.2.12)

---
updated-dependencies:
- dependency-name: "@relative-ci/agent"
  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-10-21 17:25:51 -07:00
dependabot[bot] c0fba3913c chore(deps-dev): bump @types/turndown from 5.0.4 to 5.0.5 (#7812)
Bumps [@types/turndown](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/turndown) from 5.0.4 to 5.0.5.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/turndown)

---
updated-dependencies:
- dependency-name: "@types/turndown"
  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-10-21 17:25:37 -07:00
dependabot[bot] 597106cb48 chore(deps): bump prosemirror-transform from 1.10.0 to 1.10.2 (#7813)
Bumps [prosemirror-transform](https://github.com/prosemirror/prosemirror-transform) from 1.10.0 to 1.10.2.
- [Changelog](https://github.com/ProseMirror/prosemirror-transform/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prosemirror/prosemirror-transform/compare/1.10.0...1.10.2)

---
updated-dependencies:
- dependency-name: prosemirror-transform
  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-10-21 17:25:27 -07:00
Tom Moor 02c29e06fb perf: filter -> find to reduce policies iterated through (#7816) 2024-10-21 17:24:49 -07:00
Tom Moor 7226109989 perf: Avoid expensive DocumentHelper.toMarkdown call in presenter 2024-10-21 19:23:01 -04:00
Tom Moor 85957c10b8 fix: Invalid regex error bubbles from FindAndReplace 2024-10-20 21:27:22 -04:00
Tom Moor 7250bd3bcb fix: Cannot scrub videos in Chrome when using local storage
closes #7517
2024-10-20 21:21:51 -04:00
Tom Moor 2ee7e0f832 Use view transition API to smoothly transition between light/dark theme 2024-10-20 20:02:46 -04:00
Tom Moor c5278a71de Increase ping/pong rate to increase Heroku compatibility 2024-10-20 09:56:59 -04:00
Tom Moor e41519575f tsc 2024-10-19 12:37:17 -04:00
Tom Moor 201ccf39a0 Update icon on disclosure buttons/selects 2024-10-19 12:25:54 -04:00
Tom Moor ac3285a29a tsc 2024-10-19 08:54:58 -04:00
Tom Moor fdaeb6602d fix: Support diacritics in cmd+f, closes #7801 2024-10-19 08:22:20 -04:00
Tom Moor da4cd4ebcd Improved error handling for Azure auth, add default value for AZURE_RESOURCE_ID 2024-10-19 08:05:43 -04:00
Tom Moor b6fc8fb4b1 fix: Guard unset in awareness data 2024-10-18 09:00:02 -04:00
Tom Moor 4e6572d686 fix: Mutate clipboard content when copying from a single table cell. (#7798)
* fix: Mutate clipboard content when copying from a single table cell.
closes #7794

* refactor
2024-10-18 05:35:21 -07:00
Tom Moor 9e378899ff Remove string filtering in logger 2024-10-17 22:50:11 -04:00
Tom Moor 31dafc4258 Hide remote users selections after a timeout (#7788) 2024-10-17 15:38:36 -07:00
Hemachandar 6614b23eae fix: assorted comment bugs (#7795)
* fix: assorted comment bugs

* remove policy instead of force fetch
2024-10-17 15:38:26 -07:00
Tom Moor 9e54fd1bfb fix: User exists should account for deleted workspaces, closes #7793 2024-10-17 18:14:15 -04:00
Tom Moor f0add849f9 fix: Ensure max filename length for stored attachments, closes #7785 2024-10-16 23:18:18 -04:00
Tom Moor b55915c257 fix: Include deleted workspaces when searching for available subdomains, closes #7787 2024-10-16 22:59:22 -04:00
Tom Moor bdac4360b4 chore: Remove usage of y-prosemirror fork, pull in latest fixes from upstream 2024-10-16 21:37:52 -04:00
Tom Moor 72bfbf2060 Allow returning team API keys for admins from apiKeys.list (#7766)
* Allow returning team apiKeys.list for admins from apiKeys.list

* Filter apikeys in store
2024-10-14 15:29:47 -07:00
dependabot[bot] db02b0ae6b chore(deps): bump @babel/preset-env from 7.25.7 to 7.25.8 (#7780)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.25.7 to 7.25.8.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.25.8/packages/babel-preset-env)

---
updated-dependencies:
- dependency-name: "@babel/preset-env"
  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-10-14 14:30:10 -07:00
dependabot[bot] bb40e4079a chore(deps-dev): bump nodemon from 3.1.4 to 3.1.7 (#7781)
Bumps [nodemon](https://github.com/remy/nodemon) from 3.1.4 to 3.1.7.
- [Release notes](https://github.com/remy/nodemon/releases)
- [Commits](https://github.com/remy/nodemon/compare/v3.1.4...v3.1.7)

---
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-10-14 14:29:58 -07:00
dependabot[bot] 198a96c78f chore(deps): bump emoji-regex from 10.3.0 to 10.4.0 (#7783)
Bumps [emoji-regex](https://github.com/mathiasbynens/emoji-regex) from 10.3.0 to 10.4.0.
- [Commits](https://github.com/mathiasbynens/emoji-regex/compare/v10.3.0...v10.4.0)

---
updated-dependencies:
- dependency-name: emoji-regex
  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-10-14 14:29:43 -07:00
dependabot[bot] 1dd835bb87 chore(deps-dev): bump discord-api-types from 0.37.101 to 0.37.102 (#7779)
Bumps [discord-api-types](https://github.com/discordjs/discord-api-types) from 0.37.101 to 0.37.102.
- [Release notes](https://github.com/discordjs/discord-api-types/releases)
- [Changelog](https://github.com/discordjs/discord-api-types/blob/main/CHANGELOG.md)
- [Commits](https://github.com/discordjs/discord-api-types/compare/0.37.101...0.37.102)

---
updated-dependencies:
- dependency-name: discord-api-types
  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-10-14 14:29:15 -07:00
dependabot[bot] 25c504ceaf chore(deps-dev): bump typescript from 5.4.5 to 5.6.3 (#7767)
Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.4.5 to 5.6.3.
- [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.4.5...v5.6.3)

---
updated-dependencies:
- dependency-name: 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-10-11 19:45:02 -07:00
Tom Moor 9680e57849 chore: Remove suppressImplicitAnyIndexErrors TS rule (#7760) 2024-10-11 12:46:46 -07:00
Hemachandar 0f8ac54bcb feat: include content in document mentioned email (#7756)
* feat: include content in document mentioned email

* handle doc publish flow

* add tests, doc

* including heading node

* Diff border
2024-10-11 12:30:08 -07:00
Hemachandar 936a8b2510 fix: show all document backlinks for a user (#7751)
* fix: show all document backlinks for a user

* add findByIds method to Document model

* default options param

* move filter to Document model

* docs

* fix: Backlinks from collections without direct membership not returned

---------

Co-authored-by: Tom Moor <tom.moor@gmail.com>
2024-10-11 08:38:24 -07:00
Luke Thomas b7b5e3edb9 fix: Remove docker version in compose file (#7762) 2024-10-11 06:26:38 -07:00
Translate-O-Tron 1cea59abe2 New Crowdin updates (#7730) 2024-10-10 18:22:12 -07:00
Tom Moor 8f0211057c fix: RTL headings are not considered separately for layout
closes #7757
2024-10-10 20:45:50 -04:00
Tom Moor 2bfef05137 fix: Mention with space in search is not inserted correctly, closes #7759 2024-10-10 19:59:20 -04:00
dependabot[bot] d2a99b6872 chore(deps): bump vite from 5.3.1 to 5.4.8 (#7704)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.3.1 to 5.4.8.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.8/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.8/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  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-10-10 04:47:20 -07:00
Hemachandar 6c9f265918 feat: Lossless JSON import (#7274)
* feat: Lossless JSON import

* transform node only when attachments are present in the zip
2024-10-09 19:04:04 -07:00
Tom Moor 7a8d40b9e7 feat: Add option to export table as CSV, closes #7743 2024-10-08 21:23:38 -04:00
Tom Moor 3ddffdda17 fix: Race condition rendering Mermaid diagrams in dark mode 2024-10-08 20:43:48 -04:00
Tom Moor 91396148ae fix: “Share to web” control is unresponsive when opening via “Permissions” menu item 2024-10-08 19:20:40 -04:00
Tom Moor 1c2ea2aa92 fix: Incorrect keyboard shortcut for TOC shown on macOS 2024-10-08 18:55:19 -04:00
Tom Moor ba5eb60825 fix: Remove slashes and literal newlines from markdown, closes #7691 2024-10-07 23:01:07 -04:00
Tom Moor a0e363799c fix: Add extra safety around search queries 2024-10-07 22:29:54 -04:00
Tom Moor 3d457890cd fix: Regression in e857d00e3d rendering embeds 2024-10-07 22:04:51 -04:00
Tom Moor e857d00e3d chore: Moves ProseMirror NodeView to render within main React context (#7736) 2024-10-07 17:58:00 -07:00
Tom Moor 98d8435b15 Allow search page to work with Firefox keywords, closes #7722 2024-10-07 19:55:19 -04:00
dependabot[bot] b80463665b chore(deps): bump @babel/preset-env from 7.24.7 to 7.25.7 (#7740)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.24.7 to 7.25.7.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.25.7/packages/babel-preset-env)

---
updated-dependencies:
- dependency-name: "@babel/preset-env"
  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-10-07 16:54:55 -07:00
dependabot[bot] b4ce4a2922 chore(deps): bump prosemirror-model from 1.22.3 to 1.23.0 (#7741)
Bumps [prosemirror-model](https://github.com/prosemirror/prosemirror-model) from 1.22.3 to 1.23.0.
- [Changelog](https://github.com/ProseMirror/prosemirror-model/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prosemirror/prosemirror-model/compare/1.22.3...1.23.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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-07 16:54:45 -07:00
dependabot[bot] 9bee54b07e chore(deps-dev): bump @types/react-avatar-editor from 13.0.2 to 13.0.3 (#7739)
Bumps [@types/react-avatar-editor](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-avatar-editor) from 13.0.2 to 13.0.3.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-avatar-editor)

---
updated-dependencies:
- dependency-name: "@types/react-avatar-editor"
  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-10-07 16:54:35 -07:00
Tom Moor d3c8224839 fix: Error during import with long filenames (#7738)
* fix: Stream error during import causes worker restart

* refactor

* fix: Ensure we never write filenames longer than the system can handle
2024-10-07 05:36:18 -07:00
Tom Moor 0a1c614c55 fix: Addressed several React warnings in icon picker 2024-10-06 11:38:24 -04:00
Tom Moor db4dad5e37 fix: Enter key while renaming item in sidebar should persist
fix: Renaming item in sidebar should not navigate to collection
2024-10-06 11:17:39 -04:00
Apoorv Mishra 35ff70bf14 Archive collections (#7266)
Co-authored-by: Tom Moor <tom.moor@gmail.com>
2024-10-06 05:37:11 -07:00
Tom Moor 8b5fdba6f4 chore: Remove usage of deprecated docker build image 2024-10-05 12:51:38 -04:00
dependabot[bot] e0a3ad92e0 chore(deps): bump cookie from 0.6.0 to 0.7.0 (#7734)
Bumps [cookie](https://github.com/jshttp/cookie) from 0.6.0 to 0.7.0.
- [Release notes](https://github.com/jshttp/cookie/releases)
- [Commits](https://github.com/jshttp/cookie/compare/v0.6.0...v0.7.0)

---
updated-dependencies:
- dependency-name: cookie
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-05 08:21:33 -07:00
Tom Moor 10f4889737 fix: Cloned response on network error can cause process to hang (remove) 2024-10-05 10:59:56 -04:00
Tom Moor 7f66393e63 spelling 2024-10-03 21:51:07 -04:00
Tom Moor 033b05f679 fix: User cannot update profile when MembersCanDeleteAccount setting is disabled, closes #7729 2024-10-03 20:25:35 -04:00
Tom Moor 8356d44cae Merge branch 'main' of github.com:outline/outline 2024-10-03 19:39:06 -04:00
Translate-O-Tron 030c0fd40e New Crowdin updates (#7641) 2024-10-03 16:32:38 -07:00
Tom Moor 1a02b0d9d7 Add script to backfill ApiKey hashes (#7717)
* Add hashed column for API keys

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2024-10-03 16:27:25 -07:00
Apoorv Mishra be5f092117 Show nested docs on Archive page (#7488)
* fix: nested docs should appear in archive

* fix(app): ArchivableModel

* fix(server): ArchivableModel

* fix: PartialWithArchivedAt not needed

* fix: new PartialExcept type

* fix: restore deletion

* fix: review
2024-10-02 10:10:41 +05:30
Tom Moor 0ba423feb4 fix: Improve empty state for command menu no results 2024-10-01 22:28:38 -04:00
Tom Moor a41e17f875 feat: Add 'Copy public link' option to document menu 2024-10-01 09:55:33 -04:00
Hemachandar db114fd966 perf: store document state in context (#7658)
* perf: store document state in context

* provide doc-context for shared routes
2024-10-01 05:16:32 -07:00
dependabot[bot] ce987d23ed chore(deps): bump socket.io-client from 4.7.5 to 4.8.0 (#7701)
Bumps [socket.io-client](https://github.com/socketio/socket.io) from 4.7.5 to 4.8.0.
- [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-client@4.7.5...socket.io-client@4.8.0)

---
updated-dependencies:
- dependency-name: socket.io-client
  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-09-30 17:16:53 -07:00
Tom Moor 5e61fcd336 Add hashed column for API keys (#7699)
* Add hashed column for API keys

* test

* Add obfuscatedValue getter
2024-09-30 17:16:35 -07:00
dependabot[bot] 4f84daf558 chore(deps-dev): bump @types/body-scroll-lock from 3.1.0 to 3.1.2 (#7703)
Bumps [@types/body-scroll-lock](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/body-scroll-lock) from 3.1.0 to 3.1.2.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/body-scroll-lock)

---
updated-dependencies:
- dependency-name: "@types/body-scroll-lock"
  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-09-30 16:30:17 -07:00
dependabot[bot] f80842ca20 chore(deps-dev): bump @types/jest from 29.5.12 to 29.5.13 (#7702)
Bumps [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest) from 29.5.12 to 29.5.13.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jest)

---
updated-dependencies:
- dependency-name: "@types/jest"
  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-09-30 16:30:07 -07:00
Tom Moor 53758b69fb fix: Another race condition with policy removal on startup 2024-09-30 18:57:39 -04:00
Tom Moor cd86877cb0 fix: Race condition with policies removed on app load 2024-09-30 17:51:01 -04:00
清纯的小黄瓜 ffae5d2f20 fix: sidebar cannot display correctly on mobile, when collapsed is true (#7694) 2024-09-30 05:21:44 -07:00
Hemachandar 21adfdd1bf perf: use findAll for querying document collaborators (#7697)
* perf: use findAll for querying document collaborators

* remove unnecessary compact
2024-09-29 09:43:07 -07:00
Hemachandar 91c2f60827 fix: align document and collection title (#7696) 2024-09-29 09:37:09 -07:00
Tom Moor a253d2921a Add reverse alphabetical sort for collections, closes #7692 2024-09-29 09:53:50 -04:00
Tom Moor b83d218fbe fix: Add icons to copy functionality in command menu 2024-09-29 09:37:15 -04:00
Tom Moor dce96955a1 Tweak command bar heading style 2024-09-29 09:33:09 -04:00
Tom Moor ba7c446f59 Add confirmation dialog when dragging 2024-09-29 09:31:00 -04:00
Tom Moor 7b9ec4c43a Show targeted collection/document in command bar, closes #7678 2024-09-28 22:46:41 -04:00
Tom Moor faaf0a6733 Add recently viewed documents to top of command menu (#7685)
* Add recent documents to command menu
Add priority key to actions and sections

* refactor

* Rename section

* refactor, remove more unused code
2024-09-28 17:30:40 -07:00
Hemachandar c58aafeb32 fix: check doc access before sending mention email (#7664)
* fix: check doc access before sending mention email

* refactor

---------

Co-authored-by: Tom Moor <tom.moor@gmail.com>
2024-09-28 14:29:34 -07:00
bLue 3f73c9d2bf fix: Adjust math delimiter to fix markdown import/export compatibility issue (#6727)
#6650)
2024-09-28 12:45:03 -07:00
Tom Moor b6e43e1990 Add Heading 4 to block menu 2024-09-28 13:13:25 -04:00
Tom Moor 0a2c066253 fix: Improve display of Mermaid pie charts 2024-09-28 12:31:46 -04:00
Tom Moor 840db4692e fix: Correctly decorate urlId as Unique (#7671) 2024-09-28 06:23:27 -07:00
Tom Moor fa961d7464 fix: Drop to reorder cursor appearing where move location is invalid 2024-09-28 09:21:26 -04:00
Tom Moor 3e75b24f7a fix: Table sorting does not take into account tables without a header 2024-09-27 22:12:01 -04:00
Tom Moor ce91071995 fix: post login redirect path not correctly spent, closes #7662 2024-09-26 17:35:13 -04:00
Tom Moor 9b807f7a9e v0.80.2 2024-09-25 22:50:22 -04:00
Tom Moor 17493ca0cf fix: Account for multiple existing OIDC authentication providers, closes #7638 2024-09-25 22:49:48 -04:00
Tom Moor 1d4b05c9f6 fix: Add check for paths in useLastVisitedPath, closes #7655 2024-09-25 22:00:52 -04:00
Tom Moor 8a5e42071f fix: Remove deprecated meta tag 2024-09-25 08:58:17 -04:00
Tom Moor 6b53755f5a fix: Maximum call stack size exceeded, closes #7642 2024-09-24 23:11:58 -04:00
Tom Moor 709e4f44fd fix: Allow team creation without SSO methods 2024-09-24 22:33:53 -04:00
dependabot[bot] c37646b5ad chore(deps): bump bull from 4.12.2 to 4.16.3 (#7649)
Bumps [bull](https://github.com/OptimalBits/bull) from 4.12.2 to 4.16.3.
- [Release notes](https://github.com/OptimalBits/bull/releases)
- [Changelog](https://github.com/OptimalBits/bull/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/OptimalBits/bull/compare/v4.12.2...v4.16.3)

---
updated-dependencies:
- dependency-name: bull
  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-09-23 20:37:56 -07:00
dependabot[bot] 36ca667c50 chore(deps-dev): bump discord-api-types from 0.37.87 to 0.37.101 (#7646)
Bumps [discord-api-types](https://github.com/discordjs/discord-api-types) from 0.37.87 to 0.37.101.
- [Release notes](https://github.com/discordjs/discord-api-types/releases)
- [Changelog](https://github.com/discordjs/discord-api-types/blob/main/CHANGELOG.md)
- [Commits](https://github.com/discordjs/discord-api-types/compare/0.37.87...0.37.101)

---
updated-dependencies:
- dependency-name: discord-api-types
  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-09-23 19:13:38 -07:00
uy/sun 009e66a466 fix: disable bilibili autoplay (#7639) 2024-09-23 04:50:52 -07:00
Tom Moor 7adda26c6d v0.80.1 2024-09-22 13:00:56 -04:00
Tom Moor 62860c593b fix: Version number not updated from previous release, #7635 2024-09-22 13:00:48 -04:00
Tom Moor bdc2357984 Merge branch 'main' of github.com:outline/outline 2024-09-22 12:59:16 -04:00
Tom Moor 4fc1ed0d7e Add release script (#7637) 2024-09-22 09:59:10 -07:00
Tom Moor 5d068361cc fix: Protect against dangerous characters in env 2024-09-22 12:58:49 -04:00
Tom Moor 176cfff7f8 fix: Document editors are sometimes not included in insights/collaboratorIds, closes #7613 2024-09-21 15:30:56 -04:00
Tom Moor 2fd18f7fdb fix: Add resilience for absolute attachment paths in Markdown importer, closes #7512 2024-09-21 14:34:28 -04:00
Tom Moor 34f951c511 fix: Pragmatic fix to drafts count in sidebar, closes #7219 2024-09-21 13:51:46 -04:00
Tom Moor f0c26cf8c8 refactor: Move hooks to correct folder 2024-09-21 13:27:10 -04:00
Translate-O-Tron d77ddbd7de New Crowdin updates (#7595) 2024-09-21 06:31:07 -07:00
Tom Moor 4e1038837b chore: Remove old feature flag
fix: Loading jank when creating new collection
Add italic prop to Text component
2024-09-21 09:03:20 -04:00
Tom Moor c54fcc3536 fix: Improve scroll to comment logic, closes #7435 2024-09-20 23:31:46 -04:00
Tom Moor c4fa63df3d Sticky public access toggle (#7617)
* Sticky public access toggle

* tweak
2024-09-20 18:42:50 -07:00
Tom Moor 2b42ce0c0f fix: document.content column not updated when sending text attribute through API (#7630) 2024-09-20 07:37:42 -07:00
Apoorv Mishra 3208156591 In documents.archived, allow sort: "index" in request (#7628)
* fix: allow sorting by index and filtering by collectionId

* fix: cleanup
2024-09-20 11:29:20 +05:30
Tom Moor e8577ef2a8 chore: Bump prosemirror-view 2024-09-19 20:01:22 -04:00
Tom Moor ca66dec22b Merge branch 'main' of github.com:outline/outline 2024-09-19 20:00:36 -04:00
Tom Moor 41ccad7cce Adds natural behavior of Tab, Shift-Tab in code blocks (#7622)
* Adds proper handling of Tab, Shift-Tab in code blocks

* refactor
2024-09-19 17:00:05 -07:00
Tom Moor bd52b364dd Increase collection rate limit to 50/hour 2024-09-19 09:01:05 -04:00
604 changed files with 18267 additions and 8760 deletions
+9 -8
View File
@@ -4,12 +4,6 @@ defaults: &defaults
working_directory: ~/outline
docker:
- image: cimg/node:20.10
- image: cimg/redis:5.0
- image: cimg/postgres:14.2
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: circle_test
resource_class: large
environment:
NODE_ENV: test
@@ -78,6 +72,14 @@ jobs:
test-server:
<<: *defaults
parallelism: 3
docker:
- image: cimg/node:20.10
- image: cimg/redis:5.0
- image: cimg/postgres:14.2
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: circle_test
steps:
- checkout
- restore_cache:
@@ -108,8 +110,7 @@ jobs:
executor: docker-publisher
steps:
- checkout
- setup_remote_docker:
version: 20.10.6
- setup_remote_docker
- run:
name: Install Docker buildx
command: |
+2 -2
View File
@@ -3,7 +3,7 @@ Business Source License 1.1
Parameters
Licensor: General Outline, Inc.
Licensed Work: Outline 0.79.1
Licensed Work: Outline 0.81.0
The Licensed Work is (c) 2024 General Outline, Inc.
Additional Use Grant: You may make use of the Licensed Work, provided that
you may not use the Licensed Work for a Document
@@ -15,7 +15,7 @@ Additional Use Grant: You may make use of the Licensed Work, provided that
Licensed Work by creating teams and documents
controlled by such third parties.
Change Date: 2028-09-05
Change Date: 2028-11-11
Change License: Apache License, Version 2.0
+92 -11
View File
@@ -1,8 +1,10 @@
import {
ArchiveIcon,
CollectionIcon,
EditIcon,
PadlockIcon,
PlusIcon,
RestoreIcon,
SearchIcon,
ShapesIcon,
StarredIcon,
@@ -10,16 +12,18 @@ import {
UnstarredIcon,
} from "outline-icons";
import * as React from "react";
import { toast } from "sonner";
import stores from "~/stores";
import Collection from "~/models/Collection";
import { CollectionEdit } from "~/components/Collection/CollectionEdit";
import { CollectionNew } from "~/components/Collection/CollectionNew";
import CollectionDeleteDialog from "~/components/CollectionDeleteDialog";
import ConfirmationDialog from "~/components/ConfirmationDialog";
import DynamicCollectionIcon from "~/components/Icons/CollectionIcon";
import SharePopover from "~/components/Sharing/Collection/SharePopover";
import { getHeaderExpandedKey } from "~/components/Sidebar/components/Header";
import { createAction } from "~/actions";
import { CollectionSection } from "~/actions/sections";
import { ActiveCollectionSection, CollectionSection } from "~/actions/sections";
import { setPersistedState } from "~/hooks/usePersistedState";
import history from "~/utils/history";
import { newTemplatePath, searchPath } from "~/utils/routeHelpers";
@@ -70,7 +74,7 @@ export const editCollection = createAction({
name: ({ t, isContextMenu }) =>
isContextMenu ? `${t("Edit")}` : t("Edit collection"),
analyticsName: "Edit collection",
section: CollectionSection,
section: ActiveCollectionSection,
icon: <EditIcon />,
visible: ({ activeCollectionId }) =>
!!activeCollectionId &&
@@ -96,7 +100,7 @@ export const editCollectionPermissions = createAction({
name: ({ t, isContextMenu }) =>
isContextMenu ? `${t("Permissions")}` : t("Collection permissions"),
analyticsName: "Collection permissions",
section: CollectionSection,
section: ActiveCollectionSection,
icon: <PadlockIcon />,
visible: ({ activeCollectionId }) =>
!!activeCollectionId &&
@@ -127,11 +131,22 @@ export const editCollectionPermissions = createAction({
export const searchInCollection = createAction({
name: ({ t }) => t("Search in collection"),
analyticsName: "Search collection",
section: CollectionSection,
section: ActiveCollectionSection,
icon: <SearchIcon />,
visible: ({ activeCollectionId }) =>
!!activeCollectionId &&
stores.policies.abilities(activeCollectionId).readDocument,
visible: ({ activeCollectionId }) => {
if (!activeCollectionId) {
return false;
}
const collection = stores.collections.get(activeCollectionId);
if (!collection?.isActive) {
return false;
}
return stores.policies.abilities(activeCollectionId).readDocument;
},
perform: ({ activeCollectionId }) => {
history.push(searchPath(undefined, { collectionId: activeCollectionId }));
},
@@ -140,7 +155,7 @@ export const searchInCollection = createAction({
export const starCollection = createAction({
name: ({ t }) => t("Star"),
analyticsName: "Star collection",
section: CollectionSection,
section: ActiveCollectionSection,
icon: <StarredIcon />,
keywords: "favorite bookmark",
visible: ({ activeCollectionId }) => {
@@ -167,7 +182,7 @@ export const starCollection = createAction({
export const unstarCollection = createAction({
name: ({ t }) => t("Unstar"),
analyticsName: "Unstar collection",
section: CollectionSection,
section: ActiveCollectionSection,
icon: <UnstarredIcon />,
keywords: "unfavorite unbookmark",
visible: ({ activeCollectionId }) => {
@@ -190,10 +205,76 @@ export const unstarCollection = createAction({
},
});
export const archiveCollection = createAction({
name: ({ t }) => `${t("Archive")}`,
analyticsName: "Archive collection",
section: CollectionSection,
icon: <ArchiveIcon />,
visible: ({ activeCollectionId, stores }) => {
if (!activeCollectionId) {
return false;
}
return !!stores.policies.abilities(activeCollectionId).archive;
},
perform: async ({ activeCollectionId, stores, t }) => {
const { dialogs, collections } = stores;
if (!activeCollectionId) {
return;
}
const collection = collections.get(activeCollectionId);
if (!collection) {
return;
}
dialogs.openModal({
title: t("Archive collection"),
content: (
<ConfirmationDialog
onSubmit={async () => {
await collection.archive();
toast.success(t("Collection archived"));
}}
submitText={t("Archive")}
savingText={`${t("Archiving")}`}
>
{t(
"Archiving this collection will also archive all documents within it. Documents from the collection will no longer be visible in search results."
)}
</ConfirmationDialog>
),
});
},
});
export const restoreCollection = createAction({
name: ({ t }) => t("Restore"),
analyticsName: "Restore collection",
section: CollectionSection,
icon: <RestoreIcon />,
visible: ({ activeCollectionId, stores }) => {
if (!activeCollectionId) {
return false;
}
return !!stores.policies.abilities(activeCollectionId).restore;
},
perform: async ({ activeCollectionId, stores, t }) => {
if (!activeCollectionId) {
return;
}
const collection = stores.collections.get(activeCollectionId);
if (!collection) {
return;
}
await collection.restore();
toast.success(t("Collection restored"));
},
});
export const deleteCollection = createAction({
name: ({ t }) => `${t("Delete")}`,
analyticsName: "Delete collection",
section: CollectionSection,
section: ActiveCollectionSection,
dangerous: true,
icon: <TrashIcon />,
visible: ({ activeCollectionId }) => {
@@ -227,7 +308,7 @@ export const deleteCollection = createAction({
export const createTemplate = createAction({
name: ({ t }) => t("New template"),
analyticsName: "New template",
section: CollectionSection,
section: ActiveCollectionSection,
icon: <ShapesIcon />,
keywords: "new create template",
visible: ({ activeCollectionId }) =>
+36 -3
View File
@@ -1,9 +1,10 @@
import { DoneIcon, TrashIcon } from "outline-icons";
import { DoneIcon, SmileyIcon, TrashIcon } from "outline-icons";
import * as React from "react";
import { toast } from "sonner";
import stores from "~/stores";
import Comment from "~/models/Comment";
import CommentDeleteDialog from "~/components/CommentDeleteDialog";
import ViewReactionsDialog from "~/components/Reactions/ViewReactionsDialog";
import history from "~/utils/history";
import { createAction } from "..";
import { DocumentSection } from "../sections";
@@ -52,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();
@@ -80,11 +85,39 @@ 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();
},
});
export const viewCommentReactionsFactory = ({
comment,
}: {
comment: Comment;
}) =>
createAction({
name: ({ t }) => `${t("View reactions")}`,
analyticsName: "View comment reactions",
section: DocumentSection,
icon: <SmileyIcon />,
visible: () =>
stores.policies.abilities(comment.id).read &&
comment.reactions.length > 0,
perform: ({ t, event }) => {
event?.preventDefault();
event?.stopPropagation();
stores.dialogs.openModal({
title: t("Reactions"),
content: <ViewReactionsDialog model={comment} />,
});
},
});
+164 -70
View File
@@ -27,23 +27,36 @@ import {
CopyIcon,
EyeIcon,
PadlockIcon,
GlobeIcon,
LogoutIcon,
} from "outline-icons";
import * as React from "react";
import { toast } from "sonner";
import { ExportContentType, TeamPreference } from "@shared/types";
import {
ExportContentType,
TeamPreference,
NavigationNode,
} from "@shared/types";
import { getEventFiles } from "@shared/utils/files";
import UserMembership from "~/models/UserMembership";
import DocumentDelete from "~/scenes/DocumentDelete";
import DocumentMove from "~/scenes/DocumentMove";
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";
import { getHeaderExpandedKey } from "~/components/Sidebar/components/Header";
import DocumentTemplatizeDialog from "~/components/TemplatizeDialog";
import { createAction } from "~/actions";
import { DocumentSection, TrashSection } from "~/actions/sections";
import {
ActiveDocumentSection,
DocumentSection,
TrashSection,
} from "~/actions/sections";
import env from "~/env";
import { setPersistedState } from "~/hooks/usePersistedState";
import history from "~/utils/history";
@@ -67,23 +80,24 @@ export const openDocument = createAction({
keywords: "go to",
icon: <DocumentIcon />,
children: ({ stores }) => {
const paths = stores.collections.pathsToDocuments;
const nodes = stores.collections.navigationNodes.reduce(
(acc, node) => [...acc, ...node.children],
[] as NavigationNode[]
);
return paths
.filter((path) => path.type === "document")
.map((path) => ({
// Note: using url which includes the slug rather than id here to bust
// cache if the document is renamed
id: path.url,
name: path.title,
icon: function _Icon() {
return stores.documents.get(path.id)?.isStarred ? (
<StarredIcon />
) : null;
},
section: DocumentSection,
perform: () => history.push(path.url),
}));
return nodes.map((item) => ({
// Note: using url which includes the slug rather than id here to bust
// cache if the document is renamed
id: item.url,
name: item.title,
icon: item.icon ? (
<Icon value={item.icon} color={item.color ?? undefined} />
) : (
<DocumentIcon />
),
section: DocumentSection,
perform: () => history.push(item.url),
}));
},
});
@@ -117,11 +131,30 @@ export const createDocumentFromTemplate = createAction({
section: DocumentSection,
icon: <NewDocumentIcon />,
keywords: "create",
visible: ({ currentTeamId, activeDocumentId, stores }) =>
!!currentTeamId &&
!!activeDocumentId &&
!!stores.documents.get(activeDocumentId)?.template &&
stores.policies.abilities(currentTeamId).createDocument,
visible: ({
currentTeamId,
activeCollectionId,
activeDocumentId,
stores,
}) => {
const document = activeDocumentId
? stores.documents.get(activeDocumentId)
: undefined;
if (
!currentTeamId ||
!document?.isTemplate ||
!!document?.isDraft ||
!!document?.isDeleted
) {
return false;
}
if (activeCollectionId) {
return stores.policies.abilities(activeCollectionId).createDocument;
}
return stores.policies.abilities(currentTeamId).createDocument;
},
perform: ({ activeCollectionId, activeDocumentId, sidebarContext }) =>
history.push(
newDocumentPath(activeCollectionId, { templateId: activeDocumentId }),
@@ -134,7 +167,7 @@ export const createDocumentFromTemplate = createAction({
export const createNestedDocument = createAction({
name: ({ t }) => t("New nested document"),
analyticsName: "New document",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <NewDocumentIcon />,
keywords: "create",
visible: ({ currentTeamId, activeDocumentId, stores }) =>
@@ -151,7 +184,7 @@ export const createNestedDocument = createAction({
export const starDocument = createAction({
name: ({ t }) => t("Star"),
analyticsName: "Star document",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <StarredIcon />,
keywords: "favorite bookmark",
visible: ({ activeDocumentId, stores }) => {
@@ -177,7 +210,7 @@ export const starDocument = createAction({
export const unstarDocument = createAction({
name: ({ t }) => t("Unstar"),
analyticsName: "Unstar document",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <UnstarredIcon />,
keywords: "unfavorite unbookmark",
visible: ({ activeDocumentId, stores }) => {
@@ -203,7 +236,7 @@ export const unstarDocument = createAction({
export const publishDocument = createAction({
name: ({ t }) => t("Publish"),
analyticsName: "Publish document",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <PublishIcon />,
visible: ({ activeDocumentId, stores }) => {
if (!activeDocumentId) {
@@ -245,7 +278,7 @@ export const publishDocument = createAction({
export const unpublishDocument = createAction({
name: ({ t }) => t("Unpublish"),
analyticsName: "Unpublish document",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <UnpublishIcon />,
visible: ({ activeDocumentId, stores }) => {
if (!activeDocumentId) {
@@ -276,7 +309,7 @@ export const unpublishDocument = createAction({
export const subscribeDocument = createAction({
name: ({ t }) => t("Subscribe"),
analyticsName: "Subscribe to document",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <SubscribeIcon />,
visible: ({ activeDocumentId, stores }) => {
if (!activeDocumentId) {
@@ -304,7 +337,7 @@ export const subscribeDocument = createAction({
export const unsubscribeDocument = createAction({
name: ({ t }) => t("Unsubscribe"),
analyticsName: "Unsubscribe from document",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <UnsubscribeIcon />,
visible: ({ activeDocumentId, stores }) => {
if (!activeDocumentId) {
@@ -334,7 +367,7 @@ export const unsubscribeDocument = createAction({
export const shareDocument = createAction({
name: ({ t }) => `${t("Permissions")}`,
analyticsName: "Share document",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <PadlockIcon />,
visible: ({ stores, activeDocumentId }) => {
const can = stores.policies.abilities(activeDocumentId!);
@@ -346,8 +379,6 @@ export const shareDocument = createAction({
}
const document = stores.documents.get(activeDocumentId);
const share = stores.shares.getByDocumentId(activeDocumentId);
const sharedParent = stores.shares.getByDocumentParents(activeDocumentId);
if (!document) {
return;
}
@@ -358,8 +389,6 @@ export const shareDocument = createAction({
content: (
<SharePopover
document={document}
share={share}
sharedParent={sharedParent}
onRequestClose={stores.dialogs.closeAllModals}
visible
/>
@@ -371,7 +400,7 @@ export const shareDocument = createAction({
export const downloadDocumentAsHTML = createAction({
name: ({ t }) => t("HTML"),
analyticsName: "Download document as HTML",
section: DocumentSection,
section: ActiveDocumentSection,
keywords: "html export",
icon: <DownloadIcon />,
iconInContextMenu: false,
@@ -390,7 +419,7 @@ export const downloadDocumentAsHTML = createAction({
export const downloadDocumentAsPDF = createAction({
name: ({ t }) => t("PDF"),
analyticsName: "Download document as PDF",
section: DocumentSection,
section: ActiveDocumentSection,
keywords: "export",
icon: <DownloadIcon />,
iconInContextMenu: false,
@@ -414,7 +443,7 @@ export const downloadDocumentAsPDF = createAction({
export const downloadDocumentAsMarkdown = createAction({
name: ({ t }) => t("Markdown"),
analyticsName: "Download document as Markdown",
section: DocumentSection,
section: ActiveDocumentSection,
keywords: "md markdown export",
icon: <DownloadIcon />,
iconInContextMenu: false,
@@ -434,9 +463,11 @@ export const downloadDocument = createAction({
name: ({ t, isContextMenu }) =>
isContextMenu ? t("Download") : t("Download document"),
analyticsName: "Download document",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <DownloadIcon />,
keywords: "export",
visible: ({ activeDocumentId, stores }) =>
!!activeDocumentId && stores.policies.abilities(activeDocumentId).download,
children: [
downloadDocumentAsHTML,
downloadDocumentAsPDF,
@@ -446,8 +477,10 @@ export const downloadDocument = createAction({
export const copyDocumentAsMarkdown = createAction({
name: ({ t }) => t("Copy as Markdown"),
section: DocumentSection,
section: ActiveDocumentSection,
keywords: "clipboard",
icon: <MarkdownIcon />,
iconInContextMenu: false,
visible: ({ activeDocumentId, stores }) =>
!!activeDocumentId && stores.policies.abilities(activeDocumentId).download,
perform: ({ stores, activeDocumentId, t }) => {
@@ -461,10 +494,33 @@ export const copyDocumentAsMarkdown = createAction({
},
});
export const copyDocumentShareLink = createAction({
name: ({ t }) => t("Copy public link"),
section: ActiveDocumentSection,
keywords: "clipboard share",
icon: <GlobeIcon />,
iconInContextMenu: false,
visible: ({ activeDocumentId, stores }) =>
!!activeDocumentId &&
!!stores.shares.getByDocumentId(activeDocumentId)?.published,
perform: ({ stores, activeDocumentId, t }) => {
if (!activeDocumentId) {
return;
}
const share = stores.shares.getByDocumentId(activeDocumentId);
if (share) {
copy(share.url);
toast.success(t("Link copied to clipboard"));
}
},
});
export const copyDocumentLink = createAction({
name: ({ t }) => t("Copy link"),
section: DocumentSection,
section: ActiveDocumentSection,
keywords: "clipboard",
icon: <CopyIcon />,
iconInContextMenu: false,
visible: ({ activeDocumentId }) => !!activeDocumentId,
perform: ({ stores, activeDocumentId, t }) => {
const document = activeDocumentId
@@ -480,17 +536,17 @@ export const copyDocumentLink = createAction({
export const copyDocument = createAction({
name: ({ t }) => t("Copy"),
analyticsName: "Copy document",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <CopyIcon />,
keywords: "clipboard",
children: [copyDocumentLink, copyDocumentAsMarkdown],
children: [copyDocumentLink, copyDocumentShareLink, copyDocumentAsMarkdown],
});
export const duplicateDocument = createAction({
name: ({ t, isContextMenu }) =>
isContextMenu ? t("Duplicate") : t("Duplicate document"),
analyticsName: "Duplicate document",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <DuplicateIcon />,
keywords: "copy",
visible: ({ activeDocumentId, stores }) =>
@@ -506,7 +562,7 @@ export const duplicateDocument = createAction({
stores.dialogs.openModal({
title: t("Copy document"),
content: (
<DuplicateDialog
<DocumentCopy
document={document}
onSubmit={(response) => {
stores.dialogs.closeAllModals();
@@ -534,7 +590,7 @@ export const pinDocumentToCollection = createAction({
});
},
analyticsName: "Pin document to collection",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <PinIcon />,
iconInContextMenu: false,
visible: ({ activeCollectionId, activeDocumentId, stores }) => {
@@ -570,7 +626,7 @@ export const pinDocumentToCollection = createAction({
export const pinDocumentToHome = createAction({
name: ({ t }) => t("Pin to home"),
analyticsName: "Pin document to home",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <PinIcon />,
iconInContextMenu: false,
visible: ({ activeDocumentId, currentTeamId, stores }) => {
@@ -602,7 +658,7 @@ export const pinDocumentToHome = createAction({
export const pinDocument = createAction({
name: ({ t }) => t("Pin"),
analyticsName: "Pin document",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <PinIcon />,
children: [pinDocumentToCollection, pinDocumentToHome],
});
@@ -610,7 +666,7 @@ export const pinDocument = createAction({
export const searchInDocument = createAction({
name: ({ t }) => t("Search in document"),
analyticsName: "Search document",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <SearchIcon />,
visible: ({ stores, activeDocumentId }) => {
if (!activeDocumentId) {
@@ -628,7 +684,7 @@ export const printDocument = createAction({
name: ({ t, isContextMenu }) =>
isContextMenu ? t("Print") : t("Print document"),
analyticsName: "Print document",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <PrintIcon />,
visible: ({ activeDocumentId }) => !!(activeDocumentId && window.print),
perform: () => {
@@ -687,7 +743,7 @@ export const importDocument = createAction({
export const createTemplateFromDocument = createAction({
name: ({ t }) => t("Templatize"),
analyticsName: "Templatize document",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <ShapesIcon />,
keywords: "new create template",
visible: ({ activeCollectionId, activeDocumentId, stores }) => {
@@ -722,14 +778,14 @@ export const openRandomDocument = createAction({
section: DocumentSection,
icon: <ShuffleIcon />,
perform: ({ stores, activeDocumentId }) => {
const documentPaths = stores.collections.pathsToDocuments.filter(
(path) => path.type === "document" && path.id !== activeDocumentId
);
const randomPath =
documentPaths[Math.round(Math.random() * documentPaths.length)];
const nodes = stores.collections.navigationNodes
.reduce((acc, node) => [...acc, ...node.children], [] as NavigationNode[])
.filter((node) => node.id !== activeDocumentId);
if (randomPath) {
history.push(randomPath.url);
const random = nodes[Math.round(Math.random() * nodes.length)];
if (random) {
history.push(random.url);
}
},
});
@@ -787,7 +843,7 @@ export const moveDocumentToCollection = createAction({
: t("Move");
},
analyticsName: "Move document",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <MoveIcon />,
iconInContextMenu: false,
visible: ({ activeDocumentId, stores }) => {
@@ -816,7 +872,7 @@ export const moveDocumentToCollection = createAction({
export const moveDocument = createAction({
name: ({ t }) => t("Move"),
analyticsName: "Move document",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <MoveIcon />,
visible: ({ activeDocumentId, stores }) => {
if (!activeDocumentId) {
@@ -835,7 +891,7 @@ export const moveDocument = createAction({
export const moveTemplate = createAction({
name: ({ t }) => t("Move"),
analyticsName: "Move document",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <MoveIcon />,
visible: ({ activeDocumentId, stores }) => {
if (!activeDocumentId) {
@@ -854,7 +910,7 @@ export const moveTemplate = createAction({
export const archiveDocument = createAction({
name: ({ t }) => `${t("Archive")}`,
analyticsName: "Archive document",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <ArchiveIcon />,
visible: ({ activeDocumentId, stores }) => {
if (!activeDocumentId) {
@@ -894,7 +950,7 @@ export const archiveDocument = createAction({
export const deleteDocument = createAction({
name: ({ t }) => `${t("Delete")}`,
analyticsName: "Delete document",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <TrashIcon />,
dangerous: true,
visible: ({ activeDocumentId, stores }) => {
@@ -928,7 +984,7 @@ export const deleteDocument = createAction({
export const permanentlyDeleteDocument = createAction({
name: ({ t }) => t("Permanently delete"),
analyticsName: "Permanently delete document",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <CrossIcon />,
dangerous: true,
visible: ({ activeDocumentId, stores }) => {
@@ -983,7 +1039,7 @@ export const permanentlyDeleteDocumentsInTrash = createAction({
export const openDocumentComments = createAction({
name: ({ t }) => t("Comments"),
analyticsName: "Open comments",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <CommentIcon />,
visible: ({ activeDocumentId, stores }) => {
const can = stores.policies.abilities(activeDocumentId ?? "");
@@ -998,14 +1054,14 @@ export const openDocumentComments = createAction({
return;
}
stores.ui.toggleComments(activeDocumentId);
stores.ui.toggleComments();
},
});
export const openDocumentHistory = createAction({
name: ({ t }) => t("History"),
analyticsName: "Open document history",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <HistoryIcon />,
visible: ({ activeDocumentId, stores }) => {
const can = stores.policies.abilities(activeDocumentId ?? "");
@@ -1026,7 +1082,7 @@ export const openDocumentHistory = createAction({
export const openDocumentInsights = createAction({
name: ({ t }) => t("Insights"),
analyticsName: "Open document insights",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <GraphIcon />,
visible: ({ activeDocumentId, stores }) => {
const can = stores.policies.abilities(activeDocumentId ?? "");
@@ -1063,7 +1119,7 @@ export const toggleViewerInsights = createAction({
: t("Enable viewer insights");
},
analyticsName: "Toggle viewer insights",
section: DocumentSection,
section: ActiveDocumentSection,
icon: <EyeIcon />,
visible: ({ activeDocumentId, stores }) => {
const can = stores.policies.abilities(activeDocumentId ?? "");
@@ -1084,6 +1140,42 @@ export const toggleViewerInsights = createAction({
},
});
export const leaveDocument = createAction({
name: ({ t }) => t("Leave document"),
analyticsName: "Leave document",
section: ActiveDocumentSection,
icon: <LogoutIcon />,
visible: ({ currentUserId, activeDocumentId, stores }) => {
const membership = stores.userMemberships.orderedData.find(
(m) => m.documentId === activeDocumentId && m.userId === currentUserId
);
return !!membership;
},
perform: async ({ t, location, currentUserId, activeDocumentId, stores }) => {
if (!activeDocumentId) {
return;
}
const document = stores.documents.get(activeDocumentId);
try {
if (document && location.pathname.startsWith(document.path)) {
history.push(homePath());
}
await stores.userMemberships.delete({
documentId: activeDocumentId,
userId: currentUserId,
} as UserMembership);
toast.success(t("You have left the shared document"));
} catch (err) {
toast.error(t("Could not leave document"));
}
},
});
export const rootDocumentActions = [
openDocument,
archiveDocument,
@@ -1093,6 +1185,7 @@ export const rootDocumentActions = [
importDocument,
downloadDocument,
copyDocumentLink,
copyDocumentShareLink,
copyDocumentAsMarkdown,
starDocument,
unstarDocument,
@@ -1101,6 +1194,7 @@ export const rootDocumentActions = [
subscribeDocument,
unsubscribeDocument,
duplicateDocument,
leaveDocument,
moveTemplateToWorkspace,
moveDocumentToCollection,
openRandomDocument,
+9
View File
@@ -91,6 +91,15 @@ export const navigateToSettings = createAction({
perform: () => history.push(settingsPath()),
});
export const navigateToWorkspaceSettings = createAction({
name: ({ t }) => t("Settings"),
analyticsName: "Navigate to workspace settings",
section: NavigationSection,
icon: <SettingsIcon />,
visible: () => stores.policies.abilities(stores.auth.team?.id || "").update,
perform: () => history.push(settingsPath("details")),
});
export const navigateToProfileSettings = createAction({
name: ({ t }) => t("Profile"),
analyticsName: "Navigate to profile settings",
+6
View File
@@ -98,6 +98,11 @@ export function actionToKBar(
)
: [];
const sectionPriority =
typeof action.section !== "string" && "priority" in action.section
? (action.section.priority as number) ?? 0
: 0;
return [
{
id: action.id,
@@ -108,6 +113,7 @@ export function actionToKBar(
keywords: action.keywords ?? "",
shortcut: action.shortcut || [],
icon: resolvedIcon,
priority: (1 + (action.priority ?? 0)) * (1 + (sectionPriority ?? 0)),
perform: action.perform
? () => performAction(action, context)
: undefined,
+20
View File
@@ -2,10 +2,28 @@ import { ActionContext } from "~/types";
export const CollectionSection = ({ t }: ActionContext) => t("Collection");
export const ActiveCollectionSection = ({ t, stores }: ActionContext) => {
const activeCollection = stores.collections.active;
return `${t("Collection")} · ${activeCollection?.name}`;
};
ActiveCollectionSection.priority = 0.8;
export const DeveloperSection = ({ t }: ActionContext) => t("Debug");
export const DocumentSection = ({ t }: ActionContext) => t("Document");
export const ActiveDocumentSection = ({ t, stores }: ActionContext) => {
const activeDocument = stores.documents.active;
return `${t("Document")} · ${activeDocument?.titleWithDefault}`;
};
ActiveDocumentSection.priority = 0.9;
export const RecentSection = ({ t }: ActionContext) => t("Recently viewed");
RecentSection.priority = 1;
export const RevisionSection = ({ t }: ActionContext) => t("Revision");
export const SettingsSection = ({ t }: ActionContext) => t("Settings");
@@ -21,4 +39,6 @@ export const TeamSection = ({ t }: ActionContext) => t("Workspace");
export const RecentSearchesSection = ({ t }: ActionContext) =>
t("Recent searches");
RecentSearchesSection.priority = -0.1;
export const TrashSection = ({ t }: ActionContext) => t("Trash");
-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);
+5 -13
View File
@@ -1,17 +1,14 @@
import { AnimatePresence } from "framer-motion";
import { observer, useLocalStore } from "mobx-react";
import { observer } from "mobx-react";
import * as React from "react";
import { Switch, Route, useLocation, matchPath } from "react-router-dom";
import { TeamPreference } from "@shared/types";
import ErrorSuspended from "~/scenes/ErrorSuspended";
import DocumentContext from "~/components/DocumentContext";
import type { DocumentContextValue } from "~/components/DocumentContext";
import Layout from "~/components/Layout";
import RegisterKeyDown from "~/components/RegisterKeyDown";
import Sidebar from "~/components/Sidebar";
import SidebarRight from "~/components/Sidebar/Right";
import SettingsSidebar from "~/components/Sidebar/Settings";
import type { Editor as TEditor } from "~/editor";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
@@ -25,6 +22,7 @@ import {
matchDocumentSlug as slug,
matchDocumentInsights,
} from "~/utils/routeHelpers";
import { DocumentContextProvider } from "./DocumentContext";
import Fade from "./Fade";
import { PortalContext } from "./Portal";
@@ -50,12 +48,6 @@ const AuthenticatedLayout: React.FC = ({ children }: Props) => {
const can = usePolicy(ui.activeDocumentId);
const canCollection = usePolicy(ui.activeCollectionId);
const team = useCurrentTeam();
const documentContext = useLocalStore<DocumentContextValue>(() => ({
editor: null,
setEditor: (editor: TEditor) => {
documentContext.editor = editor;
},
}));
const goToSearch = (ev: KeyboardEvent) => {
if (!ev.metaKey && !ev.ctrlKey) {
@@ -102,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 = (
@@ -125,7 +117,7 @@ const AuthenticatedLayout: React.FC = ({ children }: Props) => {
);
return (
<DocumentContext.Provider value={documentContext}>
<DocumentContextProvider>
<PortalContext.Provider value={layoutRef.current}>
<Layout
title={team.name}
@@ -142,7 +134,7 @@ const AuthenticatedLayout: React.FC = ({ children }: Props) => {
</React.Suspense>
</Layout>
</PortalContext.Provider>
</DocumentContext.Provider>
</DocumentContextProvider>
);
};
+10 -11
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,7 +35,7 @@ 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}>
{item.icon}
@@ -67,6 +65,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 +76,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 +86,4 @@ const Item = styled(Link)<{ $highlight: boolean; $withIcon: boolean }>`
}
`;
export default Breadcrumb;
export default React.forwardRef<HTMLDivElement, Props>(Breadcrumb);
+6 -2
View File
@@ -1,5 +1,5 @@
import { LocationDescriptor } from "history";
import { ExpandedIcon } from "outline-icons";
import { DisclosureIcon } from "outline-icons";
import { darken, lighten, transparentize } from "polished";
import * as React from "react";
import styled from "styled-components";
@@ -189,10 +189,14 @@ const Button = <T extends React.ElementType = "button">(
<Inner hasIcon={hasIcon} hasText={hasText} disclosure={disclosure}>
{hasIcon && ic}
{hasText && <Label hasIcon={hasIcon}>{children || value}</Label>}
{disclosure && <ExpandedIcon />}
{disclosure && <StyledDisclosureIcon />}
</Inner>
</RealButton>
);
};
const StyledDisclosureIcon = styled(DisclosureIcon)`
opacity: 0.8;
`;
export default React.forwardRef(Button);
+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);
+10 -13
View File
@@ -19,7 +19,6 @@ import Text from "~/components/Text";
import useBoolean from "~/hooks/useBoolean";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import { EmptySelectValue } from "~/types";
import { Feature, FeatureFlags } from "~/utils/FeatureFlags";
const IconPicker = React.lazy(() => import("~/components/IconPicker"));
@@ -156,18 +155,16 @@ export const CollectionForm = observer(function CollectionForm_({
/>
)}
{team.sharing &&
(!collection ||
FeatureFlags.isEnabled(Feature.newCollectionSharing)) && (
<Switch
id="sharing"
label={t("Public document sharing")}
note={t(
"Allow documents within this collection to be shared publicly on the internet."
)}
{...register("sharing")}
/>
)}
{team.sharing && (
<Switch
id="sharing"
label={t("Public document sharing")}
note={t(
"Allow documents within this collection to be shared publicly on the internet."
)}
{...register("sharing")}
/>
)}
<Flex justify="flex-end">
<Button
+6 -3
View File
@@ -1,7 +1,7 @@
import { runInAction } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import { toast } from "sonner";
import Collection from "~/models/Collection";
import useStores from "~/hooks/useStores";
import history from "~/utils/history";
import { CollectionForm, FormData } from "./CollectionForm";
@@ -17,8 +17,11 @@ export const CollectionNew = observer(function CollectionNew_({
const handleSubmit = React.useCallback(
async (data: FormData) => {
try {
const collection = new Collection(data, collections);
await collection.save();
const collection = await collections.save(data);
// Avoid flash of loading state for the new collection, we know it's empty.
runInAction(() => {
collection.documents = [];
});
onSubmit?.();
history.push(collection.path);
} catch (error) {
+45
View File
@@ -0,0 +1,45 @@
import { ArchiveIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import Collection from "~/models/Collection";
import CollectionIcon from "~/components/Icons/CollectionIcon";
import { MenuInternalLink } from "~/types";
import { archivePath, collectionPath } from "~/utils/routeHelpers";
import Breadcrumb from "./Breadcrumb";
type Props = {
collection: Collection;
};
export const CollectionBreadcrumb: React.FC<Props> = ({ collection }) => {
const { t } = useTranslation();
const items = React.useMemo(() => {
const collectionNode: MenuInternalLink = {
type: "route",
title: collection.name,
icon: <CollectionIcon collection={collection} expanded />,
to: collectionPath(collection.path),
};
const category: MenuInternalLink | undefined = collection.isArchived
? {
type: "route",
icon: <ArchiveIcon />,
title: t("Archive"),
to: archivePath(),
}
: undefined;
const output = [];
if (category) {
output.push(category);
}
output.push(collectionNode);
return output;
}, [collection, t]);
return <Breadcrumb items={items} highlightFirstItem />;
};
+1 -2
View File
@@ -201,7 +201,6 @@ const Input = styled.div`
margin: -8px;
padding: 8px;
border-radius: 8px;
transition: ${s("backgroundTransition")};
&:after {
content: "";
@@ -226,7 +225,7 @@ const Input = styled.div`
}
&[data-editing="true"] {
background: ${s("secondaryBackground")};
background: ${s("backgroundSecondary")};
}
.block-menu-trigger,
@@ -6,20 +6,27 @@ import { Portal } from "react-portal";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import { depths, s } from "@shared/styles";
import CommandBarResults from "~/components/CommandBarResults";
import SearchActions from "~/components/SearchActions";
import rootActions from "~/actions/root";
import useCommandBarActions from "~/hooks/useCommandBarActions";
import useSettingsActions from "~/hooks/useSettingsActions";
import useTemplateActions from "~/hooks/useTemplateActions";
import CommandBarResults from "./CommandBarResults";
import useRecentDocumentActions from "./useRecentDocumentActions";
import useSettingsAction from "./useSettingsAction";
import useTemplatesAction from "./useTemplatesAction";
function CommandBar() {
const { t } = useTranslation();
const settingsActions = useSettingsActions();
const templateActions = useTemplateActions();
const recentDocumentActions = useRecentDocumentActions();
const settingsAction = useSettingsAction();
const templatesAction = useTemplatesAction();
const commandBarActions = React.useMemo(
() => [...rootActions, templateActions, settingsActions],
[settingsActions, templateActions]
() => [
...recentDocumentActions,
...rootActions,
templatesAction,
settingsAction,
],
[recentDocumentActions, settingsAction, templatesAction]
);
useCommandBarActions(commandBarActions);
@@ -30,7 +37,9 @@ function CommandBar() {
<Positioner>
<Animator>
<SearchActions />
<SearchInput defaultPlaceholder={t("Type a command or search")} />
<SearchInput
defaultPlaceholder={`${t("Type a command or search")}`}
/>
<CommandBarResults />
</Animator>
</Positioner>
@@ -60,13 +69,19 @@ const Positioner = styled(KBarPositioner)`
`;
const SearchInput = styled(KBarSearch)`
padding: 16px 20px;
width: 100%;
position: relative;
padding: 16px 12px;
margin: 0 8px;
width: calc(100% - 16px);
outline: none;
border: none;
background: ${s("menuBackground")};
color: ${s("text")};
&:not(:last-child) {
border-bottom: 1px solid ${s("inputBorder")};
}
&:disabled,
&::placeholder {
color: ${s("placeholder")};
@@ -5,7 +5,7 @@ import styled, { css, useTheme } from "styled-components";
import { s, ellipsis } from "@shared/styles";
import Flex from "~/components/Flex";
import Key from "~/components/Key";
import Text from "./Text";
import Text from "~/components/Text";
type Props = {
action: ActionImpl;
@@ -1,12 +1,16 @@
import { useMatches, KBarResults } from "kbar";
import * as React from "react";
import styled from "styled-components";
import { s } from "@shared/styles";
import CommandBarItem from "~/components/CommandBarItem";
import Text from "~/components/Text";
import CommandBarItem from "./CommandBarItem";
export default function CommandBarResults() {
const { results, rootActionId } = useMatches();
if (results.length === 0) {
return null;
}
return (
<Container>
<KBarResults
@@ -14,7 +18,9 @@ export default function CommandBarResults() {
maxHeight={400}
onRender={({ item, active }) =>
typeof item === "string" ? (
<Header>{item}</Header>
<Header type="tertiary" size="xsmall" ellipsis>
{item}
</Header>
) : (
<CommandBarItem
action={item}
@@ -35,11 +41,10 @@ const Container = styled.div`
}
`;
const Header = styled.h3`
font-size: 13px;
letter-spacing: 0.04em;
const Header = styled(Text).attrs({ as: "h3" })`
letter-spacing: 0.03em;
margin: 0;
padding: 16px 0 4px 20px;
color: ${s("textTertiary")};
height: 36px;
cursor: default;
`;
+3
View File
@@ -0,0 +1,3 @@
import CommandBar from "./CommandBar";
export default CommandBar;
@@ -0,0 +1,35 @@
import { DocumentIcon } from "outline-icons";
import * as React from "react";
import Icon from "~/components/Icon";
import { createAction } from "~/actions";
import { RecentSection } from "~/actions/sections";
import useStores from "~/hooks/useStores";
import history from "~/utils/history";
import { documentPath } from "~/utils/routeHelpers";
const useRecentDocumentActions = (count = 6) => {
const { documents, ui } = useStores();
return React.useMemo(
() =>
documents.recentlyViewed
.filter((document) => document.id !== ui.activeDocumentId)
.slice(0, count)
.map((item) =>
createAction({
name: item.titleWithDefault,
analyticsName: "Recently viewed document",
section: RecentSection,
icon: item.icon ? (
<Icon value={item.icon} color={item.color ?? undefined} />
) : (
<DocumentIcon />
),
perform: () => history.push(documentPath(item)),
})
),
[count, ui.activeDocumentId, documents.recentlyViewed]
);
};
export default useRecentDocumentActions;
@@ -2,10 +2,10 @@ import { SettingsIcon } from "outline-icons";
import * as React from "react";
import { createAction } from "~/actions";
import { NavigationSection } from "~/actions/sections";
import useSettingsConfig from "~/hooks/useSettingsConfig";
import history from "~/utils/history";
import useSettingsConfig from "./useSettingsConfig";
const useSettingsActions = () => {
const useSettingsAction = () => {
const config = useSettingsConfig();
const actions = React.useMemo(
() =>
@@ -38,4 +38,4 @@ const useSettingsActions = () => {
return navigateToSettings;
};
export default useSettingsActions;
export default useSettingsAction;
@@ -0,0 +1,89 @@
import { NewDocumentIcon, ShapesIcon } from "outline-icons";
import * as React from "react";
import Icon from "~/components/Icon";
import { createAction } from "~/actions";
import {
ActiveCollectionSection,
DocumentSection,
TeamSection,
} from "~/actions/sections";
import useStores from "~/hooks/useStores";
import history from "~/utils/history";
import { newDocumentPath } from "~/utils/routeHelpers";
const useTemplatesAction = () => {
const { documents } = useStores();
React.useEffect(() => {
void documents.fetchAllTemplates();
}, [documents]);
const actions = React.useMemo(
() =>
documents.templatesAlphabetical.map((template) =>
createAction({
name: template.titleWithDefault,
analyticsName: "New document",
section: template.isWorkspaceTemplate
? TeamSection
: ActiveCollectionSection,
icon: template.icon ? (
<Icon value={template.icon} color={template.color ?? undefined} />
) : (
<NewDocumentIcon />
),
keywords: "create",
visible: ({ currentTeamId, activeCollectionId, stores }) => {
if (activeCollectionId) {
return (
stores.policies.abilities(activeCollectionId).createDocument &&
(template.collectionId === activeCollectionId ||
template.isWorkspaceTemplate)
);
}
return (
!!currentTeamId &&
stores.policies.abilities(currentTeamId).createDocument &&
template.isWorkspaceTemplate
);
},
perform: ({ activeCollectionId, sidebarContext }) =>
history.push(
newDocumentPath(template.collectionId ?? activeCollectionId, {
templateId: template.id,
}),
{
sidebarContext,
}
),
})
),
[documents.templatesAlphabetical]
);
const newFromTemplate = React.useMemo(
() =>
createAction({
id: "templates",
name: ({ t }) => t("New from template"),
placeholder: ({ t }) => t("Choose a template"),
section: DocumentSection,
icon: <ShapesIcon />,
visible: ({ currentTeamId, activeCollectionId, stores }) => {
if (activeCollectionId) {
return stores.policies.abilities(activeCollectionId).createDocument;
}
return (
!!currentTeamId &&
stores.policies.abilities(currentTeamId).createDocument
);
},
children: () => actions,
}),
[actions]
);
return newFromTemplate;
};
export default useTemplatesAction;
+64
View File
@@ -0,0 +1,64 @@
import { observer } from "mobx-react";
import * as React from "react";
import { Trans, useTranslation } from "react-i18next";
import { CollectionPermission, NavigationNode } from "@shared/types";
import type Collection from "~/models/Collection";
import ConfirmationDialog from "~/components/ConfirmationDialog";
import useStores from "~/hooks/useStores";
type Props = {
/** The navigation node to move, must represent a document. */
item: NavigationNode;
/** The collection to move the document to. */
collection: Collection;
/** The parent document to move the document under. */
parentDocumentId?: string | null;
/** The index to move the document to. */
index?: number | null;
};
function ConfirmMoveDialog({ collection, item, ...rest }: Props) {
const { documents, dialogs, collections } = useStores();
const { t } = useTranslation();
const prevCollection = collections.get(item.collectionId!);
const accessMapping: Record<Partial<CollectionPermission> | "null", string> =
{
[CollectionPermission.Admin]: t("manage access"),
[CollectionPermission.ReadWrite]: t("view and edit access"),
[CollectionPermission.Read]: t("view only access"),
null: t("no access"),
};
const handleSubmit = async () => {
await documents.move({
documentId: item.id,
collectionId: collection.id,
...rest,
});
dialogs.closeAllModals();
};
return (
<ConfirmationDialog
onSubmit={handleSubmit}
submitText={t("Move document")}
savingText={`${t("Moving")}`}
>
<Trans
defaults="Moving the document <em>{{ title }}</em> to the {{ newCollectionName }} collection will change permission for all workspace members from <em>{{ prevPermission }}</em> to <em>{{ newPermission }}</em>."
values={{
title: item.title,
prevCollectionName: prevCollection?.name,
newCollectionName: collection.name,
prevPermission: accessMapping[prevCollection?.permission || "null"],
newPermission: accessMapping[collection.permission || "null"],
}}
components={{
em: <strong />,
}}
/>
</ConfirmationDialog>
);
}
export default observer(ConfirmMoveDialog);
+1 -1
View File
@@ -35,7 +35,7 @@ function ConnectionStatus() {
};
const message = ui.multiplayerErrorCode
? codeToMessage[ui.multiplayerErrorCode]
? codeToMessage[ui.multiplayerErrorCode as keyof typeof codeToMessage]
: undefined;
return ui.multiplayerStatus === "connecting" ||
-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;
-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;
}
}
/*
@@ -49,7 +49,7 @@ const DefaultCollectionInputSelect = ({
const options = React.useMemo(
() =>
collections.publicCollections.reduce(
collections.nonPrivate.reduce(
(acc, collection) => [
...acc,
{
@@ -78,7 +78,7 @@ const DefaultCollectionInputSelect = ({
},
]
),
[collections.publicCollections, t]
[collections.nonPrivate, t]
);
if (fetching) {
+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));
+4 -8
View File
@@ -39,6 +39,7 @@ function DocumentCard(props: Props) {
const { collections } = useStores();
const theme = useTheme();
const { document, pin, canUpdatePin, isDraggable } = props;
const pinnedToHome = React.useRef(!pin?.collectionId).current;
const collection = document.collectionId
? collections.get(document.collectionId)
: undefined;
@@ -122,13 +123,13 @@ function DocumentCard(props: Props) {
<Squircle
color={
collection?.color ??
(!pin?.collectionId ? theme.slateLight : theme.slateDark)
(pinnedToHome ? theme.slateLight : theme.slateDark)
}
>
{collection?.icon &&
collection?.icon !== "letter" &&
collection?.icon !== "collection" &&
!pin?.collectionId ? (
pinnedToHome ? (
<CollectionIcon collection={collection} color="white" />
) : (
<DocumentIcon color="white" />
@@ -143,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>
-37
View File
@@ -1,37 +0,0 @@
import * as React from "react";
import { Editor } from "~/editor";
import useIdle from "~/hooks/useIdle";
export type DocumentContextValue = {
/** The current editor instance for this document. */
editor: Editor | null;
/** Set the current editor instance for this document. */
setEditor: (editor: Editor) => void;
};
const DocumentContext = React.createContext<DocumentContextValue>({
editor: null,
// eslint-disable-next-line @typescript-eslint/no-empty-function
setEditor() {},
});
export const useDocumentContext = () => React.useContext(DocumentContext);
const activityEvents = [
"click",
"mousemove",
"DOMMouseScroll",
"mousewheel",
"mousedown",
"touchstart",
"touchmove",
"focus",
];
export const useEditingFocus = () => {
const { editor } = useDocumentContext();
const isIdle = useIdle(3000, activityEvents);
return isIdle && !!editor?.view.hasFocus();
};
export default DocumentContext;
+84
View File
@@ -0,0 +1,84 @@
import { action, computed, observable } from "mobx";
import React, { PropsWithChildren } from "react";
import { Heading } from "@shared/utils/ProsemirrorHelper";
import Document from "~/models/Document";
import { Editor } from "~/editor";
class DocumentContext {
/** The current document */
document?: Document;
/** The editor instance for this document */
editor?: Editor;
@observable
isEditorInitialized: boolean = false;
@observable
headings: Heading[] = [];
@computed
get hasHeadings() {
return this.headings.length > 0;
}
@action
setDocument = (document: Document) => {
this.document = document;
this.updateState();
};
@action
setEditor = (editor: Editor) => {
this.editor = editor;
this.updateState();
};
@action
setEditorInitialized = (initialized: boolean) => {
this.isEditorInitialized = initialized;
};
@action
updateState = () => {
this.updateHeadings();
this.updateTasks();
};
private updateHeadings() {
const currHeadings = this.editor?.getHeadings() ?? [];
const hasChanged =
currHeadings.map((h) => h.level + h.title).join("") !==
this.headings.map((h) => h.level + h.title).join("");
if (hasChanged) {
this.headings = currHeadings;
}
}
private updateTasks() {
const tasks = this.editor?.getTasks() ?? [];
const total = tasks.length ?? 0;
const completed = tasks.filter((t) => t.completed).length ?? 0;
this.document?.updateTasks(total, completed);
}
}
const Context = React.createContext<DocumentContext | null>(null);
export const useDocumentContext = () => {
const ctx = React.useContext(Context);
if (!ctx) {
throw new Error(
"useDocumentContext must be used within DocumentContextProvider"
);
}
return ctx;
};
export const DocumentContextProvider = ({
children,
}: PropsWithChildren<unknown>) => {
const context = React.useMemo(() => new DocumentContext(), []);
return <Context.Provider value={context}>{children}</Context.Provider>;
};
+149
View File
@@ -0,0 +1,149 @@
import flatten from "lodash/flatten";
import { observer } from "mobx-react";
import * as React from "react";
import { Trans, useTranslation } from "react-i18next";
import { toast } from "sonner";
import styled from "styled-components";
import { NavigationNode } from "@shared/types";
import Document from "~/models/Document";
import { FlexContainer, Footer, StyledText } from "~/scenes/DocumentMove";
import Button from "~/components/Button";
import DocumentExplorer from "~/components/DocumentExplorer";
import useCollectionTrees from "~/hooks/useCollectionTrees";
import useStores from "~/hooks/useStores";
import { flattenTree } from "~/utils/tree";
import Switch from "./Switch";
import Text from "./Text";
type Props = {
/** The original document to duplicate */
document: Document;
onSubmit: (documents: Document[]) => void;
};
function DocumentCopy({ document, onSubmit }: Props) {
const { t } = useTranslation();
const { policies } = useStores();
const collectionTrees = useCollectionTrees();
const [publish, setPublish] = React.useState<boolean>(!!document.publishedAt);
const [recursive, setRecursive] = React.useState<boolean>(true);
const [selectedPath, selectPath] = React.useState<NavigationNode | null>(
null
);
const items = React.useMemo(() => {
const nodes = flatten(collectionTrees.map(flattenTree)).filter((node) =>
node.collectionId
? policies.get(node.collectionId)?.abilities.createDocument
: true
);
if (document.isTemplate) {
return nodes
.filter((node) => node.type === "collection")
.map((node) => ({ ...node, children: [] }));
}
return nodes;
}, [policies, collectionTrees, document.isTemplate]);
const handlePublishChange = React.useCallback(
(ev: React.ChangeEvent<HTMLInputElement>) => {
setPublish(ev.target.checked);
},
[]
);
const handleRecursiveChange = React.useCallback(
(ev: React.ChangeEvent<HTMLInputElement>) => {
setRecursive(ev.target.checked);
},
[]
);
const copy = async () => {
if (!selectedPath) {
toast.message(t("Select a location to copy"));
return;
}
try {
const result = await document.duplicate({
publish,
recursive,
title: document.title,
collectionId: selectedPath.collectionId,
...(selectedPath.type === "document"
? { parentDocumentId: selectedPath.id }
: {}),
});
toast.success(t("Document copied"));
onSubmit(result);
} catch (err) {
toast.error(t("Couldnt copy the document, try again?"));
}
};
return (
<FlexContainer column>
<DocumentExplorer
items={items}
onSubmit={copy}
onSelect={selectPath}
defaultValue={document.parentDocumentId || document.collectionId || ""}
/>
<OptionsContainer>
{!document.isTemplate && (
<>
{document.collectionId && (
<Text size="small">
<Switch
name="publish"
label={t("Publish")}
labelPosition="right"
checked={publish}
onChange={handlePublishChange}
/>
</Text>
)}
{document.publishedAt && document.childDocuments.length > 0 && (
<Text size="small">
<Switch
name="recursive"
label={t("Include nested documents")}
labelPosition="right"
checked={recursive}
onChange={handleRecursiveChange}
/>
</Text>
)}
</>
)}
</OptionsContainer>
<Footer justify="space-between" align="center" gap={8}>
<StyledText type="secondary">
{selectedPath ? (
<Trans
defaults="Copy to <em>{{ location }}</em>"
values={{ location: selectedPath.title }}
components={{ em: <strong /> }}
/>
) : (
t("Select a location to copy")
)}
</StyledText>
<Button disabled={!selectedPath} onClick={copy}>
{t("Copy")}
</Button>
</Footer>
</FlexContainer>
);
}
const OptionsContainer = styled.div`
margin: 16px 0 8px 0;
padding-left: 24px;
padding-right: 24px;
`;
export default observer(DocumentCopy);
+29 -7
View File
@@ -11,10 +11,11 @@ import * as React from "react";
import { useTranslation } from "react-i18next";
import AutoSizer from "react-virtualized-auto-sizer";
import { FixedSizeList as List } from "react-window";
import scrollIntoView from "smooth-scroll-into-view-if-needed";
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)
@@ -1,7 +1,7 @@
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import scrollIntoView from "smooth-scroll-into-view-if-needed";
import scrollIntoView from "scroll-into-view-if-needed";
import styled from "styled-components";
import { ellipsis } from "@shared/styles";
import { Node as SearchResult } from "~/components/DocumentExplorerNode";
+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>
)}
+1 -1
View File
@@ -140,7 +140,7 @@ const DocumentMeta: React.FC<Props> = ({
}
const nestedDocumentsCount = collection
? collection.getDocumentChildren(document.id).length
? collection.getChildrenForDocument(document.id).length
: 0;
const canShowProgressBar = isTasks && !isTemplate;
-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);
+15 -39
View File
@@ -9,7 +9,6 @@ import { mergeRefs } from "react-merge-refs";
import { Optional } from "utility-types";
import insertFiles from "@shared/editor/commands/insertFiles";
import { AttachmentPreset } from "@shared/types";
import { Heading } from "@shared/utils/ProsemirrorHelper";
import { dateLocale, dateToRelative } from "@shared/utils/date";
import { getDataTransferFiles } from "@shared/utils/files";
import parseDocumentSlug from "@shared/utils/parseDocumentSlug";
@@ -43,21 +42,14 @@ export type Props = Optional<
> & {
shareId?: string | undefined;
embedsDisabled?: boolean;
onHeadingsChange?: (headings: Heading[]) => void;
onSynced?: () => Promise<void>;
onPublish?: (event: React.MouseEvent) => void;
editorStyle?: React.CSSProperties;
};
function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
const {
id,
shareId,
onChange,
onHeadingsChange,
onCreateCommentMark,
onDeleteCommentMark,
} = props;
const { id, shareId, onChange, onCreateCommentMark, onDeleteCommentMark } =
props;
const userLocale = useUserLocale();
const locale = dateLocale(userLocale);
const { comments, documents } = useStores();
@@ -65,7 +57,6 @@ function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
const embeds = useEmbeds(!shareId);
const localRef = React.useRef<SharedEditor>();
const preferences = useCurrentUser({ rejectOnEmpty: false })?.preferences;
const previousHeadings = React.useRef<Heading[] | null>(null);
const previousCommentIds = React.useRef<string[]>();
const handleSearchLink = React.useCallback(
@@ -107,7 +98,7 @@ function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
}
// default search for anything that doesn't look like a URL
const results = await documents.searchTitles(term);
const results = await documents.searchTitles({ query: term });
return sortBy(
results.map(({ document }) => ({
@@ -212,21 +203,6 @@ function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
[]
);
// Calculate if headings have changed and trigger callback if so
const updateHeadings = React.useCallback(() => {
if (onHeadingsChange) {
const headings = localRef?.current?.getHeadings();
if (
headings &&
headings.map((h) => h.level + h.title).join("") !==
previousHeadings.current?.map((h) => h.level + h.title).join("")
) {
previousHeadings.current = headings;
onHeadingsChange(headings);
}
}
}, [localRef, onHeadingsChange]);
const updateComments = React.useCallback(() => {
if (onCreateCommentMark && onDeleteCommentMark && localRef.current) {
const commentMarks = localRef.current.getComments();
@@ -261,26 +237,25 @@ function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
const handleChange = React.useCallback(
(event) => {
onChange?.(event);
updateHeadings();
updateComments();
},
[onChange, updateComments, updateHeadings]
[onChange, updateComments]
);
const handleRefChanged = React.useCallback(
(node: SharedEditor | null) => {
if (node) {
updateHeadings();
updateComments();
}
},
[updateComments, updateHeadings]
[updateComments]
);
return (
<ErrorBoundary component="div" reloadOnChunkMissing>
<>
<LazyLoadedEditor
key={props.extensions?.length || 0}
ref={mergeRefs([ref, localRef, handleRefChanged])}
uploadFile={handleUploadFile}
embeds={embeds}
@@ -293,14 +268,15 @@ function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
placeholder={props.placeholder || ""}
defaultValue={props.defaultValue || ""}
/>
{props.editorStyle?.paddingBottom && !props.readOnly && (
<ClickablePadding
onClick={focusAtEnd}
onDrop={handleDrop}
onDragOver={handleDragOver}
minHeight={props.editorStyle.paddingBottom}
/>
)}
{props.editorStyle?.paddingBottom &&
(!props.readOnly || props.shareId) && (
<ClickablePadding
onClick={props.readOnly ? undefined : focusAtEnd}
onDrop={props.readOnly ? undefined : handleDrop}
onDragOver={props.readOnly ? undefined : handleDragOver}
minHeight={props.editorStyle.paddingBottom}
/>
)}
</>
</ErrorBoundary>
);
+20
View File
@@ -0,0 +1,20 @@
import styled from "styled-components";
import { s } from "@shared/styles";
type Props = {
/** Width of the containing element. */
width?: number | string;
/** Height of the containing element. */
height?: number | string;
/** Controls the rendered emoji size. */
size?: number;
};
export const Emoji = styled.span<Props>`
font-family: ${s("fontFamilyEmoji")};
width: ${({ width }) =>
typeof width === "string" ? width : width ? `${width}px` : "auto"};
height: ${({ height }) =>
typeof height === "string" ? height : height ? `${height}px` : "auto"};
font-size: ${({ size }) => size && `${size}px`};
`;
+1 -1
View File
@@ -138,7 +138,7 @@ class ErrorBoundary extends React.Component<Props> {
}
const Pre = styled.pre`
background: ${s("secondaryBackground")};
background: ${s("backgroundSecondary")};
padding: 16px;
border-radius: 4px;
font-size: 12px;
+7 -3
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";
@@ -27,7 +28,7 @@ import { documentHistoryPath } from "~/utils/routeHelpers";
type Props = {
document: Document;
event: Event;
event: Event<Document>;
latest?: boolean;
};
@@ -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 -1
View File
@@ -75,7 +75,7 @@ const Image = styled(Flex)`
justify-content: center;
width: 32px;
height: 32px;
background: ${s("secondaryBackground")};
background: ${s("backgroundSecondary")};
border-radius: 32px;
`;
-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;
+59 -38
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,25 +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);
@@ -51,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>
);
}
@@ -128,9 +151,8 @@ const Wrapper = styled(Flex)<WrapperProps>`
`};
padding: 12px;
transition: all 100ms ease-out;
transform: translate3d(0, 0, 0);
min-height: 64px;
min-height: ${HEADER_HEIGHT}px;
justify-content: flex-start;
${draggableOnDesktop()}
@@ -150,7 +172,6 @@ const Wrapper = styled(Flex)<WrapperProps>`
${breakpoint("tablet")`
padding: 16px;
justify-content: center;
${(props: WrapperProps) => props.$insetTitleAdjust && `padding-left: 64px;`}
`};
`;
@@ -189,4 +210,4 @@ const MobileMenuButton = styled(Button)`
}
`;
export default observer(Header);
export default observer(React.forwardRef(Header));
+1 -1
View File
@@ -61,7 +61,7 @@ export const Label = styled(Text).attrs({ size: "xsmall", weight: "bold" })<{
color?: string;
}>`
background-color: ${(props) =>
props.color ?? props.theme.secondaryBackground};
props.color ?? props.theme.backgroundSecondary};
color: ${(props) =>
props.color ? getTextColor(props.color) : props.theme.text};
width: fit-content;
@@ -125,6 +125,7 @@ function HoverPreviewDesktop({ element, data, dataLoading, onClose }: Props) {
avatarUrl={data.avatarUrl}
color={data.color}
lastActive={data.lastActive}
email={data.email}
/>
) : data.type === UnfurlResourceType.Document ? (
<HoverPreviewDocument
@@ -7,7 +7,7 @@ import { Preview, Title, Info, Card, CardContent } from "./Components";
type Props = Omit<UnfurlResponse[UnfurlResourceType.Mention], "type">;
const HoverPreviewMention = React.forwardRef(function _HoverPreviewMention(
{ avatarUrl, name, lastActive, color }: Props,
{ avatarUrl, name, lastActive, color, email }: Props,
ref: React.Ref<HTMLDivElement>
) {
return (
@@ -25,6 +25,7 @@ const HoverPreviewMention = React.forwardRef(function _HoverPreviewMention(
/>
<Flex column gap={2} justify="center">
<Title>{name}</Title>
{email && <Info>{email}</Info>}
<Info>{lastActive}</Info>
</Flex>
</Flex>
+1 -7
View File
@@ -2,7 +2,6 @@ import { observer } from "mobx-react";
import { getLuminance } from "polished";
import * as React from "react";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import { IconType } from "@shared/types";
import { IconLibrary } from "@shared/utils/IconLibrary";
import { colorPalette } from "@shared/utils/collections";
@@ -118,12 +117,7 @@ export const IconTitleWrapper = styled(Flex)<{ dir?: string }>`
z-index: 1;
${(props: { dir?: string }) =>
props.dir === "rtl" ? "right: -40px" : "left: -40px"};
${breakpoint("desktop")`
${(props: { dir?: string }) =>
props.dir === "rtl" ? "right: -44px" : "left: -44px"};
`}
props.dir === "rtl" ? "right: -44px" : "left: -44px"};
`;
export default Icon;
@@ -1,8 +0,0 @@
import styled from "styled-components";
import { s } from "@shared/styles";
export const Emoji = styled.span`
font-family: ${s("fontFamilyEmoji")};
width: 24px;
height: 24px;
`;
@@ -18,11 +18,7 @@ import {
import GridTemplate, { DataNode } from "./GridTemplate";
import SkinTonePicker from "./SkinTonePicker";
/**
* This is needed as a constant for react-window.
* Calculated from the heights of TabPanel and InputSearch.
*/
const GRID_HEIGHT = 362;
const GRID_HEIGHT = 410;
const useEmojiState = () => {
const [emojiSkinTone, setEmojiSkinTone] = usePersistedState<EmojiSkinTone>(
@@ -80,6 +76,7 @@ type Props = {
panelWidth: number;
query: string;
panelActive: boolean;
height?: number;
onEmojiChange: (emoji: string) => void;
onQueryChange: (query: string) => void;
};
@@ -90,6 +87,7 @@ const EmojiPanel = ({
panelActive,
onEmojiChange,
onQueryChange,
height = GRID_HEIGHT,
}: Props) => {
const { t } = useTranslation();
@@ -159,7 +157,7 @@ const EmojiPanel = ({
<GridTemplate
ref={scrollableRef}
width={panelWidth}
height={GRID_HEIGHT}
height={height - 48}
data={templateData}
onIconSelect={handleEmojiSelection}
/>
@@ -4,9 +4,9 @@ import React from "react";
import styled from "styled-components";
import { IconType } from "@shared/types";
import { IconLibrary } from "@shared/utils/IconLibrary";
import { Emoji } from "~/components/Emoji";
import Text from "~/components/Text";
import { TRANSLATED_CATEGORIES } from "../utils";
import { Emoji } from "./Emoji";
import Grid from "./Grid";
import { IconButton } from "./IconButton";
@@ -71,7 +71,7 @@ const GridTemplate = (
<IconButton
key={item.name}
onClick={() => onIconSelect({ id: item.name, value: item.name })}
delay={item.delay}
style={{ "--delay": `${item.delay}ms` } as React.CSSProperties}
>
<Icon as={IconLibrary.getComponent(item.name)} color={item.color}>
{item.initial}
@@ -85,7 +85,9 @@ const GridTemplate = (
key={item.id}
onClick={() => onIconSelect({ id: item.id, value: item.value })}
>
<Emoji>{item.value}</Emoji>
<Emoji width={24} height={24}>
{item.value}
</Emoji>
</IconButton>
);
});
@@ -7,7 +7,6 @@ export const IconButton = styled(NudeButton)<{ delay?: number }>`
width: 32px;
height: 32px;
padding: 4px;
--delay: ${({ delay }) => delay && `${delay}ms`};
&: ${hover} {
background: ${s("listItemHoverBackground")};
@@ -5,10 +5,10 @@ import styled from "styled-components";
import { depths, s } from "@shared/styles";
import { EmojiSkinTone } from "@shared/types";
import { getEmojiVariants } from "@shared/utils/emoji";
import { Emoji } from "~/components/Emoji";
import Flex from "~/components/Flex";
import NudeButton from "~/components/NudeButton";
import { hover } from "~/styles";
import { Emoji } from "./Emoji";
import { IconButton } from "./IconButton";
const SkinTonePicker = ({
@@ -26,7 +26,7 @@ const SkinTonePicker = ({
);
const menu = useMenuState({
placement: "bottom",
placement: "bottom-end",
});
const handleSkinClick = React.useCallback(
@@ -43,7 +43,9 @@ const SkinTonePicker = ({
<MenuItem {...menu} key={emoji.value}>
{(menuprops) => (
<IconButton {...menuprops} onClick={() => handleSkinClick(eskin)}>
<Emoji>{emoji.value}</Emoji>
<Emoji width={24} height={24}>
{emoji.value}
</Emoji>
</IconButton>
)}
</MenuItem>
+18 -17
View File
@@ -82,6 +82,7 @@ const IconPicker = ({
modal: true,
unstable_offset: [0, 0],
});
const { hide, show, visible } = popover;
const tab = useTabState({ selectedId: defaultTab });
const previouslyVisible = usePrevious(popover.visible);
@@ -96,12 +97,12 @@ const IconPicker = ({
const handleIconChange = React.useCallback(
(ic: string) => {
popover.hide();
hide();
const icType = determineIconType(ic);
const finalColor = icType === IconType.SVG ? chosenColor : null;
onChange(ic, finalColor);
},
[popover, onChange, chosenColor]
[hide, onChange, chosenColor]
);
const handleIconColorChange = React.useCallback(
@@ -118,32 +119,32 @@ const IconPicker = ({
);
const handleIconRemove = React.useCallback(() => {
popover.hide();
hide();
onChange(null, null);
}, [popover, onChange]);
}, [hide, onChange]);
const handlePopoverButtonClick = React.useCallback(
(ev: React.MouseEvent) => {
ev.stopPropagation();
if (popover.visible) {
popover.hide();
if (visible) {
hide();
} else {
popover.show();
show();
}
},
[popover]
[hide, show, visible]
);
// Popover open effect
React.useEffect(() => {
if (popover.visible && !previouslyVisible) {
if (visible && !previouslyVisible) {
onOpen?.();
} else if (!popover.visible && previouslyVisible) {
} else if (!visible && previouslyVisible) {
onClose?.();
setQuery("");
resetDefaultTab();
}
}, [popover.visible, previouslyVisible, onOpen, onClose, resetDefaultTab]);
}, [visible, previouslyVisible, onOpen, onClose, resetDefaultTab]);
// Custom click outside handling rather than using `hideOnClickOutside` from reakit so that we can
// prevent event bubbling.
@@ -198,7 +199,7 @@ const IconPicker = ({
{...tab}
id={TAB_NAMES["Icon"]}
aria-label={t("Icons")}
active={tab.selectedId === TAB_NAMES["Icon"]}
$active={tab.selectedId === TAB_NAMES["Icon"]}
>
{t("Icons")}
</StyledTab>
@@ -206,7 +207,7 @@ const IconPicker = ({
{...tab}
id={TAB_NAMES["Emoji"]}
aria-label={t("Emojis")}
active={tab.selectedId === TAB_NAMES["Emoji"]}
$active={tab.selectedId === TAB_NAMES["Emoji"]}
>
{t("Emojis")}
</StyledTab>
@@ -273,7 +274,7 @@ const TabActionsWrapper = styled(Flex)`
border-bottom: 1px solid ${s("inputBorder")};
`;
const StyledTab = styled(Tab)<{ active: boolean }>`
const StyledTab = styled(Tab)<{ $active: boolean }>`
position: relative;
font-weight: 500;
font-size: 14px;
@@ -282,15 +283,15 @@ const StyledTab = styled(Tab)<{ active: boolean }>`
border: 0;
padding: 8px 12px;
user-select: none;
color: ${({ active }) => (active ? s("textSecondary") : s("textTertiary"))};
color: ${({ $active }) => ($active ? s("textSecondary") : s("textTertiary"))};
transition: color 100ms ease-in-out;
&: ${hover} {
color: ${s("textSecondary")};
}
${({ active }) =>
active &&
${({ $active }) =>
$active &&
css`
&:after {
content: "";
+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";
+3 -3
View File
@@ -10,7 +10,7 @@ import * as React from "react";
import { VisuallyHidden } from "reakit/VisuallyHidden";
import styled, { css } from "styled-components";
import { s } from "@shared/styles";
import Button, { Inner } from "~/components/Button";
import Button, { Props as ButtonProps, Inner } from "~/components/Button";
import Text from "~/components/Text";
import useMenuHeight from "~/hooks/useMenuHeight";
import useMobile from "~/hooks/useMobile";
@@ -33,7 +33,7 @@ export type Option = {
divider?: boolean;
};
export type Props = {
export type Props = Omit<ButtonProps<any>, "onChange"> & {
id?: string;
name?: string;
value?: string | null;
@@ -313,7 +313,7 @@ const StyledButton = styled(Button)<{ $nude?: boolean }>`
margin-bottom: 16px;
display: block;
width: 100%;
cursor: default;
cursor: var(--pointer);
&:hover:not(:disabled) {
background: ${s("buttonNeutralBackground")};
+7 -1
View File
@@ -1,6 +1,8 @@
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { $Diff } from "utility-types";
import { s } from "@shared/styles";
import { CollectionPermission } from "@shared/types";
import { EmptySelectValue } from "~/types";
import InputSelect, { Props, Option, InputSelectRef } from "./InputSelect";
@@ -19,7 +21,7 @@ function InputSelectPermission(
const { t } = useTranslation();
return (
<InputSelect
<Select
ref={ref}
label={t("Permission")}
options={[
@@ -45,4 +47,8 @@ function InputSelectPermission(
);
}
const Select = styled(InputSelect)`
color: ${s("textSecondary")};
`;
export default React.forwardRef(InputSelectPermission);
+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%;
+2 -2
View File
@@ -4,7 +4,7 @@ import {
} from "@getoutline/react-roving-tabindex";
import { LocationDescriptor } from "history";
import * as React from "react";
import scrollIntoView from "smooth-scroll-into-view-if-needed";
import scrollIntoView from "scroll-into-view-if-needed";
import styled, { useTheme } from "styled-components";
import { s, ellipsis } from "@shared/styles";
import Flex from "~/components/Flex";
@@ -192,7 +192,7 @@ const Wrapper = styled.a<{
&:focus,
&:focus-within {
background: ${(props) =>
props.$hover ? props.theme.secondaryBackground : "inherit"};
props.$hover ? props.theme.backgroundSecondary : "inherit"};
}
cursor: ${(props) =>
+7 -6
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,14 +36,16 @@ const LocaleTime: React.FC<Props> = ({
shorten,
format,
relative,
tooltipDelay,
}: Props) => {
const userLocale: string = useUserLocale() || "";
const dateFormatLong = {
const userLocale = useUserLocale();
const dateFormatLong: Record<string, string> = {
en_US: "MMMM do, yyyy h:mm a",
fr_FR: "'Le 'd MMMM yyyy 'à' H:mm",
};
const formatLocaleLong = dateFormatLong[userLocale] ?? "MMMM do, yyyy h:mm a";
const formatLocaleLong =
(userLocale ? dateFormatLong[userLocale] : undefined) ??
"MMMM do, yyyy h:mm a";
// @ts-expect-error fallback to formatLocaleLong
const formatLocale = format?.[userLocale] ?? formatLocaleLong;
const [_, setMinutesMounted] = React.useState(0); // eslint-disable-line @typescript-eslint/no-unused-vars
const callback = React.useRef<() => void>();
@@ -79,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 && (
@@ -24,13 +24,15 @@ import NotificationListItem from "./NotificationListItem";
type Props = {
/** Callback when the notification panel wants to close. */
onRequestClose: () => void;
/** Whether the panel is open or not. */
isOpen: boolean;
};
/**
* A panel containing a list of notifications and controls to manage them.
*/
function Notifications(
{ onRequestClose }: Props,
{ onRequestClose, isOpen }: Props,
ref: React.RefObject<HTMLDivElement>
) {
const context = useActionContext();
@@ -58,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>
@@ -72,7 +74,7 @@ function Notifications(
<PaginatedList
fetch={notifications.fetchPage}
options={{ archived: false }}
items={notifications.orderedData}
items={isOpen ? notifications.orderedData : undefined}
renderItem={(item: Notification) => (
<NotificationListItem
key={item.id}
@@ -40,7 +40,11 @@ const NotificationsPopover: React.FC = ({ children }: Props) => {
shrink
flex
>
<Notifications onRequestClose={popover.hide} ref={scrollableRef} />
<Notifications
onRequestClose={popover.hide}
isOpen={popover.visible}
ref={scrollableRef}
/>
</StyledPopover>
</>
);
+5 -3
View File
@@ -6,9 +6,11 @@ import PaginatedList from "~/components/PaginatedList";
import EventListItem from "./EventListItem";
type Props = {
events: Event[];
events: Event<Document>[];
document: Document;
fetch: (options: Record<string, any> | undefined) => Promise<Event[]>;
fetch: (
options: Record<string, any> | undefined
) => Promise<Event<Document>[]>;
options?: Record<string, any>;
heading?: React.ReactNode;
empty?: React.ReactNode;
@@ -30,7 +32,7 @@ const PaginatedEventList = React.memo<Props>(function PaginatedEventList({
heading={heading}
fetch={fetch}
options={options}
renderItem={(item: Event, index) => (
renderItem={(item: Event<Document>, index) => (
<EventListItem
key={item.id}
event={item}
+2 -1
View File
@@ -19,7 +19,8 @@ export interface PaginatedItem {
}
type Props<T> = WithTranslation &
RootStore & {
RootStore &
React.HTMLAttributes<HTMLDivElement> & {
fetch?: (
options: Record<string, any> | undefined
) => Promise<T[] | undefined> | undefined;
+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
+172
View File
@@ -0,0 +1,172 @@
import { observer } from "mobx-react";
import { transparentize } from "polished";
import React from "react";
import { useTranslation } from "react-i18next";
import styled, { css } from "styled-components";
import { s } from "@shared/styles";
import type { ReactionSummary } from "@shared/types";
import { getEmojiId } from "@shared/utils/emoji";
import User from "~/models/User";
import { Emoji } from "~/components/Emoji";
import Flex from "~/components/Flex";
import NudeButton from "~/components/NudeButton";
import Text from "~/components/Text";
import Tooltip from "~/components/Tooltip";
import useCurrentUser from "~/hooks/useCurrentUser";
import { hover } from "~/styles";
type Props = {
/** Thin reaction data - contains the emoji & active user ids for this reaction. */
reaction: ReactionSummary;
/** Users who reacted using this emoji. */
reactedUsers: User[];
/** Whether the emoji button should be disabled (prevents add/remove events). */
disabled: boolean;
/** Callback when the user intends to add the reaction. */
onAddReaction: (emoji: string) => Promise<void>;
/** Callback when the user intends to remove the reaction. */
onRemoveReaction: (emoji: string) => Promise<void>;
};
const useTooltipContent = ({
reactedUsers,
currUser,
emoji,
active,
}: {
reactedUsers: User[];
currUser: User;
emoji: string;
active: boolean;
}) => {
const { t } = useTranslation();
if (!reactedUsers.length) {
return;
}
const transformedEmoji = `:${getEmojiId(emoji)}:`;
switch (reactedUsers.length) {
case 1: {
return t("{{ username }} reacted with {{ emoji }}", {
username: active ? t("You") : reactedUsers[0].name,
emoji: transformedEmoji,
});
}
case 2: {
const firstUsername = active ? t("You") : reactedUsers[0].name;
const secondUsername = active
? reactedUsers.find((user) => user.id !== currUser.id)?.name
: reactedUsers[1].name;
return t(
"{{ firstUsername }} and {{ secondUsername }} reacted with {{ emoji }}",
{
firstUsername,
secondUsername,
emoji: transformedEmoji,
}
);
}
default: {
const firstUsername = active ? t("You") : reactedUsers[0].name;
const count = reactedUsers.length - 1;
return t(
"{{ firstUsername }} and {{ count }} others reacted with {{ emoji }}",
{
firstUsername,
count,
emoji: transformedEmoji,
}
);
}
}
};
const Reaction: React.FC<Props> = ({
reaction,
reactedUsers,
disabled,
onAddReaction,
onRemoveReaction,
}) => {
const user = useCurrentUser();
const active = reaction.userIds.includes(user.id);
const tooltipContent = useTooltipContent({
reactedUsers,
currUser: user,
emoji: reaction.emoji,
active,
});
const handleClick = React.useCallback(
(event: React.SyntheticEvent<HTMLButtonElement>) => {
event.stopPropagation();
active
? void onRemoveReaction(reaction.emoji)
: void onAddReaction(reaction.emoji);
},
[reaction, active, onAddReaction, onRemoveReaction]
);
const DisplayedEmoji = React.useMemo(
() => (
<EmojiButton disabled={disabled} $active={active} onClick={handleClick}>
<Flex gap={6} justify="center" align="center">
<Emoji size={15}>{reaction.emoji}</Emoji>
<Count weight="xbold">{reaction.userIds.length}</Count>
</Flex>
</EmojiButton>
),
[reaction.emoji, reaction.userIds, disabled, active, handleClick]
);
return tooltipContent ? (
<Tooltip content={tooltipContent} placement="bottom">
{DisplayedEmoji}
</Tooltip>
) : (
<>{DisplayedEmoji}</>
);
};
const EmojiButton = styled(NudeButton)<{
$active: boolean;
disabled: boolean;
}>`
width: auto;
height: 28px;
padding: 6px;
border-radius: 12px;
background: ${s("backgroundTertiary")};
pointer-events: ${({ disabled }) => disabled && "none"};
&: ${hover} {
background: ${s("backgroundQuaternary")};
}
${(props) =>
props.$active &&
css`
background: ${transparentize(0.7, props.theme.accent)};
&: ${hover} {
background: ${transparentize(0.5, props.theme.accent)};
}
`}
`;
const Count = styled(Text)`
font-size: 11px;
color: ${s("buttonNeutralText")};
padding-right: 1px;
font-variant-numeric: tabular-nums;
`;
export default observer(Reaction);
+87
View File
@@ -0,0 +1,87 @@
import compact from "lodash/compact";
import { observer } from "mobx-react";
import React from "react";
import Comment from "~/models/Comment";
import useHover from "~/hooks/useHover";
import useStores from "~/hooks/useStores";
import Logger from "~/utils/Logger";
import Flex from "../Flex";
import { ResizingHeightContainer } from "../ResizingHeightContainer";
import Reaction from "./Reaction";
type Props = {
/** Model for which to show the reactions. */
model: Comment;
/** Callback when the user intends to add a reaction. */
onAddReaction: (emoji: string) => Promise<void>;
/** Callback when the user intends to remove a reaction. */
onRemoveReaction: (emoji: string) => Promise<void>;
/** classname generated by styled-components. */
className?: string;
/** Picker to render as the last element */
picker?: React.ReactElement;
};
const ReactionList: React.FC<Props> = ({
model,
onAddReaction,
onRemoveReaction,
className,
picker,
}) => {
const { users } = useStores();
const listRef = React.useRef<HTMLDivElement>(null);
const hovered = useHover({
ref: listRef,
duration: 250,
});
React.useEffect(() => {
const loadReactedUsersData = async () => {
try {
await model.loadReactedUsersData();
} catch (err) {
Logger.warn("Could not prefetch reaction data");
}
};
if (hovered) {
void loadReactedUsersData();
}
}, [hovered, model]);
const hasReactions = !!model.reactions.length;
const style = React.useMemo(() => {
if (hasReactions) {
return { minHeight: 28 };
}
return undefined;
}, [hasReactions]);
return (
<ResizingHeightContainer style={style}>
<Flex ref={listRef} className={className} align="center" gap={6} wrap>
{model.reactions.map((reaction) => {
const reactedUsers = compact(
reaction.userIds.map((id) => users.get(id))
);
return (
<Reaction
key={reaction.emoji}
reaction={reaction}
reactedUsers={reactedUsers}
disabled={model.isResolved}
onAddReaction={onAddReaction}
onRemoveReaction={onRemoveReaction}
/>
);
})}
{picker}
</Flex>
</ResizingHeightContainer>
);
};
export default observer(ReactionList);
+156
View File
@@ -0,0 +1,156 @@
import { ReactionIcon } from "outline-icons";
import React from "react";
import { useTranslation } from "react-i18next";
import { PopoverDisclosure, usePopoverState } from "reakit";
import EventBoundary from "@shared/components/EventBoundary";
import Flex from "~/components/Flex";
import NudeButton from "~/components/NudeButton";
import PlaceholderText from "~/components/PlaceholderText";
import Popover from "~/components/Popover";
import useMobile from "~/hooks/useMobile";
import useOnClickOutside from "~/hooks/useOnClickOutside";
import useWindowSize from "~/hooks/useWindowSize";
import Tooltip from "../Tooltip";
const EmojiPanel = React.lazy(
() => import("~/components/IconPicker/components/EmojiPanel")
);
type Props = {
/** Callback when an emoji is selected by the user. */
onSelect: (emoji: string) => Promise<void>;
/** Callback when the picker is opened. */
onOpen?: () => void;
/** Callback when the picker is closed. */
onClose?: () => void;
/** Optional classname. */
className?: string;
size?: number;
};
const ReactionPicker: React.FC<Props> = ({
onSelect,
onOpen,
onClose,
className,
size,
}) => {
const { t } = useTranslation();
const popover = usePopoverState({
modal: true,
unstable_offset: [0, 0],
placement: "bottom-end",
});
const { width: windowWidth } = useWindowSize();
const isMobile = useMobile();
const [query, setQuery] = React.useState("");
const contentRef = React.useRef<HTMLDivElement | null>(null);
const popoverWidth = isMobile ? windowWidth : 300;
// In mobile, popover is absolutely positioned to leave 8px on both sides.
const panelWidth = isMobile ? windowWidth - 16 : popoverWidth;
const { toggle, hide } = popover;
const handlePopoverButtonClick = React.useCallback(
(ev: React.MouseEvent) => {
ev.stopPropagation();
toggle();
},
[toggle]
);
const handleEmojiSelect = React.useCallback(
(emoji: string) => {
hide();
void onSelect(emoji);
},
[hide, onSelect]
);
// Popover open effect
React.useEffect(() => {
if (popover.visible) {
onOpen?.();
} else {
onClose?.();
}
}, [popover.visible, onOpen, onClose]);
// Custom click outside handling rather than using `hideOnClickOutside` from reakit so that we can
// prevent event bubbling.
useOnClickOutside(
contentRef,
(event) => {
if (
popover.visible &&
!popover.unstable_disclosureRef.current?.contains(event.target as Node)
) {
event.stopPropagation();
event.preventDefault();
popover.hide();
}
},
{ capture: true }
);
return (
<>
<PopoverDisclosure {...popover}>
{(props) => (
<Tooltip content={t("Add reaction")} placement="top" hideOnClick>
<NudeButton
{...props}
aria-label={t("Reaction picker")}
className={className}
onClick={handlePopoverButtonClick}
size={size}
>
<ReactionIcon size={22} />
</NudeButton>
</Tooltip>
)}
</PopoverDisclosure>
<Popover
{...popover}
ref={contentRef}
width={popoverWidth}
shrink
aria-label={t("Reaction picker")}
onClick={(e) => e.stopPropagation()}
hideOnClickOutside={false}
>
{popover.visible && (
<React.Suspense fallback={<Placeholder />}>
<EventBoundary>
<EmojiPanel
height={300}
panelWidth={panelWidth}
query={query}
onEmojiChange={handleEmojiSelect}
onQueryChange={setQuery}
panelActive
/>
</EventBoundary>
</React.Suspense>
)}
</Popover>
</>
);
};
const Placeholder = React.memo(
() => (
<Flex column gap={6} style={{ height: "300px", padding: "6px 12px" }}>
<Flex gap={8}>
<PlaceholderText height={32} minWidth={90} />
<PlaceholderText height={32} width={32} />
</Flex>
<PlaceholderText height={24} width={120} />
</Flex>
),
() => true
);
Placeholder.displayName = "ReactionPickerPlaceholder";
export default ReactionPicker;
@@ -0,0 +1,146 @@
import compact from "lodash/compact";
import { observer } from "mobx-react";
import React from "react";
import { useTranslation } from "react-i18next";
import { Tab, TabPanel, useTabState } from "reakit";
import { toast } from "sonner";
import styled, { css } from "styled-components";
import { s } from "@shared/styles";
import Comment from "~/models/Comment";
import { Avatar, AvatarSize } from "~/components/Avatar";
import { Emoji } from "~/components/Emoji";
import Flex from "~/components/Flex";
import PlaceholderText from "~/components/PlaceholderText";
import Text from "~/components/Text";
import useStores from "~/hooks/useStores";
import { hover } from "~/styles";
type Props = {
/** Model for which to show the reactions. */
model: Comment;
};
const ViewReactionsDialog: React.FC<Props> = ({ model }) => {
const { t } = useTranslation();
const { users } = useStores();
const tab = useTabState();
const { reactedUsersLoaded } = model;
React.useEffect(() => {
const loadReactedUsersData = async () => {
try {
await model.loadReactedUsersData();
} catch (err) {
toast.error(t("Could not load reactions"));
}
};
void loadReactedUsersData();
}, [t, model]);
if (!reactedUsersLoaded) {
return <PlaceHolder />;
}
return (
<>
<TabActionsWrapper>
{model.reactions.map((reaction) => (
<StyledTab
{...tab}
key={reaction.emoji}
id={reaction.emoji}
aria-label={t("Reaction")}
$active={tab.selectedId === reaction.emoji}
>
<Emoji size={16}>{reaction.emoji}</Emoji>
</StyledTab>
))}
</TabActionsWrapper>
{model.reactions.map((reaction) => {
const reactedUsers = compact(
reaction.userIds.map((id) => users.get(id))
);
return (
<StyledTabPanel {...tab} key={reaction.emoji}>
{reactedUsers.map((user) => (
<UserInfo key={user.name} align="center" gap={8}>
<Avatar model={user} size={AvatarSize.Medium} />
<Text size="medium">{user.name}</Text>
</UserInfo>
))}
</StyledTabPanel>
);
})}
</>
);
};
const PlaceHolder = React.memo(
() => (
<>
<TabActionsWrapper gap={8} style={{ paddingBottom: "10px" }}>
<PlaceholderText width={40} height={32} />
<PlaceholderText width={40} height={32} />
</TabActionsWrapper>
<UserInfo align="center" gap={12}>
<PlaceholderText width={AvatarSize.Medium} height={AvatarSize.Medium} />
<PlaceholderText height={34} />
</UserInfo>
<UserInfo align="center" gap={12}>
<PlaceholderText width={AvatarSize.Medium} height={AvatarSize.Medium} />
<PlaceholderText height={34} />
</UserInfo>
</>
),
() => true
);
PlaceHolder.displayName = "ViewReactionsPlaceholder";
const TabActionsWrapper = styled(Flex)`
border-bottom: 1px solid ${s("inputBorder")};
`;
const StyledTab = styled(Tab)<{ $active: boolean }>`
position: relative;
font-weight: 500;
font-size: 14px;
cursor: var(--pointer);
background: none;
border: 0;
border-radius: 4px 4px 0 0;
padding: 8px 12px 10px;
user-select: none;
transition: background-color 100ms ease;
&: ${hover} {
background-color: ${s("listItemHoverBackground")};
}
${({ $active }) =>
$active &&
css`
&:after {
content: "";
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 1px;
background: ${s("textSecondary")};
}
`}
`;
const StyledTabPanel = styled(TabPanel)`
height: 300px;
padding: 5px 0;
overflow-y: auto;
`;
const UserInfo = styled(Flex)`
padding: 10px 8px;
`;
export default observer(ViewReactionsDialog);
+1 -1
View File
@@ -10,7 +10,7 @@ export default function SearchActions() {
const { searches } = useStores();
React.useEffect(() => {
if (!searches.isLoaded) {
if (!searches.isLoaded && !searches.isFetching) {
void searches.fetchPage({
source: "app",
});
+5 -2
View File
@@ -19,9 +19,10 @@ import SearchListItem from "./SearchListItem";
interface Props extends React.HTMLAttributes<HTMLInputElement> {
shareId: string;
className?: string;
}
function SearchPopover({ shareId }: Props) {
function SearchPopover({ shareId, className }: Props) {
const { t } = useTranslation();
const { documents } = useStores();
const focusRef = React.useRef<HTMLElement | null>(null);
@@ -54,7 +55,8 @@ function SearchPopover({ shareId }: Props) {
const performSearch = React.useCallback(
async ({ query, ...options }) => {
if (query?.length > 0) {
const response = await documents.search(query, {
const response = await documents.search({
query,
shareId,
...options,
});
@@ -186,6 +188,7 @@ function SearchPopover({ shareId }: Props) {
onChange={handleSearchInputChange}
onFocus={handleSearchInputFocus}
onKeyDown={handleKeyDown}
className={className}
/>
)}
</PopoverDisclosure>
@@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next";
import styled, { useTheme } from "styled-components";
import Squircle from "@shared/components/Squircle";
import { Pagination } from "@shared/constants";
import { s } from "@shared/styles";
import { CollectionPermission, IconType } from "@shared/types";
import { determineIconType } from "@shared/utils/icon";
import type Collection from "~/models/Collection";
@@ -67,7 +68,7 @@ export const AccessControlList = observer(
const containerRef = React.useRef<HTMLDivElement | null>(null);
const { maxHeight, calcMaxHeight } = useMaxHeight({
elementRef: containerRef,
maxViewportPercentage: 70,
maxViewportPercentage: 65,
margin: 24,
});
@@ -201,7 +202,7 @@ export const AccessControlList = observer(
</>
)}
{team.sharing && can.share && !collectionSharingDisabled && visible && (
<>
<Sticky>
{document.members.length ? <Separator /> : null}
<PublicAccess
document={document}
@@ -209,7 +210,7 @@ export const AccessControlList = observer(
sharedParent={sharedParent}
onRequestClose={onRequestClose}
/>
</>
</Sticky>
)}
</ScrollableContainer>
);
@@ -274,6 +275,12 @@ function useUsersInCollection(collection?: Collection) {
: false;
}
const Sticky = styled.div`
background: ${s("menuBackground")};
position: sticky;
bottom: -12px;
`;
const ScrollableContainer = styled(Scrollable)`
padding: 12px 24px;
margin: -12px -24px;
@@ -1,13 +1,13 @@
import invariant from "invariant";
import debounce from "lodash/debounce";
import isEmpty from "lodash/isEmpty";
import { observer } from "mobx-react";
import { CopyIcon, GlobeIcon, InfoIcon } from "outline-icons";
import { CopyIcon, GlobeIcon, InfoIcon, QuestionMarkIcon } from "outline-icons";
import * as React from "react";
import { Trans, useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { toast } from "sonner";
import styled, { useTheme } from "styled-components";
import Flex from "@shared/components/Flex";
import Squircle from "@shared/components/Squircle";
import { s } from "@shared/styles";
import { UrlHelper } from "@shared/utils/UrlHelper";
@@ -17,7 +17,6 @@ import Input, { NativeInput } from "~/components/Input";
import Switch from "~/components/Switch";
import env from "~/env";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
import { AvatarSize } from "../../Avatar";
import CopyToClipboard from "../../CopyToClipboard";
import NudeButton from "../../NudeButton";
@@ -39,7 +38,6 @@ type Props = {
};
function PublicAccess({ document, share, sharedParent }: Props) {
const { shares } = useStores();
const { t } = useTranslation();
const theme = useTheme();
const [validationError, setValidationError] = React.useState("");
@@ -53,20 +51,30 @@ function PublicAccess({ document, share, sharedParent }: Props) {
setUrlId(share?.urlId);
}, [share?.urlId]);
const handleIndexingChanged = React.useCallback(
async (event) => {
try {
await share?.save({
allowIndexing: event.currentTarget.checked,
});
} catch (err) {
toast.error(err.message);
}
},
[share]
);
const handlePublishedChange = React.useCallback(
async (event) => {
const share = shares.getByDocumentId(document.id);
invariant(share, "Share must exist");
try {
await share.save({
await share?.save({
published: event.currentTarget.checked,
});
} catch (err) {
toast.error(err.message);
}
},
[document.id, shares]
[share]
);
const handleUrlChange = React.useMemo(
@@ -111,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} />
@@ -159,6 +167,32 @@ function PublicAccess({ document, share, sharedParent }: Props) {
/>
<ResizingHeightContainer>
{share?.published && (
<ListItem
title={
<Text type="tertiary" as={Flex}>
{t("Search engine indexing")}&nbsp;
<Tooltip
content={t(
"Disable this setting to discourage search engines from indexing the page"
)}
>
<QuestionMarkIcon size={18} />
</Tooltip>
</Text>
}
actions={
<Switch
aria-label={t("Search engine indexing")}
checked={share?.allowIndexing ?? false}
onChange={handleIndexingChanged}
width={26}
height={14}
/>
}
/>
)}
{sharedParent?.published ? (
<ShareLinkInput type="text" disabled defaultValue={shareUrl}>
{copyButton}
@@ -203,7 +237,7 @@ const StyledInfoIcon = styled(InfoIcon)`
`;
const Wrapper = styled.div`
margin-bottom: 8px;
padding-bottom: 8px;
`;
const DomainPrefix = styled.span`
@@ -8,7 +8,6 @@ import { toast } from "sonner";
import { DocumentPermission } from "@shared/types";
import Document from "~/models/Document";
import Group from "~/models/Group";
import Share from "~/models/Share";
import User from "~/models/User";
import { Avatar, GroupAvatar, AvatarSize } from "~/components/Avatar";
import NudeButton from "~/components/NudeButton";
@@ -32,26 +31,19 @@ import { AccessControlList } from "./AccessControlList";
type Props = {
/** The document to share. */
document: Document;
/** The existing share model, if any. */
share: Share | null | undefined;
/** The existing share parent model, if any. */
sharedParent: Share | null | undefined;
/** Callback fired when the popover requests to be closed. */
onRequestClose: () => void;
/** Whether the popover is visible. */
visible: boolean;
};
function SharePopover({
document,
share,
sharedParent,
onRequestClose,
visible,
}: Props) {
function SharePopover({ document, onRequestClose, visible }: Props) {
const team = useCurrentTeam();
const { t } = useTranslation();
const can = usePolicy(document);
const { shares } = useStores();
const share = shares.getByDocumentId(document.id);
const sharedParent = shares.getByDocumentParents(document.id);
const [hasRendered, setHasRendered] = React.useState(visible);
const { users, userMemberships, groups, groupMemberships } = useStores();
const [query, setQuery] = React.useState("");
@@ -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 -1
View File
@@ -15,7 +15,7 @@ export const Wrapper = styled.div`
export const Separator = styled.div`
border-top: 1px dashed ${s("divider")};
margin: 12px 0;
margin: 8px 0;
`;
export const HeaderInput = styled(Flex)`
+45 -37
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"
@@ -94,36 +93,40 @@ function AppSidebar() {
</SidebarButton>
)}
</OrganizationMenu>
<Section>
<SidebarLink
to={homePath()}
icon={<HomeIcon />}
exact={false}
label={t("Home")}
/>
<SidebarLink
to={searchPath()}
icon={<SearchIcon />}
label={t("Search")}
exact={false}
/>
{can.createDocument && (
<Overflow>
<Section>
<SidebarLink
to={draftsPath()}
icon={<DraftsIcon />}
label={
<Flex align="center" justify="space-between">
{t("Drafts")}
{documents.totalDrafts > 0 ? (
<Drafts size="xsmall" type="tertiary">
{documents.totalDrafts}
</Drafts>
) : null}
</Flex>
}
to={homePath()}
icon={<HomeIcon />}
exact={false}
label={t("Home")}
/>
)}
</Section>
<SidebarLink
to={searchPath()}
icon={<SearchIcon />}
label={t("Search")}
exact={false}
/>
{can.createDocument && (
<SidebarLink
to={draftsPath()}
icon={<DraftsIcon />}
label={
<Flex align="center" justify="space-between">
{t("Drafts")}
{documents.totalDrafts > 0 ? (
<Drafts size="xsmall" type="tertiary">
{documents.totalDrafts > 25
? "25+"
: documents.totalDrafts}
</Drafts>
) : null}
</Flex>
}
/>
)}
</Section>
</Overflow>
<Scrollable flex shadow>
<Section>
<Starred />
@@ -131,16 +134,16 @@ function AppSidebar() {
<Section>
<SharedWithMe />
</Section>
<Section auto>
<Section>
<Collections />
</Section>
{can.createDocument && (
<Section auto>
<ArchiveLink />
</Section>
)}
<Section>
{can.createDocument && (
<>
<ArchiveLink />
<TrashLink />
</>
)}
{can.createDocument && <TrashLink />}
<SidebarAction action={inviteUser} />
</Section>
</Scrollable>
@@ -150,6 +153,11 @@ function AppSidebar() {
);
}
const Overflow = styled.div`
overflow: hidden;
flex-shrink: 0;
`;
const Drafts = styled(Text)`
margin: 0 4px;
`;
+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 />}
+77 -13
View File
@@ -1,12 +1,17 @@
import { observer } from "mobx-react";
import { SidebarIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { NavigationNode } from "@shared/types";
import { metaDisplay } from "@shared/utils/keyboard";
import Flex from "~/components/Flex";
import Scrollable from "~/components/Scrollable";
import SearchPopover from "~/components/SearchPopover";
import Tooltip from "~/components/Tooltip";
import useCurrentUser from "~/hooks/useCurrentUser";
import useStores from "~/hooks/useStores";
import { hover } from "~/styles";
import history from "~/utils/history";
import { homePath, sharedDocumentPath } from "~/utils/routeHelpers";
import { useTeamContext } from "../TeamContext";
@@ -15,6 +20,7 @@ import Sidebar from "./Sidebar";
import Section from "./components/Section";
import DocumentLink from "./components/SharedDocumentLink";
import SidebarButton from "./components/SidebarButton";
import ToggleButton from "./components/ToggleButton";
type Props = {
rootNode: NavigationNode;
@@ -27,9 +33,11 @@ function SharedSidebar({ rootNode, shareId }: Props) {
const { ui, documents } = useStores();
const { t } = useTranslation();
const teamAvailable = !!team?.name;
return (
<Sidebar>
{team?.name && (
<StyledSidebar $hoverTransition={!teamAvailable}>
{teamAvailable && (
<SidebarButton
title={team.name}
image={<TeamLogo model={team} size={32} alt={t("Logo")} />}
@@ -38,11 +46,20 @@ function SharedSidebar({ rootNode, shareId }: Props) {
user ? homePath() : sharedDocumentPath(shareId, rootNode.url)
)
}
/>
>
<ToggleSidebar />
</SidebarButton>
)}
<ScrollContainer topShadow flex>
<TopSection>
<SearchPopover shareId={shareId} />
<SearchWrapper>
<StyledSearchPopover shareId={shareId} />
</SearchWrapper>
{!teamAvailable && (
<ToggleWrapper>
<ToggleSidebar />
</ToggleWrapper>
)}
</TopSection>
<Section>
<DocumentLink
@@ -55,23 +72,70 @@ function SharedSidebar({ rootNode, shareId }: Props) {
/>
</Section>
</ScrollContainer>
</Sidebar>
</StyledSidebar>
);
}
const ToggleSidebar = () => {
const { t } = useTranslation();
const { ui } = useStores();
return (
<Tooltip content={t("Toggle sidebar")} shortcut={`${metaDisplay}+.`}>
<ToggleButton
position="bottom"
image={<SidebarIcon />}
onClick={() => {
ui.toggleCollapsedSidebar();
(document.activeElement as HTMLElement)?.blur();
}}
/>
</Tooltip>
);
};
const ScrollContainer = styled(Scrollable)`
padding-bottom: 16px;
`;
const TopSection = styled(Section)`
// this weird looking && increases the specificity of the style rule
&&:first-child {
margin-top: 16px;
}
const TopSection = styled(Flex)`
padding: 8px;
flex-shrink: 0;
`;
&& {
margin-bottom: 16px;
}
const SearchWrapper = styled.div`
width: 100%;
`;
const StyledSearchPopover = styled(SearchPopover)`
width: 100%;
transition: width 100ms ease-out;
margin: 8px 0;
`;
const ToggleWrapper = styled.div`
position: absolute;
right: 0;
opacity: 0;
transform: translateX(10px);
transition: opacity 100ms ease-out, transform 100ms ease-out;
`;
const StyledSidebar = styled(Sidebar)<{ $hoverTransition: boolean }>`
${({ $hoverTransition }) =>
$hoverTransition &&
`
&: ${hover} {
${StyledSearchPopover} {
width: 85%;
}
${ToggleWrapper} {
opacity: 1;
transform: translateX(0);
}
}
`}
`;
export default observer(SharedSidebar);
+63 -34
View File
@@ -8,6 +8,7 @@ import { Avatar } from "~/components/Avatar";
import Flex from "~/components/Flex";
import useCurrentUser from "~/hooks/useCurrentUser";
import useMenuContext from "~/hooks/useMenuContext";
import useMobile from "~/hooks/useMobile";
import usePrevious from "~/hooks/usePrevious";
import useStores from "~/hooks/useStores";
import AccountMenu from "~/menus/AccountMenu";
@@ -16,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";
@@ -39,12 +41,12 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
const previousLocation = usePrevious(location);
const { isMenuOpen } = useMenuContext();
const user = useCurrentUser({ rejectOnEmpty: false });
const isMobile = useMobile();
const width = ui.sidebarWidth;
const collapsed = ui.sidebarIsClosed && !isMenuOpen;
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);
@@ -60,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(() => {
@@ -84,17 +86,25 @@ 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);
}, []);
const handleMouseDown = React.useCallback(
(event) => {
event.preventDefault();
if (!document.hasFocus()) {
return;
}
setOffset(event.pageX - width);
setResizing(true);
setAnimating(false);
@@ -102,9 +112,9 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
[width]
);
const handlePointerMove = React.useCallback(() => {
const handlePointerActivity = React.useCallback(() => {
if (ui.sidebarIsClosed) {
setHovering(true);
setHovering(document.hasFocus());
setPointerMoved(true);
}
}, [ui.sidebarIsClosed]);
@@ -113,7 +123,10 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
(ev) => {
if (hasPointerMoved) {
setHovering(
ev.pageX < width && ev.pageY < window.innerHeight && ev.pageY > 0
document.hasFocus() &&
ev.pageX < width &&
ev.pageY < window.innerHeight &&
ev.pageY > 0
);
}
},
@@ -136,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) {
@@ -151,14 +164,17 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
document.body.style.cursor = "initial";
}
window.addEventListener("blur", handleBlur);
return () => {
window.removeEventListener("blur", handleBlur);
document.removeEventListener("mousemove", handleDrag);
document.removeEventListener("mouseup", handleStopDrag);
};
}, [isResizing, handleDrag, handleStopDrag]);
}, [isResizing, handleDrag, handleBlur, handleStopDrag]);
const handleReset = React.useCallback(() => {
ui.setSidebarWidth(theme.sidebarWidth);
ui.set({ sidebarWidth: theme.sidebarWidth });
}, [ui, theme.sidebarWidth]);
React.useEffect(() => {
@@ -179,7 +195,7 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
);
return (
<>
<TooltipProvider>
<Container
ref={ref}
style={style}
@@ -189,8 +205,10 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
$isSmallerThanMinimum={isSmallerThanMinimum}
$mobileSidebarVisible={ui.mobileSidebarVisible}
$collapsed={collapsed}
$isMobile={isMobile}
className={className}
onPointerMove={handlePointerMove}
onPointerDown={handlePointerActivity}
onPointerMove={handlePointerActivity}
onPointerLeave={handlePointerLeave}
column
>
@@ -214,15 +232,7 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
/>
}
>
<NotificationsPopover>
{(rest: SidebarButtonProps) => (
<SidebarButton
{...rest}
position="bottom"
image={<NotificationIcon />}
/>
)}
</NotificationsPopover>
<Notifications />
</SidebarButton>
)}
</AccountMenu>
@@ -233,10 +243,18 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
/>
</Container>
{ui.mobileSidebarVisible && <Backdrop onClick={ui.toggleMobileSidebar} />}
</>
</TooltipProvider>
);
});
const Notifications = () => (
<NotificationsPopover>
{(rest: SidebarButtonProps) => (
<SidebarButton {...rest} position="bottom" image={<NotificationIcon />} />
)}
</NotificationsPopover>
);
const Backdrop = styled.a`
animation: ${fadeIn} 250ms ease-in-out;
position: fixed;
@@ -256,6 +274,7 @@ type ContainerProps = {
$isHovering: boolean;
$collapsed: boolean;
$hidden: boolean;
$isMobile: boolean;
};
const hoverStyles = (props: ContainerProps) => `
@@ -280,9 +299,8 @@ const Container = styled(Flex)<ContainerProps>`
width: 100%;
background: ${s("sidebarBackground")};
transition: box-shadow 150ms ease-in-out, transform 150ms ease-out,
${s("backgroundTransition")}
${(props: ContainerProps) =>
props.$isAnimating ? `,width ${ANIMATION_MS}ms ease-out` : ""};
${(props: ContainerProps) =>
props.$isAnimating ? `,width ${ANIMATION_MS}ms ease-out` : ""};
transform: translateX(
${(props) => (props.$mobileSidebarVisible ? 0 : "-100%")}
);
@@ -298,8 +316,19 @@ const Container = styled(Flex)<ContainerProps>`
& > div {
transition: opacity 150ms ease-in-out;
opacity: ${(props) =>
props.$hidden || (props.$collapsed && !props.$isHovering) ? "0" : "1"};
opacity: ${(props) => {
if (props.$hidden) {
return "0";
}
if (props.$isHovering) {
return "1";
}
if (props.$isMobile) {
return props.$mobileSidebarVisible ? "1" : "0";
} else {
return props.$collapsed ? "0" : "1";
}
}};
}
${breakpoint("tablet")`
@@ -1,41 +1,101 @@
import isUndefined from "lodash/isUndefined";
import { observer } from "mobx-react";
import { ArchiveIcon } from "outline-icons";
import * as React from "react";
import { useDrop } from "react-dnd";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import Flex from "@shared/components/Flex";
import Collection from "~/models/Collection";
import PaginatedList from "~/components/PaginatedList";
import useRequest from "~/hooks/useRequest";
import useStores from "~/hooks/useStores";
import { archivePath } from "~/utils/routeHelpers";
import SidebarLink, { DragObject } from "./SidebarLink";
import { useDropToArchive } from "../hooks/useDragAndDrop";
import { ArchivedCollectionLink } from "./ArchivedCollectionLink";
import { StyledError } from "./Collections";
import PlaceholderCollections from "./PlaceholderCollections";
import Relative from "./Relative";
import SidebarLink from "./SidebarLink";
function ArchiveLink() {
const { policies, documents } = useStores();
const { collections } = useStores();
const { t } = useTranslation();
const [{ isDocumentDropping }, dropToArchiveDocument] = useDrop({
accept: "document",
drop: async (item: DragObject) => {
const document = documents.get(item.id);
await document?.archive();
toast.success(t("Document archived"));
},
canDrop: (item) => policies.abilities(item.id).archive,
collect: (monitor) => ({
isDocumentDropping: monitor.isOver(),
}),
});
const [disclosure, setDisclosure] = React.useState<boolean>(false);
const [expanded, setExpanded] = React.useState<boolean | undefined>();
const { request, data, loading, error } = useRequest(
collections.fetchArchived,
true
);
React.useEffect(() => {
if (!isUndefined(data) && !loading && isUndefined(error)) {
setDisclosure(data.length > 0);
}
}, [data, loading, error]);
React.useEffect(() => {
setDisclosure(collections.archived.length > 0);
}, [collections.archived]);
React.useEffect(() => {
if (disclosure && isUndefined(expanded)) {
setExpanded(false);
}
}, [disclosure]);
React.useEffect(() => {
if (expanded) {
void request();
}
}, [expanded, request]);
const handleDisclosureClick = React.useCallback((ev) => {
ev.preventDefault();
ev.stopPropagation();
setExpanded((e) => !e);
}, []);
const handleClick = React.useCallback(() => {
setExpanded(true);
}, []);
const [{ isOverArchiveSection, isDragging }, dropToArchiveRef] =
useDropToArchive();
return (
<div ref={dropToArchiveDocument}>
<SidebarLink
to={archivePath()}
icon={<ArchiveIcon open={isDocumentDropping} />}
exact={false}
label={t("Archive")}
active={documents.active?.isArchived && !documents.active?.isDeleted}
isActiveDrop={isDocumentDropping}
/>
</div>
<Flex column>
<div ref={dropToArchiveRef}>
<SidebarLink
to={archivePath()}
icon={<ArchiveIcon open={isOverArchiveSection && isDragging} />}
exact={false}
label={t("Archive")}
isActiveDrop={isOverArchiveSection && isDragging}
depth={0}
expanded={disclosure ? expanded : undefined}
onDisclosureClick={handleDisclosureClick}
onClick={handleClick}
/>
</div>
{expanded === true ? (
<Relative>
<PaginatedList
aria-label={t("Archived collections")}
items={collections.archived}
loading={<PlaceholderCollections />}
renderError={(props) => <StyledError {...props} />}
renderItem={(item: Collection) => (
<ArchivedCollectionLink
key={item.id}
depth={1}
collection={item}
/>
)}
/>
</Relative>
) : null}
</Flex>
);
}
@@ -0,0 +1,47 @@
import * as React from "react";
import Collection from "~/models/Collection";
import useStores from "~/hooks/useStores";
import CollectionLink from "./CollectionLink";
import CollectionLinkChildren from "./CollectionLinkChildren";
import Relative from "./Relative";
type Props = {
collection: Collection;
depth?: number;
};
export function ArchivedCollectionLink({ collection, depth }: Props) {
const { documents } = useStores();
const [expanded, setExpanded] = React.useState(false);
const handleDisclosureClick = React.useCallback((ev) => {
ev.preventDefault();
ev.stopPropagation();
setExpanded((e) => !e);
}, []);
const handleClick = React.useCallback(() => {
setExpanded(true);
}, []);
return (
<>
<CollectionLink
depth={depth ? depth : 0}
collection={collection}
expanded={expanded}
activeDocument={documents.active}
onDisclosureClick={handleDisclosureClick}
onClick={handleClick}
/>
<Relative>
<CollectionLinkChildren
collection={collection}
expanded={expanded}
prefetchDocument={documents.prefetchDocument}
/>
</Relative>
</>
);
}
@@ -4,11 +4,10 @@ import { PlusIcon } from "outline-icons";
import * as React from "react";
import { useDrop } from "react-dnd";
import { useTranslation } from "react-i18next";
import { useHistory } from "react-router-dom";
import { CollectionValidation } from "@shared/validations";
import Collection from "~/models/Collection";
import Document from "~/models/Document";
import DocumentReparent from "~/scenes/DocumentReparent";
import ConfirmMoveDialog from "~/components/ConfirmMoveDialog";
import Fade from "~/components/Fade";
import CollectionIcon from "~/components/Icons/CollectionIcon";
import NudeButton from "~/components/NudeButton";
@@ -30,6 +29,8 @@ type Props = {
onDisclosureClick: (ev?: React.MouseEvent<HTMLButtonElement>) => void;
activeDocument: Document | undefined;
isDraggingAnyCollection?: boolean;
depth?: number;
onClick?: () => void;
};
const CollectionLink: React.FC<Props> = ({
@@ -37,13 +38,14 @@ const CollectionLink: React.FC<Props> = ({
expanded,
onDisclosureClick,
isDraggingAnyCollection,
depth,
onClick,
}: Props) => {
const { dialogs, documents, collections } = useStores();
const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean();
const [isEditing, setIsEditing] = React.useState(false);
const can = usePolicy(collection);
const { t } = useTranslation();
const history = useHistory();
const sidebarContext = useSidebarContext();
const editableTitleRef = React.useRef<RefHandle>(null);
@@ -52,9 +54,8 @@ const CollectionLink: React.FC<Props> = ({
await collection.save({
name,
});
history.replace(collection.path, history.location.state);
},
[collection, history]
[collection]
);
// Drop to re-parent document
@@ -78,20 +79,12 @@ const CollectionLink: React.FC<Props> = ({
if (
prevCollection &&
prevCollection.permission === null &&
prevCollection.permission !== collection.permission &&
!document?.isDraft
) {
dialogs.openModal({
title: t("Move document"),
content: (
<DocumentReparent
item={item}
collection={collection}
onSubmit={dialogs.closeAllModals}
onCancel={dialogs.closeAllModals}
/>
),
title: t("Change permissions?"),
content: <ConfirmMoveDialog item={item} collection={collection} />,
});
} else {
await documents.move({ documentId: id, collectionId: collection.id });
@@ -119,10 +112,15 @@ const CollectionLink: React.FC<Props> = ({
sidebarContext,
});
const handleRename = React.useCallback(() => {
editableTitleRef.current?.setIsEditing(true);
}, [editableTitleRef]);
return (
<Relative ref={drop}>
<DropToImport collectionId={collection.id}>
<SidebarLink
onClick={onClick}
to={{
pathname: collection.path,
state: { sidebarContext },
@@ -148,7 +146,7 @@ const CollectionLink: React.FC<Props> = ({
/>
}
exact={false}
depth={0}
depth={depth ? depth : 0}
menu={
!isEditing &&
!isDraggingAnyCollection && (
@@ -163,7 +161,7 @@ const CollectionLink: React.FC<Props> = ({
</NudeButton>
<CollectionMenu
collection={collection}
onRename={() => editableTitleRef.current?.setIsEditing(true)}
onRename={handleRename}
onOpen={handleMenuOpen}
onClose={handleMenuClose}
/>
@@ -2,25 +2,31 @@ import { observer } from "mobx-react";
import * as React from "react";
import { useDrop } from "react-dnd";
import { useTranslation } from "react-i18next";
import { Waypoint } from "react-waypoint";
import { toast } from "sonner";
import styled from "styled-components";
import Collection from "~/models/Collection";
import Document from "~/models/Document";
import ConfirmMoveDialog from "~/components/ConfirmMoveDialog";
import DocumentsLoader from "~/components/DocumentsLoader";
import { ResizingHeightContainer } from "~/components/ResizingHeightContainer";
import Text from "~/components/Text";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
import history from "~/utils/history";
import useCollectionDocuments from "../hooks/useCollectionDocuments";
import DocumentLink from "./DocumentLink";
import DropCursor from "./DropCursor";
import EmptyCollectionPlaceholder from "./EmptyCollectionPlaceholder";
import Folder from "./Folder";
import PlaceholderCollections from "./PlaceholderCollections";
import { DragObject } from "./SidebarLink";
import useCollectionDocuments from "./useCollectionDocuments";
import SidebarLink, { DragObject } from "./SidebarLink";
type Props = {
/** The collection to render the children of. */
collection: Collection;
/** Whether the children are shown in an expanded state. */
expanded: boolean;
/** Function to prefetch a document by ID. */
prefetchDocument?: (documentId: string) => Promise<Document | void>;
};
@@ -29,12 +35,13 @@ function CollectionLinkChildren({
expanded,
prefetchDocument,
}: Props) {
const pageSize = 250;
const can = usePolicy(collection);
const manualSort = collection.sort.field === "index";
const { documents } = useStores();
const { documents, dialogs, collections } = useStores();
const { t } = useTranslation();
const childDocuments = useCollectionDocuments(collection, documents.active);
const [showing, setShowing] = React.useState(pageSize);
// Drop to reorder document
const [{ isOverReorder, isDraggingAnyDocument }, dropToReorder] = useDrop({
@@ -52,11 +59,26 @@ function CollectionLinkChildren({
if (!collection) {
return;
}
void documents.move({
documentId: item.id,
collectionId: collection.id,
index: 0,
});
const prevCollection = collections.get(item.collectionId);
if (
prevCollection &&
prevCollection.permission !== collection.permission
) {
dialogs.openModal({
title: t("Change permissions?"),
content: (
<ConfirmMoveDialog item={item} collection={collection} index={0} />
),
});
} else {
void documents.move({
documentId: item.id,
collectionId: collection.id,
index: 0,
});
}
},
collect: (monitor) => ({
isOverReorder: !!monitor.isOver(),
@@ -64,6 +86,18 @@ function CollectionLinkChildren({
}),
});
React.useEffect(() => {
if (!expanded) {
setShowing(pageSize);
}
}, [expanded]);
const showMore = React.useCallback(() => {
if (childDocuments && childDocuments.length > showing) {
setShowing((value) => value + pageSize);
}
}, [childDocuments, showing]);
return (
<Folder expanded={expanded}>
{isDraggingAnyDocument && can.createDocument && manualSort && (
@@ -79,7 +113,7 @@ function CollectionLinkChildren({
<Loading />
</ResizingHeightContainer>
)}
{childDocuments?.map((node, index) => (
{childDocuments?.slice(0, showing).map((node, index) => (
<DocumentLink
key={node.id}
node={node}
@@ -91,7 +125,18 @@ function CollectionLinkChildren({
index={index}
/>
))}
{childDocuments?.length === 0 && <EmptyCollectionPlaceholder />}
{childDocuments?.length === 0 && (
<SidebarLink
label={
<Text type="tertiary" size="small" italic>
{t("Empty")}
</Text>
}
onClick={() => history.push(collection.url)}
depth={2}
/>
)}
<Waypoint key={showing} onEnter={showMore} fireOnRapidScroll />
</DocumentsLoader>
</Folder>
);
@@ -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,42 +50,44 @@ function Collections() {
});
return (
<Flex column>
<Header id="collections" title={t("Collections")}>
<Relative>
<PaginatedList
options={params}
aria-label={t("Collections")}
items={collections.orderedData}
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>
);
}
const StyledError = styled(Error)`
export const StyledError = styled(Error)`
font-size: 15px;
padding: 0 8px;
`;
@@ -19,6 +19,11 @@ import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
import DocumentMenu from "~/menus/DocumentMenu";
import { newNestedDocumentPath } from "~/utils/routeHelpers";
import {
useDragDocument,
useDropToReorderDocument,
useDropToReparentDocument,
} from "../hooks/useDragAndDrop";
import DropCursor from "./DropCursor";
import DropToImport from "./DropToImport";
import EditableTitle, { RefHandle } from "./EditableTitle";
@@ -26,11 +31,6 @@ import Folder from "./Folder";
import Relative from "./Relative";
import { SidebarContextType, useSidebarContext } from "./SidebarContext";
import SidebarLink from "./SidebarLink";
import {
useDragDocument,
useDropToReorderDocument,
useDropToReparentDocument,
} from "./useDragAndDrop";
type Props = {
node: NavigationNode;
@@ -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(
@@ -41,16 +41,6 @@ function EditableTitle(
setIsEditing(true);
}, []);
const handleKeyDown = React.useCallback(
(event) => {
if (event.key === "Escape") {
setIsEditing(false);
setValue(originalValue);
}
},
[originalValue]
);
const stopPropagation = React.useCallback((event) => {
event.preventDefault();
event.stopPropagation();
@@ -63,6 +53,7 @@ function EditableTitle(
const handleSave = React.useCallback(
async (ev) => {
ev.preventDefault();
ev.stopPropagation();
setIsEditing(false);
const trimmedValue = value.trim();
@@ -85,6 +76,22 @@ function EditableTitle(
[originalValue, value, onSubmit]
);
const handleKeyDown = React.useCallback(
async (ev) => {
if (ev.nativeEvent.isComposing) {
return;
}
if (ev.key === "Escape") {
setIsEditing(false);
setValue(originalValue);
}
if (ev.key === "Enter") {
await handleSave(ev);
}
},
[handleSave, originalValue]
);
React.useEffect(() => {
onEditing?.(isEditing);
}, [onEditing, isEditing]);
@@ -1,22 +0,0 @@
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import Text from "~/components/Text";
const EmptyCollectionPlaceholder = () => {
const { t } = useTranslation();
return (
<Empty type="tertiary" size="small">
{t("Empty")}
</Empty>
);
};
const Empty = styled(Text)`
margin-left: 46px;
margin-bottom: 0;
line-height: 34px;
font-style: italic;
`;
export default EmptyCollectionPlaceholder;
@@ -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>
@@ -10,7 +10,7 @@ import {
match,
} from "react-router";
import { Link } from "react-router-dom";
import scrollIntoView from "smooth-scroll-into-view-if-needed";
import scrollIntoView from "scroll-into-view-if-needed";
import history from "~/utils/history";
const resolveToLocation = (
@@ -112,8 +112,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 +154,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}

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