chore(dify-ui): update tooltip and infotip migration (#35543)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
yyh 2026-04-24 14:36:48 +08:00 committed by GitHub
parent 48e13f65dc
commit ec450eb7f9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 345 additions and 309 deletions

View File

@ -4270,11 +4270,6 @@
"count": 1
}
},
"web/app/components/workflow/nodes/_base/components/error-handle/error-handle-on-panel.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"web/app/components/workflow/nodes/_base/components/error-handle/error-handle-type-selector.tsx": {
"no-restricted-imports": {
"count": 1
@ -4293,16 +4288,6 @@
"count": 1
}
},
"web/app/components/workflow/nodes/_base/components/form-input-type-switch.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"web/app/components/workflow/nodes/_base/components/help-link.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"web/app/components/workflow/nodes/_base/components/input-support-select-var.tsx": {
"no-restricted-imports": {
"count": 1
@ -4502,22 +4487,6 @@
"count": 1
}
},
"web/app/components/workflow/nodes/agent/components/model-bar.tsx": {
"no-restricted-imports": {
"count": 1
},
"ts/no-empty-object-type": {
"count": 1
}
},
"web/app/components/workflow/nodes/agent/components/tool-icon.tsx": {
"no-restricted-imports": {
"count": 1
},
"react/unsupported-syntax": {
"count": 1
}
},
"web/app/components/workflow/nodes/agent/default.ts": {
"ts/no-explicit-any": {
"count": 3
@ -4859,11 +4828,6 @@
"count": 1
}
},
"web/app/components/workflow/nodes/knowledge-base/components/retrieval-setting/top-k-and-score-threshold.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"web/app/components/workflow/nodes/knowledge-base/components/retrieval-setting/type.ts": {
"ts/no-explicit-any": {
"count": 2
@ -4966,14 +4930,6 @@
"count": 1
}
},
"web/app/components/workflow/nodes/llm/components/json-schema-config-modal/code-editor.tsx": {
"no-restricted-imports": {
"count": 1
},
"ts/no-explicit-any": {
"count": 4
}
},
"web/app/components/workflow/nodes/llm/components/json-schema-config-modal/index.tsx": {
"no-restricted-imports": {
"count": 1
@ -5009,11 +4965,6 @@
"count": 2
}
},
"web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/edit-card/actions.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/edit-card/auto-width-input.tsx": {
"react/set-state-in-effect": {
"count": 1
@ -5235,11 +5186,6 @@
"count": 5
}
},
"web/app/components/workflow/nodes/tool/components/copy-id.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"web/app/components/workflow/nodes/tool/components/input-var-list.tsx": {
"ts/no-explicit-any": {
"count": 7
@ -5405,11 +5351,6 @@
"count": 1
}
},
"web/app/components/workflow/note-node/note-editor/toolbar/command.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"web/app/components/workflow/note-node/note-editor/utils.ts": {
"regexp/no-useless-quantifier": {
"count": 1

View File

@ -73,7 +73,7 @@ export function Infotip({
/>
<PopoverContent
placement={placement}
popupClassName={cn('max-w-[300px] px-3 py-2 system-xs-regular text-text-tertiary', popupClassName)}
popupClassName={cn('max-w-[300px] rounded-md px-3 py-2 system-xs-regular text-text-tertiary', popupClassName)}
>
{children}
</PopoverContent>

View File

@ -225,7 +225,7 @@ describe('FormInputItem branches', () => {
})
expect(screen.getByText('alpha')).toBeInTheDocument()
fireEvent.click(screen.getByRole('button'))
fireEvent.click(screen.getByText('alpha').closest('button') as HTMLButtonElement)
fireEvent.click(screen.getByText('beta'))
expect(onChange).toHaveBeenCalledWith({
@ -320,9 +320,9 @@ describe('FormInputItem branches', () => {
})
await waitFor(() => {
expect(screen.getByRole('button')).not.toBeDisabled()
expect(screen.getByText('Select options').closest('button')).not.toBeDisabled()
})
fireEvent.click(screen.getByRole('button'))
fireEvent.click(screen.getByText('Select options').closest('button') as HTMLButtonElement)
fireEvent.click(screen.getByText('trigger-option'))
expect(onChange).toHaveBeenCalledWith({

View File

@ -5,7 +5,7 @@ import type {
} from '@/app/components/workflow/types'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import Tooltip from '@/app/components/base/tooltip'
import { Infotip } from '@/app/components/base/infotip'
import Collapse from '../collapse'
import DefaultValue from './default-value'
import ErrorHandleTypeSelector from './error-handle-type-selector'
@ -57,7 +57,9 @@ const ErrorHandle = ({
<div className="mr-0.5 system-sm-semibold-uppercase text-text-secondary">
{t('nodes.common.errorHandle.title', { ns: 'workflow' })}
</div>
<Tooltip popupContent={t('nodes.common.errorHandle.tip', { ns: 'workflow' })} />
<Infotip aria-label={t('nodes.common.errorHandle.tip', { ns: 'workflow' })}>
{t('nodes.common.errorHandle.tip', { ns: 'workflow' })}
</Infotip>
{collapseIcon}
</div>
<ErrorHandleTypeSelector

View File

@ -1,12 +1,8 @@
'use client'
import type { FC } from 'react'
import { cn } from '@langgenius/dify-ui/cn'
import {
RiEditLine,
} from '@remixicon/react'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { useTranslation } from 'react-i18next'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
import Tooltip from '@/app/components/base/tooltip'
import { VarType } from '@/app/components/workflow/nodes/tool/types'
type Props = {
@ -19,28 +15,67 @@ const FormInputTypeSwitch: FC<Props> = ({
onChange,
}) => {
const { t } = useTranslation()
const variableLabel = t('nodes.common.typeSwitch.variable', { ns: 'workflow' })
const inputLabel = t('nodes.common.typeSwitch.input', { ns: 'workflow' })
return (
<div className="inline-flex h-8 shrink-0 gap-px rounded-[10px] bg-components-segmented-control-bg-normal p-0.5">
<Tooltip
popupContent={value === VarType.variable ? '' : t('nodes.common.typeSwitch.variable', { ns: 'workflow' })}
>
<div
className={cn('cursor-pointer rounded-lg px-2.5 py-1.5 text-text-tertiary hover:bg-state-base-hover', value === VarType.variable && 'bg-components-segmented-control-item-active-bg text-text-secondary shadow-xs hover:bg-components-segmented-control-item-active-bg')}
onClick={() => onChange(VarType.variable)}
>
<Variable02 className="h-4 w-4" />
</div>
</Tooltip>
<Tooltip
popupContent={value === VarType.constant ? '' : t('nodes.common.typeSwitch.input', { ns: 'workflow' })}
>
<div
className={cn('cursor-pointer rounded-lg px-2.5 py-1.5 text-text-tertiary hover:bg-state-base-hover', value === VarType.constant && 'bg-components-segmented-control-item-active-bg text-text-secondary shadow-xs hover:bg-components-segmented-control-item-active-bg')}
onClick={() => onChange(VarType.constant)}
>
<RiEditLine className="h-4 w-4" />
</div>
</Tooltip>
{value === VarType.variable
? (
<button
type="button"
aria-label={variableLabel}
className="cursor-pointer rounded-lg bg-components-segmented-control-item-active-bg px-2.5 py-1.5 text-text-secondary shadow-xs hover:bg-components-segmented-control-item-active-bg"
onClick={() => onChange(VarType.variable)}
>
<Variable02 className="h-4 w-4" />
</button>
)
: (
<Tooltip>
<TooltipTrigger
render={(
<button
type="button"
aria-label={variableLabel}
className="cursor-pointer rounded-lg px-2.5 py-1.5 text-text-tertiary hover:bg-state-base-hover"
onClick={() => onChange(VarType.variable)}
>
<Variable02 className="h-4 w-4" />
</button>
)}
/>
<TooltipContent>{variableLabel}</TooltipContent>
</Tooltip>
)}
{value === VarType.constant
? (
<button
type="button"
aria-label={inputLabel}
className="cursor-pointer rounded-lg bg-components-segmented-control-item-active-bg px-2.5 py-1.5 text-text-secondary shadow-xs hover:bg-components-segmented-control-item-active-bg"
onClick={() => onChange(VarType.constant)}
>
<span aria-hidden className="i-ri-edit-line h-4 w-4" />
</button>
)
: (
<Tooltip>
<TooltipTrigger
render={(
<button
type="button"
aria-label={inputLabel}
className="cursor-pointer rounded-lg px-2.5 py-1.5 text-text-tertiary hover:bg-state-base-hover"
onClick={() => onChange(VarType.constant)}
>
<span aria-hidden className="i-ri-edit-line h-4 w-4" />
</button>
)}
/>
<TooltipContent>{inputLabel}</TooltipContent>
</Tooltip>
)}
</div>
)
}

View File

@ -1,8 +1,7 @@
import type { BlockEnum } from '@/app/components/workflow/types'
import { RiBookOpenLine } from '@remixicon/react'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import TooltipPlus from '@/app/components/base/tooltip'
import { useNodeHelpLink } from '../hooks/use-node-help-link'
type HelpLinkProps = {
@ -17,19 +16,25 @@ const HelpLink = ({
if (!link)
return null
return (
<TooltipPlus
popupContent={t('userProfile.helpCenter', { ns: 'common' })}
>
<a
href={link}
target="_blank"
className="mr-1 flex h-6 w-6 items-center justify-center rounded-md hover:bg-state-base-hover"
>
<RiBookOpenLine className="h-4 w-4 text-gray-500" />
</a>
</TooltipPlus>
const label = t('userProfile.helpCenter', { ns: 'common' })
return (
<Tooltip>
<TooltipTrigger
render={(
<a
aria-label={label}
href={link}
target="_blank"
rel="noreferrer"
className="mr-1 flex h-6 w-6 items-center justify-center rounded-md hover:bg-state-base-hover"
>
<span aria-hidden className="i-ri-book-open-line h-4 w-4 text-gray-500" />
</a>
)}
/>
<TooltipContent>{label}</TooltipContent>
</Tooltip>
)
}

View File

@ -1,5 +1,5 @@
import type { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { fireEvent, render, screen } from '@testing-library/react'
import { render, screen } from '@testing-library/react'
import { ModelBar } from '../model-bar'
type ModelProviderItem = {
@ -52,11 +52,9 @@ describe('agent/model-bar', () => {
const emptySelector = screen.getByText((_, element) => element?.textContent === 'no-model:0')
fireEvent.mouseEnter(emptySelector)
expect(emptySelector).toBeInTheDocument()
expect(screen.getByText('indicator:red')).toBeInTheDocument()
expect(screen.getByText('workflow.nodes.agent.modelNotSelected')).toBeInTheDocument()
expect(screen.getByLabelText('workflow.nodes.agent.modelNotSelected')).toBeInTheDocument()
})
it('should render the selected model without warning when it is installed', () => {
@ -69,10 +67,8 @@ describe('agent/model-bar', () => {
it('should show a warning tooltip when the selected model is not installed', () => {
render(<ModelBar provider="openai" model="gpt-4.1" />)
fireEvent.mouseEnter(screen.getByText('openai/gpt-4.1:1'))
expect(screen.getByText('openai/gpt-4.1:1')).toBeInTheDocument()
expect(screen.getByText('indicator:red')).toBeInTheDocument()
expect(screen.getByText('workflow.nodes.agent.modelNotInstallTooltip')).toBeInTheDocument()
expect(screen.getByLabelText('workflow.nodes.agent.modelNotInstallTooltip')).toBeInTheDocument()
})
})

View File

@ -87,19 +87,17 @@ describe('agent/tool-icon', () => {
const { rerender } = render(<ToolIcon id="tool-2" providerName="author/tool-b" />)
fireEvent.mouseEnter(screen.getByText('app-icon:#fff:B'))
expect(screen.getByText('indicator:yellow')).toBeInTheDocument()
expect(screen.getByText('workflow.nodes.agent.toolNotAuthorizedTooltip:{"tool":"tool-b"}')).toBeInTheDocument()
expect(screen.getByLabelText('workflow.nodes.agent.toolNotAuthorizedTooltip:{"tool":"tool-b"}')).toBeInTheDocument()
mockWorkflowTools = []
mockMarketplaceIcon = 'https://example.com/market-tool.png'
rerender(<ToolIcon id="tool-3" providerName="market/tool-c" />)
const marketplaceIcon = screen.getByRole('img', { name: 'tool icon' })
fireEvent.mouseEnter(marketplaceIcon)
expect(marketplaceIcon).toHaveAttribute('src', 'https://example.com/market-tool.png')
expect(screen.getByText('indicator:red')).toBeInTheDocument()
expect(screen.getByText('workflow.nodes.agent.toolNotInstallTooltip:{"tool":"tool-c"}')).toBeInTheDocument()
expect(screen.getByLabelText('workflow.nodes.agent.toolNotInstallTooltip:{"tool":"tool-c"}')).toBeInTheDocument()
})
it('should fall back to the group icon while tool data is still loading', () => {

View File

@ -1,7 +1,7 @@
import type { FC } from 'react'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import Tooltip from '@/app/components/base/tooltip'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
import ModelSelector from '@/app/components/header/account-setting/model-provider-page/model-selector'
@ -10,7 +10,10 @@ import Indicator from '@/app/components/header/indicator'
type ModelBarProps = {
provider: string
model: string
} | {}
} | {
provider?: never
model?: never
}
const useAllModel = () => {
const { data: textGeneration } = useModelList(ModelTypeEnum.textGeneration)
@ -35,23 +38,27 @@ const useAllModel = () => {
export const ModelBar: FC<ModelBarProps> = (props) => {
const { t } = useTranslation()
const modelList = useAllModel()
if (!('provider' in props)) {
if (props.provider === undefined) {
const tooltip = t('nodes.agent.modelNotSelected', { ns: 'workflow' })
return (
<Tooltip
popupContent={t('nodes.agent.modelNotSelected', { ns: 'workflow' })}
triggerMethod="hover"
>
<div className="relative">
<ModelSelector
modelList={[]}
triggerClassName="bg-workflow-block-parma-bg h-6! rounded-md!"
defaultModel={undefined}
showDeprecatedWarnIcon={false}
readonly
deprecatedClassName="opacity-50"
/>
<Indicator color="red" className="absolute -top-0.5 -right-0.5" />
</div>
<Tooltip>
<TooltipTrigger
render={(
<div className="relative" aria-label={tooltip}>
<ModelSelector
modelList={[]}
triggerClassName="bg-workflow-block-parma-bg h-6! rounded-md!"
defaultModel={undefined}
showDeprecatedWarnIcon={false}
readonly
deprecatedClassName="opacity-50"
/>
<Indicator color="red" className="absolute -top-0.5 -right-0.5" />
</div>
)}
/>
<TooltipContent>{tooltip}</TooltipContent>
</Tooltip>
)
}
@ -59,23 +66,34 @@ export const ModelBar: FC<ModelBarProps> = (props) => {
provider => provider.provider === props.provider && provider.models.some(model => model.model === props.model),
)
const showWarn = modelList && !modelInstalled
return modelList && (
<Tooltip
popupContent={t('nodes.agent.modelNotInstallTooltip', { ns: 'workflow' })}
triggerMethod="hover"
disabled={!modelList || modelInstalled}
>
<div className="relative">
<ModelSelector
modelList={modelList}
triggerClassName="bg-workflow-block-parma-bg h-6! rounded-md!"
defaultModel={props}
showDeprecatedWarnIcon={false}
readonly
deprecatedClassName="opacity-50"
/>
{showWarn && <Indicator color="red" className="absolute -top-0.5 -right-0.5" />}
</div>
if (!modelList)
return null
const modelNotInstalledTooltip = t('nodes.agent.modelNotInstallTooltip', { ns: 'workflow' })
const modelSelector = (
<div className="relative" aria-label={showWarn ? modelNotInstalledTooltip : undefined}>
<ModelSelector
modelList={modelList}
triggerClassName="bg-workflow-block-parma-bg h-6! rounded-md!"
defaultModel={{
provider: props.provider,
model: props.model,
}}
showDeprecatedWarnIcon={false}
readonly
deprecatedClassName="opacity-50"
/>
{showWarn && <Indicator color="red" className="absolute -top-0.5 -right-0.5" />}
</div>
)
if (modelInstalled)
return modelSelector
return (
<Tooltip>
<TooltipTrigger render={modelSelector} />
<TooltipContent>{modelNotInstalledTooltip}</TooltipContent>
</Tooltip>
)
}

View File

@ -1,9 +1,10 @@
import type { ReactNode } from 'react'
import { cn } from '@langgenius/dify-ui/cn'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { memo, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import AppIcon from '@/app/components/base/app-icon'
import { Group } from '@/app/components/base/icons/src/vender/other'
import Tooltip from '@/app/components/base/tooltip'
import Indicator from '@/app/components/header/indicator'
import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools } from '@/service/use-tools'
import { getIconFromMarketPlace } from '@/utils/get-icon'
@ -62,44 +63,50 @@ export const ToolIcon = memo(({ providerName }: ToolIconProps) => {
throw new Error('Unknown status')
}, [name, notSuccess, status, t])
const [iconFetchError, setIconFetchError] = useState(false)
return (
<Tooltip
triggerMethod="hover"
popupContent={tooltip}
disabled={!notSuccess}
let iconContent: ReactNode = <Group className="h-3 w-3 opacity-35" />
if (!iconFetchError && icon) {
if (typeof icon === 'string') {
iconContent = (
<img
src={icon}
alt="tool icon"
className={cn('size-3.5 h-full w-full object-cover', notSuccess && 'opacity-50')}
onError={() => setIconFetchError(true)}
/>
)
}
else if (typeof icon === 'object') {
iconContent = (
<AppIcon
className={cn('size-3.5 h-full w-full object-cover', notSuccess && 'opacity-50')}
icon={icon?.content}
background={icon?.background}
/>
)
}
}
const iconNode = (
<div
aria-label={tooltip}
className={cn('relative')}
ref={containerRef}
>
<div
className={cn('relative')}
ref={containerRef}
>
<div className="flex size-5 items-center justify-center overflow-hidden rounded-md border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge">
{(() => {
if (iconFetchError || !icon)
return <Group className="h-3 w-3 opacity-35" />
if (typeof icon === 'string') {
return (
<img
src={icon}
alt="tool icon"
className={cn('size-3.5 h-full w-full object-cover', notSuccess && 'opacity-50')}
onError={() => setIconFetchError(true)}
/>
)
}
if (typeof icon === 'object') {
return (
<AppIcon
className={cn('size-3.5 h-full w-full object-cover', notSuccess && 'opacity-50')}
icon={icon?.content}
background={icon?.background}
/>
)
}
return <Group className="h-3 w-3 opacity-35" />
})()}
</div>
{indicator && <Indicator color={indicator} className="absolute -top-px -right-px" />}
<div className="flex size-5 items-center justify-center overflow-hidden rounded-md border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge">
{iconContent}
</div>
{indicator && <Indicator color={indicator} className="absolute -top-px -right-px" />}
</div>
)
if (!notSuccess || !tooltip)
return iconNode
return (
<Tooltip>
<TooltipTrigger render={iconNode} />
<TooltipContent>{tooltip}</TooltipContent>
</Tooltip>
)
})

View File

@ -9,7 +9,7 @@ import {
import { Switch } from '@langgenius/dify-ui/switch'
import { memo, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import Tooltip from '@/app/components/base/tooltip'
import { Infotip } from '@/app/components/base/infotip'
import { env } from '@/env'
export type TopKAndScoreThresholdProps = {
@ -59,10 +59,13 @@ const TopKAndScoreThreshold = ({
<div>
<div className="mb-0.5 flex h-6 items-center system-xs-medium text-text-secondary">
{t('datasetConfig.top_k', { ns: 'appDebug' })}
<Tooltip
triggerClassName="ml-0.5 shrink-0 w-3.5 h-3.5"
popupContent={t('datasetConfig.top_kTip', { ns: 'appDebug' })}
/>
<Infotip
aria-label={t('datasetConfig.top_kTip', { ns: 'appDebug' })}
className="ml-0.5 h-3.5 w-3.5"
iconClassName="h-3.5 w-3.5"
>
{t('datasetConfig.top_kTip', { ns: 'appDebug' })}
</Infotip>
</div>
<NumberField
disabled={readonly}
@ -94,10 +97,13 @@ const TopKAndScoreThreshold = ({
<div className="grow truncate system-sm-medium text-text-secondary">
{t('datasetConfig.score_threshold', { ns: 'appDebug' })}
</div>
<Tooltip
triggerClassName="shrink-0 ml-0.5 w-3.5 h-3.5"
popupContent={t('datasetConfig.score_thresholdTip', { ns: 'appDebug' })}
/>
<Infotip
aria-label={t('datasetConfig.score_thresholdTip', { ns: 'appDebug' })}
className="ml-0.5 h-3.5 w-3.5"
iconClassName="h-3.5 w-3.5"
>
{t('datasetConfig.score_thresholdTip', { ns: 'appDebug' })}
</Infotip>
</div>
<NumberField
disabled={readonly || !isScoreThresholdEnabled}

View File

@ -1,12 +1,11 @@
import type { FC } from 'react'
import type { ComponentProps, FC } from 'react'
import { cn } from '@langgenius/dify-ui/cn'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { Editor } from '@monaco-editor/react'
import { RiClipboardLine, RiIndentIncrease } from '@remixicon/react'
import copy from 'copy-to-clipboard'
import * as React from 'react'
import { useCallback, useEffect, useMemo, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import Tooltip from '@/app/components/base/tooltip'
import useTheme from '@/hooks/use-theme'
import { Theme } from '@/types/app'
@ -22,6 +21,10 @@ type CodeEditorProps = {
topContent?: React.ReactNode
} & React.HTMLAttributes<HTMLDivElement>
type EditorOnMount = NonNullable<ComponentProps<typeof Editor>['onMount']>
type MonacoEditor = Parameters<EditorOnMount>[0]
type Monaco = Parameters<EditorOnMount>[1]
const CodeEditor: FC<CodeEditorProps> = ({
value,
onUpdate,
@ -36,8 +39,8 @@ const CodeEditor: FC<CodeEditorProps> = ({
}) => {
const { t } = useTranslation()
const { theme } = useTheme()
const monacoRef = useRef<any>(null)
const editorRef = useRef<any>(null)
const monacoRef = useRef<Monaco | null>(null)
const editorRef = useRef<MonacoEditor | null>(null)
const [isMounted, setIsMounted] = React.useState(false)
const containerRef = useRef<HTMLDivElement>(null)
@ -50,7 +53,7 @@ const CodeEditor: FC<CodeEditorProps> = ({
}
}, [theme])
const handleEditorDidMount = useCallback((editor: any, monaco: any) => {
const handleEditorDidMount = useCallback<EditorOnMount>((editor, monaco) => {
editorRef.current = editor
monacoRef.current = monaco
@ -83,7 +86,7 @@ const CodeEditor: FC<CodeEditorProps> = ({
})
monaco.editor.setTheme('light-theme')
setIsMounted(true)
}, [])
}, [onBlur, onFocus])
const formatJsonContent = useCallback(() => {
if (editorRef.current)
@ -122,24 +125,36 @@ const CodeEditor: FC<CodeEditorProps> = ({
</div>
<div className="flex items-center gap-x-0.5">
{showFormatButton && (
<Tooltip popupContent={t('operation.format', { ns: 'common' })}>
<button
type="button"
className="flex h-6 w-6 items-center justify-center"
onClick={formatJsonContent}
>
<RiIndentIncrease className="h-4 w-4 text-text-tertiary" />
</button>
<Tooltip>
<TooltipTrigger
render={(
<button
type="button"
aria-label={t('operation.format', { ns: 'common' })}
className="flex h-6 w-6 items-center justify-center"
onClick={formatJsonContent}
>
<span aria-hidden className="i-ri-indent-increase h-4 w-4 text-text-tertiary" />
</button>
)}
/>
<TooltipContent>{t('operation.format', { ns: 'common' })}</TooltipContent>
</Tooltip>
)}
<Tooltip popupContent={t('operation.copy', { ns: 'common' })}>
<button
type="button"
className="flex h-6 w-6 items-center justify-center"
onClick={() => copy(value)}
>
<RiClipboardLine className="h-4 w-4 text-text-tertiary" />
</button>
<Tooltip>
<TooltipTrigger
render={(
<button
type="button"
aria-label={t('operation.copy', { ns: 'common' })}
className="flex h-6 w-6 items-center justify-center"
onClick={() => copy(value)}
>
<span aria-hidden className="i-ri-clipboard-line h-4 w-4 text-text-tertiary" />
</button>
)}
/>
<TooltipContent>{t('operation.copy', { ns: 'common' })}</TooltipContent>
</Tooltip>
</div>
</div>

View File

@ -1,8 +1,7 @@
import type { FC } from 'react'
import { RiAddCircleLine, RiDeleteBinLine, RiEditLine } from '@remixicon/react'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Tooltip from '@/app/components/base/tooltip'
type ActionsProps = {
disableAddBtn: boolean
@ -18,36 +17,59 @@ const Actions: FC<ActionsProps> = ({
onDelete,
}) => {
const { t } = useTranslation()
const addChildFieldLabel = t('nodes.llm.jsonSchema.addChildField', { ns: 'workflow' })
const editLabel = t('operation.edit', { ns: 'common' })
const removeLabel = t('operation.remove', { ns: 'common' })
return (
<div className="flex items-center gap-x-0.5">
<Tooltip popupContent={t('nodes.llm.jsonSchema.addChildField', { ns: 'workflow' })}>
<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 disabled:cursor-not-allowed disabled:text-text-disabled"
onClick={onAddChildField}
disabled={disableAddBtn}
>
<RiAddCircleLine className="h-4 w-4" />
</button>
<Tooltip>
<TooltipTrigger
render={(
<span className="inline-flex">
<button
type="button"
aria-label={addChildFieldLabel}
className="flex h-6 w-6 items-center justify-center rounded-md text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary disabled:cursor-not-allowed disabled:text-text-disabled"
onClick={onAddChildField}
disabled={disableAddBtn}
>
<span aria-hidden className="i-ri-add-circle-line h-4 w-4" />
</button>
</span>
)}
/>
<TooltipContent>{addChildFieldLabel}</TooltipContent>
</Tooltip>
<Tooltip popupContent={t('operation.edit', { ns: 'common' })}>
<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={onEdit}
>
<RiEditLine className="h-4 w-4" />
</button>
<Tooltip>
<TooltipTrigger
render={(
<button
type="button"
aria-label={editLabel}
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={onEdit}
>
<span aria-hidden className="i-ri-edit-line h-4 w-4" />
</button>
)}
/>
<TooltipContent>{editLabel}</TooltipContent>
</Tooltip>
<Tooltip popupContent={t('operation.remove', { ns: 'common' })}>
<button
type="button"
className="flex h-6 w-6 items-center justify-center rounded-md text-text-tertiary hover:bg-state-destructive-hover hover:text-text-destructive"
onClick={onDelete}
>
<RiDeleteBinLine className="h-4 w-4" />
</button>
<Tooltip>
<TooltipTrigger
render={(
<button
type="button"
aria-label={removeLabel}
className="flex h-6 w-6 items-center justify-center rounded-md text-text-tertiary hover:bg-state-destructive-hover hover:text-text-destructive"
onClick={onDelete}
>
<span aria-hidden className="i-ri-delete-bin-line h-4 w-4" />
</button>
)}
/>
<TooltipContent>{removeLabel}</TooltipContent>
</Tooltip>
</div>
)

View File

@ -20,27 +20,21 @@ describe('tool/copy-id', () => {
it('should copy content and reset copied state when mouse leaves', () => {
const { container } = render(<CopyId content="tool-123" />)
const trigger = screen.getByText('tool-123').parentElement as HTMLElement
const trigger = screen.getByRole('button', { name: 'appOverview.overview.appInfo.embedded.copy' })
const wrapper = container.querySelector('.inline-flex') as HTMLElement
act(() => {
fireEvent.mouseEnter(trigger)
})
expect(screen.getByText('appOverview.overview.appInfo.embedded.copy')).toBeInTheDocument()
act(() => {
fireEvent.click(trigger)
vi.advanceTimersByTime(100)
})
expect(copy).toHaveBeenCalledWith('tool-123')
expect(screen.getByText('appOverview.overview.appInfo.embedded.copied')).toBeInTheDocument()
expect(trigger).toHaveAccessibleName('appOverview.overview.appInfo.embedded.copied')
act(() => {
fireEvent.mouseLeave(wrapper)
vi.advanceTimersByTime(100)
fireEvent.mouseEnter(trigger)
})
expect(screen.getByText('appOverview.overview.appInfo.embedded.copy')).toBeInTheDocument()
expect(trigger).toHaveAccessibleName('appOverview.overview.appInfo.embedded.copy')
})
it('should stop click propagation from the outer wrapper', () => {

View File

@ -1,11 +1,10 @@
'use client'
import { RiFileCopyLine } from '@remixicon/react'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import copy from 'copy-to-clipboard'
import { debounce } from 'es-toolkit/compat'
import * as React from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import Tooltip from '@/app/components/base/tooltip'
type Props = {
content: string
@ -25,27 +24,33 @@ const CopyFeedbackNew = ({ content }: Props) => {
const onMouseLeave = debounce(() => {
setIsCopied(false)
}, 100)
const tooltip = (isCopied
? t(`${prefixEmbedded}.copied`, { ns: 'appOverview' })
: t(`${prefixEmbedded}.copy`, { ns: 'appOverview' })) || ''
return (
<div className="inline-flex w-full pb-0.5" onClick={e => e.stopPropagation()} onMouseLeave={onMouseLeave}>
<Tooltip
popupContent={
(isCopied
? t(`${prefixEmbedded}.copied`, { ns: 'appOverview' })
: t(`${prefixEmbedded}.copy`, { ns: 'appOverview' })) || ''
}
>
<div
className="group/copy flex w-full items-center gap-0.5"
onClick={onClickCopy}
>
<div
className="w-0 grow cursor-pointer truncate system-2xs-regular text-text-quaternary group-hover:text-text-tertiary"
>
{content}
</div>
<RiFileCopyLine className="h-3 w-3 shrink-0 text-text-tertiary opacity-0 group-hover/copy:opacity-100" />
</div>
<Tooltip>
<TooltipTrigger
render={(
<button
type="button"
aria-label={tooltip}
className="group/copy flex w-full items-center gap-0.5 text-left"
onClick={onClickCopy}
>
<span
className="w-0 grow cursor-pointer truncate system-2xs-regular text-text-quaternary group-hover:text-text-tertiary"
>
{content}
</span>
<span aria-hidden className="i-ri-file-copy-line h-3 w-3 shrink-0 text-text-tertiary opacity-0 group-hover/copy:opacity-100" />
</button>
)}
/>
<TooltipContent>
{tooltip}
</TooltipContent>
</Tooltip>
</div>
)

View File

@ -1,17 +1,10 @@
import { cn } from '@langgenius/dify-ui/cn'
import {
RiBold,
RiItalic,
RiLink,
RiListUnordered,
RiStrikethrough,
} from '@remixicon/react'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import {
memo,
useMemo,
} from 'react'
import { useTranslation } from 'react-i18next'
import Tooltip from '@/app/components/base/tooltip'
import { useStore } from '../store'
import { useCommand } from './hooks'
@ -32,15 +25,15 @@ const Command = ({
const icon = useMemo(() => {
switch (type) {
case 'bold':
return <RiBold className={cn('h-4 w-4', selectedIsBold && 'text-primary-600')} />
return <span aria-hidden className={cn('i-ri-bold h-4 w-4', selectedIsBold && 'text-primary-600')} />
case 'italic':
return <RiItalic className={cn('h-4 w-4', selectedIsItalic && 'text-primary-600')} />
return <span aria-hidden className={cn('i-ri-italic h-4 w-4', selectedIsItalic && 'text-primary-600')} />
case 'strikethrough':
return <RiStrikethrough className={cn('h-4 w-4', selectedIsStrikeThrough && 'text-primary-600')} />
return <span aria-hidden className={cn('i-ri-strikethrough h-4 w-4', selectedIsStrikeThrough && 'text-primary-600')} />
case 'link':
return <RiLink className={cn('h-4 w-4', selectedIsLink && 'text-primary-600')} />
return <span aria-hidden className={cn('i-ri-link h-4 w-4', selectedIsLink && 'text-primary-600')} />
case 'bullet':
return <RiListUnordered className={cn('h-4 w-4', selectedIsBullet && 'text-primary-600')} />
return <span aria-hidden className={cn('i-ri-list-unordered h-4 w-4', selectedIsBullet && 'text-primary-600')} />
}
}, [type, selectedIsBold, selectedIsItalic, selectedIsStrikeThrough, selectedIsLink, selectedIsBullet])
@ -60,22 +53,27 @@ const Command = ({
}, [type, t])
return (
<Tooltip
popupContent={tip}
>
<div
className={cn(
'flex h-8 w-8 cursor-pointer items-center justify-center rounded-md text-text-tertiary hover:bg-state-accent-active hover:text-text-accent',
type === 'bold' && selectedIsBold && 'bg-state-accent-active',
type === 'italic' && selectedIsItalic && 'bg-state-accent-active',
type === 'strikethrough' && selectedIsStrikeThrough && 'bg-state-accent-active',
type === 'link' && selectedIsLink && 'bg-state-accent-active',
type === 'bullet' && selectedIsBullet && 'bg-state-accent-active',
<Tooltip>
<TooltipTrigger
render={(
<button
type="button"
aria-label={tip}
className={cn(
'flex h-8 w-8 cursor-pointer items-center justify-center rounded-md text-text-tertiary hover:bg-state-accent-active hover:text-text-accent',
type === 'bold' && selectedIsBold && 'bg-state-accent-active',
type === 'italic' && selectedIsItalic && 'bg-state-accent-active',
type === 'strikethrough' && selectedIsStrikeThrough && 'bg-state-accent-active',
type === 'link' && selectedIsLink && 'bg-state-accent-active',
type === 'bullet' && selectedIsBullet && 'bg-state-accent-active',
)}
onClick={() => handleCommand(type)}
>
{icon}
</button>
)}
onClick={() => handleCommand(type)}
>
{icon}
</div>
/>
<TooltipContent>{tip}</TooltipContent>
</Tooltip>
)
}

View File

@ -44,12 +44,6 @@ This document tracks the Dify-web migration away from legacy overlay APIs.
## Allowlist maintenance
- After each migration batch, run:
```sh
pnpm -C web lint:fix --prune-suppressions <changed-files>
```
- If a migrated file was in the allowlist, remove it from `web/eslint.constants.mjs` in the same PR.
- Never increase allowlist scope to bypass new code.