add comment preview

This commit is contained in:
hjlarry 2025-09-24 12:54:54 +08:00
parent 4188c9a1dd
commit 86a9a51952
4 changed files with 127 additions and 38 deletions

View File

@ -1,19 +1,27 @@
'use client'
import type { FC } from 'react'
import { memo, useMemo } from 'react'
import { memo, useMemo, useState } from 'react'
import { useReactFlow, useViewport } from 'reactflow'
import { UserAvatarList } from '@/app/components/base/user-avatar-list'
import CommentPreview from './comment-preview'
import type { WorkflowCommentList } from '@/service/workflow-comment'
type CommentIconProps = {
comment: WorkflowCommentList
onClick: () => void
isActive?: boolean
}
export const CommentIcon: FC<CommentIconProps> = memo(({ comment, onClick }) => {
export const CommentIcon: FC<CommentIconProps> = memo(({ comment, onClick, isActive = false }) => {
const { flowToScreenPosition } = useReactFlow()
const viewport = useViewport()
const [showPreview, setShowPreview] = useState(false)
const handlePreviewClick = () => {
setShowPreview(false)
onClick()
}
const screenPosition = useMemo(() => {
return flowToScreenPosition({
@ -35,30 +43,58 @@ export const CommentIcon: FC<CommentIconProps> = memo(({ comment, onClick }) =>
)
return (
<div
className="absolute z-10 cursor-pointer"
style={{
left: screenPosition.x,
top: screenPosition.y,
transform: 'translate(-50%, -50%)',
}}
onClick={onClick}
>
<>
<div
className={'relative h-10 overflow-hidden rounded-br-full rounded-tl-full rounded-tr-full'}
style={{ width: dynamicWidth }}
className="absolute z-10"
style={{
left: screenPosition.x,
top: screenPosition.y,
transform: 'translate(-50%, -50%)',
}}
>
<div className="absolute inset-[6px] overflow-hidden rounded-br-full rounded-tl-full rounded-tr-full border border-components-panel-border bg-components-panel-bg">
<div className="flex h-full w-full items-center justify-center px-1">
<UserAvatarList
users={comment.participants}
maxVisible={3}
size={24}
/>
<div
className={isActive ? '' : 'cursor-pointer'}
onClick={isActive ? undefined : onClick}
onMouseEnter={isActive ? undefined : () => setShowPreview(true)}
onMouseLeave={isActive ? undefined : () => setShowPreview(false)}
>
<div
className={'relative h-10 overflow-hidden rounded-br-full rounded-tl-full rounded-tr-full'}
style={{ width: dynamicWidth }}
>
<div className={`absolute inset-[6px] overflow-hidden rounded-br-full rounded-tl-full rounded-tr-full border ${
isActive
? 'border-2 border-primary-500 bg-components-panel-bg'
: 'border-components-panel-border bg-components-panel-bg'
}`}>
<div className="flex h-full w-full items-center justify-center px-1">
<UserAvatarList
users={comment.participants}
maxVisible={3}
size={24}
/>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Preview panel */}
{showPreview && !isActive && (
<div
className="absolute z-20"
style={{
left: screenPosition.x - dynamicWidth / 2,
top: screenPosition.y + 20,
transform: 'translateY(-100%)',
}}
onMouseEnter={() => setShowPreview(true)}
onMouseLeave={() => setShowPreview(false)}
>
<CommentPreview comment={comment} onClick={handlePreviewClick} />
</div>
)}
</>
)
}, (prevProps, nextProps) => {
return (
@ -66,6 +102,7 @@ export const CommentIcon: FC<CommentIconProps> = memo(({ comment, onClick }) =>
&& prevProps.comment.position_x === nextProps.comment.position_x
&& prevProps.comment.position_y === nextProps.comment.position_y
&& prevProps.onClick === nextProps.onClick
&& prevProps.isActive === nextProps.isActive
)
})

View File

@ -0,0 +1,44 @@
'use client'
import type { FC } from 'react'
import { memo } from 'react'
import { UserAvatarList } from '@/app/components/base/user-avatar-list'
import type { WorkflowCommentList } from '@/service/workflow-comment'
import { useFormatTimeFromNow } from '@/app/components/workflow/hooks'
type CommentPreviewProps = {
comment: WorkflowCommentList
onClick?: () => void
}
const CommentPreview: FC<CommentPreviewProps> = ({ comment, onClick }) => {
const { formatTimeFromNow } = useFormatTimeFromNow()
return (
<div
className="w-80 cursor-pointer rounded-br-xl rounded-tl-xl rounded-tr-xl border border-components-panel-border bg-components-panel-bg p-4 shadow-lg transition-colors hover:bg-components-panel-on-panel-item-bg-hover"
onClick={onClick}
>
<div className="mb-3 flex items-center justify-between">
<UserAvatarList
users={comment.participants}
maxVisible={3}
size={24}
/>
</div>
<div className="mb-2 flex items-start">
<div className="flex min-w-0 items-center gap-2">
<div className="system-sm-medium truncate text-text-primary">{comment.created_by_account.name}</div>
<div className="system-2xs-regular shrink-0 text-text-tertiary">
{formatTimeFromNow(comment.updated_at * 1000)}
</div>
</div>
</div>
<div className="system-sm-regular break-words text-text-secondary">{comment.content}</div>
</div>
)
}
export default memo(CommentPreview)

View File

@ -216,9 +216,9 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
<div
className='absolute z-50 w-[360px] max-w-[360px]'
style={{
left: screenPosition.x,
left: screenPosition.x + 40,
top: screenPosition.y,
transform: 'translate(-50%, -100%) translateY(-24px)',
transform: 'translateY(-20%)',
}}
>
<div className='relative flex h-[360px] flex-col overflow-hidden rounded-2xl border border-components-panel-border bg-components-panel-bg shadow-xl'>

View File

@ -454,21 +454,29 @@ export const Workflow: FC<WorkflowProps> = memo(({
const canGoPrev = index > 0
const canGoNext = index < comments.length - 1
return (
<CommentThread
key={comment.id}
comment={activeComment}
loading={activeCommentLoading}
onClose={handleActiveCommentClose}
onResolve={() => handleCommentResolve(comment.id)}
onDelete={() => setPendingDeleteCommentId(comment.id)}
onPrev={canGoPrev ? () => handleCommentNavigate('prev') : undefined}
onNext={canGoNext ? () => handleCommentNavigate('next') : undefined}
onReply={(content, ids) => handleCommentReply(comment.id, content, ids ?? [])}
onReplyEdit={(replyId, content, ids) => handleCommentReplyUpdate(comment.id, replyId, content, ids ?? [])}
onReplyDelete={replyId => setPendingDeleteReply({ commentId: comment.id, replyId })}
canGoPrev={canGoPrev}
canGoNext={canGoNext}
/>
<>
<CommentIcon
key={`${comment.id}-icon`}
comment={comment}
onClick={() => handleCommentIconClick(comment)}
isActive={true}
/>
<CommentThread
key={`${comment.id}-thread`}
comment={activeComment}
loading={activeCommentLoading}
onClose={handleActiveCommentClose}
onResolve={() => handleCommentResolve(comment.id)}
onDelete={() => setPendingDeleteCommentId(comment.id)}
onPrev={canGoPrev ? () => handleCommentNavigate('prev') : undefined}
onNext={canGoNext ? () => handleCommentNavigate('next') : undefined}
onReply={(content, ids) => handleCommentReply(comment.id, content, ids ?? [])}
onReplyEdit={(replyId, content, ids) => handleCommentReplyUpdate(comment.id, replyId, content, ids ?? [])}
onReplyDelete={replyId => setPendingDeleteReply({ commentId: comment.id, replyId })}
canGoPrev={canGoPrev}
canGoNext={canGoNext}
/>
</>
)
}