mirror of
https://github.com/langgenius/dify.git
synced 2026-07-22 03:08:33 +08:00
633 lines
21 KiB
TypeScript
633 lines
21 KiB
TypeScript
import type {
|
|
UserProfile,
|
|
WorkflowCommentDetail,
|
|
WorkflowCommentList,
|
|
} from '@/app/components/workflow/comment/types'
|
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
|
import { useAtomValue } from 'jotai'
|
|
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
|
import { useReactFlow } from 'reactflow'
|
|
import { collaborationManager } from '@/app/components/workflow/collaboration/core/collaboration-manager'
|
|
import { userProfileAtom } from '@/context/account-state'
|
|
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
|
import { useParams } from '@/next/navigation'
|
|
import { consoleClient } from '@/service/client'
|
|
import { useStore } from '../store'
|
|
import { ControlMode } from '../types'
|
|
|
|
const EMPTY_USERS: UserProfile[] = []
|
|
|
|
const normalizeTimestamp = (value: number | string | null | undefined): number => {
|
|
if (value == null) return Math.floor(Date.now() / 1000)
|
|
|
|
if (typeof value === 'number') return value
|
|
|
|
const parsed = Number(value)
|
|
if (!Number.isNaN(parsed)) return parsed
|
|
|
|
return Math.floor(Date.parse(value) / 1000)
|
|
}
|
|
|
|
const toCommentDetailPreview = (comment: WorkflowCommentList): WorkflowCommentDetail => ({
|
|
...comment,
|
|
replies: [],
|
|
mentions: [],
|
|
})
|
|
|
|
export const useWorkflowComment = () => {
|
|
const params = useParams()
|
|
const appId = params.appId as string
|
|
const reactflow = useReactFlow()
|
|
const controlMode = useStore((s) => s.controlMode)
|
|
const pendingComment = useStore((s) => s.pendingComment)
|
|
const setPendingComment = useStore((s) => s.setPendingComment)
|
|
const isCommentQuickAdd = useStore((s) => s.isCommentQuickAdd)
|
|
const setCommentQuickAdd = useStore((s) => s.setCommentQuickAdd)
|
|
const isCommentPlacing = useStore((s) => s.isCommentPlacing)
|
|
const setActiveCommentId = useStore((s) => s.setActiveCommentId)
|
|
const activeCommentId = useStore((s) => s.activeCommentId)
|
|
const comments = useStore((s) => s.comments)
|
|
const setComments = useStore((s) => s.setComments)
|
|
const loading = useStore((s) => s.commentsLoading)
|
|
const setCommentsLoading = useStore((s) => s.setCommentsLoading)
|
|
const activeComment = useStore((s) => s.activeCommentDetail)
|
|
const setActiveComment = useStore((s) => s.setActiveCommentDetail)
|
|
const activeCommentLoading = useStore((s) => s.activeCommentDetailLoading)
|
|
const setActiveCommentLoading = useStore((s) => s.setActiveCommentDetailLoading)
|
|
const replySubmitting = useStore((s) => s.replySubmitting)
|
|
const setReplySubmitting = useStore((s) => s.setReplySubmitting)
|
|
const replyUpdating = useStore((s) => s.replyUpdating)
|
|
const setReplyUpdating = useStore((s) => s.setReplyUpdating)
|
|
const commentDetailCache = useStore((s) => s.commentDetailCache)
|
|
const setCommentDetailCache = useStore((s) => s.setCommentDetailCache)
|
|
const rightPanelWidth = useStore((s) => s.rightPanelWidth)
|
|
const nodePanelWidth = useStore((s) => s.nodePanelWidth)
|
|
const mentionableUsers = useStore((state) =>
|
|
appId ? (state.mentionableUsersCache[appId] ?? EMPTY_USERS) : EMPTY_USERS,
|
|
)
|
|
const mentionableUserById = useMemo(
|
|
() => new Map(mentionableUsers.map((user) => [user.id, user])),
|
|
[mentionableUsers],
|
|
)
|
|
const userProfile = useAtomValue(userProfileAtom)
|
|
const { data: isCollaborationEnabled } = useSuspenseQuery({
|
|
...systemFeaturesQueryOptions(),
|
|
select: (s) => s.enable_collaboration_mode,
|
|
})
|
|
const commentDetailCacheRef = useRef<Record<string, WorkflowCommentDetail>>(commentDetailCache)
|
|
const activeCommentIdRef = useRef<string | null>(null)
|
|
|
|
useEffect(() => {
|
|
activeCommentIdRef.current = activeCommentId ?? null
|
|
}, [activeCommentId])
|
|
|
|
useEffect(() => {
|
|
commentDetailCacheRef.current = commentDetailCache
|
|
}, [commentDetailCache])
|
|
|
|
const refreshActiveComment = useCallback(
|
|
async (commentId: string) => {
|
|
if (!appId) return
|
|
|
|
const detail = await consoleClient.apps.byAppId.workflow.comments.byCommentId.get({
|
|
params: { app_id: appId, comment_id: commentId },
|
|
})
|
|
|
|
commentDetailCacheRef.current = {
|
|
...commentDetailCacheRef.current,
|
|
[commentId]: detail,
|
|
}
|
|
setCommentDetailCache(commentDetailCacheRef.current)
|
|
setActiveComment(detail)
|
|
},
|
|
[appId, setActiveComment, setCommentDetailCache],
|
|
)
|
|
|
|
const loadComments = useCallback(async () => {
|
|
if (!appId || !isCollaborationEnabled) return
|
|
|
|
setCommentsLoading(true)
|
|
try {
|
|
const response = await consoleClient.apps.byAppId.workflow.comments.get({
|
|
params: { app_id: appId },
|
|
})
|
|
setComments(response.data)
|
|
} catch (error) {
|
|
console.error('Failed to fetch comments:', error)
|
|
} finally {
|
|
setCommentsLoading(false)
|
|
}
|
|
}, [appId, isCollaborationEnabled, setComments, setCommentsLoading])
|
|
|
|
// Setup collaboration
|
|
useEffect(() => {
|
|
if (!appId || !isCollaborationEnabled) return
|
|
|
|
const unsubscribe = collaborationManager.onCommentsUpdate(() => {
|
|
loadComments()
|
|
if (activeCommentIdRef.current) refreshActiveComment(activeCommentIdRef.current)
|
|
})
|
|
|
|
return unsubscribe
|
|
}, [appId, isCollaborationEnabled, loadComments, refreshActiveComment])
|
|
|
|
useEffect(() => {
|
|
loadComments()
|
|
}, [loadComments])
|
|
|
|
const handleCommentSubmit = useCallback(
|
|
async (content: string, mentionedUserIds: string[] = []) => {
|
|
if (!pendingComment) return
|
|
|
|
if (!appId) {
|
|
console.error('AppId is missing')
|
|
return
|
|
}
|
|
|
|
try {
|
|
// Convert screen position to flow position when submitting
|
|
const { screenToFlowPosition } = reactflow
|
|
const flowPosition = screenToFlowPosition({
|
|
x: pendingComment.pageX,
|
|
y: pendingComment.pageY,
|
|
})
|
|
|
|
const newComment = await consoleClient.apps.byAppId.workflow.comments.post({
|
|
params: { app_id: appId },
|
|
body: {
|
|
position_x: flowPosition.x,
|
|
position_y: flowPosition.y,
|
|
content,
|
|
mentioned_user_ids: mentionedUserIds,
|
|
},
|
|
})
|
|
|
|
const createdAtSeconds = normalizeTimestamp(newComment.created_at)
|
|
const createdByAccount = {
|
|
id: userProfile?.id ?? '',
|
|
name: userProfile?.name ?? '',
|
|
email: userProfile?.email ?? '',
|
|
avatar_url: userProfile?.avatar_url || userProfile?.avatar || null,
|
|
}
|
|
const mentionedUsers = mentionedUserIds
|
|
.map((mentionedId) => mentionableUserById.get(mentionedId))
|
|
.filter((user): user is NonNullable<typeof user> => Boolean(user))
|
|
const uniqueParticipantsMap = new Map<string, typeof createdByAccount>()
|
|
if (createdByAccount.id) uniqueParticipantsMap.set(createdByAccount.id, createdByAccount)
|
|
for (const user of mentionedUsers) {
|
|
if (!uniqueParticipantsMap.has(user.id)) {
|
|
uniqueParticipantsMap.set(user.id, {
|
|
id: user.id,
|
|
name: user.name,
|
|
email: user.email,
|
|
avatar_url: user.avatar_url ?? null,
|
|
})
|
|
}
|
|
}
|
|
const participants = Array.from(uniqueParticipantsMap.values())
|
|
|
|
const composedComment: WorkflowCommentList = {
|
|
id: newComment.id,
|
|
position_x: flowPosition.x,
|
|
position_y: flowPosition.y,
|
|
content,
|
|
created_by: createdByAccount.id,
|
|
created_by_account: createdByAccount,
|
|
created_at: createdAtSeconds,
|
|
updated_at: createdAtSeconds,
|
|
resolved: false,
|
|
mention_count: mentionedUserIds.length,
|
|
reply_count: 0,
|
|
participants,
|
|
}
|
|
|
|
const composedDetail: WorkflowCommentDetail = {
|
|
id: newComment.id,
|
|
position_x: flowPosition.x,
|
|
position_y: flowPosition.y,
|
|
content,
|
|
created_by: createdByAccount.id,
|
|
created_by_account: createdByAccount,
|
|
created_at: createdAtSeconds,
|
|
updated_at: createdAtSeconds,
|
|
resolved: false,
|
|
replies: [],
|
|
mentions: mentionedUserIds.map((mentionedId) => ({
|
|
mentioned_user_id: mentionedId,
|
|
mentioned_user_account: mentionableUserById.get(mentionedId) ?? null,
|
|
reply_id: null,
|
|
})),
|
|
}
|
|
|
|
setComments([...comments, composedComment])
|
|
commentDetailCacheRef.current = {
|
|
...commentDetailCacheRef.current,
|
|
[newComment.id]: composedDetail,
|
|
}
|
|
setCommentDetailCache(commentDetailCacheRef.current)
|
|
|
|
collaborationManager.emitCommentsUpdate(appId)
|
|
|
|
setPendingComment(null)
|
|
setCommentQuickAdd(false)
|
|
} catch (error) {
|
|
console.error('Failed to create comment:', error)
|
|
setPendingComment(null)
|
|
setCommentQuickAdd(false)
|
|
}
|
|
},
|
|
[
|
|
appId,
|
|
pendingComment,
|
|
setPendingComment,
|
|
setCommentQuickAdd,
|
|
reactflow,
|
|
comments,
|
|
setComments,
|
|
userProfile,
|
|
setCommentDetailCache,
|
|
mentionableUserById,
|
|
],
|
|
)
|
|
|
|
const handleCommentCancel = useCallback(() => {
|
|
setPendingComment(null)
|
|
setCommentQuickAdd(false)
|
|
}, [setPendingComment, setCommentQuickAdd])
|
|
|
|
useEffect(() => {
|
|
if (controlMode !== ControlMode.Comment && !isCommentQuickAdd) setPendingComment(null)
|
|
}, [controlMode, isCommentQuickAdd, setPendingComment])
|
|
|
|
useEffect(() => {
|
|
if (!pendingComment && !isCommentPlacing && isCommentQuickAdd) setCommentQuickAdd(false)
|
|
}, [isCommentPlacing, isCommentQuickAdd, pendingComment, setCommentQuickAdd])
|
|
|
|
const handleCommentIconClick = useCallback(
|
|
async (comment: WorkflowCommentList) => {
|
|
setPendingComment(null)
|
|
|
|
activeCommentIdRef.current = comment.id
|
|
setActiveCommentId(comment.id)
|
|
|
|
const cachedDetail = commentDetailCacheRef.current[comment.id]
|
|
setActiveComment(cachedDetail ?? toCommentDetailPreview(comment))
|
|
|
|
const hasSelectedNode = reactflow.getNodes().some((node) => node.data?.selected)
|
|
const commentPanelWidth = controlMode === ControlMode.Comment ? 420 : 0
|
|
const fallbackPanelWidth = (hasSelectedNode ? nodePanelWidth : 0) + commentPanelWidth
|
|
const effectivePanelWidth = Math.max(rightPanelWidth ?? 0, fallbackPanelWidth)
|
|
|
|
const baseHorizontalOffsetPx = 220
|
|
const panelCompensationPx = effectivePanelWidth / 2
|
|
const desiredHorizontalOffsetPx = baseHorizontalOffsetPx + panelCompensationPx
|
|
const maxOffset = Math.max(0, window.innerWidth / 2 - 60)
|
|
const horizontalOffsetPx = Math.min(desiredHorizontalOffsetPx, maxOffset)
|
|
|
|
reactflow.setCenter(comment.position_x + horizontalOffsetPx, comment.position_y, {
|
|
zoom: 1,
|
|
duration: 600,
|
|
})
|
|
|
|
if (!appId) return
|
|
|
|
setActiveCommentLoading(!cachedDetail)
|
|
|
|
try {
|
|
const detail = await consoleClient.apps.byAppId.workflow.comments.byCommentId.get({
|
|
params: { app_id: appId, comment_id: comment.id },
|
|
})
|
|
|
|
commentDetailCacheRef.current = {
|
|
...commentDetailCacheRef.current,
|
|
[comment.id]: detail,
|
|
}
|
|
setCommentDetailCache(commentDetailCacheRef.current)
|
|
|
|
if (activeCommentIdRef.current === comment.id) setActiveComment(detail)
|
|
} catch (e) {
|
|
console.warn('Failed to load workflow comment detail', e)
|
|
} finally {
|
|
setActiveCommentLoading(false)
|
|
}
|
|
},
|
|
[
|
|
appId,
|
|
controlMode,
|
|
nodePanelWidth,
|
|
reactflow,
|
|
rightPanelWidth,
|
|
setActiveComment,
|
|
setActiveCommentId,
|
|
setActiveCommentLoading,
|
|
setCommentDetailCache,
|
|
setPendingComment,
|
|
],
|
|
)
|
|
|
|
const handleCommentResolve = useCallback(
|
|
async (commentId: string) => {
|
|
if (!appId) return
|
|
|
|
setActiveCommentLoading(true)
|
|
try {
|
|
await consoleClient.apps.byAppId.workflow.comments.byCommentId.resolve.post({
|
|
params: { app_id: appId, comment_id: commentId },
|
|
})
|
|
|
|
collaborationManager.emitCommentsUpdate(appId)
|
|
|
|
await refreshActiveComment(commentId)
|
|
await loadComments()
|
|
} catch (error) {
|
|
console.error('Failed to resolve comment:', error)
|
|
} finally {
|
|
setActiveCommentLoading(false)
|
|
}
|
|
},
|
|
[appId, loadComments, refreshActiveComment, setActiveCommentLoading],
|
|
)
|
|
|
|
const handleCommentDelete = useCallback(
|
|
async (commentId: string) => {
|
|
if (!appId) return
|
|
|
|
setActiveCommentLoading(true)
|
|
try {
|
|
await consoleClient.apps.byAppId.workflow.comments.byCommentId.delete({
|
|
params: { app_id: appId, comment_id: commentId },
|
|
})
|
|
|
|
collaborationManager.emitCommentsUpdate(appId)
|
|
|
|
const updatedCache = { ...commentDetailCacheRef.current }
|
|
delete updatedCache[commentId]
|
|
commentDetailCacheRef.current = updatedCache
|
|
setCommentDetailCache(updatedCache)
|
|
|
|
const currentComments = comments.filter((c) => c.id !== commentId)
|
|
const commentIndex = comments.findIndex((c) => c.id === commentId)
|
|
const fallbackTarget =
|
|
commentIndex >= 0 ? (comments[commentIndex + 1] ?? comments[commentIndex - 1]) : undefined
|
|
|
|
await loadComments()
|
|
|
|
if (fallbackTarget) {
|
|
handleCommentIconClick(fallbackTarget)
|
|
} else if (currentComments.length > 0) {
|
|
const nextComment = currentComments[0]
|
|
handleCommentIconClick(nextComment!)
|
|
} else {
|
|
setActiveComment(null)
|
|
setActiveCommentId(null)
|
|
activeCommentIdRef.current = null
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to delete comment:', error)
|
|
} finally {
|
|
setActiveCommentLoading(false)
|
|
}
|
|
},
|
|
[
|
|
appId,
|
|
comments,
|
|
handleCommentIconClick,
|
|
loadComments,
|
|
setActiveComment,
|
|
setActiveCommentId,
|
|
setActiveCommentLoading,
|
|
setCommentDetailCache,
|
|
],
|
|
)
|
|
|
|
const handleCommentPositionUpdate = useCallback(
|
|
async (commentId: string, position: { x: number; y: number }) => {
|
|
if (!appId) return
|
|
|
|
const targetComment = comments.find((c) => c.id === commentId)
|
|
if (!targetComment) return
|
|
|
|
const nextPosition = {
|
|
position_x: position.x,
|
|
position_y: position.y,
|
|
}
|
|
|
|
const previousComments = comments
|
|
const updatedComments = comments.map((c) =>
|
|
c.id === commentId ? { ...c, ...nextPosition } : c,
|
|
)
|
|
setComments(updatedComments)
|
|
|
|
const cachedDetail = commentDetailCacheRef.current[commentId]
|
|
const updatedDetail = cachedDetail ? { ...cachedDetail, ...nextPosition } : null
|
|
if (updatedDetail) {
|
|
commentDetailCacheRef.current = {
|
|
...commentDetailCacheRef.current,
|
|
[commentId]: updatedDetail,
|
|
}
|
|
setCommentDetailCache(commentDetailCacheRef.current)
|
|
|
|
if (activeCommentIdRef.current === commentId) setActiveComment(updatedDetail)
|
|
} else if (activeComment?.id === commentId) {
|
|
setActiveComment({ ...activeComment, ...nextPosition })
|
|
}
|
|
|
|
try {
|
|
await consoleClient.apps.byAppId.workflow.comments.byCommentId.put({
|
|
params: { app_id: appId, comment_id: commentId },
|
|
body: {
|
|
content: targetComment.content,
|
|
position_x: nextPosition.position_x,
|
|
position_y: nextPosition.position_y,
|
|
},
|
|
})
|
|
collaborationManager.emitCommentsUpdate(appId)
|
|
} catch (error) {
|
|
console.error('Failed to update comment position:', error)
|
|
setComments(previousComments)
|
|
|
|
if (cachedDetail) {
|
|
commentDetailCacheRef.current = {
|
|
...commentDetailCacheRef.current,
|
|
[commentId]: cachedDetail,
|
|
}
|
|
setCommentDetailCache(commentDetailCacheRef.current)
|
|
|
|
if (activeCommentIdRef.current === commentId) setActiveComment(cachedDetail)
|
|
} else if (activeComment?.id === commentId) {
|
|
setActiveComment(activeComment)
|
|
}
|
|
}
|
|
},
|
|
[activeComment, appId, comments, setComments, setCommentDetailCache, setActiveComment],
|
|
)
|
|
|
|
const handleCommentUpdate = useCallback(
|
|
async (commentId: string, content: string, mentionedUserIds: string[] = []) => {
|
|
if (!appId) return
|
|
const trimmed = content.trim()
|
|
if (!trimmed) return
|
|
|
|
const targetComment = comments.find((c) => c.id === commentId)
|
|
const targetDetail =
|
|
commentDetailCacheRef.current[commentId] ??
|
|
(activeCommentIdRef.current === commentId ? activeComment : null)
|
|
const positionX = targetDetail?.position_x ?? targetComment?.position_x
|
|
const positionY = targetDetail?.position_y ?? targetComment?.position_y
|
|
|
|
if (positionX === undefined || positionY === undefined) return
|
|
|
|
try {
|
|
await consoleClient.apps.byAppId.workflow.comments.byCommentId.put({
|
|
params: { app_id: appId, comment_id: commentId },
|
|
body: {
|
|
content: trimmed,
|
|
position_x: positionX,
|
|
position_y: positionY,
|
|
mentioned_user_ids: mentionedUserIds,
|
|
},
|
|
})
|
|
|
|
collaborationManager.emitCommentsUpdate(appId)
|
|
|
|
await refreshActiveComment(commentId)
|
|
await loadComments()
|
|
} catch (error) {
|
|
console.error('Failed to update comment:', error)
|
|
}
|
|
},
|
|
[activeComment, appId, comments, loadComments, refreshActiveComment],
|
|
)
|
|
|
|
const handleCommentReply = useCallback(
|
|
async (commentId: string, content: string, mentionedUserIds: string[] = []) => {
|
|
if (!appId) return
|
|
const trimmed = content.trim()
|
|
if (!trimmed) return
|
|
|
|
setReplySubmitting(true)
|
|
try {
|
|
await consoleClient.apps.byAppId.workflow.comments.byCommentId.replies.post({
|
|
params: { app_id: appId, comment_id: commentId },
|
|
body: { content: trimmed, mentioned_user_ids: mentionedUserIds },
|
|
})
|
|
|
|
collaborationManager.emitCommentsUpdate(appId)
|
|
|
|
await refreshActiveComment(commentId)
|
|
await loadComments()
|
|
} catch (error) {
|
|
console.error('Failed to create reply:', error)
|
|
} finally {
|
|
setReplySubmitting(false)
|
|
}
|
|
},
|
|
[appId, loadComments, refreshActiveComment, setReplySubmitting],
|
|
)
|
|
|
|
const handleCommentReplyUpdate = useCallback(
|
|
async (
|
|
commentId: string,
|
|
replyId: string,
|
|
content: string,
|
|
mentionedUserIds: string[] = [],
|
|
) => {
|
|
if (!appId) return
|
|
const trimmed = content.trim()
|
|
if (!trimmed) return
|
|
|
|
setReplyUpdating(true)
|
|
try {
|
|
await consoleClient.apps.byAppId.workflow.comments.byCommentId.replies.byReplyId.put({
|
|
params: { app_id: appId, comment_id: commentId, reply_id: replyId },
|
|
body: { content: trimmed, mentioned_user_ids: mentionedUserIds },
|
|
})
|
|
|
|
collaborationManager.emitCommentsUpdate(appId)
|
|
|
|
await refreshActiveComment(commentId)
|
|
await loadComments()
|
|
} catch (error) {
|
|
console.error('Failed to update reply:', error)
|
|
} finally {
|
|
setReplyUpdating(false)
|
|
}
|
|
},
|
|
[appId, loadComments, refreshActiveComment, setReplyUpdating],
|
|
)
|
|
|
|
const handleCommentReplyDelete = useCallback(
|
|
async (commentId: string, replyId: string) => {
|
|
if (!appId) return
|
|
|
|
setActiveCommentLoading(true)
|
|
try {
|
|
await consoleClient.apps.byAppId.workflow.comments.byCommentId.replies.byReplyId.delete({
|
|
params: { app_id: appId, comment_id: commentId, reply_id: replyId },
|
|
})
|
|
|
|
collaborationManager.emitCommentsUpdate(appId)
|
|
|
|
await refreshActiveComment(commentId)
|
|
await loadComments()
|
|
} catch (error) {
|
|
console.error('Failed to delete reply:', error)
|
|
} finally {
|
|
setActiveCommentLoading(false)
|
|
}
|
|
},
|
|
[appId, loadComments, refreshActiveComment, setActiveCommentLoading],
|
|
)
|
|
|
|
const handleCommentNavigate = useCallback(
|
|
(direction: 'prev' | 'next') => {
|
|
const currentId = activeCommentIdRef.current
|
|
if (!currentId) return
|
|
const idx = comments.findIndex((c) => c.id === currentId)
|
|
if (idx === -1) return
|
|
const target = direction === 'prev' ? comments[idx - 1] : comments[idx + 1]
|
|
if (target) handleCommentIconClick(target)
|
|
},
|
|
[comments, handleCommentIconClick],
|
|
)
|
|
|
|
const handleActiveCommentClose = useCallback(() => {
|
|
setActiveComment(null)
|
|
setActiveCommentLoading(false)
|
|
setActiveCommentId(null)
|
|
activeCommentIdRef.current = null
|
|
}, [setActiveComment, setActiveCommentId, setActiveCommentLoading])
|
|
|
|
const handleCreateComment = useCallback(
|
|
(mousePosition: { pageX: number; pageY: number; elementX: number; elementY: number }) => {
|
|
if (controlMode === ControlMode.Comment) setPendingComment(mousePosition)
|
|
},
|
|
[controlMode, setPendingComment],
|
|
)
|
|
|
|
return {
|
|
comments,
|
|
loading,
|
|
pendingComment,
|
|
activeComment,
|
|
activeCommentLoading,
|
|
replySubmitting,
|
|
replyUpdating,
|
|
handleCommentSubmit,
|
|
handleCommentCancel,
|
|
handleCommentIconClick,
|
|
handleActiveCommentClose,
|
|
handleCommentResolve,
|
|
handleCommentDelete,
|
|
handleCommentNavigate,
|
|
handleCommentUpdate,
|
|
handleCommentReply,
|
|
handleCommentReplyUpdate,
|
|
handleCommentReplyDelete,
|
|
handleCommentPositionUpdate,
|
|
refreshActiveComment,
|
|
handleCreateComment,
|
|
loadComments,
|
|
}
|
|
}
|