mirror of
https://github.com/langgenius/dify.git
synced 2026-05-04 09:28:04 +08:00
Merge branch 'p284' into deploy/dev
This commit is contained in:
@ -4,7 +4,6 @@ import {
|
||||
import produce from 'immer'
|
||||
import {
|
||||
useReactFlow,
|
||||
useStoreApi,
|
||||
useViewport,
|
||||
} from 'reactflow'
|
||||
import { useEventListener } from 'ahooks'
|
||||
@ -19,9 +18,9 @@ import CustomNode from './nodes'
|
||||
import CustomNoteNode from './note-node'
|
||||
import { CUSTOM_NOTE_NODE } from './note-node/constants'
|
||||
import { BlockEnum } from './types'
|
||||
import { useCollaborativeWorkflow } from '@/app/components/workflow/hooks/use-collaborative-workflow'
|
||||
|
||||
const CandidateNode = () => {
|
||||
const store = useStoreApi()
|
||||
const reactflow = useReactFlow()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const candidateNode = useStore(s => s.candidateNode)
|
||||
@ -29,18 +28,15 @@ const CandidateNode = () => {
|
||||
const { zoom } = useViewport()
|
||||
const { handleNodeSelect } = useNodesInteractions()
|
||||
const { saveStateToHistory } = useWorkflowHistory()
|
||||
const collaborativeWorkflow = useCollaborativeWorkflow()
|
||||
|
||||
useEventListener('click', (e) => {
|
||||
const { candidateNode, mousePosition } = workflowStore.getState()
|
||||
|
||||
if (candidateNode) {
|
||||
e.preventDefault()
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
const { nodes, setNodes } = collaborativeWorkflow.getState()
|
||||
const { screenToFlowPosition } = reactflow
|
||||
const nodes = getNodes()
|
||||
const { x, y } = screenToFlowPosition({ x: mousePosition.pageX, y: mousePosition.pageY })
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft.push({
|
||||
|
||||
@ -37,7 +37,7 @@ const UserCursors: FC<UserCursorsProps> = ({
|
||||
return (
|
||||
<div
|
||||
key={userId}
|
||||
className="pointer-events-none absolute z-[10000] transition-all duration-150 ease-out"
|
||||
className="pointer-events-none absolute z-[8] transition-all duration-150 ease-out"
|
||||
style={{
|
||||
left: screenPos.x,
|
||||
top: screenPos.y,
|
||||
|
||||
@ -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-1 overflow-hidden rounded-br-full rounded-tl-full rounded-tr-full bg-white">
|
||||
<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
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { FC } from 'react'
|
||||
import { memo, useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Avatar from '@/app/components/base/avatar'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { MentionInput } from './mention-input'
|
||||
@ -13,6 +14,7 @@ type CommentInputProps = {
|
||||
|
||||
export const CommentInput: FC<CommentInputProps> = memo(({ position, onSubmit, onCancel }) => {
|
||||
const [content, setContent] = useState('')
|
||||
const { t } = useTranslation()
|
||||
const { userProfile } = useAppContext()
|
||||
|
||||
useEffect(() => {
|
||||
@ -46,8 +48,8 @@ export const CommentInput: FC<CommentInputProps> = memo(({ position, onSubmit, o
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative shrink-0">
|
||||
<div className="relative h-10 w-10 overflow-hidden rounded-br-full rounded-tl-full rounded-tr-full bg-primary-500">
|
||||
<div className="absolute inset-1 overflow-hidden rounded-br-full rounded-tl-full rounded-tr-full bg-white">
|
||||
<div className="relative h-8 w-8 overflow-hidden rounded-br-full rounded-tl-full rounded-tr-full bg-primary-500">
|
||||
<div className="absolute inset-[2px] overflow-hidden rounded-br-full rounded-tl-full rounded-tr-full bg-white">
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div className="h-6 w-6 overflow-hidden rounded-full">
|
||||
<Avatar
|
||||
@ -63,15 +65,15 @@ export const CommentInput: FC<CommentInputProps> = memo(({ position, onSubmit, o
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'relative z-10 flex-1 rounded-xl border border-components-chat-input-border bg-components-panel-bg-blur pb-[9px] shadow-md',
|
||||
'relative z-10 flex-1 rounded-xl border border-components-chat-input-border bg-components-panel-bg-blur pb-[4px] shadow-md',
|
||||
)}
|
||||
>
|
||||
<div className='relative px-[9px] pt-[9px]'>
|
||||
<div className='relative px-[9px] pt-[4px]'>
|
||||
<MentionInput
|
||||
value={content}
|
||||
onChange={setContent}
|
||||
onSubmit={handleMentionSubmit}
|
||||
placeholder="Add a comment"
|
||||
placeholder={t('workflow.comments.placeholder.add')}
|
||||
autoFocus
|
||||
className="relative"
|
||||
/>
|
||||
|
||||
44
web/app/components/workflow/comment/comment-preview.tsx
Normal file
44
web/app/components/workflow/comment/comment-preview.tsx
Normal 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)
|
||||
@ -2,6 +2,7 @@ import type { FC } from 'react'
|
||||
import { memo } from 'react'
|
||||
import { useStore } from '../store'
|
||||
import { ControlMode } from '../types'
|
||||
import { Comment } from '@/app/components/base/icons/src/public/other'
|
||||
|
||||
type CommentCursorProps = {
|
||||
mousePosition: { elementX: number; elementY: number }
|
||||
@ -22,10 +23,7 @@ export const CommentCursor: FC<CommentCursorProps> = memo(({ mousePosition }) =>
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path d="M10.5 6.33325H5.5H10.5ZM8 9.66658H5.5H8ZM0.5 14.6666H11.3333C13.6345 14.6666 15.5 12.8011 15.5 10.4999V5.49992C15.5 3.19874 13.6345 1.33325 11.3333 1.33325H4.66667C2.36548 1.33325 0.5 3.19874 0.5 5.49992V14.6666Z" fill="white"/>
|
||||
<path d="M10.5 6.33325H5.5M8 9.66658H5.5M0.5 14.6666H11.3333C13.6345 14.6666 15.5 12.8011 15.5 10.4999V5.49992C15.5 3.19874 13.6345 1.33325 11.3333 1.33325H4.66667C2.36548 1.33325 0.5 3.19874 0.5 5.49992V14.6666Z" stroke="black"/>
|
||||
</svg>
|
||||
<Comment className="text-text-primary" />
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
'use client'
|
||||
|
||||
import type { FC } from 'react'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiArrowUpLine, RiAtLine } from '@remixicon/react'
|
||||
import Textarea from 'react-textarea-autosize'
|
||||
import Button from '@/app/components/base/button'
|
||||
@ -29,7 +30,7 @@ export const MentionInput: FC<MentionInputProps> = memo(({
|
||||
onChange,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
placeholder = 'Add a comment',
|
||||
placeholder,
|
||||
disabled = false,
|
||||
loading = false,
|
||||
className,
|
||||
@ -37,6 +38,7 @@ export const MentionInput: FC<MentionInputProps> = memo(({
|
||||
autoFocus = false,
|
||||
}) => {
|
||||
const params = useParams()
|
||||
const { t } = useTranslation()
|
||||
const appId = params.appId as string
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
|
||||
@ -46,6 +48,77 @@ export const MentionInput: FC<MentionInputProps> = memo(({
|
||||
const [mentionPosition, setMentionPosition] = useState(0)
|
||||
const [selectedMentionIndex, setSelectedMentionIndex] = useState(0)
|
||||
const [mentionedUserIds, setMentionedUserIds] = useState<string[]>([])
|
||||
const resolvedPlaceholder = placeholder ?? t('workflow.comments.placeholder.add')
|
||||
|
||||
const mentionNameList = useMemo(() => {
|
||||
const names = mentionUsers
|
||||
.map(user => user.name?.trim())
|
||||
.filter((name): name is string => Boolean(name))
|
||||
|
||||
const uniqueNames = Array.from(new Set(names))
|
||||
uniqueNames.sort((a, b) => b.length - a.length)
|
||||
return uniqueNames
|
||||
}, [mentionUsers])
|
||||
|
||||
const highlightedValue = useMemo<ReactNode>(() => {
|
||||
if (!value)
|
||||
return ''
|
||||
|
||||
if (mentionNameList.length === 0)
|
||||
return value
|
||||
|
||||
const segments: ReactNode[] = []
|
||||
let cursor = 0
|
||||
let hasMention = false
|
||||
|
||||
while (cursor < value.length) {
|
||||
let nextMatchStart = -1
|
||||
let matchedName = ''
|
||||
|
||||
for (const name of mentionNameList) {
|
||||
const searchStart = value.indexOf(`@${name}`, cursor)
|
||||
if (searchStart === -1)
|
||||
continue
|
||||
|
||||
const previousChar = searchStart > 0 ? value[searchStart - 1] : ''
|
||||
if (searchStart > 0 && !/\s/.test(previousChar))
|
||||
continue
|
||||
|
||||
if (
|
||||
nextMatchStart === -1
|
||||
|| searchStart < nextMatchStart
|
||||
|| (searchStart === nextMatchStart && name.length > matchedName.length)
|
||||
) {
|
||||
nextMatchStart = searchStart
|
||||
matchedName = name
|
||||
}
|
||||
}
|
||||
|
||||
if (nextMatchStart === -1)
|
||||
break
|
||||
|
||||
if (nextMatchStart > cursor)
|
||||
segments.push(<span key={`text-${cursor}`}>{value.slice(cursor, nextMatchStart)}</span>)
|
||||
|
||||
const mentionEnd = nextMatchStart + matchedName.length + 1
|
||||
segments.push(
|
||||
<span key={`mention-${nextMatchStart}`} className='text-primary-600'>
|
||||
{value.slice(nextMatchStart, mentionEnd)}
|
||||
</span>,
|
||||
)
|
||||
|
||||
hasMention = true
|
||||
cursor = mentionEnd
|
||||
}
|
||||
|
||||
if (!hasMention)
|
||||
return value
|
||||
|
||||
if (cursor < value.length)
|
||||
segments.push(<span key={`text-${cursor}`}>{value.slice(cursor)}</span>)
|
||||
|
||||
return segments
|
||||
}, [value, mentionNameList])
|
||||
|
||||
const loadMentionableUsers = useCallback(async () => {
|
||||
if (!appId) return
|
||||
@ -220,12 +293,23 @@ export const MentionInput: FC<MentionInputProps> = memo(({
|
||||
return (
|
||||
<>
|
||||
<div className={cn('relative flex items-center', className)}>
|
||||
<div
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'pointer-events-none absolute inset-0 z-0 overflow-hidden whitespace-pre-wrap break-words p-1 leading-6',
|
||||
'body-lg-regular text-text-primary',
|
||||
)}
|
||||
>
|
||||
{highlightedValue}
|
||||
{''}
|
||||
</div>
|
||||
<Textarea
|
||||
ref={textareaRef}
|
||||
className={cn(
|
||||
'body-lg-regular w-full resize-none bg-transparent p-1 leading-6 text-text-primary caret-primary-500 outline-none',
|
||||
'body-lg-regular relative z-10 w-full resize-none bg-transparent p-1 leading-6 text-transparent caret-primary-500 outline-none',
|
||||
'placeholder:text-text-tertiary',
|
||||
)}
|
||||
placeholder={placeholder}
|
||||
placeholder={resolvedPlaceholder}
|
||||
autoFocus={autoFocus}
|
||||
minRows={isEditing ? 4 : 1}
|
||||
maxRows={4}
|
||||
@ -238,10 +322,10 @@ export const MentionInput: FC<MentionInputProps> = memo(({
|
||||
{!isEditing && (
|
||||
<div className="absolute bottom-0 right-1 z-20 flex items-end gap-1">
|
||||
<div
|
||||
className="z-20 flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg bg-components-button-secondary-bg hover:bg-state-base-hover"
|
||||
className="z-20 flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg hover:bg-state-base-hover"
|
||||
onClick={handleMentionButtonClick}
|
||||
>
|
||||
<RiAtLine className="h-4 w-4" />
|
||||
<RiAtLine className="h-4 w-4 text-components-button-primary-text" />
|
||||
</div>
|
||||
<Button
|
||||
className='z-20 ml-2 w-8 px-0'
|
||||
@ -249,7 +333,7 @@ export const MentionInput: FC<MentionInputProps> = memo(({
|
||||
disabled={!value.trim() || disabled || loading}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
<RiArrowUpLine className='h-4 w-4' />
|
||||
<RiArrowUpLine className='h-4 w-4 text-components-button-primary-text' />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
@ -257,14 +341,14 @@ export const MentionInput: FC<MentionInputProps> = memo(({
|
||||
{isEditing && (
|
||||
<div className="absolute bottom-0 left-1 right-1 z-20 flex items-end justify-between">
|
||||
<div
|
||||
className="z-20 flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg bg-components-button-secondary-bg hover:bg-state-base-hover"
|
||||
className="z-20 flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg hover:bg-state-base-hover"
|
||||
onClick={handleMentionButtonClick}
|
||||
>
|
||||
<RiAtLine className="h-4 w-4" />
|
||||
<RiAtLine className="h-4 w-4 text-components-button-primary-text" />
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Button variant='secondary' size='small' onClick={onCancel} disabled={loading}>
|
||||
Cancel
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
@ -272,7 +356,7 @@ export const MentionInput: FC<MentionInputProps> = memo(({
|
||||
disabled={loading || !value.trim()}
|
||||
onClick={() => handleSubmit()}
|
||||
>
|
||||
Save
|
||||
{t('common.operation.save')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -281,7 +365,7 @@ export const MentionInput: FC<MentionInputProps> = memo(({
|
||||
|
||||
{showMentionDropdown && filteredMentionUsers.length > 0 && typeof document !== 'undefined' && createPortal(
|
||||
<div
|
||||
className="fixed z-[9999] max-h-40 w-64 overflow-y-auto rounded-lg border border-components-panel-border bg-white shadow-lg"
|
||||
className="fixed z-[9999] max-h-40 w-64 overflow-y-auto rounded-lg border border-components-panel-border bg-components-panel-bg shadow-lg"
|
||||
style={{
|
||||
left: dropdownPosition.x,
|
||||
top: dropdownPosition.y,
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import type { FC } from 'react'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useReactFlow, useViewport } from 'reactflow'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiArrowDownSLine, RiArrowUpSLine, RiCheckboxCircleFill, RiCheckboxCircleLine, RiCloseLine, RiDeleteBinLine, RiMoreFill } from '@remixicon/react'
|
||||
import Avatar from '@/app/components/base/avatar'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
@ -11,6 +12,7 @@ import { useFormatTimeFromNow } from '@/app/components/workflow/hooks'
|
||||
import type { WorkflowCommentDetail, WorkflowCommentDetailReply } from '@/service/workflow-comment'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { MentionInput } from './mention-input'
|
||||
import { getUserColor } from '@/app/components/workflow/collaboration/utils/user-color'
|
||||
|
||||
type CommentThreadProps = {
|
||||
comment: WorkflowCommentDetail
|
||||
@ -28,12 +30,81 @@ type CommentThreadProps = {
|
||||
}
|
||||
|
||||
const ThreadMessage: FC<{
|
||||
authorId: string
|
||||
authorName: string
|
||||
avatarUrl?: string | null
|
||||
createdAt: number
|
||||
content: string
|
||||
}> = ({ authorName, avatarUrl, createdAt, content }) => {
|
||||
mentionedNames?: string[]
|
||||
}> = ({ authorId, authorName, avatarUrl, createdAt, content, mentionedNames }) => {
|
||||
const { formatTimeFromNow } = useFormatTimeFromNow()
|
||||
const { userProfile } = useAppContext()
|
||||
const currentUserId = userProfile?.id
|
||||
const isCurrentUser = authorId === currentUserId
|
||||
const userColor = isCurrentUser ? undefined : getUserColor(authorId)
|
||||
|
||||
const highlightedContent = useMemo<ReactNode>(() => {
|
||||
if (!content)
|
||||
return ''
|
||||
|
||||
const normalizedNames = Array.from(new Set((mentionedNames || [])
|
||||
.map(name => name.trim())
|
||||
.filter(Boolean)))
|
||||
|
||||
if (normalizedNames.length === 0)
|
||||
return content
|
||||
|
||||
const segments: ReactNode[] = []
|
||||
let hasMention = false
|
||||
let cursor = 0
|
||||
|
||||
while (cursor < content.length) {
|
||||
let nextMatchStart = -1
|
||||
let matchedName = ''
|
||||
|
||||
for (const name of normalizedNames) {
|
||||
const searchStart = content.indexOf(`@${name}`, cursor)
|
||||
if (searchStart === -1)
|
||||
continue
|
||||
|
||||
const previousChar = searchStart > 0 ? content[searchStart - 1] : ''
|
||||
if (searchStart > 0 && !/\s/.test(previousChar))
|
||||
continue
|
||||
|
||||
if (
|
||||
nextMatchStart === -1
|
||||
|| searchStart < nextMatchStart
|
||||
|| (searchStart === nextMatchStart && name.length > matchedName.length)
|
||||
) {
|
||||
nextMatchStart = searchStart
|
||||
matchedName = name
|
||||
}
|
||||
}
|
||||
|
||||
if (nextMatchStart === -1)
|
||||
break
|
||||
|
||||
if (nextMatchStart > cursor)
|
||||
segments.push(<span key={`text-${cursor}`}>{content.slice(cursor, nextMatchStart)}</span>)
|
||||
|
||||
const mentionEnd = nextMatchStart + matchedName.length + 1
|
||||
segments.push(
|
||||
<span key={`mention-${nextMatchStart}`} className='text-primary-600'>
|
||||
{content.slice(nextMatchStart, mentionEnd)}
|
||||
</span>,
|
||||
)
|
||||
hasMention = true
|
||||
cursor = mentionEnd
|
||||
}
|
||||
|
||||
if (!hasMention)
|
||||
return content
|
||||
|
||||
if (cursor < content.length)
|
||||
segments.push(<span key={`text-${cursor}`}>{content.slice(cursor)}</span>)
|
||||
|
||||
return segments
|
||||
}, [content, mentionedNames])
|
||||
|
||||
return (
|
||||
<div className={cn('flex gap-3 pt-1')}>
|
||||
@ -43,6 +114,7 @@ const ThreadMessage: FC<{
|
||||
avatar={avatarUrl || null}
|
||||
size={24}
|
||||
className={cn('h-8 w-8 rounded-full')}
|
||||
backgroundColor={userColor}
|
||||
/>
|
||||
</div>
|
||||
<div className='min-w-0 flex-1 pb-4 text-text-primary last:pb-0'>
|
||||
@ -51,7 +123,7 @@ const ThreadMessage: FC<{
|
||||
<span className='system-2xs-regular text-text-tertiary'>{formatTimeFromNow(createdAt * 1000)}</span>
|
||||
</div>
|
||||
<div className='system-sm-regular mt-1 whitespace-pre-wrap break-words text-text-secondary'>
|
||||
{content}
|
||||
{highlightedContent}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -75,6 +147,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
||||
const { flowToScreenPosition } = useReactFlow()
|
||||
const viewport = useViewport()
|
||||
const { userProfile } = useAppContext()
|
||||
const { t } = useTranslation()
|
||||
const [replyContent, setReplyContent] = useState('')
|
||||
const [activeReplyMenuId, setActiveReplyMenuId] = useState<string | null>(null)
|
||||
const [editingReply, setEditingReply] = useState<{ id: string; content: string }>({ id: '', content: '' })
|
||||
@ -120,26 +193,44 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
||||
}, [editingReply, onReplyEdit])
|
||||
|
||||
const replies = comment.replies || []
|
||||
const mentionsByTarget = useMemo(() => {
|
||||
const map = new Map<string, string[]>()
|
||||
for (const mention of comment.mentions || []) {
|
||||
const name = mention.mentioned_user_account?.name?.trim()
|
||||
if (!name)
|
||||
continue
|
||||
const key = mention.reply_id ?? 'root'
|
||||
const existing = map.get(key)
|
||||
if (existing) {
|
||||
if (!existing.includes(name))
|
||||
existing.push(name)
|
||||
}
|
||||
else {
|
||||
map.set(key, [name])
|
||||
}
|
||||
}
|
||||
return map
|
||||
}, [comment.mentions])
|
||||
|
||||
return (
|
||||
<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'>
|
||||
<div className='flex items-center justify-between rounded-t-2xl border-b border-components-panel-border bg-components-panel-bg-blur px-4 py-3'>
|
||||
<div className=' font-semibold uppercase text-text-primary'>Comment</div>
|
||||
<div className='font-semibold uppercase text-text-primary'>{t('workflow.comments.panelTitle')}</div>
|
||||
<div className='flex items-center gap-1'>
|
||||
<button
|
||||
type='button'
|
||||
disabled={loading}
|
||||
className={cn('flex h-6 w-6 items-center justify-center rounded-lg text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary disabled:cursor-not-allowed disabled:text-text-disabled disabled:hover:bg-transparent disabled:hover:text-text-disabled')}
|
||||
onClick={onDelete}
|
||||
aria-label='Delete comment'
|
||||
aria-label={t('workflow.comments.aria.deleteComment')}
|
||||
>
|
||||
<RiDeleteBinLine className='h-4 w-4' />
|
||||
</button>
|
||||
@ -148,7 +239,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
||||
disabled={comment.resolved || loading}
|
||||
className={cn('flex h-6 w-6 items-center justify-center rounded-lg text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary disabled:cursor-not-allowed disabled:text-text-disabled disabled:hover:bg-transparent disabled:hover:text-text-disabled')}
|
||||
onClick={onResolve}
|
||||
aria-label='Resolve comment'
|
||||
aria-label={t('workflow.comments.aria.resolveComment')}
|
||||
>
|
||||
{comment.resolved ? <RiCheckboxCircleFill className='h-4 w-4' /> : <RiCheckboxCircleLine className='h-4 w-4' />}
|
||||
</button>
|
||||
@ -158,7 +249,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
||||
disabled={!canGoPrev || loading}
|
||||
className={cn('flex h-6 w-6 items-center justify-center rounded-lg text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary disabled:cursor-not-allowed disabled:text-text-disabled disabled:hover:bg-transparent disabled:hover:text-text-disabled')}
|
||||
onClick={onPrev}
|
||||
aria-label='Previous comment'
|
||||
aria-label={t('workflow.comments.aria.previousComment')}
|
||||
>
|
||||
<RiArrowUpSLine className='h-4 w-4' />
|
||||
</button>
|
||||
@ -167,7 +258,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
||||
disabled={!canGoNext || loading}
|
||||
className={cn('flex h-6 w-6 items-center justify-center rounded-lg text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary disabled:cursor-not-allowed disabled:text-text-disabled disabled:hover:bg-transparent disabled:hover:text-text-disabled')}
|
||||
onClick={onNext}
|
||||
aria-label='Next comment'
|
||||
aria-label={t('workflow.comments.aria.nextComment')}
|
||||
>
|
||||
<RiArrowDownSLine className='h-4 w-4' />
|
||||
</button>
|
||||
@ -175,7 +266,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
||||
type='button'
|
||||
className='flex h-6 w-6 items-center justify-center rounded-lg text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary'
|
||||
onClick={onClose}
|
||||
aria-label='Close comment'
|
||||
aria-label={t('workflow.comments.aria.closeComment')}
|
||||
>
|
||||
<RiCloseLine className='h-4 w-4' />
|
||||
</button>
|
||||
@ -183,10 +274,12 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
||||
</div>
|
||||
<div className='relative mt-2 flex-1 overflow-y-auto px-4'>
|
||||
<ThreadMessage
|
||||
authorName={comment.created_by_account?.name || 'User'}
|
||||
authorId={comment.created_by_account?.id || ''}
|
||||
authorName={comment.created_by_account?.name || t('workflow.comments.fallback.user')}
|
||||
avatarUrl={comment.created_by_account?.avatar_url || null}
|
||||
createdAt={comment.created_at}
|
||||
content={comment.content}
|
||||
mentionedNames={mentionsByTarget.get('root')}
|
||||
/>
|
||||
{replies.length > 0 && (
|
||||
<div className='mt-2 space-y-3 pt-3'>
|
||||
@ -206,7 +299,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
||||
e.stopPropagation()
|
||||
setActiveReplyMenuId(prev => prev === reply.id ? null : reply.id)
|
||||
}}
|
||||
aria-label='Reply actions'
|
||||
aria-label={t('workflow.comments.aria.replyActions')}
|
||||
>
|
||||
<RiMoreFill className='h-4 w-4' />
|
||||
</button>
|
||||
@ -216,16 +309,16 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
||||
className='flex w-full items-center justify-start px-3 py-2 text-left text-sm text-text-secondary hover:bg-state-base-hover'
|
||||
onClick={() => handleStartEdit(reply)}
|
||||
>
|
||||
Edit reply
|
||||
{t('workflow.comments.actions.editReply')}
|
||||
</button>
|
||||
<button
|
||||
className='text-negative flex w-full items-center justify-start px-3 py-2 text-left text-sm hover:bg-state-base-hover'
|
||||
className='text-negative flex w-full items-center justify-start px-3 py-2 text-left text-sm text-text-secondary hover:bg-state-base-hover'
|
||||
onClick={() => {
|
||||
setActiveReplyMenuId(null)
|
||||
onReplyDelete?.(reply.id)
|
||||
}}
|
||||
>
|
||||
Delete reply
|
||||
{t('workflow.comments.actions.deleteReply')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@ -238,7 +331,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
||||
onChange={newContent => setEditingReply(prev => prev ? { ...prev, content: newContent } : prev)}
|
||||
onSubmit={handleEditSubmit}
|
||||
onCancel={handleCancelEdit}
|
||||
placeholder="Edit reply"
|
||||
placeholder={t('workflow.comments.placeholder.editReply')}
|
||||
disabled={loading}
|
||||
loading={loading}
|
||||
isEditing={true}
|
||||
@ -248,10 +341,12 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
||||
</div>
|
||||
) : (
|
||||
<ThreadMessage
|
||||
authorName={reply.created_by_account?.name || 'User'}
|
||||
authorId={reply.created_by_account?.id || ''}
|
||||
authorName={reply.created_by_account?.name || t('workflow.comments.fallback.user')}
|
||||
avatarUrl={reply.created_by_account?.avatar_url || null}
|
||||
createdAt={reply.created_at}
|
||||
content={reply.content}
|
||||
mentionedNames={mentionsByTarget.get(reply.id)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@ -262,7 +357,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
||||
</div>
|
||||
{loading && (
|
||||
<div className='bg-components-panel-bg/70 absolute inset-0 z-30 flex items-center justify-center text-sm text-text-tertiary'>
|
||||
Loading…
|
||||
{t('workflow.comments.loading')}
|
||||
</div>
|
||||
)}
|
||||
{onReply && (
|
||||
@ -270,7 +365,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
||||
<div className='flex items-center gap-3'>
|
||||
<Avatar
|
||||
avatar={userProfile?.avatar_url || null}
|
||||
name={userProfile?.name || 'You'}
|
||||
name={userProfile?.name || t('common.you')}
|
||||
size={24}
|
||||
className='h-8 w-8'
|
||||
/>
|
||||
@ -279,7 +374,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
||||
value={replyContent}
|
||||
onChange={setReplyContent}
|
||||
onSubmit={handleReplySubmit}
|
||||
placeholder='Reply'
|
||||
placeholder={t('workflow.comments.placeholder.reply')}
|
||||
disabled={loading}
|
||||
loading={loading}
|
||||
className='px-2'
|
||||
|
||||
@ -19,6 +19,7 @@ import RestoringTitle from './restoring-title'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { useInvalidAllLastRun } from '@/service/use-workflow'
|
||||
import { useHooksStore } from '../hooks-store'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import cn from '@/utils/classnames'
|
||||
import { collaborationManager } from '../collaboration/core/collaboration-manager'
|
||||
@ -32,6 +33,7 @@ const HeaderInRestoring = ({
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const appDetail = useAppStore.getState().appDetail
|
||||
const configsMap = useHooksStore(s => s.configsMap)
|
||||
const invalidAllLastRun = useInvalidAllLastRun(configsMap?.flowType, configsMap?.flowId)
|
||||
const {
|
||||
|
||||
@ -83,7 +83,7 @@ const OnlineUsers = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center rounded-full bg-white px-1 py-1">
|
||||
<div className="flex items-center rounded-full border border-components-panel-border bg-components-panel-bg px-1 py-1">
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center -space-x-2">
|
||||
{visibleUsers.map((user, index) => {
|
||||
@ -114,7 +114,7 @@ const OnlineUsers = () => {
|
||||
name={user.username || 'User'}
|
||||
avatar={getAvatarUrl(user)}
|
||||
size={28}
|
||||
className="ring-2 ring-white"
|
||||
className="ring-2 ring-components-panel-bg"
|
||||
backgroundColor={userColor}
|
||||
/>
|
||||
</div>
|
||||
@ -138,7 +138,7 @@ const OnlineUsers = () => {
|
||||
'flex h-7 w-7 items-center justify-center',
|
||||
'cursor-pointer rounded-full bg-gray-300',
|
||||
'text-xs font-medium text-gray-700',
|
||||
'ring-2 ring-white',
|
||||
'ring-2 ring-components-panel-bg',
|
||||
)}
|
||||
>
|
||||
+{remainingCount}
|
||||
|
||||
@ -116,7 +116,9 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
|
||||
|
||||
if (node.type === CUSTOM_NODE) {
|
||||
const checkData = getCheckData(node.data)
|
||||
let { errorMessage } = nodesExtraData![node.data.type].checkValid(checkData, t, moreDataForCheckValid)
|
||||
// temp fix nodeMetaData is undefined
|
||||
const nodeMetaData = nodesExtraData?.[node.data.type]
|
||||
let { errorMessage } = nodeMetaData?.checkValid ? nodeMetaData.checkValid(checkData, t, moreDataForCheckValid) : { errorMessage: undefined }
|
||||
|
||||
if (!errorMessage) {
|
||||
const availableVars = map[node.id].availableVars
|
||||
|
||||
@ -36,6 +36,7 @@ export const useShortcuts = (): void => {
|
||||
const {
|
||||
handleModeHand,
|
||||
handleModePointer,
|
||||
handleModeComment,
|
||||
} = useWorkflowMoveMode()
|
||||
const { handleLayout } = useWorkflowOrganize()
|
||||
const { handleToggleMaximizeCanvas } = useWorkflowCanvasMaximize()
|
||||
@ -142,6 +143,16 @@ export const useShortcuts = (): void => {
|
||||
useCapture: true,
|
||||
})
|
||||
|
||||
useKeyPress('c', (e) => {
|
||||
if (shouldHandleShortcut(e)) {
|
||||
e.preventDefault()
|
||||
handleModeComment()
|
||||
}
|
||||
}, {
|
||||
exactMatch: true,
|
||||
useCapture: true,
|
||||
})
|
||||
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.o`, (e) => {
|
||||
if (shouldHandleShortcut(e)) {
|
||||
e.preventDefault()
|
||||
|
||||
@ -113,19 +113,16 @@ export const useWorkflowComment = () => {
|
||||
|
||||
await loadComments()
|
||||
setPendingComment(null)
|
||||
setControlMode(ControlMode.Pointer)
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to create comment:', error)
|
||||
setPendingComment(null)
|
||||
setControlMode(ControlMode.Pointer)
|
||||
}
|
||||
}, [appId, pendingComment, setControlMode, setPendingComment, loadComments, reactflow])
|
||||
}, [appId, pendingComment, setPendingComment, loadComments, reactflow])
|
||||
|
||||
const handleCommentCancel = useCallback(() => {
|
||||
setPendingComment(null)
|
||||
setControlMode(ControlMode.Pointer)
|
||||
}, [setControlMode, setPendingComment])
|
||||
}, [setPendingComment])
|
||||
|
||||
const handleCommentIconClick = useCallback(async (comment: WorkflowCommentList) => {
|
||||
setPendingComment(null)
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import {
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import { useReactFlow, useStoreApi } from 'reactflow'
|
||||
import { useReactFlow } from 'reactflow'
|
||||
import produce from 'immer'
|
||||
import { useStore, useWorkflowStore } from '../store'
|
||||
import {
|
||||
@ -28,6 +28,7 @@ import { useNodesInteractionsWithoutSync } from './use-nodes-interactions-withou
|
||||
import { useNodesSyncDraft } from './use-nodes-sync-draft'
|
||||
import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { useCollaborativeWorkflow } from '@/app/components/workflow/hooks/use-collaborative-workflow'
|
||||
|
||||
export const useWorkflowInteractions = () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
@ -70,31 +71,39 @@ export const useWorkflowMoveMode = () => {
|
||||
handleSelectionCancel()
|
||||
}, [getNodesReadOnly, setControlMode, handleSelectionCancel])
|
||||
|
||||
const handleModeComment = useCallback(() => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
setControlMode(ControlMode.Comment)
|
||||
handleSelectionCancel()
|
||||
}, [getNodesReadOnly, setControlMode, handleSelectionCancel])
|
||||
|
||||
return {
|
||||
handleModePointer,
|
||||
handleModeHand,
|
||||
handleModeComment,
|
||||
}
|
||||
}
|
||||
|
||||
export const useWorkflowOrganize = () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const store = useStoreApi()
|
||||
const reactflow = useReactFlow()
|
||||
const { getNodesReadOnly } = useNodesReadOnly()
|
||||
const { saveStateToHistory } = useWorkflowHistory()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const collaborativeWorkflow = useCollaborativeWorkflow()
|
||||
|
||||
const handleLayout = useCallback(async () => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
workflowStore.setState({ nodeAnimation: true })
|
||||
const {
|
||||
getNodes,
|
||||
nodes,
|
||||
edges,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
} = collaborativeWorkflow.getState()
|
||||
const { setViewport } = reactflow
|
||||
const nodes = getNodes()
|
||||
|
||||
const loopAndIterationNodes = nodes.filter(
|
||||
node => (node.data.type === BlockEnum.Loop || node.data.type === BlockEnum.Iteration)
|
||||
@ -239,7 +248,7 @@ export const useWorkflowOrganize = () => {
|
||||
setTimeout(() => {
|
||||
handleSyncWorkflowDraft()
|
||||
})
|
||||
}, [getNodesReadOnly, store, reactflow, workflowStore, handleSyncWorkflowDraft, saveStateToHistory])
|
||||
}, [getNodesReadOnly, collaborativeWorkflow, reactflow, workflowStore, handleSyncWorkflowDraft, saveStateToHistory])
|
||||
|
||||
return {
|
||||
handleLayout,
|
||||
|
||||
@ -6,7 +6,6 @@ import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
getIncomers,
|
||||
getOutgoers,
|
||||
useStoreApi,
|
||||
} from 'reactflow'
|
||||
import type {
|
||||
Connection,
|
||||
@ -35,6 +34,7 @@ import { CUSTOM_NOTE_NODE } from '../note-node/constants'
|
||||
import { findUsedVarNodes, getNodeOutputVars, updateNodeVars } from '../nodes/_base/components/variable/utils'
|
||||
import { useAvailableBlocks } from './use-available-blocks'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { useCollaborativeWorkflow } from '@/app/components/workflow/hooks/use-collaborative-workflow'
|
||||
import {
|
||||
fetchAllBuiltInTools,
|
||||
fetchAllCustomTools,
|
||||
@ -55,26 +55,19 @@ export const useIsChatMode = () => {
|
||||
|
||||
export const useWorkflow = () => {
|
||||
const { t } = useTranslation()
|
||||
const store = useStoreApi()
|
||||
const collaborativeWorkflow = useCollaborativeWorkflow()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const { getAvailableBlocks } = useAvailableBlocks()
|
||||
const { nodesMap } = useNodesMetaData()
|
||||
|
||||
const getNodeById = useCallback((nodeId: string) => {
|
||||
const {
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const { nodes } = collaborativeWorkflow.getState()
|
||||
const currentNode = nodes.find(node => node.id === nodeId)
|
||||
return currentNode
|
||||
}, [store])
|
||||
}, [collaborativeWorkflow])
|
||||
|
||||
const getTreeLeafNodes = useCallback((nodeId: string) => {
|
||||
const {
|
||||
getNodes,
|
||||
edges,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const { nodes, edges } = collaborativeWorkflow.getState()
|
||||
const currentNode = nodes.find(node => node.id === nodeId)
|
||||
|
||||
let startNodes = nodes.filter(node => nodesMap?.[node.data.type as BlockEnum]?.metaData.isStart) || []
|
||||
@ -117,14 +110,11 @@ export const useWorkflow = () => {
|
||||
return uniqBy(list, 'id').filter((item: Node) => {
|
||||
return SUPPORT_OUTPUT_VARS_NODE.includes(item.data.type)
|
||||
})
|
||||
}, [store, nodesMap])
|
||||
}, [collaborativeWorkflow, nodesMap])
|
||||
|
||||
const getBeforeNodesInSameBranch = useCallback((nodeId: string, newNodes?: Node[], newEdges?: Edge[]) => {
|
||||
const {
|
||||
getNodes,
|
||||
edges,
|
||||
} = store.getState()
|
||||
const nodes = newNodes || getNodes()
|
||||
const { nodes: oldNodes, edges } = collaborativeWorkflow.getState()
|
||||
const nodes = newNodes || oldNodes
|
||||
const currentNode = nodes.find(node => node.id === nodeId)
|
||||
|
||||
const list: Node[] = []
|
||||
@ -167,14 +157,11 @@ export const useWorkflow = () => {
|
||||
}
|
||||
|
||||
return []
|
||||
}, [store])
|
||||
}, [collaborativeWorkflow])
|
||||
|
||||
const getBeforeNodesInSameBranchIncludeParent = useCallback((nodeId: string, newNodes?: Node[], newEdges?: Edge[]) => {
|
||||
const nodes = getBeforeNodesInSameBranch(nodeId, newNodes, newEdges)
|
||||
const {
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
const allNodes = getNodes()
|
||||
const { nodes: allNodes } = collaborativeWorkflow.getState()
|
||||
const node = allNodes.find(n => n.id === nodeId)
|
||||
const parentNodeId = node?.parentId
|
||||
const parentNode = allNodes.find(n => n.id === parentNodeId)
|
||||
@ -182,14 +169,10 @@ export const useWorkflow = () => {
|
||||
nodes.push(parentNode)
|
||||
|
||||
return nodes
|
||||
}, [getBeforeNodesInSameBranch, store])
|
||||
}, [getBeforeNodesInSameBranch, collaborativeWorkflow])
|
||||
|
||||
const getAfterNodesInSameBranch = useCallback((nodeId: string) => {
|
||||
const {
|
||||
getNodes,
|
||||
edges,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const { nodes, edges } = collaborativeWorkflow.getState()
|
||||
const currentNode = nodes.find(node => node.id === nodeId)!
|
||||
|
||||
if (!currentNode)
|
||||
@ -213,40 +196,29 @@ export const useWorkflow = () => {
|
||||
})
|
||||
|
||||
return uniqBy(list, 'id')
|
||||
}, [store])
|
||||
}, [collaborativeWorkflow])
|
||||
|
||||
const getBeforeNodeById = useCallback((nodeId: string) => {
|
||||
const {
|
||||
getNodes,
|
||||
edges,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const { nodes, edges } = collaborativeWorkflow.getState()
|
||||
const node = nodes.find(node => node.id === nodeId)!
|
||||
|
||||
return getIncomers(node, nodes, edges)
|
||||
}, [store])
|
||||
}, [collaborativeWorkflow])
|
||||
|
||||
const getIterationNodeChildren = useCallback((nodeId: string) => {
|
||||
const {
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const { nodes } = collaborativeWorkflow.getState()
|
||||
|
||||
return nodes.filter(node => node.parentId === nodeId)
|
||||
}, [store])
|
||||
}, [collaborativeWorkflow])
|
||||
|
||||
const getLoopNodeChildren = useCallback((nodeId: string) => {
|
||||
const {
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const { nodes } = collaborativeWorkflow.getState()
|
||||
|
||||
return nodes.filter(node => node.parentId === nodeId)
|
||||
}, [store])
|
||||
}, [collaborativeWorkflow])
|
||||
|
||||
const handleOutVarRenameChange = useCallback((nodeId: string, oldValeSelector: ValueSelector, newVarSelector: ValueSelector) => {
|
||||
const { getNodes, setNodes } = store.getState()
|
||||
const allNodes = getNodes()
|
||||
const { nodes: allNodes, setNodes } = collaborativeWorkflow.getState()
|
||||
const affectedNodes = findUsedVarNodes(oldValeSelector, allNodes)
|
||||
if (affectedNodes.length > 0) {
|
||||
const newNodes = allNodes.map((node) => {
|
||||
@ -257,7 +229,7 @@ export const useWorkflow = () => {
|
||||
})
|
||||
setNodes(newNodes)
|
||||
}
|
||||
}, [store])
|
||||
}, [collaborativeWorkflow])
|
||||
|
||||
const isVarUsedInNodes = useCallback((varSelector: ValueSelector) => {
|
||||
const nodeId = varSelector[0]
|
||||
@ -268,11 +240,11 @@ export const useWorkflow = () => {
|
||||
|
||||
const removeUsedVarInNodes = useCallback((varSelector: ValueSelector) => {
|
||||
const nodeId = varSelector[0]
|
||||
const { getNodes, setNodes } = store.getState()
|
||||
const { nodes, setNodes } = collaborativeWorkflow.getState()
|
||||
const afterNodes = getAfterNodesInSameBranch(nodeId)
|
||||
const effectNodes = findUsedVarNodes(varSelector, afterNodes)
|
||||
if (effectNodes.length > 0) {
|
||||
const newNodes = getNodes().map((node) => {
|
||||
const newNodes = nodes.map((node) => {
|
||||
if (effectNodes.find(n => n.id === node.id))
|
||||
return updateNodeVars(node, varSelector, [])
|
||||
|
||||
@ -280,7 +252,7 @@ export const useWorkflow = () => {
|
||||
})
|
||||
setNodes(newNodes)
|
||||
}
|
||||
}, [getAfterNodesInSameBranch, store])
|
||||
}, [getAfterNodesInSameBranch, collaborativeWorkflow])
|
||||
|
||||
const isNodeVarsUsedInNodes = useCallback((node: Node, isChatMode: boolean) => {
|
||||
const outputVars = getNodeOutputVars(node, isChatMode)
|
||||
@ -291,9 +263,7 @@ export const useWorkflow = () => {
|
||||
}, [isVarUsedInNodes])
|
||||
|
||||
const checkParallelLimit = useCallback((nodeId: string, nodeHandle = 'source') => {
|
||||
const {
|
||||
edges,
|
||||
} = store.getState()
|
||||
const { edges } = collaborativeWorkflow.getState()
|
||||
const connectedEdges = edges.filter(edge => edge.source === nodeId && edge.sourceHandle === nodeHandle)
|
||||
if (connectedEdges.length > MAX_PARALLEL_LIMIT - 1) {
|
||||
const { setShowTips } = workflowStore.getState()
|
||||
@ -302,14 +272,10 @@ export const useWorkflow = () => {
|
||||
}
|
||||
|
||||
return true
|
||||
}, [store, workflowStore, t])
|
||||
}, [collaborativeWorkflow, workflowStore, t])
|
||||
|
||||
const getRootNodesById = useCallback((nodeId: string) => {
|
||||
const {
|
||||
getNodes,
|
||||
edges,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const { nodes, edges } = collaborativeWorkflow.getState()
|
||||
const currentNode = nodes.find(node => node.id === nodeId)
|
||||
|
||||
const rootNodes: Node[] = []
|
||||
@ -349,7 +315,7 @@ export const useWorkflow = () => {
|
||||
return uniqBy(rootNodes, 'id')
|
||||
|
||||
return []
|
||||
}, [store])
|
||||
}, [collaborativeWorkflow])
|
||||
|
||||
const getStartNodes = useCallback((nodes: Node[], currentNode?: Node) => {
|
||||
const { id, parentId } = currentNode || {}
|
||||
@ -402,11 +368,7 @@ export const useWorkflow = () => {
|
||||
}, [t, workflowStore, getStartNodes])
|
||||
|
||||
const isValidConnection = useCallback(({ source, sourceHandle, target }: Connection) => {
|
||||
const {
|
||||
edges,
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const { nodes, edges } = collaborativeWorkflow.getState()
|
||||
const sourceNode: Node = nodes.find(node => node.id === source)!
|
||||
const targetNode: Node = nodes.find(node => node.id === target)!
|
||||
|
||||
@ -445,7 +407,7 @@ export const useWorkflow = () => {
|
||||
}
|
||||
|
||||
return !hasCycle(targetNode)
|
||||
}, [store, checkParallelLimit, getAvailableBlocks])
|
||||
}, [collaborativeWorkflow, checkParallelLimit, getAvailableBlocks])
|
||||
|
||||
return {
|
||||
getNodeById,
|
||||
@ -550,13 +512,10 @@ export const useNodesReadOnly = () => {
|
||||
}
|
||||
|
||||
export const useIsNodeInIteration = (iterationId: string) => {
|
||||
const store = useStoreApi()
|
||||
const collaborativeWorkflow = useCollaborativeWorkflow()
|
||||
|
||||
const isNodeInIteration = useCallback((nodeId: string) => {
|
||||
const {
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const { nodes } = collaborativeWorkflow.getState()
|
||||
const node = nodes.find(node => node.id === nodeId)
|
||||
|
||||
if (!node)
|
||||
@ -566,20 +525,17 @@ export const useIsNodeInIteration = (iterationId: string) => {
|
||||
return true
|
||||
|
||||
return false
|
||||
}, [iterationId, store])
|
||||
}, [iterationId, collaborativeWorkflow])
|
||||
return {
|
||||
isNodeInIteration,
|
||||
}
|
||||
}
|
||||
|
||||
export const useIsNodeInLoop = (loopId: string) => {
|
||||
const store = useStoreApi()
|
||||
const collaborativeWorkflow = useCollaborativeWorkflow()
|
||||
|
||||
const isNodeInLoop = useCallback((nodeId: string) => {
|
||||
const {
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const { nodes } = collaborativeWorkflow.getState()
|
||||
const node = nodes.find(node => node.id === nodeId)
|
||||
|
||||
if (!node)
|
||||
@ -589,7 +545,7 @@ export const useIsNodeInLoop = (loopId: string) => {
|
||||
return true
|
||||
|
||||
return false
|
||||
}, [loopId, store])
|
||||
}, [loopId, collaborativeWorkflow])
|
||||
return {
|
||||
isNodeInLoop,
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import type { FC } from 'react'
|
||||
import {
|
||||
Fragment,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
@ -9,6 +10,7 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { setAutoFreeze } from 'immer'
|
||||
import {
|
||||
useEventListener,
|
||||
@ -76,6 +78,7 @@ import LimitTips from './limit-tips'
|
||||
import { setupScrollToNodeListener } from './utils/node-navigation'
|
||||
import { CommentCursor, CommentIcon, CommentInput, CommentThread } from './comment'
|
||||
import { useWorkflowComment } from './hooks/use-workflow-comment'
|
||||
import UserCursors from './collaboration/components/user-cursors'
|
||||
import {
|
||||
useStore,
|
||||
useWorkflowStore,
|
||||
@ -119,6 +122,9 @@ export type WorkflowProps = {
|
||||
viewport?: Viewport
|
||||
children?: React.ReactNode
|
||||
onWorkflowDataUpdate?: (v: any) => void
|
||||
cursors?: Record<string, any>
|
||||
myUserId?: string | null
|
||||
onlineUsers?: any[]
|
||||
}
|
||||
export const Workflow: FC<WorkflowProps> = memo(({
|
||||
nodes: originalNodes,
|
||||
@ -126,13 +132,14 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
||||
viewport,
|
||||
children,
|
||||
onWorkflowDataUpdate,
|
||||
cursors,
|
||||
myUserId,
|
||||
onlineUsers,
|
||||
}) => {
|
||||
const workflowContainerRef = useRef<HTMLDivElement>(null)
|
||||
const workflowStore = useWorkflowStore()
|
||||
const reactflow = useReactFlow()
|
||||
const [isMouseOverCanvas, setIsMouseOverCanvas] = useState(false)
|
||||
const [pendingDeleteCommentId, setPendingDeleteCommentId] = useState<string | null>(null)
|
||||
const [pendingDeleteReply, setPendingDeleteReply] = useState<{ commentId: string; replyId: string } | null>(null)
|
||||
const [nodes, setNodes] = useNodesState(originalNodes)
|
||||
const [edges, setEdges] = useEdgesState(originalEdges)
|
||||
const controlMode = useStore(s => s.controlMode)
|
||||
@ -193,7 +200,10 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
||||
handleCommentReplyUpdate,
|
||||
handleCommentReplyDelete,
|
||||
} = useWorkflowComment()
|
||||
const showUserComments = useStore(s => s.showUserComments)
|
||||
const showUserCursors = useStore(s => s.showUserCursors)
|
||||
const mousePosition = useStore(s => s.mousePosition)
|
||||
const { t } = useTranslation()
|
||||
|
||||
eventEmitter?.useSubscription((v: any) => {
|
||||
if (v.type === WORKFLOW_DATA_UPDATE) {
|
||||
@ -234,6 +244,33 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
||||
setTimeout(() => handleRefreshWorkflowDraft(), 500)
|
||||
}, [syncWorkflowDraftWhenPageClose, handleRefreshWorkflowDraft])
|
||||
|
||||
// Optimized comment deletion using showConfirm
|
||||
const handleCommentDeleteClick = useCallback((commentId: string) => {
|
||||
if (!showConfirm) {
|
||||
setShowConfirm({
|
||||
title: t('workflow.comments.confirm.deleteThreadTitle'),
|
||||
desc: t('workflow.comments.confirm.deleteThreadDesc'),
|
||||
onConfirm: async () => {
|
||||
await handleCommentDelete(commentId)
|
||||
setShowConfirm(undefined)
|
||||
},
|
||||
})
|
||||
}
|
||||
}, [showConfirm, setShowConfirm, handleCommentDelete, t])
|
||||
|
||||
const handleCommentReplyDeleteClick = useCallback((commentId: string, replyId: string) => {
|
||||
if (!showConfirm) {
|
||||
setShowConfirm({
|
||||
title: t('workflow.comments.confirm.deleteReplyTitle'),
|
||||
desc: t('workflow.comments.confirm.deleteReplyDesc'),
|
||||
onConfirm: async () => {
|
||||
await handleCommentReplyDelete(commentId, replyId)
|
||||
setShowConfirm(undefined)
|
||||
},
|
||||
})
|
||||
}
|
||||
}, [showConfirm, setShowConfirm, handleCommentReplyDelete, t])
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('visibilitychange', handleSyncWorkflowDraftWhenPageClose)
|
||||
|
||||
@ -403,30 +440,6 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
||||
content={showConfirm.desc}
|
||||
/>
|
||||
)}
|
||||
{pendingDeleteCommentId && (
|
||||
<Confirm
|
||||
isShow
|
||||
title='Delete this thread?'
|
||||
content='This action will permanently delete the thread and all its replies. This cannot be undone.'
|
||||
onCancel={() => setPendingDeleteCommentId(null)}
|
||||
onConfirm={async () => {
|
||||
await handleCommentDelete(pendingDeleteCommentId)
|
||||
setPendingDeleteCommentId(null)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{pendingDeleteReply && (
|
||||
<Confirm
|
||||
isShow
|
||||
title='Delete this reply?'
|
||||
content='This reply will be removed permanently.'
|
||||
onCancel={() => setPendingDeleteReply(null)}
|
||||
onConfirm={async () => {
|
||||
await handleCommentReplyDelete(pendingDeleteReply.commentId, pendingDeleteReply.replyId)
|
||||
setPendingDeleteReply(null)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<LimitTips />
|
||||
{controlMode === ControlMode.Comment && isMouseOverCanvas && (
|
||||
<CommentCursor mousePosition={mousePosition} />
|
||||
@ -445,31 +458,39 @@ 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}
|
||||
/>
|
||||
<Fragment key={comment.id}>
|
||||
<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={() => handleCommentDeleteClick(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 => handleCommentReplyDeleteClick(comment.id, replyId)}
|
||||
canGoPrev={canGoPrev}
|
||||
canGoNext={canGoNext}
|
||||
/>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
return (showUserComments || controlMode === ControlMode.Comment) ? (
|
||||
<CommentIcon
|
||||
key={comment.id}
|
||||
comment={comment}
|
||||
onClick={() => handleCommentIconClick(comment)}
|
||||
/>
|
||||
)
|
||||
) : null
|
||||
})}
|
||||
{children}
|
||||
<ReactFlow
|
||||
@ -523,6 +544,13 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
||||
className="bg-workflow-canvas-workflow-bg"
|
||||
color='var(--color-workflow-canvas-workflow-dot-color)'
|
||||
/>
|
||||
{showUserCursors && cursors && (
|
||||
<UserCursors
|
||||
cursors={cursors}
|
||||
myUserId={myUserId || null}
|
||||
onlineUsers={onlineUsers || []}
|
||||
/>
|
||||
)}
|
||||
</ReactFlow>
|
||||
</div>
|
||||
)
|
||||
@ -530,14 +558,25 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
||||
|
||||
type WorkflowWithInnerContextProps = WorkflowProps & {
|
||||
hooksStore?: Partial<HooksStoreShape>
|
||||
cursors?: Record<string, any>
|
||||
myUserId?: string | null
|
||||
onlineUsers?: any[]
|
||||
}
|
||||
export const WorkflowWithInnerContext = memo(({
|
||||
hooksStore,
|
||||
cursors,
|
||||
myUserId,
|
||||
onlineUsers,
|
||||
...restProps
|
||||
}: WorkflowWithInnerContextProps) => {
|
||||
return (
|
||||
<HooksStoreContextProvider {...hooksStore}>
|
||||
<Workflow {...restProps} />
|
||||
<Workflow
|
||||
{...restProps}
|
||||
cursors={cursors}
|
||||
myUserId={myUserId}
|
||||
onlineUsers={onlineUsers}
|
||||
/>
|
||||
</HooksStoreContextProvider>
|
||||
)
|
||||
})
|
||||
|
||||
@ -58,7 +58,7 @@ const Node: FC<NodeProps<IterationNodeType>> = ({
|
||||
)
|
||||
}
|
||||
{
|
||||
data._children!.length === 1 && (
|
||||
data._children?.length === 1 && (
|
||||
<AddBlock
|
||||
iterationNodeId={id}
|
||||
iterationNodeData={data}
|
||||
|
||||
@ -46,7 +46,7 @@ const Node: FC<NodeProps<LoopNodeType>> = ({
|
||||
)
|
||||
}
|
||||
{
|
||||
data._children!.length === 1 && (
|
||||
data._children?.length === 1 && (
|
||||
<AddBlock
|
||||
loopNodeId={id}
|
||||
loopNodeData={data}
|
||||
|
||||
@ -9,9 +9,9 @@ import {
|
||||
RiCursorLine,
|
||||
RiFunctionAddLine,
|
||||
RiHand,
|
||||
RiMessage3Line,
|
||||
RiStickyNoteAddLine,
|
||||
} from '@remixicon/react'
|
||||
import { Comment } from '@/app/components/base/icons/src/public/other'
|
||||
import {
|
||||
useNodesReadOnly,
|
||||
useWorkflowCanvasMaximize,
|
||||
@ -33,9 +33,9 @@ const Control = () => {
|
||||
const { t } = useTranslation()
|
||||
const controlMode = useStore(s => s.controlMode)
|
||||
const maximizeCanvas = useStore(s => s.maximizeCanvas)
|
||||
const { handleModePointer, handleModeHand } = useWorkflowMoveMode()
|
||||
const { handleModePointer, handleModeHand, handleModeComment } = useWorkflowMoveMode()
|
||||
const { handleLayout } = useWorkflowOrganize()
|
||||
const { handleAddNote, handleAddComment } = useOperator()
|
||||
const { handleAddNote } = useOperator()
|
||||
const {
|
||||
nodesReadOnly,
|
||||
getNodesReadOnly,
|
||||
@ -50,14 +50,6 @@ const Control = () => {
|
||||
handleAddNote()
|
||||
}
|
||||
|
||||
const addComment = (e: MouseEvent<HTMLDivElement>) => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
e.stopPropagation()
|
||||
handleAddComment()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='pointer-events-auto flex flex-col items-center rounded-lg border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 text-text-tertiary shadow-lg'>
|
||||
<AddBlock />
|
||||
@ -104,9 +96,9 @@ const Control = () => {
|
||||
controlMode === ControlMode.Comment ? 'bg-state-accent-active text-text-accent' : 'hover:bg-state-base-hover hover:text-text-secondary',
|
||||
`${nodesReadOnly && 'cursor-not-allowed text-text-disabled hover:bg-transparent hover:text-text-disabled'}`,
|
||||
)}
|
||||
onClick={addComment}
|
||||
onClick={handleModeComment}
|
||||
>
|
||||
<RiMessage3Line className='h-4 w-4' />
|
||||
<Comment className='h-4 w-4' />
|
||||
</div>
|
||||
</TipPopup>
|
||||
<Divider className='my-1 w-3.5' />
|
||||
|
||||
@ -5,7 +5,6 @@ import type { NoteNodeType } from '../note-node/types'
|
||||
import { CUSTOM_NOTE_NODE } from '../note-node/constants'
|
||||
import { NoteTheme } from '../note-node/types'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { ControlMode } from '../types'
|
||||
|
||||
export const useOperator = () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
@ -36,14 +35,7 @@ export const useOperator = () => {
|
||||
})
|
||||
}, [workflowStore, userProfile])
|
||||
|
||||
const handleAddComment = useCallback(() => {
|
||||
workflowStore.setState({
|
||||
controlMode: ControlMode.Comment,
|
||||
})
|
||||
}, [workflowStore])
|
||||
|
||||
return {
|
||||
handleAddNote,
|
||||
handleAddComment,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import { memo, useEffect, useMemo, useRef } from 'react'
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { MiniMap } from 'reactflow'
|
||||
import UndoRedo from '../header/undo-redo'
|
||||
import ZoomInOut from './zoom-in-out'
|
||||
import VariableTrigger from '../variable-inspect/trigger'
|
||||
import VariableInspectPanel from '../variable-inspect'
|
||||
import { useStore } from '../store'
|
||||
import { ControlMode } from '../types'
|
||||
|
||||
export type OperatorProps = {
|
||||
handleUndo: () => void
|
||||
@ -13,6 +14,26 @@ export type OperatorProps = {
|
||||
|
||||
const Operator = ({ handleUndo, handleRedo }: OperatorProps) => {
|
||||
const bottomPanelRef = useRef<HTMLDivElement>(null)
|
||||
const [showMiniMap, setShowMiniMap] = useState(true)
|
||||
const showUserCursors = useStore(s => s.showUserCursors)
|
||||
const setShowUserCursors = useStore(s => s.setShowUserCursors)
|
||||
const showUserComments = useStore(s => s.showUserComments)
|
||||
const setShowUserComments = useStore(s => s.setShowUserComments)
|
||||
const controlMode = useStore(s => s.controlMode)
|
||||
const isCommentMode = controlMode === ControlMode.Comment
|
||||
|
||||
const handleToggleMiniMap = useCallback(() => {
|
||||
setShowMiniMap(prev => !prev)
|
||||
}, [])
|
||||
|
||||
const handleToggleUserCursors = useCallback(() => {
|
||||
setShowUserCursors(!showUserCursors)
|
||||
}, [showUserCursors, setShowUserCursors])
|
||||
|
||||
const handleToggleUserComments = useCallback(() => {
|
||||
setShowUserComments(!showUserComments)
|
||||
}, [showUserComments, setShowUserComments])
|
||||
|
||||
const workflowCanvasWidth = useStore(s => s.workflowCanvasWidth)
|
||||
const rightPanelWidth = useStore(s => s.rightPanelWidth)
|
||||
const setBottomPanelWidth = useStore(s => s.setBottomPanelWidth)
|
||||
@ -57,18 +78,28 @@ const Operator = ({ handleUndo, handleRedo }: OperatorProps) => {
|
||||
</div>
|
||||
<VariableTrigger />
|
||||
<div className='relative'>
|
||||
<MiniMap
|
||||
pannable
|
||||
zoomable
|
||||
style={{
|
||||
width: 102,
|
||||
height: 72,
|
||||
}}
|
||||
maskColor='var(--color-workflow-minimap-bg)'
|
||||
className='!absolute !bottom-10 z-[9] !m-0 !h-[73px] !w-[103px] !rounded-lg !border-[0.5px]
|
||||
!border-divider-subtle !bg-background-default-subtle !shadow-md !shadow-shadow-shadow-5'
|
||||
{showMiniMap && (
|
||||
<MiniMap
|
||||
pannable
|
||||
zoomable
|
||||
style={{
|
||||
width: 102,
|
||||
height: 72,
|
||||
}}
|
||||
maskColor='var(--color-workflow-minimap-bg)'
|
||||
className='!absolute !bottom-10 z-[9] !m-0 !h-[73px] !w-[103px] !rounded-lg !border-[0.5px]
|
||||
!border-divider-subtle !bg-background-default-subtle !shadow-md !shadow-shadow-shadow-5'
|
||||
/>
|
||||
)}
|
||||
<ZoomInOut
|
||||
showMiniMap={showMiniMap}
|
||||
onToggleMiniMap={handleToggleMiniMap}
|
||||
showUserCursors={showUserCursors}
|
||||
onToggleUserCursors={handleToggleUserCursors}
|
||||
showUserComments={showUserComments}
|
||||
onToggleUserComments={handleToggleUserComments}
|
||||
isCommentMode={isCommentMode}
|
||||
/>
|
||||
<ZoomInOut />
|
||||
</div>
|
||||
</div>
|
||||
<VariableInspectPanel />
|
||||
|
||||
@ -6,6 +6,8 @@ import {
|
||||
useState,
|
||||
} from 'react'
|
||||
import {
|
||||
RiCheckLine,
|
||||
RiFullscreenLine,
|
||||
RiZoomInLine,
|
||||
RiZoomOutLine,
|
||||
} from '@remixicon/react'
|
||||
@ -38,9 +40,30 @@ enum ZoomType {
|
||||
zoomTo75 = 'zoomTo75',
|
||||
zoomTo100 = 'zoomTo100',
|
||||
zoomTo200 = 'zoomTo200',
|
||||
toggleUserComments = 'toggleUserComments',
|
||||
toggleUserCursors = 'toggleUserCursors',
|
||||
toggleMiniMap = 'toggleMiniMap',
|
||||
}
|
||||
|
||||
const ZoomInOut: FC = () => {
|
||||
type ZoomInOutProps = {
|
||||
showMiniMap?: boolean
|
||||
onToggleMiniMap?: () => void
|
||||
showUserCursors?: boolean
|
||||
onToggleUserCursors?: () => void
|
||||
showUserComments?: boolean
|
||||
onToggleUserComments?: () => void
|
||||
isCommentMode?: boolean
|
||||
}
|
||||
|
||||
const ZoomInOut: FC<ZoomInOutProps> = ({
|
||||
showMiniMap = true,
|
||||
onToggleMiniMap,
|
||||
showUserCursors = true,
|
||||
onToggleUserCursors,
|
||||
showUserComments = true,
|
||||
onToggleUserComments,
|
||||
isCommentMode = false,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
zoomIn,
|
||||
@ -78,13 +101,25 @@ const ZoomInOut: FC = () => {
|
||||
key: ZoomType.zoomTo25,
|
||||
text: '25%',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
key: ZoomType.zoomToFit,
|
||||
text: t('workflow.operator.zoomToFit'),
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
key: ZoomType.toggleUserComments,
|
||||
text: t('workflow.operator.showUserComments'),
|
||||
},
|
||||
{
|
||||
key: ZoomType.toggleUserCursors,
|
||||
text: t('workflow.operator.showUserCursors'),
|
||||
},
|
||||
{
|
||||
key: ZoomType.toggleMiniMap,
|
||||
text: t('workflow.operator.showMiniMap'),
|
||||
},
|
||||
],
|
||||
]
|
||||
|
||||
const handleZoom = (type: string) => {
|
||||
@ -109,6 +144,23 @@ const ZoomInOut: FC = () => {
|
||||
if (type === ZoomType.zoomTo200)
|
||||
zoomTo(2)
|
||||
|
||||
if (type === ZoomType.toggleUserComments) {
|
||||
if (!isCommentMode)
|
||||
onToggleUserComments?.()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (type === ZoomType.toggleUserCursors) {
|
||||
onToggleUserCursors?.()
|
||||
return
|
||||
}
|
||||
|
||||
if (type === ZoomType.toggleMiniMap) {
|
||||
onToggleMiniMap?.()
|
||||
return
|
||||
}
|
||||
|
||||
handleSyncWorkflowDraft()
|
||||
}
|
||||
|
||||
@ -178,7 +230,7 @@ const ZoomInOut: FC = () => {
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-10'>
|
||||
<div className='w-[145px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-[5px]'>
|
||||
<div className='w-[192px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-[5px]'>
|
||||
{
|
||||
ZOOM_IN_OUT_OPTIONS.map((options, i) => (
|
||||
<Fragment key={i}>
|
||||
@ -192,10 +244,43 @@ const ZoomInOut: FC = () => {
|
||||
options.map(option => (
|
||||
<div
|
||||
key={option.key}
|
||||
className='system-md-regular flex h-8 cursor-pointer items-center justify-between space-x-1 rounded-lg py-1.5 pl-3 pr-2 text-text-secondary hover:bg-state-base-hover'
|
||||
className={`system-md-regular flex h-8 cursor-pointer items-center justify-between space-x-1 rounded-lg px-2 py-1.5 text-text-secondary hover:bg-state-base-hover ${
|
||||
option.key === ZoomType.toggleUserComments && isCommentMode
|
||||
? 'cursor-not-allowed opacity-50'
|
||||
: ''
|
||||
}`}
|
||||
onClick={() => handleZoom(option.key)}
|
||||
>
|
||||
<span>{option.text}</span>
|
||||
<div className='flex items-center space-x-2'>
|
||||
{option.key === ZoomType.toggleUserComments && showUserComments && (
|
||||
<RiCheckLine className='h-4 w-4 text-text-accent' />
|
||||
)}
|
||||
{option.key === ZoomType.toggleUserComments && !showUserComments && (
|
||||
<div className='h-4 w-4' />
|
||||
)}
|
||||
{option.key === ZoomType.toggleUserCursors && showUserCursors && (
|
||||
<RiCheckLine className='h-4 w-4 text-text-accent' />
|
||||
)}
|
||||
{option.key === ZoomType.toggleUserCursors && !showUserCursors && (
|
||||
<div className='h-4 w-4' />
|
||||
)}
|
||||
{option.key === ZoomType.toggleMiniMap && showMiniMap && (
|
||||
<RiCheckLine className='h-4 w-4 text-text-accent' />
|
||||
)}
|
||||
{option.key === ZoomType.toggleMiniMap && !showMiniMap && (
|
||||
<div className='h-4 w-4' />
|
||||
)}
|
||||
{option.key === ZoomType.zoomToFit && (
|
||||
<RiFullscreenLine className='h-4 w-4 text-text-tertiary' />
|
||||
)}
|
||||
{option.key !== ZoomType.toggleUserComments
|
||||
&& option.key !== ZoomType.toggleUserCursors
|
||||
&& option.key !== ZoomType.toggleMiniMap
|
||||
&& option.key !== ZoomType.zoomToFit && (
|
||||
<div className='h-4 w-4' />
|
||||
)}
|
||||
<span>{option.text}</span>
|
||||
</div>
|
||||
<div className='flex items-center space-x-0.5'>
|
||||
{
|
||||
option.key === ZoomType.zoomToFit && (
|
||||
|
||||
@ -3,9 +3,6 @@ import {
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {
|
||||
useStoreApi,
|
||||
} from 'reactflow'
|
||||
import { RiBookOpenLine, RiCloseLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
@ -25,15 +22,15 @@ import { useDocLink } from '@/context/i18n'
|
||||
import cn from '@/utils/classnames'
|
||||
import useInspectVarsCrud from '../../hooks/use-inspect-vars-crud'
|
||||
import { updateConversationVariables } from '@/service/workflow'
|
||||
import { useCollaborativeWorkflow } from '@/app/components/workflow/hooks/use-collaborative-workflow'
|
||||
|
||||
const ChatVariablePanel = () => {
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const store = useStoreApi()
|
||||
const setShowChatVariablePanel = useStore(s => s.setShowChatVariablePanel)
|
||||
const varList = useStore(s => s.conversationVariables) as ConversationVariable[]
|
||||
const updateChatVarList = useStore(s => s.setConversationVariables)
|
||||
const appId = useStore(s => s.appId)
|
||||
const appId = useStore(s => s.appId) as string
|
||||
const {
|
||||
invalidateConversationVarValues,
|
||||
} = useInspectVarsCrud()
|
||||
@ -44,27 +41,27 @@ const ChatVariablePanel = () => {
|
||||
|
||||
const [showRemoveVarConfirm, setShowRemoveConfirm] = useState(false)
|
||||
const [cacheForDelete, setCacheForDelete] = useState<ConversationVariable>()
|
||||
const collaborativeWorkflow = useCollaborativeWorkflow()
|
||||
|
||||
const getEffectedNodes = useCallback((chatVar: ConversationVariable) => {
|
||||
const { getNodes } = store.getState()
|
||||
const allNodes = getNodes()
|
||||
const { nodes: allNodes } = collaborativeWorkflow.getState()
|
||||
return findUsedVarNodes(
|
||||
['conversation', chatVar.name],
|
||||
allNodes,
|
||||
)
|
||||
}, [store])
|
||||
}, [collaborativeWorkflow])
|
||||
|
||||
const removeUsedVarInNodes = useCallback((chatVar: ConversationVariable) => {
|
||||
const { getNodes, setNodes } = store.getState()
|
||||
const { nodes, setNodes } = collaborativeWorkflow.getState()
|
||||
const effectedNodes = getEffectedNodes(chatVar)
|
||||
const newNodes = getNodes().map((node) => {
|
||||
const newNodes = nodes.map((node) => {
|
||||
if (effectedNodes.find(n => n.id === node.id))
|
||||
return updateNodeVars(node, ['conversation', chatVar.name], [])
|
||||
|
||||
return node
|
||||
})
|
||||
setNodes(newNodes)
|
||||
}, [getEffectedNodes, store])
|
||||
}, [getEffectedNodes, collaborativeWorkflow])
|
||||
|
||||
const handleEdit = (chatVar: ConversationVariable) => {
|
||||
setCurrentVar(chatVar)
|
||||
@ -151,9 +148,9 @@ const ChatVariablePanel = () => {
|
||||
|
||||
// side effects of rename conversation variable
|
||||
if (currentVar.name !== chatVar.name) {
|
||||
const { getNodes, setNodes } = store.getState()
|
||||
const { nodes, setNodes } = collaborativeWorkflow.getState()
|
||||
const effectedNodes = getEffectedNodes(currentVar)
|
||||
const newNodes = getNodes().map((node) => {
|
||||
const newNodes = nodes.map((node) => {
|
||||
if (effectedNodes.find(n => n.id === node.id))
|
||||
return updateNodeVars(node, ['conversation', currentVar.name], ['conversation', chatVar.name])
|
||||
|
||||
@ -183,7 +180,7 @@ const ChatVariablePanel = () => {
|
||||
// Revert local state on error
|
||||
updateChatVarList(varList)
|
||||
}
|
||||
}, [currentVar, getEffectedNodes, store, updateChatVarList, varList, appId, invalidateConversationVarValues])
|
||||
}, [currentVar, getEffectedNodes, collaborativeWorkflow, updateChatVarList, varList, appId, invalidateConversationVarValues])
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@ -63,10 +63,10 @@ const CommentsPanel = () => {
|
||||
return (
|
||||
<div className={cn('relative flex h-full w-[420px] flex-col rounded-l-2xl border border-components-panel-border bg-components-panel-bg')}>
|
||||
<div className='flex items-center justify-between p-4 pb-2'>
|
||||
<div className='system-xl-semibold text-text-primary'>Comments</div>
|
||||
<div className='system-xl-semibold font-semibold leading-6 text-text-primary'>Comments</div>
|
||||
<div className='relative flex items-center gap-2'>
|
||||
<button
|
||||
className='flex h-8 w-8 items-center justify-center rounded-md bg-white hover:bg-state-base-hover'
|
||||
className='flex h-8 w-8 items-center justify-center rounded-md bg-components-panel-on-panel-item-bg hover:bg-state-base-hover'
|
||||
aria-label='Filter comments'
|
||||
onClick={() => setShowFilter(v => !v)}
|
||||
>
|
||||
@ -78,21 +78,21 @@ const CommentsPanel = () => {
|
||||
className={cn('flex w-full items-center justify-between rounded-md px-2 py-2 text-left text-sm hover:bg-state-base-hover', filter === 'all' && 'bg-components-panel-on-panel-item-bg')}
|
||||
onClick={() => handleFilterChange('all')}
|
||||
>
|
||||
<span>All</span>
|
||||
<span className='text-text-secondary'>All</span>
|
||||
{filter === 'all' && <RiCheckLine className='h-4 w-4 text-text-secondary' />}
|
||||
</button>
|
||||
<button
|
||||
className={cn('mt-1 flex w-full items-center justify-between rounded-md px-2 py-2 text-left text-sm hover:bg-state-base-hover', filter === 'unresolved' && 'bg-components-panel-on-panel-item-bg')}
|
||||
onClick={() => handleFilterChange('unresolved')}
|
||||
>
|
||||
<span>Unresolved</span>
|
||||
<span className='text-text-secondary'>Unresolved</span>
|
||||
{filter === 'unresolved' && <RiCheckLine className='h-4 w-4 text-text-secondary' />}
|
||||
</button>
|
||||
<button
|
||||
className={cn('mt-1 flex w-full items-center justify-between rounded-md px-2 py-2 text-left text-sm hover:bg-state-base-hover', filter === 'mine' && 'bg-components-panel-on-panel-item-bg')}
|
||||
onClick={() => handleFilterChange('mine')}
|
||||
>
|
||||
<span>Only your threads</span>
|
||||
<span className='text-text-secondary'>Only your threads</span>
|
||||
{filter === 'mine' && <RiCheckLine className='h-4 w-4 text-text-secondary' />}
|
||||
</button>
|
||||
</div>
|
||||
@ -130,7 +130,11 @@ const CommentsPanel = () => {
|
||||
) : (
|
||||
<RiCheckboxCircleLine
|
||||
className='h-4 w-4 cursor-pointer text-text-tertiary hover:text-text-secondary'
|
||||
onClick={() => handleResolve(c)}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
handleResolve(c)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@ -147,11 +151,13 @@ const CommentsPanel = () => {
|
||||
{/* Content */}
|
||||
<div className='system-sm-regular mt-1 line-clamp-3 break-words text-text-secondary'>{c.content}</div>
|
||||
{/* Footer */}
|
||||
<div className='mt-2 flex items-center justify-between'>
|
||||
<div className='system-2xs-regular text-text-tertiary'>
|
||||
{c.reply_count} replies
|
||||
{c.reply_count > 0 && (
|
||||
<div className='mt-2 flex items-center justify-between'>
|
||||
<div className='system-2xs-regular text-text-tertiary'>
|
||||
{c.reply_count} replies
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -3,9 +3,6 @@ import {
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {
|
||||
useStoreApi,
|
||||
} from 'reactflow'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
@ -20,16 +17,17 @@ import cn from '@/utils/classnames'
|
||||
import { webSocketClient } from '@/app/components/workflow/collaboration/core/websocket-manager'
|
||||
import { useStore as useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { updateEnvironmentVariables } from '@/service/workflow'
|
||||
import { useCollaborativeWorkflow } from '@/app/components/workflow/hooks/use-collaborative-workflow'
|
||||
|
||||
const EnvPanel = () => {
|
||||
const { t } = useTranslation()
|
||||
const store = useStoreApi()
|
||||
const collaborativeWorkflow = useCollaborativeWorkflow()
|
||||
const setShowEnvPanel = useStore(s => s.setShowEnvPanel)
|
||||
const envList = useStore(s => s.environmentVariables) as EnvironmentVariable[]
|
||||
const envSecrets = useStore(s => s.envSecrets)
|
||||
const updateEnvList = useStore(s => s.setEnvironmentVariables)
|
||||
const setEnvSecrets = useStore(s => s.setEnvSecrets)
|
||||
const appId = useWorkflowStore(s => s.appId)
|
||||
const appId = useWorkflowStore(s => s.appId) as string
|
||||
|
||||
const [showVariableModal, setShowVariableModal] = useState(false)
|
||||
const [currentVar, setCurrentVar] = useState<EnvironmentVariable>()
|
||||
@ -42,25 +40,24 @@ const EnvPanel = () => {
|
||||
}
|
||||
|
||||
const getEffectedNodes = useCallback((env: EnvironmentVariable) => {
|
||||
const { getNodes } = store.getState()
|
||||
const allNodes = getNodes()
|
||||
const { nodes: allNodes } = collaborativeWorkflow.getState()
|
||||
return findUsedVarNodes(
|
||||
['env', env.name],
|
||||
allNodes,
|
||||
)
|
||||
}, [store])
|
||||
}, [collaborativeWorkflow])
|
||||
|
||||
const removeUsedVarInNodes = useCallback((env: EnvironmentVariable) => {
|
||||
const { getNodes, setNodes } = store.getState()
|
||||
const { nodes, setNodes } = collaborativeWorkflow.getState()
|
||||
const effectedNodes = getEffectedNodes(env)
|
||||
const newNodes = getNodes().map((node) => {
|
||||
const newNodes = nodes.map((node) => {
|
||||
if (effectedNodes.find(n => n.id === node.id))
|
||||
return updateNodeVars(node, ['env', env.name], [])
|
||||
|
||||
return node
|
||||
})
|
||||
setNodes(newNodes)
|
||||
}, [getEffectedNodes, store])
|
||||
}, [getEffectedNodes, collaborativeWorkflow])
|
||||
|
||||
const handleEdit = (env: EnvironmentVariable) => {
|
||||
setCurrentVar(env)
|
||||
@ -185,9 +182,9 @@ const EnvPanel = () => {
|
||||
|
||||
// side effects of rename env
|
||||
if (currentVar.name !== env.name) {
|
||||
const { getNodes, setNodes } = store.getState()
|
||||
const { nodes, setNodes } = collaborativeWorkflow.getState()
|
||||
const effectedNodes = getEffectedNodes(currentVar)
|
||||
const newNodes = getNodes().map((node) => {
|
||||
const newNodes = nodes.map((node) => {
|
||||
if (effectedNodes.find(n => n.id === node.id))
|
||||
return updateNodeVars(node, ['env', currentVar.name], ['env', env.name])
|
||||
|
||||
@ -218,7 +215,7 @@ const EnvPanel = () => {
|
||||
// Revert local state on error
|
||||
updateEnvList(envList)
|
||||
}
|
||||
}, [currentVar, envList, envSecrets, getEffectedNodes, setEnvSecrets, store, updateEnvList, appId])
|
||||
}, [currentVar, envList, envSecrets, getEffectedNodes, setEnvSecrets, collaborativeWorkflow, updateEnvList, appId])
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@ -7,7 +7,7 @@ import {
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useClickAway } from 'ahooks'
|
||||
import { useStore as useReactFlowStore, useStoreApi } from 'reactflow'
|
||||
import { useStore as useReactFlowStore } from 'reactflow'
|
||||
import {
|
||||
RiAlignBottom,
|
||||
RiAlignCenter,
|
||||
@ -22,6 +22,7 @@ import { WorkflowHistoryEvent, useWorkflowHistory } from './hooks/use-workflow-h
|
||||
import { useStore } from './store'
|
||||
import { useSelectionInteractions } from './hooks/use-selection-interactions'
|
||||
import { useWorkflowStore } from './store'
|
||||
import { useCollaborativeWorkflow } from '@/app/components/workflow/hooks/use-collaborative-workflow'
|
||||
|
||||
enum AlignType {
|
||||
Left = 'left',
|
||||
@ -42,8 +43,8 @@ const SelectionContextmenu = () => {
|
||||
const selectionMenu = useStore(s => s.selectionMenu)
|
||||
|
||||
// Access React Flow methods
|
||||
const store = useStoreApi()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const collaborativeWorkflow = useCollaborativeWorkflow()
|
||||
|
||||
// Get selected nodes for alignment logic
|
||||
const selectedNodes = useReactFlowStore(state =>
|
||||
@ -256,7 +257,7 @@ const SelectionContextmenu = () => {
|
||||
workflowStore.setState({ nodeAnimation: false })
|
||||
|
||||
// Get all current nodes
|
||||
const nodes = store.getState().getNodes()
|
||||
const { nodes, setNodes } = collaborativeWorkflow.getState()
|
||||
|
||||
// Get all selected nodes
|
||||
const selectedNodeIds = selectedNodes.map(node => node.id)
|
||||
@ -312,7 +313,7 @@ const SelectionContextmenu = () => {
|
||||
const distributeNodes = handleDistributeNodes(nodesToAlign, nodes, alignType)
|
||||
if (distributeNodes) {
|
||||
// Apply node distribution updates
|
||||
store.getState().setNodes(distributeNodes)
|
||||
setNodes(distributeNodes)
|
||||
handleSelectionContextmenuCancel()
|
||||
|
||||
// Clear guide lines
|
||||
@ -347,7 +348,7 @@ const SelectionContextmenu = () => {
|
||||
// Apply node position updates - consistent with handleNodeDrag and handleNodeDragStop
|
||||
try {
|
||||
// Directly use setNodes to update nodes - consistent with handleNodeDrag
|
||||
store.getState().setNodes(newNodes)
|
||||
setNodes(newNodes)
|
||||
|
||||
// Close popup
|
||||
handleSelectionContextmenuCancel()
|
||||
@ -366,7 +367,7 @@ const SelectionContextmenu = () => {
|
||||
catch (err) {
|
||||
console.error('Failed to update nodes:', err)
|
||||
}
|
||||
}, [store, workflowStore, selectedNodes, getNodesReadOnly, handleSyncWorkflowDraft, saveStateToHistory, handleSelectionContextmenuCancel, handleAlignNode, handleDistributeNodes])
|
||||
}, [collaborativeWorkflow, workflowStore, selectedNodes, getNodesReadOnly, handleSyncWorkflowDraft, saveStateToHistory, handleSelectionContextmenuCancel, handleAlignNode, handleDistributeNodes])
|
||||
|
||||
if (!selectionMenu)
|
||||
return null
|
||||
|
||||
@ -12,6 +12,10 @@ export type PanelSliceShape = {
|
||||
setShowDebugAndPreviewPanel: (showDebugAndPreviewPanel: boolean) => void
|
||||
showCommentsPanel: boolean
|
||||
setShowCommentsPanel: (showCommentsPanel: boolean) => void
|
||||
showUserComments: boolean
|
||||
setShowUserComments: (showUserComments: boolean) => void
|
||||
showUserCursors: boolean
|
||||
setShowUserCursors: (showUserCursors: boolean) => void
|
||||
panelMenu?: {
|
||||
top: number
|
||||
left: number
|
||||
@ -42,6 +46,10 @@ export const createPanelSlice: StateCreator<PanelSliceShape> = set => ({
|
||||
setShowDebugAndPreviewPanel: showDebugAndPreviewPanel => set(() => ({ showDebugAndPreviewPanel })),
|
||||
showCommentsPanel: false,
|
||||
setShowCommentsPanel: showCommentsPanel => set(() => ({ showCommentsPanel })),
|
||||
showUserComments: true,
|
||||
setShowUserComments: showUserComments => set(() => ({ showUserComments })),
|
||||
showUserCursors: true,
|
||||
setShowUserCursors: showUserCursors => set(() => ({ showUserCursors })),
|
||||
panelMenu: undefined,
|
||||
setPanelMenu: panelMenu => set(() => ({ panelMenu })),
|
||||
selectionMenu: undefined,
|
||||
|
||||
Reference in New Issue
Block a user