feat(editor): comment extension (#12948)

#### PR Dependency Tree


* **PR #12948** 👈
  * **PR #12980**

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Introduced inline comment functionality, allowing users to add,
resolve, and highlight comments directly within text.
  * Added a new toolbar action for inserting comments when supported.
* Inline comments are visually highlighted and can be interacted with in
the editor.

* **Enhancements**
  * Integrated a feature flag to enable or disable the comment feature.
* Improved inline manager rendering to support wrapper specs for
advanced formatting.

* **Developer Tools**
* Added mock comment provider for testing and development environments.

* **Chores**
* Updated dependencies and project references to support the new inline
comment module.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
L-Sun
2025-07-02 17:14:34 +08:00
committed by GitHub
parent a66096cdf9
commit 8ce85f708d
40 changed files with 760 additions and 14 deletions

View File

@@ -5,6 +5,8 @@ import {
type ReferenceParams,
} from '@blocksuite/affine/model';
import {
type CommentId,
type CommentProvider,
type DocModeProvider,
type EditorSetting,
GeneralSettingSchema,
@@ -13,7 +15,7 @@ import {
type ParseDocUrlService,
type ThemeExtension,
} from '@blocksuite/affine/shared/services';
import { type Workspace } from '@blocksuite/affine/store';
import type { BaseSelection, Workspace } from '@blocksuite/affine/store';
import type { TestAffineEditorContainer } from '@blocksuite/integration-test';
import { Signal, signal } from '@preact/signals-core';
import { Subject } from 'rxjs';
@@ -191,6 +193,86 @@ export function mockEditorSetting() {
return signal;
}
export function mockCommentProvider() {
class MockCommentProvider implements CommentProvider {
commentId = 0;
comments = new Map<
CommentId,
{
selections: BaseSelection[];
resolved: boolean;
}
>();
commentAddSubject = new Subject<{
id: CommentId;
selections: BaseSelection[];
}>();
commentResolveSubject = new Subject<CommentId>();
commentHighlightSubject = new Subject<CommentId | null>();
commentDeleteSubject = new Subject<CommentId>();
addComment(selections: BaseSelection[]) {
const id: CommentId = `${this.commentId++}`;
this.comments.set(id, {
selections,
resolved: false,
});
this.commentAddSubject.next({
id,
selections,
});
}
resolveComment(id: CommentId) {
const comment = this.comments.get(id);
if (!comment) return;
comment.resolved = true;
this.commentResolveSubject.next(id);
}
deleteComment(id: CommentId) {
this.comments.delete(id);
this.commentDeleteSubject.next(id);
}
highlightComment(id: CommentId | null) {
this.commentHighlightSubject.next(id);
}
getComments() {
return Array.from(this.comments.keys());
}
onCommentAdded(
callback: (id: CommentId, selections: BaseSelection[]) => void
) {
return this.commentAddSubject.subscribe(({ id, selections }) => {
callback(id, selections);
});
}
onCommentResolved(callback: (id: CommentId) => void) {
return this.commentResolveSubject.subscribe(callback);
}
onCommentDeleted(callback: (id: CommentId) => void) {
return this.commentDeleteSubject.subscribe(callback);
}
onCommentHighlighted(callback: (id: CommentId | null) => void) {
return this.commentHighlightSubject.subscribe(callback);
}
}
const provider = new MockCommentProvider();
return provider;
}
declare global {
interface Window {
editorSetting$: Signal<EditorSetting>;