From 0833d0314c844065d0af8a0ef51ddc8b01af5eab Mon Sep 17 00:00:00 2001 From: Peng Xiao Date: Mon, 7 Jul 2025 18:44:25 +0800 Subject: [PATCH] feat(core): reply actions (#13071) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix AF-2717, AF-2716 #### PR Dependency Tree * **PR #13071** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) ## Summary by CodeRabbit ## Summary by CodeRabbit * **New Features** * Introduced full editing, replying, and deletion capabilities for individual replies within comments, with consistent UI and state management. * Added support for editing drafts for both comments and replies, allowing users to start, commit, or dismiss edits. * Improved editor focus behavior for a more seamless editing experience. * Added permission checks for comment and reply creation, editing, deletion, and resolution, controlling UI elements accordingly. * Refactored reply rendering into dedicated components with enhanced permission-aware interactions. * **Bug Fixes** * Enhanced comment normalization to handle cases where content may be missing, preventing potential errors. * **Style** * Updated comment and reply UI styles for clearer editing and action states, including new hover and visibility behaviors for reply actions. * **Chores** * Removed unnecessary debugging statements from reply configuration. #### PR Dependency Tree * **PR #13071** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) --- .../src/graphql/doc-role-permissions.gql | 4 + packages/common/graphql/src/graphql/index.ts | 4 + packages/common/graphql/src/schema.ts | 4 + .../comment/comment-editor/index.tsx | 47 +- .../comment-editor/linked-widget-config.ts | 1 - .../src/components/comment/sidebar/index.tsx | 505 ++++++++++++++---- .../components/comment/sidebar/style.css.ts | 33 ++ .../comment/entities/doc-comment-store.ts | 6 +- .../modules/comment/entities/doc-comment.ts | 87 ++- .../core/src/modules/comment/types.ts | 2 +- 10 files changed, 558 insertions(+), 135 deletions(-) diff --git a/packages/common/graphql/src/graphql/doc-role-permissions.gql b/packages/common/graphql/src/graphql/doc-role-permissions.gql index 56f08b7f82..ea20e337b4 100644 --- a/packages/common/graphql/src/graphql/doc-role-permissions.gql +++ b/packages/common/graphql/src/graphql/doc-role-permissions.gql @@ -15,6 +15,10 @@ query getDocRolePermissions($workspaceId: String!, $docId: String!) { Doc_Update Doc_Users_Manage Doc_Users_Read + Doc_Comments_Create + Doc_Comments_Delete + Doc_Comments_Read + Doc_Comments_Resolve } } } diff --git a/packages/common/graphql/src/graphql/index.ts b/packages/common/graphql/src/graphql/index.ts index b86d790022..7fae7a5fe8 100644 --- a/packages/common/graphql/src/graphql/index.ts +++ b/packages/common/graphql/src/graphql/index.ts @@ -1323,6 +1323,10 @@ export const getDocRolePermissionsQuery = { Doc_Update Doc_Users_Manage Doc_Users_Read + Doc_Comments_Create + Doc_Comments_Delete + Doc_Comments_Read + Doc_Comments_Resolve } } } diff --git a/packages/common/graphql/src/schema.ts b/packages/common/graphql/src/schema.ts index 57e825eaa8..a1965c8fbb 100644 --- a/packages/common/graphql/src/schema.ts +++ b/packages/common/graphql/src/schema.ts @@ -4427,6 +4427,10 @@ export type GetDocRolePermissionsQuery = { Doc_Update: boolean; Doc_Users_Manage: boolean; Doc_Users_Read: boolean; + Doc_Comments_Create: boolean; + Doc_Comments_Delete: boolean; + Doc_Comments_Read: boolean; + Doc_Comments_Resolve: boolean; }; }; }; diff --git a/packages/frontend/core/src/components/comment/comment-editor/index.tsx b/packages/frontend/core/src/components/comment/comment-editor/index.tsx index 878bc28c37..f8409ee871 100644 --- a/packages/frontend/core/src/components/comment/comment-editor/index.tsx +++ b/packages/frontend/core/src/components/comment/comment-editor/index.tsx @@ -1,9 +1,10 @@ import { LitDocEditor, type PageEditor } from '@affine/core/blocksuite/editors'; import { SnapshotHelper } from '@affine/core/modules/comment/services/snapshot-helper'; -import { focusTextModel, type RichText } from '@blocksuite/affine/rich-text'; +import { type RichText, selectTextModel } from '@blocksuite/affine/rich-text'; import { ViewportElementExtension } from '@blocksuite/affine/shared/services'; import { type DocSnapshot, Store } from '@blocksuite/affine/store'; import { ArrowUpBigIcon } from '@blocksuite/icons/rc'; +import type { TextSelection } from '@blocksuite/std'; import { useFramework, useService } from '@toeverything/infra'; import clsx from 'clsx'; import { @@ -16,6 +17,7 @@ import { useState, } from 'react'; +import { useAsyncCallback } from '../../hooks/affine-async-hooks'; import { getCommentEditorViewManager } from './specs'; import * as styles from './style.css'; @@ -46,6 +48,7 @@ interface CommentEditorProps { export interface CommentEditorRef { getSnapshot: () => DocSnapshot | null | undefined; + focus: () => void; } // todo: get rid of circular data changes @@ -95,6 +98,32 @@ export const CommentEditor = forwardRef( const [empty, setEmpty] = useState(true); + const focusEditor = useAsyncCallback(async () => { + if (editorRef.current) { + const selectionService = editorRef.current.std.selection; + const selection = selectionService.value.at(-1) as TextSelection; + editorRef.current.std.event.active = true; + await editorRef.current.host?.updateComplete; + if (selection) { + selectTextModel( + editorRef.current.std, + selection.blockId, + selection.from.index, + selection.from.length + ); + } else { + const richTexts = Array.from( + editorRef.current?.querySelectorAll('rich-text') ?? [] + ) as unknown as RichText[]; + + const lastRichText = richTexts.at(-1); + if (lastRichText) { + lastRichText.inlineEditor?.focusEnd(); + } + } + } + }, [editorRef]); + useImperativeHandle( ref, () => ({ @@ -104,19 +133,11 @@ export const CommentEditor = forwardRef( } return snapshotHelper.getSnapshot(doc); }, + focus: focusEditor, }), - [doc, snapshotHelper] + [doc, focusEditor, snapshotHelper] ); - const focusEditor = useCallback(() => { - if (editorRef.current) { - const lastChild = editorRef.current.std.store.root?.lastChild(); - if (lastChild) { - focusTextModel(editorRef.current.std, lastChild.id); - } - } - }, [editorRef]); - useEffect(() => { let cancel = false; if (autoFocus && editorRef.current && doc) { @@ -195,7 +216,9 @@ export const CommentEditor = forwardRef( data-readonly={!!readonly} className={clsx(styles.container, 'comment-editor-viewport')} > - {doc && } + {doc && ( + + )} {!readonly && (