@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
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { Box, Mat, type Editor, type TLCommentAnchor, type TLCommentThread } from 'tldraw'
|
|
2
2
|
import { describe, expect, it } from 'vitest'
|
|
3
3
|
// This import is red until step 6's filter module is implemented — that is
|
|
4
4
|
// intentional. Implement `cluster-input.ts` per CLUSTERING-STEPS.md step 6
|
|
5
5
|
// until this suite passes, without modifying this file.
|
|
6
6
|
import { collectClusterLeaves } from './cluster-input'
|
|
7
|
+
import { defaultCommentingOptions, type CommentingOptions } from './options'
|
|
7
8
|
|
|
8
9
|
const CURRENT_PAGE = 'page:one'
|
|
9
10
|
const OTHER_PAGE = 'page:two'
|
|
@@ -27,19 +28,31 @@ function thread(
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
|
-
* Stub editor: the filter's
|
|
31
|
-
* and
|
|
32
|
-
*
|
|
31
|
+
* Stub editor: the filter's editor dependencies are the current page id, shape
|
|
32
|
+
* geometry and page transform, and the commenting options (via anchorPagePoint,
|
|
33
|
+
* read off the registered comment tool). `shapes` maps shape id → page bounds for
|
|
34
|
+
* unrotated shapes that exist, modeled as local geometry sized by the box plus a
|
|
35
|
+
* translate-only page transform; anything else resolves to undefined (deleted
|
|
36
|
+
* shape). `options` stands in for `CommentTool.configure({ ... })`; omit it for a
|
|
37
|
+
* editor with no comment tool registered, which falls back to the defaults.
|
|
33
38
|
*/
|
|
34
39
|
function stubEditor(
|
|
35
|
-
shapes: Record<string, { minX: number; minY: number; maxX: number; maxY: number }> = {}
|
|
40
|
+
shapes: Record<string, { minX: number; minY: number; maxX: number; maxY: number }> = {},
|
|
41
|
+
options?: Partial<CommentingOptions>
|
|
36
42
|
): Editor {
|
|
43
|
+
const shapeId = (shape: string | { id: string }) => (typeof shape === 'string' ? shape : shape.id)
|
|
37
44
|
return {
|
|
38
45
|
getCurrentPageId: () => CURRENT_PAGE,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
46
|
+
getStateDescendant: () =>
|
|
47
|
+
options ? { options: { ...defaultCommentingOptions, ...options } } : undefined,
|
|
48
|
+
getShape: (id: string) => (shapes[id] ? { id } : undefined),
|
|
49
|
+
getShapeGeometry: (shape: string | { id: string }) => {
|
|
50
|
+
const bounds = shapes[shapeId(shape)]
|
|
51
|
+
return { bounds: new Box(0, 0, bounds.maxX - bounds.minX, bounds.maxY - bounds.minY) }
|
|
52
|
+
},
|
|
53
|
+
getShapePageTransform: (shape: string | { id: string }) => {
|
|
54
|
+
const bounds = shapes[shapeId(shape)]
|
|
55
|
+
return bounds ? Mat.Translate(bounds.minX, bounds.minY) : undefined
|
|
43
56
|
},
|
|
44
57
|
} as unknown as Editor
|
|
45
58
|
}
|
|
@@ -67,28 +80,24 @@ describe('collectClusterLeaves anchor resolution', () => {
|
|
|
67
80
|
expect(leaves).toEqual([{ id: 't1', point: { x: 40, y: 60 } }])
|
|
68
81
|
})
|
|
69
82
|
|
|
70
|
-
it('maps shape
|
|
83
|
+
it('maps shape anchors to the shape bounds top-right corner', () => {
|
|
71
84
|
const editor = stubEditor({
|
|
72
85
|
'shape:a': { minX: 0, minY: 5, maxX: 100, maxY: 50 },
|
|
73
86
|
})
|
|
74
87
|
const leaves = collectClusterLeaves(
|
|
75
88
|
editor,
|
|
76
|
-
[
|
|
77
|
-
thread('t1', { type: 'shape', shapeId: 'shape:a' as any, x: 0, y: 0, isPrecise: false }),
|
|
78
|
-
thread('t2', { type: 'text-range', shapeId: 'shape:a' as any, from: 0, to: 3 }),
|
|
79
|
-
],
|
|
89
|
+
[thread('t1', { type: 'shape', shapeId: 'shape:a' as any, x: 0, y: 0, isPrecise: false })],
|
|
80
90
|
null
|
|
81
91
|
)
|
|
82
|
-
expect(leaves).toEqual([
|
|
83
|
-
{ id: 't1', point: { x: 100, y: 5 } },
|
|
84
|
-
{ id: 't2', point: { x: 100, y: 5 } },
|
|
85
|
-
])
|
|
92
|
+
expect(leaves).toEqual([{ id: 't1', point: { x: 100, y: 5 } }])
|
|
86
93
|
})
|
|
87
94
|
|
|
88
95
|
it('places imprecise shape leaves at a custom impreciseShapeAnchor, matching pin rendering', () => {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
96
|
+
// bottom-center instead of the default top-right
|
|
97
|
+
const editor = stubEditor(
|
|
98
|
+
{ 'shape:a': { minX: 0, minY: 0, maxX: 100, maxY: 50 } },
|
|
99
|
+
{ impreciseShapeAnchor: { x: 0.5, y: 1 } }
|
|
100
|
+
)
|
|
92
101
|
const threads = [
|
|
93
102
|
thread('imprecise', {
|
|
94
103
|
type: 'shape',
|
|
@@ -105,8 +114,7 @@ describe('collectClusterLeaves anchor resolution', () => {
|
|
|
105
114
|
isPrecise: true,
|
|
106
115
|
}),
|
|
107
116
|
]
|
|
108
|
-
|
|
109
|
-
const leaves = collectClusterLeaves(editor, threads, null, { x: 0.5, y: 1 })
|
|
117
|
+
const leaves = collectClusterLeaves(editor, threads, null)
|
|
110
118
|
expect(leaves).toEqual([
|
|
111
119
|
{ id: 'imprecise', point: { x: 50, y: 50 } },
|
|
112
120
|
{ id: 'precise', point: { x: 50, y: 25 } },
|
|
@@ -2,12 +2,11 @@ import type { Editor, TLCommentThread } from 'tldraw'
|
|
|
2
2
|
import type { LeafInput } from '../clustering/types'
|
|
3
3
|
import { anchorPagePoint } from './thread-state'
|
|
4
4
|
|
|
5
|
-
/** @
|
|
5
|
+
/** The cluster leaves for the threads pinned on the current page. @internal */
|
|
6
6
|
export function collectClusterLeaves(
|
|
7
7
|
editor: Editor,
|
|
8
8
|
threads: readonly TLCommentThread[],
|
|
9
|
-
openThreadId: string | null
|
|
10
|
-
impreciseShapeAnchor?: { x: number; y: number }
|
|
9
|
+
openThreadId: string | null
|
|
11
10
|
): LeafInput[] {
|
|
12
11
|
const pageId = editor.getCurrentPageId()
|
|
13
12
|
const leaves: LeafInput[] = []
|
|
@@ -16,7 +15,7 @@ export function collectClusterLeaves(
|
|
|
16
15
|
if (thread.id === openThreadId) continue
|
|
17
16
|
if (thread.pageId !== pageId) continue
|
|
18
17
|
|
|
19
|
-
const point = anchorPagePoint(editor, thread.anchor
|
|
18
|
+
const point = anchorPagePoint(editor, thread.anchor)
|
|
20
19
|
if (!point) continue
|
|
21
20
|
|
|
22
21
|
leaves.push({
|
|
@@ -17,5 +17,5 @@ export interface CommentBodyProps {
|
|
|
17
17
|
*/
|
|
18
18
|
export function CommentBody({ richText, resolveName }: CommentBodyProps) {
|
|
19
19
|
const html = useMemo(() => renderCommentHtml(richText, resolveName), [richText, resolveName])
|
|
20
|
-
return <div className="cmt-text" dangerouslySetInnerHTML={{ __html: html }} />
|
|
20
|
+
return <div className="tlui-cmt-text" dangerouslySetInnerHTML={{ __html: html }} />
|
|
21
21
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/* eslint-disable tldraw/no-direct-storage */
|
|
2
|
+
import type { TLRichText } from 'tldraw'
|
|
3
|
+
import { beforeEach, describe, expect, it } from 'vitest'
|
|
4
|
+
import {
|
|
5
|
+
clearCommentDraft,
|
|
6
|
+
getCommentDraft,
|
|
7
|
+
NEW_COMMENT_DRAFT,
|
|
8
|
+
replyDraftSlot,
|
|
9
|
+
saveCommentDraft,
|
|
10
|
+
} from './comment-drafts'
|
|
11
|
+
|
|
12
|
+
function richText(text: string): TLRichText {
|
|
13
|
+
return {
|
|
14
|
+
type: 'doc',
|
|
15
|
+
content: [{ type: 'paragraph', content: [{ type: 'text', text }] }],
|
|
16
|
+
} as unknown as TLRichText
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const EMPTY = { type: 'doc', content: [{ type: 'paragraph' }] } as unknown as TLRichText
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
localStorage.clear()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe('comment drafts', () => {
|
|
26
|
+
it('round-trips a draft through its slot', () => {
|
|
27
|
+
saveCommentDraft(NEW_COMMENT_DRAFT, richText('hello'))
|
|
28
|
+
expect(getCommentDraft(NEW_COMMENT_DRAFT)).toEqual(richText('hello'))
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('keeps reply drafts apart by thread id', () => {
|
|
32
|
+
saveCommentDraft(replyDraftSlot('comment-thread:a'), richText('for a'))
|
|
33
|
+
saveCommentDraft(replyDraftSlot('comment-thread:b'), richText('for b'))
|
|
34
|
+
expect(getCommentDraft(replyDraftSlot('comment-thread:a'))).toEqual(richText('for a'))
|
|
35
|
+
expect(getCommentDraft(replyDraftSlot('comment-thread:b'))).toEqual(richText('for b'))
|
|
36
|
+
expect(getCommentDraft(NEW_COMMENT_DRAFT)).toBeUndefined()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('saving empty content clears the slot instead', () => {
|
|
40
|
+
saveCommentDraft(NEW_COMMENT_DRAFT, richText('typed then deleted'))
|
|
41
|
+
saveCommentDraft(NEW_COMMENT_DRAFT, EMPTY)
|
|
42
|
+
expect(getCommentDraft(NEW_COMMENT_DRAFT)).toBeUndefined()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('clears a slot explicitly', () => {
|
|
46
|
+
saveCommentDraft(NEW_COMMENT_DRAFT, richText('posted'))
|
|
47
|
+
clearCommentDraft(NEW_COMMENT_DRAFT)
|
|
48
|
+
expect(getCommentDraft(NEW_COMMENT_DRAFT)).toBeUndefined()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('returns undefined for a corrupt stored value', () => {
|
|
52
|
+
localStorage.setItem('tldraw-comment-draft:new', '{not json')
|
|
53
|
+
expect(getCommentDraft(NEW_COMMENT_DRAFT)).toBeUndefined()
|
|
54
|
+
})
|
|
55
|
+
})
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { deleteFromLocalStorage, getFromLocalStorage, setInLocalStorage } from '@tldraw/utils'
|
|
2
|
+
import type { TLRichText } from 'tldraw'
|
|
3
|
+
import { isCommentEmpty } from '../ui/comment-extensions'
|
|
4
|
+
|
|
5
|
+
const DRAFT_KEY_PREFIX = 'tldraw-comment-draft'
|
|
6
|
+
|
|
7
|
+
/** The single slot for an unsent new-comment (placement composer) draft. The anchor is re-picked
|
|
8
|
+
* by clicking when the composer reopens, so only the text is worth keeping — and without a
|
|
9
|
+
* universal document id in the SDK, one slot is the honest scope. */
|
|
10
|
+
export const NEW_COMMENT_DRAFT = 'new'
|
|
11
|
+
|
|
12
|
+
/** The draft slot for an unsent reply on a thread. Thread record ids are globally unique and
|
|
13
|
+
* stable across reloads, so replies never need a document key. @public */
|
|
14
|
+
export function replyDraftSlot(threadId: string): string {
|
|
15
|
+
return `reply:${threadId}`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Read a saved unsent draft, or undefined when there is none (or it fails to parse). @public */
|
|
19
|
+
export function getCommentDraft(slot: string): TLRichText | undefined {
|
|
20
|
+
const raw = getFromLocalStorage(`${DRAFT_KEY_PREFIX}:${slot}`)
|
|
21
|
+
if (!raw) return undefined
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(raw) as TLRichText
|
|
24
|
+
} catch {
|
|
25
|
+
return undefined
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Save an unsent draft so click-away and reload don't lose it — the flip side of not warning
|
|
31
|
+
* before discard. Saving empty content clears the slot instead, so deleting your text and
|
|
32
|
+
* closing doesn't resurrect an empty draft later.
|
|
33
|
+
* @public
|
|
34
|
+
*/
|
|
35
|
+
export function saveCommentDraft(slot: string, value: TLRichText) {
|
|
36
|
+
if (isCommentEmpty(value)) {
|
|
37
|
+
clearCommentDraft(slot)
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
setInLocalStorage(`${DRAFT_KEY_PREFIX}:${slot}`, JSON.stringify(value))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Remove a saved draft (after posting, or when its content was emptied). @public */
|
|
44
|
+
export function clearCommentDraft(slot: string) {
|
|
45
|
+
deleteFromLocalStorage(`${DRAFT_KEY_PREFIX}:${slot}`)
|
|
46
|
+
}
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import {
|
|
2
|
+
commentSchemaRecords,
|
|
3
|
+
createCommentThread,
|
|
4
|
+
createShapeId,
|
|
5
|
+
createTLSchema,
|
|
6
|
+
createTLStore,
|
|
7
|
+
defaultBindingUtils,
|
|
8
|
+
defaultShapeUtils,
|
|
9
|
+
defaultTools,
|
|
10
|
+
Editor,
|
|
11
|
+
TLComment,
|
|
12
|
+
TLCommentThread,
|
|
13
|
+
toRichText,
|
|
14
|
+
createComment as tlCreateComment,
|
|
15
|
+
} from 'tldraw'
|
|
16
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
17
|
+
import {
|
|
18
|
+
commitCommentMutation,
|
|
19
|
+
deleteComment,
|
|
20
|
+
deleteThread,
|
|
21
|
+
editComment,
|
|
22
|
+
putCommentRecords,
|
|
23
|
+
putRecordsInCommit,
|
|
24
|
+
removeCommentRecords,
|
|
25
|
+
reopenThread,
|
|
26
|
+
resolveThread,
|
|
27
|
+
} from './comment-mutations'
|
|
28
|
+
import {
|
|
29
|
+
getCommentRecord,
|
|
30
|
+
getComments,
|
|
31
|
+
getCommentThreads,
|
|
32
|
+
getLiveComments,
|
|
33
|
+
getLiveCommentThreads,
|
|
34
|
+
} from './comment-store'
|
|
35
|
+
import { CommentTool } from './comment-tool'
|
|
36
|
+
import { openThreadId } from './state'
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* These need a real editor: what's under test is how each write interacts with the store and the
|
|
40
|
+
* undo stack, which is exactly what the history option governs.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
let editors: Editor[] = []
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
for (const editor of editors) editor.dispose()
|
|
47
|
+
editors = []
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
function makeEditor(tool?: ReturnType<typeof CommentTool.configure>): Editor {
|
|
51
|
+
const editor = new Editor({
|
|
52
|
+
store: createTLStore({ schema: createTLSchema({ records: commentSchemaRecords }) }),
|
|
53
|
+
shapeUtils: defaultShapeUtils,
|
|
54
|
+
bindingUtils: defaultBindingUtils,
|
|
55
|
+
tools: tool ? [...defaultTools, tool] : defaultTools,
|
|
56
|
+
getContainer: () => document.body,
|
|
57
|
+
})
|
|
58
|
+
editors.push(editor)
|
|
59
|
+
return editor
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function makeThread(editor: Editor, createdBy = 'me') {
|
|
63
|
+
const thread = createCommentThread({
|
|
64
|
+
pageId: editor.getCurrentPageId(),
|
|
65
|
+
anchor: { type: 'point', x: 0, y: 0 },
|
|
66
|
+
createdBy,
|
|
67
|
+
})
|
|
68
|
+
const comment = tlCreateComment({
|
|
69
|
+
threadId: thread.id,
|
|
70
|
+
pageId: thread.pageId,
|
|
71
|
+
authorId: createdBy,
|
|
72
|
+
body: toRichText('hello'),
|
|
73
|
+
})
|
|
74
|
+
putCommentRecords(editor, [thread, comment])
|
|
75
|
+
return { thread, comment }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function addComment(editor: Editor, thread: TLCommentThread, authorId = 'me') {
|
|
79
|
+
const comment = tlCreateComment({
|
|
80
|
+
threadId: thread.id,
|
|
81
|
+
pageId: thread.pageId,
|
|
82
|
+
authorId,
|
|
83
|
+
body: toRichText('reply'),
|
|
84
|
+
})
|
|
85
|
+
putCommentRecords(editor, [comment])
|
|
86
|
+
return comment
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function readComment(editor: Editor, comment: TLComment) {
|
|
90
|
+
return getCommentRecord(editor, comment.id) as TLComment | undefined
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function readThread(editor: Editor, thread: TLCommentThread) {
|
|
94
|
+
return getCommentRecord(editor, thread.id) as TLCommentThread | undefined
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
describe('editComment', () => {
|
|
98
|
+
it('replaces the body and stamps editedAt', () => {
|
|
99
|
+
const editor = makeEditor()
|
|
100
|
+
const { comment } = makeThread(editor)
|
|
101
|
+
expect(comment.editedAt).toBe(null)
|
|
102
|
+
|
|
103
|
+
editComment(editor, comment, toRichText('changed'))
|
|
104
|
+
|
|
105
|
+
const updated = readComment(editor, comment)!
|
|
106
|
+
expect(updated.body).toEqual(toRichText('changed'))
|
|
107
|
+
expect(typeof updated.editedAt).toBe('number')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
// The caller's record is a snapshot. Writing it back would revert every field it doesn't mean
|
|
111
|
+
// to touch — here `meta`, set after the caller took its copy.
|
|
112
|
+
it('edits the version in the store, not the copy it was handed', () => {
|
|
113
|
+
const editor = makeEditor()
|
|
114
|
+
const { comment } = makeThread(editor)
|
|
115
|
+
putCommentRecords(editor, [{ ...comment, meta: { pinned: true } }])
|
|
116
|
+
|
|
117
|
+
editComment(editor, comment, toRichText('changed'))
|
|
118
|
+
|
|
119
|
+
expect(readComment(editor, comment)).toMatchObject({
|
|
120
|
+
body: toRichText('changed'),
|
|
121
|
+
meta: { pinned: true },
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
// `put` is an upsert, so writing a snapshot back would re-create the record — with
|
|
126
|
+
// `isDeleted: false` — after someone else's delete had already taken it away.
|
|
127
|
+
it('does not resurrect a comment that has been removed since', () => {
|
|
128
|
+
const editor = makeEditor()
|
|
129
|
+
const { comment } = makeThread(editor)
|
|
130
|
+
removeCommentRecords(editor, [comment.id])
|
|
131
|
+
|
|
132
|
+
editComment(editor, comment, toRichText('changed'))
|
|
133
|
+
|
|
134
|
+
expect(readComment(editor, comment)).toBeUndefined()
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
describe('resolveThread and reopenThread', () => {
|
|
139
|
+
it('stamps who resolved the thread and when', () => {
|
|
140
|
+
const editor = makeEditor()
|
|
141
|
+
const { thread } = makeThread(editor)
|
|
142
|
+
|
|
143
|
+
resolveThread(editor, thread, 'ada')
|
|
144
|
+
|
|
145
|
+
const resolved = readThread(editor, thread)!.resolved!
|
|
146
|
+
expect(resolved.by).toBe('ada')
|
|
147
|
+
expect(typeof resolved.at).toBe('number')
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('clears the resolution when reopened', () => {
|
|
151
|
+
const editor = makeEditor()
|
|
152
|
+
const { thread } = makeThread(editor)
|
|
153
|
+
resolveThread(editor, thread, 'ada')
|
|
154
|
+
|
|
155
|
+
reopenThread(editor, readThread(editor, thread)!)
|
|
156
|
+
|
|
157
|
+
expect(readThread(editor, thread)!.resolved).toBe(null)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
// A thread's anchor moves without anyone touching the thread — a pin drag, or a pinned shape's
|
|
161
|
+
// delete converting it to a point. Resolving from a copy taken before that must not undo it.
|
|
162
|
+
it('resolves the version in the store, leaving an anchor moved since where it is', () => {
|
|
163
|
+
const editor = makeEditor()
|
|
164
|
+
const { thread } = makeThread(editor)
|
|
165
|
+
putCommentRecords(editor, [{ ...thread, anchor: { type: 'point', x: 50, y: 50 } }])
|
|
166
|
+
|
|
167
|
+
resolveThread(editor, thread, 'ada')
|
|
168
|
+
|
|
169
|
+
const updated = readThread(editor, thread)!
|
|
170
|
+
expect(updated.anchor).toEqual({ type: 'point', x: 50, y: 50 })
|
|
171
|
+
expect(updated.resolved!.by).toBe('ada')
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('reopens the version in the store, leaving an anchor moved since where it is', () => {
|
|
175
|
+
const editor = makeEditor()
|
|
176
|
+
const { thread } = makeThread(editor)
|
|
177
|
+
resolveThread(editor, thread, 'ada')
|
|
178
|
+
putCommentRecords(editor, [
|
|
179
|
+
{ ...readThread(editor, thread)!, anchor: { type: 'point', x: 50, y: 50 } },
|
|
180
|
+
])
|
|
181
|
+
|
|
182
|
+
reopenThread(editor, thread)
|
|
183
|
+
|
|
184
|
+
const updated = readThread(editor, thread)!
|
|
185
|
+
expect(updated.anchor).toEqual({ type: 'point', x: 50, y: 50 })
|
|
186
|
+
expect(updated.resolved).toBe(null)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('does nothing to a thread that has been removed since', () => {
|
|
190
|
+
const editor = makeEditor()
|
|
191
|
+
const { thread } = makeThread(editor)
|
|
192
|
+
removeCommentRecords(editor, [thread.id])
|
|
193
|
+
|
|
194
|
+
resolveThread(editor, thread, 'ada')
|
|
195
|
+
reopenThread(editor, thread)
|
|
196
|
+
|
|
197
|
+
expect(readThread(editor, thread)).toBeUndefined()
|
|
198
|
+
})
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
describe('deleteComment', () => {
|
|
202
|
+
it('soft-deletes: the record stays put with its flag set, for the server to prune', () => {
|
|
203
|
+
const editor = makeEditor()
|
|
204
|
+
const { comment } = makeThread(editor)
|
|
205
|
+
|
|
206
|
+
deleteComment(editor, comment)
|
|
207
|
+
|
|
208
|
+
expect(readComment(editor, comment)).toMatchObject({ isDeleted: true })
|
|
209
|
+
expect(getComments(editor)).toHaveLength(1)
|
|
210
|
+
expect(getLiveComments(editor)).toHaveLength(0)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('closes the thread when its last comment goes', () => {
|
|
214
|
+
const editor = makeEditor()
|
|
215
|
+
const { thread, comment } = makeThread(editor)
|
|
216
|
+
openThreadId.set(editor, thread.id)
|
|
217
|
+
|
|
218
|
+
deleteComment(editor, comment)
|
|
219
|
+
|
|
220
|
+
expect(openThreadId.get(editor)).toBe(null)
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('leaves the thread open while it still has comments', () => {
|
|
224
|
+
const editor = makeEditor()
|
|
225
|
+
const { thread, comment } = makeThread(editor)
|
|
226
|
+
addComment(editor, thread)
|
|
227
|
+
openThreadId.set(editor, thread.id)
|
|
228
|
+
|
|
229
|
+
deleteComment(editor, comment)
|
|
230
|
+
|
|
231
|
+
expect(openThreadId.get(editor)).toBe(thread.id)
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
// Deleting the same comment twice — a double activation, a handler holding a copy taken before
|
|
235
|
+
// the first delete — shouldn't read as "the last comment went" while a sibling is still live.
|
|
236
|
+
it('leaves the thread open when the comment was already deleted', () => {
|
|
237
|
+
const editor = makeEditor()
|
|
238
|
+
const { thread, comment } = makeThread(editor)
|
|
239
|
+
addComment(editor, thread)
|
|
240
|
+
deleteComment(editor, comment)
|
|
241
|
+
openThreadId.set(editor, thread.id)
|
|
242
|
+
|
|
243
|
+
deleteComment(editor, comment)
|
|
244
|
+
|
|
245
|
+
expect(openThreadId.get(editor)).toBe(thread.id)
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it('does not resurrect a comment that has been removed since', () => {
|
|
249
|
+
const editor = makeEditor()
|
|
250
|
+
const { comment } = makeThread(editor)
|
|
251
|
+
removeCommentRecords(editor, [comment.id])
|
|
252
|
+
|
|
253
|
+
deleteComment(editor, comment)
|
|
254
|
+
|
|
255
|
+
expect(readComment(editor, comment)).toBeUndefined()
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('leaves a different open thread alone', () => {
|
|
259
|
+
const editor = makeEditor()
|
|
260
|
+
const { comment } = makeThread(editor)
|
|
261
|
+
const other = makeThread(editor)
|
|
262
|
+
openThreadId.set(editor, other.thread.id)
|
|
263
|
+
|
|
264
|
+
deleteComment(editor, comment)
|
|
265
|
+
|
|
266
|
+
expect(openThreadId.get(editor)).toBe(other.thread.id)
|
|
267
|
+
})
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
describe('deleteThread', () => {
|
|
271
|
+
it('soft-deletes the thread record', () => {
|
|
272
|
+
const editor = makeEditor()
|
|
273
|
+
const { thread } = makeThread(editor)
|
|
274
|
+
|
|
275
|
+
deleteThread(editor, thread)
|
|
276
|
+
|
|
277
|
+
expect(readThread(editor, thread)).toMatchObject({ isDeleted: true })
|
|
278
|
+
expect(getCommentThreads(editor)).toHaveLength(1)
|
|
279
|
+
expect(getLiveCommentThreads(editor)).toHaveLength(0)
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it('closes the thread when it is the open one', () => {
|
|
283
|
+
const editor = makeEditor()
|
|
284
|
+
const { thread } = makeThread(editor)
|
|
285
|
+
openThreadId.set(editor, thread.id)
|
|
286
|
+
|
|
287
|
+
deleteThread(editor, thread)
|
|
288
|
+
|
|
289
|
+
expect(openThreadId.get(editor)).toBe(null)
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it('flags the version in the store, leaving an anchor moved since where it is', () => {
|
|
293
|
+
const editor = makeEditor()
|
|
294
|
+
const { thread } = makeThread(editor)
|
|
295
|
+
putCommentRecords(editor, [{ ...thread, anchor: { type: 'point', x: 50, y: 50 } }])
|
|
296
|
+
|
|
297
|
+
deleteThread(editor, thread)
|
|
298
|
+
|
|
299
|
+
expect(readThread(editor, thread)).toMatchObject({
|
|
300
|
+
isDeleted: true,
|
|
301
|
+
anchor: { type: 'point', x: 50, y: 50 },
|
|
302
|
+
})
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
it('does not resurrect a thread that has been removed since', () => {
|
|
306
|
+
const editor = makeEditor()
|
|
307
|
+
const { thread } = makeThread(editor)
|
|
308
|
+
removeCommentRecords(editor, [thread.id])
|
|
309
|
+
|
|
310
|
+
deleteThread(editor, thread)
|
|
311
|
+
|
|
312
|
+
expect(readThread(editor, thread)).toBeUndefined()
|
|
313
|
+
})
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
describe('history', () => {
|
|
317
|
+
it('does not put comment writes on the undo stack by default', () => {
|
|
318
|
+
const editor = makeEditor()
|
|
319
|
+
editor.markHistoryStoppingPoint()
|
|
320
|
+
const { comment } = makeThread(editor)
|
|
321
|
+
|
|
322
|
+
editor.undo()
|
|
323
|
+
|
|
324
|
+
expect(readComment(editor, comment)).toBeDefined()
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
it('puts them on the undo stack when the history option asks for it', () => {
|
|
328
|
+
const editor = makeEditor(CommentTool.configure({ history: 'record' }))
|
|
329
|
+
editor.markHistoryStoppingPoint()
|
|
330
|
+
const { comment } = makeThread(editor)
|
|
331
|
+
|
|
332
|
+
editor.undo()
|
|
333
|
+
|
|
334
|
+
expect(readComment(editor, comment)).toBeUndefined()
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
// The flag is write-once server-side, so an undo clearing it would be vetoed and rebased —
|
|
338
|
+
// leaving the UI showing a comment the server has already pruned. The shape gives the undo
|
|
339
|
+
// something recorded to rewind, so what's under test is that the delete isn't on the stack
|
|
340
|
+
// rather than that the stack is empty.
|
|
341
|
+
it('never makes a delete undoable, even with history: record', () => {
|
|
342
|
+
const editor = makeEditor(CommentTool.configure({ history: 'record' }))
|
|
343
|
+
const { comment } = makeThread(editor)
|
|
344
|
+
editor.markHistoryStoppingPoint()
|
|
345
|
+
const shapeId = createShapeId()
|
|
346
|
+
editor.createShape({ id: shapeId, type: 'geo', x: 0, y: 0 })
|
|
347
|
+
|
|
348
|
+
deleteComment(editor, comment)
|
|
349
|
+
editor.undo()
|
|
350
|
+
|
|
351
|
+
expect(editor.getShape(shapeId)).toBeUndefined()
|
|
352
|
+
expect(readComment(editor, comment)).toMatchObject({ isDeleted: true })
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
it('never makes a thread delete undoable, even with history: record', () => {
|
|
356
|
+
const editor = makeEditor(CommentTool.configure({ history: 'record' }))
|
|
357
|
+
const { thread } = makeThread(editor)
|
|
358
|
+
editor.markHistoryStoppingPoint()
|
|
359
|
+
const shapeId = createShapeId()
|
|
360
|
+
editor.createShape({ id: shapeId, type: 'geo', x: 0, y: 0 })
|
|
361
|
+
|
|
362
|
+
deleteThread(editor, thread)
|
|
363
|
+
editor.undo()
|
|
364
|
+
|
|
365
|
+
expect(editor.getShape(shapeId)).toBeUndefined()
|
|
366
|
+
expect(readThread(editor, thread)).toMatchObject({ isDeleted: true })
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
// A host can want pin drags undoable while posts and edits aren't. The drag owns its commit and
|
|
370
|
+
// writes inside it, which is what keeps `dragHistory` in charge of the mode.
|
|
371
|
+
it('lets dragHistory govern a drag on its own', () => {
|
|
372
|
+
const editor = makeEditor(CommentTool.configure({ history: 'ignore', dragHistory: 'record' }))
|
|
373
|
+
const { thread } = makeThread(editor)
|
|
374
|
+
editor.markHistoryStoppingPoint()
|
|
375
|
+
|
|
376
|
+
commitCommentMutation(
|
|
377
|
+
editor,
|
|
378
|
+
() => putRecordsInCommit(editor, [{ ...thread, anchor: { type: 'point', x: 50, y: 50 } }]),
|
|
379
|
+
'drag'
|
|
380
|
+
)
|
|
381
|
+
editor.undo()
|
|
382
|
+
|
|
383
|
+
expect(readThread(editor, thread)!.anchor).toEqual({ type: 'point', x: 0, y: 0 })
|
|
384
|
+
})
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
describe('getLiveComments and getLiveCommentThreads', () => {
|
|
388
|
+
it('hides a thread whose comments have all been deleted', () => {
|
|
389
|
+
const editor = makeEditor()
|
|
390
|
+
const { thread, comment } = makeThread(editor)
|
|
391
|
+
|
|
392
|
+
deleteComment(editor, comment)
|
|
393
|
+
|
|
394
|
+
// The thread record itself is untouched — the deleter may not be its creator — but with no
|
|
395
|
+
// live comment left it has no surface.
|
|
396
|
+
expect(readThread(editor, thread)!.isDeleted).toBe(false)
|
|
397
|
+
expect(getLiveCommentThreads(editor)).toHaveLength(0)
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
it('keeps a thread that still has a live comment', () => {
|
|
401
|
+
const editor = makeEditor()
|
|
402
|
+
const { thread, comment } = makeThread(editor)
|
|
403
|
+
addComment(editor, thread)
|
|
404
|
+
|
|
405
|
+
deleteComment(editor, comment)
|
|
406
|
+
|
|
407
|
+
expect(getLiveComments(editor)).toHaveLength(1)
|
|
408
|
+
expect(getLiveCommentThreads(editor)).toEqual([readThread(editor, thread)])
|
|
409
|
+
})
|
|
410
|
+
})
|