@tldraw/commenting 0.0.0-bootstrap → 5.3.0-canary.31036673d3f8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -2
- package/commenting.css +1139 -329
- package/dist-cjs/canvas/anchor-lifecycle.js +148 -0
- package/dist-cjs/canvas/anchor-lifecycle.js.map +7 -0
- package/dist-cjs/canvas/cluster-input.js +2 -2
- package/dist-cjs/canvas/cluster-input.js.map +2 -2
- package/dist-cjs/canvas/comment-body.js +1 -1
- package/dist-cjs/canvas/comment-body.js.map +2 -2
- package/dist-cjs/canvas/comment-drafts.js +54 -0
- package/dist-cjs/canvas/comment-drafts.js.map +7 -0
- package/dist-cjs/canvas/comment-mutations.js +123 -0
- package/dist-cjs/canvas/comment-mutations.js.map +7 -0
- package/dist-cjs/canvas/comment-reactions.js +163 -0
- package/dist-cjs/canvas/comment-reactions.js.map +7 -0
- package/dist-cjs/canvas/comment-render.js +5 -3
- package/dist-cjs/canvas/comment-render.js.map +2 -2
- package/dist-cjs/canvas/comment-store.js +19 -9
- package/dist-cjs/canvas/comment-store.js.map +2 -2
- package/dist-cjs/canvas/comment-tool.js +64 -14
- package/dist-cjs/canvas/comment-tool.js.map +2 -2
- package/dist-cjs/canvas/comments-filter-menu.js +20 -38
- package/dist-cjs/canvas/comments-filter-menu.js.map +2 -2
- package/dist-cjs/canvas/comments-overflow-menu.js +22 -27
- package/dist-cjs/canvas/comments-overflow-menu.js.map +2 -2
- package/dist-cjs/canvas/comments-overlay.js +429 -474
- package/dist-cjs/canvas/comments-overlay.js.map +2 -2
- package/dist-cjs/canvas/comments-sidebar.js +35 -32
- package/dist-cjs/canvas/comments-sidebar.js.map +2 -2
- package/dist-cjs/canvas/comments-visibility-toggle.js +65 -0
- package/dist-cjs/canvas/comments-visibility-toggle.js.map +7 -0
- package/dist-cjs/canvas/context.js +17 -0
- package/dist-cjs/canvas/context.js.map +7 -0
- package/dist-cjs/canvas/hooks.js +22 -3
- package/dist-cjs/canvas/hooks.js.map +2 -2
- package/dist-cjs/canvas/options.js +19 -2
- package/dist-cjs/canvas/options.js.map +2 -2
- package/dist-cjs/canvas/pin-stacking.js +56 -0
- package/dist-cjs/canvas/pin-stacking.js.map +7 -0
- package/dist-cjs/canvas/sidebar-filters.js +2 -2
- package/dist-cjs/canvas/sidebar-filters.js.map +2 -2
- package/dist-cjs/canvas/state.js +28 -14
- package/dist-cjs/canvas/state.js.map +2 -2
- package/dist-cjs/canvas/thread-preview.js +185 -0
- package/dist-cjs/canvas/thread-preview.js.map +7 -0
- package/dist-cjs/canvas/thread-stack.js +199 -0
- package/dist-cjs/canvas/thread-stack.js.map +7 -0
- package/dist-cjs/canvas/thread-state.js +67 -21
- package/dist-cjs/canvas/thread-state.js.map +2 -2
- package/dist-cjs/canvas/thread-view.js +397 -0
- package/dist-cjs/canvas/thread-view.js.map +7 -0
- package/dist-cjs/clustering/computeClusterTable.js +4 -4
- package/dist-cjs/clustering/computeClusterTable.js.map +2 -2
- package/dist-cjs/clustering/runtime.js +119 -7
- package/dist-cjs/clustering/runtime.js.map +2 -2
- package/dist-cjs/clustering/types.js.map +1 -1
- package/dist-cjs/index.d.ts +763 -333
- package/dist-cjs/index.js +46 -28
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/ui/byline.js +6 -4
- package/dist-cjs/ui/byline.js.map +2 -2
- package/dist-cjs/ui/comment-card.js +15 -8
- package/dist-cjs/ui/comment-card.js.map +2 -2
- package/dist-cjs/ui/comment-composer.js +137 -36
- package/dist-cjs/ui/comment-composer.js.map +2 -2
- package/dist-cjs/ui/comment-pin.js +24 -2
- package/dist-cjs/ui/comment-pin.js.map +2 -2
- package/dist-cjs/ui/comment-thread.js +9 -7
- package/dist-cjs/ui/comment-thread.js.map +2 -2
- package/dist-cjs/ui/comments-list.js +30 -18
- package/dist-cjs/ui/comments-list.js.map +2 -2
- package/dist-cjs/ui/count-badge.js +3 -2
- package/dist-cjs/ui/count-badge.js.map +2 -2
- package/dist-cjs/ui/emoji-picker.js +54 -0
- package/dist-cjs/ui/emoji-picker.js.map +7 -0
- package/dist-cjs/ui/empty-state.js +3 -3
- package/dist-cjs/ui/empty-state.js.map +2 -2
- package/dist-cjs/ui/format-time.js +4 -1
- package/dist-cjs/ui/format-time.js.map +2 -2
- package/dist-cjs/ui/reaction-picker.js +73 -0
- package/dist-cjs/ui/reaction-picker.js.map +7 -0
- package/dist-cjs/ui/reaction.js +59 -6
- package/dist-cjs/ui/reaction.js.map +2 -2
- package/dist-cjs/ui/reactions.js +20 -7
- package/dist-cjs/ui/reactions.js.map +2 -2
- package/dist-cjs/ui/{mention.js → reply-count.js} +9 -11
- package/dist-cjs/ui/reply-count.js.map +7 -0
- package/dist-cjs/ui/send-button.js +20 -1
- package/dist-cjs/ui/send-button.js.map +2 -2
- package/dist-cjs/ui/{comment-text.js → tooltip-button.js} +12 -9
- package/dist-cjs/ui/tooltip-button.js.map +7 -0
- package/dist-esm/canvas/anchor-lifecycle.mjs +128 -0
- package/dist-esm/canvas/anchor-lifecycle.mjs.map +7 -0
- package/dist-esm/canvas/cluster-input.mjs +2 -2
- package/dist-esm/canvas/cluster-input.mjs.map +2 -2
- package/dist-esm/canvas/comment-body.mjs +1 -1
- package/dist-esm/canvas/comment-body.mjs.map +2 -2
- package/dist-esm/canvas/comment-drafts.mjs +34 -0
- package/dist-esm/canvas/comment-drafts.mjs.map +7 -0
- package/dist-esm/canvas/comment-mutations.mjs +103 -0
- package/dist-esm/canvas/comment-mutations.mjs.map +7 -0
- package/dist-esm/canvas/comment-reactions.mjs +152 -0
- package/dist-esm/canvas/comment-reactions.mjs.map +7 -0
- package/dist-esm/canvas/comment-render.mjs +5 -3
- package/dist-esm/canvas/comment-render.mjs.map +2 -2
- package/dist-esm/canvas/comment-store.mjs +19 -9
- package/dist-esm/canvas/comment-store.mjs.map +2 -2
- package/dist-esm/canvas/comment-tool.mjs +67 -17
- package/dist-esm/canvas/comment-tool.mjs.map +2 -2
- package/dist-esm/canvas/comments-filter-menu.mjs +20 -38
- package/dist-esm/canvas/comments-filter-menu.mjs.map +2 -2
- package/dist-esm/canvas/comments-overflow-menu.mjs +23 -27
- package/dist-esm/canvas/comments-overflow-menu.mjs.map +2 -2
- package/dist-esm/canvas/comments-overlay.mjs +450 -479
- package/dist-esm/canvas/comments-overlay.mjs.map +3 -3
- package/dist-esm/canvas/comments-sidebar.mjs +38 -35
- package/dist-esm/canvas/comments-sidebar.mjs.map +2 -2
- package/dist-esm/canvas/comments-visibility-toggle.mjs +45 -0
- package/dist-esm/canvas/comments-visibility-toggle.mjs.map +7 -0
- package/dist-esm/canvas/context.mjs +1 -0
- package/dist-esm/canvas/context.mjs.map +7 -0
- package/dist-esm/canvas/hooks.mjs +28 -5
- package/dist-esm/canvas/hooks.mjs.map +2 -2
- package/dist-esm/canvas/options.mjs +21 -3
- package/dist-esm/canvas/options.mjs.map +2 -2
- package/dist-esm/canvas/pin-stacking.mjs +36 -0
- package/dist-esm/canvas/pin-stacking.mjs.map +7 -0
- package/dist-esm/canvas/sidebar-filters.mjs +2 -2
- package/dist-esm/canvas/sidebar-filters.mjs.map +2 -2
- package/dist-esm/canvas/state.mjs +29 -19
- package/dist-esm/canvas/state.mjs.map +2 -2
- package/dist-esm/canvas/thread-preview.mjs +170 -0
- package/dist-esm/canvas/thread-preview.mjs.map +7 -0
- package/dist-esm/canvas/thread-stack.mjs +190 -0
- package/dist-esm/canvas/thread-stack.mjs.map +7 -0
- package/dist-esm/canvas/thread-state.mjs +71 -22
- package/dist-esm/canvas/thread-state.mjs.map +2 -2
- package/dist-esm/canvas/thread-view.mjs +402 -0
- package/dist-esm/canvas/thread-view.mjs.map +7 -0
- package/dist-esm/clustering/computeClusterTable.mjs +4 -4
- package/dist-esm/clustering/computeClusterTable.mjs.map +2 -2
- package/dist-esm/clustering/runtime.mjs +119 -7
- package/dist-esm/clustering/runtime.mjs.map +2 -2
- package/dist-esm/index.d.mts +763 -333
- package/dist-esm/index.mjs +87 -41
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/ui/byline.mjs +6 -4
- package/dist-esm/ui/byline.mjs.map +2 -2
- package/dist-esm/ui/comment-card.mjs +15 -8
- package/dist-esm/ui/comment-card.mjs.map +2 -2
- package/dist-esm/ui/comment-composer.mjs +148 -36
- package/dist-esm/ui/comment-composer.mjs.map +2 -2
- package/dist-esm/ui/comment-pin.mjs +24 -2
- package/dist-esm/ui/comment-pin.mjs.map +2 -2
- package/dist-esm/ui/comment-thread.mjs +9 -7
- package/dist-esm/ui/comment-thread.mjs.map +2 -2
- package/dist-esm/ui/comments-list.mjs +43 -29
- package/dist-esm/ui/comments-list.mjs.map +2 -2
- package/dist-esm/ui/count-badge.mjs +3 -2
- package/dist-esm/ui/count-badge.mjs.map +2 -2
- package/dist-esm/ui/emoji-picker.mjs +34 -0
- package/dist-esm/ui/emoji-picker.mjs.map +7 -0
- package/dist-esm/ui/empty-state.mjs +3 -3
- package/dist-esm/ui/empty-state.mjs.map +2 -2
- package/dist-esm/ui/format-time.mjs +4 -1
- package/dist-esm/ui/format-time.mjs.map +2 -2
- package/dist-esm/ui/reaction-picker.mjs +60 -0
- package/dist-esm/ui/reaction-picker.mjs.map +7 -0
- package/dist-esm/ui/reaction.mjs +60 -7
- package/dist-esm/ui/reaction.mjs.map +2 -2
- package/dist-esm/ui/reactions.mjs +21 -8
- package/dist-esm/ui/reactions.mjs.map +2 -2
- package/dist-esm/ui/reply-count.mjs +9 -0
- package/dist-esm/ui/reply-count.mjs.map +7 -0
- package/dist-esm/ui/send-button.mjs +20 -1
- package/dist-esm/ui/send-button.mjs.map +2 -2
- package/dist-esm/ui/tooltip-button.mjs +12 -0
- package/dist-esm/ui/tooltip-button.mjs.map +7 -0
- package/package.json +6 -6
- package/src/canvas/anchor-lifecycle.test.ts +290 -0
- package/src/canvas/anchor-lifecycle.ts +205 -0
- package/src/canvas/canvas.css +461 -46
- package/src/canvas/cluster-input.test.ts +31 -23
- package/src/canvas/cluster-input.ts +3 -4
- package/src/canvas/comment-body.tsx +1 -1
- package/src/canvas/comment-drafts.test.ts +55 -0
- package/src/canvas/comment-drafts.ts +46 -0
- package/src/canvas/comment-mutations.test.ts +410 -0
- package/src/canvas/comment-mutations.ts +288 -0
- package/src/canvas/comment-reactions.test.ts +132 -0
- package/src/canvas/comment-reactions.tsx +245 -0
- package/src/canvas/comment-render.test.ts +1 -1
- package/src/canvas/comment-render.ts +9 -5
- package/src/canvas/comment-store.ts +67 -27
- package/src/canvas/comment-tool.test.ts +162 -0
- package/src/canvas/comment-tool.tsx +105 -23
- package/src/canvas/comments-filter-menu.tsx +23 -32
- package/src/canvas/comments-overflow-menu.tsx +14 -21
- package/src/canvas/comments-overlay.tsx +695 -622
- package/src/canvas/comments-sidebar.test.ts +45 -0
- package/src/canvas/comments-sidebar.tsx +76 -58
- package/src/canvas/comments-visibility-toggle.tsx +49 -0
- package/src/canvas/context.ts +47 -0
- package/src/canvas/hooks.ts +52 -11
- package/src/canvas/options.test.ts +52 -2
- package/src/canvas/options.ts +133 -9
- package/src/canvas/pin-stacking.test.ts +114 -0
- package/src/canvas/pin-stacking.ts +57 -0
- package/src/canvas/sidebar-filters.ts +4 -3
- package/src/canvas/state.ts +92 -35
- package/src/canvas/thread-preview.test.ts +91 -0
- package/src/canvas/thread-preview.tsx +304 -0
- package/src/canvas/thread-stack.tsx +252 -0
- package/src/canvas/thread-state.test.ts +209 -0
- package/src/canvas/thread-state.ts +137 -36
- package/src/canvas/thread-view.tsx +516 -0
- package/src/clustering/computeClusterTable.test.ts +24 -23
- package/src/clustering/computeClusterTable.ts +5 -5
- package/src/clustering/runtime.test.ts +217 -7
- package/src/clustering/runtime.ts +171 -17
- package/src/clustering/types.ts +9 -9
- package/src/index.ts +76 -40
- package/src/ui/byline.tsx +7 -5
- package/src/ui/comment-card.tsx +20 -10
- package/src/ui/comment-composer.tsx +206 -40
- package/src/ui/comment-pin.tsx +26 -2
- package/src/ui/comment-thread.tsx +13 -6
- package/src/ui/comments-list.test.ts +31 -0
- package/src/ui/comments-list.tsx +45 -21
- package/src/ui/comments.css +566 -281
- package/src/ui/count-badge.tsx +9 -2
- package/src/ui/emoji-picker.test.ts +25 -0
- package/src/ui/emoji-picker.tsx +76 -0
- package/src/ui/empty-state.tsx +3 -3
- package/src/ui/format-time.ts +7 -2
- package/src/ui/reaction-picker.tsx +109 -0
- package/src/ui/reaction.tsx +121 -6
- package/src/ui/reactions.tsx +69 -10
- package/src/ui/reply-count.ts +16 -0
- package/src/ui/send-button.tsx +18 -3
- package/src/ui/tooltip-button.tsx +20 -0
- package/dist-cjs/canvas/region-options.js +0 -49
- package/dist-cjs/canvas/region-options.js.map +0 -7
- package/dist-cjs/ui/avatar.js +0 -43
- package/dist-cjs/ui/avatar.js.map +0 -7
- package/dist-cjs/ui/comment-mention.js +0 -42
- package/dist-cjs/ui/comment-mention.js.map +0 -7
- package/dist-cjs/ui/comment-text.js.map +0 -7
- package/dist-cjs/ui/mention-list.js +0 -70
- package/dist-cjs/ui/mention-list.js.map +0 -7
- package/dist-cjs/ui/mention-suggestion.js +0 -206
- package/dist-cjs/ui/mention-suggestion.js.map +0 -7
- package/dist-cjs/ui/mention.js.map +0 -7
- package/dist-cjs/ui/render-markdown.js +0 -63
- package/dist-cjs/ui/render-markdown.js.map +0 -7
- package/dist-esm/canvas/region-options.mjs +0 -29
- package/dist-esm/canvas/region-options.mjs.map +0 -7
- package/dist-esm/ui/avatar.mjs +0 -23
- package/dist-esm/ui/avatar.mjs.map +0 -7
- package/dist-esm/ui/comment-mention.mjs +0 -22
- package/dist-esm/ui/comment-mention.mjs.map +0 -7
- package/dist-esm/ui/comment-text.mjs +0 -9
- package/dist-esm/ui/comment-text.mjs.map +0 -7
- package/dist-esm/ui/mention-list.mjs +0 -50
- package/dist-esm/ui/mention-list.mjs.map +0 -7
- package/dist-esm/ui/mention-suggestion.mjs +0 -186
- package/dist-esm/ui/mention-suggestion.mjs.map +0 -7
- package/dist-esm/ui/mention.mjs +0 -11
- package/dist-esm/ui/mention.mjs.map +0 -7
- package/dist-esm/ui/render-markdown.mjs +0 -45
- package/dist-esm/ui/render-markdown.mjs.map +0 -7
- package/src/canvas/region-options.ts +0 -57
- package/src/ui/avatar.tsx +0 -31
- package/src/ui/comment-mention.ts +0 -47
- package/src/ui/comment-text.tsx +0 -12
- package/src/ui/mention-list.tsx +0 -106
- package/src/ui/mention-suggestion.test.ts +0 -18
- package/src/ui/mention-suggestion.tsx +0 -285
- package/src/ui/mention.tsx +0 -9
- package/src/ui/render-markdown.tsx +0 -72
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import type { CommentListItemProps } from '../ui/comments-list'
|
|
3
|
+
import { sortSidebarRows } from './comments-sidebar'
|
|
4
|
+
|
|
5
|
+
function row(id: string, lastActivity: number, resolved = false) {
|
|
6
|
+
return {
|
|
7
|
+
item: {
|
|
8
|
+
id,
|
|
9
|
+
resolved,
|
|
10
|
+
author: { id: 'user:1', name: 'A' },
|
|
11
|
+
preview: '',
|
|
12
|
+
date: '',
|
|
13
|
+
} as unknown as CommentListItemProps,
|
|
14
|
+
lastActivity,
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('sortSidebarRows', () => {
|
|
19
|
+
it('orders threads by their most recent comment, not their first', () => {
|
|
20
|
+
// t1 was started first but was just replied to; t2 has been quiet since it was created.
|
|
21
|
+
const sorted = sortSidebarRows([row('t1', 300), row('t2', 200)])
|
|
22
|
+
expect(sorted.map((r) => r.item.id)).toEqual(['t1', 't2'])
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('sinks resolved threads below unresolved ones however recent they are', () => {
|
|
26
|
+
const sorted = sortSidebarRows([row('t1', 100), row('t2', 999, true), row('t3', 200)])
|
|
27
|
+
expect(sorted.map((r) => r.item.id)).toEqual(['t3', 't1', 't2'])
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('orders resolved threads among themselves by recency too', () => {
|
|
31
|
+
const sorted = sortSidebarRows([row('t1', 100, true), row('t2', 300, true)])
|
|
32
|
+
expect(sorted.map((r) => r.item.id)).toEqual(['t2', 't1'])
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('breaks activity ties by id so the list does not reshuffle', () => {
|
|
36
|
+
const sorted = sortSidebarRows([row('t3', 5), row('t1', 5), row('t2', 5)])
|
|
37
|
+
expect(sorted.map((r) => r.item.id)).toEqual(['t1', 't2', 't3'])
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('does not mutate the input', () => {
|
|
41
|
+
const input = [row('t1', 100), row('t2', 200)]
|
|
42
|
+
sortSidebarRows(input)
|
|
43
|
+
expect(input.map((r) => r.item.id)).toEqual(['t1', 't2'])
|
|
44
|
+
})
|
|
45
|
+
})
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { ReactNode, useRef } from 'react'
|
|
1
|
+
import { ReactNode, useCallback, useRef } from 'react'
|
|
2
2
|
import { createPortal } from 'react-dom'
|
|
3
3
|
import {
|
|
4
4
|
TLComment,
|
|
5
|
-
TLCommentId,
|
|
6
5
|
useContainer,
|
|
7
6
|
useEditor,
|
|
8
7
|
usePassThroughMouseOverEvents,
|
|
@@ -10,63 +9,55 @@ import {
|
|
|
10
9
|
useValue,
|
|
11
10
|
} from 'tldraw'
|
|
12
11
|
import { CommentListItemProps, CommentsList } from '../ui/comments-list'
|
|
13
|
-
import {
|
|
12
|
+
import { UNKNOWN_COMMENT_AUTHOR } from './comment-render'
|
|
14
13
|
import { CommentsFilterMenu } from './comments-filter-menu'
|
|
15
|
-
import {
|
|
14
|
+
import { CommentsVisibilityToggle } from './comments-visibility-toggle'
|
|
15
|
+
import { type CommentingContext } from './context'
|
|
16
16
|
import { useComments, useCommentThreads } from './hooks'
|
|
17
17
|
import { useCommentingEnabled } from './license'
|
|
18
18
|
import { useCommentingOptions } from './options'
|
|
19
19
|
import { richTextToPlaintext } from './rich-text'
|
|
20
|
-
import { openThreadId, sidebarFilters } from './state'
|
|
20
|
+
import { commentsSidebarOpen, openThreadId, sidebarFilters } from './state'
|
|
21
21
|
import { focusThread } from './thread-state'
|
|
22
22
|
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
isCommentUnread
|
|
34
|
-
|
|
35
|
-
tools?: string[]
|
|
23
|
+
/**
|
|
24
|
+
* The host wiring for {@link CanvasCommentsSidebar}: the {@link CommentingContext} fields it reads,
|
|
25
|
+
* plus the panel's own slots. A non-null `currentUserId` enables the "only your threads" filter, and
|
|
26
|
+
* an `isCommentUnread` the "only unread" one. `CanvasComments` takes the same fields, so a host
|
|
27
|
+
* mounting both can spread one object into each.
|
|
28
|
+
*
|
|
29
|
+
* @public
|
|
30
|
+
*/
|
|
31
|
+
export interface CanvasCommentsSidebarProps extends Pick<
|
|
32
|
+
CommentingContext,
|
|
33
|
+
'currentUserId' | 'resolveAuthor' | 'isCommentUnread' | 'getThreadHref'
|
|
34
|
+
> {
|
|
36
35
|
/** Header above the list. */
|
|
37
36
|
header?: ReactNode
|
|
38
37
|
/** Shown when the page has no threads. */
|
|
39
38
|
empty?: ReactNode
|
|
40
|
-
/** Where imprecise shape pins sit, so navigation centres on the same spot. Default top-right. */
|
|
41
|
-
impreciseShapeAnchor?: { x: number; y: number }
|
|
42
39
|
}
|
|
43
40
|
|
|
44
41
|
/**
|
|
45
|
-
* A comments list panel for the current page, shown while
|
|
46
|
-
* thread brings its pin into view and opens it. Batteries-included
|
|
47
|
-
* `CanvasComments`); `CommentsList` is exported for a differently-placed
|
|
42
|
+
* A comments list panel for the current page, shown while {@link commentsSidebarOpen} is set (e.g.
|
|
43
|
+
* toggled by a button). Clicking a thread brings its pin into view and opens it. Batteries-included
|
|
44
|
+
* over the store (a sibling to `CanvasComments`); `CommentsList` is exported for a differently-placed
|
|
45
|
+
* or always-on list.
|
|
48
46
|
* @public @react
|
|
49
47
|
*/
|
|
50
48
|
export function CanvasCommentsSidebar(props: CanvasCommentsSidebarProps) {
|
|
51
|
-
const {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
isCommentUnread,
|
|
55
|
-
tools,
|
|
56
|
-
header,
|
|
57
|
-
empty,
|
|
58
|
-
impreciseShapeAnchor,
|
|
59
|
-
} = props
|
|
49
|
+
const { resolveAuthor, currentUserId, isCommentUnread, header, empty, getThreadHref } = props
|
|
50
|
+
// Name-only view of the resolver, for the plaintext previews (which resolve @-mentions).
|
|
51
|
+
const resolveName = useCallback((id: string) => resolveAuthor(id)?.name, [resolveAuthor])
|
|
60
52
|
const editor = useEditor()
|
|
61
53
|
const options = useCommentingOptions()
|
|
62
|
-
const sidebarTools = tools ?? ['comment']
|
|
63
54
|
const container = useContainer()
|
|
64
55
|
const commentingEnabled = useCommentingEnabled()
|
|
65
56
|
const msg = useTranslation()
|
|
66
57
|
const threads = useCommentThreads(editor)
|
|
67
58
|
const comments = useComments(editor)
|
|
68
59
|
const currentPageId = useValue('page id', () => editor.getCurrentPageId(), [editor])
|
|
69
|
-
const
|
|
60
|
+
const open = useValue('sidebar open', () => commentsSidebarOpen.get(editor), [editor])
|
|
70
61
|
const openId = useValue('open thread', () => openThreadId.get(editor), [editor])
|
|
71
62
|
const filters = useValue('sidebar filters', () => sidebarFilters.get(editor), [editor])
|
|
72
63
|
const pageNames = useValue(
|
|
@@ -75,7 +66,7 @@ export function CanvasCommentsSidebar(props: CanvasCommentsSidebarProps) {
|
|
|
75
66
|
[editor]
|
|
76
67
|
)
|
|
77
68
|
|
|
78
|
-
if (!commentingEnabled || !
|
|
69
|
+
if (!commentingEnabled || !open) return null
|
|
79
70
|
|
|
80
71
|
// Group comments by thread (they arrive oldest-first, so [0] is each thread's first comment).
|
|
81
72
|
const byThread = new Map<string, TLComment[]>()
|
|
@@ -91,13 +82,12 @@ export function CanvasCommentsSidebar(props: CanvasCommentsSidebarProps) {
|
|
|
91
82
|
(thread) => !filters.onlyCurrentPage || thread.pageId === currentPageId
|
|
92
83
|
)
|
|
93
84
|
|
|
94
|
-
const
|
|
85
|
+
const rows: SidebarRow[] = pageThreads
|
|
95
86
|
.filter((thread) => filters.showResolved || thread.resolved == null)
|
|
96
87
|
// "Only mine" is ignored without a known user — otherwise a persisted onlyMine=true would
|
|
97
88
|
// empty the list for a signed-out viewer, with the (hidden) toggle giving no way to clear it.
|
|
98
89
|
.filter(
|
|
99
|
-
(thread) =>
|
|
100
|
-
!filters.onlyMine || currentUserId === undefined || thread.createdBy === currentUserId
|
|
90
|
+
(thread) => !filters.onlyMine || currentUserId === null || thread.createdBy === currentUserId
|
|
101
91
|
)
|
|
102
92
|
// "Only unread" is likewise ignored without a read-status source.
|
|
103
93
|
.filter(
|
|
@@ -109,6 +99,8 @@ export function CanvasCommentsSidebar(props: CanvasCommentsSidebarProps) {
|
|
|
109
99
|
.map((thread) => {
|
|
110
100
|
const threadComments = byThread.get(thread.id) ?? []
|
|
111
101
|
const first = threadComments[0]
|
|
102
|
+
// Comments arrive oldest-first, so the last one is the thread's most recent activity.
|
|
103
|
+
const last = threadComments[threadComments.length - 1]
|
|
112
104
|
let preview: ReactNode = ''
|
|
113
105
|
// The `ThreadPreview` component slot overrides the built-in plaintext default.
|
|
114
106
|
const ThreadPreview = options.components.ThreadPreview
|
|
@@ -120,27 +112,31 @@ export function CanvasCommentsSidebar(props: CanvasCommentsSidebarProps) {
|
|
|
120
112
|
)
|
|
121
113
|
}
|
|
122
114
|
return {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
115
|
+
item: {
|
|
116
|
+
id: thread.id,
|
|
117
|
+
author: resolveAuthor(thread.createdBy) ?? UNKNOWN_COMMENT_AUTHOR,
|
|
118
|
+
preview,
|
|
119
|
+
date: new Date((first ?? thread).createdAt).toISOString(),
|
|
120
|
+
resolved: thread.resolved != null,
|
|
121
|
+
// The page label only earns its place when it adds information: multiple pages
|
|
122
|
+
// exist, and the thread is somewhere other than where you already are.
|
|
123
|
+
page:
|
|
124
|
+
pageNames.size > 1 && thread.pageId !== currentPageId
|
|
125
|
+
? pageNames.get(thread.pageId)
|
|
126
|
+
: undefined,
|
|
127
|
+
count: threadComments.length,
|
|
128
|
+
selected: openId === thread.id,
|
|
129
|
+
href: getThreadHref?.(thread.id),
|
|
130
|
+
},
|
|
131
|
+
lastActivity: (last ?? thread).createdAt,
|
|
131
132
|
}
|
|
132
133
|
})
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (!!a.resolved !== !!b.resolved) return a.resolved ? 1 : -1
|
|
136
|
-
return b.date.localeCompare(a.date)
|
|
137
|
-
})
|
|
134
|
+
|
|
135
|
+
const items = sortSidebarRows(rows).map((row) => row.item)
|
|
138
136
|
|
|
139
137
|
const focus = (id: string) => {
|
|
140
138
|
const thread = threads.find((t) => t.id === id)
|
|
141
|
-
|
|
142
|
-
// spot the pin renders at when the anchor is configured via `CommentTool.configure`.
|
|
143
|
-
if (thread) focusThread(editor, thread, impreciseShapeAnchor ?? options.impreciseShapeAnchor)
|
|
139
|
+
if (thread) focusThread(editor, thread)
|
|
144
140
|
}
|
|
145
141
|
|
|
146
142
|
return (
|
|
@@ -149,12 +145,12 @@ export function CanvasCommentsSidebar(props: CanvasCommentsSidebarProps) {
|
|
|
149
145
|
items={items}
|
|
150
146
|
header={header ?? msg('comments.title')}
|
|
151
147
|
headerAction={
|
|
152
|
-
<div className="cmt-list__header-actions">
|
|
148
|
+
<div className="tlui-cmt-list__header-actions">
|
|
153
149
|
<CommentsFilterMenu
|
|
154
|
-
canFilterByAuthor={currentUserId !==
|
|
150
|
+
canFilterByAuthor={currentUserId !== null}
|
|
155
151
|
canFilterByUnread={isCommentUnread !== undefined}
|
|
156
152
|
/>
|
|
157
|
-
<
|
|
153
|
+
<CommentsVisibilityToggle />
|
|
158
154
|
</div>
|
|
159
155
|
}
|
|
160
156
|
empty={
|
|
@@ -169,6 +165,28 @@ export function CanvasCommentsSidebar(props: CanvasCommentsSidebarProps) {
|
|
|
169
165
|
)
|
|
170
166
|
}
|
|
171
167
|
|
|
168
|
+
/** A list row paired with the sort key that isn't part of what the row displays. */
|
|
169
|
+
interface SidebarRow {
|
|
170
|
+
item: CommentListItemProps
|
|
171
|
+
/** When the thread's most recent comment was posted — what the list orders by. */
|
|
172
|
+
lastActivity: number
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Order the list: unresolved threads first, then by most recent activity, id as a stable tiebreak.
|
|
177
|
+
* Recency is the thread's *latest* comment, not its first, so a thread someone just replied to rises
|
|
178
|
+
* to the top instead of staying wherever it was started. (The row still shows the thread's opening
|
|
179
|
+
* comment and its date — that's what identifies the thread; only the ordering follows the replies.)
|
|
180
|
+
*/
|
|
181
|
+
export function sortSidebarRows(rows: readonly SidebarRow[]): readonly SidebarRow[] {
|
|
182
|
+
return [...rows].sort(
|
|
183
|
+
(a, b) =>
|
|
184
|
+
Number(!!a.item.resolved) - Number(!!b.item.resolved) ||
|
|
185
|
+
b.lastActivity - a.lastActivity ||
|
|
186
|
+
(a.item.id < b.item.id ? -1 : 1)
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
172
190
|
/** The sidebar surface, portaled into the container. It scrolls its own list, so — unlike tldraw's
|
|
173
191
|
* wheel-transparent panels — a wheel over it doesn't pan the canvas. Hover still passes through so
|
|
174
192
|
* shapes beneath it stay interactive. */
|
|
@@ -176,7 +194,7 @@ function SidebarPanel({ container, children }: { container: HTMLElement; childre
|
|
|
176
194
|
const ref = useRef<HTMLDivElement>(null)
|
|
177
195
|
usePassThroughMouseOverEvents(ref)
|
|
178
196
|
return createPortal(
|
|
179
|
-
<div ref={ref} className="cmt-canvas-sidebar">
|
|
197
|
+
<div ref={ref} className="tlui-cmt-canvas-sidebar" onContextMenu={(e) => e.stopPropagation()}>
|
|
180
198
|
{children}
|
|
181
199
|
</div>,
|
|
182
200
|
container
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useEditor, useTranslation, useValue } from 'tldraw'
|
|
2
|
+
import { TooltipButton } from '../ui/tooltip-button'
|
|
3
|
+
import { commentsHidden, toggleCommentsHidden } from './state'
|
|
4
|
+
|
|
5
|
+
/** The sidebar header's show/hide toggle for comment pins — an eye that closes while comments
|
|
6
|
+
* are hidden. The same state as the Shift+C shortcut.
|
|
7
|
+
* @public @react */
|
|
8
|
+
export function CommentsVisibilityToggle() {
|
|
9
|
+
const editor = useEditor()
|
|
10
|
+
const msg = useTranslation()
|
|
11
|
+
const hidden = useValue('comments hidden', () => commentsHidden.get(editor), [editor])
|
|
12
|
+
const label = hidden ? msg('comments.show') : msg('comments.hide')
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<TooltipButton
|
|
16
|
+
tooltip={label}
|
|
17
|
+
className="tlui-cmt-header-btn"
|
|
18
|
+
onClick={() => toggleCommentsHidden(editor)}
|
|
19
|
+
>
|
|
20
|
+
{hidden ? <EyeClosedIcon /> : <EyeOpenIcon />}
|
|
21
|
+
</TooltipButton>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function EyeOpenIcon() {
|
|
26
|
+
return (
|
|
27
|
+
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" aria-hidden="true">
|
|
28
|
+
<path
|
|
29
|
+
d="M7.5 11C4.80285 11 2.52952 9.62184 1.09622 7.50001C2.52952 5.37816 4.80285 4 7.5 4C10.1971 4 12.4705 5.37816 13.9038 7.5C12.4705 9.62183 10.1971 11 7.5 11ZM7.5 3C4.30786 3 1.65639 4.70638 0.0760002 7.23501C-0.0253338 7.39715 -0.0253334 7.60288 0.0760014 7.76501C1.65639 10.2936 4.30786 12 7.5 12C10.6921 12 13.3436 10.2936 14.924 7.76501C15.0253 7.60288 15.0253 7.39715 14.924 7.23501C13.3436 4.70638 10.6921 3 7.5 3ZM7.5 9.5C8.60457 9.5 9.5 8.60457 9.5 7.5C9.5 6.39543 8.60457 5.5 7.5 5.5C6.39543 5.5 5.5 6.39543 5.5 7.5C5.5 8.60457 6.39543 9.5 7.5 9.5Z"
|
|
30
|
+
fill="currentColor"
|
|
31
|
+
fillRule="evenodd"
|
|
32
|
+
clipRule="evenodd"
|
|
33
|
+
/>
|
|
34
|
+
</svg>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function EyeClosedIcon() {
|
|
39
|
+
return (
|
|
40
|
+
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" aria-hidden="true">
|
|
41
|
+
<path
|
|
42
|
+
d="M14.7649 6.07596C14.9991 6.22231 15.0703 6.53079 14.9239 6.76495C14.4849 7.46743 13.9632 8.10645 13.3702 8.66305L14.5712 9.86406C14.7664 10.0593 14.7664 10.3759 14.5712 10.5712C14.3759 10.7664 14.0593 10.7664 13.8641 10.5712L12.6011 9.30817C11.805 9.90283 10.9089 10.3621 9.93375 10.651L10.383 12.3277C10.4544 12.5944 10.2961 12.8685 10.0294 12.94C9.76267 13.0115 9.4885 12.8532 9.41704 12.5865L8.95917 10.8775C8.48743 10.958 8.00036 11 7.50001 11C6.99965 11 6.51257 10.958 6.04082 10.8775L5.58299 12.5864C5.51153 12.8532 5.23737 13.0115 4.97064 12.94C4.7039 12.8686 4.5456 12.5944 4.61706 12.3277L5.06625 10.651C4.09111 10.3621 3.19503 9.90282 2.39889 9.30815L1.1359 10.5712C0.940638 10.7664 0.624058 10.7664 0.428798 10.5712C0.233537 10.3759 0.233537 10.0593 0.428798 9.86405L1.62982 8.66303C1.03682 8.10643 0.515113 7.46742 0.0760677 6.76495C-0.0702867 6.53079 0.000898544 6.22231 0.235065 6.07596C0.469231 5.9296 0.777703 6.00079 0.924058 6.23496C1.40354 7.00213 1.989 7.68057 2.66233 8.2427C2.67315 8.25096 2.6837 8.25972 2.69397 8.26898C4.00897 9.35527 5.65537 10 7.50001 10C10.3078 10 12.6564 8.5063 14.076 6.23495C14.2223 6.00079 14.5308 5.9296 14.7649 6.07596Z"
|
|
43
|
+
fill="currentColor"
|
|
44
|
+
fillRule="evenodd"
|
|
45
|
+
clipRule="evenodd"
|
|
46
|
+
/>
|
|
47
|
+
</svg>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type CommentAuthor, type MentionMember } from '@tldraw/mentions'
|
|
2
|
+
import { type ReactNode } from 'react'
|
|
3
|
+
import { type TLComment, type TLCommentId, type TLCommentThreadId } from 'tldraw'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The live, host-supplied half of commenting: who the viewer is, how author ids become names, read
|
|
7
|
+
* status, and the mention roster. (The static half is {@link CommentingOptions}, configured once on
|
|
8
|
+
* the tool.)
|
|
9
|
+
*
|
|
10
|
+
* Every commenting surface reads the same set, so a host mounting more than one — `CanvasComments`
|
|
11
|
+
* and `CanvasCommentsSidebar`, say — can build it once and spread it into each rather than
|
|
12
|
+
* repeating the props:
|
|
13
|
+
*
|
|
14
|
+
* ```tsx
|
|
15
|
+
* const commenting: CommentingContext = { currentUserId, resolveAuthor, isCommentUnread }
|
|
16
|
+
*
|
|
17
|
+
* <CanvasComments {...commenting} />
|
|
18
|
+
* <CanvasCommentsSidebar {...commenting} />
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* @public
|
|
22
|
+
*/
|
|
23
|
+
export interface CommentingContext {
|
|
24
|
+
/** The signed-in user's id, or null for a read-only viewer. Only a signed-in user composes. */
|
|
25
|
+
currentUserId: string | null
|
|
26
|
+
/** Map an author id to their display info, or `undefined` when the id can't be resolved. */
|
|
27
|
+
resolveAuthor(id: string): CommentAuthor | undefined
|
|
28
|
+
/** Called after any comment (a new thread's first comment, or a reply) is posted. */
|
|
29
|
+
onPostComment?(comment: TLComment): void
|
|
30
|
+
/** Whether a comment is unread for the current user (return true for unread). */
|
|
31
|
+
isCommentUnread?(commentId: TLCommentId): boolean
|
|
32
|
+
/**
|
|
33
|
+
* Called for each unread comment shown to the user in an open thread popover, so hosts can
|
|
34
|
+
* record a read receipt. Needs `isCommentUnread` to know what's unread.
|
|
35
|
+
*/
|
|
36
|
+
onCommentRead?(commentId: TLCommentId): void
|
|
37
|
+
/** Resolve the members matching an `@`-query in the composers (sync or async). */
|
|
38
|
+
getMentionSuggestions?(query: string): MentionMember[] | Promise<MentionMember[]>
|
|
39
|
+
/** Override a mention-picker row's content. */
|
|
40
|
+
renderMentionSuggestion?(member: MentionMember): ReactNode
|
|
41
|
+
/**
|
|
42
|
+
* The host's URL for a thread, so surfaces can link to it — e.g. the sidebar's rows render as
|
|
43
|
+
* anchors when this is present, letting ctrl/cmd-click and middle-click open the thread in a
|
|
44
|
+
* new tab (a plain click still selects the thread in place; the href isn't followed).
|
|
45
|
+
*/
|
|
46
|
+
getThreadHref?(threadId: TLCommentThreadId): string | undefined
|
|
47
|
+
}
|
package/src/canvas/hooks.ts
CHANGED
|
@@ -1,28 +1,69 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
computed,
|
|
3
|
+
Computed,
|
|
4
|
+
Editor,
|
|
5
|
+
TLComment,
|
|
6
|
+
TLCommentThread,
|
|
7
|
+
TLCommentThreadId,
|
|
8
|
+
useValue,
|
|
9
|
+
WeakCache,
|
|
10
|
+
} from 'tldraw'
|
|
11
|
+
import { getLiveComments, getLiveCommentThreads } from './comment-store'
|
|
3
12
|
|
|
4
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* The comment threads that should render (pins, sidebar), reactively — the live set described by
|
|
15
|
+
* {@link getLiveCommentThreads}. Use `getCommentThreads` for the unfiltered set, including
|
|
16
|
+
* soft-deleted threads awaiting the server's prune.
|
|
17
|
+
*
|
|
18
|
+
* @public
|
|
19
|
+
*/
|
|
5
20
|
export function useCommentThreads(editor: Editor): TLCommentThread[] {
|
|
6
|
-
return useValue('comment threads', () =>
|
|
21
|
+
return useValue('comment threads', () => getLiveCommentThreads(editor), [editor])
|
|
7
22
|
}
|
|
8
23
|
|
|
9
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Every live comment grouped by thread, oldest first within each thread. One computed shared by
|
|
26
|
+
* every mounted thread: each `useThreadComments` would otherwise scan and sort the whole comment
|
|
27
|
+
* set, so N open threads did N full passes on every comment mutation. Cached per editor so the
|
|
28
|
+
* grouping survives re-renders and is rebuilt only when the comment set actually changes.
|
|
29
|
+
*/
|
|
30
|
+
const commentsByThread = new WeakCache<Editor, Computed<Map<TLCommentThreadId, TLComment[]>>>()
|
|
31
|
+
|
|
32
|
+
function getCommentsByThread(editor: Editor) {
|
|
33
|
+
return commentsByThread.get(editor, () =>
|
|
34
|
+
computed('comments by thread', () => {
|
|
35
|
+
const byThread = new Map<TLCommentThreadId, TLComment[]>()
|
|
36
|
+
for (const comment of getLiveComments(editor)) {
|
|
37
|
+
const existing = byThread.get(comment.threadId)
|
|
38
|
+
if (existing) existing.push(comment)
|
|
39
|
+
else byThread.set(comment.threadId, [comment])
|
|
40
|
+
}
|
|
41
|
+
for (const comments of byThread.values()) {
|
|
42
|
+
comments.sort((a, b) => a.createdAt - b.createdAt)
|
|
43
|
+
}
|
|
44
|
+
return byThread
|
|
45
|
+
})
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A thread's live comments, oldest first, reactively. @public */
|
|
10
50
|
export function useThreadComments(editor: Editor, threadId: TLCommentThreadId): TLComment[] {
|
|
11
51
|
return useValue(
|
|
12
52
|
'thread comments',
|
|
13
|
-
() =>
|
|
14
|
-
getComments(editor)
|
|
15
|
-
.filter((c) => c.threadId === threadId)
|
|
16
|
-
.sort((a, b) => a.createdAt - b.createdAt),
|
|
53
|
+
() => getCommentsByThread(editor).get().get(threadId) ?? EMPTY_COMMENTS,
|
|
17
54
|
[editor, threadId]
|
|
18
55
|
)
|
|
19
56
|
}
|
|
20
57
|
|
|
21
|
-
/**
|
|
58
|
+
/** Shared empty result so a thread with no comments doesn't churn referential equality. */
|
|
59
|
+
const EMPTY_COMMENTS: TLComment[] = []
|
|
60
|
+
|
|
61
|
+
/** Every live comment in the store ({@link getLiveComments}), oldest first, reactively. Group by
|
|
62
|
+
* `threadId` for per-thread lists. @public */
|
|
22
63
|
export function useComments(editor: Editor): TLComment[] {
|
|
23
64
|
return useValue(
|
|
24
65
|
'all comments',
|
|
25
|
-
() =>
|
|
66
|
+
() => getLiveComments(editor).sort((a, b) => a.createdAt - b.createdAt),
|
|
26
67
|
[editor]
|
|
27
68
|
)
|
|
28
69
|
}
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import type { Editor } from 'tldraw'
|
|
2
2
|
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { commitCommentMutation } from './comment-mutations'
|
|
3
4
|
import { CommentTool } from './comment-tool'
|
|
4
|
-
import {
|
|
5
|
-
|
|
5
|
+
import {
|
|
6
|
+
defaultCommentingOptions,
|
|
7
|
+
getCanComment,
|
|
8
|
+
getCommentingOptions,
|
|
9
|
+
type CommentingOptions,
|
|
10
|
+
} from './options'
|
|
11
|
+
import { openThreadId, pendingComment } from './state'
|
|
6
12
|
|
|
7
13
|
// The StateNode constructor doesn't call any editor methods, so a bare stub is enough to
|
|
8
14
|
// instantiate a configured tool and read its merged options.
|
|
@@ -78,6 +84,50 @@ describe('getCommentingOptions', () => {
|
|
|
78
84
|
})
|
|
79
85
|
})
|
|
80
86
|
|
|
87
|
+
describe('getCanComment', () => {
|
|
88
|
+
it('defaults to requiring a current user', () => {
|
|
89
|
+
const { editor } = stubEditor(defaultCommentingOptions)
|
|
90
|
+
expect(getCanComment(editor, 'alice')).toBe(true)
|
|
91
|
+
expect(getCanComment(editor, null)).toBe(false)
|
|
92
|
+
expect(getCanComment(editor, undefined)).toBe(false)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('normalizes an undefined viewer to null for the callback', () => {
|
|
96
|
+
const calls: Array<string | null> = []
|
|
97
|
+
const { editor } = stubEditor({
|
|
98
|
+
...defaultCommentingOptions,
|
|
99
|
+
canComment: (ctx) => {
|
|
100
|
+
calls.push(ctx.currentUserId)
|
|
101
|
+
return false
|
|
102
|
+
},
|
|
103
|
+
})
|
|
104
|
+
getCanComment(editor, undefined)
|
|
105
|
+
expect(calls).toEqual([null])
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('delegates to the canComment callback, passing the editor and viewer', () => {
|
|
109
|
+
const calls: Array<{ editor: Editor; currentUserId: string | null }> = []
|
|
110
|
+
const { editor } = stubEditor({
|
|
111
|
+
...defaultCommentingOptions,
|
|
112
|
+
canComment: (ctx) => {
|
|
113
|
+
calls.push(ctx)
|
|
114
|
+
return ctx.currentUserId === 'alice'
|
|
115
|
+
},
|
|
116
|
+
})
|
|
117
|
+
expect(getCanComment(editor, 'alice')).toBe(true)
|
|
118
|
+
expect(getCanComment(editor, 'bob')).toBe(false)
|
|
119
|
+
expect(calls).toEqual([
|
|
120
|
+
{ editor, currentUserId: 'alice' },
|
|
121
|
+
{ editor, currentUserId: 'bob' },
|
|
122
|
+
])
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('lets the callback fully replace the signed-in default', () => {
|
|
126
|
+
const { editor } = stubEditor({ ...defaultCommentingOptions, canComment: () => true })
|
|
127
|
+
expect(getCanComment(editor, null)).toBe(true)
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
81
131
|
describe('commitCommentMutation', () => {
|
|
82
132
|
it('uses options.history for a mutation and returns the callback result', () => {
|
|
83
133
|
const { editor, runCalls } = stubEditor({
|