sync the comment panel and canvas

This commit is contained in:
hjlarry 2025-09-17 09:13:31 +08:00
parent 4d3adec738
commit 3eac26929a
4 changed files with 63 additions and 23 deletions

View File

@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { useCallback, useEffect, useRef } from 'react'
import { useParams } from 'next/navigation'
import { useReactFlow } from 'reactflow'
import { useStore } from '../store'
@ -16,31 +16,42 @@ export const useWorkflowComment = () => {
const setPendingComment = useStore(s => s.setPendingComment)
const setActiveCommentId = useStore(s => s.setActiveCommentId)
const activeCommentId = useStore(s => s.activeCommentId)
const [comments, setComments] = useState<WorkflowCommentList[]>([])
const [loading, setLoading] = useState(false)
const [activeComment, setActiveComment] = useState<WorkflowCommentDetail | null>(null)
const [activeCommentLoading, setActiveCommentLoading] = useState(false)
const commentDetailCacheRef = useRef<Record<string, WorkflowCommentDetail>>({})
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 commentDetailCache = useStore(s => s.commentDetailCache)
const setCommentDetailCache = useStore(s => s.setCommentDetailCache)
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 loadComments = useCallback(async () => {
if (!appId) return
setLoading(true)
setCommentsLoading(true)
try {
const commentsData = await fetchWorkflowComments(appId)
setComments(commentsData)
}
catch (error) {
catch (error) {
console.error('Failed to fetch comments:', error)
}
finally {
setLoading(false)
finally {
setCommentsLoading(false)
}
}, [appId])
}, [appId, setComments, setCommentsLoading])
useEffect(() => {
loadComments()
@ -89,14 +100,14 @@ export const useWorkflowComment = () => {
setActiveCommentId(comment.id)
const cachedDetail = commentDetailCacheRef.current[comment.id]
setActiveComment(cachedDetail || comment)
const fallbackDetail = cachedDetail ?? comment
setActiveComment(fallbackDetail)
reactflow.setCenter(comment.position_x, comment.position_y, { zoom: 1, duration: 600 })
if (!appId) return
if (!cachedDetail)
setActiveCommentLoading(true)
setActiveCommentLoading(!cachedDetail)
try {
const detailResponse = await fetchWorkflowComment(appId, comment.id)
@ -106,6 +117,7 @@ export const useWorkflowComment = () => {
...commentDetailCacheRef.current,
[comment.id]: detail,
}
setCommentDetailCache(commentDetailCacheRef.current)
if (activeCommentIdRef.current === comment.id)
setActiveComment(detail)
@ -116,7 +128,7 @@ export const useWorkflowComment = () => {
finally {
setActiveCommentLoading(false)
}
}, [appId, reactflow, setPendingComment])
}, [appId, reactflow, setActiveComment, setActiveCommentId, setActiveCommentLoading, setCommentDetailCache, setControlMode, setPendingComment])
const handleActiveCommentClose = useCallback(() => {
setActiveComment(null)
@ -124,7 +136,7 @@ export const useWorkflowComment = () => {
setActiveCommentId(null)
setControlMode(ControlMode.Pointer)
activeCommentIdRef.current = null
}, [setActiveCommentId, setControlMode])
}, [setActiveComment, setActiveCommentId, setActiveCommentLoading, setControlMode])
const handleCreateComment = useCallback((mousePosition: { pageX: number; pageY: number }) => {
if (controlMode === ControlMode.Comment) {

View File

@ -1,5 +1,4 @@
import { memo, useCallback, useMemo, useState } from 'react'
import { useReactFlow } from 'reactflow'
import { RiCheckLine, RiCheckboxCircleFill, RiCheckboxCircleLine, RiCloseLine, RiFilter3Line } from '@remixicon/react'
import { useStore } from '@/app/components/workflow/store'
import type { WorkflowCommentList } from '@/service/workflow-comment'
@ -16,8 +15,7 @@ const CommentsPanel = () => {
const activeCommentId = useStore(s => s.activeCommentId)
const setActiveCommentId = useStore(s => s.setActiveCommentId)
const setControlMode = useStore(s => s.setControlMode)
const { comments, loading, loadComments } = useWorkflowComment()
const reactFlow = useReactFlow()
const { comments, loading, loadComments, handleCommentIconClick } = useWorkflowComment()
const params = useParams()
const appId = params.appId as string
const { formatTimeFromNow } = useFormatTimeFromNow()
@ -26,10 +24,8 @@ const CommentsPanel = () => {
const [showFilter, setShowFilter] = useState(false)
const handleSelect = useCallback((comment: WorkflowCommentList) => {
// center viewport on the comment position and activate it
reactFlow.setCenter(comment.position_x, comment.position_y, { zoom: 1, duration: 600 })
setActiveCommentId(comment.id)
}, [reactFlow, setActiveCommentId])
handleCommentIconClick(comment)
}, [handleCommentIconClick])
const { userProfile } = useAppContext()

View File

@ -0,0 +1,28 @@
import type { StateCreator } from 'zustand'
import type { WorkflowCommentDetail, WorkflowCommentList } from '@/service/workflow-comment'
export type CommentSliceShape = {
comments: WorkflowCommentList[]
setComments: (comments: WorkflowCommentList[]) => void
commentsLoading: boolean
setCommentsLoading: (loading: boolean) => void
activeCommentDetail: WorkflowCommentDetail | null
setActiveCommentDetail: (comment: WorkflowCommentDetail | null) => void
activeCommentDetailLoading: boolean
setActiveCommentDetailLoading: (loading: boolean) => void
commentDetailCache: Record<string, WorkflowCommentDetail>
setCommentDetailCache: (cache: Record<string, WorkflowCommentDetail>) => void
}
export const createCommentSlice: StateCreator<CommentSliceShape> = set => ({
comments: [],
setComments: comments => set({ comments }),
commentsLoading: false,
setCommentsLoading: commentsLoading => set({ commentsLoading }),
activeCommentDetail: null,
setActiveCommentDetail: activeCommentDetail => set({ activeCommentDetail }),
activeCommentDetailLoading: false,
setActiveCommentDetailLoading: activeCommentDetailLoading => set({ activeCommentDetailLoading }),
commentDetailCache: {},
setCommentDetailCache: commentDetailCache => set({ commentDetailCache }),
})

View File

@ -20,6 +20,8 @@ import type { NodeSliceShape } from './node-slice'
import { createNodeSlice } from './node-slice'
import type { PanelSliceShape } from './panel-slice'
import { createPanelSlice } from './panel-slice'
import type { CommentSliceShape } from './comment-slice'
import { createCommentSlice } from './comment-slice'
import type { ToolSliceShape } from './tool-slice'
import { createToolSlice } from './tool-slice'
import type { VersionSliceShape } from './version-slice'
@ -44,6 +46,7 @@ export type Shape =
HistorySliceShape &
NodeSliceShape &
PanelSliceShape &
CommentSliceShape &
ToolSliceShape &
VersionSliceShape &
WorkflowDraftSliceShape &
@ -67,6 +70,7 @@ export const createWorkflowStore = (params: CreateWorkflowStoreParams) => {
...createHistorySlice(...args),
...createNodeSlice(...args),
...createPanelSlice(...args),
...createCommentSlice(...args),
...createToolSlice(...args),
...createVersionSlice(...args),
...createWorkflowDraftSlice(...args),