@blocknote/core 0.24.2 → 0.25.0
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/dist/blocknote.cjs +12 -0
- package/dist/blocknote.cjs.map +1 -0
- package/dist/blocknote.js +4754 -3514
- package/dist/blocknote.js.map +1 -1
- package/dist/comments.cjs +2 -0
- package/dist/comments.cjs.map +1 -0
- package/dist/comments.js +593 -0
- package/dist/comments.js.map +1 -0
- package/dist/style.css +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/webpack-stats.json +1 -1
- package/package.json +39 -26
- package/src/api/blockManipulation/commands/insertBlocks/__snapshots__/insertBlocks.test.ts.snap +1022 -378
- package/src/api/blockManipulation/commands/mergeBlocks/__snapshots__/mergeBlocks.test.ts.snap +730 -270
- package/src/api/blockManipulation/commands/moveBlocks/__snapshots__/moveBlocks.test.ts.snap +3100 -1260
- package/src/api/blockManipulation/commands/removeBlocks/__snapshots__/removeBlocks.test.ts.snap +438 -162
- package/src/api/blockManipulation/commands/replaceBlocks/__snapshots__/replaceBlocks.test.ts.snap +1168 -432
- package/src/api/blockManipulation/commands/splitBlock/__snapshots__/splitBlock.test.ts.snap +930 -378
- package/src/api/blockManipulation/commands/updateBlock/__snapshots__/updateBlock.test.ts.snap +2485 -1015
- package/src/api/blockManipulation/commands/updateBlock/updateBlock.test.ts +28 -1
- package/src/api/blockManipulation/commands/updateBlock/updateBlock.ts +1 -1
- package/src/api/blockManipulation/selections/__snapshots__/selection.test.ts.snap +292 -108
- package/src/api/blockManipulation/setupTestEnv.ts +14 -1
- package/src/api/blockManipulation/tables/tables.test.ts +1987 -0
- package/src/api/blockManipulation/tables/tables.ts +887 -0
- package/src/api/clipboard/__snapshots__/external/pasteEndOfParagraph.html +66 -24
- package/src/api/clipboard/__snapshots__/external/pasteEndOfParagraphText.html +66 -24
- package/src/api/clipboard/__snapshots__/external/pasteImage.html +66 -24
- package/src/api/clipboard/__snapshots__/external/pasteParagraphInCustomBlock.html +66 -24
- package/src/api/clipboard/__snapshots__/external/pasteTable.html +132 -48
- package/src/api/clipboard/__snapshots__/external/pasteTableInExistingTable.html +136 -44
- package/src/api/clipboard/toClipboard/copyExtension.ts +2 -3
- package/src/api/exporters/html/__snapshots__/table/headerCols/external.html +1 -0
- package/src/api/exporters/html/__snapshots__/table/headerCols/internal.html +1 -0
- package/src/api/exporters/html/__snapshots__/table/headerRows/external.html +1 -0
- package/src/api/exporters/html/__snapshots__/table/headerRows/internal.html +1 -0
- package/src/api/exporters/html/__snapshots__/table/headersRows/external.html +1 -0
- package/src/api/exporters/html/__snapshots__/table/headersRows/internal.html +1 -0
- package/src/api/exporters/html/__snapshots__/table/mixedCellColors/external.html +1 -0
- package/src/api/exporters/html/__snapshots__/table/mixedCellColors/internal.html +1 -0
- package/src/api/exporters/html/__snapshots__/table/mixedRowspansAndColspans/external.html +1 -0
- package/src/api/exporters/html/__snapshots__/table/mixedRowspansAndColspans/internal.html +1 -0
- package/src/api/exporters/markdown/__snapshots__/table/headerCols/markdown.md +4 -0
- package/src/api/exporters/markdown/__snapshots__/table/headerRows/markdown.md +4 -0
- package/src/api/exporters/markdown/__snapshots__/table/mixedCellColors/markdown.md +5 -0
- package/src/api/exporters/markdown/__snapshots__/table/mixedRowspansAndColspans/markdown.md +5 -0
- package/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap +985 -20
- package/src/api/nodeConversions/blockToNode.ts +63 -20
- package/src/api/nodeConversions/nodeToBlock.ts +75 -13
- package/src/api/parsers/html/__snapshots__/parse-notion-html.json +145 -54
- package/src/api/testUtil/cases/defaultSchema.ts +782 -9
- package/src/api/testUtil/partialBlockTestUtil.ts +39 -4
- package/src/blocks/TableBlockContent/TableBlockContent.ts +11 -5
- package/src/blocks/defaultBlockTypeGuards.ts +8 -0
- package/src/comments/index.ts +9 -0
- package/src/comments/models/User.ts +8 -0
- package/src/comments/threadstore/DefaultThreadStoreAuth.ts +106 -0
- package/src/comments/threadstore/ThreadStore.ts +134 -0
- package/src/comments/threadstore/ThreadStoreAuth.ts +13 -0
- package/src/comments/threadstore/TipTapThreadStore.ts +292 -0
- package/src/comments/threadstore/yjs/RESTYjsThreadStore.ts +144 -0
- package/src/comments/threadstore/yjs/YjsThreadStore.test.ts +294 -0
- package/src/comments/threadstore/yjs/YjsThreadStore.ts +340 -0
- package/src/comments/threadstore/yjs/YjsThreadStoreBase.ts +48 -0
- package/src/comments/threadstore/yjs/yjsHelpers.ts +121 -0
- package/src/comments/types.ts +117 -0
- package/src/editor/Block.css +16 -8
- package/src/editor/BlockNoteEditor.ts +269 -92
- package/src/editor/BlockNoteExtensions.ts +24 -1
- package/src/editor/BlockNoteTipTapEditor.ts +5 -1
- package/src/editor/editor.css +17 -0
- package/src/extensions/BackgroundColor/BackgroundColorExtension.ts +1 -1
- package/src/extensions/Comments/CommentMark.ts +61 -0
- package/src/extensions/Comments/CommentsPlugin.ts +301 -0
- package/src/extensions/Comments/userstore/UserStore.ts +72 -0
- package/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +9 -5
- package/src/extensions/LinkToolbar/LinkToolbarPlugin.ts +3 -3
- package/src/extensions/ShowSelection/ShowSelectionPlugin.ts +52 -0
- package/src/extensions/TableHandles/TableHandlesPlugin.ts +409 -57
- package/src/extensions/TextAlignment/TextAlignmentExtension.ts +2 -0
- package/src/extensions/TextColor/TextColorExtension.ts +1 -1
- package/src/i18n/locales/ar.ts +23 -0
- package/src/i18n/locales/de.ts +15 -0
- package/src/i18n/locales/en.ts +25 -1
- package/src/i18n/locales/es.ts +16 -1
- package/src/i18n/locales/fr.ts +23 -0
- package/src/i18n/locales/hr.ts +18 -0
- package/src/i18n/locales/is.ts +24 -1
- package/src/i18n/locales/it.ts +15 -0
- package/src/i18n/locales/ja.ts +23 -0
- package/src/i18n/locales/ko.ts +23 -0
- package/src/i18n/locales/nl.ts +23 -0
- package/src/i18n/locales/no.ts +23 -0
- package/src/i18n/locales/pl.ts +23 -0
- package/src/i18n/locales/pt.ts +23 -0
- package/src/i18n/locales/ru.ts +23 -0
- package/src/i18n/locales/uk.ts +23 -0
- package/src/i18n/locales/vi.ts +23 -0
- package/src/i18n/locales/zh.ts +23 -0
- package/src/index.ts +6 -4
- package/src/schema/blocks/types.ts +32 -2
- package/src/util/browser.ts +1 -1
- package/src/util/table.ts +107 -0
- package/types/src/api/blockManipulation/tables/tables.d.ts +343 -0
- package/types/src/api/blockManipulation/tables/tables.test.d.ts +1 -0
- package/types/src/api/clipboard/toClipboard/copyExtension.d.ts +1 -1
- package/types/src/blocks/TableBlockContent/TableBlockContent.d.ts +1 -2
- package/types/src/blocks/defaultBlockTypeGuards.d.ts +3 -0
- package/types/src/comments/index.d.ts +9 -0
- package/types/src/comments/models/User.d.ts +8 -0
- package/types/src/comments/threadstore/DefaultThreadStoreAuth.d.ts +47 -0
- package/types/src/comments/threadstore/ThreadStore.d.ts +121 -0
- package/types/src/comments/threadstore/ThreadStoreAuth.d.ts +12 -0
- package/types/src/comments/threadstore/TipTapThreadStore.d.ts +97 -0
- package/types/src/comments/threadstore/yjs/RESTYjsThreadStore.d.ts +83 -0
- package/types/src/comments/threadstore/yjs/YjsThreadStore.d.ts +79 -0
- package/types/src/comments/threadstore/yjs/YjsThreadStore.test.d.ts +1 -0
- package/types/src/comments/threadstore/yjs/YjsThreadStoreBase.d.ts +15 -0
- package/types/src/comments/threadstore/yjs/yjsHelpers.d.ts +13 -0
- package/types/src/comments/types.d.ts +109 -0
- package/types/src/editor/BlockNoteEditor.d.ts +146 -66
- package/types/src/editor/BlockNoteExtensions.d.ts +4 -0
- package/types/src/extensions/Collaboration/createCollaborationExtensions.d.ts +1 -1
- package/types/src/extensions/Comments/CommentMark.d.ts +2 -0
- package/types/src/extensions/Comments/CommentsPlugin.d.ts +49 -0
- package/types/src/extensions/Comments/userstore/UserStore.d.ts +31 -0
- package/types/src/extensions/ShowSelection/ShowSelectionPlugin.d.ts +15 -0
- package/types/src/extensions/TableHandles/TableHandlesPlugin.d.ts +66 -1
- package/types/src/i18n/locales/de.d.ts +15 -0
- package/types/src/i18n/locales/en.d.ts +20 -0
- package/types/src/i18n/locales/es.d.ts +15 -0
- package/types/src/i18n/locales/hr.d.ts +18 -0
- package/types/src/i18n/locales/it.d.ts +15 -0
- package/types/src/index.d.ts +5 -4
- package/types/src/pm-nodes/BlockContainer.d.ts +2 -2
- package/types/src/pm-nodes/BlockGroup.d.ts +2 -2
- package/types/src/schema/blocks/types.d.ts +23 -2
- package/types/src/util/browser.d.ts +1 -1
- package/types/src/util/table.d.ts +12 -0
- package/dist/blocknote.umd.cjs +0 -11
- package/dist/blocknote.umd.cjs.map +0 -1
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { Block, PartialBlock } from "../../blocks/defaultBlocks.js";
|
|
2
2
|
import { BlockNoteSchema } from "../../editor/BlockNoteSchema.js";
|
|
3
3
|
import UniqueID from "../../extensions/UniqueID/UniqueID.js";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
BlockSchema,
|
|
6
|
+
PartialTableCell,
|
|
7
|
+
TableCell,
|
|
8
|
+
TableContent,
|
|
9
|
+
} from "../../schema/blocks/types.js";
|
|
5
10
|
import {
|
|
6
11
|
InlineContent,
|
|
7
12
|
InlineContentSchema,
|
|
@@ -28,8 +33,16 @@ function textShorthandToStyledText(
|
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
function partialContentToInlineContent(
|
|
31
|
-
content:
|
|
32
|
-
|
|
36
|
+
content:
|
|
37
|
+
| PartialInlineContent<any, any>
|
|
38
|
+
| PartialTableCell<any, any>
|
|
39
|
+
| TableContent<any>
|
|
40
|
+
| undefined
|
|
41
|
+
):
|
|
42
|
+
| InlineContent<any, any>[]
|
|
43
|
+
| TableContent<any>
|
|
44
|
+
| TableCell<any, any>
|
|
45
|
+
| undefined {
|
|
33
46
|
if (typeof content === "string") {
|
|
34
47
|
return textShorthandToStyledText(content);
|
|
35
48
|
}
|
|
@@ -59,6 +72,8 @@ function partialContentToInlineContent(
|
|
|
59
72
|
return {
|
|
60
73
|
type: "tableContent",
|
|
61
74
|
columnWidths: content.columnWidths,
|
|
75
|
+
headerRows: content.headerRows,
|
|
76
|
+
headerCols: content.headerCols,
|
|
62
77
|
rows: content.rows.map((row) => ({
|
|
63
78
|
...row,
|
|
64
79
|
cells: row.cells.map(
|
|
@@ -66,6 +81,18 @@ function partialContentToInlineContent(
|
|
|
66
81
|
),
|
|
67
82
|
})),
|
|
68
83
|
};
|
|
84
|
+
} else if (content?.type === "tableCell") {
|
|
85
|
+
return {
|
|
86
|
+
type: "tableCell",
|
|
87
|
+
content: partialContentToInlineContent(content.content) as any[],
|
|
88
|
+
props: {
|
|
89
|
+
backgroundColor: content.props?.backgroundColor ?? "default",
|
|
90
|
+
textColor: content.props?.textColor ?? "default",
|
|
91
|
+
textAlignment: content.props?.textAlignment ?? "left",
|
|
92
|
+
colspan: content.props?.colspan ?? 1,
|
|
93
|
+
rowspan: content.props?.rowspan ?? 1,
|
|
94
|
+
},
|
|
95
|
+
} satisfies TableCell<any, any>;
|
|
69
96
|
}
|
|
70
97
|
|
|
71
98
|
return content;
|
|
@@ -103,7 +130,13 @@ export function partialBlockToBlockForTesting<
|
|
|
103
130
|
contentType === "inline"
|
|
104
131
|
? []
|
|
105
132
|
: contentType === "table"
|
|
106
|
-
? {
|
|
133
|
+
? {
|
|
134
|
+
type: "tableContent",
|
|
135
|
+
columnWidths: undefined,
|
|
136
|
+
headerRows: undefined,
|
|
137
|
+
headerCols: undefined,
|
|
138
|
+
rows: [],
|
|
139
|
+
}
|
|
107
140
|
: (undefined as any),
|
|
108
141
|
children: [] as any,
|
|
109
142
|
...partialBlock,
|
|
@@ -131,6 +164,8 @@ export function partialBlockToBlockForTesting<
|
|
|
131
164
|
content?.columnWidths ||
|
|
132
165
|
content?.rows[0]?.cells.map(() => undefined) ||
|
|
133
166
|
[],
|
|
167
|
+
headerRows: content?.headerRows || undefined,
|
|
168
|
+
headerCols: content?.headerCols || undefined,
|
|
134
169
|
rows:
|
|
135
170
|
content?.rows.map((row) => ({
|
|
136
171
|
cells: row.cells.map((cell) => partialContentToInlineContent(cell)),
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { Node } from "@tiptap/core";
|
|
2
1
|
import { TableCell } from "@tiptap/extension-table-cell";
|
|
3
2
|
import { TableHeader } from "@tiptap/extension-table-header";
|
|
4
3
|
import { TableRow } from "@tiptap/extension-table-row";
|
|
@@ -102,12 +101,12 @@ export const TableBlockContent = createStronglyTypedTiptapNode({
|
|
|
102
101
|
return new BlockNoteTableView(node, EMPTY_CELL_WIDTH, {
|
|
103
102
|
...(this.options.domAttributes?.blockContent || {}),
|
|
104
103
|
...HTMLAttributes,
|
|
105
|
-
}) as NodeView;
|
|
104
|
+
}) as NodeView; // needs cast, tiptap types (wrongly) doesn't support return tableview here
|
|
106
105
|
};
|
|
107
106
|
},
|
|
108
107
|
});
|
|
109
108
|
|
|
110
|
-
const TableParagraph =
|
|
109
|
+
const TableParagraph = createStronglyTypedTiptapNode({
|
|
111
110
|
name: "tableParagraph",
|
|
112
111
|
group: "tableContent",
|
|
113
112
|
content: "inline*",
|
|
@@ -160,10 +159,17 @@ export const Table = createBlockSpecFromStronglyTypedTiptapNode(
|
|
|
160
159
|
TableExtension,
|
|
161
160
|
TableParagraph,
|
|
162
161
|
TableHeader.extend({
|
|
163
|
-
|
|
162
|
+
/**
|
|
163
|
+
* We allow table headers and cells to have multiple tableContent nodes because
|
|
164
|
+
* when merging cells, prosemirror-tables will concat the contents of the cells naively.
|
|
165
|
+
* This would cause that content to overflow into other cells when prosemirror tries to enforce the cell structure.
|
|
166
|
+
*
|
|
167
|
+
* So, we manually fix this up when reading back in the `nodeToBlock` and only ever place a single tableContent back into the cell.
|
|
168
|
+
*/
|
|
169
|
+
content: "tableContent+",
|
|
164
170
|
}),
|
|
165
171
|
TableCell.extend({
|
|
166
|
-
content: "tableContent",
|
|
172
|
+
content: "tableContent+",
|
|
167
173
|
}),
|
|
168
174
|
TableRow,
|
|
169
175
|
]
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CellSelection } from "prosemirror-tables";
|
|
1
2
|
import type { BlockNoteEditor } from "../editor/BlockNoteEditor.js";
|
|
2
3
|
import {
|
|
3
4
|
BlockFromConfig,
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
defaultInlineContentSchema,
|
|
15
16
|
} from "./defaultBlocks.js";
|
|
16
17
|
import { defaultProps } from "./defaultProps.js";
|
|
18
|
+
import { Selection } from "prosemirror-state";
|
|
17
19
|
|
|
18
20
|
export function checkDefaultBlockTypeInSchema<
|
|
19
21
|
BlockType extends keyof DefaultBlockSchema,
|
|
@@ -159,3 +161,9 @@ export function checkBlockHasDefaultProp<
|
|
|
159
161
|
> {
|
|
160
162
|
return checkBlockTypeHasDefaultProp(prop, block.type, editor);
|
|
161
163
|
}
|
|
164
|
+
|
|
165
|
+
export function isTableCellSelection(
|
|
166
|
+
selection: Selection
|
|
167
|
+
): selection is CellSelection {
|
|
168
|
+
return selection instanceof CellSelection;
|
|
169
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from "./models/User.js";
|
|
2
|
+
export * from "./threadstore/DefaultThreadStoreAuth.js";
|
|
3
|
+
export * from "./threadstore/ThreadStore.js";
|
|
4
|
+
export * from "./threadstore/ThreadStoreAuth.js";
|
|
5
|
+
export * from "./threadstore/TipTapThreadStore.js";
|
|
6
|
+
export * from "./threadstore/yjs/RESTYjsThreadStore.js";
|
|
7
|
+
export * from "./threadstore/yjs/YjsThreadStore.js";
|
|
8
|
+
export * from "./threadstore/yjs/YjsThreadStoreBase.js";
|
|
9
|
+
export * from "./types.js";
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { CommentData, ThreadData } from "../types.js";
|
|
2
|
+
import { ThreadStoreAuth } from "./ThreadStoreAuth.js";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* The DefaultThreadStoreAuth class defines the authorization rules for interacting with comments.
|
|
6
|
+
* We take a role ("comment" or "editor") and implement the rules.
|
|
7
|
+
*
|
|
8
|
+
* This class is then used in the UI to show / hide specific interactions.
|
|
9
|
+
*
|
|
10
|
+
* Rules:
|
|
11
|
+
* - View-only users should not be able to see any comments
|
|
12
|
+
* - Comment-only users and editors can:
|
|
13
|
+
* - - create new comments / replies / reactions
|
|
14
|
+
* - - edit / delete their own comments / reactions
|
|
15
|
+
* - - resolve / unresolve threads
|
|
16
|
+
* - Editors can also delete any comment or thread
|
|
17
|
+
*/
|
|
18
|
+
export class DefaultThreadStoreAuth extends ThreadStoreAuth {
|
|
19
|
+
constructor(
|
|
20
|
+
private readonly userId: string,
|
|
21
|
+
private readonly role: "comment" | "editor"
|
|
22
|
+
) {
|
|
23
|
+
super();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Auth: should be possible by anyone with comment access
|
|
28
|
+
*/
|
|
29
|
+
canCreateThread(): boolean {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Auth: should be possible by anyone with comment access
|
|
35
|
+
*/
|
|
36
|
+
canAddComment(_thread: ThreadData): boolean {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Auth: should only be possible by the comment author
|
|
42
|
+
*/
|
|
43
|
+
canUpdateComment(comment: CommentData): boolean {
|
|
44
|
+
return comment.userId === this.userId;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Auth: should be possible by the comment author OR an editor of the document
|
|
49
|
+
*/
|
|
50
|
+
canDeleteComment(comment: CommentData): boolean {
|
|
51
|
+
return comment.userId === this.userId || this.role === "editor";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Auth: should only be possible by an editor of the document
|
|
56
|
+
*/
|
|
57
|
+
canDeleteThread(_thread: ThreadData): boolean {
|
|
58
|
+
return this.role === "editor";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Auth: should be possible by anyone with comment access
|
|
63
|
+
*/
|
|
64
|
+
canResolveThread(_thread: ThreadData): boolean {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Auth: should be possible by anyone with comment access
|
|
70
|
+
*/
|
|
71
|
+
canUnresolveThread(_thread: ThreadData): boolean {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Auth: should be possible by anyone with comment access
|
|
77
|
+
*
|
|
78
|
+
* Note: will also check if the user has already reacted with the same emoji. TBD: is that a nice design or should this responsibility be outside of auth?
|
|
79
|
+
*/
|
|
80
|
+
canAddReaction(comment: CommentData, emoji?: string): boolean {
|
|
81
|
+
if (!emoji) {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return !comment.reactions.some(
|
|
86
|
+
(reaction) =>
|
|
87
|
+
reaction.emoji === emoji && reaction.userIds.includes(this.userId)
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Auth: should be possible by anyone with comment access
|
|
93
|
+
*
|
|
94
|
+
* Note: will also check if the user has already reacted with the same emoji. TBD: is that a nice design or should this responsibility be outside of auth?
|
|
95
|
+
*/
|
|
96
|
+
canDeleteReaction(comment: CommentData, emoji?: string): boolean {
|
|
97
|
+
if (!emoji) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return comment.reactions.some(
|
|
102
|
+
(reaction) =>
|
|
103
|
+
reaction.emoji === emoji && reaction.userIds.includes(this.userId)
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { CommentBody, CommentData, ThreadData } from "../types.js";
|
|
2
|
+
import { ThreadStoreAuth } from "./ThreadStoreAuth.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ThreadStore is an abstract class that defines the interface
|
|
6
|
+
* to read / add / update / delete threads and comments.
|
|
7
|
+
*/
|
|
8
|
+
export abstract class ThreadStore {
|
|
9
|
+
public readonly auth: ThreadStoreAuth;
|
|
10
|
+
|
|
11
|
+
constructor(auth: ThreadStoreAuth) {
|
|
12
|
+
this.auth = auth;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A "thread" in the ThreadStore only contains information about the content
|
|
17
|
+
* of the thread / comments. It does not contain information about the position.
|
|
18
|
+
*
|
|
19
|
+
* This function can be implemented to store the thread in the document (by creating a mark)
|
|
20
|
+
* If not implemented, default behavior will apply (creating the mark via TipTap)
|
|
21
|
+
* See CommentsPlugin.ts for more details.
|
|
22
|
+
*/
|
|
23
|
+
abstract addThreadToDocument?(options: {
|
|
24
|
+
threadId: string;
|
|
25
|
+
selection: {
|
|
26
|
+
prosemirror: {
|
|
27
|
+
head: number;
|
|
28
|
+
anchor: number;
|
|
29
|
+
};
|
|
30
|
+
yjs: {
|
|
31
|
+
head: any;
|
|
32
|
+
anchor: any;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
}): Promise<void>;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Creates a new thread with an initial comment.
|
|
39
|
+
*/
|
|
40
|
+
abstract createThread(options: {
|
|
41
|
+
initialComment: {
|
|
42
|
+
body: CommentBody;
|
|
43
|
+
metadata?: any;
|
|
44
|
+
};
|
|
45
|
+
metadata?: any;
|
|
46
|
+
}): Promise<ThreadData>;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Adds a comment to a thread.
|
|
50
|
+
*/
|
|
51
|
+
abstract addComment(options: {
|
|
52
|
+
comment: {
|
|
53
|
+
body: CommentBody;
|
|
54
|
+
metadata?: any;
|
|
55
|
+
};
|
|
56
|
+
threadId: string;
|
|
57
|
+
}): Promise<CommentData>;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Updates a comment in a thread.
|
|
61
|
+
*/
|
|
62
|
+
abstract updateComment(options: {
|
|
63
|
+
comment: {
|
|
64
|
+
body: CommentBody;
|
|
65
|
+
metadata?: any;
|
|
66
|
+
};
|
|
67
|
+
threadId: string;
|
|
68
|
+
commentId: string;
|
|
69
|
+
}): Promise<void>;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Deletes a comment from a thread.
|
|
73
|
+
*/
|
|
74
|
+
abstract deleteComment(options: {
|
|
75
|
+
threadId: string;
|
|
76
|
+
commentId: string;
|
|
77
|
+
}): Promise<void>;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Deletes a thread.
|
|
81
|
+
*/
|
|
82
|
+
abstract deleteThread(options: { threadId: string }): Promise<void>;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Marks a thread as resolved.
|
|
86
|
+
*/
|
|
87
|
+
abstract resolveThread(options: { threadId: string }): Promise<void>;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Marks a thread as unresolved.
|
|
91
|
+
*/
|
|
92
|
+
abstract unresolveThread(options: { threadId: string }): Promise<void>;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Adds a reaction to a comment.
|
|
96
|
+
*
|
|
97
|
+
* Auth: should be possible by anyone with comment access
|
|
98
|
+
*/
|
|
99
|
+
abstract addReaction(options: {
|
|
100
|
+
threadId: string;
|
|
101
|
+
commentId: string;
|
|
102
|
+
emoji: string;
|
|
103
|
+
}): Promise<void>;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Deletes a reaction from a comment.
|
|
107
|
+
*
|
|
108
|
+
* Auth: should be possible by the reaction author
|
|
109
|
+
*/
|
|
110
|
+
abstract deleteReaction(options: {
|
|
111
|
+
threadId: string;
|
|
112
|
+
commentId: string;
|
|
113
|
+
emoji: string;
|
|
114
|
+
}): Promise<void>;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Retrieve data for a specific thread.
|
|
118
|
+
*/
|
|
119
|
+
abstract getThread(threadId: string): ThreadData;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Retrieve all threads.
|
|
123
|
+
*/
|
|
124
|
+
abstract getThreads(): Map<string, ThreadData>;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Subscribe to changes in the thread store.
|
|
128
|
+
*
|
|
129
|
+
* @returns a function to unsubscribe from the thread store
|
|
130
|
+
*/
|
|
131
|
+
abstract subscribe(
|
|
132
|
+
cb: (threads: Map<string, ThreadData>) => void
|
|
133
|
+
): () => void;
|
|
134
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { CommentData, ThreadData } from "../types.js";
|
|
2
|
+
|
|
3
|
+
export abstract class ThreadStoreAuth {
|
|
4
|
+
abstract canCreateThread(): boolean;
|
|
5
|
+
abstract canAddComment(thread: ThreadData): boolean;
|
|
6
|
+
abstract canUpdateComment(comment: CommentData): boolean;
|
|
7
|
+
abstract canDeleteComment(comment: CommentData): boolean;
|
|
8
|
+
abstract canDeleteThread(thread: ThreadData): boolean;
|
|
9
|
+
abstract canResolveThread(thread: ThreadData): boolean;
|
|
10
|
+
abstract canUnresolveThread(thread: ThreadData): boolean;
|
|
11
|
+
abstract canAddReaction(comment: CommentData, emoji?: string): boolean;
|
|
12
|
+
abstract canDeleteReaction(comment: CommentData, emoji?: string): boolean;
|
|
13
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
TCollabComment,
|
|
3
|
+
TCollabThread,
|
|
4
|
+
TiptapCollabProvider,
|
|
5
|
+
} from "@hocuspocus/provider";
|
|
6
|
+
import {
|
|
7
|
+
CommentBody,
|
|
8
|
+
CommentData,
|
|
9
|
+
CommentReactionData,
|
|
10
|
+
ThreadData,
|
|
11
|
+
} from "../types.js";
|
|
12
|
+
import { ThreadStore } from "./ThreadStore.js";
|
|
13
|
+
import { ThreadStoreAuth } from "./ThreadStoreAuth.js";
|
|
14
|
+
|
|
15
|
+
type ReactionAsTiptapData = {
|
|
16
|
+
emoji: string;
|
|
17
|
+
createdAt: number;
|
|
18
|
+
userId: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The `TiptapThreadStore` integrates with Tiptap's collaboration provider for comment management.
|
|
23
|
+
* You can pass a `TiptapCollabProvider` to the constructor which takes care of storing the comments.
|
|
24
|
+
*
|
|
25
|
+
* Under the hood, this actually works similarly to the `YjsThreadStore` implementation. (comments are stored in the Yjs document)
|
|
26
|
+
*/
|
|
27
|
+
export class TiptapThreadStore extends ThreadStore {
|
|
28
|
+
constructor(
|
|
29
|
+
private readonly userId: string,
|
|
30
|
+
private readonly provider: TiptapCollabProvider,
|
|
31
|
+
auth: ThreadStoreAuth // TODO: use?
|
|
32
|
+
) {
|
|
33
|
+
super(auth);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Creates a new thread with an initial comment.
|
|
38
|
+
*/
|
|
39
|
+
public async createThread(options: {
|
|
40
|
+
initialComment: {
|
|
41
|
+
body: CommentBody;
|
|
42
|
+
metadata?: any;
|
|
43
|
+
};
|
|
44
|
+
metadata?: any;
|
|
45
|
+
}): Promise<ThreadData> {
|
|
46
|
+
let thread = this.provider.createThread({
|
|
47
|
+
data: options.metadata,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
thread = this.provider.addComment(thread.id, {
|
|
51
|
+
content: options.initialComment.body,
|
|
52
|
+
data: {
|
|
53
|
+
metadata: options.initialComment.metadata,
|
|
54
|
+
userId: this.userId,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return this.tiptapThreadToThreadData(thread);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// TipTapThreadStore does not support addThreadToDocument
|
|
62
|
+
public addThreadToDocument = undefined;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Adds a comment to a thread.
|
|
66
|
+
*/
|
|
67
|
+
public async addComment(options: {
|
|
68
|
+
comment: {
|
|
69
|
+
body: CommentBody;
|
|
70
|
+
metadata?: any;
|
|
71
|
+
};
|
|
72
|
+
threadId: string;
|
|
73
|
+
}): Promise<CommentBody> {
|
|
74
|
+
const thread = this.provider.addComment(options.threadId, {
|
|
75
|
+
content: options.comment.body,
|
|
76
|
+
data: {
|
|
77
|
+
metadata: options.comment.metadata,
|
|
78
|
+
userId: this.userId,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return this.tiptapCommentToCommentData(
|
|
83
|
+
thread.comments[thread.comments.length - 1]
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Updates a comment in a thread.
|
|
89
|
+
*/
|
|
90
|
+
public async updateComment(options: {
|
|
91
|
+
comment: {
|
|
92
|
+
body: CommentBody;
|
|
93
|
+
metadata?: any;
|
|
94
|
+
};
|
|
95
|
+
threadId: string;
|
|
96
|
+
commentId: string;
|
|
97
|
+
}) {
|
|
98
|
+
const comment = this.provider.getThreadComment(
|
|
99
|
+
options.threadId,
|
|
100
|
+
options.commentId,
|
|
101
|
+
true
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
if (!comment) {
|
|
105
|
+
throw new Error("Comment not found");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
this.provider.updateComment(options.threadId, options.commentId, {
|
|
109
|
+
content: options.comment.body,
|
|
110
|
+
data: {
|
|
111
|
+
...comment.data,
|
|
112
|
+
metadata: options.comment.metadata,
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private tiptapCommentToCommentData(comment: TCollabComment): CommentData {
|
|
118
|
+
const reactions: CommentReactionData[] = [];
|
|
119
|
+
|
|
120
|
+
for (const reaction of (comment.data?.reactions ||
|
|
121
|
+
[]) as ReactionAsTiptapData[]) {
|
|
122
|
+
const existingReaction = reactions.find(
|
|
123
|
+
(r) => r.emoji === reaction.emoji
|
|
124
|
+
);
|
|
125
|
+
if (existingReaction) {
|
|
126
|
+
existingReaction.userIds.push(reaction.userId);
|
|
127
|
+
existingReaction.createdAt = new Date(
|
|
128
|
+
Math.min(existingReaction.createdAt.getTime(), reaction.createdAt)
|
|
129
|
+
);
|
|
130
|
+
} else {
|
|
131
|
+
reactions.push({
|
|
132
|
+
emoji: reaction.emoji,
|
|
133
|
+
createdAt: new Date(reaction.createdAt),
|
|
134
|
+
userIds: [reaction.userId],
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
type: "comment",
|
|
141
|
+
id: comment.id,
|
|
142
|
+
body: comment.content,
|
|
143
|
+
metadata: comment.data?.metadata,
|
|
144
|
+
userId: comment.data?.userId,
|
|
145
|
+
createdAt: new Date(comment.createdAt),
|
|
146
|
+
updatedAt: new Date(comment.updatedAt),
|
|
147
|
+
reactions,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private tiptapThreadToThreadData(thread: TCollabThread): ThreadData {
|
|
152
|
+
return {
|
|
153
|
+
type: "thread",
|
|
154
|
+
id: thread.id,
|
|
155
|
+
comments: thread.comments.map((comment) =>
|
|
156
|
+
this.tiptapCommentToCommentData(comment)
|
|
157
|
+
),
|
|
158
|
+
resolved: !!thread.resolvedAt,
|
|
159
|
+
metadata: thread.data?.metadata,
|
|
160
|
+
createdAt: new Date(thread.createdAt),
|
|
161
|
+
updatedAt: new Date(thread.updatedAt),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Deletes a comment from a thread.
|
|
167
|
+
*/
|
|
168
|
+
public async deleteComment(options: { threadId: string; commentId: string }) {
|
|
169
|
+
this.provider.deleteComment(options.threadId, options.commentId);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Deletes a thread.
|
|
174
|
+
*/
|
|
175
|
+
public async deleteThread(options: { threadId: string }) {
|
|
176
|
+
this.provider.deleteThread(options.threadId);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Marks a thread as resolved.
|
|
181
|
+
*/
|
|
182
|
+
public async resolveThread(options: { threadId: string }) {
|
|
183
|
+
this.provider.updateThread(options.threadId, {
|
|
184
|
+
resolvedAt: new Date().toISOString(),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Marks a thread as unresolved.
|
|
190
|
+
*/
|
|
191
|
+
public async unresolveThread(options: { threadId: string }) {
|
|
192
|
+
this.provider.updateThread(options.threadId, {
|
|
193
|
+
resolvedAt: null,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Adds a reaction to a comment.
|
|
199
|
+
*
|
|
200
|
+
* Auth: should be possible by anyone with comment access
|
|
201
|
+
*/
|
|
202
|
+
public async addReaction(options: {
|
|
203
|
+
threadId: string;
|
|
204
|
+
commentId: string;
|
|
205
|
+
emoji: string;
|
|
206
|
+
}) {
|
|
207
|
+
const comment = this.provider.getThreadComment(
|
|
208
|
+
options.threadId,
|
|
209
|
+
options.commentId,
|
|
210
|
+
true
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
if (!comment) {
|
|
214
|
+
throw new Error("Comment not found");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
this.provider.updateComment(options.threadId, options.commentId, {
|
|
218
|
+
data: {
|
|
219
|
+
...comment.data,
|
|
220
|
+
reactions: [
|
|
221
|
+
...((comment.data?.reactions || []) as ReactionAsTiptapData[]),
|
|
222
|
+
{
|
|
223
|
+
emoji: options.emoji,
|
|
224
|
+
createdAt: Date.now(),
|
|
225
|
+
userId: this.userId,
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Deletes a reaction from a comment.
|
|
234
|
+
*
|
|
235
|
+
* Auth: should be possible by the reaction author
|
|
236
|
+
*/
|
|
237
|
+
public async deleteReaction(options: {
|
|
238
|
+
threadId: string;
|
|
239
|
+
commentId: string;
|
|
240
|
+
emoji: string;
|
|
241
|
+
}) {
|
|
242
|
+
const comment = this.provider.getThreadComment(
|
|
243
|
+
options.threadId,
|
|
244
|
+
options.commentId,
|
|
245
|
+
true
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
if (!comment) {
|
|
249
|
+
throw new Error("Comment not found");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
this.provider.updateComment(options.threadId, options.commentId, {
|
|
253
|
+
data: {
|
|
254
|
+
...comment.data,
|
|
255
|
+
reactions: (
|
|
256
|
+
(comment.data?.reactions || []) as ReactionAsTiptapData[]
|
|
257
|
+
).filter(
|
|
258
|
+
(reaction) =>
|
|
259
|
+
reaction.emoji !== options.emoji && reaction.userId !== this.userId
|
|
260
|
+
),
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
public getThread(threadId: string): ThreadData {
|
|
266
|
+
const thread = this.provider.getThread(threadId);
|
|
267
|
+
|
|
268
|
+
if (!thread) {
|
|
269
|
+
throw new Error("Thread not found");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return this.tiptapThreadToThreadData(thread);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
public getThreads(): Map<string, ThreadData> {
|
|
276
|
+
return new Map(
|
|
277
|
+
this.provider
|
|
278
|
+
.getThreads()
|
|
279
|
+
.map((thread) => [thread.id, this.tiptapThreadToThreadData(thread)])
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
public subscribe(cb: (threads: Map<string, ThreadData>) => void): () => void {
|
|
284
|
+
const newCb = () => {
|
|
285
|
+
cb(this.getThreads());
|
|
286
|
+
};
|
|
287
|
+
this.provider.watchThreads(newCb);
|
|
288
|
+
return () => {
|
|
289
|
+
this.provider.unwatchThreads(newCb);
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
}
|