fix: web style

This commit is contained in:
hjlarry
2026-04-09 15:20:00 +08:00
parent 6cce56f028
commit c3c84419e7
21 changed files with 343 additions and 320 deletions

View File

@ -1,4 +1,3 @@
/* eslint-disable ts/no-explicit-any */
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import AccessControlDialog from '../access-control-dialog'

View File

@ -1,4 +1,3 @@
/* eslint-disable ts/no-explicit-any */
import type { App } from '@/types/app'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { toast } from '@/app/components/base/ui/toast'

View File

@ -73,7 +73,6 @@ const ImageInput: FC<UploaderProps> = ({
const handleShowImage = () => {
if (isAnimatedImage) {
return (
// eslint-disable-next-line next/no-img-element
<img src={inputImage?.url} alt="" data-testid="animated-image" />
)
}

View File

@ -1,4 +1,3 @@
/* eslint-disable next/no-img-element */
import type { FC } from 'react'
import type { ImageFile } from '@/types/app'
import { useState } from 'react'

View File

@ -1,4 +1,3 @@
/* eslint-disable next/no-img-element */
import type { ExtraProps } from 'streamdown'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'

View File

@ -208,10 +208,10 @@ export const CommentIcon: FC<CommentIconProps> = memo(({ comment, onClick, isAct
onMouseLeave={handleMouseLeave}
>
<div
className="relative h-10 rounded-br-full rounded-tl-full rounded-tr-full"
className="relative h-10 rounded-tl-full rounded-tr-full rounded-br-full"
style={{ width: dynamicWidth }}
>
<div className={`absolute inset-[6px] overflow-hidden rounded-br-full rounded-tl-full rounded-tr-full border bg-components-panel-bg transition-shadow ${
<div className={`absolute inset-[6px] overflow-hidden rounded-tl-full rounded-tr-full rounded-br-full border bg-components-panel-bg transition-shadow ${
isActive
? 'border-primary-500 ring-1 ring-primary-500'
: 'border-components-panel-border'

View File

@ -136,8 +136,8 @@ export const CommentInput: FC<CommentInputProps> = memo(({ position, onSubmit, o
className="relative shrink-0 cursor-move"
onPointerDown={handleDragPointerDown}
>
<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-components-panel-bg-blur">
<div className="relative h-8 w-8 overflow-hidden rounded-tl-full rounded-tr-full rounded-br-full bg-primary-500">
<div className="absolute inset-[2px] overflow-hidden rounded-tl-full rounded-tr-full rounded-br-full bg-components-panel-bg-blur">
<div className="flex h-full w-full items-center justify-center">
<div className="h-6 w-6 overflow-hidden rounded-full">
<Avatar
@ -156,7 +156,7 @@ export const CommentInput: FC<CommentInputProps> = memo(({ position, onSubmit, o
'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 pl-[9px] pt-[4px]">
<div className="relative pt-[4px] pl-[9px]">
<MentionInput
value={content}
onChange={setContent}

View File

@ -44,8 +44,8 @@ const CommentPreview: FC<CommentPreviewProps> = ({ comment, onClick }) => {
<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">
<div className="truncate system-sm-medium text-text-primary">{comment.created_by_account.name}</div>
<div className="shrink-0 system-2xs-regular text-text-tertiary">
{formatTimeFromNow(comment.updated_at * 1000)}
</div>
</div>

View File

@ -1,5 +0,0 @@
export { CommentIcon } from './comment-icon'
export { CommentInput } from './comment-input'
export { CommentCursor } from './cursor'
export { MentionInput } from './mention-input'
export { CommentThread } from './thread'

View File

@ -3,7 +3,6 @@
import type { ReactNode } from 'react'
import type { UserProfile } from '@/service/workflow-comment'
import { RiArrowUpLine, RiAtLine, RiLoader2Line } from '@remixicon/react'
import { useParams } from 'next/navigation'
import {
forwardRef,
memo,
@ -21,6 +20,7 @@ import Textarea from 'react-textarea-autosize'
import Avatar from '@/app/components/base/avatar'
import Button from '@/app/components/base/button'
import EnterKey from '@/app/components/base/icons/src/public/common/EnterKey'
import { useParams } from '@/next/navigation'
import { fetchMentionableUsers } from '@/service/workflow-comment'
import { cn } from '@/utils/classnames'
import { useStore, useWorkflowStore } from '../store'
@ -511,7 +511,7 @@ const MentionInputInner = forwardRef<HTMLTextAreaElement, MentionInputProps>(({
<div
aria-hidden
className={cn(
'pointer-events-none absolute inset-0 z-0 overflow-hidden whitespace-pre-wrap break-words p-1 leading-6',
'inset-0 pointer-events-none absolute z-0 overflow-hidden p-1 leading-6 break-words whitespace-pre-wrap',
'body-lg-regular text-text-primary',
)}
style={{ paddingRight, paddingBottom }}
@ -528,7 +528,7 @@ const MentionInputInner = forwardRef<HTMLTextAreaElement, MentionInputProps>(({
<Textarea
ref={textareaRef}
className={cn(
'body-lg-regular relative z-10 w-full resize-none bg-transparent p-1 leading-6 text-transparent caret-primary-500 outline-none',
'relative z-10 w-full resize-none bg-transparent p-1 body-lg-regular leading-6 text-transparent caret-primary-500 outline-none',
'placeholder:text-text-tertiary',
)}
style={{ paddingRight, paddingBottom }}
@ -546,7 +546,7 @@ const MentionInputInner = forwardRef<HTMLTextAreaElement, MentionInputProps>(({
{!isEditing && (
<div
ref={setActionContainerRef}
className="absolute bottom-0 right-1 z-20 flex items-end gap-1"
className="absolute right-1 bottom-0 z-20 flex items-end gap-1"
>
<div
className={cn(
@ -575,7 +575,7 @@ const MentionInputInner = forwardRef<HTMLTextAreaElement, MentionInputProps>(({
{isEditing && (
<div
ref={setActionContainerRef}
className="absolute bottom-0 left-1 right-1 z-20 flex items-end justify-between"
className="absolute right-1 bottom-0 left-1 z-20 flex items-end justify-between"
>
<div
className={cn(
@ -615,7 +615,7 @@ const MentionInputInner = forwardRef<HTMLTextAreaElement, MentionInputProps>(({
{showMentionDropdown && filteredMentionUsers.length > 0 && typeof document !== 'undefined' && createPortal(
<div
className="bg-components-panel-bg/95 fixed z-[9999] max-h-[248px] w-[280px] overflow-y-auto rounded-xl border-[0.5px] border-components-panel-border shadow-lg backdrop-blur-[10px]"
className="fixed z-[9999] max-h-[248px] w-[280px] overflow-y-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg/95 shadow-lg backdrop-blur-[10px]"
style={{
left: dropdownPosition.x,
[dropdownPosition.placement === 'top' ? 'bottom' : 'top']: dropdownPosition.placement === 'top'
@ -628,7 +628,7 @@ const MentionInputInner = forwardRef<HTMLTextAreaElement, MentionInputProps>(({
<div
key={user.id}
className={cn(
'flex cursor-pointer items-center gap-2 rounded-md py-1 pl-2 pr-3 hover:bg-state-base-hover',
'flex cursor-pointer items-center gap-2 rounded-md py-1 pr-3 pl-2 hover:bg-state-base-hover',
index === selectedMentionIndex && 'bg-state-base-hover',
)}
onClick={() => insertMention(user)}

View File

@ -3,18 +3,22 @@
import type { FC, ReactNode } from 'react'
import type { WorkflowCommentDetail, WorkflowCommentDetailReply } from '@/service/workflow-comment'
import { RiArrowDownSLine, RiArrowUpSLine, RiCheckboxCircleFill, RiCheckboxCircleLine, RiCloseLine, RiDeleteBinLine, RiMoreFill } from '@remixicon/react'
import { useParams } from 'next/navigation'
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useReactFlow, useViewport } from 'reactflow'
import Avatar from '@/app/components/base/avatar'
import Divider from '@/app/components/base/divider'
import InlineDeleteConfirm from '@/app/components/base/inline-delete-confirm'
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
import Tooltip from '@/app/components/base/tooltip'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from '@/app/components/base/ui/dropdown-menu'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/app/components/base/ui/tooltip'
import { getUserColor } from '@/app/components/workflow/collaboration/utils/user-color'
import { useAppContext } from '@/context/app-context'
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
import { useParams } from '@/next/navigation'
import { cn } from '@/utils/classnames'
import { useStore } from '../store'
import { MentionInput } from './mention-input'
@ -133,7 +137,7 @@ const ThreadMessage: FC<{
<span className="system-sm-medium text-text-primary">{authorName}</span>
<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">
<div className="mt-1 system-sm-regular break-words whitespace-pre-wrap text-text-secondary">
{highlightedContent}
</div>
</div>
@ -369,71 +373,75 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
<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
id="comment-thread-title"
className="font-semibold uppercase text-text-primary"
className="font-semibold text-text-primary uppercase"
>
{t('comments.panelTitle', { ns: 'workflow' })}
</div>
<div className="flex items-center gap-1">
<Tooltip
popupContent={t('comments.aria.deleteComment', { ns: 'workflow' })}
position="top"
popupClassName="!px-2 !py-1.5"
>
<button
type="button"
disabled={loading}
className={cn('flex h-6 w-6 items-center justify-center rounded-lg text-text-tertiary hover:bg-state-destructive-hover hover:text-text-destructive disabled:cursor-not-allowed disabled:text-text-disabled disabled:hover:bg-transparent disabled:hover:text-text-disabled')}
onClick={onDelete}
aria-label={t('comments.aria.deleteComment', { ns: 'workflow' })}
>
<RiDeleteBinLine className="h-4 w-4" />
</button>
<Tooltip>
<TooltipTrigger>
<button
type="button"
disabled={loading}
className={cn('flex h-6 w-6 items-center justify-center rounded-lg text-text-tertiary hover:bg-state-destructive-hover hover:text-text-destructive disabled:cursor-not-allowed disabled:text-text-disabled disabled:hover:bg-transparent disabled:hover:text-text-disabled')}
onClick={onDelete}
aria-label={t('comments.aria.deleteComment', { ns: 'workflow' })}
>
<RiDeleteBinLine className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent placement="top" popupClassName="!px-2 !py-1.5">
{t('comments.aria.deleteComment', { ns: 'workflow' })}
</TooltipContent>
</Tooltip>
<Tooltip
popupContent={t('comments.aria.resolveComment', { ns: 'workflow' })}
position="top"
popupClassName="!px-2 !py-1.5"
>
<button
type="button"
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={t('comments.aria.resolveComment', { ns: 'workflow' })}
>
{comment.resolved ? <RiCheckboxCircleFill className="h-4 w-4" /> : <RiCheckboxCircleLine className="h-4 w-4" />}
</button>
<Tooltip>
<TooltipTrigger>
<button
type="button"
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={t('comments.aria.resolveComment', { ns: 'workflow' })}
>
{comment.resolved ? <RiCheckboxCircleFill className="h-4 w-4" /> : <RiCheckboxCircleLine className="h-4 w-4" />}
</button>
</TooltipTrigger>
<TooltipContent placement="top" popupClassName="!px-2 !py-1.5">
{t('comments.aria.resolveComment', { ns: 'workflow' })}
</TooltipContent>
</Tooltip>
<Divider type="vertical" className="h-3.5" />
<Tooltip
popupContent={t('comments.aria.previousComment', { ns: 'workflow' })}
position="top"
popupClassName="!px-2 !py-1.5"
>
<button
type="button"
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={t('comments.aria.previousComment', { ns: 'workflow' })}
>
<RiArrowUpSLine className="h-4 w-4" />
</button>
<Tooltip>
<TooltipTrigger>
<button
type="button"
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={t('comments.aria.previousComment', { ns: 'workflow' })}
>
<RiArrowUpSLine className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent placement="top" popupClassName="!px-2 !py-1.5">
{t('comments.aria.previousComment', { ns: 'workflow' })}
</TooltipContent>
</Tooltip>
<Tooltip
popupContent={t('comments.aria.nextComment', { ns: 'workflow' })}
position="top"
popupClassName="!px-2 !py-1.5"
>
<button
type="button"
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={t('comments.aria.nextComment', { ns: 'workflow' })}
>
<RiArrowDownSLine className="h-4 w-4" />
</button>
<Tooltip>
<TooltipTrigger>
<button
type="button"
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={t('comments.aria.nextComment', { ns: 'workflow' })}
>
<RiArrowDownSLine className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent placement="top" popupClassName="!px-2 !py-1.5">
{t('comments.aria.nextComment', { ns: 'workflow' })}
</TooltipContent>
</Tooltip>
<button
type="button"
@ -470,88 +478,78 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
className="group relative -mx-4 rounded-lg px-4 py-2 transition-colors hover:bg-components-panel-on-panel-item-bg-hover"
>
{isOwnReply && !isReplyEditing && (
<PortalToFollowElem
placement="bottom-end"
open={activeReplyMenuId === reply.id}
onOpenChange={(open) => {
if (!open) {
setDeletingReplyId(null)
setActiveReplyMenuId(null)
}
}}
<div
className={cn(
'absolute top-1 right-1 gap-1',
activeReplyMenuId === reply.id ? 'flex' : 'hidden group-hover:flex',
)}
data-reply-menu
>
<div
className={cn(
'absolute right-1 top-1 gap-1',
activeReplyMenuId === reply.id ? 'flex' : 'hidden group-hover:flex',
)}
data-reply-menu
<DropdownMenu
open={activeReplyMenuId === reply.id}
onOpenChange={(open) => {
if (!open)
setDeletingReplyId(null)
setActiveReplyMenuId(open ? reply.id : null)
}}
>
<PortalToFollowElemTrigger asChild>
<button
type="button"
className="flex h-6 w-6 items-center justify-center rounded-md text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary"
onClick={(e) => {
e.stopPropagation()
setDeletingReplyId(null)
setActiveReplyMenuId(prev => prev === reply.id ? null : reply.id)
}}
aria-label={t('comments.aria.replyActions', { ns: 'workflow' })}
>
<RiMoreFill className="h-4 w-4" />
</button>
</PortalToFollowElemTrigger>
</div>
<PortalToFollowElemContent
className="z-[100] w-36 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-[10px]"
data-reply-menu
>
{/* Menu buttons - hidden when showing delete confirm */}
<div className={cn(deletingReplyId === reply.id ? 'hidden' : 'block')}>
<button
className="flex w-full items-center justify-start rounded-t-xl px-3 py-2 text-left text-sm text-text-secondary hover:bg-state-base-hover"
onClick={(e) => {
e.stopPropagation()
handleStartEdit(reply)
}}
>
{t('comments.actions.editReply', { ns: 'workflow' })}
</button>
<button
className="text-negative flex w-full items-center justify-start rounded-b-xl px-3 py-2 text-left text-sm text-text-secondary hover:bg-state-base-hover"
onClick={(e) => {
e.stopPropagation()
e.preventDefault()
if (onReplyDeleteDirect) {
setDeletingReplyId(reply.id)
}
else {
setActiveReplyMenuId(null)
onReplyDelete?.(reply.id)
}
}}
>
{t('comments.actions.deleteReply', { ns: 'workflow' })}
</button>
</div>
<DropdownMenuTrigger
className="flex h-6 w-6 items-center justify-center rounded-md text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary"
aria-label={t('comments.aria.replyActions', { ns: 'workflow' })}
>
<RiMoreFill className="h-4 w-4" />
</DropdownMenuTrigger>
<DropdownMenuContent
placement="bottom-end"
sideOffset={4}
popupClassName="z-[100] w-36 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-[10px]"
data-reply-menu
>
<div className={cn(deletingReplyId === reply.id ? 'hidden' : 'block')}>
<button
className="flex w-full items-center justify-start rounded-t-xl px-3 py-2 text-left text-sm text-text-secondary hover:bg-state-base-hover"
onClick={(e) => {
e.stopPropagation()
handleStartEdit(reply)
}}
>
{t('comments.actions.editReply', { ns: 'workflow' })}
</button>
<button
className="text-negative flex w-full items-center justify-start rounded-b-xl px-3 py-2 text-left text-sm text-text-secondary hover:bg-state-base-hover"
onClick={(e) => {
e.stopPropagation()
e.preventDefault()
if (onReplyDeleteDirect) {
setDeletingReplyId(reply.id)
}
else {
setActiveReplyMenuId(null)
onReplyDelete?.(reply.id)
}
}}
>
{t('comments.actions.deleteReply', { ns: 'workflow' })}
</button>
</div>
{/* Delete confirmation - shown when deletingReplyId matches */}
<div className={cn(deletingReplyId === reply.id ? 'block' : 'hidden')}>
<InlineDeleteConfirm
title={t('comments.actions.deleteReply', { ns: 'workflow' })}
onConfirm={() => {
setDeletingReplyId(null)
setActiveReplyMenuId(null)
onReplyDeleteDirect?.(reply.id)
}}
onCancel={() => {
setDeletingReplyId(null)
}}
className="m-0 w-full border-0 shadow-none"
/>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
<div className={cn(deletingReplyId === reply.id ? 'block' : 'hidden')}>
<InlineDeleteConfirm
title={t('comments.actions.deleteReply', { ns: 'workflow' })}
onConfirm={() => {
setDeletingReplyId(null)
setActiveReplyMenuId(null)
onReplyDeleteDirect?.(reply.id)
}}
onCancel={() => {
setDeletingReplyId(null)
}}
className="m-0 w-full border-0 shadow-none"
/>
</div>
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
{isReplyEditing
? (
@ -599,7 +597,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">
<div className="inset-0 absolute z-30 flex items-center justify-center bg-components-panel-bg/70 text-sm text-text-tertiary">
{t('comments.loading', { ns: 'workflow' })}
</div>
)}

View File

@ -5,11 +5,11 @@ import { useEffect, useState } from 'react'
import { useReactFlow } from 'reactflow'
import Avatar from '@/app/components/base/avatar'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import Tooltip from '@/app/components/base/tooltip'
Popover,
PopoverContent,
PopoverTrigger,
} from '@/app/components/base/ui/popover'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/app/components/base/ui/tooltip'
import { useAppContext } from '@/context/app-context'
import { getAvatar } from '@/service/common'
import { cn } from '@/utils/classnames'
@ -111,7 +111,7 @@ const OnlineUsers = () => {
className={cn(
'flex h-8 items-center rounded-full border-[0.5px] border-components-panel-border',
'bg-components-panel-bg py-1 shadow-xs shadow-shadow-shadow-3 backdrop-blur-[10px]',
hasCounter ? 'min-w-[87px] gap-px pl-1 pr-1.5' : 'gap-1 px-1.5',
hasCounter ? 'min-w-[87px] gap-px pr-1.5 pl-1' : 'gap-1 px-1.5',
)}
>
<div className="flex h-6 items-center">
@ -120,117 +120,104 @@ const OnlineUsers = () => {
const isCurrentUser = user.user_id === currentUserId
const userColor = isCurrentUser ? undefined : getUserColor(user.user_id)
return (
<Tooltip
key={`${user.sid}-${index}`}
popupContent={renderDisplayName(
user,
'system-xs-medium text-text-secondary',
'text-text-quaternary',
)}
position="bottom"
triggerMethod="hover"
needsDelay={false}
asChild
popupClassName="flex h-[28px] w-[85px] items-center justify-center gap-1 rounded-md border-[0.5px] border-components-panel-border bg-components-tooltip-bg px-3 py-[6px] shadow-lg shadow-shadow-shadow-5 backdrop-blur-[10px]"
noDecoration
>
<div
className={cn(
'relative flex size-6 items-center justify-center',
index > 0 && '-ml-1.5',
!isCurrentUser && 'cursor-pointer transition-transform hover:scale-110',
)}
style={{ zIndex: visibleUsers.length - index }}
onClick={() => !isCurrentUser && jumpToUserCursor(user.user_id)}
<Tooltip key={`${user.sid}-${index}`}>
<TooltipTrigger>
<div
className={cn(
'relative flex size-6 items-center justify-center',
index > 0 && '-ml-1.5',
!isCurrentUser && 'cursor-pointer transition-transform hover:scale-110',
)}
style={{ zIndex: visibleUsers.length - index }}
onClick={() => !isCurrentUser && jumpToUserCursor(user.user_id)}
>
<Avatar
name={user.username || 'User'}
avatar={getAvatarUrl(user)}
size={24}
className="ring-1 ring-components-panel-bg"
backgroundColor={userColor}
/>
</div>
</TooltipTrigger>
<TooltipContent
placement="bottom"
sideOffset={4}
popupClassName="flex h-[28px] w-[85px] items-center justify-center gap-1 rounded-md border-[0.5px] border-components-panel-border bg-components-tooltip-bg px-3 py-[6px] shadow-lg shadow-shadow-shadow-5 backdrop-blur-[10px]"
>
<Avatar
name={user.username || 'User'}
avatar={getAvatarUrl(user)}
size={24}
className="ring-1 ring-components-panel-bg"
backgroundColor={userColor}
/>
</div>
{renderDisplayName(
user,
'system-xs-medium text-text-secondary',
'text-text-quaternary',
)}
</TooltipContent>
</Tooltip>
)
})}
{remainingCount > 0 && (
<PortalToFollowElem
open={dropdownOpen}
onOpenChange={setDropdownOpen}
placement="bottom-start"
offset={{
mainAxis: 8,
crossAxis: -48,
}}
>
<PortalToFollowElemTrigger
onClick={() => setDropdownOpen(prev => !prev)}
asChild
>
<div className="flex items-center gap-1">
<div
className={cn(
'flex h-6 w-6 cursor-pointer select-none items-center justify-center rounded-full bg-components-icon-bg-midnight-solid text-[10px] font-semibold uppercase leading-[12px] text-white ring-1 ring-components-panel-bg',
visibleUsers.length > 0 && '-ml-1',
)}
>
+
{remainingCount}
<Popover open={dropdownOpen} onOpenChange={setDropdownOpen}>
<PopoverTrigger
render={(
<div className="flex items-center gap-1">
<div
className={cn(
'flex h-6 w-6 cursor-pointer items-center justify-center rounded-full bg-components-icon-bg-midnight-solid text-[10px] leading-[12px] font-semibold text-white uppercase ring-1 ring-components-panel-bg select-none',
visibleUsers.length > 0 && '-ml-1',
)}
>
+
{remainingCount}
</div>
<ChevronDownIcon className="h-3 w-3 cursor-pointer text-gray-500" />
</div>
<ChevronDownIcon className="h-3 w-3 cursor-pointer text-gray-500" />
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent
)}
/>
<PopoverContent
placement="bottom-start"
sideOffset={8}
alignOffset={-48}
className="z-[9999]"
popupClassName={cn(
'mt-1.5 flex max-h-[200px] w-[240px] flex-col overflow-y-auto',
'rounded-xl border-[0.5px] border-components-panel-border',
'bg-components-panel-bg-blur p-1 shadow-lg shadow-shadow-shadow-5 backdrop-blur-[10px]',
)}
>
<div
className={cn(
'mt-1.5',
'flex flex-col',
'max-h-[200px] w-[240px] overflow-y-auto',
'rounded-xl border-[0.5px] border-components-panel-border',
'bg-components-panel-bg-blur p-1',
'shadow-lg shadow-shadow-shadow-5',
'backdrop-blur-[10px]',
)}
>
{onlineUsers.map((user) => {
const isCurrentUser = user.user_id === currentUserId
const userColor = isCurrentUser ? undefined : getUserColor(user.user_id)
return (
<div
key={user.sid}
className={cn(
'flex items-center gap-2 rounded-lg px-3 py-1.5',
!isCurrentUser && 'cursor-pointer hover:bg-components-panel-on-panel-item-bg-hover',
)}
onClick={() => {
if (!isCurrentUser) {
jumpToUserCursor(user.user_id)
setDropdownOpen(false)
}
}}
>
<div className="relative">
<Avatar
name={user.username || 'User'}
avatar={getAvatarUrl(user)}
size={24}
backgroundColor={userColor}
/>
</div>
{renderDisplayName(
user,
'system-xs-medium text-text-secondary',
'text-text-tertiary',
)}
{onlineUsers.map((user) => {
const isCurrentUser = user.user_id === currentUserId
const userColor = isCurrentUser ? undefined : getUserColor(user.user_id)
return (
<div
key={user.sid}
className={cn(
'flex items-center gap-2 rounded-lg px-3 py-1.5',
!isCurrentUser && 'cursor-pointer hover:bg-components-panel-on-panel-item-bg-hover',
)}
onClick={() => {
if (!isCurrentUser) {
jumpToUserCursor(user.user_id)
setDropdownOpen(false)
}
}}
>
<div className="relative">
<Avatar
name={user.username || 'User'}
avatar={getAvatarUrl(user)}
size={24}
backgroundColor={userColor}
/>
</div>
)
})}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
{renderDisplayName(
user,
'system-xs-medium text-text-secondary',
'text-text-tertiary',
)}
</div>
)
})}
</PopoverContent>
</Popover>
)}
</div>
</div>

View File

@ -89,7 +89,7 @@ export const useLeaderRestoreListener = () => {
ns: 'workflow',
userName: data.initiatorName,
versionName: data.versionName || data.versionId,
}), { duration: 3000 })
}), { timeout: 3000 })
performRestore(data)
})
@ -102,7 +102,7 @@ export const useLeaderRestoreListener = () => {
ns: 'workflow',
userName: data.initiatorName,
versionName: data.versionName || data.versionId,
}), { duration: 3000 })
}), { timeout: 3000 })
})
return unsubscribe

View File

@ -1,10 +1,10 @@
import type { UserProfile, WorkflowCommentDetail, WorkflowCommentList } from '@/service/workflow-comment'
import { useParams } from 'next/navigation'
import { useCallback, useEffect, useRef } from 'react'
import { useReactFlow } from 'reactflow'
import { collaborationManager } from '@/app/components/workflow/collaboration/core/collaboration-manager'
import { useAppContext } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useParams } from '@/next/navigation'
import { createWorkflowComment, createWorkflowCommentReply, deleteWorkflowComment, deleteWorkflowCommentReply, fetchWorkflowComment, fetchWorkflowComments, resolveWorkflowComment, updateWorkflowComment, updateWorkflowCommentReply } from '@/service/workflow-comment'
import { useStore } from '../store'
import { ControlMode } from '../types'

View File

@ -1,4 +1,5 @@
import { useCallback } from 'react'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useStore, useWorkflowStore } from '../store'
import { ControlMode } from '../types'
import { useEdgesInteractionsWithoutSync } from './use-edges-interactions-without-sync'
@ -29,6 +30,7 @@ export const useWorkflowMoveMode = () => {
const setControlMode = useStore(s => s.setControlMode)
const { getNodesReadOnly } = useNodesReadOnly()
const { handleSelectionCancel } = useSelectionInteractions()
const isCommentModeAvailable = useGlobalPublicStore(s => s.systemFeatures.enable_collaboration_mode)
const handleModePointer = useCallback(() => {
if (getNodesReadOnly())
@ -45,8 +47,18 @@ export const useWorkflowMoveMode = () => {
handleSelectionCancel()
}, [getNodesReadOnly, handleSelectionCancel, setControlMode])
const handleModeComment = useCallback(() => {
if (getNodesReadOnly() || !isCommentModeAvailable)
return
setControlMode(ControlMode.Comment)
handleSelectionCancel()
}, [getNodesReadOnly, handleSelectionCancel, isCommentModeAvailable, setControlMode])
return {
handleModePointer,
handleModeHand,
handleModeComment,
isCommentModeAvailable,
}
}

View File

@ -54,8 +54,11 @@ import { cn } from '@/utils/classnames'
import CandidateNode from './candidate-node'
import UserCursors from './collaboration/components/user-cursors'
import { collaborationManager } from './collaboration/core/collaboration-manager'
import { CommentCursor, CommentIcon, CommentInput, CommentThread } from './comment'
import CommentManager from './comment-manager'
import { CommentIcon } from './comment/comment-icon'
import { CommentInput } from './comment/comment-input'
import { CommentCursor } from './comment/cursor'
import { CommentThread } from './comment/thread'
import {
CUSTOM_EDGE,
CUSTOM_NODE,
@ -545,7 +548,7 @@ export const Workflow: FC<WorkflowProps> = memo(({
<CandidateNode />
<CommentManager />
<div
className="pointer-events-none absolute left-0 top-0 z-[60] flex w-12 items-center justify-center p-1 pl-2"
className="pointer-events-none absolute top-0 left-0 z-[60] flex w-12 items-center justify-center p-1 pl-2"
style={{ height: controlHeight }}
>
<Control />

View File

@ -1,6 +1,5 @@
import type { WorkflowCommentList } from '@/service/workflow-comment'
import { RiCheckboxCircleFill, RiCheckboxCircleLine, RiCheckLine, RiCloseLine, RiFilter3Line } from '@remixicon/react'
import { useParams } from 'next/navigation'
import { memo, useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider'
@ -12,6 +11,7 @@ import { useStore } from '@/app/components/workflow/store'
import { ControlMode } from '@/app/components/workflow/types'
import { useAppContext } from '@/context/app-context'
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
import { useParams } from '@/next/navigation'
import { resolveWorkflowComment } from '@/service/workflow-comment'
import { cn } from '@/utils/classnames'
@ -68,7 +68,7 @@ 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 font-semibold leading-6 text-text-primary">{t('comments.panelTitle', { ns: 'workflow' })}</div>
<div className="system-xl-semibold leading-6 font-semibold text-text-primary">{t('comments.panelTitle', { ns: 'workflow' })}</div>
<div className="relative flex items-center gap-2">
<button
className={cn(
@ -85,7 +85,7 @@ const CommentsPanel = () => {
/>
</button>
{showFilter && (
<div className="absolute right-10 top-9 z-50 min-w-[184px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg backdrop-blur-[10px]">
<div className="absolute top-9 right-10 z-50 min-w-[184px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg backdrop-blur-[10px]">
<button
className={cn('flex w-full items-center justify-between rounded-md px-2 py-2 text-left text-sm hover:bg-state-base-hover', !showOnlyMine && 'bg-components-panel-on-panel-item-bg')}
onClick={() => {
@ -94,7 +94,7 @@ const CommentsPanel = () => {
}}
>
<span className="text-text-secondary">All</span>
{!showOnlyMine && <RiCheckLine className="h-4 w-4 text-primary-600" />}
{!showOnlyMine && <RiCheckLine className="text-primary-600 h-4 w-4" />}
</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', showOnlyMine && 'bg-components-panel-on-panel-item-bg')}
@ -104,7 +104,7 @@ const CommentsPanel = () => {
}}
>
<span className="text-text-secondary">Only your threads</span>
{showOnlyMine && <RiCheckLine className="h-4 w-4 text-primary-600" />}
{showOnlyMine && <RiCheckLine className="text-primary-600 h-4 w-4" />}
</button>
<Divider type="horizontal" className="my-1" />
<div
@ -116,7 +116,7 @@ const CommentsPanel = () => {
<span className="text-sm text-text-secondary">Show resolved</span>
<Switch
size="md"
defaultValue={showResolvedComments}
value={showResolvedComments}
onChange={(checked) => {
setShowResolvedComments(checked)
}}
@ -172,14 +172,14 @@ const CommentsPanel = () => {
{/* Header row: creator + time */}
<div className="flex items-start">
<div className="flex min-w-0 items-center gap-2">
<div className="system-sm-medium truncate text-text-primary">{c.created_by_account.name}</div>
<div className="system-2xs-regular shrink-0 text-text-tertiary">
<div className="truncate system-sm-medium text-text-primary">{c.created_by_account.name}</div>
<div className="shrink-0 system-2xs-regular text-text-tertiary">
{formatTimeFromNow(c.updated_at * 1000)}
</div>
</div>
</div>
{/* Content */}
<div className="system-sm-regular mt-1 line-clamp-3 break-words text-text-secondary">{c.content}</div>
<div className="mt-1 line-clamp-3 system-sm-regular break-words text-text-secondary">{c.content}</div>
{/* Footer */}
{c.reply_count > 0 && (
<div className="mt-2 flex items-center justify-between">
@ -195,7 +195,7 @@ const CommentsPanel = () => {
)
})}
{!loading && filteredSorted.length === 0 && (
<div className="system-sm-regular mt-6 text-center text-text-tertiary">{t('comments.noComments', { ns: 'workflow' })}</div>
<div className="mt-6 text-center system-sm-regular text-text-tertiary">{t('comments.noComments', { ns: 'workflow' })}</div>
)}
</div>
</div>

View File

@ -145,7 +145,7 @@ export const VersionHistoryPanel = ({
const emitRestoreIntent = useCallback(async (item: VersionHistory) => {
try {
const { collaborationManager } = await import('../../collaboration')
const { collaborationManager } = await import('../../collaboration/core/collaboration-manager')
collaborationManager.emitRestoreIntent({
versionId: item.id,
versionName: item.marked_name,
@ -160,7 +160,7 @@ export const VersionHistoryPanel = ({
const emitRestoreComplete = useCallback(async (item: VersionHistory, success: boolean, errorMessage?: string) => {
try {
const { collaborationManager } = await import('../../collaboration')
const { collaborationManager } = await import('../../collaboration/core/collaboration-manager')
collaborationManager.emitRestoreComplete({
versionId: item.id,
success,
@ -178,7 +178,7 @@ export const VersionHistoryPanel = ({
if (!appId)
return
const { collaborationManager } = await import('../../collaboration')
const { collaborationManager } = await import('../../collaboration/core/collaboration-manager')
collaborationManager.emitWorkflowUpdate(appId)
}
catch (error) {
@ -259,7 +259,7 @@ export const VersionHistoryPanel = ({
return (
<div className="flex h-full w-[268px] flex-col rounded-l-2xl border-y-[0.5px] border-l-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl shadow-shadow-shadow-5">
<div className="flex items-center gap-x-2 px-4 pt-3">
<div className="system-xl-semibold flex-1 py-1 text-text-primary">{t('versionHistory.title', { ns: 'workflow' })}</div>
<div className="flex-1 py-1 system-xl-semibold text-text-primary">{t('versionHistory.title', { ns: 'workflow' })}</div>
<Filter
filterValue={filterValue}
isOnlyShowNamedVersions={isOnlyShowNamedVersions}
@ -315,7 +315,7 @@ export const VersionHistoryPanel = ({
? <RiLoader2Line className="h-3.5 w-3.5 animate-spin text-text-accent" />
: <RiArrowDownDoubleLine className="h-3.5 w-3.5 text-text-accent" />}
</div>
<div className="system-xs-medium-uppercase py-px text-text-accent">
<div className="py-px system-xs-medium-uppercase text-text-accent">
{t('common.loadMore', { ns: 'workflow' })}
</div>
</div>

View File

@ -78,6 +78,11 @@
"count": 1
}
},
"app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx": {
"style/no-trailing-spaces": {
"count": 1
}
},
"app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chart-view.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 1
@ -1366,10 +1371,18 @@
"count": 1
}
},
"app/components/apps/__tests__/list.spec.tsx": {
"unused-imports/no-unused-vars": {
"count": 1
}
},
"app/components/apps/app-card.tsx": {
"no-restricted-imports": {
"count": 3
},
"perfectionist/sort-imports": {
"count": 1
},
"react/component-hook-factories": {
"count": 1
},
@ -8330,7 +8343,7 @@
},
"app/components/workflow/hooks/index.ts": {
"no-barrel-files/no-barrel-files": {
"count": 25
"count": 29
}
},
"app/components/workflow/hooks/use-checklist.ts": {
@ -8346,6 +8359,11 @@
"count": 3
}
},
"app/components/workflow/hooks/use-edges-interactions.ts": {
"perfectionist/sort-imports": {
"count": 1
}
},
"app/components/workflow/hooks/use-helpline.ts": {
"ts/no-explicit-any": {
"count": 1
@ -9019,6 +9037,9 @@
}
},
"app/components/workflow/nodes/_base/node.tsx": {
"no-restricted-imports": {
"count": 1
},
"tailwindcss/enforce-consistent-class-order": {
"count": 5
},
@ -11610,6 +11631,17 @@
"count": 4
}
},
"contract/console/apps.ts": {
"style/eol-last": {
"count": 1
},
"style/no-multiple-empty-lines": {
"count": 1
},
"style/no-trailing-spaces": {
"count": 1
}
},
"context/app-context-provider.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 1

View File

@ -1,31 +1,32 @@
import type {
CreateCommentParams,
CreateReplyParams,
UpdateCommentParams,
WorkflowCommentCreateRes,
WorkflowCommentDetail,
WorkflowCommentList,
WorkflowCommentReply,
WorkflowCommentResolveRes,
WorkflowCommentUpdateRes,
CreateCommentParams as ContractCreateCommentParams,
CreateReplyParams as ContractCreateReplyParams,
UpdateCommentParams as ContractUpdateCommentParams,
UserProfile as ContractUserProfile,
WorkflowCommentCreateRes as ContractWorkflowCommentCreateRes,
WorkflowCommentDetail as ContractWorkflowCommentDetail,
WorkflowCommentDetailMention as ContractWorkflowCommentDetailMention,
WorkflowCommentDetailReply as ContractWorkflowCommentDetailReply,
WorkflowCommentList as ContractWorkflowCommentList,
WorkflowCommentReply as ContractWorkflowCommentReply,
WorkflowCommentResolveRes as ContractWorkflowCommentResolveRes,
WorkflowCommentUpdateRes as ContractWorkflowCommentUpdateRes,
} from '@/contract/console/workflow-comment'
import type { CommonResponse } from '@/models/common'
import { consoleClient } from './client'
export type {
CreateCommentParams,
CreateReplyParams,
UpdateCommentParams,
UserProfile,
WorkflowCommentCreateRes,
WorkflowCommentDetail,
WorkflowCommentDetailMention,
WorkflowCommentDetailReply,
WorkflowCommentList,
WorkflowCommentReply,
WorkflowCommentResolveRes,
WorkflowCommentUpdateRes,
} from '@/contract/console/workflow-comment'
export type CreateCommentParams = ContractCreateCommentParams
export type CreateReplyParams = ContractCreateReplyParams
export type UpdateCommentParams = ContractUpdateCommentParams
export type UserProfile = ContractUserProfile
export type WorkflowCommentCreateRes = ContractWorkflowCommentCreateRes
export type WorkflowCommentDetail = ContractWorkflowCommentDetail
export type WorkflowCommentDetailMention = ContractWorkflowCommentDetailMention
export type WorkflowCommentDetailReply = ContractWorkflowCommentDetailReply
export type WorkflowCommentList = ContractWorkflowCommentList
export type WorkflowCommentReply = ContractWorkflowCommentReply
export type WorkflowCommentResolveRes = ContractWorkflowCommentResolveRes
export type WorkflowCommentUpdateRes = ContractWorkflowCommentUpdateRes
export const fetchWorkflowComments = async (appId: string): Promise<WorkflowCommentList[]> => {
const response = await consoleClient.workflowComments.list({

View File

@ -1,5 +1,5 @@
import type { BlockEnum, ConversationVariable, EnvironmentVariable } from '@/app/components/workflow/types'
import type { WorkflowDraftFeaturesPayload } from '@/contract/console/workflow'
import type { WorkflowDraftFeaturesPayload as ContractWorkflowDraftFeaturesPayload } from '@/contract/console/workflow'
import type { CommonResponse } from '@/models/common'
import type { FlowType } from '@/types/common'
import type {
@ -13,7 +13,7 @@ import { get, post } from './base'
import { consoleClient } from './client'
import { getFlowPrefix } from './utils'
export type { WorkflowDraftFeaturesPayload } from '@/contract/console/workflow'
export type WorkflowDraftFeaturesPayload = ContractWorkflowDraftFeaturesPayload
export const fetchWorkflowDraft = (url: string) => {
return get(url, {}, { silent: true }) as Promise<FetchWorkflowDraftResponse>
@ -130,7 +130,7 @@ export const updateConversationVariables = ({ appId, conversationVariables }: {
export const updateFeatures = ({ appId, features }: {
appId: string
features: WorkflowDraftFeaturesPayload
features: ContractWorkflowDraftFeaturesPayload
}) => {
return consoleClient.workflowDraft.updateFeatures({
params: { appId },